instar 1.3.1337 → 1.3.1339

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 (49) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +8 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  5. package/dist/core/PostUpdateMigrator.js +13 -0
  6. package/dist/core/PostUpdateMigrator.js.map +1 -1
  7. package/dist/core/SessionRefresh.d.ts +21 -0
  8. package/dist/core/SessionRefresh.d.ts.map +1 -1
  9. package/dist/core/SessionRefresh.js +81 -30
  10. package/dist/core/SessionRefresh.js.map +1 -1
  11. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  12. package/dist/core/devGatedFeatures.js +6 -0
  13. package/dist/core/devGatedFeatures.js.map +1 -1
  14. package/dist/core/types.d.ts +12 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/core/types.js.map +1 -1
  17. package/dist/data/standards-guard-index.json +1 -1
  18. package/dist/data/standards-guard-index.meta.json +2 -2
  19. package/dist/data/standards-registry.meta.json +1 -1
  20. package/dist/scaffold/templates.d.ts.map +1 -1
  21. package/dist/scaffold/templates.js +4 -0
  22. package/dist/scaffold/templates.js.map +1 -1
  23. package/dist/server/routes.d.ts.map +1 -1
  24. package/dist/server/routes.js +32 -0
  25. package/dist/server/routes.js.map +1 -1
  26. package/dist/threadline/AgentTrustManager.d.ts +40 -3
  27. package/dist/threadline/AgentTrustManager.d.ts.map +1 -1
  28. package/dist/threadline/AgentTrustManager.js +55 -7
  29. package/dist/threadline/AgentTrustManager.js.map +1 -1
  30. package/dist/threadline/ThreadlineBootstrap.d.ts +7 -0
  31. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  32. package/dist/threadline/ThreadlineBootstrap.js +34 -15
  33. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  34. package/dist/threadline/inboundIdLedgerWiring.d.ts +2 -2
  35. package/dist/threadline/relayUnknownSenderTrust.d.ts +136 -0
  36. package/dist/threadline/relayUnknownSenderTrust.d.ts.map +1 -0
  37. package/dist/threadline/relayUnknownSenderTrust.js +208 -0
  38. package/dist/threadline/relayUnknownSenderTrust.js.map +1 -0
  39. package/package.json +1 -1
  40. package/src/data/builtin-manifest.json +63 -63
  41. package/src/data/standards-guard-index.json +1 -1
  42. package/src/data/standards-guard-index.meta.json +2 -2
  43. package/src/data/standards-registry.meta.json +1 -1
  44. package/src/scaffold/templates.ts +4 -0
  45. package/upgrades/1.3.1338.md +30 -0
  46. package/upgrades/1.3.1339.md +20 -0
  47. package/upgrades/a2a-relay-unknown-sender-trust.eli16.md +21 -0
  48. package/upgrades/side-effects/a2a-relay-unknown-sender-trust.md +129 -0
  49. package/upgrades/side-effects/sessions-refresh-prebind.md +113 -0
