Skip to content

Make deployment placement a parameter, and stop a twin's retract from deleting production's binary - #7456

Merged
briansrls merged 33 commits into
mainfrom
live-deploy-placement-axis
Jul 31, 2026
Merged

briansrls merged 33 commits into
mainfrom
live-deploy-placement-axis

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

The defect

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.

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.

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, not new modeling

HostDashboardInstance already carries every fact live_deploy hardcoded, and already proves the point: srv1_live (8080) and srv1_lab (18081) are two instances on one host with disjoint roots and distinct named 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 this survived. Every consumer reads cleanly from its argument; the single point where the argument was invented is three nullary calls in one file.

deployment_spec_for(instance, target, identity)

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_binary through the host layout to /opt/gunbc/bin/gunbc. The existing dashboard_instance_owned_paths check covers origin, source, attempts and attempt-state — not serve_binary — so it passed while the deployment's owned-artifact set overlapped production on exactly that path.

ServeBinary is Owned, and retract tears Owned members down:

retracting a lab would have deleted the binary production was executing.

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

  • 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 their parameters — spec was in scope and art.path already held the unit path, but both used globals.
  • The systemd unit was staged under one constant and installed from another, agreeing only by coincidence. Parameterizing one side alone would have staged one name and installed another.

One deliberate stop

Unit names are passed, not derived. 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.

Verification

check result
compile --target dag 0 blocking errors
srv1 emitted apply script vs main byte-identical, 3708 bytes
srv1 emitted retract script vs main byte-identical
a_twin_deployment_on_one_host_collides_with_nothing red → green after the binary fix
twin_teardown_removes_nothing_production_owns green
retract_is_the_reverse_of_what_this_spec_applies green

Byte-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

… 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>
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

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.

gunbc-ci-auto-heal and others added 14 commits July 30, 2026 03:23
…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>
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>
@gunbai-bot

gunbai-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Both findings from review 45153 are fixed, and the earlier unit-modeling finding from review 45056 with them.

Refused probes collapsing into fabricated negatives (dashboard_provider_readiness) — confirmed and fixed. The fold asked two-valued questions of three-valued observations three times over: dashboard_target_directory_present answers false for an absent path and for a probe that could not run, and the two .success branches did the same. An unreachable host produced ProviderStateRootAbsent complete with a device-login command for the operator to type — actionable remediation for a condition never observed.

The module already had the vocabulary it was bypassing: DashboardPathObservation's PathObservationRefused arm, and exec_refusal_reason_note stating this exact rule one screen up. So the fix is the fold consuming the discrimination that was already made, plus a fourth readiness arm ProviderReadinessRefused { stage, reason } — carrying the stage that refused and deliberately no remediation, because nothing is known about the provider when the transport did not deliver the question.

Derived duration on a bare scalar — fixed, and the finding also applied to the rewrite I had in flight. NumericDateLifetime's seconds is now std.measure.Second, and codex_id_token_observed_lifetime_seconds: Int is now codex_id_token_observed_lifetime: Second. Note on carrier choice: I used std.measure.Second rather than std.types.Duration, because Duration is Int where range(min: 0) — a bare Int alias, which is the fork rather than the canonical surface. Second = Measure<Time, One, Nat> is the std.measure carrier the rest of the tree consumes (extdeps/memory, extdeps/docker, extdeps/sec/facts), and extdeps/tools/curl.dag already declares Second values directly, so the extdeps-boundary precedent is established.

The RFC wire-level NumericDate stays a raw JSON-shaped value, as the finding allows — that is genuinely what RFC 7519 puts on the wire. Because Second counts a Nat, the fold gained a third arm: a token whose expiry precedes its issuance has no non-negative lifetime, so it refuses rather than wrapping or clamping to zero.

All three are covered by witnesses green by execution, including a control asserting an unobserved exit never serializes as 0.

…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>
@gunbai-bot

gunbai-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

review 45173 is correct, confirmed by execution before fixing — bash -n .github/live-deploy-srv1-retract.sh failed with syntax error near unexpected token '(' on line 22. Fixed in 6c0e489.

The bug, as diagnosed. live_deploy_tailscale_teardown_refused_poison contains deployment's routing, and tailscale_teardown_refusal_command wrapped it in single quotes by concatenation. A POSIX single-quoted string 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 systemd disable/removal, daemon-reload, worktree and binary removal and tree-sync teardown never ran, and neither did the refusal message. The arm built to stop the line dropped the rest of the line instead. Your read of the mechanism was exact.

