@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.
@@ -1,34 +1,16 @@
1
1
  import path from 'node:path';
2
- import { randomUUID, sign } from 'node:crypto';
3
- import { canonical, sha256 } from './local-integrity.mjs';
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
- readContextJournal, saveContextJournal } from './scope-context-state.mjs';
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
- state.attestation = await observeScopeContext(location, state, proposal); await saveContextJournal(location, state);
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 journal(location, input);
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 { scopeRead, scopeTarget } from './local-scope-files.mjs';
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 scopeRead(location.root, relative, { repositoryRoot: location.root });
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
- export async function observeScopeContext(location, state, proposal) {
29
- const challenge = contextChallenge(state.challenged.challenge, location, proposal);
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 }, { maximumExpiresAt: Date.parse(challenge.expiresAt) });
53
- if (!observed.attestation || canonical(observed.attestation.importContexts) !== canonical(challenge.importContexts)
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、绑定、旧上下文和失败历史,不授予写入或完成权限。',