@mnemonik/shared 7.154.2 → 7.155.2

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.
Files changed (52) hide show
  1. package/dist/ast/grammars.d.ts +4 -2
  2. package/dist/ast/grammars.d.ts.map +1 -1
  3. package/dist/ast/grammars.js +45 -3
  4. package/dist/ast/grammars.js.map +1 -1
  5. package/dist/codeScanner.d.ts +26 -329
  6. package/dist/codeScanner.d.ts.map +1 -1
  7. package/dist/codeScanner.js +65 -1141
  8. package/dist/codeScanner.js.map +1 -1
  9. package/dist/fileSelection.d.ts +451 -0
  10. package/dist/fileSelection.d.ts.map +1 -0
  11. package/dist/fileSelection.js +1256 -0
  12. package/dist/fileSelection.js.map +1 -0
  13. package/dist/hookTrace.d.ts +40 -0
  14. package/dist/hookTrace.d.ts.map +1 -1
  15. package/dist/hookTrace.js +126 -0
  16. package/dist/hookTrace.js.map +1 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/runtimeAssets.d.ts +12 -0
  22. package/dist/runtimeAssets.d.ts.map +1 -0
  23. package/dist/runtimeAssets.js +43 -0
  24. package/dist/runtimeAssets.js.map +1 -0
  25. package/dist/runtimeReader.d.ts +15 -2
  26. package/dist/runtimeReader.d.ts.map +1 -1
  27. package/dist/runtimeReader.js +110 -21
  28. package/dist/runtimeReader.js.map +1 -1
  29. package/dist/sourceRedaction.d.ts +45 -1
  30. package/dist/sourceRedaction.d.ts.map +1 -1
  31. package/dist/sourceRedaction.js +62 -22
  32. package/dist/sourceRedaction.js.map +1 -1
  33. package/dist/sourceRedactionPool.d.ts +198 -0
  34. package/dist/sourceRedactionPool.d.ts.map +1 -0
  35. package/dist/sourceRedactionPool.js +756 -0
  36. package/dist/sourceRedactionPool.js.map +1 -0
  37. package/dist/vueSource.d.ts +0 -2
  38. package/dist/vueSource.d.ts.map +1 -1
  39. package/dist/vueSource.js +8 -19
  40. package/dist/vueSource.js.map +1 -1
  41. package/package.json +6 -2
  42. package/src/ast/grammars.ts +52 -7
  43. package/src/codeScanner.ts +121 -1321
  44. package/src/fileSelection.ts +1440 -0
  45. package/src/hookTrace.ts +143 -0
  46. package/src/index.ts +31 -2
  47. package/src/runtimeAssets.ts +49 -0
  48. package/src/runtimeReader.ts +120 -28
  49. package/src/sourceRedaction.ts +85 -20
  50. package/src/sourceRedactionPool.ts +877 -0
  51. package/src/vueSource.ts +8 -23
  52. package/tests/runtimeVerify.test.ts +166 -0
package/src/vueSource.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { SFCDescriptor } from '@vue/compiler-sfc';
2
- import { MAX_AST_PARSE_BYTES } from './ast/astChunker.js';
3
2
  import { MAX_SCANNED_FILE_BYTES } from './codeScanner.js';
4
3
 
5
4
  /** A fallback scrub may repair syntax; it must never become graph evidence. */
@@ -11,18 +10,7 @@ export type VueTemplateNode = TemplateRoot | TemplateRoot['children'][number];
11
10
  type ParsedVueSource = {
12
11
  descriptor: SFCDescriptor;
13
12
  nodeTypes: typeof import('@vue/compiler-core').NodeTypes;
14
- /** Set on the sanitation path only; the template walk throws past it. */
15
- templateNodeLimit?: number;
16
13
  };
17
-
18
- /**
19
- * Sanitation runs on the API server's event loop (scan push, file push, documents), and
20
- * its edit loop costs the file's length per edit, so it keeps the 2 MiB and 200,000-node
21
- * bounds until it moves off that loop. Connected-code reads run in the parse pool and
22
- * take any file up to the product's 10 MB ceiling.
23
- */
24
- const SANITATION_MAX_BYTES = MAX_AST_PARSE_BYTES;
25
- const SANITATION_TEMPLATE_NODE_LIMIT = 200_000;
26
14
  export async function parseVueSource(
27
15
  source: string,
28
16
  forSanitation = false
@@ -30,10 +18,11 @@ export async function parseVueSource(
30
18
  // Sanitation still inspects marked bytes; graph extraction must continue to reject them.
31
19
  if (!forSanitation && source.includes(VUE_FIDELITY_MARKER))
32
20
  return { reason: 'vue_source_fidelity_unavailable' };
33
- if (
34
- Buffer.byteLength(source, 'utf8') >
35
- (forSanitation ? SANITATION_MAX_BYTES : MAX_SCANNED_FILE_BYTES)
36
- )
21
+ // One ceiling for sanitation and connected code: the product's 10 MB scan
22
+ // ceiling. Sanitation no longer stops at 2 MiB: its edits are applied in one
23
+ // pass (sourceRedaction.ts applySpanEdits), and on the API server components
24
+ // are scrubbed in a redaction child, off the event loop (sourceRedactionServer.ts).
25
+ if (Buffer.byteLength(source, 'utf8') > MAX_SCANNED_FILE_BYTES)
37
26
  return { reason: 'file_too_large' };
38
27
  let clearCache: (() => void) | undefined;
39
28
  try {
@@ -56,9 +45,7 @@ export async function parseVueSource(
56
45
  descriptor.customBlocks.length
57
46
  )
58
47
  return { reason: 'vue_preprocessor_or_custom_block_unsupported' };
59
- return forSanitation
60
- ? { descriptor, nodeTypes, templateNodeLimit: SANITATION_TEMPLATE_NODE_LIMIT }
61
- : { descriptor, nodeTypes };
48
+ return { descriptor, nodeTypes };
62
49
  } catch {
63
50
  return { reason: 'vue_parse_failed' };
64
51
  } finally {
@@ -76,13 +63,11 @@ export function walkVueTemplate(
76
63
  const pending: Array<{ node: VueTemplateNode; scoped: boolean }> = descriptor.template?.ast
77
64
  ? [{ node: descriptor.template.ast, scoped: false }]
78
65
  : [];
79
- // Iterative and linear in the template's nodes; only sanitation is bounded by count.
80
- const limit = parsed.templateNodeLimit ?? Infinity;
81
- let count = 0;
66
+ // Iterative and linear in the template's nodes, with no count limit: the
67
+ // 200,000-node bound guarded a scrubber that copied the file per edit.
82
68
  while (pending.length) {
83
69
  const item = pending.pop();
84
70
  if (!item) break;
85
- if (++count > limit) throw new Error('vue_template_work_limit');
86
71
  const { node } = item;
87
72
  const scoped =
88
73
  item.scoped ||
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Runtime verification checks every file of a runtime before anything in it is
3
+ * imported. It used to await each filesystem call in turn and inspect every
4
+ * ancestor directory again for every file: a 1,929-file hook runtime took
5
+ * 6.6 s, which `mnemonik status` paid on every run (26 Oct 2026). These tests
6
+ * pin the bounded parallelism and the once-per-directory inspection, and that
7
+ * every check still refuses what it refused before.
8
+ */
9
+ import assert from 'node:assert/strict';
10
+ import { createHash } from 'node:crypto';
11
+ import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
12
+ import { createRequire, syncBuiltinESMExports } from 'node:module';
13
+ import { tmpdir } from 'node:os';
14
+ import { dirname, join } from 'node:path';
15
+ import { after, beforeEach, test } from 'node:test';
16
+ import { RuntimeError, RuntimeReader, VERIFY_CONCURRENCY } from '../src/runtimeReader.js';
17
+
18
+ const posix = process.platform !== 'win32';
19
+ const fsPromises = createRequire(import.meta.url)(
20
+ 'node:fs/promises'
21
+ ) as typeof import('node:fs/promises');
22
+ const realOpen = fsPromises.open;
23
+ const realLstat = fsPromises.lstat;
24
+ const calls = { open: 0, lstat: 0, inFlight: 0, maxInFlight: 0 };
25
+ const roots: string[] = [];
26
+
27
+ function observe(delayMs: number): void {
28
+ fsPromises.open = (async (...args: Parameters<typeof realOpen>) => {
29
+ calls.open += 1;
30
+ calls.inFlight += 1;
31
+ calls.maxInFlight = Math.max(calls.maxInFlight, calls.inFlight);
32
+ try {
33
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
34
+ return await realOpen(...args);
35
+ } finally {
36
+ calls.inFlight -= 1;
37
+ }
38
+ }) as typeof realOpen;
39
+ fsPromises.lstat = (async (...args: Parameters<typeof realLstat>) => {
40
+ calls.lstat += 1;
41
+ return realLstat(...args);
42
+ }) as typeof realLstat;
43
+ syncBuiltinESMExports();
44
+ }
45
+
46
+ function restore(): void {
47
+ fsPromises.open = realOpen;
48
+ fsPromises.lstat = realLstat;
49
+ syncBuiltinESMExports();
50
+ }
51
+
52
+ beforeEach(() => {
53
+ Object.assign(calls, { open: 0, lstat: 0, inFlight: 0, maxInFlight: 0 });
54
+ });
55
+ after(async () => {
56
+ restore();
57
+ await Promise.all(roots.map((root) => rm(root, { recursive: true, force: true })));
58
+ });
59
+
60
+ const sha256 = (bytes: Buffer) => createHash('sha256').update(bytes).digest('hex');
61
+
62
+ /** A codex runtime of `count` files spread over nested folders, laid out as installs leave it. */
63
+ async function runtime(count: number) {
64
+ const state = await mkdtemp(join(tmpdir(), 'runtime-verify-'));
65
+ roots.push(state);
66
+ await chmod(state, 0o700);
67
+ const directory = join(state, 'runtimes', 'codex', '1.0.0');
68
+ const files: Record<string, { sha256: string; size: number; executable: boolean }> = {};
69
+ let totalSize = 0;
70
+ for (let index = 0; index < count; index++) {
71
+ const name = `node_modules/pkg-${index % 4}/lib/deep/file-${index}.js`;
72
+ const bytes = Buffer.from(`export const value = ${index};\n`);
73
+ await mkdir(dirname(join(directory, name)), { recursive: true, mode: 0o700 });
74
+ await writeFile(join(directory, name), bytes, { mode: 0o600 });
75
+ files[name] = { sha256: sha256(bytes), size: bytes.length, executable: false };
76
+ totalSize += bytes.length;
77
+ }
78
+ const manifest = Buffer.from(
79
+ JSON.stringify({
80
+ schemaVersion: 1,
81
+ artifact: 'codex',
82
+ version: '1.0.0',
83
+ entry: 'node_modules/pkg-0/lib/deep/file-0.js',
84
+ files,
85
+ totalSize,
86
+ source: { kind: 'npm', packages: [], launchedFrom: '/fixture' },
87
+ })
88
+ );
89
+ await writeFile(join(directory, 'manifest.json'), manifest, { mode: 0o600 });
90
+ await writeFile(
91
+ join(state, 'runtimes', 'codex', 'current'),
92
+ JSON.stringify({ current: { version: '1.0.0', manifestSha256: sha256(manifest) } }),
93
+ { mode: 0o600 }
94
+ );
95
+ return { state, directory, reader: new RuntimeReader(state) };
96
+ }
97
+
98
+ async function refusal(work: Promise<unknown>): Promise<string> {
99
+ try {
100
+ await work;
101
+ } catch (error) {
102
+ return error instanceof RuntimeError ? error.reason : String(error);
103
+ }
104
+ return 'accepted';
105
+ }
106
+
107
+ test(
108
+ 'verifies a runtime with up to VERIFY_CONCURRENCY files in flight, never more',
109
+ { skip: !posix },
110
+ async () => {
111
+ const { reader } = await runtime(64);
112
+ observe(5);
113
+ try {
114
+ const verified = await reader.verifyRuntime('codex');
115
+ assert.equal(verified.reference.version, '1.0.0');
116
+ } finally {
117
+ restore();
118
+ }
119
+ assert.equal(calls.maxInFlight, VERIFY_CONCURRENCY);
120
+ // Every file is still opened and read.
121
+ assert.ok(calls.open >= 64, `opened ${calls.open}`);
122
+ }
123
+ );
124
+
125
+ test('inspects each directory once, not once per file beneath it', { skip: !posix }, async () => {
126
+ const { reader } = await runtime(64);
127
+ observe(0);
128
+ try {
129
+ await reader.verifyRuntime('codex');
130
+ } finally {
131
+ restore();
132
+ }
133
+ // Inspecting every ancestor for every file cost about ten lstat calls per
134
+ // file here; now the directories are inspected once and files by fstat.
135
+ assert.ok(calls.lstat < 64, `lstat calls: ${calls.lstat}`);
136
+ });
137
+
138
+ test(
139
+ 'still refuses a changed byte, a wrong mode, a link and an extra file',
140
+ { skip: !posix },
141
+ async () => {
142
+ const changed = await runtime(20);
143
+ await writeFile(
144
+ join(changed.directory, 'node_modules/pkg-1/lib/deep/file-5.js'),
145
+ 'export const value = 6;\n',
146
+ { mode: 0o600 }
147
+ );
148
+ assert.equal(await refusal(changed.reader.verifyRuntime('codex')), 'digest_mismatch');
149
+
150
+ const mode = await runtime(20);
151
+ await chmod(join(mode.directory, 'node_modules/pkg-2/lib/deep/file-6.js'), 0o644);
152
+ assert.equal(await refusal(mode.reader.verifyRuntime('codex')), 'permission');
153
+
154
+ const link = await runtime(20);
155
+ const target = join(link.directory, 'node_modules/pkg-3/lib/deep/file-7.js');
156
+ await rm(target);
157
+ await symlink(join(link.directory, 'node_modules/pkg-0/lib/deep/file-0.js'), target);
158
+ assert.equal(await refusal(link.reader.verifyRuntime('codex')), 'permission');
159
+
160
+ const extra = await runtime(20);
161
+ await writeFile(join(extra.directory, 'node_modules/pkg-0/lib/deep/planted.js'), 'x', {
162
+ mode: 0o600,
163
+ });
164
+ assert.equal(await refusal(extra.reader.verifyRuntime('codex')), 'digest_mismatch');
165
+ }
166
+ );