The fix consumes the shared authority rather than escaping in place. extdeps.exec.command.shell_quote is the POSIX quoter this tree already has, and tmux_staged_provider_start_note already instructs callers to use it for precisely this; git_pathspec_exclude_note records the same lesson from the same failure mode, also caught by bash -n. Hand-spelling quotes at a call site is how the class recurs, so the call site no longer spells them. Emitted line 22 is now …every other deployment'\''s routing…, and bash -n exits 0 on both the retract and apply scripts.

On the witness. I did not add a bash -n runner: that needs a host effect, and the corpus batch these claims run in is hermetic. Two structural claims instead, both green by execution:

  • witness_retract_refusal_is_shell_quote_escaped — the emitted retract carries the close-escape-reopen sequence and never the raw deployment's routing.
  • shell_quote_escapes_an_embedded_apostrophe — the shared quoter exercised on the character that broke the call site.

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 ('…' is 3, '…'\''…' is 5), so counting quotes proves nothing. What separates them is the escape form.

On why the golden gate missed it — witness_retract_script_matches_committed_golden compares emission against committed bytes, and both sides were equally unparseable. Byte-equality and target-language validity are different oracle families and this file had only the first. That gap is the real finding behind the finding.

Also in this push: regen_stage0 after merging main with binaries rebuilt post-merge, which clears the regen_verify_gate_passes red (now PASS locally, along with self_host_realized_comparison_staleness_gate_holds).

gunbc-ci-auto-heal and others added 2 commits July 30, 2026 18:39
…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>
@gunbai-bot

gunbai-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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 .github/live-deploy-srv1-retract.sh had not. Regenerated via main_wet on dag/tools/generated_artifact_gate.dag. The refusal now sits at line 30, after every teardown and before the receipt:

sudo -n rm -f /opt/gunbc/bin/gunbc
sudo -n rm -f /etc/systemd/system/gunbc-tree-sync.service /etc/gunbc-tree-sync.env
sudo -n systemctl daemon-reload
sudo -n rm -rf /opt/gunbc/gunbc
echo '__GUNBC_TAILSCALE_TEARDOWN_UNSCOPED__ …' >&2; false
echo live-deploy-receipt host=srv1 fold=retract verdict=converged

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 false aborts a fail-fast pipeline exactly as well as a parse error does. Proven by execution rather than reasoned — set -euo pipefail; echo x >&2; false; echo SIBLING prints no SIBLING and exits 1.

I took the ordering option rather than dropping the false. Dropping it would let the pipeline reach verdict=converged while the tailscale mapping is still live — a fabricated-success receipt, which is the arm §5 forbids. Refusing last keeps all three properties: everything removable is removed, no widen to the node-global reset, nonzero exit with no convergence receipt.

The ordering is not hand-edited. deployment_owned_steps_retract_order derives retract as the reverse of what the spec applied, and re-transcribing a list here would reinstate exactly the two-places-one-fact defect that derivation removed. So the effects are partitioned — acting teardowns in their derived order, then refusing ones — and a refusal cannot precede a real teardown by construction rather than by a lucky order.

Witness: witness_retract_refusal_follows_every_teardown asserts over the emitted text that every teardown command precedes the refusal marker and that verdict=converged does not. A presence-only claim (which the existing twin witness makes) passes in both the broken and the fixed emission, so it could not have caught this.

Also in this push: 4 non-exhaustive matches on DashboardApplyPreflightResult in the apply witness test, left open when DashboardApplyPreflightBootstrapPending was added. gunbc compile --target dag is now 0 blocking errors.

On the two duration findings in review 45016 — both were already fixed in the previous push (NumericDateLifetime → std.measure.Second, codex_id_token_observed_lifetime_seconds: Int → codex_id_token_observed_lifetime: Second); details in my earlier comment.

gunbc-ci-auto-heal and others added 2 commits July 30, 2026 19:43
…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>
@gunbai-bot

gunbai-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

review 45197 confirmed and fixed in 25db066 — thank you, this was the sharpest finding on the PR.

serve_binary became instance-derived while the step that creates its containing directory kept production's literal /opt/gunbc/bin. install does not create missing parents, so a twin apply reached the copy with no <instance-root>/bin and failed on what reads as a permissions problem — which undercut the PR's own claim that twin deployments are safe to run. It is the same half-parameterization the placement de-fork exists to remove, surviving one level down: the value moved to the argument and the directory that must exist for the value to be writable stayed behind. And it was invisible for exactly the reason every other srv1-only gap in this lane was — production's parent and the literal agree.

The parent is now derived from the path being installed, so there is no second literal to drift. Production is byte-identical (/opt/gunbc/bin/gunbc → /opt/gunbc/bin), confirmed by the committed golden passing 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. The witness asserts both directions, since the positive half alone passes even if the emission still creates production's too.

On the two non-blocking notes in review 45204:

The verdict=converged receipt line. Keeping it deliberately. It is unreachable for a spec that owns a tailscale mapping, which is every spec today — but the emission is generic, and the line is the convergence receipt for a retract whose teardowns all act. Deleting it would mean a fully-converged retract prints no receipt, and it becomes reachable the moment the tailscale refusal dissolves (its trigger is named on tailscale_teardown_cannot_be_scoped_note). I'd rather have a receipt that is currently unreached than no receipt on the path that succeeds.

deploy_teardown_refuses inheriting a disposition by omission. One factual correction: it cannot. The match is over OwnedArtifactKind and this compiler treats a non-exhaustive match as a hard diagnostic — I hit exactly that error while writing it (non-exhaustive match: missing variant(s) …), and separately cleared four of them on DashboardApplyPreflightResult in this same push. A new artifact kind fails the compile until it states an arm; it cannot default quietly.

What your suggestion does buy, which the exhaustiveness check does not, is forcing the arm to be a considered statement rather than a copied false. That is a real difference and a good follow-up, but lifting TeardownDisposition onto OwnedArtifactKind touches the artifact model shared with spec.dag and the membership reconcile, so it belongs in its own PR rather than bolted onto this one.

gunbc-ci-auto-heal and others added 3 commits July 30, 2026 20:30
…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>
@gunbai-bot

gunbai-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

All three confirmed and fixed in 21f4a13. Each was verified by execution before I touched anything.

review 45207, finding 1 — stale retract witnesses. Confirmed: ran both, both FAIL. The branch replaced sudo -n tailscale serve reset || true with the refusal, and these two claims were never updated. Fixed by inverting them rather than deleting the conjunct — witness_retract_script_owned_only was requiring the one command in the script that is definitionally not owned-only, since serve reset is node-global. It now forbids any sudo -n tailscale and requires the refusal marker; the second claim does the same. Both conjuncts are needed: the marker alone passes an emission that refuses and resets, the absence alone passes one that silently drops the mapping. Both now PASS, and the golden gates still match.

review 45207, finding 2 — the codex-account fork. Confirmed, and it was wider than reported. host_layout named the account a second time (roadmap_codex_account_label + roadmap_codex_account()) while gunbc.provider_account already carried codex_roadmap_account_label + codex_roadmap_account_selection() — two names for one concept in two modules. Neither local name had a consumer, and the two path rows inlined the literal anyway, so the module declared an authority and then ignored it. Rather than wiring the duplicate through, I deleted it and both rows now consume codex_roadmap_account_selection(). The account's home is provider_account; host_layout's business is host paths. Derived value is byte-identical, which an existing claim pins.

review 45213 — the observation collapse. Confirmed, and the root was worse than the Bool projection you named: dashboard_target_path_present never constructed an observation at all — it returned dashboard_target_probe(...).success, conflating absent with refused at the source. Both named consumers read it bare.

The predicate is deleted. dashboard_missing_target_executables becomes a survey returning ExecutablesSurveyed | ExecutableSurveyRefused, refusing on the first unobservable path — once a probe cannot be delivered the remaining probes carry no information either, and a partial missing-list assembled from the ones that happened to answer is the partial observation that looks complete. DashboardBootstrapPreflight gains a third variant DashboardBootstrapUnobserved, deliberately carrying no remediation; it is a separate variant rather than a reason string so a match that forgets the case fails to compile. The root probe gets the same treatment, because its collapse chose the wrong path — which directory is the bootstrap parent depends on whether the instance root exists, so a refused root observation makes the next question meaningless.

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 dashboard_observation_present_bit, used only to fill the record whose refusal channel is populated by the same fold.

Two new discriminating claims, both PASS: a refused writability probe must not land in DashboardBootstrapRefused carrying "not writable", and an unreachable root must refuse naming the instance root rather than describing the provisioning parent. Neither is constructible against the predecessor.

review 45217 — the hand-walked predicate. Confirmed. Whether a member can be torn down is a fact about the member, so it moved beside deployment_step_ownership in live_deploy/spec.dag as a typed TeardownDisposition, per-layer and total — kind, then step. The emitter keeps only an ordering projection that calls the accessor and enumerates no kinds, so a new OwnedArtifactKind changes exactly one match, in the module that owns the coproduct.

The lift also recovers something the Bool could not express: the two refusals are not the same refusal. An ensured tailscale package is TeardownOutsideOwnership — not this deployment's to remove at any scope. The owned serve mapping is TeardownWouldExceedOwnership — its own, refusing only because the CLI's single removal verb is node-global. They dissolve on different triggers, and a model that cannot tell them apart cannot dissolve either. The new claim asserts the cause, not merely that a refusal occurred, since swapping the two causes leaves every emitted byte identical.

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 — count(required) == 15 (now 16) and count(rows) == 14 (now 16). Same class as review 45207's finding 1. The substantive half: the serve binary left the required-executables roster because apply now installs it, and a roster still demanding it would refuse every first apply to a host without one. The claim now asserts that absence directly, since re-adding it would keep the count at 16 by displacing something else.

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.

@gunbai-bot

gunbai-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Thanks — this is the review that found the thing three automated passes walked past. Findings 1, 3 and 4 fixed in a64e67e; 2 written down; 5 answered below. Everything verified by execution.

Finding 1 — the lane with no consumers. Confirmed exactly: codex_device_prompt_parse, shape_tmux_capture_pane_argv_for, TmuxCaptureFidelity and both fixtures had zero callers, production or witness. A parser written against real captured bytes had never once been run against them.

I wired it rather than splitting, because the brand's claim was one honest function away from being true. tmux_captured_pane_rendering is that producer, and it belongs in extdeps.tmux for the reason your own note gives: whether a capture carries escapes is a tmux fact. A CapturePlainText capture inhabits RenderedTerminalText; a CaptureWithEscapes capture does not and returns a typed arm instead. So escape-carrying bytes cannot reach the parser at all — the wall, not a check.

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: count(live_paths) == 5 and count(twin_paths) == 5 inside a_twin_deployment_on_one_host_collides_with_nothing are the same class.

Deleting them is right for one and wrong for three, which is worth being precise about. witness_retract_owned_only was nothing but the count — deleted, since retract_is_the_reverse_of_what_this_spec_applies already pins membership and order. The other three were carrying a non-degeneracy guard their shape disguised: all over an empty list is vacuously true, so a twin whose owned set collapsed to nothing would satisfy the disjointness claim perfectly. Deleting those would have quietly weakened the safety claim this PR exists to make.

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 — DeploymentIdentity → DeploymentNames. Done. Agreed on the reasoning; it carries unit and mapping names, not a principal.

Finding 2 — not fixed, trigger written down as you suggested, in emitted_shell_parseability_oracle_note. It names the closure (ingest the emitted script through the bounded bash grammar read forward, or bash -n over the two committed artifacts as the cheaper interim) and states the honest bound rather than implying the point fix covered the class: a quoting defect in a new call site here still ships green today.

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, verify_attempt (item 3) has no caller yet. It is the producer; verification_segment in roadmap_workflow_progress is still Pending with detail "awaiting independent validation receipt", which is exactly what it now produces. The missing link is receipt read-back, and for ssh instances that is what I deliberately refuse today — no content-carrying transport bound. Flipping the segment without it would have the segment reporting verification complete on a receipt nobody could read, which is the shape the last three reviews were all about.

Second, srv1_live_deployment_names still has no dissolution trigger — you flagged it as worth one and I agree, but it wants a scheduled unit rename (stop old, install derived, verify health, remove old) that I did not want to invent inside this PR.

Gate-equivalent compile clean (0 blocking). Full floor running on the settled tree now.

gunbc-ci-auto-heal and others added 8 commits July 30, 2026 21:53
…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>
@gunbai-bot

gunbai-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

P0 #1 and P0 #2 — both closed at 1f344b1

They turned out to be one defect and one consequence of it, so #2 is a net deletion rather than a re-engineering.

#1 — tailscale_serve_path was a name nothing configured

Confirmed by execution before fixing: the field is referenced zero times by the emitter. The emitted line was built from the port alone:

if ! sudo -n tailscale serve status 2>/dev/null | grep -q ':8080'; then sudo -n tailscale serve --bg 8080; fi

A bare serve binds the default HTTPS listener at the root mount, so production and the twin configured the same endpoint. The member-path disjointness witness compared "tailscale-serve" against "tailscale-serve-srv1-lab" and passed — the model's identity axis and tailscale's configuration key were unrelated quantities, so no strengthening of a claim over the former could ever have observed a collision in the latter. That is why this was a check of the wrong thing rather than a check that was too weak.

extdeps/tailscale/serve.dag now carries the key the upstream actually uses. The docs tell you what it is by what off requires — "all original flags are required" — so the identity is (listener port × mount × service), not the backend. The member path is a projection of the endpoint, so member-path disjointness and endpoint disjointness became the same statement instead of two that could disagree.

The port-only EnableBackground operation is deleted rather than corrected: its argv is the collision shape, and it had zero consumers.

Mount is namespaced by instance_id — the same axis that already namespaces roots, ports, units and tmux sockets — so hand-picking is not reintroduced (dissolution trigger: the containment tree becoming the naming authority). Production keeps the root mount as a declared placement, the same deliberate stop and same trigger as its unit name.

#2 — the ordering partition dissolves

It existed because TailscaleServeMapping refused, and it refused on an uncited claim that the CLI has no scoped removal. You were right that this is wrong for the current CLI. With the endpoint modeled, teardown acts — so tailscale_teardown_refusal_command, live_deploy_tailscale_teardown_refused_poison, retract_effects_acting_before_refusing, deploy_teardown_stops_the_line, the TeardownWouldExceedOwnership cause and the three notes I wrote defending them all delete.

The retract now removes the route first, in derived reverse-apply order — which is the dependency ordering the partition was destroying.

The version wall

Scoped removal arrived in client 1.52, so it is a fact about a host's binary. Three-valued and grounded on extdeps.version.semver: the verdict carries the SemVerVersion it was computed from (a Bool cannot be re-derived when the host is upgraded), and the boundary is compared with semver_compare rather than a second ladder. srv1 records its measured 1.98.3; nothing is asserted for srv2/srv3.

The refusal fires at emit, not in the script: a retract artifact is not produced at all for an unobserved or too-old client. That is the construction-side reading of "refuse before mutation" — the dangerous artifact does not exist — and it also means a stop-the-line arm cannot reappear among the teardowns.

Emitted diff (both scripts bash -n clean)

 apply:
-if ! sudo -n tailscale serve status 2>/dev/null | 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 here is zero.

Witnesses — 16 green by execution, including both goldens

Two are new:

  • twin_and_production_configure_disjoint_tailscale_endpoints asserts the endpoint in both directions on both scripts. The positive half (twin carries its own --set-path) passes against the broken state, so the negative half — production's spelling absent from the twin's apply and retract — is what closes it. The retract conjuncts are not a restatement: an off missing --set-path targets the root mount, which is production's, and would remove production's routing while exiting zero.
  • retract_is_not_emitted_without_an_observed_scoped_removal pins unobserved, unsupported and measured apart. Collapsing unobserved into either pole is the two-states-one-label defect this change repairs.

teardown_disposition_names_why_each_refusal_refuses is replaced by teardown_disposition_separates_owned_from_ensured, with the record kept of why: that claim was a well-formed assertion over a well-modeled fact that faithfully pinned a false premise, and would have stayed green for as long as the premise went unchecked.

Still open from your review

P1 #3, #4, #5, #6, #7 and P2 #8, #9, #10 are not addressed here, and the PR body is still materially stale. Happy to take those in this PR or the split you proposed — your call.

@briansrls
briansrls merged commit da37e37 into main Jul 31, 2026
5 checks passed
@briansrls
briansrls deleted the live-deploy-placement-axis branch July 31, 2026 03:58
gunbai-bot Bot pushed a commit that referenced this pull request Jul 31, 2026
Bring branch current with main (deployment placement #7456) on top of
the prior merge-admission conflict resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant