Skip to content

Flint 0.5#9

Open
irzhywau wants to merge 552 commits into
upstream/0.6-devfrom
flint-0.5
Open

Flint 0.5#9
irzhywau wants to merge 552 commits into
upstream/0.6-devfrom
flint-0.5

Conversation

@irzhywau

@irzhywau irzhywau commented Jul 2, 2026

Copy link
Copy Markdown

No description provided.

claude added 30 commits June 27, 2026 21:10
…fix BUG-1 zombie reaping

Re-integrates the parked W2 consent work onto the hardened flint line and fixes
the BUG-1 VM-lifecycle leak, as a verified slice ahead of the step-6 grant crux.

BUG-1 (zombie reaping): RunningVm::has_exited() reaps a self-exited crosvm child
via try_wait() (a zombie still answers kill(pid,0), so the liveness-only sweep
never collected it). reap_dead_capsules now iter_mut + has_exited so it actually
reaps; is_running() stays a cheap &self probe for read-only status queries. New
KVM-free unit tests cover the reaping decision.

W2 step 3 (binding): AffordanceBinding + four Option binding fields
(capsule/principal_id/method_id/input_hash) on PendingCapabilityRequest, plus
create_affordance_request. Reconciled with flint's existing requester_capsule_id
(G-ID): a single create_request_inner now carries BOTH the requester identity
(who asked) and the affordance binding (what consent is scoped to); neither
overwrites the other. Tests prove they coexist through a store round-trip.

W2 step 4 (consent path): shared elastos_common::canonical_input_hash (key-order
independent so gateway and runtime agree); /api/capability/request accepts the
four binding fields and routes an all-four-present request through
create_affordance_request, rejecting a partial set fail-closed; the gateway's
flat 403 for User-approval / high-risk affordances is replaced by an
InvocationGate that POSTs a scoped consent request to the runtime over HTTP
(the runtime remains the sole key holder; the edge only terminates protocol).

Behaviour-neutral for existing session-capability callers (all binding fields
default None). Gates: elastos-crosvm 22, elastos-runtime capability::pending 19,
elastos-server lib 850 pass; fmt + clippy -D warnings clean on touched crates.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…-consent enforcement crux)

When a pending affordance-consent request is approved, the grant now READS its
binding and mints a token scoped to the EXACT affordance + arguments, distinct
from the ordinary session/capability grant (which keeps flint's G-ID path):

- TokenConstraints gains method_id + input_hash (sealed pub(crate) fields) and a
  for_affordance() constructor (single-use, non-delegatable, bound). The binding
  is hashed into CapabilityToken::signable_bytes, so the (method_id, input_hash)
  the user approved is cryptographically sealed into the token — it cannot be
  replayed against a different method or with different arguments. Proven by a
  tamper test: swapping either field fails signature verification.
- The grant handler branches on the request binding: an affordance request mints
  at the BOUND capsule (request.capsule), single-use, +1h expiry; an ordinary
  request keeps the requester_capsule_id (G-ID) session path unchanged. A
  handler test proves the granted token carries the binding and validates only
  at the bound capsule.

Roadmap updated: W2 now ~60% (steps 1-4 re-integrated, 5 by construction, 6 done;
7-11 remain). Notes the binding-capsule identity-domain follow-up for step 10.

Gates: elastos-runtime + elastos-server lib tests pass (incl. 3 new W2 tests);
fmt + clippy -D warnings clean on touched crates.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…al consent identity (redemption enforcement)

The runtime becomes the SOLE validator for affordance-consent tokens. A capsule
redeems its granted token for a single invocation; the edge cannot bypass it.

- POST /api/capability/validate-and-consume (authenticated, not shell-only): the
  caller identity is the AUTHENTICATED session's vm-{name} (never the request
  body — Principle 16). It decodes the token, requires it to be an
  affordance-consent token (carries a binding), re-checks the EXACT method and
  re-hashes the arguments (shared canonical_input_hash) against the sealed
  binding, THEN runs capability_manager.validate() — which performs the full 12
  checks AND the atomic single-use consume + signed CapabilityUse. The binding
  checks precede the consume, so a mismatch fails closed and never burns the use.
  Every failure returns a distinct code with no internal prose (no topology leak).

- Identity domain (step 7a): consent now binds the canonical "vm-{name}" (the
  G-ID founder-ratified identity every gate validates against), not the bare
  manifest name — so the affordance token lives in the ONE identity domain rather
  than a second one (the anti-pattern G-ID already eliminated).

- Test proves the full request -> grant -> redeem path: a correct redemption
  consumes once, replay is refused, and method-swap / args-swap / wrong-caller
  each fail closed WITHOUT burning the single use.

Roadmap: W2 ~70% (steps 1-7 done; 8-11 remain). Gates: elastos-server lib 852
pass; clippy -D warnings + fmt clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…consent dispatch (compiler-enforced)

"No consent-gated dispatch without a live, redeemed grant" becomes a property the
COMPILER enforces, not a call-order convention.

- ValidatedAffordanceGrant: an unforgeable witness with a module-private field —
  the ONLY constructor is redeem_affordance_grant on a successful
  validate-and-consume. dispatch_consented_affordance requires it BY VALUE, so a
  consent-gated affordance cannot be dispatched without proof of redemption; it
  also re-asserts the grant authorises this exact method (defence in depth).

- plan_affordance_dispatch(gate, has_consent_token): a pure, total, unit-tested
  routing decision — Direct / RaiseConsent / RedeemThenDispatch. The invoke
  handler now drives off it: low-risk dispatches directly; a consent-gated first
  call raises a consent request (202); a consent-gated retry (token presented)
  redeems then dispatches.

- redeem_affordance_grant forwards the CALLER's own authorization to
  validate-and-consume, so the runtime authenticates the redemption as the bound
  capsule (vm-{name}) — not the shell. Any non-success fails closed: no witness,
  therefore no dispatch.

Honest scope: the live identity round-trip is fail-closed and is verified
end-to-end by the step-10 journey test; this slice lands the structural gate.

Roadmap: W2 ~80% (steps 1-8 done; 9-11 remain). Gates: elastos-server lib 853
pass; clippy -D warnings + fmt clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ing audit for affordance use

The redemption now produces a portable, verifiable receipt AND fails closed
unless a durable signed use-record was written — "if there's no receipt, there's
no act" at the protocol layer.

- AffordanceGrantReceiptV1 (new capability/receipt.rs): runtime-signed (the
  capability issuer key, the trust root) attestation binding the exact
  (capsule, method_id, input_hash, resource, action, token_id, redeemed_at).
  Domain-separated, length-prefixed signable digest; verify() fails closed on any
  malformed signature. Tests: a fresh receipt verifies under the issuer key;
  tampering ANY bound field breaks verification; a different key does not verify.

- Blocking audit: in CapabilityManager::validate(), an affordance (bound,
  single-use) token's success path emits the signed durable CapabilityUse via
  emit() and fails closed with the new ValidationError::AuditWriteFailed if it
  cannot be written (mirrors the AUD-2/AUD-3 fail-closed audit fixes). Ordinary
  capability tokens keep the existing best-effort emit — no behaviour change for
  any other caller.

- validate-and-consume returns the signed receipt; AuditWriteFailed maps to 503
  (a server fault), distinct from the caller-authorization 403s. The redemption
  test now asserts the returned receipt verifies under the runtime key and binds
  the exact (method, args, capsule).

Decision: runtime-sign (the runtime holds the issuer key and is the trust root).
Follow-up: sign the gateway provider-effect telemetry envelope (signer_did:None) —
the authoritative attestation is this runtime-signed receipt.

Roadmap: W2 ~90% (steps 1-9 done; 10 journey + 11 alignment/docs remain). Gates:
elastos-runtime capability 128 pass (3 receipt tests); elastos-server lib 853
pass; clippy -D warnings + fmt clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…+ fail-closed branches

test_affordance_consent_journey walks the real loop with live components
(CapabilityManager + PendingRequestStore): a consent-gated request -> grant ->
redeem -> a signed receipt that VERIFIES under the runtime capability key and
binds the exact (capsule, method, args). Then every "no" path fails closed:

- Deny: the user refuses; the request is denied fail-closed (no redeemable token).
- Revoked: a grant revoked before redemption cannot be redeemed.
- Not-an-affordance-token: an ordinary session token (no binding) is refused at
  validate-and-consume (it must use the ordinary capability gate).

The (method-swap, arg-swap, wrong-caller, replay) matrix is covered by
validate_and_consume_enforces_binding_single_use_and_caller; signature / expiry /
revocation primitives are enforced inside validate() (capability-conformance
battery) and flow through validate-and-consume unchanged.

Honest scope: the gateway->runtime HTTP redeem round-trip runs against the test
harness's stub runtime, so the live forwarded-bearer -> vm-{name} round-trip
remains an integration check, not unit-verified.

Roadmap: W2 ~95% (steps 1-10 done; 11 alignment+docs remains, then W2 CLOSED).
Gates: elastos-server lib 854 pass; clippy -D warnings + fmt clean; alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…e; CLOSE W2

W2 (unstub the consent act path) is complete and now un-regressable. Five scoped
ratchet assertions in check-wci-alignment.sh fail the build if any W2 guarantee
silently regresses:

- the flat 403 consent stub must not return to gateway_capsule_catalog.rs (scoped
  to source so the doc that quotes the old string is exempt);
- validate-and-consume must stay the registered redemption route (server.rs);
- consent dispatch must go through dispatch_consented_affordance + the
  ValidatedAffordanceGrant witness (gateway);
- affordance use must fail closed via AuditWriteFailed (manager.rs).

Each assertion is non-vacuous (patterns confirmed present/absent on the current
tree) and the gate stays green.

Docs: ROADMAP.md and W2_CONSENT_PLAN.md mark W2 CLOSED (steps 1-11) and record the
one open follow-up (sign the gateway provider-effect telemetry envelope;
signer_did:None) — the authoritative attestation is already the runtime-signed
receipt. The live gateway->runtime forwarded-bearer->vm-{name} redeem round-trip
is integration-verified, not unit-tested.

W2 summary: consent is real, cryptographically enforced, single-use + bound,
witness-gated at dispatch, blocking-audited, receipted, and pinned. Next: W0/W1
(make the halo a core-computed fact). Gates: alignment-check OK; no Rust changed
this slice (elastos-server lib 854 pass stands from step 10).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…gine)

Make blast-radius a COMPUTED fact, not a self-rating. ReachDescriptorV1::derive
computes reach from the capsule's ENFORCED capability (isolation tier +
guest_network/carrier net access) and the affordance's concrete (resource,
operation) — never from the self-declared AffordanceRisk, which a capsule can set
to anything. This is the data the shell's halo will render, honest by
construction.

- ReachDescriptorV1 { egress(None|Open), isolation(Data|Wasm|MicroVm|HostProcess),
  scope(Object|Collection|System|Unknown), reversibility(Reversible|OneWay|
  Unknown), observed }. observed=false when an affordance is resource-less or
  operation-less, so a vague act renders "incomplete", never falsely cool.
- declared_understates_reach(&AffordanceRisk): the advisory "a clone must lie"
  detector — flags a capsule claiming low risk while the observed reach is far
  (open egress, irreversible, or system-wide). Declared risk stays advisory; the
  core stamps the observed reach and only flags the contradiction (does not gate).
- 6 tests: sandboxed read reaches nowhere; networked microVM delete over a
  collection reaches far; a carrier service is a host process with open egress; a
  scheme wildcard is system scope; resource/operation-less affordances are not
  fully observed; declared-low-but-far-reaching is flagged.

Schema is V1 and additive — it is the shell's reach contract (and feeds ESP v0 in
W4). Next (W0b): wire derive() into the inspect/plan projection the shell reads;
W1 adds EgressReach::Allowlisted (per-host egress-as-capability).

Roadmap: W0 IN PROGRESS (engine landed). Gates: elastos-common 92 pass (6 reach
tests); clippy -D warnings + fmt clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ell reads

Every declared affordance in the capsule catalog now carries its CORE-DERIVED
reach, so a shell can render the blast-radius halo from real state rather than the
self-declared risk.

- AffordanceReachView { interface_id, method_id, risk, reach, declared_understates_reach }
  is attached as a parallel `affordance_reach` list on the catalog entry,
  computed in catalog_capsule_summary via ReachDescriptorV1::derive from the
  manifest's isolation type + ENFORCED permissions and each method's concrete
  (resource, operation). The manifest descriptor (CapsuleAffordanceDescriptor)
  stays a pure declaration — reach is projected alongside it, never mutated in.
- The declared-understates-reach flag rides the projection, so the "a clone must
  lie" contradiction (claims low, reaches far) is visible to whatever renders it.

Test: a capsule whose `scan.all` declares risk=read over `elastos://*` projects
egress=none / isolation=wasm / scope=system / reversibility=reversible /
observed=true AND declared_understates_reach=true; a contained `open.one` over a
specific resource projects scope=object and is not flagged.

Roadmap: W0 CLOSED (engine + projection). Next: W1 (egress-as-capability adds
EgressReach::Allowlisted + enforcement at the net boundary). Gates: elastos-server
lib 855 pass; clippy -D warnings + fmt clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ail-closed)

The reach vocabulary now distinguishes a LEASHED egress from a wide-open one —
the moat point: a verified capsule with Open egress can be visibly more dangerous
than one scoped to a handful of hosts.

- EgressReach gains Allowlisted (between None and Open).
- EgressAllowlist { allowed_hosts, allowed_schemes } with a fail-closed permits():
  a (host, scheme) is permitted ONLY if both are explicitly listed; an empty
  list permits nothing, so "no grant" and "grant nothing" both deny.
- EgressReach::resolve(has_net_capability, Option<&EgressAllowlist>) is honest and
  fail-closed: no capability -> None; capability + non-empty allow -> Allowlisted;
  capability + no/empty allow -> Open (surfaced as the dangerous reading, never
  quietly downgraded).
