Skip to content

docs: Relay requirements for approval, authority and execution state (M4b, R6 to R10) - #44

Merged
sinkog merged 2 commits into
develfrom
docs/m4b-relay-requirements
Nov 8, 2026
Merged

sinkog merged 2 commits into
develfrom
docs/m4b-relay-requirements

Conversation

@sinkog

@sinkog sinkog commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

What

Documentation only (no code, no Relay change): the requirements the module needs from the Relay so that an execution is not only internally consistent (M4a, merged) but also authorized, happens at most once, and leaves evidence. The module can prove a request matches its plan and echo an execution_digest; it cannot prove that anyone allowed it, that it has not run before, or what happened to it.

  • docs/design/relay-requirements.md: new section R6 to R10, each with current behaviour (read from the Relay at 55a899f, not executed), required behaviour, a negative test and an acceptance condition. All needed, not yet raised in CIC-Relay.
  • docs/design/specs/approval-and-execution-state.md (new, status concept): what an approval names (execution_digest, approver, policy decision id, scope, validity, the module artifact digest verified at binding), the execution state of one approval, the ProofTrace evidence, the entry points, and the acceptance tests.
  • docs/design/README.md index entry.

The three points the spec fixes

  1. Atomic, durable single use. Reservation is a compare-and-set, durable and acknowledged before the first sign/actuate of the execution; of any number of concurrent requests for one approval at most one wins. A write-ahead record precedes every provider request. A reservation is released only when no request was attempted.
  2. UNKNOWN is not failure and is not retryable. If a request was sent and the answer lost, the execution is neither failed nor released, nothing is resent automatically, and it leaves UNKNOWN only through recorded reconciliation by evidence (for an asynchronous operation including correlating the Work Request with the execution). An ambiguous answer (5xx, gateway timeout) is not proof of no effect. Whether OCI accepts an idempotency token the digest could be derived into is NOT MEASURED and not assumed.
  3. ProofTrace keeps five things apart: requested, authorized, sent, confirmed by the provider, uncertain; plus the authenticated run id, the policy decision id, the verified module artifact digest, the reservation transition and the three digests. A signed observation is its own kind and is never recorded as the proven physical state.

Review F1 and F2 (commit 5b0a6ed)

F1 - the approval must bind the actual host call, not only the module's entry point. cic-flow.sign signs whatever bytes the guest hands it and cic-flow.actuate forwards the guest's own method, URL, headers (its Authorization verbatim) and body (read at 55a899f), so a valid execution_digest and a verified approval prove nothing about which HTTP request leaves. R7 now states as a required security property that, before every actuate that can cause a side effect, the relay checks from its own authenticated context that the actual method, target, body and step belong to the reserved, approved execution (the requests an execution may make come from the approved execution, never from the guest); that a sign serving no such step is refused; and that a guest-supplied digest or trace id is not authority. The module's M4a gate covers only its own execute path and cannot protect the host boundary. New negative tests: a different request under a valid approval, and a direct call of cic-flow.actuate/sign outside an approved context. New spec section "The host boundary". How the relay does it stays its own design.

F2 - the safety record is fail-closed. The reservation and the write-ahead record are safety state, not telemetry: if they cannot be made durable, no provider request is sent; they are kept apart from optional trace processing (display, aggregation, export). New read fact: the cabinet's ProofTrace recorder is optional (nil skips it, core/cabinet/service.go:99, :105-106) and runs once, after all steps, with a failed write logged and the workflow continuing (:299-307): a post-hoc, best-effort record, not a write-ahead one. New negative test: the store fails at the reservation and at the write-ahead record, no provider request in either case.

Relay facts used (read at 55a899f, not executed)

  • The guest's auth context is the component id (core/cabinet/service.go:228); the cabinet dispatches only init, process, get, notify to a guest (:234-240).
  • The cic-flow host functions get cicFlowDeps{sign, doer, egress}: no run, principal or plan (cmd/relay/cic_flow_host_funcs.go:51-55).
  • sign and actuate accept a trace_id and never use it; the guest chooses it, so it cannot be an authority.
  • actuate returns every transport error as one error text, so "never sent" and "sent, answer lost" look alike to the guest.
  • No audit or ProofTrace sink is used in the cic-flow host functions.
  • core/nexus/ois (Intent, Policy.Authorize) has non-test importers only in core/nexus/recorder and tools/statechain-demo; cmd/relay and core/cabinet do not use it: the model exists, the link to the module's execution breaks there.

NOT VERIFIED (said so in the documents): whether the Relay has a durable store and its guarantees; whether it sees the operation name (execute/destroy/invoke) before the guest runs; which correlation method exists for a lost asynchronous answer.

