@entrorove/skill 0.1.23 → 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.
- package/AGREEMENT.md +6 -6
- package/CHANGELOG.md +5 -0
- package/SKILL.md +12 -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 +17 -82
- 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 +22 -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-cli.mjs +49 -16
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +124 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -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 +97 -0
- package/local-formula-engine.mjs +111 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +107 -0
- package/local-ledger.mjs +2 -7
- 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 +96 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +86 -0
- package/local-mutation-files.mjs +245 -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 +38 -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 +153 -3
- package/release-state.json +153 -17
- package/release.mjs +238 -4
- package/releases.json +6 -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 +1887 -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
|
}
|
|
@@ -313,7 +303,8 @@ async function stdinJson() {
|
|
|
313
303
|
raw += chunk;
|
|
314
304
|
if (raw.length > 1_000_000) throw new Error('Input is too large');
|
|
315
305
|
}
|
|
316
|
-
|
|
306
|
+
let value;
|
|
307
|
+
try { value = JSON.parse(raw); } catch { throw new Error('Input must be valid JSON'); }
|
|
317
308
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Input must be a JSON object');
|
|
318
309
|
return value;
|
|
319
310
|
}
|
|
@@ -325,38 +316,6 @@ async function preparePrivateDirectory(parent) {
|
|
|
325
316
|
if (process.platform !== 'win32') await fs.chmod(parent, 0o700);
|
|
326
317
|
}
|
|
327
318
|
|
|
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
319
|
async function agreementAccepted(home) {
|
|
361
320
|
const file = agreementPath(home);
|
|
362
321
|
const stat = await statOrNull(file);
|
|
@@ -398,9 +357,9 @@ async function writeAgreementAcceptance(data, home) {
|
|
|
398
357
|
|
|
399
358
|
export { buildInvocation };
|
|
400
359
|
|
|
401
|
-
function remoteDependencies() {
|
|
360
|
+
export function remoteDependencies(home = os.homedir()) {
|
|
402
361
|
return {
|
|
403
|
-
install, agreementAccepted, agreementRequired, readCredential, home
|
|
362
|
+
install: () => install({ home }), agreementAccepted, agreementRequired, readCredential, home,
|
|
404
363
|
apiOrigin: () => new URL(apiUrl).origin,
|
|
405
364
|
};
|
|
406
365
|
}
|
|
@@ -409,37 +368,11 @@ async function invoke(data) {
|
|
|
409
368
|
return invokeRemote(data, remoteDependencies());
|
|
410
369
|
}
|
|
411
370
|
|
|
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
|
-
}
|
|
371
|
+
export { buildReview };
|
|
429
372
|
|
|
430
373
|
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
|
-
}
|
|
374
|
+
const result = await submitSkillFeedback(data, remoteDependencies());
|
|
375
|
+
if (result.error === 'agreement-required') process.exitCode = 2;
|
|
443
376
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
444
377
|
}
|
|
445
378
|
|
|
@@ -458,6 +391,8 @@ async function main() {
|
|
|
458
391
|
process.stdout.write(JSON.stringify({ accepted, version: agreementVersion }) + '\n');
|
|
459
392
|
} else if (command === 'invoke' && args.length === 0) {
|
|
460
393
|
await invoke(await stdinJson());
|
|
394
|
+
} else if (command === 'host-message') {
|
|
395
|
+
await runTaskMessageCli(args, await stdinJson(), remoteDependencies());
|
|
461
396
|
} else if (command === 'review' && args.length === 0) {
|
|
462
397
|
await submitReview(await stdinJson());
|
|
463
398
|
} else if (command === 'status' && args.length === 1) {
|
|
@@ -479,12 +414,12 @@ async function main() {
|
|
|
479
414
|
} else if (command === 'local') {
|
|
480
415
|
await runLocal(args);
|
|
481
416
|
} else {
|
|
482
|
-
throw new Error('Usage: entrorove install | configure | agreement | invoke | status <requestId> | flows | review | local (JSON on stdin)');
|
|
417
|
+
throw new Error('Usage: entrorove install | configure | agreement | invoke | host-message [adapterModulePath] | status <requestId> | flows | review | local (JSON on stdin)');
|
|
483
418
|
}
|
|
484
419
|
}
|
|
485
420
|
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
486
421
|
main().catch((error) => {
|
|
487
|
-
process.stderr.write(`${error.message}\n`);
|
|
422
|
+
process.stderr.write(`${error instanceof SyntaxError ? 'Invalid JSON data' : error.message}\n`);
|
|
488
423
|
process.exitCode = 1;
|
|
489
424
|
});
|
|
490
425
|
}
|
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.' });
|