- is_far_reaching already keys on Open only, so a leashed (allowlisted) egress is
  correctly NOT far-reaching while an open one is.

HONEST SCOPE: this is the egress-capability MODEL + its fail-closed matcher.
Enforcing it at the kernel network boundary (the crosvm TAP firewall) and
threading a bound allow-list through the launch path is W1b — it requires
/dev/kvm + CAP_NET_ADMIN, which this sandbox host lacks, so it is deliberately
deferred to a host that has them rather than faked. WASM is already
default-no-internet by construction (no socket surface in its WasiCtx).

Roadmap: W1 IN PROGRESS (model landed; W1b = boundary firewall on the KVM lane).
Gates: elastos-common 95 pass (3 new egress tests); clippy -D warnings + fmt
clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…l token)

The privileged shell session token is no longer tied to the magic capsule name
"shell" — it goes to the user-selected ACTIVE shell, and only if that capsule
holds the Shell role.

- shell_token_eligible(name, role, active_shell) = role == Shell && name ==
  active_shell. Fail-closed: a non-Shell capsule can NEVER receive the shell
  token even if the active-shell pointer names it.
- Supervisor gains an `active_shell` pointer (default "shell" => behaviour-neutral)
  with set_active_shell()/active_shell(), so a different Shell-role capsule can be
  the active shell without the runtime hardcoding a name — the backend the
  shell-picker (W6) selects against.
- The launch site swaps `name == "shell"` for the role-based decision; the bundled
  shell (role=shell) is unaffected.

Test: the bundled shell stays eligible; a different Shell-role capsule selected as
the active shell is eligible; a non-Shell capsule is never eligible; a Shell-role
capsule that is not the active shell is not eligible.

Deferred (W3b): rename the headless "shell" decision-engine to "consent-broker"
(a wide, mechanical rename — safer as its own clearly-scoped slice).

Roadmap: W3 IN PROGRESS (shell de-hardcoded; consent-broker rename = W3b). Gates:
elastos-server lib 856 pass; clippy -D warnings + fmt clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…m shipped state

ESP (the ElastOS Shell Protocol) v0: the read-only projection contract a shell
renders. Written by EXTRACTION — every fact/verb cites the exact struct + route
that already produces it; nothing is invented.

- docs/ESP_V0.md: the inviolable read-only-projection rule; the real transport
  (HTTP GET JSON + the 202 consent flow); per-fact `schema`-tag versioning;
  must-ignore-unknown for FACTS with the honest caveat that verb INPUTS use
  deny_unknown_fields; the 5 fact families (catalog/v1, interfaces/v1, reach.v1,
  affordance-consent-pending/v1, affordance.receipt.v1) with routes + source
  structs; the 4 verbs (request/grant/deny/validate-and-consume + invoke); and an
  initialize handshake DEFINED but marked not-yet-implemented. Degraded surfaces
  called out: EgressReach::Allowlisted modeled-not-enforced (W1b),
  ReachDescriptor.observed, the redeem->dispatch live round-trip (integration-only).

- elastos/esp/esp_v0.ts + README: shared TS types mirroring the serde shapes
  (snake_case enum wire values; optional = skip_serializing_if/default; fact types
  carry an index signature for must-ignore-unknown). Type-checks clean under
  `tsc --noEmit --strict`.

- check-wci-alignment.sh: W4 ratchet — the schema tags + routes ESP_V0.md
  documents must still exist where it cites them, so the spec cannot silently
  drift from what the runtime serves.

No Rust behaviour change (doc + types + script only) — elastos-server 856 /
elastos-common 95 suites stand. Roadmap: W4 DONE; next W5 (the v1 projection
shell + hero dDRM act). Gates: tsc --noEmit --strict clean; alignment-check OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…P-pure, in-cloud tested)

The first pixel of the product, as verifiable projection logic: a headless,
ESP-pure client that renders the never-seen two-channel object from REAL ESP v0
fact shapes — it holds no key and re-derives no crypto, it projects the runtime's
verdicts (read-only projection principle).

- two_channel.ts: Channel 1 trustMaterial(capsule) projects the runtime's
  trust_state verdict (verified | content_addressed | unsigned; fail-honest:
  unknown -> unsigned, never over-trusted). Channel 2 blastRadius(reach) projects
  the core-computed reach into a hazard band (open egress is high-hazard; a
  LEASHED/allowlisted egress reads cooler than an open one — the W1 point), honest
  about `observed` (unobserved -> incomplete, never falsely cool). twoChannel()
  composes them with refuseTrained (verified && hot) + declaredUnderstatesReach.
- consent_act.ts: the hero dDRM act shapes — consentPendingIsWellFormed and
  receiptMatchesRequest (the receipt must attest the EXACT act requested; the
  runtime did the crypto, the shell asserts the binding).
- 11 node:test cases prove it, headlined by "a VERIFIED capsule can read MORE
  dangerous than an UNSIGNED one" — you can refuse the thing the green check
  trained you to trust — and the hero act's receipt-binds-the-act safety.
- Toolchain: tsconfig (strict + noUnusedLocals/Parameters + noImplicitReturns),
  a tiny node-shims.d.ts so it type-checks without @types/node, package.json
  (type: module), build/ gitignored. `tsc` clean; `node --test` 11/11.

W5b (the visual Svelte shell) is the browser/local lane. No Rust change
(esp_v0.ts gained the real trust_state/signature_state/cid_state fields) — the
856/95 suites stand. Gates: tsc strict + node --test 11 pass; alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…il-closed)

"A shell among shells", made selectable — pure ESP projection over the catalog
facts, no Rust change (the catalog already emits role + launchable).

- shell_picker.ts: isShellSelectable(capsule) = role=="shell" && launchable — the
  ESP mirror of the runtime's shell_token_eligible, so a non-shell can NEVER
  become the active shell. selectableShells(catalog), shellTrustCard (reuses the
  two-channel trust-material so a user picks a shell knowing its trust verdict),
  shellPicker(catalog, active?) (defaults to first selectable; a stale/non-shell
  active falls back fail-closed), withActiveShell (returns null for a non-shell).
- refraction.ts: RefractionState<T> — two registered shells over ONE projected
  state; toggleFocus swaps the lens and carries the projected state through
  UNCHANGED (one source of authority, no migration). The verifiable model under
  W5b's ~320ms visual cross-fade.
- 21 node:test (with two_channel's): only shell+launchable selectable; non-shell
  never; picker default/select/fallback; the toggle preserves + round-trips the
  projected state. node-shims.d.ts gained deepEqual/strictEqual.

The picker only RENDERS + REQUESTS; the runtime's set_active_shell (W3a) is the
sole thing that actually switches authority (ESP read-only principle). No Rust
change — the 856/95 suites stand. Gates: tsc strict + node --test 21 pass;
alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…fact (the flywheel turns)

The same signed receipt that delights the consumer IS the compliance evidence a
regulator/insurer forces — re-projected, fail-closed.

- ai_act_audit.ts: toAiActAuditRecord(consent, receipt) maps a redemption onto the
  controls an auditor checks -> AiActAuditRecordV1 (schema elastos.audit.ai-act.v1):
  the `act` (from the signed receipt), `human_oversight` (EU AI Act Art 14 —
  required for any user-approval OR high-risk class), `record_keeping` (Art 12 —
  the tamper-evident signed receipt), and a plain-language control mapping.
- containmentEvidence(record): fail-closed at the audit layer — Art 12 fails on an
  UNSIGNED record (no receipt -> no provable act); Art 14 fails when a high-risk or
  user-approval act executed without human consent. `contained` requires both.
- 26 node:test (with the rest of esp/): the mapping; a user-approved+signed act is
  contained; an unsigned record fails Art 12; a high-risk runtime-policy act (no
  human) fails Art 14; a low-risk automated act with a signed record is contained.

This closes the in-cloud core of the full W0-W7 wedge sequence: consent is real
and receipted (W2), the halo is truthful (W0), egress is modeled (W1a), the shell
is de-hardcoded and selectable (W3a/W6), the protocol is written (W4), the
two-channel object renders (W5a), and the receipt is now the compliance artifact
(W7). Pure projection, no Rust change — the 856/95 suites stand. Gates: tsc
strict + node --test 26 pass; alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ide shell rename)

