@entrorove/skill 0.1.22 → 0.1.24

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 (155) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CHANGELOG.md +10 -0
  3. package/SKILL.md +12 -4
  4. package/archguard-runtime-build.mjs +15 -0
  5. package/blueprint-criterion-runner.mjs +110 -0
  6. package/blueprint-criterion-wire.mjs +78 -0
  7. package/calctool-runtime-build.mjs +59 -0
  8. package/calctool-runtime-entry.mjs +92 -0
  9. package/cli.mjs +18 -83
  10. package/local-approved-plan.mjs +74 -15
  11. package/local-archguard-ast.mjs +371 -0
  12. package/local-archguard-checkpoint.mjs +123 -0
  13. package/local-archguard-components.mjs +177 -0
  14. package/local-archguard-contract.mjs +109 -0
  15. package/local-archguard-contracts.mjs +42 -0
  16. package/local-archguard-evaluate.mjs +120 -0
  17. package/local-archguard-language-ast.mjs +165 -0
  18. package/local-archguard-mutation-fence.mjs +28 -0
  19. package/local-archguard-operations.mjs +25 -0
  20. package/local-archguard-scope.mjs +64 -0
  21. package/local-archguard-signing.mjs +52 -0
  22. package/local-archguard-snapshot.mjs +126 -0
  23. package/local-archguard-state.mjs +200 -0
  24. package/local-archguard.mjs +74 -0
  25. package/local-budget-git-attributes.mjs +49 -0
  26. package/local-budget-git-loader.mjs +61 -0
  27. package/local-budget-git-native-attributes.mjs +81 -0
  28. package/local-budget-git-native-darwin.mjs +104 -0
  29. package/local-budget-git-native-linux.mjs +93 -0
  30. package/local-budget-git-native-reads.mjs +88 -0
  31. package/local-budget-git-native-request.mjs +53 -0
  32. package/local-budget-git-native-runtime.mjs +18 -0
  33. package/local-budget-git-native.c +313 -0
  34. package/local-budget-git-native.mjs +109 -0
  35. package/local-budget-git-outcome.mjs +33 -0
  36. package/local-budget-git-scratch-loader.mjs +158 -0
  37. package/local-budget-git-scratch.mjs +112 -0
  38. package/local-budget-git-system-attributes.mjs +44 -0
  39. package/local-budget-git.mjs +92 -0
  40. package/local-budget-grants.mjs +82 -0
  41. package/local-budget-handoff-completion.mjs +70 -0
  42. package/local-budget-handoff-contracts.mjs +53 -0
  43. package/local-budget-handoff.mjs +229 -0
  44. package/local-budget-lock.mjs +55 -0
  45. package/local-budget-reads.mjs +135 -0
  46. package/local-budget-renewal.mjs +86 -34
  47. package/local-budget-source-bindings.mjs +51 -0
  48. package/local-budget-store.mjs +131 -0
  49. package/local-budget-transition.mjs +46 -0
  50. package/local-budget.mjs +185 -0
  51. package/local-calctool-cli.mjs +22 -0
  52. package/local-calctool-contracts.mjs +42 -0
  53. package/local-calctool-runtime.mjs +10 -0
  54. package/local-calctool-storage.mjs +101 -0
  55. package/local-calctool.mjs +103 -0
  56. package/local-cli.mjs +49 -16
  57. package/local-contract-types.mjs +46 -0
  58. package/local-contracts.mjs +124 -0
  59. package/local-credential.mjs +101 -0
  60. package/local-decimal.mjs +85 -0
  61. package/local-evidence-verification.mjs +199 -0
  62. package/local-execution.mjs +115 -304
  63. package/local-final-gate16-bundle.mjs +21 -0
  64. package/local-final-gate16-checks.mjs +88 -0
  65. package/local-final-gate16-contract.mjs +15 -0
  66. package/local-final-gate16-plan.mjs +66 -0
  67. package/local-final-gate16-proof.mjs +75 -0
  68. package/local-final-gate16-release.mjs +17 -0
  69. package/local-final-gate16-state.mjs +70 -0
  70. package/local-final-gate16-worker.mjs +19 -0
  71. package/local-final-gate16.mjs +137 -0
  72. package/local-formula-ast.mjs +97 -0
  73. package/local-formula-engine.mjs +111 -0
  74. package/local-formula-tests.mjs +61 -0
  75. package/local-formula.mjs +25 -125
  76. package/local-integrity.mjs +22 -41
  77. package/local-invocations.mjs +107 -0
  78. package/local-ledger.mjs +2 -7
  79. package/local-merge-archguard-verification.d.mts +44 -0
  80. package/local-merge-archguard-verification.mjs +240 -0
  81. package/local-merge-archguard.mjs +138 -0
  82. package/local-merge-artifacts.mjs +86 -0
  83. package/local-merge-authority.mjs +89 -0
  84. package/local-merge-contracts.mjs +56 -0
  85. package/local-merge-controller.mjs +170 -0
  86. package/local-merge-head.mjs +56 -0
  87. package/local-merge-journal.mjs +77 -0
  88. package/local-merge-mutation.mjs +97 -0
  89. package/local-merge-rollback.mjs +126 -0
  90. package/local-merge-snapshot-rollback.mjs +26 -0
  91. package/local-merge-storage.mjs +103 -0
  92. package/local-merge-targets.mjs +47 -0
  93. package/local-merge-transaction.mjs +133 -0
  94. package/local-mutation-audit.mjs +34 -0
  95. package/local-mutation-authority.mjs +96 -0
  96. package/local-mutation-batch-receipt.mjs +84 -0
  97. package/local-mutation-batch.mjs +143 -0
  98. package/local-mutation-binding.mjs +86 -0
  99. package/local-mutation-files.mjs +245 -0
  100. package/local-mutation-pass-store.mjs +63 -0
  101. package/local-mutation-snapshot.mjs +200 -0
  102. package/local-mutation-wire.mjs +89 -0
  103. package/local-probe.mjs +133 -11
  104. package/local-scope-candidate.mjs +28 -0
  105. package/local-scope-context.mjs +28 -0
  106. package/local-scope-files.mjs +106 -0
  107. package/local-scope-git-attributes.mjs +200 -0
  108. package/local-scope-git-comparison.mjs +8 -0
  109. package/local-scope-git-history.mjs +72 -0
  110. package/local-scope-git-reads.mjs +66 -0
  111. package/local-scope-git.mjs +124 -0
  112. package/local-scope-repository.mjs +249 -0
  113. package/local-scope-signature.mjs +70 -0
  114. package/local-scope-snapshot.mjs +39 -0
  115. package/local-scope-state.mjs +204 -0
  116. package/local-scope-workspaces.mjs +83 -0
  117. package/local-swarm-authority.mjs +143 -0
  118. package/local-swarm-baseline.mjs +32 -0
  119. package/local-swarm-completion.mjs +102 -0
  120. package/local-swarm-contracts.mjs +47 -0
  121. package/local-swarm-mutation.mjs +142 -0
  122. package/local-swarm-priority.mjs +237 -0
  123. package/local-swarm-state.mjs +102 -0
  124. package/local-swarm-storage.mjs +135 -0
  125. package/local-swarm.mjs +179 -0
  126. package/local-task-handoff-completion-wire.mjs +71 -0
  127. package/local-task-handoff-contracts.mjs +66 -0
  128. package/local-task-handoff-fence.mjs +478 -0
  129. package/local-task-handoff-observation.mjs +38 -0
  130. package/local-task-handoff-overlay-wire.mjs +94 -0
  131. package/local-task-handoff-read-authority.mjs +236 -0
  132. package/local-task-handoff-storage.mjs +73 -0
  133. package/local-task-handoff-write-archive.mjs +80 -0
  134. package/local-task-handoff-write-constraints.mjs +66 -0
  135. package/local-task-handoff-write-receipts.mjs +99 -0
  136. package/local-task-handoff-write-wire.mjs +99 -0
  137. package/local-task-routing.mjs +202 -22
  138. package/manifest.json +1 -1
  139. package/package.json +153 -3
  140. package/release-state.json +153 -17
  141. package/release.mjs +238 -4
  142. package/releases.json +12 -0
  143. package/remote-client.mjs +127 -36
  144. package/remote-transport.mjs +80 -0
  145. package/review-client.mjs +38 -0
  146. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  147. package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
  148. package/runtime/git-native-darwin-v1.mjs +5 -0
  149. package/runtime/git-native-linux-v1.mjs +5 -0
  150. package/task-message-cli.mjs +47 -0
  151. package/task-message-entry.mjs +159 -0
  152. package/workflow-local-client.mjs +35 -0
  153. package/workflow-local-contracts.mjs +58 -0
  154. package/workflow-local-request.mjs +53 -0
  155. package/workflow-local-runner.mjs +350 -0
