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.
- package/dashboard/index.html +73 -11
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +15 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/coordination/ReviewExchange.d.ts +135 -0
- package/dist/coordination/ReviewExchange.d.ts.map +1 -0
- package/dist/coordination/ReviewExchange.js +192 -0
- package/dist/coordination/ReviewExchange.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +33 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SecretStore.d.ts.map +1 -1
- package/dist/core/SecretStore.js +10 -1
- package/dist/core/SecretStore.js.map +1 -1
- package/dist/core/TransferByNickname.d.ts +6 -0
- package/dist/core/TransferByNickname.d.ts.map +1 -1
- package/dist/core/TransferByNickname.js +18 -6
- package/dist/core/TransferByNickname.js.map +1 -1
- package/dist/core/seamlessnessConfig.d.ts.map +1 -1
- package/dist/core/seamlessnessConfig.js +12 -1
- package/dist/core/seamlessnessConfig.js.map +1 -1
- package/dist/core/types.d.ts +23 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +8 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/MentorAutonomousGuardian.d.ts.map +1 -1
- package/dist/scheduler/MentorAutonomousGuardian.js +1 -0
- package/dist/scheduler/MentorAutonomousGuardian.js.map +1 -1
- package/dist/server/AgentServer.d.ts +5 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +9 -1
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +16 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +8 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +227 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +8 -1
- package/upgrades/1.3.271.md +154 -0
- package/upgrades/autoloop-gate-compliance.eli16.md +11 -0
- package/upgrades/side-effects/autoloop-gate-compliance.md +43 -0
- package/upgrades/side-effects/dashboard-pool-sessions.md +103 -0
- package/upgrades/side-effects/review-exchange-protocol.md +60 -0
- package/upgrades/side-effects/secret-drop-store-first.md +117 -0
- 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).
|