The consent decision-engine was implicitly conflated with the capsule role
`Shell`: `shell_only_middleware` gated capability grant/deny/revoke + lifecycle
endpoints by calling `session.is_shell()` directly. A naive wide "shell →
consent-broker" rename was NOT cleanly shippable — the trust boundary is not a
separable entity from the role (KNOWN_GAPS G-CIE: "the grant root IS the trusted
shell"), so renaming the role is forbidden and renaming only the middleware would
leave a misleading `consent_broker` gate still calling `is_shell()`.

Instead, name the decision-engine at one documented seam:

- add `Session::is_consent_broker()` — the consent-authorization predicate,
  delegating to `is_shell()` today with a doc bridge; a future dedicated
  consent-broker tier changes only this one method.
- rename `shell_only_middleware → consent_broker_only_middleware`, routing it
  through `is_consent_broker()`; update the import + 4 application sites + 403
  message + the 2 doc-comment references + the KNOWN_GAPS G-CIE note.

Pure clarity, no behavior change: the predicate is byte-equivalent today, and a
capsule session is still fail-closed NOT a consent broker (new unit test pins
both the equivalence and the fail-closed case). The capsule role `Shell`,
`active_shell`, and `shell_token_eligible` are untouched.

Gates: rustfmt --check, clippy -D warnings (runtime+server), session/middleware/
capability tests green, WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…on (DoS)

All three carrier bridges checked MAX_LINE_BYTES only AFTER `read_line` /
`.lines()` had already buffered the whole line, so an untrusted guest could OOM
the host by sending a line with no newline (the WASM API bridge had no bound at
all). The size check was decorative — the allocation it guarded already happened.

Add a fail-closed bounded reader that caps allocation DURING the read:

- `read_bounded_line` (async, AsyncBufRead) + `read_bounded_line_sync` (BufRead),
  sharing a `BoundedLine { Line | Eof | TooLarge }` outcome. Each uses
  `take(MAX+1).read_until(b'\n')` so the read itself stops at the cap, then
  drains to the next newline IN BOUNDED CHUNKS (DRAIN_CHUNK_BYTES) so the stream
  realigns to the following request without reintroducing the OOM.
- wired into all three loops: the async serial carrier bridge (the BUG-6-named
  `read_line`), the sync WASM carrier bridge, and the sync WASM API bridge.

Test-first (in-memory pipes, no VM): `read_bounded_line_*` prove a complete line
reads, CRLF is stripped, an oversized line is rejected TooLarge AND the stream
realigns so the next request parses, oversized-at-EOF terminates cleanly, and the
sync twin enforces the same bound + realign.

Gates: cargo test (36 carrier_bridge tests green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-6 + ROADMAP updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
`next_cid` advanced with an unchecked `*next += 1` and never consulted the live
VM set, so once the counter wrapped (overflow) it could re-hand a vsock CID a
running VM still held — two VMs sharing a CID breaks vsock routing.

Replace with a pure `allocate_cid(from, in_use) -> Option<(cid, advance)>`:
- floors the transport-reserved low CIDs (0/1/2) so guests start at MIN_GUEST_CID
  (3);
- skips any CID currently in the `running` map's `vsock_cid` set;
- wraps the u32 space via `checked_add().unwrap_or(MIN_GUEST_CID)` instead of
  overflowing;
- bounded scan (`in_use.len() + 1` candidates — pigeonhole) and returns None to
  fail closed if every CID is occupied. The launch site now holds the counter
  lock across the live-set scan (serializes concurrent launches) and refuses the
  launch on None rather than colliding.

Test-first (pure, VM-free): `supervisor::tests::allocate_cid_*` prove the
reserved floor, skip-live, wrap-without-overflow, the wrap-does-not-collide-with-
a-live-VM case (the bug), and free-within-bound.

Gates: cargo test (29 supervisor tests green), clippy -D warnings,
rustfmt --check. KNOWN_GAPS BUG-8 updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… final poll

The carrier `request_capability` loop was sleep-then-check, so a grant recorded
between the last in-loop poll and loop exit was never observed — the capsule got
a spurious timeout even though the consent broker had granted.

Extract `await_capability_decision` + a `CapabilityDecision` enum: poll the
pending store on the interval, then do ONE final read after the loop so a
late-landing decision is caught. The carrier bridge now routes its grant/deny/
expire/timeout handling through it (same observable responses).

Test-first + deterministic (no timing flake): with `max_polls = 0` the loop body
never runs, so a pre-seeded grant can ONLY be found by the trailing read —
`await_capability_decision_catches_a_grant_after_the_loop` would time out under
the old code. A clean-timeout case guards against a false grant.

Residual (noted in KNOWN_GAPS): the WASM API bridge's HTTP poll has the same
shape; its trailing-read fix needs an HTTP-mock test to gate and is a tracked
follow-up (separate transport, not the named carrier loop).

Gates: cargo test (38 carrier_bridge tests green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-5 + ROADMAP updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…g failure

A single-use capability is consumed at validate() before send_raw, so a provider
failure burned the grant on a no-op (the holder had to re-consent for a failure
that wasn't their fault).

An object-capability audit established that refunding on a general provider Err
is UNSAFE: no carrier provider op is atomic on its Err path (write-then-fail is
possible) and ProviderError::{Provider,NotFound,Io} cannot distinguish "acted
then failed" from "never acted" — so a refund there could enable a second
execution of a partially-applied write (catastrophic for the actuator/payment
classes that are the point of the AI-Act containment story).

Land the one PROVABLY-safe slice: refund ONLY on ProviderError::NoProvider (a
routing failure — the registry invoked nothing), via a new saturating
CapabilityStore::refund_token_use + CapabilityManager::refund_use. Every other
failure keeps the use consumed (fail-closed). The carrier dispatch binds the
consumed token id once (single-assignment; we only reach dispatch with a
validated, consumed token) and refunds it on the NoProvider arm.

Test-first:
- capability::store::refund_token_use_is_the_saturating_inverse_of_try_use —
  the core mechanic (returns exactly one slot; saturates at 0, never mints uses).
- carrier_bridge::carrier_invoke_refunds_single_use_grant_on_missing_provider —
  the SAME single-use token reaches the provider lookup TWICE through the public
  handle_request path; under the old burn-on-failure code the second call would
  be capability_denied.

Residual (deferred, bigger refactor, recorded in KNOWN_GAPS): op-failure refunds
need a provider "did-not-act" signal before they can be made safe.

Gates: cargo test (39 carrier_bridge + 5 store green), clippy -D warnings,
rustfmt --check, WCI alignment OK. KNOWN_GAPS BUG-4 + ROADMAP updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Define docs/ESP_SHELL_UI.md: every Svelte component the shell needs, mapped onto
the proven, tested ESP projection it renders — the UI adds no new logic, it only
projects what the headless esp/*.ts layer already computed (W5a/W6/W7).

- The one invariant (every pixel a read-only projection of signed runtime state;
  no component holds a key, token, or authority; actions emit INTENTS the runtime
  gates, never authority the view mints).
- Eight component contracts (props = ESP fact type + schema tag; the headless fn
  it projects; what it renders; the intent it may emit): CapsuleCard,
  TwoChannelObject (refuseTrained = verified && hot), HazardMeter, ConsentSheet,
  ReceiptBadge, ShellPicker, RefractionToggle, AiActAuditCard (Art 12/14 contained).
- Composition tree + a schema-tag pinning table so the alignment ratchet can later
  guard the pixel⇄fact mapping, mirroring the W4 ESP route pins.

Alignment ratchet gains W5b pins (the AiActAuditCard fact tag must exist in
esp/ai_act_audit.ts; the spec must define the card and codify the no-authority
invariant). The live Svelte paint + visual snapshot test stay on the browser/
local lane (tracked as W5b implementation in ROADMAP).

Gates: WCI alignment OK (new W5b pins pass); ESP TS unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
A team code-quality audit (Kent Beck seat) over the campaign diff, each finding
adversarially verified before applying:

- F1 (confirmed): the `request_too_large` reply was hand-built in all three bridge
  loops with drifting formatting. Extract one `oversized_request_error()` so the
  wire shape cannot diverge between the microVM, WASM-carrier, and WASM-API bridges.
- F2 (already satisfied): the sync bounded reader already documents itself as the
  "synchronous twin of read_bounded_line … same semantics" — the divergence-catching
  cross-reference the finding asked for. No change needed.
- F3 (corrected): the finding claimed the WASM-API HTTP poll has "no bug — the last
  iteration is the final check." That premise is FALSE: the loop shares BUG-5's
  post-last-poll window (it is the tracked BUG-5 residual). Added an HONEST comment
  pointing at KNOWN_GAPS, NOT the finding's incorrect "fine" framing.

No behavior change. Gates: cargo test (39 carrier_bridge green), clippy -D warnings,
rustfmt --check.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… safe refund

Close the BUG-4 residual's MECHANISM: providers can now signal a provably-pre-
effect rejection, and the carrier safely refunds the single use on it.

- Add `ProviderError::DidNotAct(String)` with a load-bearing ocap contract on the
  variant: a provider may return it ONLY when it is certain nothing was mutated/
  emitted/spent (precondition/validation failure), so a replay is an idempotent
  no-op. Any failure that MIGHT have partially acted must keep using Provider/Io/
  NotFound so the use stays consumed.
- The carrier refunds the consumed single use on `DidNotAct` (mirroring the
  `NoProvider` slice); every other Err keeps it consumed (fail-closed). Only the
  two provably-no-effect classes are refund-safe.
- Wire the new variant through the two exhaustive matches: `Display` and the
  storage `From<ProviderError>` (→ 400-class InvalidPath).

Test-first (mock providers, no production behavior change):
- carrier_invoke_refunds_single_use_grant_on_did_not_act — a DidNotAct failure
  refunds, so the SAME single-use token reaches the provider twice.
- carrier_invoke_keeps_single_use_consumed_on_acted_failure — an acted Provider
  failure keeps the use consumed (second call capability_denied).

Real-provider migrations (e.g. read-only InspectProvider) are a tracked follow-up
in KNOWN_GAPS with a per-op "provably no side effect" gate + a response-shape
caveat — not a blanket sweep.

Also records the G3b decision in KNOWN_GAPS: keep preview/enforce as DISJOINT
tables (cross-checked), NOT a shared table (which would make the conformance test
vacuous) — correcting a swarm proposal.

Gates: cargo test (871 server lib + 33 provider green), clippy -D warnings,
rustfmt --check, WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ider manifests

Generalize the single rights fixture to a UNIVERSAL pin: enumerate every shipped
provider manifest (22 with authority) and assert, for every declared op, that the
verb-map-ENFORCED action (`required_action_for`) is a MEMBER of the manifest's
PREVIEWED action set (`plan_provider_operation`). The two tables stay DISJOINT
(no shared function), so drift fails loudly — Option B, not a shared table (which
would make the test vacuous).

Key correction during build: a naive `previewed == enforced` is wrong universally
— a multi-action capability block legitimately previews a union, so the right
invariant is membership (the manifest must declare AT LEAST the enforced action);
a true drift is enforcement requiring an action the manifest never grants for the
op (a verb-map Admin fallthrough).

The pin surfaced 49 real drifts (all fail-CLOSED today: previewed-but-denied,
never an escalation). A 4-agent classification swarm triaged them into a
documented `known_divergences` ledger (read-safe / write / execute-egress /
manifest-over-declares / HIGH-RISK-keep-Admin). The ledger is a ratchet: a NEW
drift fails the test, and removing a fixed op without a real fix also fails (it
cannot rot). Per-op RESOLUTION (verb-map entries for safe reads; restructure
blanket-`execute` manifests like did/*; KEEP Admin for wallet-signing / key-release
/ decrypt / seal / broadcast) is tracked follow-up — deliberately NOT a bulk
enforcement loosen.

This CLOSES the G3b "drift can hide" gap. KNOWN_GAPS G3 row updated.

Gates: cargo test (rights + universal conformance green), clippy -D warnings,
rustfmt --check, WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…ller refactor

Verified the response-shape gate the KNOWN_GAPS caveat required before migrating
InspectProvider to emit ProviderError::DidNotAct. Result: NOT a drop-in. Its
send_raw Ok(error-status Value) contract is consumed by BOTH transports (the
carrier AND the gateway provider proxy, gateway_provider_proxy.rs:1399) plus ~21
inspect-test assertion sites. Emitting Err(DidNotAct) would change the response
shape on both transports and break those assertions — a scoped refactor, not a
quick PoC.

Recommendation recorded: the first real DidNotAct migration should target a
carrier-only provider (no gateway dependency on its send_raw error shape), or do
InspectProvider as its own chunk that updates the gateway proxy + tests. The
DidNotAct mechanism itself stays proven by last turn's mock-provider PoC.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…cked ops

Shrink the G3b preview≠enforce ledger by 11, the swarm-verified subset where the
op was UNMAPPED (→ fail-closed Admin) but its shipped manifest ALREADY declares
the matching action — so adding the verb-map entry makes preview==enforce and
loosens enforcement only to the action the author already authorized (a pure
preview-honesty fix, no new authority).

Graydon-seat verification read each impl and confirmed the action class:
  Read:   download, events, metadata_index, read_bytes, reconstruct_listing,
          export_graph, prepare_publish (pure assembly — "NEVER published")
  Write:  restore, write_bytes, import_graph
  Delete: empty_trash (fs::remove_dir_all)

object-provider `share` deliberately STAYS in the ledger (grants access —
security-touching, held for a dedicated review). The ratchet did its job: it
flagged `prepare_publish` as healed-but-still-listed until I removed it, so the
ledger cannot rot.

Gates: cargo test (872 server lib, incl. universal conformance, green),
clippy -D warnings, rustfmt --check, WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…/ai/llama/tunnel)

Drain 12 more G3b drifts by making the manifests HONEST: the blanket
single-action capability blocks declared one action for ops the verb map enforces
at finer granularity, so preview≠enforce. Split each into per-action blocks that
match the (unchanged) verb-map enforcement — a preview-honesty change that grants
no new authority and drops no op (Miller-seat verified every op's action class).

- did-provider:   [execute]* -> read{get_did,resolve,get_nickname,get_persona_did}
                   + write{set_nickname} + execute{sign_chat_message,verify,verify_did_recovery}
- ai-provider:    [execute]* -> read{list_backends,ping} + execute{chat_completions}
- llama-provider: [execute]* -> read{status,health,list_models} + execute{chat_completions}
                   (health/list_models verb-map Read added in ch1)
- tunnel-provider:[admin]*   -> read{status,ping} + admin{start,stop}

Same resource/reason/audit_events preserved per manifest. The conformance ratchet
again caught healed-but-still-listed ops (did/*) until removed from the ledger.

Tracked separately (Miller): did `get_persona_did` now previews Read (matching the
EXISTING Read enforcement), but its impl create-on-first-call has Write semantics —
a pre-existing verb-map question, not a G3b drift.

G3b drain now 23/49 resolved; 26 sensitive-tail drifts remain (egress/actuator/
keys/signing — KEEP Admin).

Gates: cargo test (common 95 + runtime 345 + server 872, all green),
clippy -D warnings, rustfmt --check, WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
… splits

Drain 8 more G3b drifts. Two egress-security agents read the net-provider and
exit-provider implementations and confirmed: their egress ops (connect/stream/
http; quote/open_stream/close_stream/http_fetch) are genuinely Execute, and
NEITHER provider has any operation that mutates persistent state — so the manifest
`write` action was dead/over-declared.

- net-provider:  [read,write]* -> read{status,resolve} + execute{connect,stream,http}
- exit-provider: [read,write]* -> read{status} + execute{quote,open_stream,close_stream,http_fetch}
  (dead `write` dropped from both; enforcement unchanged — pure preview honesty)
- encrypt-provider: [write]{status,seal} -> read{status} + write{seal}
  `status` drift fixed; `seal` STAYS in the ledger (HIGH-RISK: CEK sealing,
  enforced Admin) for a dedicated review.

G3b drain now 31/49 resolved; 18 dangerous-tail drifts remain at Admin/tracked
(browser-actuator, drm open, encrypt seal, wallet signing/approval/secret-export,
key release, decrypt, chain broadcast, object share).

Gates: cargo test (common 95 + runtime 345 + server 872, all green),
clippy -D warnings, rustfmt --check, WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
Drain 5 more G3b drifts. A UI-actuation security agent read the
browser-engine-adapter implementation and confirmed: launch/attach_stream/
close_page/input/webrtc_signal are Execute (UI actuation — driving a headless
browser page), sandbox-scoped, with NO host command injection and NO secret
custody, so Execute (not Admin) is correct; status/page_status/screenshot/frame
are Reads; and NO op mutates persistent state (the manifest `write` was dead).

  browser-engine-adapter: [read,write]* ->
    read{status,page_status,screenshot,frame} +
    execute{launch,attach_stream,close_page,input,webrtc_signal}
  (dead `write` dropped; enforcement unchanged — pure preview honesty)

G3b drain now 36/49 resolved; 13 dangerous-tail drifts remain at Admin/tracked
(drm open, encrypt seal, wallet signing/approval/secret-export, key release,
decrypt, chain broadcast, object share).

Gates: cargo test (872 server lib green), clippy -D warnings, rustfmt --check,
WCI alignment OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
…d teeth

W1b design contract (KVM-lane build): a per-capsule kernel firewall on the
capsule's TAP that physically drops egress outside its declared EgressReach,
fail-closed, with every blocked attempt written to the signed flight recorder.

- Keys on the now-honest egress capability (G3b drain: net/exit declare `execute`)
  and the existing reach vocabulary (EgressReach::{None,Allowlisted,Open},
  EgressAllowlist::permits, EgressReach::resolve).
- Threads the EXISTING TAP launch hook (supervisor.rs:1115, guest_network) — install
  a per-TAP nftables chain (None=drop-all, Allowlisted=allow-resolved-only,
  Open=allow+tagged-hot); tear down on reap (leak-free, mirrors BUG-2/3).
- Names the DNS problem honestly (host-scoped allowlist vs IP filter) with a
  host-side DNS-proxy primary + IP-set interim.
- Spells out the privilege requirements (/dev/kvm + CAP_NET_ADMIN + nftables) — why
  it is the KVM lane, not in-cloud — plus a 5-test plan incl. a compromised-guest
  backstop.

The convergence: reach model + honest egress capability + flight recorder become
one enforced thing — packet-level proof the agent stayed in its lane (EU AI Act
containment). Alignment pins anchor the doc to the reach API + TAP hook it builds on.

Gates: WCI alignment OK (new W1b pins pass).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FEL7iSfBWL2JiAFDy8fq5z
claude added 30 commits July 7, 2026 02:23
Folds the two-seat council review of Sprint 35. Both seats returned
SHIP-WITH-FIXES; the central money invariant (no double-refund,
no refund-a-confirmed) already held. Two HIGH structural findings
converged on the idempotency guard's durability.

Red-team F1 (HIGH) + guardian F3 (HIGH) — the guard was blind if the
pending record failed to land AFTER an irreversible broadcast, and
money-bearing keys were evictable, both reopening the cross-window
double-buy. Fixed structurally:
- RECORD-BEFORE-BROADCAST: the pay closure now durably custodies the
  key as Pending via PaymentLedger::begin_attempt BEFORE calling
  provider.pay; if the ledger cannot custody it (per-capsule pending
  cap, ledger full of money-bearing keys, persist failure) it REFUNDS
  and DECLINES without broadcasting — money never moves into an
  unrecordable state. After pay, finalize() transitions the placeholder
  to the outcome. begin_attempt reopens a provably-nothing-moved
  terminal for a legit retry and refuses a money-bearing key
  (AlreadyActive).
- NEVER EVICT MONEY-BEARING KEYS: eviction now sheds only
  NotCharged/ResolvedNotCharged (provably nothing moved); Pending/
  Performed/ResolvedCharged are protected, so an eviction can never
  forget a charged key. A cap full of money-bearing keys refuses new
  inserts fail-closed.

Guardian F1/F2 + red-team F3 (HIGH/MED) — confirmation fail-closed:
tx_confirmation_live now gates BOTH verdicts on the depth floor (a
shallow revert is HELD, not refunded — a reorg could re-include it),
and an absent/empty/unparseable receipt status reads as HOLD not
success (a hostile receipt with a blockNumber but no status cannot
promote a buy). Extracted a pure classify_receipt() so these
money-critical rules are CI-testable without a live chain.

Wording/doc folds: guardian F3 (corrected the mis-stated "charged keys
never evicted" — now true by construction), F4 (added the ledger
snapshot back-compat test), F5 (FLINT doc no longer calls a DRM buy a
rail-trust attestation — it is chain-confirmed), F6 (reconcile doc
comment); red-team F2 (pending-quota availability regression),
F4 (best-effort receipt binding), F5 (drm:tx= discriminator) all stated
as tracked MKT-DRM residuals.

New ratchets: payment_ledger::tests::{money_bearing_keys_are_never_
evicted, begin_attempt_custodies_reopens_and_refuses, pre_s35_ledger_
snapshot_without_token_id_round_trips}; chain_tx::confirmation_tests::
classify_receipt_is_fail_closed_on_status_and_depth. Serialized the two
env-mutating build_pay_rail tests behind a shared lock.

Gate green: elastos-runtime 432, elastos-server 1230; clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes the primary open MKT-DRM residual: the cap was enforced in spend
units while the actual on-chain charge is the listing's pay-token
amount, so the meter bounded intent, not the money leaving the wallet.

The price gate. The DrmSettler trait gains quote() — a READ-ONLY price
source (buy_authority::quote_buy, no broadcast). The provider now:
resolve → quote → PRICE-GATE → settle. The gate converts the mandate's
cap to pay-token units via a declared mapping (amount ×
ELASTOS_DRM_SPEND_UNIT) and refuses BEFORE broadcast if it does not
cover the on-chain price (NotCharged/refund; fail-closed on an
unparseable price or a conversion overflow) — never buy above what the
mandate authorized. settle() binds the gated price as the buy's
expected price, so buy_access's own abort-on-drift fires if the live
price changed between quote and broadcast (closes the TOCTOU: the buy
can never settle above the gated price).

Declared unit mapping, fail-closed. ELASTOS_DRM_SPEND_UNIT is
REQUIRED on the live Chain rail (pay-token smallest-units per spend
unit, e.g. 1000000 for USDC) — build_pay_rail refuses to wire the DRM
rail without it rather than silently assume 1:1. Dev/chain-mock quote
free, so the gate is a no-op there.

Receipt honesty. rail_ref now carries ;price=<price>;tok=<pay_token>,
so verify-receipt shows the pay-token price actually authorized, not
just the spend-unit amount.

Ratchets: drm_marketplace::tests::{a_buy_below_the_on_chain_price_is_
refused_before_broadcast (settler never called), an_exact_match_buy_
proceeds_and_the_rail_ref_names_the_price (unit scaling + rail_ref),
an_unparseable_price_is_refused_before_broadcast};
server::tests::drm_rail_obeys_the_mock_money_discipline extended for
the missing-unit wire refusal. The S34/S35 e2e updated for the new
quote step + rail_ref format.

Gate green: elastos-runtime 432, elastos-server 1233; dev-modes lane
passes; clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Folds the two-seat council review of Sprint 36. Both seats returned
SHIP-WITH-FIXES; the money-invariant findings were the ship-blockers.

F1 (both seats — the buy above the cap): the gate compared the mandate
cap against the listing's PER-UNIT price, but buy_access charges
price × quantity, and the DRM BuyTarget left quantity unset — so an
ambient ELASTOS_DDRM_BUY_QUANTITY>1 env could settle N× the gated
price. Fixed: the DRM buy PINS quantity=1 (a pay-for-access buy is one
ACCESS_TOKEN; the per-unit gate is now the total charge, and no env can
inflate it).

F2 (both seats — pay-token flip): the buy bound only expected_price, so
ensure_no_drift's payToken check was a tautology (compared the settle-
time re-read against itself). A seller swapping the listing's pay-token
at an equal numeric price between quote and broadcast would settle in a
different, possibly far-more-valuable token. Fixed: quote_buy now
returns the RAW pay-token (not a "native" alias), and the DRM buy binds
expected_pay_token = quote.pay_token, so abort-on-drift fails closed on
a token flip too.

F3 (guardian — one unit maps one token): a single global spend_unit was
applied to whatever token a listing quoted (heterogeneous per-listing
tokens make one mapping unsatisfiable). Added a REQUIRED (live Chain)
ELASTOS_DRM_PAY_TOKEN: a listing quoting any other token is refused
before broadcast, so the declared unit always denominates the declared
token and the ceiling stays literal. build_pay_rail refuses to wire
the live rail without it.

Extracted ChainDrmMarketplace::buy_target so the money-critical target
invariants (quantity=1, price + pay-token drift armed, pinned
operative/tokenId) are CI-testable without a live chain.

New ratchets: drm_marketplace::tests::{the_drm_buy_target_pins_
quantity_and_arms_price_and_pay_token_drift, a_listing_in_a_different_
pay_token_than_declared_is_refused}; server::tests::drm_rail_obeys_the_
mock_money_discipline extended for the ELASTOS_DRM_PAY_TOKEN wire
refusal. Docs (FLINT row, rail runbook, KNOWN_GAPS) qualify the
"literal ceiling" claim with the operator-declared-mapping residual and
document the quantity pin + pay-token requirement.

Gate green: elastos-runtime 432, elastos-server 1235; dev-modes lane
passes; clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lity

Four-seat quality council (money spine, dispatch surface, receipt
primitives, docs) over the whole Flint core. Every small-and-safe finding
folded; structural refactors ledgered in docs/CODE_QUALITY_LEDGER.md.

Money spine:
- Pre-broadcast refusal sentinels are shared consts (buy_authority) now
  referenced by both the producers and the DRM refund classifier, so a
  rewording can never silently degrade a provable refund into a hold;
  pinned by a sentinel-anchor test.
- Ledger admission (per-capsule pending cap + money-bearing-aware
  eviction) extracted to one admit_new_key home used by record and
  begin_attempt; a failed reopen now restores the WHOLE prior record
  (status, rail_note, refund_applied) with a ratchet test.
- record/record_with_token re-documented as test-seeding only (the
  production path is begin_attempt/finalize).
- ensure_budget post-publish persist failure now reports Poisoned (the
  honest variant), matching the file's own S29 F1 rule; ratchet added.
- reconcile_drm_confirmations Confirmed/Reverted arms share one spine;
  quote/settle sold-out refusal is one const; parse_drm_tx doc + dead
  unwrap_or cleaned; contradictory test name fixed; RAIL_TIMEOUT named;
  in-flight slot guard simplified; guarded unwrap in tx_confirmation_live
  restructured to a match; buy_authority tests moved to tempfile.

Receipt primitives:
- The frozen-serialization invariant now lives on AuditEvent and
  ChainedRecord themselves (not just three fields): field order/names on
  EVERY variant are load-bearing for every signed chain.
- One shared recompute_record_hash canonicalization recipe used by both
  verify_chain and verify_mandate_receipt.
- signable_digest never silently hashes empty bytes on a serialization
  failure (expect over unwrap_or_default) and the SecureTimestamp JSON
  preimage coupling is documented + byte-pinned by a ratchet test.
- responsible-entity syntactic bound has one home
  (capability::responsible_entity_syntax_ok) used by all three choke
  points; denial-reason test now exhaustive (WrongAgent/NoGrant pinned);
  silent pubkey-publish and corrupt-counter-file failures now log loudly;
  dead allow(unused_imports) removed.

Dispatch surface:
- drm_rail test now takes the crate-wide ddrm_env_lock (flaky-CI race
  with rights/mint authority tests closed) with documented lock order;
  build_pay_rail test guards ELASTOS_PAYMENT_RAIL; one shared EnvGuard.
- RequestCapabilityOutput docs corrected to the actual wire statuses;
  trivial drm_state wrapper removed; ELASTOS_WEB_MAX_SPEND_CAP now warns
  on an unparseable override; chain_tx test module moved past the items
  that followed it.

Docs / presentability:
- Flint is now discoverable from the front door: README Documentation
  table + What Works Today, and docs/README.md deep reference.
- FLINT_MANDATE_ENGINE.md: branch-anchored opener replaced with what
  Flint IS; the 1,900-word runtime.pay table cell exploded into a real
  "payment spine" section; mandate=standing-grant vocabulary stated;
  stale test counts and sprint-log tone removed.
- DRM_MARKETPLACE_RAIL.md: DDRM_LEDGER annotated as not-a-typo,
  REQUIRED-in-practice vars marked, and a three-surface payment-state
  mapping table (rail/ledger/dispatch) added.
- KNOWN_GAPS.md retitled runtime-wide; MKT-DRM residual list headed
  honestly; GLOSSARY gains the Flint vocabulary; CODE_QUALITY_LEDGER.md
  tracks the deferred structural cleanups (CQ-1..CQ-10).

Gate: runtime 434 + server 1237 + common 103 green; dev-modes lane green;
clippy --tests clean in the Flint core.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Closes KNOWN_GAPS MKT-DRM residual 2b: reconcile_drm_confirmations was
correct but inert (operator/automation had to invoke it). The runtime now
drives it unattended.

The scheduler (spawn_drm_reconcile_scheduler, one per process, spawned
next to the one rail build):
- OFF BY DEFAULT: arms only when ELASTOS_DRM_RECONCILE_INTERVAL_SECS is
  set (u64 >= 1) AND the rail carries a DrmConfirmer (a new PayRail
  field, present only on the DRM rail — structurally un-armable on
  HTTP/mock). Malformed interval/batch env REFUSES to arm; an interval
  on a non-DRM rail warns and stays off. The arming decision is a pure
  fn (drm_reconcile_schedule_from_env), CI-pinned.
- ZERO new money-moving code: a tick is the SAME
  reconcile_drm_confirmations pass the manual reconcile drives; every
  charge/refund still flows through reconcile_payment_core, exactly-once
  by the ledger's resolve semantics.
- BOUNDED + ROTATING (council F1 fold): at most
  ELASTOS_DRM_RECONCILE_BATCH (default 64) entries per tick, overflow
  counted as `skipped`, and a rotating cursor starts each tick after the
  previous tick's last scanned seq (wrapping) — a stuck-unconfirmed
  prefix can never starve the entries (or refunds) behind it.
- PANIC-ISOLATED per entry, with ledger-truth accounting (council F3
  fold): a panicking confirmer/reconcile holds THAT entry and the tick
  continues; a panic after the durable resolution is accounted by what
  the ledger says and raises a money-state alarm, never a silent hold.
- NON-WEDGING (council guardian F2 fold): at most one tick in flight —
  a hung chain RPC is skipped loudly each interval, never awaited into
  a dead schedule and never stacked into new blocked threads. Ticks run
  on the blocking pool; missed ticks delay (no catch-up bursts).
- ATTESTED honestly (council F2 fold): a tick that SETTLED anything
  appends a best-effort signed drm_reconcile_tick event; idle and
  held-only ticks stay off the chain, so a stuck pending cannot grow
  the signed chain by one event per tick.

Ratchets: a_tick_is_bounded_oldest_first_and_reports_what_it_skipped,
a_stuck_oldest_entry_cannot_starve_the_entries_behind_it,
a_panicking_confirmer_holds_that_entry_and_the_tick_continues,
only_a_settling_tick_is_attested_held_and_idle_ticks_are_silent,
the_drm_scheduler_arms_only_with_an_interval_and_a_drm_rail.

Docs: DRM_MARKETPLACE_RAIL.md (env block + scheduler section + honest
residuals), KNOWN_GAPS.md (MKT-DRM 2b CLOSED with stated residuals:
opt-in by design; no subprocess read deadline yet; best-effort tick
event), FLINT_MANDATE_ENGINE.md (payment-spine bullet).

Gate: runtime 434 + server 1242 + common 103 green; dev-modes lane
green; clippy clean. Council: two seats (principles guardian, red team),
both SHIP-WITH-FIXES, all findings folded above with ratchets.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…tor watches

The first slice of the North-Star surface, shipped inside the existing
Mandates shell app. STRICTLY read-only: every pixel is a projection of
the one enforcing registry + meter + ledger (same Arcs as the pay gate);
the panel has no buy verb — operators grant, agents act through the
existing signed-intent dispatch.

GET /api/apps/mandates/marketplace (launch-token gated, 503 when pay is
unwired — the Money panel's posture):
- ASSETS: every DRM asset the ACTIVE pay-mandates scope (resource
  elastos://runtime/pay/<asset>, method runtime.pay), with live on-chain
  quotes via the existing read-only quote_buy path. Quote reads are
  SINGLE-FLIGHT (council fold: a miss claims an in-flight sentinel under
  the cache lock before any read starts, so "one live chain read per
  asset per TTL window" is literal under any concurrency — the S33 XSS
  actor cannot amplify browser fetches into a blocking-pool storm),
  TTL-cached (30s, pruned on every pass, post-fetch stamped), and
  fresh-read capped per view with cache hits FREE — coverage rotates
  across refreshes instead of permanently starving the tail (council
  fold, with a rotation ratchet).
- BUYS: the ledger's records with honest wording — a broadcast is
  "awaiting chain confirmation", never a purchase (regression-tested
  against "purchas*"); confirmed carries its tx. Every PENDING buy is
  ALWAYS shown (council fold: a flood of newer settled entries can never
  push a live obligation out of sight — ratcheted); the settled tail is
  windowed with the window STATED (buys_total/buys_limit, "showing X of
  N" in the panel). Outside the live Chain rights mode the panel says
  quotes are free/synthetic rather than displaying fake prices.
- UI renders exclusively via textContent (asset ids, quote errors, rail
  notes and tx are external bytes; tx display server-bounded).

The demo lane: `elastos mandate market-demo <asset> [--amount N]` — cap
→ single-asset pay-mandate bound to an ephemeral agent key → the agent's
signed buy through the existing dispatch → revoke + cap cleanup (a
grant whose token id cannot be read warns LOUDLY about the live
authority, mirroring `demo` — council fold). The buy's ledger record
remains for the panel to show.

New read-only accessors: PaymentLedger::{recent(limit), len, is_empty}.

Council: two seats (principles guardian, red team), both
SHIP-WITH-FIXES; no XSS / auth bypass / money movement found; all
findings folded above with ratchets
(quote_coverage_rotates_instead_of_starving_the_tail,
a_pending_buy_is_never_truncated_by_a_flood_of_settled_entries, +
marketplace_projects_pay_mandates_and_ledger_buys_with_honest_wording,
marketplace_route_requires_home_launch_token,
marketplace_without_a_rail_reads_unwired).

Docs: FLINT_MANDATE_ENGINE.md (Watch row + operator-surface section),
DRM_MARKETPLACE_RAIL.md (Watching it — the panel + the demo).

Gate: runtime 434 + server 1247 + common 103 green; dev-modes lane
green; clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
The loop's last structural gap closes: an agent could buy under a
mandate but not ask what things cost. runtime.market_quote is a READ
affordance behind the existing dispatch gate — quote the live terms of
a granted asset, and receive them in the dispatch response.

One quote spine (P5): a new crate::market_quote module owns the
single-flight, TTL-cached, read-only quote path (MarketQuoter trait;
LiveMarketQuoter = the existing quote_buy, no new chain code). The S38
Marketplace panel was REWIRED onto it and PayRail gained a shared
quote_cache Arc, so the panel and the affordance ride one identical
fan-out bound: at most one live chain read per asset per 30s window,
whoever asks, under any concurrency.

The affordance:
- MANDATE-SCOPED, no market-wide oracle: the resource is the same pay
  resource the buy uses; the envelope gate's exact match confines
  quoting to granted assets. One envelope carries ONE action, so quote
  (read) and buy (execute) are TWO grants on one resource — the docs
  and the demo now say and do exactly that (council guardian F1: the
  demo originally issued one execute grant, so its quote was denied
  wrong_action — dead on arrival; fixed with two grants, both revoked
  at exit).
- EXPLICIT disclosure channel: IntentExecution::Performed gained
  agent_visible_report — per-executor opt-in data surfaced as
  DispatchIntentOutput.report ONLY on a Matched performance, bounded
  and printable-filtered at the pipeline boundary (guardian F4,
  mirroring rail_ref's emit-site discipline). market_quote discloses
  the canonical terms; every other affordance passes None — state_get's
  one-bit rule stays structural, pinned by an e2e negative control.
- HONEST reconciliation: discovery ("" declared) performs as declared;
  attested mode echoes the ACTUAL terms so a changed listing reconciles
  diverged, never a fabricated match — attesting the terms as of the
  spine's last read (TTL caveat documented, guardian F3). A failed read
  declines with the bounded error; dev/mock synthetic quotes are
  documented as such (guardian F5).
- BOUNDED like money (council red-team F1): MAX_INFLIGHT_QUOTES=8 with
  an RAII slot guard — K distinct assets against a hung RPC can no
  longer park K blocking threads and starve the pay pipeline's pool;
  over the bound refuses with retry, never queues. Chain-sourced terms
  are held to the SAME bound as the agent's declaration before being
  signed or disclosed (red-team F2 — validate what we sign); and the
  cache prunes once per pass instead of per asset (red-team F3).

market-demo is now quote-THEN-buy: the agent reads the terms under its
read-mandate, decides, and only then dispatches the buy under its
execute-mandate — the first decision-making agent act in Flint.

Ratchets: market_quote_discovery_returns_terms_via_the_disclosure_channel,
market_quote_attested_mode_echoes_actual_terms,
market_quote_declines_on_read_failure_and_outside_the_pay_namespace,
market_quote_shares_the_single_flight_cache,
market_quote_refuses_malformed_terms_from_the_quote_source,
market_quote_inflight_reads_are_bounded_fail_closed,
an_agent_quotes_its_asset_and_state_get_still_discloses_nothing,
+ market_quote module unit tests.

Docs: FLINT_MANDATE_ENGINE.md affordance row (two-grants rule, TTL
caveat, dev-mode honesty), DRM_MARKETPLACE_RAIL.md (agent quoting +
demo).

Gate: runtime 434 + server 1256 + common 103 green; dev-modes lane
green; clippy clean. Council: two seats, both SHIP-WITH-FIXES, all
findings folded with ratchets.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… chain

Closes the S37/S39-ledgered hung-read residual at its root. run_chain_capsule
(the ONE shared chain-provider subprocess helper — rights gate, resolve,
quote, buy prepare/broadcast, receipt/confirmation reads) now bounds the
whole conversation: ELASTOS_CHAIN_READ_DEADLINE_SECS (default 30; malformed
or <1 => the default, loudly), child spawned in its own process group, a
watchdog SIGKILLs the group at expiry, the child is always reaped.

The money line (both council seats verified SAFE at every consumer): a
deadline error carries CHAIN_DEADLINE_MARKER + "UNRESOLVED". On the SEND leg
it classifies INDETERMINATE (the tx may have broadcast — hold, reconcile),
NEVER a refund. On read legs it is an ordinary fail-closed refusal/hold.

Council: two seats, both SHIP-WITH-FIXES; all findings folded:
- pid/pgid-reuse kill window (red-team F1 / guardian F3): the watchdog is
  disarmed and joined BEFORE the child is reaped, so it can only ever kill a
  still-live, un-reaped child.
- unbounded response-line length (red-team F2): read_capsule_line is now
  length-capped (MAX_CAPSULE_LINE = 4 MiB), so a firehose provider is a
  bounded error, not an OOM.
- hostile-provider refund break (red-team F3, HIGH, pre-existing): a
  `chain-provider op failed:` error is post-broadcast by construction, so
  is_pre_broadcast_refusal now refuses to read any op-failed error as a
  pre-broadcast refusal (=> Indeterminate/hold).
- error honesty (guardian F2): the deadline error keeps the underlying error
  and `fired` is set only inside the unix kill.
- doc over-claim (guardian F1): the deadline bounds ONE chain conversation;
  the wallet-provider sign leg and rights-provider decide leg are separate
  undeadlined subprocesses — the tracked sibling residual. Plus the
  availability-cliff note and comment-precision nits (F4/F5).

Ratchets: rights_authority::{a_hung_chain_provider_is_killed_at_the_deadline,
a_malformed_deadline_env_keeps_the_default_protection},
drm_marketplace::{a_chain_deadline_error_is_never_classified_as_a_refund,
a_post_broadcast_op_error_is_never_read_as_a_pre_broadcast_refusal}.

Gate: runtime 434 + server 1260 + common 103 green; dev-modes green; clippy
clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…unded reap

Factor Sprint 40's chain-conversation watchdog into ONE shared module
(api/capsule_watchdog.rs) and extend the deadline discipline to the two
remaining unbounded subprocess conversations a money/access path traverses:

  - wallet-provider SIGN leg (WalletSession::exchange): a timeout is a
    PRE-broadcast refusal — the tx was never signed, so provably NotCharged.
    drm_marketplace::is_pre_broadcast_refusal recognizes the marker ⇒ refund,
    the exact mirror of the chain send-leg (Indeterminate/hold) rule.
  - rights-provider DECIDE leg (run_rights_capsule): a timeout DENIES access
    (fail-closed — every access consumer denies on Err).

The shared module owns the subtle bits so no third copy can drift: the
process-group spawn, the disarm-before-reap ordering (a recycled pid can
never take a stray group kill), the length-capped read, and the shared
deadline knob.

Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):

  - guardian F1 (HIGH) + F2: the disarm-before-reap left the REAP itself
    unbounded — a provider that answered every op then refused to exit on
    EOF/shutdown would park the runtime thread forever on child.wait(), the
    very hang the deadline exists to end, slipped past the read into the reap.
    New reap_grouped() gives a brief grace then group-SIGKILLs an un-exiting
    child; wired at all three sites (chain, rights, wallet shutdown).
  - guardian F3 (P12): scoped the "every money/access boundary is bounded"
    doc claim to the three provider conversations actually bounded, restored
    the unix-only qualifier, and registered the access-path sidecar residual.
  - guardian F4: added the live-kill ratchet the money-critical sign leg
    lacked (a_hung_wallet_provider_is_killed_and_classified_pre_broadcast),
    asserting bounded return + marker + end-to-end refund classification.
  - guardian F5: a watchdog fire now poisons the session so no later leg runs
    on the dead child (a fired-but-read-Ok race can no longer degrade a
    provable refund to a stranded Indeterminate hold); deadline errors append
    the underlying read error for diagnostics.
  - guardian F6 / red-team F1: honesty comments — the marker names the
    session's purpose (any pre-broadcast leg), the wallet read is now capped,
    and a footgun note that a future human-consent approve must run OUTSIDE
    this deadline.
  - red-team F2: DeadlineWatchdog now disarms on Drop, so an un-disarmed
    watchdog is a no-op rather than a latent full-deadline group-kill.

Gate: server lib 1263 (default) / 1264 (dev-modes), runtime 434, common 96 —
all green; every new ratchet returns bounded (~1s, not the stub's 300s).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ng open/view

Close the last residual the Sprint 41 guardian left open: the access-path
sidecar helper reads (the media/object content authorities and the trustless
grant sidecar) were still unbounded, so a hung or hostile content-authority
subprocess could park a request thread forever. This wires those reads onto
the EXISTING shared capsule_watchdog module (no fourth copy of the ordering)
and adds the shared pieces they need:

  - ReapGuard: a spawned child that is BOUNDED-reaped on drop unless disarmed,
    so EVERY early return between spawn and the point the child is handed to
    its owner reaps it (no leaked live process, no zombie).
  - read_line_deadlined / read_open_deadlined: per-op VIEW reads use the short
    shared deadline; content-OPEN descriptor reads use a new, longer
    content_open_deadline (ELASTOS_CONTENT_OPEN_DEADLINE_SECS, default 120)
    because a media open runs ffmpeg + a seal handshake before it answers.
  - read_to_eof_deadlined: the ONE bounded one-shot read (the grant sidecar).
  - ACCESS_DEADLINE_MARKER: a content-authority timeout is a DENY (fail-closed)
    — the mirror of the rights-decide rule, NEVER a money decision (these sit
    on the open/view paths, not the pay spine).

Applied to object_authority (both descriptor reads + the object/page VIEW
reads + grouped spawn + bounded Drop), media_authority (both descriptor reads
+ the segment VIEW read + grouped spawn + bounded Drop, its local reaper
replaced by the shared ReapGuard), and access_grant (the one-shot run_sidecar).

Council fold (principles-guardian + red-team, both SHIP-WITH-FIXES):
  - RT-F1 (HIGH) / guardian-1: object_authority leaked a LIVE child on a
    well-formed-but-incomplete descriptor (field-extraction `?` after a good
    read). Closed by the shared ReapGuard — every early return now reaps.
  - RT-F2 (MED-HIGH): access_grant leaked the child on the stdin-write (EPIPE)
    path — the reap ran only after the read. Closed by the same guard.
  - RT-F3 (MEDIUM): the RPC-tuned 30s deadline silently capped an ffmpeg open
    (an availability cliff). Split into the separate content_open_deadline.
  - RT-F4 / guardian-2: child_pid() returned 0 → a fire would kill(-0) the
    gateway's OWN process group. ReapGuard::pid panics instead, and arm now
    refuses to kill on pid 0.
  - guardian-3: access_grant hand-rolled a fourth copy of the arm/read/disarm/
    reap ordering. Factored into the shared read_to_eof_deadlined (which also
    now carries the marker, closing the doc over-claim).
  - guardian-4: reap_grouped returns the ExitStatus so the one-shot folds a
    non-success exit into its error.
  - guardian-5/6/7/8: fmt fix; a hung-VIEW-read ratchet (the per-op leg,
    distinct from the open); the write-leg-is-sub-pipe-buffer note; the stale
    reap-backstop comment corrected; the prepare/assemble spawn_blocking
    follow-up registered in KNOWN_GAPS.

Ratchets (all bounded ~1-2s vs a 300s stub): a hung media/object authority and
a hung grant sidecar are each killed and DENIED, and a sidecar that answers the
descriptor then hangs on a VIEW read is separately bounded.

Docs: the S41 sidecar residual is now CLOSED in KNOWN_GAPS, DRM_MARKETPLACE_RAIL
and FLINT_MANDATE_ENGINE — every content-open/view + provider conversation the
pay/access pipeline traverses is bounded, reap included, kill unix-only.

Gate: server lib 1267 (default) / 1268 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean on the touched files.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…lassifier

The last soft spot in the money classifier: `drm_marketplace::is_pre_broadcast_refusal`
decided refund-vs-hold by matching SUBSTRINGS of `buy_access`'s error string —
provider-controlled text touching a money decision. This makes the decision a
TYPE, not a string.

  - `buy_access` now returns `Result<BuyOutcome, BuyError>` where
    `BuyError::{PreBroadcast, Indeterminate}` is decided BY CONSTRUCTION: each
    error site is mapped by its position relative to the broadcast op. Every
    `broadcast_signed_*`/`broadcast_mock` call and every post-broadcast op is
    `.map_err(Indeterminate)`; every pre-send leg (wallet-not-linked, source,
    sold-out, drift, the wallet SIGN leg, the chain PREPARE read, the missing-
    signature precondition, the dev record) is `.map_err(PreBroadcast)`.
  - `DrmSettleError::from_buy_error` maps the variant. The string classifier
    (`is_pre_broadcast_refusal` + its `for_test` bridge + the pre-broadcast
    sentinel list + the `chain-provider op failed` exclusion) is DELETED.
  - There is NO `From<String> for BuyError`, so a bare `?` on a broadcast call
    will not compile — a future refactor cannot silently mis-map a send error.
  - Side benefit the type unlocks: a chain deadline on the PREPARE (read) leg is
    now correctly refundable while the same marker on the SEND leg stays held —
    a distinction the string could not draw (all deltas are safe-or-refund).

Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed the invariant — no provider input can refund a possibly-sent
tx — holds by construction):
  - guardian F1 (must-fix, P12): rewrote three wallet_signer doc/test comments
    that still pointed at the deleted classifier — the wallet marker is now
    diagnostic text, classification is the call-site variant.
  - guardian F2 (P12 honesty): the prepare-leg refinement is transitively
    covered by the sign-leg call-site mapping; reworded KNOWN_GAPS so "Proven
    by" no longer adjoins an unratcheted claim, and registered the dedicated
    prepare-leg fixture as a follow-up.
  - guardian F3: added ratchets for the two previously-untested variant sites —
    a dev record failure types PreBroadcast (a behavior change the typing makes
    correct) and a post-broadcast record failure types Indeterminate.
  - guardian F4 (P16): documented the trusted-binary caveat on the PreBroadcast
    variant (the runtime owns the only broadcast; env allowlist is the follow-up).
  - red-team INFO-2: removed the dead `needs a signed transaction` phrase check.

Retired ratchet intents preserved and strengthened: the hostile-provider proof
is now `from_buy_error_classifies_by_variant_not_by_string` (an Indeterminate
error embedding EVERY sentinel + both deadline markers still holds) plus the
end-to-end `a_broadcast_op_error_types_the_buy_as_indeterminate_even_with_a_sentinel`.

Gate: server lib 1264 (default) / 1269 (dev-modes), runtime 434, common 96 —
all green; fmt + clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…RM 2d

The DRM confirmation reconciler used to identify its pending records by
sniffing `rail_note` for a `drm:tx=` prefix, so a hostile HTTP payment
endpoint whose Indeterminate body began `drm:tx=` could get its pending
polled/resolved by the DRM driver (council S35 red-team F5, fail-closed
direction but wrong). This replaces the note-sniff with a STRUCTURED tag.

  - `PaymentRail::{Unknown, Http, Drm}` on `PaymentRecord`, stamped from the
    paying provider at `begin_attempt` via a new REQUIRED `PaymentProvider::
    rail()` method (no default — the tag is now a COMPILE-TIME property, the
    S43 type-over-comment lesson). `DrmMarketplaceProvider::rail()==Drm`,
    `HttpPaymentProvider::rail()==Http`.
  - `reconcile_drm_confirmations` selects via `is_drm_pending`: Drm→ours,
    Http→never (regardless of note), Unknown→bounded pre-S44 note fallback.
    A positively-tagged Http pending — even one whose body a hostile endpoint
    crafted to begin `drm:tx=` — is never polled by the DRM driver.
  - Back-compat: `#[serde(default)]` ⇒ pre-S44 snapshots load as `Unknown` and
    keep the note fallback so in-flight pendings reconcile across the upgrade;
    a re-persisted legacy record gains the tag.

Plus docs/LIVE_BUY_RUNBOOK.md — the operator-driven procedure for a real
testnet buy (Grant→Act→Prove); deliberately OUT of the CI gate (the sandbox
has no funded wallet/network — the gate proves everything else via mocks).

Council fold (principles-guardian SHIP-WITH-FIXES + red-team SHIP-CLEAN; the
red team confirmed both money directions — no hostile HTTP pending is
DRM-resolvable, and no genuine DRM pending is stranded by mis-tagging, since a
money-bearing pending is un-reopenable so its tag is immutable):
  - guardian F1 (ship-blocking, P12): fixed a KNOWN_GAPS self-contradiction
    (the residual index said 2d open while the cell marked it CLOSED) + the
    stale row title.
  - guardian F2 / red-team F1: made `rail()` a REQUIRED trait method (removed
    the default) so "every new record is positively tagged" holds by
    construction, not by a doc comment — 7 impls updated.
  - guardian F3 / red-team F5: ratcheted the reopen-adopts-rail behavior
    (and that a money-bearing pending's tag is immutable ⇒ no stranding) +
    an e2e assertion that the production pay closure stamps `Drm`.
  - guardian F4 / red-team F2: a Drm-tagged pending with no `drm:tx=` note is
    now visible (a one-time WARN) instead of silently folded into the
    never-mining set; ratcheted (confirmer never called, money unchanged).
  - red-team F4: documented the forward-incompatible-downgrade constraint on
    `PaymentRail` (unknown rail string fails load fail-closed, no serde(other)).
  - guardian F5/F6: qualified the S35 round-trip comment and marked
    `seed_pendings` as deliberately exercising the legacy fallback.

Gate: server lib 1268 (default) / 1273 (dev-modes), runtime 434, common 96 —
all green; clippy clean on the touched regions.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…chain scaffold

The live-money last mile, in two honest halves. The sandbox has no funded
wallet or network, so a real on-chain buy CANNOT run here — and nothing in
this change claims one did. What ships:

(A) GATE-RUNNABLE (runs every push) — two tests in `verify_receipt_cmd` prove a
    receipt carrying a DRM settlement `rail_ref` in its signed `CapabilityUse`
    is an admissible artifact through the ACTUAL standalone `verify-receipt`
    CLI evaluator: written to disk as JSON, read back, verified AUTHENTIC
    (exit 0) with the pinned signer, the `drm:tx=` reference surviving the
    round-trip; and a byte-tampered settlement tx breaks the record-hash
    recompute ⇒ INVALID (exit 1). This is the auditor's artifact path, beyond
    the in-memory `verify_mandate_receipt` the S35 e2e already covers.

(B) NETWORK-GATED (operator/CI only) — a new `live-chain` cargo feature gates
    `tests/live_drm_buy.rs`, an `#[ignore]`d integration test that drives the
    real `ChainDrmMarketplace` (resolve → on-chain price gate → sign → broadcast
    → confirm) per docs/LIVE_BUY_RUNBOOK.md. Triple-fenced from the gate: the
    whole file is `#![cfg(feature = "live-chain")]`, the fn is `#[ignore]`d, and
    CI never enables the feature. It asserts the buy is HELD at broadcast
    (`Indeterminate` with a real tx, never charged — the S35 invariant) then
    confirms on-chain.

Council fold (both seats SHIP-WITH-FIXES; both verified by execution that the
gate tests are load-bearing, the live test cannot leak into the gate — proven
three ways — and NOTHING claims a live buy executed):
  - red-team F1: aligned the gate fixture's emit shape to the real reconciler
    (`Action::Execute` on `elastos://runtime/pay/<payee>`) and softened the
    "exact shape" wording so the fixture doesn't over-claim production fidelity.
  - both F2: reworded the live test's doc from "the whole stack / never refund
    a sent tx" to the honest provider→chain leg it actually drives; the
    ledger/reconcile/receipt legs are gate-proven against mocks + driven live by
    the gateway per runbook §2.
  - red-team F3: added a network-free CI `cargo check --features live-chain
    --tests` step so a rename can't silently rot the operator's only live tool.
  - guardian F1: assert `ELASTOS_DDRM_BUY_SIGN=wallet` up front (equally
    mandatory with the empty subject) so a misconfig fails with the true cause.
  - red-team F4: assert the rail_ref carries a real 0x+64-hex EVM tx before
    polling, so a stub binary can't green the confirm loop on garbage.
  - red-team F5: pin the tamper failure to `!hashes_ok` (not merely
    `!structurally_valid`) — the mechanism is now a ratchet.
  - guardian F3 / red-team F7: documented that a live re-run RE-SPENDS (settle
    ignores the idempotency key; the standalone test bypasses ledger dedup) in
    the runbook + the timeout assert, and that a `live-chain` build (pulls in
    dev-modes) must never be reused as a release artifact.
  - guardian F6: KNOWN_GAPS + runbook now state plainly this has NOT yet been
    run against a live testnet in this repo (record the first run's tx hash).

Gate: server lib 1268 (default) / 1273 (dev-modes), bin 105 (+2 CLI ratchets),
runtime 434, common 96 — all green; live-chain test compiles + is correctly
ignored (0 run in the gate).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ic delta)

CI declares `cargo fmt --all -- --check` but the tree had drifted (344 diff
sites across 24 files) — a gate that cannot pass is a lie the honesty regime
cannot carry (S46 council Track B4). This is `cargo fmt --all` verbatim:
purely mechanical reflow, zero semantic change, proven by the unchanged test
counts on the follow-up commit's full gate.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…leg ratchet, spawn_blocking)

Three council carry-overs closed, continuing the drive to 10/10 while the live
Base run waits for the operator:

  - B1 (P16, council S43 guardian F4 partial): `capsule_watchdog::spawn_grouped`
    — the single seam every provider/sidecar spawn goes through — now strips
    RUNTIME_ONLY_SECRETS from the child env: ELASTOS_DDRM_BUY_SIGNED_TX (a
    fully broadcastable signed tx; leaked to a hostile provider binary it could
    be broadcast out-of-band while the read leg fails "pre-broadcast") and
    ELASTOS_PAYMENT_TOKEN (the HTTP rail bearer). Both are consumed exclusively
    in-process and passed onward as explicit op arguments, so no capsule has a
    legitimate use for the env copy. Ratchet:
    `runtime_only_secrets_are_stripped_from_spawned_capsules` (canary secrets
    ABSENT in the child; ordinary ELASTOS_* config still passes through).
    Honest scope: a targeted denylist of runtime-only secrets — the full
    per-capsule env allowlist remains the stronger tracked hardening.
  - B2 (closes council S43 guardian F2): the dedicated PREPARE-leg ratchet
    `buy_authority::a_chain_prepare_deadline_types_the_buy_as_pre_broadcast` —
    a chain stub answers the LISTING read then hangs only on
    `prepare_transaction`; the buy is killed at the deadline and typed
    `BuyError::PreBroadcast` (refund), carrying the SAME CHAIN_DEADLINE_MARKER
    the SEND leg holds as Indeterminate — the call site decides, not the bytes.
    Guards the ordering invariant a refactor could silently break (hoisting the
    prepare out of the sign closure).
  - B3 (closes council S42 guardian F8): `access_grant::prepare`/`assemble` in
    viewer_open now run inside `tokio::task::spawn_blocking` — the
    deadline-bounded (≤~31s) sidecar wait holds a blocking-pool thread, never
    an async worker; a panicked task maps to the same fail-closed error arm.
  - KNOWN_GAPS updated for all three closures (F4 marked PARTIALLY closed —
    the allowlist stays tracked).

(The preceding commit 133c340 is the mechanical repo-wide `cargo fmt` that
makes CI's declared fmt gate truthful — B4.)

Gate: server lib 1269 (default) / 1275 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; clippy clean on touched files.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ard + gate the dev-modes lane

The S46 council (red-team SHIP-WITH-FIXES + guardian SHIP-WITH-FIXES) proved
three of the sprint's claims outran the code; this folds all of them:

  - red-team F1 / guardian F1 (HIGH, the blocker): "single spawn seam" was
    FALSE — `ProviderBridge::spawn` (every general provider capsule), the
    carrier service spawn, and the shell-capsule spawns (main/serve_cmd)
    bypassed `spawn_grouped` and inherited the rail bearer under a normal
    HTTP-rail config. The strip now lives as ONE shared list
    (`elastos_runtime::provider::RUNTIME_ONLY_SECRETS`) applied at ALL FIVE
    capsule seams; host-tool spawns (git/tar/self re-exec, which legitimately
    keeps its env) are deliberately not stripped.
  - red-team F5 (MEDIUM): the canary proved the helper strips, not that spawns
    route through it. Added the SOURCE-STRUCTURAL guard
    `every_command_spawn_site_is_a_known_seam_and_capsule_seams_strip_secrets`:
    every `Command::new` site in elastos-server/elastos-runtime must be a
    classified capsule-seam-carrying-the-strip or a consciously allowlisted
    host tool — a NEW spawn path fails the gate until classified.
  - guardian F2 (MAJOR, P12): `assemble_cached` (the popup-free re-open path)
    still shelled the grant sidecar on the async executor one line below the
    fix — now also in spawn_blocking, preserving its fall-back-to-enrolled arm.
  - guardian F3 (MAJOR, gate honesty): the dev-modes construction ratchets
    (S43 typed BuyError, the S46 prepare-leg deadline) were NEVER COMPILED by
    the gate. `just _verify-tail` and ci.yml now both run the
    `--features dev-modes` lib lane — a ratchet outside the gate cannot ratchet.
  - guardian F4 (MINOR, accepted): the new test's env hygiene matches its S43
    siblings' pattern; adopting the RAII EnvGuard across the ddrm test family
    is noted follow-up work, not folded here.
  - Both seats verified the fmt commit (133c340) mechanically reproducible
    bit-for-bit, B2 (prepare-leg ratchet) rigorous, and B3 semantics preserved.
  - KNOWN_GAPS updated to the now-true wording (all seams named, the false
    first-cut claim recorded, the structural guard cited).

Gate: server lib 1270 (default) / 1276 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ps (Track C1)

The media/object viewer session stores were unbounded HashMaps: an entry —
holding MB-scale init bytes, sealed material, and an Arc to a live
KEY-AUTHORITY SUBPROCESS — lived until an explicit client close. Sustained
opens grew memory and process count without limit; an abandoned tab pinned a
child process forever. Worse (S47 guardian F2): /init, /cover, and the
manifests had NO expiry check, so an expired session's bytes were still
served (only segment/bytes/page reads failed closed).

New `api/session_bounds.rs` — ONE discipline both stores apply:
  - SWEEP: every admission and every lookup removes all EXPIRED entries.
  - CAP: at MAX_VIEWER_SESSIONS (256/store) evict soonest-to-expire live
    sessions (TTL order, not LRU — fixed-window sessions, no per-read
    bookkeeping, ungameable by touching your own sessions) with a warn.
  - DEFERRED-DROP CONTRACT (the council crux, both seats HIGH/MEDIUM): a
    session drop can reap a subprocess with ~1s grace; the sweep runs under
    the store Mutex, so dropping in place would stall EVERY viewer's lookup
    for N×1s on a mass sweep (worst case ~4 min with 255 hung authorities).
    The helpers RETURN removed values; call sites (put/get/remove, both
    stores) drop them after releasing the lock.
  - Fail-closed: an evicted session's next read is "no such session" — the
    viewer re-opens through the FULL authorization gate. Eviction can never
    grant access (red-team confirmed; in-flight reads finish on their own
    Arc clone).

Council fold (both seats SHIP-WITH-FIXES; the blocker was pre-folded):
  - F1 (HIGH): deferred off-lock drops — including the pre-existing
    remove_*_session single-drop.
  - G-F2 (P12): module docs now credit the lookup-sweep with closing the
    previously-unchecked /init//cover/manifest expiry (the change undersold
    itself).
  - RT-F2/G-F3: the process-global, principal-blind cap + the authorized-
    opener precondition + per-principal fairness as future work — documented.
  - RT-F6/G-F4: honest worst-case math on the cap (≈2.3 GB + ~512 procs/store
    fully adversarial, resting on MAX_CAPSULE_LINE) replaces "the host
    survives".
  - G-F5a: the wiring test now inserts the expired session LAST, so the
    LOOKUP sweep is provably the remover.
  - G-F5b: the object store's missing wiring test recorded as an honest gap
    (non-optional Arc<ObjectAuthorityProc> holds a real Child; wiring is
    byte-identical to the tested media store).
  - Clock/concurrency/off-by-one all verified consistent by the red team
    (sweep and route guards share the same predicate; no serve-after-sweep
    window; a backward clock step keeps MORE, never mass-sweeps).

Gate: server lib 1275 (default) / 1281 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…er contract (Track D1+D2)

The North Star breadth move: prove the market-provider seam is not DRM-shaped
by shipping a second chain-settled vertical behind it, and publish the
contract a third party could implement a provider from.

D1 — docs/SPEC-market-provider-v1.md: the versioned contract. The runtime owns
every money DECISION (mandate gate, meter, record-before-broadcast ledger,
receipts, reconciliation); a provider owns only the money REPORT (the
two-generals pay() classification + the compiler-required rail() tag + the
fail-safe confirmation reader). A conformance checklist maps each obligation
to its mechanical enforcement.

D2 — api/erc20_checkout.rs (`Erc20CheckoutProvider`): `runtime.pay
{payee: 0x…, amount}` becomes `transfer(payee, amount × ELASTOS_ERC20_SPEND_UNIT)`
on ONE operator-declared token:
  - Typed by construction (the S43 discipline): bad address / unit overflow /
    calldata / the wallet SIGN leg (incl. the chain PREPARE read) are
    NotCharged — provably nothing moved; the broadcast op and after are
    Indeterminate carrying `erc20:tx=<hash>;to=…;amount=…;tok=…`
    (delimiter-stripped per component). A broadcast-accepted checkout is NEVER
    charged until confirmed — Ok-at-broadcast does not exist.
  - `rail() = PaymentRail::Erc20` (new variant; snake_case "erc20";
    forward-compat posture per the S44 contract — a coordinated upgrade).
  - Mock settlement: dev-modes + the explicit mock-money opt-in. Live: wallet
    managed signing (dev-modes; a release build refuses NotCharged — the
    external-signature checkout flow is a stated follow-up, same posture as
    the DRM rail).

The chain-settled reconciler, generalized rail-STRICTLY:
  - `parse_chain_tx(rail, note)`: Drm parses only `drm:tx=`, Erc20 only
    `erc20:tx=` (a cross-rail note is a tx-less orphan — left Pending, warned,
    never polled); the pre-S44 `Unknown` legacy fallback stays DRM-ONLY (it
    never widens to new rails); Http never parses.
  - `is_drm_pending` → `is_chain_settled_pending` ({Drm, Erc20} = ours);
    ONE shared `confirm_chain_tx` (receipt + depth floor) both verticals use;
    `Erc20CheckoutProvider` impls the confirmer via it, so the in-runtime
    scheduler polls checkout pendings exactly like DRM buys.

Wiring — `ELASTOS_PAYMENT_RAIL=erc20` mirrors the DRM arm's fail-closed
discipline: ELASTOS_ERC20_TOKEN + ELASTOS_ERC20_SPEND_UNIT required (the cap
is a literal on-chain ceiling, never a silent 1-wei assumption); durable
meter/ledger required; mock mode refuses without ELASTOS_ALLOW_MOCK_PAYMENTS.

7 new ratchets: transfer-calldata words; junk payees refused pre-money;
unit-overflow refused pre-money; broadcast held-Indeterminate with a
parseable ref (e2e, dev-modes); Erc20 pendings reconciled + the S44 walls
hold for the new rail (hostile Http `erc20:tx=` never polled; Unknown legacy
stays DRM-only); parse_chain_tx rail-strict; the "erc20" serde round-trip.

Gate: server lib 1281 (default) / 1288 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift. Council review in flight; findings fold
as a follow-up commit.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…ot warn

Council S48 red-team returned SHIP-CLEAN (all five money invariants confirmed
by construction: no charge-at-broadcast, no refund-after-send, the cap is a
literal ceiling, cross-rail steering impossible, the payee cannot forge
calldata). Two LOW footguns folded:

  - LOW-2: ELASTOS_ERC20_MODE parse is now case-insensitive AND an
    unrecognized non-empty value REFUSES TO WIRE (fail-closed) rather than
    silently defaulting to Live — a typo'd mode must be visible, never a
    surprise settlement mode. Empty/absent still defaults Live (the stricter
    path).
  - LOW-1: a boot-time warn when mock mode wires — mock settlements produce
    tx hashes the LIVE confirmer can never find, so mock pendings hold their
    reservations until manually resolved (money-safe over-hold, stated).

INFO-1 (the wallet-capsule trust posture on the live sign leg) is inherited
from the DRM rail unchanged and already documented there.

Gate: check clean, fmt 0 drift, server lib default 1281 green.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…r + wiring ratchet

Council S48 guardian returned SHIP-WITH-FIXES (the money core verified clean —
every leg typed by construction, no Ok path exists, the S44 walls hold and are
re-ratcheted for the new rail). All seven findings folded:

  - F1 (HIGH, gate break): erc20_checkout's dev-modes-shadowed code left 5
    clippy warnings in the default build, breaking the declared `-D warnings`
    gate. Fixed with cfg-gated imports/helpers + a documented release-shape
    dead_code allow. And went further (the S46 gate-truth rule): the WHOLE
    workspace is now clippy-clean at 0 warnings — folded the pre-existing
    drift too (wasm.rs too_many_arguments documented allow; session_bounds
    deliberately-write-only field; parse_chain_tx lifetime elision;
    intent_executor + bench doc-list indentation; market_quote's unit error
    is now the named `ReadInFlight` type; carrier's 3 test env-lock guards
    held across await get a justified allow — the lint targets production
    deadlock risk, not test env serialization).
  - F2 (MEDIUM, P11+P12): the rail selector is now CLOSED-WORLD — an
    unrecognized non-empty ELASTOS_PAYMENT_RAIL refuses to wire (fail-closed)
    instead of silently falling through to the HTTP/mock arms; the SPEC §6
    "never degrades silently" sentence is now true of the selector itself.
  - F3 (MEDIUM, P12): `erc20_rail_obeys_the_wiring_discipline` — the missing
    wiring ratchet mirroring the DRM sibling: typo'd rail refuses; no token /
    no unit / malformed unit / unrecognized mode / mock-without-opt-in all
    refuse; fully-declared wires durable with the Erc20 tag + the confirmer.
    (Takes the documented two-lock order — the first run caught an env race.)
  - F4: SPEC row 4 reworded honestly (provider broadcast ratchet + the
    shared-spine e2e via DRM, not a uniform "e2e").
  - F5: the constructor's spend_unit clamp is now debug_assert'd + documented
    (never silent).
  - F6 (P12): wiring Live mode on a release build now warns at boot that
    every checkout refuses until the external-signature flow ships.
  - F7: SPEC §6 names the shared chain-config envs (the DDRM-era names; a
    rail-neutral rename is tracked); the scheduler's "not the DRM rail"
    doc/warn updated to "not a chain-settled rail".

Gate: server lib 1282 (default) / 1289 (dev-modes), bin 105, runtime 434,
common 96 — all green; fmt 0 drift; cargo clippy --workspace --all-targets:
ZERO warnings (the -D warnings gate passes truthfully).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…d by a conformance ratchet (Track D4)

docs/SPEC-mandate-v1.md: the byte-level formats behind Grant → Act → Prove,
written FROM the code so an independent party can implement verification with
no Flint code:
  - §1 the trust model, stated without inflation (self-asserted signer — an
    unpinned verification is NOT a trust decision; tamper-evident not
    tamper-proof; the two completeness bounds).
  - §2 the chained record + the exact hash preimage
    (domain ‖ seq_be8 ‖ prev_hash32 ‖ event_json) + sig over the 32 RAW hash
    bytes; the one canonicalization recipe (re-serialize the deserialized
    event).
  - §3 the receipt document; §4 the set-binding message byte layout (scope tag
    byte, BE length prefixes, record hashes as 64-char ASCII hex).
  - §5 the mandate events (INTERNALLY tagged via "type"; absent-when-unset
    rules that keep old chains verifying; rail_ref formats cross-referenced to
    SPEC-market-provider-v1).
  - §6 the verification algorithm step by step + every verdict bit + the
    fail-closed exit-code contract (0/1/3/4).
  - §7 the signed intent + its preimage (domain WITH trailing NUL,
    LITTLE-endian length prefixes — the endianness difference vs §2/§4 is
    called out), and what the runtime enforces before executing.
  - §8 versioning: frozen shapes, append-only optional fields, new tag for any
    break.

The conformance ratchet `audit::tests::the_wire_format_matches_spec_mandate_v1`
pins the schema tag, both domain strings, every serialized key of the
document/record/scope/events, the timestamp shape, and that an exported
receipt verifies AUTHENTIC under §6. Writing it caught two real spec errors
before review (the event enum is internally tagged, and SecureTimestamp is
{unix_secs, monotonic_seq} — not the shapes first drafted), which is exactly
the failure mode the ratchet exists to prevent.

Gate: runtime 435 (+1), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings. Council review in flight; findings
fold as a follow-up commit.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hets

Council: guardian SHIP-WITH-FIXES (3 HIGH P12 defects), red-team
SHIP-WITH-FIXES (the crypto core sound — 3 domains separated, scope-tag byte
defeats binding replay, no path lets a self-signed forgery reach exit 0 —
but the spec under-specified canonicalization and had two over-claims). All
folded, some in CODE:

  - guardian F1 (HIGH): §5 listed a phantom `capsule_id` on capability_revoke;
    the real event is exactly {type, timestamp, token_id, reason}. Fixed the
    spec AND added a byte-identity fixture for revoke (and grant) to the
    conformance ratchet — revoke was the one §5 event the ratchet hadn't pinned,
    which is exactly how the error slipped in.
  - guardian F2 (HIGH) / red-team domain check: §7 cited a ratchet that only
    pinned the timestamp segment. Added the known-answer ratchet
    `the_intent_preimage_digest_is_frozen` — the WHOLE preimage (trailing-NUL
    domain, field order, little-endian prefixes) as one fixed digest — and the
    spec now cites it truthfully.
  - guardian F3 (HIGH): event byte-ORDER was neither specified nor pinned. §5
    now gives byte-exact compact-JSON templates (type first, declared order,
    escaping rules) and the ratchet pins grant+revoke bytes.
  - red-team F3 (MEDIUM, CODE): the receipt verifier used non-strict ed25519
    while the intent path uses verify_strict — two conforming verifiers could
    disagree on a malleated signature. Switched both receipt sig checks to
    verify_strict; the spec's Primitives now mandates strict verification.
  - red-team F1/F2/F4/F5/F7 + guardian F4/F6/F9: canonicalization caveat on the
    "no Flint code" claim; the contiguous-end-truncation exception stated in §1;
    "a PRESENT set_binding MUST verify for both scopes" (the dangerous
    accept-a-tamper reading closed); "verify over the RECOMPUTED hash, never the
    claimed"; §7 qualified (agent-key binding only when bound; intent_id replay
    key vs the flint-<sig> payment key); grant-position-free + set_binding null
    accepted; lowercase-hex + length-not-the-separator domain notes.

Gate: runtime 436 (+1 KAT), server 1282/1289, bin 105, common 96 — all green;
fmt 0 drift; workspace clippy 0 warnings.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Add the bounded-offer negotiation affordance: an agent under a pay-mandate
makes an OFFER for a mandate-scoped asset and an injected Negotiator seller
answers accept/counter/reject, closing the shop loop (quote -> negotiate -> pay).

The provable property: the offer rides the signed input_hash as a canonical
positive integer of spend units, and an offer above the mandate's un-spent cap
(SpendMeter::remaining) is refused BEFORE it reaches the seller — an agent can
never propose to commit its operator beyond granted authority. Non-value-moving
by construction: it only reads the cap (no reserve, debit, or broadcast), so no
money moves; settlement stays runtime.pay. A read-authority probe (like
market_quote): it reconciles `read` and takes a read mandate on the pay
resource — the dispatch gate authorizes only the fixed Action enum, so a made-up
`negotiate` action could never be granted; the OFFER, not the action, makes it a
proposal. The receipt attests the bounded offer, not the counterparty's
disposition (which the runtime cannot verify); the seller's terms ride the
response's disclosure channel as ephemeral market data.

Production ListingNegotiator: a fixed-price DRM seller backed by the same
read-only quote spine, reusing the buy gate's EXACT spend-unit conversion +
pay-token guard (no second, divergent money-domain conversion). Wired only on
the DRM rail (unwired on HTTP/ERC-20, which have no listing to negotiate against).

Ratchet a_faithful_negotiate_returns_a_gate_legal_action_that_reconciles_matched
pins the returned action to a gate-authorizable, declaration-matching value so
the affordance can never regress to an action no token could authorize.

Also fixes a stray SecureTimestamp clone the clippy -D warnings gate flagged in
S49 test code.

Full gate green: server lib 1300, dev-modes ok, bin 105, runtime 436, common 96;
fmt clean; clippy -D warnings clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…n + read-only seam contract

Two-seat adversarial council: red-team SHIP-CLEAN (verified byte-identical
buy-gate parity, cap-before-seller, non-value-moving, no report forgery);
guardian SHIP-WITH-FIXES (3 MED + 2 LOW honesty/by-construction gaps). Both
independently corroborated the pre-council action fix already landed. All folded:

MED1 — the seller's rejection reason reaches the agent/logs but was unbounded
(and embedded unsanitized chain-sourced token/price). The executor now bounds
(<=200) and sanitizes (ascii-graphic + space) it, the same discipline the
Performed report is held to. Ratchet a_hostile_seller_reject_reason_is_bounded_and_sanitized.

MED2 — "reuses the buy gate's EXACT conversion" was by-comment, not by
construction (a second, hand-synced copy). Extracted the ONE
authorize_amount_against_listing (token guard + price parse + amount*spend_unit
+ cover boundary); both the DRM buy gate and the negotiate seller now call it,
so an accept corresponds exactly to what the buy will pass. Buy-gate messages
preserved (22 drm_marketplace tests green).

MED3 — the read classification was honest only for the wired seller. Added a
NORMATIVE contract to the Negotiator trait: implementations MUST be
observationally read-only; an outbound-negotiating seller must reclassify the
affordance action before being wired.

LOW4 — the ratchet pinned a hand-copied action set. Added FromStr for Action
(the one inverse of Display) and routed all three hand-rolled action matches
(dispatch gate + two provisioning handlers) through parse_action; the ratchet
now pins via FromStr. Round-trip test in token.rs.

LOW5 — softened the panic-path comments to name the real residual: a seller
that panics mid-read of the shared quote spine leaves a TTL-bounded (~30s)
cache slot; money state stays clean and it is not agent-triggerable.

Full gate green: server lib 1302, dev-modes 1309, bin 105, runtime 437,
common 96; fmt clean; clippy -D warnings clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…hanged (Track C2)

MEASURE-FIRST. Baseline (benches/audit_emit.rs, this box): memory-only emit
2.2 us/op (~446k ops/s); file-backed fsync-per-record ~950 us/op — a ~1.1k
emits/s durable ceiling at ~430x the CPU cost, with every concurrent emitter
serialized behind it. That fsync-under-the-chain-lock is the KNOWN_GAPS G8
blocker keeping ordinary grant/use events best-effort.

THE REWRITE (three measured cuts; the first two were rejected by their own
numbers): emit now appends in chain order under the chain lock (seq + sign +
ordered write + BufWriter flush), publishes written_seq, then WAITS until a
flusher's sync_all covers its seq — N concurrent emits share one fsync.
- Cut 1 anchored the tail-truncation head anchor under the flush lock: 1.5x.
- Cut 2 fsynced through the writer mutex: appenders (holding chain, waiting on
  writer) convoyed behind every ~1 ms fsync — batches collapsed to ~1 and
  8 threads measured SLOWER than serial (725 vs 1057 ops/s). Rejected.
- Final: the flusher fsyncs a CLONED fd (fsync commits the inode, not the fd),
  so appends proceed during the fsync; the anchor is guarded-monotone on its
  own mutex off the flush path. 8 threads: 3116 emits/s — 2.9x past the
  single-writer ceiling (pre-S51 they were pinned AT it). Single-threaded
  latency unchanged (one fsync each; nothing to coalesce).

THE CONTRACT IS UNCHANGED AND HARDENED: Ok(()) still means THIS record is
durably fsynced. A failed write/flush/fsync now POISONS the log — the failing
emit and every later one return Err (fail-closed) — which also retires the
pre-S51 latent hazard where retrying a failed seq could append a duplicate
seq behind half-landed bytes and corrupt the chain for verifiers. The head
anchor only ever records durable covers (never over-claims).

Ratchets: concurrent_emits_group_commit_and_the_chain_stays_perfect (8x25
threads -> contiguous, signed, verify_chain-clean, anchor at full count,
re-opens with_file_verified) and a_failed_durable_write_poisons_the_log_
fail_closed. Bench gains an 8-thread regime that verifies the chain after
timing, so its number counts correct commits only. KNOWN_GAPS G8/perf notes
updated: fail-closed grant/use is now a policy decision, not a blocked one.

Lanes green at commit: runtime lib 439 (all 50 audit), server lib 1302,
bin 105, common 96, fmt clean. dev-modes + clippy tail and the two-seat
council run next; their findings land in the fold commit.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…usher unwind guard

Two-seat adversarial council on the group-commit rewrite: both seats
SHIP-WITH-FIXES, and both independently CONFIRMED the core invariants sound
(Ok => durably fsynced; anchor never over-claims; flusher handoff liveness;
Release/Acquire cover pairing; cloned-fd fsync semantics). All findings folded:

F1 (guardian HIGH == red-team F2, the one real race): the poison gate was
check-then-append — an emit queued on the chain lock during another emit's
write failure could re-derive the FAILED seq and append behind its half-landed
bytes (duplicate-seq corruption; fail-closed in every consequence but the
chain bricked, and in the worst interleaving the racer's own record could be
acknowledged on a never-verifiable chain). Closed by construction: the
write-failure arm poisons while STILL HOLDING the chain lock, and emit
re-checks poison UNDER the chain lock (chain->flush nesting verified acyclic).
Ratchet: a_poisoned_log_refuses_before_assigning_a_seq_or_writing.

F2 (both seats MED): "restart re-opens from the durable prefix" was claimed,
not implemented — a crash-torn tail (provably never-acknowledged: an
acknowledged record's full line+\n was fsynced) made the verified open REFUSE
a healthy log, and S51's standing flushed-not-fsynced batch widened the
window. Closed: quarantine_torn_tail at open moves a non-newline-terminated
trailing fragment to <log>.torn-tail (preserved, never destroyed) and resumes
from the intact prefix; safe because it cannot remove an acknowledged record
and grants a tamperer nothing beyond the anchor floor they already had via a
clean line-boundary cut. Mid-file corruption still refuses (tamper). Ratchet:
a_torn_tail_is_quarantined_and_the_log_resumes_from_the_durable_prefix.

F3 (guardian MED / red-team LOW): a panic-poisoned flush mutex could strand
flushing=true (every later custody emit blocking forever — worse than
fail-closed). All flush-lock sites now recover via PoisonError::into_inner
(FlushState is single-statement-writes valid), a FlusherGuard clears+poisons
on unwind, and the loud tracing moved outside the lock.

F4 (guardian MED, resolved BY MEASUREMENT): the empty-anchor-after-crash brick
is closed by treating an EMPTY anchor as absent (skip the floor for that open,
loudly). The fold's first cut ALSO fsynced the anchor temp — and the bench
rejected it: a single-threaded emitter is its own flusher, so it doubled the
per-record cost (~950us -> ~1.7ms). Reverted with the honest rationale (the
<=20-byte single-sector payload is complete-or-empty after a crash, never
partial garbage; non-empty garbage stays fail-closed).

F5 (guardian LOW): anchored_seq now seeds from max(resumed head, ON-DISK
anchor) so an unverified reopen of a truncated log can never regress the
anchor and destroy truncation evidence. F6 (guardian LOW): measured-number
drift harmonized to the S51 decision run. RESIDUAL noted in KNOWN_GAPS:
dual-writer same-file (no flock) is pre-existing; advisory flock is the
tracked close.

Post-fold measurements (uncontended): single-writer ~874 us/op (~1.1k/s,
unchanged from pre-fold); 8-thread group commit 2947 emits/s (2.6x the
single-writer ceiling). Full gate green: runtime 441 (52 audit), server 1302,
dev-modes 1309, bin 105, common 96; fmt clean; clippy -D warnings clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…acket Flint scope

(a) AuditLog::with_file now holds an exclusive advisory flock on <log>.lock
for the log's lifetime — the SAME single-opener discipline the spend meter and
payment ledger hold (council S28 F4 lineage), closing the S51 red-team
dual-writer residual on the LAST unprotected durable custody store. Two live
instances on one file would keep independent chain heads, mint the same seqs,
and corrupt the on-disk chain (and under the S51 group commit one instance's
fsync covers the sibling's uncoordinated bytes); a second opener now fails
WouldBlock fail-closed. Taken BEFORE the torn-tail quarantine so racing
openers cannot both mutate the file. Ratchet:
a_second_live_opener_of_the_same_log_is_refused (refusal + verified-open
refusal + reopen-after-drop). The gateway's lazy audit-log constructor now
serializes construction (its benign both-construct race would otherwise
surface a spurious flock refusal); server_infra distinguishes the
already-open-elsewhere refusal from tamper in its boot error (council LOW2).

(b) The dev-only echo_stdout println moved OUTSIDE the chain lock (S51
red-team F4) — a blocked stdout can no longer serialize every emit — and now
echoes only records that actually committed.

(c) docs/AUDITOR_PACKET.md gains section 7: the Flint mandate/money plane as
the second external-audit scope (the four invariant families to attack, the
two wire-format specs, reproduction commands, and the honest note that the
internal two-seat council reduces engagement cost but does not replace
independent assurance). Deliberately NOT touched: live-buy env wiring
(cosmetic renames deferred until after the operator's real-transaction test).

Council (combined seat): SHIP-CLEAN — drop-order (lock releases after the
writer flushes), O_CLOEXEC fd non-inheritance, the no-real-dual-opener sweep
(CLI paths never open the live log file), double-checked OnceLock init, and
the packet's invariant claims all verified against source; both LOW honesty
notes folded (Unix-only qualifiers; the boot-error branch).

Full gate green: runtime 442 (53 audit), server 1302, dev-modes 1309, bin
105, common 96; fmt clean; clippy -D warnings clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
… opt-in

ELASTOS_AUDIT_FAIL_CLOSED_USE=1 makes EVERY ordinary capability use-record in
CapabilityManager::validate BLOCKING: an act whose signed use-record cannot be
durably written is DENIED (ValidationError::AuditWriteFailed) — the same
"no act without a durable record" discipline affordance-consent tokens have
always had (W2 step 9). This is the G8 flip the S51 group commit unblocked
(concurrent validates now share fsyncs, so the cost is ~1 fsync of latency,
not a throughput collapse); it ships as an OPT-IN so the default stays
byte-identical best-effort — no behavior change for existing deployments
ahead of the operator's live-transaction test.

- The switch parse is strictness-directional and pure-function-tested:
  unset/0/false => off, 1/true => on, GARBAGE => ON with a loud warn (a
  switch whose only job is to be stricter never silently weakens).
- Env read once per constructor (new / load_or_generate / with_key);
  with_fail_closed_use(bool) sets the policy explicitly for tests/embedders.
- KNOWN ORDERING BOUND documented (same as the affordance path always had):
  a use-limited token's count is consumed before the record lands, so a use
  burned on a failed record is spent without an act — fail-closed, never a
  free act.
- Ratchets: the_fail_closed_use_opt_in_denies_undurable_acts_and_the_default_does_not
  (both directions against one failing-log seam) +
  the_fail_closed_use_switch_never_weakens_on_garbage.
- KNOWN_GAPS G8 updated: the policy flip now EXISTS as an operator choice;
  the remaining default-ON decision is deliberately the operator's, not a
  code gap. Ordinary grants already have their fail-closed surface
  (grant_durable, S24).

Runtime lanes green at commit time (runtime 444 incl. the 2 new ratchets,
common 96, fmt clean); the server lanes + workspace clippy complete in the
in-flight gate before push.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
…rd rationale

Combined council seat: SHIP-WITH-FIXES, doc-only — code semantics verified
correct, including that refunding a use on AuditWriteFailed would be UNSAFE
by the codebase's own BUG-4 contract (an Err emit may still have landed;
refunding could leave a durable success=true record the counter no longer
accounts for — burning keeps counter >= chain, the conservative direction).

F1 (MED): the poison-composition consequence was the load-bearing operational
fact and was stated nowhere an operator reads. Now stated plainly in the
field doc and KNOWN_GAPS: with the flag ON, one failed append/fsync poisons
the durable log and EVERY act process-wide denies until an operator restart
re-verifies the durable prefix — the deliberate posture (acts halt when the
custody plane is dead), and DoS-shaped (disk-full = capability-plane outage);
retries during the outage still burn use-limited tokens' counts.

F2 (LOW): the deny's own success=false use-record deliberately STAYS
best-effort under the flag — a deny is not an act, so a lost failure-record
is observability loss, never an unrecorded act; reasoned at
audit_validation_failure and in KNOWN_GAPS.

F4 (LOW): the memory-only claim hedged honestly ("short of a panic-poisoned
chain lock") + new ratchet the_fail_closed_use_opt_in_is_harmless_on_a_
memory_only_log (no durable writer => the flag adds no availability risk).

F3/F5: no code change — ordering bound already documented (F1's sentence adds
the retry-burn corollary); env-read-in-constructor cleared against precedent
with the with_fail_closed_use escape hatch.

Full gate green: runtime 445 (3 S53 ratchets), server 1302, dev-modes 1309,
bin 105, common 96; fmt clean; clippy -D warnings clean on the folded tree.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BnpmuD7RtQ3NuTfRQJGrQb

# Conflicts:
#	elastos/crates/elastos-compute/src/providers/wasm.rs
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.

3 participants