@entrorove/skill 0.1.71 → 0.1.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CAPABILITY-MAP.md +5 -0
  2. package/CHANGELOG.md +47 -0
  3. package/DESIGN-GATES.md +5 -9
  4. package/DESIGN-STAGED-ACCEPTANCE.md +103 -0
  5. package/IDE-EXECUTION.md +25 -4
  6. package/REUSE-DISCIPLINE.md +3 -3
  7. package/SKILL.md +15 -4
  8. package/cli.mjs +8 -4
  9. package/cloud-mutation-authority.mjs +8 -2
  10. package/cloud-task-client.mjs +10 -0
  11. package/cloud-watch-client.mjs +8 -2
  12. package/design-browser-checks.mjs +18 -5
  13. package/design-browser-pages.mjs +96 -0
  14. package/design-browser-state.mjs +1 -0
  15. package/design-browser.mjs +36 -40
  16. package/design-contracts.mjs +4 -1
  17. package/design-declaration.mjs +3 -0
  18. package/design-frameworks.mjs +2 -1
  19. package/design-gate.mjs +3 -0
  20. package/design-operations.mjs +4 -1
  21. package/design-policy.mjs +12 -4
  22. package/design-release.mjs +6 -0
  23. package/design-research-network.mjs +82 -0
  24. package/design-research.mjs +8 -25
  25. package/design-runner.mjs +3 -0
  26. package/design-stage-contracts.mjs +12 -0
  27. package/design-stage.mjs +78 -0
  28. package/design-storage.mjs +63 -0
  29. package/fs-request-trace.mjs +67 -0
  30. package/local-contracts.mjs +1 -1
  31. package/local-discipline-repository.mjs +4 -2
  32. package/local-invocations.mjs +6 -0
  33. package/local-mutation-files.mjs +13 -5
  34. package/local-probe.mjs +20 -7
  35. package/local-recovery-budget.mjs +31 -0
  36. package/local-scope-repository.mjs +18 -3
  37. package/local-scope-signature.mjs +2 -2
  38. package/local-source-execution.mjs +47 -0
  39. package/manifest.json +1 -1
  40. package/mutation-challenge-refresh.mjs +3 -0
  41. package/mutation-client.mjs +17 -5
  42. package/native-client.mjs +4 -1
  43. package/package.json +19 -2
  44. package/release-source-groups.mjs +7 -0
  45. package/release-state.json +70 -53
  46. package/release.mjs +9 -3
  47. package/releases.json +42 -0
  48. package/remote-capability-cache.mjs +20 -0
  49. package/remote-client.mjs +25 -5
  50. package/remote-transport.mjs +14 -0
  51. package/scope-amendment-client.mjs +90 -22
  52. package/scope-amendment-observer.mjs +9 -3
  53. package/scope-amendment-reference.mjs +22 -4
  54. package/scope-amendment-remote.mjs +20 -6
  55. package/scope-amendment-retirement.mjs +4 -2
  56. package/scope-amendment-state.mjs +10 -6
  57. package/scope-context-client.mjs +24 -8
  58. package/scope-context-journal.mjs +13 -6
  59. package/scope-context-remote.mjs +20 -9
  60. package/scope-context-state.mjs +4 -1
  61. package/scope-context-wire.mjs +9 -1
  62. package/scope-evidence-local.mjs +52 -0
  63. package/scope-evidence-remote.mjs +87 -0
  64. package/scope-evidence-wire.mjs +120 -0
  65. package/scope-runner-client.mjs +92 -35
  66. package/scope-runner-journal.mjs +109 -0
  67. package/scope-write-observation.d.mts +4 -0
  68. package/scope-write-observation.mjs +43 -0
  69. package/service-prerequisite.mjs +87 -0
  70. package/task-entry-client.mjs +3 -0
  71. package/workflow-observation-client.mjs +2 -0
  72. package/workflow-observation-remote.mjs +13 -5
@@ -1,3 +1,4 @@
1
+ import { prerequisiteFields } from './service-prerequisite.mjs';
1
2
  import path from 'node:path';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { canonical, sha256 } from './local-integrity.mjs';
