instar 1.3.270 → 1.3.271

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dashboard/index.html +73 -11
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +15 -2
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/coordination/ReviewExchange.d.ts +135 -0
  6. package/dist/coordination/ReviewExchange.d.ts.map +1 -0
  7. package/dist/coordination/ReviewExchange.js +192 -0
  8. package/dist/coordination/ReviewExchange.js.map +1 -0
  9. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  10. package/dist/core/PostUpdateMigrator.js +33 -1
  11. package/dist/core/PostUpdateMigrator.js.map +1 -1
  12. package/dist/core/SecretStore.d.ts.map +1 -1
  13. package/dist/core/SecretStore.js +10 -1
  14. package/dist/core/SecretStore.js.map +1 -1
  15. package/dist/core/TransferByNickname.d.ts +6 -0
  16. package/dist/core/TransferByNickname.d.ts.map +1 -1
  17. package/dist/core/TransferByNickname.js +18 -6
  18. package/dist/core/TransferByNickname.js.map +1 -1
  19. package/dist/core/seamlessnessConfig.d.ts.map +1 -1
  20. package/dist/core/seamlessnessConfig.js +12 -1
  21. package/dist/core/seamlessnessConfig.js.map +1 -1
  22. package/dist/core/types.d.ts +23 -0
  23. package/dist/core/types.d.ts.map +1 -1
  24. package/dist/core/types.js.map +1 -1
  25. package/dist/scaffold/templates.d.ts.map +1 -1
  26. package/dist/scaffold/templates.js +8 -1
  27. package/dist/scaffold/templates.js.map +1 -1
  28. package/dist/scheduler/MentorAutonomousGuardian.d.ts.map +1 -1
  29. package/dist/scheduler/MentorAutonomousGuardian.js +1 -0
  30. package/dist/scheduler/MentorAutonomousGuardian.js.map +1 -1
  31. package/dist/server/AgentServer.d.ts +5 -0
  32. package/dist/server/AgentServer.d.ts.map +1 -1
  33. package/dist/server/AgentServer.js +9 -1
  34. package/dist/server/AgentServer.js.map +1 -1
  35. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  36. package/dist/server/CapabilityIndex.js +16 -0
  37. package/dist/server/CapabilityIndex.js.map +1 -1
  38. package/dist/server/routes.d.ts +8 -0
  39. package/dist/server/routes.d.ts.map +1 -1
  40. package/dist/server/routes.js +227 -4
  41. package/dist/server/routes.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/data/builtin-manifest.json +64 -64
  44. package/src/scaffold/templates.ts +8 -1
  45. package/upgrades/1.3.271.md +154 -0
  46. package/upgrades/autoloop-gate-compliance.eli16.md +11 -0
  47. package/upgrades/side-effects/autoloop-gate-compliance.md +43 -0
  48. package/upgrades/side-effects/dashboard-pool-sessions.md +103 -0
  49. package/upgrades/side-effects/review-exchange-protocol.md +60 -0
  50. package/upgrades/side-effects/secret-drop-store-first.md +117 -0
  51. package/upgrades/side-effects/transfer-idempotency-exactly-once.md +105 -0
