instar 1.3.682 → 1.3.684
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/core/DynamicMcpManager.d.ts +105 -0
- package/dist/core/DynamicMcpManager.d.ts.map +1 -0
- package/dist/core/DynamicMcpManager.js +119 -0
- package/dist/core/DynamicMcpManager.js.map +1 -0
- package/dist/core/DynamicMcpService.d.ts +60 -0
- package/dist/core/DynamicMcpService.d.ts.map +1 -0
- package/dist/core/DynamicMcpService.js +76 -0
- package/dist/core/DynamicMcpService.js.map +1 -0
- package/dist/core/McpApprovalNonceStore.d.ts +46 -0
- package/dist/core/McpApprovalNonceStore.d.ts.map +1 -0
- package/dist/core/McpApprovalNonceStore.js +83 -0
- package/dist/core/McpApprovalNonceStore.js.map +1 -0
- package/dist/core/McpLoadedSetStore.d.ts +39 -0
- package/dist/core/McpLoadedSetStore.d.ts.map +1 -0
- package/dist/core/McpLoadedSetStore.js +82 -0
- package/dist/core/McpLoadedSetStore.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +21 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +20 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +89 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/dynamicMcpConfig.d.ts +141 -0
- package/dist/core/dynamicMcpConfig.d.ts.map +1 -0
- package/dist/core/dynamicMcpConfig.js +125 -0
- package/dist/core/dynamicMcpConfig.js.map +1 -0
- package/dist/core/frameworkSessionLaunch.d.ts +11 -0
- package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
- package/dist/core/frameworkSessionLaunch.js +7 -0
- package/dist/core/frameworkSessionLaunch.js.map +1 -1
- package/dist/core/mcpPidCapture.d.ts +43 -0
- package/dist/core/mcpPidCapture.d.ts.map +1 -0
- package/dist/core/mcpPidCapture.js +44 -0
- package/dist/core/mcpPidCapture.js.map +1 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/McpIdleOffloadSweep.d.ts +61 -0
- package/dist/monitoring/McpIdleOffloadSweep.d.ts.map +1 -0
- package/dist/monitoring/McpIdleOffloadSweep.js +89 -0
- package/dist/monitoring/McpIdleOffloadSweep.js.map +1 -0
- package/dist/monitoring/ProcessFootprintMonitor.d.ts +104 -0
- package/dist/monitoring/ProcessFootprintMonitor.d.ts.map +1 -0
- package/dist/monitoring/ProcessFootprintMonitor.js +183 -0
- package/dist/monitoring/ProcessFootprintMonitor.js.map +1 -0
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +10 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/monitoring/mcpIdleLiveOffload.d.ts +68 -0
- package/dist/monitoring/mcpIdleLiveOffload.d.ts.map +1 -0
- package/dist/monitoring/mcpIdleLiveOffload.js +59 -0
- package/dist/monitoring/mcpIdleLiveOffload.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +4 -2
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +5 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +191 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +6 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +62 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates.ts +4 -2
- package/upgrades/1.3.683.md +48 -0
- package/upgrades/1.3.684.md +62 -0
- package/upgrades/process-footprint-monitor.eli16.md +47 -0
- package/upgrades/side-effects/dynamic-mcp-awareness.md +36 -0
- package/upgrades/side-effects/dynamic-mcp-baseline-at-spawn.md +74 -0
- package/upgrades/side-effects/dynamic-mcp-capability-classification.md +36 -0
- package/upgrades/side-effects/dynamic-mcp-driver.md +53 -0
- package/upgrades/side-effects/dynamic-mcp-idle-sweep.md +40 -0
- package/upgrades/side-effects/dynamic-mcp-launch-resolver.md +44 -0
- package/upgrades/side-effects/dynamic-mcp-loaded-set-store.md +37 -0
- package/upgrades/side-effects/dynamic-mcp-nonce-store.md +38 -0
- package/upgrades/side-effects/dynamic-mcp-pure-cores.md +53 -0
- package/upgrades/side-effects/dynamic-mcp-real-offload.md +57 -0
- package/upgrades/side-effects/dynamic-mcp-routes-wiring.md +59 -0
- package/upgrades/side-effects/dynamic-mcp-service.md +41 -0
- package/upgrades/side-effects/dynamic-mcp-sweep-wiring.md +46 -0
- package/upgrades/side-effects/process-footprint-monitor.md +69 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Added the **Dynamic MCP Lifecycle** (⚗️ experimental, ships DARK) — the on-demand
|
|
9
|
+
loader for heavy MCP servers. MCP servers (Playwright's Chromium, Electron bridges)
|
|
10
|
+
are heavy and mostly idle, and stacked idle copies were a dominant share of the
|
|
11
|
+
process footprint behind the 2026-06-26 resource panic. This lets a claude-code
|
|
12
|
+
session launch with a LEAN MCP set and load a heavy server only when it's needed
|
|
13
|
+
(a `claude --resume` restart preserves the conversation), then offload it when idle.
|
|
14
|
+
|
|
15
|
+
This ship lands the foundation + the load-on-demand path, all behind the explicit
|
|
16
|
+
`sessions.dynamicMcp.enabled` flag (off by default — every `/mcp/*` route 503s when
|
|
17
|
+
dark, and `buildSessionMcpFlags` returns a byte-identical launch). The design was
|
|
18
|
+
converged via a two-reviewer adversarial pass (DYNAMIC-MCP-LIFECYCLE-SPEC), which
|
|
19
|
+
caught and designed out two CRITICAL bugs before any code: the offload-leak (killing
|
|
20
|
+
a session does NOT cascade to its MCP children — they reparent to launchd, so a naive
|
|
21
|
+
offload would leak a Chromium) and an approval self-certification hole (the agent
|
|
22
|
+
could have rubber-stamped its own change over the shared Bearer token).
|
|
23
|
+
|
|
24
|
+
Tracked follow-ups before the dev-gate flip: the idle-offload background sweep + real
|
|
25
|
+
process-cleanup, the operator-approval route (approve a load from a non-autonomous
|
|
26
|
+
chat), and the live-user-channel proof (driving it through Telegram as the operator).
|
|
27
|
+
|
|
28
|
+
## What to Tell Your User
|
|
29
|
+
|
|
30
|
+
If a user asks "can you free up resources from idle browser/MCP servers?" or "why did
|
|
31
|
+
my session restart to add a tool?", the agent now has a real mechanism for it
|
|
32
|
+
(experimental, off by default): start a session lean, load a heavy tool only when
|
|
33
|
+
needed, drop it when idle. During an autonomous run the agent loads on demand and
|
|
34
|
+
carries on; otherwise it asks the operator before a restart, and it can never approve
|
|
35
|
+
its own change.
|
|
36
|
+
|
|
37
|
+
## Summary of New Capabilities
|
|
38
|
+
|
|
39
|
+
- `GET /mcp/session/:topicId` — what MCP servers a topic's session is running with.
|
|
40
|
+
- `POST /mcp/load` / `POST /mcp/offload` — request an on-demand load/offload
|
|
41
|
+
(Bearer-gated; authorization requires a live preapproval or an operator yes — the
|
|
42
|
+
agent cannot self-approve).
|
|
43
|
+
- A lean baseline MCP set at spawn (`sessions.dynamicMcp.keepWarm`), fail-safe to the
|
|
44
|
+
full `.mcp.json`.
|
|
45
|
+
- All dark by default (`sessions.dynamicMcp.enabled`); a no-op on single-server /
|
|
46
|
+
no-`.mcp.json` agents.
|
|
47
|
+
|
|
48
|
+
## Evidence
|
|
49
|
+
|
|
50
|
+
- **Spec:** `docs/specs/DYNAMIC-MCP-LIFECYCLE-SPEC.md` — converged (2 adversarial
|
|
51
|
+
reviewers, folded 2 CRITICAL + 9 MAJOR + 4 MINOR findings) + approved.
|
|
52
|
+
- **Tests (three tiers, 98 across 10 files):** unit cores (baseline/mutation/idle
|
|
53
|
+
decision, driver orchestration incl. two-phase rollback + capture-then-reap + the
|
|
54
|
+
verified-auth gate, nonce store, loaded-set store, composition service,
|
|
55
|
+
buildSessionMcpFlags); integration (the `/mcp/*` routes over the real createRoutes
|
|
56
|
+
pipeline incl. the C4 body-nonce-rejection); e2e (the routes alive on the real
|
|
57
|
+
AgentServer boot — 200 enabled, 401 unauth, 503 disabled).
|
|
58
|
+
- **Dark-safety:** a dedicated test asserts the spawn path is byte-identical when the
|
|
59
|
+
feature is off; the service construction is fail-safe (a throw ⇒ null ⇒ routes 503),
|
|
60
|
+
so a wiring fault can never break server boot.
|
|
61
|
+
- **Awareness/migration:** the "Dynamic MCP Lifecycle" CLAUDE.md section ships to new
|
|
62
|
+
agents (generateClaudeMd) and existing agents (migrateClaudeMd, idempotent), tested.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Process-footprint monitor — ELI16 overview
|
|
2
|
+
|
|
3
|
+
## The problem in plain words
|
|
4
|
+
|
|
5
|
+
On 2026-06-26 the machine running this agent crashed hard — a kernel panic that forced
|
|
6
|
+
a reboot. The trigger was resource exhaustion: too many processes piled up on one
|
|
7
|
+
machine (several full agent stacks plus their heavy helper "MCP" servers — a whole
|
|
8
|
+
Chromium for the browser tool, an Electron app for another — most of them sitting idle).
|
|
9
|
+
The number of processes climbed slowly over time until the operating system hit an
|
|
10
|
+
internal limit and gave up.
|
|
11
|
+
|
|
12
|
+
The painful part: nobody SAW it coming. The agent already tracks CPU and memory, but it
|
|
13
|
+
never tracked the simplest, most relevant number for this kind of crash — *how many
|
|
14
|
+
processes are running on this machine right now, and is that number climbing?* That
|
|
15
|
+
measurement was missing, so the buildup was invisible until it was too late.
|
|
16
|
+
|
|
17
|
+
## What this change does
|
|
18
|
+
|
|
19
|
+
This adds a small, observe-only monitor that fills exactly that gap. On an interval it
|
|
20
|
+
counts the agent-relevant processes on the machine and sorts them into buckets:
|
|
21
|
+
|
|
22
|
+
- **agent CLIs** — the per-session reasoning processes (claude / codex / gemini),
|
|
23
|
+
- **MCP servers** — the heavy, mostly-idle helper servers (Playwright's Chromium, etc.),
|
|
24
|
+
matched by the same precise signatures the cleanup sweep already uses,
|
|
25
|
+
- **other node** — the agent's servers, lifelines, and wrappers.
|
|
26
|
+
|
|
27
|
+
It keeps a rolling window of those readings so a TREND is visible — is the count rising,
|
|
28
|
+
stable, or falling? You can read it any time:
|
|
29
|
+
|
|
30
|
+
`GET /resources/footprint` → `{ enabled, latest: { total, byKind, rssBytes }, trend,
|
|
31
|
+
overThreshold, samples }`
|
|
32
|
+
|
|
33
|
+
## What it deliberately does NOT do
|
|
34
|
+
|
|
35
|
+
It is **observe-only**. It never kills, throttles, or blocks anything — reclaiming
|
|
36
|
+
processes is the job of the existing reapers. This monitor only MEASURES, so it can't
|
|
37
|
+
cause harm. There is an optional heads-up that can fire when the count crosses a
|
|
38
|
+
threshold, but it's off by default ("measure first") — the goal of this first step is
|
|
39
|
+
just to make the climb visible.
|
|
40
|
+
|
|
41
|
+
It ships **dark** (off on the fleet, on for development agents) so it's exercised here
|
|
42
|
+
before any wider rollout, and every reading path fails safe: if the process scan fails,
|
|
43
|
+
the monitor keeps its last reading rather than crashing. The route returns a clear
|
|
44
|
+
"unavailable" (503) when the monitor is turned off.
|
|
45
|
+
|
|
46
|
+
The point: the next time the process footprint starts climbing toward danger, it will be
|
|
47
|
+
a number you can watch — not an invisible buildup that ends in a crash.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP agent awareness + migration
|
|
2
|
+
|
|
3
|
+
**Change:** A "Dynamic MCP Lifecycle" CLAUDE.md section (honestly experimental/dark),
|
|
4
|
+
delivered to new agents (generateClaudeMd) and existing agents (migrateClaudeMd) via
|
|
5
|
+
one shared `DYNAMIC_MCP_CLAUDEMD_SECTION(port)` helper. 3 unit tests.
|
|
6
|
+
|
|
7
|
+
## 1. Blast radius
|
|
8
|
+
Documentation only. No runtime behavior, no gate, no route. New agents get the
|
|
9
|
+
section at init; existing agents get it appended once on the next update.
|
|
10
|
+
|
|
11
|
+
## 2. Reversibility
|
|
12
|
+
Reversible — remove the helper + call sites + migration block. The migration is
|
|
13
|
+
add-if-missing; it never rewrites or deletes existing CLAUDE.md content.
|
|
14
|
+
|
|
15
|
+
## 3. State / data touched
|
|
16
|
+
Appends one section to the agent's own CLAUDE.md on update (idempotent, content-
|
|
17
|
+
sniffed on the heading). Nothing else.
|
|
18
|
+
|
|
19
|
+
## 4. Failure modes
|
|
20
|
+
The migration inherits migrateClaudeMd's guards (missing CLAUDE.md ⇒ skipped; read
|
|
21
|
+
error ⇒ recorded, no crash). Idempotent: a second run is a no-op (test-verified
|
|
22
|
+
byte-identical).
|
|
23
|
+
|
|
24
|
+
## 5. Security / authority
|
|
25
|
+
None. The section TEXT states the Know-Your-Principal authorization rule (the agent
|
|
26
|
+
cannot self-approve), which is enforced in code elsewhere; this is awareness, not
|
|
27
|
+
enforcement.
|
|
28
|
+
|
|
29
|
+
## 6. Framework generality
|
|
30
|
+
Documentation; the capability it describes is claude-code-scoped (stated in the
|
|
31
|
+
text). No framework-launch surface touched.
|
|
32
|
+
|
|
33
|
+
## 7. Tests
|
|
34
|
+
3 unit tests: the section lands on an existing agent (dark tag + /mcp/session + the
|
|
35
|
+
needs-approval rule); idempotent second run (byte-identical); generateClaudeMd
|
|
36
|
+
parity. Plus the existing migrator tests still green. tsc clean.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP baseline-at-spawn wiring
|
|
2
|
+
|
|
3
|
+
**Change:** Wire the dynamic-MCP launch-set resolver into the real interactive
|
|
4
|
+
spawn path so a claude-code session can launch with a lean/explicit MCP subset
|
|
5
|
+
instead of the full `.mcp.json`. Three edits:
|
|
6
|
+
- `src/core/frameworkSessionLaunch.ts` — add `mcpFlags?: string[]` to
|
|
7
|
+
`InteractiveLaunchOptions`; `claudeCodeBuilder` pushes them into argv (right
|
|
8
|
+
before returning). Empty/absent ⇒ no flags ⇒ full `.mcp.json` (the default).
|
|
9
|
+
- `src/core/SessionManager.ts` — new fail-safe private `buildSessionMcpFlags(topicId,
|
|
10
|
+
framework)` (reads `.mcp.json` + the committed loaded-set state file,
|
|
11
|
+
`resolveSessionMcpServers` → `filterMcpConfig` → writes a unique filtered config,
|
|
12
|
+
seeds the committed state file from baseline when absent); wired into the
|
|
13
|
+
`buildInteractiveLaunch` call via a spread.
|
|
14
|
+
- `src/core/types.ts` — `SessionManagerConfig.dynamicMcp?` config field (type-only,
|
|
15
|
+
NOT in ConfigDefaults).
|
|
16
|
+
|
|
17
|
+
## 1. Blast radius
|
|
18
|
+
**Zero while dark** (the default). `buildSessionMcpFlags` returns `[]` on the FIRST
|
|
19
|
+
line when `dynamicMcp.enabled !== true`, so no `mcpFlags` are passed, so
|
|
20
|
+
`claudeCodeBuilder` pushes nothing → the launch argv is byte-identical to today.
|
|
21
|
+
A dedicated test asserts this no-op. Behavior changes ONLY when an operator sets
|
|
22
|
+
`dynamicMcp.enabled:true` + a `keepWarm` trim.
|
|
23
|
+
|
|
24
|
+
## 2. Reversibility
|
|
25
|
+
Fully reversible. Removing the field/flag/method returns launch to its prior
|
|
26
|
+
behavior. The filtered config + state files live under `.instar/state/` and are
|
|
27
|
+
inert once the feature is disabled (the resolver enabled-gates FIRST, so a
|
|
28
|
+
disabled feature ignores any state file — verified by the C3 test).
|
|
29
|
+
|
|
30
|
+
## 3. State / data touched
|
|
31
|
+
Writes two best-effort files under `<projectDir>/.instar/state/` ONLY when enabled
|
|
32
|
+
+ trimming: a per-topic committed loaded-set (`mcp-loaded/<topic>.json`, atomic
|
|
33
|
+
temp+rename) and a unique filtered `--mcp-config` (`session-mcp-config/mcp-<topic>-<ts>.json`).
|
|
34
|
+
Never mutates `.mcp.json` (read-only; `filterMcpConfig` returns a new object).
|
|
35
|
+
|
|
36
|
+
## 4. Failure modes
|
|
37
|
+
Fail-safe to the FULL `.mcp.json` (`[]`) on EVERY error: unreadable/absent
|
|
38
|
+
`.mcp.json`, write failure, undefined topicId, any thrown error (outer try/catch).
|
|
39
|
+
A seed-write failure is swallowed and the launch still proceeds with the resolved
|
|
40
|
+
set. The worst case is "the full tool set", never "a session stranded without its
|
|
41
|
+
tools". State-file-unreadable-but-config-readable falls back to the LEAN baseline,
|
|
42
|
+
not full config (M6) — so a transient state error cannot relaunch every heavy
|
|
43
|
+
server warm and re-create the resource-panic condition.
|
|
44
|
+
|
|
45
|
+
## 5. Security / authority
|
|
46
|
+
None exercised here. This is launch-time config selection only — no restart of a
|
|
47
|
+
live session, no operator-approval flow (those land in the driver/routes commits
|
|
48
|
+
with the server-minted-nonce gate). No authority is taken; the agent cannot self-
|
|
49
|
+
authorize anything via this change.
|
|
50
|
+
|
|
51
|
+
## 6. Framework generality
|
|
52
|
+
The MCP `--mcp-config` / `--strict-mcp-config` mechanism is **Claude-Code
|
|
53
|
+
specific**. `buildSessionMcpFlags` returns `[]` for any framework other than
|
|
54
|
+
`claude-code` (explicit gate, tested), and `claudeCodeBuilder` is the only builder
|
|
55
|
+
that consumes `mcpFlags` — the **codex-cli**, **gemini-cli**, and **pi-cli**
|
|
56
|
+
builders ignore the option entirely. codex-cli configures MCP via `mcp_servers` in
|
|
57
|
+
its config.toml and gemini-cli differs again, so a per-framework MCP-trim is NOT
|
|
58
|
+
attempted for them here; this is a deliberate Claude-Code-scoped capability, not a
|
|
59
|
+
Claude-only assumption baked into a framework-general path. The `framework` is
|
|
60
|
+
resolved per-spawn (the topic's framework), so a codex session on a mixed-framework
|
|
61
|
+
agent is correctly excluded.
|
|
62
|
+
|
|
63
|
+
## 7. Tests
|
|
64
|
+
8 wiring tests (`session-manager-dynamic-mcp-flags.test.ts`) against a real temp
|
|
65
|
+
projectDir: dark ⇒ [] (no-op); non-claude-code ⇒ []; enabled+no-keepWarm ⇒ [];
|
|
66
|
+
enabled+lean ⇒ trimmed config + seeded committed state; committed state file wins;
|
|
67
|
+
un-committed state file ignored; unreadable `.mcp.json` ⇒ [] (fail-safe); undefined
|
|
68
|
+
topic ⇒ []. Plus the 26 + 10 pure-core tests. tsc clean across the project.
|
|
69
|
+
|
|
70
|
+
## Scope note
|
|
71
|
+
Ships behind the EXPLICIT `dynamicMcp.enabled` flag (not the dev-agent gate) on
|
|
72
|
+
purpose: baseline-trim must NOT go live before the on-demand load/offload driver
|
|
73
|
+
exists, or a trimmed dev session could be stranded without Playwright and no way to
|
|
74
|
+
load it. The flip to the dev-agent gate is the final step once the full path lands.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP capability classification + silent-fallback ratchet
|
|
2
|
+
|
|
3
|
+
**Change:** Two CI-guardrail fixes for the dynamic-MCP PR (#1293):
|
|
4
|
+
- `src/server/CapabilityIndex.ts`: add `{ prefix: 'mcp', reason: ... }` to
|
|
5
|
+
INTERNAL_PREFIXES — the `/mcp/*` routes are dark/experimental (503 when off), so they
|
|
6
|
+
are agent-invisible in /capabilities until matured (the honest classification; the
|
|
7
|
+
agent learns the feature via the CLAUDE.md awareness section).
|
|
8
|
+
- `tests/unit/no-silent-fallbacks.test.ts`: raise the ratchet baseline 476→488 with a
|
|
9
|
+
justification — the +12 are the feature's INTENTIONAL fail-safes (fail toward full
|
|
10
|
+
.mcp.json / abort, never a wrong action), documented in the spec + side-effects.
|
|
11
|
+
|
|
12
|
+
## 1. Blast radius
|
|
13
|
+
Zero behavior change. INTERNAL_PREFIXES classification only affects whether /mcp shows
|
|
14
|
+
in the /capabilities discovery list (it doesn't — correct for a dark feature). The
|
|
15
|
+
ratchet baseline is a test threshold, not runtime code.
|
|
16
|
+
|
|
17
|
+
## 2. Reversibility
|
|
18
|
+
Fully reversible — remove the prefix entry / restore the baseline number.
|
|
19
|
+
|
|
20
|
+
## 3. State / data touched
|
|
21
|
+
None.
|
|
22
|
+
|
|
23
|
+
## 4. Failure modes
|
|
24
|
+
None introduced. The classification is the deliberate "skip discovery" choice the lint
|
|
25
|
+
offers; the ratchet still prevents net regressions beyond 488.
|
|
26
|
+
|
|
27
|
+
## 5. Security / authority
|
|
28
|
+
None. Hiding a dark feature from discovery is the conservative choice (does not surface
|
|
29
|
+
an unavailable capability as available).
|
|
30
|
+
|
|
31
|
+
## 6. Framework generality
|
|
32
|
+
N/A (classification + a test threshold).
|
|
33
|
+
|
|
34
|
+
## 7. Tests
|
|
35
|
+
144 tests pass (capabilities-discoverability 139 + no-silent-fallbacks 5). tsc clean.
|
|
36
|
+
These were the only two failing tests across the #1293 CI shards (1/4 + 3/4).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP load/offload driver
|
|
2
|
+
|
|
3
|
+
**Change:** New `src/core/DynamicMcpManager.ts` — the load/offload orchestration
|
|
4
|
+
for the dynamic-MCP lifecycle. All IO is INJECTED (state write, restart, pid
|
|
5
|
+
capture/reap, authorization, mid-tool-use probe); the class touches no
|
|
6
|
+
fs/process/network directly. 16 unit tests via fakes.
|
|
7
|
+
|
|
8
|
+
## 1. Blast radius
|
|
9
|
+
Zero at runtime. The class has NO importer yet (verified by grep) — nothing
|
|
10
|
+
instantiates it. It becomes live only when the routes/sweep wire it with real
|
|
11
|
+
deps (a later commit, behind the dark flag).
|
|
12
|
+
|
|
13
|
+
## 2. Reversibility
|
|
14
|
+
Fully reversible — deleting the file + tests removes it. No persisted state, no
|
|
15
|
+
migration, no config consumed.
|
|
16
|
+
|
|
17
|
+
## 3. State / data touched
|
|
18
|
+
None directly. The class CALLS injected `writeLoadedSet` / `reapPids` / etc., but
|
|
19
|
+
in this commit those are only ever the test fakes.
|
|
20
|
+
|
|
21
|
+
## 4. Failure modes
|
|
22
|
+
The orchestration is designed fail-safe: a non-`ok` restart rolls the committed
|
|
23
|
+
state back to the prior set (no phantom unapproved change); an offload whose
|
|
24
|
+
restart fails does NOT reap the captured pids (the old session is still alive); a
|
|
25
|
+
mid-tool-use `true` OR `null` aborts an offload (fail-closed); audit is best-effort
|
|
26
|
+
(a throwing audit sink never breaks a change). Authorization fails closed: an
|
|
27
|
+
unverified request returns needs-approval, never proceeds.
|
|
28
|
+
|
|
29
|
+
## 5. Security / authority
|
|
30
|
+
This is the authority-bearing core, so the gate is built to the "Agent Proposes,
|
|
31
|
+
Operator Approves" + Know-Your-Principal standards: a change proceeds ONLY when
|
|
32
|
+
the topic is LIVE-preapproved (re-checked via the injected `isPreapproved`, not
|
|
33
|
+
trusted from the caller) OR an operator-supplied single-use nonce is consumed
|
|
34
|
+
successfully. An `agent`-actor request that is not preapproved can NEVER proceed —
|
|
35
|
+
it returns needs-approval with a server-minted nonce. The approval prompt text is
|
|
36
|
+
server-authored (a fixed template), never agent free-text. No authority is
|
|
37
|
+
exercised in this commit (nothing wires it live).
|
|
38
|
+
|
|
39
|
+
## 6. Framework generality
|
|
40
|
+
Not applicable here — the manager is framework-neutral orchestration over injected
|
|
41
|
+
deps. The Claude-Code-specific surface (the actual `--mcp-config` restart) lives in
|
|
42
|
+
the deps' implementations, which the wiring commit will scope to claude-code (the
|
|
43
|
+
restart dep returns `unsupported-unbound`/`not_telegram_bound` for sessions the
|
|
44
|
+
restart mechanism doesn't support).
|
|
45
|
+
|
|
46
|
+
## 7. Tests
|
|
47
|
+
16 unit tests (`dynamic-mcp-manager.test.ts`): no-op short-circuits (already-loaded
|
|
48
|
+
/ not-loaded / unknown-server, no restart); the auth gate both sides incl. valid vs
|
|
49
|
+
invalid nonce; two-phase write-then-commit + rollback on a failed restart;
|
|
50
|
+
not_telegram_bound ⇒ unsupported-unbound; offload capture→restart→reap ordering;
|
|
51
|
+
no-reap on a failed offload restart; mid-tool-use true/null abort; load skips the
|
|
52
|
+
mid-tool-use check; per-topic serialization (no two restarts in flight for one
|
|
53
|
+
topic). tsc clean.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP idle-offload sweep (orchestration)
|
|
2
|
+
|
|
3
|
+
**Change:** New `src/monitoring/McpIdleOffloadSweep.ts` — the automatic
|
|
4
|
+
"offload a heavy MCP server once it's been idle a while" trigger. Stateful
|
|
5
|
+
orchestration over INJECTED deps (proc listing, session→topic, signature→server,
|
|
6
|
+
mid-tool-use, keep-warm, requestOffload). 9 unit tests.
|
|
7
|
+
|
|
8
|
+
## 1. Blast radius
|
|
9
|
+
Zero at runtime. No importer yet — nothing ticks it until the background-interval
|
|
10
|
+
wiring lands (a thin follow-up). The whole feature is also behind the dark
|
|
11
|
+
`sessions.dynamicMcp.enabled` flag.
|
|
12
|
+
|
|
13
|
+
## 2. Reversibility
|
|
14
|
+
Fully reversible — delete the file + tests. No persisted state (the idle clocks are
|
|
15
|
+
in-memory and pruned).
|
|
16
|
+
|
|
17
|
+
## 3. State / data touched
|
|
18
|
+
None on disk. An in-memory Map of per-proc idle clocks, pruned each tick for procs
|
|
19
|
+
that vanished (cannot grow unbounded).
|
|
20
|
+
|
|
21
|
+
## 4. Failure modes
|
|
22
|
+
Fail-closed by construction: a busy OR unknown(null) mid-tool-use resets the idle
|
|
23
|
+
clock (never ages an ambiguous session toward an offload); an unmapped server or a
|
|
24
|
+
non-topic-bound session is skipped; a thrown requestOffload never breaks the sweep.
|
|
25
|
+
It delegates the actual offload to the authorization-gated driver, so the same
|
|
26
|
+
"agent can't self-approve" + "abort if mid-tool-use" + capture-then-reap guarantees
|
|
27
|
+
apply — the sweep adds no new destructive path of its own.
|
|
28
|
+
|
|
29
|
+
## 5. Security / authority
|
|
30
|
+
No new authority. An idle-offload it triggers goes through the SAME
|
|
31
|
+
authorization-gated requestOffload as an explicit request — on a non-preapproved
|
|
32
|
+
session it returns needs-approval (no silent restart).
|
|
33
|
+
|
|
34
|
+
## 6. Framework generality
|
|
35
|
+
Framework-neutral orchestration; the mid-tool-use dep is framework-aware upstream.
|
|
36
|
+
|
|
37
|
+
## 7. Tests
|
|
38
|
+
9 unit tests: disabled no-op; below-window no-offload; offload once the window is
|
|
39
|
+
crossed; mid-tool-use reset (busy + unknown); keep-warm exclusion; dry-run logs-only;
|
|
40
|
+
non-topic-bound skip; clock pruning. tsc clean.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP launch-set resolver
|
|
2
|
+
|
|
3
|
+
**Change:** Add one pure function `resolveSessionMcpServers` (+ its input type) to
|
|
4
|
+
`src/core/dynamicMcpConfig.ts`, with 8 unit tests. It encodes the load-bearing
|
|
5
|
+
launch-set resolution ORDER from the converged spec (DYNAMIC-MCP-LIFECYCLE-SPEC.md),
|
|
6
|
+
folding convergence findings C3 (enabled-gate first ⇒ clean rollback) and M6
|
|
7
|
+
(state-unreadable ⇒ lean baseline, not full config). No wiring — pure, computes a
|
|
8
|
+
list only.
|
|
9
|
+
|
|
10
|
+
## 1. Blast radius
|
|
11
|
+
Zero at runtime. The function is not yet called by any live path; it is a pure
|
|
12
|
+
addition to an already-dark module. Behavior change lands only when the IO caller
|
|
13
|
+
(SessionManager.buildSessionMcpFlags) wires it in a later commit, behind a flag.
|
|
14
|
+
|
|
15
|
+
## 2. Reversibility
|
|
16
|
+
Fully reversible — removing the function + tests returns the module to the prior
|
|
17
|
+
committed state (1fbf13bbb). No persisted state, no migration, no config consumed.
|
|
18
|
+
|
|
19
|
+
## 3. State / data touched
|
|
20
|
+
None. Reads nothing, writes nothing. Returns a fresh array or null.
|
|
21
|
+
|
|
22
|
+
## 4. Failure modes
|
|
23
|
+
None reachable here (no IO). The function is total over its typed inputs. Its
|
|
24
|
+
DESIGN intent is fail-safe: the caller passes `stateFileUnreadable:true` on a read
|
|
25
|
+
error and the resolver returns the lean baseline (M6), never full config; and the
|
|
26
|
+
enabled-gate returns null (full) first so a disabled feature is a clean no-op (C3).
|
|
27
|
+
|
|
28
|
+
## 5. Security / authority
|
|
29
|
+
None. No authority exercised; the live-restart authority + the operator-approval
|
|
30
|
+
nonce flow land in the driver/routes commits with their own review.
|
|
31
|
+
|
|
32
|
+
## 6. Framework generality
|
|
33
|
+
The resolver itself is framework-data-driven: it takes `framework` as input and
|
|
34
|
+
returns null (full .mcp.json) for any non-`claude-code` framework, because the MCP
|
|
35
|
+
`--mcp-config` mechanism is Claude-Code-specific (codex-cli uses `mcp_servers` in
|
|
36
|
+
config.toml; gemini differs). The eventual spawn caller will resolve THIS topic's
|
|
37
|
+
framework and pass it in. No Claude-only assumption is baked in beyond that
|
|
38
|
+
explicit, tested gate.
|
|
39
|
+
|
|
40
|
+
## 7. Tests
|
|
41
|
+
8 new unit tests (26 total in the file): disabled-with-committed-state ⇒ null (C3);
|
|
42
|
+
non-claude-code ⇒ null; committed state wins; empty committed ⇒ []; unreadable ⇒
|
|
43
|
+
lean baseline (M6); unreadable + no baseline ⇒ null; baseline present ⇒ baseline;
|
|
44
|
+
no state + no baseline ⇒ null. tsc clean.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP loaded-set state store
|
|
2
|
+
|
|
3
|
+
**Change:** New `src/core/McpLoadedSetStore.ts` — durable per-topic "which MCP
|
|
4
|
+
servers is this session running with" state, with two-phase commit + atomic writes.
|
|
5
|
+
7 unit tests.
|
|
6
|
+
|
|
7
|
+
## 1. Blast radius
|
|
8
|
+
Zero at runtime. No importer yet — the spawn builder and driver wire it in a later
|
|
9
|
+
commit (behind the dark flag). `buildSessionMcpFlags` currently inlines an
|
|
10
|
+
equivalent read; the composition commit will route both through this store.
|
|
11
|
+
|
|
12
|
+
## 2. Reversibility
|
|
13
|
+
Fully reversible — delete the file + tests. No migration; the on-disk JSON it would
|
|
14
|
+
write is inert when the feature is off.
|
|
15
|
+
|
|
16
|
+
## 3. State / data touched
|
|
17
|
+
Writes `<dir>/<topicId>.json` ONLY when its `write` is called (by the driver/spawn
|
|
18
|
+
builder in a later commit). Atomic temp+rename. Holds no secrets — just server
|
|
19
|
+
NAMES + a committed flag + timestamp.
|
|
20
|
+
|
|
21
|
+
## 4. Failure modes
|
|
22
|
+
`read`/`readCommitted` return null on absent/unreadable/torn files (never throw);
|
|
23
|
+
`exists` lets the caller distinguish absent from unreadable (drives the M6 fail-to-
|
|
24
|
+
lean decision). `write` throws on a genuine fs failure so the driver can roll back —
|
|
25
|
+
the driver is responsible for fail-safety. The two-phase contract (un-committed is
|
|
26
|
+
invisible to the reader) is the M1/M3 fix.
|
|
27
|
+
|
|
28
|
+
## 5. Security / authority
|
|
29
|
+
None. No authority; just state IO of server names.
|
|
30
|
+
|
|
31
|
+
## 6. Framework generality
|
|
32
|
+
Not applicable — framework-neutral state file; no launch/inject surface.
|
|
33
|
+
|
|
34
|
+
## 7. Tests
|
|
35
|
+
7 unit tests: absent ⇒ nulls; committed write readable; un-committed exists-but-
|
|
36
|
+
readCommitted-null (M1/M3); commit-after-in-flight; de-dup; torn file ⇒ read-null-
|
|
37
|
+
exists-true; no leftover .tmp (atomic). tsc clean.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP approval nonce store
|
|
2
|
+
|
|
3
|
+
**Change:** New `src/core/McpApprovalNonceStore.ts` — the single-use, change-bound,
|
|
4
|
+
TTL approval nonce substrate for the dynamic-MCP authorization gate (fold C4). 8
|
|
5
|
+
unit tests.
|
|
6
|
+
|
|
7
|
+
## 1. Blast radius
|
|
8
|
+
Zero at runtime. No importer yet — nothing mints/consumes from it until the
|
|
9
|
+
approval routes wire it (a later commit, behind the dark flag).
|
|
10
|
+
|
|
11
|
+
## 2. Reversibility
|
|
12
|
+
Fully reversible — delete the file + tests. In-memory only; no persisted state, no
|
|
13
|
+
migration, no config.
|
|
14
|
+
|
|
15
|
+
## 3. State / data touched
|
|
16
|
+
None on disk. An in-memory Map of (topicId|kind|server) → {nonce, expiresAt},
|
|
17
|
+
TTL-bounded, pruned on read. Holds no secrets.
|
|
18
|
+
|
|
19
|
+
## 4. Failure modes
|
|
20
|
+
Fails CLOSED: consume returns false for a never-minted change, a wrong nonce, an
|
|
21
|
+
expired nonce, or a binding mismatch. A wrong attempt does NOT burn the real nonce
|
|
22
|
+
(the genuine operator can retry). Single-use on success.
|
|
23
|
+
|
|
24
|
+
## 5. Security / authority
|
|
25
|
+
This IS the authority substrate. The nonce is the proof that an operator-
|
|
26
|
+
authenticated approval happened: it is server-minted, random (crypto.randomBytes),
|
|
27
|
+
bound to the exact (topicId, kind, server), single-use, and TTL-expiring. An agent
|
|
28
|
+
cannot forge, reuse, or cross-replay one — closing the "agent self-certifies
|
|
29
|
+
approved:true over the shared Bearer" hole (C4). No authority is exercised in this
|
|
30
|
+
commit (nothing wires it live).
|
|
31
|
+
|
|
32
|
+
## 6. Framework generality
|
|
33
|
+
Not applicable — framework-neutral in-memory store; no launch/inject surface.
|
|
34
|
+
|
|
35
|
+
## 7. Tests
|
|
36
|
+
8 unit tests: consume-once; single-use (second fails); wrong value fails without
|
|
37
|
+
burning the real one; bound (cross server/kind/topic replay rejected); never-minted
|
|
38
|
+
fails; TTL expiry; re-mint replaces prior; size()/prune. tsc clean.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP pure cores
|
|
2
|
+
|
|
3
|
+
**Change:** Two pure, dark-by-default decision modules for the on-demand MCP
|
|
4
|
+
lifecycle: `src/core/dynamicMcpConfig.ts` (baseline trim + load/offload mutation)
|
|
5
|
+
and `src/monitoring/mcpIdleLiveOffload.ts` (idle-offload eligibility). 28 unit
|
|
6
|
+
tests. No wiring — these compute answers only; nothing spawns, kills, or writes.
|
|
7
|
+
|
|
8
|
+
## 1. Blast radius
|
|
9
|
+
Zero at runtime. Neither module is imported by any live code path in this commit;
|
|
10
|
+
they are pure functions with no side effects. The eventual wiring (next commit)
|
|
11
|
+
is where behavior changes, and it ships behind a disabled flag.
|
|
12
|
+
|
|
13
|
+
## 2. Reversibility
|
|
14
|
+
Fully reversible — deleting the two files and their tests returns the tree to
|
|
15
|
+
its prior state. Nothing persisted, no migrations, no config consumed yet.
|
|
16
|
+
|
|
17
|
+
## 3. State / data touched
|
|
18
|
+
None. No files written, no DB, no network. `mutateLoadedServers` and
|
|
19
|
+
`resolveBaselineServers` return fresh arrays; `filterMcpConfig` returns a new
|
|
20
|
+
object and never mutates its input (explicitly tested).
|
|
21
|
+
|
|
22
|
+
## 4. Failure modes
|
|
23
|
+
Fail-closed by construction. `decideIdleLiveOffload` keeps the server on ANY
|
|
24
|
+
uncertainty: feature off, owner not live, light signature, keep-warm, mid-tool-use
|
|
25
|
+
true OR unknown (null), or idle clock not yet crossed. `mutateLoadedServers`
|
|
26
|
+
rejects loading an undefined server and no-ops a redundant load/offload. There is
|
|
27
|
+
no path that can drop a tool out from under a session in this commit (no actuation).
|
|
28
|
+
|
|
29
|
+
## 5. Security / authority
|
|
30
|
+
None exercised here. The actuation layer (restart a live session) carries real
|
|
31
|
+
authority and lands in the wiring commit with the full Tier-2 spec ceremony —
|
|
32
|
+
including the now-decided authorization model: an autonomous/preapproved session
|
|
33
|
+
loads + restarts autonomously; a non-preapproved session must ASK the operator
|
|
34
|
+
("ready for a quick restart?") before acting. That gate is NOT in this commit.
|
|
35
|
+
|
|
36
|
+
## 6. Framework generality
|
|
37
|
+
Not applicable to these two pure modules (they touch no framework launch/inject
|
|
38
|
+
surface). NOTE for the wiring commit: injecting the lean baseline at spawn WILL
|
|
39
|
+
touch the Claude-code launch path, and MCP config is a Claude-Code-specific
|
|
40
|
+
mechanism (`--mcp-config`/`--strict-mcp-config`); codex-cli and gemini-cli handle
|
|
41
|
+
MCP differently. The wiring will scope the baseline-trim + restart to the
|
|
42
|
+
claude-code framework branch and state that reasoning in its own artifact.
|
|
43
|
+
|
|
44
|
+
## 7. Tests
|
|
45
|
+
28 unit tests covering both sides of every decision boundary (baseline
|
|
46
|
+
on/off/trim, load known/unknown/redundant, offload present/absent/to-empty,
|
|
47
|
+
idle-offload each gate both ways, fail-closed on unknown mid-tool-use). tsc clean.
|
|
48
|
+
|
|
49
|
+
## Decision context
|
|
50
|
+
Supersedes the static per-topic profile model (#1292, held as draft) per Justin's
|
|
51
|
+
2026-06-27 correction: MCP needs are not knowable at launch and must be mutable
|
|
52
|
+
mid-session via restart. Design now fully decided: genuinely-lean baseline +
|
|
53
|
+
load-on-demand gated by preapproval (autonomous = preapproved; else ask).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP real offload primitives (capture-then-reap)
|
|
2
|
+
|
|
3
|
+
**Change:** Complete the offload path (elevated from follow-up to required by the
|
|
4
|
+
operator's full-lifecycle directive). Two edits:
|
|
5
|
+
- `src/core/mcpPidCapture.ts` (new, pure): `captureHeavyMcpPidsForSession` — given the
|
|
6
|
+
live MCP process scan + process tree + tmux pane map, returns the heavy MCP child
|
|
7
|
+
PIDs belonging to a specific session AND server. Conservative: unknown/non-heavy
|
|
8
|
+
server or unresolvable session ⇒ no pid.
|
|
9
|
+
- `src/server/AgentServer.ts`: replace the three conservative offload stubs in the
|
|
10
|
+
DynamicMcpService construction with REAL primitives — `captureHeavyPids` (reaper
|
|
11
|
+
ps-listing + the pure capture), `reapPids` (the reaper's `killProcess`), and
|
|
12
|
+
`isMidToolUse` (live pane frame + `looksActivelyWorking`).
|
|
13
|
+
|
|
14
|
+
## 1. Blast radius
|
|
15
|
+
Dark by default (`sessions.dynamicMcp.enabled`); when off, the service is built but
|
|
16
|
+
its methods are never invoked (the routes 503). When ON, the offload path can now
|
|
17
|
+
actually drop a server + reap its orphaned heavy process. Service construction stays
|
|
18
|
+
fail-safe (a throw ⇒ null ⇒ routes 503), and the e2e boot is unaffected (verified).
|
|
19
|
+
|
|
20
|
+
## 2. Reversibility
|
|
21
|
+
Reversible — revert the stubs. No persisted state beyond the existing loaded-set file.
|
|
22
|
+
|
|
23
|
+
## 3. State / data touched
|
|
24
|
+
`captureHeavyPids` READS the process table (ps) + tmux pane map (read-only).
|
|
25
|
+
`reapPids` sends SIGTERM/SIGKILL to the captured PIDs (the reaper's audited
|
|
26
|
+
`killProcess`). No new files; the reaper audit path is reused.
|
|
27
|
+
|
|
28
|
+
## 4. Failure modes — THE destructive-authority review
|
|
29
|
+
`reapPids` is the only destructive op. It is tightly bounded: it kills ONLY the PIDs
|
|
30
|
+
`captureHeavyPids` returned, which are ONLY procs that (a) match a HEAVY MCP signature
|
|
31
|
+
mapped from the offloaded server name, AND (b) resolve via the ppid-walk to the EXACT
|
|
32
|
+
target session. An unknown/non-heavy server captures nothing; an orphan or
|
|
33
|
+
wrong-session proc captures nothing — so the worst case is "reap nothing" (a missed
|
|
34
|
+
reclaim the generic reaper still backstops), never "kill the wrong process". The kill
|
|
35
|
+
only runs AFTER an authorized offload's restart is CONFIRMED (the old session is gone),
|
|
36
|
+
and the offload aborts entirely if the session is — or might be — mid-tool-use. Every
|
|
37
|
+
step is fail-safe (capture ⇒ [] on any error; reap ⇒ best-effort per-pid try/catch;
|
|
38
|
+
mid-tool-use ⇒ null ⇒ abort on any uncertainty).
|
|
39
|
+
|
|
40
|
+
## 5. Security / authority
|
|
41
|
+
No new authority surface — the change only makes the already-gated offload actually
|
|
42
|
+
reclaim. The kill is gated behind: feature enabled + an authorized offload (live
|
|
43
|
+
preapproval or operator nonce) + a confirmed restart + not-mid-tool-use. The agent
|
|
44
|
+
cannot trigger an unauthorized kill.
|
|
45
|
+
|
|
46
|
+
## 6. Framework generality
|
|
47
|
+
The MCP-server processes are framework-agnostic (a Chromium is a Chromium); the
|
|
48
|
+
mid-tool-use probe passes the session's framework to `looksActivelyWorking` (which is
|
|
49
|
+
framework-aware). The capture/reap is claude-code-relevant in practice (only
|
|
50
|
+
claude-code sessions get a trimmed/dynamic MCP set), but the pid-capture itself is not
|
|
51
|
+
Claude-specific.
|
|
52
|
+
|
|
53
|
+
## 7. Tests
|
|
54
|
+
7 unit tests for the pure capture (target-session-only, unknown/light server ⇒ none,
|
|
55
|
+
wrong-session ⇒ none, orphan ⇒ none, multiple pids, the conservative server→signature
|
|
56
|
+
map). The e2e AgentServer boot + the routes integration remain green with the new
|
|
57
|
+
wiring. tsc clean.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Side-effects review — dynamic-MCP routes + AgentServer wiring (live-authority)
|
|
2
|
+
|
|
3
|
+
**Change:** Make the dynamic-MCP feature reachable. Three edits:
|
|
4
|
+
- `src/server/routes.ts` — `RouteContext.dynamicMcpService?` field + three routes:
|
|
5
|
+
`GET /mcp/session/:topicId`, `POST /mcp/load`, `POST /mcp/offload`. Bearer-gated
|
|
6
|
+
(global middleware); 503 when the service is absent/disabled (dark by default).
|
|
7
|
+
- `src/server/AgentServer.ts` — build `DynamicMcpService` with the host's REAL
|
|
8
|
+
primitives (restart→SessionRefresh via `getSessionForTopic`, isPreapproved→
|
|
9
|
+
`activeAutonomousJobs`) and thread it into `routeCtx`. Construction is fail-safe
|
|
10
|
+
(a throw ⇒ null ⇒ routes 503).
|
|
11
|
+
- Integration test (9 cases) over the real createRoutes pipeline.
|
|
12
|
+
|
|
13
|
+
## 1. Blast radius
|
|
14
|
+
Reachable but DARK by default: every `/mcp/*` route 503s unless
|
|
15
|
+
`sessions.dynamicMcp.enabled === true` (explicit flag, not the dev-gate yet). The
|
|
16
|
+
service construction is wrapped in try/catch → null on any error, so a wiring fault
|
|
17
|
+
can NEVER break server boot — it just disables the feature. When dark, `routeCtx`
|
|
18
|
+
gains one extra (unused) field; nothing else changes.
|
|
19
|
+
|
|
20
|
+
## 2. Reversibility
|
|
21
|
+
Fully reversible — remove the field/routes/construction. No migration; the on-disk
|
|
22
|
+
state the service would write only happens when an enabled change is requested.
|
|
23
|
+
|
|
24
|
+
## 3. State / data touched
|
|
25
|
+
None unless enabled + a change is requested (then McpLoadedSetStore writes under
|
|
26
|
+
`.instar/state/mcp-loaded/`). Reads `.mcp.json`. Never mutates `.mcp.json`.
|
|
27
|
+
|
|
28
|
+
## 4. Failure modes
|
|
29
|
+
Service construction fail-safe (→ null → 503). The restart primitive maps a null
|
|
30
|
+
SessionRefresh / unbound session to `not_telegram_bound` ⇒ `unsupported-unbound`
|
|
31
|
+
(no crash). isPreapproved fails CLOSED on any error. v1 OFFLOAD is conservatively
|
|
32
|
+
inert (isMidToolUse ⇒ null ⇒ the driver aborts every offload; captureHeavyPids ⇒ [])
|
|
33
|
+
so there is NO leak and NO bad restart until the paneTail probe + reaper pid-capture
|
|
34
|
+
land in a follow-up.
|
|
35
|
+
|
|
36
|
+
## 5. Security / authority — THE load-bearing review
|
|
37
|
+
The agent-facing routes are Bearer-gated and ALWAYS act as `{kind:'agent'}` — they
|
|
38
|
+
NEVER honor a caller-supplied `nonce` from the request body. This is the C4 fix made
|
|
39
|
+
concrete: the agent RECEIVES the nonce in a needs-approval response, so if the route
|
|
40
|
+
honored a body nonce the agent could self-approve over the shared Bearer. An
|
|
41
|
+
integration test asserts that a body `nonce` does NOT authorize a not-preapproved
|
|
42
|
+
change. The only paths that complete a change are (a) a live-preapproved topic
|
|
43
|
+
(active autonomous run — the operator's own standing grant) or (b) — FOLLOW-UP — an
|
|
44
|
+
operator-authenticated approval route (dashboard PIN / sentinel-bound yes) that
|
|
45
|
+
consumes the server-minted nonce. Until that route lands, a non-preapproved change
|
|
46
|
+
returns needs-approval and performs no restart. No authority can be self-granted.
|
|
47
|
+
|
|
48
|
+
## 6. Framework generality
|
|
49
|
+
The routes/service are framework-neutral; the Claude-Code-specific surface is the
|
|
50
|
+
restart primitive (SessionRefresh, which returns not_telegram_bound for sessions it
|
|
51
|
+
can't restart ⇒ unsupported-unbound). The baseline-trim that the restart re-applies
|
|
52
|
+
is already claude-code-scoped in buildSessionMcpFlags (prior commit). codex-cli /
|
|
53
|
+
gemini-cli sessions are not trimmed and a restart of them carries no mcpFlags.
|
|
54
|
+
|
|
55
|
+
## 7. Tests
|
|
56
|
+
9 integration tests (`dynamic-mcp-routes.test.ts`): 503 absent / 503 disabled / GET
|
|
57
|
+
state shape / GET 400 / POST load applied + restart / POST load needs-approval (202,
|
|
58
|
+
no restart) / C4 body-nonce-rejected / POST 400 missing server / offload conservative
|
|
59
|
+
409 abort. Plus the ~105 unit tests for the underlying blocks. tsc clean.
|