@@ -0,0 +1,58 @@
1
+ import { array, enumeration, integer, ledgerIdentifier, object, relativePath, sha256 } from './local-contract-types.mjs';
2
+
3
+ const exact = properties => object(properties, Object.keys(properties));
4
+ const boundedArray = (items, minimum, maximum) => ({ ...array(items, minimum), maxItems: maximum });
5
+ const id = ledgerIdentifier;
6
+ const absolutePath = { type: 'string', minLength: 2, maxLength: 4096, pattern: '^/[^\\u0000-\\u001f\\u007f]+$' };
7
+ const timestamp = { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$' };
8
+ const uuid = { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' };
9
+ const shortText = { type: 'string', minLength: 1, maxLength: 4096, pattern: '\\S' };
10
+ const authorityReceipt = exact({ schemaVersion: { const: 'entrorove.workflow-authority-receipt/1.0' },
11
+ keyId: sha256, payloadDigest: sha256, signature: { type: 'string', pattern: '^[A-Za-z0-9_-]{86}$' } });
12
+ const manifestFile = exact({ path: { ...relativePath, maxLength: 500 }, sha256 });
13
+ const testEntry = exact({ testId: id, path: { ...relativePath, maxLength: 500 } });
14
+ const executionPolicy = exact({ executable: absolutePath, executableSha256: sha256,
15
+ args: boundedArray({ type: 'string', maxLength: 4096, pattern: '^[^\\u0000]*$' }, 0, 128),
16
+ environment: { type: 'object', maxProperties: 64, propertyNames: { pattern: '^[A-Za-z_][A-Za-z0-9_]*$' },
17
+ additionalProperties: { type: 'string', maxLength: 16_384, pattern: '^[^\\u0000]*$' } },
18
+ timeoutMs: integer(1, 300_000), maxOutputBytes: integer(1, 1024 * 1024), requiredExitCode: { const: 0 },
19
+ });
20
+ const baseline = exact({ schemaVersion: { const: 'validator.golden-baseline/1.0' }, baselineId: id,
21
+ source: exact({ kind: enumeration(['repository-commit', 'artifact', 'approved-record']), locator: shortText, digestSha256: sha256 }),
22
+ version: id, frozen: { const: true }, frozenAt: timestamp, frozenBy: id, testsSha256: sha256,
23
+ });
24
+ const brainFields = ['planSha256', 'reportSha256', 'checksSha256', 'scopeContractSha256', 'snapshotSha256', 'adapterSha256'];
25
+ const contracts = object({
26
+ brain: exact({ schemaVersion: { const: 'brain.frozen-runner/1.0' }, planId: uuid, reportId: uuid,
27
+ ...Object.fromEntries(brainFields.map(field => [field, sha256])) }),
28
+ aimlock: exact({ goalId: id, scopeContractSha256: sha256, snapshotSha256: sha256 }),
29
+ blueprint: exact({ blueprintId: id, acceptanceReportSha256: sha256 }),
30
+ archguard: exact({ contractSha256: sha256, ledgerSha256: sha256, driftStatus: enumeration(['green', 'yellow', 'red']) }),
31
+ });
32
+ contracts.anyOf = [{ required: ['aimlock'] }, { required: ['brain'] }];
33
+ export const WORKFLOW_EXECUTION_PLAN_SCHEMA = exact({ schemaVersion: { const: 'validator.execution-plan/1.0' }, frozen: { const: true },
34
+ planId: id, validationRunId: id, memberId: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$' },
35
+ chainId: { type: 'string', pattern: '^chn-[0-9a-f-]{36}$' }, artifactSha256: sha256,
36
+ files: boundedArray(manifestFile, 1, 512), tests: boundedArray(testEntry, 1, 512), policy: executionPolicy,
37
+ goldenBaseline: baseline, contracts,
38
+ });
39
+ export const WORKFLOW_ACCEPTED_PLAN_SCHEMA = exact({ schemaVersion: { const: 'entrorove.workflow-local-plan/1.0' },
40
+ planId: id, flowId: id, ownerId: id, taskDigest: sha256, chainDigest: sha256, chainRevision: integer(1),
41
+ acceptedAt: timestamp, expiresAt: timestamp, runnerKeyId: sha256,
42
+ steps: boundedArray(exact({ stepId: id, sequence: integer(1, 128),
43
+ dependsOn: { ...boundedArray(id, 0, 128), uniqueItems: true }, executionPlan: WORKFLOW_EXECUTION_PLAN_SCHEMA,
44
+ executionPlanSha256: sha256 }), 1, 128), authorityReceipt,
45
+ });
46
+ WORKFLOW_ACCEPTED_PLAN_SCHEMA.properties.blueprintBinding = exact({
47
+ schemaVersion: { const: 'entrorove.blueprint-plan-binding/1.0' }, blueprintId: id, revision: integer(0),
48
+ blueprintSha256: { type: 'string', pattern: '^sha256:[0-9a-f]{64}$' }, machineTasksDigest: sha256,
49
+ });
50
+ export const WORKFLOW_LOCAL_LEASE_SCHEMA = exact({ schemaVersion: { const: 'entrorove.workflow-local-lease/1.0' },
51
+ leaseId: id, planId: id, planDigest: sha256, flowId: id, ownerId: id, taskDigest: sha256, chainDigest: sha256,
52
+ chainRevision: integer(1), stepId: id, sequence: integer(1, 128), generation: integer(1), issuedAt: timestamp, expiresAt: timestamp,
53
+ verifiedDependencies: { ...boundedArray(exact({ stepId: id, evidenceDigest: sha256 }), 0, 128), uniqueItems: true }, authorityReceipt,
54
+ });
55
+ export const WORKFLOW_LOCAL_OPERATION_SCHEMA = object({ repositoryRoot: absolutePath, home: absolutePath,
56
+ acceptedPlan: WORKFLOW_ACCEPTED_PLAN_SCHEMA, lease: WORKFLOW_LOCAL_LEASE_SCHEMA, stepId: id,
57
+ approvalPath: absolutePath, signerConfigPath: absolutePath,
58
+ }, ['repositoryRoot', 'acceptedPlan', 'lease', 'stepId', 'approvalPath', 'signerConfigPath']);
@@ -0,0 +1,53 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { withStateLock } from './local-state-lock.mjs';
4
+ import { buildInvocation } from './remote-client.mjs';
5
+ import { canonical, sha256 } from './local-integrity.mjs';
6
+ import { context, regular, atomic } from './local-task-handoff-storage.mjs';
7
+
8
+ function original(output, requestId) {
9
+ if (!output || output.requestId !== requestId || output.schemaVersion !== 'entrorove.skill.response/1.0'
10
+ || output.status !== 'reported' || output.engineeringAuthorization !== 'cloud-required') {
11
+ throw new Error(`Original execution authority is unavailable; reconcile requestId=${requestId} without a new attempt`);
12
+ }
13
+ return output;
14
+ }
15
+ export async function managedExecutionAuthority(input, task, client) {
16
+ const ctx = await context({ ...input, flowId: task.flowId }, true);
17
+ const identity = { taskId: task.taskId, flowId: task.flowId, ownerId: task.ownerId,
18
+ leaseId: input.lease.leaseId, generation: input.lease.generation, leaseDigest: sha256(canonical(input.lease)) };
19
+ const file = path.join(path.dirname(ctx.file), `workflow-request-${sha256(canonical(identity))}.json`);
20
+ return withStateLock(file, async () => {
21
+ let record;
22
+ try {
23
+ const { bytes, stat } = await regular(file);
24
+ if (stat.nlink !== 1 || (stat.mode & 0o777) !== 0o600
25
+ || (typeof process.getuid === 'function' && stat.uid !== process.getuid())) throw new Error('Original workflow request must be an owned private file');
26
+ record = JSON.parse(bytes.toString('utf8'));
27
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
28
+ if (record) {
29
+ if (record.schemaVersion !== 'entrorove.workflow-local-request/1.0' || canonical(record.identity) !== canonical(identity)
30
+ || typeof record.attempt?.attemptNonce !== 'string' || Object.keys(record).sort().join('|') !== 'attempt|identity|requestId|schemaVersion') {
31
+ throw new Error('Original workflow request binding is invalid; do not create a replacement execution nonce');
32
+ }
33
+ const { requestId } = buildInvocation('handoff-execution-authority', record.attempt);
34
+ const { attemptNonce: _nonce, ...binding } = record.attempt;
35
+ if (requestId !== record.requestId || canonical(binding)
36
+ !== canonical({ taskId: task.taskId, flowId: task.flowId, leaseId: identity.leaseId, generation: identity.generation })) {
37
+ throw new Error('Original execution request lost its frozen task, lease or generation');
38
+ }
39
+ return { attempt: record.attempt, output: original(await client.status(record.requestId), record.requestId) };
40
+ }
41
+ const attempt = { taskId: task.taskId, flowId: task.flowId, leaseId: identity.leaseId,
42
+ generation: identity.generation, attemptNonce: randomUUID() };
43
+ const { requestId } = buildInvocation('handoff-execution-authority', attempt);
44
+ await atomic(file, { schemaVersion: 'entrorove.workflow-local-request/1.0', identity, attempt, requestId });
45
+ let output;
46
+ try { output = await client.invoke('handoff-execution-authority', attempt); }
47
+ catch (error) {
48
+ if (!error?.outcomeUnknown || error.requestId !== requestId) throw error;
49
+ output = await client.status(requestId);
50
+ }
51
+ return { attempt, output: original(output, requestId) };
52
+ });
53
+ }
@@ -0,0 +1,350 @@
1
+ import { createHash, createPrivateKey, createPublicKey, randomUUID, sign, verify } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import * as approvedRunner from './local-approved-plan.mjs';
6
+ import { withStateLock } from './local-state-lock.mjs';
7
+ import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
8
+ import { beginTaskHandoffExecution, finishTaskHandoffExecution, readTaskHandoffFence, withTaskHandoffReadFence } from './local-task-handoff-fence.mjs';
9
+ import { WORKFLOW_LOCAL_OPERATION_SCHEMA } from './workflow-local-contracts.mjs';
10
+
11
+ export const WORKFLOW_LOCAL_PLAN_SCHEMA = 'entrorove.workflow-local-plan/1.0';
12
+ export const WORKFLOW_LOCAL_LEASE_SCHEMA = 'entrorove.workflow-local-lease/1.0';
13
+ export const WORKFLOW_LOCAL_RESULT_SCHEMA = 'entrorove.workflow-local-result/1.0';
14
+ const STATE_SCHEMA = 'entrorove.workflow-local-state/1.0';
15
+ const AUTHORITY_SCHEMA = 'entrorove.workflow-authority-receipt/1.0';
16
+ const BINDING_SCHEMA = 'entrorove.workflow-local-binding/1.0';
17
+ const SHA = /^[0-9a-f]{64}$/;
18
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
19
+ const MAX_CONTROL_BYTES = 1024 * 1024;
20
+ const MAX_STATE_BYTES = 8 * 1024 * 1024;
21
+ const MAX_PLAN_MS = 86_400_000;
22
+ const MAX_LEASE_MS = 600_000;
23
+
24
+ function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
25
+ function sha(value) { return createHash('sha256').update(value).digest('hex'); }
26
+ function canonical(value) {
27
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value);
28
+ if (typeof value === 'number' && Number.isFinite(value)) return JSON.stringify(value);
29
+ if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
30
+ if (!value || typeof value !== 'object' || ![null, Object.prototype].includes(Object.getPrototypeOf(value))) fail('ENTROROVE_WORKFLOW_JSON', 'workflow values must be finite JSON');
31
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
32
+ }
33
+ function exact(value, fields, code, label) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join('|') !== [...fields].sort().join('|')) fail(code, `${label} has invalid fields`);
35
+ return value;
36
+ }
37
+ function safeId(value, label) { if (typeof value !== 'string' || !ID.test(value)) fail('ENTROROVE_WORKFLOW_BINDING', `${label} must be a safe identifier`); return value; }
38
+ function unsigned(value) { return Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'authorityReceipt')); }
39
+ function positive(value) { return Number.isSafeInteger(value) && value > 0; }
40
+ function publicKey(encoded, code) {
41
+ try {
42
+ if (typeof encoded !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) fail(code, 'configured public key is invalid');
43
+ const bytes = Buffer.from(encoded, 'base64'); const key = createPublicKey({ key: bytes, format: 'der', type: 'spki' });
44
+ if (key.asymmetricKeyType !== 'ed25519' || key.export({ format: 'der', type: 'spki' }).toString('base64') !== encoded) fail(code, 'configured public key must be canonical Ed25519 SPKI');
45
+ return { key, keyId: sha(bytes), encoded };
46
+ } catch { fail(code, 'configured public key must be canonical Ed25519 SPKI'); }
47
+ }
48
+ function verifyAuthority(value, configured) {
49
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('ENTROROVE_WORKFLOW_AUTHORITY', 'signed workflow authority must be an object');
50
+ const payload = unsigned(value); const receipt = exact(value.authorityReceipt,
51
+ ['schemaVersion', 'keyId', 'payloadDigest', 'signature'], 'ENTROROVE_WORKFLOW_AUTHORITY', 'authority receipt');
52
+ const bytes = Buffer.from(canonical(payload));
53
+ const signature = typeof receipt.signature === 'string' ? Buffer.from(receipt.signature, 'base64url') : Buffer.alloc(0);
54
+ if (bytes.length > MAX_CONTROL_BYTES || receipt.schemaVersion !== AUTHORITY_SCHEMA || receipt.keyId !== configured.keyId
55
+ || receipt.payloadDigest !== sha(bytes) || typeof receipt.signature !== 'string' || !/^[A-Za-z0-9_-]{86}$/.test(receipt.signature)
56
+ || signature.length !== 64 || signature.toString('base64url') !== receipt.signature
57
+ || !verify(null, bytes, configured.key, signature)) fail('ENTROROVE_WORKFLOW_AUTHORITY', 'workflow authority signature or payload binding is invalid');
58
+ return payload;
59
+ }
60
+ function lifetime(start, end, max, now, permitExpired = false) {
61
+ const issued = Date.parse(start); const expires = Date.parse(end);
62
+ if (typeof start !== 'string' || typeof end !== 'string' || !Number.isFinite(issued) || !Number.isFinite(expires)
63
+ || issued > now || expires <= issued || expires - issued > max || (!permitExpired && expires <= now)) fail('ENTROROVE_WORKFLOW_EXPIRED', 'workflow authority is expired or has invalid issuance');
64
+ }
65
+ function validatePlan(plan) {
66
+ exact(plan, ['schemaVersion', 'planId', 'flowId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision', 'acceptedAt', 'expiresAt', 'runnerKeyId', 'steps',
67
+ ...(plan.blueprintBinding ? ['blueprintBinding'] : [])], 'ENTROROVE_WORKFLOW_PLAN_FIELDS', 'accepted plan');
68
+ if (plan.blueprintBinding) {
69
+ const binding = exact(plan.blueprintBinding, ['schemaVersion', 'blueprintId', 'revision', 'blueprintSha256', 'machineTasksDigest'],
70
+ 'ENTROROVE_WORKFLOW_BLUEPRINT_BINDING', 'Blueprint binding');
71
+ safeId(binding.blueprintId, 'blueprintId');
72
+ if (binding.schemaVersion !== 'entrorove.blueprint-plan-binding/1.0' || !Number.isSafeInteger(binding.revision) || binding.revision < 0
73
+ || !/^sha256:[0-9a-f]{64}$/.test(binding.blueprintSha256) || !SHA.test(binding.machineTasksDigest)) {
74
+ fail('ENTROROVE_WORKFLOW_BLUEPRINT_BINDING', 'Blueprint plan binding is invalid');
75
+ }
76
+ }
77
+ if (plan.schemaVersion !== WORKFLOW_LOCAL_PLAN_SCHEMA || !SHA.test(plan.taskDigest) || !SHA.test(plan.chainDigest)
78
+ || !SHA.test(plan.runnerKeyId) || !positive(plan.chainRevision) || !Array.isArray(plan.steps) || !plan.steps.length || plan.steps.length > 128) fail('ENTROROVE_WORKFLOW_PLAN_FIELDS', 'accepted plan contract is invalid');
79
+ for (const field of ['planId', 'flowId', 'ownerId']) safeId(plan[field], field);
80
+ const seen = new Set();
81
+ for (const [index, step] of plan.steps.entries()) {
82
+ exact(step, ['stepId', 'sequence', 'dependsOn', 'executionPlan', 'executionPlanSha256'], 'ENTROROVE_WORKFLOW_PLAN_STEPS', 'accepted step');
83
+ safeId(step.stepId, 'stepId');
84
+ if (step.sequence !== index + 1 || seen.has(step.stepId) || !Array.isArray(step.dependsOn) || new Set(step.dependsOn).size !== step.dependsOn.length
85
+ || step.dependsOn.some(id => !seen.has(id))) fail('ENTROROVE_WORKFLOW_PLAN_STEPS', 'accepted steps must have unique ordered identities and earlier dependencies');
86
+ if (!SHA.test(step.executionPlanSha256) || sha(canonical(step.executionPlan) + '\n') !== step.executionPlanSha256) fail('ENTROROVE_WORKFLOW_PLAN_HASH', 'frozen execution plan SHA-256 does not match its bytes');
87
+ if (step.executionPlan?.schemaVersion !== 'validator.execution-plan/1.0' || step.executionPlan.frozen !== true
88
+ || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(step.executionPlan.memberId) || !/^chn-[0-9a-f-]{36}$/.test(step.executionPlan.chainId)) fail('ENTROROVE_WORKFLOW_PLAN_FIELDS', 'execution plan must have compatible Validator member and chain identifiers');
89
+ seen.add(step.stepId);
90
+ }
91
+ return plan;
92
+ }
93
+ function validateLease(lease, plan, stepId) {
94
+ exact(lease, ['schemaVersion', 'leaseId', 'planId', 'planDigest', 'flowId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision',
95
+ 'stepId', 'sequence', 'generation', 'issuedAt', 'expiresAt', 'verifiedDependencies'], 'ENTROROVE_WORKFLOW_BINDING', 'local lease');
96
+ safeId(lease.leaseId, 'leaseId'); safeId(stepId, 'stepId');
97
+ const step = plan.steps.find(item => item.stepId === stepId);
98
+ if (!step || lease.schemaVersion !== WORKFLOW_LOCAL_LEASE_SCHEMA || lease.stepId !== stepId || lease.sequence !== step.sequence
99
+ || lease.planDigest !== sha(canonical(plan)) || !positive(lease.generation)
100
+ || ['planId', 'flowId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision'].some(key => lease[key] !== plan[key])) fail('ENTROROVE_WORKFLOW_BINDING', 'lease does not match the exact original plan, task, chain, owner and step');
101
+ if (!Array.isArray(lease.verifiedDependencies) || lease.verifiedDependencies.length !== step.dependsOn.length) fail('ENTROROVE_WORKFLOW_DEPENDENCY', 'cloud verified dependency receipts are required');
102
+ const dependencies = lease.verifiedDependencies.map(item => {
103
+ exact(item, ['stepId', 'evidenceDigest'], 'ENTROROVE_WORKFLOW_DEPENDENCY', 'verified dependency');
104
+ if (!step.dependsOn.includes(item.stepId) || !SHA.test(item.evidenceDigest)) fail('ENTROROVE_WORKFLOW_DEPENDENCY', 'cloud verified dependency proof does not match the accepted step');
105
+ return item.stepId;
106
+ });
107
+ if (new Set(dependencies).size !== dependencies.length) fail('ENTROROVE_WORKFLOW_DEPENDENCY', 'cloud verified dependencies must be unique');
108
+ return step;
109
+ }
110
+ async function realDirectory(value, code) {
111
+ if (typeof value !== 'string' || !path.isAbsolute(value)) fail(code, 'directory must be absolute');
112
+ const stat = await fs.lstat(value);
113
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail(code, 'directory must be a real directory');
114
+ return fs.realpath(value);
115
+ }
116
+ async function privateHierarchy(home, parts, create = true) {
117
+ let directory = home;
118
+ for (const part of parts) {
119
+ directory = path.join(directory, part);
120
+ if (create) await fs.mkdir(directory, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
121
+ let stat;
122
+ try { stat = await fs.lstat(directory); } catch (error) {
123
+ if (!create && error.code === 'ENOENT') return path.join(home, ...parts);
124
+ throw error;
125
+ }
126
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (typeof process.getuid === 'function' && stat.uid !== process.getuid())) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'workflow state directory must be real and owned');
127
+ if (create) await fs.chmod(directory, 0o700);
128
+ else if ((stat.mode & 0o777) !== 0o700) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'existing workflow state directory must be private');
129
+ }
130
+ return directory;
131
+ }
132
+ async function external(root, file) {
133
+ if (typeof file !== 'string' || !path.isAbsolute(file)) fail('ENTROROVE_WORKFLOW_SIGNER', 'signer control path must be absolute');
134
+ const stat = await fs.lstat(file); const real = await fs.realpath(file); const relative = path.relative(root, real);
135
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_CONTROL_BYTES || stat.nlink !== 1
136
+ || (typeof process.getuid === 'function' && stat.uid !== process.getuid()) || (stat.mode & 0o777) !== 0o600
137
+ || relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`))) fail('ENTROROVE_WORKFLOW_SIGNER', 'signer controls must be external owned regular 0600 files');
138
+ return { path: real, bytes: await fs.readFile(real) };
139
+ }
140
+ async function loadSigner(root, signerConfigPath, runnerKeyId) {
141
+ const file = await external(root, signerConfigPath); let config;
142
+ try { config = JSON.parse(file.bytes.toString('utf8')); } catch { fail('ENTROROVE_WORKFLOW_SIGNER', 'signer configuration JSON is invalid'); }
143
+ exact(config, ['schemaVersion', 'privateKeyPath', 'keyId', 'receiptTtlMs'], 'ENTROROVE_WORKFLOW_SIGNER', 'runner signer');
144
+ if (config.schemaVersion !== 'validator.runner-signer/1.0' || config.keyId !== runnerKeyId
145
+ || !positive(config.receiptTtlMs) || config.receiptTtlMs > MAX_LEASE_MS) fail('ENTROROVE_WORKFLOW_SIGNER', 'runner signer does not match the accepted runner key');
146
+ const secret = await external(root, config.privateKeyPath);
147
+ try {
148
+ const privateKey = createPrivateKey(secret.bytes); const der = createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
149
+ if (privateKey.asymmetricKeyType !== 'ed25519' || sha(der) !== runnerKeyId) fail('ENTROROVE_WORKFLOW_SIGNER', 'runner private key fingerprint does not match the accepted plan');
150
+ return { privateKey, publicKey: der.toString('base64'), config };
151
+ } catch (error) { if (error.code === 'ENTROROVE_WORKFLOW_SIGNER') throw error; fail('ENTROROVE_WORKFLOW_SIGNER', 'runner private key is invalid'); }
152
+ finally { secret.bytes.fill(0); }
153
+ }
154
+ function identity(authority) {
155
+ return { repositoryDigest: sha(authority.root).slice(0, 32), planId: authority.plan.planId, planDigest: authority.lease.planDigest,
156
+ flowId: authority.plan.flowId, ownerId: authority.plan.ownerId, stepId: authority.step.stepId,
157
+ sequence: authority.step.sequence, leaseId: authority.lease.leaseId, generation: authority.lease.generation,
158
+ leaseDigest: sha(canonical(authority.lease)) };
159
+ }
160
+ function binding(authority, processResult) {
161
+ const { plan, lease, step } = authority;
162
+ return { schemaVersion: BINDING_SCHEMA, planId: plan.planId, planDigest: lease.planDigest, flowId: plan.flowId, ownerId: plan.ownerId,
163
+ taskDigest: plan.taskDigest, chainDigest: plan.chainDigest, chainRevision: plan.chainRevision, stepId: step.stepId, sequence: step.sequence,
164
+ executionPlanSha256: step.executionPlanSha256, leaseId: lease.leaseId, leaseDigest: sha(canonical(lease)), generation: lease.generation,
165
+ verifiedDependencies: lease.verifiedDependencies, processResultSha256: sha(canonical(processResult)),
166
+ executionAuthorityDigest: sha(canonical(authority.executionAuthority)),
167
+ executionAttemptNonce: authority.executionAuthority.attemptNonce, executionStartedAt: processResult.startedAt };
168
+ }
169
+ function executionAuthority(value, authority) {
170
+ const configured = publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY');
171
+ const payload = verifyAuthority(value, configured);
172
+ exact(payload, ['schemaVersion', 'authorityId', 'attemptNonce', 'taskId', 'flowId', 'ownerId', 'taskDigest', 'chainDigest',
173
+ 'leaseId', 'generation', 'epoch', 'status', 'runnerKeyId', 'localRepositoryDigest', 'issuedAt', 'expiresAt'],
174
+ 'ENTROROVE_WORKFLOW_AUTHORITY', 'execution authority');
175
+ for (const field of ['authorityId', 'attemptNonce', 'taskId']) safeId(payload[field], field);
176
+ const { plan, lease } = authority;
177
+ if (payload.schemaVersion !== 'entrorove.task-execution-epoch-authority/1.0' || payload.status !== 'active'
178
+ || !Number.isSafeInteger(payload.epoch) || payload.epoch < 0 || payload.localRepositoryDigest !== sha(authority.root).slice(0, 32)
179
+ || ['flowId', 'ownerId', 'taskDigest', 'chainDigest', 'runnerKeyId'].some(key => payload[key] !== plan[key])
180
+ || payload.leaseId !== lease.leaseId || payload.generation !== lease.generation) {
181
+ fail('ENTROROVE_WORKFLOW_AUTHORITY', 'execution authority does not match the original signed plan and lease');
182
+ }
183
+ lifetime(payload.issuedAt, payload.expiresAt, 30_000, Date.now(), true);
184
+ return value;
185
+ }
186
+ async function readState(file) {
187
+ try {
188
+ const stat = await fs.lstat(file);
189
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > MAX_STATE_BYTES || (stat.mode & 0o777) !== 0o600
190
+ || (typeof process.getuid === 'function' && stat.uid !== process.getuid())) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'workflow state must be an owned regular 0600 file');
191
+ return JSON.parse(await fs.readFile(file, 'utf8'));
192
+ } catch (error) { if (error.code === 'ENOENT') return null; if (error.code === 'ENTROROVE_WORKFLOW_STATE_INVALID') throw error; fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'workflow state is unreadable or invalid'); }
193
+ }
194
+ async function writeState(file, state) {
195
+ const bytes = canonical(state) + '\n'; if (Buffer.byteLength(bytes) > MAX_STATE_BYTES) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'workflow state exceeds its bounded storage limit');
196
+ const temporary = `${file}.${randomUUID()}.tmp`; const handle = await fs.open(temporary, 'wx', 0o600);
197
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
198
+ await fs.rename(temporary, file); const directory = await fs.open(path.dirname(file), 'r');
199
+ try { await directory.sync(); } finally { await directory.close(); }
200
+ }
201
+ function checkState(state, authority) {
202
+ exact(state, ['schemaVersion', ...Object.keys(identity(authority)), 'executionAuthority', 'status', 'pid', 'startedAt', 'finishedAt', 'result', 'resultDigest'],
203
+ 'ENTROROVE_WORKFLOW_STATE_INVALID', 'workflow state');
204
+ if (state.schemaVersion !== STATE_SCHEMA || !['running', 'reported', 'failed'].includes(state.status)
205
+ || Object.entries(identity(authority)).some(([key, value]) => state[key] !== value)) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'workflow state does not match this original lease');
206
+ if (state.executionAuthority !== null) authority.executionAuthority = executionAuthority(state.executionAuthority, authority);
207
+ if (state.status === 'running') return;
208
+ if (!authority.executionAuthority) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'completed workflow has no original fresh cloud execution authority');
209
+ const result = state.result;
210
+ if (!result || result.status !== state.status || state.resultDigest !== sha(canonical(result)) || result.cached !== false
211
+ || result.statePath !== authority.file || !Array.isArray(result.evidence)) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'completed workflow result digest is invalid');
212
+ if (result.evidence.length === 0 && result.status === 'failed' && result.error && result.receipt === null) return;
213
+ const item = result.evidence[0]; const receipt = result.receipt; const key = publicKey(result.receiptPublicKey, 'ENTROROVE_WORKFLOW_STATE_INVALID');
214
+ if (result.evidence.length !== 1 || key.keyId !== authority.plan.runnerKeyId || receipt?.keyId !== key.keyId
215
+ || !result.subject?.policy || !result.process || receipt.schemaVersion !== 'validator.execution-receipt/1.0'
216
+ || item?.subjectDigest !== sha(canonical(result.subject)) || receipt.subjectDigest !== item.subjectDigest
217
+ || canonical(item.subject) !== canonical(result.subject) || canonical(item.receipt) !== canonical(receipt)
218
+ || item.command !== result.subject.policy.command || item.artifactSha256 !== result.subject.artifactSha256
219
+ || item.exitCode !== result.process.exitCode || receipt.result?.exitCode !== item.exitCode
220
+ || receipt.result.durationMs !== item.durationMs || receipt.result.summary !== item.summary
221
+ || receipt.result.runner !== 'trusted-runner' || item.runner !== 'trusted-runner'
222
+ || receipt.result.passed !== (result.status === 'reported') || (result.process.status === 'succeeded') !== (result.status === 'reported')
223
+ || canonical(result.subject.policy.workflowBinding) !== canonical(binding(authority, result.process))) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'completed workflow evidence binding is invalid');
224
+ const { signature, ...core } = receipt;
225
+ const bytes = typeof signature === 'string' ? Buffer.from(signature, 'base64url') : Buffer.alloc(0);
226
+ if (typeof signature !== 'string' || !/^[A-Za-z0-9_-]{86}$/.test(signature)
227
+ || bytes.length !== 64 || bytes.toString('base64url') !== signature
228
+ || !verify(null, Buffer.from(canonical(core)), key.key, bytes)) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'completed workflow evidence signature is invalid');
229
+ }
230
+ function live(pid) { if (!positive(pid)) return false; try { process.kill(pid, 0); return true; } catch (error) { return error.code !== 'ESRCH'; } }
231
+ async function claim(authority, authorizeMutation, checkBudget) {
232
+ return withStateLock(authority.file, async () => {
233
+ const state = await readState(authority.file);
234
+ if (state) {
235
+ checkState(state, authority);
236
+ if (state.status !== 'running') return { ...structuredClone(state.result), cached: true };
237
+ await checkBudget();
238
+ fail(live(state.pid) ? 'ENTROROVE_WORKFLOW_BUSY' : 'ENTROROVE_WORKFLOW_RECOVERY_REQUIRED', live(state.pid)
239
+ ? 'workflow local step is still running' : 'workflow local outcome is unknown; cloud reconciliation is required before any new execution');
240
+ }
241
+ await checkBudget(); await authorizeMutation();
242
+ const now = Date.now(); lifetime(authority.plan.acceptedAt, authority.plan.expiresAt, MAX_PLAN_MS, now);
243
+ lifetime(authority.lease.issuedAt, authority.lease.expiresAt, MAX_LEASE_MS, now);
244
+ await writeState(authority.file, { schemaVersion: STATE_SCHEMA, ...identity(authority), executionAuthority: null, status: 'running', pid: process.pid,
245
+ startedAt: new Date(now).toISOString(), finishedAt: null, result: null, resultDigest: null });
246
+ return null;
247
+ });
248
+ }
249
+ function signedResult(authority, execution, signer) {
250
+ if (!Number.isInteger(execution.process.exitCode)) return { schemaVersion: WORKFLOW_LOCAL_RESULT_SCHEMA, status: 'failed',
251
+ cached: false, statePath: authority.file, process: execution.process, evidence: [], receipt: null,
252
+ error: { code: 'ENTROROVE_WORKFLOW_PROCESS_INTERRUPTED', message: 'actual process ended without a completed integer exit code' } };
253
+ const { execution: _execution, ...originalSubject } = execution.subject;
254
+ const subject = { ...originalSubject, executedAt: execution.process.finishedAt, policy: { ...originalSubject.policy, workflowBinding: binding(authority, execution.process) } };
255
+ const original = execution.evidence[0]; const { receipt: _receipt, subject: _subject, subjectDigest: _digest, ...base } = original;
256
+ const evidence = { ...base, subject, subjectDigest: sha(canonical(subject)), runner: 'trusted-runner' };
257
+ const issuedAtMs = Date.now(), issuedAt = new Date(issuedAtMs).toISOString();
258
+ const core = { schemaVersion: 'validator.execution-receipt/1.0', keyId: signer.config.keyId, subjectDigest: evidence.subjectDigest,
259
+ nonce: randomUUID(), issuedAt, expiresAt: new Date(issuedAtMs + signer.config.receiptTtlMs).toISOString(),
260
+ result: { runner: 'trusted-runner', passed: execution.process.status === 'succeeded', exitCode: evidence.exitCode,
261
+ durationMs: evidence.durationMs, summary: evidence.summary } };
262
+ const receipt = { ...core, signature: sign(null, Buffer.from(canonical(core)), signer.privateKey).toString('base64url') };
263
+ return { schemaVersion: WORKFLOW_LOCAL_RESULT_SCHEMA, status: execution.process.status === 'succeeded' ? 'reported' : 'failed',
264
+ cached: false, statePath: authority.file, subject, process: execution.process, evidence: [{ ...evidence, receipt }], receipt,
265
+ receiptPublicKey: signer.publicKey, approvalSha256: execution.approvalSha256, approvedBy: execution.approvedBy };
266
+ }
267
+ async function finalize(authority, result) {
268
+ return withStateLock(authority.file, async () => {
269
+ const state = await readState(authority.file); if (!state) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'running state disappeared');
270
+ checkState(state, authority);
271
+ if (state.status !== 'running' || state.pid !== process.pid) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'running state ownership changed');
272
+ await writeState(authority.file, { ...state, status: result.status, finishedAt: new Date().toISOString(), result, resultDigest: sha(canonical(result)) });
273
+ return result;
274
+ });
275
+ }
276
+ async function prepare(input, create = true) {
277
+ const fields = ['repositoryRoot', 'acceptedPlan', 'lease', 'stepId', 'approvalPath', 'signerConfigPath',
278
+ ...(input?.executionAttemptNonce === undefined ? [] : ['executionAttemptNonce'])];
279
+ exact(input, input?.home === undefined ? fields : [...fields, 'home'], 'ENTROROVE_WORKFLOW_PLAN_FIELDS', 'workflow runner input');
280
+ if (input.executionAttemptNonce !== undefined) safeId(input.executionAttemptNonce, 'executionAttemptNonce');
281
+ if (process.platform === 'win32' || typeof process.getuid !== 'function') fail('ENTROROVE_POSIX_REQUIRED', 'workflow local runner requires POSIX');
282
+ const configured = publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY');
283
+ const plan = validatePlan(verifyAuthority(input.acceptedPlan, configured)); const lease = verifyAuthority(input.lease, configured);
284
+ const step = validateLease(lease, plan, input.stepId);
285
+ lifetime(plan.acceptedAt, plan.expiresAt, MAX_PLAN_MS, Date.now(), true); lifetime(lease.issuedAt, lease.expiresAt, MAX_LEASE_MS, Date.now(), true);
286
+ const root = await realDirectory(input.repositoryRoot, 'ENTROROVE_ROOT_INVALID');
287
+ const home = await realDirectory(input.home ?? os.homedir(), 'ENTROROVE_HOME_INVALID');
288
+ const directory = await privateHierarchy(home, ['.entrorove', 'workflow-local', sha(root).slice(0, 32), plan.flowId, step.stepId], create);
289
+ return { root, plan, lease, step, file: path.join(directory, 'state.json') };
290
+ }
291
+
292
+ export async function observeWorkflowLocalStep(rawInput) {
293
+ const authority = await prepare(structuredClone(rawInput), false);
294
+ const checkBudget = () => assertBudgetRunnerAllowed({ repositoryRoot: authority.root, home: rawInput.home, flowId: authority.plan.flowId });
295
+ if (!await readState(authority.file)) { await checkBudget(); return null; }
296
+ const original = { home: rawInput.home, repositoryRoot: authority.root, flowId: authority.plan.flowId };
297
+ const fence = { ...original, epoch: (await readTaskHandoffFence(original)).epoch, leaseId: authority.lease.leaseId };
298
+ return withTaskHandoffReadFence(fence, () => withStateLock(authority.file, async () => {
299
+ const state = await readState(authority.file); if (!state) { await checkBudget(); return null; }
300
+ checkState(state, authority);
301
+ if (state.status === 'running') { await checkBudget(); fail('ENTROROVE_WORKFLOW_RECOVERY_REQUIRED', 'original process outcome must be reconciled; a fresh nonce cannot restart its lease'); }
302
+ return { ...structuredClone(state.result), cached: true };
303
+ }));
304
+ }
305
+ export async function runWorkflowLocalStep(rawInput) {
306
+ let input; try { input = structuredClone(rawInput); } catch { fail('ENTROROVE_WORKFLOW_JSON', 'workflow values must be finite JSON'); }
307
+ const authority = await prepare(input);
308
+ const checkBudget = () => assertBudgetRunnerAllowed({ repositoryRoot: authority.root, home: input.home, flowId: authority.plan.flowId });
309
+ const original = { home: input.home, repositoryRoot: authority.root, flowId: authority.plan.flowId };
310
+ const fence = { ...original, epoch: (await readTaskHandoffFence(original)).epoch, leaseId: authority.lease.leaseId };
311
+ const cached = await withTaskHandoffReadFence(fence, (_state, { authorizeMutation }) => claim(authority, authorizeMutation, checkBudget));
312
+ if (cached) return cached;
313
+ let execution, activity;
314
+ try {
315
+ activity = await beginTaskHandoffExecution({ ...fence, stepId: authority.step.stepId, generation: authority.lease.generation,
316
+ attemptNonce: input.executionAttemptNonce, executionId: `run-${sha(canonical(authority.lease)).slice(0, 48)}` });
317
+ authority.executionAuthority = executionAuthority(activity.authority, authority);
318
+ await withStateLock(authority.file, async () => {
319
+ const state = await readState(authority.file); if (!state) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'running state disappeared before its authority binding');
320
+ checkState(state, authority);
321
+ if (state.status !== 'running' || state.pid !== process.pid || state.executionAuthority !== null) fail('ENTROROVE_WORKFLOW_STATE_INVALID', 'running authority ownership changed');
322
+ await writeState(authority.file, { ...state, executionAuthority: activity.authority });
323
+ });
324
+ const signer = await loadSigner(authority.root, input.signerConfigPath, authority.plan.runnerKeyId);
325
+ if (typeof approvedRunner.runApprovedPlanObject !== 'function') fail('ENTROROVE_WORKFLOW_RUNNER_UNAVAILABLE', 'approved object plan runner is unavailable');
326
+ execution = await approvedRunner.runApprovedPlanObject({ repositoryRoot: authority.root, executionPlan: authority.step.executionPlan,
327
+ approvalPath: input.approvalPath, signerConfigPath: input.signerConfigPath, executionDeadlineAt: authority.lease.expiresAt });
328
+ return await finalize(authority, signedResult(authority, execution, signer));
329
+ } catch (error) {
330
+ if (!error.execution && !execution) {
331
+ await withStateLock(authority.file, async () => {
332
+ const state = await readState(authority.file); if (state?.status === 'running' && state.pid === process.pid) { checkState(state, authority); await fs.unlink(authority.file); }
333
+ });
334
+ throw error;
335
+ }
336
+ return await finalize(authority, { schemaVersion: WORKFLOW_LOCAL_RESULT_SCHEMA, status: 'failed', cached: false, statePath: authority.file,
337
+ process: error.execution?.process ?? execution.process, evidence: [], receipt: null,
338
+ error: { code: error.code ?? 'ENTROROVE_WORKFLOW_EXECUTION_FAILED', message: error.message } });
339
+ } finally {
340
+ if (activity) await finishTaskHandoffExecution({ ...fence, ...activity });
341
+ }
342
+ }
343
+
344
+ export const WORKFLOW_LOCAL_CAPABILITIES = Object.freeze({ schemaVersion: 'entrorove.workflow-local-runner/1.0',
345
+ operations: ['workflow-run-step'], operationSchemas: { 'workflow-run-step': WORKFLOW_LOCAL_OPERATION_SCHEMA },
346
+ acceptedPlanSchema: WORKFLOW_LOCAL_PLAN_SCHEMA, leaseSchema: WORKFLOW_LOCAL_LEASE_SCHEMA,
347
+ authorityKeyEnvironment: 'ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY', stateDirectory: '~/.entrorove/workflow-local',
348
+ reportedIsVerified: false, limits: { maxSteps: 128, maxControlBytes: MAX_CONTROL_BYTES, maxStateBytes: MAX_STATE_BYTES,
349
+ maxPlanLifetimeMs: MAX_PLAN_MS, maxLeaseLifetimeMs: MAX_LEASE_MS },
350
+ });