@@ -0,0 +1,21 @@
1
+ # A first message is not a grant of trust (ELI16)
2
+
3
+ Agents talk to each other through a shared relay on the internet. Normally a message is locked so only the receiver can read it, and before the receiver acts on it, a checkpoint looks the sender up in its trust list. A sender that is not on the list may only "ping" to see if anyone is there.
4
+
5
+ There is a second kind of message: one the receiver cannot unlock, because it does not have the sender's keys yet. These arrive as plain text. They skipped the checkpoint completely. Every one of them was treated as coming from a known, trusted-enough agent. The receiver sent an automatic "got it" reply, put the message in its inbox, and could start a working session with full tools to answer it. Worse, that first message quietly wrote the sender onto the trust list as "verified", so from then on it was trusted on every route.
6
+
7
+ The relay does prove one thing: the sender really holds the key that matches its name tag. It does not prove who the sender is, and nobody ever decided to trust it. That is the hole this change closes.
8
+
9
+ With the change switched on, a plain-text message is judged by the trust list the same way a locked message is. A sender that is not on the list gets nothing: no "got it" reply, no inbox entry, no session, and it is not written onto the list. Its automatic "got it" replies to messages we sent still count, so peers we write to do not look unreachable. A sender that is on the list is handled at the level the list actually holds for it. Passwords and other secrets are never accepted this way, because secrets must always travel locked.
10
+
11
+ One detail turned out to matter. The checkpoint lets a stranger's "ping" through, expecting it to be answered on the spot. But the code after the checkpoint does not answer pings on the spot; it treats them like any message and can start a session. So on this route a stranger's ping is dropped too. The same gap on the locked route is recorded for its own fix.
12
+
13
+ It starts in watch-only mode, on development agents only. In watch-only mode every message is delivered exactly as before, and each one that would have been dropped is written to the log and counted on the agent's private health page. Strangers who write during this time are still added to the list as before, but they are marked as "only a first contact", and that mark is saved, so it survives a restart. When watch-only mode is switched off, a marked entry counts as no trust at all, on both routes, until someone actually grants that agent trust.
14
+
15
+ Agents already on the list before this change are left alone. On the development computer both existing entries were created by exactly this hole; deciding which of them are real is the operator's call, made from the counts.
16
+
17
+ **What ships now.** On for development agents, watch-only. Off for everyone else.
18
+
19
+ **What needs deciding later.** After two days of counts, grant trust to the real peers and switch watch-only mode off on the development agent. Turning it on for everyone needs an easier way to grant trust than one peer at a time.
20
+
21
+ **Status.** Built under the operator's standing approval of 2026-10-08.
@@ -0,0 +1,129 @@
1
+ # Side-Effects Review — A2A relay unknown-sender trust
2
+
3
+ **Version / slug:** `a2a-relay-unknown-sender-trust`
4
+ **Date:** `2026-10-09`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent (see below)`
7
+
8
+ Spec: docs/specs/a2a-relay-unknown-sender-trust.md (three cross-model rounds, one internal reviewer, conformance gate; approved under the operator's standing approval of 2026-10-08 14:49). Tracking action ACT-066.
9
+
10
+ ## Summary of the change
11
+
12
+ The relay client raises `unknown-sender` for a message whose sender's keys this agent does not hold. `ThreadlineBootstrap`'s handler skipped `InboundMessageGate`, emitted `gate-passed` at `verified` (reason `relay-authenticated`) and recorded the interaction, which created a fingerprint profile at `verified` / `setup-default`. The handler now calls `handleRelayUnknownSender` (new pure module `src/threadline/relayUnknownSenderTrust.ts`) after the existing payload-size check.
13
+
14
+ - off (`enabled` omitted on a non-development agent, or `false`): today's handling, byte for byte.
15
+ - dry-run (default when on): today's delivery and profile write; the verdict is counted and a would-refuse logged. A profile created by this path is created already marked `relayFirstContact: true`.
16
+ - enforcing (`dryRun: false`): a refused message is dropped before `gate-passed`; an allowed one is emitted at the held level (reason `probe` for a profiled sender's ping/health). Interaction recorded only for an existing profile.
17
+
18
+ Verdict: held level from the trust manager; a marked, still-`setup-default` profile counts as none; `ack` passes at every level; `credential-share` never; an `untrusted` sender passes nothing else (no probes); otherwise the gate's `getAllowedOperationsByFingerprint` table.
19
+
20
+ `AgentTrustManager`: optional live `newFingerprintProfileLevel` (only lowers to `untrusted`, throw-safe) and `onFingerprintProfileCreated`; `getOrCreateProfileByFingerprint` takes `createOpts.relayFirstContact`; while enforcing, `getTrustLevelByFingerprint` / `getAllowedOperationsByFingerprint` read a marked, still-`setup-default` profile as `untrusted`. `server.ts` supplies the live mode reader. `routes.ts` authed `/health` carries mode + counters + `unmarkedSetupDefaultProfiles`. `ThreadlineConfig.relayUnknownSenderTrust` type; `DEV_GATED_FEATURES` `a2aRelayUnknownSenderTrust`; CLAUDE.md template + `migrateClaudeMd` section (framework-shadowed). The unknown-sender message content no longer carries a `type: undefined` key.
21
+
22
+ ## Decision-point inventory
23
+
24
+ - Pass or drop an unknown-sender relay message by sender trust — **add** — invariant: the trust manager's level table (with the ack / credential / untrusted-probe differences stated in the spec); enforced only with `dryRun: false`.
25
+ - Trust level handed to the `gate-passed` consumer from this path — **modify** — `verified` today; when enforcing, the held level.
26
+ - Level of a new fingerprint profile — **modify** — `verified` today; `untrusted` while enforcing.
27
+ - Gate's fingerprint trust reads — **modify** — a marked, still-`setup-default` profile reads `untrusted` while enforcing. Unmarked profiles unchanged.
28
+ - `InboundMessageGate` logic — **pass-through** — not changed.
29
+
30
+ ---
31
+
32
+ ## 1. Over-block
33
+
34
+ Only when enforcing (deliberate operator setting):
35
+
36
+ - A real peer that has no trust profile, or only a watch-only first-contact profile, is dropped until the operator grants it trust. That is the intended change; the rollout reads `wouldRefuse` first and grants real peers (ACT-069).
37
+ - An untrusted sender's `ping`/`health` is dropped (the gate would pass it). It is a stranger's probe; nothing answers probes inline today.
38
+ - A `verified` sender asking for an operation above its level (e.g. `task-request`) is dropped, as the gate drops it.
39
+ - A plaintext `credential-share` is dropped at every level.
40
+ - A throwing lookup while enforcing drops the message (fail closed).
41
+ - A new fingerprint profile created by any path while enforcing (including the gate's credential dry-run branch) starts `untrusted`; an operator grant still lands.
42
+
43
+ Off and dry-run drop nothing.
44
+
45
+ ## 2. Under-block
46
+
47
+ - Unmarked `setup-default` profiles keep `verified`: those written before this change, while the check was off, or (in dry-run) by other creators such as the gate's own interaction record and the pairing paths. All are counted in `unmarkedSetupDefaultProfiles` on `/health` for the operator to decide.
48
+ - Rate limits and replay protection of the gate are not applied on this path (as today).
49
+ - A mode read failure is treated as off.
50
+ - The MCP stdio process's trust manager can overwrite server-side state (pre-existing, ACT-052).
51
+ - The end-to-end path still routes an untrusted sender's probe to a session (pre-existing, ACT-056 filed 2026-10-09).
52
+
53
+ ## 3. Level-of-abstraction fit
54
+
55
+ The authority stays `AgentTrustManager`; the module only reads its level and table. The drop happens at the one point where the unknown-sender path hands messages onward (`gate-passed`), mirroring the gate. The profile default lives in the manager because it is the only creator of profiles.
56
+
57
+ ## 4. Signal vs authority compliance
58
+
59
+ **Required reference:** docs/signal-vs-authority.md
60
+
61
+ - [ ] No — this change produces a signal consumed by an existing smart gate.
62
+ - [ ] No — this change has no block/allow surface.
63
+ - [x] Yes — but the logic is not brittle pattern-matching: a deterministic lookup in the trust manager's enumerable level-to-operation table, the same invariant the relay gate already holds.
64
+ - [ ] ⚠️ Yes, with brittle logic.
65
+
66
+ No content is judged. In its default (dry-run) form it blocks nothing and only produces a log line and counters.
67
+
68
+ ## 4b. Judgment-point check
69
+
70
+ No new heuristic at a competing-signals point. The one judgment — when to leave dry-run — is the operator's (`## Decision points touched`, ACT-069).
71
+
72
+ ## 5. Interactions
73
+
74
+ - **Ack stage:** acks pass at every level and are consumed by `runRelayAckStage` before routing; our own auto-ack is sent only to non-`untrusted` senders, so a dropped or untrusted sender gets none.
75
+ - **Inbound-id ledger:** reason `relay-authenticated` keeps the `relay-unknown-sender` ingress; `probe` skips the ledger, as for the gate.
76
+ - **Local-route trust (sibling):** a same-machine sender refused on the local route falls back to the relay; while this check enforces, a no-profile sender is now dropped there too, closing the gap the sibling spec recorded.
77
+ - **Gate:** while enforcing, its fingerprint reads honour the first-contact mark, so the two relay paths agree.
78
+ - **Double-fire / races:** none. Synchronous, single-threaded; one log line per verdict.
79
+
80
+ ## 6. External surfaces
81
+
82
+ - Other agents: when enforcing, strangers' relay messages go unanswered (no ack), as gate blocks already do.
83
+ - Install base: none by default (dark on the fleet).
84
+ - Persistent state: an optional `relayFirstContact` field on trust profiles (older versions ignore it).
85
+ - Authed `/health`: new `threadline.relayUnknownSenderTrust` block; not on the unauthenticated `/health` (tested).
86
+
87
+ ## 6b. Operator-surface quality
88
+
89
+ No new operator surface; granting trust uses the existing `threadline_trust` / dashboard paths.
90
+
91
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
92
+
93
+ **Machine-local BY DESIGN** (`physical-credential-locality`): unknown-sender messages arrive only on the machine holding the relay connection (one per agent identity), and are judged against that machine's trust manager. The first-contact mark lives on the profile it qualifies and can only remove a grant, so losing it on a move cannot add one. Counters per machine. No notices, no URLs.
94
+
95
+ ## 8. Rollback cost
96
+
97
+ - Live: `threadline.relayUnknownSenderTrust.enabled: false` (read per message) restores today; `dryRun: true` stops dropping.
98
+ - Profiles created `untrusted` while enforcing stay so; grant with `threadline_trust`. Marked profiles revert to ordinary `verified` reads once the check is off.
99
+ - Code revert: pure patch, no data migration.
100
+
101
+ ## Conclusion
102
+
103
+ Review changed the design in five ways (probes dropped; durable first-contact mark replacing an in-memory set; mark written atomically with the profile; credential-share refused; unmarked-profile count on `/health`). Clear to ship dev-gated in dry-run. Enforcement on the development agent is decided from the evidence on ACT-069.
104
+
105
+ ---
106
+
107
+ ## Second-pass review (if required)
108
+
109
+ **Reviewer:** independent reviewer subagent (read-only; given the code and this artifact)
110
+ **Independent read of the artifact: concur**
111
+
112
+ Concur. Off and dry-run delivery are unchanged (the profile hook returns `verified` unless enforcing; `isUngrantedFirstContact` applies only while enforcing; the dropped `type: undefined` key was already omitted by `JSON.stringify` and every reader uses `content.type`). Enforcing leaks nothing: an unprofiled or marked sender resolves `untrusted`, everything but `ack` is dropped before `gate-passed`, the ack is consumed by the ack stage before auto-ack, inbox and routing, our auto-ack requires a non-`untrusted` level, and the gate's fingerprint reads apply the same mark. Signal-vs-authority holds (deterministic table lookup, no content judged, dry-run default).
113
+
114
+ One imprecision raised and fixed: section 2 now says unmarked `setup-default` profiles can also be written in dry-run by other creators (the gate's interaction record, the pairing paths), all counted on `/health`.
115
+
116
+ ---
117
+
118
+ ## Evidence pointers
119
+
120
+ - `tests/unit/a2a-relay-unknown-sender-trust.test.ts` — 32 tests.
121
+ - `tests/integration/threadline/a2a-relay-unknown-sender-trust.test.ts` — 4 tests.
122
+ - `tests/e2e/threadline/a2a-relay-unknown-sender-trust-alive.test.ts` — 3 tests, real relay.
123
+ - Risk check, development machine, 2026-10-09: two fingerprint profiles, both `verified` / `setup-default` (created by this path).
124
+
125
+ ---
126
+
127
+ ## Class-Closure Declaration (display-only mirror)
128
+
129
+ No self-triggered controller — not applicable. The check runs only inside an inbound relay message and fires no restart, spawn, notify or retry.
@@ -0,0 +1,113 @@
1
+ # Side-Effects Review — POST /sessions/refresh answers target refusals before the 202 (EVO-025)
2
+
3
+ **Version / slug:** `sessions-refresh-prebind`
4
+ **Date:** `2026-09-24`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent (required: session lifecycle)`
7
+
8
+ ## Summary of the change
9
+
10
+ `POST /sessions/refresh` returned 202 "Refresh scheduled" and only 500ms later called `SessionRefresh.refreshSession()`; target refusals (`not_telegram_bound`, `session_not_found`, `refresh_in_progress`, `rate_limited`, `slack_respawner_unwired`) reached only `console.warn`. A caller passing a display name ("Jev") instead of the tmux name ("echo-jev") was told the restart was scheduled when it was refused. The detect phase of `refreshSession()` is extracted verbatim into a private `resolveTarget()`; a new read-only `precheckRefusal(sessionName)` runs `resolveTarget()` plus the in-flight check plus a non-recording rate-limit check. The route calls it before the existing busy precheck and answers a refusal synchronously (409 with `{code, error}`; 429 for `rate_limited`). The `not_telegram_bound` and `session_not_found` messages gain a display-name hint naming the matching running session's `tmuxSession`. Files: `src/core/SessionRefresh.ts`, `src/server/routes.ts`, `tests/unit/SessionRefresh.test.ts`, `tests/unit/sessions-refresh-route.test.ts`.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `POST /sessions/refresh` admission — modify — target refusals are now answered pre-202 instead of post-202 in the log.
15
+ - `SessionRefresh.refreshSession` detect phase — pass-through — same checks, same order, same refusal codes (the not_telegram_bound / session_not_found messages gain the display-name hint, which also reaches the post-202 log line); moved into `resolveTarget()` and shared.
16
+ - `SessionRefresh.precheckRefusal` — add — read-only early answer; never kills, records, or respawns.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ The early check refuses only when `refreshSession()` itself would refuse with the same code at that moment, using the same lookups. A binding that is registered in the ≤500ms between the precheck and the scheduled call used to succeed and is now refused (the caller can retry immediately); this window is negligible and the caller now learns the result. No legitimate request that would have been refreshed is refused for any other reason. The work gate is not duplicated here (it has its own precheck), so `force:true` behaviour is unchanged.
23
+
24
+ ---
25
+
26
+ ## 2. Under-block
27
+
28
+ A refusal that arises only after the 202 (binding removed or the session exiting in the 500ms window, a work-gate verdict change, a respawner failure) is still reported only in the log — `refreshSession()` re-runs every check authoritatively, so this is a reporting gap, not a safety gap. The self-refresh case (the caller is the session being killed) still needs the 202-before-kill ordering and keeps it.
29
+
30
+ ---
31
+
32
+ ## 3. Level-of-abstraction fit
33
+
34
+ Correct layer: the orchestrator owns the lookup logic and exposes it once; the route only translates a refusal to an HTTP status. No lookup is re-implemented in the route (unlike `restart-all`, which inlines the Telegram lookup). The display-name hint lives in the orchestrator next to the state lookup it depends on.
35
+
36
+ ---
37
+
38
+ ## 4. Signal vs authority compliance
39
+
40
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
41
+
42
+ - [x] No — this change has no new block/allow surface.
43
+
44
+ The refusals are the orchestrator's existing deterministic refusals (binding existence, session existence, in-flight, rate cap) — structural invariants, not judgment. The change moves when the caller hears them; it grants no new authority and removes none. The display-name case is deliberately a hint, never a remap.
45
+
46
+ ---
47
+
48
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
49
+
50
+ No new static heuristic at a competing-signals decision point. The checks are enumerable invariants (is there a binding, is there a running session with this tmux name, is a refresh in flight, is the fixed rate cap reached).
51
+
52
+ ---
53
+
54
+ ## 5. Interactions
55
+
56
+ - **Shadowing:** the new precheck runs before the busy precheck. For an unbound name the caller now gets 409 `not_telegram_bound` instead of a busy verdict — correct, since the refresh could not run anyway. Likewise a session that is both busy and out of rate budget now gets 429 `rate_limited` where it used to get 409 `session-busy`; both are correct refusals. The busy precheck and post-202 authoritative path are unchanged.
57
+ - **Double-fire:** none — the precheck performs no action.
58
+ - **Races:** `precheckRefusal` reads `inFlight` and the rate window without writing (the rate check's opportunistic prune of expired timestamps is idempotent and already happens on every refresh). Test proves repeated prechecks consume no budget.
59
+ - **Feedback loops:** the loop-guarded mcp-autorefresh hook POSTs this route fire-and-forget; a 409 is simply logged by curl, no retry loop. Internal callers (ContextWedgeSentinel, quota swap, restart-all) call `refreshSession()` directly and are unaffected.
60
+
61
+ ---
62
+
63
+ ## 6. External surfaces
64
+
65
+ - The HTTP response for a refused target changes from 202 to 409/429 with `{code, error}`. This is the intended fix; the documented usage already says to pass the tmux name. No in-repo caller depends on a 202 for a refused name (grep of src, scripts, templates).
66
+ - No persistent state, no external systems, no notices.
67
+ - Operator surface: no operator-facing actions added.
68
+
69
+ ---
70
+
71
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
72
+
73
+ No operator surface — not applicable.
74
+
75
+ ---
76
+
77
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
78
+
79
+ Machine-local by design: a refresh acts on a tmux session on the machine serving the request, and the orchestrator's in-flight set and rate window are already per-process. No notices, no durable state, no URLs.
80
+
81
+ ---
82
+
83
+ ## 8. Rollback cost
84
+
85
+ Pure code change — revert and ship a patch. No persistent state, no migration, no agent state repair. During a rollback window callers would again receive 202 for refused names (the prior behaviour).
86
+
87
+ ---
88
+
89
+ ## Conclusion
90
+
91
+ Small correctness fix: the route now tells callers the truth about target refusals it previously hid in the log. The detect phase was extracted without behaviour change (the existing SessionRefresh, Slack-arm, route, integration and e2e swap-continuity suites pass unchanged apart from the route mock gaining the new method). GET /sessions already exposes the binding as `platform`/`platformId`; the stored `topicId` field is left as-is rather than redefined. Second-pass reviewer concurred; clear to ship.
92
+
93
+ ---
94
+
95
+ ## Second-pass review (if required)
96
+
97
+ **Reviewer:** independent general-purpose reviewer subagent (read-only, same worktree)
98
+ **Independent read of the artifact: concur**
99
+
100
+ Concur: the extraction is behaviour-preserving, `precheckRefusal` is non-mutating (the rate-window prune only drops already-expired timestamps), the self-refresh 202 still precedes the kill, and the suites plus `tsc` pass. Two artifact corrections raised and applied: messages (not only codes) gain the display-name hint, and busy + rate-exhausted now answers 429 instead of 409 session-busy.
101
+
102
+ ---
103
+
104
+ ## Evidence pointers
105
+
106
+ - `npx vitest run tests/unit/SessionRefresh.test.ts tests/unit/sessions-refresh-route.test.ts tests/unit/sessionRefresh-slack.test.ts tests/integration/swap-continuity-antithrash-routes.test.ts tests/e2e/swap-continuity-antithrash-lifecycle.test.ts` → 5 files, 74 tests passed.
107
+ - `tsc --noEmit` clean.
108
+
109
+ ---
110
+
111
+ ## Class-Closure Declaration (display-only mirror)
112
+
113
+ No agent-authored-artifact defect — not applicable. (Route response-ordering bug in server source; not a prompt/hook/config/skill/standards defect, and not a self-triggered controller.)