@entrorove/skill 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGREEMENT.md +6 -6
- package/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +18 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +35 -83
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +24 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-chain-bindings.mjs +93 -0
- package/local-chain-broker.mjs +61 -0
- package/local-chain-contracts.mjs +21 -0
- package/local-chain-controller.mjs +321 -0
- package/local-chain-human.mjs +48 -0
- package/local-chain-lock.mjs +54 -0
- package/local-chain-operations.mjs +7 -0
- package/local-chain-plan.mjs +232 -0
- package/local-chain-release-labels.mjs +12 -0
- package/local-chain-results.mjs +75 -0
- package/local-cli.mjs +65 -24
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +126 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-error.mjs +23 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +106 -0
- package/local-formula-engine.mjs +125 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +142 -0
- package/local-ledger.mjs +15 -9
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +103 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +89 -0
- package/local-mutation-files.mjs +270 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +41 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +164 -3
- package/release-state.json +164 -18
- package/release.mjs +242 -4
- package/releases.json +12 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1910 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
package/cli.mjs
CHANGED
|
@@ -6,18 +6,19 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { compareVersions } from './version.mjs';
|
|
9
|
+
import { credentialPath, readCredential, writeCredential } from './local-credential.mjs';
|
|
9
10
|
import { runLocal } from './local-cli.mjs';
|
|
11
|
+
import { runTaskMessageCli } from './task-message-cli.mjs';
|
|
10
12
|
import { apiUrl, buildInvocation, invokeRemote, queryFlows, queryStatus } from './remote-client.mjs';
|
|
11
13
|
const packageDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
|
|
14
|
+
import { buildReview, submitSkillFeedback, feedbackNoticeVersion } from './review-client.mjs';
|
|
13
15
|
const markerName = '.entrorove-managed.json';
|
|
14
16
|
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json'];
|
|
15
17
|
const markerSchema = 'entrorove.managed-install/1.0';
|
|
16
|
-
const credentialSchema = 'entrorove.credential/1.0';
|
|
17
18
|
const agreementSchema = 'entrorove.agreement-acceptance/1.0';
|
|
18
19
|
const installLockWaitMs = 10_000;
|
|
19
20
|
const installLockStaleMs = 120_000;
|
|
20
|
-
export const agreementVersion =
|
|
21
|
+
export const agreementVersion = feedbackNoticeVersion;
|
|
21
22
|
|
|
22
23
|
function sha256(content) {
|
|
23
24
|
return createHash('sha256').update(content).digest('hex');
|
|
@@ -25,7 +26,8 @@ function sha256(content) {
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
async function readJson(file) {
|
|
28
|
-
|
|
29
|
+
let value;
|
|
30
|
+
try { value = JSON.parse(await fs.readFile(file, 'utf8')); } catch { throw new Error('Invalid JSON object'); }
|
|
29
31
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
30
32
|
throw new Error(`Invalid JSON object: ${file}`);
|
|
31
33
|
}
|
|
@@ -59,18 +61,6 @@ function globalInstallPath(home = os.homedir()) {
|
|
|
59
61
|
return path.join(home, '.codex', 'skills', 'entrorove');
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
function credentialPath(home = os.homedir()) {
|
|
63
|
-
if (process.platform === 'win32') {
|
|
64
|
-
const appData = process.env.LOCALAPPDATA;
|
|
65
|
-
if (!appData || !path.isAbsolute(appData)) throw new Error('LOCALAPPDATA is required');
|
|
66
|
-
return path.join(appData, 'Entrorove', 'credential.json');
|
|
67
|
-
}
|
|
68
|
-
if (process.platform === 'darwin') {
|
|
69
|
-
return path.join(home, 'Library', 'Application Support', 'Entrorove', 'credential.json');
|
|
70
|
-
}
|
|
71
|
-
return path.join(home, '.local', 'share', 'entrorove', 'credential.json');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
64
|
function agreementPath(home = os.homedir()) {
|
|
75
65
|
return path.join(path.dirname(credentialPath(home)), 'agreement.json');
|
|
76
66
|
}
|
|
@@ -298,12 +288,13 @@ async function cleanProjectDuplicates(target, cwd) {
|
|
|
298
288
|
export async function install({ home = os.homedir(), cwd = process.cwd() } = {}) {
|
|
299
289
|
const bundle = await sourceBundle();
|
|
300
290
|
const target = globalInstallPath(home);
|
|
291
|
+
const stale = await agreementStale(home);
|
|
301
292
|
const parent = path.dirname(target);
|
|
302
293
|
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
|
303
294
|
return withInstallLock(parent, async () => {
|
|
304
295
|
const updated = await updateGlobalInstall(target, bundle);
|
|
305
296
|
const removed = await cleanProjectDuplicates(target, cwd);
|
|
306
|
-
return { version: bundle.manifest.version, updated, removed };
|
|
297
|
+
return { version: bundle.manifest.version, updated, removed, agreementStale: stale };
|
|
307
298
|
});
|
|
308
299
|
}
|
|
309
300
|
|
|
@@ -313,7 +304,8 @@ async function stdinJson() {
|
|
|
313
304
|
raw += chunk;
|
|
314
305
|
if (raw.length > 1_000_000) throw new Error('Input is too large');
|
|
315
306
|
}
|
|
316
|
-
|
|
307
|
+
let value;
|
|
308
|
+
try { value = JSON.parse(raw); } catch { throw new Error('Input must be valid JSON'); }
|
|
317
309
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Input must be a JSON object');
|
|
318
310
|
return value;
|
|
319
311
|
}
|
|
@@ -325,38 +317,6 @@ async function preparePrivateDirectory(parent) {
|
|
|
325
317
|
if (process.platform !== 'win32') await fs.chmod(parent, 0o700);
|
|
326
318
|
}
|
|
327
319
|
|
|
328
|
-
async function writeCredential(token, home) {
|
|
329
|
-
if (typeof token !== 'string' || !/^ent_[A-Za-z0-9_-]{43}$/.test(token)) {
|
|
330
|
-
throw new Error('Invalid entrorove key');
|
|
331
|
-
}
|
|
332
|
-
const file = credentialPath(home);
|
|
333
|
-
const parent = path.dirname(file);
|
|
334
|
-
await preparePrivateDirectory(parent);
|
|
335
|
-
const existing = await statOrNull(file);
|
|
336
|
-
if (existing && (!existing.isFile() || existing.isSymbolicLink())) throw new Error('Credential path is not a regular file');
|
|
337
|
-
const stage = path.join(parent, `.credential-${randomUUID()}.tmp`);
|
|
338
|
-
const handle = await fs.open(stage, 'wx', 0o600);
|
|
339
|
-
try {
|
|
340
|
-
await handle.writeFile(JSON.stringify({ schemaVersion: credentialSchema, token }) + '\n');
|
|
341
|
-
await handle.sync();
|
|
342
|
-
} finally {
|
|
343
|
-
await handle.close();
|
|
344
|
-
}
|
|
345
|
-
await fs.rename(stage, file);
|
|
346
|
-
if (process.platform !== 'win32') await fs.chmod(file, 0o600);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function readCredential(home) {
|
|
350
|
-
const file = credentialPath(home);
|
|
351
|
-
const stat = await fs.lstat(file);
|
|
352
|
-
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Credential path is not a regular file');
|
|
353
|
-
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) throw new Error('Credential permissions must be 0600');
|
|
354
|
-
const data = await readJson(file);
|
|
355
|
-
if (data.schemaVersion !== credentialSchema || typeof data.token !== 'string'
|
|
356
|
-
|| !/^ent_[A-Za-z0-9_-]{43}$/.test(data.token)) throw new Error('Invalid credential file');
|
|
357
|
-
return data.token;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
320
|
async function agreementAccepted(home) {
|
|
361
321
|
const file = agreementPath(home);
|
|
362
322
|
const stat = await statOrNull(file);
|
|
@@ -371,6 +331,22 @@ async function agreementAccepted(home) {
|
|
|
371
331
|
&& Number.isFinite(Date.parse(data.acceptedAt));
|
|
372
332
|
}
|
|
373
333
|
|
|
334
|
+
async function agreementStale(home) {
|
|
335
|
+
const file = agreementPath(home);
|
|
336
|
+
const stat = await statOrNull(file);
|
|
337
|
+
if (!stat) return false;
|
|
338
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return true;
|
|
339
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) return true;
|
|
340
|
+
try {
|
|
341
|
+
const data = await readJson(file);
|
|
342
|
+
return data.schemaVersion !== agreementSchema || data.version !== agreementVersion
|
|
343
|
+
|| data.answer !== 'accept' || typeof data.acceptedAt !== 'string'
|
|
344
|
+
|| !Number.isFinite(Date.parse(data.acceptedAt));
|
|
345
|
+
} catch {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
374
350
|
async function writeAgreementAcceptance(data, home) {
|
|
375
351
|
if (Object.keys(data).sort().join(',') !== 'answer,version'
|
|
376
352
|
|| data.version !== agreementVersion || !['accept', 'decline'].includes(data.answer)) {
|
|
@@ -398,9 +374,9 @@ async function writeAgreementAcceptance(data, home) {
|
|
|
398
374
|
|
|
399
375
|
export { buildInvocation };
|
|
400
376
|
|
|
401
|
-
function remoteDependencies() {
|
|
377
|
+
export function remoteDependencies(home = os.homedir()) {
|
|
402
378
|
return {
|
|
403
|
-
install, agreementAccepted, agreementRequired, readCredential, home
|
|
379
|
+
install: () => install({ home }), agreementAccepted, agreementRequired, readCredential, home,
|
|
404
380
|
apiOrigin: () => new URL(apiUrl).origin,
|
|
405
381
|
};
|
|
406
382
|
}
|
|
@@ -409,37 +385,11 @@ async function invoke(data) {
|
|
|
409
385
|
return invokeRemote(data, remoteDependencies());
|
|
410
386
|
}
|
|
411
387
|
|
|
412
|
-
export
|
|
413
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)
|
|
414
|
-
|| Object.keys(data).sort().join(',') !== 'comment,ide,publicConsent,rating'
|
|
415
|
-
|| typeof data.ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(data.ide.trim())
|
|
416
|
-
|| !Number.isInteger(data.rating) || data.rating < 1 || data.rating > 5
|
|
417
|
-
|| typeof data.comment !== 'string' || data.comment.trim().length < 4
|
|
418
|
-
|| data.comment.trim().length > 280 || data.publicConsent !== true) {
|
|
419
|
-
throw new Error('Expected user-authored review with ide, rating 1-5, comment, and publicConsent:true');
|
|
420
|
-
}
|
|
421
|
-
return {
|
|
422
|
-
url: reviewUrl,
|
|
423
|
-
body: {
|
|
424
|
-
ide: data.ide.trim(), rating: data.rating,
|
|
425
|
-
comment: data.comment.trim(), publicConsent: true,
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
}
|
|
388
|
+
export { buildReview };
|
|
429
389
|
|
|
430
390
|
async function submitReview(data) {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
const response = await fetch(review.url, {
|
|
434
|
-
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(15_000),
|
|
435
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
436
|
-
body: JSON.stringify(review.body),
|
|
437
|
-
});
|
|
438
|
-
if (!response.ok) throw new Error(`entrorove review failed: HTTP ${response.status}`);
|
|
439
|
-
const result = await response.json();
|
|
440
|
-
if (result?.published !== true || typeof result.id !== 'string') {
|
|
441
|
-
throw new Error('Invalid entrorove review response');
|
|
442
|
-
}
|
|
391
|
+
const result = await submitSkillFeedback(data, remoteDependencies());
|
|
392
|
+
if (result.error === 'agreement-required') process.exitCode = 2;
|
|
443
393
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
444
394
|
}
|
|
445
395
|
|
|
@@ -458,6 +408,8 @@ async function main() {
|
|
|
458
408
|
process.stdout.write(JSON.stringify({ accepted, version: agreementVersion }) + '\n');
|
|
459
409
|
} else if (command === 'invoke' && args.length === 0) {
|
|
460
410
|
await invoke(await stdinJson());
|
|
411
|
+
} else if (command === 'host-message') {
|
|
412
|
+
await runTaskMessageCli(args, await stdinJson(), remoteDependencies());
|
|
461
413
|
} else if (command === 'review' && args.length === 0) {
|
|
462
414
|
await submitReview(await stdinJson());
|
|
463
415
|
} else if (command === 'status' && args.length === 1) {
|
|
@@ -479,12 +431,12 @@ async function main() {
|
|
|
479
431
|
} else if (command === 'local') {
|
|
480
432
|
await runLocal(args);
|
|
481
433
|
} else {
|
|
482
|
-
throw new Error('Usage: entrorove install | configure | agreement | invoke | status <requestId> | flows | review | local (JSON on stdin)');
|
|
434
|
+
throw new Error('Usage: entrorove install | configure | agreement | invoke | host-message [adapterModulePath] | status <requestId> | flows | review | local (JSON on stdin)');
|
|
483
435
|
}
|
|
484
436
|
}
|
|
485
437
|
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
486
438
|
main().catch((error) => {
|
|
487
|
-
process.stderr.write(`${error.message}\n`);
|
|
439
|
+
process.stderr.write(`${error instanceof SyntaxError ? 'Invalid JSON data' : error.message}\n`);
|
|
488
440
|
process.exitCode = 1;
|
|
489
441
|
});
|
|
490
442
|
}
|
package/local-approved-plan.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { LOCAL_OPERATION_SCHEMAS } from './local-contracts.mjs';
|
|
1
2
|
import { createHash, createPrivateKey, createPublicKey, randomUUID, sign } from 'node:crypto';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
4
|
import { promises as fs, constants } from 'node:fs';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { performance } from 'node:perf_hooks';
|
|
8
|
+
import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
|
|
7
9
|
|
|
8
10
|
export const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
|
|
9
11
|
export const PLAN_SCHEMA = 'validator.execution-plan/1.0';
|
|
@@ -18,24 +20,37 @@ function canonical(value) { if (Array.isArray(value)) return `[${value.map(canon
|
|
|
18
20
|
function safeRelative(value, label = 'path') { if (typeof value !== 'string' || !value || value !== value.normalize('NFC') || path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || value.includes('\\') || /[\u0000-\u001f\u007f]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) fail('ENTROROVE_PATH_UNSAFE', `${label} must be a normalized relative path`); return value; }
|
|
19
21
|
function inside(root, target, label) { const relative = path.relative(root, target); if (relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`))) return; fail('ENTROROVE_PATH_ESCAPE', `${label} escapes repositoryRoot`); }
|
|
20
22
|
async function rootOf(value) { if (typeof value !== 'string' || !path.isAbsolute(value)) fail('ENTROROVE_ROOT_INVALID', 'repositoryRoot must be absolute'); const stat = await fs.lstat(value); if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_ROOT_INVALID', 'repositoryRoot must be a real directory'); return fs.realpath(value); }
|
|
21
|
-
async function regular(file, max, privateFile = false) { const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > max) fail('ENTROROVE_FILE_INVALID', 'control file must be a bounded regular file'); if (privateFile && process.platform !== 'win32' && ((typeof process.getuid === 'function' && stat.uid !== process.getuid()) || (stat.mode & 0o777) !== 0o600 || stat.nlink !== 1)) fail('ENTROROVE_CONTROL_PERMISSIONS', 'control file must be owned and mode 0600');
|
|
23
|
+
async function regular(file, max, privateFile = false) { const stat = await fs.lstat(file); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > max) fail('ENTROROVE_FILE_INVALID', 'control file must be a bounded regular file'); if (privateFile && process.platform !== 'win32' && ((typeof process.getuid === 'function' && stat.uid !== process.getuid()) || (stat.mode & 0o777) !== 0o600 || stat.nlink !== 1)) fail('ENTROROVE_CONTROL_PERMISSIONS', 'control file must be owned and mode 0600'); const bytes = await fs.readFile(file); return { bytes, sha256: digest(bytes) }; }
|
|
22
24
|
async function projectFile(root, relative) { const safe = safeRelative(relative); const target = path.resolve(root, ...safe.split('/')); inside(root, target, safe); const stat = await fs.lstat(target); if (!stat.isFile() || stat.isSymbolicLink() || await fs.realpath(target) !== target) fail('ENTROROVE_FILE_INVALID', `${safe} must be a regular file`); return { path: safe, target, stat, ...(await regular(target, RUNNER_LIMITS.maxFileBytes)) }; }
|
|
23
25
|
async function externalFile(root, value, label) { if (typeof value !== 'string' || !path.isAbsolute(value)) fail('ENTROROVE_CONTROL_PATH', `${label} must be an absolute path`); const entry = await fs.lstat(value); if (!entry.isFile() || entry.isSymbolicLink()) fail('ENTROROVE_CONTROL_FILE', `${label} must be a regular non-symlink file`); const target = await fs.realpath(value); const relative = path.relative(root, target); if (relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`))) fail('ENTROROVE_PATH_ESCAPE', `${label} must be outside repositoryRoot`); return { path: target, ...(await regular(target, RUNNER_LIMITS.maxControlBytes, true)) }; }
|
|
24
26
|
function exactObject(value, fields, label) { if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join('|') !== [...fields].sort().join('|')) fail('ENTROROVE_PLAN_FIELDS', `${label} has invalid fields`); return value; }
|
|
25
27
|
function boundedInt(value, max, label) { if (!Number.isSafeInteger(value) || value <= 0 || value > max) fail('ENTROROVE_LIMIT_INVALID', `${label} must be a positive bounded integer`); }
|
|
26
|
-
function planArtifact(files) { return
|
|
28
|
+
function planArtifact(files) { return validatorPlanArtifact(files); }
|
|
29
|
+
function validatorPlanArtifact(files) { return digest(`validator.file-manifest/1.0\n${files.map(file => `${file.path}\0${file.sha256}`).join('\n')}`); }
|
|
27
30
|
|
|
28
|
-
function validatePlan(plan) {
|
|
29
|
-
exactObject(plan, ['schemaVersion', 'frozen', 'planId', 'validationRunId', 'memberId', 'chainId', 'artifactSha256', 'files', 'tests', 'policy', 'goldenBaseline', 'contracts'], 'execution plan');
|
|
31
|
+
function validatePlan(plan, artifactDigest = planArtifact) {
|
|
32
|
+
exactObject(plan, ['schemaVersion', 'frozen', 'planId', 'validationRunId', 'memberId', 'chainId', 'artifactSha256', 'files', 'tests', 'policy', 'goldenBaseline', 'contracts', ...(plan?.evidencePolicy === undefined ? [] : ['evidencePolicy'])], 'execution plan');
|
|
30
33
|
if (plan.schemaVersion !== PLAN_SCHEMA || plan.frozen !== true || !ID.test(plan.planId) || !ID.test(plan.validationRunId) || !ID.test(plan.memberId) || !ID.test(plan.chainId) || !SHA256.test(plan.artifactSha256) || !Array.isArray(plan.files) || plan.files.length === 0 || plan.files.length > RUNNER_LIMITS.maxFiles) fail('ENTROROVE_PLAN_INVALID', 'plan must be frozen and bounded');
|
|
31
34
|
for (let i = 0; i < plan.files.length; i += 1) { const file = exactObject(plan.files[i], ['path', 'sha256'], 'manifest entry'); safeRelative(file.path); if (!SHA256.test(file.sha256) || (i > 0 && plan.files[i - 1].path >= file.path)) fail('ENTROROVE_PLAN_MANIFEST', 'manifest paths must be sorted and unique'); }
|
|
32
|
-
if (
|
|
35
|
+
if (artifactDigest(plan.files) !== plan.artifactSha256) fail('ENTROROVE_PLAN_MANIFEST', 'artifact manifest digest mismatch');
|
|
33
36
|
const policy = exactObject(plan.policy, ['executable', 'executableSha256', 'args', 'environment', 'timeoutMs', 'maxOutputBytes', 'requiredExitCode'], 'execution policy');
|
|
34
37
|
if (typeof policy.executable !== 'string' || !path.isAbsolute(policy.executable) || !SHA256.test(policy.executableSha256) || !Array.isArray(policy.args) || policy.args.length > 128 || policy.args.some(value => typeof value !== 'string' || value.includes('\0') || value.length > 4096)) fail('ENTROROVE_POLICY_INVALID', 'executable and args are invalid');
|
|
35
38
|
if (!policy.environment || typeof policy.environment !== 'object' || Array.isArray(policy.environment) || Object.keys(policy.environment).length > 64 || Object.entries(policy.environment).some(([key, value]) => !ENV.test(key) || typeof value !== 'string' || value.includes('\0') || value.length > 16_384)) fail('ENTROROVE_POLICY_ENVIRONMENT', 'environment must be explicit bounded strings');
|
|
36
39
|
boundedInt(policy.timeoutMs, RUNNER_LIMITS.maxTimeoutMs, 'timeoutMs'); boundedInt(policy.maxOutputBytes, RUNNER_LIMITS.maxOutputBytes, 'maxOutputBytes'); if (policy.requiredExitCode !== 0) fail('ENTROROVE_POLICY_EXIT', 'requiredExitCode must be zero');
|
|
37
40
|
if (!Array.isArray(plan.tests) || plan.tests.length === 0 || plan.tests.length > RUNNER_LIMITS.maxFiles) fail('ENTROROVE_PLAN_TESTS', 'tests must be non-empty');
|
|
38
41
|
for (const test of plan.tests) { exactObject(test, ['testId', 'path'], 'test'); if (!ID.test(test.testId) || !plan.files.some(file => file.path === test.path) || !policy.args.includes(test.path)) fail('ENTROROVE_PLAN_TESTS', 'each test must be in the manifest and explicit in args'); }
|
|
42
|
+
if (plan.evidencePolicy !== undefined) {
|
|
43
|
+
const evidencePolicy = exactObject(plan.evidencePolicy, ['kind', 'runner', 'flowId', 'stepId', ...(plan.evidencePolicy?.localVerification === undefined ? [] : ['localVerification'])], 'evidence policy');
|
|
44
|
+
if (!['test', 'build', 'lint', 'security', 'benchmark'].includes(evidencePolicy.kind)
|
|
45
|
+
|| !['local', 'trusted-runner', 'sandbox'].includes(evidencePolicy.runner)
|
|
46
|
+
|| !ID.test(String(evidencePolicy.flowId ?? '')) || !ID.test(String(evidencePolicy.stepId ?? ''))) fail('ENTROROVE_EVIDENCE_POLICY', 'evidence policy must bind kind, runner, flow and step');
|
|
47
|
+
if (evidencePolicy.localVerification !== undefined) {
|
|
48
|
+
const local = exactObject(evidencePolicy.localVerification, ['operation', 'requestedTarget', 'requirementsSha256'], 'local verification policy');
|
|
49
|
+
if (!['sandbox-run', 'fuzz-input', 'perf-benchmark', 'intrusive-test'].includes(local.operation)
|
|
50
|
+
|| typeof local.requestedTarget !== 'string' || !local.requestedTarget.trim() || local.requestedTarget.length > 2000
|
|
51
|
+
|| !SHA256.test(String(local.requirementsSha256 ?? ''))) fail('ENTROROVE_EVIDENCE_POLICY', 'local verification requires its exact operation, target and requirements digest');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
if (plan.contracts?.archguard?.driftStatus === 'red') fail('ENTROROVE_ARCHGUARD_BLOCKED', 'ArchGuard red blocks execution'); return plan;
|
|
40
55
|
}
|
|
41
56
|
|
|
@@ -52,8 +67,10 @@ async function signer(root, input, policy) {
|
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
async function authority(input) {
|
|
70
|
+
await assertBudgetRunnerAllowed(input);
|
|
55
71
|
if (process.platform === 'win32' || typeof process.getuid !== 'function') fail('ENTROROVE_POSIX_REQUIRED', 'approved plan runner requires POSIX');
|
|
56
|
-
|
|
72
|
+
const fields = ['repositoryRoot', 'planPath', 'approvalPath', 'signerConfigPath'];
|
|
73
|
+
exactObject(input, [...fields, ...['home', 'flowId'].filter(field => input[field] !== undefined)], 'runner input'); const root = await rootOf(input.repositoryRoot); const planFile = await projectFile(root, input.planPath); if (planFile.bytes.length > RUNNER_LIMITS.maxControlBytes) fail('ENTROROVE_PLAN_SIZE', 'plan exceeds control limit');
|
|
57
74
|
let plan; try { plan = validatePlan(JSON.parse(planFile.bytes.toString('utf8'))); } catch (cause) { throw cause; } const approval = await validateApproval(root, input, planFile.sha256); const signing = await signer(root, input, plan.policy); const executable = await regular(plan.policy.executable, RUNNER_LIMITS.maxExecutableBytes); if (executable.sha256 !== plan.policy.executableSha256) fail('ENTROROVE_EXECUTABLE_HASH', 'executable SHA-256 mismatch');
|
|
58
75
|
for (const file of plan.files) { const actual = await projectFile(root, file.path); if (actual.sha256 !== file.sha256) fail('ENTROROVE_PLAN_FILE_HASH', `${file.path} changed before execution`); }
|
|
59
76
|
return { root, plan, planFile, approval, signing };
|
|
@@ -61,21 +78,63 @@ async function authority(input) {
|
|
|
61
78
|
|
|
62
79
|
function runCommand(authority) {
|
|
63
80
|
return new Promise((resolve, reject) => {
|
|
64
|
-
const started = performance.now(); const { policy } = authority.plan; const
|
|
65
|
-
|
|
66
|
-
const
|
|
81
|
+
const started = performance.now(); const { policy } = authority.plan; const remaining = authority.executionDeadlineAt ? Date.parse(authority.executionDeadlineAt) - Date.now() : policy.timeoutMs;
|
|
82
|
+
if (remaining <= 0) { reject(Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' })); return; }
|
|
83
|
+
const child = spawn(policy.executable, policy.args, { cwd: authority.root, env: { ...policy.environment }, shell: false, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
84
|
+
const stdout = []; const stderr = []; let bytes = 0; let dropped = 0; let timedOut = false; let terminating = false; let spawned = false; let processStartedAt = null; let spawnError = null; let timer; let killTimer; const stdoutHash = createHash('sha256'); const stderrHash = createHash('sha256');
|
|
85
|
+
const terminate = () => { if (terminating) return; terminating = true; try { process.kill(-child.pid, 'SIGTERM'); } catch {} killTimer = setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }, 250); killTimer.unref(); };
|
|
67
86
|
const consume = (target, hash, chunk) => { const data = Buffer.from(chunk); hash.update(data); const keep = Math.max(0, Math.min(data.length, policy.maxOutputBytes - bytes)); if (keep) target.push(data.subarray(0, keep)); bytes += keep; dropped += data.length - keep; if (dropped) terminate(); };
|
|
68
|
-
child.stdout.on('data', chunk => consume(stdout, stdoutHash, chunk)); child.stderr.on('data', chunk => consume(stderr, stderrHash, chunk));
|
|
69
|
-
child.once('
|
|
87
|
+
child.stdout.on('data', chunk => consume(stdout, stdoutHash, chunk)); child.stderr.on('data', chunk => consume(stderr, stderrHash, chunk));
|
|
88
|
+
child.once('spawn', () => { spawned = true; processStartedAt = new Date().toISOString(); }); child.once('error', error => { spawnError = error; terminate(); }); timer = setTimeout(() => { timedOut = true; terminate(); }, Math.min(policy.timeoutMs, remaining));
|
|
89
|
+
child.once('close', (exitCode, signal) => {
|
|
90
|
+
clearTimeout(timer); clearTimeout(killTimer);
|
|
91
|
+
const processResult = { schemaVersion: RUNNER_SCHEMA, status: exitCode === 0 && signal === null && !timedOut && !dropped && !spawnError ? 'succeeded' : 'failed', exitCode, signal, timedOut, outputLimitExceeded: dropped > 0, durationMs: Math.max(0, Math.round(performance.now() - started)), stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), stdoutSha256: stdoutHash.digest('hex'), stderrSha256: stderrHash.digest('hex'), capturedBytes: bytes, droppedBytes: dropped,
|
|
92
|
+
...(authority.objectPlan ? { started: spawned, startedAt: processStartedAt, finishedAt: new Date().toISOString(), executionDeadlineExceeded: timedOut && remaining <= policy.timeoutMs } : {}) };
|
|
93
|
+
if (spawnError) { spawnError.execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', process: processResult, evidence: [], effects: spawned ? 'unknown' : 'none' }; reject(spawnError); return; }
|
|
94
|
+
resolve(processResult);
|
|
95
|
+
});
|
|
70
96
|
});
|
|
71
97
|
}
|
|
72
|
-
function subject(authority, result) { return { schemaVersion: 'validator.validation-subject/1.0', artifactSha256: authority.plan.artifactSha256, memberId: authority.plan.memberId, chainId: authority.plan.chainId, executedAt: new Date().toISOString(), files: authority.plan.files, validationRunId: authority.plan.validationRunId, planId: authority.plan.planId, tests: authority.plan.tests, policy: { command: [authority.plan.policy.executable, ...authority.plan.policy.args].map(value => JSON.stringify(value)).join(' '), requiredExitCode: 0, executionPlanSha256: authority.planFile.sha256 }, goldenBaseline: authority.plan.goldenBaseline, contracts: authority.plan.contracts, execution: { exitCode: result.exitCode, signal: result.signal, timedOut: result.timedOut, outputLimitExceeded: result.outputLimitExceeded } }; }
|
|
73
|
-
function evidence(authority, result, sub) { const summary = result.timedOut ? 'Approved validation command exceeded its deadline' : result.outputLimitExceeded ? 'Approved validation command exceeded its output limit' : result.signal ? `Approved validation command ended on signal ${result.signal}` : `Approved validation command exited with code ${result.exitCode}`; const base = { schemaVersion: 'entrorove.test-evidence/2.0', evidenceId: `${authority.plan.validationRunId}:process`, kind: 'test', runner: authority.signing ? 'trusted-runner' : 'local', command: [authority.plan.policy.executable, ...authority.plan.policy.args].map(value => JSON.stringify(value)).join(' '), exitCode: result.exitCode, durationMs: result.durationMs, summary, artifactSha256: authority.plan.artifactSha256, subject: sub, subjectDigest: digest(canonical(sub)) };
|
|
74
|
-
if (!authority.signing || !Number.isInteger(
|
|
98
|
+
function subject(authority, result) { return { schemaVersion: 'validator.validation-subject/1.0', artifactSha256: authority.plan.artifactSha256, memberId: authority.plan.memberId, chainId: authority.plan.chainId, executedAt: new Date().toISOString(), files: authority.plan.files, validationRunId: authority.plan.validationRunId, planId: authority.plan.planId, tests: authority.plan.tests, policy: { command: [authority.plan.policy.executable, ...authority.plan.policy.args].map(value => JSON.stringify(value)).join(' '), requiredExitCode: 0, executionPlanSha256: authority.planFile.sha256, ...(authority.plan.evidencePolicy ? { evidenceKind: authority.plan.evidencePolicy.kind, evidenceRunner: authority.plan.evidencePolicy.runner, workflowBinding: { flowId: authority.plan.evidencePolicy.flowId, stepId: authority.plan.evidencePolicy.stepId }, ...(authority.plan.evidencePolicy.localVerification ? { localVerification: authority.plan.evidencePolicy.localVerification } : {}) } : {}) }, goldenBaseline: authority.plan.goldenBaseline, contracts: authority.plan.contracts, execution: { exitCode: result.exitCode, signal: result.signal, timedOut: result.timedOut, outputLimitExceeded: result.outputLimitExceeded } }; }
|
|
99
|
+
function evidence(authority, result, sub) { const summary = result.timedOut ? 'Approved validation command exceeded its deadline' : result.outputLimitExceeded ? 'Approved validation command exceeded its output limit' : result.signal ? `Approved validation command ended on signal ${result.signal}` : `Approved validation command exited with code ${result.exitCode}`; const base = { schemaVersion: 'entrorove.test-evidence/2.0', evidenceId: `${authority.plan.validationRunId}:process`, kind: authority.plan.evidencePolicy?.kind ?? 'test', runner: authority.signing ? authority.plan.evidencePolicy?.runner ?? 'trusted-runner' : 'local', ...(authority.plan.evidencePolicy ? { flowId: authority.plan.evidencePolicy.flowId, stepId: authority.plan.evidencePolicy.stepId } : {}), command: [authority.plan.policy.executable, ...authority.plan.policy.args].map(value => JSON.stringify(value)).join(' '), exitCode: result.exitCode === null ? 1 : result.exitCode, durationMs: result.durationMs, summary, artifactSha256: authority.plan.artifactSha256, subject: sub, subjectDigest: digest(canonical(sub)) };
|
|
100
|
+
if (!authority.signing || !Number.isInteger(base.exitCode)) return base; const privateKey = authority.signing.privateKey; const receiptCore = { schemaVersion: RECEIPT_SCHEMA, keyId: authority.signing.config.keyId, subjectDigest: base.subjectDigest, nonce: randomUUID(), issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + authority.signing.config.receiptTtlMs).toISOString(), result: { runner: 'trusted-runner', passed: result.status === 'succeeded', exitCode: result.exitCode === null ? 1 : result.exitCode, durationMs: result.durationMs, summary } }; const receipt = { ...receiptCore, signature: sign(null, Buffer.from(canonical(receiptCore)), privateKey).toString('base64url') }; return { ...base, receipt };
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
export async function runApprovedPlan(input) { const owner = await authority(input); const result = await runCommand(owner); try { const after = await authority(input); if (after.planFile.sha256 !== owner.planFile.sha256) fail('ENTROROVE_PLAN_CHANGED', 'plan changed during execution'); } catch (cause) { const execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', error: cause.message, process: result, evidence: [] }; const error = new Error(cause.message); error.code = cause.code; error.execution = execution; throw error; }
|
|
78
104
|
const sub = subject(owner, result); const item = evidence(owner, result, sub); const status = !owner.signing && result.status === 'succeeded' ? 'incomplete' : result.status; return { schemaVersion: RUNNER_SCHEMA, status, planSha256: owner.planFile.sha256, approvalSha256: owner.approval.sha256, approvedBy: owner.approval.approval.approvedBy, subject: sub, process: result, evidence: [item], receipt: item.receipt ?? null, ...(owner.signing ? { receiptPublicKey: createPublicKey(owner.signing.privateKey).export({ format: 'der', type: 'spki' }).toString('base64') } : {}), signingBoundary: owner.signing ? 'configured-signature-only' : 'local-evidence-only' };
|
|
79
105
|
}
|
|
80
106
|
|
|
81
|
-
|
|
107
|
+
async function objectAuthority(input, afterExecution = false) {
|
|
108
|
+
await assertBudgetRunnerAllowed(input);
|
|
109
|
+
if (process.platform === 'win32' || typeof process.getuid !== 'function') fail('ENTROROVE_POSIX_REQUIRED', 'approved plan runner requires POSIX');
|
|
110
|
+
const fields = ['repositoryRoot', 'executionPlan', 'approvalPath', 'signerConfigPath', ...['home', 'flowId'].filter(field => input[field] !== undefined)];
|
|
111
|
+
exactObject(input, input?.executionDeadlineAt === undefined ? fields : [...fields, 'executionDeadlineAt'], 'object runner input');
|
|
112
|
+
const root = await rootOf(input.repositoryRoot); const bytes = Buffer.from(canonical(input.executionPlan) + '\n');
|
|
113
|
+
if (bytes.length > RUNNER_LIMITS.maxControlBytes) fail('ENTROROVE_PLAN_SIZE', 'plan exceeds control limit');
|
|
114
|
+
let parsed; try { parsed = JSON.parse(bytes.toString('utf8')); } catch { fail('ENTROROVE_PLAN_INVALID', 'execution plan must contain JSON values'); }
|
|
115
|
+
const plan = validatePlan(parsed, validatorPlanArtifact); const planFile = { bytes, sha256: digest(bytes) };
|
|
116
|
+
const approval = await validateApproval(root, input, planFile.sha256); const signing = await signer(root, input, plan.policy);
|
|
117
|
+
const executable = await regular(plan.policy.executable, RUNNER_LIMITS.maxExecutableBytes);
|
|
118
|
+
if (executable.sha256 !== plan.policy.executableSha256) fail('ENTROROVE_EXECUTABLE_HASH', 'executable SHA-256 mismatch');
|
|
119
|
+
for (const file of plan.files) { const actual = await projectFile(root, file.path); if (actual.sha256 !== file.sha256) fail('ENTROROVE_PLAN_FILE_HASH', `${file.path} changed before execution`); }
|
|
120
|
+
if (input.executionDeadlineAt !== undefined && (typeof input.executionDeadlineAt !== 'string' || !Number.isFinite(Date.parse(input.executionDeadlineAt))
|
|
121
|
+
|| (!afterExecution && Date.parse(input.executionDeadlineAt) <= Date.now()))) fail('ENTROROVE_WORKFLOW_EXPIRED', 'workflow execution deadline is invalid or expired');
|
|
122
|
+
return { root, plan, planFile, approval, signing, objectPlan: true, ...(input.executionDeadlineAt === undefined ? {} : { executionDeadlineAt: input.executionDeadlineAt }) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runApprovedPlanObject(input) {
|
|
126
|
+
const owner = await objectAuthority(input); const result = await runCommand(owner);
|
|
127
|
+
try { const after = await objectAuthority(input, true); if (after.planFile.sha256 !== owner.planFile.sha256) fail('ENTROROVE_PLAN_CHANGED', 'plan changed during execution'); }
|
|
128
|
+
catch (cause) {
|
|
129
|
+
const error = new Error(cause.message); error.code = cause.code;
|
|
130
|
+
error.execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', error: cause.message, process: result, evidence: [], effects: 'unknown' }; throw error;
|
|
131
|
+
}
|
|
132
|
+
const sub = subject(owner, result); const item = evidence(owner, result, sub);
|
|
133
|
+
return { schemaVersion: RUNNER_SCHEMA, status: !owner.signing && result.status === 'succeeded' ? 'incomplete' : result.status,
|
|
134
|
+
planSha256: owner.planFile.sha256, approvalSha256: owner.approval.sha256, approvedBy: owner.approval.approval.approvedBy,
|
|
135
|
+
subject: sub, process: result, evidence: [item], receipt: item.receipt ?? null,
|
|
136
|
+
...(owner.signing ? { receiptPublicKey: createPublicKey(owner.signing.privateKey).export({ format: 'der', type: 'spki' }).toString('base64') } : {}),
|
|
137
|
+
signingBoundary: owner.signing ? 'configured-signature-only' : 'local-evidence-only' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const RUNNER_CAPABILITIES = Object.freeze({ schemaVersion: RUNNER_SCHEMA, operations: ['capabilities', 'run-approved-plan'], operationSchemas: { capabilities: LOCAL_OPERATION_SCHEMAS.capabilities, 'run-approved-plan': LOCAL_OPERATION_SCHEMAS['run-approved-plan'] }, limits: RUNNER_LIMITS, boundary: 'POSIX subprocess runner; external approval is required; local evidence remains incomplete without a configured signer.' });
|