@@ -0,0 +1,154 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ One additive line in the mentor autoloop prompt (`src/scheduler/MentorAutonomousGuardian.ts`), locked by three new unit-test assertions. Earned from PR #792's no-silent-fallbacks CI failure, where a spec's "best-effort, never throws" wording invited the swallowed catch the ratchet counts.
9
+
10
+ The dashboard sessions list now shows EVERY session across ALL machines in the
11
+ multi-machine pool, and every session row states which machine it runs on
12
+ (a machine-nickname badge). Sessions on the machine you're viewing stay fully
13
+ interactive (live terminal, close button); sessions on other machines appear
14
+ as informational rows with a tooltip pointing at the owning machine's
15
+ dashboard.
16
+
17
+ Under the hood: `GET /sessions` self-tags sessions with `machineId` /
18
+ `machineNickname` when the pool is wired, and a new `GET /sessions?scope=pool`
19
+ aggregates every reachable peer's session list (existing cross-machine Bearer
20
+ auth, 5s timeout per peer). A dead or slow peer degrades to a `pool.failed`
21
+ entry — the page never breaks. The plain `/sessions` route keeps its
22
+ back-compatible array shape.
23
+
24
+ Added the ReviewExchange protocol — the structured way two mandate-named agents complete
25
+ a mutual code-review sign-off without the operator relaying messages between them. One
26
+ exchange tracks one review package (content-addressed by checksum, fixed at creation)
27
+ through a strict linear lifecycle: proposed, delivered, verdict recorded, complete — or
28
+ changes-requested, which is terminal so a stale approval can never be replayed onto
29
+ reworked code. Both sign-offs — the reviewing peer's authenticated approval and the
30
+ owner's countersignature — are checked against the operator's coordination mandate
31
+ before acceptance, and every accepted signature stores the audit reference of the gate
32
+ decision that authorized it. With no mandate issued, every sign-off refuses: the
33
+ protocol inherits deny-by-default and ships inert.
34
+
35
+ A submitted Secret Drop is now persisted store-first to the durable
36
+ AES-256-GCM encrypted SecretStore the instant it is received, instead of
37
+ living only in the server's in-memory `received` map. The retrieve route
38
+ transparently falls back to the durable copy when the in-memory one is gone
39
+ (server restart, compaction, cross-machine handoff), and a successful consume
40
+ deletes both copies — one-time semantics are preserved. Opt out with
41
+ `secrets.persistDrops: false` in `.instar/config.json`.
42
+
43
+ This closes the recurring "the user dropped a secret and the agent lost it"
44
+ failure: with the fleet shipping many releases a day, every release restarted
45
+ the server and evaporated any un-consumed submission.
46
+
47
+ Also: the SecretStore master-key manager now forces file-key mode during test
48
+ runs (`VITEST` / `NODE_ENV=test`). The OS keychain entry is machine-global —
49
+ a test constructing a SecretStore against a fresh stateDir used to generate a
50
+ new master key and silently overwrite that shared entry, making every real
51
+ agent vault on the machine undecryptable for keychain-resolving readers. No
52
+ test can touch the real keychain anymore. A `secrets.forceFileKey` config
53
+ plumb is also available for operators who want to pin the key source.
54
+
55
+ The CLAUDE.md template's Secret Drop "Security" bullet is rewritten (the old
56
+ "in-memory only, never written to disk" claim is no longer true), and an
57
+ idempotent migration updates existing agents' CLAUDE.md in place.
58
+
59
+ Two fixes for the 2026-06-05 "move this to the laptop" noise incident, where
60
+ one user message produced contradictory "Moving to Laptop" / "I can't move
61
+ this right now (rate-limited)" replies and executed multiple times.
62
+
63
+ 1. **Idempotent topic transfer.** The transfer planner now recognizes a move
64
+ request targeting the machine the topic is already on — or already pinned
65
+ to — BEFORE the rate limit. Duplicates of an already-satisfied move answer
66
+ "This conversation is already running on X — nothing to move" instead of a
67
+ rate-limit rejection. The rate limit keeps its real job: damping rapid
68
+ flip-flops between DIFFERENT machines.
69
+
70
+ 2. **Exactly-once ingress turns on with the pool.** The exactly-once inbound
71
+ message gate (the dedupe ledger that stops retried/replayed messages from
72
+ re-executing commands) shipped default-OFF — even on machines whose
73
+ multi-machine session pool was live. The default is now stage-aware: a pool
74
+ at `live-transfer` or `rebalance` gets `exactlyOnceIngress` ON
75
+ automatically. An explicit `multiMachine.exactlyOnceIngress: false` still
76
+ wins.
77
+
78
+ ## What to Tell Your User
79
+
80
+ Nothing user-visible. The autonomous mentor loop's instructions now include how to satisfy the repo's ratchet CI gates, so mentor-driven fixes stop tripping no-silent-fallbacks.
81
+
82
+ Open the dashboard on any of your machines and you'll see all your sessions in
83
+ one list — each with a badge saying which machine it's running on. No more
84
+ opening one dashboard per machine to find a session.
85
+
86
+ Your agent can now run a real code review with a partner agent under the permission
87
+ slip you issue — no more relaying review messages between them yourself. The agents
88
+ deliver the code package, record the reviewer's verdict, and counter-sign, and every
89
+ step is checked against your permission slip and written to the same tamper-evident
90
+ log. If you revoke the slip mid-review, the review stops on the spot. Until you issue
91
+ a slip, nothing changes — every review step refuses by default.
92
+
93
+ If you hand me a secret through a drop link, it now survives my server
94
+ restarting, updating, or the conversation moving between machines. You should
95
+ never have to re-send a secret because "I lost it" again. Secrets are stored
96
+ encrypted and are deleted as soon as they're consumed.
97
+
98
+ Asking me to move a conversation to a machine it's already on (or repeating
99
+ the request) now gets a calm "already running there" instead of a confusing
100
+ "rate-limited" error. And if my internals retry a message under load, the
101
+ command runs once — not four times.
102
+
103
+ ## Summary of New Capabilities
104
+
105
+ - `buildAutoloopGoal` includes a gate-compliance paragraph: intentional fail-open catches must report via DegradationReporter or carry an inline `@silent-fallback-ok` justification — never a ratchet baseline bump.
106
+ - The mentor is instructed to spell out these gate notes in any task brief or spec it authors for another agent.
107
+
108
+ - Dashboard sessions list shows every session across the pool with a machine badge per row.
109
+ - `GET /sessions?scope=pool` → `{ sessions, pool: { peersOk, failed, ... } }` (agents can answer "what's running across my machines?").
110
+ - `GET /sessions` sessions carry `machineId`/`machineNickname` on pooled installs.
111
+ - CLAUDE.md template bullet + idempotent migration so existing agents learn the capability.
112
+
113
+ - Create a review exchange for an artifact under a mandate, content-addressed so the
114
+ signatures bind to exactly the code that was reviewed.
115
+ - Record delivery, the peer's authenticated verdict, and the owner's countersignature —
116
+ with both sign-offs evaluated through the mandate gate and refused on any deny.
117
+ - Inspect exchanges and their signatures, each carrying the audit reference of the
118
+ gate decision that authorized it.
119
+ - Maturity: stable engine and routes; ships inert (deny-by-default) until the operator
120
+ issues a mandate with the sign-code-review authority.
121
+
122
+ - Secret Drop submissions persist store-first to the encrypted SecretStore and survive server churn.
123
+ - Retrieval transparently falls back to the durable copy; consume deletes both copies.
124
+ - `secrets.persistDrops: false` opts out (default ON).
125
+ - Tests can never pollute the machine-global SecretStore keychain entry (structural guard).
126
+ - `secrets.forceFileKey` pins the master-key source to the per-agent file key.
127
+
128
+ - Duplicate "move to X" requests no-op as "already there" — never "rate-limited."
129
+ - The transfer planner accepts an optional `currentPinOf` hook (pin-aware idempotency).
130
+ - `exactlyOnceIngress` defaults ON whenever the session pool routes real traffic.
131
+
132
+ ## Evidence
133
+
134
+ Three tiers green: `tests/unit/dashboard-sessionMachineBadge.test.ts` (8),
135
+ `tests/unit/PostUpdateMigrator-poolSessionsVisibility.test.ts` (3),
136
+ `tests/integration/sessions-pool-scope.test.ts` (5 — including a REAL second
137
+ HTTP server as the peer and a dead-peer degradation case), and
138
+ `tests/e2e/sessions-pool-scope-lifecycle.test.ts` (3 — real AgentServer,
139
+ feature alive single-machine). tsc + lint clean.
140
+
141
+ `tests/integration/secret-drop-store-first.test.ts` (5 route-level tests, both
142
+ sides of every boundary), `tests/unit/secret-store.test.ts` (keychain
143
+ pollution guard), `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts`
144
+ (+3 migration tests), and the Tier-3 e2e
145
+ `tests/e2e/secret-drop-store-first-lifecycle.test.ts` — which boots two REAL
146
+ AgentServers on one stateDir and proves a secret submitted to process 1 is
147
+ retrievable from process 2 after a true restart. tsc + lint clean.
148
+
149
+ `tests/unit/TransferByNickname.test.ts` (+5, including the verbatim incident
150
+ case: already-on-target inside the rate window → noop) and
151
+ `tests/unit/seamlessnessConfig.test.ts` (+4, stage coupling both ways +
152
+ explicit-override both ways). tsc + lint clean. Production logs
153
+ (2026-06-05T04:20Z double pin, zero `exactly-once` lines on a live-transfer
154
+ pool) are the incident evidence.
@@ -0,0 +1,11 @@
1
+ # Mentor autoloop prompt: gate-compliance line
2
+
3
+ Instar has an automated "mentor loop" — a prompt that tells a developer agent how to run one cycle of dogfooding a mentee agent: check its health, assign it a real task, observe, and fix what breaks as a proper PR.
4
+
5
+ That prompt already teaches the ship discipline (three test tiers, the dev gate, verify-before-claim). But it said nothing about the repo's *ratchet gates* — CI checks like no-silent-fallbacks that count known-bad patterns and fail if the count ever goes UP.
6
+
7
+ We learned why that matters the hard way: a spec told the builder a helper should be "best-effort, never throws." The builder did exactly that — wrapped it in a catch that swallowed the error — and CI went red, because a swallowed catch is exactly what the no-silent-fallbacks ratchet counts. The spec's own wording invited the failure.
8
+
9
+ This change adds one paragraph to the mentor-loop prompt: an intentional fail-open catch must either report the failure (through DegradationReporter) or carry an inline `@silent-fallback-ok` justification — and you never bump a ratchet baseline just to get CI green. It also tells the mentor to spell these notes out whenever it authors a task brief or spec for another agent, so the next builder doesn't get trapped by well-meaning "never throws" guidance.
10
+
11
+ One prompt paragraph, three new test assertions locking it in. No behavior change anywhere else.
@@ -0,0 +1,43 @@
1
+ # Side-Effects Review — Mentor autoloop gate-compliance line
2
+
3
+ **Version / slug:** `autoloop-gate-compliance`
4
+ **Date:** `2026-06-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `echo second-pass checklist`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds one static paragraph to `buildAutoloopGoal()` in `src/scheduler/MentorAutonomousGuardian.ts` — the deterministic prompt assembled for the autonomous mentor dogfooding loop. The paragraph teaches ratchet-gate compliance: intentional fail-open catches must report via DegradationReporter or carry an inline `@silent-fallback-ok` justification (never a baseline bump), and task briefs/specs authored for other agents must spell these notes out explicitly. Earned from PR #792's no-silent-fallbacks CI failure, where the spec's "best-effort, never throws" guidance invited the exact swallowed catch the ratchet counts.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `buildAutoloopGoal` — modified — one additional template line appended to the returned prompt string. Pure string assembly; no new branches, no I/O, no config.
15
+ - `tests/unit/MentorAutonomousGuardian.test.ts` — modified — three new content assertions on the assembled prompt.
16
+
17
+ ## 1. Over-block
18
+
19
+ None possible. The function returns a string; nothing is gated, blocked, or filtered. The added text instructs the *consuming agent*; it cannot prevent any operation by itself.
20
+
21
+ ## 2. Over-permit
22
+
23
+ None. No permission surface is touched.
24
+
25
+ ## 3. Token/cost impact
26
+
27
+ The prompt grows by ~430 characters, sent once per autonomous-fix guardian cycle (budget- and min-interval-gated, single-instance). Negligible.
28
+
29
+ ## 4. Behavior change for existing consumers
30
+
31
+ The only consumer is `MentorAutonomousGuardian` → spawned loop session goal text. Existing assertions on the prompt (cycle steps, discipline line, topics) are unchanged and still pass — the new line is purely additive. The `degrades gracefully when topics are unset` test still passes (the added text contains no topic interpolation).
32
+
33
+ ## 5. Failure modes
34
+
35
+ A string literal cannot throw at runtime. The TypeScript build verifies the template syntax. If the guidance itself were wrong, the blast radius is advisory-only (an agent following it would still produce gate-passing code — DegradationReporter and `@silent-fallback-ok` are both legitimate, documented paths in `tests/unit/no-silent-fallbacks.test.ts`).
36
+
37
+ ## 6. Migration parity
38
+
39
+ None required: the prompt is assembled at runtime from shipped code; every agent gets it on its next update through the normal release path. No installed files, hooks, config defaults, or templates change.
40
+
41
+ ## 7. Rollback
42
+
43
+ Revert the commit. No persisted state references the new text.
@@ -0,0 +1,103 @@
1
+ # Side-Effects Review — Dashboard pool-wide sessions (every session, every machine)
2
+
3
+ **Version / slug:** `dashboard-pool-sessions`
4
+ **Date:** `2026-06-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required` (additive read-only aggregation + UI; no authority, no mutation; every boundary test-pinned across three tiers)
7
+
8
+ ## Summary of the change
9
+
10
+ `GET /sessions` self-tags each session with `machineId`/`machineNickname` when
11
+ the pool is wired, and `GET /sessions?scope=pool` aggregates every reachable
12
+ peer's plain `/sessions` (new `resolvePeerUrls` route hook off the machine
13
+ identity registry), tagging remote sessions with their machine + `remote:true`.
14
+ The dashboard polls the aggregate every 15s and renders a machine badge on
15
+ every session row; remote rows are informational (no subscribe/close). CLAUDE.md
16
+ template + idempotent migration carry the agent awareness.
17
+
18
+ ## Decision-point inventory
19
+
20
+ 1. Pool wired vs not → self-tag vs omit fields. Both sides pinned (integration).
21
+ 2. `scope=pool` vs plain → envelope vs back-compatible array. Both pinned
22
+ (integration + e2e).
23
+ 3. Peer reachable vs dead → merged+tagged vs `pool.failed` entry. Both pinned
24
+ against a REAL second HTTP server / a dead port.
25
+ 4. Dashboard row local vs remote → interactive vs informational (click, close,
26
+ active highlight, row key namespace). All pinned (unit).
27
+ 5. Migration: section-present-without-marker / marker-present / no-section.
28
+ All three pinned.
29
+
30
+ ## 1. Over-block
31
+
32
+ Nothing rejected. The plain route's shape and fields are unchanged for
33
+ existing callers (additive fields only). `scope=pool` is opt-in.
34
+
35
+ ## 2. Under-block
36
+
37
+ - Remote sessions are view-only — no remote kill or terminal streaming (named
38
+ non-goals; the tooltip routes the user to the owning machine's dashboard).
39
+ - The aggregate trusts each peer's self-report; a peer that lies about its
40
+ sessions is already inside the trust boundary (same authToken).
41
+ - The 15s poll means a remote session can be up to ~15s stale — acceptable for
42
+ a visibility surface; local sessions remain real-time via the WebSocket.
43
+
44
+ ## 3. Level-of-abstraction fit
45
+
46
+ Aggregation lives in the one route that owns session listing, reusing each
47
+ peer's EXISTING plain route (no new peer surface, no recursion — peers are
48
+ fetched without `scope`). Peer discovery (`resolvePeerUrls`) is wired in
49
+ `server.ts` from the same identity-registry source the lease/marker transports
50
+ already use. The dashboard keeps the WebSocket as the live local feed and
51
+ layers the poll on top — remote state is kept in a separate array precisely
52
+ because the WS replaces the local one each broadcast.
53
+
54
+ ## 4. Signal vs authority compliance
55
+
56
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
57
+
58
+ No authority added anywhere: read-only aggregation, fail-soft per peer
59
+ (`failed` entries, never a 500), best-effort dashboard poll (a failed tick
60
+ retries next interval).
61
+
62
+ ## 5. Interactions
63
+
64
+ - Cross-machine fetch uses the established Bearer-authToken pattern
65
+ (placement-proxy precedent) with a 5s `AbortSignal.timeout` so a hung peer
66
+ can't pin the route.
67
+ - No recursion: the aggregate always fetches peers' PLAIN `/sessions`.
68
+ - The WebSocket session feed is untouched; local rows merely inherit the self
69
+ nickname at render time from the poll response.
70
+ - tmux-name collisions across machines are handled by namespacing remote row
71
+ keys (`remote:<machineId>:<tmux>`).
72
+
73
+ ## 6. External surfaces
74
+
75
+ One query param on an existing authed route; one optional RouteContext hook;
76
+ dashboard HTML/JS; CLAUDE.md template bullet + idempotent migration. No config,
77
+ no notifications, no new files on disk.
78
+
79
+ ## 7. Rollback cost
80
+
81
+ Trivial — revert the PR. The plain route shape is unchanged, so nothing else
82
+ depends on the new fields; no state, no schema, no migration side effects
83
+ beyond an extra CLAUDE.md bullet (inert text).
84
+
85
+ ## Conclusion
86
+
87
+ Additive, read-only, fail-soft visibility feature at the minimal scope that
88
+ satisfies the operator requirement verbatim: every session shows on the
89
+ dashboard, and every row states its machine.
90
+
91
+ ## Second-pass review (if required)
92
+
93
+ Not required — see header.
94
+
95
+ ## Evidence pointers
96
+
97
+ - `tests/unit/dashboard-sessionMachineBadge.test.ts` (8) — UI contract.
98
+ - `tests/unit/PostUpdateMigrator-poolSessionsVisibility.test.ts` (3) — parity.
99
+ - `tests/integration/sessions-pool-scope.test.ts` (5) — route contract incl. a
100
+ REAL peer server + dead-peer degradation.
101
+ - `tests/e2e/sessions-pool-scope-lifecycle.test.ts` (3) — feature-alive on the
102
+ production init path.
103
+ - `docs/specs/dashboard-pool-sessions.md` + `.eli16.md`.
@@ -0,0 +1,60 @@
1
+ # Side-effects review — ReviewExchange protocol (coordination-mandate spec §7, G2.3)
2
+
3
+ Spec: `docs/specs/coordination-mandate.md` (approved by Justin, A/A/B, 2026-06-05) — §7
4
+ names G2.3: "autonomous code-review protocol over Threadline, gated by the
5
+ `sign-code-review` authority". Change: new `src/coordination/ReviewExchange.ts` engine +
6
+ six `/review-exchange*` routes wired through the existing coordination block in
7
+ `AgentServer` → `RouteContext`.
8
+
9
+ ## 1. Blast radius
10
+
11
+ Additive only. No existing route, gate, store, or sentinel is modified. The engine rides
12
+ the EXISTING MandateGate — it adds no new authority kind, no new signing key, and no new
13
+ trust root. Inert by inheritance: with no mandate issued (the universal state at deploy),
14
+ every sign-off path refuses 403, so shipping this changes NO behavior until the operator
15
+ issues a mandate AND an agent drives an exchange.
16
+
17
+ ## 2. State / data
18
+
19
+ One new file under the `.instar/state/` convention, created on first use:
20
+ `review-exchanges.json` (the exchange records). No schema migration; an absent file is an
21
+ empty list. Audit entries land in the EXISTING `mandate-audit.jsonl` hash chain (the gate
22
+ records them; this engine only stores the returned entry hashes).
23
+
24
+ ## 3. Security model
25
+
26
+ - **No new authorization surface.** Both sign-offs (peer approve + owner countersign) are
27
+ evaluated through the same MandateGate (`sign-code-review` authority): named party,
28
+ bounds, expiry, revocation — every decision audited. A deny refuses the state
29
+ transition; the route surfaces it as 403 with the gate's reason (gate-deny precedence
30
+ over not-found, so deny-by-default cannot masquerade as a 404).
31
+ - **requester ≠ authorizer preserved.** The exchange routes are Bearer-gated (no PIN by
32
+ design): creating/recording an exchange delegates nothing — the GATE is what authorizes
33
+ sign-offs, and the gate's authority comes only from the PIN-issued mandate.
34
+ - **Peer asymmetry stated, not hidden.** The peer's instance may not run this engine, so
35
+ the peer's signature is their authenticated Threadline verdict message recorded BY
36
+ REFERENCE (`kind: 'authenticated-peer-verdict'`, evidence = the message ref). The gate
37
+ still evaluates the peer's fingerprint identically. The caller is responsible for only
38
+ recording verdicts that actually arrived over the authenticated relay — the same trust
39
+ the existing A2A surface already places in the relay's sender authentication.
40
+ - **Content addressing.** `packageSha256` is fixed at creation and has no update path; a
41
+ changed package is a different exchange. `request-changes` is terminal — rework
42
+ restarts as a new exchange, so a stale approval can never be replayed onto new code.
43
+ - **Kill switch reaches the protocol.** Revoking the mandate mid-exchange blocks every
44
+ subsequent sign-off (tested at unit + e2e tiers).
45
+
46
+ ## 4. Failure modes
47
+
48
+ - Engine init failure → `coordination` stays null → all routes 503 (deny-safe, no boot
49
+ block; same try/catch as the mandate engine).
50
+ - Torn/absent state file → empty list (`@silent-fallback-ok` annotated).
51
+ - A gate deny mid-protocol leaves the exchange in its prior state (no partial advance) —
52
+ asserted by tests on both deny paths.
53
+
54
+ ## 5. Test coverage
55
+
56
+ 11 unit + 7 integration + 4 e2e (22 total): both sides of every decision boundary —
57
+ creation validation, linear-order enforcement, gate-deny on both sign-offs,
58
+ deny-by-default with no mandate, named-party refusals (stranger AND wrong-party), bounds
59
+ mismatch, request-changes terminality, revocation mid-exchange, audit-hash wiring
60
+ integrity on the production boot path, 503 when unavailable.
@@ -0,0 +1,117 @@
1
+ # Side-Effects Review — Secret Drop store-first durable persistence
2
+
3
+ **Version / slug:** `secret-drop-store-first`
4
+ **Date:** `2026-06-04`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required` (single decision boundary, both sides test-pinned; operator-mandated incident fix)
7
+
8
+ ## Summary of the change
9
+
10
+ A submitted Secret Drop is persisted store-first to the durable AES-256-GCM
11
+ encrypted SecretStore (`secretDrops.<token>`) the instant it is received, the
12
+ retrieve route transparently falls back to that copy when the in-memory one is
13
+ gone, and a successful consume deletes both. Opt out with
14
+ `secrets.persistDrops: false` (absence = enabled). Plus a structural
15
+ test-environment guard in `MasterKeyManager`: any VITEST/NODE_ENV=test run is
16
+ forced to file-key mode so no test can overwrite the machine-global keychain
17
+ entry (the failure the first run of this PR's own test caused live on
18
+ 2026-06-05, remediated same hour).
19
+
20
+ ## Decision-point inventory
21
+
22
+ 1. `persistDrops !== false` — persist on submit / fall back on retrieve / clean
23
+ on consume vs pre-existing in-memory-only behavior. Both sides covered in
24
+ `secret-drop-store-first.test.ts`.
25
+ 2. `consume` true/false against the durable copy — delete vs preserve. Both
26
+ sides covered.
27
+ 3. `inTestRun` in `MasterKeyManager` — file-key forced vs production keychain
28
+ resolution. Test side pinned in `secret-store.test.ts`; production side is
29
+ the unchanged existing behavior.
30
+
31
+ ## 1. Over-block
32
+
33
+ Nothing is rejected. Submission, retrieval, and consume flows accept exactly
34
+ what they accepted before. A durable-persist FAILURE never blocks the
35
+ submission (best-effort + loud ERROR log) — the user's drop always succeeds if
36
+ the in-memory submit succeeded.
37
+
38
+ ## 2. Under-block
39
+
40
+ - A secret submitted while `persistDrops:false` still has the original churn
41
+ exposure — explicit operator opt-out, documented.
42
+ - The durable copy does not yet sync cross-machine (Phase 4) — a drop made on
43
+ the laptop is durable on the laptop only.
44
+ - The broader key-coherence disease (machine-global keychain account, no
45
+ key-id header, silent-empty on decrypt failure) is NOT fixed here — filed as
46
+ a follow-up finding; this PR only makes tests structurally unable to trigger
47
+ it.
48
+
49
+ ## 3. Level-of-abstraction fit
50
+
51
+ The persist lives in the submit route directly after the in-memory `submit()`
52
+ succeeds — the single place a submission is born. The fallback lives in the
53
+ one retrieve route the hardened helper uses. No new routes, no SecretDrop
54
+ class changes, no helper changes. The test guard lives in the
55
+ `MasterKeyManager` constructor — the single chokepoint every key resolution
56
+ passes through (Structure > Willpower: per-test `forceFileKey` conventions
57
+ would rot).
58
+
59
+ ## 4. Signal vs authority compliance
60
+
61
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
62
+
63
+ No blocking authority added. Persist is best-effort (loud log, never 500s);
64
+ fallback only widens what retrieval can find; the test guard constrains TESTS
65
+ only, never production resolution.
66
+
67
+ ## 5. Interactions
68
+
69
+ - The 15-min in-memory TTL/idle cleanup still runs; after cleanup the durable
70
+ copy now serves the retrieve instead of a 404 — that is the fix working.
71
+ - `onStuckConsumer` notify-only hardening unchanged.
72
+ - The Telegram confirm + agent nudge fire AFTER the persist, so by the time
73
+ the agent is told "retrieve me," the durable copy already exists.
74
+ - `secret-drop-retrieve.mjs` needs no changes — it talks to the same route.
75
+ - SecretStore `set()` is read-modify-write; a decrypt failure throws BEFORE
76
+ any write, so the persist path cannot clobber an existing store it cannot
77
+ read (verified against the live 2026-06-05 key-split incident).
78
+
79
+ ## 6. External surfaces
80
+
81
+ No new HTTP routes. One new config block (`secrets.persistDrops`,
82
+ `secrets.forceFileKey` — both optional, absence = current defaults). CLAUDE.md
83
+ template Security bullet rewritten + idempotent PostUpdateMigrator migration
84
+ for existing agents. One new encrypted-at-rest artifact class:
85
+ `secretDrops.<token>` entries inside the existing vault file.
86
+
87
+ ## 7. Rollback cost
88
+
89
+ Low. `secrets.persistDrops: false` is an instant behavioral rollback without a
90
+ deploy. Full revert restores in-memory-only semantics; any `secretDrops.*`
91
+ entries already in vaults are inert data deletable via `SecretStore.delete`.
92
+ No schema, no irreversible op.
93
+
94
+ ## Conclusion
95
+
96
+ Operator-mandated incident fix at the minimal coherent scope: one persist
97
+ call, one fallback branch, one cleanup branch, one structural test guard.
98
+ Every decision boundary has both sides pinned by tests; the failure it fixes
99
+ was reproduced live twice during the build (auto-update restart ate a 1h
100
+ token; test run split the vault keys).
101
+
102
+ ## Second-pass review (if required)
103
+
104
+ Not required — see header. The riskiest element (writing secrets to disk) is
105
+ the operator's explicit instruction, encrypted with the pre-existing vault
106
+ machinery, and consume-cleaned.
107
+
108
+ ## Evidence pointers
109
+
110
+ - `tests/integration/secret-drop-store-first.test.ts` — 5 route-level tests.
111
+ - `tests/unit/secret-store.test.ts` — keychain pollution guard.
112
+ - `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` — 3
113
+ migration tests (rewrite, idempotency, fresh-inject wording).
114
+ - `docs/specs/secret-drop-store-first.md` + `.eli16.md` — spec + overview.
115
+ - Live remediation evidence: vault backup at
116
+ `/tmp/config.secrets.enc.backup-1780635105`; keychain entry cdat
117
+ `2026-06-05T04:46:39Z` matched the test run start `04:46:38`.
@@ -0,0 +1,105 @@
1
+ # Side-Effects Review — Transfer idempotency + exactly-once-when-live
2
+
3
+ **Version / slug:** `transfer-idempotency-exactly-once`
4
+ **Date:** `2026-06-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required` (pure-planner reorder + config-resolution default; every boundary side test-pinned)
7
+
8
+ ## Summary of the change
9
+
10
+ 1. `planTransferByNickname`: the already-satisfied checks (`owner === target`,
11
+ NEW `pin === target` via optional `currentPinOf`) now run BEFORE the rate
12
+ limit, so a duplicate of an already-satisfied move plans `noop` ("already
13
+ there") instead of `reject/rate-limited`. The consumer wires `currentPinOf`
14
+ from `TopicPlacementPinStore` and words the `already-on-target` noop as
15
+ "already running on X — nothing to move."
16
+ 2. `resolveSeamlessnessConfig`: `exactlyOnceIngress` defaults ON when the
17
+ session pool stage is `live-transfer`/`rebalance` (a live pool without the
18
+ ingress dedupe ledger re-executes retried/replayed user commands — the
19
+ 2026-06-05 4×-execution incident). Explicit values always win.
20
+
21
+ ## Decision-point inventory
22
+
23
+ 1. `owner === target` → noop vs continue. Both sides pinned (incident test +
24
+ transfer test).
25
+ 2. `pin === target` → noop vs continue; absent `currentPinOf` → old behavior.
26
+ All three pinned.
27
+ 3. Rate-limit window for a real move (different target) → still rejected.
28
+ Pinned.
29
+ 4. Stage-derived `exactlyOnceIngress` default: off (none/dark/shadow), on
30
+ (live-transfer/rebalance), explicit override both directions. All pinned.
31
+
32
+ ## 1. Over-block
33
+
34
+ Nothing new is rejected. The planner now rejects strictly LESS (duplicates
35
+ that previously rate-limited now noop). The exactly-once default only
36
+ activates a gate that itself fails open (ledger errors fall through to normal
37
+ routing, per the existing route code).
38
+
39
+ ## 2. Under-block
40
+
41
+ - The rate limit no longer applies to same-target duplicates — deliberate:
42
+ re-pinning the same value is idempotent. Rapid same-target spam costs one
43
+ pin-store write + one "already there" reply per message; the per-topic
44
+ Telegram traffic itself is bounded upstream.
45
+ - The exactly-once default does NOT retroactively enable the ledger on pools
46
+ still in dark/shadow — pre-live stages keep the old dark default by design.
47
+
48
+ ## 3. Level-of-abstraction fit
49
+
50
+ The idempotency fix lives in the pure planner (the single decision function
51
+ both the natural-language path and `POST /pool/transfer` flow through), not in
52
+ the Telegram consumer. The exactly-once coupling lives in config RESOLUTION
53
+ (`resolveSeamlessnessConfig`) — the one place defaults are derived — not in
54
+ the wiring site.
55
+
56
+ ## 4. Signal vs authority compliance
57
+
58
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
59
+
60
+ No new blocking authority. The planner reorder converts rejections into
61
+ no-ops. The exactly-once gate it activates was already authority-bearing by
62
+ design (spec §8 G3a) and fails open on any ledger error.
63
+
64
+ ## 5. Interactions
65
+
66
+ - `noop` consumer behavior unchanged structurally (pin set + release-if-self
67
+ + message): re-setting an identical pin refreshes `updatedAt`, which can
68
+ extend the rate window for a SUBSEQUENT different-target move by up to 10s
69
+ — bounded, harmless.
70
+ - Activating `exactlyOnceIngress` on live pools also activates the
71
+ reply-marker transport (same flag, existing wiring) — that is the intended
72
+ pairing per spec §8 G3a.
73
+ - Sentinel intercepts (emergency stop / pause) run BEFORE the ingress gate in
74
+ the route — unchanged; an emergency stop is never deduped away.
75
+
76
+ ## 6. External surfaces
77
+
78
+ No new routes, no new config keys (an existing key's DEFAULT becomes
79
+ stage-aware). One user-visible message wording improvement. No migration:
80
+ config absence means "follow the default," which is the point.
81
+
82
+ ## 7. Rollback cost
83
+
84
+ Trivial. `exactlyOnceIngress: false` is an instant per-agent opt-out without
85
+ deploy; reverting the PR restores both old behaviors. No state, no schema.
86
+
87
+ ## Conclusion
88
+
89
+ Two small, surgical changes at the correct layers, each killing one half of a
90
+ user-facing incident: duplicates narrated as failures, and duplicates
91
+ executing at all. Strictly less rejection, strictly more dedupe, both
92
+ test-pinned on every side.
93
+
94
+ ## Second-pass review (if required)
95
+
96
+ Not required — see header.
97
+
98
+ ## Evidence pointers
99
+
100
+ - `tests/unit/TransferByNickname.test.ts` — 5 new idempotency tests (incl. the
101
+ verbatim incident case).
102
+ - `tests/unit/seamlessnessConfig.test.ts` — 4 new coupling tests.
103
+ - Production evidence: `logs/server.log` 2026-06-05T04:20:43/04:20:55 double
104
+ pin + zero `exactly-once` lines; topic 13481 screenshot (Moving →
105
+ rate-limited → Moving → rate-limited).