polygram 0.38.7 → 0.39.0

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.
@@ -0,0 +1,296 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+ const { readFileSync } = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const {
8
+ auditProgram,
9
+ parseSource,
10
+ parsesAsModule,
11
+ } = require('./hook-source-policy');
12
+
13
+ const HOOK_BUNDLE_SOURCE_DIR = path.join(__dirname, 'hooks');
14
+ const HOOK_BUNDLE_SOURCES = Object.freeze([
15
+ 'observer-capture.js',
16
+ 'observer-entry.js',
17
+ ]);
18
+ const HOOK_BUNDLE_ENTRY = 'observer-entry.js';
19
+ const HOOK_BUNDLE_PATH = path.join(
20
+ HOOK_BUNDLE_SOURCE_DIR,
21
+ 'hook-observer.bundle.js',
22
+ );
23
+
24
+ // Bundled code reaches the runtime through these facades and through nothing
25
+ // else. The allowlist is closed at the member level, not the module level: a
26
+ // facade that exposed all of `node:fs` would hand the closure an arbitrary
27
+ // external read, which is precisely what it must not have.
28
+ const ALLOWED_MODULE_FACADES = Object.freeze({
29
+ 'node:crypto': Object.freeze(['createHash']),
30
+ 'node:fs': Object.freeze(['writeFileSync']),
31
+ 'node:path': Object.freeze(['join']),
32
+ });
33
+
34
+ // Names bound in every module's own scope. `require` is the facade loader;
35
+ // the rest are bound to `undefined` so a bundled module cannot reach the real
36
+ // loader, the real process, or the Function constructor by name.
37
+ const INJECTED_BINDINGS = Object.freeze([
38
+ 'module',
39
+ 'exports',
40
+ 'require',
41
+ 'process',
42
+ 'globalThis',
43
+ 'global',
44
+ 'Function',
45
+ ]);
46
+
47
+ // The curated process surface. `getBuiltinModule`, `binding`, `dlopen`, `env`
48
+ // and `mainModule` are absent by construction rather than by inspection.
49
+ const PROCESS_FACADE_KEYS = Object.freeze(['argv', 'pid', 'setExitCode', 'stdin']);
50
+
51
+ const SOURCE_NAME_RE = /^[a-z0-9][a-z0-9.-]*\.js$/;
52
+ const MAX_SOURCE_BYTES = 256 * 1024;
53
+
54
+ class CodexHookBundleError extends Error {
55
+ constructor(message, code, action) {
56
+ super(message);
57
+ this.name = 'CodexHookBundleError';
58
+ this.code = code;
59
+ this.action = action;
60
+ }
61
+ }
62
+
63
+ function invalid(message) {
64
+ return new CodexHookBundleError(
65
+ message,
66
+ 'CODEX_HOOK_BUNDLE_INVALID',
67
+ 'Declare the checked-in hook sources exactly, and rebuild.',
68
+ );
69
+ }
70
+
71
+ function unsafe(message) {
72
+ return new CodexHookBundleError(
73
+ message,
74
+ 'CODEX_HOOK_BUNDLE_UNSAFE_SOURCE',
75
+ 'Keep every executed input inside the bundle; a hook source may use only '
76
+ + 'the declared syntax, identifiers, and facade members.',
77
+ );
78
+ }
79
+
80
+ function specifierKey(specifier) {
81
+ return specifier.startsWith('./') ? specifier : `./${specifier}`;
82
+ }
83
+
84
+ function defaultAllowedSpecifiers(sources) {
85
+ return [
86
+ ...Object.keys(ALLOWED_MODULE_FACADES),
87
+ ...sources.map(specifierKey),
88
+ ];
89
+ }
90
+
91
+ function facadeMemberIndex() {
92
+ return new Map(
93
+ Object.entries(ALLOWED_MODULE_FACADES)
94
+ .map(([specifier, members]) => [specifier, new Set(members)]),
95
+ );
96
+ }
97
+
98
+ // The mechanical proof that a checked-in source stays inside the closure. The
99
+ // emitted facades narrow what that code can reach at run time, but they are
100
+ // defence in depth: this gate is what decides whether a source may ship.
101
+ function assertSourcePolicy(text, {
102
+ label = 'source',
103
+ allowedSpecifiers = defaultAllowedSpecifiers(HOOK_BUNDLE_SOURCES),
104
+ } = {}) {
105
+ let program;
106
+ try {
107
+ program = parseSource(text);
108
+ } catch (error) {
109
+ // Module syntax parses only as a module, and is a policy refusal rather
110
+ // than a malformed file: the bundle format has no import machinery.
111
+ if (parsesAsModule(text)) {
112
+ throw unsafe(`Codex hook source ${label} contains ECMAScript module syntax.`);
113
+ }
114
+ throw invalid(`Codex hook source ${label} does not parse: ${error.message}`);
115
+ }
116
+ auditProgram(program, {
117
+ reject(reason) {
118
+ throw unsafe(`Codex hook source ${label} contains ${reason}.`);
119
+ },
120
+ allowedSpecifiers: new Set(allowedSpecifiers),
121
+ facadeMembers: facadeMemberIndex(),
122
+ processMembers: new Set(PROCESS_FACADE_KEYS),
123
+ });
124
+ return text;
125
+ }
126
+
127
+ // The generated wrapper is not source the policy can audit — it exists to
128
+ // deny — so the emitted file is checked for the one property that matters
129
+ // here: no specifier outside the closure reaches the real loader.
130
+ function assertEmittedBundle(text, { allowedSpecifiers }) {
131
+ const allowed = new Set(allowedSpecifiers);
132
+ for (const match of text.matchAll(/\brequire\s*\(([^)]*)\)/g)) {
133
+ const literal = /^\s*(['"])([^'"]+)\1\s*$/.exec(match[1]);
134
+ if (!literal) {
135
+ throw unsafe('The generated bundle requires a computed module specifier.');
136
+ }
137
+ if (!allowed.has(literal[2]) && !allowed.has(specifierKey(literal[2]))) {
138
+ throw unsafe(
139
+ `The generated bundle loads ${literal[2]} from outside the closure.`,
140
+ );
141
+ }
142
+ }
143
+ return text;
144
+ }
145
+
146
+ function renderFacades() {
147
+ const modules = Object.entries(ALLOWED_MODULE_FACADES)
148
+ .map(([specifier, members]) => {
149
+ const fields = members
150
+ .map((member) => `${member}: require(${JSON.stringify(specifier)}).${member}`)
151
+ .join(', ');
152
+ return ` ${JSON.stringify(specifier)}: Object.freeze({ ${fields} })`;
153
+ })
154
+ .join(',\n');
155
+ return ` Object.freeze({\n${modules}\n })`;
156
+ }
157
+
158
+ // The raw loader is used once, in this argument list, and is never bound to a
159
+ // name. Bundled modules run inside the function below, where `require` and
160
+ // `process` are the frozen facades and the remaining escape hatches are bound
161
+ // to undefined.
162
+ function renderBundle(modules, entry) {
163
+ const bodies = modules.map(({ name, text }) => [
164
+ ` __definitions[${JSON.stringify(specifierKey(name))}] = `
165
+ + `function (${INJECTED_BINDINGS.join(', ')}) {`,
166
+ text.endsWith('\n') ? text.slice(0, -1) : text,
167
+ ' };',
168
+ '',
169
+ ].join('\n')).join('\n');
170
+
171
+ return [
172
+ "'use strict';",
173
+ '',
174
+ '// Generated from the checked-in Codex hook sources by the bundle build',
175
+ '// step. Bundled modules see only the frozen facades passed in below, so',
176
+ '// the protected closure is the runtime, this file, and nothing else.',
177
+ '// Edit the sources and rebuild; never edit this file by hand.',
178
+ '',
179
+ '(function (__facades, __process) {',
180
+ ' const __definitions = Object.create(null);',
181
+ ' const __instances = Object.create(null);',
182
+ '',
183
+ ' function __load(__specifier) {',
184
+ ' if (Object.hasOwn(__facades, __specifier)) return __facades[__specifier];',
185
+ ' const __definition = __definitions[__specifier];',
186
+ ' if (__definition === undefined) {',
187
+ " throw new Error('bundled module ' + __specifier + ' is outside the closure');",
188
+ ' }',
189
+ ' let __instance = __instances[__specifier];',
190
+ ' if (__instance === undefined) {',
191
+ ' __instance = { exports: {} };',
192
+ ' __instances[__specifier] = __instance;',
193
+ ' __definition(',
194
+ ' __instance, __instance.exports, __load, __process,',
195
+ ' undefined, undefined, undefined,',
196
+ ' );',
197
+ ' }',
198
+ ' return __instance.exports;',
199
+ ' }',
200
+ '',
201
+ bodies,
202
+ ` __load(${JSON.stringify(specifierKey(entry))});`,
203
+ '}(',
204
+ renderFacades(),
205
+ ',',
206
+ ' Object.freeze({',
207
+ ' argv: Object.freeze(process.argv.slice()),',
208
+ ' pid: process.pid,',
209
+ ' setExitCode(code) { process.exitCode = code; },',
210
+ ' stdin: process.stdin,',
211
+ ' })',
212
+ '));',
213
+ '',
214
+ ].join('\n');
215
+ }
216
+
217
+ function readSource(sourceDir, name) {
218
+ if (typeof name !== 'string' || !SOURCE_NAME_RE.test(name)) {
219
+ throw invalid(
220
+ `Codex hook source ${JSON.stringify(name)} is not a checked-in `
221
+ + 'JavaScript file name.',
222
+ );
223
+ }
224
+ let text;
225
+ try {
226
+ text = readFileSync(path.join(sourceDir, name), 'utf8');
227
+ } catch (error) {
228
+ throw invalid(`Codex hook source ${name} could not be read.`);
229
+ }
230
+ if (Buffer.byteLength(text) > MAX_SOURCE_BYTES) {
231
+ throw invalid(`Codex hook source ${name} is larger than the bundle limit.`);
232
+ }
233
+ return text;
234
+ }
235
+
236
+ function buildHookBundle({
237
+ sourceDir = HOOK_BUNDLE_SOURCE_DIR,
238
+ sources = HOOK_BUNDLE_SOURCES,
239
+ entry = HOOK_BUNDLE_ENTRY,
240
+ } = {}) {
241
+ if (typeof sourceDir !== 'string' || !path.isAbsolute(sourceDir)) {
242
+ throw invalid('Codex hook source directory must be an absolute path.');
243
+ }
244
+ if (!Array.isArray(sources) || sources.length === 0
245
+ || new Set(sources).size !== sources.length) {
246
+ throw invalid('Codex hook sources must be a unique non-empty list.');
247
+ }
248
+ if (!sources.includes(entry)) {
249
+ throw invalid(`Codex hook entry ${entry} is not a declared source.`);
250
+ }
251
+ const allowedSpecifiers = defaultAllowedSpecifiers(sources);
252
+ const modules = sources.map((name) => {
253
+ const text = readSource(sourceDir, name);
254
+ assertSourcePolicy(text, { label: name, allowedSpecifiers });
255
+ return { name, text };
256
+ });
257
+ return assertEmittedBundle(renderBundle(modules, entry), { allowedSpecifiers });
258
+ }
259
+
260
+ // The single trusted artifact body. A checked-in bundle that its sources no
261
+ // longer produce is refused here, so nothing downstream can install or attest
262
+ // a stale one.
263
+ function readCheckedInBundle() {
264
+ const expected = buildHookBundle();
265
+ let contents;
266
+ try {
267
+ contents = readFileSync(HOOK_BUNDLE_PATH, 'utf8');
268
+ } catch (error) {
269
+ throw invalid('The checked-in Codex hook bundle is missing.');
270
+ }
271
+ if (contents !== expected) {
272
+ throw invalid(
273
+ 'The checked-in Codex hook bundle is not what its sources build.',
274
+ );
275
+ }
276
+ return Object.freeze({
277
+ contents,
278
+ path: HOOK_BUNDLE_PATH,
279
+ sha256: createHash('sha256').update(contents).digest('hex'),
280
+ });
281
+ }
282
+
283
+ module.exports = {
284
+ ALLOWED_MODULE_FACADES,
285
+ CodexHookBundleError,
286
+ HOOK_BUNDLE_ENTRY,
287
+ HOOK_BUNDLE_PATH,
288
+ HOOK_BUNDLE_SOURCES,
289
+ HOOK_BUNDLE_SOURCE_DIR,
290
+ INJECTED_BINDINGS,
291
+ PROCESS_FACADE_KEYS,
292
+ assertEmittedBundle,
293
+ assertSourcePolicy,
294
+ buildHookBundle,
295
+ readCheckedInBundle,
296
+ };
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+ const path = require('node:path');
5
+
6
+ const { isAttestationReceipt } = require('./hook-artifacts');
7
+
8
+ const MAX_ARTIFACTS = 8;
9
+ const MAX_ARGV = 16;
10
+ const MAX_TOKEN_LENGTH = 4096;
11
+
12
+ // Path tokens: no whitespace, no quoting, no shell metacharacter, no
13
+ // expansion. What survives this set cannot change meaning inside the command
14
+ // string Codex stores and executes.
15
+ const PATH_TOKEN_RE = /^[A-Za-z0-9._/@:+=-]+$/;
16
+
17
+ // A literal argument carries no separator at all, so nothing in the argument
18
+ // list is ambiguous between a word and a path.
19
+ const ARGV_LITERAL_RE = /^[A-Za-z0-9._@:+=-]+$/;
20
+ const CODEX_NAME_RE = /^codex([-.].*)?$/i;
21
+ const MOVING_ALIASES = new Set(['current', 'latest', 'stable']);
22
+
23
+ class CodexHookCommandError extends Error {
24
+ constructor(message, code, action) {
25
+ super(message);
26
+ this.name = 'CodexHookCommandError';
27
+ this.code = code;
28
+ this.action = action;
29
+ }
30
+ }
31
+
32
+ function invalid(message) {
33
+ return new CodexHookCommandError(
34
+ message,
35
+ 'CODEX_HOOK_COMMAND_INVALID',
36
+ 'Declare hook commands as typed descriptors over attested versioned '
37
+ + 'paths and literal arguments.',
38
+ );
39
+ }
40
+
41
+ function unattested(message) {
42
+ return new CodexHookCommandError(
43
+ message,
44
+ 'CODEX_HOOK_COMMAND_UNATTESTED',
45
+ 'Attest the protected artifact version before rendering a command from it.',
46
+ );
47
+ }
48
+
49
+ function assertCommandPath(value, label) {
50
+ if (typeof value !== 'string' || value.length === 0
51
+ || value.length > MAX_TOKEN_LENGTH) {
52
+ throw invalid(`The hook ${label} path must be a bounded string.`);
53
+ }
54
+ if (!PATH_TOKEN_RE.test(value)) {
55
+ throw invalid(`The hook ${label} path carries an unsafe character.`);
56
+ }
57
+ if (!path.isAbsolute(value) || path.normalize(value) !== value
58
+ || value.includes('//') || value.endsWith('/')) {
59
+ throw invalid(
60
+ `The hook ${label} path must be absolute and canonical; a PATH name or `
61
+ + 'relative path is never resolved.',
62
+ );
63
+ }
64
+ const segments = value.split('/').filter(Boolean);
65
+ if (segments.some((segment) => MOVING_ALIASES.has(segment))) {
66
+ throw invalid(
67
+ `The hook ${label} path names a moving alias instead of a version.`,
68
+ );
69
+ }
70
+ return value;
71
+ }
72
+
73
+ function assertNotCodex(value, label) {
74
+ if (CODEX_NAME_RE.test(path.basename(value))) {
75
+ throw invalid(
76
+ `The hook ${label} names Codex; a hook never nests a Codex invocation.`,
77
+ );
78
+ }
79
+ return value;
80
+ }
81
+
82
+ // Arguments are typed, so what a token means is declared rather than inferred
83
+ // from its spelling: a literal is a word with no separator in it, and a path
84
+ // argument is an executed input that the attestation must already cover.
85
+ function assertArgument(entry, attestedPaths) {
86
+ if (!entry || typeof entry !== 'object') {
87
+ throw invalid(
88
+ 'A hook argument must be a typed literal or attested-path entry.',
89
+ );
90
+ }
91
+ if (entry.kind === 'literal') {
92
+ const { value } = entry;
93
+ if (typeof value !== 'string' || value.length === 0
94
+ || value.length > MAX_TOKEN_LENGTH || !ARGV_LITERAL_RE.test(value)) {
95
+ throw invalid(
96
+ 'A hook literal argument must be a bounded word without separators, '
97
+ + 'whitespace, quoting, expansion, or shell metacharacters.',
98
+ );
99
+ }
100
+ return assertNotCodex(value, 'argument');
101
+ }
102
+ if (entry.kind === 'attested-path') {
103
+ const target = assertNotCodex(
104
+ assertCommandPath(entry.path, 'argument'),
105
+ 'argument',
106
+ );
107
+ if (!attestedPaths.has(target)) {
108
+ throw unattested('A hook argument names an unattested path.');
109
+ }
110
+ return target;
111
+ }
112
+ throw invalid(
113
+ 'A hook argument must declare kind "literal" or "attested-path".',
114
+ );
115
+ }
116
+
117
+ // The rendering is a pure function of the descriptor, and `commandSha256` is
118
+ // the digest of that rendering — never of hand-written command text.
119
+ function renderHookCommand(descriptor, attestation) {
120
+ if (!descriptor || typeof descriptor !== 'object') {
121
+ throw invalid('A hook command descriptor is required.');
122
+ }
123
+ // Only the object a full attestation returned may authorise a rendering; a
124
+ // receipt-shaped literal proves nothing about what is on disk.
125
+ if (!isAttestationReceipt(attestation)) {
126
+ throw unattested(
127
+ 'A hook command needs a live receipt from a full artifact attestation.',
128
+ );
129
+ }
130
+ const { runtime, artifacts, argv } = descriptor;
131
+ if (!runtime || typeof runtime !== 'object'
132
+ || runtime.kind !== 'protected-runtime') {
133
+ throw invalid('The descriptor runtime must be a protected runtime.');
134
+ }
135
+ if (!Array.isArray(artifacts) || artifacts.length === 0
136
+ || artifacts.length > MAX_ARTIFACTS) {
137
+ throw invalid('The descriptor must name one to eight protected artifacts.');
138
+ }
139
+ if (!Array.isArray(argv) || argv.length > MAX_ARGV) {
140
+ throw invalid('The descriptor arguments must be a bounded literal list.');
141
+ }
142
+
143
+ const runtimePath = assertNotCodex(
144
+ assertCommandPath(runtime.path, 'runtime'),
145
+ 'runtime',
146
+ );
147
+ if (runtimePath !== attestation.runtime.path
148
+ || attestation.runtime.kind !== 'protected-runtime') {
149
+ throw unattested('The descriptor runtime is not the attested runtime.');
150
+ }
151
+
152
+ const attested = new Map(
153
+ attestation.artifacts.map((entry) => [entry.path, entry]),
154
+ );
155
+ const seen = new Set();
156
+ const artifactPaths = artifacts.map((entry) => {
157
+ if (!entry || typeof entry !== 'object'
158
+ || entry.kind !== 'protected-artifact') {
159
+ throw invalid('Each descriptor artifact must be a protected artifact.');
160
+ }
161
+ const artifactPath = assertCommandPath(entry.path, 'artifact');
162
+ if (seen.has(artifactPath)) {
163
+ throw invalid('A descriptor names the same artifact twice.');
164
+ }
165
+ seen.add(artifactPath);
166
+ const match = attested.get(artifactPath);
167
+ if (!match || match.kind !== 'protected-artifact') {
168
+ throw unattested('A descriptor artifact is not in the attested closure.');
169
+ }
170
+ return artifactPath;
171
+ });
172
+
173
+ const attestedPaths = new Set([
174
+ attestation.runtime.path,
175
+ ...attestation.artifacts.map((entry) => entry.path),
176
+ ]);
177
+ const tokens = [
178
+ runtimePath,
179
+ ...artifactPaths,
180
+ ...argv.map((entry) => assertArgument(entry, attestedPaths)),
181
+ ];
182
+ const command = tokens.map((token) => `'${token}'`).join(' ');
183
+ return Object.freeze({
184
+ command,
185
+ sha256: createHash('sha256').update(command).digest('hex'),
186
+ });
187
+ }
188
+
189
+ module.exports = {
190
+ CodexHookCommandError,
191
+ renderHookCommand,
192
+ };