instar 1.3.102 → 1.3.103
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/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/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/NEXT.eli16.md +71 -0
- package/upgrades/side-effects/secret-externalization-hook-resolver-audit.md +48 -0
|
@@ -42,9 +42,13 @@ fi
|
|
|
42
42
|
|
|
43
43
|
PORT="${INSTAR_PORT:-4042}"
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
# Auth: env first (SessionManager injects it per spawned session and it
|
|
46
|
+
# survives secret-externalization), legacy plaintext-config fallback with
|
|
47
|
+
# string-type guard so the { "secret": true } placeholder produced by
|
|
48
|
+
# SecretMigrator cannot leak through as a bogus Bearer.
|
|
49
|
+
AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
|
|
50
|
+
if [ -z "$AUTH_TOKEN" ] && [ -f ".instar/config.json" ]; then
|
|
51
|
+
AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
|
|
48
52
|
fi
|
|
49
53
|
|
|
50
54
|
# Escape message for JSON
|
|
@@ -324,7 +324,11 @@ escalate_via_peer() {
|
|
|
324
324
|
node_bin=$(resolve_node || true)
|
|
325
325
|
[ -z "$node_bin" ] && continue
|
|
326
326
|
local peer_meta
|
|
327
|
-
|
|
327
|
+
# The string-type guard on authToken protects against the { secret: true }
|
|
328
|
+
# placeholder that SecretMigrator writes after externalizing the auth token:
|
|
329
|
+
# `c.authToken || ''` would otherwise return the object itself (truthy)
|
|
330
|
+
# which serializes to "[object Object]" — never a valid Bearer.
|
|
331
|
+
peer_meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const t=typeof c.authToken==='string'?c.authToken:'';process.stdout.write((c.port||'')+'\t'+t)" "$peer_dir/.instar/config.json" 2>/dev/null || true)
|
|
328
332
|
peer_port="${peer_meta%% *}"
|
|
329
333
|
peer_auth="${peer_meta##* }"
|
|
330
334
|
[ -z "$peer_port" ] && continue
|
|
@@ -434,11 +438,16 @@ probe_server_identity() {
|
|
|
434
438
|
node_bin=$(resolve_node || true)
|
|
435
439
|
[ -z "$node_bin" ] && return 0
|
|
436
440
|
|
|
437
|
-
# Read port + authToken in one node invocation. The authToken is
|
|
438
|
-
# because /health gates the `project` field behind authentication
|
|
439
|
-
# it the probe can
|
|
441
|
+
# Read port + authToken in one node invocation. The authToken is needed
|
|
442
|
+
# because /health gates the `project` field behind authentication; without
|
|
443
|
+
# it the probe can still hit /health for a basic alive check but loses the
|
|
444
|
+
# identity disambiguation. INSTAR_AUTH_TOKEN env is preferred when set
|
|
445
|
+
# (preserves cross-version compatibility). The string-type guard rejects the
|
|
446
|
+
# { secret: true } placeholder SecretMigrator writes after externalizing the
|
|
447
|
+
# auth token — `String(c.authToken || '')` would otherwise yield
|
|
448
|
+
# "[object Object]" and the Bearer would be invalid.
|
|
440
449
|
local meta
|
|
441
|
-
meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));process.stdout.write(String(c.port||'')+'\t'+
|
|
450
|
+
meta=$("$node_bin" -e "const c=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const t=process.env.INSTAR_AUTH_TOKEN||(typeof c.authToken==='string'?c.authToken:'');process.stdout.write(String(c.port||'')+'\t'+t)" "$config_file" 2>/dev/null || true)
|
|
442
451
|
port="${meta%% *}"
|
|
443
452
|
auth_token="${meta##* }"
|
|
444
453
|
# Lifeline-only agents have no port; treat as healthy from probe POV.
|
|
@@ -47,15 +47,23 @@ if (!token || !mode || mode === '--consume') {
|
|
|
47
47
|
process.exit(2);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// Auth-token resolution: INSTAR_AUTH_TOKEN env first (SessionManager and
|
|
51
|
+
// JobScheduler inject it for every spawned context; survives the
|
|
52
|
+
// secret-externalization refactor that moved authToken out of config.json into
|
|
53
|
+
// the encrypted store), legacy plaintext-config fallback with a string-type
|
|
54
|
+
// guard so the { "secret": true } placeholder produced by SecretMigrator
|
|
55
|
+
// cannot leak through as a bogus Bearer.
|
|
56
|
+
let authToken = process.env.INSTAR_AUTH_TOKEN || '';
|
|
51
57
|
let port;
|
|
52
58
|
try {
|
|
53
59
|
const config = JSON.parse(fs.readFileSync('.instar/config.json', 'utf-8'));
|
|
54
|
-
authToken = config.authToken;
|
|
60
|
+
if (!authToken && typeof config.authToken === 'string') authToken = config.authToken;
|
|
55
61
|
port = config.port;
|
|
56
62
|
} catch (e) {
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
if (!authToken) {
|
|
64
|
+
process.stderr.write('cannot read .instar/config.json: ' + e.message + '\n');
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
// Port resolution: env > config. Mirrors the telegram-reply.sh precedence
|
|
@@ -294,15 +294,21 @@ if patch_file or artifact_type:
|
|
|
294
294
|
# Key derivation: HMAC-SHA256(authToken, "serendipity-v1:" + sessionId)
|
|
295
295
|
signing_key = os.environ.get("SERENDIPITY_SIGNING_KEY", "")
|
|
296
296
|
if not signing_key:
|
|
297
|
-
# Derive from auth token + session
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
297
|
+
# Derive from auth token + session. Token: INSTAR_AUTH_TOKEN env first
|
|
298
|
+
# (SessionManager injects it; survives secret-externalization), legacy
|
|
299
|
+
# plaintext-config fallback with a string-type guard so the
|
|
300
|
+
# { "secret": true } placeholder produced by SecretMigrator cannot leak.
|
|
301
|
+
auth_token = os.environ.get("INSTAR_AUTH_TOKEN", "")
|
|
302
|
+
if not auth_token:
|
|
303
|
+
config_path = os.environ.get("CONFIG_FILE", "")
|
|
304
|
+
if config_path and os.path.exists(config_path):
|
|
305
|
+
try:
|
|
306
|
+
cfg = json.load(open(config_path))
|
|
307
|
+
v = cfg.get("authToken", "")
|
|
308
|
+
if isinstance(v, str):
|
|
309
|
+
auth_token = v
|
|
310
|
+
except:
|
|
311
|
+
pass
|
|
306
312
|
session_id = finding["source"]["sessionId"]
|
|
307
313
|
key_material = f"serendipity-v1:{session_id}"
|
|
308
314
|
signing_key = hmac.new(
|
|
@@ -44,7 +44,14 @@ if [ -f ".instar/config.json" ]; then
|
|
|
44
44
|
CONFIG_PORT=$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('port',''))" 2>/dev/null)
|
|
45
45
|
[ -n "$CONFIG_PORT" ] && PORT="$CONFIG_PORT"
|
|
46
46
|
fi
|
|
47
|
-
|
|
47
|
+
# Auth: env first (SessionManager injects it into spawned sessions; survives
|
|
48
|
+
# secret-externalization), legacy plaintext-config fallback with string-type
|
|
49
|
+
# guard so the { "secret": true } placeholder produced by SecretMigrator
|
|
50
|
+
# cannot leak as a bogus Bearer.
|
|
51
|
+
AUTH="${INSTAR_AUTH_TOKEN:-}"
|
|
52
|
+
if [ -z "$AUTH" ]; then
|
|
53
|
+
AUTH=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
|
|
54
|
+
fi
|
|
48
55
|
fi
|
|
49
56
|
|
|
50
57
|
PORT="${PORT:-4042}"
|
|
@@ -75,9 +75,13 @@ if [ -z "$MSG" ]; then
|
|
|
75
75
|
fi
|
|
76
76
|
|
|
77
77
|
# Resolve config-derived values from .instar/config.json (single python3
|
|
78
|
-
# invocation). Env > config > 4040-warn for port
|
|
79
|
-
#
|
|
80
|
-
|
|
78
|
+
# invocation). Env > config > 4040-warn for port. Auth: INSTAR_AUTH_TOKEN env
|
|
79
|
+
# first (SessionManager injects it per spawned session; survives the
|
|
80
|
+
# secret-externalization refactor that moved authToken out of config.json into
|
|
81
|
+
# the encrypted store), legacy plaintext-config fallback with a string-type
|
|
82
|
+
# guard so the { "secret": true } placeholder produced by SecretMigrator
|
|
83
|
+
# cannot leak through as a bogus Bearer.
|
|
84
|
+
AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
|
|
81
85
|
AGENT_ID=""
|
|
82
86
|
CONFIG_PORT=""
|
|
83
87
|
if [ -f ".instar/config.json" ]; then
|
|
@@ -87,11 +91,13 @@ try:
|
|
|
87
91
|
c = json.load(open('.instar/config.json'))
|
|
88
92
|
except Exception:
|
|
89
93
|
sys.exit(0)
|
|
90
|
-
|
|
94
|
+
v = c.get('authToken', '')
|
|
95
|
+
print(v if isinstance(v, str) else '')
|
|
91
96
|
print(c.get('projectName', ''))
|
|
92
97
|
print(c.get('port', ''))
|
|
93
98
|
" 2>/dev/null)
|
|
94
|
-
|
|
99
|
+
CONFIG_AUTH=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '1p')
|
|
100
|
+
[ -z "$AUTH_TOKEN" ] && AUTH_TOKEN="$CONFIG_AUTH"
|
|
95
101
|
AGENT_ID=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '2p')
|
|
96
102
|
CONFIG_PORT=$(printf '%s\n' "$CONFIG_VALUES" | sed -n '3p')
|
|
97
103
|
fi
|
|
@@ -33,10 +33,13 @@ fi
|
|
|
33
33
|
|
|
34
34
|
PORT="${INSTAR_PORT:-4040}"
|
|
35
35
|
|
|
36
|
-
# Read auth token
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
# Read auth token: env first (SessionManager injects it per spawned session and
|
|
37
|
+
# it survives secret-externalization), legacy plaintext-config fallback with
|
|
38
|
+
# string-type guard so the { "secret": true } placeholder produced by
|
|
39
|
+
# SecretMigrator cannot leak through as a bogus Bearer.
|
|
40
|
+
AUTH_TOKEN="${INSTAR_AUTH_TOKEN:-}"
|
|
41
|
+
if [ -z "$AUTH_TOKEN" ] && [ -f ".instar/config.json" ]; then
|
|
42
|
+
AUTH_TOKEN=$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)
|
|
40
43
|
fi
|
|
41
44
|
|
|
42
45
|
# Escape for JSON
|
|
@@ -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,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,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.
|