Skip to content

Commit 09e97a7

Browse files
committed
v0.6.0 - plugin MCP: HTTP attach-only bridge
Replace the Claude plugin MCP child-process bridge with a Node stdio-to-HTTP proxy that attaches to the running daemon over /mcp-rpc. SessionStart is status-only and no longer resolves or shells into cortex.exe. Remove legacy dev app URL routing and local-spawn fallback behavior. Validation: node --test plugins/cortex-plugin/scripts/run-mcp.contract.test.cjs; node plugins/cortex-plugin/scripts/dry-run-matrix.cjs; node --check plugin scripts; python tools/audit_spawn_paths.py --strict; live stdin parse-error smoke against local daemon.
1 parent 2eefc9d commit 09e97a7

5 files changed

Lines changed: 870 additions & 620 deletions

File tree

plugins/cortex-plugin/ROUTING.md

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,57 @@
1-
# Cortex Plugin Routing Policy (App-First + Local Service-First)
1+
# Cortex Plugin Routing Policy (HTTP Attach-Only)
22

3-
The plugin prefers app-managed routing, but local plugin mode is allowed for
4-
plugin-only users. In local mode, `cortex plugin mcp` performs service-first
5-
daemon ensure on Windows and then bridges MCP.
3+
The Claude Code plugin never starts a Cortex daemon and never shells into a
4+
`cortex` binary for the MCP bridge. The Control Center/service owns daemon
5+
lifecycle. The plugin attaches to an already-running daemon over HTTP.
66

77
## Priority Order
8+
89
1. Explicit plugin URL (`CLAUDE_PLUGIN_OPTION_CORTEX_URL`)
910
2. App route URL (`CORTEX_APP_URL`)
10-
3. Local route (`localhost` with service-first ensure)
11-
12-
## Binary Selection Order (for MCP bridge and SessionStart probes)
13-
1. Explicit binary override (`CORTEX_APP_BINARY`, `CORTEX_DAEMON_BINARY`, `CORTEX_PLUGIN_CORTEX_BINARY`)
14-
2. App-managed canonical install (`~/.cortex/bin/cortex[.exe]`)
15-
3. Common workspace dev/release builds (`~/cortex/daemon-rs/target*`)
16-
4. Bundled plugin runtime binary (`CLAUDE_PLUGIN_DATA/bin/cortex[.exe]`) -- allowed as local fallback when safe
11+
3. Local attach-only route (`http://127.0.0.1:7437`)
1712

18-
This keeps plugin tooling aligned with the app daemon in development and avoids
19-
relying on stale bundled binaries when a canonical app-managed binary exists.
20-
21-
## Local Binary Safety Gate
22-
- In local mode, plugin scripts reject temporary runtime binaries by default.
23-
- App-managed binaries are preferred; plugin-bundled fallback is allowed when safe.
24-
- Optional strict mode:
25-
- `CORTEX_PLUGIN_REQUIRE_APP_BINARY=1` forces app-managed binary only.
26-
- Optional compatibility override:
27-
- `CORTEX_PLUGIN_ALLOW_BUNDLED_BINARY=1` permits bundled fallback even when strict mode is set.
13+
`CORTEX_DEV_PREFER_APP=1` is strict: it requires `CORTEX_APP_URL` and fails
14+
instead of falling back.
2815

2916
## Supported Environment Inputs
17+
3018
- `CLAUDE_PLUGIN_OPTION_CORTEX_URL`
3119
- Preferred explicit route for team/remote endpoint.
3220
- `CLAUDE_PLUGIN_OPTION_CORTEX_API_KEY`
33-
- Optional API key for explicit remote route.
21+
- Required API key for non-local explicit remote routes.
3422
- `CORTEX_APP_URL`
3523
- Preferred app-managed endpoint route during development.
24+
- `CORTEX_API_KEY`
25+
- Fallback API key for app-managed remote routes.
3626
- `CORTEX_PLUGIN_DRY_RUN=1`
37-
- Prints resolved route and exits without launching MCP bridge child.
38-
- `CORTEX_APP_BINARY`, `CORTEX_DAEMON_BINARY`, `CORTEX_PLUGIN_CORTEX_BINARY`
39-
- Optional explicit binary override for plugin bridge/hook execution.
40-
- `CORTEX_WORKSPACE_ROOT`
41-
- Optional workspace root used to discover dev/release daemon builds.
42-
- `CORTEX_PLUGIN_REQUIRE_APP_BINARY=1`
43-
- Optional strict policy to require app-managed binary in local mode.
44-
- `CORTEX_PLUGIN_ALLOW_BUNDLED_BINARY=1`
45-
- Optional escape hatch to permit bundled plugin binaries when strict mode is active.
27+
- Prints resolved route and exits without opening the MCP proxy loop.
28+
29+
Legacy local-spawn toggles are ignored by the MCP entry point because no local
30+
spawn path exists there anymore.
4631

