polygram 0.38.7 → 0.38.9
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/plugin.json +1 -1
- package/config.example.json +1 -0
- package/docs/2026-08-16-003-codex-protected-hook-artifact-runbook.md +323 -0
- package/lib/codex/hook-artifacts.js +1033 -0
- package/lib/codex/hook-bundle.js +296 -0
- package/lib/codex/hook-command.js +192 -0
- package/lib/codex/hook-runtime.js +533 -0
- package/lib/codex/hook-source-policy.js +440 -0
- package/lib/codex/hooks/hook-observer.bundle.js +185 -0
- package/lib/codex/hooks/observer-capture.js +61 -0
- package/lib/codex/hooks/observer-entry.js +74 -0
- package/lib/codex/runtime-controller.js +52 -2
- package/lib/codex/runtime-profile.js +215 -31
- package/package.json +8 -2
- package/scripts/build-codex-hook-bundle.js +74 -0
- package/scripts/codex-hook-artifacts.js +136 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const acorn = require('acorn');
|
|
4
|
+
|
|
5
|
+
// A positive syntax policy over the checked-in hook sources. A facade is a
|
|
6
|
+
// convenience for the code that passes this gate, not a sandbox: `writeFileSync
|
|
7
|
+
// ['constructor']` reaches the host realm from any function value, and the
|
|
8
|
+
// property name can be assembled at run time, so no textual scan can see it.
|
|
9
|
+
// What the gate accepts is therefore enumerated here, and everything else —
|
|
10
|
+
// unknown syntax, unknown free identifiers, computed property access, and any
|
|
11
|
+
// member outside a facade's exact surface — is refused.
|
|
12
|
+
|
|
13
|
+
const ALLOWED_NODE_TYPES = new Set([
|
|
14
|
+
'ArrayExpression',
|
|
15
|
+
'ArrayPattern',
|
|
16
|
+
'ArrowFunctionExpression',
|
|
17
|
+
'AssignmentExpression',
|
|
18
|
+
'AssignmentPattern',
|
|
19
|
+
'AwaitExpression',
|
|
20
|
+
'BinaryExpression',
|
|
21
|
+
'BlockStatement',
|
|
22
|
+
'BreakStatement',
|
|
23
|
+
'CallExpression',
|
|
24
|
+
'CatchClause',
|
|
25
|
+
'ChainExpression',
|
|
26
|
+
'ConditionalExpression',
|
|
27
|
+
'ContinueStatement',
|
|
28
|
+
'DoWhileStatement',
|
|
29
|
+
'EmptyStatement',
|
|
30
|
+
'ExpressionStatement',
|
|
31
|
+
'ForOfStatement',
|
|
32
|
+
'ForStatement',
|
|
33
|
+
'FunctionDeclaration',
|
|
34
|
+
'FunctionExpression',
|
|
35
|
+
'Identifier',
|
|
36
|
+
'IfStatement',
|
|
37
|
+
'Literal',
|
|
38
|
+
'LogicalExpression',
|
|
39
|
+
'MemberExpression',
|
|
40
|
+
'NewExpression',
|
|
41
|
+
'ObjectExpression',
|
|
42
|
+
'ObjectPattern',
|
|
43
|
+
'Program',
|
|
44
|
+
'Property',
|
|
45
|
+
'RestElement',
|
|
46
|
+
'ReturnStatement',
|
|
47
|
+
'SpreadElement',
|
|
48
|
+
'TemplateElement',
|
|
49
|
+
'TemplateLiteral',
|
|
50
|
+
'ThrowStatement',
|
|
51
|
+
'TryStatement',
|
|
52
|
+
'UnaryExpression',
|
|
53
|
+
'UpdateExpression',
|
|
54
|
+
'VariableDeclaration',
|
|
55
|
+
'VariableDeclarator',
|
|
56
|
+
'WhileStatement',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Free identifiers a source may name. `require` and `process` are the injected
|
|
60
|
+
// facades; the rest are realm intrinsics with no loader on them.
|
|
61
|
+
const ALLOWED_FREE_IDENTIFIERS = new Set([
|
|
62
|
+
'Array',
|
|
63
|
+
'Boolean',
|
|
64
|
+
'Buffer',
|
|
65
|
+
'Date',
|
|
66
|
+
'Error',
|
|
67
|
+
'Infinity',
|
|
68
|
+
'JSON',
|
|
69
|
+
'Math',
|
|
70
|
+
'NaN',
|
|
71
|
+
'Number',
|
|
72
|
+
'Object',
|
|
73
|
+
'Promise',
|
|
74
|
+
'String',
|
|
75
|
+
'clearTimeout',
|
|
76
|
+
'exports',
|
|
77
|
+
'module',
|
|
78
|
+
'process',
|
|
79
|
+
'require',
|
|
80
|
+
'setTimeout',
|
|
81
|
+
'undefined',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const ALLOWED_CONSTRUCTORS = new Set(['Error', 'Promise']);
|
|
85
|
+
|
|
86
|
+
// Property names that hand back a function's realm, its prototype chain, or a
|
|
87
|
+
// rebound receiver.
|
|
88
|
+
const DENIED_PROPERTIES = new Set([
|
|
89
|
+
'__defineGetter__',
|
|
90
|
+
'__defineSetter__',
|
|
91
|
+
'__lookupGetter__',
|
|
92
|
+
'__lookupSetter__',
|
|
93
|
+
'__proto__',
|
|
94
|
+
'apply',
|
|
95
|
+
'arguments',
|
|
96
|
+
'bind',
|
|
97
|
+
'call',
|
|
98
|
+
'caller',
|
|
99
|
+
'constructor',
|
|
100
|
+
'prototype',
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// Names that denote a loader, a child process, or an external read on any
|
|
104
|
+
// receiver. A hook source has no use for them, so they are refused wherever
|
|
105
|
+
// they appear rather than only on a receiver the gate can resolve.
|
|
106
|
+
const DENIED_MEMBER_NAMES = new Set([
|
|
107
|
+
'binding',
|
|
108
|
+
'createReadStream',
|
|
109
|
+
'dlopen',
|
|
110
|
+
'exec',
|
|
111
|
+
'execFile',
|
|
112
|
+
'execFileSync',
|
|
113
|
+
'execSync',
|
|
114
|
+
'fork',
|
|
115
|
+
'getBuiltinModule',
|
|
116
|
+
'open',
|
|
117
|
+
'openSync',
|
|
118
|
+
'read',
|
|
119
|
+
'readFile',
|
|
120
|
+
'readFileSync',
|
|
121
|
+
'readSync',
|
|
122
|
+
'readdir',
|
|
123
|
+
'readdirSync',
|
|
124
|
+
'require',
|
|
125
|
+
'spawn',
|
|
126
|
+
'spawnSync',
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
// Ambient intrinsics carry reflection that hands back a function's realm, so
|
|
130
|
+
// their surface is positive too: a member not listed here is refused rather
|
|
131
|
+
// than reasoned about.
|
|
132
|
+
const GLOBAL_MEMBER_POLICY = new Map([
|
|
133
|
+
['Array', new Set(['from', 'isArray', 'of'])],
|
|
134
|
+
['Buffer', new Set(['alloc', 'byteLength', 'concat', 'from', 'isBuffer'])],
|
|
135
|
+
['Date', new Set(['now'])],
|
|
136
|
+
['JSON', new Set(['parse', 'stringify'])],
|
|
137
|
+
['Math', new Set(['abs', 'ceil', 'floor', 'max', 'min', 'round'])],
|
|
138
|
+
['Number', new Set(['isFinite', 'isInteger', 'parseFloat', 'parseInt'])],
|
|
139
|
+
['Object', new Set(['assign', 'entries', 'freeze', 'fromEntries', 'hasOwn', 'keys', 'values'])],
|
|
140
|
+
['Promise', new Set(['all', 'allSettled', 'race', 'reject', 'resolve'])],
|
|
141
|
+
['String', new Set(['fromCharCode'])],
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// The members a bundled module may use on the injected `process` facade. The
|
|
145
|
+
// facade carries only these, and the gate refuses the rest by name so a source
|
|
146
|
+
// cannot be written against a wider host `process`.
|
|
147
|
+
const ALLOWED_PROCESS_MEMBERS = new Set(['argv', 'pid', 'setExitCode', 'stdin']);
|
|
148
|
+
|
|
149
|
+
function isNode(value) {
|
|
150
|
+
return Boolean(value) && typeof value === 'object' && typeof value.type === 'string';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function children(node) {
|
|
154
|
+
const found = [];
|
|
155
|
+
for (const [key, value] of Object.entries(node)) {
|
|
156
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
for (const entry of value) if (isNode(entry)) found.push(entry);
|
|
159
|
+
} else if (isNode(value)) {
|
|
160
|
+
found.push(value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Only the binding side of a pattern declares names. A default value is an
|
|
167
|
+
// ordinary expression — descending into it would let `(value = process) => …`
|
|
168
|
+
// register `process` as a local name.
|
|
169
|
+
function collectPatternNames(node, names) {
|
|
170
|
+
if (!isNode(node)) return;
|
|
171
|
+
if (node.type === 'Identifier') {
|
|
172
|
+
names.add(node.name);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (node.type === 'Property') {
|
|
176
|
+
collectPatternNames(node.value, names);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (node.type === 'AssignmentPattern') {
|
|
180
|
+
collectPatternNames(node.left, names);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const child of children(node)) collectPatternNames(child, names);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const FUNCTION_TYPES = new Set([
|
|
187
|
+
'ArrowFunctionExpression',
|
|
188
|
+
'FunctionDeclaration',
|
|
189
|
+
'FunctionExpression',
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
// Scopes are resolved lexically. A binding introduced inside one function or
|
|
193
|
+
// block must not vouch for an ambient identifier at a use site that binding
|
|
194
|
+
// never covers, so every name is looked up along its own scope chain.
|
|
195
|
+
function createScope(parent) {
|
|
196
|
+
return { parent, names: new Set() };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolvesIn(scope, name) {
|
|
200
|
+
for (let current = scope; current; current = current.parent) {
|
|
201
|
+
if (current.names.has(name)) return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Only `var` hoists to the enclosing function; the walk stops at nested
|
|
207
|
+
// functions, which own their own hoisting. A function declared inside a block
|
|
208
|
+
// is block-scoped under strict mode, so it is collected by that block rather
|
|
209
|
+
// than promoted outward from here.
|
|
210
|
+
function collectHoistedNames(node, names) {
|
|
211
|
+
if (FUNCTION_TYPES.has(node.type)) return;
|
|
212
|
+
if (node.type === 'VariableDeclaration' && node.kind === 'var') {
|
|
213
|
+
for (const declarator of node.declarations) collectPatternNames(declarator.id, names);
|
|
214
|
+
}
|
|
215
|
+
for (const child of children(node)) collectHoistedNames(child, names);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function collectLexicalNames(statements, names) {
|
|
219
|
+
for (const statement of statements ?? []) {
|
|
220
|
+
if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
|
|
221
|
+
for (const declarator of statement.declarations) {
|
|
222
|
+
collectPatternNames(declarator.id, names);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (statement.type === 'FunctionDeclaration' && statement.id) {
|
|
226
|
+
names.add(statement.id.name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function scopeFor(node, parent) {
|
|
232
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
233
|
+
const scope = createScope(parent);
|
|
234
|
+
if (node.id) scope.names.add(node.id.name);
|
|
235
|
+
for (const param of node.params) collectPatternNames(param, scope.names);
|
|
236
|
+
if (node.body.type === 'BlockStatement') {
|
|
237
|
+
collectHoistedNames(node.body, scope.names);
|
|
238
|
+
collectLexicalNames(node.body.body, scope.names);
|
|
239
|
+
}
|
|
240
|
+
return scope;
|
|
241
|
+
}
|
|
242
|
+
if (node.type === 'BlockStatement') {
|
|
243
|
+
const scope = createScope(parent);
|
|
244
|
+
collectLexicalNames(node.body, scope.names);
|
|
245
|
+
return scope;
|
|
246
|
+
}
|
|
247
|
+
if (node.type === 'ForStatement' || node.type === 'ForOfStatement') {
|
|
248
|
+
const scope = createScope(parent);
|
|
249
|
+
const declaration = node.init ?? node.left;
|
|
250
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
251
|
+
for (const declarator of declaration.declarations) {
|
|
252
|
+
collectPatternNames(declarator.id, scope.names);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return scope;
|
|
256
|
+
}
|
|
257
|
+
if (node.type === 'CatchClause') {
|
|
258
|
+
const scope = createScope(parent);
|
|
259
|
+
if (node.param) collectPatternNames(node.param, scope.names);
|
|
260
|
+
return scope;
|
|
261
|
+
}
|
|
262
|
+
return parent;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function programScope(program) {
|
|
266
|
+
const scope = createScope(null);
|
|
267
|
+
collectHoistedNames(program, scope.names);
|
|
268
|
+
collectLexicalNames(program.body, scope.names);
|
|
269
|
+
return scope;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function requireSpecifier(node) {
|
|
273
|
+
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier'
|
|
274
|
+
|| node.callee.name !== 'require') {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return node;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// `const { a } = require('x')` and `const ns = require('x')` are the only
|
|
281
|
+
// shapes a source may use, so every imported name stays statically known.
|
|
282
|
+
function collectFacadeBindings(program, reject, allowedSpecifiers, facadeMembers) {
|
|
283
|
+
const namespaces = new Map();
|
|
284
|
+
const visit = (node, parent) => {
|
|
285
|
+
const call = requireSpecifier(node);
|
|
286
|
+
if (call) {
|
|
287
|
+
if (call.arguments.length !== 1 || call.arguments[0].type !== 'Literal'
|
|
288
|
+
|| typeof call.arguments[0].value !== 'string') {
|
|
289
|
+
reject('a require of a computed module specifier');
|
|
290
|
+
}
|
|
291
|
+
const specifier = call.arguments[0].value;
|
|
292
|
+
if (!allowedSpecifiers.has(specifier)) {
|
|
293
|
+
reject(`a require of ${specifier}, which is outside the closure`);
|
|
294
|
+
}
|
|
295
|
+
if (!parent || parent.type !== 'VariableDeclarator' || parent.init !== call) {
|
|
296
|
+
reject('a require outside a plain binding declaration');
|
|
297
|
+
}
|
|
298
|
+
const members = facadeMembers.get(specifier);
|
|
299
|
+
if (parent.id.type === 'ObjectPattern') {
|
|
300
|
+
for (const property of parent.id.properties) {
|
|
301
|
+
if (property.type !== 'Property' || property.computed
|
|
302
|
+
|| property.key.type !== 'Identifier') {
|
|
303
|
+
reject('a computed or spread import binding');
|
|
304
|
+
}
|
|
305
|
+
if (members && !members.has(property.key.name)) {
|
|
306
|
+
reject(`the member ${property.key.name} of ${specifier}, which the facade does not carry`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else if (parent.id.type === 'Identifier') {
|
|
310
|
+
if (members) namespaces.set(parent.id.name, specifier);
|
|
311
|
+
} else {
|
|
312
|
+
reject('a require bound to a pattern the gate cannot follow');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const child of children(node)) visit(child, node);
|
|
316
|
+
};
|
|
317
|
+
visit(program, null);
|
|
318
|
+
return namespaces;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function auditProgram(program, {
|
|
322
|
+
reject,
|
|
323
|
+
allowedSpecifiers,
|
|
324
|
+
facadeMembers,
|
|
325
|
+
processMembers = ALLOWED_PROCESS_MEMBERS,
|
|
326
|
+
}) {
|
|
327
|
+
const namespaces = collectFacadeBindings(program, reject, allowedSpecifiers, facadeMembers);
|
|
328
|
+
const skipped = new Set();
|
|
329
|
+
|
|
330
|
+
const visit = (node, scope) => {
|
|
331
|
+
if (!ALLOWED_NODE_TYPES.has(node.type)) {
|
|
332
|
+
reject(`the unsupported syntax ${node.type}`);
|
|
333
|
+
}
|
|
334
|
+
if (node.type === 'ObjectPattern') {
|
|
335
|
+
// Destructuring reads properties without a member expression, and a
|
|
336
|
+
// pattern over an arbitrary value is how `constructor` is lifted off a
|
|
337
|
+
// function. Only an import binding destructures a value here.
|
|
338
|
+
for (const property of node.properties) {
|
|
339
|
+
if (property.type !== 'Property' || property.computed
|
|
340
|
+
|| property.key.type !== 'Identifier') {
|
|
341
|
+
reject('an unsupported destructuring shape');
|
|
342
|
+
} else if (DENIED_PROPERTIES.has(property.key.name)
|
|
343
|
+
|| DENIED_MEMBER_NAMES.has(property.key.name)) {
|
|
344
|
+
reject(`the destructured property ${property.key.name}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (node.type === 'VariableDeclarator' && node.id.type === 'ObjectPattern'
|
|
349
|
+
&& !(node.init && requireSpecifier(node.init))) {
|
|
350
|
+
reject('a destructuring of a value that is not an import');
|
|
351
|
+
}
|
|
352
|
+
if (node.type === 'MemberExpression') {
|
|
353
|
+
if (node.computed) {
|
|
354
|
+
const index = node.property;
|
|
355
|
+
if (index.type !== 'Literal' || typeof index.value !== 'number') {
|
|
356
|
+
reject('computed member access');
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
if (node.property.type !== 'Identifier') {
|
|
360
|
+
reject('a member access the gate cannot read');
|
|
361
|
+
}
|
|
362
|
+
skipped.add(node.property);
|
|
363
|
+
if (DENIED_PROPERTIES.has(node.property.name)) {
|
|
364
|
+
reject(`the property escape ${node.property.name}`);
|
|
365
|
+
}
|
|
366
|
+
if (DENIED_MEMBER_NAMES.has(node.property.name)) {
|
|
367
|
+
reject(`the denied member ${node.property.name}`);
|
|
368
|
+
}
|
|
369
|
+
if (node.object.type === 'Identifier') {
|
|
370
|
+
// An import binding is checked wherever it is named; the ambient
|
|
371
|
+
// rules apply only where the name is not bound by an inner scope.
|
|
372
|
+
const specifier = namespaces.get(node.object.name);
|
|
373
|
+
const members = specifier ? facadeMembers.get(specifier) : null;
|
|
374
|
+
if (members && !members.has(node.property.name)) {
|
|
375
|
+
reject(`the member ${node.property.name} of ${specifier}, which the facade does not carry`);
|
|
376
|
+
}
|
|
377
|
+
if (!resolvesIn(scope, node.object.name)) {
|
|
378
|
+
if (node.object.name === 'process'
|
|
379
|
+
&& !processMembers.has(node.property.name)) {
|
|
380
|
+
reject(`the host process member ${node.property.name}`);
|
|
381
|
+
}
|
|
382
|
+
const intrinsic = GLOBAL_MEMBER_POLICY.get(node.object.name);
|
|
383
|
+
if (intrinsic && !intrinsic.has(node.property.name)) {
|
|
384
|
+
reject(`the reflective member ${node.object.name}.${node.property.name}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (node.type === 'Property' && !node.computed && node.key.type === 'Identifier') {
|
|
391
|
+
skipped.add(node.key);
|
|
392
|
+
}
|
|
393
|
+
if (node.type === 'NewExpression'
|
|
394
|
+
&& (node.callee.type !== 'Identifier' || !ALLOWED_CONSTRUCTORS.has(node.callee.name))) {
|
|
395
|
+
reject('a construction outside the allowed set');
|
|
396
|
+
}
|
|
397
|
+
if (node.type === 'CallExpression'
|
|
398
|
+
&& !['Identifier', 'MemberExpression'].includes(node.callee.type)) {
|
|
399
|
+
reject('a call on a computed callee');
|
|
400
|
+
}
|
|
401
|
+
if (node.type === 'Identifier' && !skipped.has(node)
|
|
402
|
+
&& !resolvesIn(scope, node.name) && !ALLOWED_FREE_IDENTIFIERS.has(node.name)) {
|
|
403
|
+
reject(`the unknown identifier ${node.name}`);
|
|
404
|
+
}
|
|
405
|
+
const inner = scopeFor(node, scope);
|
|
406
|
+
for (const child of children(node)) visit(child, inner);
|
|
407
|
+
};
|
|
408
|
+
visit(program, programScope(program));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function parseSource(text) {
|
|
412
|
+
return acorn.parse(text, {
|
|
413
|
+
ecmaVersion: 2022,
|
|
414
|
+
sourceType: 'script',
|
|
415
|
+
allowReturnOutsideFunction: false,
|
|
416
|
+
allowAwaitOutsideFunction: false,
|
|
417
|
+
allowHashBang: false,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parsesAsModule(text) {
|
|
422
|
+
try {
|
|
423
|
+
acorn.parse(text, { ecmaVersion: 2022, sourceType: 'module' });
|
|
424
|
+
return true;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
module.exports = {
|
|
431
|
+
ALLOWED_FREE_IDENTIFIERS,
|
|
432
|
+
GLOBAL_MEMBER_POLICY,
|
|
433
|
+
DENIED_MEMBER_NAMES,
|
|
434
|
+
ALLOWED_NODE_TYPES,
|
|
435
|
+
ALLOWED_PROCESS_MEMBERS,
|
|
436
|
+
DENIED_PROPERTIES,
|
|
437
|
+
auditProgram,
|
|
438
|
+
parseSource,
|
|
439
|
+
parsesAsModule,
|
|
440
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Generated from the checked-in Codex hook sources by the bundle build
|
|
4
|
+
// step. Bundled modules see only the frozen facades passed in below, so
|
|
5
|
+
// the protected closure is the runtime, this file, and nothing else.
|
|
6
|
+
// Edit the sources and rebuild; never edit this file by hand.
|
|
7
|
+
|
|
8
|
+
(function (__facades, __process) {
|
|
9
|
+
const __definitions = Object.create(null);
|
|
10
|
+
const __instances = Object.create(null);
|
|
11
|
+
|
|
12
|
+
function __load(__specifier) {
|
|
13
|
+
if (Object.hasOwn(__facades, __specifier)) return __facades[__specifier];
|
|
14
|
+
const __definition = __definitions[__specifier];
|
|
15
|
+
if (__definition === undefined) {
|
|
16
|
+
throw new Error('bundled module ' + __specifier + ' is outside the closure');
|
|
17
|
+
}
|
|
18
|
+
let __instance = __instances[__specifier];
|
|
19
|
+
if (__instance === undefined) {
|
|
20
|
+
__instance = { exports: {} };
|
|
21
|
+
__instances[__specifier] = __instance;
|
|
22
|
+
__definition(
|
|
23
|
+
__instance, __instance.exports, __load, __process,
|
|
24
|
+
undefined, undefined, undefined,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return __instance.exports;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
__definitions["./observer-capture.js"] = function (module, exports, require, process, globalThis, global, Function) {
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const { createHash } = require('node:crypto');
|
|
34
|
+
const { writeFileSync } = require('node:fs');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
|
|
37
|
+
const CAPTURE_SCHEMA = 'u23-hook-observation/v1';
|
|
38
|
+
const MAX_CAPTURE_ATTEMPTS = 64;
|
|
39
|
+
|
|
40
|
+
// The observation carries digests and the turn identity only. A hook payload
|
|
41
|
+
// can contain prompt text and paths, so nothing derived from it is written
|
|
42
|
+
// verbatim: the capture is evidence that a hook ran and which turn it ran for,
|
|
43
|
+
// not a copy of the turn.
|
|
44
|
+
function buildCaptureRecord({
|
|
45
|
+
event,
|
|
46
|
+
status,
|
|
47
|
+
observedAtEpochMs,
|
|
48
|
+
payload,
|
|
49
|
+
payloadBytes,
|
|
50
|
+
parsed,
|
|
51
|
+
}) {
|
|
52
|
+
let turnId = null;
|
|
53
|
+
if (status === 'ok' && parsed && typeof parsed === 'object'
|
|
54
|
+
&& typeof parsed.turn_id === 'string') {
|
|
55
|
+
turnId = parsed.turn_id;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
schema: CAPTURE_SCHEMA,
|
|
59
|
+
event,
|
|
60
|
+
status,
|
|
61
|
+
observedAtEpochMs,
|
|
62
|
+
payloadBytes,
|
|
63
|
+
payloadSha256: typeof payload === 'string'
|
|
64
|
+
? createHash('sha256').update(payload).digest('hex')
|
|
65
|
+
: null,
|
|
66
|
+
pid: process.pid,
|
|
67
|
+
turnId,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// One event can fire many times in a session, so the writer claims a fresh
|
|
72
|
+
// name with an exclusive create instead of overwriting an earlier capture.
|
|
73
|
+
function writeCaptureRecord(captureDir, record) {
|
|
74
|
+
const serialized = `${JSON.stringify(record, Object.keys(record).sort(), 2)}\n`;
|
|
75
|
+
for (let attempt = 0; attempt < MAX_CAPTURE_ATTEMPTS; attempt += 1) {
|
|
76
|
+
const file = path.join(
|
|
77
|
+
captureDir,
|
|
78
|
+
`${record.event}-${record.pid}-${attempt}.json`,
|
|
79
|
+
);
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(file, serialized, { flag: 'wx', mode: 0o600 });
|
|
82
|
+
return file;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error && error.code === 'EEXIST') continue;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new Error('hook observation capture names are exhausted');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { buildCaptureRecord, writeCaptureRecord, CAPTURE_SCHEMA };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
__definitions["./observer-entry.js"] = function (module, exports, require, process, globalThis, global, Function) {
|
|
95
|
+
'use strict';
|
|
96
|
+
|
|
97
|
+
// Stamped at process entry, before stdin is touched. Stamping after the drain
|
|
98
|
+
// measures the writer rather than the hook, which silently inflates every
|
|
99
|
+
// margin derived from the capture.
|
|
100
|
+
const OBSERVED_AT_EPOCH_MS = Date.now();
|
|
101
|
+
|
|
102
|
+
const { buildCaptureRecord, writeCaptureRecord } = require('./observer-capture.js');
|
|
103
|
+
|
|
104
|
+
const MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
105
|
+
const STDIN_TIMEOUT_MS = 5_000;
|
|
106
|
+
|
|
107
|
+
// An oversized payload is rejected whole. Parsing the prefix that arrived
|
|
108
|
+
// before the limit would record a truncated turn as a complete observation.
|
|
109
|
+
function readPayload(stdin, limit, timeoutMs) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let bytes = 0;
|
|
113
|
+
let settled = false;
|
|
114
|
+
const finish = (status, payload) => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
stdin.removeAllListeners();
|
|
119
|
+
stdin.pause();
|
|
120
|
+
resolve({ status, payload, bytes });
|
|
121
|
+
};
|
|
122
|
+
const timer = setTimeout(() => finish('timeout', null), timeoutMs);
|
|
123
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
124
|
+
stdin.on('data', (chunk) => {
|
|
125
|
+
bytes += chunk.length;
|
|
126
|
+
if (bytes > limit) {
|
|
127
|
+
finish('overflow', null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
chunks.push(chunk);
|
|
131
|
+
});
|
|
132
|
+
stdin.once('error', () => finish('unreadable', null));
|
|
133
|
+
stdin.once('end', () => finish('ok', Buffer.concat(chunks).toString('utf8')));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main(argv, stdin) {
|
|
138
|
+
const [event, captureDir] = argv;
|
|
139
|
+
if (typeof event !== 'string' || event.length === 0
|
|
140
|
+
|| typeof captureDir !== 'string' || captureDir.length === 0) {
|
|
141
|
+
process.setExitCode(2);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const read = await readPayload(stdin, MAX_PAYLOAD_BYTES, STDIN_TIMEOUT_MS);
|
|
145
|
+
let status = read.status;
|
|
146
|
+
let parsed = null;
|
|
147
|
+
if (status === 'ok') {
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(read.payload);
|
|
150
|
+
} catch {
|
|
151
|
+
status = 'unparsable';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
writeCaptureRecord(captureDir, buildCaptureRecord({
|
|
155
|
+
event,
|
|
156
|
+
status,
|
|
157
|
+
observedAtEpochMs: OBSERVED_AT_EPOCH_MS,
|
|
158
|
+
payload: read.payload,
|
|
159
|
+
payloadBytes: read.bytes,
|
|
160
|
+
parsed,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main(process.argv.slice(2), process.stdin).catch(() => {
|
|
165
|
+
// A hook that cannot record its observation must not take the turn down
|
|
166
|
+
// with it; the missing capture is the signal the gate reads.
|
|
167
|
+
process.setExitCode(1);
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
__load("./observer-entry.js");
|
|
172
|
+
}(
|
|
173
|
+
Object.freeze({
|
|
174
|
+
"node:crypto": Object.freeze({ createHash: require("node:crypto").createHash }),
|
|
175
|
+
"node:fs": Object.freeze({ writeFileSync: require("node:fs").writeFileSync }),
|
|
176
|
+
"node:path": Object.freeze({ join: require("node:path").join })
|
|
177
|
+
})
|
|
178
|
+
,
|
|
179
|
+
Object.freeze({
|
|
180
|
+
argv: Object.freeze(process.argv.slice()),
|
|
181
|
+
pid: process.pid,
|
|
182
|
+
setExitCode(code) { process.exitCode = code; },
|
|
183
|
+
stdin: process.stdin,
|
|
184
|
+
})
|
|
185
|
+
));
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash } = require('node:crypto');
|
|
4
|
+
const { writeFileSync } = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const CAPTURE_SCHEMA = 'u23-hook-observation/v1';
|
|
8
|
+
const MAX_CAPTURE_ATTEMPTS = 64;
|
|
9
|
+
|
|
10
|
+
// The observation carries digests and the turn identity only. A hook payload
|
|
11
|
+
// can contain prompt text and paths, so nothing derived from it is written
|
|
12
|
+
// verbatim: the capture is evidence that a hook ran and which turn it ran for,
|
|
13
|
+
// not a copy of the turn.
|
|
14
|
+
function buildCaptureRecord({
|
|
15
|
+
event,
|
|
16
|
+
status,
|
|
17
|
+
observedAtEpochMs,
|
|
18
|
+
payload,
|
|
19
|
+
payloadBytes,
|
|
20
|
+
parsed,
|
|
21
|
+
}) {
|
|
22
|
+
let turnId = null;
|
|
23
|
+
if (status === 'ok' && parsed && typeof parsed === 'object'
|
|
24
|
+
&& typeof parsed.turn_id === 'string') {
|
|
25
|
+
turnId = parsed.turn_id;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
schema: CAPTURE_SCHEMA,
|
|
29
|
+
event,
|
|
30
|
+
status,
|
|
31
|
+
observedAtEpochMs,
|
|
32
|
+
payloadBytes,
|
|
33
|
+
payloadSha256: typeof payload === 'string'
|
|
34
|
+
? createHash('sha256').update(payload).digest('hex')
|
|
35
|
+
: null,
|
|
36
|
+
pid: process.pid,
|
|
37
|
+
turnId,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// One event can fire many times in a session, so the writer claims a fresh
|
|
42
|
+
// name with an exclusive create instead of overwriting an earlier capture.
|
|
43
|
+
function writeCaptureRecord(captureDir, record) {
|
|
44
|
+
const serialized = `${JSON.stringify(record, Object.keys(record).sort(), 2)}\n`;
|
|
45
|
+
for (let attempt = 0; attempt < MAX_CAPTURE_ATTEMPTS; attempt += 1) {
|
|
46
|
+
const file = path.join(
|
|
47
|
+
captureDir,
|
|
48
|
+
`${record.event}-${record.pid}-${attempt}.json`,
|
|
49
|
+
);
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(file, serialized, { flag: 'wx', mode: 0o600 });
|
|
52
|
+
return file;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error && error.code === 'EEXIST') continue;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error('hook observation capture names are exhausted');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { buildCaptureRecord, writeCaptureRecord, CAPTURE_SCHEMA };
|