context-mode 1.0.163 → 1.0.164
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +34 -29
- package/build/adapters/antigravity-cli/index.d.ts +1 -1
- package/build/adapters/antigravity-cli/index.js +7 -6
- package/build/adapters/pi/extension.js +14 -13
- package/build/lifecycle.d.ts +48 -0
- package/build/lifecycle.js +111 -0
- package/build/security.d.ts +65 -0
- package/build/security.js +138 -4
- package/build/server.js +73 -4
- package/build/session/extract.js +70 -0
- package/cli.bundle.mjs +184 -183
- package/configs/antigravity-cli/plugin.json +1 -1
- package/configs/copilot-cli/.github/plugin/plugin.json +1 -1
- package/hooks/routing-block.mjs +1 -2
- package/hooks/security.bundle.mjs +2 -2
- package/hooks/session-extract.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -3
- package/server.bundle.mjs +141 -140
- package/scripts/install-antigravity-cli-plugin.mjs +0 -141
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.164"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "context-mode",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.164",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Mert Koseoğlu"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.164",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.164",
|
|
4
4
|
"description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.164",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.164",
|
|
4
4
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mert Koseoğlu",
|
package/README.md
CHANGED
|
@@ -825,9 +825,11 @@ Full documentation: [`docs/adapters/kimi-code.md`](docs/adapters/kimi-code.md)
|
|
|
825
825
|
</details>
|
|
826
826
|
|
|
827
827
|
<details>
|
|
828
|
-
<summary><strong>Antigravity</strong> — MCP-only, no hooks</summary>
|
|
828
|
+
<summary><strong>Antigravity IDE</strong> — MCP-only, no hooks</summary>
|
|
829
829
|
|
|
830
|
-
**
|
|
830
|
+
> This is the Antigravity **desktop IDE**. For the `agy` **command-line tool**, see **Antigravity CLI (`agy`)** below — it installs as a full plugin with hooks.
|
|
831
|
+
|
|
832
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), the Antigravity IDE installed.
|
|
831
833
|
|
|
832
834
|
**Install:**
|
|
833
835
|
|
|
@@ -868,43 +870,28 @@ Full configs: [`configs/antigravity/mcp_config.json`](configs/antigravity/mcp_co
|
|
|
868
870
|
<details>
|
|
869
871
|
<summary><strong>Antigravity CLI (<code>agy</code>)</strong> — plugin (MCP + skill + hooks)</summary>
|
|
870
872
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
`agy` has a native **plugin** system, so context-mode installs as a first-class agy plugin that bundles the MCP server, a routing rule, a routing skill, and bounded hooks in one step.
|
|
874
|
-
|
|
875
|
-
**Install** (same one-command pattern as OpenClaw — the script resolves the bundle path for you):
|
|
876
|
-
|
|
877
|
-
1. Make the MCP server available globally (the plugin runs the `context-mode` binary):
|
|
878
|
-
|
|
879
|
-
```bash
|
|
880
|
-
npm install -g context-mode
|
|
881
|
-
```
|
|
882
|
-
|
|
883
|
-
2. Clone and install the plugin:
|
|
884
|
-
|
|
885
|
-
```bash
|
|
886
|
-
git clone https://github.com/mksglu/context-mode.git
|
|
887
|
-
cd context-mode
|
|
888
|
-
npm run install:agy
|
|
889
|
-
```
|
|
890
|
-
|
|
891
|
-
`npm run install:agy` runs `agy plugin install` on the bundle at `configs/antigravity-cli/` — registering the MCP server from the bundle's native `mcp_config.json`, the routing rule, routing skill, bounded `PreToolUse`, `PostToolUse` capture, and best-effort `Stop` capture hooks — then clears agy's stale tool-schema cache so the `ctx_*` tools appear in the model's tool list (agy caches MCP schemas and doesn't refresh them; an old cache hides the tools). The installer is cross-platform Node (runs natively on Windows, macOS, and Linux — no bash required). Restart `agy`.
|
|
873
|
+
> The `agy` **command-line tool**, not the Antigravity desktop IDE above.
|
|
892
874
|
|
|
893
|
-
|
|
875
|
+
**Prerequisites:** Node.js >= 22.5 (or Bun), Antigravity CLI (`agy`) **≥ 1.0.7** (`agy update` to upgrade). Verified on agy 1.0.10.
|
|
894
876
|
|
|
895
|
-
|
|
877
|
+
**Install:**
|
|
896
878
|
|
|
897
|
-
|
|
879
|
+
```bash
|
|
880
|
+
npm install -g context-mode # the plugin's MCP server + hooks run the global binary
|
|
881
|
+
agy plugin install https://github.com/mksglu/context-mode/tree/main/configs/antigravity-cli # registers MCP + rule + skill + hooks
|
|
882
|
+
```
|
|
898
883
|
|
|
899
|
-
|
|
884
|
+
Restart `agy`.
|
|
900
885
|
|
|
901
|
-
**
|
|
886
|
+
**MCP-only (no plugin, no hooks):** if you only want the `ctx_*` tools, skip the plugin and add context-mode to agy's global MCP profile `~/.gemini/config/mcp_config.json` (distinct from the Antigravity IDE's `~/.gemini/antigravity/` path), then restart `agy`:
|
|
902
887
|
|
|
903
888
|
```json
|
|
904
889
|
{ "mcpServers": { "context-mode": { "command": "context-mode" } } }
|
|
905
890
|
```
|
|
906
891
|
|
|
907
|
-
**
|
|
892
|
+
**Verify:** type `ctx stats` in an agy session, or run any prompt from [Try It](#try-it) and check the savings. `context-mode doctor` confirms MCP + hook registration. Remove with `agy plugin uninstall context-mode`.
|
|
893
|
+
|
|
894
|
+
**Routing:** the routing rule and skill provide the instruction layer; bounded `PreToolUse` blocks high-flood tools and `PostToolUse` captures sessions. The bundle pins `CONTEXT_MODE_PLATFORM=antigravity-cli` so agy is detected even when Claude Code is co-installed ([#774](https://github.com/mksglu/context-mode/issues/774)).
|
|
908
895
|
|
|
909
896
|
</details>
|
|
910
897
|
|
|
@@ -1567,6 +1554,24 @@ Commands chained with `&&`, `;`, or `|` are split — each part is checked separ
|
|
|
1567
1554
|
|
|
1568
1555
|
**deny** always wins over **allow**. More specific (project-level) rules override global ones.
|
|
1569
1556
|
|
|
1557
|
+
### Project-boundary containment
|
|
1558
|
+
|
|
1559
|
+
`ctx_execute_file` is confined to the project root. A `path` that resolves **outside** the workspace — an absolute path like `/home/user/secrets`, a `../../` traversal, or a project-local symlink whose target escapes the project — is refused with a `File access blocked` error. This closes the [#852](https://github.com/mksglu/context-mode/issues/852) escape vector where an agent, denied an out-of-project read by the host sandbox, retried through the MCP sandbox (the host's MCP approval prompt cannot inspect the tool's input params, so the escape was invisible to the approver).
|
|
1560
|
+
|
|
1561
|
+
The guard is **on by default** and requires no configuration. To intentionally process a file outside the project (e.g. a shared log under `/var/log`), opt that path back in with the **same `permissions.allow` rule you already use for the host `Read` tool** — there is no context-mode-specific env flag:
|
|
1562
|
+
|
|
1563
|
+
```json
|
|
1564
|
+
{
|
|
1565
|
+
"permissions": {
|
|
1566
|
+
"allow": ["Read(/var/log/**)"]
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
context-mode honors that allow rule (read from your `.claude/settings.json` / `~/.claude/settings.json`) exactly as Claude Code does, so an out-of-project grant lives in one place and stays meaningful.
|
|
1572
|
+
|
|
1573
|
+
Reviewing the prompt: the `ctx_execute` / `ctx_execute_file` approval titles now read as code execution ("Run code in a sandbox…", "Run code over a file…") so an unfamiliar reviewer can recognise the action class even though the MCP prompt renders only the tool title and raw arguments. `ctx_execute` and `ctx_batch_execute` run arbitrary code and still inherit the process's filesystem access, so the boundary guard is a defense-in-depth layer for the *file-read* tool, not a full OS sandbox — treat approving any execution tool as approving arbitrary code, and keep host-level sandboxing enabled.
|
|
1574
|
+
|
|
1570
1575
|
### Network fetch hardening
|
|
1571
1576
|
|
|
1572
1577
|
`ctx_fetch_and_index` blocks dangerous URL targets by default:
|
|
@@ -24,7 +24,7 @@ export declare function antigravityCliConfigDir(): string;
|
|
|
24
24
|
export declare function antigravityCliHooksPath(): string;
|
|
25
25
|
/**
|
|
26
26
|
* `agy plugin install <bundle>` registers MCP + hook + skill into agy's plugin
|
|
27
|
-
* profile under ~/.gemini/config/plugins/<name>/ (verified on agy 1.0.6) — the
|
|
27
|
+
* profile under ~/.gemini/config/plugins/<name>/ (verified on agy 1.0.6, re-verified 1.0.10) — the
|
|
28
28
|
* canonical install. The global mcp_config.json / hooks.json paths above are the
|
|
29
29
|
* manual (no-plugin) fallback. doctor must recognize BOTH.
|
|
30
30
|
*/
|
|
@@ -33,7 +33,7 @@ export function antigravityCliHooksPath() {
|
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
35
|
* `agy plugin install <bundle>` registers MCP + hook + skill into agy's plugin
|
|
36
|
-
* profile under ~/.gemini/config/plugins/<name>/ (verified on agy 1.0.6) — the
|
|
36
|
+
* profile under ~/.gemini/config/plugins/<name>/ (verified on agy 1.0.6, re-verified 1.0.10) — the
|
|
37
37
|
* canonical install. The global mcp_config.json / hooks.json paths above are the
|
|
38
38
|
* manual (no-plugin) fallback. doctor must recognize BOTH.
|
|
39
39
|
*/
|
|
@@ -130,8 +130,9 @@ function readRegisteredHooks(paths) {
|
|
|
130
130
|
/* unreadable/missing — try next */
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
// Stop is registered when possible, but agy 1.0.6 `-p` probes did not emit it
|
|
134
|
-
//
|
|
133
|
+
// Stop is registered when possible, but agy 1.0.6 `-p` probes did not emit it
|
|
134
|
+
// (no Stop-hook change in agy's changelog through 1.0.10). Treat it as
|
|
135
|
+
// best-effort so doctor does not mark a working Pre/Post install
|
|
135
136
|
// as degraded solely because Stop is absent.
|
|
136
137
|
return { ...found, ok: found.preOk && found.postOk };
|
|
137
138
|
}
|
|
@@ -251,7 +252,7 @@ export class AntigravityCliAdapter extends AntigravityAdapter {
|
|
|
251
252
|
check: "MCP registration",
|
|
252
253
|
status: "fail",
|
|
253
254
|
message: "context-mode not found in Antigravity CLI mcpServers",
|
|
254
|
-
fix: "
|
|
255
|
+
fix: "agy plugin install https://github.com/mksglu/context-mode/tree/main/configs/antigravity-cli",
|
|
255
256
|
};
|
|
256
257
|
}
|
|
257
258
|
getInstalledVersion() {
|
|
@@ -333,8 +334,8 @@ export class AntigravityCliAdapter extends AntigravityAdapter {
|
|
|
333
334
|
status: ok ? "pass" : "warn",
|
|
334
335
|
message: ok
|
|
335
336
|
? `PreToolUse guard and PostToolUse capture configured in ${where}${stopOk ? "; best-effort Stop hook also configured" : ""}`
|
|
336
|
-
: `Antigravity CLI hooks incomplete (${missing || "none found"} missing) — MCP tools still work, but bounded routing enforcement and session capture are degraded. Run \`
|
|
337
|
-
...(ok ? {} : { fix: "
|
|
337
|
+
: `Antigravity CLI hooks incomplete (${missing || "none found"} missing) — MCP tools still work, but bounded routing enforcement and session capture are degraded. Run \`agy plugin install https://github.com/mksglu/context-mode/tree/main/configs/antigravity-cli\` or \`context-mode upgrade\` to repair hooks.`,
|
|
338
|
+
...(ok ? {} : { fix: "agy plugin install https://github.com/mksglu/context-mode/tree/main/configs/antigravity-cli" }),
|
|
338
339
|
},
|
|
339
340
|
];
|
|
340
341
|
}
|
|
@@ -565,11 +565,22 @@ export default function piExtension(pi) {
|
|
|
565
565
|
// Pi-3 + Pi-4: Always build active_memory (not just post-compact),
|
|
566
566
|
// capped at 500 tokens via buildAutoInjection. Falls back to inline
|
|
567
567
|
// budget loop if the helper is unavailable.
|
|
568
|
-
|
|
568
|
+
//
|
|
569
|
+
// Issue #856 — do NOT re-inject `role` as a standing behavioral_directive
|
|
570
|
+
// on every turn. A casual past phrase that classified as a role would
|
|
571
|
+
// otherwise be pinned and replayed each turn ("since you said 'that's
|
|
572
|
+
// fine for now', I'll leave it"), producing a do-nothing loop. Defense in
|
|
573
|
+
// depth: even if a stale `role` event exists (from an older build, or a
|
|
574
|
+
// genuine persona the user has since moved past), it must not become an
|
|
575
|
+
// inescapable per-turn standing order. Role events stay in the DB and
|
|
576
|
+
// remain queryable via ctx_search(source: "session-events"); intent,
|
|
577
|
+
// skills, decisions, and the resume snapshot are unaffected.
|
|
578
|
+
const activeEvents = db
|
|
579
|
+
.getEvents(_sessionId, {
|
|
569
580
|
minPriority: 3,
|
|
570
581
|
limit: 50,
|
|
571
|
-
})
|
|
572
|
-
|
|
582
|
+
})
|
|
583
|
+
.filter((e) => String(e.category ?? "") !== "role");
|
|
573
584
|
if (activeEvents.length > 0) {
|
|
574
585
|
const buildAuto = await getAutoInjection(pluginRoot);
|
|
575
586
|
let memoryContext = "";
|
|
@@ -578,14 +589,6 @@ export default function piExtension(pi) {
|
|
|
578
589
|
category: String(e.category ?? ""),
|
|
579
590
|
data: String(e.data ?? ""),
|
|
580
591
|
})));
|
|
581
|
-
const bdMatch = memoryContext.match(/(<behavioral_directive>\n[^<]*\n<\/behavioral_directive>)/);
|
|
582
|
-
if (bdMatch) {
|
|
583
|
-
behavioralDirective = bdMatch[1];
|
|
584
|
-
memoryContext = memoryContext.replace(bdMatch[1], "");
|
|
585
|
-
if (memoryContext.match(/^<session_state[^>]*>\s*<\/session_state>\s*$/)) {
|
|
586
|
-
memoryContext = "";
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
592
|
}
|
|
590
593
|
// Fallback (or if helper produced empty output): inline 500-token cap.
|
|
591
594
|
if (!memoryContext) {
|
|
@@ -611,8 +614,6 @@ export default function piExtension(pi) {
|
|
|
611
614
|
parts.push(resume.snapshot);
|
|
612
615
|
db.markResumeConsumed(_sessionId);
|
|
613
616
|
}
|
|
614
|
-
if (behavioralDirective)
|
|
615
|
-
parts.push(behavioralDirective);
|
|
616
617
|
// Store extra context (routing anchor, active_memory, resume, behavioralDirective)
|
|
617
618
|
// for injection via the 'context' hook as a message, NOT as a systemPrompt
|
|
618
619
|
// modification. Mutating systemPrompt breaks prefix prompt caching on
|
package/build/lifecycle.d.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* next poll tick), which closes the multi-day CPU-spin window seen in
|
|
12
12
|
* #311/#388 without reintroducing the false-positive shutdowns of #236.
|
|
13
13
|
*
|
|
14
|
+
* Additionally, for MCP BRIDGE CHILDREN only (CONTEXT_MODE_BRIDGE_DEPTH>0), a
|
|
15
|
+
* request-idle self-shutdown reaps a child that a pi/omp sub-context abandoned
|
|
16
|
+
* while its long-lived parent keeps running (#854) — gated so the depth-0
|
|
17
|
+
* keep-alive servers #602 restored are never reaped, never via stdin EOF, and
|
|
18
|
+
* never while a tool call is in flight (#643).
|
|
19
|
+
*
|
|
14
20
|
* Cross-platform: macOS, Linux, Windows.
|
|
15
21
|
*/
|
|
16
22
|
export interface LifecycleGuardOptions {
|
|
@@ -20,6 +26,12 @@ export interface LifecycleGuardOptions {
|
|
|
20
26
|
onShutdown: () => void;
|
|
21
27
|
/** Injectable parent-alive check (for testing). Default: ppid-based check. */
|
|
22
28
|
isParentAlive?: () => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* #854: request-idle shutdown timeout (ms) for MCP bridge children. Default:
|
|
31
|
+
* {@link bridgeChildIdleTimeoutMs}() — 0 (disabled) unless CONTEXT_MODE_BRIDGE_DEPTH>0.
|
|
32
|
+
* Exposed for testing.
|
|
33
|
+
*/
|
|
34
|
+
bridgeIdleMs?: number;
|
|
23
35
|
}
|
|
24
36
|
/** Injectable dependencies for {@link makeDefaultIsParentAlive}. */
|
|
25
37
|
export interface IsParentAliveDeps {
|
|
@@ -59,6 +71,42 @@ export declare function makeDefaultIsParentAlive(deps?: IsParentAliveDeps): () =
|
|
|
59
71
|
* Exported for unit-testing.
|
|
60
72
|
*/
|
|
61
73
|
export declare function lifecycleGuardIntervalForEnv(env?: NodeJS.ProcessEnv): number;
|
|
74
|
+
/**
|
|
75
|
+
* #854: idle-shutdown timeout (ms) for an MCP BRIDGE CHILD. Returns 0 (disabled)
|
|
76
|
+
* unless this process is a bridge child (CONTEXT_MODE_BRIDGE_DEPTH>0). depth-0 /
|
|
77
|
+
* absent always returns 0, so the long-lived keep-alive servers that #602
|
|
78
|
+
* restored are NEVER reaped on idle. Default for bridge children is 3 min;
|
|
79
|
+
* override with CONTEXT_MODE_BRIDGE_IDLE_MS (a non-positive value disables it).
|
|
80
|
+
* The reaper additionally never fires while a tool call is in flight (see
|
|
81
|
+
* {@link noteRequestStart}), so the window only bounds how fast *abandoned*
|
|
82
|
+
* children drain — it does not cap legitimate long-running calls.
|
|
83
|
+
*
|
|
84
|
+
* Exported for unit-testing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function bridgeChildIdleTimeoutMs(env?: NodeJS.ProcessEnv): number;
|
|
87
|
+
/**
|
|
88
|
+
* #854: record MCP activity (inbound message or response). The server calls this
|
|
89
|
+
* so the bridge-child idle reaper in {@link startLifecycleGuard} can distinguish
|
|
90
|
+
* an actively-used child from an abandoned one. Cheap; safe on the hot path.
|
|
91
|
+
*/
|
|
92
|
+
export declare function noteMcpActivity(): void;
|
|
93
|
+
/**
|
|
94
|
+
* #854: mark a tool call as started. Suppresses the bridge-child idle reaper so a
|
|
95
|
+
* single long-running ctx_execute / ctx_batch_execute (which sends one inbound
|
|
96
|
+
* frame then runs unbounded, #643) is never reaped mid-execution.
|
|
97
|
+
*/
|
|
98
|
+
export declare function noteRequestStart(): void;
|
|
99
|
+
/** #854: mark a tool call as finished (success or error). */
|
|
100
|
+
export declare function noteRequestEnd(): void;
|
|
101
|
+
/**
|
|
102
|
+
* #854: wrap an MCP stdio transport's `onmessage` so each inbound message
|
|
103
|
+
* refreshes the idle clock. Best-effort: call after `connect()` (onmessage set);
|
|
104
|
+
* a no-op if it isn't a function, and a throw in noteMcpActivity never breaks
|
|
105
|
+
* dispatch. No stdin touch (preserves the #236 contract). Exported for testing.
|
|
106
|
+
*/
|
|
107
|
+
export declare function attachMcpActivityTap(transport: {
|
|
108
|
+
onmessage?: (message: unknown, extra?: unknown) => unknown;
|
|
109
|
+
} | null | undefined): void;
|
|
62
110
|
/**
|
|
63
111
|
* Start the lifecycle guard. Returns a cleanup function.
|
|
64
112
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
package/build/lifecycle.js
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* next poll tick), which closes the multi-day CPU-spin window seen in
|
|
12
12
|
* #311/#388 without reintroducing the false-positive shutdowns of #236.
|
|
13
13
|
*
|
|
14
|
+
* Additionally, for MCP BRIDGE CHILDREN only (CONTEXT_MODE_BRIDGE_DEPTH>0), a
|
|
15
|
+
* request-idle self-shutdown reaps a child that a pi/omp sub-context abandoned
|
|
16
|
+
* while its long-lived parent keeps running (#854) — gated so the depth-0
|
|
17
|
+
* keep-alive servers #602 restored are never reaped, never via stdin EOF, and
|
|
18
|
+
* never while a tool call is in flight (#643).
|
|
19
|
+
*
|
|
14
20
|
* Cross-platform: macOS, Linux, Windows.
|
|
15
21
|
*/
|
|
16
22
|
import { execFileSync } from "node:child_process";
|
|
@@ -94,6 +100,80 @@ export function lifecycleGuardIntervalForEnv(env = process.env) {
|
|
|
94
100
|
return 30_000;
|
|
95
101
|
return 1000;
|
|
96
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* #854: idle-shutdown timeout (ms) for an MCP BRIDGE CHILD. Returns 0 (disabled)
|
|
105
|
+
* unless this process is a bridge child (CONTEXT_MODE_BRIDGE_DEPTH>0). depth-0 /
|
|
106
|
+
* absent always returns 0, so the long-lived keep-alive servers that #602
|
|
107
|
+
* restored are NEVER reaped on idle. Default for bridge children is 3 min;
|
|
108
|
+
* override with CONTEXT_MODE_BRIDGE_IDLE_MS (a non-positive value disables it).
|
|
109
|
+
* The reaper additionally never fires while a tool call is in flight (see
|
|
110
|
+
* {@link noteRequestStart}), so the window only bounds how fast *abandoned*
|
|
111
|
+
* children drain — it does not cap legitimate long-running calls.
|
|
112
|
+
*
|
|
113
|
+
* Exported for unit-testing.
|
|
114
|
+
*/
|
|
115
|
+
export function bridgeChildIdleTimeoutMs(env = process.env) {
|
|
116
|
+
const depth = Number.parseInt(env.CONTEXT_MODE_BRIDGE_DEPTH ?? "", 10);
|
|
117
|
+
if (!Number.isFinite(depth) || depth <= 0)
|
|
118
|
+
return 0;
|
|
119
|
+
const raw = env.CONTEXT_MODE_BRIDGE_IDLE_MS;
|
|
120
|
+
if (raw !== undefined) {
|
|
121
|
+
const v = Number.parseInt(raw, 10);
|
|
122
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
123
|
+
}
|
|
124
|
+
return 180_000;
|
|
125
|
+
}
|
|
126
|
+
// #854 idle-reaper state, module-level by design: an MCP server is exactly one
|
|
127
|
+
// process (one StdioServerTransport + one lifecycle guard), so these are never
|
|
128
|
+
// shared across concurrent servers in production. Multiple startLifecycleGuard()
|
|
129
|
+
// instances arise only in tests, which pair/reset these explicitly.
|
|
130
|
+
/** Last MCP activity timestamp (inbound message, tool-call start/end, or response). */
|
|
131
|
+
let _lastMcpActivity = Date.now();
|
|
132
|
+
/** In-flight tool-call count — the reaper never fires while this is > 0. */
|
|
133
|
+
let _inFlight = 0;
|
|
134
|
+
/**
|
|
135
|
+
* #854: record MCP activity (inbound message or response). The server calls this
|
|
136
|
+
* so the bridge-child idle reaper in {@link startLifecycleGuard} can distinguish
|
|
137
|
+
* an actively-used child from an abandoned one. Cheap; safe on the hot path.
|
|
138
|
+
*/
|
|
139
|
+
export function noteMcpActivity() {
|
|
140
|
+
_lastMcpActivity = Date.now();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* #854: mark a tool call as started. Suppresses the bridge-child idle reaper so a
|
|
144
|
+
* single long-running ctx_execute / ctx_batch_execute (which sends one inbound
|
|
145
|
+
* frame then runs unbounded, #643) is never reaped mid-execution.
|
|
146
|
+
*/
|
|
147
|
+
export function noteRequestStart() {
|
|
148
|
+
_inFlight++;
|
|
149
|
+
_lastMcpActivity = Date.now();
|
|
150
|
+
}
|
|
151
|
+
/** #854: mark a tool call as finished (success or error). */
|
|
152
|
+
export function noteRequestEnd() {
|
|
153
|
+
if (_inFlight > 0)
|
|
154
|
+
_inFlight--;
|
|
155
|
+
_lastMcpActivity = Date.now();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* #854: wrap an MCP stdio transport's `onmessage` so each inbound message
|
|
159
|
+
* refreshes the idle clock. Best-effort: call after `connect()` (onmessage set);
|
|
160
|
+
* a no-op if it isn't a function, and a throw in noteMcpActivity never breaks
|
|
161
|
+
* dispatch. No stdin touch (preserves the #236 contract). Exported for testing.
|
|
162
|
+
*/
|
|
163
|
+
export function attachMcpActivityTap(transport) {
|
|
164
|
+
if (!transport)
|
|
165
|
+
return;
|
|
166
|
+
const prev = typeof transport.onmessage === "function" ? transport.onmessage.bind(transport) : null;
|
|
167
|
+
if (!prev)
|
|
168
|
+
return;
|
|
169
|
+
transport.onmessage = (message, extra) => {
|
|
170
|
+
try {
|
|
171
|
+
noteMcpActivity();
|
|
172
|
+
}
|
|
173
|
+
catch { /* never break message dispatch */ }
|
|
174
|
+
return prev(message, extra);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
97
177
|
/**
|
|
98
178
|
* Start the lifecycle guard. Returns a cleanup function.
|
|
99
179
|
* Skipped automatically when stdin is a TTY (e.g. OpenCode ts-plugin).
|
|
@@ -142,9 +222,40 @@ export function startLifecycleGuard(opts) {
|
|
|
142
222
|
if (!process.stdin.isTTY) {
|
|
143
223
|
process.stdin.on("end", onStdinEnd);
|
|
144
224
|
}
|
|
225
|
+
// #854: request-idle self-shutdown for MCP BRIDGE CHILDREN only
|
|
226
|
+
// (CONTEXT_MODE_BRIDGE_DEPTH>0). Pi/omp loads the extension once per
|
|
227
|
+
// sub-context and spawns one bridge child each, tearing them down only at
|
|
228
|
+
// session_shutdown — which never fires for sub-contexts while the long-lived
|
|
229
|
+
// parent stays alive, so idle children accumulate (#854, same class as #565).
|
|
230
|
+
// A bridge child that receives no inbound MCP message for `idleMs` exits
|
|
231
|
+
// itself; the extension's single-flight path respawns one on the next call.
|
|
232
|
+
//
|
|
233
|
+
// Scoped strictly to depth>0 so the depth-0 keep-alive servers that #602
|
|
234
|
+
// restored are never reaped on idle. The trigger is idle TIME via
|
|
235
|
+
// noteMcpActivity() (NOT stdin EOF), so the #236 contract — and lifecycle's
|
|
236
|
+
// hands-off-stdin invariant — are untouched.
|
|
237
|
+
const idleMs = opts.bridgeIdleMs ?? bridgeChildIdleTimeoutMs();
|
|
238
|
+
let idleTimer;
|
|
239
|
+
if (idleMs > 0) {
|
|
240
|
+
_lastMcpActivity = Date.now();
|
|
241
|
+
idleTimer = setInterval(() => {
|
|
242
|
+
// Reap only when truly quiescent: NO tool call in flight AND no MCP
|
|
243
|
+
// activity for `idleMs`. The in-flight guard prevents reaping a child
|
|
244
|
+
// mid-execution during a long single ctx_execute/batch that sends no
|
|
245
|
+
// further messages (#643 unbounded calls) — the false-reap regression the
|
|
246
|
+
// adversarial review flagged.
|
|
247
|
+
if (_inFlight === 0 && Date.now() - _lastMcpActivity >= idleMs) {
|
|
248
|
+
process.stderr.write(`[context-mode] idle MCP bridge child self-shutdown after ${idleMs}ms with no activity (#854)\n`);
|
|
249
|
+
shutdown();
|
|
250
|
+
}
|
|
251
|
+
}, Math.max(1000, Math.min(Math.floor(idleMs / 4), 30_000)));
|
|
252
|
+
idleTimer.unref();
|
|
253
|
+
}
|
|
145
254
|
return () => {
|
|
146
255
|
stopped = true;
|
|
147
256
|
clearInterval(timer);
|
|
257
|
+
if (idleTimer)
|
|
258
|
+
clearInterval(idleTimer);
|
|
148
259
|
for (const sig of signals)
|
|
149
260
|
process.removeListener(sig, shutdown);
|
|
150
261
|
process.stdin.removeListener("end", onStdinEnd);
|
package/build/security.d.ts
CHANGED
|
@@ -76,6 +76,19 @@ export declare function readBashPolicies(projectDir?: string, globalSettingsPath
|
|
|
76
76
|
* Each inner array contains the extracted glob strings.
|
|
77
77
|
*/
|
|
78
78
|
export declare function readToolDenyPatterns(toolName: string, projectDir?: string, globalSettingsPath?: string): string[][];
|
|
79
|
+
/**
|
|
80
|
+
* Read `permissions.{deny|allow}` globs for a tool from every settings file in
|
|
81
|
+
* precedence order (project local → project shared → adapter globals).
|
|
82
|
+
*
|
|
83
|
+
* Generalizes the original deny-only reader so the project-boundary guard
|
|
84
|
+
* (#852) can consult the SAME `permissions.allow` rules the user already
|
|
85
|
+
* maintains for the host's `Read` tool — instead of inventing a context-mode-
|
|
86
|
+
* specific opt-out env that would rot into dead code. A user who legitimately
|
|
87
|
+
* needs an out-of-project read expresses it once, in the host config, e.g.
|
|
88
|
+
* `"permissions": { "allow": ["Read(/var/log/**)"] }`, and both the host and
|
|
89
|
+
* context-mode honor it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function readToolPermissionPatterns(toolName: string, kind: "deny" | "allow", projectDir?: string, globalSettingsPath?: string): string[][];
|
|
79
92
|
interface CommandDecision {
|
|
80
93
|
decision: PermissionDecision;
|
|
81
94
|
matchedPattern?: string;
|
|
@@ -130,6 +143,58 @@ export declare function evaluateFilePath(filePath: string, denyGlobs: string[][]
|
|
|
130
143
|
denied: boolean;
|
|
131
144
|
matchedPattern?: string;
|
|
132
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Pure, algorithmic (no-regex) test: does `filePath` resolve to a location
|
|
148
|
+
* inside `projectRoot`?
|
|
149
|
+
*
|
|
150
|
+
* Issue #852 — `ctx_execute_file` previously fed its `path` argument straight
|
|
151
|
+
* into `resolve(projectRoot, path)`. Because `path.resolve` lets an *absolute*
|
|
152
|
+
* argument win outright, an agent could read any file on the host
|
|
153
|
+
* (`/home/user/secret`, `/etc/passwd`) regardless of the project root, and
|
|
154
|
+
* `../` traversal escaped just as easily. Claude Code's harness sandbox cannot
|
|
155
|
+
* inspect MCP input params, so the user approving the MCP call could not see
|
|
156
|
+
* that the path escaped the workspace. This guard re-anchors the path to the
|
|
157
|
+
* project boundary.
|
|
158
|
+
*
|
|
159
|
+
* Containment is decided on the *resolved* form. When the file (or its parent
|
|
160
|
+
* chain) exists, the symlink-canonical form is ALSO required to stay inside —
|
|
161
|
+
* this closes the symlink-escape class (a project-local `safe.log` whose
|
|
162
|
+
* realpath points at `~/.ssh/id_rsa`), mirroring `evaluateFilePath`.
|
|
163
|
+
*
|
|
164
|
+
* A path equal to the project root itself counts as inside. Comparison is
|
|
165
|
+
* case-insensitive on Windows/macOS to match those filesystems' semantics.
|
|
166
|
+
*
|
|
167
|
+
* Returns `true` when `projectRoot` is falsy (no boundary to enforce) so the
|
|
168
|
+
* caller's fail-open posture is preserved when the root cannot be resolved.
|
|
169
|
+
*/
|
|
170
|
+
export declare function isPathInsideProject(filePath: string, projectRoot: string | undefined, caseInsensitive?: boolean): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Decide whether `filePath` may be processed, given the project boundary AND
|
|
173
|
+
* the user's existing host `Read(...)` allow rules.
|
|
174
|
+
*
|
|
175
|
+
* Decision order:
|
|
176
|
+
* 1. Inside the project root → allowed (the common case; no config needed).
|
|
177
|
+
* 2. Outside the project, but matching a `permissions.allow` `Read(...)` glob
|
|
178
|
+
* the user already configured for the host → allowed. This is the
|
|
179
|
+
* principled escape hatch: a deliberate out-of-project read is expressed
|
|
180
|
+
* ONCE in the host config the user already maintains, reusing the same
|
|
181
|
+
* mechanism Claude Code itself uses to whitelist a path outside the
|
|
182
|
+
* sandbox — no context-mode-specific opt-out env that would rot into
|
|
183
|
+
* dead code.
|
|
184
|
+
* 3. Outside the project, no allow match → denied (closes the #852 escape).
|
|
185
|
+
*
|
|
186
|
+
* `allowGlobs` has the same per-settings-file shape as the deny globs returned
|
|
187
|
+
* by `readToolPermissionPatterns(toolName, "allow", …)`. Allow-matching reuses
|
|
188
|
+
* `evaluateFilePath` so absolute/`..`/symlink-canonical candidate resolution is
|
|
189
|
+
* identical to the deny path — one matcher, no divergence.
|
|
190
|
+
*
|
|
191
|
+
* Fail-open on an unknown project root (boundary cannot be computed) so the
|
|
192
|
+
* guard never blocks legitimate in-project work when resolution fails.
|
|
193
|
+
*/
|
|
194
|
+
export declare function evaluateProjectContainment(filePath: string, projectRoot: string | undefined, allowGlobs?: string[][], caseInsensitive?: boolean): {
|
|
195
|
+
allowed: boolean;
|
|
196
|
+
reason: "inside" | "allow-rule" | "outside";
|
|
197
|
+
};
|
|
133
198
|
/**
|
|
134
199
|
* Scan non-shell code for shell-escape calls and extract the embedded
|
|
135
200
|
* command strings.
|