Publish reviewed bounded JWT ready handoff and retained owner history
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
f60e7419a9
commit
0f47e0c666
13 changed files with 2002 additions and 0 deletions
82
docs/openbao-negative-token-producer-decision.json
Normal file
82
docs/openbao-negative-token-producer-decision.json
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"proposal_id": "KEY-WP-0039-T02/private-producer-handoff-v4",
|
||||
"status": "owner_choice_required",
|
||||
"native_authority": false,
|
||||
"native_readiness": false,
|
||||
"lifecycle_manifest_sha256": "84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14",
|
||||
"recommended_human": {
|
||||
"client_id": "netkingdom-bootstrap-console",
|
||||
"audience": "netkingdom-bootstrap-console",
|
||||
"scope": "openid",
|
||||
"callback": "http://127.0.0.1:8876/oidc/callback",
|
||||
"existing_client_purpose_reuse_admitted": false,
|
||||
"fresh_mfa_verified": false
|
||||
},
|
||||
"coding_agent": {
|
||||
"client_id": "codex-railiance-platform",
|
||||
"audience": "codex-railiance-platform",
|
||||
"scope": "openbao:login",
|
||||
"subject": "service:codex:railiance-platform",
|
||||
"tenant": "tenant:platform",
|
||||
"secret_env": "KEYCAPE_CODEX_RAILIANCE_PLATFORM_CLIENT_SECRET",
|
||||
"live_registration_verified": false,
|
||||
"existing_custody_admission_receipt": null,
|
||||
"consumer_delivery_coordinates": null,
|
||||
"alternative_if_missing": "Platform-owned new narrow admission proposal; do not silently provision"
|
||||
},
|
||||
"handoff": {
|
||||
"adapter": "scripts/openbao_negative_private_handoff.py",
|
||||
"pipe_fd": 3,
|
||||
"private_tmpfs_jwt_files_approved": false,
|
||||
"exact_launcher_source_digest": null,
|
||||
"ssh_transport_admission_receipt": null,
|
||||
"destination": "railiance01 Linux main-thread V5 stdin",
|
||||
"no_pty": true,
|
||||
"caller_files_explicit_consumption": true,
|
||||
"delivery_timeout_seconds": 5,
|
||||
"decoded_claim_checks_are_signature_verification": false,
|
||||
"launcher": "scripts/openbao_negative_private_launcher.py",
|
||||
"ready_schema": "key39-private-input-ready/v1",
|
||||
"ready_before_any_token_load_consume_write": true,
|
||||
"ready_requires_nonce_source_envelope_health_window_match": true,
|
||||
"marker_is_authenticated_authority": false,
|
||||
"launcher_or_transport_admitted": false,
|
||||
"remote_child_sha256": "24b7b803e4eb7e6e7c0700a069dad0ec315ee7eb5538a88b9e56147f8fab5cdd",
|
||||
"remote_wrapper_sha256": "76ac013d1a676a0dfa58aea1ce87b0c3e72483ccd21f586a22c5475e9132f6c1",
|
||||
"stdio_alias_rejection": "device/inode aliases of0/1/2 and data/control pipes",
|
||||
"exclusive_same_uid_custody_required": true,
|
||||
"atomic_inode_delete_guaranteed": false,
|
||||
"credential_copy_cleanup_verified": false,
|
||||
"credential_files_remain_possible": true,
|
||||
"private_unknown_implies_sticky_consumer_unknown": true,
|
||||
"preparation_total_seconds": 5,
|
||||
"original_budget_shared_from_main_through_contract_spawn_ready_and_handoff": true,
|
||||
"source_files_nonblocking_nofollow_regular_stable": true,
|
||||
"source_contract_envelope_max_bytes": 65536,
|
||||
"source_hash_file_max_bytes": 262144
|
||||
},
|
||||
"node_route": {
|
||||
"selected_reviewed_origin": null,
|
||||
"allowed_v5_origins": [
|
||||
"http://127.0.0.1:18200",
|
||||
"http://openbao.openbao.svc:8200"
|
||||
],
|
||||
"owner_route_receipt": null,
|
||||
"public_health_preflight_guard_source_digest": null,
|
||||
"health_before_tokens_credentials_mutations_restarts": true,
|
||||
"workstation_observation_is_node_route_evidence": false
|
||||
},
|
||||
"decisions_required": [
|
||||
"Founder existing console reuse versus new client",
|
||||
"NetKingdom current registration/MFA/callback/runtime/guarded health ordering",
|
||||
"Platform coding-agent live/custody/consumer admission and node route",
|
||||
"Exact trusted launcher and private-file allowance",
|
||||
"Fresh exact founder live proposal decision after all evidence",
|
||||
"Separate founder directSSH control profile, exact launcher and continuously exclusive sameUID private tmpfs custody; marker is not a grant"
|
||||
],
|
||||
"previous_producer_manifest_sha256": "569672ecdb3f1f5b46122183dc3b82e83ff168c07c87db7211ec26bc1ce6184a",
|
||||
"previous_producer_consumer_refusal_sha256": "a1ed14e6b2db1ae77c3fd638fd7e47b5bd50c25eebba418e31aba89bb4b7f88e",
|
||||
"lifecycle_proposal_id": "KEY-WP-0039-T02/attended-lifecycle-v9-ready-input",
|
||||
"previous_pair_source_refusal_sha256": "8353eb8aae11be8ce874887d6a5ca10bf37c6fba73c3f85f0644a7546dd7d33f",
|
||||
"previous_source_refusal_sha256": "38abf660701194f85934a076a0c01d3047e6bfe790e02ba1b0e4616e356634e6"
|
||||
}
|
||||
356
docs/openbao-negative-token-producer-decision.md
Normal file
356
docs/openbao-negative-token-producer-decision.md
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
# KEY39 private token producer and node-route decision packet
|
||||
|
||||
Proposal `KEY-WP-0039-T02/private-producer-handoff-v1`, source preparation only.
|
||||
Lifecycle V5 is unchanged (manifest d1f997624db46693a7126a6eb6435f6b3a353d5bd67dcc8a539683e4b7147285).
|
||||
This packet names concrete supported flows and a reviewable local handoff. It
|
||||
supplies no admitted credential route, authenticated approval or native readiness.
|
||||
T01 stays done and T02 open; no new workplan or task is created.
|
||||
|
||||
## Existing flows and exact owner choice
|
||||
|
||||
Choose existing `netkingdom-bootstrap-console` for the human class refusal if
|
||||
NetKingdom/founder approve this one attended use of their existing client and
|
||||
literal `http://127.0.0.1:8876/oidc/callback` listener. Its source registration is
|
||||
public authorization_code, scopes openid/profile/email/groups, default audience
|
||||
netkingdom-bootstrap-console. Request only `openid`; do not request approval or
|
||||
OpenBao permissions. `keycape login` verifies RS256 issuer/access audience/scopes,
|
||||
ID-token client audience/nonce and matching subject. It binds before emitting a
|
||||
browser URL, validates exact callback host/path/state, uses S256 PKCE, and has a
|
||||
five-minute attempt bound. The provider handles password/MFA. Fresh deployed
|
||||
MFA/registration evidence is required; the handoff requires human aal2/mfa=true.
|
||||
Do not infer MFA from the client fragment: it has no per-client mfaRequired pin.
|
||||
Do not run the bootstrap console simultaneously on port8876. The same client's
|
||||
localhost callback is not substituted: the CLI requires a literal loopback IP.
|
||||
|
||||
Concrete producer invocation after that decision, on the attended workstation:
|
||||
|
||||
```
|
||||
keycape login --issuer https://kc.coulomb.social \
|
||||
--client-id netkingdom-bootstrap-console --audience netkingdom-bootstrap-console \
|
||||
--scope openid --redirect-uri http://127.0.0.1:8876/oidc/callback \
|
||||
--out /OWNER-PRIVATE-TMPFS-DIR/key39-human.json
|
||||
```
|
||||
|
||||
The private directory is new owner-controlled mode0700 tmpfs outside Git; the
|
||||
CLI creates the file mode0600 exclusively. No invocation is performed here.
|
||||
Current browser access-token lifetime is server TokenLifetime; per-client
|
||||
TokenLifetime is used only for client_credentials. Do not promise a new one-minute
|
||||
human lifetime by adding that field to a browser registration.
|
||||
|
||||
Alternative only if the owner rejects existing-client use: request one separately
|
||||
reviewed public client `key39-negative-human`, audience of the same name, exact
|
||||
callback `http://127.0.0.1:8877/oidc/callback`, scopes [openid], grantTypes
|
||||
[authorization_code], mfaRequired true, no role/serviceSubject/secretRef. That
|
||||
requires a new source registration, reviewed native merge/rollback and changed
|
||||
handoff audience binding. It is not part of V5's six temporary clients and must
|
||||
not silently enter that mutation. The current adapter intentionally refuses it.
|
||||
Reuse of informed-decision-approver is unsuitable for this CLI: its registered
|
||||
callback is HTTPS decisions.coulomb.social, not literal loopback. OpenBao admin
|
||||
is another source-compatible callback but its admin relying-party purpose is
|
||||
unnecessary for this probe and is not the selected plan.
|
||||
|
||||
Coding-agent producer is exactly the existing accepted source identity:
|
||||
|
||||
```
|
||||
keycape service-token --issuer https://kc.coulomb.social \
|
||||
--client-id codex-railiance-platform --audience codex-railiance-platform \
|
||||
--scope openbao:login --secret-env KEYCAPE_CODEX_RAILIANCE_PLATFORM_CLIENT_SECRET \
|
||||
--out /OWNER-PRIVATE-TMPFS-DIR/key39-agent.json
|
||||
```
|
||||
|
||||
Subject service:codex:railiance-platform, tenant:platform, roles [coding-agent],
|
||||
principal_type service; fifteen-minute JWT, no refresh or ID token. The secret
|
||||
must arrive from an existing **admitted** owner-controlled delivery. No client
|
||||
secret request or generation occurs here. A source fragment is not live
|
||||
registration or consumer entitlement. The inspected Platform operational identity
|
||||
is AppRole; its OpenBao token cannot substitute for this JWT. Its historical
|
||||
coding-agent prose still names tenant:coulomb, whereas current KeyCape source
|
||||
uses tenant:platform. Platform must reconcile its exact proposed/installed
|
||||
metadata, not use that historical prose as evidence.
|
||||
|
||||
No reviewed coding-agent client-side custody reader/consumer delivery receipt
|
||||
was found in the inspected CCR/registry sources. Platform must either return
|
||||
exact existing admission coordinates and fresh live registration/delivery evidence
|
||||
or own a new narrow custody/issuer/consumer admission proposal. Do not borrow
|
||||
secrets-engine's credential, widen its role, read raw KV, or mint an ephemeral
|
||||
coding-agent lookalike to manufacture this case. If unavailable, the joint
|
||||
session remains blocked and coding-agent refusal inconclusive; V5 requires both
|
||||
private token inputs and cannot silently skip the case.
|
||||
|
||||
## Private handoff adapter
|
||||
|
||||
`scripts/openbao_negative_private_handoff.py` adapts the two existing CLI files
|
||||
to V5 private stdin JSON keys human_token and coding_agent_token. It makes no
|
||||
network request, login, secret retrieval, registration, deployment or executor
|
||||
launch. The source is Linux main-thread only. Before file consumption it requires
|
||||
an inherited **writable pipe FD >=3**, never stdout/stderr, a new owner mode0700
|
||||
non-symlink directory outside Git, single-link mode0600 regular files owned by
|
||||
the current UID, bounded reads and unchanged inode/size/timestamps. JSON rejects
|
||||
duplicate/nonfinite keys; token length is bounded. Both files must be distinct.
|
||||
A second inode/metadata compare immediately precedes each unlink.
|
||||
|
||||
The adapter checks decoded issuer/audience/principal/agent claims and at least
|
||||
300 seconds remaining lifetime, plus human MFA shape. These are not signature
|
||||
verification or authenticated custody: its tests deliberately use unsigned
|
||||
synthetic JWT shapes. The founder must run the pinned current KeyCape CLI and
|
||||
retain exclusive custody continuously; arbitrary downloaded/preexisting files
|
||||
are inadmissible despite matching mode or claims. CLI signature verification is
|
||||
the issuer-verification boundary. Require a fresh remaining-lifetime check before
|
||||
native execution and ensure token validity through the actual presentation; the
|
||||
five-minute adapter floor is not a grant for a longer session.
|
||||
|
||||
Explicit `--consume-owner-files` authorizes removal of precisely these inspected
|
||||
caller-owned files. After both load successfully, the adapter consumes them
|
||||
before writing the pipe and closes its inherited endpoint on every exit. Partial
|
||||
unlink failure records possible retained files; partial/broken/timed-out delivery
|
||||
records unknown delivery and must stop before native mutation, with no retry.
|
||||
Delivery has a hard five-second SIGALRM bound; preexisting timer refuses and
|
||||
handler/timer state is restored. Receipts contain booleans and fixed failure
|
||||
codes only; a pipe write completion does not prove downstream validation or native
|
||||
acceptance. No stdout token path exists. A private pipe still requires a trusted
|
||||
consumer: the adapter cannot authenticate its peer, approval JSON or native go.
|
||||
|
||||
The founder-reviewed launcher must create the pipe and start a pinned destination
|
||||
wrapper before running the adapter with that endpoint inherited as FD3. The
|
||||
wrapper may invoke the already approved SSH transport to railiance01 and forward
|
||||
stdin without PTY, shell tracing, tee, terminal/stdout copies, token argv/env,
|
||||
logging, agent custody or persistent remote files. The destination reads the
|
||||
private JSON with V5; no additional file copy. Exact workstation producer and
|
||||
node wrapper paths, hashes, SSH host identity/transport grant, consumer startup
|
||||
order, refusal gating and receipt handling must be owner-reviewed first. This
|
||||
packet does not ship or admit such a launcher and cannot claim a full handoff.
|
||||
Token files must be consumed before starting V5; on failed preparation the owner
|
||||
removes remaining exact files. This packet requires founder choice to admit the
|
||||
two bounded caller-owned JWT files: V5's no-copy rule for generated six client
|
||||
secrets is unchanged. Python memory/Unix unlink are not secure-zeroization claims;
|
||||
use an owner tmpfs directory and process termination. Issued JWTs expire normally.
|
||||
No issuer-side JWT revocation is claimed. Any OpenBao acceptance remains V5's
|
||||
bounded self-revoke/Platform reconciliation obligation.
|
||||
|
||||
## Mandatory route and decision ordering
|
||||
|
||||
The child is a Linux main-thread process on railiance01 using its k3s kubeconfig.
|
||||
Workstation loopback18200 tunnel evidence does not prove node loopback18200;
|
||||
openbao.openbao.svc normally resolves from Pods, not necessarily the host. Keep
|
||||
native proposal gated until Platform/NetKingdom verify one of V5's exact origins
|
||||
**from the actual node process placement**, using a separately reviewed founder
|
||||
port-forward path if required. Do not expand V5 origins or use workstation role
|
||||
observations as route proof.
|
||||
|
||||
Before any token exchange, temporary credential generation, Secret creation,
|
||||
registry/env mutation or restart, the owner preflight must run a bounded public
|
||||
Bao health request from that exact placement to the selected origin. It follows
|
||||
no redirect or ambient proxy, caps response bytes and total elapsed time, sends
|
||||
no auth header and prints only normalized initialized/unsealed/health booleans.
|
||||
Verify acceptable active/standby health semantics independently, current TLS or
|
||||
local port-forward ownership, no unintended public bind and non-secret issuer
|
||||
connectivity. No auth, KV/data path or raw error output. The V5 transport currently
|
||||
allows login/revoke only; this additional public health/route preflight belongs
|
||||
in the reviewed launcher/NetKingdom guard, not an unreviewed V5 edit.
|
||||
|
||||
Owner sequence: select source plan -> independent KeyCape/NetKingdom/Platform
|
||||
reviews -> approved exact launcher plus route/health preflight -> admitted
|
||||
producer/custody and human browser attempt -> private-pipe delivery -> destination
|
||||
validates complete stdin/envelope/source/runtime/role receipts -> native action
|
||||
only under fresh exact founder authorization -> V5 cleanup/revoke plus producer
|
||||
cleanup. Any unavailable route/custody/client/complete input keeps T02 open.
|
||||
|
||||
## Exact acknowledgments still required
|
||||
|
||||
- KeyCape: selected client/claims/CLI verification, lifetime and MFA source;
|
||||
this packet is preparation, not a formal source acknowledgment of a launcher.
|
||||
- NetKingdom: actual existing registration/image, client reuse and callback
|
||||
listener ownership, browser/workstation/node placement, guarded public health
|
||||
preflight ordering and exact restart/cleanup runtime pins.
|
||||
- Platform: coding-agent existing custody/consumer entitlement or new proposal,
|
||||
corrected tenant metadata, route/port-forward ownership, fresh bounded role/
|
||||
leeway/effective-policy observation, session revoke and unknown reconciliation.
|
||||
- Founder: existing-client purpose reuse versus new registration, explicit private
|
||||
tmpfs JWT-file allowance, admitted exact launcher/transport/target/window/cleanup
|
||||
obligation and actual supervised custody. Source acceptance is not that go.
|
||||
|
||||
18 synthetic adapter checks pass: successful private write and file consumption,
|
||||
stdout/stderr/regular/read-only FD refusal, explicit consumption, symlink/hardlink/
|
||||
public mode/parent/Git/oversize/duplicate JSON refusal, wrong audience/MFA/tenant/
|
||||
role/AppRole/expiry refusal, replacement before unlink, hard write timeout with
|
||||
signal restoration and value-safe partial/broken delivery. Original V5/failed
|
||||
review histories and all source/current owner records remain retained.
|
||||
|
||||
# V2 correction and exact child-ready integration
|
||||
|
||||
The V1 packet text above is retained history. Proposal
|
||||
`KEY-WP-0039-T02/private-producer-handoff-v2` supersedes its unsafe no-ready
|
||||
adapter and its precise-inode-removal wording. V1's immutable manifest239aaa
|
||||
and NetKingdom consumer refusal a1ed14e6b2db1ae77c3fd638fd7e47b5bd50c25eebba418e31aba89bb4b7f88e
|
||||
remain recorded as the same correction trial. The exact compatible consumer is
|
||||
`KEY-WP-0039-T02/attended-lifecycle-v9-ready-input`, separately pinned below;
|
||||
V5/V6/V7/V8 source/review history is never implicitly repinned. No owner admission
|
||||
or founder go is supplied by V2.
|
||||
|
||||
The KeyCape launcher is `scripts/openbao_negative_private_launcher.py`. It
|
||||
validates its own and its adjacent adapter's exact source hashes plus the local
|
||||
copy of the exact remote envelope. The remote child/wrapper paths and hashes
|
||||
must occur in that envelope; proposal/target/source owner receipts, finite
|
||||
window/health pin and founder/file-allowance receipt references must match the
|
||||
reviewed contract. These fields are procedural constraints only: editable JSON
|
||||
cannot authenticate approval. The founder must authenticate/retain the actual
|
||||
exact disposition and own this entire launcher/custody/host context.
|
||||
|
||||
The source launcher alone creates SSH stdin/stdout pipes, a fresh random256-bit
|
||||
nonce and fixed direct SSH argv to railiance01. It uses no PTY, strict existing
|
||||
host-key checking, batch mode, no agent forwarding, no remote forwarding, finite
|
||||
connection/keepalive bounds, and a minimal environment that excludes confidential
|
||||
client credentials. It does not create or request a certificate, token, credential
|
||||
or route. This is a proposed **separate founder-admitted direct SSH profile**:
|
||||
the normal warden silent lane has different stdout rules and is not silently
|
||||
repurposed. The founder/transport owner must review actual node host identity,
|
||||
SSH certificate/session entitlement and pinned remote paths before execution.
|
||||
No SSH command was invoked during preparation.
|
||||
|
||||
On railiance01 the independently NetKingdom-owned pinned wrapper creates a
|
||||
local dedicated child-ready pipe and launches the exact pinned Python child
|
||||
with inherited SSH stdin. No invented extra SSH file-descriptor forwarding is
|
||||
used. Only after exact source/envelope checks, original action-window start and
|
||||
pinned public Bao health succeeds does the child emit a bounded value-free
|
||||
marker to that local pipe, close it, and enter the V8 bounded private receiver.
|
||||
The wrapper validates that local marker and forwards exactly one newline JSON
|
||||
marker on SSH stdout. SSH stdout stays open until child completion; Key reads
|
||||
only to the first newline, never waits for SSH stdout EOF. Child stdout/stderr
|
||||
stay suppressed; protected receipt remains exclusive on the node.
|
||||
|
||||
Ready schema `key39-private-input-ready/v1` has exactly these keys:
|
||||
schema, session_nonce, proposal_id, target, executor_sha256, envelope_sha256,
|
||||
bao_health_identity_sha256, valid_until_epoch, receive_budget_seconds. Marker
|
||||
must bind the new nonce and exact consumer proposal/target/source/envelope/health
|
||||
pin and epoch; receive budget must be finite nonboolean, positive and <=15s.
|
||||
Marker size is <=4096 bytes. An old/missing/malformed/mismatched/failed/expired
|
||||
ready marker produces **zero token file loads, zero consumption, zero private
|
||||
bytes**, then closes local endpoints. A readiness marker is not authentication,
|
||||
JWT verification, custody admission, routing authority or a live grant. It is
|
||||
trusted only because the reviewed launcher captures the intended pinned wrapper
|
||||
under the separately admitted authenticated host/session context. There is no
|
||||
standalone adapter CLI accepting a ready file, boolean or user-provided marker.
|
||||
|
||||
The adapter validates two distinct kernel pipe identities: private data endpoint
|
||||
write-only, control endpoint read-only, each FD>=3 and each device/inode distinct
|
||||
from stdin/stdout/stderr and from each other. Actual duplicates of stdout/stderr
|
||||
are refused even when their descriptor number is >=3. Captured SSH stdout is a
|
||||
separate control pipe owned by the launcher; it never becomes token output.
|
||||
|
||||
The launch preparation cap is5s. The adapter has one total5s deadline covering
|
||||
ready wait, caller-file load, cleanup and delivery. After ready this existing
|
||||
timer only clips downward against its remaining budget, received child budget
|
||||
and current remaining approval lifetime. Advertised child budget is not compared
|
||||
as <=current remaining wall time (transport necessarily consumes time); the
|
||||
actual delivery timer clips to both. Before each write and after completion the
|
||||
same absolute expiry/monotonic delivery bound is rechecked. No retries or clock
|
||||
reset; stdin closes to provide complete EOF. A partial write, transport break or
|
||||
expiry remains unknown and cannot be native proof. Launcher waits only for a
|
||||
value-free child/transport exit under remaining action +300s cleanup +30s margin;
|
||||
local transport teardown never proves remote custody or process reconciliation.
|
||||
Native receipt/unknown-session acceptance remains Platform/NetKingdom's.
|
||||
|
||||
## Honest file-cleanup boundary
|
||||
|
||||
Owner mode0700/0600, no symlink/hardlink/Git and unchanged identity/size/time
|
||||
checks remain. **The compare and unlink are not atomic inode-conditioned delete.**
|
||||
Continuously exclusive same-UID custody of the directory, no concurrent writers
|
||||
and no attacker sharing that UID are mandatory for this proposed file approach.
|
||||
No helper boolean can prove that condition. The founder must control the new
|
||||
private tmpfs directory/process context and retain exclusive custody; no shared
|
||||
agent/shell/daemon may modify it. Any known interference requires abort and owner
|
||||
reconciliation before another attempt.
|
||||
|
||||
V2 removes V1's exact-inode/atomic implications. Its retained stat-to-unlink race
|
||||
check deliberately shows that a concurrent same-UID replacement can still be
|
||||
removed; that is outside the admitted exclusive-custody assumption and is not
|
||||
represented as a fixed filesystem capability. Even after nominal path removal,
|
||||
receipts retain `atomic_inode_delete_guaranteed=false`,
|
||||
`exclusive_same_uid_custody_required=true`, `credential_copy_cleanup_verified=false`
|
||||
and `credential_files_remain_possible=true`. `caller_path_removal_completed`
|
||||
means only these unlink calls completed under that assumption. It is not token
|
||||
copy inventory, secure erase or cleanup proof. Owner verification/process
|
||||
termination/tmpfs teardown remains required; issued JWTs expire normally.
|
||||
If that exclusive context cannot be admitted, do not use these private files;
|
||||
separately implement/review a process-to-process in-memory producer instead.
|
||||
|
||||
The existing human-client reuse versus new registration, live MFA, coding-agent
|
||||
client-side custody/consumer delivery, exact authenticated launcher/SSH profile,
|
||||
private tmpfs files/exclusive custody, actual node route/provider/role/leeway,
|
||||
quiet restart window and fresh founder disposition are still unapproved choices.
|
||||
This source packet does not resolve them. Source-pinned readiness eliminates
|
||||
premature token transport only within the admitted trusted launcher context.
|
||||
|
||||
Exact compatible consumer manifest SHA256 84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14;
|
||||
child 24b7b803e4eb7e6e7c0700a069dad0ec315ee7eb5538a88b9e56147f8fab5cdd;
|
||||
node wrapper 76ac013d1a676a0dfa58aea1ce87b0c3e72483ccd21f586a22c5475e9132f6c1.
|
||||
31 distinct supplied producer/launcher synthetic checks pass; 74 supplied
|
||||
compatible consumer source checks pass. No actual SSH, login, tokens, custody
|
||||
or native route was used. Fresh independent consumer and Platform source
|
||||
reviews of this exact V2 and V9 remain necessary before founder choice.
|
||||
|
||||
# V3 remote-unknown reporting correction
|
||||
|
||||
Proposal `KEY-WP-0039-T02/private-producer-handoff-v3`. Exact NetKingdom V9
|
||||
manifest84542ddc and child/wrapper bytes are unchanged; this is only a new Key
|
||||
launcher reporting revision in the same preparation trial. Platform V2 pair
|
||||
review finding RPF-KEY39-PAIR-V2-F01 is retained unchanged, SHA256
|
||||
8353eb8aae11be8ce874887d6a5ca10bf37c6fba73c3f85f0644a7546dd7d33f.
|
||||
|
||||
After copying adapter outcome, `private_input_delivery_unknown=true` immediately
|
||||
sets sticky `consumer_outcome_unknown=true` and `consumer_process_completed=false`
|
||||
before any failure/early return. A complete payload can reach the remote child
|
||||
before local write/EOF confirmation times out. Terminating local SSH cannot
|
||||
prove that no remote operation occurred, that the child stopped or that remote
|
||||
custody/cleanup completed. No retry or subsequent local success rewrites this
|
||||
attempt. Native acceptance and unknown resolution stay independent owner duties.
|
||||
|
||||
33 distinct checks pass, including actual full kernel-pipe JSON write followed
|
||||
by SIGALRM interruption before confirmation, and an actual4096-byte partial
|
||||
write/timeout; both retain remote unknown after local terminate. No actual SSH,
|
||||
protected consumer or credentials were involved. All V2/V1 source, unsuccessful
|
||||
reviews and previous narrow issuer acknowledgments remain frozen history.
|
||||
Exact source return and all prior native/client/custody/transport/file choices
|
||||
remain required. Source reporting correction is not runtime admission.
|
||||
|
||||
# V4 source-acquisition and shared preparation deadline correction
|
||||
|
||||
Proposal `KEY-WP-0039-T02/private-producer-handoff-v4`, same trial. The exact
|
||||
Net V9 manifest84542ddc and child/wrapper are unchanged. NetKingdom V3 consumer
|
||||
refusal NET-PRODUCER-V3-F01 stays frozen, SHA256
|
||||
38abf660701194f85934a076a0c01d3047e6bfe790e02ba1b0e4616e356634e6. V1/V2/V3 source and earlier acknowledgments/refusals remain history.
|
||||
|
||||
Actual CLI now starts one absolute monotonic5-second preparation deadline before
|
||||
contract acquisition. Initial source read, source/envelope/hash checks and SSH
|
||||
spawn then ready/file/consumption/write phase all use only remaining time from
|
||||
that original deadline. Phase timers may disarm/rearm, but each is clipped to
|
||||
original remaining time; launch never restarts the budget spent reading contract.
|
||||
After ready, child receive budget and absolute approval expiry only clip further.
|
||||
Once private input is delivered, existing separate action/cleanup completion
|
||||
wait remains; it cannot renew preparation or mutation authority.
|
||||
|
||||
Contract/envelope reads use bounded4096-byte chunks, at most65536 bytes, absolute
|
||||
paths, no observed symlink ancestry, O_NOFOLLOW/O_NONBLOCK, single-link regular
|
||||
files with no group/world write, and stable device/inode/UID/mode/link/size/time
|
||||
comparisons. FIFOs/devices/symlinks/empty/oversized files refuse before private
|
||||
file access/SSH. Source digest reads share that path with at most262144 bytes.
|
||||
All actual CLI source acquisition runs under the finite remaining preparation
|
||||
alarm; a stalled filesystem call cannot escape this total deadline. Observed
|
||||
path/source races refuse; continuously trusted exclusive custody remains required
|
||||
and no atomic pathname/exec protection or authenticated metadata grant is claimed.
|
||||
|
||||
40 distinct producer/launcher checks pass, including actual CLI FIFO/symlink/
|
||||
oversize refusal, delayed regular read under the hard timer with restored signals,
|
||||
a main contract-read delay followed by launch delay that cannot renew the budget,
|
||||
source drift/chunk bounds and earlier actual full/partialwrite stickyunknown.
|
||||
Nine retained real paired handshake regressions also pass using actual pinned
|
||||
Net wrapper and child main with only local fake SSH/health/execute substitutions:
|
||||
no network, Kubernetes, protected credentials or native mutation. The original
|
||||
Net fixture files are copied unchanged except the Key stage path, solely as
|
||||
source regression evidence, never a new foreign formal owner review.
|
||||
|
||||
Fresh exact independent pair source returns and all unadmitted human/MFA/client/
|
||||
coding-agent custody/consumer/directSSH/exclusive tmpfs/route/provider/role/leeway/
|
||||
readiness-Argo/maintenance-exception/founder/native-acceptance duties still gate
|
||||
use. No source-only helper choice, ready marker or editable approval JSON supplies
|
||||
that authority. T01done/T02open; actual native outcome remains pending.
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"schema": "founder-source-design-choice/v1",
|
||||
"proposal_id": "KEY-WP-0039-T02/founder-source-design-v1",
|
||||
"status": "pending_founder_source_direction_only",
|
||||
"submitted": false,
|
||||
"memo_sha256": "1816e48b44eb874e4c97db82ee56ed27c23083ddcfc322d9de9876132e802688",
|
||||
"source_pair": {
|
||||
"key_producer_manifest_sha256": "d6f641dc1e4aae7d36658d4199d89af14d2fd7a4bd4e526856eb340234f2d33e",
|
||||
"net_consumer_manifest_sha256": "84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14",
|
||||
"net_qualified_consumer_review_sha256": "9566955b77db4a0b041887ef4a6aff6c7a02ef30cc0727a4deb6c29bbdc79518",
|
||||
"platform_fresh_exact_pair_source_return": "required separately"
|
||||
},
|
||||
"choices": [
|
||||
{
|
||||
"id": "human_producer",
|
||||
"recommended": "existing_bootstrap_console_purpose_reuse_source_direction",
|
||||
"options": [
|
||||
"existing_bootstrap_console_purpose_reuse_source_direction",
|
||||
"new_narrow_public_client_source_proposal",
|
||||
"defer"
|
||||
],
|
||||
"selected": null,
|
||||
"exact_existing_client": "netkingdom-bootstrap-console",
|
||||
"exact_audience": "netkingdom-bootstrap-console",
|
||||
"scope": "openid",
|
||||
"callback": "http://127.0.0.1:8876/oidc/callback",
|
||||
"pending": [
|
||||
"Net owner purpose reuse/live registration",
|
||||
"Sole intended workstation CLI listener; no existing port8876 occupant",
|
||||
"Actual provider MFA receipt; verified aal2/mfa claims"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "coding_agent",
|
||||
"recommended": "return_existing_admission_or_prepare_narrow_platform_proposal",
|
||||
"options": [
|
||||
"return_existing_admission_or_prepare_narrow_platform_proposal",
|
||||
"defer"
|
||||
],
|
||||
"selected": null,
|
||||
"client_audience": "codex-railiance-platform",
|
||||
"subject": "service:codex:railiance-platform",
|
||||
"tenant": "tenant:platform",
|
||||
"scope": "openbao:login",
|
||||
"custody_admission_receipt": null,
|
||||
"consumer_coordinates": null,
|
||||
"new_ccr_number": null,
|
||||
"approle_jwt_substitution_allowed": false
|
||||
},
|
||||
{
|
||||
"id": "transport",
|
||||
"recommended": "retain_proposed_founder_direct_ssh_source_profile",
|
||||
"options": [
|
||||
"retain_proposed_founder_direct_ssh_source_profile",
|
||||
"redesign_and_review",
|
||||
"defer"
|
||||
],
|
||||
"selected": null,
|
||||
"host": "railiance01",
|
||||
"compatible_with_warden_silent_lane": false,
|
||||
"authenticated_host_session_entitlement_receipt": null,
|
||||
"runtime_paths": null,
|
||||
"granted": false
|
||||
},
|
||||
{
|
||||
"id": "private_material",
|
||||
"recommended": "conditionally_retain_exclusive_owner_tmpfs_file_source_design",
|
||||
"options": [
|
||||
"conditionally_retain_exclusive_owner_tmpfs_file_source_design",
|
||||
"review_in_memory_producer_design",
|
||||
"defer"
|
||||
],
|
||||
"selected": null,
|
||||
"exclusive_same_uid_custody_verified": false,
|
||||
"file_allowance_receipt": null,
|
||||
"atomic_unlink_claimed": false,
|
||||
"secure_zeroization_claimed": false
|
||||
}
|
||||
],
|
||||
"permitted_effect_of_source_direction": "Owner source/design preparation and exact independent review only",
|
||||
"cost_and_risk_scope": {
|
||||
"external_spend_authorized_eur": 0,
|
||||
"native_cost_risk_decision": "separate future scoped proposal required",
|
||||
"source_risks": [
|
||||
"Purpose reuse/MFA/callback listener unadmitted",
|
||||
"Coding-agent custody missing",
|
||||
"Distinct SSH profile/session identity unadmitted",
|
||||
"SameUID private-file exclusivity non-atomic/no zeroization",
|
||||
"Provider/plugin/role/leeway and readiness/Argo/maintenanceexception facts pending"
|
||||
]
|
||||
},
|
||||
"native_authorized": false,
|
||||
"credentials_or_login_authorized": false,
|
||||
"ssh_or_portforward_authorized": false,
|
||||
"registrations_or_restart_authorized": false,
|
||||
"architecture_whole_v0_9_accepted": false,
|
||||
"agent_autonomy_promoted": false,
|
||||
"standing_privilege_or_default_flip": false,
|
||||
"KEY39_T02_closed": false
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
# Founder source-design choice for KEY39 private input
|
||||
|
||||
Proposal ID: `KEY-WP-0039-T02/founder-source-design-v1`.
|
||||
Disposition: pending founder choice. Scope: source-design direction only.
|
||||
|
||||
Recommended: retain the existing bootstrap-console human client for one attended
|
||||
negative-class producer, require Platform to return exact existing coding-agent
|
||||
admission or a new narrow owner proposal, and retain the reviewed direct SSH
|
||||
ready-channel design for later separate admission. Approving that direction
|
||||
allows owner source preparation and review. It does not allow a login, credential
|
||||
request/read, client registration, port-forward, SSH session, native preflight,
|
||||
issuer restart or mutation.
|
||||
|
||||
## Exact source under consideration
|
||||
|
||||
Key producer V4 manifest:
|
||||
`d6f641dc1e4aae7d36658d4199d89af14d2fd7a4bd4e526856eb340234f2d33e`,
|
||||
`/tmp/keycape-jwt-producer-handoff-v4-stage/manifest.json`.
|
||||
|
||||
Net consumer V9 manifest:
|
||||
`84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14`,
|
||||
`/tmp/keycape-jwt-ready-v9-stage/manifest.json`.
|
||||
|
||||
Launcher `46e57504b3185cc13f1656901a0ca02cc3887527cb09566e270f57b76099ab9c`;
|
||||
adapter `faf63a3e9504dcbb46847a114275f83d508521e095410a47f92f02b86ba82b1b`;
|
||||
child `24b7b803e4eb7e6e7c0700a069dad0ec315ee7eb5538a88b9e56147f8fab5cdd`;
|
||||
node wrapper `76ac013d1a676a0dfa58aea1ce87b0c3e72483ccd21f586a22c5475e9132f6c1`.
|
||||
|
||||
NetKingdom's exact qualified consumer source agreement:
|
||||
`9566955b77db4a0b041887ef4a6aff6c7a02ef30cc0727a4deb6c29bbdc79518`.
|
||||
Its 40 maker and 13 independent checks establish source behavior with synthetic
|
||||
transport/health/private tokens. Platform's fresh exact-pair source return is
|
||||
required separately. KeyCape's issuer/interface judgment stays inside its INTENT
|
||||
Tooling boundary. No source return is a protected-operation entitlement.
|
||||
|
||||
## Choices to record
|
||||
|
||||
| Choice | Recommended source direction | Alternative and consequence |
|
||||
|---|---|---|
|
||||
| Human producer | Owner-permitted reuse of `netkingdom-bootstrap-console` for one attended CLI session; request `openid` only. | If NetKingdom rejects purpose reuse, propose public `key39-negative-human`, audience same name, literal callback `http://127.0.0.1:8877/oidc/callback`, scopes [openid], authorization_code, mfaRequired true. This needs a new source/registration/rollback packet and changed adapter audience. No registration is implied. |
|
||||
| Coding-agent input | Require Platform's exact existing `codex-railiance-platform` live registration and client-side custody/consumer delivery receipts. | If missing, stop this native case and let Platform prepare a narrow custody/issuer/consumer admission proposal with coordinates/owners settled first. No invented CCR number or entitlement; AppRole is not a JWT substitute. |
|
||||
| Transport source profile | Retain proposed founder-owned direct SSH captured-control profile to railiance01, with pinned child/wrapper/envelope and exact-child-ready gate. | If that profile cannot be independently admitted, keep the native session gated and redesign/review a compatible transport. Do not reuse the warden silent lane or eager shell piping. |
|
||||
| Caller-token material | Source-design option: two owner-created private tmpfs files, mode0700 directory/mode0600 single-link files, then explicit consumption and pipe-only transfer. | If continuously exclusive same-UID custody cannot be established, do not use the file design; separately review an in-memory producer. Source approval does not admit either custody mechanism. |
|
||||
|
||||
No answer, partial answer or rejected choice defaults to a gated native session.
|
||||
The recommendation is a direction for source preparation, not an automatic
|
||||
registration, credential generation or admission.
|
||||
|
||||
## Human identity, callback and MFA contract
|
||||
|
||||
Exact candidate client/audience: `netkingdom-bootstrap-console`.
|
||||
Issuer: `https://kc.coulomb.social`. Requested scope: `openid`.
|
||||
Literal callback: `http://127.0.0.1:8876/oidc/callback`.
|
||||
|
||||
The current source registration is public authorization_code and lists this
|
||||
literal callback separately from localhost. KeyCape CLI requires literal
|
||||
loopback, binds `127.0.0.1:8876` before emitting its browser URL, and accepts
|
||||
only GET, exact Host `127.0.0.1:8876`, exact path `/oidc/callback`, one matching
|
||||
state and one code. Fresh state/nonce/S256 PKCE, five-minute attempt timeout,
|
||||
RS256 discovery/JWKS verification, access audience/requested scopes and ID-token
|
||||
client audience/nonce/matching subject remain the existing source contracts.
|
||||
Do not alias localhost, change a callback string or redirect a browser to the
|
||||
node. Producer and browser are on the founder-owned workstation.
|
||||
|
||||
Before any future attempt, NetKingdom must confirm this exact client is live,
|
||||
accept its proposed CLI purpose reuse, establish that no bootstrap console or
|
||||
other listener owns port8876, and identify the sole intended CLI listener/process
|
||||
in the attended workstation context. Bind failure stops, never kills or adopts
|
||||
another listener. A public callback and source registration alone are not that
|
||||
runtime evidence.
|
||||
|
||||
This registration has no per-client mfaRequired pin. Earlier deployment-default
|
||||
MFA statements are historical, not current proof. The future owner receipt must
|
||||
confirm actual provider/client MFA behavior; producer handoff requires verified
|
||||
human claims `assurance.level=aal2` and `assurance.mfa=true`, not an unenforced
|
||||
caller label. The CLI cryptographically verifies returned tokens; the adapter's
|
||||
decoded shape check does not. A human token may differ on audience/tenant/roles
|
||||
as well as principal_type, so this is human-class refusal, not isolated proof
|
||||
of principal_type enforcement. Browser access-token lifetime uses the server
|
||||
lifetime; adding per-client tokenLifetime does not shorten that flow.
|
||||
|
||||
## Coding-agent contract and actual missing custody
|
||||
|
||||
Exact accepted source identity:
|
||||
client/audience `codex-railiance-platform`, subject
|
||||
`service:codex:railiance-platform`, tenant `tenant:platform`, roles [coding-agent],
|
||||
principal_type service, scope `openbao:login`, fifteen-minute JWT.
|
||||
Secret reference name: `KEYCAPE_CODEX_RAILIANCE_PLATFORM_CLIENT_SECRET`.
|
||||
This is a reference, never a value or a request for one.
|
||||
|
||||
Source acceptance does not prove the client is live or supply consumer-side
|
||||
secret injection. No admitted coding-agent custody/consumer receipt was found
|
||||
in the inspected source set. Platform must return exact existing admission and
|
||||
fresh registration/delivery ownership or prepare a separate narrow proposal.
|
||||
Historical Platform coding-agent prose names tenant:coulomb and describes an
|
||||
operational AppRole; it cannot supersede current tenant:platform issuer source
|
||||
or manufacture the requested JWT. Do not skip this case, borrow secrets-engine's
|
||||
credential, retrieve raw KV, widen an AppRole/policy or make a temporary coding-
|
||||
agent lookalike. Full KEY39 native evidence remains incomplete until settled.
|
||||
|
||||
## Proposed source profile and trust limits
|
||||
|
||||
The source launcher creates captured stdin/data and stdout/control pipes for a
|
||||
fixed direct SSH command to railiance01: no PTY, strict existing host-key checking,
|
||||
batch mode, no agent forwarding or remote forwarding, bounded connect/keepalive
|
||||
and credential-excluding environment. The node wrapper creates a local ready
|
||||
pipe; SSH does not forward arbitrary extra file descriptors. Child/wrapper/
|
||||
envelope hashes, exact target `sso/keycape`, proposal, fresh nonce, public-health
|
||||
identity hash, original finite window and receive budget bind the returned marker.
|
||||
No marker means zero caller-token loads/consumption/private bytes. Caller STDIO
|
||||
aliases are refused by kernel pipe identity. Source acquisition and handoff share
|
||||
one original five-second preparation budget; V8's bounded complete EOF receive
|
||||
and original action/cleanup limits remain. Uncertain delivery stays remote
|
||||
consumer uncertainty after local SSH termination.
|
||||
|
||||
This captured control stdout profile is **incompatible with the existing warden
|
||||
silent-output lane**. It is not an exception inferred from shell prefixes or
|
||||
source review. Future direct SSH admission must name the authenticated founder
|
||||
identity/session/certificate entitlement, exact host key/transport/runtime paths,
|
||||
producer/consumer process identities and custody/window/cleanup obligations.
|
||||
No certificate, credential, SSH access or profile activation is requested here.
|
||||
The ready marker synchronizes source readiness; it does not authenticate a peer,
|
||||
issuer, provider, founder decision or privileged grant by itself.
|
||||
|
||||
The file option requires continuously exclusive same-UID directory custody, no
|
||||
concurrent writer and owner tmpfs/process teardown. Stat/unlink is non-atomic.
|
||||
Receipts explicitly keep atomic delete and copy cleanup unverified and possible
|
||||
copies true. Unix unlink/Python memory are not secure-zeroization guarantees;
|
||||
remaining or unknown copies/sessions require owner reconciliation. If these
|
||||
assumptions cannot be established, the file option must remain unused.
|
||||
|
||||
## Facts still required before a separate native decision
|
||||
|
||||
NetKingdom/Platform must verify the actual same-node route. Workstation
|
||||
loopback18200 does not prove node loopback18200; Pod DNS is not assumed for the
|
||||
host. A public health cluster digest detects changes but does not authenticate
|
||||
the provider. Actual admitted route/Service/cluster identity, freshly observed
|
||||
installed JWT plugin, role/config/effective policies and all expiration,
|
||||
clock-skew and not-before leeway interactions are pending native facts.
|
||||
|
||||
Issuer readiness_state, actual Argo ownership and production-tier mapping must
|
||||
be established authoritatively. Absence of a declaration is not nonproduction.
|
||||
If adopted/production-approved, temporary direct CAS changes require an explicit
|
||||
reviewed one-use maintenance exception, close-down entry and reconciliation.
|
||||
Headroom, quiet restart window, exact runtime revision, approved temporary names
|
||||
and cleanup/recovery remain required.
|
||||
|
||||
Future native founder go must be a distinct authenticated scoped disposition for
|
||||
exact approved source/runtime/target/window/route/producer/credential custody and
|
||||
bounded abort/revoke/restoration/recovery duties. Source-design approval today
|
||||
cannot be replayed as that go. Actual eight refusals including expiry positive
|
||||
control, revoked unexpected acceptance, baseline restoration/removed clients/
|
||||
temporary-object absence/private-copy reconciliation remain required afterward.
|
||||
|
||||
This memo approves no architecture-wide v0.9 acceptance, agent autonomy promotion,
|
||||
standing privilege/default flip, routing activation, native KEY39 closure,
|
||||
Platform/Mason acceptance or Secrets Engine full adoption. It leaves T01done and
|
||||
T02open under the existing work record, with all failed/revised source history.
|
||||
74
docs/reviews/2026-10-11-key39-v9-v4-keycape-source-ack.json
Normal file
74
docs/reviews/2026-10-11-key39-v9-v4-keycape-source-ack.json
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"schema": "owner-source-review/v1",
|
||||
"review_id": "KEY-ACK-20261011-KEY39-V9-PRODUCER-V4",
|
||||
"reviewer": "codex acting in KeyCape issuer-owner scope",
|
||||
"role_scope": "KeyCape INTENT authentication/issuance/claims and caller token lifecycle only",
|
||||
"decision": "approved",
|
||||
"qualification": "Exact bounded issuer/private interface source compatibility only; all native/transport/custody/founder admissions remain false",
|
||||
"consumer_proposal_id": "KEY-WP-0039-T02/attended-lifecycle-v9-ready-input",
|
||||
"consumer_manifest_sha256": "84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14",
|
||||
"producer_proposal_id": "KEY-WP-0039-T02/private-producer-handoff-v4",
|
||||
"producer_manifest_sha256": "d6f641dc1e4aae7d36658d4199d89af14d2fd7a4bd4e526856eb340234f2d33e",
|
||||
"work_record": {
|
||||
"task": "KEY-WP-0039-T03",
|
||||
"status": "done",
|
||||
"same_task_refinement": true,
|
||||
"current_owner_work_record_sha256": "57d5f1a9cd5d15737d47a9273bf01355ba818b2748e1a43fd7ff2138bed50f4f",
|
||||
"completed_inspection_receipt_path": "/tmp/keycape-jwt-v9-v4-owner-decision-stage/key-cape/docs/reviews/2026-10-11-key39-v9-v4-keycape-work-record.json",
|
||||
"completed_inspection_receipt_sha256": "b5074fdc22eb8f62ae035254bfb8311272cf8a7ec6ed14b60eebd14f5ef4046c",
|
||||
"completed_before_formal_acknowledgment": true
|
||||
},
|
||||
"scope_accepted": [
|
||||
"Original finite source/window/public health gates precede nonce-bound ready and complete bounded private input; action window is not renewed",
|
||||
"V4 actual CLI regular bounded nofollow/nonblock stable source acquisition starts under one ORIGINAL5s preparation budget shared with launch/ready/filewrite",
|
||||
"Exact child/wrapper/envelope/health/window source marker matches private Key reader before ANY token load/consume/write; fixed owner-created SSH data/control pipes and stdio alias refusal",
|
||||
"Private uncertain write remains sticky remote uncertainty after local transport teardown; full/partial failure histories retained",
|
||||
"Existing issuer/client claim design, original runner and explicit unauthenticated shape/ready metadata limits unchanged",
|
||||
"Non-atomic unlink requires continuous exclusive sameUID custody; copy cleanup remains unverified/possible"
|
||||
],
|
||||
"findings": [],
|
||||
"fresh_qualified_net_consumer_review": {
|
||||
"path": "/tmp/netkingdom-key39-producer-v4-consumer-review-stage/owner-review.json",
|
||||
"sha256": "9566955b77db4a0b041887ef4a6aff6c7a02ef30cc0727a4deb6c29bbdc79518",
|
||||
"source_only": true,
|
||||
"not_a_KeyCape_Net_runtime_review": true
|
||||
},
|
||||
"validation": {
|
||||
"key_source_tests_retained": 40,
|
||||
"net_exact_pair_independent_checks": 13,
|
||||
"own_fresh_interface_tests": 3,
|
||||
"all_passed": true,
|
||||
"own_checks_path": "/tmp/keycape-jwt-v9-v4-owner-decision-stage/evidence/test_keycape_ready_interface.py",
|
||||
"own_checks_sha256": "ba85e5ff2cda5c7c2cc86f50bde67bca027e2e63be1c5195f06764b8e1cd5716",
|
||||
"synthetic_unsigned_private_fixtures_only": true,
|
||||
"native_or_credentials": false
|
||||
},
|
||||
"excluded": [
|
||||
"Net formal runtime/route/port-forward/restarts/cleanup acceptance",
|
||||
"Platform formal provider/plugin/role/policy/leeway/custody/PEP/backend acceptance",
|
||||
"Founders actual identity/session/source/runtime/route/window/native decision",
|
||||
"Human client purpose/MFA/listener and coding-agent live custody/consumer admission",
|
||||
"Direct SSH profile, file allowance/exclusive UID context, authenticated transport/runtime authority",
|
||||
"Whole architecture v0.9 acceptance, autonomy promotion, standing privileges/default flips or native task closure"
|
||||
],
|
||||
"pending_platform_exact_pair_return": "Required separately; old V2pairrefusal is not repinned",
|
||||
"founder_source_design_memo": {
|
||||
"path": "/tmp/keycape-jwt-v9-v4-owner-decision-stage/key-cape/docs/proposals/2026-10-11-key39-founder-source-design-decision.md",
|
||||
"sha256": "1816e48b44eb874e4c97db82ee56ed27c23083ddcfc322d9de9876132e802688",
|
||||
"status": "unsubmitted source-direction choice only",
|
||||
"native_go": false
|
||||
},
|
||||
"sources_receipt_sha256": "2a7a587264ceeb902040417d0bc5c329af461590d8bc99edf818ec5699f0a5b0",
|
||||
"prior_v2_bound_ack_sha256": "8293e18dcffe7b8e0748880753974219b99d347a173f75a32cc1aad2b4c30f95",
|
||||
"preserved_refusals": [
|
||||
"a1ed14e6b2db1ae77c3fd638fd7e47b5bd50c25eebba418e31aba89bb4b7f88e",
|
||||
"8353eb8aae11be8ce874887d6a5ca10bf37c6fba73c3f85f0644a7546dd7d33f",
|
||||
"38abf660701194f85934a076a0c01d3047e6bfe790e02ba1b0e4616e356634e6"
|
||||
],
|
||||
"same_trial_no_additional_scored_success": true,
|
||||
"native_authority_granted": false,
|
||||
"native_route_observed": false,
|
||||
"private_producer_admission": false,
|
||||
"founder_design_disposition_submitted": false,
|
||||
"KEY39_T02_closed": false
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
{
|
||||
"owner": "key-cape",
|
||||
"consumer_manifest_path": "/tmp/keycape-jwt-ready-v9-stage/manifest.json",
|
||||
"consumer_manifest_sha256": "84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14",
|
||||
"producer_manifest_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/manifest.json",
|
||||
"producer_manifest_sha256": "d6f641dc1e4aae7d36658d4199d89af14d2fd7a4bd4e526856eb340234f2d33e",
|
||||
"qualified_net_consumer_review_path": "/tmp/netkingdom-key39-producer-v4-consumer-review-stage/owner-review.json",
|
||||
"qualified_net_consumer_review_sha256": "9566955b77db4a0b041887ef4a6aff6c7a02ef30cc0727a4deb6c29bbdc79518",
|
||||
"exact_consumer_entries": [
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/openbao-negative-session.py",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/openbao-negative-session.py",
|
||||
"original_sha256": "da4e0ab767e0c3bebb48bcf5ab578b50845d9fa7211a6771661a0e1baa4314ab",
|
||||
"staged_sha256": "24b7b803e4eb7e6e7c0700a069dad0ec315ee7eb5538a88b9e56147f8fab5cdd",
|
||||
"previous_frozen_staged_sha256": "da4e0ab767e0c3bebb48bcf5ab578b50845d9fa7211a6771661a0e1baa4314ab"
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/test_openbao_negative_session.py",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/test_openbao_negative_session.py",
|
||||
"original_sha256": "01e23f2b4fc77824a161a57acb3a52d19aad846b3b6c0fd3abd81afa1dc2985c",
|
||||
"staged_sha256": "01e23f2b4fc77824a161a57acb3a52d19aad846b3b6c0fd3abd81afa1dc2985c",
|
||||
"previous_frozen_staged_sha256": "01e23f2b4fc77824a161a57acb3a52d19aad846b3b6c0fd3abd81afa1dc2985c"
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/test_openbao_node_route.py",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/test_openbao_node_route.py",
|
||||
"original_sha256": "9f3d8d4297c5ef68f6c6bf7ca21e981e37d7eceb836a60bc254d33b0f5129047",
|
||||
"staged_sha256": "1c57053be6e8472174e4034fdfbf34b3e1729f5e239b3a9af89d9e59a08d6cb6",
|
||||
"previous_frozen_staged_sha256": "9f3d8d4297c5ef68f6c6bf7ca21e981e37d7eceb836a60bc254d33b0f5129047"
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/openbao-negative-node-route.md",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/openbao-negative-node-route.md",
|
||||
"original_sha256": "a04c94512efec7a524b15a3eec61d86de5a41df872e59a3114093063a11e75a0",
|
||||
"staged_sha256": "cad3e3dc481d45295cc014e48285e81614911dfb4a10523883f3044f6085f363",
|
||||
"previous_frozen_staged_sha256": "a04c94512efec7a524b15a3eec61d86de5a41df872e59a3114093063a11e75a0"
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/test_openbao_private_input.py",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/test_openbao_private_input.py",
|
||||
"original_sha256": "a59c2cd4ec895c1718d52fd12f64666f1be45ae423fde476755b6d77a89280c2",
|
||||
"staged_sha256": "84277edb5bd7858885173dea802b6bdc4f0f0c7785d6e200ee9a98d8553891a1",
|
||||
"previous_frozen_staged_sha256": "a59c2cd4ec895c1718d52fd12f64666f1be45ae423fde476755b6d77a89280c2"
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/openbao-negative-node-wrapper.py",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/openbao-negative-node-wrapper.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "76ac013d1a676a0dfa58aea1ce87b0c3e72483ccd21f586a22c5475e9132f6c1"
|
||||
},
|
||||
{
|
||||
"repo": "net-kingdom",
|
||||
"path": "sso-mfa/k8s/keycape/test_openbao_private_ready.py",
|
||||
"staged_path": "/tmp/keycape-jwt-ready-v9-stage/net-kingdom/sso-mfa/k8s/keycape/test_openbao_private_ready.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "8a7a033b1c509ba21397d1d74580c44f24592045b2b9de50fe87638607224277"
|
||||
}
|
||||
],
|
||||
"exact_producer_entries": [
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "docs/openbao-negative-token-producer-decision.json",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/docs/openbao-negative-token-producer-decision.json",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "7b3e4a53ac4af33c780f16c3f1aa9cde0154040ad2747a8d361dd79bbb7f435c",
|
||||
"previous_frozen_staged_sha256": "ee5dccb8d3b263584127edff73a6f9292b29eb2725f982cdfdbd784141bdd98f"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "docs/openbao-negative-token-producer-decision.md",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/docs/openbao-negative-token-producer-decision.md",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "38e4c9660ca57a1fb6e5ff04f214af61dd1a58abf564a5ed22216d909e27fc73",
|
||||
"previous_frozen_staged_sha256": "50994f0882bac8c78227e8d8669f8be7ab9a07a8a35f1f8cc0bc5caeac275aa3"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "scripts/openbao_negative_private_handoff.py",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/scripts/openbao_negative_private_handoff.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "faf63a3e9504dcbb46847a114275f83d508521e095410a47f92f02b86ba82b1b",
|
||||
"previous_frozen_staged_sha256": "faf63a3e9504dcbb46847a114275f83d508521e095410a47f92f02b86ba82b1b"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "scripts/openbao_negative_private_launcher.py",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/scripts/openbao_negative_private_launcher.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "46e57504b3185cc13f1656901a0ca02cc3887527cb09566e270f57b76099ab9c",
|
||||
"previous_frozen_staged_sha256": "2a54568df541c38d43ca2482f037761770e6df206fb1cb88c1222ed7291f5e98"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "scripts/test_openbao_negative_private_handoff.py",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/scripts/test_openbao_negative_private_handoff.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "e4eb600718e7d93b0e801ac3ab1427af9895a6e5d877c1134369e049e0849797",
|
||||
"previous_frozen_staged_sha256": "e4eb600718e7d93b0e801ac3ab1427af9895a6e5d877c1134369e049e0849797"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "scripts/test_openbao_negative_private_launcher.py",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/scripts/test_openbao_negative_private_launcher.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "243426a85075b3447adeb2ca320a13711409744f71837a06699de6ad5b04ee9d",
|
||||
"previous_frozen_staged_sha256": "243426a85075b3447adeb2ca320a13711409744f71837a06699de6ad5b04ee9d"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "scripts/test_openbao_negative_private_preparation.py",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/scripts/test_openbao_negative_private_preparation.py",
|
||||
"original_sha256": null,
|
||||
"staged_sha256": "8e4dd327c4a339a0151ecba57db5987965b5d536ca18123fb7c74a047a241548"
|
||||
},
|
||||
{
|
||||
"repo": "key-cape",
|
||||
"path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md",
|
||||
"staged_path": "/tmp/keycape-jwt-producer-handoff-v4-stage/key-cape/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md",
|
||||
"original_sha256": "57d5f1a9cd5d15737d47a9273bf01355ba818b2748e1a43fd7ff2138bed50f4f",
|
||||
"staged_sha256": "f6113d269f127976518acb48eca818fd5d6f12421f4e4c742e8a84f49a14360b",
|
||||
"previous_frozen_staged_sha256": "1385f0b692a031fd52046e1dd98c42ff7a5a4ca4d1c340551ec8751a07e80ee6"
|
||||
}
|
||||
],
|
||||
"completed_inspection_receipt_sha256": "b5074fdc22eb8f62ae035254bfb8311272cf8a7ec6ed14b60eebd14f5ef4046c",
|
||||
"founder_source_memo_sha256": "1816e48b44eb874e4c97db82ee56ed27c23083ddcfc322d9de9876132e802688",
|
||||
"founder_source_choices_sha256": "d2f6e2fc733d8092a27a1dafb2f77601cbff398c5606b12bb053b4cd7bcd4091",
|
||||
"current_key_source_pins": [
|
||||
{
|
||||
"path": "AGENTS.md",
|
||||
"sha256": "77c8e1ac762e7c101cd09a3aa184993309ed057821bd53fb0d318979a45c9315"
|
||||
},
|
||||
{
|
||||
"path": "INTENT.md",
|
||||
"sha256": "b22cb440691bb399b064ee10ab1a03e1e07838da1051bf6235fbfe2cf820eb10"
|
||||
},
|
||||
{
|
||||
"path": "src/internal/authclient/cli.go",
|
||||
"sha256": "376a8e0d1d60825a8b3ef21d371e3fc353c0a34e461712a6899e264278edbe7f"
|
||||
},
|
||||
{
|
||||
"path": "src/internal/authclient/client.go",
|
||||
"sha256": "61fb150349c5d3714450a90834c59af9a579a8a649c4149297a402cfcf6e39c5"
|
||||
},
|
||||
{
|
||||
"path": "src/internal/server/oidc/token.go",
|
||||
"sha256": "8422cc9dbd39c88dfe835b43f6262cb120bc0d2e5b08898a151167b7555368d3"
|
||||
},
|
||||
{
|
||||
"path": "config/service-clients.example.yaml",
|
||||
"sha256": "b7e17698a0946f387c481b7e27fe9ddea22cdd9db2e34e32a50457e02a34b18e"
|
||||
},
|
||||
{
|
||||
"path": "docs/native-authentication.md",
|
||||
"sha256": "378a5fa8174580bde811b716ec8e987339013b9f8d7725cb398fa3b9f8d95655"
|
||||
},
|
||||
{
|
||||
"path": "workplans/KEY-WP-0039-openbao-negative-probe-tokens.md",
|
||||
"sha256": "57d5f1a9cd5d15737d47a9273bf01355ba818b2748e1a43fd7ff2138bed50f4f"
|
||||
}
|
||||
],
|
||||
"current_net_human_fragment_source_pin": "5f727f3146016184da867ce3c304ac0b19f7ceee63c479f6ea2606f7183640f5"
|
||||
}
|
||||
32
docs/reviews/2026-10-11-key39-v9-v4-keycape-work-record.json
Normal file
32
docs/reviews/2026-10-11-key39-v9-v4-keycape-work-record.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"record_kind": "completed-owner-source-inspection",
|
||||
"workplan": "KEY-WP-0039",
|
||||
"task": "KEY-WP-0039-T03",
|
||||
"same_completed_task_refinement": true,
|
||||
"status": "done",
|
||||
"established_before_formal_source_acknowledgment": true,
|
||||
"current_owner_work_record_path": "/home/worsch/key-cape/workplans/KEY-WP-0039-openbao-negative-probe-tokens.md",
|
||||
"current_owner_work_record_sha256": "57d5f1a9cd5d15737d47a9273bf01355ba818b2748e1a43fd7ff2138bed50f4f",
|
||||
"exact_net_manifest_sha256": "84542ddcff00932fc7ce22dcb2626be8942a3c006c5098a62fdd6b2478d0cd14",
|
||||
"exact_producer_manifest_sha256": "d6f641dc1e4aae7d36658d4199d89af14d2fd7a4bd4e526856eb340234f2d33e",
|
||||
"scope": "KeyCape issuer/private caller-token interface only, not Net or Platform formal role",
|
||||
"completed_checks": [
|
||||
"Exact source/manifest entries verified",
|
||||
"Fresh qualified Net consumer source review verified exact95\u2026sha and pair fields",
|
||||
"Three actual child emitter + current Key V4 reader tests pass without token file loads on wrong/expired marker",
|
||||
"Current CLI issuer/client/callback/MFA source inspected",
|
||||
"Source-design choices separated from native authority; staged existing T02 notes untouched"
|
||||
],
|
||||
"validation": {
|
||||
"key_maker_checks_retained": 40,
|
||||
"net_independent_exact_pair_checks": 13,
|
||||
"own_fresh_interface_checks": 3,
|
||||
"no_network_credentials_native_or_owner_checkout_writes": true
|
||||
},
|
||||
"prior_v2_bound_ack_preserved_sha256": "8293e18dcffe7b8e0748880753974219b99d347a173f75a32cc1aad2b4c30f95",
|
||||
"producer_T02_notes_preserved_not_applied": true,
|
||||
"states_retained": {
|
||||
"T01": "done",
|
||||
"T02": "open"
|
||||
}
|
||||
}
|
||||
266
scripts/openbao_negative_private_handoff.py
Normal file
266
scripts/openbao_negative_private_handoff.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Owner-held verified KeyCape CLI files -> inherited pipe; never normal output.
|
||||
No login, secret retrieval, network, executor launch or authenticated admission.
|
||||
Decoded claims below are shape/lifetime checks, not signature verification.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import contextlib
|
||||
import fcntl
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import stat
|
||||
import threading
|
||||
import time
|
||||
|
||||
MAX_FILE = 32768
|
||||
MAX_TOKEN = 16384
|
||||
|
||||
class Refused(Exception):
|
||||
pass
|
||||
|
||||
def need(condition, code):
|
||||
if not condition:
|
||||
raise Refused(code)
|
||||
|
||||
def unique(pairs):
|
||||
out = {}
|
||||
for key, value in pairs:
|
||||
need(key not in out, 'duplicate_json_key')
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
def decode(raw):
|
||||
try:
|
||||
return json.loads(raw, object_pairs_hook=unique,
|
||||
parse_constant=lambda _: (_ for _ in ()).throw(Refused('nonfinite_json')))
|
||||
except Refused:
|
||||
raise
|
||||
except Exception:
|
||||
raise Refused('invalid_json') from None
|
||||
|
||||
@contextlib.contextmanager
|
||||
def deadline(seconds):
|
||||
need(type(seconds) in (int, float) and math.isfinite(seconds) and 0 < seconds <= 5,
|
||||
'invalid_delivery_budget')
|
||||
need(threading.current_thread() is threading.main_thread(), 'main_thread_required')
|
||||
need(signal.getitimer(signal.ITIMER_REAL) == (0.0, 0.0), 'existing_timer_refused')
|
||||
previous = signal.getsignal(signal.SIGALRM)
|
||||
def expired(*_):
|
||||
raise Refused('delivery_timeout')
|
||||
try:
|
||||
signal.signal(signal.SIGALRM, expired)
|
||||
signal.setitimer(signal.ITIMER_REAL, seconds)
|
||||
yield
|
||||
finally:
|
||||
signal.setitimer(signal.ITIMER_REAL, 0)
|
||||
signal.signal(signal.SIGALRM, previous)
|
||||
|
||||
READY_SCHEMA = 'key39-private-input-ready/v1'
|
||||
READY_KEYS = {'schema','session_nonce','proposal_id','target','executor_sha256',
|
||||
'envelope_sha256','bao_health_identity_sha256','valid_until_epoch',
|
||||
'receive_budget_seconds'}
|
||||
|
||||
def finite(value):
|
||||
try:
|
||||
return type(value) in (int,float) and math.isfinite(value)
|
||||
except (OverflowError,ValueError):
|
||||
return False
|
||||
|
||||
def endpoint(fd, writable):
|
||||
need(type(fd) is int and fd >= 3, 'private_pipe_fd_required')
|
||||
try:
|
||||
identity = os.fstat(fd)
|
||||
mode = fcntl.fcntl(fd,fcntl.F_GETFL) & os.O_ACCMODE
|
||||
need(stat.S_ISFIFO(identity.st_mode) and mode ==
|
||||
(os.O_WRONLY if writable else os.O_RDONLY), 'private_pipe_fd_required')
|
||||
for standard in (0,1,2):
|
||||
try:
|
||||
current = os.fstat(standard)
|
||||
except OSError:
|
||||
continue
|
||||
need((identity.st_dev,identity.st_ino) != (current.st_dev,current.st_ino),
|
||||
'stdio_pipe_alias_refused')
|
||||
return identity.st_dev,identity.st_ino
|
||||
except OSError:
|
||||
raise Refused('private_pipe_fd_required') from None
|
||||
|
||||
def wait_ready(fd, expected):
|
||||
need(isinstance(expected,dict) and set(expected) == READY_KEYS - {'schema','receive_budget_seconds'},
|
||||
'expected_child_pins_required')
|
||||
need(finite(expected['valid_until_epoch']) and expected['valid_until_epoch'] > time.time(),
|
||||
'ready_window_expired')
|
||||
raw = b''
|
||||
while len(raw) < 4096:
|
||||
chunk = os.read(fd,1)
|
||||
need(chunk, 'child_ready_missing')
|
||||
if chunk == b'\n':
|
||||
break
|
||||
raw += chunk
|
||||
else:
|
||||
raise Refused('child_ready_oversized')
|
||||
marker = decode(raw)
|
||||
need(isinstance(marker,dict) and set(marker) == READY_KEYS
|
||||
and marker.get('schema') == READY_SCHEMA, 'child_ready_invalid')
|
||||
need(all(marker.get(key) == value for key,value in expected.items()), 'child_ready_mismatch')
|
||||
need(finite(marker['valid_until_epoch']) and finite(marker['receive_budget_seconds'])
|
||||
and 0 < marker['receive_budget_seconds'] <= 15
|
||||
and marker['valid_until_epoch'] > time.time(),
|
||||
'ready_window_expired')
|
||||
return marker
|
||||
|
||||
def claims(token, kind, now):
|
||||
need(isinstance(token, str) and 0 < len(token) <= MAX_TOKEN, 'invalid_token_shape')
|
||||
parts = token.split('.')
|
||||
need(len(parts) == 3 and all(parts), 'invalid_token_shape')
|
||||
try:
|
||||
obj = decode(base64.urlsafe_b64decode(parts[1] + '=' * (-len(parts[1]) % 4)))
|
||||
need(isinstance(obj, dict), 'invalid_token_shape')
|
||||
exp, iat = obj.get('exp'), obj.get('iat')
|
||||
need(type(exp) in (int,float) and type(iat) in (int,float)
|
||||
and math.isfinite(exp) and math.isfinite(iat)
|
||||
and iat <= now + 30 and iat < exp and exp >= now + 300, 'token_window_invalid')
|
||||
need(obj.get('iss') == 'https://kc.coulomb.social', 'token_source_invalid')
|
||||
if kind == 'human':
|
||||
need(obj.get('principal_type') == 'human'
|
||||
and obj.get('aud') == 'netkingdom-bootstrap-console'
|
||||
and isinstance(obj.get('sub'), str) and obj['sub'], 'human_shape_invalid')
|
||||
assurance = obj.get('assurance', {})
|
||||
need(isinstance(assurance,dict) and assurance.get('level') == 'aal2'
|
||||
and assurance.get('mfa') is True, 'human_mfa_shape_invalid')
|
||||
else:
|
||||
need(obj.get('principal_type') == 'service'
|
||||
and obj.get('aud') == 'codex-railiance-platform'
|
||||
and obj.get('sub') == 'service:codex:railiance-platform'
|
||||
and obj.get('tenant') == 'tenant:platform'
|
||||
and obj.get('roles') == ['coding-agent']
|
||||
and obj.get('scope') == 'openbao:login', 'coding_agent_shape_invalid')
|
||||
except Refused:
|
||||
raise
|
||||
except Exception:
|
||||
raise Refused('invalid_token_shape') from None
|
||||
return token
|
||||
|
||||
def load(path, kind, now):
|
||||
p = Path(path)
|
||||
need(p.is_absolute() and p.parent.resolve() == p.parent, 'private_path_required')
|
||||
need(not any((ancestor / '.git').is_file() or (ancestor / '.git' / 'HEAD').exists()
|
||||
for ancestor in p.parents), 'git_path_refused')
|
||||
directory = os.open(p.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
||||
try:
|
||||
ds = os.fstat(directory)
|
||||
need(ds.st_uid == os.getuid() and stat.S_IMODE(ds.st_mode) == 0o700,
|
||||
'private_directory_required')
|
||||
fd = os.open(p.name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory)
|
||||
try:
|
||||
original = os.fstat(fd)
|
||||
need(stat.S_ISREG(original.st_mode) and original.st_uid == os.getuid()
|
||||
and stat.S_IMODE(original.st_mode) == 0o600 and original.st_nlink == 1
|
||||
and 0 < original.st_size <= MAX_FILE, 'private_file_required')
|
||||
raw = b''
|
||||
while len(raw) <= MAX_FILE:
|
||||
chunk = os.read(fd, MAX_FILE + 1 - len(raw))
|
||||
if not chunk:
|
||||
break
|
||||
raw += chunk
|
||||
need(len(raw) <= MAX_FILE, 'private_file_oversized')
|
||||
after = os.fstat(fd)
|
||||
need((original.st_size,original.st_mtime_ns,original.st_ctime_ns) ==
|
||||
(after.st_size,after.st_mtime_ns,after.st_ctime_ns), 'private_file_changed')
|
||||
obj = decode(raw)
|
||||
need(isinstance(obj,dict) and obj.get('token_type','').lower() == 'bearer',
|
||||
'invalid_cli_token_file')
|
||||
token = claims(obj.get('access_token'), kind, now)
|
||||
return token, directory, p.name, original
|
||||
finally:
|
||||
os.close(fd)
|
||||
except BaseException:
|
||||
os.close(directory)
|
||||
raise
|
||||
|
||||
def consume(item):
|
||||
# Comparison is not atomic inode-conditioned unlink. Continuous exclusive
|
||||
# same-UID directory custody is mandatory; concurrent writers are outside scope.
|
||||
_, directory, name, original = item
|
||||
current = os.stat(name, dir_fd=directory, follow_symlinks=False)
|
||||
need((current.st_dev,current.st_ino,current.st_uid,current.st_nlink,current.st_size,
|
||||
current.st_mtime_ns,current.st_ctime_ns) ==
|
||||
(original.st_dev,original.st_ino,original.st_uid,original.st_nlink,original.st_size,
|
||||
original.st_mtime_ns,original.st_ctime_ns), 'private_file_changed')
|
||||
os.unlink(name, dir_fd=directory)
|
||||
|
||||
def deliver(human_path, agent_path, fd, *, ready_fd=None, expected=None,
|
||||
consume_owner_files=False, budget=5):
|
||||
receipt = {'adapter_performs_native_backend_actions': False, 'native_accepted': False,
|
||||
'private_input_delivery_confirmed': False, 'caller_path_removal_completed': False,
|
||||
'exclusive_same_uid_custody_required': True, 'atomic_inode_delete_guaranteed': False,
|
||||
'child_ready_matched': False,
|
||||
'credential_files_remain_possible': True, 'credential_copy_cleanup_verified': False,
|
||||
'private_input_delivery_unknown': False}
|
||||
items = []
|
||||
writing = False
|
||||
try:
|
||||
need(consume_owner_files is True, 'explicit_owner_file_consumption_required')
|
||||
data_identity = endpoint(fd,True)
|
||||
control_identity = endpoint(ready_fd,False)
|
||||
need(data_identity != control_identity, 'data_control_pipe_alias_refused')
|
||||
need(isinstance(expected,dict) and finite(expected.get('valid_until_epoch')),
|
||||
'expected_child_pins_required')
|
||||
budget = min(budget, expected['valid_until_epoch'] - time.time())
|
||||
with deadline(budget):
|
||||
marker = wait_ready(ready_fd,expected)
|
||||
receipt['child_ready_matched'] = True
|
||||
remaining_alarm = signal.getitimer(signal.ITIMER_REAL)[0]
|
||||
remaining = min(remaining_alarm, marker['receive_budget_seconds'],
|
||||
expected['valid_until_epoch'] - time.time())
|
||||
need(remaining > 0, 'ready_window_expired')
|
||||
# Clip the existing timer; never restart or extend its original budget.
|
||||
signal.setitimer(signal.ITIMER_REAL, remaining)
|
||||
delivery_deadline = time.monotonic() + remaining
|
||||
now = time.time()
|
||||
items.append(load(human_path, 'human', now))
|
||||
items.append(load(agent_path, 'coding-agent', now))
|
||||
need((items[0][3].st_dev,items[0][3].st_ino) !=
|
||||
(items[1][3].st_dev,items[1][3].st_ino), 'distinct_files_required')
|
||||
payload = json.dumps({'human_token':items[0][0], 'coding_agent_token':items[1][0]},
|
||||
separators=(',', ':')).encode() + b'\n'
|
||||
# Unlink inspected caller-owned inodes; Python memory is not a zeroization guarantee.
|
||||
need(time.time() < expected['valid_until_epoch'] and
|
||||
time.monotonic() < delivery_deadline, 'ready_window_expired')
|
||||
for item in items:
|
||||
consume(item)
|
||||
receipt.update(caller_path_removal_completed=True)
|
||||
writing = True
|
||||
offset = 0
|
||||
while offset < len(payload):
|
||||
need(time.time() < expected['valid_until_epoch'] and
|
||||
time.monotonic() < delivery_deadline, 'ready_window_expired')
|
||||
written = os.write(fd, payload[offset:])
|
||||
need(written > 0, 'private_delivery_failed')
|
||||
offset += written
|
||||
need(time.time() < expected['valid_until_epoch'] and
|
||||
time.monotonic() < delivery_deadline, 'ready_window_expired')
|
||||
receipt['private_input_delivery_confirmed'] = True
|
||||
except Refused as error:
|
||||
receipt['failure_code'] = str(error)
|
||||
except Exception:
|
||||
receipt['failure_code'] = 'private_delivery_failed'
|
||||
finally:
|
||||
if writing and not receipt['private_input_delivery_confirmed']:
|
||||
receipt['private_input_delivery_unknown'] = True
|
||||
for item in items:
|
||||
os.close(item[1])
|
||||
# Launcher owns these endpoints: EOF even after partial delivery.
|
||||
for owned in {fd,ready_fd}:
|
||||
if type(owned) is int and owned >= 3:
|
||||
try:
|
||||
os.close(owned)
|
||||
except OSError:
|
||||
pass
|
||||
return receipt
|
||||
|
||||
# Deliberately no standalone ready-file/ready-boolean CLI. Only the reviewed
|
||||
# source launcher supplies its own captured child-control endpoint and exact pins.
|
||||
238
scripts/openbao_negative_private_launcher.py
Normal file
238
scripts/openbao_negative_private_launcher.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Founder-held, source-pinned direct SSH launcher; no credentials on normal IO.
|
||||
Ready marker is a procedural readiness check, NEVER authenticated live authority.
|
||||
Use only after exact source/host/transport/client/custody/file allowance admission.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import subprocess
|
||||
import stat
|
||||
import time
|
||||
|
||||
ADAPTER = Path(__file__).with_name('openbao_negative_private_handoff.py')
|
||||
spec = importlib.util.spec_from_file_location('private_handoff', ADAPTER)
|
||||
handoff = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(handoff)
|
||||
PROPOSAL = 'KEY-WP-0039-T02/attended-lifecycle-v9-ready-input'
|
||||
TARGET = 'sso/keycape'
|
||||
PREPARATION_TOTAL_SECONDS = 5
|
||||
MAX_PUBLIC_BYTES = 65536
|
||||
MAX_SOURCE_BYTES = 262144
|
||||
CONTRACT_KEYS = {'schema','proposal_id','target','remote_host','remote_wrapper_path',
|
||||
'remote_wrapper_sha256','remote_child_path','remote_child_sha256','remote_envelope_path',
|
||||
'local_envelope_path','remote_receipt_path','founder_decision_id',
|
||||
'private_tmpfs_file_allowance_receipt','local_launcher_sha256','local_adapter_sha256'}
|
||||
|
||||
def read_stable(path, limit):
|
||||
# Always called under the shared preparation timer in the actual CLI graph.
|
||||
# NONBLOCK + regular-file check refuses FIFO/device. Resolution refuses
|
||||
# observed symlink ancestors; metadata compares detect observed source drift.
|
||||
p = Path(path)
|
||||
handoff.need(p.is_absolute() and p.resolve() == p, 'source_regular_path_required')
|
||||
fd = os.open(p,os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
||||
try:
|
||||
before = os.fstat(fd)
|
||||
handoff.need(stat.S_ISREG(before.st_mode) and before.st_nlink == 1
|
||||
and not stat.S_IMODE(before.st_mode) & 0o022
|
||||
and 0 < before.st_size <= limit, 'source_file_invalid')
|
||||
chunks,size = [],0
|
||||
while size <= limit:
|
||||
chunk = os.read(fd,min(4096,limit+1-size))
|
||||
if not chunk:
|
||||
break
|
||||
size += len(chunk)
|
||||
handoff.need(size <= limit,'source_file_oversized')
|
||||
chunks.append(chunk)
|
||||
after = os.fstat(fd)
|
||||
fields = lambda v:(v.st_dev,v.st_ino,v.st_uid,v.st_mode,v.st_nlink,
|
||||
v.st_size,v.st_mtime_ns,v.st_ctime_ns)
|
||||
handoff.need(fields(before) == fields(after) and size == before.st_size,
|
||||
'source_file_changed')
|
||||
return b''.join(chunks)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(read_stable(path,MAX_SOURCE_BYTES)).hexdigest()
|
||||
|
||||
def remaining_preparation(preparation_deadline):
|
||||
handoff.need(handoff.finite(preparation_deadline),'preparation_window_invalid')
|
||||
remaining = preparation_deadline-time.monotonic()
|
||||
handoff.need(0 < remaining <= PREPARATION_TOTAL_SECONDS,'preparation_window_expired')
|
||||
return remaining
|
||||
|
||||
def pin(value):
|
||||
return isinstance(value,str) and re.fullmatch('[0-9a-f]{64}',value) is not None
|
||||
|
||||
def remote_path(value):
|
||||
handoff.need(isinstance(value,str) and len(value) <= 256
|
||||
and re.fullmatch('/[A-Za-z0-9_./-]+',value) is not None
|
||||
and '..' not in Path(value).parts and not value.endswith('/'), 'remote_path_invalid')
|
||||
return value
|
||||
|
||||
def read_public(path):
|
||||
raw = read_stable(path,MAX_PUBLIC_BYTES)
|
||||
obj = handoff.decode(raw)
|
||||
handoff.need(isinstance(obj,dict),'source_contract_invalid')
|
||||
return raw,obj
|
||||
|
||||
def validate_contract(contract):
|
||||
handoff.need(isinstance(contract,dict) and set(contract) == CONTRACT_KEYS
|
||||
and contract['schema'] == 'key39-private-launch-contract/v1'
|
||||
and contract['proposal_id'] == PROPOSAL and contract['target'] == TARGET
|
||||
and contract['remote_host'] == 'railiance01','source_contract_invalid')
|
||||
for field in ('remote_wrapper_path','remote_child_path','remote_envelope_path','remote_receipt_path'):
|
||||
remote_path(contract[field])
|
||||
handoff.need(len({contract[key] for key in ('remote_wrapper_path','remote_child_path',
|
||||
'remote_envelope_path','remote_receipt_path')}) == 4,'remote_paths_must_differ')
|
||||
for field in ('remote_wrapper_sha256','remote_child_sha256','local_launcher_sha256','local_adapter_sha256'):
|
||||
handoff.need(pin(contract[field]),'source_pin_required')
|
||||
handoff.need(digest(__file__) == contract['local_launcher_sha256']
|
||||
and digest(ADAPTER) == contract['local_adapter_sha256'],'local_source_pin_changed')
|
||||
handoff.need(all(isinstance(contract[k],str) and 0 < len(contract[k]) <= 256
|
||||
for k in ('founder_decision_id','private_tmpfs_file_allowance_receipt')),
|
||||
'founder_receipt_required')
|
||||
raw,envelope = read_public(contract['local_envelope_path'])
|
||||
handoff.need(envelope.get('proposal_id') == PROPOSAL and envelope.get('target') == TARGET
|
||||
and envelope.get('disposition') == 'approved'
|
||||
and envelope.get('founder_decision_id') == contract['founder_decision_id'],
|
||||
'source_envelope_mismatch')
|
||||
expiry = envelope.get('valid_until_epoch')
|
||||
window = envelope.get('maximum_session_seconds')
|
||||
handoff.need(handoff.finite(expiry) and type(window) is int and 420 <= window <= 900
|
||||
and 0 < expiry-time.time() <= window,'source_window_invalid')
|
||||
health = envelope.get('bao_health_identity_sha256')
|
||||
handoff.need(pin(health),'source_health_pin_required')
|
||||
hashes = envelope.get('source_hashes',{})
|
||||
handoff.need(isinstance(hashes,dict) and
|
||||
hashes.get(contract['remote_child_path']) == contract['remote_child_sha256'] and
|
||||
hashes.get(contract['remote_wrapper_path']) == contract['remote_wrapper_sha256'],
|
||||
'remote_source_pin_missing')
|
||||
owners = envelope.get('owner_acknowledgments',{})
|
||||
handoff.need(isinstance(owners,dict) and all(isinstance(owners.get(k),str) and owners[k]
|
||||
for k in ('key-cape','net-kingdom','railiance-platform')),'owner_source_receipts_required')
|
||||
return hashlib.sha256(raw).hexdigest(),envelope
|
||||
|
||||
def launch_and_deliver(contract,human_path,agent_path,*,consume_owner_files=False,
|
||||
preparation_deadline=None):
|
||||
receipt = {'native_accepted':False,'authenticated_authority_established_by_helper':False,
|
||||
'child_ready_matched':False,'private_input_delivery_confirmed':False,
|
||||
'consumer_process_completed':False,'consumer_outcome_unknown':False}
|
||||
process = None
|
||||
owned_fds = set()
|
||||
if preparation_deadline is None:
|
||||
preparation_deadline = time.monotonic() + PREPARATION_TOTAL_SECONDS
|
||||
try:
|
||||
with handoff.deadline(remaining_preparation(preparation_deadline)):
|
||||
envelope_hash,envelope = validate_contract(contract)
|
||||
nonce = secrets.token_hex(32)
|
||||
expected = {'session_nonce':nonce,'proposal_id':PROPOSAL,'target':TARGET,
|
||||
'executor_sha256':contract['remote_child_sha256'],'envelope_sha256':envelope_hash,
|
||||
'bao_health_identity_sha256':envelope['bao_health_identity_sha256'],
|
||||
'valid_until_epoch':envelope['valid_until_epoch']}
|
||||
command = ['python3',contract['remote_wrapper_path'],
|
||||
'--child',contract['remote_child_path'],'--child-sha256',contract['remote_child_sha256'],
|
||||
'--envelope',contract['remote_envelope_path'],'--envelope-sha256',envelope_hash,
|
||||
'--receipt',contract['remote_receipt_path'],'--session-nonce',nonce]
|
||||
argv = ['ssh','-T','-oBatchMode=yes','-oStrictHostKeyChecking=yes',
|
||||
'-oForwardAgent=no','-oClearAllForwardings=yes','-oConnectTimeout=5',
|
||||
'-oServerAliveInterval=5','-oServerAliveCountMax=1',
|
||||
'railiance01',shlex.join(command)]
|
||||
# Do not pass arbitrary confidential producer environment to SSH.
|
||||
allowed = ('PATH','HOME','USER','LOGNAME','LANG','LC_ALL','SSH_AUTH_SOCK')
|
||||
environment = {key:os.environ[key] for key in allowed if key in os.environ}
|
||||
process = subprocess.Popen(argv,stdin=subprocess.PIPE,stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,env=environment,close_fds=True)
|
||||
remaining_preparation(preparation_deadline)
|
||||
# Distinct process-owned pipes; no arbitrary caller ready file or boolean.
|
||||
write_fd = os.dup(process.stdin.fileno()); owned_fds.add(write_fd)
|
||||
ready_fd = os.dup(process.stdout.fileno()); owned_fds.add(ready_fd)
|
||||
process.stdin.close()
|
||||
# Close parent's duplicate read endpoint; only owned ready_fd is used.
|
||||
process.stdout.close()
|
||||
delivery = handoff.deliver(human_path,agent_path,write_fd,ready_fd=ready_fd,
|
||||
expected=expected,consume_owner_files=consume_owner_files,
|
||||
budget=remaining_preparation(preparation_deadline))
|
||||
owned_fds.clear() # deliver owns and closes both endpoints
|
||||
receipt.update(delivery)
|
||||
# Private bytes may already have reached the remote child even if local
|
||||
# write completion/EOF confirmation failed. Local SSH teardown proves no
|
||||
# remote no-effects or cleanup outcome; never clear this unknown here.
|
||||
if delivery['private_input_delivery_unknown']:
|
||||
receipt['consumer_outcome_unknown'] = True
|
||||
receipt['consumer_process_completed'] = False
|
||||
if not delivery['private_input_delivery_confirmed']:
|
||||
return receipt
|
||||
# Value-free exit status only; child receipt and native acceptance remain owner duties.
|
||||
wait_budget = max(0,min(envelope['valid_until_epoch']-time.time(),
|
||||
envelope['maximum_session_seconds']-180)) + 330
|
||||
try:
|
||||
rc = process.wait(timeout=wait_budget)
|
||||
except subprocess.TimeoutExpired:
|
||||
receipt.update(failure_code='consumer_completion_unconfirmed',consumer_outcome_unknown=True)
|
||||
return receipt
|
||||
receipt['consumer_process_completed'] = rc == 0
|
||||
receipt['consumer_outcome_unknown'] = rc != 0
|
||||
if rc != 0:
|
||||
receipt['failure_code'] = 'consumer_completion_unconfirmed'
|
||||
except handoff.Refused as error:
|
||||
receipt['failure_code'] = str(error)
|
||||
except Exception:
|
||||
receipt['failure_code'] = 'private_launcher_incomplete'
|
||||
if receipt['private_input_delivery_confirmed']:
|
||||
receipt['consumer_outcome_unknown'] = True
|
||||
finally:
|
||||
for fd in owned_fds:
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
if process is not None:
|
||||
# Local teardown never claims remote process/custody reconciliation.
|
||||
try:
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait(timeout=1)
|
||||
for stream in (process.stdin,process.stdout):
|
||||
if stream is not None:
|
||||
stream.close()
|
||||
except Exception:
|
||||
receipt['transport_cleanup_unconfirmed'] = True
|
||||
receipt['consumer_outcome_unknown'] = True
|
||||
return receipt
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--contract',required=True)
|
||||
parser.add_argument('--human-file',required=True)
|
||||
parser.add_argument('--agent-file',required=True)
|
||||
parser.add_argument('--consume-owner-files',action='store_true')
|
||||
args = parser.parse_args()
|
||||
preparation_deadline = time.monotonic() + PREPARATION_TOTAL_SECONDS
|
||||
try:
|
||||
with handoff.deadline(remaining_preparation(preparation_deadline)):
|
||||
_,contract = read_public(args.contract)
|
||||
receipt = launch_and_deliver(contract,args.human_file,args.agent_file,
|
||||
consume_owner_files=args.consume_owner_files,preparation_deadline=preparation_deadline)
|
||||
except handoff.Refused as error:
|
||||
receipt = {'native_accepted':False,'private_input_delivery_confirmed':False,
|
||||
'failure_code':str(error)}
|
||||
except Exception:
|
||||
receipt = {'native_accepted':False,'private_input_delivery_confirmed':False,
|
||||
'failure_code':'private_launcher_contract_invalid'}
|
||||
print(json.dumps(receipt,separators=(',',':')))
|
||||
return 0 if receipt.get('consumer_process_completed') is True else 1
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
228
scripts/test_openbao_negative_private_handoff.py
Normal file
228
scripts/test_openbao_negative_private_handoff.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import base64
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
REAL_WRITE=os.write
|
||||
spec = importlib.util.spec_from_file_location('handoff',Path(__file__).with_name('openbao_negative_private_handoff.py'))
|
||||
s = importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
|
||||
|
||||
def jwt(kind='human', **changes):
|
||||
obj={'iss':'https://kc.coulomb.social','iat':time.time()-1,'exp':time.time()+700,
|
||||
'principal_type':'human','aud':'netkingdom-bootstrap-console','sub':'fixture-person',
|
||||
'assurance':{'level':'aal2','mfa':True}}
|
||||
if kind == 'agent':
|
||||
obj.update(principal_type='service',aud='codex-railiance-platform',
|
||||
sub='service:codex:railiance-platform',tenant='tenant:platform',
|
||||
roles=['coding-agent'],scope='openbao:login')
|
||||
obj.update(changes)
|
||||
body=base64.urlsafe_b64encode(json.dumps(obj).encode()).decode().rstrip('=')
|
||||
return 'fixture.'+body+'.fixture'
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp=tempfile.TemporaryDirectory();self.dir=Path(self.tmp.name)
|
||||
self.h=self.dir/'human.json';self.a=self.dir/'agent.json'
|
||||
self.write(self.h,jwt());self.write(self.a,jwt('agent'))
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
def write(self,p,token):
|
||||
p.write_text(json.dumps({'access_token':token,'token_type':'Bearer','expires_in':700}));p.chmod(0o600)
|
||||
def run_delivery(self,**kw):
|
||||
r,w=os.pipe();cr,cw=os.pipe()
|
||||
expected=self.expected()
|
||||
REAL_WRITE(cw,json.dumps(dict(expected,schema=s.READY_SCHEMA,receive_budget_seconds=10)).encode()+b'\n')
|
||||
os.close(cw)
|
||||
receipt=s.deliver(kw.pop("human_path",str(self.h)),str(self.a),w,ready_fd=cr,expected=expected,consume_owner_files=True,**kw)
|
||||
raw=os.read(r,65536);os.close(r)
|
||||
return receipt,raw
|
||||
def expected(self):
|
||||
return {'session_nonce':'1'*64,'proposal_id':'KEY-WP-0039-T02/attended-lifecycle-v9-ready-input',
|
||||
'target':'sso/keycape','executor_sha256':'2'*64,'envelope_sha256':'3'*64,
|
||||
'bao_health_identity_sha256':'4'*64,'valid_until_epoch':time.time()+600}
|
||||
def test_success_only_private_pipe_and_consumes_owned_files(self):
|
||||
receipt,raw=self.run_delivery()
|
||||
obj=json.loads(raw)
|
||||
self.assertEqual(set(obj),{'human_token','coding_agent_token'})
|
||||
self.assertTrue(receipt['private_input_delivery_confirmed'])
|
||||
self.assertFalse(receipt['native_accepted'])
|
||||
self.assertNotIn('fixture',json.dumps(receipt))
|
||||
self.assertFalse(self.h.exists());self.assertFalse(self.a.exists())
|
||||
def test_stdout_stderr_and_regular_fd_refused_before_file_consumption(self):
|
||||
for fd in (1,2):
|
||||
receipt=s.deliver(str(self.h),str(self.a),fd,consume_owner_files=True)
|
||||
self.assertEqual(receipt['failure_code'],'private_pipe_fd_required')
|
||||
with tempfile.TemporaryFile() as f:
|
||||
dup=os.dup(f.fileno())
|
||||
receipt=s.deliver(str(self.h),str(self.a),dup,consume_owner_files=True)
|
||||
self.assertEqual(receipt['failure_code'],'private_pipe_fd_required')
|
||||
self.assertEqual(f.tell(),0)
|
||||
self.assertTrue(self.h.exists());self.assertTrue(self.a.exists())
|
||||
def test_read_only_pipe_refused_before_consuming_files(self):
|
||||
r,w=os.pipe()
|
||||
receipt=s.deliver(str(self.h),str(self.a),r,consume_owner_files=True)
|
||||
os.close(w)
|
||||
self.assertEqual(receipt['failure_code'],'private_pipe_fd_required')
|
||||
self.assertTrue(self.h.exists());self.assertTrue(self.a.exists())
|
||||
def test_git_worktree_refused_before_delivery(self):
|
||||
marker=self.dir/'.git';marker.write_text('gitdir: fixture')
|
||||
receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'git_path_refused');self.assertFalse(raw)
|
||||
def test_symlinked_parent_refused(self):
|
||||
alias=self.dir/'alias';alias.symlink_to(self.dir,target_is_directory=True)
|
||||
receipt,raw=self.run_delivery(human_path=str(alias/'human.json'))
|
||||
self.assertEqual(receipt['failure_code'],'private_path_required')
|
||||
self.assertFalse(raw);self.assertTrue(self.h.exists())
|
||||
|
||||
def test_explicit_consumption_required(self):
|
||||
r,w=os.pipe();receipt=s.deliver(str(self.h),str(self.a),w)
|
||||
self.assertEqual(receipt['failure_code'],'explicit_owner_file_consumption_required')
|
||||
self.assertEqual(os.read(r,10),b'');os.close(r);self.assertTrue(self.h.exists())
|
||||
def test_symlink_refused_no_delivery(self):
|
||||
target=self.dir/'target';self.h.rename(target);self.h.symlink_to(target)
|
||||
receipt,raw=self.run_delivery();self.assertFalse(raw)
|
||||
self.assertFalse(receipt['private_input_delivery_confirmed']);self.assertTrue(target.exists())
|
||||
def test_hardlinked_file_refused(self):
|
||||
os.link(self.h,self.dir/'extra');receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'private_file_required');self.assertFalse(raw)
|
||||
def test_public_mode_refused(self):
|
||||
self.a.chmod(0o644);receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'private_file_required');self.assertFalse(raw)
|
||||
def test_public_directory_refused(self):
|
||||
self.dir.chmod(0o755);receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'private_directory_required');self.assertFalse(raw)
|
||||
def test_oversize_file_refused_before_delivery(self):
|
||||
self.a.write_bytes(b'x'*(s.MAX_FILE+1));receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'private_file_required');self.assertFalse(raw)
|
||||
def test_wrong_human_audience_or_missing_mfa_refused(self):
|
||||
for changes in ({'aud':'openbao-admin'},{'assurance':{'level':'aal1','mfa':False}}):
|
||||
self.write(self.h,jwt(**changes));receipt,raw=self.run_delivery()
|
||||
self.assertFalse(raw);self.assertFalse(receipt['private_input_delivery_confirmed'])
|
||||
def test_approle_string_or_wrong_coding_claim_refused(self):
|
||||
for token in ('hvs.not-a-jwt',jwt('agent',tenant='tenant:coulomb'),jwt('agent',roles=['secrets-engine'])):
|
||||
self.write(self.a,token);receipt,raw=self.run_delivery()
|
||||
self.assertFalse(raw);self.assertFalse(receipt['private_input_delivery_confirmed'])
|
||||
def test_expired_bool_or_infinite_timestamp_refused(self):
|
||||
for expiry in (time.time()-1,True,float('inf')):
|
||||
self.write(self.a,jwt('agent',exp=expiry));receipt,raw=self.run_delivery()
|
||||
self.assertFalse(raw);self.assertFalse(receipt['private_input_delivery_confirmed'])
|
||||
def test_duplicate_json_refused(self):
|
||||
self.a.write_text('{"token_type":"Bearer","access_token":"a","access_token":"b"}')
|
||||
receipt,raw=self.run_delivery();self.assertEqual(receipt['failure_code'],'duplicate_json_key');self.assertFalse(raw)
|
||||
def test_file_replacement_before_consumption_not_unlinked(self):
|
||||
original=s.consume
|
||||
def replace(item):
|
||||
self.h.unlink();self.write(self.h,jwt())
|
||||
return original(item)
|
||||
with patch.object(s,'consume',side_effect=replace):receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'private_file_changed');self.assertFalse(raw);self.assertTrue(self.h.exists())
|
||||
def test_blocked_partial_write_is_bounded_unknown_and_timer_restored(self):
|
||||
previous=signal.getsignal(signal.SIGALRM);started=time.monotonic()
|
||||
with patch.object(s.os,'write',side_effect=lambda *args:time.sleep(0.1)):
|
||||
receipt,raw=self.run_delivery(budget=0.01)
|
||||
self.assertLess(time.monotonic()-started,0.06)
|
||||
self.assertEqual(receipt['failure_code'],'delivery_timeout')
|
||||
self.assertTrue(receipt['private_input_delivery_unknown']);self.assertFalse(raw)
|
||||
self.assertFalse(self.h.exists());self.assertFalse(self.a.exists())
|
||||
self.assertEqual(signal.getitimer(signal.ITIMER_REAL),(0.0,0.0));self.assertIs(signal.getsignal(signal.SIGALRM),previous)
|
||||
def test_broken_pipe_fixed_error_no_raw_exception(self):
|
||||
with patch.object(s.os,'write',side_effect=RuntimeError('protected raw value')):
|
||||
receipt,raw=self.run_delivery()
|
||||
self.assertEqual(receipt['failure_code'],'private_delivery_failed')
|
||||
self.assertTrue(receipt['private_input_delivery_unknown']);self.assertNotIn('protected',json.dumps(receipt));self.assertFalse(raw)
|
||||
|
||||
if __name__ == '__main__':unittest.main()
|
||||
|
||||
class ReadyTests(unittest.TestCase):
|
||||
setUp=Tests.setUp
|
||||
tearDown=Tests.tearDown
|
||||
write=Tests.write
|
||||
expected=Tests.expected
|
||||
run_delivery=Tests.run_delivery
|
||||
# Concrete ready-channel counterexamples; base safety cases run only once.
|
||||
def with_marker(self, marker, budget=0.05):
|
||||
r,w=os.pipe();cr,cw=os.pipe();expected=self.expected()
|
||||
raw=marker(expected) if callable(marker) else marker
|
||||
REAL_WRITE(cw,raw);os.close(cw)
|
||||
with patch.object(s,'load',wraps=s.load) as load:
|
||||
receipt=s.deliver(str(self.h),str(self.a),w,ready_fd=cr,expected=expected,
|
||||
consume_owner_files=True,budget=budget)
|
||||
calls=load.call_count
|
||||
data=os.read(r,65536);os.close(r)
|
||||
return receipt,data,calls
|
||||
def test_no_child_ready_or_mismatch_means_zero_file_load_and_private_bytes(self):
|
||||
for marker in (b'', b'not-json\n', lambda e:json.dumps(dict(e,schema=s.READY_SCHEMA,
|
||||
receive_budget_seconds=10,session_nonce='0'*64)).encode()+b'\n',
|
||||
lambda e:json.dumps(dict(e,schema=s.READY_SCHEMA,receive_budget_seconds=True)).encode()+b'\n'):
|
||||
receipt,data,calls=self.with_marker(marker)
|
||||
self.assertFalse(receipt['private_input_delivery_confirmed']);self.assertFalse(data)
|
||||
self.assertEqual(calls,0);self.assertTrue(self.h.exists());self.assertTrue(self.a.exists())
|
||||
def test_no_ready_eof_hard_timeout_zero_file_load_or_consumption(self):
|
||||
r,w=os.pipe();cr,cw=os.pipe();started=time.monotonic()
|
||||
try:
|
||||
with patch.object(s,'load') as load:
|
||||
receipt=s.deliver(str(self.h),str(self.a),w,ready_fd=cr,expected=self.expected(),
|
||||
consume_owner_files=True,budget=0.01)
|
||||
load.assert_not_called()
|
||||
self.assertLess(time.monotonic()-started,0.06)
|
||||
self.assertEqual(receipt['failure_code'],'delivery_timeout');self.assertFalse(os.read(r,10))
|
||||
self.assertTrue(self.h.exists());self.assertTrue(self.a.exists())
|
||||
finally:os.close(cw);os.close(r)
|
||||
def test_stdout_and_stderr_pipe_duplicates_refused(self):
|
||||
for standard in (1,2):
|
||||
r,w=os.pipe();backup=os.dup(standard)
|
||||
try:
|
||||
os.dup2(w,standard);duplicate=os.dup(standard)
|
||||
receipt=s.deliver(str(self.h),str(self.a),duplicate,consume_owner_files=True)
|
||||
finally:os.dup2(backup,standard);os.close(backup);os.close(w)
|
||||
self.assertEqual(receipt['failure_code'],'stdio_pipe_alias_refused')
|
||||
self.assertEqual(os.read(r,10),b'');os.close(r)
|
||||
self.assertTrue(self.h.exists());self.assertTrue(self.a.exists())
|
||||
def test_data_and_control_same_pipe_refused(self):
|
||||
r,w=os.pipe();readback=os.dup(r)
|
||||
receipt=s.deliver(str(self.h),str(self.a),w,ready_fd=r,expected=self.expected(),consume_owner_files=True)
|
||||
self.assertEqual(receipt['failure_code'],'data_control_pipe_alias_refused')
|
||||
self.assertEqual(os.read(readback,10),b'');os.close(readback)
|
||||
def test_oversize_ready_refused_before_load(self):
|
||||
receipt,data,calls=self.with_marker(b'x'*4096+b'\n')
|
||||
self.assertEqual(receipt['failure_code'],'child_ready_oversized');self.assertFalse(data);self.assertEqual(calls,0)
|
||||
def test_received_short_budget_clips_timer_before_token_load(self):
|
||||
marker=lambda e:json.dumps(dict(e,schema=s.READY_SCHEMA,receive_budget_seconds=0.01)).encode()+b'\n'
|
||||
original=s.load
|
||||
def delayed(*args):time.sleep(0.1);return original(*args)
|
||||
started=time.monotonic()
|
||||
with patch.object(s,'load',side_effect=delayed):receipt,data,calls=self.with_marker(marker)
|
||||
self.assertLess(time.monotonic()-started,0.06);self.assertFalse(data)
|
||||
self.assertEqual(receipt['failure_code'],'delivery_timeout');self.assertTrue(self.h.exists())
|
||||
def test_real_partial_write_stays_unknown_and_conservative_cleanup(self):
|
||||
import fcntl
|
||||
self.write(self.h,jwt(padding='h'*7000));self.write(self.a,jwt('agent',padding='a'*7000))
|
||||
r,w=os.pipe();fcntl.fcntl(w,fcntl.F_SETPIPE_SZ,4096)
|
||||
cr,cw=os.pipe();e=self.expected();REAL_WRITE(cw,json.dumps(dict(e,schema=s.READY_SCHEMA,
|
||||
receive_budget_seconds=10)).encode()+b'\n');os.close(cw)
|
||||
receipt=s.deliver(str(self.h),str(self.a),w,ready_fd=cr,expected=e,consume_owner_files=True,budget=0.015)
|
||||
data=os.read(r,65536);os.close(r)
|
||||
self.assertEqual(len(data),4096);self.assertTrue(receipt['private_input_delivery_unknown'])
|
||||
self.assertFalse(receipt['private_input_delivery_confirmed'])
|
||||
self.assertTrue(receipt['credential_files_remain_possible'])
|
||||
self.assertFalse(receipt['credential_copy_cleanup_verified'])
|
||||
def test_stat_unlink_race_retained_as_exclusive_custody_violation_not_atomic_claim(self):
|
||||
original=s.os.stat;replaced=[]
|
||||
def race(path,*args,**kwargs):
|
||||
current=original(path,*args,**kwargs)
|
||||
if path=='human.json' and kwargs.get('dir_fd') is not None and not replaced:
|
||||
self.h.unlink();self.write(self.h,jwt(sub='replacement-fixture'))
|
||||
replaced.append(True)
|
||||
return current
|
||||
with patch.object(s.os,'stat',side_effect=race):receipt,data=self.run_delivery()
|
||||
self.assertTrue(replaced);self.assertFalse(self.h.exists())
|
||||
self.assertTrue(receipt['caller_path_removal_completed'])
|
||||
self.assertFalse(receipt['atomic_inode_delete_guaranteed'])
|
||||
self.assertTrue(receipt['exclusive_same_uid_custody_required'])
|
||||
self.assertTrue(receipt['credential_files_remain_possible'])
|
||||
self.assertFalse(receipt['credential_copy_cleanup_verified'])
|
||||
145
scripts/test_openbao_negative_private_launcher.py
Normal file
145
scripts/test_openbao_negative_private_launcher.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
spec=importlib.util.spec_from_file_location('launcher',Path(__file__).with_name('openbao_negative_private_launcher.py'))
|
||||
l=importlib.util.module_from_spec(spec);spec.loader.exec_module(l)
|
||||
spec=importlib.util.spec_from_file_location('fixtures',Path(__file__).with_name('test_openbao_negative_private_handoff.py'))
|
||||
f=importlib.util.module_from_spec(spec);spec.loader.exec_module(f)
|
||||
|
||||
class FakeProcess:
|
||||
def __init__(self,argv,contract,envelope_hash,envelope,marker='valid'):
|
||||
self.argv=argv;self.rc=None;self.terminated=False;self.waits=[]
|
||||
self.read_data,w=os.pipe();self.stdin=os.fdopen(w,'wb',buffering=0)
|
||||
r,cw=os.pipe();self.stdout=os.fdopen(r,'rb',buffering=0)
|
||||
args=shlex.split(argv[-1]);nonce=args[args.index('--session-nonce')+1]
|
||||
ready={'schema':'key39-private-input-ready/v1','session_nonce':nonce,
|
||||
'proposal_id':l.PROPOSAL,'target':l.TARGET,'executor_sha256':contract['remote_child_sha256'],
|
||||
'envelope_sha256':envelope_hash,'bao_health_identity_sha256':envelope['bao_health_identity_sha256'],
|
||||
'valid_until_epoch':envelope['valid_until_epoch'],'receive_budget_seconds':15}
|
||||
if marker=='wrong':ready['session_nonce']='0'*64
|
||||
if marker!='missing':os.write(cw,json.dumps(ready).encode()+b'\n')
|
||||
os.close(cw)
|
||||
def wait(self,timeout):
|
||||
self.waits.append(timeout);self.rc=-15 if self.terminated else 0;return self.rc
|
||||
def poll(self):return self.rc
|
||||
def terminate(self):self.terminated=True
|
||||
def kill(self):self.terminated=True
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp=tempfile.TemporaryDirectory();self.dir=Path(self.tmp.name)
|
||||
self.h=self.dir/'human.json';self.a=self.dir/'agent.json'
|
||||
for p,token in ((self.h,f.jwt()),(self.a,f.jwt('agent'))):
|
||||
p.write_text(json.dumps({'access_token':token,'token_type':'Bearer'}));p.chmod(0o600)
|
||||
self.envelope={'proposal_id':l.PROPOSAL,'target':l.TARGET,'disposition':'approved',
|
||||
'founder_decision_id':'fixture-founder','valid_until_epoch':time.time()+600,
|
||||
'maximum_session_seconds':900,'bao_health_identity_sha256':'4'*64,
|
||||
'source_hashes':{'/run/key39/child.py':'2'*64,'/run/key39/wrapper.py':'3'*64},
|
||||
'owner_acknowledgments':{k:'fixture-source' for k in ('key-cape','net-kingdom','railiance-platform')}}
|
||||
self.e=self.dir/'envelope.json';self.e.write_text(json.dumps(self.envelope))
|
||||
self.contract={'schema':'key39-private-launch-contract/v1','proposal_id':l.PROPOSAL,
|
||||
'target':l.TARGET,'remote_host':'railiance01','remote_wrapper_path':'/run/key39/wrapper.py',
|
||||
'remote_wrapper_sha256':'3'*64,'remote_child_path':'/run/key39/child.py',
|
||||
'remote_child_sha256':'2'*64,'remote_envelope_path':'/run/key39/approved.json',
|
||||
'local_envelope_path':str(self.e),'remote_receipt_path':'/run/key39/receipt.json',
|
||||
'founder_decision_id':'fixture-founder','private_tmpfs_file_allowance_receipt':'fixture-file-allowance',
|
||||
'local_launcher_sha256':l.digest(l.__file__),'local_adapter_sha256':l.digest(l.ADAPTER)}
|
||||
self.processes=[]
|
||||
def tearDown(self):
|
||||
for p in self.processes:
|
||||
os.close(p.read_data)
|
||||
self.tmp.cleanup()
|
||||
def run_launcher(self,marker='valid'):
|
||||
def popen(argv,**kwargs):
|
||||
_,envelope=l.read_public(self.e)
|
||||
self.assertEqual(kwargs['stdin'],l.subprocess.PIPE)
|
||||
self.assertEqual(kwargs['stdout'],l.subprocess.PIPE)
|
||||
self.assertEqual(kwargs['stderr'],l.subprocess.DEVNULL)
|
||||
self.assertNotIn('KEYCAPE_CODEX_RAILIANCE_PLATFORM_CLIENT_SECRET',kwargs['env'])
|
||||
self.assertIn('-oStrictHostKeyChecking=yes',argv);self.assertIn('-T',argv)
|
||||
p=FakeProcess(argv,self.contract,l.digest(self.e),envelope,marker);self.processes.append(p);return p
|
||||
with patch.object(l.subprocess,'Popen',side_effect=popen) as spawn, \
|
||||
patch.dict(os.environ,{'KEYCAPE_CODEX_RAILIANCE_PLATFORM_CLIENT_SECRET':'PRIVATE-FIXTURE'}):
|
||||
receipt=l.launch_and_deliver(self.contract,str(self.h),str(self.a),consume_owner_files=True)
|
||||
return receipt,spawn.call_count
|
||||
def test_complete_pinned_child_marker_then_private_pipe_delivery_no_actual_ssh(self):
|
||||
receipt,count=self.run_launcher();self.assertEqual(count,1)
|
||||
self.assertTrue(receipt['child_ready_matched']);self.assertTrue(receipt['consumer_process_completed'])
|
||||
self.assertFalse(receipt['native_accepted']);self.assertFalse(receipt['authenticated_authority_established_by_helper'])
|
||||
raw=os.read(self.processes[0].read_data,65536);self.assertEqual(set(json.loads(raw)),{'human_token','coding_agent_token'})
|
||||
self.assertNotIn('fixture-person',json.dumps(receipt));self.assertFalse(self.h.exists());self.assertFalse(self.a.exists())
|
||||
def test_missing_or_wrong_marker_retains_files_and_writes_zero_tokens(self):
|
||||
for marker in ('missing','wrong'):
|
||||
receipt,count=self.run_launcher(marker)
|
||||
self.assertEqual(count,1);self.assertFalse(receipt['child_ready_matched'])
|
||||
self.assertEqual(os.read(self.processes[-1].read_data,65536),b'')
|
||||
self.assertTrue(self.h.exists());self.assertTrue(self.a.exists())
|
||||
def test_changed_local_source_pin_refuses_before_transport_and_file_read(self):
|
||||
self.contract['local_adapter_sha256']='0'*64
|
||||
receipt,count=self.run_launcher();self.assertEqual(count,0)
|
||||
self.assertEqual(receipt['failure_code'],'local_source_pin_changed');self.assertTrue(self.h.exists())
|
||||
def test_wrong_host_or_remote_command_path_refuses_before_transport(self):
|
||||
for field,value in (('remote_host','other-node'),('remote_wrapper_path','/tmp/x;cat'),
|
||||
('remote_child_path','/run/key39/../child.py')):
|
||||
old=self.contract[field];self.contract[field]=value
|
||||
receipt,count=self.run_launcher();self.contract[field]=old
|
||||
self.assertEqual(count,0);self.assertFalse(receipt['private_input_delivery_confirmed']);self.assertTrue(self.h.exists())
|
||||
def test_expired_or_wrong_founder_source_contract_no_spawn_no_file_access(self):
|
||||
for key,value in (('valid_until_epoch',time.time()-1),('founder_decision_id','other')):
|
||||
current=json.loads(self.e.read_text());current[key]=value;self.e.write_text(json.dumps(current))
|
||||
receipt,count=self.run_launcher();self.assertEqual(count,0);self.assertTrue(self.h.exists())
|
||||
self.e.write_text(json.dumps(self.envelope))
|
||||
|
||||
def test_full_pipe_payload_then_confirmation_timeout_keeps_remote_unknown(self):
|
||||
original_deliver=l.handoff.deliver
|
||||
original_write=os.write
|
||||
def interrupted_delivery(*args,**kwargs):
|
||||
kwargs['budget']=0.01
|
||||
def write_then_stall(fd,payload):
|
||||
written=original_write(fd,payload)
|
||||
time.sleep(0.1)
|
||||
return written
|
||||
with patch.object(l.handoff.os,'write',side_effect=write_then_stall):
|
||||
return original_deliver(*args,**kwargs)
|
||||
with patch.object(l.handoff,'deliver',side_effect=interrupted_delivery):
|
||||
receipt,count=self.run_launcher()
|
||||
self.assertEqual(count,1)
|
||||
data=os.read(self.processes[0].read_data,65536)
|
||||
self.assertEqual(set(json.loads(data)),{'human_token','coding_agent_token'})
|
||||
self.assertTrue(receipt['private_input_delivery_unknown'])
|
||||
self.assertFalse(receipt['private_input_delivery_confirmed'])
|
||||
self.assertTrue(receipt['consumer_outcome_unknown'])
|
||||
self.assertFalse(receipt['consumer_process_completed'])
|
||||
self.assertTrue(self.processes[0].terminated)
|
||||
self.assertFalse(self.h.exists());self.assertFalse(self.a.exists())
|
||||
self.assertFalse(receipt['native_accepted'])
|
||||
def test_real_partial_pipe_write_unknown_is_sticky_after_local_terminate(self):
|
||||
import fcntl
|
||||
for p,token in ((self.h,f.jwt(padding='h'*7000)),(self.a,f.jwt('agent',padding='a'*7000))):
|
||||
p.write_text(json.dumps({'access_token':token,'token_type':'Bearer'}));p.chmod(0o600)
|
||||
original_process=FakeProcess
|
||||
original_deliver=l.handoff.deliver
|
||||
def small_pipe(*args,**kwargs):
|
||||
proc=original_process(*args,**kwargs)
|
||||
fcntl.fcntl(proc.stdin.fileno(),fcntl.F_SETPIPE_SZ,4096)
|
||||
return proc
|
||||
def bounded(*args,**kwargs):
|
||||
kwargs['budget']=0.015
|
||||
return original_deliver(*args,**kwargs)
|
||||
with patch.dict(globals(),{'FakeProcess':small_pipe}),patch.object(l.handoff,'deliver',side_effect=bounded):
|
||||
receipt,count=self.run_launcher()
|
||||
data=os.read(self.processes[0].read_data,65536)
|
||||
self.assertEqual(len(data),4096)
|
||||
self.assertTrue(receipt['private_input_delivery_unknown'])
|
||||
self.assertTrue(receipt['consumer_outcome_unknown'])
|
||||
self.assertFalse(receipt['consumer_process_completed'])
|
||||
self.assertTrue(self.processes[0].terminated)
|
||||
self.assertFalse(receipt['native_accepted'])
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
91
scripts/test_openbao_negative_private_preparation.py
Normal file
91
scripts/test_openbao_negative_private_preparation.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
spec=importlib.util.spec_from_file_location('prep_launcher',Path(__file__).with_name('openbao_negative_private_launcher.py'))
|
||||
l=importlib.util.module_from_spec(spec);spec.loader.exec_module(l)
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def actual_cli(self,path):
|
||||
started=time.monotonic()
|
||||
result=subprocess.run([sys.executable,l.__file__,'--contract',str(path),
|
||||
'--human-file','/NEVER-READ-HUMAN','--agent-file','/NEVER-READ-AGENT'],
|
||||
capture_output=True,timeout=0.4)
|
||||
self.assertEqual(result.returncode,1);self.assertEqual(result.stderr,b'')
|
||||
self.assertLess(time.monotonic()-started,0.25)
|
||||
value=json.loads(result.stdout);self.assertFalse(value['native_accepted'])
|
||||
self.assertFalse(value['private_input_delivery_confirmed'])
|
||||
return value
|
||||
def test_actual_main_fifo_refused_without_waiting_for_writer(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'contract.fifo';os.mkfifo(path,0o600)
|
||||
self.assertEqual(self.actual_cli(path)['failure_code'],'source_file_invalid')
|
||||
def test_actual_main_symlink_contract_refused(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
original=Path(folder)/'contract.json';original.write_text('{}')
|
||||
path=Path(folder)/'alias';path.symlink_to(original)
|
||||
self.assertEqual(self.actual_cli(path)['failure_code'],'source_regular_path_required')
|
||||
def test_actual_main_oversize_contract_refused(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'contract.json';path.write_bytes(b'x'*(l.MAX_PUBLIC_BYTES+1))
|
||||
self.assertEqual(self.actual_cli(path)['failure_code'],'source_file_invalid')
|
||||
def main_with(self,path,**patches):
|
||||
out=io.StringIO()
|
||||
argv=['launcher','--contract',str(path),'--human-file','/NEVER-READ-HUMAN',
|
||||
'--agent-file','/NEVER-READ-AGENT']
|
||||
with patch.object(sys,'argv',argv),patch.object(sys,'stdout',out), \
|
||||
patch.object(l.subprocess,'Popen') as spawn,patch.object(l.handoff,'load') as load:
|
||||
rc=l.main();spawn.assert_not_called();load.assert_not_called()
|
||||
self.assertEqual(rc,1)
|
||||
return json.loads(out.getvalue())
|
||||
def test_actual_main_slow_regular_read_has_hard_total_bound_and_signal_restore(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'contract.json';path.write_text('{}')
|
||||
original=l.os.read
|
||||
def slow(fd,size):time.sleep(0.1);return original(fd,size)
|
||||
previous=signal.getsignal(signal.SIGALRM);started=time.monotonic()
|
||||
with patch.object(l,'PREPARATION_TOTAL_SECONDS',0.01),patch.object(l.os,'read',side_effect=slow):
|
||||
receipt=self.main_with(path)
|
||||
self.assertEqual(receipt['failure_code'],'delivery_timeout')
|
||||
self.assertLess(time.monotonic()-started,0.06)
|
||||
self.assertEqual(signal.getitimer(signal.ITIMER_REAL),(0.0,0.0));self.assertIs(signal.getsignal(signal.SIGALRM),previous)
|
||||
def test_actual_main_launch_does_not_renew_budget_spent_acquiring_contract(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'contract.json';path.write_text('{}')
|
||||
original=l.read_public
|
||||
def acquire(path):time.sleep(0.02);return original(path)
|
||||
def slow_validate(contract):time.sleep(0.02);return '0'*64,{}
|
||||
started=time.monotonic()
|
||||
with patch.object(l,'PREPARATION_TOTAL_SECONDS',0.03), \
|
||||
patch.object(l,'read_public',side_effect=acquire), \
|
||||
patch.object(l,'validate_contract',side_effect=slow_validate):
|
||||
receipt=self.main_with(path)
|
||||
self.assertEqual(receipt['failure_code'],'delivery_timeout');self.assertLess(time.monotonic()-started,0.065)
|
||||
def test_observed_source_change_refused_and_reads_are_bounded(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'contract.json';path.write_text('{}')
|
||||
original=l.os.read;changed=[];sizes=[]
|
||||
def mutate(fd,size):
|
||||
sizes.append(size)
|
||||
if not changed:path.write_text('{"x":1}');changed.append(True)
|
||||
return original(fd,size)
|
||||
with patch.object(l.os,'read',side_effect=mutate):
|
||||
with self.assertRaisesRegex(l.handoff.Refused,'source_file_changed'):l.read_public(path)
|
||||
self.assertTrue(all(0<size<=4096 for size in sizes))
|
||||
def test_source_digest_fifo_and_symlink_refuse(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path=Path(folder)/'source.fifo';os.mkfifo(path,0o600)
|
||||
with self.assertRaisesRegex(l.handoff.Refused,'source_file_invalid'):l.digest(path)
|
||||
regular=Path(folder)/'source.py';regular.write_text('fixture')
|
||||
alias=Path(folder)/'alias';alias.symlink_to(regular)
|
||||
with self.assertRaisesRegex(l.handoff.Refused,'source_regular_path_required'):l.digest(alias)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
|
@ -145,6 +145,7 @@ operator/ESO capability and nonmember checks remain independent acceptance.
|
|||
id: KEY-WP-0039-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "54d37ad8-2352-51c2-bb2e-af6070dbbd73"
|
||||
```
|
||||
|
||||
2026-10-11: Completed KeyCape's own issuer-source inspection for
|
||||
|
|
@ -231,3 +232,66 @@ SHA256 ba872dc53061b1dcca9dea1e8897597c394c1f9365e0fc49ee6f2095f57f11e0
|
|||
foreign owners' own source decisions, not KeyCape runtime/backend reviews or
|
||||
founder native authority. Formal KeyCape bounded issuer acknowledgment follows
|
||||
in docs/reviews/2026-10-11-key39-lifecycle-v5-keycape-source-ack.json.
|
||||
|
||||
|
||||
2026-10-11 T02 continuing source preparation: private producer decision packet
|
||||
`KEY-WP-0039-T02/private-producer-handoff-v1` stages supported existing CLI
|
||||
flows, selected existing netkingdom-bootstrap-console literal-loopback human
|
||||
client subject to owner purpose/MFA/listener admission, and the existing
|
||||
codex-railiance-platform source identity subject to actual live/custody/consumer
|
||||
admission. No existing coding-agent delivery receipt was found; AppRole tokens
|
||||
do not substitute for issuer JWTs. A bounded pipe-only adapter for the current
|
||||
CLI's two caller-owned private files has 18 passing synthetic checks, with
|
||||
identity-guarded explicit file consumption and fixed value-free failure output.
|
||||
Decoded claim shape does not prove signatures or authenticated admission.
|
||||
See docs/openbao-negative-token-producer-decision.md and .json for exact owner
|
||||
choices and missing receipts. The exact trusted workstation-to-node launcher,
|
||||
private-file allowance and railiance01 Bao route/public-health guard still need
|
||||
source review and founder disposition before any token/temporary credential/
|
||||
mutation/restart. No registration, custody, login, native action or credential
|
||||
request occurred. V5 and failed review histories retained; T01 stays done and
|
||||
T02 open. No new work record or native acceptance is inferred.
|
||||
|
||||
|
||||
2026-10-11 same T02 source-preparation correction for
|
||||
KEY-WP-0039-T02/private-producer-handoff-v2: NetKingdom's exact V1 consumer
|
||||
refusal remains recorded (no child-ready gate, stdio alias, non-atomic unlink).
|
||||
V2 has source-pinned private launch/ready handshake compatible with exact
|
||||
Net V9 manifest84542ddc: fresh nonce/source/envelope/health/window marker before
|
||||
ANY caller-token file load/consume/write; separate local data/control pipes
|
||||
reject kernel identity aliases of stdin/stdout/stderr. Adapter read/write budget
|
||||
only clips downward after readiness. 31 distinct producer/launcher synthetic
|
||||
checks and 74 compatible consumer supplied checks pass. No SSH/native/login or
|
||||
credential access was performed. Stat/unlink remains non-atomic and requires
|
||||
continuously exclusive sameUID custody; the retained race test is a custody
|
||||
violation counterexample, not a claim of repaired filesystem atomicity. Receipt
|
||||
keeps atomic delete/copy cleanup unverified and possible copies true. Exact
|
||||
trusted launcher/directSSH profile/private tmpfs allowance, selected human
|
||||
client/MFA purpose reuse and coding-agent registration/custody/delivery remain
|
||||
unadmitted owner choices. Native node route, role/leeway/window/founder decision
|
||||
and actual negative/cleanup acceptance remain pending. T01done and T02open; no
|
||||
new task or plan, and V1/V5-V8/current failure histories are not overwritten.
|
||||
|
||||
|
||||
2026-10-11 same T02 producer-preparation correction V3: Platform V2 pair
|
||||
source refusal RPF-KEY39-PAIR-V2-F01 is retained. Private write uncertainty
|
||||
now propagates into sticky remote consumer uncertainty before launcher early
|
||||
return; local SSH termination never proves remote no-effects/recovery. Actual
|
||||
full-payload then interrupted confirmation and4096-byte partialpipe failures
|
||||
are retained regressions. 33 distinct source checks pass. Exact Net V9 source
|
||||
bytes unchanged; new source reviews required, not implicit repin. Native
|
||||
client/MFA/coding-agent custody/transport/tmpfsfile and actualroute/session
|
||||
authority remain unadmitted. T01done/T02open; no new task/plan or native action.
|
||||
|
||||
|
||||
2026-10-11 same T02 producer-preparation V4: exact V3 Net consumer refusal
|
||||
NET-PRODUCER-V3-F01 retained. Actual CLI contract acquisition now bounded
|
||||
nofollow/nonblocking regular stable reads and one ORIGINAL5-second preparation
|
||||
budget across contract/source/envelope/SSHspawn/ready/file/delivery phases, not
|
||||
a renewed launcher allowance. 40 own source tests plus9 retained paired
|
||||
wrapper/child handshake regressions pass with synthetic/no native transport.
|
||||
V3stickyremoteunknown/full+partialdelivery regressions retained; Net V9 exact
|
||||
bytes unchanged. Prior issuer/custody/transport/file/route/readiness-Argo/role/
|
||||
maintenanceexception/founder choices remain unadmitted. No SSH/network/Secrets/
|
||||
credentials/native/owner checkout changes; no task/plan duplication. T01done
|
||||
and T02open; source correction is not native acceptance.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue