instar 1.3.102 → 1.3.104
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/dist/commands/gate.d.ts.map +1 -1
- package/dist/commands/gate.js +8 -1
- package/dist/commands/gate.js.map +1 -1
- package/dist/commands/nuke.d.ts.map +1 -1
- package/dist/commands/nuke.js +19 -10
- package/dist/commands/nuke.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +81 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +33 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +155 -20
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/TopicPlacementPinStore.d.ts +37 -0
- package/dist/core/TopicPlacementPinStore.d.ts.map +1 -0
- package/dist/core/TopicPlacementPinStore.js +95 -0
- package/dist/core/TopicPlacementPinStore.js.map +1 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +13 -0
- package/dist/server/middleware.js.map +1 -1
- package/dist/threadline/listener-daemon.d.ts.map +1 -1
- package/dist/threadline/listener-daemon.js +18 -8
- package/dist/threadline/listener-daemon.js.map +1 -1
- package/dist/threadline/mcp-stdio-entry.js +8 -3
- package/dist/threadline/mcp-stdio-entry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/threadline-bridge-backfill.mjs +7 -1
- package/src/data/builtin-manifest.json +29 -29
- package/src/templates/hooks/compaction-recovery.sh +24 -9
- package/src/templates/hooks/session-start.sh +9 -2
- package/src/templates/hooks/slack-channel-context.sh +8 -1
- package/src/templates/hooks/telegram-topic-context.sh +14 -2
- package/src/templates/scripts/imessage-reply.sh +7 -3
- package/src/templates/scripts/instar-watchdog.sh +14 -5
- package/src/templates/scripts/secret-drop-retrieve.mjs +12 -4
- package/src/templates/scripts/serendipity-capture.sh +15 -9
- package/src/templates/scripts/slack-reply.sh +8 -1
- package/src/templates/scripts/telegram-reply.sh +11 -5
- package/src/templates/scripts/whatsapp-reply.sh +7 -4
- package/upgrades/1.3.103.md +77 -0
- package/upgrades/1.3.104.md +58 -0
- package/upgrades/NEXT.eli16.md +71 -0
- package/upgrades/side-effects/mesh-rpc-auth-and-transfer-activation.md +72 -0
- package/upgrades/side-effects/secret-externalization-hook-resolver-audit.md +48 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
review-convergence: complete
|
|
3
|
+
approved: true
|
|
4
|
+
approved-by: justin (verbal, topic 2169: "yes! then we need to do a full, robust audit to fix this whole class of issues since it keeps coming up")
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Upgrade Guide — vNEXT
|
|
8
|
+
|
|
9
|
+
<!-- bump: patch -->
|
|
10
|
+
|
|
11
|
+
## What Changed
|
|
12
|
+
|
|
13
|
+
**Fleet-wide fix for the silent-403 class.** Every shipped hook, script, Node
|
|
14
|
+
helper, and migrator-emitted template that resolves the agent's `authToken`
|
|
15
|
+
now survives the secret-externalization refactor. Two related bugs landed
|
|
16
|
+
together because both produced the same user-visible symptom (a hook that
|
|
17
|
+
stops emitting any output):
|
|
18
|
+
|
|
19
|
+
- **Auth-token resolution: env-first with a string-type guard.**
|
|
20
|
+
`INSTAR_AUTH_TOKEN` (set by `SessionManager` per spawned session and by
|
|
21
|
+
`JobScheduler` per scheduled job) is checked first. The disk fallback now
|
|
22
|
+
guards `cfg.authToken` against non-string values, so the literal
|
|
23
|
+
`{ "secret": true }` placeholder produced by `SecretMigrator` after
|
|
24
|
+
multi-machine pairing can never leak through as a Bearer token. (Previously,
|
|
25
|
+
scripts sent `[object Object]` or `{'secret': True}` as the Authorization
|
|
26
|
+
header, the server 403'd, and the script's downstream Python parser
|
|
27
|
+
silently exited on the error JSON — no output at all.)
|
|
28
|
+
|
|
29
|
+
- **Port-parse tolerates whitespace.** The `grep -o '"port":[0-9]*'` pattern
|
|
30
|
+
used in every hook required no whitespace between the colon and the
|
|
31
|
+
number, but our prettified `config.json` writes `"port": 4042` (with
|
|
32
|
+
space). The pattern silently produced empty `$PORT`, the hook exited
|
|
33
|
+
early, and not a single HTTP request reached the server. Replaced with
|
|
34
|
+
`grep -oE '"port"[[:space:]]*:[[:space:]]*[0-9]+'` + digit-only extraction.
|
|
35
|
+
|
|
36
|
+
The structural cure includes a unit-tier lint that fails any future re-
|
|
37
|
+
introduction of the broken pattern (positive + negative-verified) and a new
|
|
38
|
+
migration pass (`migrateSecretExternalizationSurvivability`) that upgrades
|
|
39
|
+
deployed auxiliary scripts (`imessage-reply.sh`, `serendipity-capture.sh`,
|
|
40
|
+
`slack-channel-context.sh`) without touching custom forks. The canonical
|
|
41
|
+
always-overwrite hooks (`session-start.sh`, `compaction-recovery.sh`,
|
|
42
|
+
`telegram-topic-context.sh`) heal on next auto-update by the existing
|
|
43
|
+
`migrateHooks` path.
|
|
44
|
+
|
|
45
|
+
## What to Tell Your User
|
|
46
|
+
|
|
47
|
+
If your agent recently went silent after compaction — emitting only the
|
|
48
|
+
wall-clock-time block and then nothing else, despite a healthy server — this
|
|
49
|
+
is the fix. Topic-history injection is back. No configuration needed.
|
|
50
|
+
|
|
51
|
+
After auto-update, send yourself a Telegram message in any forum topic.
|
|
52
|
+
The agent's response should reference what you actually said — not greet
|
|
53
|
+
you like it's the first message. If it doesn't, something else is wrong;
|
|
54
|
+
please flag.
|
|
55
|
+
|
|
56
|
+
## Summary of New Capabilities
|
|
57
|
+
|
|
58
|
+
| Capability | How to Use |
|
|
59
|
+
|-----------|-----------|
|
|
60
|
+
| Auth-token resolution survives secret-externalization | Automatic. Every shipped script + hook now reads `INSTAR_AUTH_TOKEN` env first, then falls back to `config.json` with a string-type guard. |
|
|
61
|
+
| Port-parse tolerates JSON whitespace | Automatic. Hooks no longer exit-early when `config.json` is prettified. |
|
|
62
|
+
| Lint blocks future regressions | `tests/unit/secret-externalization-hook-resolver-lint.test.ts` greps every shell + Node read for the broken pattern and fails on any reintroduction. |
|
|
63
|
+
| Auxiliary-script migration for deployed agents | Existing agents with the old `imessage-reply.sh` / `serendipity-capture.sh` / `slack-channel-context.sh` get the fix on next auto-update via `migrateSecretExternalizationSurvivability`. Custom forks (no shipped marker) are untouched. |
|
|
64
|
+
|
|
65
|
+
## Evidence
|
|
66
|
+
|
|
67
|
+
- 13 new tests across 3 tiers (unit + integration-grade + e2e). All green on a
|
|
68
|
+
full `vitest` run.
|
|
69
|
+
- 35 PostUpdateMigrator-* unit test files (232 tests) verified clean (no
|
|
70
|
+
regression from migrator extensions).
|
|
71
|
+
- Manual smoke test against a real externalized agent: the fixed
|
|
72
|
+
`getTelegramTopicContextHook()` content correctly fetches topic history
|
|
73
|
+
with INSTAR_AUTH_TOKEN env alone (config.json holds the placeholder), and
|
|
74
|
+
correctly degrades to the unauth branch without leaking the placeholder
|
|
75
|
+
when env is missing.
|
|
76
|
+
- Side-effects review:
|
|
77
|
+
`upgrades/side-effects/secret-externalization-hook-resolver-audit.md`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
**Two fixes that make running one agent across multiple machines actually work
|
|
8
|
+
end-to-end.**
|
|
9
|
+
|
|
10
|
+
First, a real bug: the private channel machines use to send each other commands
|
|
11
|
+
was being blocked by the normal login check before it could even verify the
|
|
12
|
+
message's own signature. Because each machine has its own login token, one
|
|
13
|
+
machine could never present a token the other would accept — so every
|
|
14
|
+
cross-machine action (checking if a peer is alive, handing a conversation to
|
|
15
|
+
another machine, moving one) was silently refused. That channel is already
|
|
16
|
+
protected by a strong per-machine cryptographic signature, so it now correctly
|
|
17
|
+
skips the token check and relies on the signature. This is the missing piece
|
|
18
|
+
that made a second machine show as offline even when both machines could reach
|
|
19
|
+
each other perfectly.
|
|
20
|
+
|
|
21
|
+
Second, the headline "move this to the mini" feature is now wired all the way
|
|
22
|
+
through. Saying "move this to <machine nickname>" (or "run this on <nickname>")
|
|
23
|
+
in a conversation now pins that conversation to the named machine and hands it
|
|
24
|
+
over, so it continues there. Before, the phrase was understood but nothing acted
|
|
25
|
+
on it.
|
|
26
|
+
|
|
27
|
+
## What to Tell Your User
|
|
28
|
+
|
|
29
|
+
If you run on one machine, nothing changes. If you run across two machines: a
|
|
30
|
+
second machine will now correctly show as online and become eligible to receive
|
|
31
|
+
conversations, and you can say "move this to the mini" (using whatever nickname
|
|
32
|
+
shows on your Machines list) to hand the current conversation to that machine —
|
|
33
|
+
it picks up there. Both machines need to update for this to work.
|
|
34
|
+
|
|
35
|
+
## Summary of New Capabilities
|
|
36
|
+
|
|
37
|
+
- The machine-to-machine command channel is exempt from the API token check and
|
|
38
|
+
authed solely by its per-machine cryptographic signature — so cross-machine
|
|
39
|
+
presence, delivery, and transfer work over the network, not just in tests.
|
|
40
|
+
- New `TopicPlacementPinStore`: durably pins a conversation to a chosen machine.
|
|
41
|
+
- "move this to <nickname>" / "run this on <nickname>" is wired on inbound:
|
|
42
|
+
recognize the command, pin the conversation, release local ownership, and let
|
|
43
|
+
it re-place onto the named machine via the existing place-and-resume path.
|
|
44
|
+
|
|
45
|
+
## Evidence
|
|
46
|
+
|
|
47
|
+
- `tests/unit/mesh-rpc-auth-exemption.test.ts` — the machine command route
|
|
48
|
+
reaches its handler with no token (and ignores a wrong one — the signature is
|
|
49
|
+
the auth); a normal protected route still requires the token.
|
|
50
|
+
- `tests/unit/topic-placement-pin-store.test.ts` — pin set/get/clear, durable
|
|
51
|
+
across restarts, tolerant of a corrupt file.
|
|
52
|
+
- `tests/unit/transfer-activation-wiring.test.ts` — the recognizer/planner are
|
|
53
|
+
wired on inbound before routing, a transfer sets the pin + releases ownership,
|
|
54
|
+
the pin is passed into placement, and the whole path is dark-gated.
|
|
55
|
+
- Found on real hardware (laptop + Mac mini): the mini stayed offline because its
|
|
56
|
+
`/mesh/rpc` returned 401 before the signed envelope was checked; with the
|
|
57
|
+
exemption the cross-machine calls authenticate off the envelope as designed.
|
|
58
|
+
- Side-effects: `upgrades/side-effects/mesh-rpc-auth-and-transfer-activation.md`.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# vNEXT — plain English overview
|
|
2
|
+
|
|
3
|
+
## What this change is
|
|
4
|
+
|
|
5
|
+
Your agent has a bunch of helper scripts that need to talk to its own local
|
|
6
|
+
server. They authenticate using an "auth token" — basically a password the
|
|
7
|
+
server expects in every request.
|
|
8
|
+
|
|
9
|
+
Recently we added a feature that **moves that password out of the plain
|
|
10
|
+
config file into an encrypted store**, so the password no longer sits in
|
|
11
|
+
plain text on disk. That's a good security improvement.
|
|
12
|
+
|
|
13
|
+
But when we did that, we forgot to update *every* place the helpers read
|
|
14
|
+
the password from. So a bunch of helpers kept reading the plain config
|
|
15
|
+
file, found the placeholder marker (a tiny note that says "the real
|
|
16
|
+
password lives in the encrypted store now"), and used **that marker** as
|
|
17
|
+
the password. The server rejected every request. The helpers didn't
|
|
18
|
+
notice the rejection — they just quietly emitted nothing.
|
|
19
|
+
|
|
20
|
+
The user-visible result: your agent comes back from a memory compaction
|
|
21
|
+
and has no idea what conversation you were in. So it greets you like a
|
|
22
|
+
stranger.
|
|
23
|
+
|
|
24
|
+
## What already exists
|
|
25
|
+
|
|
26
|
+
- The `INSTAR_AUTH_TOKEN` environment variable. Every time the agent
|
|
27
|
+
spawns a new session, it sets this env var with the real password.
|
|
28
|
+
- The encrypted secret store at `.instar/secrets/config.secrets.enc`,
|
|
29
|
+
managed by `SecretMigrator` / `SecretStore`.
|
|
30
|
+
- A migration system (`PostUpdateMigrator`) that rewrites helper scripts
|
|
31
|
+
on every auto-update. Some helpers were already on this track; others
|
|
32
|
+
weren't.
|
|
33
|
+
|
|
34
|
+
## What's new
|
|
35
|
+
|
|
36
|
+
- **Every helper now reads `INSTAR_AUTH_TOKEN` env first.** The env var
|
|
37
|
+
is always present in a session, so it's the canonical path.
|
|
38
|
+
- **The disk-fallback now has a "is this actually a password?" check.**
|
|
39
|
+
If the config file says `{ "secret": true }` (the placeholder), the
|
|
40
|
+
guard rejects it and falls through to no-auth instead of sending the
|
|
41
|
+
placeholder as a password.
|
|
42
|
+
- **A second, unrelated bug** also got fixed in the same change: the
|
|
43
|
+
helpers parsed the server port from the config file with a regex that
|
|
44
|
+
refused to allow whitespace, so `"port": 4042` (the format we
|
|
45
|
+
actually ship) was unparseable. The helpers exited silently with no
|
|
46
|
+
port, hitting nothing. Replaced with a whitespace-tolerant pattern.
|
|
47
|
+
- **A new automated check** scans every helper for this broken pattern
|
|
48
|
+
and refuses to commit any future change that reintroduces it.
|
|
49
|
+
- **An update routine** that fixes the helpers on existing deployed
|
|
50
|
+
agents (without overwriting any custom scripts the user wrote
|
|
51
|
+
themselves).
|
|
52
|
+
|
|
53
|
+
## What you need to decide
|
|
54
|
+
|
|
55
|
+
Nothing. This is a bug fix, no configuration involved. Auto-update will
|
|
56
|
+
heal every deployed agent on next version bump.
|
|
57
|
+
|
|
58
|
+
## How to verify it worked after deploy
|
|
59
|
+
|
|
60
|
+
After your agent updates past the version that includes this fix, send
|
|
61
|
+
yourself a Telegram message in any forum topic. The agent's response
|
|
62
|
+
should reference what you actually said — not greet you like it's the
|
|
63
|
+
first message. If it doesn't, something else is wrong; please flag.
|
|
64
|
+
|
|
65
|
+
## Why this matters more than it might look
|
|
66
|
+
|
|
67
|
+
This is a "silent failure" class — the helpers were broken but emitted
|
|
68
|
+
no error, so the bug only surfaced through the symptom (incoherent
|
|
69
|
+
agent replies after compaction). That makes the failure invisible until
|
|
70
|
+
a user notices the symptom. The structural lint added in this change
|
|
71
|
+
makes the class itself impossible to reintroduce silently.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Side-effects review — /mesh/rpc bearer-auth exemption + transfer-by-nickname activation
|
|
2
|
+
|
|
3
|
+
## What was happening (real-hardware, 2026-05-29)
|
|
4
|
+
|
|
5
|
+
After the HTTP presence transport (B, v1.3.102) deployed to the laptop, the Mac
|
|
6
|
+
mini STILL showed offline in `GET /pool`. Root cause, found by probing the mini's
|
|
7
|
+
`/mesh/rpc` over its tunnel: **401 "Missing or invalid Authorization header"**.
|
|
8
|
+
|
|
9
|
+
`/mesh/rpc` — the §L0 machine-to-machine command transport — was sitting behind
|
|
10
|
+
the general API bearer-token middleware, but `MeshRpcClient.send` sends only the
|
|
11
|
+
signed envelope (no `Authorization` header), and it *can't* send a useful one:
|
|
12
|
+
each machine install holds its own `authToken`, so the sender has no token the
|
|
13
|
+
receiver would accept. So EVERY cross-machine call — the new presence pull AND
|
|
14
|
+
`deliverMessage` / `place` / `claim` / `transfer` — was rejected 401 before the
|
|
15
|
+
dispatcher's envelope check ever ran. The pool passed all in-process dispatcher
|
|
16
|
+
tests yet was completely dead over a real tunnel.
|
|
17
|
+
|
|
18
|
+
Separately, the headline "move this to <nickname>" trigger was unbuilt: the
|
|
19
|
+
recognizer + planner existed as pure tested units but had no caller, nothing
|
|
20
|
+
persisted the pin, and `SessionRouter.route()` was called with no `topicMetadata`
|
|
21
|
+
— so placement always fell back to least-loaded and a relocation command did
|
|
22
|
+
nothing.
|
|
23
|
+
|
|
24
|
+
## The fix
|
|
25
|
+
|
|
26
|
+
1. **`src/server/middleware.ts`** — exempt `/mesh/rpc` from the bearer gate, next
|
|
27
|
+
to the other signature/HMAC-authed m2m endpoints (`/a2a/inbox`, relay). The
|
|
28
|
+
route is authed SOLELY by its Ed25519 signed, recipient-bound envelope
|
|
29
|
+
(verify → RBAC → nonce, in the dispatcher). This single change is what makes
|
|
30
|
+
the cross-machine pool function over the wire at all.
|
|
31
|
+
|
|
32
|
+
2. **`src/core/TopicPlacementPinStore.ts`** (new) — durable `{topicId → {preferredMachine,
|
|
33
|
+
pinned, updatedAt}}`. `asTopicMetadata()` shapes the pin for
|
|
34
|
+
`PlacementExecutor.decide`; `lastUpdatedAtMs()` feeds the rate-limit guard.
|
|
35
|
+
|
|
36
|
+
3. **`src/commands/server.ts`** — wire the §L4 transfer-by-nickname activation
|
|
37
|
+
(gated on stage ≠ 'dark', BEFORE the route() interception): recognize the
|
|
38
|
+
command → plan → on transfer/noop set the pin AND release local ownership so
|
|
39
|
+
the topic's next message re-places onto the pinned machine via the
|
|
40
|
+
already-wired placeAndClaim → spawnOnMachine → owner-side `onAccepted` resume
|
|
41
|
+
path; confirm-required/reject reply to the user; the command message is
|
|
42
|
+
consumed. And pass `topicMetadata: pinStore.asTopicMetadata(sessionKey)` into
|
|
43
|
+
`route()` so placement honors the pin.
|
|
44
|
+
|
|
45
|
+
## Blast radius
|
|
46
|
+
|
|
47
|
+
- **The auth exemption is the only change to a live, always-on code path.** It
|
|
48
|
+
REMOVES a gate from `/mesh/rpc`; the dispatcher's envelope verification (which
|
|
49
|
+
already ran on every accepted call) becomes the sole auth — strictly stronger
|
|
50
|
+
than a shared bearer for cross-machine (per-peer Ed25519 keys, recipient
|
|
51
|
+
binding, nonce replay protection). A non-mesh / single-machine agent never
|
|
52
|
+
receives a `/mesh/rpc` POST, so the exemption is inert for it.
|
|
53
|
+
- **The activation is dark-gated.** The recognizer/pin/route-metadata wiring only
|
|
54
|
+
runs when `_sessionPoolStage() !== 'dark'` (default dark), so it ships inert and
|
|
55
|
+
is exercised only in the controlled rollout/proof. The pin store is a new file
|
|
56
|
+
under `stateDir/session-pool/`; nothing reads it unless the activation runs.
|
|
57
|
+
- **No new config / route / schema / hook / skill → no migration.** Server +
|
|
58
|
+
middleware + one new core module; existing agents pick it up on the next
|
|
59
|
+
release. Both machines need it (the receiver's `/mesh/rpc` must accept the
|
|
60
|
+
envelope; the router needs the recognizer + pin).
|
|
61
|
+
|
|
62
|
+
## Tests
|
|
63
|
+
|
|
64
|
+
- `tests/unit/mesh-rpc-auth-exemption.test.ts` — `/mesh/rpc` reaches its handler
|
|
65
|
+
with NO Authorization header (and even with a wrong bearer — the envelope is
|
|
66
|
+
the auth); a normal protected path still 401s without a token.
|
|
67
|
+
- `tests/unit/topic-placement-pin-store.test.ts` — set/get/asTopicMetadata/
|
|
68
|
+
lastUpdatedAtMs/clear, durable across instances, corrupt-file tolerance.
|
|
69
|
+
- `tests/unit/transfer-activation-wiring.test.ts` — server boot constructs the
|
|
70
|
+
pin store + recognizer/planner imports, recognizes on inbound before route(),
|
|
71
|
+
applies a transfer (sets pin + releases ownership), returns when handled, passes
|
|
72
|
+
the pin as `topicMetadata`, and is dark-gated.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Side-effects review — secret-externalization hook resolver audit
|
|
2
|
+
|
|
3
|
+
## What changed
|
|
4
|
+
|
|
5
|
+
Two related fleet-wide classes are fixed in one PR.
|
|
6
|
+
|
|
7
|
+
1. **Auth-token reads survive secret-externalization.** Every shipped shell hook, shell script, Node helper, and migrator-emitted template string that resolves the instar server's `authToken` now:
|
|
8
|
+
- Prefers `INSTAR_AUTH_TOKEN` env (set by `SessionManager` per spawned session and by `JobScheduler` per scheduled job — always available inside the Claude Code session).
|
|
9
|
+
- Falls back to reading `config.json` with a string-type guard. When `SecretMigrator` has externalized the token, on-disk `authToken` is the literal placeholder `{ "secret": true }`; the guard rejects it and yields empty so the placeholder cannot leak as a Bearer.
|
|
10
|
+
2. **Port-parse regex tolerates whitespace.** `grep -o '"port":[0-9]*'` is replaced by `grep -oE '"port"[[:space:]]*:[[:space:]]*[0-9]+'` + digit-only extraction. The previous form silently produced empty PORT whenever `config.json` had `"port": 4042` (with space) — and our prettified configs all do — making every history-injection hook exit early on a healthy install. Both bugs together produced the 2026-05-29 silent-history failure.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
The 2026-05-29 incident: Justin's `telegram-topic-context.sh` stopped injecting topic history after the agent compacted. The hook silently 403'd because `authToken` had been moved to the encrypted secret store and the hook was still reading the literal `{ secret: true }` placeholder out of `config.json` as a Bearer token. The agent came back with no topic context, sent an incoherent reply, and Justin had to manually trace the root cause back. This is the SECOND time a script in this class has bitten a deployed agent (the 2026-05-28 `telegram-reply.sh` 403 was the first), so the fix is structural: env-first everywhere + string-type guard + lint that catches the broken pattern + migrator that upgrades stale-on-disk copies.
|
|
15
|
+
|
|
16
|
+
## Risk surface
|
|
17
|
+
|
|
18
|
+
- **Hook scripts (always-overwritten):** `session-start.sh`, `compaction-recovery.sh`, `telegram-topic-context.sh`. Existing agents get the fix on first auto-update. The canonical content lives inside `PostUpdateMigrator.getHookContent()`; the parallel `src/templates/hooks/*.sh` files are kept in sync (the manifest tracks their SHA256). Risk: low — these are already overwritten on every migration tick.
|
|
19
|
+
- **Reply scripts (SHA-based migration):** `telegram-reply.sh` upgrades via `migrateReplyScriptToPortConfig` (pre-PR SHA `371d7e8f…` is already in the prior-shipped allowlist; agents get the fix on first auto-update). `slack-reply.sh` and `whatsapp-reply.sh` upgrade via `migrateReplyScriptTo408`, extended to also fire when `INSTAR_AUTH_TOKEN` is missing — previously the function skipped any script with HTTP 408 handling, which would have stranded a 408-aware-but-auth-broken copy forever. Risk: low — the marker-string check still protects custom forks.
|
|
20
|
+
- **Auxiliary scripts (new migration):** `imessage-reply.sh`, `serendipity-capture.sh`, `slack-channel-context.sh` get a new `migrateSecretExternalizationSurvivability` pass that upgrades shipped copies and skips custom forks (shipped-marker check). Risk: low — guarded by both the marker check and the negative `INSTAR_AUTH_TOKEN` check.
|
|
21
|
+
- **CLI/in-process callers:** `gate.ts` and `nuke.ts` did raw `JSON.parse(fs.readFileSync('config.json'))` then `cfg.authToken` directly. Patched to env-first + string-type-guard. The other CLI commands go through `loadConfig()` (which calls `mergeConfigWithSecrets`) and are already safe. Risk: low — the change is the same env-first/string-guard pattern.
|
|
22
|
+
- **Daemons:** `listener-daemon.ts` (HMAC-derives a signing key from `authToken`), `mcp-stdio-entry.ts` (uses `authToken` as the inbound Bearer). Both now env-first + string-type-guard. Risk: low — the env-first path is preferred when the daemon is launched by the spawn pipeline (which exports the var); the disk fallback still works for plaintext-config installs.
|
|
23
|
+
- **CI / lint:** new unit test `secret-externalization-hook-resolver-lint.test.ts` greps every shell + Node read of `authToken`. The lint MUST go green on the fixed tree (verified) and MUST fail on any re-introduction of the broken pattern (verified via destructive negative test). Risk: low — the lint is allowlisted for in-process `loadConfig` callers; future authors won't trip on the safe path.
|
|
24
|
+
|
|
25
|
+
## Bug surfaces eliminated
|
|
26
|
+
|
|
27
|
+
- Topic-history injection silently 403s after secret externalization. (Direct cause of the 2026-05-29 incident.)
|
|
28
|
+
- `telegram-reply.sh` 403s when invoked from a session whose env is missing — partially mitigated in the 2026-05-28 fix but the audit confirmed the disk-fallback path was still broken; this PR closes both halves.
|
|
29
|
+
- `session-start.sh` could not fetch working-memory or shared-state after externalization (same root cause as the topic-history bug).
|
|
30
|
+
- `compaction-recovery.sh` lost its post-compaction memory query after externalization.
|
|
31
|
+
- A standby-machine daemon whose key derivation went through `authToken` would silently derive a key from `{ secret: true }` and lose peer connectivity. (Speculative — no incident observed, but the path was broken.)
|
|
32
|
+
|
|
33
|
+
## Migration footprint
|
|
34
|
+
|
|
35
|
+
- `migrateHooks()` already always-overwrites the three canonical hooks → existing agents heal automatically on next auto-update.
|
|
36
|
+
- `migrateScripts()` already SHA-migrates `telegram-reply.sh` and now (with the extended INSTAR_AUTH_TOKEN check) auto-migrates `slack-reply.sh` + `whatsapp-reply.sh` when the previously-shipped marker is present.
|
|
37
|
+
- `migrateSecretExternalizationSurvivability()` (NEW) auto-migrates `imessage-reply.sh`, `serendipity-capture.sh`, `slack-channel-context.sh`. Idempotent on the post-fix shape.
|
|
38
|
+
|
|
39
|
+
## Testing
|
|
40
|
+
|
|
41
|
+
- Unit (lint): `tests/unit/secret-externalization-hook-resolver-lint.test.ts` — 3 tests. Greps `src/templates/`, `src/`, `scripts/` for the broken pattern; verified negative (broken pattern caught) and positive (clean pattern passes).
|
|
42
|
+
- Unit (migrator): `tests/unit/secret-externalization-survivability-migrator.test.ts` — 5 tests. Seeds broken state, runs `migrate()`, asserts upgrade. Custom-fork test verifies the marker check protects user-modified scripts.
|
|
43
|
+
- Integration-grade (in `tests/unit/`): `tests/unit/secret-externalization-hook-injection.test.ts` — 2 tests. Spins an in-process stub server (bound to all interfaces; ASYNC `spawn` is required because `execFileSync` blocks Node's event loop and freezes the stub). Seeds `config.json` with the placeholder, runs the canonical migrator-emitted hook, asserts injection works with env-first auth and never leaks the placeholder as a Bearer.
|
|
44
|
+
- E2E: `tests/e2e/secret-externalization-hook-lifecycle.test.ts` — 3 tests. Simulates pre-fix agent → `migrate()` → asserts every shipped script carries the env-first canary; verifies idempotency on a second pass; verifies the canonical migrator-emitted hooks always carry the canary.
|
|
45
|
+
|
|
46
|
+
## Test count
|
|
47
|
+
|
|
48
|
+
13 new tests across 3 tiers (per Testing Integrity Standard). 29 existing related tests (`PostUpdateMigrator-time-injection.test.ts`, `compaction-telegram-context.test.ts`) remain green.
|