From 903616f2b7a956d07182f1d98a4b4bf7d589cc1e Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 19 Oct 2026 14:47:02 +0200 Subject: [PATCH] Retain interactive isolation target and identity review --- .../interactive-launch-r1/README.md | 98 +++++++++++++++++++ .../interactive-launch-r1/SHA256SUMS | 6 ++ .../interactive-launch-r1/observation.json | 46 +++++++++ .../interactive-launch-r1/probe_identity.py | 38 +++++++ .../interactive-launch-r1/source-pins.json | 27 +++++ .../test_probe_identity.py | 21 ++++ .../interactive-launch-r1/validation.json | 8 ++ .../interactive-launch-r2/README.md | 98 +++++++++++++++++++ .../interactive-launch-r2/SHA256SUMS | 9 ++ .../interactive-launch-r2/TARGET.md | 90 +++++++++++++++++ .../interactive-launch-r2/observation.json | 46 +++++++++ .../interactive-launch-r2/probe_identity.py | 43 ++++++++ .../provisioning-contract.json | 49 ++++++++++ .../interactive-launch-r2/source-pins.json | 27 +++++ .../test_probe_identity.py | 30 ++++++ .../interactive-launch-r2/validation.json | 10 ++ .../workstation-metadata.json | 45 +++++++++ ...UST-WP-0073-agent-credential-separation.md | 13 +++ 18 files changed, 704 insertions(+) create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/README.md create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/SHA256SUMS create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/observation.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/probe_identity.py create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/source-pins.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/test_probe_identity.py create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r1/validation.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/README.md create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/SHA256SUMS create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/TARGET.md create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/observation.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/probe_identity.py create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/provisioning-contract.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/source-pins.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/test_probe_identity.py create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/validation.json create mode 100644 docs/changes/CUST-WP-0073/interactive-launch-r2/workstation-metadata.json diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/README.md b/docs/changes/CUST-WP-0073/interactive-launch-r1/README.md new file mode 100644 index 0000000..e1d8d96 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/README.md @@ -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. diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/SHA256SUMS b/docs/changes/CUST-WP-0073/interactive-launch-r1/SHA256SUMS new file mode 100644 index 0000000..a19a2ae --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/SHA256SUMS @@ -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 diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/observation.json b/docs/changes/CUST-WP-0073/interactive-launch-r1/observation.json new file mode 100644 index 0000000..8790d5f --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/observation.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": [] + } +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/probe_identity.py b/docs/changes/CUST-WP-0073/interactive-launch-r1/probe_identity.py new file mode 100644 index 0000000..feac576 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/probe_identity.py @@ -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()) diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/source-pins.json b/docs/changes/CUST-WP-0073/interactive-launch-r1/source-pins.json new file mode 100644 index 0000000..8864121 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/source-pins.json @@ -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" + } + } +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/test_probe_identity.py b/docs/changes/CUST-WP-0073/interactive-launch-r1/test_probe_identity.py new file mode 100644 index 0000000..9ff3493 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/test_probe_identity.py @@ -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() diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r1/validation.json b/docs/changes/CUST-WP-0073/interactive-launch-r1/validation.json new file mode 100644 index 0000000..0e99f09 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r1/validation.json @@ -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 +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/README.md b/docs/changes/CUST-WP-0073/interactive-launch-r2/README.md new file mode 100644 index 0000000..56bedd1 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/README.md @@ -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. diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/SHA256SUMS b/docs/changes/CUST-WP-0073/interactive-launch-r2/SHA256SUMS new file mode 100644 index 0000000..d4ddf68 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/SHA256SUMS @@ -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 diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/TARGET.md b/docs/changes/CUST-WP-0073/interactive-launch-r2/TARGET.md new file mode 100644 index 0000000..ea7b2d0 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/TARGET.md @@ -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. diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/observation.json b/docs/changes/CUST-WP-0073/interactive-launch-r2/observation.json new file mode 100644 index 0000000..8790d5f --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/observation.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": [] + } +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/probe_identity.py b/docs/changes/CUST-WP-0073/interactive-launch-r2/probe_identity.py new file mode 100644 index 0000000..4edfb87 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/probe_identity.py @@ -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()) diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/provisioning-contract.json b/docs/changes/CUST-WP-0073/interactive-launch-r2/provisioning-contract.json new file mode 100644 index 0000000..986dd41 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/provisioning-contract.json @@ -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 +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/source-pins.json b/docs/changes/CUST-WP-0073/interactive-launch-r2/source-pins.json new file mode 100644 index 0000000..8864121 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/source-pins.json @@ -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" + } + } +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/test_probe_identity.py b/docs/changes/CUST-WP-0073/interactive-launch-r2/test_probe_identity.py new file mode 100644 index 0000000..4cbe779 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/test_probe_identity.py @@ -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() diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/validation.json b/docs/changes/CUST-WP-0073/interactive-launch-r2/validation.json new file mode 100644 index 0000000..e123861 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/validation.json @@ -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 +} diff --git a/docs/changes/CUST-WP-0073/interactive-launch-r2/workstation-metadata.json b/docs/changes/CUST-WP-0073/interactive-launch-r2/workstation-metadata.json new file mode 100644 index 0000000..dba9422 --- /dev/null +++ b/docs/changes/CUST-WP-0073/interactive-launch-r2/workstation-metadata.json @@ -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" +} diff --git a/workplans/CUST-WP-0073-agent-credential-separation.md b/workplans/CUST-WP-0073-agent-credential-separation.md index d6b913e..16a752b 100644 --- a/workplans/CUST-WP-0073-agent-credential-separation.md +++ b/workplans/CUST-WP-0073-agent-credential-separation.md @@ -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.