@polygraph/codex-plugin 0.4.51 → 0.4.53
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
|
|
6
6
|
const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -8,6 +8,13 @@ const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
8
8
|
const AGENT_TYPES = new Set(['claude', 'codex', 'opencode', 'cursor']);
|
|
9
9
|
const COMMAND_HOOK_TOOL = /^mcp__(?:plugin_polygraph_)?polygraph[-_]mcp__/;
|
|
10
10
|
const OPENCODE_TOOL = /^polygraph(?:(?:-|_)mcp)?_/;
|
|
11
|
+
// Cursor reports MCP tools as `MCP:<tool>` with no server namespace, so the
|
|
12
|
+
// shim filters on the claim-worthy tool names to avoid spawning the CLI on
|
|
13
|
+
// every unrelated MCP call. This list is an optimization mirror of
|
|
14
|
+
// PARENT_CLAIM_POLICIES in the Polygraph CLI (parent-session-claim-evidence);
|
|
15
|
+
// classification authority stays in the CLI.
|
|
16
|
+
const CURSOR_MCP_CLAIM_TOOL =
|
|
17
|
+
/^MCP:(?:add_repo|allow_agent|archive_session|associate_pr|create_pr|deny_agent|git_fetch|link_reference|mark_pr_ready|pack_and_copy|push_branch|spawn_agent|start_session|stop_agent|update_session|upload_artifact)$/;
|
|
11
18
|
|
|
12
19
|
function nonEmptyString(value) {
|
|
13
20
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
@@ -30,6 +37,7 @@ export function buildLinkAgentSessionArgs({
|
|
|
30
37
|
transcriptPath,
|
|
31
38
|
pid,
|
|
32
39
|
source,
|
|
40
|
+
hookOperation,
|
|
33
41
|
}) {
|
|
34
42
|
const session = nonEmptyString(polygraphSessionId);
|
|
35
43
|
const harnessSession = nonEmptyString(agentSessionId);
|
|
@@ -52,14 +60,38 @@ export function buildLinkAgentSessionArgs({
|
|
|
52
60
|
args.push('--pid', String(pid));
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
// Cursor post-tool evidence rides the hook payload (the transcript stores
|
|
64
|
+
// no tool results); forwarded verbatim, classified by the CLI. The
|
|
65
|
+
// operation travels on STDIN, never argv: toolInput can carry an entire
|
|
66
|
+
// upload_artifact document and Linux caps one argv string at 128KB, so an
|
|
67
|
+
// inline argument would kill the spawn with E2BIG and silently lose the
|
|
68
|
+
// evidence. The flag tells the CLI to read stdin; older strict CLIs
|
|
69
|
+
// reject it, which is why publication is gated on the Ocean deployment.
|
|
70
|
+
let input;
|
|
71
|
+
if (hookOperation && typeof hookOperation === 'object') {
|
|
72
|
+
args.push('--hook-operation-stdin');
|
|
73
|
+
input = JSON.stringify(hookOperation);
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
args.push('--source', claimSource);
|
|
56
|
-
return args;
|
|
77
|
+
return { args, input };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Node runtime for the JS-entry fallback. This shim also runs inside
|
|
82
|
+
* non-Node hosts (the opencode plugin executes it in-process, and opencode
|
|
83
|
+
* is a compiled Bun binary), where process.execPath is not a Node
|
|
84
|
+
* executable — fall back to PATH resolution there.
|
|
85
|
+
*/
|
|
86
|
+
function nodeRuntime() {
|
|
87
|
+
const base = basename(process.execPath).toLowerCase();
|
|
88
|
+
return base === 'node' || base === 'node.exe' ? process.execPath : 'node';
|
|
57
89
|
}
|
|
58
90
|
|
|
59
91
|
export function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
|
|
60
92
|
if (isManagedChildEnvironment(env)) return false;
|
|
61
93
|
|
|
62
|
-
const args = buildLinkAgentSessionArgs(claim);
|
|
94
|
+
const { args, input } = buildLinkAgentSessionArgs(claim);
|
|
63
95
|
const command = nonEmptyString(env?.POLYGRAPH_CLI) ?? 'polygraph';
|
|
64
96
|
const commandEnv = nonEmptyString(claim.polygraphSessionId) ? env : { ...env };
|
|
65
97
|
if (commandEnv !== env) {
|
|
@@ -67,11 +99,23 @@ export function linkAgentSession(claim, spawn = spawnSync, env = process.env) {
|
|
|
67
99
|
delete commandEnv.POLYGRAPH_CAPTURE_TOKEN;
|
|
68
100
|
}
|
|
69
101
|
|
|
70
|
-
const
|
|
102
|
+
const spawnOptions = {
|
|
71
103
|
encoding: 'utf8',
|
|
72
104
|
env: commandEnv,
|
|
73
|
-
stdio: ['ignore', 'ignore', 'pipe'],
|
|
74
|
-
|
|
105
|
+
stdio: [input === undefined ? 'ignore' : 'pipe', 'ignore', 'pipe'],
|
|
106
|
+
...(input === undefined ? {} : { input }),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let result = spawn(command, args, spawnOptions);
|
|
110
|
+
|
|
111
|
+
// POLYGRAPH_CLI may point at a plain JS entry that cannot be spawned
|
|
112
|
+
// directly: a dev build without the executable bit, or a platform that
|
|
113
|
+
// cannot exec scripts. A spawn that failed to LAUNCH ran nothing, so the
|
|
114
|
+
// retry under a Node runtime is side-effect free — and anything that
|
|
115
|
+
// spawns directly today keeps its exact behavior.
|
|
116
|
+
if (result?.error && /\.[cm]?js$/i.test(command)) {
|
|
117
|
+
result = spawn(nodeRuntime(), [command, ...args], spawnOptions);
|
|
118
|
+
}
|
|
75
119
|
|
|
76
120
|
if (result?.error) throw result.error;
|
|
77
121
|
if (result?.status !== 0) {
|
|
@@ -126,6 +170,24 @@ export function buildCommandHookLink(payload, agentType, env = process.env) {
|
|
|
126
170
|
return isPolygraphMcpToolName(payload.tool_name) ? common : undefined;
|
|
127
171
|
}
|
|
128
172
|
|
|
173
|
+
// Cursor's camelCase postToolUse: the payload carries the whole operation
|
|
174
|
+
// (tool_name `MCP:<tool>`, tool_input, tool_output) and is forwarded as
|
|
175
|
+
// evidence because the cursor transcript stores no tool results and lags
|
|
176
|
+
// the hook. The CLI classifies the operation; this filter only avoids
|
|
177
|
+
// spawning the CLI for unrelated tools.
|
|
178
|
+
if (payload.hook_event_name === 'postToolUse') {
|
|
179
|
+
const toolName = nonEmptyString(payload.tool_name);
|
|
180
|
+
if (!toolName || !CURSOR_MCP_CLAIM_TOOL.test(toolName)) return undefined;
|
|
181
|
+
return {
|
|
182
|
+
...common,
|
|
183
|
+
hookOperation: {
|
|
184
|
+
toolName,
|
|
185
|
+
toolInput: payload.tool_input,
|
|
186
|
+
toolOutput: payload.tool_output,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
129
191
|
return undefined;
|
|
130
192
|
}
|
|
131
193
|
|
package/package.json
CHANGED
|
@@ -40,7 +40,12 @@ For each id, launch one background poller subagent whose entire job is to block
|
|
|
40
40
|
|
|
41
41
|
- **Claude Code** — a background `Task` with `subagent_type: "polygraph:polygraph-delegate-subagent"`, `run_in_background: true`, and description `Delegate to <repo>`. Fall back to the bare agent name only if the namespaced form is not found.
|
|
42
42
|
- **OpenCode** — invoke `@polygraph-delegate-subagent`.
|
|
43
|
-
- **Codex** — launch `agent_type: "polygraph-delegate-subagent"` via Codex's own `spawn_agent`, and collect it with `wait_agent
|
|
43
|
+
- **Codex** — launch `agent_type: "polygraph-delegate-subagent"` via Codex's own `spawn_agent`, and collect it with `wait_agent`, passing a long `timeout_ms` (five minutes or more): `wait_agent` returns as soon as the poller stops, so a short timeout only adds wake-ups that burn tokens and fill the user-visible transcript with waiting noise.
|
|
44
|
+
- **Cursor** — a background `Task` with `subagent_type: "polygraph-delegate-subagent"`, `run_in_background: true`, and description `Delegate to <repo>`. Collect it with `Await`.
|
|
45
|
+
|
|
46
|
+
A collect step that returns while the poller subagent is still running has not failed. It has only reached the end of its collection window. Collect the same background-task id again, as many times as it takes for the poller subagent to stop. A poller that runs for several minutes is ordinary, and it is never a reason to take the wait back into the main conversation.
|
|
47
|
+
|
|
48
|
+
The background-task id is the handle your own harness returned when you launched the poller. It is not the Polygraph delegation id (`frontend-1`), which addresses the child agent. Once the poller subagent has finished, do not collect it again: read the child instead, as described below. A child that stops for attention also ends the poller, so treat that as a finished poller and not as a collection window running out.
|
|
44
49
|
|
|
45
50
|
The poller has exactly one tool and cannot read logs. It exits with a few lines naming the repo, the id, and the final status. That message is a doorbell, not a report — it tells you the child is worth reading, and nothing about what the child did.
|
|
46
51
|
|