4732
## Route Matrix
48-
- Plugin URL set -> route `remote` -> pass `--url` and optional `--api-key`
49-
- No plugin URL, app URL set -> route `remote` -> pass `--url`
50-
- No plugin/app URL -> route `local` -> service-first ensure on Windows, then local bridge
33+
34+
- Plugin URL set -> route `remote` -> Node stdio-to-HTTP proxy
35+
- No plugin URL, app URL set -> route `remote` -> Node stdio-to-HTTP proxy
36+
- No plugin/app URL -> route `local` -> Node stdio-to-HTTP proxy to `127.0.0.1:7437`
37+
- `CORTEX_DEV_PREFER_APP=1` without `CORTEX_APP_URL` -> explicit failure
5138

5239
## Lifecycle Guarantees
40+
5341
- Plugin SessionStart hook is status-only and never starts/stops daemon.
54-
- SessionStart probes `/readiness` first and falls back to `/health` for older daemons.
55-
- Plugin MCP bridge never direct-spawns daemon binaries itself.
56-
- Local plugin mode delegates daemon readiness to `cortex plugin mcp` service-first policy (Windows) or returns a clear unsupported-local-ensure error on non-Windows.
57-
- If local mode resolves only temporary binaries, plugin blocks fallback and surfaces safe-binary guidance.
42+
- SessionStart probes `/readiness` first and falls back to `/health`.
43+
- Plugin MCP bridge posts JSON-RPC to `/mcp-rpc` with:
44+
- `X-Cortex-Request: true`
45+
- `Authorization: Bearer <token>` for local or remote authenticated routes
46+
- `X-Source-Agent`
47+
- optional `X-Source-Model`
48+
- Local token auth is read from `CORTEX_TOKEN_PATH`, `CORTEX_HOME/cortex.token`,
49+
or `~/.cortex/cortex.token`.
50+
- If local mode cannot reach a ready daemon, the bridge exits with
51+
`APP_INIT_REQUIRED` and instructs the user to open Cortex Control Center.
5852

5953
## Lockstep Requirement
60-
Plugin-bundled daemon versions and app daemon versions should ship in lockstep.
6154

62-
Minimum release guard:
63-
1. Build plugin bundle from the same daemon commit used by app release artifacts.
64-
2. Keep plugin version and daemon release manifest aligned in release checklist.
65-
3. Add CI guard that fails release when plugin bundle daemon version differs from app daemon version.
55+
Plugin release artifacts and app daemon versions should still ship in lockstep,
56+
but plugin MCP routing no longer depends on a bundled or canonical daemon binary.
57+

plugins/cortex-plugin/scripts/dry-run-matrix.cjs

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,78 +2,67 @@
22
/**
33
* Dry-run matrix for `run-mcp.cjs` routing resolver.
44
*
5-
* Invokes `resolveRoute` with 5 representative environment shapes and asserts
6-
* the resulting (mode, spawnAllowed) pair matches the documented behavior
7-
* matrix in `docs/internal/v060/plugin-routing.md`.
8-
*
9-
* Exit code 0 = all 5 pass. Exit code 1 = at least one mismatch.
10-
*
11-
* Run: `node plugins/cortex-plugin/scripts/dry-run-matrix.cjs`
5+
* The plugin MCP entry point is HTTP attach-only. It never local-spawns a
6+
* cortex daemon process; missing local daemon readiness is reported at runtime.
127
*/
138

149
const assert = require('node:assert/strict');
15-
const { resolveRoute } = require('./run-mcp.cjs');
10+
const { DEFAULT_LOCAL_BASE_URL, resolveRoute } = require('./run-mcp.cjs');
1611

