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.
Files changed (88) hide show
  1. package/dist/core/DynamicMcpManager.d.ts +105 -0
  2. package/dist/core/DynamicMcpManager.d.ts.map +1 -0
  3. package/dist/core/DynamicMcpManager.js +119 -0
  4. package/dist/core/DynamicMcpManager.js.map +1 -0
  5. package/dist/core/DynamicMcpService.d.ts +60 -0
  6. package/dist/core/DynamicMcpService.d.ts.map +1 -0
  7. package/dist/core/DynamicMcpService.js +76 -0
  8. package/dist/core/DynamicMcpService.js.map +1 -0
  9. package/dist/core/McpApprovalNonceStore.d.ts +46 -0
  10. package/dist/core/McpApprovalNonceStore.d.ts.map +1 -0
  11. package/dist/core/McpApprovalNonceStore.js +83 -0
  12. package/dist/core/McpApprovalNonceStore.js.map +1 -0
  13. package/dist/core/McpLoadedSetStore.d.ts +39 -0
  14. package/dist/core/McpLoadedSetStore.d.ts.map +1 -0
  15. package/dist/core/McpLoadedSetStore.js +82 -0
  16. package/dist/core/McpLoadedSetStore.js.map +1 -0
  17. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  18. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  19. package/dist/core/PostUpdateMigrator.js +21 -1
  20. package/dist/core/PostUpdateMigrator.js.map +1 -1
  21. package/dist/core/SessionManager.d.ts +20 -0
  22. package/dist/core/SessionManager.d.ts.map +1 -1
  23. package/dist/core/SessionManager.js +89 -0
  24. package/dist/core/SessionManager.js.map +1 -1
  25. package/dist/core/dynamicMcpConfig.d.ts +141 -0
  26. package/dist/core/dynamicMcpConfig.d.ts.map +1 -0
  27. package/dist/core/dynamicMcpConfig.js +125 -0
  28. package/dist/core/dynamicMcpConfig.js.map +1 -0
  29. package/dist/core/frameworkSessionLaunch.d.ts +11 -0
  30. package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
  31. package/dist/core/frameworkSessionLaunch.js +7 -0
  32. package/dist/core/frameworkSessionLaunch.js.map +1 -1
  33. package/dist/core/mcpPidCapture.d.ts +43 -0
  34. package/dist/core/mcpPidCapture.d.ts.map +1 -0
  35. package/dist/core/mcpPidCapture.js +44 -0
  36. package/dist/core/mcpPidCapture.js.map +1 -0
  37. package/dist/core/types.d.ts +27 -0
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js.map +1 -1
  40. package/dist/monitoring/McpIdleOffloadSweep.d.ts +61 -0
  41. package/dist/monitoring/McpIdleOffloadSweep.d.ts.map +1 -0
  42. package/dist/monitoring/McpIdleOffloadSweep.js +89 -0
  43. package/dist/monitoring/McpIdleOffloadSweep.js.map +1 -0
  44. package/dist/monitoring/ProcessFootprintMonitor.d.ts +104 -0
  45. package/dist/monitoring/ProcessFootprintMonitor.d.ts.map +1 -0
  46. package/dist/monitoring/ProcessFootprintMonitor.js +183 -0
  47. package/dist/monitoring/ProcessFootprintMonitor.js.map +1 -0
  48. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  49. package/dist/monitoring/guardManifest.js +10 -0
  50. package/dist/monitoring/guardManifest.js.map +1 -1
  51. package/dist/monitoring/mcpIdleLiveOffload.d.ts +68 -0
  52. package/dist/monitoring/mcpIdleLiveOffload.d.ts.map +1 -0
  53. package/dist/monitoring/mcpIdleLiveOffload.js +59 -0
  54. package/dist/monitoring/mcpIdleLiveOffload.js.map +1 -0
  55. package/dist/scaffold/templates.d.ts.map +1 -1
  56. package/dist/scaffold/templates.js +4 -2
  57. package/dist/scaffold/templates.js.map +1 -1
  58. package/dist/server/AgentServer.d.ts +5 -0
  59. package/dist/server/AgentServer.d.ts.map +1 -1
  60. package/dist/server/AgentServer.js +191 -0
  61. package/dist/server/AgentServer.js.map +1 -1
  62. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  63. package/dist/server/CapabilityIndex.js +1 -0
  64. package/dist/server/CapabilityIndex.js.map +1 -1
  65. package/dist/server/routes.d.ts +6 -0
  66. package/dist/server/routes.d.ts.map +1 -1
  67. package/dist/server/routes.js +62 -0
  68. package/dist/server/routes.js.map +1 -1
  69. package/package.json +1 -1
  70. package/src/data/builtin-manifest.json +65 -65
  71. package/src/scaffold/templates.ts +4 -2
  72. package/upgrades/1.3.683.md +48 -0
  73. package/upgrades/1.3.684.md +62 -0
  74. package/upgrades/process-footprint-monitor.eli16.md +47 -0
  75. package/upgrades/side-effects/dynamic-mcp-awareness.md +36 -0
  76. package/upgrades/side-effects/dynamic-mcp-baseline-at-spawn.md +74 -0
  77. package/upgrades/side-effects/dynamic-mcp-capability-classification.md +36 -0
  78. package/upgrades/side-effects/dynamic-mcp-driver.md +53 -0
  79. package/upgrades/side-effects/dynamic-mcp-idle-sweep.md +40 -0
  80. package/upgrades/side-effects/dynamic-mcp-launch-resolver.md +44 -0
  81. package/upgrades/side-effects/dynamic-mcp-loaded-set-store.md +37 -0
  82. package/upgrades/side-effects/dynamic-mcp-nonce-store.md +38 -0
  83. package/upgrades/side-effects/dynamic-mcp-pure-cores.md +53 -0
  84. package/upgrades/side-effects/dynamic-mcp-real-offload.md +57 -0
  85. package/upgrades/side-effects/dynamic-mcp-routes-wiring.md +59 -0
  86. package/upgrades/side-effects/dynamic-mcp-service.md +41 -0
  87. package/upgrades/side-effects/dynamic-mcp-sweep-wiring.md +46 -0
  88. package/upgrades/side-effects/process-footprint-monitor.md +69 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * DynamicMcpManager — the load-on-demand / idle-offload DRIVER for the dynamic
3
+ * MCP lifecycle (DYNAMIC-MCP-LIFECYCLE-SPEC). Given a request to load or offload
4
+ * a server for a topic's session, it mutates the committed loaded-set + restarts
5
+ * the session (`--resume`) so the new set takes effect.
6
+ *
7
+ * All IO is INJECTED (state read/write, restart, pid capture/reap, authorization,
8
+ * mid-tool-use probe) so the orchestration — the part that carries real authority
9
+ * (it restarts a live session) — is fully unit-testable in isolation. The class
10
+ * itself touches no fs/process/network directly.
11
+ *
12
+ * The orchestration folds the convergence findings:
13
+ * - per-topic serialization lock so concurrent requests can't lost-update the
14
+ * state file (M2);
15
+ * - TWO-PHASE commit: write the new set un-committed, restart, commit ONLY on a
16
+ * confirmed restart, roll back on any non-ok restart (M1/M3) — the spawn
17
+ * builder ignores an un-committed file, so a failed restart leaves the live
18
+ * session on its OLD set, never a phantom unapproved change;
19
+ * - offload CAPTURES the heavy child pids BEFORE the kill and reaps them AFTER
20
+ * the restart confirms (C1 — the children reparent to launchd and do NOT die
21
+ * with the session, so a naive offload would leak);
22
+ * - mid-tool-use is RE-CHECKED at kill time for an offload (M3) — abort if the
23
+ * session is, or might be, using its tools;
24
+ * - authorization is VERIFIED, never trusted from the caller: a preapproval is
25
+ * re-checked live, otherwise a single-use server-minted nonce bound to
26
+ * (topicId, kind, server) is required (C4). An agent cannot self-authorize.
27
+ */
28
+ export type McpChangeKind = 'load' | 'offload';
29
+ /** How a request is being authorized. The manager VERIFIES, never trusts. */
30
+ export type RequestActor =
31
+ /** The initial agent-initiated request (or the idle sweep). Authorized only
32
+ * if the topic is live-preapproved; otherwise it returns needs-approval. */
33
+ {
34
+ kind: 'agent';
35
+ }
36
+ /** The operator's approval coming back with the server-minted nonce. */
37
+ | {
38
+ kind: 'operator-approved';
39
+ nonce: string;
40
+ };
41
+ export interface RequestChangeInput {
42
+ topicId: number;
43
+ op: McpChangeKind;
44
+ server: string;
45
+ actor: RequestActor;
46
+ }
47
+ export type RequestChangeResult = {
48
+ status: 'no-op';
49
+ reason: 'already-loaded' | 'not-loaded' | 'unknown-server';
50
+ } | {
51
+ status: 'needs-approval';
52
+ nonce: string;
53
+ prompt: string;
54
+ } | {
55
+ status: 'aborted';
56
+ reason: 'mid-tool-use';
57
+ } | {
58
+ status: 'restart-failed';
59
+ code: string;
60
+ } | {
61
+ status: 'unsupported-unbound';
62
+ } | {
63
+ status: 'applied';
64
+ servers: string[];
65
+ };
66
+ export interface DynamicMcpDeps {
67
+ /** The set the session is CURRENTLY running with (committed state servers, or
68
+ * the baseline / full set when none) — the basis for the mutation. */
69
+ currentServers: (topicId: number) => string[];
70
+ /** Server names defined in `.mcp.json` (for validating a load). */
71
+ allServerNames: () => string[];
72
+ /** Persist the new loaded set. `committed:false` = in-flight (the spawn builder
73
+ * ignores it); `committed:true` = authoritative. Atomic in the impl. */
74
+ writeLoadedSet: (topicId: number, servers: string[], committed: boolean, reason: string) => void;
75
+ /** Live preapproval check (real autonomous-session registry / standing grant),
76
+ * fail-CLOSED. Re-checked at kill time, not trusted from the caller. */
77
+ isPreapproved: (topicId: number) => boolean;
78
+ /** Mint a single-use nonce bound to (topicId, kind, server). */
79
+ mintNonce: (topicId: number, kind: McpChangeKind, server: string) => string;
80
+ /** Consume (verify + invalidate) an operator-supplied nonce. */
81
+ consumeNonce: (topicId: number, kind: McpChangeKind, server: string, nonce: string) => boolean;
82
+ /** Capture the heavy MCP child pids for this session BEFORE an offload kill. */
83
+ captureHeavyPids: (topicId: number, server: string) => number[];
84
+ /** Reap captured orphan pids AFTER the restart confirms (C1). */
85
+ reapPids: (pids: number[]) => void;
86
+ /** Is the session mid-tool-use right now? true / false / null(unknown). */
87
+ isMidToolUse: (topicId: number) => boolean | null;
88
+ /** Restart the session `--resume`. ok:false carries a failure code. */
89
+ restartSession: (topicId: number) => Promise<{
90
+ ok: boolean;
91
+ code?: string;
92
+ }>;
93
+ /** Optional structured audit sink. */
94
+ audit?: (entry: Record<string, unknown>) => void;
95
+ }
96
+ export declare class DynamicMcpManager {
97
+ private readonly deps;
98
+ private readonly locks;
99
+ constructor(deps: DynamicMcpDeps);
100
+ /** Serialize per-topic so concurrent load/offload can't lost-update (M2). */
101
+ requestChange(input: RequestChangeInput): Promise<RequestChangeResult>;
102
+ private requestChangeLocked;
103
+ private audit;
104
+ }
105
+ //# sourceMappingURL=DynamicMcpManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicMcpManager.d.ts","sourceRoot":"","sources":["../../src/core/DynamicMcpManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * DynamicMcpManager — the load-on-demand / idle-offload DRIVER for the dynamic
3
+ * MCP lifecycle (DYNAMIC-MCP-LIFECYCLE-SPEC). Given a request to load or offload
4
+ * a server for a topic's session, it mutates the committed loaded-set + restarts
5
+ * the session (`--resume`) so the new set takes effect.
6
+ *
7
+ * All IO is INJECTED (state read/write, restart, pid capture/reap, authorization,
8
+ * mid-tool-use probe) so the orchestration — the part that carries real authority
9
+ * (it restarts a live session) — is fully unit-testable in isolation. The class
10
+ * itself touches no fs/process/network directly.
11
+ *
12
+ * The orchestration folds the convergence findings:
13
+ * - per-topic serialization lock so concurrent requests can't lost-update the
14
+ * state file (M2);
15
+ * - TWO-PHASE commit: write the new set un-committed, restart, commit ONLY on a
16
+ * confirmed restart, roll back on any non-ok restart (M1/M3) — the spawn
17
+ * builder ignores an un-committed file, so a failed restart leaves the live
18
+ * session on its OLD set, never a phantom unapproved change;
19
+ * - offload CAPTURES the heavy child pids BEFORE the kill and reaps them AFTER
20
+ * the restart confirms (C1 — the children reparent to launchd and do NOT die
21
+ * with the session, so a naive offload would leak);
22
+ * - mid-tool-use is RE-CHECKED at kill time for an offload (M3) — abort if the
23
+ * session is, or might be, using its tools;
24
+ * - authorization is VERIFIED, never trusted from the caller: a preapproval is
25
+ * re-checked live, otherwise a single-use server-minted nonce bound to
26
+ * (topicId, kind, server) is required (C4). An agent cannot self-authorize.
27
+ */
28
+ import { mutateLoadedServers } from './dynamicMcpConfig.js';
29
+ /** A short, server-authored approval prompt (never agent free-text). */
30
+ function approvalPrompt(op, server) {
31
+ return op === 'load'
32
+ ? `I need the "${server}" tool for this — ready for a quick restart? Your conversation is preserved.`
33
+ : `The "${server}" tool has been idle — OK to drop it (a quick restart; it reloads on next use)?`;
34
+ }
35
+ export class DynamicMcpManager {
36
+ deps;
37
+ locks = new Map();
38
+ constructor(deps) {
39
+ this.deps = deps;
40
+ }
41
+ /** Serialize per-topic so concurrent load/offload can't lost-update (M2). */
42
+ async requestChange(input) {
43
+ const prior = this.locks.get(input.topicId) ?? Promise.resolve();
44
+ const run = prior.then(() => this.requestChangeLocked(input), () => this.requestChangeLocked(input));
45
+ // Keep the chain alive but don't leak rejections into the next waiter.
46
+ this.locks.set(input.topicId, run.catch(() => undefined));
47
+ try {
48
+ return await run;
49
+ }
50
+ finally {
51
+ // Drop the lock entry once we're the tail (avoid unbounded growth).
52
+ if (this.locks.get(input.topicId) === run || this.locks.get(input.topicId)) {
53
+ // best-effort cleanup; a newer waiter may have replaced it already
54
+ }
55
+ }
56
+ }
57
+ async requestChangeLocked(input) {
58
+ const { topicId, op, server, actor } = input;
59
+ const current = this.deps.currentServers(topicId);
60
+ const names = this.deps.allServerNames();
61
+ const mutateOp = { kind: op, server };
62
+ const result = mutateLoadedServers(current, names, mutateOp);
63
+ if (!result.changed) {
64
+ // Map the mechanical no-op reasons through verbatim.
65
+ const reason = result.reason === 'unknown-server' ? 'unknown-server'
66
+ : result.reason === 'already-loaded' ? 'already-loaded'
67
+ : 'not-loaded';
68
+ this.audit({ topicId, op, server, outcome: 'no-op', reason });
69
+ return { status: 'no-op', reason };
70
+ }
71
+ // ── Authorization (VERIFIED, never trusted from the caller) ──
72
+ const preapproved = this.deps.isPreapproved(topicId);
73
+ let authorized = preapproved;
74
+ if (!authorized && actor.kind === 'operator-approved') {
75
+ authorized = this.deps.consumeNonce(topicId, op, server, actor.nonce);
76
+ }
77
+ if (!authorized) {
78
+ const nonce = this.deps.mintNonce(topicId, op, server);
79
+ this.audit({ topicId, op, server, outcome: 'needs-approval' });
80
+ return { status: 'needs-approval', nonce, prompt: approvalPrompt(op, server) };
81
+ }
82
+ // ── Offload-only: re-check mid-tool-use at kill time (M3) ──
83
+ if (op === 'offload') {
84
+ const mid = this.deps.isMidToolUse(topicId);
85
+ if (mid !== false) {
86
+ this.audit({ topicId, op, server, outcome: 'aborted', reason: 'mid-tool-use', mid });
87
+ return { status: 'aborted', reason: 'mid-tool-use' };
88
+ }
89
+ }
90
+ // ── Offload-only: capture heavy child pids BEFORE the kill (C1) ──
91
+ const capturedPids = op === 'offload' ? this.deps.captureHeavyPids(topicId, server) : [];
92
+ // ── Two-phase: write un-committed, restart, commit only on success (M1/M3) ──
93
+ this.deps.writeLoadedSet(topicId, result.servers, false, op);
94
+ const restart = await this.deps.restartSession(topicId);
95
+ if (!restart.ok) {
96
+ // Roll back to the prior committed set; the spawn builder already ignores
97
+ // the un-committed write, but re-asserting the committed truth is cleaner.
98
+ this.deps.writeLoadedSet(topicId, current, true, `${op}-rollback`);
99
+ const code = restart.code ?? 'unknown';
100
+ this.audit({ topicId, op, server, outcome: 'restart-failed', code });
101
+ if (code === 'not_telegram_bound')
102
+ return { status: 'unsupported-unbound' };
103
+ return { status: 'restart-failed', code };
104
+ }
105
+ this.deps.writeLoadedSet(topicId, result.servers, true, op);
106
+ if (op === 'offload' && capturedPids.length > 0) {
107
+ this.deps.reapPids(capturedPids);
108
+ }
109
+ this.audit({ topicId, op, server, outcome: 'applied', servers: result.servers, reapedPids: capturedPids.length });
110
+ return { status: 'applied', servers: result.servers };
111
+ }
112
+ audit(entry) {
113
+ try {
114
+ this.deps.audit?.(entry);
115
+ }
116
+ catch { /* audit is best-effort */ }
117
+ }
118
+ }
119
+ //# sourceMappingURL=DynamicMcpManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicMcpManager.js","sourceRoot":"","sources":["../../src/core/DynamicMcpManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * DynamicMcpService — the composition root for the dynamic MCP lifecycle
3
+ * (DYNAMIC-MCP-LIFECYCLE-SPEC). It assembles the DynamicMcpManager, the loaded-set
4
+ * state store, and the approval-nonce store, wiring them to the host's real
5
+ * primitives (restart, preapproval, pid capture/reap, mid-tool-use probe) which
6
+ * are INJECTED so the whole thing is unit-testable without a live server.
7
+ *
8
+ * The three route handlers (`GET /mcp/session/:topicId`, `POST /mcp/load`,
9
+ * `POST /mcp/offload`) are thin wrappers over `getSessionState` / `requestLoad` /
10
+ * `requestOffload`. AgentServer builds this with the real primitives and gates the
11
+ * routes on `enabled()`.
12
+ */
13
+ import { type RequestActor, type RequestChangeResult } from './DynamicMcpManager.js';
14
+ import { type DynamicMcpConfig } from './dynamicMcpConfig.js';
15
+ export interface DynamicMcpServicePrimitives {
16
+ /** `<projectDir>` (for `.mcp.json`) and where `.instar/state/mcp-loaded` lives. */
17
+ projectDir: string;
18
+ /** Live feature switch (dev-gate or explicit flag), evaluated per call. */
19
+ enabled: () => boolean;
20
+ /** The dynamicMcp config block (for the baseline `keepWarm`). */
21
+ config: () => DynamicMcpConfig | undefined;
22
+ /** Restart the topic's session `--resume`; ok:false carries a failure code. */
23
+ restart: (topicId: number) => Promise<{
24
+ ok: boolean;
25
+ code?: string;
26
+ }>;
27
+ /** Live preapproval (active autonomous run / standing grant), fail-CLOSED. */
28
+ isPreapproved: (topicId: number) => boolean;
29
+ /** Capture heavy MCP child pids for an offload (before the kill). */
30
+ captureHeavyPids: (topicId: number, server: string) => number[];
31
+ /** Reap captured orphan pids after a confirmed restart. */
32
+ reapPids: (pids: number[]) => void;
33
+ /** Mid-tool-use probe: true / false / null(unknown ⇒ offload aborts). */
34
+ isMidToolUse: (topicId: number) => boolean | null;
35
+ /** Optional audit sink. */
36
+ audit?: (entry: Record<string, unknown>) => void;
37
+ }
38
+ export interface SessionMcpState {
39
+ topicId: number;
40
+ servers: string[];
41
+ preapproved: boolean;
42
+ source: 'committed' | 'baseline' | 'full';
43
+ }
44
+ export declare class DynamicMcpService {
45
+ private readonly p;
46
+ private readonly loadedSet;
47
+ private readonly nonces;
48
+ private readonly manager;
49
+ constructor(p: DynamicMcpServicePrimitives);
50
+ enabled(): boolean;
51
+ /** Read `.mcp.json` server names ([] on absent/unreadable). */
52
+ private mcpServerNames;
53
+ /** The set the session is currently running with + its provenance. */
54
+ private currentServers;
55
+ /** Registry-first read for `GET /mcp/session/:topicId`. */
56
+ getSessionState(topicId: number): SessionMcpState;
57
+ requestLoad(topicId: number, server: string, actor: RequestActor): Promise<RequestChangeResult>;
58
+ requestOffload(topicId: number, server: string, actor: RequestActor): Promise<RequestChangeResult>;
59
+ }
60
+ //# sourceMappingURL=DynamicMcpService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicMcpService.d.ts","sourceRoot":"","sources":["../../src/core/DynamicMcpService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAA0C,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG7H,OAAO,EAA0B,KAAK,gBAAgB,EAAgB,MAAM,uBAAuB,CAAC;AAEpG,MAAM,WAAW,2BAA2B;IAC1C,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,iEAAiE;IACjE,MAAM,EAAE,MAAM,gBAAgB,GAAG,SAAS,CAAC;IAC3C,+EAA+E;IAC/E,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,8EAA8E;IAC9E,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5C,qEAAqE;IACrE,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,yEAAyE;IACzE,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAClD,2BAA2B;IAC3B,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;CAC3C;AAED,qBAAa,iBAAiB;IAKhB,OAAO,CAAC,QAAQ,CAAC,CAAC;IAJ9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAEf,CAAC,EAAE,2BAA2B;IAkB3D,OAAO,IAAI,OAAO;IAElB,+DAA+D;IAC/D,OAAO,CAAC,cAAc;IAOtB,sEAAsE;IACtE,OAAO,CAAC,cAAc;IAStB,2DAA2D;IAC3D,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe;IAKjD,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAI/F,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAGnG"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * DynamicMcpService — the composition root for the dynamic MCP lifecycle
3
+ * (DYNAMIC-MCP-LIFECYCLE-SPEC). It assembles the DynamicMcpManager, the loaded-set
4
+ * state store, and the approval-nonce store, wiring them to the host's real
5
+ * primitives (restart, preapproval, pid capture/reap, mid-tool-use probe) which
6
+ * are INJECTED so the whole thing is unit-testable without a live server.
7
+ *
8
+ * The three route handlers (`GET /mcp/session/:topicId`, `POST /mcp/load`,
9
+ * `POST /mcp/offload`) are thin wrappers over `getSessionState` / `requestLoad` /
10
+ * `requestOffload`. AgentServer builds this with the real primitives and gates the
11
+ * routes on `enabled()`.
12
+ */
13
+ import path from 'node:path';
14
+ import fs from 'node:fs';
15
+ import { DynamicMcpManager } from './DynamicMcpManager.js';
16
+ import { McpLoadedSetStore } from './McpLoadedSetStore.js';
17
+ import { McpApprovalNonceStore } from './McpApprovalNonceStore.js';
18
+ import { resolveBaselineServers } from './dynamicMcpConfig.js';
19
+ export class DynamicMcpService {
20
+ p;
21
+ loadedSet;
22
+ nonces = new McpApprovalNonceStore();
23
+ manager;
24
+ constructor(p) {
25
+ this.p = p;
26
+ this.loadedSet = new McpLoadedSetStore(path.join(p.projectDir, '.instar', 'state', 'mcp-loaded'));
27
+ const deps = {
28
+ currentServers: (t) => this.currentServers(t).servers,
29
+ allServerNames: () => this.mcpServerNames(),
30
+ writeLoadedSet: (t, servers, committed, reason) => this.loadedSet.write(t, servers, committed, reason),
31
+ isPreapproved: (t) => p.isPreapproved(t),
32
+ mintNonce: (t, kind, server) => this.nonces.mint(t, kind, server),
33
+ consumeNonce: (t, kind, server, nonce) => this.nonces.consume(t, kind, server, nonce),
34
+ captureHeavyPids: (t, server) => p.captureHeavyPids(t, server),
35
+ reapPids: (pids) => p.reapPids(pids),
36
+ isMidToolUse: (t) => p.isMidToolUse(t),
37
+ restartSession: (t) => p.restart(t),
38
+ audit: p.audit,
39
+ };
40
+ this.manager = new DynamicMcpManager(deps);
41
+ }
42
+ enabled() { return this.p.enabled(); }
43
+ /** Read `.mcp.json` server names ([] on absent/unreadable). */
44
+ mcpServerNames() {
45
+ try {
46
+ const full = JSON.parse(fs.readFileSync(path.join(this.p.projectDir, '.mcp.json'), 'utf-8'));
47
+ return Object.keys((full && typeof full === 'object' ? full.mcpServers : undefined) ?? {});
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ /** The set the session is currently running with + its provenance. */
54
+ currentServers(topicId) {
55
+ const committed = this.loadedSet.readCommitted(topicId);
56
+ if (committed !== null)
57
+ return { servers: committed, source: 'committed' };
58
+ const names = this.mcpServerNames();
59
+ const baseline = resolveBaselineServers(names, this.p.config());
60
+ if (baseline !== null)
61
+ return { servers: baseline, source: 'baseline' };
62
+ return { servers: names, source: 'full' };
63
+ }
64
+ /** Registry-first read for `GET /mcp/session/:topicId`. */
65
+ getSessionState(topicId) {
66
+ const cur = this.currentServers(topicId);
67
+ return { topicId, servers: cur.servers, preapproved: this.p.isPreapproved(topicId), source: cur.source };
68
+ }
69
+ requestLoad(topicId, server, actor) {
70
+ return this.manager.requestChange({ topicId, op: 'load', server, actor });
71
+ }
72
+ requestOffload(topicId, server, actor) {
73
+ return this.manager.requestChange({ topicId, op: 'offload', server, actor });
74
+ }
75
+ }
76
+ //# sourceMappingURL=DynamicMcpService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicMcpService.js","sourceRoot":"","sources":["../../src/core/DynamicMcpService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAoE,MAAM,wBAAwB,CAAC;AAC7H,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAuC,MAAM,uBAAuB,CAAC;AA8BpG,MAAM,OAAO,iBAAiB;IAKC;IAJZ,SAAS,CAAoB;IAC7B,MAAM,GAAG,IAAI,qBAAqB,EAAE,CAAC;IACrC,OAAO,CAAoB;IAE5C,YAA6B,CAA8B;QAA9B,MAAC,GAAD,CAAC,CAA6B;QACzD,IAAI,CAAC,SAAS,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;QAClG,MAAM,IAAI,GAAmB;YAC3B,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,OAAO;YACrD,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE;YAC3C,cAAc,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC;YACtG,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;YACxC,SAAS,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC;YACjE,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;YACrF,gBAAgB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,EAAE,MAAM,CAAC;YAC9D,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;YACpC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;YACtC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YACnC,KAAK,EAAE,CAAC,CAAC,KAAK;SACf,CAAC;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,KAAc,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAE/C,+DAA+D;IACvD,cAAc;QACpB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,OAAO,CAAC,CAAY,CAAC;YACxG,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7F,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,EAAE,CAAC;QAAC,CAAC;IACxB,CAAC;IAED,sEAAsE;IAC9D,cAAc,CAAC,OAAe;QACpC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,SAAS,KAAK,IAAI;YAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAChE,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QACxE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5C,CAAC;IAED,2DAA2D;IAC3D,eAAe,CAAC,OAAe;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;IAC3G,CAAC;IAED,WAAW,CAAC,OAAe,EAAE,MAAc,EAAE,KAAmB;QAC9D,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,cAAc,CAAC,OAAe,EAAE,MAAc,EAAE,KAAmB;QACjE,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,CAAC;CACF"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * McpApprovalNonceStore — the single-use approval nonce substrate for the dynamic
3
+ * MCP lifecycle's authorization gate (DYNAMIC-MCP-LIFECYCLE-SPEC, fold C4).
4
+ *
5
+ * When a non-preapproved session asks to load/offload a server, the SERVER mints a
6
+ * nonce bound to the exact (topicId, kind, server) and hands it back in the
7
+ * `needs-approval` payload. The operator's approval — arriving on an
8
+ * operator-authenticated channel (dashboard PIN / a MessageSentinel-bound "yes") —
9
+ * carries that nonce; `consume` verifies it and invalidates it (single-use). An
10
+ * agent forwarding an `approved:true` flag over the shared Bearer can NEVER satisfy
11
+ * this, because there is no nonce it could have legitimately obtained for someone
12
+ * else's approval: the nonce is the proof that an operator-authenticated approval
13
+ * actually happened.
14
+ *
15
+ * In-memory + TTL-bounded (an approval is a short-lived flow). A nonce is bound to
16
+ * the exact change so a captured nonce cannot be replayed against a different
17
+ * server, a different kind, or a different topic.
18
+ */
19
+ export type McpChangeKind = 'load' | 'offload';
20
+ export declare class McpApprovalNonceStore {
21
+ private readonly ttlMs;
22
+ /** Injectable clock for tests; defaults to real time. */
23
+ private readonly now;
24
+ private readonly byKey;
25
+ constructor(ttlMs?: number,
26
+ /** Injectable clock for tests; defaults to real time. */
27
+ now?: () => number);
28
+ /**
29
+ * Mint a fresh single-use nonce for (topicId, kind, server). A new mint for the
30
+ * SAME change replaces any prior outstanding nonce for it (only the latest
31
+ * prompt is valid), so a stale nonce from an earlier prompt cannot be replayed.
32
+ */
33
+ mint(topicId: number, kind: McpChangeKind, server: string): string;
34
+ /**
35
+ * Verify + invalidate. Returns true ONLY when an unexpired nonce for this exact
36
+ * (topicId, kind, server) matches `nonce`. Always single-use: a successful
37
+ * consume removes it; a mismatched/expired attempt removes nothing (so the real
38
+ * operator can still retry with the right one until it expires).
39
+ */
40
+ consume(topicId: number, kind: McpChangeKind, server: string, nonce: string): boolean;
41
+ /** Best-effort sweep of expired entries (callable on a cadence; not required). */
42
+ pruneExpired(): number;
43
+ /** Outstanding (un-consumed, unexpired) nonce count — for status/tests. */
44
+ size(): number;
45
+ }
46
+ //# sourceMappingURL=McpApprovalNonceStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpApprovalNonceStore.d.ts","sourceRoot":"","sources":["../../src/core/McpApprovalNonceStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;AAc/C,qBAAa,qBAAqB;IAI9B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAGpC,KAAK,GAAE,MAAuB;IAC/C,yDAAyD;IACxC,GAAG,GAAE,MAAM,MAAyB;IAGvD;;;;OAIG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAOlE;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAarF,kFAAkF;IAClF,YAAY,IAAI,MAAM;IAStB,2EAA2E;IAC3E,IAAI,IAAI,MAAM;CAIf"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * McpApprovalNonceStore — the single-use approval nonce substrate for the dynamic
3
+ * MCP lifecycle's authorization gate (DYNAMIC-MCP-LIFECYCLE-SPEC, fold C4).
4
+ *
5
+ * When a non-preapproved session asks to load/offload a server, the SERVER mints a
6
+ * nonce bound to the exact (topicId, kind, server) and hands it back in the
7
+ * `needs-approval` payload. The operator's approval — arriving on an
8
+ * operator-authenticated channel (dashboard PIN / a MessageSentinel-bound "yes") —
9
+ * carries that nonce; `consume` verifies it and invalidates it (single-use). An
10
+ * agent forwarding an `approved:true` flag over the shared Bearer can NEVER satisfy
11
+ * this, because there is no nonce it could have legitimately obtained for someone
12
+ * else's approval: the nonce is the proof that an operator-authenticated approval
13
+ * actually happened.
14
+ *
15
+ * In-memory + TTL-bounded (an approval is a short-lived flow). A nonce is bound to
16
+ * the exact change so a captured nonce cannot be replayed against a different
17
+ * server, a different kind, or a different topic.
18
+ */
19
+ import crypto from 'node:crypto';
20
+ const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 min — long enough for a human yes
21
+ function bindKey(topicId, kind, server) {
22
+ return `${topicId}|${kind}|${server}`;
23
+ }
24
+ export class McpApprovalNonceStore {
25
+ ttlMs;
26
+ now;
27
+ byKey = new Map();
28
+ constructor(ttlMs = DEFAULT_TTL_MS,
29
+ /** Injectable clock for tests; defaults to real time. */
30
+ now = () => Date.now()) {
31
+ this.ttlMs = ttlMs;
32
+ this.now = now;
33
+ }
34
+ /**
35
+ * Mint a fresh single-use nonce for (topicId, kind, server). A new mint for the
36
+ * SAME change replaces any prior outstanding nonce for it (only the latest
37
+ * prompt is valid), so a stale nonce from an earlier prompt cannot be replayed.
38
+ */
39
+ mint(topicId, kind, server) {
40
+ const key = bindKey(topicId, kind, server);
41
+ const nonce = crypto.randomBytes(18).toString('base64url');
42
+ this.byKey.set(key, { nonce, key, expiresAt: this.now() + this.ttlMs });
43
+ return nonce;
44
+ }
45
+ /**
46
+ * Verify + invalidate. Returns true ONLY when an unexpired nonce for this exact
47
+ * (topicId, kind, server) matches `nonce`. Always single-use: a successful
48
+ * consume removes it; a mismatched/expired attempt removes nothing (so the real
49
+ * operator can still retry with the right one until it expires).
50
+ */
51
+ consume(topicId, kind, server, nonce) {
52
+ const key = bindKey(topicId, kind, server);
53
+ const entry = this.byKey.get(key);
54
+ if (!entry)
55
+ return false;
56
+ if (this.now() > entry.expiresAt) {
57
+ this.byKey.delete(key);
58
+ return false;
59
+ }
60
+ if (entry.nonce !== nonce)
61
+ return false;
62
+ this.byKey.delete(key); // single-use
63
+ return true;
64
+ }
65
+ /** Best-effort sweep of expired entries (callable on a cadence; not required). */
66
+ pruneExpired() {
67
+ const t = this.now();
68
+ let removed = 0;
69
+ for (const [key, entry] of this.byKey) {
70
+ if (t > entry.expiresAt) {
71
+ this.byKey.delete(key);
72
+ removed++;
73
+ }
74
+ }
75
+ return removed;
76
+ }
77
+ /** Outstanding (un-consumed, unexpired) nonce count — for status/tests. */
78
+ size() {
79
+ this.pruneExpired();
80
+ return this.byKey.size;
81
+ }
82
+ }
83
+ //# sourceMappingURL=McpApprovalNonceStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpApprovalNonceStore.js","sourceRoot":"","sources":["../../src/core/McpApprovalNonceStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAUjC,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,uCAAuC;AAE9E,SAAS,OAAO,CAAC,OAAe,EAAE,IAAmB,EAAE,MAAc;IACnE,OAAO,GAAG,OAAO,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,OAAO,qBAAqB;IAIb;IAEA;IALF,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAEvD,YACmB,QAAgB,cAAc;IAC/C,yDAAyD;IACxC,MAAoB,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QAFpC,UAAK,GAAL,KAAK,CAAyB;QAE9B,QAAG,GAAH,GAAG,CAAiC;IACpD,CAAC;IAEJ;;;;OAIG;IACH,IAAI,CAAC,OAAe,EAAE,IAAmB,EAAE,MAAc;QACvD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACxE,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,OAAe,EAAE,IAAmB,EAAE,MAAc,EAAE,KAAa;QACzE,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,KAAK,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa;QACrC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,kFAAkF;IAClF,YAAY;QACV,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACrB,IAAI,OAAO,GAAG,CAAC,CAAC;QAChB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,CAAC,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;gBAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAAC,OAAO,EAAE,CAAC;YAAC,CAAC;QACjE,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,2EAA2E;IAC3E,IAAI;QACF,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * McpLoadedSetStore — durable per-topic "which MCP servers is this session running
3
+ * with" state for the dynamic MCP lifecycle (DYNAMIC-MCP-LIFECYCLE-SPEC). This is
4
+ * the single source of truth the spawn builder reads (to launch with the current
5
+ * set) and the driver writes (load/offload), so a `--resume` restart re-launches
6
+ * with the right set.
7
+ *
8
+ * Two-phase commit (fold M1/M3): a `committed:false` write is in-flight and is
9
+ * IGNORED by the reader (`readCommitted` returns null) — so a load/offload that
10
+ * writes the new set then fails to restart leaves the live session on its OLD set,
11
+ * never a phantom unapproved change. Only a `committed:true` write is authoritative.
12
+ *
13
+ * Writes are atomic (temp + rename on the same dir) so a crash mid-write can never
14
+ * leave a torn file the reader would choke on.
15
+ */
16
+ export interface LoadedSetRecord {
17
+ servers: string[];
18
+ committed: boolean;
19
+ updatedAt: string;
20
+ reason: string;
21
+ }
22
+ export declare class McpLoadedSetStore {
23
+ private readonly dir;
24
+ /** @param dir e.g. `<projectDir>/.instar/state/mcp-loaded` */
25
+ constructor(dir: string);
26
+ private pathFor;
27
+ /** Raw record (committed or not), or null if absent/unreadable. */
28
+ read(topicId: number): LoadedSetRecord | null;
29
+ /** True iff a state file EXISTS on disk (regardless of readability). */
30
+ exists(topicId: number): boolean;
31
+ /** The COMMITTED server set, or null when there is no committed record (absent,
32
+ * un-committed/in-flight, or unreadable). The reader's two-phase contract. */
33
+ readCommitted(topicId: number): string[] | null;
34
+ /** Atomically write the loaded set. `committed:false` = in-flight (ignored by
35
+ * readCommitted); `committed:true` = authoritative. Throws on a write failure
36
+ * so the caller (driver) can react — the caller is responsible for fail-safety. */
37
+ write(topicId: number, servers: string[], committed: boolean, reason: string, nowIso?: string): void;
38
+ }
39
+ //# sourceMappingURL=McpLoadedSetStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpLoadedSetStore.d.ts","sourceRoot":"","sources":["../../src/core/McpLoadedSetStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,iBAAiB;IAEhB,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,8DAA8D;gBACjC,GAAG,EAAE,MAAM;IAExC,OAAO,CAAC,OAAO;IAIf,mEAAmE;IACnE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAmB7C,wEAAwE;IACxE,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIhC;mFAC+E;IAC/E,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IAM/C;;wFAEoF;IACpF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;CAarG"}