@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
package/scope-context-client.mjs
CHANGED
|
@@ -1,34 +1,16 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import { canonical
|
|
2
|
+
import { sign } from 'node:crypto';
|
|
3
|
+
import { canonical } from './local-integrity.mjs';
|
|
4
4
|
import { withStateLock } from './local-state-lock.mjs';
|
|
5
|
-
import { atomicMutationJson14, readMutationJson14 } from './local-mutation-files.mjs';
|
|
6
5
|
import { bootstrapCloudMutationAuthority } from './cloud-mutation-authority.mjs';
|
|
7
6
|
import { contextInput, contextFail, retainedContexts, originalContextDigest, contextChallenge } from './scope-context-wire.mjs';
|
|
8
7
|
import { scopeContextLocation, effectiveScopeContexts, retainContextAuthority,
|
|
9
|
-
|
|
8
|
+
saveContextJournal } from './scope-context-state.mjs';
|
|
10
9
|
import { contextRemote } from './scope-context-remote.mjs';
|
|
11
|
-
import { inspectContextEvidence, observeScopeContext } from './scope-context-observer.mjs';
|
|
10
|
+
import { inspectContextEvidence, observeScopeContext, prepareScopeContext } from './scope-context-observer.mjs';
|
|
11
|
+
import { contextJournal, renewContextChallenge } from './scope-context-journal.mjs';
|
|
12
12
|
|
|
13
13
|
const schemaVersion = 'entrorove.scope-context-connection/1.0';
|
|
14
|
-
async function archive(location, state) {
|
|
15
|
-
if (!state.attemptId) return;
|
|
16
|
-
const file = path.join(location.directory, `${state.attemptId}-${sha256(canonical(state))}.json`);
|
|
17
|
-
try { if (canonical(await readMutationJson14(file)) !== canonical(state)) contextFail('Context attempt history changed'); }
|
|
18
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(file, state, { privateFrom: location.stateRoot }); }
|
|
19
|
-
}
|
|
20
|
-
async function journal(location, input) {
|
|
21
|
-
let state = await readContextJournal(location, input);
|
|
22
|
-
if (state.previous || input.action === 'status' && state.completed
|
|
23
|
-
|| input.action === 'apply' && state.completed && !state.applied || state.rejection && !state.pending) {
|
|
24
|
-
await archive(location, state.previous ?? state); state = {};
|
|
25
|
-
}
|
|
26
|
-
if (!state.attemptId) {
|
|
27
|
-
state = { schemaVersion: 'entrorove.scope-context-journal/1.0', localBindingDigest: location.localBindingDigest,
|
|
28
|
-
input, attemptId: randomUUID() }; await saveContextJournal(location, state);
|
|
29
|
-
}
|
|
30
|
-
return state;
|
|
31
|
-
}
|
|
32
14
|
function proposalFor(status, input, location) {
|
|
33
15
|
const proposal = status.proposal;
|
|
34
16
|
if (!proposal || proposal.proposalId !== input.proposalId || proposal.proposalDigest !== input.proposalDigest
|
|
@@ -73,7 +55,7 @@ async function review(location, state, dependencies) {
|
|
|
73
55
|
}
|
|
74
56
|
async function applied(location, state) {
|
|
75
57
|
const effective = state.applied.contextAuthority;
|
|
76
|
-
const challenge = contextChallenge(state.challenged.challenge, location, state.proposal, { allowExpired: true });
|
|
58
|
+
const challenge = contextChallenge((state.signingChallenge ?? state.challenged).challenge, location, state.proposal, { allowExpired: true });
|
|
77
59
|
if (state.applied.grantsExecutionAuthority !== false || !effective?.record
|
|
78
60
|
|| canonical(effective.record.attestation) !== canonical(state.attestation)
|
|
79
61
|
|| ['proposalId', 'proposalDigest', 'ownerId', 'taskDigest', 'chainDigest'].some(field => effective.record[field] !== challenge[field])
|
|
@@ -94,16 +76,18 @@ async function apply(location, state, dependencies) {
|
|
|
94
76
|
state.proposal = proposal; await saveContextJournal(location, state);
|
|
95
77
|
if (!state.challenged) await contextRemote('challenged', 'scope-runner-context-challenge', { flowId: location.binding.flowId,
|
|
96
78
|
proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attemptId: state.attemptId }, state, location, dependencies);
|
|
97
|
-
contextChallenge(state.challenged.challenge, location, proposal);
|
|
98
79
|
if (!state.attestation) {
|
|
99
|
-
|
|
80
|
+
const prepared = await prepareScopeContext(location, state, proposal);
|
|
81
|
+
state.attestation = await observeScopeContext(location, state, proposal, prepared,
|
|
82
|
+
() => renewContextChallenge(location, state, proposal, dependencies));
|
|
83
|
+
await saveContextJournal(location, state);
|
|
100
84
|
}
|
|
101
85
|
await contextRemote('applied', 'scope-runner-context-apply', { flowId: location.binding.flowId,
|
|
102
86
|
proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attestation: state.attestation }, state, location, dependencies);
|
|
103
87
|
return applied(location, state);
|
|
104
88
|
}
|
|
105
89
|
async function run(location, input, dependencies) {
|
|
106
|
-
const state = await
|
|
90
|
+
const state = await contextJournal(location, input);
|
|
107
91
|
if (state.pending) await contextRemote(state.pending.stage, state.pending.operation, state.pending.input, state, location, dependencies);
|
|
108
92
|
try {
|
|
109
93
|
if (input.action === 'review') return await review(location, state, dependencies);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
4
|
+
import { atomicMutationJson14, readMutationJson14 } from './local-mutation-files.mjs';
|
|
5
|
+
import { contextFail } from './scope-context-wire.mjs';
|
|
6
|
+
import { readContextJournal, saveContextJournal } from './scope-context-state.mjs';
|
|
7
|
+
import { contextRemote } from './scope-context-remote.mjs';
|
|
8
|
+
|
|
9
|
+
export async function archiveContextAttempt(location, state) {
|
|
10
|
+
if (!state.attemptId) return;
|
|
11
|
+
const file = path.join(location.directory, `${state.attemptId}-${sha256(canonical(state))}.json`);
|
|
12
|
+
try { if (canonical(await readMutationJson14(file)) !== canonical(state)) contextFail('Context attempt history changed'); }
|
|
13
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(file, state, { privateFrom: location.stateRoot }); }
|
|
14
|
+
}
|
|
15
|
+
function reusableSnapshot(state, input) {
|
|
16
|
+
return input.action === 'apply' && state.snapshot && state.challenged && !state.pending && !state.completed && !state.applied
|
|
17
|
+
&& state.rejection && !state.rejection.requestId && !state.rejection.output;
|
|
18
|
+
}
|
|
19
|
+
export async function contextJournal(location, input) {
|
|
20
|
+
let state = await readContextJournal(location, input);
|
|
21
|
+
if (!state.previous && reusableSnapshot(state, input)) {
|
|
22
|
+
await archiveContextAttempt(location, state);
|
|
23
|
+
for (const field of ['rejection', 'checked', 'attestation', 'signingChallenge', 'signingAttemptId']) delete state[field];
|
|
24
|
+
await saveContextJournal(location, state);
|
|
25
|
+
} else if (state.previous || !state.pending && !state.applied && (state.rejection
|
|
26
|
+
|| input.action === 'status' && state.completed || input.action === 'apply' && state.completed)) {
|
|
27
|
+
await archiveContextAttempt(location, state.previous ?? state); state = {};
|
|
28
|
+
}
|
|
29
|
+
if (!state.attemptId) {
|
|
30
|
+
state = { schemaVersion: 'entrorove.scope-context-journal/1.0', localBindingDigest: location.localBindingDigest,
|
|
31
|
+
input, attemptId: randomUUID() }; await saveContextJournal(location, state);
|
|
32
|
+
}
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
export async function renewContextChallenge(location, state, proposal, dependencies) {
|
|
36
|
+
if (state.pending) contextFail('Recover the original pending request before any challenge renewal');
|
|
37
|
+
await archiveContextAttempt(location, state);
|
|
38
|
+
delete state.signingChallenge;
|
|
39
|
+
state.signingAttemptId = randomUUID(); await saveContextJournal(location, state);
|
|
40
|
+
const output = await contextRemote('signingChallenge', 'scope-runner-context-challenge', { flowId: location.binding.flowId,
|
|
41
|
+
proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attemptId: state.signingAttemptId },
|
|
42
|
+
state, location, dependencies);
|
|
43
|
+
return output.challenge;
|
|
44
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID, sign } from 'node:crypto';
|
|
2
2
|
import { canonical } from './local-integrity.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { scopeBuildSourceRead, scopeTarget } from './local-scope-files.mjs';
|
|
4
4
|
import { snapshotPlan, snapshotVerify } from './local-execution.mjs';
|
|
5
5
|
import { collectScopeFacts } from './local-scope-repository.mjs';
|
|
6
6
|
import { readScopeSnapshot, verifyFreshScopeSnapshot } from './local-scope-snapshot.mjs';
|
|
@@ -20,22 +20,27 @@ export async function inspectContextEvidence(location, contexts, evidencePaths)
|
|
|
20
20
|
for (const relative of [...paths].sort()) {
|
|
21
21
|
const target = await scopeTarget(location.root, relative);
|
|
22
22
|
if (!target.stat.isFile() || target.stat.size > 1_048_576) contextFail('Build evidence must be an actual ordinary bounded repository file');
|
|
23
|
-
const read = await
|
|
23
|
+
const read = await scopeBuildSourceRead(location.root, relative, { repositoryRoot: location.root });
|
|
24
|
+
if (read.bytes > 1_048_576) contextFail('Build evidence exceeds its actual 1 MiB inspection bound');
|
|
24
25
|
evidence.push({ path: relative, sha256: read.sha256 });
|
|
25
26
|
}
|
|
26
27
|
return evidence;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const evidence = await inspectContextEvidence(location, challenge.importContexts, challenge.evidence.map(file => file.path));
|
|
31
|
-
if (canonical(evidence) !== canonical(challenge.evidence)) contextFail('Actual build configuration or loader evidence changed since review');
|
|
32
|
-
if (!state.snapshotId) { state.snapshotId = `context-${randomUUID()}`; await saveContextJournal(location, state); }
|
|
33
|
-
const input = { repositoryRoot: location.root, home: location.home, flowId: challenge.flowId, stepId: challenge.stepId,
|
|
29
|
+
function observationInput(location, state, proposal, challenge) {
|
|
30
|
+
return { repositoryRoot: location.root, home: location.home, flowId: challenge.flowId, stepId: challenge.stepId,
|
|
34
31
|
snapshotId: state.snapshotId, paths: [...location.binding.targetPaths], importContexts: challenge.importContexts,
|
|
35
32
|
contextRevisionDigest: proposal.proposalDigest, contextEvidence: challenge.evidence,
|
|
36
33
|
signerConfigPath: location.signerConfigPath, ownerId: challenge.ownerId,
|
|
37
34
|
task: challenge.task, taskDigest: challenge.taskDigest, chainDigest: challenge.chainDigest,
|
|
38
35
|
repositoryId: challenge.repositoryId, challenge: challenge.challenge };
|
|
36
|
+
}
|
|
37
|
+
export async function prepareScopeContext(location, state, proposal) {
|
|
38
|
+
// An expired server challenge can still identify read-only preparation; it cannot sign or apply.
|
|
39
|
+
const challenge = contextChallenge(state.challenged.challenge, location, proposal, { allowExpired: true });
|
|
40
|
+
const evidence = await inspectContextEvidence(location, challenge.importContexts, challenge.evidence.map(file => file.path));
|
|
41
|
+
if (canonical(evidence) !== canonical(challenge.evidence)) contextFail('Actual build configuration or loader evidence changed since review');
|
|
42
|
+
if (!state.snapshotId) { state.snapshotId = `context-${randomUUID()}`; await saveContextJournal(location, state); }
|
|
43
|
+
const input = observationInput(location, state, proposal, challenge);
|
|
39
44
|
if (!state.snapshot) {
|
|
40
45
|
if (state.snapshotStarted) contextFail('Interrupted context snapshot must be inspected before another observation; preserve its files');
|
|
41
46
|
state.snapshotStarted = true; await saveContextJournal(location, state);
|
|
@@ -48,13 +53,34 @@ export async function observeScopeContext(location, state, proposal) {
|
|
|
48
53
|
const snapshot = await readScopeSnapshot(input, location.root);
|
|
49
54
|
const facts = await collectScopeFacts(location.root, input.paths, undefined, snapshot.known, input);
|
|
50
55
|
facts.snapshotEvidence = await verifyFreshScopeSnapshot(input, location.root, facts);
|
|
56
|
+
return { input, facts, evidence };
|
|
57
|
+
}
|
|
58
|
+
function sameObservation(candidate, input, challenge) {
|
|
59
|
+
const withoutNonce = value => Object.fromEntries(Object.entries(value).filter(([field]) => field !== 'challenge'));
|
|
60
|
+
if (canonical(withoutNonce(candidate.input)) !== canonical(withoutNonce(input))
|
|
61
|
+
|| canonical(candidate.evidence) !== canonical(challenge.evidence)) contextFail('Fresh challenge replaced original preparation identity');
|
|
62
|
+
}
|
|
63
|
+
export async function observeScopeContext(location, state, proposal, prepared, renewChallenge) {
|
|
64
|
+
const candidate = prepared ?? await prepareScopeContext(location, state, proposal);
|
|
65
|
+
let challenge = contextChallenge((state.signingChallenge ?? state.challenged).challenge, location, proposal,
|
|
66
|
+
{ allowExpired: typeof renewChallenge === 'function' });
|
|
67
|
+
const input = observationInput(location, state, proposal, challenge);
|
|
68
|
+
sameObservation(candidate, input, challenge);
|
|
69
|
+
const beforeSign = renewChallenge && (async () => {
|
|
70
|
+
challenge = contextChallenge(await renewChallenge(), location, proposal);
|
|
71
|
+
sameObservation(candidate, observationInput(location, state, proposal, challenge), challenge);
|
|
72
|
+
return { challenge: challenge.challenge, maximumExpiresAt: Date.parse(challenge.expiresAt) };
|
|
73
|
+
});
|
|
74
|
+
const facts = candidate.facts;
|
|
51
75
|
const observed = await attestScopeFacts(input, { repositoryRoot: location.root, facts,
|
|
52
|
-
snapshotDigest: facts.snapshotEvidence.snapshotDigest }, {
|
|
53
|
-
|
|
76
|
+
snapshotDigest: facts.snapshotEvidence.snapshotDigest }, beforeSign ? { beforeSign }
|
|
77
|
+
: { maximumExpiresAt: Date.parse(challenge.expiresAt) });
|
|
78
|
+
if (!observed.attestation || observed.attestation.challenge !== challenge.challenge
|
|
79
|
+
|| canonical(observed.attestation.importContexts) !== canonical(challenge.importContexts)
|
|
54
80
|
|| observed.attestation.contextRevisionDigest !== proposal.proposalDigest
|
|
55
81
|
|| Date.parse(observed.attestation.expiresAt) > Date.parse(challenge.expiresAt)) contextFail('Context candidate requires its actual purpose-bound original S proof');
|
|
56
82
|
const after = await inspectContextEvidence(location, challenge.importContexts, challenge.evidence.map(file => file.path));
|
|
57
|
-
if (canonical(after) !== canonical(evidence)) contextFail('Build evidence changed while observing its actual imports');
|
|
83
|
+
if (canonical(after) !== canonical(candidate.evidence)) contextFail('Build evidence changed while observing its actual imports');
|
|
58
84
|
const core = { schemaVersion: 'entrorove.scope-context-attestation/1.0', challengeId: challenge.challengeId,
|
|
59
85
|
proposalDigest: proposal.proposalDigest, scopeAttestation: observed.attestation };
|
|
60
86
|
return { ...core, signature: sign(null, Buffer.from(canonical(core)), location.key).toString('base64url') };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const SCOPE_CONTEXT_SOURCE_FILES = [
|
|
2
2
|
'scope-context-client.mjs', 'scope-context-observer.mjs', 'scope-context-observation.mjs', 'scope-context-remote.mjs',
|
|
3
|
-
'scope-context-state.mjs', 'scope-context-wire.mjs', 'scope-context-release.mjs',
|
|
3
|
+
'scope-context-state.mjs', 'scope-context-wire.mjs', 'scope-context-journal.mjs', 'scope-context-release.mjs',
|
|
4
4
|
]
|
|
5
5
|
export const scopeContextReleaseNote = {
|
|
6
6
|
zh: '新增首次范围门禁前的构建上下文纠正:原账号审核真实配置和构建证据,原 S 签名完整路径快照,通过服务挑战追加上下文权威记录;保留原 flow、绑定、旧上下文和失败历史,不授予写入或完成权限。',
|