1712
const cases = [
1813
{
19-
name: '1. Explicit URL set remote route, no local spawn',
14+
name: '1. Explicit URL set - remote HTTP route, no local spawn',
2015
config: { cortexUrl: 'https://cortex.myteam.com:7437' },
2116
env: {},
2217
expected: { mode: 'remote', spawnAllowed: false, hasUrl: true, reasonMatch: /explicit plugin URL/ }
2318
},
2419
{
25-
name: '2. Dev prefer app + URL set remote route, no local spawn',
20+
name: '2. Dev prefer app + CORTEX_APP_URL set - remote HTTP route, no local spawn',
2621
config: {},
2722
env: {
2823
CORTEX_DEV_PREFER_APP: '1',
29-
CORTEX_DEV_APP_URL: 'http://127.0.0.1:7437'
24+
CORTEX_APP_URL: 'http://127.0.0.1:7437'
3025
},
3126
expected: { mode: 'remote', spawnAllowed: false, hasUrl: true, reasonMatch: /dev prefer app/ }
3227
},
3328
{
34-
name: '3. Dev prefer app + NO URL — explicit failure',
29+
name: '3. Dev prefer app + NO CORTEX_APP_URL - explicit failure',
3530
config: {},
3631
env: { CORTEX_DEV_PREFER_APP: '1' },
37-
expected: { mode: 'fail', spawnAllowed: false, hasUrl: false, reasonMatch: /CORTEX_DEV_PREFER_APP=1/ }
32+
expected: { mode: 'fail', spawnAllowed: false, hasUrl: false, reasonMatch: /CORTEX_APP_URL/ }
3833
},
3934
{
40-
name: '4. No URL, local spawn allowed — local route',
35+
name: '4. No URL - local HTTP attach-only route, no local spawn',
4136
config: {},
4237
env: {},
43-
expected: { mode: 'local', spawnAllowed: true, hasUrl: false, reasonMatch: /local service-first/ }
38+
expected: { mode: 'local', spawnAllowed: false, hasUrl: true, reasonMatch: /local HTTP attach-only/ }
4439
},
4540
{
46-
name: '5. No URL, local spawn disabled — explicit failure',
41+
name: '5. Local-spawn disable flag is redundant - still local HTTP attach-only',
4742
config: {},
4843
env: { CORTEX_DEV_DISABLE_LOCAL_SPAWN: '1' },
49-
expected: {
50-
mode: 'fail',
51-
spawnAllowed: false,
52-
hasUrl: false,
53-
reasonMatch: /CORTEX_DEV_DISABLE_LOCAL_SPAWN=1/
54-
}
44+
expected: { mode: 'local', spawnAllowed: false, hasUrl: true, reasonMatch: /local HTTP attach-only/ }
5545
},
56-
// Extra bonus cases to lock corner behavior
5746
{
5847
name: '6. Explicit URL beats dev prefer app',
5948
config: { cortexUrl: 'https://explicit.example' },
6049
env: {
6150
CORTEX_DEV_PREFER_APP: '1',
62-
CORTEX_DEV_APP_URL: 'http://should-be-ignored'
51+
CORTEX_APP_URL: 'http://should-be-ignored'
6352
},
6453
expected: { mode: 'remote', spawnAllowed: false, hasUrl: true, reasonMatch: /explicit plugin URL/ }
6554
},
6655
{
67-
name: '7. Local disable overridden by CORTEX_PLUGIN_ALLOW_LOCAL_SPAWN',
56+
name: '7. Legacy local-spawn allow flag is ignored - no spawn path exists',
6857
config: {},
6958
env: {
7059
CORTEX_DEV_DISABLE_LOCAL_SPAWN: '1',
7160
CORTEX_PLUGIN_ALLOW_LOCAL_SPAWN: '1'
7261
},
73-
expected: { mode: 'local', spawnAllowed: true, hasUrl: false, reasonMatch: /local service-first/ }
62+
expected: { mode: 'local', spawnAllowed: false, hasUrl: true, reasonMatch: /local HTTP attach-only/ }
7463
},
7564
{
76-
name: '8. CORTEX_APP_URL alone remote app route',
65+
name: '8. CORTEX_APP_URL alone - remote app route',
7766
config: {},
7867
env: { CORTEX_APP_URL: 'http://127.0.0.1:7437' },
7968
expected: { mode: 'remote', spawnAllowed: false, hasUrl: true, reasonMatch: /app route/ }
@@ -92,11 +81,14 @@ for (const tc of cases) {
9281
} else {
9382
assert.equal(route.url, '', 'expected empty url');
9483
}
84+
if (route.mode === 'local') {
85+
assert.equal(route.url, DEFAULT_LOCAL_BASE_URL);
86+
}
9587
if (tc.expected.reasonMatch) {
9688
assert.match(route.reason, tc.expected.reasonMatch, 'reason mismatch');
9789
}
9890
console.log(`PASS ${tc.name}`);
99-
console.log(` mode=${route.mode} spawnAllowed=${route.spawnAllowed} reason="${route.reason}"`);
91+
console.log(` -> mode=${route.mode} spawnAllowed=${route.spawnAllowed} reason="${route.reason}"`);
10092
pass++;
10193
} catch (err) {
10294
console.error(`FAIL ${tc.name}`);
@@ -107,3 +99,4 @@ for (const tc of cases) {
10799

108100
console.log(`\n${pass}/${cases.length} passed. ${fail} failed.`);
109101
process.exit(fail === 0 ? 0 : 1);
102+

0 commit comments

Comments
 (0)