Acceptance tests written for later (no Relay implementation here)

The seven required cases (and, after the review, three more: a different request under a valid approval, the host function called directly, and a durable record that cannot be written) (no or invalid approval; approval for another execution_digest; expired or revoked; two concurrent uses; lost OCI answer; changed module artifact; destroy/invoke without approval), plus: reconciliation needed to leave UNKNOWN, crash after the reservation, crash after the write-ahead record, a module that refuses with zero sends, and the shape of the trace. Each states when "no provider request" holds (zero sign and zero actuate).

Module-side follow-up (concept, not built)

destroy and invoke have no plan and no digest, so the module cannot yet hand the Relay a value to approve for them. A digest over the same framing (operation, kind, target, and for invoke the canonical body) is recorded as a concept item.

Not in this PR

No Relay code, no module code, no module.wasm rebuild or signature, no production-integration claim. Nothing here is implemented in the Relay today. No merge without explicit owner GO.

🤖 Generated with Claude Code

Gábor Z Sinkó added 2 commits November 8, 2026 14:07
…(M4b, R6 to R10)

The module now proves a request is internally consistent and echoes its
execution_digest (M4a). This records, as requirements toward the Relay and as a
concept-level spec, what has to be added so an execution is also authorized, happens
at most once and leaves evidence: an authenticated run-bound execution context (R6),
approval verification naming the execution_digest before the first provider call (R7),
an atomic durable reservation with a write-ahead record before each provider request
and an UNKNOWN state that is never resent automatically and leaves only through
recorded reconciliation (R8), a ProofTrace that keeps requested, authorized, sent,
confirmed and uncertain apart and names the decision context (R9), and destroy and
invoke under the same model (R10). Each item has the current behaviour read from the
Relay at 55a899f, the required behaviour, a negative test and an acceptance condition;
what was not read is marked NOT VERIFIED. The seven acceptance cases and the crash and
module-refusal cases are written as tests for later. Documentation only: no code, no
Relay change.

---
[signing-metadata]
key = cic-my-sign-key
signature = vault:v1:MEUCIF4XuTubkyAMdUO0odGU1S3GaCjDiKMlkhemaYaBPXteAiEAv/jXy04OQ1OgLLaoYKceeaxIC/xD9GWNs+M/WvAL958=
hash-algorithm = sha256
digest = 0cPd/Geh8e9QRlNKY7G0bO0EdurpuFeWxpAwGicECLk=

[certificate]
-----BEGIN CERTIFICATE-----
MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw
QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV
BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy
MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb
MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe
i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU
yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS
WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0
lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW
-----END CERTIFICATE-----
…fail-closed (M4b, review F1 and F2)

F1: the cic-flow host functions are independent of each other and of any plan: sign
signs the bytes the guest hands it, actuate forwards the guest's own method, URL,
headers and body. A valid execution_digest and approval therefore prove nothing about
which HTTP request leaves. R7 now states as a required security property that, before
every actuate that can cause a side effect, the relay checks from its own authenticated
context that the actual method, target, body and step belong to the reserved, approved
execution, that a sign serving no such step is refused, and that a digest or trace id
supplied by the guest is not authority. Two new negative tests: a different request
under a valid approval, and a direct call of the host function outside an approved
context. The spec gets a section on the host boundary.

F2: the reservation and the write-ahead record are safety state, not telemetry: if they
cannot be made durable no provider request is sent, and they are kept apart from
optional trace processing. The cabinet's recorder at 55a899f is optional, runs once after
all steps and continues on a failed write, so it is a post-hoc best-effort record (read:
core/cabinet/service.go:99, :105-106, :299-307). A test for a failing store is added.
Documentation only.

---
[signing-metadata]
key = cic-my-sign-key
signature = vault:v1:MEUCIHBcFbHkfWq+LzDdJk57zY3PfvrbbJpozZ9KRfJQBraqAiEA7aV/FmgWI4XDnGfiqQSUEJvuBS/s8hrbFh5HtrCscJc=
hash-algorithm = sha256
digest = YG1uxcICA5NtGtUbOksRFXtrjvAqM/OnyocQX8e+bNw=

[certificate]
-----BEGIN CERTIFICATE-----
MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw
QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV
BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy
MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb
MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe
i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU
yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS
WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF
BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0
lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW
-----END CERTIFICATE-----
@sinkog
sinkog marked this pull request as ready for review November 8, 2026 13:30
@sinkog
sinkog merged commit f48246e into devel Nov 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant