@entrorove/skill 0.1.69 → 0.1.71
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/CHANGELOG.md +10 -0
- package/IDE-EXECUTION.md +5 -3
- package/SKILL.md +3 -3
- package/cloud-locale.d.mts +7 -0
- package/cloud-locale.mjs +59 -0
- package/cloud-task-client.mjs +21 -13
- package/cloud-watch-client.mjs +11 -3
- package/design-lint.mjs +20 -2
- package/ide-presentation.mjs +15 -4
- package/local-archguard-components.mjs +31 -4
- package/local-budget-reads.mjs +7 -2
- package/local-budget.mjs +3 -2
- package/local-delegated-modify-analysis.mjs +16 -7
- package/local-scope-build-data.mjs +73 -0
- package/local-scope-build-facts.mjs +33 -0
- package/local-scope-build-io.mjs +57 -0
- package/local-scope-build-read.mjs +22 -0
- package/local-scope-build-readset.mjs +49 -0
- package/local-scope-files.mjs +33 -8
- package/local-scope-read-set.mjs +66 -0
- package/local-scope-repository.mjs +96 -28
- package/local-scope-signature.mjs +50 -5
- package/local-scope-state.mjs +7 -5
- package/local-scope-uni-build.mjs +85 -0
- package/local-scope-uni-conditional.mjs +56 -0
- package/local-scope-uni-directives.mjs +96 -0
- package/local-scope-uni-expressions.mjs +106 -0
- package/local-scope-workspaces.mjs +17 -5
- package/manifest.json +1 -1
- package/package.json +16 -2
- package/release-source-groups.mjs +39 -0
- package/release-state.json +36 -22
- package/release.mjs +6 -41
- package/releases.json +12 -0
- package/scope-context-client.mjs +11 -27
- package/scope-context-journal.mjs +44 -0
- package/scope-context-observer.mjs +37 -11
- package/scope-context-release.mjs +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { protectedBuildRelativePath } from './local-scope-build-read.mjs';
|
|
4
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
5
|
+
|
|
6
|
+
export const buildConfigurationNames = ['vite.config.js', 'vite.config.mjs', 'vite.config.cjs', 'vite.config.ts',
|
|
7
|
+
'vite.config.mts', 'vite.config.cts', 'vue.config.js', 'vue.config.cjs', 'webpack.config.js', 'webpack.config.cjs',
|
|
8
|
+
'webpack.config.mjs', 'webpack.config.ts', 'rollup.config.js', 'rollup.config.mjs', 'rollup.config.ts',
|
|
9
|
+
'babel.config.js', 'babel.config.cjs', 'babel.config.json', '.babelrc', '.babelrc.json', '.babelrc.js',
|
|
10
|
+
'next.config.js', 'next.config.mjs', 'next.config.ts', 'nuxt.config.js', 'nuxt.config.ts', 'svelte.config.js'];
|
|
11
|
+
export function buildDataPath(base, value) {
|
|
12
|
+
if (typeof value !== 'string' || value.includes('\\') || value.includes('\0') || path.posix.isAbsolute(value)) {
|
|
13
|
+
throw Object.assign(new Error('Build data references require literal repository-relative paths'), { code: 'ENTROROVE_PATH_UNSAFE' });
|
|
14
|
+
}
|
|
15
|
+
const relative = path.posix.normalize(path.posix.join(base, value)).replace(/\/$/, '');
|
|
16
|
+
if (relative === '..' || relative.startsWith('../')) throw Object.assign(new Error('Build data references cannot escape the original repository'), { code: 'ENTROROVE_PATH_UNSAFE' });
|
|
17
|
+
if (protectedBuildRelativePath(relative)) throw uniConditionalError(relative, 0, 'Build data cannot read environment or private control values');
|
|
18
|
+
return relative;
|
|
19
|
+
}
|
|
20
|
+
export function buildScriptReferences(manifest, filename = 'package.json') {
|
|
21
|
+
const references = new Set(); let opaque = false;
|
|
22
|
+
for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
23
|
+
if (typeof command !== 'string') throw uniConditionalError(filename, 0, 'Build package scripts require string commands');
|
|
24
|
+
const relevant = /^(?:dev|build)(?::|$)/.test(name) || /(?:^|\s)(?:--(?:experimental-loader|loader|require|import)(?:=|\s)|-r\s)/.test(command);
|
|
25
|
+
if (!relevant) continue;
|
|
26
|
+
opaque = true;
|
|
27
|
+
// Literal references are data only, with no shell, cwd or execution claim.
|
|
28
|
+
if (/[$`\n\r|<>;]/.test(command) || /(?:^|&&)\s*cd\s/.test(command)) continue;
|
|
29
|
+
for (const segment of command.split('&&')) {
|
|
30
|
+
const words = segment.match(/"[^"]*"|'[^']*'|[^\s]+/g)?.map(word => /^['"]/.test(word) ? word.slice(1, -1) : word) ?? [];
|
|
31
|
+
let cursor = words[0] === 'cross-env' ? 1 : 0;
|
|
32
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[cursor] ?? '')) cursor++;
|
|
33
|
+
if (words[cursor] === 'node' && words[cursor + 1] && !words[cursor + 1].startsWith('-')) references.add(words[cursor + 1]);
|
|
34
|
+
for (let index = cursor + 1; index < words.length; index++) {
|
|
35
|
+
if (['--config', '-c', '--experimental-loader', '--loader', '--require', '--import', '-r'].includes(words[index]) && words[index + 1]) references.add(words[++index]);
|
|
36
|
+
else if (/^--(?:config|experimental-loader|loader|require|import)=/.test(words[index])) references.add(words[index].slice(words[index].indexOf('=') + 1));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { opaque, references: [...references].sort() };
|
|
41
|
+
}
|
|
42
|
+
export function buildModuleData(source, filename) {
|
|
43
|
+
let ast;
|
|
44
|
+
try { ast = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); }
|
|
45
|
+
catch (error) { if (!(error instanceof RangeError)) throw error; throw Object.assign(uniConditionalError(filename, 0, 'Build data syntax exceeds its parser bound'), { code: 'ENTROROVE_SCOPE_LIMIT' }); }
|
|
46
|
+
const references = new Set(); let nodes = 0;
|
|
47
|
+
const inspect = node => {
|
|
48
|
+
if (++nodes > 8192) throw Object.assign(uniConditionalError(filename, 0, 'Build data syntax exceeds its 8192-node bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
49
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) references.add(node.moduleSpecifier.text);
|
|
50
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require'
|
|
51
|
+
&& node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])) references.add(node.arguments[0].text);
|
|
52
|
+
if (ts.isPropertyAssignment(node) && !ts.isComputedPropertyName(node.name) && ['loader', 'use'].includes(node.name.text)
|
|
53
|
+
&& ts.isStringLiteralLike(node.initializer)) references.add(node.initializer.text);
|
|
54
|
+
// Function bodies remain opaque build code. Inspect module-level literal
|
|
55
|
+
// declarations without simulating callbacks or proving a runtime closure.
|
|
56
|
+
if (ts.isFunctionLike(node)) return;
|
|
57
|
+
ts.forEachChild(node, inspect);
|
|
58
|
+
};
|
|
59
|
+
inspect(ast);
|
|
60
|
+
const literalRoots = [];
|
|
61
|
+
for (const statement of ast.statements) {
|
|
62
|
+
let expression = ts.isExportAssignment(statement) ? statement.expression : ts.isExpressionStatement(statement)
|
|
63
|
+
&& ts.isBinaryExpression(statement.expression) && statement.expression.left.getText(ast) === 'module.exports'
|
|
64
|
+
&& statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken ? statement.expression.right : undefined;
|
|
65
|
+
while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression))) expression = expression.expression;
|
|
66
|
+
if (!expression || !ts.isObjectLiteralExpression(expression)) continue;
|
|
67
|
+
const literal = Object.create(null);
|
|
68
|
+
for (const property of expression.properties) if (ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name)
|
|
69
|
+
&& ['root', 'envDir', 'context'].includes(property.name.text) && ts.isStringLiteralLike(property.initializer)) literal[property.name.text] = property.initializer.text;
|
|
70
|
+
literalRoots.push(literal);
|
|
71
|
+
}
|
|
72
|
+
return { references: [...references].filter(value => value.startsWith('.')).sort(), literalRoots };
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { packageBoundary, scopeFail, scopePath, scopeBuildSourceRead } from './local-scope-files.mjs';
|
|
2
|
+
|
|
3
|
+
// Opaque build programs are readonly origins. They never replace source identities
|
|
4
|
+
// or establish a complete compiled graph, and are never executed by inspection.
|
|
5
|
+
export async function observeBuildFacts(root, proof, input, graph) {
|
|
6
|
+
if (!proof || !Array.isArray(proof.observations) || !Array.isArray(proof.unresolvedBuildSources)) {
|
|
7
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', 'Build inspection requires explicit observed files and uncertainty origins');
|
|
8
|
+
}
|
|
9
|
+
for (const file of proof.observations) {
|
|
10
|
+
scopePath(file.path);
|
|
11
|
+
if (!/^[a-f0-9]{64}$/.test(file.sha256 ?? '')) scopeFail('ENTROROVE_IMPORT_INVALID', 'Build observation has no actual file hash');
|
|
12
|
+
const previous = graph.metadata.get(file.path);
|
|
13
|
+
if (previous && previous.sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build metadata changed during source inspection');
|
|
14
|
+
graph.metadata.set(file.path, { path: file.path, sha256: file.sha256 });
|
|
15
|
+
}
|
|
16
|
+
for (const origin of proof.unresolvedBuildSources) {
|
|
17
|
+
const file = graph.metadata.get(origin.path);
|
|
18
|
+
if (!file || file.sha256 !== origin.sha256 || typeof origin.reason !== 'string' || !/^[a-z][a-z-]{0,95}$/.test(origin.reason)) {
|
|
19
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', 'Opaque build origin must match an actual readonly file observation');
|
|
20
|
+
}
|
|
21
|
+
if (!graph.visited.has(file.path)) {
|
|
22
|
+
if (graph.visited.size >= 1000) scopeFail('ENTROROVE_SCOPE_LIMIT', 'import graph exceeds the 1000-file inspection bound');
|
|
23
|
+
const boundary = await packageBoundary(root, file.path, input, scopeBuildSourceRead);
|
|
24
|
+
graph.visited.set(file.path, { ...file, module: boundary.path, boundary });
|
|
25
|
+
}
|
|
26
|
+
if (graph.visited.get(file.path).sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Opaque build origin differs from its actual source node');
|
|
27
|
+
if (graph.dynamicSources.some(item => item.path === file.path)) continue;
|
|
28
|
+
graph.dynamicSources.push(file);
|
|
29
|
+
graph.unresolvedImports.push({ path: file.path, line: 1, column: 1, reason: origin.reason,
|
|
30
|
+
message: 'Build behavior remains unproven; this origin is read-only and source projection grants no compiled dependency guarantee' });
|
|
31
|
+
}
|
|
32
|
+
return proof.unresolvedBuildSources.length > 0;
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { scopeFail, scopeProbeTarget, scopeTarget } from './local-scope-files.mjs';
|
|
3
|
+
import { isProtectedBuildPath, protectedBuildRelativePath as protectedPath } from './local-scope-build-read.mjs';
|
|
4
|
+
|
|
5
|
+
const dependency = relative => relative.split('/').includes('node_modules');
|
|
6
|
+
|
|
7
|
+
// Inspect names and physical destinations before a build helper can read bytes.
|
|
8
|
+
// A dependency symlink is never permission to read an environment/control file.
|
|
9
|
+
export function createScopeBuildIO(root, readMetadata, hashMetadata) {
|
|
10
|
+
const protectedDestination = actual => isProtectedBuildPath(root, actual);
|
|
11
|
+
const probe = async relative => {
|
|
12
|
+
if (relative === '.') return { exists: true, file: false, directory: true };
|
|
13
|
+
const target = await scopeProbeTarget(root, relative, dependency(relative));
|
|
14
|
+
return { exists: target.exists, file: Boolean(target.stat?.isFile()), directory: Boolean(target.stat?.isDirectory()) };
|
|
15
|
+
};
|
|
16
|
+
const readTarget = async (relative, operation) => {
|
|
17
|
+
if (protectedPath(relative)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build inspection cannot read environment or control files');
|
|
18
|
+
const target = await scopeProbeTarget(root, relative, dependency(relative));
|
|
19
|
+
if (target.exists && protectedDestination(await fs.realpath(target.target))) {
|
|
20
|
+
scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build dependency resolves to an environment or control file; values were not read');
|
|
21
|
+
}
|
|
22
|
+
return operation(relative);
|
|
23
|
+
};
|
|
24
|
+
const read = relative => readTarget(relative, readMetadata);
|
|
25
|
+
const inspectDirectory = async (relative, maximumEntries) => {
|
|
26
|
+
if (protectedPath(relative)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build inspection cannot enter a private control directory');
|
|
27
|
+
const target = relative === '.' ? root : (await scopeTarget(root, relative)).target;
|
|
28
|
+
const before = await fs.lstat(target), realPath = await fs.realpath(target);
|
|
29
|
+
if (!before.isDirectory() || before.isSymbolicLink() || protectedDestination(realPath)) {
|
|
30
|
+
scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build directory must be a real non-private directory');
|
|
31
|
+
}
|
|
32
|
+
const identity = { dev: before.dev, ino: before.ino, mode: before.mode & 0o777, realPath };
|
|
33
|
+
const verify = async () => {
|
|
34
|
+
const current = await fs.lstat(target), actual = await fs.realpath(target);
|
|
35
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino
|
|
36
|
+
|| (current.mode & 0o777) !== identity.mode || actual !== realPath || protectedDestination(actual)) {
|
|
37
|
+
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build directory identity changed during name observation');
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
await verify();
|
|
41
|
+
const directory = await fs.opendir(target), environmentFiles = [], children = []; let entries = 0;
|
|
42
|
+
let iterating = false;
|
|
43
|
+
try {
|
|
44
|
+
await verify(); iterating = true;
|
|
45
|
+
for await (const entry of directory) {
|
|
46
|
+
if (++entries > maximumEntries) scopeFail('ENTROROVE_SCOPE_LIMIT', 'Conditional build directory exceeds its bounded entry inspection');
|
|
47
|
+
if (entry.name === '.env' || entry.name.startsWith('.env.')) environmentFiles.push(entry.name);
|
|
48
|
+
children.push({ name: entry.name, kind: entry.isSymbolicLink() ? 'symlink' : entry.isDirectory() ? 'directory'
|
|
49
|
+
: entry.isFile() ? 'file' : 'unsupported' });
|
|
50
|
+
}
|
|
51
|
+
} finally { if (!iterating) await directory.close(); }
|
|
52
|
+
await verify();
|
|
53
|
+
children.sort((left, right) => left.name.localeCompare(right.name));
|
|
54
|
+
return { environmentFiles: environmentFiles.sort(), entries, children, identity };
|
|
55
|
+
};
|
|
56
|
+
return { read, probe, inspectDirectory, ...(hashMetadata ? { hashMetadata: relative => readTarget(relative, hashMetadata) } : {}) };
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const protectedBuildRelativePath = value => value.split(/[\\/]/).some(part => part === '.env' || part.startsWith('.env.')
|
|
5
|
+
|| ['.git', '.entrorove', '.aimlock', '.archguard', '.ssh', '.aws', '.gnupg', '.kube', '.azure',
|
|
6
|
+
'.codex', '.claude', '.agents', '.npmrc', '.netrc', '.pypirc', '.gitconfig', 'id_rsa', 'id_ed25519'].includes(part)
|
|
7
|
+
|| /\.(?:pem|key)$/i.test(part));
|
|
8
|
+
export function isProtectedBuildPath(root, actual) {
|
|
9
|
+
const relative = path.relative(root, actual);
|
|
10
|
+
return protectedBuildRelativePath(relative === '..' || relative.startsWith(`..${path.sep}`) ? actual : relative);
|
|
11
|
+
}
|
|
12
|
+
export async function assertBuildReadTarget(root, relative, target, descriptor) {
|
|
13
|
+
if (protectedBuildRelativePath(relative) || isProtectedBuildPath(root, target)) {
|
|
14
|
+
throw Object.assign(new Error('Build dependency resolves to an environment or control file; values were not read'),
|
|
15
|
+
{ code: 'ENTROROVE_IMPORT_UNSUPPORTED' });
|
|
16
|
+
}
|
|
17
|
+
const actual = await fs.stat(target);
|
|
18
|
+
if (!actual.isFile() || actual.dev !== descriptor.dev || actual.ino !== descriptor.ino) {
|
|
19
|
+
throw Object.assign(new Error('Build dependency destination changed before reading any source bytes'),
|
|
20
|
+
{ code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
2
|
+
import { buildDataPath } from './local-scope-build-data.mjs';
|
|
3
|
+
import { sha256 } from './local-integrity.mjs';
|
|
4
|
+
|
|
5
|
+
const absent = error => ['ENOENT', 'ENOTDIR'].includes(error.code);
|
|
6
|
+
const changed = path => Object.assign(uniConditionalError(path, 0, 'Build data or candidate absence changed during inspection'), { code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
7
|
+
const shape = value => value ? { entries: value.entries, environmentFiles: [...value.environmentFiles].sort(),
|
|
8
|
+
...(value.identity ? { identity: { ...value.identity } } : {}),
|
|
9
|
+
...(value.children ? { children: value.children.map(({ name, kind }) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name)) } : {}) } : null;
|
|
10
|
+
export function createBuildDataReadset(read, inspectDirectory) {
|
|
11
|
+
const files = new Map(), directories = new Map(); let bytes = 0, entries = 0;
|
|
12
|
+
const observe = async relative => {
|
|
13
|
+
buildDataPath('.', relative); if (files.has(relative)) return files.get(relative);
|
|
14
|
+
if (files.size >= 2048) throw uniConditionalError(relative, 0, 'Build data exceeds its 2048-path inspection bound');
|
|
15
|
+
let item; try { item = await read(relative); } catch (error) { if (!absent(error)) throw error; }
|
|
16
|
+
if (item) {
|
|
17
|
+
const content = Buffer.from(item.contentBase64, 'base64'), size = content.length;
|
|
18
|
+
if (size > 1_048_576 || bytes + size > 8_388_608) throw Object.assign(uniConditionalError(relative, 0, 'Build data exceeds its 1-MiB/file or 8-MiB bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
19
|
+
if (item.path !== relative || !/^[a-f0-9]{64}$/.test(item.sha256) || item.sha256 !== sha256(content)) throw uniConditionalError(relative, 0, 'Build data observation has no actual path and hash');
|
|
20
|
+
bytes += size;
|
|
21
|
+
}
|
|
22
|
+
files.set(relative, item ?? null); return item ?? null;
|
|
23
|
+
};
|
|
24
|
+
const directory = async relative => {
|
|
25
|
+
if (relative !== '.') buildDataPath('.', relative);
|
|
26
|
+
if (directories.has(relative)) return directories.get(relative);
|
|
27
|
+
if (directories.size >= 128 || typeof inspectDirectory !== 'function') throw uniConditionalError(relative, 0, 'Build directory absence requires bounded actual inspection');
|
|
28
|
+
let value; try { value = await inspectDirectory(relative, Math.min(8192, 32768 - entries)); } catch (error) { if (!absent(error)) throw error; }
|
|
29
|
+
if (value) {
|
|
30
|
+
if (!Number.isInteger(value.entries) || value.entries < 0 || !Array.isArray(value.environmentFiles)
|
|
31
|
+
|| value.entries > 8192 || entries + value.entries > 32768) throw Object.assign(uniConditionalError(relative, 0, 'Build directory exceeds its bounded name inspection'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
32
|
+
entries += value.entries;
|
|
33
|
+
}
|
|
34
|
+
const result = shape(value); directories.set(relative, result); return result;
|
|
35
|
+
};
|
|
36
|
+
const verify = async () => {
|
|
37
|
+
for (const [relative, expected] of files) {
|
|
38
|
+
let actual; try { actual = await read(relative); } catch (error) { if (!absent(error)) throw error; }
|
|
39
|
+
if ((actual?.sha256 ?? null) !== (expected?.sha256 ?? null) || actual?.realPath !== expected?.realPath
|
|
40
|
+
|| actual?.mode !== expected?.mode || JSON.stringify(actual?.referenceRoot) !== JSON.stringify(expected?.referenceRoot)) throw changed(relative);
|
|
41
|
+
}
|
|
42
|
+
let checked = 0;
|
|
43
|
+
for (const [relative, expected] of directories) {
|
|
44
|
+
let actual; try { actual = await inspectDirectory(relative, Math.min(8192, 32768 - checked)); } catch (error) { if (!absent(error)) throw error; }
|
|
45
|
+
checked += actual?.entries ?? 0; if (JSON.stringify(shape(actual)) !== JSON.stringify(expected)) throw changed(relative);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return { observe, directory, verify, get observations() { return [...files.values()].filter(Boolean).map(({ path, sha256 }) => ({ path, sha256 })).sort((a, b) => a.path.localeCompare(b.path)); } };
|
|
49
|
+
}
|
package/local-scope-files.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { canonical, sha256 } from './local-integrity.mjs';
|
|
|
4
4
|
import { readBudgetedProjectFile } from './local-budget-reads.mjs';
|
|
5
5
|
import { verifiedSfnt } from './local-scope-font.mjs';
|
|
6
6
|
import { verifiedZip } from './local-scope-zip.mjs';
|
|
7
|
+
import { observeScopeAbsence, observeScopeRead, observeScopeTarget } from './local-scope-read-set.mjs';
|
|
8
|
+
import { assertBuildReadTarget, protectedBuildRelativePath } from './local-scope-build-read.mjs';
|
|
7
9
|
|
|
8
10
|
export function scopeFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
9
11
|
export function observedScopeRisk(files) {
|
|
@@ -66,6 +68,7 @@ export async function scopeTarget(root, relative, allowMissing = false, allowDep
|
|
|
66
68
|
current = path.join(current, part);
|
|
67
69
|
let stat;
|
|
68
70
|
try { stat = await fs.lstat(current); } catch (error) {
|
|
71
|
+
if (error.code === 'ENOENT') observeScopeAbsence(root, relative, () => scopeTarget(root, relative, true, allowDependency));
|
|
69
72
|
if (error.code === 'ENOENT' && allowMissing) return { target: path.join(root, relative), exists: false, stat: null };
|
|
70
73
|
throw error;
|
|
71
74
|
}
|
|
@@ -82,7 +85,11 @@ export async function scopeTarget(root, relative, allowMissing = false, allowDep
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
if (index < parts.length - 1 && !stat.isDirectory()) await invalidScopeTarget(root, relative, current, 'non-directory-ancestor');
|
|
85
|
-
if (index === parts.length - 1)
|
|
88
|
+
if (index === parts.length - 1) {
|
|
89
|
+
const target = { target: current, exists: true, stat, dependency: followDependency };
|
|
90
|
+
observeScopeTarget(root, relative, target, () => scopeTarget(root, relative, true, allowDependency));
|
|
91
|
+
return target;
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
// Only speculative, read-only resolver candidates may treat a non-directory ancestor as absent.
|
|
@@ -90,10 +97,11 @@ export async function scopeProbeTarget(root, relative, allowDependency = false)
|
|
|
90
97
|
try { return await scopeTarget(root, relative, true, allowDependency); }
|
|
91
98
|
catch (error) {
|
|
92
99
|
if (error.code !== 'ENTROROVE_FILE_INVALID' || error.reason !== 'non-directory-ancestor') throw error;
|
|
100
|
+
observeScopeAbsence(root, relative, () => scopeProbeTarget(root, relative, allowDependency));
|
|
93
101
|
return { target: path.join(root, relative), exists: false, stat: null };
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
|
-
async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
104
|
+
async function safeScopeRead(root, relative, input, allowDependency = true, verifyReadTarget) {
|
|
97
105
|
const dependency = allowDependency && dependencyReference(relative), target = await scopeTarget(root, relative, false, dependency);
|
|
98
106
|
if (!target.stat.isFile()) await invalidScopeTarget(root, relative, target.target, 'not-regular-file');
|
|
99
107
|
if (target.stat.size > 16_777_216) scopeFail('ENTROROVE_SCOPE_LIMIT', 'scope source exceeds the 16 MiB inspection bound');
|
|
@@ -101,7 +109,15 @@ async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
|
101
109
|
try {
|
|
102
110
|
const before = await handle.stat();
|
|
103
111
|
const beforeReal = dependency ? await fs.realpath(target.target) : null;
|
|
104
|
-
|
|
112
|
+
if (verifyReadTarget) {
|
|
113
|
+
if (before.ino !== target.stat.ino || before.dev !== target.stat.dev) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build source changed before its descriptor was opened');
|
|
114
|
+
await verifyReadTarget({ target: beforeReal ?? target.target, descriptor: before });
|
|
115
|
+
}
|
|
116
|
+
const boundedVerify = verifyReadTarget ? async actual => {
|
|
117
|
+
if (actual.descriptor.ino !== before.ino || actual.descriptor.dev !== before.dev) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build source changed before its accounted descriptor was opened');
|
|
118
|
+
await verifyReadTarget(actual);
|
|
119
|
+
} : undefined;
|
|
120
|
+
const content = input ? await readBudgetedProjectFile(input, target.target, { sourcePath: relative, verifyReadTarget: boundedVerify }) : await handle.readFile();
|
|
105
121
|
const after = await handle.stat();
|
|
106
122
|
const latest = dependency ? await fs.stat(target.target) : await fs.lstat(target.target);
|
|
107
123
|
const afterReal = dependency ? await fs.realpath(target.target) : null;
|
|
@@ -109,10 +125,19 @@ async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
|
109
125
|
|| after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || latest.ino !== after.ino
|
|
110
126
|
|| latest.dev !== after.dev || !dependency && latest.isSymbolicLink() || beforeReal !== afterReal) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope source changed during inspection');
|
|
111
127
|
await scopeTarget(root, relative, false, dependency);
|
|
112
|
-
|
|
128
|
+
const digest = sha256(content);
|
|
129
|
+
observeScopeRead(root, relative, { sha256: digest, mode: after.mode & 0o777, realPath: afterReal ?? target.target }, async () => {
|
|
130
|
+
const actual = await safeScopeRead(root, relative, input, allowDependency, verifyReadTarget);
|
|
131
|
+
return { sha256: actual.sha256, mode: actual.mode, realPath: await fs.realpath(target.target) };
|
|
132
|
+
});
|
|
133
|
+
return { content, mode: after.mode & 0o777, sha256: digest, bytes: content.length };
|
|
113
134
|
} finally { await handle.close(); }
|
|
114
135
|
}
|
|
115
136
|
export const scopeRead = (root, relative, input = { repositoryRoot: root }) => safeScopeRead(root, relative, input, true);
|
|
137
|
+
export function scopeBuildSourceRead(root, relative, input = { repositoryRoot: root }) {
|
|
138
|
+
if (protectedBuildRelativePath(relative)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build inspection cannot read environment or credential values');
|
|
139
|
+
return safeScopeRead(root, relative, input, true, ({ target, descriptor }) => assertBuildReadTarget(root, relative, target, descriptor));
|
|
140
|
+
}
|
|
116
141
|
// Internal control roles only: signed manifest metadata and already validated private signer files.
|
|
117
142
|
export const scopeControlRead = (root, relative) => safeScopeRead(root, relative, null, false);
|
|
118
143
|
const excluded = new Set(['.git', 'node_modules', '.entrorove', '.aimlock', '.archguard']);
|
|
@@ -140,12 +165,12 @@ export function lineCount(content) {
|
|
|
140
165
|
if (!content.length || verifiedSfnt(content) || verifiedZip(content)) return 0;
|
|
141
166
|
return content.toString('utf8').split(/\r\n|\n|\r/).length - (/[\r\n]$/.test(content.toString('utf8')) ? 1 : 0);
|
|
142
167
|
}
|
|
143
|
-
export async function packageBoundary(root, relative, input = { repositoryRoot: root }) {
|
|
168
|
+
export async function packageBoundary(root, relative, input = { repositoryRoot: root }, readSource = scopeRead) {
|
|
144
169
|
let directory = path.posix.dirname(relative);
|
|
145
170
|
while (true) {
|
|
146
171
|
const manifestPath = directory === '.' ? 'package.json' : `${directory}/package.json`;
|
|
147
172
|
try {
|
|
148
|
-
const read = await
|
|
173
|
+
const read = await readSource(root, manifestPath, input); let manifest;
|
|
149
174
|
try { manifest = JSON.parse(read.content.toString('utf8')); } catch { scopeFail('ENTROROVE_PACKAGE_INVALID', 'package manifest is not valid JSON'); }
|
|
150
175
|
return { path: directory, manifestPath, manifestDigest: read.sha256, name: typeof manifest.name === 'string' ? manifest.name : null };
|
|
151
176
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
@@ -154,11 +179,11 @@ export async function packageBoundary(root, relative, input = { repositoryRoot:
|
|
|
154
179
|
}
|
|
155
180
|
return { path: relative.includes('/') ? relative.split('/')[0] : '.', manifestPath: null, manifestDigest: null, name: null };
|
|
156
181
|
}
|
|
157
|
-
export async function stableScope(root, files, input = { repositoryRoot: root }) {
|
|
182
|
+
export async function stableScope(root, files, input = { repositoryRoot: root }, readSource = scopeRead) {
|
|
158
183
|
for (const file of files) {
|
|
159
184
|
if (file.exists === false) {
|
|
160
185
|
if ((await scopeTarget(root, file.path, true)).exists) scopeFail('ENTROROVE_SCOPE_CHANGED', 'a deleted target reappeared during inspection');
|
|
161
|
-
} else if ((await
|
|
186
|
+
} else if ((await readSource(root, file.path, input)).sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope files changed before the observation completed');
|
|
162
187
|
}
|
|
163
188
|
}
|
|
164
189
|
export const scopeDigest = value => sha256(canonical(value));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const context = new AsyncLocalStorage();
|
|
7
|
+
const MAX_RECORDS = 32_768, MAX_PATH_BYTES = 4_194_304, MAX_DIRECTORY_ENTRIES = 32_768;
|
|
8
|
+
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
9
|
+
const changed = () => fail('ENTROROVE_SCOPE_CHANGED', 'Original scope resolver observations changed before signing');
|
|
10
|
+
const fingerprint = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
11
|
+
function record(root, relative, kind, expected, inspect, directoryEntries = 0) {
|
|
12
|
+
const state = context.getStore();
|
|
13
|
+
if (!state || state.sealed) return;
|
|
14
|
+
if (root !== state.root || typeof relative !== 'string' || typeof inspect !== 'function') {
|
|
15
|
+
fail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Scope read observations require their original in-process root');
|
|
16
|
+
}
|
|
17
|
+
const key = `${kind}:${relative}`, checksum = fingerprint(expected), previous = state.records.get(key);
|
|
18
|
+
if (previous) { if (previous.checksum !== checksum) changed(); return; }
|
|
19
|
+
state.pathBytes += Buffer.byteLength(relative);
|
|
20
|
+
state.directoryEntries += directoryEntries;
|
|
21
|
+
if (state.records.size >= MAX_RECORDS || state.pathBytes > MAX_PATH_BYTES || state.directoryEntries > MAX_DIRECTORY_ENTRIES) {
|
|
22
|
+
fail('ENTROROVE_SCOPE_LIMIT', 'Scope resolver observations exceed their bounded in-process read set');
|
|
23
|
+
}
|
|
24
|
+
state.records.set(key, { checksum, inspect });
|
|
25
|
+
}
|
|
26
|
+
export function observeScopeAbsence(root, relative, inspect) {
|
|
27
|
+
record(root, relative, 'absence', false, async () => (await inspect()).exists);
|
|
28
|
+
}
|
|
29
|
+
export function observeScopeRead(root, relative, expected, inspect) {
|
|
30
|
+
record(root, relative, 'read', expected, inspect);
|
|
31
|
+
}
|
|
32
|
+
export function observeScopeTarget(root, relative, target, inspect) {
|
|
33
|
+
const kind = value => ({ exists: value.exists, file: value.stat?.isFile() ?? false, directory: value.stat?.isDirectory() ?? false });
|
|
34
|
+
record(root, relative, 'target', kind(target), async () => kind(await inspect()));
|
|
35
|
+
}
|
|
36
|
+
export function observeScopeDirectory(root, relative, names, inspect) {
|
|
37
|
+
record(root, relative, 'directory', names, inspect, names.length);
|
|
38
|
+
}
|
|
39
|
+
function sameRoot(before, after) {
|
|
40
|
+
return after.isDirectory() && !after.isSymbolicLink() && before.dev === after.dev && before.ino === after.ino;
|
|
41
|
+
}
|
|
42
|
+
export async function captureScopeReadSet(root, action) {
|
|
43
|
+
if (context.getStore() || typeof root !== 'string' || !path.isAbsolute(root) || typeof action !== 'function') {
|
|
44
|
+
fail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Scope read capture requires one actual independent observation');
|
|
45
|
+
}
|
|
46
|
+
const original = await fs.lstat(root);
|
|
47
|
+
if (!sameRoot(original, original) || await fs.realpath(root) !== root) {
|
|
48
|
+
fail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Scope read capture requires the original real repository root');
|
|
49
|
+
}
|
|
50
|
+
const state = { root, records: new Map(), pathBytes: 0, directoryEntries: 0, sealed: false };
|
|
51
|
+
let value;
|
|
52
|
+
try { value = await context.run(state, action); } finally { state.sealed = true; }
|
|
53
|
+
const verify = () => context.run(undefined, async () => {
|
|
54
|
+
try {
|
|
55
|
+
if (!sameRoot(original, await fs.lstat(root)) || await fs.realpath(root) !== root) changed();
|
|
56
|
+
for (const observation of state.records.values()) {
|
|
57
|
+
if (fingerprint(await observation.inspect()) !== observation.checksum) changed();
|
|
58
|
+
}
|
|
59
|
+
if (!sameRoot(original, await fs.lstat(root)) || await fs.realpath(root) !== root) changed();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code?.startsWith('ENTROROVE_')) throw error;
|
|
62
|
+
changed();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return { value, verify };
|
|
66
|
+
}
|