@polygraph/codex-plugin 0.4.29 → 0.4.30
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/hooks/hooks.json
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
"type": "command",
|
|
9
9
|
"command": "node ${PLUGIN_ROOT}/hooks/reinject-polygraph-context.mjs",
|
|
10
10
|
"statusMessage": "Re-injecting Polygraph session context"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "node ${PLUGIN_ROOT}/hooks/record-session-mapping.mjs codex",
|
|
15
|
+
"statusMessage": "Recording Polygraph agent capture mapping"
|
|
11
16
|
}
|
|
12
17
|
]
|
|
13
18
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Hidden SessionStart hook — records an agent-capture mapping file that binds
|
|
2
|
+
// this agent's session id to the Polygraph session id in the environment.
|
|
3
|
+
// Used by both the Claude Code plugin (agentType=claude) and the Codex plugin
|
|
4
|
+
// (agentType=codex). The agentType is passed as the first CLI argument so the
|
|
5
|
+
// same script ships in both plugin artifacts.
|
|
6
|
+
//
|
|
7
|
+
// File contract (must match the Polygraph CLI reader exactly):
|
|
8
|
+
// ~/.polygraph/sidecars/<POLYGRAPH_SESSION_ID>/mapping-<agentType>-<agentSessionId>.json
|
|
9
|
+
//
|
|
10
|
+
// Behaviour:
|
|
11
|
+
// - Silent no-op when POLYGRAPH_SESSION_ID is unset.
|
|
12
|
+
// - Silent no-op when POLYGRAPH_CHILD_AGENT is set (child agents must not
|
|
13
|
+
// register themselves as parents).
|
|
14
|
+
// - Atomic write: write to <path>.tmp-<pid>, then rename over final path.
|
|
15
|
+
// - Refresh: when a valid prior mapping for the same session already exists,
|
|
16
|
+
// preserve its firstSeenAt and only update lastSeenAt + mutable fields.
|
|
17
|
+
// - All failures are silently swallowed; never writes to stdout (Claude Code
|
|
18
|
+
// injects hook stdout into the model context); never exits non-zero.
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
renameSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
function readStdin() {
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(0, 'utf8');
|
|
35
|
+
} catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tryParseJson(str) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(str);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitizeFilename(str) {
|
|
49
|
+
return str.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write (or refresh) the agent-capture mapping file.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} opts
|
|
56
|
+
* @param {string} opts.agentType 'claude' | 'codex'
|
|
57
|
+
* @param {string} opts.agentSessionId The harness's own session id.
|
|
58
|
+
* @param {string} opts.polygraphSessionId Value of POLYGRAPH_SESSION_ID.
|
|
59
|
+
* @param {string} opts.cwd Agent working directory.
|
|
60
|
+
* @param {string} [opts.transcriptPath] Absolute transcript path; omit when unknown.
|
|
61
|
+
* @param {number} [opts.pid] Harness process id; omit when not knowable.
|
|
62
|
+
* @param {string} [home] Override HOME for testing.
|
|
63
|
+
*/
|
|
64
|
+
export function writeCaptureMapping(
|
|
65
|
+
{ agentType, agentSessionId, polygraphSessionId, cwd, transcriptPath, pid },
|
|
66
|
+
home = process.env.HOME?.trim() || homedir()
|
|
67
|
+
) {
|
|
68
|
+
const sidecarDir = join(home, '.polygraph', 'sidecars', polygraphSessionId);
|
|
69
|
+
mkdirSync(sidecarDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const filenamePart = sanitizeFilename(`${agentType}-${agentSessionId}`);
|
|
72
|
+
const finalPath = join(sidecarDir, `mapping-${filenamePart}.json`);
|
|
73
|
+
const tmpPath = `${finalPath}.tmp-${process.pid}`;
|
|
74
|
+
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
// Refresh semantics: preserve firstSeenAt from a valid prior mapping.
|
|
78
|
+
let firstSeenAt = now;
|
|
79
|
+
if (existsSync(finalPath)) {
|
|
80
|
+
const existing = tryParseJson(readFileSync(finalPath, 'utf8'));
|
|
81
|
+
if (
|
|
82
|
+
existing !== null &&
|
|
83
|
+
existing.version === 1 &&
|
|
84
|
+
existing.polygraphSessionId === polygraphSessionId &&
|
|
85
|
+
existing.agentSessionId === agentSessionId &&
|
|
86
|
+
Number.isFinite(existing.firstSeenAt)
|
|
87
|
+
) {
|
|
88
|
+
firstSeenAt = existing.firstSeenAt;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mapping = {
|
|
93
|
+
version: 1,
|
|
94
|
+
polygraphSessionId,
|
|
95
|
+
agentType,
|
|
96
|
+
agentSessionId,
|
|
97
|
+
cwd,
|
|
98
|
+
...(transcriptPath != null ? { transcriptPath } : {}),
|
|
99
|
+
...(pid != null ? { pid } : {}),
|
|
100
|
+
source: 'hook',
|
|
101
|
+
firstSeenAt,
|
|
102
|
+
lastSeenAt: now,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
writeFileSync(tmpPath, JSON.stringify(mapping, null, 2) + '\n');
|
|
106
|
+
renameSync(tmpPath, finalPath);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function main() {
|
|
110
|
+
try {
|
|
111
|
+
const polygraphSessionId = process.env.POLYGRAPH_SESSION_ID;
|
|
112
|
+
if (!polygraphSessionId) return;
|
|
113
|
+
if (process.env.POLYGRAPH_CHILD_AGENT) return;
|
|
114
|
+
|
|
115
|
+
const agentType = process.argv[2];
|
|
116
|
+
if (!agentType) return;
|
|
117
|
+
|
|
118
|
+
let payload = {};
|
|
119
|
+
const raw = readStdin();
|
|
120
|
+
if (raw) {
|
|
121
|
+
const parsed = tryParseJson(raw);
|
|
122
|
+
if (parsed !== null) payload = parsed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const agentSessionId =
|
|
126
|
+
typeof payload.session_id === 'string' ? payload.session_id : '';
|
|
127
|
+
if (!agentSessionId) return;
|
|
128
|
+
|
|
129
|
+
const cwd =
|
|
130
|
+
typeof payload.cwd === 'string' && payload.cwd
|
|
131
|
+
? payload.cwd
|
|
132
|
+
: process.cwd();
|
|
133
|
+
|
|
134
|
+
// transcript_path is present on Claude/Codex payloads; may be null — omit
|
|
135
|
+
// the field when absent or null rather than writing null into the mapping.
|
|
136
|
+
const transcriptPath =
|
|
137
|
+
typeof payload.transcript_path === 'string' && payload.transcript_path
|
|
138
|
+
? payload.transcript_path
|
|
139
|
+
: undefined;
|
|
140
|
+
|
|
141
|
+
writeCaptureMapping({
|
|
142
|
+
agentType,
|
|
143
|
+
agentSessionId,
|
|
144
|
+
polygraphSessionId,
|
|
145
|
+
cwd,
|
|
146
|
+
transcriptPath,
|
|
147
|
+
// process.ppid is the harness pid when the hook is spawned as a child.
|
|
148
|
+
pid: process.ppid,
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
// Silent — a broken hook must never break the agent session.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Run only when executed directly as a hook, not when imported (e.g. by tests).
|
|
156
|
+
// realpathSync both sides so the check holds when the plugin lives under a
|
|
157
|
+
// symlinked path (e.g. macOS /tmp -> /private/tmp).
|
|
158
|
+
function isMainModule() {
|
|
159
|
+
if (!process.argv[1]) return false;
|
|
160
|
+
try {
|
|
161
|
+
return (
|
|
162
|
+
realpathSync(process.argv[1]) ===
|
|
163
|
+
realpathSync(fileURLToPath(import.meta.url))
|
|
164
|
+
);
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isMainModule()) {
|
|
171
|
+
main();
|
|
172
|
+
}
|