Repository navigation
Make deployment placement a parameter, and stop a twin's retract from deleting production's binary - #7456
Conversation
… deleting production's binary deployment_owned_steps and deployment_service_config were NULLARY. Zero arguments, so they returned THE deployment rather than A deployment, and gunbc_roadmap_unit_name was a single value naming the only unit that could ever exist. The consequence is sharper than "srv1 is hardcoded": a second deployment on the same machine was not awkward, it was UNEXPRESSIBLE -- it would have collided on unit name, unit path, binary path, repo root and port 8080, every one of them a lone data row. That framing hid the axis. Host is not what varies; INSTANCE is, and host is one of its fields. Deploying twice on one machine and once on each of two machines are the same operation with different arguments, and a model where the first is impossible has picked the wrong parameter. Three separate "srv1-only" gaps were patched one at a time before anyone asked why they kept appearing (operator, 2026-07-30: "host should be independent of deploy target -- we should be able to just as easily deploy a twin process on srv1 as srv2"). This is a de-fork onto an authority that already existed. HostDashboardInstance carries instance_root, repo_root, serve_binary, dispatch_worktree_root, listen_host, listen_port and instance_id, and already runs two instances on one host (srv1_live 8080, srv1_lab 18081, disjoint roots, distinct tmux servers). live_deploy referenced it ZERO times while carrying a parallel srv1-shaped copy. Everything downstream of DeploymentSpec was already parameterized, which is why the defect survived: every consumer reads cleanly from its argument, and the single point where the argument was invented is three nullary calls in one file. THE HAZARD THE TWIN WITNESS FOUND. Writing the twin claim turned it red on one path. Both instances resolved serve_binary through the host layout to /opt/gunbc/bin/gunbc, and dashboard_instance_owned_paths -- the existing disjointness check -- covers origin, source, attempts and attempt-state but NOT serve_binary. ServeBinary is an Owned artifact and retract tears Owned members down, so retracting a lab would have deleted the binary production was executing. That could not fire while twins were unexpressible, so the hazard is created by the capability and had to close in the same change that grants it. Operator ruling: the processes are mutually exclusive, so each instance owns its binary, derived from its instance root. srv1-live declares its path directly rather than through the constructor and is untouched. Also fixed, found while parameterizing: retract was a second hand-written list of the same five steps reversed (now derived from the spec's own steps, so it is the reverse of what THIS spec applied); the emit sites reached past spec and art.path for globals that were already in scope; and the systemd unit was STAGED under one constant and INSTALLED from another, agreeing only by coincidence. Unit names are passed rather than derived, deliberately. Deriving from instance_id is the stronger model and is what dashboard_instance_service_session_name already does for tmux, but srv1-live runs as gunbc-roadmap.service and would be silently renamed: the apply installs the new unit while the old keeps serving, then retract orphans it. DISSOLVES ON a scheduled rename. Verified by execution: srv1's emitted apply (3708 bytes) and retract scripts are BYTE-IDENTICAL to main. The twin claim went red on the shared binary and green after the fix; twin_teardown_removes_nothing_production_owns states the property in retract's own terms because teardown is the destructive direction and an apply-side claim would not have caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…paths the plan verified but never created WIP checkpoint before merging the placement-axis branch, whose instance-owned serve_binary this work depends on.
…ve account isolation from it auth.json was going to be a SecretRef pointing at a String. That loses every fact which decides how the credential may be handled, and those facts are exactly what gets debugged later at the worst possible moment. Decomposed by observation, never memory: read from a real file on srv2 (codex-cli 0.145.0, 2026-07-30) by inspecting structure and never values -- key names, string lengths, JWT segment counts, and the decoded id_token claim set. MOST OF THE LEAVES ARE NOT OPENAI'S. iss/sub/aud/exp/iat/jti are RFC 7519 registered claims; email/email_verified/name/auth_time/acr/sid are OIDC Core 1.0; amr values are the RFC 8176 registry; three identifiers are RFC 9562 UUIDs; four timestamps are RFC 3339. Typing those inside a vendor module would have coined a local copy of exp free to disagree about seconds versus milliseconds. Four extdeps modules now own them, each citing its spec, and codex keeps only what is genuinely OpenAI's: the auth.json layout and the https://api.openai.com/auth private claim namespace. Three findings the decomposition produced: - amr mixes registered short names (pop, mfa) with vendor URNs (urn:openai:amr:passkey) in one array. A closed enum rejects the vendor values; a string list loses that half of them are specified. RFC 7519's colon rule makes the split decidable. - auth_time and iat are different instants. A silent refresh updates iat but not auth_time, so a max-age policy on iat is satisfied forever without the human ever proving presence again. - The file holds two mutually exclusive auth modes as one record, with the unused OPENAI_API_KEY nulled. Modeled as a coproduct: code that checks the key, finds null, and concludes unauthenticated is reading a perfectly good subscription login. THE MECHANISM QUESTION, ANSWERED BY MEASUREMENT. The default state root's auth.json carries last_refresh 07-27 04:22 with a matching mtime, while seventeen concurrent sessions bound to it have run past the 3600s access-token lifetime for three days without the file being rewritten. If refresh rotated the refresh token the CLI would have to persist it or lose access. So the file is written at LOGIN, not at refresh -- read-mostly, which is why concurrent binding is safe and why the write-contention hazard does not occur. Recorded as CodexPersistenceObservedLoginOnly, deliberately distinct from a proven CodexPersistsAtLoginOnly: three days of one host is an observation, not a contract. Sharing discipline is DERIVED from that persistence rather than declared beside it, and admit_account_binding has no arm for SharingDisciplineUnknown at any N including one -- so a deployment against an uncharacterized credential is unwritable rather than merely discouraged. Account isolation splits by owner: CODEX_HOME selecting an independent store is codex's capability and lives in extdeps; the ~/.codex@<label> suffix is ours and lives in gunbc. The convention is CITED, not invented -- three roots already exist on srv2 and minting a different scheme would have forked a live one. host_layout's codex home now derives from an account selection instead of a literal that silently encoded home, default-root name and convention together. Green by execution: derived path equals the observed authenticated directory (compared against a measurement, not against itself); default account resolves to ~/.codex where the seventeen sessions live. Also corrects a note of mine that went false within the hour: srv2's row said the state root "has never been created here", which stopped being true at login. Prose describing mutable host state goes stale silently, so the row now states intent only and liveness belongs to the probe that observes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ight requiring its own output Three pieces that close the loop a dispatch used to leave open. VERIFICATION HAD NO PRODUCER. roadmap_attempt_workflow_stage_note says it outright -- Environment, Workspace and Agent are grounded execution facts while Verification, Publication, Review and Goal Audit "remain desired obligations until independent producers ground them". So a dispatch ended at "agent turn completed" and a human went and looked, which is the loop the belt exists to close. Worse, the page renders a desired obligation identically to a grounded one, so an unimplemented stage reads as a stalled one. The case that proved it: srv2's first dispatch spent a full provider turn to report its node was "already implemented in ancestor commit f67a0c6; no new edits were needed". That is exactly what the node's declared validation decides -- deterministically, one command, no provider. verify_node runs the contract's own validations and returns one of five outcomes, because there are five remedies. Unbound means no validation is named and nothing failed; the fix is to author a contract, and collapsing it into a failure would report fifty imaginary regressions across the current board. Failed means it ran and answered no. Refused means it could not run at all -- an environment problem that is never evidence about the work, and merging it with Failed is the fabricated-verdict shape this repository keeps finding (a missing cargo reported as rustfmt drift, an idle socket as a bad request). An empty validations list is Unbound, not Passed: zero checks passing is not green. dispatch_disposition_for derives what happens next, so verify-first makes the agent CONDITIONAL: Passed yields ReadyForAcceptance and spends no provider, Failed is the only state warranting one, Refused dispatches nothing. This also inverts what green means -- a completed turn says a provider believes it is done; a pass says the declared control held when executed. Evidence, not testimony. Green by execution against a node whose answer was independently known: VerificationPassed{v2-emitter-producer-provenance, validations_run: 1} in one minute, agreeing with the agent's separate conclusion. validations_run is non-zero, so it is not the vacuous pass an empty contract would give. PREFLIGHT REQUIRED ITS OWN OUTPUT. dashboard_required_target_executables listed instance.serve_binary, which was fine while instances borrowed production's always-present binary. Making it instance-owned turned it into a deliverable of the very plan preflight gates -- observed on srv2 as a refusal naming a path only the blocked apply could create. The roster now holds preconditions only; dirname and chmod join because the install step runs them. Whether the binary landed is answered after apply by the readback, where an output belongs. CURSOR IS MODELED from its own --help and --list-models on srv2, not from documentation. Its auth is a bare value (--api-key / CURSOR_API_KEY), so unlike codex there is no interactive flow and no state root to mint. The structural finding: there is NO CURSOR_CONFIG_DIR, so cursor's account axis is the key rather than a directory, and provider_account's @Label suffix scheme is a codex convention that does not transfer. The credential is deliberately absent from shape_cursor_agent_argv -- --api-key would put a live secret in a process argument readable from /proc and captured by every ps and audit log. Fixes #7456's CI: isolated_instance_carries_the_layout_it_was_given asserted serve_binary comes from the layout, which the instance-owned change made untrue. The claim caught a real behaviour shift; it now asserts instance-derivation and still proves the layout supplies the two fields it genuinely does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…step that unblocks it
Review findings 1 and 3 (2026-07-30).
THE TWIN WITNESS WAS COVERAGE BY ILLUSION. It compared five declared artifact
paths and pinned count == 5, so its proof was bounded by the symbolic footprint
while three destructive effects lived outside it:
- the systemd unit hardcoded --function roadmap_serve_handle, which binds
srv1_live_dashboard_instance(). A deployed twin would listen on the lab
port, work from the lab tree, own the lab unit -- and then IDENTIFY AND
OPERATE AS PRODUCTION. Every path check passes; the process simply serves
the wrong dashboard.
- gunbc-tree-sync.service and /etc/gunbc-tree-sync.env were host-wide
literals. A twin apply overwrote both with its own tree destination and
restarted the shared unit, pointing production's sync at the lab's tree; a
twin retract deleted both.
- teardown ran `tailscale serve reset`, which resets the node's ENTIRE serve
config, so retracting a lab removed production's routing.
None of these is a DeploymentArtifactStep path. I let the witness's scope define
the risk: it caught the shared binary, so the binary became the problem and I
stopped looking.
serve_function now rides DeploymentServiceConfig, so the emitter has no literal
to fall back to. Tree-sync unit and env ride DeploymentIdentity beside unit_name
-- derived for new instances, srv1-live's running names preserved, because
deriving would rename a live unit as a side effect. serve_binary joins
dashboard_instance_owned_paths, which is why the old claim needed a
hand-written binary-inequality conjunct: a check that needs a supplement to
cover the thing it exists to cover is not covering it.
Tailscale teardown REFUSES. `tailscale serve --help` on this host lists exactly
serve <target>, serve status and serve reset -- there is no per-port removal.
A global reset issued to remove one mapping is the absorbing fallback stated
exactly: the precise operation is unavailable, so the superset runs, nothing is
"missed", and another deployment's routing is destroyed while retract reports
success. Dissolves on scoped removal upstream, or on the deployment learning it
owns the only mapping.
The replacement claims read the EMITTED SCRIPTS, and absence is the load-bearing
direction -- containing your own names is compatible with also containing
someone else's. Green: emitted_twin_scripts_touch_none_of_productions_effects,
emitted_twin_retract_removes_nothing_of_productions, and the original path-list
claim all true.
A FRESH HOST COULD NOT REACH THE STEP THAT UNBLOCKS IT. Preflight ran in full
before any plan operation and refuses on provider-auth -- but the op that
creates the state root a login writes into IS a plan operation. So a new
instance got: refuse, directory never created, `codex login` exits "that path
does not exist", hand-create it. srv2 never hit this because the login happened
before the ensure op existed, which is the masking condition: invisible on every
host provisioned by hand first.
DashboardApplyPreflightBootstrapPending is the third outcome the two-valued
result could not express -- "cannot proceed" and "can proceed up to the point a
human is required" have different next actions. The apply now runs the
credential-free prefix (directories, state root, serve binary; none reads or
writes a secret) and then escalates, carrying the bootstrap receipts so a reader
sees which prerequisites were satisfied before the stop. Both wet entries treat
it as failure: the deployment did not converge and must not report that it did.
The remediation now projects shape_codex_device_login_env_args_for_program
instead of hand-concatenating `CODEX_HOME=... codex login` -- the plain browser
flow this same PR proves cannot complete on a headless host, spelled a second
time beside the shaper built to prevent it. The mkdir is gone: creating the
state root is the deployment's job now that the apply reaches it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review finding 3b (2026-07-30). dashboard_target_path_present reduced its probe to .success, so an SSH transport refusal, an unreachable host, or a missing test binary all arrived as "the path is absent" -- and the same collapse fed provider_runtime_ready and provider_authenticated, so a broken transport reported as an unauthenticated provider. The SSH arm already HAD the distinction: typed_argv_probe_over_ssh returns TypedArgvExecRefused carrying a reason, discarded when converting to the two-state DashboardExecResult. The information existed; the conversion threw it away. dashboard_exec_refused's own signature -- empty stdout, populated stderr -- is what makes it recoverable. This is worse than a mislabel because the wrong label is ACTIONABLE. An operator told the state root is absent creates it; the truth was that nothing could be observed at all. Same class as a missing cargo reported as rustfmt drift and an idle socket reported as a malformed request, but with a remedy attached. Refusals ride ALONGSIDE the fields rather than converting each Bool to a tri-state. Ten probes feed one DashboardInstanceObserved, and a transport failure does not invalidate one field -- it invalidates the whole observation. Per-field tri-states would let a caller keep trusting the nine that happened to answer, which is exactly the reasoning that makes a partial observation look complete. dashboard_instance_reconcile consults observation_refusals FIRST, before the ownership law and before any Bool, because every branch below reasons from fields that are false under a refused probe just as surely as under genuine absence. It refuses rather than planning the superset: "create everything, it is safe if it already exists" is the absorbing fallback wearing a justification. Discriminating claim, green: two observations with IDENTICAL Bools -- every presence field false -- differing only in the refusal channel. The refused one must refuse; the genuinely-absent one must still derive a plan. Asserting either alone would pass in a degenerate world; holding the Bools fixed and varying only the channel isolates the fact under test, and it fails the moment reconcile reads a field before the channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… provider-state-ensure-and-readback
The CI red was stale generated Rust, not the wet-wrapper return type. std.types gained RenderedTerminalText -- the brand seaming terminal rendering to the codex prompt parser -- and stage0 was never regenerated, so the drift gate named that single file. regen_stage0 wrote 108 files; two changed. Note the first attempt exited 1 with 'spawn cargo fmt: No such file or directory' -- the third missing-toolchain failure tonight to surface as something other than a missing toolchain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… cursor auth participate in construction Item 5 — the RFC/OIDC types encoded one observed token as the standard. RFC 7519 registered claims are all optional (they were all required); aud keeps its string-or-array wire union (it was normalized to a list); NumericDate admits non-integer values (it was Int-only) and its derived lifetime is a std.measure.Second, refusing fractional and inverted pairs rather than rounding; StringOrURI now ENFORCES colon-implies-URI through an RFC 3986 scheme check instead of branding the rule; OIDC's required core moves to the ID Token profile where the obligation actually lives, and auth_time/acr/amr/sid/azp/nonce/email/name become optional. The AMR colon split no longer infers 'private' from URI shape. What this provider was observed to carry is recorded separately as CodexObservedProfile. Rfc3339Timestamp's same-offset sort claim was false with fractional seconds and is corrected. Item 4 — admit_account_binding returned a value nobody consumed while instance construction wrote DashboardExistingProviderState directly, so the wall was vocabulary. The admitted binding is now a required field, so a provider state cannot exist without one; the single-session path is total over evidenced disciplines so the four fleet declarations need no refusal arm. concurrent_sessions is no longer an unbounded Int (zero and negative refuse at the decoder). CodexPersistenceObservedLoginOnly stays evidence-scoped — ConcurrentBindingObservedUpTo carrying the observed ceiling — instead of promoting to unbounded ConcurrentBindingSafe. CursorInvocation.auth is now read: the shape is program + argv + environment, the two arms produce observably different processes, and the automation constructor can only materialize into CURSOR_API_KEY. Also review 45153: dashboard_provider_readiness collapsed refused probes into ProviderStateRootAbsent / ProviderRuntimeUnavailable / ProviderUnauthenticated, so an unreachable host produced actionable but fabricated remediation. Adds ProviderReadinessRefused, carrying the stage and reason and deliberately no remediation, consuming the three-valued observation this module already had. Three new witnesses, all green by execution; the three modules they cover previously had no consumer at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…verify roadmap_verify was a useful verifier library wearing the Verification producer's name, wrong in two ways. Wrong host: verify_node called belt_exec — a local WitnessBin.Run — and never read instance.provision_transport. srv1_lab is provisioned over ssh to srv1, so selecting it changed the paths the command was built from and not the machine that ran it: verification of an srv1 instance executed on srv2, against paths absent there, failing in a way that reads as incomplete work rather than as a refusal. Wrong subject: it verified instance.repo_root, the base clone — the one tree an attempt is guaranteed not to have touched. No attempt identity, no revision, no receipt, nothing that could transition into Publication. Split into two operations, because there were always two questions. verify_base_before_dispatch asks whether the obligation already holds, for which the base tree is the right subject and whose answer decides whether spending a provider is warranted. verify_attempt takes attempt key, worktree and revision, executes through the instance transport, and returns a receipt binding validation, realized command, host, worktree, revision, exit and evidence. Exit is a coproduct: a process that never ran has no exit code and zero is the wrong answer for it. Receipt persistence follows the same transport and REFUSES for ssh instances rather than writing an srv1 attempt's evidence to srv2's disk — no content-carrying transport is bound here yet, and that is named with its dissolution trigger rather than papered over. Both wet entries now return ProcessExit, as a func must. Refused exits FAILURE alongside failed and unbound: exiting success on a check that could not run is the absorbing fallback in its purest form. The binding law is now one authority. contract_closure / contract_has_closing_validation live in roadmap_execution_contract and are consumed by the roadmap projection, the verifier and dispatch admission, which previously decided it three times and disagreed — any ExecutionContract counted as bound in the projection while an empty validation list counted as unbound in the verifier. Empty lists are constructible, so a node bound to one left the acceptance frontier while staying unverifiable and undispatchable. Three-valued, so dispatch_environment keeps its two located diagnostics. No behaviour change on today's corpus: all four bound nodes emit exactly one validation. Witness green by execution, including the host-routing claim that goes red against the previous behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both findings from review 45153 are fixed, and the earlier unit-modeling finding from review 45056 with them. Refused probes collapsing into fabricated negatives ( The module already had the vocabulary it was bypassing: Derived duration on a bare scalar — fixed, and the finding also applied to the rewrite I had in flight. The RFC wire-level All three are covered by witnesses green by execution, including a control asserting an unobserved exit never serializes as |
…error (review 45173) regen_stage0 restores the caret parse smoke test block to compiler_tests.rs, which the branch had deleted — that is the regen_verify_gate_passes red on this branch. Run after merging main, with binaries rebuilt post-merge so the fixed point is computed by a compiler that agrees with the tree. review 45173, confirmed by execution: bash -n rejected the committed retract script. live_deploy_tailscale_teardown_refused_poison contains an apostrophe (deployment's routing) and tailscale_teardown_refusal_command wrapped it in single quotes by concatenation. POSIX single-quoted strings cannot contain a single quote, so the shell closed at the apostrophe, reopened at the next quote, and hit EOF unterminated. Under set -euo pipefail the parse error aborted the script at that line, so every teardown step after it — systemd disable and removal, daemon-reload, worktree and binary removal, tree-sync teardown — never ran, and neither did the refusal message the arm exists to print. The arm designed to stop the line dropped the rest of the line instead. Fixed by consuming extdeps.exec.command.shell_quote, the shared POSIX authority tmux_staged_provider_start_note already instructs callers to use for exactly this, rather than hand-spelling quotes at the call site. bash -n now passes on both emitted scripts. The golden gate could not catch this: it compares emission against committed bytes and both sides were equally unparseable. Byte-equality and target-language validity are different oracle families. Adds the second one as two claims — the emitted refusal carries the close-escape-reopen sequence and never the raw apostrophe, and the shared quoter is exercised on the character that broke the call site. Quote PARITY is deliberately not the discriminator: it is odd in both the broken and the fixed emission, so counting quotes proves nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
review 45173 is correct, confirmed by execution before fixing — The bug, as diagnosed. The fix consumes the shared authority rather than escaping in place. On the witness. I did not add a
Worth recording why quote parity is not the discriminator, since it is the obvious first idea: the count is odd in both the broken and the fixed emission ( On why the golden gate missed it — Also in this push: |
…ose the preflight match review 45016, third finding, INDEPENDENT of the quoting bug and not fixed by fixing it: retract order is the reverse of apply order and the tailscale mapping is applied last, so its refusal — which ends in `false` — was emitted FIRST. Under set -euo pipefail with FailFast that aborted the pipeline before the systemd unit, worktree root, serve binary, tree-sync files and repo tree were removed. Every retract, production and twin, tore down nothing. Proven by execution: `set -euo pipefail; echo x >&2; false; echo SIBLING` never prints SIBLING and exits 1. A syntactically valid `false` aborts a fail-fast pipeline exactly as well as a parse error does. Two unrelated causes, one silent outcome, one green golden gate — which is the argument for the second oracle family. The ordering is NOT hand-edited: deployment_owned_steps_retract_order derives retract as the reverse of what this spec applied, and re-transcribing a list would reinstate the two-places-one-fact defect that derivation removed. Instead the effects are PARTITIONED — teardowns that can act, in their derived order, then teardowns that refuse — so a refusal cannot precede a real teardown by construction. Refusing last stays fail-closed: the refusal still ends in `false`, and it now fires AFTER the removable artifacts are gone and BEFORE the verdict=converged receipt. So retract removes everything it legitimately can, never widens to the node-global tailscale reset, exits nonzero, and never prints a convergence receipt while the mapping is live. Dropping the `false` would print that receipt — the fabricated-success arm §5 forbids. Golden regenerated (the gap review 45192 caught: the emitter was fixed and the committed .sh was not). bash -n passes; the refusal now sits after every teardown and before the receipt, asserted by witness_retract_refusal_follows_every_teardown over the emitted text — presence-only claims pass in both the broken and fixed emission. Also closes 4 non-exhaustive matches on DashboardApplyPreflightResult in the apply witness test, which the branch left open when DashboardApplyPreflightBootstrapPending was added. compile --target dag is now 0 blocking errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Two more findings addressed in e2d526a. review 45192 — the golden wasn't regenerated. Correct, and caught me mid-change: the emitter partition had landed and review 45016 third finding — the refusal aborted its siblings. This one is independent of the quoting bug and was not fixed by fixing it, which is worth stating plainly: a syntactically valid I took the ordering option rather than dropping the The ordering is not hand-edited. Witness: Also in this push: 4 non-exhaustive matches on On the two duration findings in review 45016 — both were already fixed in the previous push ( |
…led (review 45197) serve_binary became instance-derived in this PR; the step that CREATES its containing directory kept production's literal, 'sudo -n install -d -m 0755 /opt/gunbc/bin'. install does not create missing parents, so a twin apply reached the copy with no <instance-root>/bin to copy into and failed on what reads as a permissions problem — undercutting the PR's own claim that twin deployments are expressible and safe to run. Same half-parameterization the placement de-fork exists to remove, surviving one level down: the value moved to the argument, the directory that has to exist for the value to be writable stayed behind. Invisible because production's parent and the literal agree — the condition that hid every srv1-only gap in this lane until a second instance existed. The parent is derived FROM the path being installed rather than declared beside it, so there is no second literal to drift. Production is byte identical (the parent of /opt/gunbc/bin/gunbc is /opt/gunbc/bin), which the committed golden confirms without regeneration. Mode and ownership are deliberately unchanged: production's bin directory is root-owned, and switching to install_d_owned_command would silently re-own a live directory to satisfy a twin. Witness asserts both directions — the twin creates its own bin directory AND production's literal is absent from the twin script. The positive half alone passes even if the emission still creates production's too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
review 45197 confirmed and fixed in 25db066 — thank you, this was the sharpest finding on the PR.
The parent is now derived from the path being installed, so there is no second literal to drift. Production is byte-identical ( On the two non-blocking notes in review 45204: The
What your suggestion does buy, which the exhaustiveness check does not, is forcing the arm to be a considered statement rather than a copied |
…sal to its member review 45207 — two retract witnesses still required `tailscale serve reset`, which the emission now refuses. Confirmed by execution: both FAILed. The assertions invert from require to forbid — a claim named owned_only was demanding the one command that is definitionally not owned-only. Also collapses the codex-account fork that decomposition introduced: host_layout named the label a second time beside provider_account's, with no consumer, while both path rows inlined the literal anyway. Deleted in favour of the authority. review 45213 — dashboard_target_path_present probed and returned .success, never constructing an observation, and two preflight consumers read it bare: a refused transport reported a required executable as missing and a bootstrap parent as not writable, each with actionable remediation for a condition nobody observed. The same fabricated-verdict shape as 45153, in the fix for 45153. The predicate is deleted; the executable roster is a survey that refuses on the first unobservable path, and bootstrap gains DashboardBootstrapUnobserved, a third verdict carrying no instruction. The note that shipped with the first fix argued the leak was safe because a refusal channel existed elsewhere — a discipline nothing enforces is not a wall, and the two callers that mattered did not consult it. review 45217 — deploy_teardown_refuses hand-walked MemberEffect, DeploymentStep and OwnedArtifactKind in one Bool. Whether a member can be torn down is a fact about the member, so it moves beside deployment_step_ownership as a typed disposition, per-layer and total. The two refusals are now distinguishable: an ensured package is outside ownership, the serve mapping is owned but has only a node-global removal verb. Emission byte-identical; both goldens still match. Also corrects two count-pinned witnesses the branch's own changes had made stale (required executables 15->16, plan rows 14->16) — the serve binary left the required roster because apply now installs it, which the claim asserts directly rather than only in the count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three confirmed and fixed in review 45207, finding 1 — stale retract witnesses. Confirmed: ran both, both review 45207, finding 2 — the codex-account fork. Confirmed, and it was wider than reported. review 45213 — the observation collapse. Confirmed, and the root was worse than the The predicate is deleted. You were also right about the note. The version that shipped argued the collapse was safe because the refusal rode on a channel callers could consult. A discipline nothing enforces is not a wall, and the two callers that mattered did not consult it — the leak reappeared one layer below the fix that introduced it. What survives is one projection named Two new discriminating claims, both PASS: a refused writability probe must not land in review 45217 — the hand-walked predicate. Confirmed. Whether a member can be torn down is a fact about the member, so it moved beside The lift also recovers something the Emission is byte-identical; both committed goldens still match by execution. One thing none of the reviews raised, found while verifying. Two count-pinned claims in the apply witness had gone stale against the branch's own changes — Gate-equivalent compile is clean (0 blocking). Full local floor re-running now — I killed the previous run because these edits landed mid-flight and would have made its results inconsistent. |
|
Thanks — this is the review that found the thing three automated passes walked past. Findings 1, 3 and 4 fixed in Finding 1 — the lane with no consumers. Confirmed exactly: I wired it rather than splitting, because the brand's claim was one honest function away from being true. The two fixtures turned out to be better than I expected: same prompt, captured two ways on the same day, carrying different one-time codes. That makes them a discriminating pair rather than two samples, so the witness pins each code to its own fixture and a witness silently reading the wrong file cannot pass. Three claims, all PASS: the plain path yields the pasteable code, the escape-retaining path refuses to render, and — the RED control — the escaped rendering genuinely would produce an unpasteable code. Without that third one the pair would pass equally well if the escapes were harmless and the brand pointless. Finding 3 — count transcriptions. Confirmed, and there were four, not two: Deleting them is right for one and wrong for three, which is worth being precise about. They became relations between live rosters instead — production non-empty, twin mirrors it one-for-one. Both survive #7478's sixth member, and the mirror is strictly stronger than the literal pair: it also catches a twin that gained or lost a member production did not. Finding 4 — Finding 2 — not fixed, trigger written down as you suggested, in Finding 5 — scope. Fair, and I'd have preferred the split. I'm not re-cutting the PR at this point unless you want it: the auth lane is now consumed and witnessed rather than inert, which removes the substance of the objection, and the mechanical risk of extracting ~1,600 lines across ten files exceeds what the split would buy at this stage. Your call — say the word and I'll do it. On #7478 ordering — agreed, and the count fix above is what makes it safe: those three assertions no longer red when a sixth member lands, so the two can go in either order without a renumbering pass. Two things I'd flag back. First, Second, Gate-equivalent compile clean (0 blocking). Full floor running on the settled tree now. |
…late the route count to its roster The extdeps citation gate (cli_run.rs:31022) keys on the exact declaration name extdeps_external_authority_anchor. Five modules new in this PR carried real cited authorities under per-module names — jwt_external_authority, oidc_core_external_authority, rfc3339_external_authority, codex_auth_external_authority_anchor and cursor_cli_external_authority — so every one read as uncited to ExtdepsExternalAuthorityGate. The citations were there; the names were the defect, and a per-module name for one concept is exactly the nickname a canonical name exists to prevent. oidc keeps its RFC 8176 row as a second citation. witness_live_table_builds_ready_routes pinned count(t.routes) == 16 while this PR's /provider.json route made it 17 — a third instance of the live-state count-transcription class. Replaced with a relation to roadmap_serve_route_specs(): the build drops nothing. Stronger than any literal, because it catches a silently dropped route, which is the hazard serve_page_response_note in the same module names. Both found by the full local floor; the first also reproduced in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-axis # Conflicts: # dag/gunbc/host_layout.dag # dag/gunbc/live_deploy/emit.dag # dag/gunbc/live_deploy/spec.dag # dag/test/claim/live_deploy/emit_test.dag
…k live #7478 landed gunbc.managed_directory, whose own note states it is "the live consumer that takes [FilePermissions] off" the inert roster — but the roster row stayed. v2.lens.inert_carrier then reads rostered-but-not-inert, which is exactly what inert_carrier_row_dissolve prescribes deleting: "a live consumer reads the carrier; the stale-roster check then forces this row's deletion." Measured rather than guessed: unrostered=0, stale=1, and the live inert set is the roster minus FilePermissions. None of this branch's new carriers are unrostered. This is a main-side latent red, not one this branch introduced. main is green at a589c5f with both managed_directory and the stale row present, so #7478's affected set did not select this witness; a broader diff does, which is the selection-coverage shape the scheduled falsifier exists to catch after the fact. RED controls kept green: count_not_in_roster_detects_unrostered and count_stale_roster_detects_stale_entry both still fire on synthetic input, so the lens still discriminates rather than having been made vacuous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ion's route P0 #1 and P0 #2 from the operator review, which turn out to be one defect and one consequence of it. #1. `tailscale_serve_path` was a name nothing configured. It held "tailscale-serve" for production and "tailscale-serve-srv1-lab" for the twin — two distinct strings, which is exactly what the member-path disjointness witness compared and exactly why it passed. The emitter referenced the field zero times: it built `tailscale serve --bg <port>` from the port alone, and a bare serve binds the DEFAULT HTTPS listener at the ROOT mount, so both deployments configured the SAME endpoint and a twin apply would silently repoint production's routing at the lab. The disjointness was real and meaningless — the model's identity axis and tailscale's configuration key were unrelated quantities, so no strengthening of a claim over the former could observe a collision in the latter. `extdeps.tailscale.serve` now carries the key the upstream actually uses (listener port x mount x backend), the member path is a PROJECTION of it, and the port-only `EnableBackground` operation is deleted rather than corrected so the collision shape is unwritable. The mount is namespaced by instance_id — the same axis that already namespaces roots, ports, units and tmux sockets — so two instances cannot produce one endpoint without sharing an identity the registry forbids. Production keeps the root mount as a declared placement, the same deliberate stop (and the same dissolution trigger) as its unit name. #2 dissolves rather than being re-engineered. The refusing-teardowns-last partition existed because TailscaleServeMapping refused, and it refused on an uncited claim that the CLI has no scoped removal. The upstream documents `serve <flags> off`. With the endpoint modeled, teardown ACTS — so the poison marker, the refusal command, the ordering partition, the constant-false predicate and the three notes defending them all delete, and the retract now removes the route FIRST, in derived reverse-apply order, which is the dependency ordering the partition was destroying. Scoped removal arrived in client 1.52, so it is a fact about a host's binary rather than about tailscale. The capability is three-valued and grounded on extdeps.version.semver (the verdict carries the version it was computed from, and the boundary is compared by semver_compare rather than a second ladder). srv1 is recorded at its measured 1.98.3; nothing else is asserted for unmeasured hosts. The refusal fires at EMIT — a retract artifact is not produced at all for an unobserved or too-old client — so no stop-the-line arm can reappear among the teardowns. Emitted diff, both scripts bash -n clean: apply: - if ! sudo -n tailscale serve status ... | grep -q ':8080'; then sudo -n tailscale serve --bg 8080; fi + sudo -n tailscale serve --bg --https=443 8080 retract: + sudo -n tailscale serve --https=443 off (first, before the unit) - echo '__GUNBC_TAILSCALE_TEARDOWN_UNSCOPED__ ...' >&2; false This is NOT byte-preserving for production and is not claimed to be. The explicit --https=443 is required because the upstream demands every original flag on an off command, so a mapping applied without the listener flag cannot state it when removing itself. 443 is the listener production already serves on. The idempotence guard is deleted, not replaced: it grepped unstructured status text for a port that appears nowhere in the configuration key, and a mapping held by a DIFFERENT deployment on that port satisfied it — so it skipped precisely when another deployment held the endpoint. Configuring an endpoint to a backend it already has is a no-op upstream, so the correct number of observations is zero. Witnesses (16 green by execution, incl. both goldens): the new twin_and_production_configure_disjoint_tailscale_endpoints asserts the endpoint in BOTH directions on BOTH scripts — production's spelling must be ABSENT from the twin's apply and retract, since the positive half alone passes against the broken state; retract_is_not_emitted_without_an_observed_scoped_removal pins unobserved, unsupported and measured apart, because collapsing unobserved into either pole is the two-states-one-label defect this change repairs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
P0 #1 and P0 #2 — both closed at
|
Bring branch current with main (deployment placement #7456) on top of the prior merge-admission conflict resolution. Co-authored-by: Cursor <cursoragent@cursor.com>
The defect
deployment_owned_stepsanddeployment_service_configwere nullary. Zero arguments, so they returned the deployment rather than a deployment, andgunbc_roadmap_unit_namewas a single value naming the only unit that could ever exist.That makes the consequence sharper than "srv1 is hardcoded": a second deployment on the same machine was not awkward, it was unexpressible. It would have collided on unit name, unit path, binary path, repo root and port 8080 — every one of them a lone data row.
Host is not the axis. Instance is, and host is one of its fields. Deploying twice on one machine and once on each of two machines are the same operation with different arguments; a model where the first is impossible has fused two independent facts. Three separate "srv1-only" gaps got patched one at a time before anyone asked why they kept appearing.
This is a de-fork, not new modeling
HostDashboardInstancealready carries every factlive_deployhardcoded, and already proves the point:srv1_live(8080) andsrv1_lab(18081) are two instances on one host with disjoint roots and distinct named tmux servers.live_deployreferenced it zero times while carrying a parallel srv1-shaped copy.Everything downstream of
DeploymentSpecwas already parameterized — which is why this survived. Every consumer reads cleanly from its argument; the single point where the argument was invented is three nullary calls in one file.instance— what is placed, at which root and port ·target— which machine ·identity— which names it owns in the host's unit and tailscale namespaces.The hazard the twin witness found
Writing the twin claim turned it red on one path.
Both instances resolved
serve_binarythrough the host layout to/opt/gunbc/bin/gunbc. The existingdashboard_instance_owned_pathscheck covers origin, source, attempts and attempt-state — notserve_binary— so it passed while the deployment's owned-artifact set overlapped production on exactly that path.ServeBinaryisOwned, and retract tears Owned members down:This could not fire while twins were unexpressible. The hazard is created by the capability, so it had to close in the same change that grants it — landing the de-fork alone would have shipped a footgun that looked like a feature.
Operator ruling: the processes are mutually exclusive, so each instance owns its binary, derived from its instance root. srv1-live declares its path directly rather than through the constructor and is untouched.
Also fixed, found while parameterizing
specwas in scope andart.pathalready held the unit path, but both used globals.One deliberate stop
Unit names are passed, not derived. Deriving from
instance_idis the stronger model and is whatdashboard_instance_service_session_namealready does for tmux — but srv1-live runs asgunbc-roadmap.serviceand would be silently renamed: the apply installs the new unit while the old keeps serving, then retract orphans it. Dissolves on a scheduled rename.Verification
compile --target daga_twin_deployment_on_one_host_collides_with_nothingtwin_teardown_removes_nothing_production_ownsretract_is_the_reverse_of_what_this_spec_appliesByte-identity is the oracle that separates "refactor that preserves behaviour" from "rewrite that quietly alters a live deploy." Every value srv1 reads moved from a literal to a field read and the output did not shift a byte.
The twin claim is the discriminating one, and it earned its keep by going red first.
🤖 Generated with Claude Code