docs(change): propose mediated single-file effect contract (CR-OC-001A) - #119
Merged
Merged
Conversation
Requirements proposal only. Implementation authority: none. No code, tests, schema, CLI, runtime integration, IPC, file access, OpenClaw installation, or capability behavior is changed. CR-OC-001A defines how one exact single-file content transition is represented, validated, and bound, as the first of five slices in the mediated OpenClaw experiment. The proposed module is pure: it reads no file, writes no file, opens no connection, and touches no database. Contract highlights: - target_file_id is the tool-facing identifier; a caller-supplied path is never accepted where a file ID belongs, and canonical_relpath is carried for evidence legibility only, never as an input channel. - proposed_bytes is transient. It is verified against expected_post_digest and then never digested directly, never projected, and never persisted. Every digest that carries the post-digest binds the content transitively without handling it. - declared_context_digest and broker_connection_id remain structurally separate in the effect, the projection, and the API. - The authorized effect maps onto the merged CR-YK-002 capability without changing AuthorizationRequest or the capability schema. - Replay semantics are keyed to the client request, not merely the capability: at most one claim per client request. - The closed validation vocabulary covers only conditions this module can actually detect. Path traversal, symlink handling, broker availability, capability lifecycle, and execution outcomes are deliberately absent, because a code for an undetectable condition is a false capability claim in vocabulary form. Three honesty boundaries are recorded rather than left for a reviewer to notice: - A pure module cannot distinguish a broker-minted broker_connection_id from a forged one. This slice establishes only that it is a separate field with separate provenance participating in the effect digest. Trustworthiness is entirely a CR-OC-001D property. - Digesting client-declared invocation context makes it tamper-evident after the fact. It does not make it true when first supplied, and nothing here authenticates the runtime, agent, or session. - The bounded claim is that an exact authorized pre-content digest became an exact authorized post-content digest. That is not exact filesystem state: timestamps and file identity may change during replacement, and owner, DACL, regular-file status, and workspace containment are left to CR-OC-001C with their own tests. One sequencing consequence is recorded now rather than discovered later. scope_digest covers the whole authorized effect including broker_connection_id, and CR-YK-002 binds scope_digest immutably at claim time. A capability is therefore bound to one broker connection: it cannot be claimed across a reconnect, and issuance must follow connection establishment. That is the intended containment property but a real ordering constraint on CR-OC-001B and CR-OC-001D. The mutant table names a designated killer test for each mutant and records that the declared-identity mutant is weaker than the rest, since it is a naming and structure defect rather than a behavioral one and a determined rename could defeat its test. Reviewers, not tests, are the control there. The named-pipe hardening redlines are recorded as CR-OC-001D requirements and are explicitly not satisfied by any earlier slice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for poetic-quokka-0fd859 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Review of PR #119 found six internal contradictions and underspecifications that documentation-only CI cannot detect. Still a requirements proposal; implementation authority remains none. 1. proposed_bytes must be digested directly. The document claimed they are "never digested directly" while also requiring verification against expected_post_digest, which necessarily hashes the exact bytes. That claim was false. Replaced with a retention rule: the bytes are hashed transiently to verify sha256(exact_proposed_bytes) == expected_post_digest, and never enter canonical JSON, any digest input, the projection, or durable evidence. Exact-byte treatment is now binding -- UTF-8 validity is a gate rather than a normalization step, with no Unicode normalization, no newline conversion, no BOM insertion or removal, and content_size_bytes measured on the original sequence. Without this, two conforming implementations could hash different post-content from identical input. 2. The bounded claim overstated provenance. The objective said "broker-authenticated connection binding" and the acceptance claim said "broker-generated connection identifier", both contradicting the honesty section. All positive claims now read "a distinct broker-connection identifier field whose provenance and connection binding are not established by this slice", and broker-generated/broker-authenticated are reserved for CR-OC-001D. The same precision is applied to target_file_id, which this slice validates syntactically without establishing allowlist membership. A provenance table now states, per field, what is and is not established. 3. Request identity omitted the connection binding. request_digest was defined over the proposal, which does not carry broker_connection_id, while the effect and scope digest do -- so the same client_request_id and request_digest could recur unchanged after a reconnect even though the authorized effect had changed. triagecore.mediated_client_request.v1 now includes broker_connection_id, a repeat on a different connection is request_id_reuse_mismatch rather than idempotent replay, and the integrated ordering is explicit: reservation follows connection establishment, not only issuance. 4. The replay vocabulary contradicted statelessness. A module that stores nothing cannot own request_id_reuse_mismatch, because it cannot discover that a prior request exists. Resolved by splitting comparison from storage: classify_request_replay is a pure function receiving both bindings as arguments and returning new_request, idempotent_replay, or request_id_reuse_mismatch, with CR-OC-001B supplying stored values atomically. Also recorded that the "one capability per client request" rule is not representable here at all, since capability_id appears nowhere in this slice, and is therefore a CR-OC-001B obligation to bind capability_id one-to-one with client_request_id. 5. Canonical objects were undefined. "Digest of the canonical proposal/decision linkage" is not implementable. Five versioned objects are now specified field by field with closed field sets, a schema discriminator inside the digest input, required non-null fields (optional values are forbidden rather than omitted, so an omitted-key form cannot exist), and the existing canonical_json_bytes representation. Domain separation follows the house pattern from governed_decision.py -- a field inside the envelope, not a byte prefix. A cross-domain distinctness test is required rather than assumed. 6. Two downstream-boundary details. canonical_relpath was described as something no authorization may key off, but it is in the effect and does affect scope_digest; the rule is now stated correctly as integrity-bound for evidence consistency but never a resolution channel, with resolution exclusively through target_file_id. The CR-OC-001D pipe redlines were incomplete and now carry the full set: DACL limited to Account A, Account B, and explicitly selected operator identities; no Everyone or anonymous ACE; explicit denial of NT AUTHORITY\NETWORK and anonymous access; a broker-owned rendezvous artifact; shim verification of both server process and expected account before content is transmitted; and a negative test for remote-style clients. Test contract expands from 11 to 17 items and the mutant table from six to nine, adding killers for request-digest connection binding, byte-normalization, and schema-discriminator removal. Backlog and change-log entries are corrected to match; both previously repeated the false "never digested" claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clarifies how classify_request_replay represents "no existing binding". The
prior signature took four required strings while its rules also required an
absence case, which is not expressible in that shape. Requirements proposal
only; implementation authority remains none.
The signature becomes:
RequestBinding
client_request_id
request_digest
classify_request_replay(
existing: RequestBinding | None,
incoming: RequestBinding,
) -> new_request | idempotent_replay | request_id_reuse_mismatch
None is the explicit absence sentinel. RequestBinding is a comparison input,
not a sixth canonical digest object: it is never canonicalized, digested, or
persisted, so the "optional values are forbidden, not omitted" rule does not
reach it. That rule governs the five canonical digest objects only, where an
omitted-key form must not exist because it would digest differently. Absence
here is a real state the caller must be able to express. Within a
RequestBinding both fields remain required and non-null.
The one input that cannot arise from a correct caller is now defined rather
than left open. CR-OC-001B looks up by the incoming client_request_id, so a
non-null existing binding carrying a different ID indicates a caller defect.
That input is invalid and must raise.
Raising is chosen over returning because both available return values would be
wrong in a way that matters: new_request would let a lookup bug hand back
mismatched state and obtain fresh authority, and request_id_reuse_mismatch
would report a reuse conflict that never happened. Raising surfaces the defect
where it can be fixed, and matches the existing house treatment of
caller-contract violations -- CapabilityClaimStore.claim raises
CapabilityStoreError for a blank claimant rather than inventing a reason code.
Test contract grows to 18 items, adding the raise-on-mismatched-existing-ID
case and splitting the classification assertions by branch. Tail items are
renumbered and the mutant table cross-reference is updated accordingly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requirements proposal only. Implementation authority: none. No code, tests, schema, CLI, runtime integration, IPC, file access, OpenClaw installation, or capability behavior is changed. Three documentation files, 727 insertions.
CR-OC-001A defines how one exact single-file content transition is represented, validated, and bound — the first of five slices in the mediated OpenClaw experiment. The proposed module is pure: it reads no file, writes no file, opens no connection, and touches no database.
What the contract fixes
target_file_id, never a caller-supplied pathcanonical_relpathtarget_file_idproposed_bytesare hashed transiently to verifysha256(exact_proposed_bytes) == expected_post_digest, then dropped. The rule is retention, not "never digested"content_size_bytesmeasures the original sequencedeclared_context_digestandbroker_connection_idstay structurally separate in the effect, the projection, and the APIAuthorizationRequestor the capability schemaPath traversal, symlink handling, broker availability, capability lifecycle, and execution outcomes are deliberately absent from the validation vocabulary. A reason code for a condition the module cannot observe is a false capability claim in vocabulary form.
Provenance is asserted, never established
broker_connection_iddeclared_invocation_contexttarget_file_idA pure module cannot tell a broker-minted identifier from a forged one, and digesting a value makes it tamper-evident without making it true. broker-generated and broker-authenticated are reserved for CR-OC-001D.
Canonical objects
Closed field sets; every field required and non-null, so an omitted-key form cannot exist. Domain separation follows the house pattern from
triage_core/governed_decision.py— aschemafield inside the canonicalized envelope, not a byte prefix. Cross-domain distinctness is a required test, not an assumption.Connection binding runs through the whole sequence
Both
scope_digestandrequest_digestcoverbroker_connection_id. Without it in request identity, the sameclient_request_idandrequest_digestcould recur unchanged after a reconnect while the authorized effect had changed, and a reservation store would report idempotent replay for authority issued against a dead connection.Reservation, not merely issuance, sits downstream of connection establishment. A repeated
client_request_idon a different connection isrequest_id_reuse_mismatch, not an idempotent replay.Replay classification is pure
A stateless module cannot both own
request_id_reuse_mismatchand store nothing, so comparison and storage are split: the comparison lives here and is testable, the storage lives in CR-OC-001B.Noneis the explicit absence sentinel; the "optional values forbidden" rule governs the five canonical digest objects only. A non-nullexistingwhose ID differs fromincomingcannot arise from a correct caller and raises rather than returning a classification, matching howCapabilityClaimStore.claimtreats a blank claimant.Recorded as a CR-OC-001B obligation because it is not representable here: the reservation row must bind
capability_idone-to-one withclient_request_id.capability_idappears nowhere in this slice.Bounded acceptance claim
It may not be used to support: that OpenClaw is contained; that the context is authenticated; that the broker-connection identifier was broker-generated; that the target file identifier came from a trusted allowlist; that replay is prevented in practice; that paths are safe; that a capability was claimed; that any file was changed; or that OS privilege separation exists.
The content claim is also not a filesystem claim. Timestamps and file identity may change during replacement; owner, DACL, regular-file status, and workspace containment are left to CR-OC-001C with their own tests.
Tests
18 test-contract items and nine mutants, each with a designated killer test. Following the CR-YK-002 precedent, a mutant check counts only when the test is shown to fail against the mutated version.
The declared-identity mutant is recorded as weaker than the rest: it is a naming and structure defect rather than a behavioral one, so its test asserts the projection key set and the absence of any accessor presenting declared context as authenticated identity. A determined rename could still defeat it. Reviewers, not tests, are the control there.
Downstream
CR-OC-001D requirements, recorded here and satisfied by no earlier slice:
PIPE_REJECT_REMOTE_CLIENTSon creation;FILE_FLAG_FIRST_PIPE_INSTANCEwith a cryptographically random per-run pipe name;EveryoneACE and no anonymous-access ACE;NT AUTHORITY\NETWORKand of anonymous access;proposed_bytesare transmitted;Server verification matters for confidentiality even though the shim holds no mutation authority: a squatter winning the pipe name would otherwise receive proposed file contents.
Each slice requires its own approval. None is authorized by this document.
🤖 Generated with Claude Code