instar 1.2.4 → 1.2.6
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/init.d.ts.map +1 -1
- package/dist/commands/init.js +37 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +46 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts +68 -0
- package/dist/server/CapabilityIndex.d.ts.map +1 -0
- package/dist/server/CapabilityIndex.js +711 -0
- package/dist/server/CapabilityIndex.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +17 -403
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +70 -62
- package/src/scaffold/templates.ts +2 -1
- package/src/templates/scripts/secret-drop-retrieve.mjs +125 -0
- package/upgrades/1.2.5.md +56 -0
- package/upgrades/1.2.6.md +42 -0
- package/upgrades/side-effects/capabilities-introspection.md +119 -0
- package/upgrades/side-effects/secret-drop-hardened-retrieve-template.md +123 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* secret-drop-retrieve.mjs — fetch a Secret Drop submission and stream the
|
|
4
|
+
* requested field value to stdout WITHOUT ever printing the response body.
|
|
5
|
+
* All informational output goes to stderr and is limited to field NAMES,
|
|
6
|
+
* lengths, and HTTP status — never values.
|
|
7
|
+
*
|
|
8
|
+
* Hardened replacement for the ad-hoc `curl + jq` / `curl + python` pattern
|
|
9
|
+
* that historically leaked credentials in plaintext into the Bash tool
|
|
10
|
+
* transcript. The lesson: when probing an unknown JSON shape, NEVER fall
|
|
11
|
+
* back to `console.log(JSON.stringify(body))` — that's how plaintext
|
|
12
|
+
* credentials end up in shell history, session transcripts, and downstream
|
|
13
|
+
* LLM context.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node .instar/scripts/secret-drop-retrieve.mjs <token> <field-name>
|
|
17
|
+
* → prints the field value to stdout (single line, no trailing newline)
|
|
18
|
+
* → suitable for piping: `... password | gh secret set X`
|
|
19
|
+
*
|
|
20
|
+
* node .instar/scripts/secret-drop-retrieve.mjs <token> --names
|
|
21
|
+
* → prints field names + lengths to stderr; nothing to stdout
|
|
22
|
+
* → use to discover what fields the submission contains
|
|
23
|
+
*
|
|
24
|
+
* node .instar/scripts/secret-drop-retrieve.mjs <token> <field> --consume
|
|
25
|
+
* → opt into one-shot semantics (the submission is removed after read).
|
|
26
|
+
* Default is peek (non-destructive) per the 2026-05-20 hardening.
|
|
27
|
+
*
|
|
28
|
+
* Config:
|
|
29
|
+
* Reads `authToken` and `port` from `.instar/config.json` relative to cwd.
|
|
30
|
+
* `INSTAR_PORT` env var overrides the config port.
|
|
31
|
+
*
|
|
32
|
+
* Exit codes:
|
|
33
|
+
* 0 — value or names printed successfully
|
|
34
|
+
* 1 — HTTP error, missing field, or unexpected response shape
|
|
35
|
+
* 2 — usage error (missing args, cannot read config)
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
|
|
40
|
+
const args = process.argv.slice(2);
|
|
41
|
+
const token = args[0];
|
|
42
|
+
const mode = args[1];
|
|
43
|
+
const consume = args.includes('--consume');
|
|
44
|
+
|
|
45
|
+
if (!token || !mode || mode === '--consume') {
|
|
46
|
+
process.stderr.write('usage: secret-drop-retrieve.mjs <token> <field-name|--names> [--consume]\n');
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let authToken;
|
|
51
|
+
let port;
|
|
52
|
+
try {
|
|
53
|
+
const config = JSON.parse(fs.readFileSync('.instar/config.json', 'utf-8'));
|
|
54
|
+
authToken = config.authToken;
|
|
55
|
+
port = config.port;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
process.stderr.write('cannot read .instar/config.json: ' + e.message + '\n');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Port resolution: env > config. Mirrors the telegram-reply.sh precedence
|
|
62
|
+
// so a single explicit INSTAR_PORT override flows through every agent script.
|
|
63
|
+
const resolvedPort = process.env.INSTAR_PORT || port;
|
|
64
|
+
if (!resolvedPort) {
|
|
65
|
+
process.stderr.write('cannot resolve port: no INSTAR_PORT env and no port in .instar/config.json\n');
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const url = `http://localhost:${resolvedPort}/secrets/retrieve/${token}${consume ? '?consume=true' : ''}`;
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Authorization': `Bearer ${authToken}` },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
// Surface ONLY status + the structured error message field, never the body.
|
|
77
|
+
let errMsg = '(no error message)';
|
|
78
|
+
try {
|
|
79
|
+
const body = await res.json();
|
|
80
|
+
if (typeof body?.error === 'string') errMsg = body.error;
|
|
81
|
+
} catch {
|
|
82
|
+
// Don't fall back to printing raw response body — that's the leak we're
|
|
83
|
+
// explicitly hardening against. Just report the HTTP status.
|
|
84
|
+
}
|
|
85
|
+
process.stderr.write(`HTTP ${res.status}: ${errMsg}\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const body = await res.json();
|
|
90
|
+
|
|
91
|
+
// Locate the values bag. The server's known shape is body.values; defend
|
|
92
|
+
// against future shape changes by checking the two most-likely nestings,
|
|
93
|
+
// but NEVER print the body itself.
|
|
94
|
+
const values = body?.values
|
|
95
|
+
?? body?.submission?.values
|
|
96
|
+
?? body?.fields
|
|
97
|
+
?? null;
|
|
98
|
+
|
|
99
|
+
if (!values || typeof values !== 'object') {
|
|
100
|
+
process.stderr.write('retrieve: response did not contain a values bag (keys at top: '
|
|
101
|
+
+ Object.keys(body || {}).join(', ') + ')\n');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (mode === '--names') {
|
|
106
|
+
// Field-name listing for debugging — names + lengths only, never values.
|
|
107
|
+
for (const k of Object.keys(values)) {
|
|
108
|
+
const v = values[k];
|
|
109
|
+
const len = typeof v === 'string' ? v.length : -1;
|
|
110
|
+
process.stderr.write(` ${k} (length ${len})\n`);
|
|
111
|
+
}
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const v = values[mode];
|
|
116
|
+
if (typeof v !== 'string') {
|
|
117
|
+
process.stderr.write(`retrieve: field '${mode}' not found (available: `
|
|
118
|
+
+ Object.keys(values).join(', ') + ')\n');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Stream the value to stdout, no trailing newline so it pipes cleanly into
|
|
123
|
+
// `gh secret set` or `bw unlock --raw`. process.stdout.write (not console.log)
|
|
124
|
+
// — no extra newline, no trailing whitespace to confuse downstream consumers.
|
|
125
|
+
process.stdout.write(v);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: minor -->
|
|
4
|
+
<!-- minor = new agent-facing capability without breaking changes -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
**feat(secret-drop): ship the hardened retrieve helper as a template — and rewrite every line of agent guidance that previously taught the unsafe pattern.**
|
|
9
|
+
|
|
10
|
+
The 2026-05-20 incident exposed a credential-leak class: when an agent retrieved a Secret Drop submission via a raw `curl /secrets/retrieve/TOKEN`, the response body — including the secret value — landed in the Bash tool transcript. The fix existed as a one-off `secret-drop-retrieve.mjs` on a single agent's machine, but no other agent had it, and every line of agent-facing guidance (CLAUDE.md template, the spawned-session system message, the stuck-consumer retry message) still recommended the unsafe `curl` pattern.
|
|
11
|
+
|
|
12
|
+
This release closes the gap on six interlocking surfaces:
|
|
13
|
+
|
|
14
|
+
1. **Ship the hardened helper as a template.** `src/templates/scripts/secret-drop-retrieve.mjs` streams the requested field value to stdout, prints field NAMES + lengths to stderr, and structurally refuses to print the response body. New agents get it via `installSecretDropRetrieve` (called from three `init.ts` paths next to `installSerendipityCapture`).
|
|
15
|
+
|
|
16
|
+
2. **Migrate existing agents.** `PostUpdateMigrator.migrateScripts` always-overwrites `.instar/scripts/secret-drop-retrieve.mjs` on every update (same pattern as `convergence-check.sh`). Idempotent — running the migrator twice produces identical content.
|
|
17
|
+
|
|
18
|
+
3. **Rewrite the spawn message.** `src/server/routes.ts` no longer instructs the agent to `curl ... /secrets/retrieve/TOKEN` when a Secret Drop arrives. The new message points at `node .instar/scripts/secret-drop-retrieve.mjs TOKEN <field-name>` with an explicit warning against the raw curl pattern.
|
|
19
|
+
|
|
20
|
+
4. **Rewrite the stuck-consumer retry message.** Same hardened command form, same explicit warning.
|
|
21
|
+
|
|
22
|
+
5. **Rewrite the CLAUDE.md template.** `src/scaffold/templates.ts` documents the hardened command as the required retrieval pattern, with a one-line explanation of the leak class.
|
|
23
|
+
|
|
24
|
+
6. **Rewrite existing CLAUDE.md files.** `PostUpdateMigrator.migrateClaudeMd` detects the legacy `curl /secrets/retrieve/TOKEN` line (port-tolerant — matches any local port literal) and replaces it with the hardened guidance. Idempotent: a CLAUDE.md that already documents `secret-drop-retrieve.mjs` is left alone.
|
|
25
|
+
|
|
26
|
+
7. **Update the /capabilities `secrets.retrievalHint`.** The hint now gives the actual command form so an agent reading `/capabilities` learns the safe pattern directly from the discovery surface.
|
|
27
|
+
|
|
28
|
+
## What to Tell Your User
|
|
29
|
+
|
|
30
|
+
No user-visible behavior change. Agents will start using the hardened retrieve helper automatically — the next time they receive a Secret Drop submission, the spawned session reads the hardened command form from the system message, not the unsafe curl. Existing agents pick up the helper script and the CLAUDE.md rewrite on the next instar update run.
|
|
31
|
+
|
|
32
|
+
If you were ever shown the unsafe curl-against-secrets-retrieve pattern in chat, that guidance has been retired across every surface; the hardened helper is the only documented path now.
|
|
33
|
+
|
|
34
|
+
## Summary of New Capabilities
|
|
35
|
+
|
|
36
|
+
For the agent:
|
|
37
|
+
|
|
38
|
+
- `.instar/scripts/secret-drop-retrieve.mjs TOKEN field-name` — streams the value to stdout, never prints the body.
|
|
39
|
+
- `.instar/scripts/secret-drop-retrieve.mjs TOKEN --names` — discover field names + lengths.
|
|
40
|
+
- `.instar/scripts/secret-drop-retrieve.mjs TOKEN field-name --consume` — opt into one-shot semantics.
|
|
41
|
+
|
|
42
|
+
## Evidence
|
|
43
|
+
|
|
44
|
+
The leak class was reproduced before the fix: running `curl -X POST -H "Authorization: Bearer $AUTH" http://localhost:4042/secrets/retrieve/<token>` against a known submission prints the full JSON `{"values":{"password":"actual-secret-here"}, ...}` to stdout, which lands in the Bash tool transcript.
|
|
45
|
+
|
|
46
|
+
After the fix, the hardened helper isolates the value path: `node .instar/scripts/secret-drop-retrieve.mjs <token> password` streams only `actual-secret-here` to stdout (no newline, pipeable), and prints nothing else. The `--names` mode prints only `password (length 16)` to stderr; stdout stays empty.
|
|
47
|
+
|
|
48
|
+
New tests:
|
|
49
|
+
- `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` — 8 cases covering script install (presence, executable bit, content invariants, idempotency) + CLAUDE.md rewrite (legacy → hardened, idempotent, port-tolerant).
|
|
50
|
+
- The existing `tests/unit/capabilities-discoverability.test.ts` continues to pass with the updated `secrets.retrievalHint` string.
|
|
51
|
+
|
|
52
|
+
Spec: `docs/specs/secret-drop-hardened-retrieve-template.md`
|
|
53
|
+
ELI16: `docs/specs/secret-drop-hardened-retrieve-template.eli16.md`
|
|
54
|
+
Side-effects: `upgrades/side-effects/secret-drop-hardened-retrieve-template.md`
|
|
55
|
+
|
|
56
|
+
Origin: 2026-05-20 leak incident (topic 9984) + 2026-05-21 case-study audit (topic 11141, follow-up #1).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: minor -->
|
|
4
|
+
<!-- minor = internal refactor with new test surface, no behavioral change -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
**refactor(capabilities): /capabilities and its lint now share one source of truth — src/server/CapabilityIndex.ts.**
|
|
9
|
+
|
|
10
|
+
PR #290 surfaced the discoverability gap that started the 2026-05-21 case study. PR #292 closed the workaround-reflex side by retiring the unsafe Secret Drop retrieval pattern. This release closes the last loose end: the /capabilities handler used to be a 440-line hand-curated object literal in routes.ts, and the discoverability lint kept a parallel hand-written INTERNAL_ALLOWLIST. Two surfaces had to stay in sync; nothing structurally enforced the sync.
|
|
11
|
+
|
|
12
|
+
After this release:
|
|
13
|
+
|
|
14
|
+
- A new module, src/server/CapabilityIndex.ts, holds the full registry — one entry per capability with a typed build function plus an INTERNAL_PREFIXES allowlist for operator-only routes.
|
|
15
|
+
- The /capabilities handler in routes.ts shrinks from ~440 lines of inline logic to ~25 lines that iterate the index.
|
|
16
|
+
- The discoverability lint in tests/unit/capabilities-discoverability.test.ts imports CAPABILITY_INDEX and INTERNAL_PREFIXES directly instead of duplicating policy in test code.
|
|
17
|
+
- A new CapabilityIndex unit-test file pins module-level invariants: every entry key is unique, every prefix is unique across the registry, INTERNAL_PREFIXES has a reason on every entry, and the secrets entry continues to surface the hardened-retrieval hint.
|
|
18
|
+
|
|
19
|
+
The response shape is unchanged. Every key that appeared in /capabilities before still appears with the same nested fields. Existing consumers (agents, dashboard, integration tests) see identical output.
|
|
20
|
+
|
|
21
|
+
## What to Tell Your User
|
|
22
|
+
|
|
23
|
+
No user-visible behavior change. Adding a new top-level route prefix to instar now requires a single deliberate classification in CapabilityIndex.ts — either claim it under a CAPABILITY_INDEX entry (surfaces it to agents) or add it to INTERNAL_PREFIXES with a one-line reason (skips discovery). The lint refuses unclassified prefixes at CI time, so the next primitive cannot slip through silently.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
For the agent: nothing new at runtime. The /capabilities response carries the same data; the way it is produced is now a single iteration over a typed array.
|
|
28
|
+
|
|
29
|
+
For contributors: a new file to maintain, but only when adding or renaming top-level route prefixes. The compiler enforces shape; the lint enforces classification.
|
|
30
|
+
|
|
31
|
+
## Evidence
|
|
32
|
+
|
|
33
|
+
Behavioral parity verified:
|
|
34
|
+
- The 76-case integration suite that touches /capabilities (tests/integration/view-tunnel-routes.test.ts, tests/integration/publishing-routes.test.ts, tests/integration/external-operation-safety-routes.test.ts, tests/integration/imessage-routes.test.ts) passes unchanged.
|
|
35
|
+
- The discoverability lint shrunk from 81 cases (with a hand-written allowlist) to 84 cases that read from the source-of-truth module. All green.
|
|
36
|
+
- New CapabilityIndex.test.ts pins 9 invariants on the registry itself. All green.
|
|
37
|
+
|
|
38
|
+
Spec: docs/specs/capabilities-introspection.md
|
|
39
|
+
ELI16: docs/specs/capabilities-introspection.eli16.md
|
|
40
|
+
Side-effects: upgrades/side-effects/capabilities-introspection.md
|
|
41
|
+
|
|
42
|
+
Origin: 2026-05-21 case-study audit (topic 11141, follow-up #2 of two).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Side-Effects Review — Capabilities Introspection
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `capabilities-introspection`
|
|
4
|
+
**Date:** `2026-05-21`
|
|
5
|
+
**Author:** `Echo (instar-developing agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required` (pure refactor; response shape preserved; no new decision-point surface)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Extracts `src/server/CapabilityIndex.ts` as the single source of truth for the /capabilities self-discovery surface AND the discoverability lint that PR #290 introduced. The /capabilities handler shrinks from ~440 lines of inline build logic to ~25 lines that iterate the index. The lint rewrites to import the registry directly instead of maintaining a parallel hand-written allowlist. Adds a CapabilityIndex unit-test file that pins module-level invariants.
|
|
11
|
+
|
|
12
|
+
Files touched:
|
|
13
|
+
|
|
14
|
+
- `src/server/CapabilityIndex.ts` — new module (the registry).
|
|
15
|
+
- `src/server/routes.ts` — /capabilities handler refactor.
|
|
16
|
+
- `tests/unit/capabilities-discoverability.test.ts` — switch from inline allowlist to imports.
|
|
17
|
+
- `tests/unit/CapabilityIndex.test.ts` — new module-level invariant tests.
|
|
18
|
+
- `src/data/builtin-manifest.json` — regenerated hashes.
|
|
19
|
+
- `upgrades/NEXT.md` — release notes.
|
|
20
|
+
- `docs/specs/capabilities-introspection.{md,eli16.md}` — spec + ELI16.
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
- /capabilities response builder — **modify (refactor)** — same output, different production path.
|
|
25
|
+
- Discoverability lint — **modify** — same invariant, different source of truth.
|
|
26
|
+
|
|
27
|
+
No new gate/block surface. No runtime decisions changed.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
34
|
+
|
|
35
|
+
No block/allow surface — over-block not applicable. The lint refuses CI for unclassified route prefixes (the desired behavior). A new route prefix that the author forgot to classify fails the lint with a precise error pointing at CapabilityIndex.ts.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Under-block
|
|
40
|
+
|
|
41
|
+
**What failure modes does this still miss?**
|
|
42
|
+
|
|
43
|
+
The lint catches the structural gap (prefix not classified) but does not validate the SEMANTIC correctness of a classification — a contributor could claim a public agent-facing route under INTERNAL_PREFIXES with a bogus reason and the lint would pass. The reason field is reviewer-enforced, not machine-enforced. Acceptable: the reviewer is the right authority for semantic intent; the machine's job is to ensure the choice is made and documented.
|
|
44
|
+
|
|
45
|
+
The CapabilityIndex.test.ts regression guard for the secrets entry's retrievalHint is intentionally narrow — it asserts the hint mentions `secret-drop-retrieve.mjs` and "NEVER prints the response body." A reviewer could weaken the hint in other ways that still pass the regex (e.g., remove the explicit warning against raw curl). That's a residual risk; the broader feature-delivery-completeness lint catches the related CLAUDE.md → /capabilities parity.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. Level-of-abstraction fit
|
|
50
|
+
|
|
51
|
+
**Is this at the right layer?**
|
|
52
|
+
|
|
53
|
+
Yes. The CapabilityIndex module sits in `src/server/` alongside routes.ts and SecretDrop.ts. It depends on the route-context type via a `type-only` import to avoid a circular module dependency at runtime. The lint sits in `tests/unit/` and imports from `src/server/` as a regular dependency, which is the standard test-to-source direction.
|
|
54
|
+
|
|
55
|
+
A higher-level abstraction (e.g., deriving CAPABILITY_INDEX from the Express router at runtime) would not replace the classification step — the router doesn't carry semantic intent. The static registry is the right shape.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 4. Signal vs authority compliance
|
|
60
|
+
|
|
61
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
62
|
+
|
|
63
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
64
|
+
|
|
65
|
+
- [x] No — this change has no block/allow surface at runtime.
|
|
66
|
+
|
|
67
|
+
The lint has CI-time block authority but only via the CI gate's enforcement; it emits a signal that a smart authority (the reviewer + branch protection) acts on. The lint's logic is straightforward set arithmetic — not a brittle heuristic.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 5. Interactions
|
|
72
|
+
|
|
73
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
74
|
+
|
|
75
|
+
- **Shadowing:** none. The /capabilities handler is the only consumer of the registry at runtime; the lint is the only consumer at test time.
|
|
76
|
+
- **Double-fire:** none. Each invocation produces a fresh map; no shared mutable state.
|
|
77
|
+
- **Races:** none. CAPABILITY_INDEX is a frozen const at module load; the build functions are pure.
|
|
78
|
+
- **Feedback loops:** none. /capabilities reads from the registry; nothing writes back.
|
|
79
|
+
|
|
80
|
+
The refactor preserves all existing consumers of /capabilities. Concretely verified: `tests/integration/view-tunnel-routes.test.ts` (privateViewer assertion), `tests/integration/publishing-routes.test.ts` (publishing assertion), `tests/integration/external-operation-safety-routes.test.ts` (externalOperationSafety assertion), `tests/integration/imessage-routes.test.ts` (imessage assertion) — all 76 cases pass unchanged.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 6. External surfaces
|
|
85
|
+
|
|
86
|
+
**Does this change anything visible outside the immediate code path?**
|
|
87
|
+
|
|
88
|
+
- **Agents on the same machine:** see the same /capabilities response. Field names, nesting, values, iteration order all preserved.
|
|
89
|
+
- **Dashboard:** unchanged. The dashboard reads the JSON and renders selected fields; field names are stable.
|
|
90
|
+
- **External systems:** none touched.
|
|
91
|
+
- **Persistent state:** none introduced.
|
|
92
|
+
- **Timing / runtime conditions:** no measurable latency change. The iteration over CAPABILITY_INDEX is O(n) with n=~30; the prior inline literal was also O(n) at JIT eval time.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 7. Rollback cost
|
|
97
|
+
|
|
98
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
99
|
+
|
|
100
|
+
Pure code change. Revert the three new files (CapabilityIndex.ts, CapabilityIndex.test.ts, the spec/artifact docs) and the routes.ts diff. The lint test reverts to its prior body. No persistent state, no agent-state repair, no user-visible regression during the rollback window.
|
|
101
|
+
|
|
102
|
+
Realistic rollback: revert + patch release. Under 5 minutes.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Conclusion
|
|
107
|
+
|
|
108
|
+
This is a low-risk refactor that closes the parallel-surfaces gap introduced by PR #290. The /capabilities response shape is preserved byte-for-byte; the lint is now anchored to a typed module instead of a hand-maintained test allowlist; module-level invariants are pinned by a new test file. No second-pass review required.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Evidence pointers
|
|
113
|
+
|
|
114
|
+
- Spec: `docs/specs/capabilities-introspection.md`
|
|
115
|
+
- ELI16: `docs/specs/capabilities-introspection.eli16.md`
|
|
116
|
+
- New tests: `tests/unit/CapabilityIndex.test.ts` (9 cases), refactored `tests/unit/capabilities-discoverability.test.ts` (84 cases). All green.
|
|
117
|
+
- Integration tests that exercise /capabilities (76 cases) pass unchanged.
|
|
118
|
+
- `npx tsc --noEmit` clean.
|
|
119
|
+
- Origin: 2026-05-21 case-study audit (topic 11141, follow-up #2 of two).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Side-Effects Review — Secret Drop Hardened Retrieve Template
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `secret-drop-hardened-retrieve-template`
|
|
4
|
+
**Date:** `2026-05-21`
|
|
5
|
+
**Author:** `Echo (instar-developing agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required` (template install + documentation rewrite; no new gate/block authority)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships the hardened `secret-drop-retrieve.mjs` as an `src/templates/scripts/` template; wires it into `init.ts` (for new agents) and `PostUpdateMigrator.migrateScripts` (for existing agents); rewrites the `[secret-drop-received]` spawn message, the `[secret-drop-stuck]` retry message, the CLAUDE.md template, and the `/capabilities.secrets.retrievalHint` to instruct the hardened command form. Adds a `migrateClaudeMd` block that detects the legacy `curl /secrets/retrieve/TOKEN` line and rewrites it in place; idempotent and port-tolerant.
|
|
11
|
+
|
|
12
|
+
Files touched:
|
|
13
|
+
|
|
14
|
+
- `src/templates/scripts/secret-drop-retrieve.mjs` — new template.
|
|
15
|
+
- `src/commands/init.ts` — `installSecretDropRetrieve` + three call sites.
|
|
16
|
+
- `src/core/PostUpdateMigrator.ts` — `migrateScripts` block (always-overwrite); `migrateClaudeMd` block (port-tolerant rewrite).
|
|
17
|
+
- `src/server/routes.ts` — spawn message + stuck-consumer retry message + `/capabilities.secrets.retrievalHint`.
|
|
18
|
+
- `src/scaffold/templates.ts` — CLAUDE.md Secret Drop documentation block.
|
|
19
|
+
- `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` — new tests (8 cases).
|
|
20
|
+
- `upgrades/NEXT.md` — release notes (minor bump).
|
|
21
|
+
- `docs/specs/secret-drop-hardened-retrieve-template.{md,eli16.md}` — spec + ELI16.
|
|
22
|
+
|
|
23
|
+
## Decision-point inventory
|
|
24
|
+
|
|
25
|
+
- `[secret-drop-received]` spawn message — **modify** — guidance the agent reads to learn how to retrieve. Unsafe → hardened.
|
|
26
|
+
- `[secret-drop-stuck]` retry message — **modify** — same retrieval path on the retry branch.
|
|
27
|
+
- CLAUDE.md Secret Drop block (template + migrator) — **modify** — agent's static retrieval guidance.
|
|
28
|
+
- `/capabilities.secrets.retrievalHint` — **modify** — discovery-surface guidance.
|
|
29
|
+
|
|
30
|
+
No new gate/block surface introduced. The `/secrets/retrieve` endpoint behavior is unchanged.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. Over-block
|
|
35
|
+
|
|
36
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
37
|
+
|
|
38
|
+
No block/allow surface — over-block not applicable. The migrator's CLAUDE.md rewrite is the closest thing to a block: it refuses to touch a CLAUDE.md that already documents `secret-drop-retrieve.mjs` (idempotent skip). That's the desired behavior — over-block would require the migrator to refuse legitimate rewrites, which it does not.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
|
|
44
|
+
**What failure modes does this still miss?**
|
|
45
|
+
|
|
46
|
+
The migrator's regex matches the exact legacy-line pattern. A CLAUDE.md that someone manually customized to use a different unsafe pattern (e.g., `wget` instead of `curl`, or a piped one-liner) would slip past the rewrite. Acceptable: the rewrite is the structural fix for the documented unsafe pattern; bespoke unsafe variants are out of scope and would be visible to anyone reading their own CLAUDE.md.
|
|
47
|
+
|
|
48
|
+
The hardened helper itself still reads `.instar/config.json` for the authToken. If the config file is missing, the script exits 2 with a clear stderr message; if the file is present but `authToken` is empty, the script does a request with an empty bearer and the server returns 401 — the script prints the 401 status (no body). Both paths are safe.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. Level-of-abstraction fit
|
|
53
|
+
|
|
54
|
+
**Is this at the right layer?**
|
|
55
|
+
|
|
56
|
+
The change lives at the agent-facing instruction layer, which is the correct level. Three reasons:
|
|
57
|
+
|
|
58
|
+
1. The leak is caused by the agent running a documented command verbatim. The fix is to make the documented command safe.
|
|
59
|
+
2. Adding a server-side gate (e.g., refuse `/secrets/retrieve` without a hardened-client header) would create a brittle detector with block authority — violating the signal-vs-authority principle. The server stays permissive; the agent layer is hardened by making safe the only documented path.
|
|
60
|
+
3. The hardened helper itself is a thin wrapper around `fetch`. It doesn't introduce new architecture; it constrains how the value flows out of the response.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 4. Signal vs authority compliance
|
|
65
|
+
|
|
66
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
67
|
+
|
|
68
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
69
|
+
|
|
70
|
+
- [x] No — this change has no block/allow surface at runtime.
|
|
71
|
+
|
|
72
|
+
The migrator's CLAUDE.md rewrite has commit-time block authority via the precommit hook, but only by virtue of being a code change; the migrator itself does not refuse anything at runtime. The hardened helper has no gate logic — it just streams a value or exits with an error code.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 5. Interactions
|
|
77
|
+
|
|
78
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
79
|
+
|
|
80
|
+
- **Shadowing:** the `[secret-drop-received]` spawn message replaces the existing one verbatim (no append, no shadow). The existing 5-minute cleanup timer and the stuck-consumer event from PR #288 are unchanged — both rely on server-side state, not on the system-message content.
|
|
81
|
+
- **Double-fire:** the migrator runs each of `migrateScripts` and `migrateClaudeMd` once per `instar update` invocation. Neither path is recursive.
|
|
82
|
+
- **Races:** the migrator runs single-threaded against an agent that is not yet started; no concurrent writers to the files it touches.
|
|
83
|
+
- **Feedback loops:** none. The agent reading the new guidance produces no input back into the migrator.
|
|
84
|
+
|
|
85
|
+
The `PostUpdateMigrator.loadRelayTemplate` helper is reused (not duplicated) for loading the new template. Symmetric with `getTelegramReplyScript`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 6. External surfaces
|
|
90
|
+
|
|
91
|
+
**Does this change anything visible outside the immediate code path?**
|
|
92
|
+
|
|
93
|
+
- **Agents on the same machine:** yes — every existing agent picks up `.instar/scripts/secret-drop-retrieve.mjs` on the next update, and any agent whose CLAUDE.md still has the legacy retrieve line gets it rewritten in place. The behavioral change is that the agent learns a safer command form. The unsafe `/secrets/retrieve` endpoint continues to work unchanged for any caller (including custom integrations); only the guidance is updated.
|
|
94
|
+
- **Other agents on the install base:** same — Codex / Gemini agents that read CLAUDE.md / AGENTS.md / GEMINI.md get the rewrite via `migrateFrameworkShadowCapabilities` (existing migrator path that mirrors CLAUDE.md changes to non-Claude shadows).
|
|
95
|
+
- **External systems:** no external surface touched.
|
|
96
|
+
- **Persistent state:** none beyond the file install (which is by design).
|
|
97
|
+
- **Timing / runtime conditions:** the new helper makes one `fetch` call per invocation; same latency profile as the curl it replaces.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 7. Rollback cost
|
|
102
|
+
|
|
103
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
104
|
+
|
|
105
|
+
Pure code change. Revert the migrator + init + templates + spec/artifact files. The script previously installed by the migrator stays on disk for existing agents (the rollback doesn't delete). The CLAUDE.md rewrites previously applied to existing agents also stay — which is fine because the hardened guidance points at a script that still exists (the install path doesn't go away during a code-only rollback).
|
|
106
|
+
|
|
107
|
+
Realistic rollback: revert + patch release. Under 5 minutes. The only concern would be a downstream agent that started depending on a quirk of the hardened helper (e.g., expecting an exact stderr format). The script's output contract is documented in the file header; downstream tooling using it should pin against that, not against ephemeral implementation details.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Conclusion
|
|
112
|
+
|
|
113
|
+
This is a low-risk hardening change focused on agent-facing guidance. It closes the structural side of the 2026-05-20 leak class: every documented path now teaches the safe pattern. The runtime change is a script install + a sequence of string rewrites; no new decision points introduced; signal-vs-authority preserved. No second-pass review required.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Evidence pointers
|
|
118
|
+
|
|
119
|
+
- Spec: `docs/specs/secret-drop-hardened-retrieve-template.md`
|
|
120
|
+
- ELI16: `docs/specs/secret-drop-hardened-retrieve-template.eli16.md`
|
|
121
|
+
- Tests: `tests/unit/PostUpdateMigrator-secretDropHardenedRetrieve.test.ts` (8 cases, all green).
|
|
122
|
+
- Existing coverage that continues to pass with the updated guidance strings: `tests/unit/capabilities-discoverability.test.ts`, `tests/unit/secret-drop-hardening.test.ts`.
|
|
123
|
+
- Origin: 2026-05-20 leak incident (topic 9984); 2026-05-21 case-study audit (topic 11141, follow-up #1).
|