@@ -9,17 +10,67 @@ import { scopeAmendmentLocation, readScopeAmendmentState, saveScopeAmendmentStat
9
10
  import { amendmentRemote, scopeAmendmentStatus } from './scope-amendment-remote.mjs';
10
11
  import { observeScopeAmendment } from './scope-amendment-observer.mjs';
11
12
  import { retainScopeAmendmentReference } from './scope-amendment-reference.mjs';
13
+ import { saveLocalScopeEvidence, ensureScopeEvidence, validateAppliedEvidence } from './scope-evidence-remote.mjs';
14
+ import { consumeRecovery, recoveryFailureFields } from './local-recovery-budget.mjs';
12
15
 
13
16
  const schemaVersion = 'entrorove.scope-amendment-connection/1.0';
17
+ const submissionFields = ['evidencePending', 'evidenceDescriptor', 'evidencePrepared', 'evidenceSignature',
18
+ 'attestation', 'attestationDigest', 'attestationRef', 'evidenceResult', 'applied', 'rejection'];
19
+ const signingChallenge = state => (state.signingChallenge ?? state.challenged).challenge;
20
+ function sameChallenge(value, original, location, attemptId, options) {
21
+ const fresh = scopeAmendmentChallenge(value, location, options);
22
+ const changing = new Set(['authorityReceipt', 'attemptId', 'challengeId', 'challenge', 'issuedAt', 'expiresAt']);
23
+ const immutable = challenge => Object.fromEntries(Object.entries(challenge).filter(([field]) => !changing.has(field)));
24
+ if (canonical(immutable(fresh)) !== canonical(immutable(original)) || fresh.attemptId !== attemptId
25
+ || fresh.challenge === original.challenge || fresh.challengeId === original.challengeId) {
26
+ amendmentFail('Fresh amendment challenge changed its original task, signer, step, roles or authority head');
27
+ }
28
+ return fresh;
29
+ }
30
+ async function archiveSigningObservation(location, state) {
31
+ const checksum = sha256(canonical(state)), archive = path.join(location.directory, `${state.attemptId}-${checksum}.json`);
32
+ try { if (canonical(await readMutationJson14(archive)) !== canonical(state)) amendmentFail('Original signing observation history changed'); }
33
+ catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(archive, state, { privateFrom: location.stateRoot }); }
34
+ return checksum;
35
+ }
36
+ export async function renewScopeAmendmentChallenge(location, state, remote) {
37
+ if (submissionFields.some(field => Object.hasOwn(state, field)) || canonical(state.identity) !== canonical(location.identity)
38
+ || state.snapshotStarted !== true || !state.snapshot || state.snapshot.snapshotId !== state.snapshotId
39
+ || state.snapshot.flowId !== location.identity.flowId || state.snapshot.contractDigest !== location.identity.proposalDigest) {
40
+ amendmentFail('Fresh amendment challenge requires the same verified snapshot before any signed evidence or application');
41
+ }
42
+ const original = scopeAmendmentChallenge(state.challenged?.challenge, location, { allowExpired: true });
43
+ if (state.snapshot.stepId !== original.stepId) amendmentFail('Amendment snapshot replaced its original challenge step');
44
+ if (state.pending && (state.pending.stage !== 'signingChallenge' || !state.signingAttemptId)) {
45
+ amendmentFail('Recover the original pending amendment request before signing challenge renewal');
46
+ }
47
+ const retained = state.signingChallenge && sameChallenge(state.signingChallenge.challenge, original, location,
48
+ state.signingAttemptId, { allowExpired: true });
49
+ if (!state.pending && retained && Date.parse(retained.expiresAt) > Date.now()) {
50
+ return retained;
51
+ }
52
+ if (!state.pending && (!state.signingAttemptId || state.signingChallenge)) {
53
+ const archivedDigest = await archiveSigningObservation(location, state);
54
+ consumeRecovery(state, 'signing', state.signingAttemptId ?? state.attemptId);
55
+ delete state.signingChallenge; state.signingAttemptId = randomUUID();
56
+ state.signingRecovery = { archivedDigest, snapshotId: state.snapshotId, requiresSnapshotVerification: true,
57
+ grantsExecutionAuthority: false, recoveredAt: new Date().toISOString() };
58
+ await saveScopeAmendmentState(location, state);
59
+ }
60
+ const issued = await remote('signingChallenge', 'workflow-scope-challenge', { proposalId: location.identity.proposalId,
61
+ proposalDigest: location.identity.proposalDigest, receiverId: location.binding.runnerId, attemptId: state.signingAttemptId });
62
+ return sameChallenge(issued.challenge, original, location, state.signingAttemptId);
63
+ }
14
64
  function validateProposal(status, input) {
15
65
  const proposal = status.proposal;
16
66
  if (!proposal || proposal.proposalId !== input.proposalId || proposal.proposalDigest !== input.proposalDigest
17
67
  || !['pending', 'approved', 'declined'].includes(proposal.approval)) amendmentFail('Current account has no matching original scope proposal');
18
68
  return proposal;
19
69
  }
