Retain interactive isolation target and identity review
This commit is contained in:
parent
03eb3fbd3e
commit
903616f2b7
18 changed files with 704 additions and 0 deletions
98
docs/changes/CUST-WP-0073/interactive-launch-r1/README.md
Normal file
98
docs/changes/CUST-WP-0073/interactive-launch-r1/README.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Actual interactive Custodian assignment: supported handoff proposal
|
||||
|
||||
2026-10-19; preparation only. User selected this interactive Codex assignment
|
||||
first. No host/user creation, authentication, handoff, sudo, deployment, daemon
|
||||
restart, or production mutation occurred. This adds evidence to CUST-WP-0073;
|
||||
it neither supersedes the original credential-boundary revision 2 nor proves
|
||||
admission. The protected k3s preflight remains separately reviewable at
|
||||
`/tmp/cust73-privileged-preflight-r1`; its earlier boundary-choice field is
|
||||
historical and is superseded by the user's explicit interactive-first choice.
|
||||
|
||||
## Observed current execution boundary
|
||||
|
||||
`observation.json` records a UTC-bound, sanitized process ancestry. In the tool
|
||||
PID namespace, parent PID 1 executes the installed Codex 0.163.0 app-server-daemon
|
||||
binary and all observed UIDs are 1000. PATH resolves a different Codex installation.
|
||||
This does not establish the outer desktop frontend, transport endpoint, or
|
||||
daemon invocation: PID namespace ancestry ends at PID 1. No raw command line,
|
||||
environment, authentication file, session transcript, or credential was printed.
|
||||
The inspected user config has no explicit approval/sandbox override or MCP server
|
||||
entries; this does not describe managed settings, frontend apps, or dynamic tools.
|
||||
|
||||
## Supported route and concrete operator integration
|
||||
|
||||
The official [remote connections guide](https://learn.chatgpt.com/docs/remote-connections)
|
||||
supports an SSH connection that starts Codex under the remote login user, then
|
||||
desktop handoff of an existing chat to a saved matching Git project. Select the
|
||||
chat's run location, choose destination, and confirm Hand off. Handoff transfers
|
||||
chat/Git state and interrupts an active response. This chat cannot self-handoff;
|
||||
the operator or a separate chat must perform it. Confirm the installed frontend
|
||||
exposes these controls before provisioning; this environment cannot inspect its UI.
|
||||
|
||||
Use a dedicated non-admin OS account on an approved target, with a private home
|
||||
and private Codex state, no inherited SSH agent/admin kubeconfig/OpenBao tokens,
|
||||
no sudo or container-control groups, and an isolated repo checkout. Choose the
|
||||
target in the proposal rather than assuming the current workstation or the live
|
||||
k3s server is already suitable. Do not copy the operator's `.codex`, `.ssh`,
|
||||
`.kube`, environment, or global Git credential helper. Authentication for this
|
||||
account requires its own approved owner route. SSH alias must specify the new
|
||||
account and pinned host identity, with agent forwarding disabled.
|
||||
|
||||
After account provisioning and owner-authentication approval: verify `codex` on
|
||||
that account's login PATH; add the alias in desktop Settings > Connections;
|
||||
save the same Git repository/subdirectory there; capture its version/digest,
|
||||
OS identity and policy. Use the native handoff UI for THIS chat. No custom
|
||||
Glas rein is required merely to obtain the documented interactive transport.
|
||||
Keep old chat paused after handoff until admission passes. Failure returns to
|
||||
supervised preparation; it does not enable the old credential-bearing execution
|
||||
path for assignment work. Preserve receipts and explicitly record any rollback.
|
||||
|
||||
The [app-server approval protocol](https://learn.chatgpt.com/docs/app-server)
|
||||
sends command/file approval requests to the client with thread and turn IDs.
|
||||
Retain this native approval UI, and capture both approved and denied canary
|
||||
outcomes in the destination chat. Its decision never grants OS sudo rights.
|
||||
Custom clients would need these request/response flows; building one is not the
|
||||
minimal route. Direct network exposure of app-server is unnecessary.
|
||||
|
||||
## Concrete owner work
|
||||
|
||||
* Host provisioning owner (railiance-infra for managed estate hosts): add dedicated
|
||||
account/private checkout and explicit no-admin group/sudo policy. Existing
|
||||
`ansible/roles/ssh_ca_host/tasks/main.yml` installs per-user principals but does
|
||||
not create or confine a Codex account. Add its principal mapping only alongside
|
||||
the new account; never map the agt principal to tegwick/admin. Resolve an
|
||||
unmanaged workstation owner explicitly if that is the selected target.
|
||||
* ops-warden: existing SSH certificate route, after exact account/principal and
|
||||
target approval. It does not vend Codex authentication or Kubernetes tokens.
|
||||
* Custodian assignment supervisor: bind proposal revision, actual chat identity,
|
||||
target and host account; operator performs native handoff and verifies the new
|
||||
chat execution receipt. A new unrelated CLI session is not this acceptance.
|
||||
* Glas: accept/record the externally attached interactive assignment's evidence
|
||||
if its existing admission API is used; add a truthful interactive capability
|
||||
only after proof. Current gateway/rein catalog and bounded sand-boxer runner
|
||||
do not implement this frontend transport. Do not gate this route on another
|
||||
Claude startup rehearsal or claim the old native lane proves Codex isolation.
|
||||
|
||||
## Acceptance, without touching credential contents
|
||||
|
||||
Run `probe_identity.py EXPECTED_UID EXPECTED_MACHINE_ID_SHA256` from a tool call
|
||||
in the handed-off chat, with expected values set by the supervisor's target
|
||||
receipt. It rejects UID 0 or the current UID 1000 and suppresses all credential
|
||||
values. This is a partial identity check, not admission by itself.
|
||||
|
||||
Then bind an external supervisor observation of the app-server/tool process
|
||||
identity and thread to that same receipt; child-only probes can be fabricated by
|
||||
launching a child and therefore never suffice. Verify: private state, denied
|
||||
admin kubeconfig/SSH socket/OpenBao access, no privileged group/socket/sudo
|
||||
paths, bounded Kubernetes credential and aggregate PLUS every-namespace RBAC
|
||||
denials from the original packet, actual denied/approved native UI canaries,
|
||||
and no old process receiving tool execution. Inventory frontend connectors and
|
||||
dynamic tools separately: moving the shell UID does not revoke frontend account
|
||||
capabilities. Disable or explicitly scope external privileged tools.
|
||||
|
||||
Unresolved concrete gates: selected target/account, installed UI handoff support,
|
||||
account/SSH/authentication approval, protected k3s selector preflight and host
|
||||
mode600 approval if targeting that host, authoritative namespace inventory,
|
||||
actual-chat identity/approval/credential-denial receipts, external-tool scope.
|
||||
No acceptance status should move to done from these fixture or documentation
|
||||
results.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ad6383e665e1d9866929d350ca17f65f337c3fe84396537b86f1bd404cd19cee README.md
|
||||
f4fd07b9e35bc2163accaeca921713ff2a3fe439ccc60f550e0e35c30eccb348 observation.json
|
||||
62fe730496e3ce8087af9753349dd3cb9556435a7a5bfb7dd7bddf14ba549f7f probe_identity.py
|
||||
c9bdb1a755ad56c5adce4ff2930c3fc627ee36eb226f7315eb9b015189b991fe source-pins.json
|
||||
781f490edf93c119b7ceb5aea34c4796b16ace9104e5cb6ecaa7aa5e6a38c6c7 test_probe_identity.py
|
||||
5c95e40fe85e6ee928d598ff2c435023db0d743f65e85d62d5d0fdf76e9d481c validation.json
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"utc": "2026-10-19T12:34:11.864590+00:00",
|
||||
"codex": "/home/worsch/.nvm/versions/node/v24.11.1/bin/codex",
|
||||
"ancestry": [
|
||||
{
|
||||
"pid": 3,
|
||||
"comm": "python3",
|
||||
"uid": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1000"
|
||||
],
|
||||
"ppid": 2,
|
||||
"exe": "/usr/bin/python3.12"
|
||||
},
|
||||
{
|
||||
"pid": 2,
|
||||
"comm": "bash",
|
||||
"uid": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1000"
|
||||
],
|
||||
"ppid": 1,
|
||||
"exe": "/usr/bin/bash"
|
||||
},
|
||||
{
|
||||
"pid": 1,
|
||||
"comm": "codex",
|
||||
"uid": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1000"
|
||||
],
|
||||
"ppid": 0,
|
||||
"exe": "/home/worsch/.codex/packages/app-server-daemon/releases/0.163.0-x86_64-unknown-linux-musl/bin/codex"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"mcp_names": [],
|
||||
"feature_names": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Partial read-only identity evidence. Never an admission authority."""
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
def evaluate(expected_uid, expected_machine, actual_uid, machine_digest):
|
||||
return {
|
||||
"identity_matches": actual_uid == expected_uid
|
||||
and actual_uid not in (0, 1000)
|
||||
and len(expected_machine) == 64
|
||||
and expected_machine == machine_digest,
|
||||
"actual_uid": actual_uid,
|
||||
"assignment_admitted": False,
|
||||
"scope": "partial_process_identity_only",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
expected_uid = int(sys.argv[1])
|
||||
expected_machine = sys.argv[2]
|
||||
machine = hashlib.sha256(Path('/etc/machine-id').read_bytes()).hexdigest()
|
||||
result = evaluate(expected_uid, expected_machine, os.geteuid(), machine)
|
||||
result['capture_utc'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
except Exception:
|
||||
result = {'identity_matches': False, 'assignment_admitted': False,
|
||||
'reason': 'identity_probe_refused'}
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0 if result['identity_matches'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"glas-harness": {
|
||||
"head": "a19bff487d41f18c58a8fd713f6d4e04c3a218d5",
|
||||
"files": {
|
||||
"src/glas_harness/gateway.py": "5e998bfa53ade5dafbae799dc04b77031b6414e2af49797f45727d59d7397d42",
|
||||
"registry/reins/rein-aharness.yaml": "8cdbe7b5ee45e2fb5ca2ed089cab377580034dc261c46bfcf889bddb68cf3da0"
|
||||
}
|
||||
},
|
||||
"sand-boxer": {
|
||||
"head": "c7c310266aba96ab7dd03cd8bfcaa65f22a3b66e",
|
||||
"files": {
|
||||
"src/sandboxer/extensions/bwrap_runner.py": "c83cdd2758bae58cd5d5694f5d6752736f59d479cfc6e9f0f8841c1f5596cba6"
|
||||
}
|
||||
},
|
||||
"railiance-infra": {
|
||||
"head": "8b7ade9f66fe95d7002b5494d6302763a168827b",
|
||||
"files": {
|
||||
"ansible/roles/ssh_ca_host/tasks/main.yml": "c840ccb53d351ca6a1bb8b7a8406efae4451bb651707b03353f5145aa8f21fd3"
|
||||
}
|
||||
},
|
||||
"state-hub": {
|
||||
"head": "39b51a21a51204675cc46d4b51c00fddd1851449",
|
||||
"files": {
|
||||
"scripts/configure-codex.sh": "c6e0e2f47f5ff9e34f7b04a887e3e8e942fcd3eb78c5713be9d1eb4d49091c32"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import unittest
|
||||
from probe_identity import evaluate
|
||||
|
||||
|
||||
class IdentityTests(unittest.TestCase):
|
||||
def test_old_identity_and_root_never_pass(self):
|
||||
for uid in (0, 1000):
|
||||
self.assertFalse(evaluate(uid, 'a'*64, uid, 'a'*64)['identity_matches'])
|
||||
|
||||
def test_wrong_host_or_user_fail(self):
|
||||
self.assertFalse(evaluate(1200, 'a'*64, 1201, 'a'*64)['identity_matches'])
|
||||
self.assertFalse(evaluate(1200, 'a'*64, 1200, 'b'*64)['identity_matches'])
|
||||
|
||||
def test_matching_child_is_never_assignment_admission(self):
|
||||
result = evaluate(1200, 'a'*64, 1200, 'a'*64)
|
||||
self.assertTrue(result['identity_matches'])
|
||||
self.assertFalse(result['assignment_admitted'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"interactive_first": true,
|
||||
"fixture_tests": 3,
|
||||
"privileged_preflight_fixture_tests": 8,
|
||||
"live_cutover": false,
|
||||
"privileged_execution": false,
|
||||
"actual_assignment_admitted": false
|
||||
}
|
||||
98
docs/changes/CUST-WP-0073/interactive-launch-r2/README.md
Normal file
98
docs/changes/CUST-WP-0073/interactive-launch-r2/README.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Actual interactive Custodian assignment: supported handoff proposal
|
||||
|
||||
2026-10-19; revision 2, preparation only. Revision 1 is preserved unchanged.
|
||||
See TARGET.md and provisioning-contract.json for the concrete destination. User selected this interactive Codex assignment
|
||||
first. No host/user creation, authentication, handoff, sudo, deployment, daemon
|
||||
restart, or production mutation occurred. This adds evidence to CUST-WP-0073;
|
||||
it neither supersedes the original credential-boundary revision 2 nor proves
|
||||
admission. The protected k3s preflight remains separately reviewable at
|
||||
`/tmp/cust73-privileged-preflight-r1`; its earlier boundary-choice field is
|
||||
historical and is superseded by the user's explicit interactive-first choice.
|
||||
|
||||
## Observed current execution boundary
|
||||
|
||||
`observation.json` records a UTC-bound, sanitized process ancestry. In the tool
|
||||
PID namespace, parent PID 1 executes the installed Codex 0.163.0 app-server-daemon
|
||||
binary and all observed UIDs are 1000. PATH resolves a different Codex installation.
|
||||
This does not establish the outer desktop frontend, transport endpoint, or
|
||||
daemon invocation: PID namespace ancestry ends at PID 1. No raw command line,
|
||||
environment, authentication file, session transcript, or credential was printed.
|
||||
The inspected user config has no explicit approval/sandbox override or MCP server
|
||||
entries; this does not describe managed settings, frontend apps, or dynamic tools.
|
||||
|
||||
## Supported route and concrete operator integration
|
||||
|
||||
The official [remote connections guide](https://learn.chatgpt.com/docs/remote-connections)
|
||||
supports an SSH connection that starts Codex under the remote login user, then
|
||||
desktop handoff of an existing chat to a saved matching Git project. Select the
|
||||
chat's run location, choose destination, and confirm Hand off. Handoff transfers
|
||||
chat/Git state and interrupts an active response. This chat cannot self-handoff;
|
||||
the operator or a separate chat must perform it. Confirm the installed frontend
|
||||
exposes these controls before provisioning; this environment cannot inspect its UI.
|
||||
|
||||
Use a dedicated non-admin OS account on an approved target, with a private home
|
||||
and private Codex state, no inherited SSH agent/admin kubeconfig/OpenBao tokens,
|
||||
no sudo or container-control groups, and an isolated repo checkout. Use the dedicated workstation distribution proposed in TARGET.md; the current
|
||||
operator distribution and production server are not pre-approved destinations. Do not copy the operator's `.codex`, `.ssh`,
|
||||
`.kube`, environment, or global Git credential helper. Authentication for this
|
||||
account requires its own approved owner route. SSH alias must specify the new
|
||||
account and pinned host identity, with agent forwarding disabled.
|
||||
|
||||
After account provisioning and owner-authentication approval: verify `codex` on
|
||||
that account's login PATH; add the alias in desktop Settings > Connections;
|
||||
save the same Git repository/subdirectory there; capture its version/digest,
|
||||
OS identity and policy. Use the native handoff UI for THIS chat. No custom
|
||||
Glas rein is required merely to obtain the documented interactive transport.
|
||||
Keep old chat paused after handoff until admission passes. Failure returns to
|
||||
supervised preparation; it does not enable the old credential-bearing execution
|
||||
path for assignment work. Preserve receipts and explicitly record any rollback.
|
||||
|
||||
The [app-server approval protocol](https://learn.chatgpt.com/docs/app-server)
|
||||
sends command/file approval requests to the client with thread and turn IDs.
|
||||
Retain this native approval UI, and capture both approved and denied canary
|
||||
outcomes in the destination chat. Its decision never grants OS sudo rights.
|
||||
Custom clients would need these request/response flows; building one is not the
|
||||
minimal route. Direct network exposure of app-server is unnecessary.
|
||||
|
||||
## Concrete owner work
|
||||
|
||||
* Host provisioning owner (railiance-infra for managed estate hosts): add dedicated
|
||||
account/private checkout and explicit no-admin group/sudo policy. Existing
|
||||
`ansible/roles/ssh_ca_host/tasks/main.yml` installs per-user principals but does
|
||||
not create or confine a Codex account. Add its principal mapping only alongside
|
||||
the new account; never map the agt principal to tegwick/admin. Resolve an
|
||||
unmanaged workstation owner explicitly if that is the selected target.
|
||||
* ops-warden: existing SSH certificate route, after exact account/principal and
|
||||
target approval. It does not vend Codex authentication or Kubernetes tokens.
|
||||
* Custodian assignment supervisor: bind proposal revision, actual chat identity,
|
||||
target and host account; operator performs native handoff and verifies the new
|
||||
chat execution receipt. A new unrelated CLI session is not this acceptance.
|
||||
* Glas: accept/record the externally attached interactive assignment's evidence
|
||||
if its existing admission API is used; add a truthful interactive capability
|
||||
only after proof. Current gateway/rein catalog and bounded sand-boxer runner
|
||||
do not implement this frontend transport. Do not gate this route on another
|
||||
Claude startup rehearsal or claim the old native lane proves Codex isolation.
|
||||
|
||||
## Acceptance, without touching credential contents
|
||||
|
||||
Run `probe_identity.py EXPECTED_UID EXPECTED_MACHINE_ID_SHA256` from a tool call
|
||||
in the handed-off chat, with expected values set by the supervisor's target
|
||||
receipt. It rejects UID 0 or the original machine-ID/UID pair and suppresses all credential
|
||||
values. This is a partial identity check, not admission by itself.
|
||||
|
||||
Then bind an external supervisor observation of the app-server/tool process
|
||||
identity and thread to that same receipt; child-only probes can be fabricated by
|
||||
launching a child and therefore never suffice. Verify: private state, denied
|
||||
admin kubeconfig/SSH socket/OpenBao access, no privileged group/socket/sudo
|
||||
paths, bounded Kubernetes credential and aggregate PLUS every-namespace RBAC
|
||||
denials from the original packet, actual denied/approved native UI canaries,
|
||||
and no old process receiving tool execution. Inventory frontend connectors and
|
||||
dynamic tools separately: moving the shell UID does not revoke frontend account
|
||||
capabilities. Disable or explicitly scope external privileged tools.
|
||||
|
||||
Unresolved concrete gates: approval/provisioning of proposed target/account, installed UI handoff support,
|
||||
account/SSH/authentication approval, protected k3s selector preflight and host
|
||||
mode600 approval if targeting that host, authoritative namespace inventory,
|
||||
actual-chat identity/approval/credential-denial receipts, external-tool scope.
|
||||
No acceptance status should move to done from these fixture or documentation
|
||||
results.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
a4de5b7169f2cb9ca507d966aaa3f9cdf1f455fbd18f081a30333ceb3309e191 README.md
|
||||
c98fb8513680fb76e2eb47462690874df6a5212ac5e2cf51362ea220a74268ab TARGET.md
|
||||
f4fd07b9e35bc2163accaeca921713ff2a3fe439ccc60f550e0e35c30eccb348 observation.json
|
||||
1dea78b11d589f9270da975a7a7a2a11f51f8b1eba274da1c8d1dd01a9b90b22 probe_identity.py
|
||||
682b46f1548d9df685273861657aa5c41efae7dc0572edd70933f8d52d65aa6a provisioning-contract.json
|
||||
c9bdb1a755ad56c5adce4ff2930c3fc627ee36eb226f7315eb9b015189b991fe source-pins.json
|
||||
9c19707c11f36c72e9539c8536c21f444a39f6236ea8f615d423602267b55940 test_probe_identity.py
|
||||
ca9512654414b1175087141b2a1793393b8f105e17af42d90d1318e60d526270 validation.json
|
||||
39af77d96203bacedf7971483978f9baedbe64d75f2f041f66805ae73eca23f0 workstation-metadata.json
|
||||
90
docs/changes/CUST-WP-0073/interactive-launch-r2/TARGET.md
Normal file
90
docs/changes/CUST-WP-0073/interactive-launch-r2/TARGET.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Recommended target and provisioning source contract
|
||||
|
||||
Recommendation: workstation `bnt-lap001`, new WSL2 distribution
|
||||
`custodian-codex`, Linux account `custodian-codex`, private home
|
||||
`/home/custodian-codex`, project `/home/custodian-codex/work/the-custodian`,
|
||||
desktop SSH alias `custodian-codex-workstation`. This names the proposed target;
|
||||
it does not assert the distribution, account, SSH listener, or connection exists.
|
||||
No infrastructure spend is proposed.
|
||||
|
||||
## Why this destination
|
||||
|
||||
| Option | Evidence and consequence |
|
||||
| --- | --- |
|
||||
| Another UID in current WSL distribution | Only `worsch` UID1000 currently exists. Windows drive is visible, operator `.codex` and `.kube` directories are mode755, and a container socket is visible. Directory permissions alone do not prove secret-file readability. A second UID alone cannot establish the necessary namespace, socket and Windows boundary. |
|
||||
| Production `railiance01` | Prior metadata proves a running k3s server and admin kubeconfig mode0644; protected selector preflight and mode600 change remain outstanding. Attaching this assignment there adds production proximity and requires hardening/restart approval. |
|
||||
| New workstation WSL2 distribution | WSL2 substrate is present. A clean filesystem and deliberately limited account avoid production placement and reuse of operator home/state. Windows shares, interop, shared mounts and network-reachable host control planes still require explicit denial proof. This is the recommended preparation target. |
|
||||
|
||||
Read-only `wsl.exe --list --verbose` succeeded after explicit filesystem sandbox
|
||||
escalation; no distribution was started or modified. Inventory: Ubuntu-18.04
|
||||
(WSL1, stopped), Ubuntu (WSL2, stopped), archlinux (WSL2, stopped), Ubuntu-24.04
|
||||
(WSL2, running), Debian (WSL2, stopped). None is proven disposable or isolated.
|
||||
Linux-side `virsh`, QEMU, virt-install and Podman were absent; `/dev/kvm` absent;
|
||||
Docker and bwrap commands present. Command presence is not authorization or proof
|
||||
of an isolated long-lived service. Do not reuse any stopped distribution.
|
||||
|
||||
The Linux metadata observation is timestamped in workstation-metadata.json.
|
||||
Windows inventory was obtained on 2026-10-19 during this review. Source-only
|
||||
contract is provisioning-contract.json; it is intentionally not a deployment
|
||||
script and refuses readiness until the listed artifact/connection facts exist.
|
||||
|
||||
## Exact source-level work
|
||||
|
||||
1. Custodian/workstation owner adds a reviewed distribution recipe for the named
|
||||
guest: pinned Ubuntu 24.04 rootfs SHA, resource limits, private distribution
|
||||
backing storage owned by the operator, no imported operator filesystem or
|
||||
private state. Do not export/import the current credential-bearing distro.
|
||||
This is workstation configuration, outside railiance-infra's currently
|
||||
declared two-server inventory; do not silently enroll it there.
|
||||
2. In the guest recipe, disable Windows executable interop and automatic Windows
|
||||
drive mounting, and prevent access to shared WSL/Docker mounts. Root-owned
|
||||
boot/service policy must enforce this before agent startup. No Windows
|
||||
executables, host drive, forwarded agent, Docker daemon socket or privileged
|
||||
network control-plane endpoint may be reachable by the account. Verify the
|
||||
effective namespace and network access, not merely configuration values.
|
||||
3. Create the named account with its own group, password login locked, private
|
||||
home/state, no sudo/admin/docker/lxd groups or delegation. Provision a clean
|
||||
checkout and root-owned pinned Codex executable. Install only approved
|
||||
dependency tools. Its login shell must resolve that executable, not the
|
||||
operator's NVM installation. No credential helper/global operator Git config.
|
||||
4. Configure a guest-only SSH service for that user: no root/password login,
|
||||
agent forwarding, TCP forwarding or X11; transport and repository access
|
||||
separately scoped. The desktop connects through an approved loopback-only
|
||||
endpoint; actual free port and Windows/WSL reachability must be verified
|
||||
before fixing the SSH alias. SSH stdio/session startup is required, so do
|
||||
not impose a force-command that breaks app-server startup. Pin host identity.
|
||||
5. Route SSH authentication via warden's catalog, and Codex inference login via
|
||||
its actual credential owner. Neither is issued by this proposal. Install
|
||||
account-scoped authentication only after approval; do not copy operator auth.
|
||||
6. Operator adds the alias to the desktop connection list, saves the matching
|
||||
repository project, checks native approval UI, and hands off THIS chat.
|
||||
Record destination machine-ID digest/UID, external process identity and
|
||||
original/destination chat association. The machine-ID must be fresh for the
|
||||
new distribution; a duplicated image identity fails host binding.
|
||||
|
||||
Verification must include effective absence of `/mnt/c`, Windows executable
|
||||
interop, shared `/mnt/wsl` control sockets, operator homes, SSH agents and admin
|
||||
credentials, plus denied access to network-accessible Docker/host control planes.
|
||||
WSL2 being available does not prove these conditions. Supervisor-owned canaries
|
||||
can test denial without opening real credentials. The unchanged full assignment
|
||||
admission requirements in README.md and the original CUST73 packet still apply.
|
||||
|
||||
Rollback: stop assignment work, disconnect destination in the desktop, retain
|
||||
receipt and paused original chat; operator may stop the dedicated guest after
|
||||
collecting evidence. Never unregister/delete the guest or silently resume tools
|
||||
under the old operator identity. No production restart is part of this route.
|
||||
|
||||
Minimum operator decision, only once this source contract is reviewed: approve
|
||||
creating `custodian-codex` as a new local WSL2 distribution on `bnt-lap001`, or
|
||||
identify an already-approved isolated workstation guest to substitute. Rootfs
|
||||
artifact and SSH port validation are implementation work, not reasons to ask
|
||||
the user an open-ended target-design question.
|
||||
|
||||
## Probe correction
|
||||
|
||||
Revision 1 incorrectly rejected UID1000 globally. Revision 2 rejects root and
|
||||
the originating pair `(4f615ffa89bf23a138468bad4a51ba074a4523f7b2dabd0322dc1458f092f0f6,
|
||||
1000)`. A distinct host with UID1000 can pass the partial identity check; a new
|
||||
UID on the same host can also pass. Neither is an admission result. Supervisor
|
||||
identity must bind machine digest plus UID to the known SSH host identity and
|
||||
actual chat; machine-ID alone is not a cryptographic host identity.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"utc": "2026-10-19T12:34:11.864590+00:00",
|
||||
"codex": "/home/worsch/.nvm/versions/node/v24.11.1/bin/codex",
|
||||
"ancestry": [
|
||||
{
|
||||
"pid": 3,
|
||||
"comm": "python3",
|
||||
"uid": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1000"
|
||||
],
|
||||
"ppid": 2,
|
||||
"exe": "/usr/bin/python3.12"
|
||||
},
|
||||
{
|
||||
"pid": 2,
|
||||
"comm": "bash",
|
||||
"uid": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1000"
|
||||
],
|
||||
"ppid": 1,
|
||||
"exe": "/usr/bin/bash"
|
||||
},
|
||||
{
|
||||
"pid": 1,
|
||||
"comm": "codex",
|
||||
"uid": [
|
||||
"1000",
|
||||
"1000",
|
||||
"1000",
|
||||
"1000"
|
||||
],
|
||||
"ppid": 0,
|
||||
"exe": "/home/worsch/.codex/packages/app-server-daemon/releases/0.163.0-x86_64-unknown-linux-musl/bin/codex"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"mcp_names": [],
|
||||
"feature_names": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Partial read-only identity evidence. Never an admission authority."""
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ORIGIN_UID = 1000
|
||||
ORIGIN_MACHINE = '4f615ffa89bf23a138468bad4a51ba074a4523f7b2dabd0322dc1458f092f0f6'
|
||||
|
||||
|
||||
def evaluate(expected_uid, expected_machine, actual_uid, machine_digest):
|
||||
return {
|
||||
"identity_matches": actual_uid == expected_uid
|
||||
and actual_uid != 0
|
||||
and (actual_uid, machine_digest) != (ORIGIN_UID, ORIGIN_MACHINE)
|
||||
and len(expected_machine) == 64
|
||||
and all(c in '0123456789abcdef' for c in expected_machine)
|
||||
and expected_machine == machine_digest,
|
||||
"actual_uid": actual_uid,
|
||||
"assignment_admitted": False,
|
||||
"scope": "partial_process_identity_only",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
expected_uid = int(sys.argv[1])
|
||||
expected_machine = sys.argv[2]
|
||||
machine = hashlib.sha256(Path('/etc/machine-id').read_bytes()).hexdigest()
|
||||
result = evaluate(expected_uid, expected_machine, os.geteuid(), machine)
|
||||
result['capture_utc'] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
except Exception:
|
||||
result = {'identity_matches': False, 'assignment_admitted': False,
|
||||
'reason': 'identity_probe_refused'}
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
return 0 if result['identity_matches'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"proposal_id": "CUST-WP-0073-interactive-launch",
|
||||
"revision": 2,
|
||||
"status": "source_proposal_not_applied",
|
||||
"target": {
|
||||
"workstation": "bnt-lap001",
|
||||
"substrate": "WSL2",
|
||||
"distribution": "custodian-codex",
|
||||
"account": "custodian-codex",
|
||||
"uid": null,
|
||||
"home": "/home/custodian-codex",
|
||||
"project": "/home/custodian-codex/work/the-custodian",
|
||||
"ssh_alias": "custodian-codex-workstation",
|
||||
"ssh_endpoint": null,
|
||||
"rootfs_sha256": null,
|
||||
"codex_sha256": null,
|
||||
"host_identity": null
|
||||
},
|
||||
"account_policy": {
|
||||
"password_login": false,
|
||||
"sudo": false,
|
||||
"supplementary_admin_groups": [],
|
||||
"home_mode": "0700",
|
||||
"codex_state_mode": "0700",
|
||||
"operator_auth_copy": false
|
||||
},
|
||||
"required_effective_denials": [
|
||||
"Windows executable interop",
|
||||
"Windows drive mounts",
|
||||
"shared WSL and container-control sockets",
|
||||
"operator homes and SSH agent",
|
||||
"admin kubeconfig and OpenBao credentials",
|
||||
"network-accessible host and container control planes"
|
||||
],
|
||||
"ssh_policy": {
|
||||
"exposure": "operator-loopback-only",
|
||||
"root_login": false,
|
||||
"password_login": false,
|
||||
"agent_forwarding": false,
|
||||
"tcp_forwarding": false,
|
||||
"x11_forwarding": false,
|
||||
"native_stdio_session": true
|
||||
},
|
||||
"attachment": "native-desktop-existing-chat-handoff",
|
||||
"approval": "native-app-server-client-approval-ui-retained",
|
||||
"spend_authorized": false,
|
||||
"live_changes_authorized_by_this_file": false,
|
||||
"actual_assignment_admitted": false
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"glas-harness": {
|
||||
"head": "a19bff487d41f18c58a8fd713f6d4e04c3a218d5",
|
||||
"files": {
|
||||
"src/glas_harness/gateway.py": "5e998bfa53ade5dafbae799dc04b77031b6414e2af49797f45727d59d7397d42",
|
||||
"registry/reins/rein-aharness.yaml": "8cdbe7b5ee45e2fb5ca2ed089cab377580034dc261c46bfcf889bddb68cf3da0"
|
||||
}
|
||||
},
|
||||
"sand-boxer": {
|
||||
"head": "c7c310266aba96ab7dd03cd8bfcaa65f22a3b66e",
|
||||
"files": {
|
||||
"src/sandboxer/extensions/bwrap_runner.py": "c83cdd2758bae58cd5d5694f5d6752736f59d479cfc6e9f0f8841c1f5596cba6"
|
||||
}
|
||||
},
|
||||
"railiance-infra": {
|
||||
"head": "8b7ade9f66fe95d7002b5494d6302763a168827b",
|
||||
"files": {
|
||||
"ansible/roles/ssh_ca_host/tasks/main.yml": "c840ccb53d351ca6a1bb8b7a8406efae4451bb651707b03353f5145aa8f21fd3"
|
||||
}
|
||||
},
|
||||
"state-hub": {
|
||||
"head": "39b51a21a51204675cc46d4b51c00fddd1851449",
|
||||
"files": {
|
||||
"scripts/configure-codex.sh": "c6e0e2f47f5ff9e34f7b04a887e3e8e942fcd3eb78c5713be9d1eb4d49091c32"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import unittest
|
||||
from probe_identity import evaluate, ORIGIN_MACHINE
|
||||
|
||||
|
||||
class IdentityTests(unittest.TestCase):
|
||||
def test_old_identity_and_root_never_pass(self):
|
||||
self.assertFalse(evaluate(0, 'a'*64, 0, 'a'*64)['identity_matches'])
|
||||
self.assertFalse(evaluate(1000, ORIGIN_MACHINE, 1000, ORIGIN_MACHINE)['identity_matches'])
|
||||
|
||||
def test_same_uid_distinct_host_is_valid_partial_identity(self):
|
||||
self.assertTrue(evaluate(1000, 'a'*64, 1000, 'a'*64)['identity_matches'])
|
||||
|
||||
def test_separate_uid_same_host_is_valid_partial_identity(self):
|
||||
self.assertTrue(evaluate(1200, ORIGIN_MACHINE, 1200, ORIGIN_MACHINE)['identity_matches'])
|
||||
|
||||
def test_malformed_digest_refused(self):
|
||||
self.assertFalse(evaluate(1200, 'z'*64, 1200, 'z'*64)['identity_matches'])
|
||||
|
||||
def test_wrong_host_or_user_fail(self):
|
||||
self.assertFalse(evaluate(1200, 'a'*64, 1201, 'a'*64)['identity_matches'])
|
||||
self.assertFalse(evaluate(1200, 'a'*64, 1200, 'b'*64)['identity_matches'])
|
||||
|
||||
def test_matching_child_is_never_assignment_admission(self):
|
||||
result = evaluate(1200, 'a'*64, 1200, 'a'*64)
|
||||
self.assertTrue(result['identity_matches'])
|
||||
self.assertFalse(result['assignment_admitted'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"revision": 2,
|
||||
"identity_fixture_tests": 6,
|
||||
"interactive_first": true,
|
||||
"live_cutover": false,
|
||||
"privileged_execution": false,
|
||||
"read_only_wsl_inventory_escalated": true,
|
||||
"new_guest_created": false,
|
||||
"actual_assignment_admitted": false
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"hostname": "bnt-lap001",
|
||||
"machine_id_sha256": "4f615ffa89bf23a138468bad4a51ba074a4523f7b2dabd0322dc1458f092f0f6",
|
||||
"kernel_release": "6.18.54.1-microsoft-standard-WSL2",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "worsch",
|
||||
"uid": 1000,
|
||||
"home": "/home/worsch"
|
||||
}
|
||||
],
|
||||
"candidate_account_exists": false,
|
||||
"metadata": {
|
||||
"/etc/ssh/sshd_config": {
|
||||
"exists": false
|
||||
},
|
||||
"/usr/sbin/sshd": {
|
||||
"exists": false
|
||||
},
|
||||
"/run/docker.sock": {
|
||||
"uid": 65534,
|
||||
"gid": 65534,
|
||||
"mode": "0o660"
|
||||
},
|
||||
"/etc/rancher/k3s/k3s.yaml": {
|
||||
"exists": false
|
||||
},
|
||||
"/home/worsch/.kube": {
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"mode": "0o755"
|
||||
},
|
||||
"/home/worsch/.ssh": {
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"mode": "0o700"
|
||||
},
|
||||
"/home/worsch/.codex": {
|
||||
"uid": 1000,
|
||||
"gid": 1000,
|
||||
"mode": "0o755"
|
||||
}
|
||||
},
|
||||
"capture_finished_utc": "2026-10-19T12:37:26.268366+00:00"
|
||||
}
|
||||
|
|
@ -433,3 +433,16 @@ with privileges. The historical interactive-choice field predates the interview;
|
|||
the decision above supersedes that field without rewriting the original evidence.
|
||||
Root-owned staging and an exact supervisor-approved execution revision remain
|
||||
required for a live protected-input receipt; this preparation grants no cutover.
|
||||
|
||||
Supported interactive transport and target preparation are retained at
|
||||
`docs/changes/CUST-WP-0073/interactive-launch-r2/`. Official documentation
|
||||
supports SSH-connected app-server and native existing-chat handoff; availability
|
||||
in this installed frontend remains a user check. The proposed destination is a
|
||||
new dedicated `custodian-codex` WSL2 distribution/account on `bnt-lap001`, with
|
||||
no operator home, Windows interop/drives or privileged shared sockets. WSL2
|
||||
availability is verified; the guest, account and connection do not exist yet.
|
||||
An exact provisioning recipe and effective-denial proof remain implementation
|
||||
work, not acceptance inferred from configuration. Revision 1 is retained;
|
||||
revision 2 corrects the partial identity probe to reject the original host/UID
|
||||
pair rather than UID1000 globally. Six probe tests and packet hashes pass.
|
||||
Neither partial identity nor a child-process probe admits the actual assignment.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue