@entrorove/skill 0.1.73 → 0.1.81
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/CAPABILITY-MAP.md +7 -2
- package/CHANGELOG.md +51 -0
- package/DESIGN-GATES.md +5 -9
- package/DESIGN-STAGED-ACCEPTANCE.md +103 -0
- package/IDE-EXECUTION.md +21 -4
- package/REUSE-DISCIPLINE.md +3 -3
- package/SKILL.md +14 -4
- package/cli.mjs +10 -6
- package/cloud-mutation-authority.mjs +8 -2
- package/cloud-task-client.mjs +10 -0
- package/cloud-watch-client.mjs +8 -2
- package/design-browser-checks.mjs +18 -5
- package/design-browser-pages.mjs +96 -0
- package/design-browser-state.mjs +1 -0
- package/design-browser.mjs +36 -40
- package/design-contracts.mjs +4 -1
- package/design-declaration.mjs +3 -0
- package/design-frameworks.mjs +2 -1
- package/design-gate.mjs +3 -0
- package/design-operations.mjs +4 -1
- package/design-policy.mjs +12 -4
- package/design-release.mjs +6 -0
- package/design-research-network.mjs +82 -0
- package/design-research.mjs +8 -25
- package/design-runner.mjs +3 -0
- package/design-stage-contracts.mjs +12 -0
- package/design-stage.mjs +78 -0
- package/design-storage.mjs +63 -0
- package/fs-request-trace.mjs +67 -0
- package/local-approved-process.mjs +5 -1
- package/local-cli.mjs +40 -1
- package/local-contracts.mjs +1 -1
- package/local-discipline-repository.mjs +4 -2
- package/local-invocations.mjs +6 -0
- package/local-mutation-files.mjs +13 -5
- package/local-probe.mjs +8 -4
- package/local-recovery-budget.mjs +31 -0
- package/local-scope-repository.mjs +18 -3
- package/local-scope-signature.mjs +2 -2
- package/local-source-execution.mjs +47 -0
- package/manifest.json +1 -1
- package/mutation-challenge-refresh.mjs +3 -0
- package/mutation-client.mjs +17 -5
- package/native-client.mjs +4 -1
- package/package.json +16 -2
- package/release-source-groups.mjs +2 -0
- package/release-state.json +70 -55
- package/release.mjs +6 -1
- package/releases.json +48 -0
- package/remote-capability-cache.mjs +20 -0
- package/remote-client.mjs +24 -4
- package/scope-amendment-client.mjs +62 -6
- package/scope-amendment-observer.mjs +9 -3
- package/scope-amendment-remote.mjs +20 -6
- package/scope-amendment-retirement.mjs +4 -2
- package/scope-amendment-state.mjs +9 -5
- package/scope-context-client.mjs +7 -0
- package/scope-context-journal.mjs +8 -2
- package/scope-context-remote.mjs +16 -5
- package/scope-evidence-remote.mjs +16 -7
- package/scope-runner-client.mjs +39 -8
- package/scope-runner-journal.mjs +82 -5
- package/scope-write-observation.d.mts +4 -0
- package/scope-write-observation.mjs +43 -0
- package/service-prerequisite.mjs +87 -0
- package/task-entry-client.mjs +3 -0
- package/workflow-observation-client.mjs +3 -0
- package/workflow-observation-connection.mjs +4 -1
- package/workflow-observation-interruption.mjs +12 -0
- package/workflow-observation-recovery.mjs +42 -0
- package/workflow-observation-remote.mjs +13 -5
- package/workflow-observation-runner.mjs +13 -6
package/design-research.mjs
CHANGED
|
@@ -1,43 +1,23 @@
|
|
|
1
|
-
import https from 'node:https';
|
|
2
|
-
import dns from 'node:dns/promises';
|
|
3
1
|
import net from 'node:net';
|
|
4
2
|
import path from 'node:path';
|
|
5
3
|
import { parse as yaml } from 'yaml';
|
|
6
4
|
import { mutationRead14, mutationTarget14 } from './local-mutation-files.mjs';
|
|
7
5
|
import { sha256 } from './local-integrity.mjs';
|
|
8
6
|
import { designFail } from './design-policy.mjs';
|
|
7
|
+
import { publicResearchAddresses, researchResponse } from './design-research-network.mjs';
|
|
9
8
|
|
|
10
9
|
import { FRAMEWORK_DOC_HOSTS } from './design-frameworks.mjs';
|
|
11
10
|
export { FRAMEWORK_DOC_HOSTS } from './design-frameworks.mjs';
|
|
12
11
|
export function researchUrl(value) {
|
|
13
12
|
let url; try { url = new URL(value); } catch { designFail('RESEARCH_URL', 'Research requires an absolute public HTTPS URL'); }
|
|
14
13
|
if (url.protocol !== 'https:' || url.username || url.password || url.port && url.port !== '443' || url.hash
|
|
15
|
-
|| net.isIP(url.hostname) || /(^|\.)(localhost|local|internal|test|invalid)$/i.test(url.hostname)
|
|
14
|
+
|| net.isIP(url.hostname.replace(/^\[|\]$/g, '')) || /(^|\.)(localhost|local|internal|test|invalid)$/i.test(url.hostname)
|
|
16
15
|
|| [...url.searchParams.keys()].some(key => /token|secret|password|key|signature|credential/i.test(key))) designFail('RESEARCH_URL', 'Research URLs must be public, credential-free HTTPS references');
|
|
17
16
|
return url;
|
|
18
17
|
}
|
|
19
|
-
function publicIp(address) {
|
|
20
|
-
if (net.isIP(address) !== 4) return false;
|
|
21
|
-
const [a, b, c] = address.split('.').map(Number);
|
|
22
|
-
return ![0, 10, 127].includes(a) && a < 224 && !(a === 169 && b === 254)
|
|
23
|
-
&& !(a === 172 && b >= 16 && b <= 31) && !(a === 192 && (b === 168 || b === 0 && [0, 2].includes(c)))
|
|
24
|
-
&& !(a === 198 && ([18, 19].includes(b) || b === 51 && c === 100)) && !(a === 203 && b === 0 && c === 113)
|
|
25
|
-
&& !(a === 100 && b >= 64 && b <= 127);
|
|
26
|
-
}
|
|
27
18
|
export async function fetchDesignReference(value, redirects = 0) {
|
|
28
|
-
const url = researchUrl(value), addresses = await
|
|
29
|
-
|
|
30
|
-
const result = await new Promise((resolve, reject) => {
|
|
31
|
-
const req = https.get(url, { headers: { accept: 'text/html,text/plain,application/json', 'user-agent': 'Entrorove design research' },
|
|
32
|
-
lookup: (_host, options, callback) => callback(null, options.all ? [addresses[0]] : addresses[0].address, 4) }, response => {
|
|
33
|
-
const chunks = []; let bytes = 0;
|
|
34
|
-
response.on('data', chunk => { bytes += chunk.length; if (bytes > 2_097_152) req.destroy(Object.assign(new Error('Research response exceeds 2 MiB'), { code: 'ENTROROVE_DESIGN_RESEARCH_SIZE' })); else chunks.push(chunk); });
|
|
35
|
-
response.on('end', () => resolve({ status: response.statusCode, location: response.headers.location, type: response.headers['content-type'], body: Buffer.concat(chunks) }));
|
|
36
|
-
response.on('error', reject);
|
|
37
|
-
});
|
|
38
|
-
req.setTimeout(15_000, () => req.destroy(Object.assign(new Error('Research fetch timed out'), { code: 'ENTROROVE_DESIGN_RESEARCH_TIMEOUT' })));
|
|
39
|
-
req.on('error', reject);
|
|
40
|
-
});
|
|
19
|
+
const url = researchUrl(value), addresses = await publicResearchAddresses(url.hostname);
|
|
20
|
+
const result = await researchResponse(url, addresses);
|
|
41
21
|
if ([301, 302, 303, 307, 308].includes(result.status)) {
|
|
42
22
|
if (!result.location || redirects >= 4) designFail('RESEARCH_REDIRECT', 'Research redirect did not resolve to a bounded public reference');
|
|
43
23
|
return fetchDesignReference(new URL(result.location, url).href, redirects + 1);
|
|
@@ -76,7 +56,10 @@ export async function assertDeclaredDesignFrameworks(input, design) {
|
|
|
76
56
|
if (!checked.has(name)) {
|
|
77
57
|
const actual = JSON.parse((await mutationRead14(input, input.repositoryRoot, name)).content.toString('utf8'));
|
|
78
58
|
const packages = { ...actual.dependencies, ...actual.devDependencies };
|
|
79
|
-
if (Object.keys(packages).some(packageName => Object.hasOwn(FRAMEWORK_DOC_HOSTS, packageName)
|
|
59
|
+
if (Object.keys(packages).some(packageName => Object.hasOwn(FRAMEWORK_DOC_HOSTS, packageName)
|
|
60
|
+
&& !design.frameworks.some(framework => framework.packageName === packageName && framework.packagePath === name))) {
|
|
61
|
+
designFail('FRAMEWORK_MISSING', 'Research every actual declared framework in its nearest UI package using that package file and locked version');
|
|
62
|
+
}
|
|
80
63
|
checked.add(name);
|
|
81
64
|
}
|
|
82
65
|
break;
|
package/design-runner.mjs
CHANGED
|
@@ -3,10 +3,12 @@ import { assertDesignDeclaration } from './design-declaration.mjs';
|
|
|
3
3
|
import { assertDesignWrite } from './design-gate.mjs';
|
|
4
4
|
import { designUiCandidate } from './design-ui.mjs';
|
|
5
5
|
import { designFail } from './design-policy.mjs';
|
|
6
|
+
import { assertDesignStageSources } from './design-stage.mjs';
|
|
6
7
|
|
|
7
8
|
export async function designRunnerBefore(input, authority) {
|
|
8
9
|
const policy = authority.plan.policy.discipline, originals = new Map();
|
|
9
10
|
if (!policy || policy.access === 'read-only') return originals;
|
|
11
|
+
if (policy.design) await assertDesignStageSources(input, policy.design);
|
|
10
12
|
assertDesignDeclaration(policy, policy.writes.flatMap(write => [write.targetPath, ...(write.destinationPath ? [write.destinationPath] : [])]));
|
|
11
13
|
for (const write of policy.writes) {
|
|
12
14
|
const target = await mutationTarget14(authority.root, write.targetPath, true);
|
|
@@ -19,6 +21,7 @@ export async function designRunnerBefore(input, authority) {
|
|
|
19
21
|
}
|
|
20
22
|
export async function designRunnerAfter(input, authority, changed, originals) {
|
|
21
23
|
const policy = authority.plan.policy.discipline;
|
|
24
|
+
if (policy?.design) await assertDesignStageSources(input, policy.design);
|
|
22
25
|
for (const name of changed) {
|
|
23
26
|
const target = await mutationTarget14(authority.root, name, true);
|
|
24
27
|
const source = target.exists && target.stat.size <= 2_097_152
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { object, array, text, identifier, relativePath, sha256 } from './local-contract-types.mjs';
|
|
2
|
+
|
|
3
|
+
const names = min => ({ ...array(identifier, min), maxItems: 256, uniqueItems: true });
|
|
4
|
+
const source = object({ path: relativePath, sha256 }, ['path', 'sha256']);
|
|
5
|
+
export const DESIGN_STAGE_SCHEMA = object({ id: identifier, requirementIds: names(1), pageIds: names(1), edgeIds: names(0),
|
|
6
|
+
readOnlyPages: { ...array(object({ pageId: identifier, sources: { ...array(source, 1), maxItems: 128 } }, ['pageId', 'sources']), 0), maxItems: 128 } },
|
|
7
|
+
['id', 'requirementIds', 'pageIds', 'edgeIds', 'readOnlyPages']);
|
|
8
|
+
const value = { anyOf: [{ type: 'string', maxLength: 8192 }, { type: 'null' }] };
|
|
9
|
+
export const DESIGN_HANDOFF_SCHEMA = object({ area: { enum: ['local-storage', 'session-storage'] },
|
|
10
|
+
key: { ...text, maxLength: 128, pattern: '^[^\\u0000-\\u001f\\u007f]+$' },
|
|
11
|
+
method: { enum: ['setItem', 'removeItem'] }, before: value, after: value, count: { const: 1 } },
|
|
12
|
+
['area', 'key', 'method', 'before', 'after', 'count']);
|
package/design-stage.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { designFail } from './design-policy.mjs';
|
|
2
|
+
import { browserStepHandoffs } from './design-storage.mjs';
|
|
3
|
+
|
|
4
|
+
export function validateDesignStage(policy) {
|
|
5
|
+
const stage = policy.rules.stage;
|
|
6
|
+
if (!stage) return;
|
|
7
|
+
const active = new Set(stage.pageIds), passive = new Set(stage.readOnlyPages.map(item => item.pageId));
|
|
8
|
+
if (active.size !== stage.pageIds.length || passive.size !== stage.readOnlyPages.length
|
|
9
|
+
|| stage.requirementIds.some(id => !policy.site.requirementIds.includes(id))
|
|
10
|
+
|| policy.site.pages.some(page => Number(active.has(page.id)) + Number(passive.has(page.id)) !== 1)
|
|
11
|
+
|| [...active, ...passive].some(id => !policy.site.pages.some(page => page.id === id))) {
|
|
12
|
+
designFail('STAGE', 'A stage retains the whole site and partitions every original page into executable or source-bound read-only context');
|
|
13
|
+
}
|
|
14
|
+
for (const id of stage.edgeIds) {
|
|
15
|
+
const edge = policy.site.edges.find(item => item.id === id);
|
|
16
|
+
if (!edge || !active.has(edge.from) || !active.has(edge.to)) designFail('STAGE', 'Every executable stage edge must retain its original endpoints');
|
|
17
|
+
}
|
|
18
|
+
const sources = new Map();
|
|
19
|
+
for (const page of stage.readOnlyPages) for (const source of page.sources) {
|
|
20
|
+
if (!policy.rules.verificationPaths.includes(source.path)
|
|
21
|
+
|| sources.has(source.path) && sources.get(source.path) !== source.sha256
|
|
22
|
+
|| page.sources.filter(item => item.path === source.path).length !== 1) {
|
|
23
|
+
designFail('STAGE', 'Read-only context requires exact existing source hashes in the unchanged verification range');
|
|
24
|
+
}
|
|
25
|
+
sources.set(source.path, source.sha256);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const executableDesignPages = policy => policy.rules.stage?.pageIds ?? policy.site.pages.map(page => page.id);
|
|
29
|
+
export const executableDesignEdges = policy => policy.rules.stage?.edgeIds ?? policy.site.edges.map(edge => edge.id);
|
|
30
|
+
export function designAcceptanceScope(policy) {
|
|
31
|
+
return { mode: policy.rules.stage ? 'stage' : 'whole-site', ...(policy.rules.stage ? { stageId: policy.rules.stage.id } : {}),
|
|
32
|
+
pageIds: executableDesignPages(policy), edgeIds: executableDesignEdges(policy),
|
|
33
|
+
readOnlyPageIds: (policy.rules.stage?.readOnlyPages ?? []).map(page => page.pageId),
|
|
34
|
+
claimsWholeBusinessCompletion: false };
|
|
35
|
+
}
|
|
36
|
+
export async function assertDesignStageSources(input, policy) {
|
|
37
|
+
const sources = new Map((policy.rules.stage?.readOnlyPages ?? []).flatMap(page => page.sources.map(source => [source.path, source.sha256])));
|
|
38
|
+
if (!sources.size) return;
|
|
39
|
+
const { repositoryIntegrityFile } = await import('./local-discipline-repository.mjs');
|
|
40
|
+
for (const [name, expected] of sources) {
|
|
41
|
+
const actual = await repositoryIntegrityFile(input.repositoryRoot, name);
|
|
42
|
+
if (actual.sha256 !== expected) designFail('STAGE_SOURCE_CHANGED', 'A frozen read-only context source changed; preserve this result and obtain a truthful new baseline');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function assertDesignStageWrite(policy, name) {
|
|
46
|
+
const expected = (policy.rules.stage?.readOnlyPages ?? []).flatMap(page => page.sources).find(source => source.path === name);
|
|
47
|
+
if (expected) designFail('STAGE_READ_ONLY', 'This source is frozen as read-only context in the original approved stage');
|
|
48
|
+
}
|
|
49
|
+
const sensitive = /token|secret|password|passphrase|credential|authorization|cookie|session.?id|private.?key|api.?key|^(?:auth|roles?|permissions?|authenticated|isAdmin|isLoggedIn|accessLevel)$/i;
|
|
50
|
+
function safeHandoffValue(value) {
|
|
51
|
+
if (value === null) return true;
|
|
52
|
+
if (/-----BEGIN .*PRIVATE KEY|\bBearer\s+|https?:\/\/[^\s/]+:[^\s/]+@/i.test(value)) return false;
|
|
53
|
+
let parsed; try { parsed = JSON.parse(value); } catch { return !/[?&](?:token|key|secret|password|signature)=/i.test(value); }
|
|
54
|
+
const pending = [parsed]; let nodes = 0;
|
|
55
|
+
while (pending.length) {
|
|
56
|
+
if (++nodes > 1024) return false;
|
|
57
|
+
const node = pending.pop();
|
|
58
|
+
if (node && typeof node === 'object') for (const [key, item] of Object.entries(node)) {
|
|
59
|
+
if (sensitive.test(key)) return false;
|
|
60
|
+
pending.push(item);
|
|
61
|
+
}
|
|
62
|
+
else if (typeof node === 'string' && /\bBearer\s+|[?&](?:token|secret|password|signature)=/i.test(node)) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
export function validateDesignHandoffs(step, edge) {
|
|
67
|
+
const handoffs = browserStepHandoffs(step, edge);
|
|
68
|
+
if (!handoffs.length) return;
|
|
69
|
+
if (!['navigate', 'open'].includes(edge.kind) || step.action === 'fill' || step.mutations.length
|
|
70
|
+
|| step.storageHandoffs?.length && step.readBack
|
|
71
|
+
|| step.readBack && (step.readBack.jsonPointer || step.readBack.kind !== 'local-storage')
|
|
72
|
+
|| new Set(handoffs.map(item => `${item.area}\0${item.key}`)).size !== handoffs.length
|
|
73
|
+
|| handoffs.some(item => sensitive.test(item.key)
|
|
74
|
+
|| (item.method === 'removeItem') !== (item.after === null)
|
|
75
|
+
|| !safeHandoffValue(item.before) || !safeHandoffValue(item.after))) {
|
|
76
|
+
designFail('HANDOFF', 'Only an explicit navigation/open action may declare exact noncredential storage handoffs; no HTTP commit or success authority is granted');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { canonical } from './local-integrity.mjs';
|
|
2
|
+
|
|
3
|
+
export function browserStepHandoffs(step, edge) {
|
|
4
|
+
if (step.storageHandoffs?.length) return step.storageHandoffs;
|
|
5
|
+
if (!['navigate', 'open'].includes(edge.kind) || step.readBack?.kind !== 'local-storage') return [];
|
|
6
|
+
const proof = step.readBack;
|
|
7
|
+
return [{ area: 'local-storage', key: proof.key, method: proof.after === null ? 'removeItem' : 'setItem', count: 1,
|
|
8
|
+
before: proof.before === null ? null : canonical(proof.before), after: proof.after === null ? null : canonical(proof.after), format: 'json' }];
|
|
9
|
+
}
|
|
10
|
+
function normalized(value, format) {
|
|
11
|
+
if (value === null || format !== 'json') return value;
|
|
12
|
+
try { return canonical(JSON.parse(value)); } catch { return undefined; }
|
|
13
|
+
}
|
|
14
|
+
async function storageSnapshot(page) {
|
|
15
|
+
return page.evaluate(() => {
|
|
16
|
+
let total = 0;
|
|
17
|
+
const read = store => {
|
|
18
|
+
if (store.length > 256) throw new Error('storage observation limit');
|
|
19
|
+
return Object.fromEntries(Array.from({ length: store.length }, (_, index) => {
|
|
20
|
+
const key = store.key(index), value = store.getItem(key); total += key.length + value.length;
|
|
21
|
+
if (total > 1_048_576) throw new Error('storage observation limit');
|
|
22
|
+
return [key, value];
|
|
23
|
+
}));
|
|
24
|
+
};
|
|
25
|
+
return { 'local-storage': read(localStorage), 'session-storage': read(sessionStorage) };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function browserStorage(base, parents) {
|
|
29
|
+
let before = new Map(), actor;
|
|
30
|
+
const empty = () => ({ 'local-storage': {}, 'session-storage': {} });
|
|
31
|
+
return {
|
|
32
|
+
async begin(context, page) {
|
|
33
|
+
actor = page; before = new Map();
|
|
34
|
+
for (const current of context.pages()) if (new URL(current.url()).origin === base.origin) before.set(current, await storageSnapshot(current));
|
|
35
|
+
},
|
|
36
|
+
async check(context, current, handoffs, writes, commits, failures) {
|
|
37
|
+
if (commits) return;
|
|
38
|
+
const same = (value, expected, format) => normalized(value, format) === normalized(expected, format);
|
|
39
|
+
if (writes.length !== handoffs.length || handoffs.some(expected => writes.filter(event =>
|
|
40
|
+
[actor, current].includes(event.page) && event.area === expected.area && event.key === expected.key && event.method === expected.method
|
|
41
|
+
&& same(event.before, expected.before, expected.format) && same(event.after, expected.after, expected.format)).length !== 1)) {
|
|
42
|
+
failures.push('unexpected-browser-storage-write');
|
|
43
|
+
}
|
|
44
|
+
for (const page of context.pages()) {
|
|
45
|
+
if (new URL(page.url()).origin !== base.origin) continue;
|
|
46
|
+
const original = before.get(page) ?? before.get(parents.get(page)) ?? empty(), after = await storageSnapshot(page);
|
|
47
|
+
for (const area of ['local-storage', 'session-storage']) {
|
|
48
|
+
const expected = { ...original[area] };
|
|
49
|
+
const applies = area === 'local-storage' || page === actor || page === current && !before.has(page);
|
|
50
|
+
for (const change of handoffs.filter(value => applies && value.area === area)) {
|
|
51
|
+
const value = Object.hasOwn(expected, change.key) ? expected[change.key] : null;
|
|
52
|
+
if (!same(value, change.before, change.format)) failures.push('handoff-before-state');
|
|
53
|
+
if (change.after === null) delete expected[change.key]; else expected[change.key] = change.after;
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(after[area]).length !== Object.keys(expected).length || Object.keys(expected).some(key => {
|
|
56
|
+
const declaration = handoffs.find(value => applies && value.area === area && value.key === key);
|
|
57
|
+
return !Object.hasOwn(after[area], key) || !same(after[area][key], expected[key], declaration?.format);
|
|
58
|
+
})) failures.push('handoff-storage-state');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHook } from 'node:async_hooks';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const sdkRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function captureFsStack(exclude) {
|
|
8
|
+
const value = {}; Error.captureStackTrace(value, exclude); return value;
|
|
9
|
+
}
|
|
10
|
+
export function fsTraceStack(stack, root = sdkRoot) {
|
|
11
|
+
const frames = [];
|
|
12
|
+
for (const line of String(stack ?? '').split('\n').slice(1, 40)) {
|
|
13
|
+
const match = /(?:\()?((?:file:\/\/\/|node:)[^()\s]+):(\d+):(\d+)\)?$/.exec(line.trim());
|
|
14
|
+
if (!match) continue;
|
|
15
|
+
const [, source, row, column] = match; let module;
|
|
16
|
+
if (source.startsWith('node:')) module = source;
|
|
17
|
+
else {
|
|
18
|
+
let filename; try { filename = fileURLToPath(source); } catch { continue; }
|
|
19
|
+
const relative = path.relative(root, filename);
|
|
20
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || !/^[A-Za-z0-9_./-]+\.mjs$/.test(relative)) continue;
|
|
21
|
+
if (relative === 'fs-request-trace.mjs') continue;
|
|
22
|
+
module = relative;
|
|
23
|
+
}
|
|
24
|
+
frames.push({ module, line: Number(row), column: Number(column) });
|
|
25
|
+
if (frames.length >= 12) break;
|
|
26
|
+
}
|
|
27
|
+
return frames;
|
|
28
|
+
}
|
|
29
|
+
export function createFsRequestTrace({ hookFactory = createHook, now = () => performance.now(),
|
|
30
|
+
repeat = setInterval, cancel = clearInterval, write = value => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
31
|
+
stack = captureFsStack, thresholdMs = 15_000, maximumPending = 128 } = {}) {
|
|
32
|
+
if (!Number.isSafeInteger(thresholdMs) || thresholdMs < 1 || !Number.isSafeInteger(maximumPending)
|
|
33
|
+
|| maximumPending < 1 || maximumPending > 128) throw new Error('Invalid bounded FS trace settings');
|
|
34
|
+
const pending = new Map(); let dropped = 0, reported = 0, enabled = true;
|
|
35
|
+
const emit = value => { try { write({ schemaVersion: 'entrorove.fs-request-trace/1.0', metadataOnly: true, ...value }); } catch {} };
|
|
36
|
+
const hook = hookFactory({
|
|
37
|
+
init: function fsInit(asyncId, type, triggerAsyncId) {
|
|
38
|
+
if ((type !== 'FSREQPROMISE' && type !== 'FSREQCALLBACK') || !enabled) return;
|
|
39
|
+
if (pending.size >= maximumPending) { dropped++; return; }
|
|
40
|
+
try { pending.set(asyncId, { asyncId, triggerAsyncId, resourceType: type, created: now(), stack: stack(fsInit), reported: false }); }
|
|
41
|
+
catch { dropped++; }
|
|
42
|
+
},
|
|
43
|
+
destroy(asyncId) { pending.delete(asyncId); },
|
|
44
|
+
promiseResolve(asyncId) { pending.delete(asyncId); },
|
|
45
|
+
});
|
|
46
|
+
const check = () => {
|
|
47
|
+
for (const request of pending.values()) {
|
|
48
|
+
const elapsedMs = Math.max(0, Math.floor(now() - request.created));
|
|
49
|
+
if (request.reported || elapsedMs < thresholdMs) continue;
|
|
50
|
+
request.reported = true; reported++;
|
|
51
|
+
let creationStack = [];
|
|
52
|
+
try { creationStack = fsTraceStack(typeof request.stack === 'string' ? request.stack : request.stack?.stack); } catch {}
|
|
53
|
+
request.stack = null;
|
|
54
|
+
emit({ kind: 'fs-request-stall', resourceType: request.resourceType, asyncId: request.asyncId,
|
|
55
|
+
triggerAsyncId: request.triggerAsyncId, elapsedMs, pendingCount: pending.size, dropped,
|
|
56
|
+
creationStack, creationStackAvailable: creationStack.length > 0, cancelsRequest: false, grantsExecutionAuthority: false });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
hook.enable(); const timer = repeat(check, Math.min(1000, thresholdMs)); timer.unref?.();
|
|
60
|
+
const stop = () => {
|
|
61
|
+
if (enabled) { enabled = false; hook.disable(); cancel(timer); pending.clear(); }
|
|
62
|
+
return { enabled, pendingCount: pending.size, dropped, reported };
|
|
63
|
+
};
|
|
64
|
+
stop.inspect = () => ({ enabled, pendingCount: pending.size, dropped, reported });
|
|
65
|
+
return stop;
|
|
66
|
+
}
|
|
67
|
+
export const startFsTrace = () => createFsRequestTrace();
|
|
@@ -25,7 +25,11 @@ function supervisionError(code, message, effects = 'unknown') {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export async function runApprovedProcess(authority) {
|
|
28
|
-
const started = performance.now()
|
|
28
|
+
const started = performance.now();
|
|
29
|
+
let input;
|
|
30
|
+
try { input = launchInput(authority); }
|
|
31
|
+
catch (error) { throw Object.assign(error, { execution: { schemaVersion: RUNNER_SCHEMA, status: 'failed', evidence: [],
|
|
32
|
+
effects: 'none', prelaunchVerified: true } }); }
|
|
29
33
|
return new Promise((resolve, reject) => {
|
|
30
34
|
let finished = false;
|
|
31
35
|
const supervisor = fork(SUPERVISOR, [], { execPath: process.execPath, execArgv: [], env: {}, detached: true,
|
package/local-cli.mjs
CHANGED
|
@@ -44,6 +44,45 @@ async function stdinJson() {
|
|
|
44
44
|
return value;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Discovery takes an optional input object. A host that keeps stdin open
|
|
48
|
+
// without writing must not block the skill, so wait only briefly for data.
|
|
49
|
+
export async function optionalStdinJson(idleMs = 200) {
|
|
50
|
+
if (process.stdin.isTTY) return {};
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
let raw = '';
|
|
53
|
+
let settled = false;
|
|
54
|
+
let timer;
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
if (timer) clearTimeout(timer);
|
|
57
|
+
process.stdin.off('data', onData);
|
|
58
|
+
process.stdin.off('end', onEnd);
|
|
59
|
+
process.stdin.off('error', onError);
|
|
60
|
+
process.stdin.pause();
|
|
61
|
+
};
|
|
62
|
+
const finish = (error) => {
|
|
63
|
+
if (settled) return;
|
|
64
|
+
settled = true;
|
|
65
|
+
cleanup();
|
|
66
|
+
if (error) return reject(error);
|
|
67
|
+
if (!raw.trim()) return resolve({});
|
|
68
|
+
let value;
|
|
69
|
+
try { value = JSON.parse(raw); } catch { return reject(new Error('Local input must be valid JSON')); }
|
|
70
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
71
|
+
return reject(new Error('local input must be a JSON object'));
|
|
72
|
+
}
|
|
73
|
+
resolve(value);
|
|
74
|
+
};
|
|
75
|
+
const onData = chunk => { raw += chunk; };
|
|
76
|
+
const onEnd = () => finish();
|
|
77
|
+
const onError = error => finish(error);
|
|
78
|
+
process.stdin.on('data', onData);
|
|
79
|
+
process.stdin.once('end', onEnd);
|
|
80
|
+
process.stdin.once('error', onError);
|
|
81
|
+
timer = setTimeout(() => finish(), idleMs);
|
|
82
|
+
process.stdin.resume();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
47
86
|
const operations = {
|
|
48
87
|
'mutation-consumption-attest': attestMutationConsumption14,
|
|
49
88
|
'mutation-delegate-prepare': preparePreservedMutationSigner,
|
|
@@ -102,7 +141,7 @@ export async function runLocal(args = [], dependencies) {
|
|
|
102
141
|
try {
|
|
103
142
|
const operation = args[0];
|
|
104
143
|
if (operation === 'capabilities' || operation === 'help') {
|
|
105
|
-
validateLocalInput(operation,
|
|
144
|
+
validateLocalInput(operation, await optionalStdinJson());
|
|
106
145
|
process.stdout.write(`${JSON.stringify(capabilitiesFor(args[1]))}\n`); return;
|
|
107
146
|
}
|
|
108
147
|
const handler = operations[operation];
|
package/local-contracts.mjs
CHANGED
|
@@ -101,7 +101,7 @@ const scopeInteraction = object({ type: enumeration(['confirm', 'choice', 'multi
|
|
|
101
101
|
const scopeTask = object({ goal: { ...text, pattern: '\\S', maxLength: 12000 }, workType: enumeration(['implementation', 'documentation', 'calculation', 'merge', 'review', 'decision', 'diagnosis']),
|
|
102
102
|
difficulty: enumeration(['low', 'medium', 'high']), risk: enumeration(['low', 'medium', 'high']), crossModule: { type: 'boolean' }, parallel: { type: 'boolean' }, needParallel: { type: 'boolean' },
|
|
103
103
|
unclear: { type: 'boolean' }, architectureContract: { type: 'boolean' }, explicitAimlockRequested: { type: 'boolean' }, explicitEntroroveRequested: { type: 'boolean' }, userSpecifiedSkills: array(text),
|
|
104
|
-
requirements: { ...array(scopeRequirement, 1), maxItems: 500 }, phase: enumeration(['plan', 'coordinate', 'architecture', 'verify', 'merge', 'decision']), baselineRef: text, interaction: scopeInteraction }, ['goal', 'workType', 'difficulty', 'risk']);
|
|
104
|
+
requirements: { ...array(scopeRequirement, 1), maxItems: 500 }, authorization: record, phase: enumeration(['plan', 'coordinate', 'architecture', 'verify', 'merge', 'decision']), baselineRef: text, interaction: scopeInteraction }, ['goal', 'workType', 'difficulty', 'risk']);
|
|
105
105
|
const probe = { ...step, paths: probePaths, targetPaths: probePaths, task: scopeTask, baselineRef: text, snapshotId: identifier,
|
|
106
106
|
targetSymbols: array(text, 1), contextMapPath: relativePath, importContexts, contextRevisionDigest: sha256,
|
|
107
107
|
contextEvidence: { ...array(object({ path: relativePath, sha256 }, ['path', 'sha256'])), maxItems: 200 }, signerConfigPath: nullable(text),
|
|
@@ -100,8 +100,10 @@ export async function disciplineRepositoryObservation(input, root, { analyze = f
|
|
|
100
100
|
let graph;
|
|
101
101
|
try { graph = roots.length ? await importGraph(root, roots, input, disciplineAnalysisLimits) : { edges: [], dynamicSources: [], complete: true }; }
|
|
102
102
|
catch (error) {
|
|
103
|
-
if (!['ENTROROVE_IMPORT_UNRESOLVED', 'ENTROROVE_IMPORT_INVALID', 'ENTROROVE_IMPORT_UNSUPPORTED', 'ENTROROVE_SCOPE_LIMIT', 'ENTROROVE_BUDGET_SCOPE'].includes(error.code)) throw error;
|
|
104
|
-
|
|
103
|
+
if (!['ENTROROVE_IMPORT_UNRESOLVED', 'ENTROROVE_IMPORT_INVALID', 'ENTROROVE_IMPORT_UNSUPPORTED', 'ENTROROVE_IMPORT_ADDRESS_UNSUPPORTED', 'ENTROROVE_SCOPE_LIMIT', 'ENTROROVE_BUDGET_SCOPE'].includes(error.code)) throw error;
|
|
104
|
+
const address = error.code === 'ENTROROVE_IMPORT_ADDRESS_UNSUPPORTED'
|
|
105
|
+
? { path: error.sourcePath, sha256: error.sourceSha256, selectorDigest: error.selectorDigest } : {};
|
|
106
|
+
graph = { edges: [], dynamicSources: [], complete: false }; unresolved.push({ ...address, reason: error.code, message: error.message });
|
|
105
107
|
}
|
|
106
108
|
for (const [relative, content] of overrides) if (semanticSource(relative) && /\.(?:[cm]?[jt]sx?|vue)$/i.test(relative)) {
|
|
107
109
|
try {
|
package/local-invocations.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { withStateLock } from './local-state-lock.mjs';
|
|
6
|
+
import { servicePrerequisite } from './service-prerequisite.mjs';
|
|
6
7
|
|
|
7
8
|
const SHA = /^[a-f0-9]{64}$/;
|
|
8
9
|
const UUID = /^[a-f0-9-]{36}$/;
|
|
@@ -66,6 +67,11 @@ export async function appendInvocation(value, home = os.homedir()) {
|
|
|
66
67
|
|| !['remote', 'local'].includes(value.kind)) throw new Error('Invalid invocation ledger metadata');
|
|
67
68
|
const record = { schemaVersion: 'entrorove.invocation/1.0', eventId: randomUUID(), at: new Date().toISOString(),
|
|
68
69
|
kind: value.kind, operation: value.operation, requestId: value.requestId, event: value.event };
|
|
70
|
+
if (value.servicePrerequisite !== undefined) {
|
|
71
|
+
const prerequisite = servicePrerequisite({ status: 403 }, value.servicePrerequisite);
|
|
72
|
+
if (value.event !== 'rejected' || !prerequisite) throw new Error('Invalid service prerequisite rejection receipt');
|
|
73
|
+
record.servicePrerequisite = prerequisite;
|
|
74
|
+
}
|
|
69
75
|
if (value.durationMs !== undefined) {
|
|
70
76
|
if (!Number.isSafeInteger(value.durationMs) || value.durationMs < 0) throw new Error('Invalid local invocation duration');
|
|
71
77
|
record.durationMs = value.durationMs;
|
package/local-mutation-files.mjs
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { constants, promises as fs } from 'node:fs';
|
|
1
|
+
import nodeFs, { constants, promises as fs } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { setImmediate as yieldMetadata14 } from 'node:timers/promises';
|
|
5
6
|
import { canonical, sha256 } from './local-integrity.mjs';
|
|
6
7
|
import { readBudgetedProjectFile } from './local-budget-reads.mjs';
|
|
7
8
|
|
|
8
9
|
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
9
10
|
export const mutationFail14 = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
11
|
+
let metadataObservations14 = 0;
|
|
12
|
+
// Observe metadata afresh without waiting for a queued filesystem completion.
|
|
13
|
+
// Yield between bounded groups so timers and signals still run during scans.
|
|
14
|
+
async function lstatMetadata14(target) {
|
|
15
|
+
if (++metadataObservations14 % 128 === 0) await yieldMetadata14();
|
|
16
|
+
return nodeFs.lstatSync(target);
|
|
17
|
+
}
|
|
10
18
|
export function mutationId14(value, field) {
|
|
11
19
|
if (typeof value !== 'string' || !ID.test(value)) mutationFail14('ENTROROVE_ID_INVALID', `${field} must be a safe identifier`);
|
|
12
20
|
return value;
|
|
@@ -46,11 +54,11 @@ export function mutationInside14(root, target, field = 'path') {
|
|
|
46
54
|
async function inspectPath(target, { allowMissing = false, final = 'file', code = 'ENTROROVE_FILE_INVALID' } = {}) {
|
|
47
55
|
const resolved = absolute(target, 'filesystem path');
|
|
48
56
|
const base = path.parse(resolved).root; const parts = path.relative(base, resolved).split(path.sep).filter(Boolean);
|
|
49
|
-
let current = base; const ancestors = [{ path: base, stat: await
|
|
57
|
+
let current = base; const ancestors = [{ path: base, stat: await lstatMetadata14(base) }];
|
|
50
58
|
if (!parts.length) return { target: resolved, stat: ancestors[0].stat, exists: true, ancestors };
|
|
51
59
|
for (const [index, part] of parts.entries()) {
|
|
52
60
|
current = path.join(current, part); let stat;
|
|
53
|
-
try { stat = await
|
|
61
|
+
try { stat = await lstatMetadata14(current); } catch (error) {
|
|
54
62
|
if (error.code === 'ENOENT' && allowMissing) return { target: resolved, exists: false, stat: null, ancestors };
|
|
55
63
|
throw error;
|
|
56
64
|
}
|
|
@@ -71,7 +79,7 @@ async function canonicalDirectoryPath(value, { allowMissing = false, code }) {
|
|
|
71
79
|
for (const [index, part] of parts.entries()) {
|
|
72
80
|
const candidate = path.join(current, part);
|
|
73
81
|
let stat;
|
|
74
|
-
try { stat = await
|
|
82
|
+
try { stat = await lstatMetadata14(candidate); } catch (error) {
|
|
75
83
|
if (error.code === 'ENOENT' && allowMissing) return path.join(current, ...parts.slice(index));
|
|
76
84
|
throw error;
|
|
77
85
|
}
|
|
@@ -90,7 +98,7 @@ async function canonicalDirectoryPath(value, { allowMissing = false, code }) {
|
|
|
90
98
|
}
|
|
91
99
|
async function assertAncestors(item, code = 'ENTROROVE_STATE_CHANGED') {
|
|
92
100
|
for (const ancestor of item.ancestors) {
|
|
93
|
-
const stat = await
|
|
101
|
+
const stat = await lstatMetadata14(ancestor.path);
|
|
94
102
|
if (stat.isSymbolicLink() || !stat.isDirectory() || !sameNode(stat, ancestor.stat)) {
|
|
95
103
|
mutationFail14(code, 'mutation path ancestors changed during filesystem access');
|
|
96
104
|
}
|
package/local-probe.mjs
CHANGED
|
@@ -71,15 +71,19 @@ async function probeHandoffCandidate(input, root, candidate, signingOptions) {
|
|
|
71
71
|
}
|
|
72
72
|
function probeSigningOptions(options) {
|
|
73
73
|
if (!options || typeof options !== 'object' || Array.isArray(options)
|
|
74
|
-
|| Object.keys(options).some(key =>
|
|
75
|
-
|| options.maximumExpiresAt !== undefined && (!Number.isSafeInteger(options.maximumExpiresAt) || options.maximumExpiresAt < 1)
|
|
76
|
-
|
|
74
|
+
|| Object.keys(options).some(key => !['maximumExpiresAt', 'beforeSign'].includes(key))
|
|
75
|
+
|| options.maximumExpiresAt !== undefined && (!Number.isSafeInteger(options.maximumExpiresAt) || options.maximumExpiresAt < 1)
|
|
76
|
+
|| options.beforeSign !== undefined && typeof options.beforeSign !== 'function') {
|
|
77
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Internal probe signing options require a finite deadline or an original bound challenge callback');
|
|
77
78
|
}
|
|
78
|
-
return { maximumExpiresAt: options.maximumExpiresAt };
|
|
79
|
+
return { maximumExpiresAt: options.maximumExpiresAt, beforeSign: options.beforeSign };
|
|
79
80
|
}
|
|
80
81
|
export async function probe(input, options = {}) {
|
|
81
82
|
const signingOptions = probeSigningOptions(options);
|
|
82
83
|
rejectScopePrivileges(input);
|
|
84
|
+
if (signingOptions.beforeSign && Object.hasOwn(input, 'handoffCandidate')) {
|
|
85
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Temporary handoff candidates cannot renew an ordinary scope challenge');
|
|
86
|
+
}
|
|
83
87
|
const root = await scopeRoot(input.repositoryRoot);
|
|
84
88
|
const candidate = await verifiedScopeCandidate(input, root);
|
|
85
89
|
if (candidate && await handoffReadBudget(input)) return probeHandoffCandidate(input, root, candidate, signingOptions);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Durable per-operation limits. These are recovery limits, not an execution
|
|
2
|
+
// permission, and callers cannot enlarge them through request payloads.
|
|
3
|
+
const limits = Object.freeze({ attempt: 2, signing: 2, status: 3, prerequisite: 2 });
|
|
4
|
+
const fail = recovery => Object.assign(new Error('Automatic recovery limit reached; preserve the original task and pending evidence, and report the concrete failure before another attempt'),
|
|
5
|
+
{ code: 'ENTROROVE_RECOVERY_EXHAUSTED', recovery: structuredClone(recovery), automaticRetryAllowed: false });
|
|
6
|
+
export function consumeRecovery(state, kind, eventId) {
|
|
7
|
+
if (!Object.hasOwn(limits, kind)) throw new Error('Unknown recovery boundary');
|
|
8
|
+
const recovery = Object.hasOwn(state, 'recovery') ? state.recovery : { schemaVersion: 'entrorove.recovery-budget/1.0', counts: {} };
|
|
9
|
+
if (!recovery || recovery.schemaVersion !== 'entrorove.recovery-budget/1.0' || !recovery.counts || Array.isArray(recovery.counts)
|
|
10
|
+
|| Object.keys(recovery).some(key => !['schemaVersion', 'counts', 'events'].includes(key))
|
|
11
|
+
|| Object.entries(recovery.counts).some(([key, value]) => !Object.hasOwn(limits, key)
|
|
12
|
+
|| !Number.isSafeInteger(value) || value < 0 || value > limits[key])) {
|
|
13
|
+
throw Object.assign(new Error('Original recovery accounting is invalid'), { code: 'ENTROROVE_RECOVERY_STATE' });
|
|
14
|
+
}
|
|
15
|
+
if (recovery.events && (Array.isArray(recovery.events) || Object.entries(recovery.events).some(([key, value]) =>
|
|
16
|
+
!Object.hasOwn(limits, key) || typeof value !== 'string' || !value || value.length > 256 || !recovery.counts[key]))) {
|
|
17
|
+
throw Object.assign(new Error('Original recovery event is invalid'), { code: 'ENTROROVE_RECOVERY_STATE' });
|
|
18
|
+
}
|
|
19
|
+
if (eventId !== undefined && (typeof eventId !== 'string' || !eventId || eventId.length > 256)) throw new Error('Invalid recovery event');
|
|
20
|
+
if (eventId && recovery.events?.[kind] === eventId) return structuredClone(recovery);
|
|
21
|
+
if ((recovery.counts[kind] ?? 0) >= limits[kind]) throw fail(recovery);
|
|
22
|
+
recovery.counts[kind] = (recovery.counts[kind] ?? 0) + 1;
|
|
23
|
+
if (eventId) recovery.events = { ...recovery.events, [kind]: eventId };
|
|
24
|
+
state.recovery = recovery;
|
|
25
|
+
return structuredClone(recovery);
|
|
26
|
+
}
|
|
27
|
+
export function recoveryFailureFields(error) {
|
|
28
|
+
return error?.code === 'ENTROROVE_RECOVERY_EXHAUSTED' || error?.code === 'ENTROROVE_RECOVERY_STATE'
|
|
29
|
+
? { code: error.code, message: error.message, automaticRetryAllowed: false,
|
|
30
|
+
...(error.recovery ? { recovery: error.recovery } : {}) } : {};
|
|
31
|
+
}
|
|
@@ -21,6 +21,7 @@ import { uniConditionalVariants } from './local-scope-uni-conditional.mjs';
|
|
|
21
21
|
import { createUniBuildInspection } from './local-scope-uni-build.mjs';
|
|
22
22
|
import { createScopeBuildIO } from './local-scope-build-io.mjs';
|
|
23
23
|
import { observeBuildFacts } from './local-scope-build-facts.mjs';
|
|
24
|
+
import { scopeWriteFiles, scopeWriteObservation } from './scope-write-observation.mjs';
|
|
24
25
|
|
|
25
26
|
const execute = promisify(execFile);
|
|
26
27
|
const inspectionFreshness = new WeakMap();
|
|
@@ -247,7 +248,11 @@ function declarationModules(content, from) {
|
|
|
247
248
|
}
|
|
248
249
|
async function resolveImport(root, from, specifier, workspace, input, aliases, configPath, declarations) {
|
|
249
250
|
if (specifier.startsWith('/') || /^[A-Za-z]:/.test(specifier) || specifier.includes('\\') || /[\u0000-\u001f\u007f]/.test(specifier)) {
|
|
250
|
-
|
|
251
|
+
// This is a source expression, not permission to probe an absolute target.
|
|
252
|
+
// Strict dependency inspection still rejects it; whole-repository usage
|
|
253
|
+
// observation can retain this specific semantic uncertainty without I/O.
|
|
254
|
+
throw Object.assign(new Error('Source module address has no supported local projection; no target was inspected'),
|
|
255
|
+
{ code: 'ENTROROVE_IMPORT_ADDRESS_UNSUPPORTED', sourcePath: from, selectorDigest: sha256(specifier) });
|
|
251
256
|
}
|
|
252
257
|
if (/^(?:https?:|file:|data:)/i.test(specifier)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'remote source imports cannot be inspected as local facts');
|
|
253
258
|
if (declarations.has(specifier)) return null;
|
|
@@ -367,7 +372,10 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
367
372
|
catch (error) {
|
|
368
373
|
const missing = error.code === 'ENTROROVE_IMPORT_UNRESOLVED' && specifier.startsWith('.')
|
|
369
374
|
? error.missingCandidates?.filter(candidate => pendingCreates.has(candidate)) : null;
|
|
370
|
-
if (error.code !== 'ENTROROVE_IMPORT_UNRESOLVED')
|
|
375
|
+
if (error.code !== 'ENTROROVE_IMPORT_UNRESOLVED') {
|
|
376
|
+
if (error.code === 'ENTROROVE_IMPORT_ADDRESS_UNSUPPORTED') error.sourceSha256 = read.sha256;
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
371
379
|
unresolvedImports.push({ path: relative, specifier, reason: missing?.length ? 'approved-create-not-present' : 'source-import-unresolved',
|
|
372
380
|
candidatePaths: missing?.length ? missing : error.missingCandidates ?? [] });
|
|
373
381
|
dynamic = true;
|
|
@@ -435,6 +443,13 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
|
|
|
435
443
|
const verifyGraph = inspectionFreshness.get(graph), observedMetadata = graph.workspaceMetadata.map(file => ({ path: file.path, sha256: file.sha256 }));
|
|
436
444
|
inspectionFreshness.set(graph, async () => { await verifyGraph(); await stableScope(root, observedMetadata, input, scopeBuildSourceRead); });
|
|
437
445
|
const observedGit = await observeGitFacts(root, files, baselineRef, input); const history = observedGit.facts;
|
|
446
|
+
let writeScope;
|
|
447
|
+
if (input.scopeWritePaths !== undefined) {
|
|
448
|
+
const writable = scopeWriteFiles(files, input.scopeWritePaths);
|
|
449
|
+
const observed = await observeGitFacts(root, writable, baselineRef, input);
|
|
450
|
+
if (['available', 'head', 'baseline'].some(field => observed.facts[field] !== history[field])) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Write observation lost the full scope Git identity');
|
|
451
|
+
writeScope = scopeWriteObservation(writable, observed, knownFiles);
|
|
452
|
+
}
|
|
438
453
|
const modules = [...new Map(graph.nodes.map(node => [node.boundary.path, node.boundary])).values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
439
454
|
await stableScope(root, files, input);
|
|
440
455
|
await stableScope(root, modules.filter(module => module.manifestPath).map(module => ({ path: module.manifestPath, sha256: module.manifestDigest })), input, scopeBuildSourceRead);
|
|
@@ -445,7 +460,7 @@ export async function collectScopeFacts(root, requested, baselineRef, knownFiles
|
|
|
445
460
|
: sourceLines + files.filter(file => !file.exists).reduce((sum, file) => sum + (knownFiles.find(previous => previous.path === file.path)?.lines ?? 0), 0);
|
|
446
461
|
return { targetPaths: [...new Set(requested)].sort(), fileCount: files.length, files, bytes, sourceLines, tokenEstimate: Math.ceil(bytes / 4), modules,
|
|
447
462
|
crossModule: modules.length > 1, risk: observedScopeRisk(files), importGraph: graph, git: history,
|
|
448
|
-
estimatedChangedLines };
|
|
463
|
+
estimatedChangedLines, ...(writeScope ? { writeScope } : {}) };
|
|
449
464
|
}
|
|
450
465
|
async function contextEvidence(root, graph, input) {
|
|
451
466
|
if (input.contextEvidence === undefined) return;
|
|
@@ -37,8 +37,8 @@ function binding(input, result) {
|
|
|
37
37
|
}
|
|
38
38
|
async function signingChallenge(input, beforeSign, maximumExpiresAt) {
|
|
39
39
|
if (beforeSign === undefined) return { input, maximumExpiresAt };
|
|
40
|
-
if (typeof beforeSign !== 'function' ||
|
|
41
|
-
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Challenge renewal
|
|
40
|
+
if (typeof beforeSign !== 'function' || Object.hasOwn(input, 'handoffCandidate')) {
|
|
41
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Challenge renewal requires an original bound scope observation');
|
|
42
42
|
}
|
|
43
43
|
const fresh = await beforeSign();
|
|
44
44
|
if (!fresh || Object.keys(fresh).sort().join(',') !== 'challenge,maximumExpiresAt'
|