20
- function validateApplied(output, state, location) {
21
- const effective = output.effectiveAuthority, record = scopeRevisionAuthority(effective?.record, 'entrorove.scope-revision-authority/1.0');
22
- const challenge = scopeAmendmentChallenge(state.challenged.challenge, location, { allowExpired: true }), refresh = challenge.purpose === 'scope-revision-refresh';
70
+ async function validateApplied(output, state, location) {
71
+ const effective = output.effectiveAuthority, version = effective?.record?.schemaVersion === 'entrorove.scope-revision-authority/2.0' ? '2.0' : '1.0';
72
+ const record = scopeRevisionAuthority(effective?.record, `entrorove.scope-revision-authority/${version}`);
73
+ const challenge = scopeAmendmentChallenge(signingChallenge(state), location, { allowExpired: true }), refresh = challenge.purpose === 'scope-revision-refresh';
23
74
  if (output.status !== 'succeeded' || output.grantsExecutionAuthority !== false || output.claimsCompletion !== false
24
75
  || output.scopeRevisionDigest !== sha256(canonical(record)) || effective.revisionDigest !== output.scopeRevisionDigest
25
76
  || output.revision !== effective.revision || record.revision !== effective.revision
@@ -30,21 +81,26 @@ function validateApplied(output, state, location) {
30
81
  || canonical(record.files.map(file => ({ ...file, sha256: null }))) !== canonical(challenge.files.map(file => ({ ...file, sha256: null })))) {
31
82
  amendmentFail('Applied scope amendment lost its signed original authority or exact roles');
32
83
  }
33
- const fact = refresh ? scopeRevisionAuthority(effective.fact, 'entrorove.scope-revision-facts/1.0') : record;
84
+ const factVersion = effective?.fact?.schemaVersion === 'entrorove.scope-revision-facts/2.0' ? '2.0' : '1.0';
85
+ const fact = refresh ? scopeRevisionAuthority(effective.fact, `entrorove.scope-revision-facts/${factVersion}`) : record;
86
+ const compact = fact.schemaVersion.endsWith('/2.0');
87
+ const attestation = compact ? await validateAppliedEvidence(fact, state, location, challenge,
88
+ refresh ? 'scope-refresh' : 'scope-revision') : state.attestation;
34
89
  if (refresh && (fact.scopeRevisionDigest !== output.scopeRevisionDigest || fact.factRevision !== challenge.expectedFactRevision
35
90
  || ['flowId', 'ownerId', 'taskDigest', 'chainDigest'].some(field => fact[field] !== challenge[field]))
36
- || canonical(fact.candidateAttestation) !== canonical(state.attestation)
37
- || fact.scope?.attestationDigest !== sha256(canonical(state.attestation.scopeAttestation))
91
+ || !compact && canonical(fact.candidateAttestation) !== canonical(attestation)
92
+ || fact.scope?.attestationDigest !== sha256(canonical(attestation.scopeAttestation))
93
+ || compact && effective.candidateAttestationDigest !== sha256(canonical(attestation))
38
94
  || effective.factRevision !== (refresh ? fact.factRevision : 0) || output.factRevision !== effective.factRevision) {
39
95
  amendmentFail('Applied scope proof differs from this exact actual signed candidate');
40
96
  }
41
97
  return { schemaVersion, status: 'succeeded', flowId: challenge.flowId, proposalId: challenge.proposalId,
42
98
  proposalDigest: challenge.proposalDigest, scopeRevisionDigest: output.scopeRevisionDigest, revision: output.revision,
43
99
  factRevision: effective.factRevision, grantsExecutionAuthority: false, claimsCompletion: false,
44
- ...(refresh && state.attestation.memberMutation ? { localAction: {
100
+ ...(refresh && attestation.memberMutation ? { localAction: {
45
101
  command: 'npx @entrorove/skill mutation-settle <absolute repositoryRoot>', input: {
46
- flowId: challenge.flowId, taskId: state.attestation.memberMutation.consumptionReceipt.taskId,
47
- stepId: state.attestation.memberMutation.consumptionReceipt.stepId, gatePassPath: location.identity.gatePassPath } } } : {}),
102
+ flowId: challenge.flowId, taskId: attestation.memberMutation.consumptionReceipt.taskId,
103
+ stepId: attestation.memberMutation.consumptionReceipt.stepId, gatePassPath: location.identity.gatePassPath } } } : {}),
48
104
  nextStep: output.nextStep, ...(state.feedback ? { feedback: state.feedback } : {}) };
49
105
  }
50
106
  async function archiveAttempt(location, state) {
@@ -52,27 +108,36 @@ async function archiveAttempt(location, state) {
52
108
  const archive = path.join(location.directory, `${state.attemptId}.json`);
53
109
  try { if (canonical(await readMutationJson14(archive)) !== canonical(state)) amendmentFail('Original scope amendment attempt history changed'); }
54
110
  catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(archive, state, { privateFrom: location.stateRoot }); }
55
- for (const key of Object.keys(state)) if (!['schemaVersion', 'identity'].includes(key)) delete state[key];
111
+ if (!state.applied) consumeRecovery(state, 'attempt', state.attemptId);
112
+ const completed = Boolean(state.applied);
113
+ for (const key of Object.keys(state)) if (!['schemaVersion', 'identity', ...(completed ? [] : ['recovery'])].includes(key)) delete state[key];
56
114
  await saveScopeAmendmentState(location, state);
57
115
  }
58
116
  async function finish(location, state, dependencies) {
59
- if (!state.attestation) {
60
- state.attestation = await observeScopeAmendment(location, state);
61
- state.attestationDigest = sha256(canonical(state.attestation)); await saveScopeAmendmentState(location, state);
117
+ if (!state.evidenceDescriptor && !state.applied) {
118
+ const attestation = state.attestation ?? await observeScopeAmendment(location, state, {
119
+ beforeSign: () => renewScopeAmendmentChallenge(location, state,
120
+ (stage, operation, payload) => amendmentRemote(stage, operation, payload, state, location, dependencies)) });
121
+ const challenge = scopeAmendmentChallenge(signingChallenge(state), location, { allowExpired: true });
122
+ await saveLocalScopeEvidence(attestation, challenge.purpose === 'scope-revision-refresh' ? 'scope-refresh' : 'scope-revision',
123
+ challenge, state, location, saveScopeAmendmentState);
124
+ }
125
+ if (!state.applied) {
126
+ const attestationRef = await ensureScopeEvidence(state, location, dependencies, signingChallenge(state), saveScopeAmendmentState);
127
+ await amendmentRemote('applied', 'workflow-scope-apply', { proposalId: location.identity.proposalId,
128
+ proposalDigest: location.identity.proposalDigest, attestationRef }, state, location, dependencies);
62
129
  }
63
- const applied = state.applied ?? await amendmentRemote('applied', 'workflow-scope-apply', {
64
- proposalId: location.identity.proposalId, proposalDigest: location.identity.proposalDigest,
65
- attestation: state.attestation }, state, location, dependencies);
66
- const result = validateApplied(applied, state, location);
130
+ const applied = state.applied, result = await validateApplied(applied, state, location);
67
131
  await retainScopeAmendmentReference(location, applied); return result;
68
132
  }
69
133
  async function amend(location, dependencies) {
70
134
  const state = await readScopeAmendmentState(location);
135
+ if (state.evidencePending || state.pending?.operation === 'scope-evidence-prepare') return finish(location, state, dependencies);
71
136
  if (state.pending) {
72
137
  const pending = state.pending;
73
138
  await amendmentRemote(pending.stage, pending.input.operation, pending.input.payload, state, location, dependencies);
74
139
  if (pending.stage === 'applied') {
75
- const result = validateApplied(state.applied, state, location);
140
+ const result = await validateApplied(state.applied, state, location);
76
141
  await retainScopeAmendmentReference(location, state.applied); return result;
77
142
  }
78
143
  return finish(location, state, dependencies);
@@ -84,7 +149,8 @@ async function amend(location, dependencies) {
84
149
  requiresOriginalOwnerApproval: true, scopeExpansionAutomaticallyAuthorized: false } };
85
150
  if (state.challenged && !state.applied && !state.rejection) {
86
151
  const challenge = scopeAmendmentChallenge(state.challenged.challenge, location, { allowExpired: true });
87
- if (Date.parse(challenge.expiresAt) > Date.now()) return finish(location, state, dependencies);
152
+ if (Date.parse(challenge.expiresAt) > Date.now() || state.snapshot && state.snapshotStarted === true
153
+ && !submissionFields.some(field => Object.hasOwn(state, field))) return finish(location, state, dependencies);
88
154
  state.rejection = { code: 'ENTROROVE_SCOPE_AMENDMENT_CHALLENGE_EXPIRED', effects: 'none',
89
155
  reason: 'signed-unused-challenge-expired-before-next-post' }; await saveScopeAmendmentState(location, state);
90
156
  }
@@ -95,7 +161,7 @@ async function amend(location, dependencies) {
95
161
  const challenge = scopeAmendmentChallenge(challenged.challenge, location);
96
162
  if (status.effective ? challenge.purpose !== 'scope-revision-refresh'
97
163
  || challenge.authorityRevisionDigest !== status.effective.revisionDigest || challenge.expectedFactRevision !== status.effective.factRevision + 1
98
- || challenge.currentScopeArtifactDigest !== sha256(canonical(status.effective.scope))
164
+ || challenge.currentScopeArtifactDigest !== (status.effective.scopeArtifactDigest ?? sha256(canonical(status.effective.scope)))
99
165
  : challenge.purpose !== 'scope-revision' || challenge.expectedRevision !== proposal.expectedRevision
100
166
  || challenge.previousDigest !== proposal.previousDigest) amendmentFail('Scope amendment challenge has a different original authority head');
101
167
  return finish(location, state, dependencies);
@@ -111,13 +177,15 @@ export async function runScopeAmend(repositoryRoot, input, dependencies) {
111
177
  } catch (error) {
112
178
  const requestId = /^[a-f0-9]{64}$/.test(error?.requestId ?? '') ? error.requestId : undefined;
113
179
  return { schemaVersion, status: 'blocked', flowId: input?.flowId,
180
+ ...prerequisiteFields(error),
114
181
  code: error?.code?.startsWith('ENTROROVE_') ? error.code : 'ENTROROVE_SCOPE_AMENDMENT_PREREQUISITE',
115
182
  message: ['ENTROROVE_SCOPE_AMENDMENT_BINDING', 'ENTROROVE_SCOPE_AMENDMENT_STATE', 'ENTROROVE_SCOPE_AMENDMENT_UNKNOWN',
116
183
  'ENTROROVE_SCOPE_AMENDMENT_NOT_SENT', 'ENTROROVE_SCOPE_AMENDMENT_MUTATION_PENDING'].includes(error?.code) ? error.message
117
184
  : 'Scope amendment stopped; preserve the original proposal, signer, journal and actual evidence.',
118
- ...(requestId ? { requestId } : {}), ...(error?.output ? { output: error.output } : {}),
185
+ ...(requestId ? { requestId } : {}), ...(error?.uploadId ? { uploadId: error.uploadId } : {}), ...(error?.output ? { output: error.output } : {}),
119
186
  ...(error?.feedback ? { feedback: error.feedback } : {}),
120
- ...(error?.outcomeUnknown ? { outcomeUnknown: true, nextAction: { operation: 'scope-amend', requestId,
187
+ ...recoveryFailureFields(error),
188
+ ...(error?.outcomeUnknown ? { outcomeUnknown: true, nextAction: { operation: 'scope-amend', ...(requestId ? { requestId } : { uploadId: error.uploadId }),
121
189
  method: 'GET', reuseOriginalInput: true, repeatPost: false } } : {}) };
122
190
  }
123
191
  }
@@ -21,12 +21,14 @@ async function inspectFiles(location, challenge) {
21
21
  physical.add(identity);
22
22
  }
23
23
  }
24
- export async function observeScopeAmendment(location, state) {
25
- const challenge = scopeAmendmentChallenge(state.challenged.challenge, location);
24
+ export async function observeScopeAmendment(location, state, { beforeSign } = {}) {
25
+ const original = scopeAmendmentChallenge(state.challenged.challenge, location, { allowExpired: beforeSign !== undefined });
26
+ let challenge = original;
26
27
  await inspectFiles(location, challenge);
27
28
  if (!state.snapshotId) { state.snapshotId = `scope-amend-${randomUUID()}`; await saveScopeAmendmentState(location, state); }
28
29
  const input = { repositoryRoot: location.root, home: location.home, flowId: challenge.flowId, stepId: challenge.stepId,
29
30
  snapshotId: state.snapshotId, paths: challenge.files.map(file => file.path).sort(),
31
+ scopeWritePaths: challenge.files.filter(file => file.access !== 'read-only').map(file => file.path).sort(),
30
32
  contractDigest: challenge.proposalDigest, importContexts: location.contextAuthority?.record.importContexts ?? location.binding.importContexts,
31
33
  ...(location.contextAuthority ? { contextRevisionDigest: location.contextAuthority.revisionDigest,
32
34
  contextEvidence: location.contextAuthority.record.evidence } : {}),
@@ -56,7 +58,11 @@ export async function observeScopeAmendment(location, state) {
56
58
  const observed = await attestScopeFacts({ ...input, ownerId: challenge.ownerId, taskDigest: challenge.taskDigest,
57
59
  chainDigest: challenge.chainDigest, repositoryId: challenge.repositoryId, challenge: challenge.challenge,
58
60
  signerConfigPath: location.signerConfigPath }, { repositoryRoot: location.root, facts,
59
- snapshotDigest: facts.snapshotEvidence.snapshotDigest }, { maximumExpiresAt: Date.parse(challenge.expiresAt) });
61
+ snapshotDigest: facts.snapshotEvidence.snapshotDigest }, { maximumExpiresAt: Date.parse(challenge.expiresAt),
62
+ ...(beforeSign === undefined ? {} : { beforeSign: async () => {
63
+ challenge = await beforeSign();
64
+ return { challenge: challenge.challenge, maximumExpiresAt: Date.parse(challenge.expiresAt) };
65
+ } }) });
60
66
  const memberMutation = await scopeAmendmentMutation(location, state, challenge, facts);
61
67
  const core = { schemaVersion: 'entrorove.scope-revision-attestation/1.0', purpose: challenge.purpose,
62
68
  proposalId: challenge.proposalId, proposalDigest: challenge.proposalDigest, expectedRevision: challenge.expectedRevision,
@@ -1,7 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { canonical, sha256 } from './local-integrity.mjs';
3
- import { mutationHome14, privateMutationDirectory14, readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
3
+ import { mutationHome14, mutationFile14, privateMutationDirectory14, readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
4
4
  import { scopeRevisionAuthority, amendmentFail } from './scope-amendment-wire.mjs';
5
+ import { readScopeEvidence } from './scope-evidence-local.mjs';
6
+ import { evidenceReference } from './scope-evidence-wire.mjs';
7
+ import { trustedOrigin } from './remote-transport.mjs';
5
8
 
6
9
  async function referenceLocation(root, home, flowId, revisionDigest) {
7
10
  const stateRoot = path.join(await mutationHome14(home), '.entrorove');
@@ -11,21 +14,36 @@ async function referenceLocation(root, home, flowId, revisionDigest) {
11
14
  export async function retainScopeAmendmentReference(location, output) {
12
15
  const effective = output.effectiveAuthority, target = await referenceLocation(location.root, location.home,
13
16
  location.identity.flowId, output.scopeRevisionDigest);
14
- const value = { schemaVersion: 'entrorove.scope-amendment-reference/1.0', input: {
17
+ const compact = effective.record.schemaVersion === 'entrorove.scope-revision-authority/2.0';
18
+ if (compact) await readScopeEvidence(location, evidenceReference(effective.record.candidateAttestationRef, undefined, location));
19
+ const value = { schemaVersion: compact ? 'entrorove.scope-amendment-reference/2.0' : 'entrorove.scope-amendment-reference/1.0',
20
+ ...(compact ? { apiOrigin: location.apiOrigin } : {}), input: {
15
21
  flowId: location.identity.flowId, proposalId: location.identity.proposalId, proposalDigest: location.identity.proposalDigest }, record: effective.record };
16
22
  await privateMutationDirectory14(target.directory, { privateFrom: target.stateRoot });
17
23
  try { if (canonical(await readMutationJson14(target.file)) !== canonical(value)) amendmentFail('Scope amendment original authority reference changed'); }
18
24
  catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(target.file, value, { privateFrom: target.stateRoot }); }
19
25
  }
26
+ async function readReferenceEvidence(reference, input) {
27
+ const origin = trustedOrigin(reference.apiOrigin), stateRoot = path.join(await mutationHome14(input.home), '.entrorove');
28
+ const base = path.join(stateRoot, 'scope-runners', sha256(`${origin}:${reference.record.flowId}:${input.repositoryRoot}`));
29
+ const file = path.join(base, 'identity', 'binding.json'), item = await mutationFile14(file);
30
+ if (item.stat.size > 1_000_000 || item.stat.nlink !== 1) amendmentFail('Original amendment signer binding is invalid');
31
+ const binding = await readMutationJson14(file);
32
+ if (binding.repositoryRoot !== input.repositoryRoot || binding.apiOrigin !== origin || binding.flowId !== reference.record.flowId) amendmentFail('Original amendment signer binding changed');
33
+ const location = { base, binding };
34
+ await readScopeEvidence(location, evidenceReference(reference.record.candidateAttestationRef, undefined, location));
35
+ }
20
36
  export async function scopeAmendmentRefreshAction(input, pass) {
21
37
  if (!pass.scopeRevisionDigest) return undefined;
22
38
  try {
23
39
  const target = await referenceLocation(input.repositoryRoot, input.home, pass.flowId, pass.scopeRevisionDigest);
24
40
  const reference = await readMutationJson14(target.file);
25
- const record = scopeRevisionAuthority(reference.record, 'entrorove.scope-revision-authority/1.0');
26
- if (reference.schemaVersion !== 'entrorove.scope-amendment-reference/1.0' || sha256(canonical(record)) !== pass.scopeRevisionDigest
41
+ const compact = reference.schemaVersion === 'entrorove.scope-amendment-reference/2.0';
42
+ const record = scopeRevisionAuthority(reference.record, `entrorove.scope-revision-authority/${compact ? '2.0' : '1.0'}`);
43
+ if (!['entrorove.scope-amendment-reference/1.0', 'entrorove.scope-amendment-reference/2.0'].includes(reference.schemaVersion) || sha256(canonical(record)) !== pass.scopeRevisionDigest
27
44
  || record.flowId !== pass.flowId || record.taskDigest !== pass.taskDigest || record.scope.keyId !== pass.memberScopeKeyId
28
45
  || ['flowId', 'proposalId', 'proposalDigest'].some(field => reference.input[field] !== record[field])) amendmentFail('Stored refresh reference has another original authority');
46
+ if (compact) await readReferenceEvidence(reference, input);
29
47
  return { command: 'npx @entrorove/skill scope-amend <absolute repositoryRoot>',
30
48
  input: { ...reference.input, gatePassPath: input.gatePassPath }, resumeOriginalFlow: true, requiredBefore: 'mutation-settle' };
31
49
  } catch {
@@ -7,6 +7,8 @@ import { readInvocations } from './local-invocations.mjs';
7
7
  import { saveScopeAmendmentState } from './scope-amendment-state.mjs';
8
8
  import { amendmentFail } from './scope-amendment-wire.mjs';
9
9
  import { retireMissingChallenge } from './scope-amendment-retirement.mjs';
10
+ import { prerequisiteError, retainPrerequisite, retryKnownPrerequisite } from './service-prerequisite.mjs';
11
+ import { consumeRecovery } from './local-recovery-budget.mjs';
10
12
 
11
13
  const unknown = requestId => Object.assign(new Error('Scope amendment outcome is unknown; recover the original request with GET only'),
12
14
  { code: 'ENTROROVE_SCOPE_AMENDMENT_UNKNOWN', outcomeUnknown: true, requestId });
@@ -16,6 +18,8 @@ async function recover(pending, dependencies) {
16
18
  const response = await requestTransport(origin, `/api/skill/status/${pending.requestId}`, {
17
19
  headers: { authorization: `Bearer ${token}`, ...ideClientHeaders() } });
18
20
  const result = parseTransportResponse(response, 'original scope amendment status');
21
+ const prerequisite = prerequisiteError(response, result, { requestId: pending.requestId });
22
+ if (prerequisite) throw Object.assign(unknown(pending.requestId), { servicePrerequisite: prerequisite.servicePrerequisite });
19
23
  if (response.status === 404 && result?.error === 'request-not-found') {
20
24
  throw Object.assign(unknown(pending.requestId), { requestNotFound: true });
21
25
  }
@@ -25,7 +29,8 @@ async function recover(pending, dependencies) {
25
29
  verifyAutomaticFeedback(result.feedback, pending.requestId, pending.requestId);
26
30
  result.feedback = await acknowledgeAutomaticFeedback(result.feedback, origin, token, ideClientHeaders());
27
31
  return { response, result };
28
- } catch (error) { throw error?.requestNotFound ? error : unknown(pending.requestId); }
32
+ } catch (error) { throw error?.requestNotFound ? error : Object.assign(unknown(pending.requestId),
33
+ error?.servicePrerequisite ? { servicePrerequisite: error.servicePrerequisite } : {}); }
29
34
  }
30
35
  async function notSent(pending, home) {
31
36
  try {
@@ -36,10 +41,14 @@ async function notSent(pending, home) {
36
41
  export async function amendmentRemote(stage, operation, payload, state, location, dependencies) {
37
42
  const input = { flowId: location.identity.flowId, operation, payload }, requestId = buildInvocation('continue', input).requestId;
38
43
  let response, result;
39
- if (state.pending) {
44
+ const existing = Boolean(state.pending);
45
+ if (existing) {
40
46
  if (state.pending.stage !== stage || state.pending.requestId !== requestId || canonical(state.pending.input) !== canonical(input)) {
41
47
  amendmentFail('Recover the original amendment request before another operation');
42
48
  }
49
+ }
50
+ if (existing && !await retryKnownPrerequisite(state, location, saveScopeAmendmentState)) {
51
+ consumeRecovery(state, 'status'); await saveScopeAmendmentState(location, state);
43
52
  if (state.challengeRetirement) await retireMissingChallenge(state, location, dependencies, recover, notSent);
44
53
  try { ({ response, result } = await recover(state.pending, dependencies)); }
45
54
  catch (error) {
@@ -48,16 +57,18 @@ export async function amendmentRemote(stage, operation, payload, state, location
48
57
  ({ response, result } = await recover(state.pending, dependencies));
49
58
  }
50
59
  } else {
51
- state.pending = { stage, operation: 'continue', input, requestId };
52
- await saveScopeAmendmentState(location, state);
60
+ if (!existing) { state.pending = { stage, operation: 'continue', input, requestId };
61
+ await saveScopeAmendmentState(location, state); }
53
62
  try { ({ response, result } = await invokeRemoteResult({ operation: 'continue', input }, dependencies)); }
54
63
  catch (error) {
64
+ if (await retainPrerequisite(error, state, location, saveScopeAmendmentState)) throw error;
55
65
  if (await notSent(state.pending, dependencies.home)) {
66
+ state.rejection = { code: 'ENTROROVE_SCOPE_AMENDMENT_NOT_SENT', effects: 'none', requestId };
56
67
  delete state.pending; await saveScopeAmendmentState(location, state);
57
68
  throw Object.assign(new Error('Scope amendment was not transmitted; repair its prerequisite and reuse the original input'),
58
69
  { code: 'ENTROROVE_SCOPE_AMENDMENT_NOT_SENT', knownBeforeTransmission: true, requestId });
59
70
  }
60
- throw unknown(requestId);
71
+ throw Object.assign(unknown(requestId), error?.servicePrerequisite ? { servicePrerequisite: error.servicePrerequisite } : {});
61
72
  }
62
73
  }
63
74
  if (!result?.output || result.output.requestId !== requestId) throw unknown(requestId);
@@ -80,5 +91,8 @@ export async function scopeAmendmentStatus(flowId, dependencies) {
80
91
  input: { flowId, operation: 'workflow-scope-status', payload: {} } }, dependencies);
81
92
  if (!response.ok || result?.output?.status !== 'succeeded') throw new Error();
82
93
  return result.output.result ?? result.output;
83
- } catch { amendmentFail('Current account cannot read its original live scope revision; no amendment mutation was sent'); }
94
+ } catch (error) {
95
+ if (error?.servicePrerequisite) throw error;
96
+ amendmentFail('Current account cannot read its original live scope revision; no amendment mutation was sent');
97
+ }
84
98
  }
@@ -2,6 +2,7 @@ import { canonical, sha256 } from './local-integrity.mjs';
2
2
  import { buildInvocation, invokeRemoteResult } from './remote-client.mjs';
3
3
  import { scopeRevisionAuthority, amendmentFail } from './scope-amendment-wire.mjs';
4
4
  import { saveScopeAmendmentState } from './scope-amendment-state.mjs';
5
+ import { retainPrerequisite, retryKnownPrerequisite } from './service-prerequisite.mjs';
5
6
 
6
7
  export async function retireMissingChallenge(state, location, dependencies, recover, notSent) {
7
8
  const original = state.pending;
@@ -14,14 +15,15 @@ export async function retireMissingChallenge(state, location, dependencies, reco
14
15
  if (attempt && (attempt.requestId !== call.requestId || canonical(attempt.input) !== canonical(input))) {
15
16
  amendmentFail('Challenge retirement must retain its original preparation envelope');
16
17
  }
17
- if (!attempt) {
18
- attempt = state.challengeRetirement = { requestId: call.requestId, input, state: 'pending' };
18
+ if (!attempt || await retryKnownPrerequisite(state, location, saveScopeAmendmentState, 'challengeRetirement')) {
19
+ if (!attempt) attempt = state.challengeRetirement = { requestId: call.requestId, input, state: 'pending' };
19
20
  await saveScopeAmendmentState(location, state);
20
21
  try {
21
22
  const { result } = await invokeRemoteResult({ operation: 'scope-challenge-retire', input }, dependencies);
22
23
  attempt.output = result.output; attempt.feedback = result.feedback; attempt.state = 'completed';
23
24
  await saveScopeAmendmentState(location, state);
24
25
  } catch (error) {
26
+ if (await retainPrerequisite(error, state, location, saveScopeAmendmentState, 'challengeRetirement')) throw error;
25
27
  if (await notSent(attempt, dependencies.home)) {
26
28
  attempt.state = 'not-sent'; await saveScopeAmendmentState(location, state);
27
29
  }
@@ -10,17 +10,21 @@ import { trustedOrigin } from './remote-transport.mjs';
10
10
  import { boundScopeContexts } from './scope-context-state.mjs';
11
11
 
12
12
  const fail = message => scopeFail('ENTROROVE_SCOPE_AMENDMENT_STATE', message);
13
- const maximumStateBytes = 1_000_000;
13
+ const maximumControlBytes = 1_000_000;
14
+ // Retained scope combines original facts and verified history, not a single
15
+ // signing control. Keep its aggregate bound separate from key/config files.
16
+ const maximumRetainedScopeBytes = 8_000_000;
14
17
  // A journal retains the challenge, S proof, original pending request and full
15
18
  // signed response together. Each signed authority retains its own 1 MB bound.
16
19
  const maximumJournalBytes = 8_000_000;
17
- async function privateItem(file, final = 'file', maximumBytes = maximumStateBytes) {
20
+ async function privateItem(file, final = 'file', maximumBytes = maximumControlBytes) {
18
21
  const value = await mutationFile14(file, { final });
19
22
  if (process.getuid && value.stat.uid !== process.getuid()
20
23
  || process.platform !== 'win32' && value.stat.mode & 0o077
21
- || final === 'file' && (value.stat.nlink !== 1 || value.stat.size > maximumBytes)) {
22
- fail('Scope amendment controls must be bounded private files owned by the current OS account');
24
+ || final === 'file' && value.stat.nlink !== 1) {
25
+ fail('Scope amendment controls must be private single-link files owned by the current OS account');
23
26
  }
27
+ if (final === 'file' && value.stat.size > maximumBytes) fail(`Scope amendment file exceeds the ${maximumBytes} byte limit`);
24
28
  return value;
25
29
  }
26
30
  async function privateText(file) {
@@ -68,7 +72,7 @@ export async function scopeAmendmentLocation(repositoryRoot, input, dependencies
68
72
  if (key.asymmetricKeyType !== 'ed25519' || der.toString('base64') !== binding.publicKey
69
73
  || sha256(der) !== binding.keyId) fail('Original scope signing identity changed');
70
74
  const originalStatePath = path.join(stateRoot, 'scope', sha256(root).slice(0, 32), input.flowId, 'state.json');
71
- await privateItem(originalStatePath);
75
+ await privateItem(originalStatePath, 'file', maximumRetainedScopeBytes);
72
76
  const saved = await readMutationJson14(originalStatePath);
73
77
  const original = await retainedScope({ repositoryRoot: root, home, flowId: input.flowId,
74
78
  stepId: saved.stepId, snapshotId: saved.originalSnapshot?.snapshotId }, root);
@@ -80,7 +84,7 @@ export async function scopeAmendmentLocation(repositoryRoot, input, dependencies
80
84
  const directory = path.join(base, 'amendments', sha256(canonical(input)));
81
85
  await privateMutationDirectory14(directory, { privateFrom: stateRoot });
82
86
  const contextAuthority = await boundScopeContexts(base, binding, dependencies);
83
- return { root, home, stateRoot, directory, file: path.join(directory, 'journal.json'), identity,
87
+ return { root, home, apiOrigin, base, stateRoot, directory, file: path.join(directory, 'journal.json'), identity,
84
88
  binding, key, signerConfigPath, original, contextAuthority };
85
89
  }
86
90
  export async function readScopeAmendmentState(location) {
@@ -1,3 +1,4 @@
1
+ import { prerequisiteFields } from './service-prerequisite.mjs';
1
2
  import path from 'node:path';
2
3
  import { sign } from 'node:crypto';
3
4
  import { canonical } from './local-integrity.mjs';
@@ -9,6 +10,8 @@ import { scopeContextLocation, effectiveScopeContexts, retainContextAuthority,
9
10
  import { contextRemote } from './scope-context-remote.mjs';
10
11
  import { inspectContextEvidence, observeScopeContext, prepareScopeContext } from './scope-context-observer.mjs';
11
12
  import { contextJournal, renewContextChallenge } from './scope-context-journal.mjs';
13
+ import { recoveryFailureFields } from './local-recovery-budget.mjs';
14
+ import { saveLocalScopeEvidence, ensureScopeEvidence, validateAppliedEvidence } from './scope-evidence-remote.mjs';
12
15
 
13
16
  const schemaVersion = 'entrorove.scope-context-connection/1.0';
14
17
  function proposalFor(status, input, location) {
@@ -22,6 +25,9 @@ function proposalFor(status, input, location) {
22
25
  }
23
26
  function waiting(value, flowId) {
24
27
  const status = value.proposal?.approval?.status;
28
+ if (status === 'approved') return { ...value, schemaVersion, status: 'succeeded', flowId,
29
+ grantsExecutionAuthority: false, claimsCompletion: false, nextOperation: 'scope-context',
30
+ nextInput: { action: 'apply', flowId, proposalId: value.proposal.proposalId, proposalDigest: value.proposal.proposalDigest } };
25
31
  return { ...value, schemaVersion, status: status === 'declined' ? 'blocked' : 'needs-input', flowId,
26
32
  grantsExecutionAuthority: false, claimsCompletion: false,
27
33
  nextStep: value.nextStep ?? { operation: 'interaction-answer', requiresOriginalOwnerApproval: true,
@@ -56,8 +62,10 @@ async function review(location, state, dependencies) {
56
62
  async function applied(location, state) {
57
63
  const effective = state.applied.contextAuthority;
58
64
  const challenge = contextChallenge((state.signingChallenge ?? state.challenged).challenge, location, state.proposal, { allowExpired: true });
65
+ const compact = effective?.record?.schemaVersion === 'entrorove.scope-context-authority/2.0';
66
+ if (compact) await validateAppliedEvidence(effective.record, state, location, challenge, 'scope-context');
59
67
  if (state.applied.grantsExecutionAuthority !== false || !effective?.record
60
- || canonical(effective.record.attestation) !== canonical(state.attestation)
68
+ || !compact && canonical(effective.record.attestation) !== canonical(state.attestation)
61
69
  || ['proposalId', 'proposalDigest', 'ownerId', 'taskDigest', 'chainDigest'].some(field => effective.record[field] !== challenge[field])
62
70
  || canonical(effective.record.importContexts) !== canonical(challenge.importContexts)
63
71
  || canonical(effective.record.evidence) !== canonical(challenge.evidence)) contextFail('Applied context authority differs from this exact actual candidate');
@@ -76,14 +84,20 @@ async function apply(location, state, dependencies) {
76
84
  state.proposal = proposal; await saveContextJournal(location, state);
77
85
  if (!state.challenged) await contextRemote('challenged', 'scope-runner-context-challenge', { flowId: location.binding.flowId,
78
86
  proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attemptId: state.attemptId }, state, location, dependencies);
79
- if (!state.attestation) {
87
+ if (!state.evidenceDescriptor) {
88
+ let attestation = state.attestation;
89
+ if (!attestation) {
80
90
  const prepared = await prepareScopeContext(location, state, proposal);
81
- state.attestation = await observeScopeContext(location, state, proposal, prepared,
91
+ attestation = await observeScopeContext(location, state, proposal, prepared,
82
92
  () => renewContextChallenge(location, state, proposal, dependencies));
83
- await saveContextJournal(location, state);
93
+ }
94
+ await saveLocalScopeEvidence(attestation, 'scope-context', (state.signingChallenge ?? state.challenged).challenge,
95
+ state, location, saveContextJournal);
84
96
  }
97
+ const attestationRef = await ensureScopeEvidence(state, location, dependencies,
98
+ (state.signingChallenge ?? state.challenged).challenge, saveContextJournal);
85
99
  await contextRemote('applied', 'scope-runner-context-apply', { flowId: location.binding.flowId,
86
- proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attestation: state.attestation }, state, location, dependencies);
100
+ proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attestationRef }, state, location, dependencies);
87
101
  return applied(location, state);
88
102
  }
89
103
  async function run(location, input, dependencies) {
@@ -96,7 +110,7 @@ async function run(location, input, dependencies) {
96
110
  state.completed = true; await saveContextJournal(location, state);
97
111
  return { schemaVersion, ...status, status: 'succeeded', flowId: input.flowId, grantsExecutionAuthority: false, claimsCompletion: false };
98
112
  } catch (error) {
99
- if (!state.pending) { state.rejection ??= { code: error.code, message: error.message }; await saveContextJournal(location, state); }
113
+ if (!state.pending && !state.evidencePending) { state.rejection ??= { code: error.code, message: error.message }; await saveContextJournal(location, state); }
100
114
  throw error;
101
115
  }
102
116
  }
@@ -109,10 +123,12 @@ export async function runScopeContext(repositoryRoot, input, dependencies) {
109
123
  return await withStateLock(path.join(location.base, 'connection'), () => run(location, input, dependencies));
110
124
  } catch (error) {
111
125
  return { schemaVersion, status: 'blocked', flowId: input?.flowId,
126
+ ...prerequisiteFields(error),
112
127
  code: error?.code?.startsWith('ENTROROVE_') ? error.code : 'ENTROROVE_SCOPE_CONTEXT_PREREQUISITE',
113
128
  message: error?.code?.startsWith('ENTROROVE_') ? error.message : 'Context recovery stopped; preserve original signer, binding, journal and evidence.',
114
- ...(error?.requestId ? { requestId: error.requestId } : {}), ...(error?.output ? { output: error.output } : {}),
129
+ ...(error?.requestId ? { requestId: error.requestId } : {}), ...(error?.uploadId ? { uploadId: error.uploadId } : {}), ...(error?.output ? { output: error.output } : {}),
130
+ ...recoveryFailureFields(error),
115
131
  ...(error?.outcomeUnknown ? { outcomeUnknown: true, nextAction: { operation: 'scope-context', method: 'GET',
116
- requestId: error.requestId, reuseOriginalInput: true, repeatPost: false } } : {}) };
132
+ ...(error.requestId ? { requestId: error.requestId } : { uploadId: error.uploadId }), reuseOriginalInput: true, repeatPost: false } } : {}) };
117
133
  }
118
134
  }
@@ -5,6 +5,7 @@ import { atomicMutationJson14, readMutationJson14 } from './local-mutation-files
5
5
  import { contextFail } from './scope-context-wire.mjs';
6
6
  import { readContextJournal, saveContextJournal } from './scope-context-state.mjs';
7
7
  import { contextRemote } from './scope-context-remote.mjs';
8
+ import { consumeRecovery } from './local-recovery-budget.mjs';
8
9
 
9
10
  export async function archiveContextAttempt(location, state) {
10
11
  if (!state.attemptId) return;
@@ -13,28 +14,34 @@ export async function archiveContextAttempt(location, state) {
13
14
  catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(file, state, { privateFrom: location.stateRoot }); }
14
15
  }
15
16
  function reusableSnapshot(state, input) {
16
- return input.action === 'apply' && state.snapshot && state.challenged && !state.pending && !state.completed && !state.applied
17
+ return input.action === 'apply' && state.snapshot && state.challenged && !state.pending && !state.evidencePending && !state.completed && !state.applied
17
18
  && state.rejection && !state.rejection.requestId && !state.rejection.output;
18
19
  }
19
20
  export async function contextJournal(location, input) {
20
21
  let state = await readContextJournal(location, input);
21
22
  if (!state.previous && reusableSnapshot(state, input)) {
22
23
  await archiveContextAttempt(location, state);
23
- for (const field of ['rejection', 'checked', 'attestation', 'signingChallenge', 'signingAttemptId']) delete state[field];
24
+ consumeRecovery(state, 'attempt', state.signingAttemptId ?? state.attemptId);
25
+ for (const field of ['rejection', 'checked', 'attestation', 'signingChallenge', 'signingAttemptId',
26
+ 'evidenceDescriptor', 'evidenceSignature', 'evidencePrepared', 'attestationRef', 'evidenceResult']) delete state[field];
24
27
  await saveContextJournal(location, state);
25
- } else if (state.previous || !state.pending && !state.applied && (state.rejection
28
+ } else if (state.previous || !state.pending && !state.evidencePending && !state.applied && (state.rejection
26
29
  || input.action === 'status' && state.completed || input.action === 'apply' && state.completed)) {
27
- await archiveContextAttempt(location, state.previous ?? state); state = {};
30
+ const previous = state.previous ?? state;
31
+ await archiveContextAttempt(location, previous);
32
+ const recovery = previous.completed ? undefined : consumeRecovery(previous, 'attempt', previous.attemptId);
33
+ state = { ...(recovery ? { recovery } : {}) };
28
34
  }
29
35
  if (!state.attemptId) {
30
36
  state = { schemaVersion: 'entrorove.scope-context-journal/1.0', localBindingDigest: location.localBindingDigest,
31
- input, attemptId: randomUUID() }; await saveContextJournal(location, state);
37
+ input, attemptId: randomUUID(), ...(state.recovery ? { recovery: state.recovery } : {}) }; await saveContextJournal(location, state);
32
38
  }
33
39
  return state;
34
40
  }
35
41
  export async function renewContextChallenge(location, state, proposal, dependencies) {
36
- if (state.pending) contextFail('Recover the original pending request before any challenge renewal');
42
+ if (state.pending || state.evidencePending) contextFail('Recover the original pending request before any challenge renewal');
37
43
  await archiveContextAttempt(location, state);
44
+ consumeRecovery(state, 'signing', state.signingAttemptId ?? state.attemptId);
38
45
  delete state.signingChallenge;
39
46
  state.signingAttemptId = randomUUID(); await saveContextJournal(location, state);
40
47
  const output = await contextRemote('signingChallenge', 'scope-runner-context-challenge', { flowId: location.binding.flowId,