Where the line actually is
8 replacement-critical gaps, rankedTwenty-nine missing invariants, but only eight things to build. The rest ride along — freshness tracking is meaningless without continuation, a deaf-bot startup check is meaningless without addressing.
No channel, and no serving process
relay is one poll cycle, one tenant, one thread, then exit — against devchannel, a semantics fixture. There is no Telegram adapter, no loop, no signal handling, and no markdown stripping. Worse: there is no addressing. run_relay turns every message in the matched channel into a billed model turn, and Disposition::NotAddressed is constructed nowhere in the tree.
39.1 · 39.2 · 39.3 · 39.5 · 19.2 · 19.3 · 38.2
No tenant registry, and none of its six refusals
Python refuses the whole registry on any of six uniqueness violations, each with a named production defect behind it. Rust has none — not even the single-tenant half: nothing compares the runtime credential to the record-store credential, and nothing rejects an operator token in a member slot. RouteState is complete, tested, and called by nothing.
2.1 · 2.2 · 2.3 · 2.6 — B-4, B-5, B-6, B-8
No conversation continuation
forge::build passes None for the thread pointer, hardcoded. Journal::thread_pointer has no caller outside its own tests. Continuation { Fresh, Chain } appears in DESIGN.md and does not exist in the tree. Every turn is a fresh thread with the whole book re-sent — which makes the product a stateless Q&A, not the group conversation it is sold as.
27.2 · 7.3 · 7.7 — C-4, D-9, D-14, D-16
Confinement can be proven, but not established
§L-8 records the apply path proven live once — fresh → DRIFTED, applied → VERIFIED, one tool re-enabled → DRIFTED. But the thing that applied it is examples/confine.rs, explicitly labelled dev scaffolding, which records nothing in the lifecycle store. And MemberConfinement is referenced nowhere in crates/forge, so lifecycle inspect --ironclaw reports member_confinement as Unverifiable(NotObserved) on every run. The measurement exists; the operation does not.
11.1 · 11.2 · 11.3 · 18.2 — A-5, §L-1 through §L-8
No lifecycle write path
declare, reach and activate exist, are well-typed and are reachable only from examples/seed.rs. Nothing can bring a tenant into existence or record that it did. The current manual tool — provision.sh — imports five Python modules, so it dies with the language.
15.2 · 15.4 — F-1, F-2
Residual authority has no Rust home
Deleting a member does not revoke its bearer — re-measured at this pin. Two unexpired sessions sit in the live ledger today, and if Python retires nothing reports them and nothing can measure their revocation. The §J table already names the type this wants: Revoked(ProbeEvidence).
34.1 · 34.2 · 35.1 · 36.1 — F-4 through F-11
The egress boundary lives only in the repo being retired
A-5's outer layer — internal-only networks, a gateway, an image-bound verification stamp derived from the running container rather than from a file — is the last thing between a confinement bypass and the open internet. Rust's only compose file is a disposable probe on an ordinary bridge network. The systemd hardening and the off-box backup go the same way.
12.1 · 12.2 · 38.1 · 38.3
The answer-quality claim is unearned
The persona bytes are byte-identical and the envelope framing is present, so this is likely to pass. But "likely" is not the standard this repo holds itself to, and its own README calls the eval corpus the bar for replacement. No grader has ever seen a Rust-produced answer.
6.4, evidence half — multi/eval/cases.py
Three open questions, answered
asked without assuming the answerShould Activated require positive confinement evidence?
What Python's activation actually guaranteed: a tenant becomes routable only after its own credentials measured own-org read, fail-closed on an unknown token, cross-org non-leakage, a completed model turn, and the staged entry loading through the real registry validator — and only after confine-member.sh re-read the live surface and exited non-zero on any survivor.
Rust covers the first four through SmokeEvidence, constructible only from a passing outcome. The fifth has no registry to validate against. Confinement is covered only as Stage::MemberConfined — and this crate's own doctrine says a stage is "a note about ourselves", not evidence about the world. The asymmetry is real: smoke is evidence, confinement is a claim.
But the honest answer to "can a service become routable in a state we consider unsafe?" is not yet, because nothing can make one routable at all. That is the missing writer, not a defect in Activated.
Not a standalone gap. Do not add a stage. Make reach(Stage::MemberConfined, …) take a &Confinement and accept only Verified — the same pattern SmokeEvidence and Revoked(ProbeEvidence) already prescribe. One parameter, and "confined without measurement" becomes unrepresentable.
Is "can prove" without "can establish" enough to replace?
No — and the gap sits in a narrower place than observes but cannot apply. The policy, the drift model and the achievable floor are all built and mutation-validated, and §L-8 records the apply path exercised live. What is missing is that the only thing which can apply it is a dev example that writes nothing down, and that no operator command can even read the result back.
This is not theoretical. §L-1, measured on a freshly minted member at this pin: 52 catalogued tools, 50 at always_allow, 2 at ask_each_time, agent.auto_approve_tools: true. Confinement is mandatory, not hardening — every tenant minted after Python retires would hold unapproved wildcard outbound HTTP while holding that tenant's book.
Replacement-critical, and small. Promote the example: ironworks lifecycle confine <slug> — authenticate as the member, read the catalogue, deny everything outside the policy authority-changing tools first (§L-3: sixteen of them, not two), set approval off, re-read, assess, and refuse to record the stage unless the verdict is Verified. One file, plus one signature change. Not a policy engine.
Does PASS make NotStarted look healthier than it is?
Reproduced, not reasoned — an instance that has provisioned nothing:
recorded reached preflight_passed org_registration absent — measured, nothing there tenant_data absent — measured, nothing there member absent — measured, nothing there member_confinement absent — measured, nothing there registry_entry absent — measured, nothing there verdict PASS EXIT=0
Since F-12 makes the exit code the machine contract, an operator loop over slugs reads this as healthy. Python already paid for this in the mirror: its Report.exit_code returns 3 when every row is SKIPPED, under the comment "A REPORT THAT EVALUATED NOTHING IS NOT A PASS."
But the verdict logic is right — there genuinely is no disagreement here, and turning this into Blocked would make BLOCKED mean two things.
Presentation quality, one line short of operationally misleading. Render the meaning with the verdict — PASS — no disagreement — and append the readiness fact when progress is not Activated: not routable: nothing reached. Keep exit 0 for inspect; let a future doctor own readiness. Do not touch the lattice.
The necessary-guarantee matrix
120 invariants across 43 areas| Ref | Invariant | Current Rust state | Class | RC |
|---|---|---|---|---|
| Organization identity, scope, isolation | ||||
| 1.1 | A credential resolves to exactly one org, server-side; no caller may assert one | ServiceInstance<Unverified>::verify → ScopeEvidence; Ask has no org field | Keep | RC |
| 1.2 | Unverified identity refuses to serve — a control fails closed | verify() returns Result; no offline mode in boot(); the_same_outage_before_verification_serves_nothing | Keep | RC |
| 1.3 | Tenancy is in the key, not in a WHERE clause | RecordScope; no unscoped Book method exists | Keep | RC |
| 1.4 | A store answer for another org is refused, not returned | BookFailure::ScopeDrift on every response; prove.sh check 3 | New | — |
| 3.1 | Every isolation check needs a positive control | a_degraded_catalog_still_builds_a_turn beside the refusals; chain check runs first | Keep | RC |
| Registry, uniqueness, routing | ||||
| 2.1 | The tenant set loads or refuses as a whole — never per file | None. No registry exists; boot builds one instance from CLI flags and two env vars | Missing | RC |
| 2.2 | Six uniqueness refusals: slug · channel · member=operator · member=account · shared member · shared account | None — not even the single-tenant half; nothing compares the two credentials or rejects an operator token | Missing | RC |
| 2.3 | Sealed-member status is probed, never inferred from the environment | None. Admin can reach the surface; nothing asserts the serving credential is refused there | Missing | RC |
| 2.4 | A browser User-Agent on the admin probe is a security control, not cosmetics | Absent — Admin sends none. Only bites once 2.3 exists | Missing | RC |
| 2.5 | No ambient tenant; no single-tenant fallback | True — but only because there is one tenant per process, which is the wrong reason | Keep | RC |
| 2.6 | Routing is three-valued; a collision never presents as a match | RouteState + route::resolve — complete, tested, called by nothing | Keep | RC |
| 2.7 | Guidance is bound to one slug and one service | Guidance::parse checks the marker's slug and service | Keep | RC |
| 2.8 | …and lives at exactly one canonical path | Absent — there is no registry directory for a path to be canonical to. Returns with 2.1 | Drop | — |
| 2.9 | A misconfigured address name is a startup failure, not runtime silence | No addressing at all, so no name to be missing | Missing | RC |
| Model-visible input, pins, capability | ||||
| 6.1 | Every IronWorks-controlled model-visible byte is constructible only through a verified instance | melt/ModelTurn private, TurnInput::new crate-private; 6 compile_fail doctests, 2 mutation-validated | Redesigned | RC |
| 6.2 | Order is the product: parts, guidance heading, safety tail last | Composition::new, melt; persona bytes diff-identical to Python | Keep | RC |
| 6.3 | The alloy is re-sent every turn — a once-only injection drifts | build_turn melts its own composition each call | Keep | RC |
| 6.4 | Record text is evidence, never instruction; never called "trusted" | envelope.rs provenance lines; vr-006 injection trap carried over; sanitizing it is forbidden in comment | Keep | RC |
| 6.5 | A speaker name cannot forge envelope structure, and never resolves records | sanitize_speaker, SPEAKER_MAX=64; no resolver exists to misuse it | Keep | RC |
| 6.6 | Operator commentary never reaches the model; length checked on stripped content | strip_html_comments after marker validation; MIN_CONTENT on the stripped body | Keep | RC |
| 6.7 | An empty book is declared, never implied | RecordsState::Empty → "ACCOUNT RECORDS STATUS: empty" | Keep | RC |
| 6.8 | Degraded record state carries a typed cause, never a caller's sentence | Degradation::{Unavailable, Partial{unreadable}} — adapters report a cause, core chooses the words | New | — |
| 6.9 | The org renders as its id, never its display name | Org comes from ScopeEvidence; there is no name to substitute | Keep | RC |
| 9.1 | No fallback literal for a pin — a pin is a privacy decision wearing a version number | No Default on ModelPin/ModelName; no env override; ModelSelection::from_pin is the only constructor | Redesigned | RC |
| 9.2 | Which model produced a stored answer is unverifiable after the fact | Measured ("reborn" on retrieve); recorded, and no check pretends otherwise | Keep | RC |
| 10.1 | An instance may only ever narrow its service's capabilities | CapabilityProfile::attenuate is a field-wise min; no widening operation; no Access::Write variant | Redesigned | RC |
| 10.2 | Runtime-supplied model inputs are named, never enumerated | ModelInput::RuntimeSupplied; the foreign share measured at 13,171 tokens | Keep | RC |
| Records: selection, freshness, degradation | ||||
| 7.1 | The record store degrades; conversation survives | fetch_records → Degraded(Unavailable); prove.sh check 4 | Keep | RC |
| 7.2 | A malformed row is dropped, not trusted, and the model is told the book is short | Partial{unreadable: n} rendered as a count; an_unusable_catalog_row_is_dropped_rather_than_failing_the_read | Keep | RC |
| 7.3 | Freshness is measured, not asked for — supplied maps id to the version shown | Absent. Rust fetches every catalog entry every turn. Moot today; required by gap 3 | Missing | — |
| 7.4 | An unknown supplied-version re-fetches once, never never-again | Absent — no supplied set to hold a version | Missing | — |
| 7.5 | No target means widen, not supply nothing | Whole-book prefetch subsumes the resolver: Rust always widens, so the failure mode cannot occur | Redesigned | — |
| 7.6 | A catalogued-but-unfetchable account gets a versioned negative result | Absent; every miss re-attempts each turn — which is Rust's normal mode, so the cost bound it protects does not yet exist | Missing | — |
| 7.7 | Data-starvation recovery: never chain to a thread that was never given records | Nothing chains, so nothing to recover from. Rides on gap 3 | Missing | — |
| 7.8 | Bookkeeping only after a confirmed-complete turn | Structurally true; finish() writes the pointer only on Outcome::Completed | Keep | RC |
| 7.9 | The seam supplies context; it does not reason — deterministic prefetch only | No second agent loop, no model-generated fetch, no scoring | Keep | RC |
| Turn durability, idempotency, recovery | ||||
| 24.1 | One store, one transaction: the response id and the pointer become durable together | the_response_id_and_the_pointer_commit_together; a_refused_commit_writes_neither_half | Keep | RC |
| 24.2 | Ambiguity is the default; the flag comes from evidence, never from having built a request | record_unknown before dispatch; an_accepted_turn_cannot_regress_to_unknown_or_unsent | Keep | RC |
| 25.1 | Never a new logical turn; an idempotent replay is permitted when byte identity is proven | RequestDigest, may_replay; the envelope is a pure function of Ask (ADR 0005) | Redesigned | RC |
| 25.2 | The window before the response id is durable is unrecoverable | Measured false at both pins — IronClaw's idempotency index owns the recovery. prove.sh check 6 replays and recovers | Superseded | RC |
| 25.3 | An idempotency-key collision is an alarm, not a retry | Cause::IdempotencyConflict → OperatorRequired with a NonEmptyReason | Keep | RC |
| 26.1 | Once a response id is durable, fetch; never regenerate | Recovery::Fetch; prove.sh check 7 proves a total create-count of 1 | Keep | RC |
| 26.2 | Poll-deadline exhaustion is not "unsent" | Cause::BudgetExhausted keeps the id and advises no retry | Keep | RC |
| 26.3 | A response id is not a capability | Not implemented, and need not be — the runtime returns 404 cross-tenant (measured) | Superseded | — |
| 26.4 | A thread pointer carries its tenant — upstream will not catch a cross-wire | ThreadPointer{instance, response}; TurnError::ForeignThread; ThreadScope gates both directions | Keep | RC |
| 27.1 | One wall-clock budget for the whole turn | Deadline threaded through create and poll; per-phase HTTP timeouts (ADR 0004) | Redesigned | RC |
| 27.2 | A rejected continuity pointer self-heals once, on a derived second key | Absent — nothing chains. Rides on gap 3 | Missing | — |
| Delivery, cursor, compaction | ||||
| 28.1 | DELIVERED and ACKED are different facts | Derived safe_cursor vs stored cursor_acked; finished_is_not_acknowledged | Keep | RC |
| 28.2 | Delivery evidence is three-valued and never overwritten downward | DeliveryState join-semilattice, joined from disk; the join laws tested exhaustively | Redesigned | RC |
| 28.3 | HTTP status is not the success signal; "could not read the reply" is not "the reply said no" | AttemptOutcome::{Complete, Refused, Ambiguous}; unread bodies map to Ambiguous | Keep | RC |
| 29.1 | A resend that may duplicate is an operator decision, never a retry | Repair::ResendMayDuplicate; ironworks redeliver is the only way past, and never runs a model | Keep | RC |
| 30.1 | Journal the whole batch before any worker starts | note_batch before the loop; a_journalled_batch_holds_the_cursor_before_any_of_it_is_worked_on | Keep | RC |
| 30.2 | The safe cursor is the lowest open update across every tenant | cursor::safe_cursor — derived, so crash boundary B6 is unrepresentable (ADR 0006) | Redesigned | RC |
| 30.3 | Cross-tenant coordination may order work, never acquire tenant authority | Openness (two values) through a one-method trait; dependencies_are_the_narrow_set asserts the crate's own manifest | New | — |
| 30.4 | An in-flight update is never re-dispositioned | AlreadyDispositioned; an_update_that_cost_a_turn_can_never_be_relabelled_as_noise | Keep | RC |
| 30.5 | Compaction is bounded by the acknowledged cursor and never forgets an update that cost a turn | Disposition::cost_a_turn() — a predicate, not an enumeration that was wrong twice | Redesigned | RC |
| 30.6 | Settled is not succeeded — a parked turn stops holding the cursor but keeps its alarm | Openness::Settled includes OperatorRequired; operator_required_settles_the_cursor_and_keeps_the_alarm | Keep | RC |
| 30.7 | The journal holds identifiers only | the_journal_holds_no_content greps the store's actual bytes | Keep | RC |
| Tool confinement and network containment | ||||
| 11.1 | Network authority is removed, not absent by default — a fresh member is fully open (§L-1) | No production writer. assess() is pure and observe reads; the only applier is examples/confine.rs, which records nothing | Missing | RC |
| 11.2 | Deny authority-changing tools first — sixteen of them, not two (§L-3) | AUTHORITY_CHANGING + Drift::AuthorityChangingReachable at severity 0; the ordering itself lives only in the example | Redesigned | RC |
| 11.3 | A confinement verdict must be non-vacuous — prove a real surface was read and acted on | Empty catalogues are refused, but a truncated 3-entry all-disabled catalogue still assesses as Verified. No test covers it | Missing | RC |
| 11.4 | "Every catalogued tool is disabled" is not "the model has no other executable authority" | confinement.rs states it exactly — custody-backed, not locked, catalogue completeness unproven | Keep | RC |
| 11.5 | The allowed set is derived from the product, not inherited (§L-2) | Empty against Python's 11 tools, derived from a request body with no tools field — but no Rust turn has yet been served by a fully-denied member | Redesigned | RC |
| 11.6 | Two operator-config tools cannot be disabled; approval-off is the containment | UNDISABLEABLE + the auto-approve clause; the_best_achievable_state_is_verified_and_approval_off_is_what_makes_it_so | Keep | RC |
| 12.1 | The runtime has no unrestricted outbound route, derived from the container and never from a file | None. The only compose file is a disposable probe on an ordinary bridge network | Missing | RC |
| 12.2 | No automatic fail-open: if the gateway dies, the product breaks and containment holds | Absent | Missing | RC |
| Credential custody and secret storage | ||||
| 13.1 | The model holds no credential authority; no credential on the runtime request | The request builder has no field for one; Credential is redacted-Debug with no Display | Redesigned | RC |
| 13.2 | A credential minted for one purpose cannot be used for another | CredentialPurpose::{RuntimeMember, RecordScope} inside CredentialRef | New | — |
| 13.3 | Nothing in the domain model holds a secret value | CredentialRef{purpose, handle} — a careless {:?} prints a keyring path, not a token | Redesigned | RC |
| 13.4 | Redaction of text we did not construct, such as an upstream error body | No redactor. Lower exposure (auth is a header), but the guarantee is absence-of-a-known-path, not construction | Redesigned | — |
| 13.5 | The operator-credential variable list is named once — one reader rejects, the other redacts | Neither reader exists. Returns with 2.2 | Drop | — |
| 14.1 | Journals hold stages and opaque ids only, enforced by type and not by a name blacklist | OpaqueId + typed methods; the_store_holds_no_credential_material reads the file's bytes | Redesigned | RC |
| 14.2 | Private state is written with its mode set before any content exists | LifecycleStore::open sets no permissions — and the store names every tenant, org, service, channel and member id | Missing | RC |
| Provisioning, compensation, activation | ||||
| 15.1 | Provisioning creates authority in several systems, and a partial state is a fact on disk | LifecycleStore, Stage, stage_history with created_id and adopted — model only | Keep | RC |
| 15.2 | Every check makeable before creating authority is made first | Stage::PreflightPassed exists; no checks are implemented | Missing | RC |
| 15.3 | Absence of local evidence is not evidence of absent external authority | plan() asks the inventory before running a stage; NextAction::AdoptExisting — Python fixed only the org case | Redesigned | RC |
| 15.4 | A tenant can be brought into existence at all | No writer. forge exposes only lifecycle inspect and lifecycle members | Missing | RC |
| 16.1 | Resume restarts at the first stage not reached; ordering is the contract | Stage: Ord by declaration; StageSkipped/StageWouldRegress refusals | Keep | RC |
| 17.1 | A failure compensates exactly what it created and reports the rest as residual | A flag with no executor — and with nothing creating, there is nothing to compensate | Drop | — |
| 17.2 | A failed run never destroys tenant data | reversible_by_compensation is false for TenantData; failed_provisioning_never_compensates_tenant_data | Keep | RC |
| 18.1 | A tenant becomes routable only after isolation and reachability were measured against real credentials | activate() takes SmokeEvidence, constructible only from a passing outcome; refuses before every stage | Keep | RC |
| 18.2 | …and only after its member's tool surface was certified confined | MemberConfined sits before Staged, but is a bare stage recordable with no evidence. See question A | Missing | RC |
| 18.3 | Activation is a file rename, and the compensator is disarmed in the same instant | No file, no trap — the window does not exist | Drop | — |
| Serving observation and the operator surface | ||||
| 19.1 | Routable is not serving; "nothing has asked" is not "no" | ServingObservation is a new and correct model with no producer — observe_serving has no caller | Missing | RC |
| 19.2 | Health is forward progress, not liveness — heartbeat against progress and the in-flight deadline | Absent. No daemon to be wedged yet | Missing | RC |
| 19.3 | A recorded pid separates "running here" from "ran here recently", released compare-and-clear | Absent | Missing | RC |
| 20.1 | Inspection separates intent, recorded progress and observed authority; disagreement is surfaced | inspect() — pure, four axes, Custody four-valued; prove-lifecycle.sh 5/5 against a real service | New | — |
| 20.2 | A reporting command cannot mutate what it reports on | open_readonly + require_writable; inspection_leaves_the_store_byte_identical; proof compares file bytes | Redesigned | RC |
| 31.1 | BLOCKED is not a PASS | Verdict four-valued, exit 0/2/3; an_inspection_with_no_reconciler_is_blocked_not_passing | Keep | RC |
| 31.2 | A waiver without a recorded reason is not a waiver | Verdict::AcceptedRisk(NonEmptyReason) — constructible only with one, and never inferred | Redesigned | RC |
| 31.3 | A report that evaluated nothing is not a PASS | Violated in one shape: a NotStarted instance with everything absent renders PASS, exit 0. See question C | Missing | RC |
| 31.4 | Ask by exit code, not by parsing a list | run_lifecycle_inspect returns the verdict's exit code — which is exactly why 31.3 matters | Keep | RC |
| 32.1 | One command answers "is this safe and ready", with CONFIG and LIVE never merged | Absent. Three dev proof scripts needing a disposable stack, plus two per-instance lifecycle commands | Missing | RC |
| Reconciliation | ||||
| 21.1 | A reconciler reports only what the system it asked is entitled to say; silence is not absence | AccountService reports one fact; unmentioned authorities become Unverifiable(NotObserved) | New | — |
| 21.2 | A refused credential is evidence about the credential, never about whether the authority exists | a_refused_credential_is_unverifiable_never_absent — proven live | New | — |
| 22.1 | Deleting a member is not revoking its token | Stated in ironclaw.rs; the fleet report prints it beside every dangling id | Keep | RC |
| 22.2 | The admin API cannot attribute a member to a tenant | IronclawMember::observe deliberately ignores the spec; all_member_ids reads past names and emails | Redesigned | RC |
| 23.1 | Fleet reconciliation finds authority no instance accounts for | unaccounted_members — both directions, NothingToReconcile as a variant, repeating-cursor refusal | Redesigned | RC |
| 23.2 | Exhaustive cursor traversal; a truncated listing is never zero unknowns | FleetInventory::Blocked; 102-of-102 traversal measured at 1.4.0 | Keep | RC |
| Deprovisioning and residual authority | ||||
| 33.1 | The deletion scope is authenticated from the credential; registry metadata can never redirect deletion | Absent — destructive, rare, and operator-mediated by design | Missing | — |
| 33.2 | The teardown choreography and its four exit codes | Drop the choreography, keep the remainder (34.1) | Drop | — |
| 33.3 | Route absence is positively established in memory as well as on disk | A process that re-reads its registry per cycle makes the whole probe unnecessary | Drop | — |
| 34.1 | A residual session is recorded with its expiry and no token material, and never stops being reported | Absent entirely — two unexpired entries live in the Python ledger today | Missing | RC |
| 34.2 | Validity is measured; meaning is declared; a re-record keeps its classification | AcceptedRisk(NonEmptyReason) is the right shape with no ledger to attach to | Missing | RC |
| 35.1 | REVOKED comes only from a probe that measured a rejection, behind a negative control | Absent. The §J table already names the type: Revoked(ProbeEvidence) | Missing | RC |
| 36.1 | An expiry is a fact about a token, not about the call that recorded it — first write wins | Absent | Missing | RC |
| 36.2 | Suspension does not remove authority from an issued session | Modelled as a deliberate absence, with the reasoning recorded in the crate docs | Redesigned | RC |
| Pins, upgrade, serving host | ||||
| 37.1 | A pin bump is a qualification pass, not an edit; never infer compatibility from source similarity | docs/UPGRADE.md with a measured contract table; probe-up.sh refuses an unlabelled image; the literal is asserted in two places | Redesigned | RC |
| 37.2 | A boot-path regression is a failing proof, not a silent diff | prove-boot.sh 4/4 asserts both capability sets and fresh + existing volume boot; DAC_OVERRIDE measured and rejected | New | — |
| 38.1 | The process holding every tenant's credential runs boxed in | Absent — no unit, no ProtectSystem, no BindPaths | Missing | RC |
| 38.2 | Something restarts it, and something alerts a human when it stops making progress | Absent | Missing | RC |
| 38.3 | Durable state is backed up off-box, encrypted, with a rehearsed restore | Absent — and the Rust journal path is in no backup set | Missing | RC |
| 38.4 | Restart is routine, so shutdown is graceful | Structurally free from the derived cursor — but no signal handling, because there is no loop | Redesigned | RC |
| Channel and concurrency | ||||
| 39.1 | Messages actually reach and leave the tenant | devchannel is a semantics fixture. There is no Telegram | Missing | RC |
| 39.2 | Only a summon causes a turn — mention or reply-to-bot, one shared implementation | run_relay bills a turn for every message in the channel; Disposition::NotAddressed is constructed nowhere | Missing | RC |
| 39.3 | Channel formatting is deterministic, not requested; strip rather than convert | Absent — prompt guidance was measured to cut emphasis 6→3 per reply, never to 0 | Missing | RC |
| 39.4 | Split on line boundaries; hard-split only a single over-long line | chunk() is correct and char-boundary safe — but there is nothing to strip before it | Keep | RC |
| 39.5 | A client-facing error is one sentence, the same whatever broke | Absent — failures print to operator stderr; nothing is said in the room | Missing | RC |
| 40.1 | Serial within a tenant, bounded-concurrent across tenants | The safety half is already a property of the derived cursor; four tenants do not need a pool | Drop | — |
| 40.2 | A shared connection with check_same_thread=False and a re-entrant lock | A Python storage necessity; Send/Sync and Mutex<Connection> replace it | Drop | — |
No rows match that combination.
Minimum closeout plan
smallest slice per gap · what not to buildTelegram channel and a serving loop
crates/core/src/route.rs, address.rs
crates/forge/src/main.rs
crates/lifecycle
- Invariant
- Only a summon causes a turn; formatting is deterministic; a client is told once when something breaks; health is forward progress, not liveness.
- Slice
- A
channel-telegramcrate behind the surfacechannel-httpalready implements.addressed()/summoned()as pure functions incorebesideroute::resolve.to_plain()beforechunk().relay --loopwith SIGTERM handling and a progress row feedingServingObservation. - Not
- A worker pool. A JSON-state migration path.
parse_mode. A second formatter anywhere in the tree. - Proof
prove-delivery.shre-run against Telegram for one real group; a unit table foraddressed()covering mention, reply-to-bot, case and the missing-username startup failure; a byte test that stripping precedes chunking and no marker spans a boundary.
Tenant registry and fail-closed validation
crates/core/src/route.rs
crates/reconcile/src/ironclaw.rs
- Invariant
- The tenant set loads or refuses as a whole; six uniqueness refusals; every tenant is a sealed member, probed against the runtime and never inferred from this process's environment.
- Slice
Registry::load(dir)returning validated specs or one error naming the file and the defect. Wireroute::resolveinto the relay. A startup probe asserting each member credential is refused on the admin surface, with the browser UA.- Not
- The
*.envshell-sourceable format.GUIDANCE_FILEoverrides.ORG_IDas anything but unverified metadata. Per-tenantMODEL.FACT_FIELDS/NAME_STOPWORDSuntil a tenant needs them. - Proof
- Six refusal tests, one per uniqueness row, each asserting the whole registry refuses; a positive control that a valid two-tenant set loads; a live probe proving a member token gets 401/403 where the operator token does not.
Continuation and the supplied set
crates/journal/src/lib.rs
crates/forge/src/main.rs
- Invariant
- A group's conversation continues; the chain breaks deliberately on alloy-fingerprint change and on a bounded turn or token count; an account is re-sent only when its catalog version moved;
ever_suppliedsurvives restart; a rejected pointer self-heals once on a derived second key. - Slice
Continuation { Fresh, Chain(ThreadPointer) }in core; readJournal::thread_pointerindrive(); asuppliedtable written inside the existingcommit_completedtransaction;Noneversion means re-fetch-once; the self-heal keysha256(base ‖ "\0fresh-thread").- Not
- Keyword-based target resolution — it failed in both directions and whole-book widening is already correct at this scale. The orphan attempt-count fallback until a book is large enough for it to bite. Anything from
handoff.py. - Proof
- An exhaustive table over (supplied version, catalog version) including both
Nonecases; a restart test thatever_suppliedsurvives; a live proof that a chained second turn re-sends no record, and that a fingerprint change breaks the chain.
Apply, verify and record confinement
crates/lifecycle/src/{lib,confinement}.rs
crates/forge/src/main.rs
dev/prove-confinement.sh (new)
- Invariant
- Every catalogued model tool outside the allowed set is Disabled at the recorded member's scope, except the two the runtime refuses, which sit at
ask_each_timewith auto-approval off — and no stage may claim confinement that nothing measured. - Slice
ironworks lifecycle confine <slug>: seven steps, authority-changing first, refusing to record the stage unlessVerified. Add the non-vacuity guard. WireMemberConfinementintoinspect --ironclaw. Reconcile the catalogue count first —UPGRADE.mdsays 53 entries at this pin and§L-1says 52, and a counted claim cannot rest on two numbers.- Not
- A generic policy engine. A non-empty allowed set. A
Lockedstate — §L-6 measured that 1.4.0 has none and thelockedfield is inert. A catalogue cache. - Proof
- §L-8 already has fresh → DRIFTED, applied → VERIFIED, re-enabled → DRIFTED. What it does not have: a member-authenticated re-read through the operator command, a truncated-catalogue fixture that must not verify, and one real turn served by a fully-denied member — which is what earns the empty allowed set.
A lifecycle write path — or an explicit decision not to have one
crates/lifecycle/src/lib.rs
dev/prove-lifecycle.sh
- Invariant
- A partial provisioning run is a fact on disk, and authority that already exists is adopted rather than duplicated.
- Slice
lifecycle declare | reach | activateas operator commands over the existing store, driven byplan(). The operator performs each external step by hand and records it;NextActionsays what is next andAdoptExistingstops them minting a second credential.- Not
- An automated provisioner. The compensation trap. The staging-directory rename.
data_seeded. The--dry-run/--resume/--statustrio —plan()subsumes resume andinspectsubsumes status. - Proof
- Extend
prove-lifecycle.shwith a walk that stops at each stage, crashes, and resumes to the rightNextAction; a test that activation is impossible without smoke and without confinement evidence.
Residual authority: ledger and probe
crates/reconcile/src/ironclaw.rs
crates/forge/src/main.rs
- Invariant
- A residual session is recorded with its expiry and no token material; the expiry is written once; REVOKED comes only from a probe that measured a rejection behind a negative control; a re-record keeps its classification; a gate is never made green by forgetting.
- Slice
- A
residualtable in the lifecycle store, first-write-wins on the expiry columns.Revoked(ProbeEvidence)constructible only by the probe. ReuseAcceptedRisk(NonEmptyReason)for the waiver. Migrate the two live entries. - Not
residual drop. The JSON file format.TEST_RESIDUALas a magic string — it is a waiver with a reason. The exit-2-while-outstanding convention beyond whatVerdict::exit_codealready gives.- Proof
- First-write-wins under repeated recording; a re-record keeps a classification;
Revokedunconstructible without a probe, as a compile_fail doctest; a live probe whose deliberately-accepted negative control yields BLOCKED, not REVOKED.
Carry the boundary and the host, without carrying Python
crates/forge (optional)
- Invariant
- The runtime has no unrestricted outbound route, verified against the running container and never inferred from a file; the process holding every tenant's credential runs boxed in, restarts, and its state is backed up off-box.
- Slice
- Copy the egress overlay, nginx config, connect-proxy, probe script, systemd unit and backup timer in as operator artifacts. Reimplement only the four-state container read in Rust if the doctor wants it, keeping the image-bound stamp. Add the Rust journal and lifecycle store to the backup set.
- Not
- The activate/rollback choreography in Rust — leave it shell. The
CONFIGUREDstate, which never existed. The wholeegress/proof/tree. - Proof
probe-egress.shpassing against the Rust stack's runtime container with the stamp bound to the pinned image id;prove-boot.shstill 4/4 under the overlay.
Earn the answer-quality claim
crates/corpus (if graders are ported)
- Invariant
- The analyst surfaces contradictions, refuses injected instructions, and does not present stale facts as current.
- Slice
- Run the existing graders against answers produced by
ironworks turnoverfixtures/bookandfixtures/eval.guidance.md, three runs per case. Port the graders — about 220 lines of regex with no seam imports — or shell out once and record the result. - Not
- A new grading harness. A new corpus.
adversarial.py. - Proof
- A recorded run with per-case verdicts and the REVIEW count, committed beside
docs/experiments/.
Three cheap fixes that are not individually critical: the verdict rendering from question C (one line); a 0600 mode on the lifecycle store before content exists; and the confinement non-vacuity guard, which rides inside gap 4.
Do not port
30 behaviours, with why — a guard against reintroductionFormats and files
- *.env registry filesThe shell-sourceable format itself caused quote-stripping and command-substitution defects. The invariant is "one tenant, one validated record", not "one shell file".
- provision-journal/<slug>.jsonSuperseded by a typed store with no k=v surface to smuggle a token through.
- deprovision/<slug>.json receiptsPlus the authenticated → account_revoked → complete trio: receipts for a choreography that should not be automated.
- residual-authority.jsonThe ledger survives as a table; the JSON file does not.
- bridge-threads.json + migrationA legacy source for a store Rust never wrote.
- .staging/ and the activation renameThe staged/live split existed because the seam globbed a directory. A registry that validates before it routes needs no unservable half-state.
- The journal field-name blacklistThe §J table already says the type replaces it — and a blacklist is exactly what a new field name walks past.
Not dropped: identities.json belongs to the Account Service, not to IronWorks.
Lifecycle states and abstractions
- activated as a stageIt is
ProvisionProgress::Activated{smoke}, which makes activation without a passing smoke unrepresentable rather than merely disallowed. - data_seeded as a claim about rowsRust records that the step settled, which is all Python's own else-branch ever meant.
- The nine-state update enumCollapsed into two orthogonal axes plus a disposition. DeliveryRetry and DeliveryReconcile are repairs a state implies, not states.
- ACKED as a stored per-update factDerived from one monotone value, which makes crash boundary B6 unrepresentable rather than defended.
- Enumerated compactable statesReplaced by a predicate, because the enumeration was wrong twice — most expensively when an operator alarm turned out to be age-compactable.
- Suspension semanticsMeasured not to remove authority from an issued session. Modelling it as revocation would be a lie in the type system.
- handoff.py266 lines, explicitly NOT WIRED, no product caller, and three unrelated meanings of the word in one repo.
- route_revocation.pyA systemd/ps/kill probe needed only because the registry is loaded once and held forever. Re-reading per cycle removes the module.
Workarounds superseded by measurement
- RECOVERY_BLOCKED for the pre-id windowMeasured recoverable at both pins by idempotent replay. Keep the state; drop the belief that this window enters it.
- CONFINE_VERIFY_ONLY and the re-POST-everything loopCost one request per tool per client and tripped the rate limiter on the mandatory post-bump re-run.
- The non-empty KEEP listInherited from a runtime that offered the model a tool surface regardless. Rust sends no tools field and needs none.
- confine-existing.shA one-time back-fill for tenants minted before confinement wiring existed.
- BROWSER_UA on /v1/responsesThe instance is UA-blind; the header exists for a hosted edge. Keep it on the admin probe, which is a real control.
- Host-migration one-offsmigrate-image, repoint-hostname, enable-device-link, sync-vm, backup-laptop-agency.
- The Python quality gategate-coverage.sh, run-quality.py, test_doc_refs.py, test_ci_contracts.py — cargo test, clippy and four proof scripts are the equivalent.
- Another product's materialdeploy/secretary, deploy/vidgen, and the SOUL / MULTI.template / MULTIMEDIATOR / operational-tail personas.
- adversarial.pyThe traps live in the fixtures the corpus already carries.
Old assumptions and stale concurrency
- BRIDGE_MAX_WORKERS and the thread poolThe property it protected is now a consequence of
min, not a feature. Four tenants do not need a pool. - check_same_thread=False and a re-entrant lockA Python storage necessity with no analogue.
- Import-time-capture defencesLazy path resolvers, module
__getattr__, "must precede the import" ceremony. Carry the rule forward, never the mechanisms. - redact.py threaded through every print siteReplaced by a credential type that holds no value. A narrow redactor for upstream error bodies may earn its place later; the threading must not come back.
- service.audience / model_policy / guidance:"required" / tool_policyThe internal composition is gone, guidance is non-optional, the model comes only from the pin, and tool policy is a type rather than a path to a shell script.
- SALES_GROUP_ID and any ambient single-tenant pathIt hand-built a tenant with no guidance-validated persona. Rust's current one-tenant-per-process shape is a gap, not this design.
What Rust gained
not parity requirements — replacement is judged on preserved guaranteesRecordScope is obtainable only from a verified instance, and no store method exists without one.Book::catalog, Book::recordmin.ADR 0006