@entrorove/skill 0.1.24 → 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/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +5 -0
- package/SKILL.md +7 -1
- package/calctool-runtime-entry.mjs +2 -2
- package/cli.mjs +18 -1
- package/local-archguard-checkpoint.mjs +1 -1
- package/local-archguard-snapshot.mjs +12 -12
- package/local-calctool-cli.mjs +3 -1
- package/local-calctool-runtime.mjs +1 -1
- package/local-calctool.mjs +1 -1
- 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 +23 -15
- package/local-contract-types.mjs +1 -1
- package/local-contracts.mjs +2 -0
- package/local-error.mjs +23 -0
- package/local-execution.mjs +1 -1
- package/local-final-gate16-bundle.mjs +2 -2
- package/local-formula-ast.mjs +23 -14
- package/local-formula-engine.mjs +17 -3
- package/local-formula.mjs +1 -1
- package/local-invocations.mjs +35 -0
- package/local-ledger.mjs +13 -2
- package/local-mutation-authority.mjs +13 -6
- package/local-mutation-binding.mjs +5 -2
- package/local-mutation-files.mjs +29 -4
- package/local-task-handoff-observation.mjs +4 -1
- package/manifest.json +1 -1
- package/package.json +12 -1
- package/release-state.json +38 -28
- package/release.mjs +5 -1
- package/releases.json +6 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +40 -17
package/CAPABILITY-MAP.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
This is the operation-level inventory for the eight legacy domains. `equivalent` means the v2 operation preserves the contract; `narrower` means the operation is available behind a stricter v2 gate or with a smaller side-effect boundary; `uncovered` means the old operation remains intentionally unavailable and callers must receive an explicit unsupported result.
|
|
4
4
|
|
|
5
|
+
## Enforcement boundary (§2.5)
|
|
6
|
+
|
|
7
|
+
The mapped controls apply only to actions executed through Entrorove. They do not control direct shell or operating-system effects performed by an agent or host IDE, including `docker rm`, `DROP DATABASE`, `cp -r`, or arbitrary file writes. Bypass effects may be recorded and can be rejected at delivery or sent back for rework, but they never receive `verified` status and never count as verified delivery. entrorove 管得住“走它的动作”,管不住“不走它的动作”。
|
|
8
|
+
|
|
5
9
|
| Legacy domain | Legacy operations | v2 mapping and semantics |
|
|
6
10
|
| --- | --- | --- |
|
|
7
11
|
| aimlock | capabilities, help, intake, classify, scope-contract, skill-route, propose-nodes, accept-nodes, snapshot-plan, mutate-gate, continuity-check, interrupt, keep-alive, delivery-doc, validate-json, snapshot-verify, run-status, chain-plan, chain-status, feedback | `capabilities/help/intake` → equivalent shared discovery; `classify/skill-route/scope-contract/propose-nodes/accept-nodes/continuity-check/validate-json` → equivalent through `task-describe/message-route`; `snapshot-plan/snapshot-verify/mutate-gate/interrupt/keep-alive/run-status/chain-plan/chain-status/feedback/delivery-doc` → narrower through persisted flow state, local guarded writes, cancellation, status, and evidence gates. |
|
|
@@ -15,4 +19,6 @@ This is the operation-level inventory for the eight legacy domains. `equivalent`
|
|
|
15
19
|
|
|
16
20
|
The local execution surface also covers `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `gate-verify`, `guarded-write`, `budget-init`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, `budget-auto-renew-request`, `budget-auto-renew`, `budget-auto-renew-stop`, `peer-coordinate`, `spawn-bind`, `peer-complete`, `complete`, `lease-verify`, `evidence-sign`, `evidence-verify`, `merge-verified`, `rollback`, `final-gate`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `calculation-final-gate`, `run-approved-plan`, `branch-create`, `branch-list`, `branch-switch`, `diff-report`, `preflight`, `ledger-query`, `chain-init`, `chain-resume`, `chain-status`, `chain-answer`, `branch-create-virtual`, `chain-ledger-init`, `chain-ledger-status`, `chain-ledger-query`, `probe`, `reassess`, `task-describe`, `message-route`, `message-accept`, and `task-resume`. Local branch operations are virtual ledger records and never create Git branches or worktrees.
|
|
17
21
|
|
|
22
|
+
The local CLI reports thrown failures as `entrorove.local-error/1.0` with `schemaVersion`, `status`, `code`, and `message`. Formula contracts require non-empty `add`/`mul` argument lists, reject zero denominators consistently, and carry an explicit `roundingPolicy` (half-away-from-zero, two decimal places by default).
|
|
23
|
+
|
|
18
24
|
The only intentionally uncovered legacy surface is an old notifier or an operation that would execute remote filesystem changes. These are represented as planned or narrowed behavior in capabilities; no adapter may silently substitute a different operation.
|
package/CHANGELOG.md
CHANGED
package/SKILL.md
CHANGED
|
@@ -5,10 +5,14 @@ description: An engineering skill that understands a task, chooses a fitting wor
|
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.25.
|
|
9
9
|
|
|
10
10
|
Read the user's goal and repository instructions first. Preserve the original task through planning, execution and verification. Use the workflow only where it helps; a small, low-risk edit can proceed directly. Never claim a reported step is a verified result.
|
|
11
11
|
|
|
12
|
+
## Enforcement boundary (§2.5)
|
|
13
|
+
|
|
14
|
+
Entrorove governs only actions that pass through its skill entry points. It can bind paths, passes, snapshots, and evidence for those actions, but it does not control direct shell or operating-system side effects run by an agent or its host IDE, such as `docker rm`, `DROP DATABASE`, `cp -r`, or arbitrary file writes. Bypassing the skill may be recorded, rejected at delivery, or require the work to be redone; a bypass side effect never receives `verified` status and never counts as verified delivery. In the contract's terms: entrorove 管得住“走它的动作”,管不住“不走它的动作”。
|
|
15
|
+
|
|
12
16
|
Install or update the managed copy with `npx @entrorove/skill install`. The installer keeps one global managed entrorove skill, replaces its older pristine version, removes only pristine duplicate installations, and preserves hand-edited files. An older package cannot overwrite a newer managed version.
|
|
13
17
|
|
|
14
18
|
Register or sign in by email at https://entrorove.com. Enter the account key once with `npx @entrorove/skill configure` and `{"token":"..."}` on standard input. The credential persists in the current OS user's restricted local store across skill upgrades. Before asking again, use the stored credential; a missing or invalid credential must be reported explicitly. Never place the key in arguments, URLs, project files or logs. entrorove accounts and keys are independent of other services.
|
|
@@ -39,6 +43,8 @@ Use `npx @entrorove/skill status <requestId>` after a connection loss. It perfor
|
|
|
39
43
|
|
|
40
44
|
The local execution layer is available through `npx @entrorove/skill local <operation> [repositoryRoot]` with a JSON object on standard input. `local capabilities` lists the complete contract. Operations include `snapshot-plan`, `snapshot-verify`, `mutate-gate`, `gate-issue`, `gate-verify`, `guarded-write`, `budget-init`, `budget-context`, `budget-read`, `budget-status`, `budget-extend`, budget renewal confirmation, `peer-coordinate`, `spawn-bind`, `peer-complete`, `lease-verify`, `probe`, `reassess`, execution ledger and virtual branch records, `chain-init`, `chain-resume`, `chain-status`, `chain-answer`, `evidence-sign`, `evidence-verify`, `merge-verified`, `rollback`, `formula-evaluate`, `formula-graph`, `formula-final-gate`, `run-approved-plan` and `final-gate`. State is kept only under `~/.entrorove/`; snapshots never create Git branches or worktrees. Guarded writes require a short-lived Ed25519 pass bound to the flow, step, snapshot digest and path set. Local evidence uses `entrorove.test-evidence/2.0`; unsigned local evidence remains incomplete and verified delivery requires a trusted receipt. Approved plan execution requires a frozen plan, matching approval and unchanged files; without a configured runner signer it reports local evidence as incomplete.
|
|
41
45
|
|
|
46
|
+
Local CLI failures use the machine-readable `entrorove.local-error/1.0` envelope `{schemaVersion,status,code,message}` on stderr. `status` is `failed` for invalid input or computation errors and `blocked` for missing mutation, lease, snapshot or handoff authority. Formula `add` and `mul` require at least one argument; `div`, `safeDivide` and `percentOf` reject a zero denominator with `DIV_ZERO`. The formula engine declares `round` as half-away-from-zero with two decimal places by default; an engine may declare another `decimalPlaces` value from 0 through 28 in its `roundingPolicy`.
|
|
47
|
+
|
|
42
48
|
When the user gives an actual evaluation after using the skill, offer to publish that feedback. Only after they explicitly agree to public display under the current notice, send their unchanged words, IDE name and 1–5 rating as JSON on standard input to `npx @entrorove/skill review`, including `invocationRequestId` from their actual successful call, `publicConsent: true` and `noticeVersion: "2026-10-07.1"`. The same account must own that successful call within the last 30 days; a recorded usage event, failed call or unknown result does not qualify. Three public reviews per account per day share one server quota across API replicas. Public output exposes only IDE, rating, original text and time, never account or invocation identity. Never invent, rewrite or infer a review from task completion. The website reads accepted reviews directly from entrorove's independent database.
|
|
43
49
|
|
|
44
50
|
Feedback command example (replace the request ID with the original successful invocation ID, and use only the user’s actual words after explicit public consent):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildFormulaEngine, compileFormulaEngine, validateFormulaEngine, formulaDigest } from './local-formula-engine.mjs';
|
|
2
2
|
import { formulaError } from './local-decimal.mjs';
|
|
3
|
-
import { FORMULA_OPERATORS } from './local-formula-ast.mjs';
|
|
3
|
+
import { FORMULA_OPERATORS, FORMULA_ROUNDING_POLICY } from './local-formula-ast.mjs';
|
|
4
4
|
|
|
5
5
|
const CLOUD_OPERATIONS = ['capabilities', 'help', 'intake', 'validate', 'compile-inline'];
|
|
6
6
|
const LOCAL_OPERATIONS = ['run', 'compile', 'generate', 'verify', 'estimate', 'impact', 'status', 'inventory', 'purge',
|
|
@@ -30,7 +30,7 @@ export function createCalctoolEntrypoint(core) {
|
|
|
30
30
|
const output = await core.run(request, options);
|
|
31
31
|
return { ...output, capabilities: { ...output.capabilities, operations: CLOUD_OPERATIONS, localRunnerOperations: LOCAL_OPERATIONS,
|
|
32
32
|
formulaOperators: FORMULA_OPERATORS, engineSchema: 'engine.spec/1', formulaRuntime: 'decimal28-ast/1.0',
|
|
33
|
-
decimalPolicy: { precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' } },
|
|
33
|
+
decimalPolicy: { precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' }, roundingPolicy: FORMULA_ROUNDING_POLICY },
|
|
34
34
|
localRunner: { command: 'entrorove local calctool-<operation>', requiredFor: LOCAL_OPERATIONS } };
|
|
35
35
|
}
|
|
36
36
|
if (request.operation === 'validate') {
|
package/cli.mjs
CHANGED
|
@@ -288,12 +288,13 @@ async function cleanProjectDuplicates(target, cwd) {
|
|
|
288
288
|
export async function install({ home = os.homedir(), cwd = process.cwd() } = {}) {
|
|
289
289
|
const bundle = await sourceBundle();
|
|
290
290
|
const target = globalInstallPath(home);
|
|
291
|
+
const stale = await agreementStale(home);
|
|
291
292
|
const parent = path.dirname(target);
|
|
292
293
|
await fs.mkdir(parent, { recursive: true, mode: 0o700 });
|
|
293
294
|
return withInstallLock(parent, async () => {
|
|
294
295
|
const updated = await updateGlobalInstall(target, bundle);
|
|
295
296
|
const removed = await cleanProjectDuplicates(target, cwd);
|
|
296
|
-
return { version: bundle.manifest.version, updated, removed };
|
|
297
|
+
return { version: bundle.manifest.version, updated, removed, agreementStale: stale };
|
|
297
298
|
});
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -330,6 +331,22 @@ async function agreementAccepted(home) {
|
|
|
330
331
|
&& Number.isFinite(Date.parse(data.acceptedAt));
|
|
331
332
|
}
|
|
332
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
|
+
|
|
333
350
|
async function writeAgreementAcceptance(data, home) {
|
|
334
351
|
if (Object.keys(data).sort().join(',') !== 'answer,version'
|
|
335
352
|
|| data.version !== agreementVersion || !['accept', 'decline'].includes(data.answer)) {
|
|
@@ -11,7 +11,7 @@ import { signRunnerReceipt } from './local-archguard-signing.mjs';
|
|
|
11
11
|
|
|
12
12
|
async function verifyWriteAuthorization(ctx, input, snapshot) {
|
|
13
13
|
const savedPass = await readJson(input.gatePassPath);
|
|
14
|
-
const verified = await verifyMutationPass({ ...input, repositoryRoot: ctx.root, targetPath: input.path });
|
|
14
|
+
const verified = await verifyMutationPass({ ...input, repositoryRoot: ctx.root, targetPath: input.path }, { allowTargetDrift: true });
|
|
15
15
|
const pass = verified.pass;
|
|
16
16
|
if (canonical(pass) !== canonical(savedPass) || canonical(await readJson(input.gatePassPath)) !== canonical(savedPass)) fail('ARCH_PASS_RACE', 'gate pass changed during verification');
|
|
17
17
|
if (pass.snapshotId !== snapshot.snapshotId || pass.snapshotReceipt.snapshotDigest !== snapshot.snapshotDigest
|
|
@@ -6,7 +6,7 @@ import { snapshotPlan, snapshotVerify } from './local-execution.mjs';
|
|
|
6
6
|
import { context, safeId, safePath, noLinks, canonical, sha256, readBytes, readJson,
|
|
7
7
|
atomicJson, seal, unseal, withExclusiveLock, appendLedger, readLedger, fail } from './local-archguard-state.mjs';
|
|
8
8
|
import { freezeContract, loadContractLock } from './local-archguard-contract.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
|
|
10
10
|
|
|
11
11
|
export const MISSING_SHA256 = sha256('entrorove.missing-file/1.0');
|
|
12
12
|
export function sourcePaths(paths) {
|
|
@@ -27,16 +27,15 @@ export function drift(ledger) {
|
|
|
27
27
|
interrupted: shared.length > 0, ruleIds: shared, consecutiveBlocks: shared.length ? 3 : 0 };
|
|
28
28
|
}
|
|
29
29
|
function snapshotFile(ctx, snapshotId) { return path.join(ctx.directory, 'snapshots', `${safeId(snapshotId, 'snapshotId')}.json`); }
|
|
30
|
-
async function cachedSource(ctx, snapshotId, file) {
|
|
31
|
-
const
|
|
30
|
+
async function cachedSource(ctx, snapshotId, file, copyLayout) {
|
|
31
|
+
const snapshotRoot = path.join(ctx.home, '.entrorove', 'snapshots', ctx.repositoryDigest, ctx.flowId, safeId(snapshotId, 'snapshotId'));
|
|
32
|
+
const copyRoot = copyLayout === 'files/1.0' ? path.join(snapshotRoot, 'files') : snapshotRoot;
|
|
33
|
+
const target = path.join(copyRoot, safePath(file.path));
|
|
32
34
|
await noLinks(target, !file.exists);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
const bytes = await readBudgetedProjectFile({ home: ctx.home, repositoryRoot: ctx.root, flowId: ctx.flowId }, target, { sourcePath: file.path });
|
|
39
|
-
await noLinks(target);
|
|
35
|
+
const manifest = { paths: [safePath(file.path)], files: [file], ...(copyLayout === undefined ? {} : { copyLayout }) };
|
|
36
|
+
const read = await readSnapshotCopy14({ home: ctx.home, repositoryRoot: ctx.root, flowId: ctx.flowId }, snapshotRoot, manifest, file.path);
|
|
37
|
+
if (!read.exists) return null;
|
|
38
|
+
const bytes = read.content;
|
|
40
39
|
if (sha256(bytes) !== file.sha256) fail('ARCH_SNAPSHOT_TAMPERED', 'native snapshot source digest changed');
|
|
41
40
|
return bytes;
|
|
42
41
|
}
|
|
@@ -46,7 +45,8 @@ export async function loadSnapshot(ctx, snapshotId) {
|
|
|
46
45
|
if (snapshot.repositoryDigest !== ctx.repositoryDigest || snapshot.flowId !== ctx.flowId || snapshot.snapshotId !== snapshotId
|
|
47
46
|
|| snapshot.contentDigest !== sha256(canonical(snapshot.files))) fail('ARCH_SNAPSHOT_TAMPERED', 'snapshot is not bound to the repository or content');
|
|
48
47
|
const files = [];
|
|
49
|
-
|
|
48
|
+
const copyLayout = snapshot.receipt?.copyLayout;
|
|
49
|
+
for (const file of snapshot.files) { const bytes = await cachedSource(ctx, snapshotId, file, copyLayout); files.push({ ...file, content: bytes?.toString('base64') ?? null }); }
|
|
50
50
|
return { ...snapshot, files };
|
|
51
51
|
}
|
|
52
52
|
export async function archguardSnapshot(input) {
|
|
@@ -99,7 +99,7 @@ export async function restoreSnapshot(ctx, snapshot) {
|
|
|
99
99
|
}
|
|
100
100
|
const temporary = `${target}.${randomUUID()}.rollback`;
|
|
101
101
|
if (file.exists) {
|
|
102
|
-
const bytes = await cachedSource(ctx, snapshot.snapshotId, file);
|
|
102
|
+
const bytes = await cachedSource(ctx, snapshot.snapshotId, file, snapshot.receipt?.copyLayout);
|
|
103
103
|
if (file.content !== bytes.toString('base64')) fail('ARCH_SNAPSHOT_TAMPERED', 'snapshot content does not match its budgeted native copy');
|
|
104
104
|
await fs.writeFile(temporary, bytes, { flag: 'wx', mode: file.mode });
|
|
105
105
|
}
|
package/local-calctool-cli.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { runCalctoolLocal } from './local-calctool.mjs';
|
|
4
4
|
import { CALCTOOL_LOCAL_OPERATION_SCHEMAS } from './local-calctool-contracts.mjs';
|
|
5
|
+
import { localErrorEnvelope } from './local-error.mjs';
|
|
5
6
|
|
|
6
7
|
export async function runCalctoolCli(args = process.argv.slice(2)) {
|
|
7
8
|
let source = ''; for await (const chunk of process.stdin) source += chunk;
|
|
@@ -17,6 +18,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
|
|
17
18
|
const result = await runCalctoolCli(); process.stdout.write(JSON.stringify(result) + '\n');
|
|
18
19
|
if (['blocked', 'failed', 'incomplete'].includes(result.status)) process.exitCode = 2;
|
|
19
20
|
} catch (cause) {
|
|
20
|
-
|
|
21
|
+
const error = localErrorEnvelope(cause, 'CALCTOOL_INPUT_INVALID');
|
|
22
|
+
process.stderr.write(`${JSON.stringify(error)}\n`); process.exitCode = error.status === 'blocked' ? 2 : 1;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
const runtimeUrl = new URL('./runtime/calctool-runtime-v7.0.43.mjs', import.meta.url);
|
|
4
|
-
const expectedSha256 = '
|
|
4
|
+
const expectedSha256 = '5473427c350411f62c16ac5239fa0db7b96e7b2c19458f9046768dcb4ada87d1';
|
|
5
5
|
export async function calctoolRuntimeBytes() {
|
|
6
6
|
const bytes = await readFile(runtimeUrl);
|
|
7
7
|
if (createHash('sha256').update(bytes).digest('hex') !== expectedSha256) throw Object.assign(new Error('Calctool packaged runtime digest mismatch'), { code: 'FORMULA_RUNTIME_INTEGRITY' });
|
package/local-calctool.mjs
CHANGED
|
@@ -45,7 +45,7 @@ const TOOL_SOURCE = `import { readFile } from 'node:fs/promises';
|
|
|
45
45
|
import { evaluateCompiledProgram, verifyCompiledProgram } from './runtime.mjs';
|
|
46
46
|
let input = ''; for await (const chunk of process.stdin) input += chunk;
|
|
47
47
|
try { const program = JSON.parse(await readFile(new URL('./program.json', import.meta.url), 'utf8')); if(process.argv.includes('--verify')) { const result=verifyCompiledProgram(program); process.stdout.write(JSON.stringify(result)+'\\n'); process.exitCode=result.computationsPassed?0:2; } else { const values = evaluateCompiledProgram(program, JSON.parse(input)); process.stdout.write(JSON.stringify({status:'reported',programDigest:program.programDigest,values})+'\\n'); } }
|
|
48
|
-
catch (error) { process.stderr.write(JSON.stringify({
|
|
48
|
+
catch (error) { const code=error.code??'FORMULA_INPUT_INVALID'; const status=/(?:AUTHORITY_REQUIRED|LEASE_EXPIRED|NOT_ACCEPTED|REVOKED)$/.test(code)?'blocked':'failed'; process.stderr.write(JSON.stringify({schemaVersion:'entrorove.local-error/1.0',status,code,message:error instanceof SyntaxError?'Formula input or artifact must be valid JSON':error.message})+'\\n'); process.exitCode=status==='blocked'?2:1; }
|
|
49
49
|
`;
|
|
50
50
|
async function store(input, generate) {
|
|
51
51
|
const program = await programFrom(input);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { validateLocalInput } from './local-contracts.mjs';
|
|
6
|
+
import { observeWorkflowLocalStep } from './workflow-local-runner.mjs';
|
|
7
|
+
import { validateLocalChainPlan } from './local-chain-plan.mjs';
|
|
8
|
+
|
|
9
|
+
export const CHAIN_PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT = await fs.realpath(CHAIN_PACKAGE_ROOT);
|
|
11
|
+
const SKILLS = ['aimlock', 'archguard', 'blueprint', 'calctool', 'confirm-protocol', 'mergeguard', 'swarm', 'validator'];
|
|
12
|
+
export function chainFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
13
|
+
export function chainCanonical(value) {
|
|
14
|
+
if (Array.isArray(value)) return `[${value.map(chainCanonical).join(',')}]`;
|
|
15
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${chainCanonical(value[key])}`).join(',')}}`;
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
export const chainSha = value => createHash('sha256').update(value).digest('hex');
|
|
19
|
+
export function chainExact(value, required, optional = []) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || required.some(key => !Object.hasOwn(value, key))
|
|
21
|
+
|| Object.keys(value).some(key => !required.includes(key) && !optional.includes(key))) {
|
|
22
|
+
chainFail('ENTROROVE_CHAIN_FIELDS', 'chain input has missing or unknown fields');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function chainControlFile(file, maximum = 8 * 1024 * 1024) {
|
|
26
|
+
const descriptor = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
27
|
+
try {
|
|
28
|
+
const before = await descriptor.stat();
|
|
29
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maximum
|
|
30
|
+
|| (typeof process.getuid === 'function' && before.uid !== process.getuid())) {
|
|
31
|
+
chainFail('ENTROROVE_CHAIN_CONTROL_INVALID', 'chain control must be a bounded owned regular file');
|
|
32
|
+
}
|
|
33
|
+
const bytes = await descriptor.readFile(), after = await descriptor.stat(), current = await fs.lstat(file);
|
|
34
|
+
if (bytes.length !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
35
|
+
|| before.mtimeMs !== after.mtimeMs || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) {
|
|
36
|
+
chainFail('ENTROROVE_CHAIN_CONTROL_CHANGED', 'chain control changed during its actual read');
|
|
37
|
+
}
|
|
38
|
+
return { bytes, stat: after };
|
|
39
|
+
} finally { await descriptor.close(); }
|
|
40
|
+
}
|
|
41
|
+
async function sourceNames() {
|
|
42
|
+
const rootNames = (await fs.readdir(ROOT)).filter(name => name.endsWith('.mjs'));
|
|
43
|
+
const runtimeNames = (await fs.readdir(path.join(ROOT, 'runtime'))).filter(name => name.endsWith('.mjs')).map(name => `runtime/${name}`);
|
|
44
|
+
return [...rootNames, ...runtimeNames, 'package.json', 'manifest.json'].sort();
|
|
45
|
+
}
|
|
46
|
+
export async function captureChainPackage(plan) {
|
|
47
|
+
for (const skill of plan.skills) {
|
|
48
|
+
if (!SKILLS.includes(skill.skillId) || await fs.realpath(skill.packageRoot) !== ROOT) {
|
|
49
|
+
chainFail('ENTROROVE_CHAIN_SKILL_PACKAGE', 'native chain skills must use the current fixed Entrorove package');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const files = [];
|
|
53
|
+
for (const name of await sourceNames()) files.push({ path: name, sha256: chainSha((await chainControlFile(path.join(ROOT, name))).bytes) });
|
|
54
|
+
const metadata = JSON.parse((await chainControlFile(path.join(ROOT, 'package.json'))).bytes.toString('utf8'));
|
|
55
|
+
if (metadata.name !== '@entrorove/skill') chainFail('ENTROROVE_CHAIN_SKILL_PACKAGE', 'installed native package identity is invalid');
|
|
56
|
+
return { packageRoot: ROOT, name: metadata.name, version: metadata.version, files,
|
|
57
|
+
authorityKeyDigest: chainSha(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY ?? '') };
|
|
58
|
+
}
|
|
59
|
+
export async function checkChainPackage(snapshot) {
|
|
60
|
+
const current = await captureChainPackage(snapshot.plan);
|
|
61
|
+
if (chainCanonical(current) !== chainCanonical(snapshot.package)) chainFail('ENTROROVE_CHAIN_PACKAGE_CHANGED', 'installed source, metadata or authority changed after chain initialization');
|
|
62
|
+
}
|
|
63
|
+
export async function validateChainBindings(planValue, values, identity) {
|
|
64
|
+
const plan = validateLocalChainPlan(planValue);
|
|
65
|
+
if (!Array.isArray(values)) chainFail('ENTROROVE_CHAIN_BINDING', 'command bindings must be an explicit array');
|
|
66
|
+
const commands = plan.steps.filter(step => step.kind === 'command'), bindings = [];
|
|
67
|
+
if (values.length !== commands.length) chainFail('ENTROROVE_CHAIN_BINDING', 'every command requires exactly one original signed workflow mapping');
|
|
68
|
+
for (const step of commands) {
|
|
69
|
+
const selected = values.filter(value => value.stepId === step.stepId);
|
|
70
|
+
if (selected.length !== 1) chainFail('ENTROROVE_CHAIN_BINDING', 'command mapping must have a unique original step ID');
|
|
71
|
+
chainExact(selected[0], ['stepId', 'workflowInput']);
|
|
72
|
+
const input = validateLocalInput('workflow-run-step', structuredClone(selected[0].workflowInput));
|
|
73
|
+
const native = input.acceptedPlan.steps.find(value => value.stepId === input.stepId);
|
|
74
|
+
if (input.repositoryRoot !== identity.repositoryRoot || input.home !== identity.home || input.stepId !== step.stepId
|
|
75
|
+
|| input.acceptedPlan.flowId !== identity.flowId || input.acceptedPlan.ownerId !== identity.ownerId
|
|
76
|
+
|| input.lease.stepId !== step.stepId || !native || chainCanonical(native.dependsOn) !== chainCanonical(step.dependsOn)) {
|
|
77
|
+
chainFail('ENTROROVE_CHAIN_BINDING', 'command mapping changed its original home, repository, flow, owner or step dependencies');
|
|
78
|
+
}
|
|
79
|
+
const policy = native.executionPlan.policy;
|
|
80
|
+
if (chainCanonical([step.input.executable, step.input.args, step.input.environment, step.input.timeoutMs, step.input.evidenceKind])
|
|
81
|
+
!== chainCanonical([policy.executable, policy.args, policy.environment, policy.timeoutMs, native.executionPlan.evidencePolicy?.kind ?? 'test'])) {
|
|
82
|
+
chainFail('ENTROROVE_CHAIN_BINDING', 'command policy differs from its actual frozen workflow plan');
|
|
83
|
+
}
|
|
84
|
+
if (path.resolve(identity.repositoryRoot, step.input.workingDirectory) !== identity.repositoryRoot) {
|
|
85
|
+
chainFail('ENTROROVE_CHAIN_COMMAND_CWD_UNSUPPORTED', 'the fixed native workflow runner currently executes only at its original repository root');
|
|
86
|
+
}
|
|
87
|
+
// This real observer verifies the configured Ed25519 plan and lease without
|
|
88
|
+
// starting a process or inventing an execution authority.
|
|
89
|
+
await observeWorkflowLocalStep(input);
|
|
90
|
+
bindings.push({ stepId: step.stepId, workflowInput: input });
|
|
91
|
+
}
|
|
92
|
+
return bindings;
|
|
93
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { chainCanonical as canonical, chainFail as fail } from './local-chain-bindings.mjs';
|
|
3
|
+
import { inspectLocalChainResult } from './local-chain-plan.mjs';
|
|
4
|
+
|
|
5
|
+
const STAGES = Object.freeze({ 'workflow-gate': 'aimlock', 'workflow-contract': 'aimlock',
|
|
6
|
+
'workflow-plan': 'blueprint', 'workflow-dispatch': 'swarm', 'workflow-verdict': 'validator' });
|
|
7
|
+
function nativeSchema(runtime, operation) {
|
|
8
|
+
const schemas = runtime.operationSchemas;
|
|
9
|
+
const selected = Array.isArray(schemas) ? schemas.find(value => value.operation === operation) : schemas?.[operation];
|
|
10
|
+
return selected?.inputSchema ?? selected?.input ?? selected;
|
|
11
|
+
}
|
|
12
|
+
export async function captureChainCloud(plan, client) {
|
|
13
|
+
if (!plan.skills.length) return null;
|
|
14
|
+
const output = await client.invoke('capabilities', {}), capabilities = output.capabilities;
|
|
15
|
+
if (output.status !== 'succeeded' || !capabilities?.operationSchemas?.continue
|
|
16
|
+
|| !Array.isArray(capabilities.runtimes)) fail('ENTROROVE_CHAIN_DISCOVERY', 'original cloud discovery is incomplete');
|
|
17
|
+
const runtimes = plan.skills.map(skill => {
|
|
18
|
+
const matches = capabilities.runtimes.filter(runtime => runtime.slug === skill.skillId);
|
|
19
|
+
if (matches.length !== 1 || typeof matches[0].version !== 'string' || typeof matches[0].endpoint !== 'string') {
|
|
20
|
+
fail('ENTROROVE_CHAIN_DISCOVERY', 'declared native skill has no unique original runtime metadata');
|
|
21
|
+
}
|
|
22
|
+
return structuredClone(matches[0]);
|
|
23
|
+
});
|
|
24
|
+
return { runtimes, operationSchemas: structuredClone(capabilities.operationSchemas),
|
|
25
|
+
workflowOperations: structuredClone(capabilities.workflowOperations ?? null) };
|
|
26
|
+
}
|
|
27
|
+
export async function checkChainCloud(state, client) {
|
|
28
|
+
if (canonical(await captureChainCloud(state.plan, client)) !== canonical(state.cloud)) {
|
|
29
|
+
fail('ENTROROVE_CHAIN_DISCOVERY_CHANGED', 'original cloud runtime or operation metadata changed after initialization');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function checkChainSkillDispatch(original, state, resumed, input) {
|
|
33
|
+
const next = resumed.nextStep;
|
|
34
|
+
if (resumed.canContinue !== true || next?.operation !== 'continue'
|
|
35
|
+
|| next.input?.flowId !== state.identity.flowId || next.input.operation !== original.operation) {
|
|
36
|
+
fail('ENTROROVE_CHAIN_STAGE_BINDING', 'declared skill must be the exact next operation of its original workflow');
|
|
37
|
+
}
|
|
38
|
+
const slug = STAGES[original.operation] ?? resumed.route?.slug;
|
|
39
|
+
if (slug !== original.skillId) fail('ENTROROVE_CHAIN_SKILL_BINDING', 'declared skill differs from the original native workflow stage');
|
|
40
|
+
const runtime = state.cloud.runtimes.find(value => value.slug === original.skillId);
|
|
41
|
+
const schema = state.cloud.workflowOperations?.operationSchemas?.[original.operation] ?? nativeSchema(runtime, original.operation);
|
|
42
|
+
if (schema) {
|
|
43
|
+
const validate = new Ajv({ strict: true, strictRequired: false, allErrors: true }).compile(schema);
|
|
44
|
+
if (!validate(input)) fail('ENTROROVE_CHAIN_SKILL_INPUT', 'bound input violates its fixed original operation schema');
|
|
45
|
+
}
|
|
46
|
+
// Native operations without public nested schemas remain server-validated on
|
|
47
|
+
// the exact original active route; no arbitrary endpoint is opened here.
|
|
48
|
+
}
|
|
49
|
+
export function inspectChainSkillResponse(original, state, response) {
|
|
50
|
+
if (response.flowId !== state.identity.flowId || (response.taskId !== undefined && response.taskId !== state.identity.taskId)) {
|
|
51
|
+
fail('ENTROROVE_CHAIN_RESULT_BINDING', 'skill result lost its original flow or task identity');
|
|
52
|
+
}
|
|
53
|
+
if (!response.result || typeof response.result !== 'object' || Array.isArray(response.result)) {
|
|
54
|
+
fail('ENTROROVE_CHAIN_RESULT_BINDING', 'skill result must retain its actual native structured output');
|
|
55
|
+
}
|
|
56
|
+
const native = response.result;
|
|
57
|
+
if (native.source === 'needs-clarification' || native.intake?.complete === false) {
|
|
58
|
+
return { state: 'waiting', code: 'ENTROROVE_CHAIN_CLARIFICATION_REQUIRED', eligible: false };
|
|
59
|
+
}
|
|
60
|
+
return inspectLocalChainResult(original, response);
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WORKFLOW_LOCAL_OPERATION_SCHEMA } from './workflow-local-contracts.mjs';
|
|
2
|
+
|
|
3
|
+
const id = { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' };
|
|
4
|
+
const text = { type: 'string', minLength: 1 };
|
|
5
|
+
const object = (properties, required = Object.keys(properties)) => ({ type: 'object', additionalProperties: false, properties, required });
|
|
6
|
+
const array = items => ({ type: 'array', items });
|
|
7
|
+
const pointer = { type: 'string', pattern: '^(?:/(?:[^~]|~[01])*)*$' };
|
|
8
|
+
const step = object({ stepId: id, kind: { enum: ['skill', 'coordinator', 'command'] },
|
|
9
|
+
skillId: { anyOf: [id, { type: 'null' }] }, operation: id, input: { type: 'object' }, dependsOn: array(id),
|
|
10
|
+
bindings: array(object({ stepId: id, source: pointer, target: { ...pointer, minLength: 1 } })),
|
|
11
|
+
continueWhen: object({ answer: { anyOf: [text, { ...array(text), minItems: 1, uniqueItems: true }] } }),
|
|
12
|
+
}, ['stepId', 'kind', 'skillId', 'operation', 'input', 'dependsOn', 'bindings']);
|
|
13
|
+
export const LOCAL_CHAIN_PLAN_INPUT_SCHEMA = object({ schemaVersion: { const: 'entrorove.execution-plan/1.0' }, chainId: id,
|
|
14
|
+
skills: array(object({ skillId: id, packageRoot: text })), steps: { ...array(step), minItems: 1 } });
|
|
15
|
+
const identity = { repositoryRoot: text, home: text, chainId: id };
|
|
16
|
+
export const LOCAL_CHAIN_OPERATION_SCHEMAS = Object.freeze({
|
|
17
|
+
'chain-init': object({ repositoryRoot: text, home: text, flowId: id, ownerId: id, taskId: id,
|
|
18
|
+
plan: LOCAL_CHAIN_PLAN_INPUT_SCHEMA, commandBindings: array(object({ stepId: id, workflowInput: WORKFLOW_LOCAL_OPERATION_SCHEMA })) }),
|
|
19
|
+
'chain-resume': object(identity), 'chain-status': object(identity),
|
|
20
|
+
'chain-answer': object({ ...identity, actorId: id }),
|
|
21
|
+
});
|