@entrorove/skill 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGREEMENT.md +6 -6
- package/CHANGELOG.md +10 -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 +18 -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 +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 +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 +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/AGREEMENT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# entrorove 使用、隐私与广告信息协议
|
|
2
2
|
|
|
3
|
-
版本:2026-10-
|
|
3
|
+
版本:2026-10-07.1
|
|
4
4
|
|
|
5
5
|
本协议是首次使用 entrorove 技能时的合并告知与使用条款。只有用户明确选择“同意”后,技能请求才会发送。IDE 的“完全访问”权限只是本地权限,不代表同意本协议。
|
|
6
6
|
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
- 安装包不会因安装本身自动扫描或上传项目文件。只有在发起技能调用时,才会向 entrorove API 发送 IDE 明确组装的请求负载。
|
|
16
16
|
- 账号服务会处理登录邮箱、密钥哈希与前缀、加密保存的完整密钥、会话与安全记录、调用时间/数量,以及用户自愿提交的留言。已登录用户可再次复制自己的完整密钥。
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
- IDE 评价须绑定同一账户最近 30 天内至少一次真实成功的技能调用,且用户另行明确同意按本版本告知公开其原文。服务保存原调用身份、结果摘要、用户原文、公开同意和时间;公开列表仅展示 IDE 名称、评分、原文和时间,最多最近 30 条,不展示账户、邮箱、调用身份或同意回执。每个账户每日最多提交 3 条。
|
|
18
|
+
- 服务会持久保存明确提交的任务、需求、消息、工作流计划、合同、执行状态和验证证据,以便恢复及核对结果。明确提交的范围快照或审查证据可能包含源文件内容或副本;请只提交任务所需内容,避免密钥和其他不必要的私人数据。不出售个人信息。推广选择使用密钥的连续调用时长和调用次数判断展示时机,不分析项目内容或建立跨站画像。
|
|
19
19
|
- 本地同意回执仅保存协议版本、同意结果和时间,不包含邮箱、项目内容或密钥。
|
|
20
20
|
|
|
21
21
|
## 3. 消息与广告
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
|
|
42
42
|
# entrorove Terms, Privacy and Advertising Notice
|
|
43
43
|
|
|
44
|
-
Version: 2026-10-
|
|
44
|
+
Version: 2026-10-07.1
|
|
45
45
|
|
|
46
46
|
This combined notice applies on first use. A skill request is sent only after an explicit **Accept** choice. Granting an IDE “full access” permission does not accept this agreement.
|
|
47
47
|
|
|
48
48
|
- **Use and access.** entrorove acts only on tasks, context and permissions supplied by the user and IDE. Elevated permissions may permit file changes, command execution and external calls. Review scope, keep backups and separately confirm consequential actions.
|
|
49
|
-
- **Data.** Installation alone does not scan or upload project files. The account service processes login email, key hashes and prefixes, an encrypted copy of the full key that a signed-in user can copy again, sessions and security records, invocation time/count, and messages submitted voluntarily.
|
|
50
|
-
- **Public reviews.** An IDE review
|
|
49
|
+
- **Data.** Installation alone does not scan or upload project files. The account service processes login email, key hashes and prefixes, an encrypted copy of the full key that a signed-in user can copy again, sessions and security records, invocation time/count, and messages submitted voluntarily. Submitted tasks, requirements, messages, workflow plans, contracts, execution states and verification evidence are persisted for recovery and result checks. Scope snapshots or review evidence explicitly submitted for a task may contain source contents or copies. Submit only necessary material and avoid credentials or unnecessary private data. It does not sell personal information. Promotion timing uses consecutive invocation duration and count for the same key, without examining project content or building a cross-site profile.
|
|
50
|
+
- **Public reviews.** An IDE review requires at least one actual successful skill invocation by the same account within the last 30 days, and a separate explicit consent to public display under this notice version. The service saves the original invocation identity, result digest, unchanged user text, consent and time. The public list shows only the IDE name, rating, original text and timestamp, limited to the latest 30 entries; account details, email, invocation identity and consent receipts stay private. Each account may submit at most three reviews per day.
|
|
51
51
|
- **Advertising.** A configured message may appear beneath a long-running-task notice. Commercial content carries its configured category label as `【Category】` and includes an HTTPS link. Administrators set active dates, task-duration windows, selection weights and probability bounds. The service adjusts display probability using consecutive invocation duration and count, without inspecting task text or project files. Third-party links are governed by their operators.
|
|
52
52
|
- **Risk.** The skill is provided as available without a guarantee that every result is correct, complete, uninterrupted or fit for a particular purpose. Review and test changes before production use. To the maximum extent permitted by applicable law, entrorove excludes indirect, incidental, special and consequential losses. Nothing excludes non-waivable liability, fraud, wilful misconduct or mandatory consumer rights.
|
|
53
53
|
- **Choice.** Declining prevents the skill request from being sent. Material changes require a new explicit acceptance. Mandatory local laws may provide additional rights. Qualified counsel should review this notice before production release for the actual operating and user jurisdictions.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.24 — 2026-10-07
|
|
4
|
+
|
|
5
|
+
- 更新已验收能力并完成发布前校验。
|
|
6
|
+
- Updated accepted capabilities and completed release preflight validation.
|
|
7
|
+
|
|
8
|
+
## 0.1.23 — 2026-10-06
|
|
9
|
+
|
|
10
|
+
- 修复受管安装漏装能力映射文档的问题;旧安装自动补齐,用户编辑仍受保护。
|
|
11
|
+
- Fixed the missing capability map in managed installations; older installations are repaired while user edits remain protected.
|
|
12
|
+
|
|
3
13
|
## 0.1.22 — 2026-10-04
|
|
4
14
|
|
|
5
15
|
- 完整补齐隔离检测、运行时生成、基线契约、云任务取消与事件流、能力覆盖映射及隐私告知。
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ 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.24.
|
|
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
|
|
|
@@ -13,11 +13,13 @@ Install or update the managed copy with `npx @entrorove/skill install`. The inst
|
|
|
13
13
|
|
|
14
14
|
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.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Every invocation rereads that same credential file, including clients already running. After an authenticated account page supplies the current key and `configure` synchronizes it once, all tasks under the same OS user automatically use the new value. A revoked or expired key cannot obtain new permissions for itself: return to the authenticated account page and synchronize once through JSON stdin. Status recovery only queries the original request; it never rotates credentials or repeats the operation.
|
|
17
|
+
|
|
18
|
+
On first use or after a material agreement revision, the CLI returns a Confirm Protocol-compatible agreement interaction. Render it with the IDE's native confirmation UI, or show its `chatFallback` text when no adapter exists. The user must explicitly accept with `npx @entrorove/skill agreement` and JSON `{"version":"2026-10-07.1","answer":"accept"}` on standard input before retrying the skill request. A full-access IDE permission never counts as agreement acceptance. The combined terms, privacy and advertising notice is installed as `AGREEMENT.md`.
|
|
17
19
|
|
|
18
20
|
## Work with a task
|
|
19
21
|
|
|
20
|
-
Send JSON on standard input to `npx @entrorove/skill invoke`.
|
|
22
|
+
Send JSON on standard input to `npx @entrorove/skill invoke`. Before every execution call, discover `operation: "capabilities"` with `{}` and validate input against the returned operation schema. The CLI performs remote discovery automatically; local adapters discover the installed contract before execution. Discovery failure stops execution. Adapters that expose the four-step task route can call `task-describe`, `message-route`, `message-accept` and `task-resume` in order; `start` remains the one-call workflow equivalent. Then start with `operation: "start"` and an `input.task` containing the goal, work type, risk, difficulty and observed scope facts. Work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision` and `diagnosis`. Assess facts from the real task and repository; do not invent certainty. entrorove chooses the workflow. A small task may return `decision: "direct"`, meaning the IDE should do the work without another service call. The public envelope is `entrorove.skill.request/2.0`; `requestId` is the SHA-256 digest of `operation + canonical(input)`. Version 1.0 remains accepted for older callers.
|
|
21
23
|
|
|
22
24
|
```json
|
|
23
25
|
{"operation":"start","input":{"task":{"goal":"Update the checkout flow and verify it","workType":"implementation","risk":"medium","difficulty":"medium","crossModule":true,"parallel":false,"unclear":false,"architectureContract":false}}}
|
|
@@ -37,4 +39,10 @@ Use `npx @entrorove/skill status <requestId>` after a connection loss. It perfor
|
|
|
37
39
|
|
|
38
40
|
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.
|
|
39
41
|
|
|
40
|
-
When the user gives an actual evaluation after using the skill, offer to publish that feedback. Only after they explicitly agree to public display, send their
|
|
42
|
+
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
|
+
|
|
44
|
+
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):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{"invocationRequestId":"<original successful request ID>","ide":"Codex","rating":5,"comment":"<unchanged user words>","publicConsent":true,"noticeVersion":"2026-10-07.1"}
|
|
48
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
const file = 'archguard-runtime-v7.0.44.mjs';
|
|
4
|
+
const expected = '836f20b270139371bef43e3934fa7232371e42d46371c513db2a5e3c5622b215';
|
|
5
|
+
const source = await readFile(new URL(`../../assets/runtime-source/${file}`, import.meta.url));
|
|
6
|
+
if (createHash('sha256').update(source).digest('hex') !== expected) throw new Error('Archguard pinned generation source digest mismatch');
|
|
7
|
+
const target = new URL(`./runtime/${file}`, import.meta.url);
|
|
8
|
+
if (process.argv.includes('--check')) {
|
|
9
|
+
const actual = await readFile(target);
|
|
10
|
+
if (!source.equals(actual)) throw new Error('Packaged Archguard runtime differs from its pinned first-party generation source');
|
|
11
|
+
} else {
|
|
12
|
+
await mkdir(new URL('./runtime/', import.meta.url), { recursive: true });
|
|
13
|
+
await writeFile(target, source);
|
|
14
|
+
}
|
|
15
|
+
process.stdout.write(`${file} ${expected}\n`);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { sign } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { canonical, sha256 as sha } from './local-integrity.mjs';
|
|
5
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
6
|
+
import { atomic, json, context } from './local-task-handoff-storage.mjs';
|
|
7
|
+
import { loadTaskHandoffSigner } from './local-task-handoff-observation.mjs';
|
|
8
|
+
import { consumeTaskHandoffExecutionAuthority, beginTaskHandoffExecution, finishTaskHandoffExecution,
|
|
9
|
+
readTaskHandoffFence, withTaskHandoffWriteFence } from './local-task-handoff-fence.mjs';
|
|
10
|
+
import { runApprovedPlanObject } from './local-approved-plan.mjs';
|
|
11
|
+
import { verifyBlueprintCriterionAttempt, blueprintCriterionSubject } from './blueprint-criterion-wire.mjs';
|
|
12
|
+
|
|
13
|
+
function fail(message) { throw Object.assign(new Error(message), { code: 'ENTROROVE_BLUEPRINT_CRITERION_STATE' }); }
|
|
14
|
+
async function prepare(input, allowExpired = false) {
|
|
15
|
+
verifyBlueprintCriterionAttempt(input.attempt, allowExpired);
|
|
16
|
+
const ctx = await context({ repositoryRoot: input.repositoryRoot, home: input.home, flowId: input.attempt.flowId }, true);
|
|
17
|
+
if (ctx.repo !== input.attempt.localRepositoryDigest) fail('criterion repository differs from its actual signed runner identity');
|
|
18
|
+
const home = await fs.realpath(ctx.home), directory = path.join(home, '.entrorove', 'blueprint-criterion', ctx.repo, ctx.flowId, input.attempt.attemptId);
|
|
19
|
+
let current = home;
|
|
20
|
+
for (const part of path.relative(home, directory).split(path.sep)) {
|
|
21
|
+
current = path.join(current, part);
|
|
22
|
+
try { await fs.mkdir(current, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
23
|
+
const value = await fs.lstat(current);
|
|
24
|
+
if (!value.isDirectory() || value.isSymbolicLink() || (value.mode & 0o077)
|
|
25
|
+
|| (typeof process.getuid === 'function' && value.uid !== process.getuid())) fail('criterion state requires its real original owned private execution home');
|
|
26
|
+
}
|
|
27
|
+
return { ...ctx, home, file: path.join(directory, 'state.json'), attempt: input.attempt };
|
|
28
|
+
}
|
|
29
|
+
async function read(ctx) {
|
|
30
|
+
let state; try { state = await json(ctx.file, 8 * 1024 * 1024); }
|
|
31
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
32
|
+
const stat = await fs.lstat(ctx.file);
|
|
33
|
+
if ((stat.mode & 0o777) !== 0o600 || stat.nlink !== 1 || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
34
|
+
|| state.schemaVersion !== 'entrorove.blueprint-criterion-local-state/1.0' || state.attemptDigest !== sha(canonical(ctx.attempt))
|
|
35
|
+
|| state.attemptId !== ctx.attempt.attemptId || state.executionHome !== ctx.home || state.flowId !== ctx.flowId
|
|
36
|
+
|| state.originalBusinessStepId !== ctx.attempt.originalBusinessStepId) fail('criterion local state lost its original signed attempt or execution home');
|
|
37
|
+
if (state.result && state.resultDigest !== sha(canonical(state.result))) fail('criterion actual result digest is invalid');
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
export async function consumeBlueprintCriterionExecutionAuthority(input) {
|
|
41
|
+
const ctx = await prepare(input), attempt = ctx.attempt;
|
|
42
|
+
return consumeTaskHandoffExecutionAuthority({ repositoryRoot: ctx.root, home: ctx.home, flowId: ctx.flowId,
|
|
43
|
+
taskId: attempt.taskId, leaseId: attempt.lease.leaseId, generation: 1, attemptNonce: attempt.lease.attemptNonce,
|
|
44
|
+
authority: attempt.executionAuthority });
|
|
45
|
+
}
|
|
46
|
+
async function claim(ctx) {
|
|
47
|
+
return withStateLock(ctx.file, async () => {
|
|
48
|
+
const prior = await read(ctx);
|
|
49
|
+
if (prior) {
|
|
50
|
+
if (!['reported', 'failed'].includes(prior.status) || !prior.result) fail('criterion result is running or unknown; reconcile it without a new process');
|
|
51
|
+
return { ...structuredClone(prior.result), cached: true };
|
|
52
|
+
}
|
|
53
|
+
verifyBlueprintCriterionAttempt(ctx.attempt);
|
|
54
|
+
await atomic(ctx.file, { schemaVersion: 'entrorove.blueprint-criterion-local-state/1.0', attemptId: ctx.attempt.attemptId,
|
|
55
|
+
attemptDigest: sha(canonical(ctx.attempt)), executionHome: ctx.home, flowId: ctx.flowId,
|
|
56
|
+
originalBusinessStepId: ctx.attempt.originalBusinessStepId, status: 'running', pid: process.pid,
|
|
57
|
+
startedAt: new Date().toISOString(), finishedAt: null, result: null, resultDigest: null });
|
|
58
|
+
return null;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function signedResult(ctx, execution, signer) {
|
|
62
|
+
const attempt = ctx.attempt, processResult = { ...execution.process, spawned: execution.process.started === true,
|
|
63
|
+
executable: attempt.executionPlan.policy.executable, args: attempt.executionPlan.policy.args };
|
|
64
|
+
if (!Number.isInteger(processResult.exitCode) || !processResult.spawned) fail('criterion actual process outcome is interrupted or unknown');
|
|
65
|
+
const evidence = { schemaVersion: 'entrorove.test-evidence/2.0', evidenceId: attempt.evidenceId, kind: 'test', runner: 'trusted-runner',
|
|
66
|
+
command: attempt.command, artifactSha256: attempt.artifactSha256, exitCode: processResult.exitCode,
|
|
67
|
+
durationMs: processResult.durationMs, summary: processResult.stdout.trim().slice(0, 4_000) || `Actual criterion exited ${processResult.exitCode}` };
|
|
68
|
+
const subject = blueprintCriterionSubject(attempt, evidence, processResult), issuedAt = new Date().toISOString();
|
|
69
|
+
const core = { schemaVersion: 'validator.execution-receipt/1.0', keyId: attempt.runnerKeyId, subjectDigest: sha(canonical(subject)),
|
|
70
|
+
nonce: attempt.lease.attemptNonce, issuedAt, expiresAt: new Date(Date.now() + signer.config.receiptTtlMs).toISOString(),
|
|
71
|
+
result: { runner: 'trusted-runner', passed: processResult.exitCode === 0 && processResult.status === 'succeeded',
|
|
72
|
+
exitCode: processResult.exitCode, durationMs: evidence.durationMs, summary: evidence.summary } };
|
|
73
|
+
const receipt = { ...core, signature: sign(null, Buffer.from(canonical(core)), signer.privateKey).toString('base64url') };
|
|
74
|
+
return { status: core.result.passed ? 'reported' : 'failed', cached: false, process: processResult,
|
|
75
|
+
evidence: { ...evidence, subject, subjectDigest: core.subjectDigest, receipt } };
|
|
76
|
+
}
|
|
77
|
+
async function finalize(ctx, result) {
|
|
78
|
+
return withStateLock(ctx.file, async () => {
|
|
79
|
+
const state = await read(ctx);
|
|
80
|
+
if (state?.status !== 'running' || state.pid !== process.pid) fail('criterion actual running ownership changed');
|
|
81
|
+
await atomic(ctx.file, { ...state, status: result.status, finishedAt: result.process.finishedAt,
|
|
82
|
+
result, resultDigest: sha(canonical(result)) });
|
|
83
|
+
return result;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export async function runBlueprintCriterionAttempt(input) {
|
|
87
|
+
const ctx = await prepare(input, true), attempt = ctx.attempt;
|
|
88
|
+
const original = { repositoryRoot: ctx.root, home: ctx.home, flowId: ctx.flowId };
|
|
89
|
+
const fence = { ...original, epoch: (await readTaskHandoffFence(original)).epoch, leaseId: attempt.lease.leaseId };
|
|
90
|
+
const cached = await withTaskHandoffWriteFence(fence, () => claim(ctx));
|
|
91
|
+
if (cached) return cached;
|
|
92
|
+
let activity;
|
|
93
|
+
try {
|
|
94
|
+
activity = await beginTaskHandoffExecution({ ...fence, stepId: attempt.criterionStepId, generation: 1,
|
|
95
|
+
attemptNonce: attempt.lease.attemptNonce, executionId: `criterion-run-${attempt.attemptId}` });
|
|
96
|
+
if (canonical(activity.authority) !== canonical(attempt.executionAuthority)) fail('criterion running authority differs from its formal server attempt');
|
|
97
|
+
const signer = await loadTaskHandoffSigner(ctx, input.signerConfigPath, attempt.runnerKeyId);
|
|
98
|
+
const execution = await runApprovedPlanObject({ repositoryRoot: ctx.root, executionPlan: attempt.executionPlan,
|
|
99
|
+
approvalPath: input.approvalPath, signerConfigPath: input.signerConfigPath,
|
|
100
|
+
executionDeadlineAt: attempt.expiresAt });
|
|
101
|
+
// The approved runner checks this signed deadline immediately before its actual spawn, after all control/manifest IO.
|
|
102
|
+
return await finalize(ctx, signedResult(ctx, execution, signer));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
await withStateLock(ctx.file, async () => {
|
|
105
|
+
const state = await read(ctx);
|
|
106
|
+
if (state?.status === 'running' && state.pid === process.pid) await atomic(ctx.file, { ...state, status: 'unknown' });
|
|
107
|
+
});
|
|
108
|
+
throw error;
|
|
109
|
+
} finally { if (activity) await finishTaskHandoffExecution({ ...fence, ...activity }); }
|
|
110
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { canonical, sha256 as sha } from './local-integrity.mjs';
|
|
3
|
+
|
|
4
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
5
|
+
const SHA = /^[a-f0-9]{64}$/;
|
|
6
|
+
const FIELDS = ['schemaVersion', 'attemptId', 'flowId', 'blueprintSha256', 'compilationReceiptDigest', 'compilationReceiptRevision',
|
|
7
|
+
'planDigest', 'scopeArtifactDigest', 'criterionId', 'taskRef', 'ownerId', 'taskId', 'taskDigest', 'chainDigest', 'chainRevision',
|
|
8
|
+
'blueprintId', 'blueprintRevision', 'nodeRefs', 'criterionStepId', 'originalBusinessStepId', 'businessEvidenceDigest',
|
|
9
|
+
'businessVerifiedAt', 'runnerKeyId', 'localRepositoryDigest', 'executionPlan', 'executionPlanSha256', 'command',
|
|
10
|
+
'artifactSha256', 'verifiedDependencies', 'evidenceId', 'lease', 'executionAuthority', 'issuedAt', 'expiresAt'];
|
|
11
|
+
function fail(message) { throw Object.assign(new Error(message), { code: 'ENTROROVE_BLUEPRINT_CRITERION_AUTHORITY' }); }
|
|
12
|
+
function exact(value, fields) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
14
|
+
|| Object.keys(value).sort().join('|') !== [...fields].sort().join('|')) fail('criterion authority has invalid exact fields');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function signed(value, fields) {
|
|
18
|
+
const { authorityReceipt: receipt, ...body } = exact(value, [...fields, 'authorityReceipt']);
|
|
19
|
+
exact(receipt, ['schemaVersion', 'keyId', 'payloadDigest', 'signature']);
|
|
20
|
+
const encoded = process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY, bytes = Buffer.from(encoded ?? '', 'base64');
|
|
21
|
+
let key; try { key = createPublicKey({ key: bytes, type: 'spki', format: 'der' }); } catch { fail('criterion cloud verification key is unavailable'); }
|
|
22
|
+
const signature = Buffer.from(receipt.signature ?? '', 'base64url');
|
|
23
|
+
if (key.asymmetricKeyType !== 'ed25519' || key.export({ type: 'spki', format: 'der' }).toString('base64') !== encoded
|
|
24
|
+
|| receipt.schemaVersion !== 'entrorove.workflow-authority-receipt/1.0' || receipt.keyId !== sha(bytes)
|
|
25
|
+
|| receipt.payloadDigest !== sha(canonical(body)) || signature.length !== 64 || signature.toString('base64url') !== receipt.signature
|
|
26
|
+
|| !verify(null, Buffer.from(canonical(body)), key, signature)) fail('criterion cloud signature or exact binding is invalid');
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
export function verifyBlueprintCriterionAttempt(attempt, allowExpired = false) {
|
|
30
|
+
const fields = Object.hasOwn(attempt ?? {}, 'retryOf') ? [...FIELDS, 'retryOf'] : FIELDS;
|
|
31
|
+
const body = signed(attempt, fields), lease = exact(body.lease, ['leaseId', 'generation', 'attemptNonce', 'epoch', 'issuedAt', 'expiresAt']);
|
|
32
|
+
const authority = signed(body.executionAuthority, ['schemaVersion', 'authorityId', 'attemptNonce', 'taskId', 'flowId', 'ownerId',
|
|
33
|
+
'taskDigest', 'chainDigest', 'leaseId', 'generation', 'epoch', 'status', 'runnerKeyId', 'localRepositoryDigest', 'issuedAt', 'expiresAt']);
|
|
34
|
+
for (const field of ['attemptId', 'flowId', 'criterionId', 'taskRef', 'ownerId', 'taskId', 'blueprintId', 'criterionStepId',
|
|
35
|
+
'originalBusinessStepId', 'evidenceId']) if (!ID.test(body[field] ?? '')) fail('criterion identifier is invalid');
|
|
36
|
+
for (const field of ['compilationReceiptDigest', 'planDigest', 'scopeArtifactDigest', 'taskDigest', 'chainDigest',
|
|
37
|
+
'businessEvidenceDigest', 'runnerKeyId', 'executionPlanSha256', 'artifactSha256']) if (!SHA.test(body[field] ?? '')) fail('criterion digest is invalid');
|
|
38
|
+
if (Object.hasOwn(body, 'retryOf')) {
|
|
39
|
+
const prior = exact(body.retryOf, ['attemptId', 'attemptDigest', 'resultDigest', 'verifiedAt']);
|
|
40
|
+
if (!ID.test(prior.attemptId ?? '') || prior.attemptId === body.attemptId || !SHA.test(prior.attemptDigest ?? '')
|
|
41
|
+
|| !SHA.test(prior.resultDigest ?? '') || !Number.isFinite(Date.parse(prior.verifiedAt))
|
|
42
|
+
|| Date.parse(prior.verifiedAt) >= Date.parse(body.issuedAt)) fail('criterion retry lost its immutable failed predecessor binding');
|
|
43
|
+
}
|
|
44
|
+
const issued = Date.parse(body.issuedAt), expires = Date.parse(body.expiresAt), verified = Date.parse(body.businessVerifiedAt);
|
|
45
|
+
const command = [body.executionPlan.policy.executable, ...body.executionPlan.policy.args].map(value => JSON.stringify(value)).join(' ');
|
|
46
|
+
const manifest = sha(`validator.file-manifest/1.0\n${body.executionPlan.files.map(file => `${file.path}\0${file.sha256}`).join('\n')}`);
|
|
47
|
+
if (body.schemaVersion !== 'entrorove.blueprint-criterion-attempt/1.0' || authority.schemaVersion !== 'entrorove.task-execution-epoch-authority/1.0'
|
|
48
|
+
|| authority.status !== 'active' || !/^sha256:[a-f0-9]{64}$/.test(body.blueprintSha256) || !Number.isFinite(verified)
|
|
49
|
+
|| !Number.isFinite(issued) || !Number.isFinite(expires) || issued <= verified || issued > Date.now() || expires <= issued
|
|
50
|
+
|| expires - issued > 30_000 || (!allowExpired && expires <= Date.now()) || lease.generation !== 1
|
|
51
|
+
|| !Number.isSafeInteger(lease.epoch) || lease.epoch < 0
|
|
52
|
+
|| body.command !== command || body.artifactSha256 !== manifest || body.executionPlan.artifactSha256 !== manifest
|
|
53
|
+
|| body.executionPlanSha256 !== sha(canonical(body.executionPlan) + '\n') || body.evidenceId !== `criterion:${body.attemptId}`
|
|
54
|
+
|| ['flowId', 'ownerId', 'taskId', 'taskDigest', 'chainDigest', 'runnerKeyId', 'localRepositoryDigest', 'issuedAt', 'expiresAt']
|
|
55
|
+
.some(field => authority[field] !== body[field])
|
|
56
|
+
|| ['leaseId', 'generation', 'attemptNonce', 'epoch', 'issuedAt', 'expiresAt'].some(field => authority[field] !== lease[field])) {
|
|
57
|
+
fail('criterion attempt changed its real independent lease, verified original business step or frozen command/manifest');
|
|
58
|
+
}
|
|
59
|
+
return body;
|
|
60
|
+
}
|
|
61
|
+
export function blueprintCriterionExecutionBinding(attempt, processResult) {
|
|
62
|
+
return { schemaVersion: 'entrorove.blueprint-criterion-execution/1.0', attemptId: attempt.attemptId,
|
|
63
|
+
attemptDigest: sha(canonical(attempt)), executionAuthorityDigest: sha(canonical(attempt.executionAuthority)),
|
|
64
|
+
leaseId: attempt.lease.leaseId, generation: 1, attemptNonce: attempt.lease.attemptNonce,
|
|
65
|
+
ownerId: attempt.ownerId, taskId: attempt.taskId, taskDigest: attempt.taskDigest, chainDigest: attempt.chainDigest,
|
|
66
|
+
planDigest: attempt.planDigest, taskRef: attempt.taskRef, originalBusinessStepId: attempt.originalBusinessStepId,
|
|
67
|
+
businessEvidenceDigest: attempt.businessEvidenceDigest, businessVerifiedAt: attempt.businessVerifiedAt,
|
|
68
|
+
executionPlanSha256: attempt.executionPlanSha256, manifestDigest: attempt.artifactSha256,
|
|
69
|
+
verifiedDependencies: attempt.verifiedDependencies, processResultSha256: sha(canonical(processResult)),
|
|
70
|
+
executionStartedAt: processResult.startedAt, executionFinishedAt: processResult.finishedAt };
|
|
71
|
+
}
|
|
72
|
+
export function blueprintCriterionSubject(attempt, evidence, processResult) {
|
|
73
|
+
return { schemaVersion: 'entrorove.blueprint-acceptance-subject/1.1', flowId: attempt.flowId, stepId: attempt.criterionStepId,
|
|
74
|
+
blueprintId: attempt.blueprintId, revision: attempt.blueprintRevision, blueprintSha256: attempt.blueprintSha256,
|
|
75
|
+
criterionId: attempt.criterionId, nodeRefs: [...attempt.nodeRefs].sort(), evidenceId: evidence.evidenceId,
|
|
76
|
+
kind: evidence.kind, artifactSha256: evidence.artifactSha256, policy: { command: evidence.command, requiredExitCode: 0,
|
|
77
|
+
criterionExecution: blueprintCriterionExecutionBinding(attempt, processResult) } };
|
|
78
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile, writeFile, mkdir, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const directory = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const file = 'calctool-runtime-v7.0.43.mjs';
|
|
8
|
+
const pinnedSha256 = '74571381daa3426547d2a19c7f61fce1895163f5d1c4e6b5c451197d9aec823b';
|
|
9
|
+
const modules = ['local-decimal.mjs', 'local-formula-ast.mjs', 'local-formula-engine.mjs', 'local-formula-tests.mjs', 'calctool-runtime-entry.mjs'];
|
|
10
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
function isolated(source) {
|
|
12
|
+
if (/cli[._-]?tax/i.test(source)) throw new Error('Calctool generation contains forbidden external coupling');
|
|
13
|
+
}
|
|
14
|
+
function bundle(source, name) {
|
|
15
|
+
if (source.split('\n').length > 501) throw new Error(`Calctool first-party source exceeds 500 lines: ${name}`);
|
|
16
|
+
const withoutImports = source.replace(/^import\s+[^;]+;\n/gm, '');
|
|
17
|
+
if (/^\s*import\s/m.test(withoutImports)) throw new Error(`Calctool source has an unsupported import: ${name}`);
|
|
18
|
+
return withoutImports.replace(/^export /gm, '').trim();
|
|
19
|
+
}
|
|
20
|
+
async function sourceContent() {
|
|
21
|
+
const pinned = await readFile(resolve(directory, '../../assets/runtime-source', file), 'utf8');
|
|
22
|
+
if (sha(pinned) !== pinnedSha256) throw new Error('Calctool pinned generation source digest mismatch');
|
|
23
|
+
isolated(pinned);
|
|
24
|
+
const core = pinned.replace(/^import\s+[^;]+;\n/gm, '').replace(/^export \{\n[\s\S]*?^\}\s*;?\n/gm, '')
|
|
25
|
+
.replace(/^export \{[^\n]+\};\n/gm, '').replace(/^export async function run/m, 'async function run');
|
|
26
|
+
if (/^export /m.test(core)) throw new Error('Calctool pinned core export boundary changed');
|
|
27
|
+
const parts = [];
|
|
28
|
+
for (const name of modules) {
|
|
29
|
+
const source = await readFile(resolve(directory, name), 'utf8'); isolated(source); parts.push(bundle(source, name));
|
|
30
|
+
}
|
|
31
|
+
return '// Generated by calctool-runtime-build.mjs from the pinned Calctool snapshot and first-party formula modules.\n'
|
|
32
|
+
+ "import { createHash } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n"
|
|
33
|
+
+ 'const calctoolCore = (() => {\n' + core + '\nreturn { run, validateRequest, blockedResponse, validateRunPlan, mergeSwarmArtifacts, decomposeRequirementsToRunPlan, readyTasks, probeEnvironment, adaptEnvironment };\n})();\n\n'
|
|
34
|
+
+ parts.join('\n\n') + '\n\nexport const run = createCalctoolEntrypoint(calctoolCore);\n'
|
|
35
|
+
+ 'export const runLocalCore = (operation, input) => runCalctoolCoreOperation(calctoolCore, operation, input);\n'
|
|
36
|
+
+ 'export const probeLocalEnvironment = calctoolCore.probeEnvironment;\nexport const adaptLocalEnvironment = calctoolCore.adaptEnvironment;\n'
|
|
37
|
+
+ 'export const evaluateCompiledProgram = evaluateFormulaProgram;\nexport const verifyCompiledProgram = verifyFormulaTests;\n';
|
|
38
|
+
}
|
|
39
|
+
export async function buildCalctoolRuntime({ outputPath = resolve(directory, 'runtime', file), checkOnly = false } = {}) {
|
|
40
|
+
const destination = resolve(outputPath);
|
|
41
|
+
if (destination === resolve(directory, '../../assets/runtime-source', file) || destination === fileURLToPath(import.meta.url)) throw new Error('Calctool generator cannot overwrite its pinned source');
|
|
42
|
+
const content = await sourceContent(); isolated(content);
|
|
43
|
+
if (checkOnly) {
|
|
44
|
+
const current = await readFile(destination);
|
|
45
|
+
if (!current.equals(Buffer.from(content))) throw new Error('Generated Calctool runtime is stale');
|
|
46
|
+
} else {
|
|
47
|
+
await mkdir(dirname(destination), { recursive: true }); const temporary = `${destination}.tmp-${randomUUID()}`;
|
|
48
|
+
try { await writeFile(temporary, content, { flag: 'wx', mode: 0o644 }); await rename(temporary, destination); }
|
|
49
|
+
finally { await rm(temporary, { force: true }); }
|
|
50
|
+
}
|
|
51
|
+
return { outputPath: destination, sha256: sha(content), bytes: Buffer.byteLength(content), pinnedSha256, firstPartyModules: modules };
|
|
52
|
+
}
|
|
53
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
54
|
+
const args = process.argv.slice(2); const checkOnly = args.includes('--check');
|
|
55
|
+
const outputIndex = args.indexOf('--output'); const outputPath = outputIndex >= 0 ? args[outputIndex + 1] : undefined;
|
|
56
|
+
if (args.some((arg, index) => arg !== '--check' && arg !== '--output' && index !== outputIndex + 1) || (outputIndex >= 0 && !outputPath)) throw new Error('Invalid Calctool generator arguments');
|
|
57
|
+
try { process.stdout.write(JSON.stringify(await buildCalctoolRuntime({ outputPath, checkOnly })) + '\n'); }
|
|
58
|
+
catch (cause) { process.stderr.write(cause.message + '\n'); process.exitCode = 1; }
|
|
59
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { buildFormulaEngine, compileFormulaEngine, validateFormulaEngine, formulaDigest } from './local-formula-engine.mjs';
|
|
2
|
+
import { formulaError } from './local-decimal.mjs';
|
|
3
|
+
import { FORMULA_OPERATORS } from './local-formula-ast.mjs';
|
|
4
|
+
|
|
5
|
+
const CLOUD_OPERATIONS = ['capabilities', 'help', 'intake', 'validate', 'compile-inline'];
|
|
6
|
+
const LOCAL_OPERATIONS = ['run', 'compile', 'generate', 'verify', 'estimate', 'impact', 'status', 'inventory', 'purge',
|
|
7
|
+
'brain-handshake', 'brain-invoke', 'brain-events', 'brain-cancel', 'brain-complete', 'brain-resume', 'brain-status',
|
|
8
|
+
'probe-env', 'adapt-config', 'intake-round', 'compile-tool', 'blueprint-orchestrate', 'auto-pipeline', 'final-gate', 'swarm-orchestrate'];
|
|
9
|
+
function failure(request, cause) {
|
|
10
|
+
return { schemaVersion: 'calctool.skill.response/1.0', requestId: request?.requestId ?? 'unknown', status: 'blocked',
|
|
11
|
+
validation: { valid: false, guarantee: 'blocked', findings: [{ severity: 'P0', ruleId: cause.code ?? 'FORMULA_INVALID', entityRef: 'input', message: cause.message }] } };
|
|
12
|
+
}
|
|
13
|
+
function compiled(request, engine) {
|
|
14
|
+
const program = compileFormulaEngine(engine);
|
|
15
|
+
return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId, status: 'succeeded', state: 'reported',
|
|
16
|
+
revision: program.engineVersion, engine: program.engine, program,
|
|
17
|
+
validation: { valid: true, guarantee: 'formula-compiled', findings: [] },
|
|
18
|
+
artifacts: [{ path: `${program.engineId}/engine.json`, kind: 'engine-definition', engineId: program.engineId,
|
|
19
|
+
engineVersion: program.engineVersion, digest: program.engineDigest },
|
|
20
|
+
{ path: `${program.engineId}/program.json`, kind: 'formula-program', engineId: program.engineId,
|
|
21
|
+
engineVersion: program.engineVersion, runtimeVersion: program.runtimeVersion, digest: program.programDigest }],
|
|
22
|
+
nextStep: { operation: 'calctool-verify', instruction: 'Execute the compiled engine benchmark samples locally, then submit bound evidence to the independent validator.' } };
|
|
23
|
+
}
|
|
24
|
+
export function createCalctoolEntrypoint(core) {
|
|
25
|
+
return async function run(request, options = {}) {
|
|
26
|
+
const requestFindings = core.validateRequest(request);
|
|
27
|
+
if (requestFindings.length) return core.blockedResponse(request?.requestId ?? 'unknown', request, requestFindings);
|
|
28
|
+
try {
|
|
29
|
+
if (request.operation === 'capabilities') {
|
|
30
|
+
const output = await core.run(request, options);
|
|
31
|
+
return { ...output, capabilities: { ...output.capabilities, operations: CLOUD_OPERATIONS, localRunnerOperations: LOCAL_OPERATIONS,
|
|
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' } },
|
|
34
|
+
localRunner: { command: 'entrorove local calctool-<operation>', requiredFor: LOCAL_OPERATIONS } };
|
|
35
|
+
}
|
|
36
|
+
if (request.operation === 'validate') {
|
|
37
|
+
const findings = validateFormulaEngine(request.input?.engine);
|
|
38
|
+
if (findings.length) return core.blockedResponse(request.requestId, request, findings);
|
|
39
|
+
return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId, status: 'succeeded', state: 'reported',
|
|
40
|
+
validation: { valid: true, guarantee: 'formula-definition-valid', findings: [] } };
|
|
41
|
+
}
|
|
42
|
+
if (request.operation === 'compile-inline') return compiled(request, buildFormulaEngine(request.input?.requirements));
|
|
43
|
+
if (LOCAL_OPERATIONS.includes(request.operation)) return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId,
|
|
44
|
+
status: 'incomplete', validation: { valid: false, guarantee: 'local-runner-required', findings: [] },
|
|
45
|
+
localRunner: { required: true, operation: `calctool-${request.operation}`, command: `entrorove local calctool-${request.operation}` } };
|
|
46
|
+
return core.run(request, options);
|
|
47
|
+
} catch (cause) { return failure(request, cause); }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resumeCalctoolRun(core, request) {
|
|
52
|
+
const input = request.input; const plan = input.runPlan;
|
|
53
|
+
const errors = core.validateRunPlan(plan);
|
|
54
|
+
if (errors.length) throw formulaError('RUN_PLAN_INVALID', errors.join('; '));
|
|
55
|
+
const known = new Set(plan.tasks.map(task => task.workItemId)); const seen = new Set();
|
|
56
|
+
const groups = ['completedWorkItemIds', 'inFlightWorkItemIds', 'uncertainWorkItemIds'].map(field => {
|
|
57
|
+
const ids = input[field] ?? [];
|
|
58
|
+
if (!Array.isArray(ids)) throw formulaError('RUN_RESUME_INVALID', 'resume checkpoints must contain work item arrays');
|
|
59
|
+
for (const id of ids) {
|
|
60
|
+
if (!known.has(id) || seen.has(id)) throw formulaError('RUN_RESUME_INVALID', 'resume checkpoint contains an unknown, duplicate or conflicting work item');
|
|
61
|
+
seen.add(id);
|
|
62
|
+
}
|
|
63
|
+
return new Set(ids);
|
|
64
|
+
});
|
|
65
|
+
const [completed, inFlight, uncertain] = groups;
|
|
66
|
+
for (const task of plan.tasks) if ((completed.has(task.workItemId) || inFlight.has(task.workItemId))
|
|
67
|
+
&& task.dependsOn.some(id => !completed.has(id))) throw formulaError('RUN_RESUME_INVALID', 'completed or executing work items require completed dependencies');
|
|
68
|
+
const availableSlots = Math.max(0, plan.limits.maxParallel - inFlight.size - uncertain.size);
|
|
69
|
+
const ready = uncertain.size ? [] : core.readyTasks(plan, completed).filter(task => !inFlight.has(task.workItemId));
|
|
70
|
+
const dispatchable = ready.slice(0, Math.min(availableSlots, plan.limits.maxReadyPageSize));
|
|
71
|
+
const ordered = group => plan.tasks.filter(task => group.has(task.workItemId)).map(task => task.workItemId);
|
|
72
|
+
const checkpoint = { completedWorkItemIds: ordered(completed), inFlightWorkItemIds: ordered(inFlight), uncertainWorkItemIds: ordered(uncertain) };
|
|
73
|
+
const remaining = plan.tasks.filter(task => !completed.has(task.workItemId));
|
|
74
|
+
return { schemaVersion: 'calctool.skill.response/1.0', requestId: request.requestId, status: uncertain.size ? 'incomplete' : 'reported',
|
|
75
|
+
resumed: true, executed: false, runPlan: plan, runPlanRef: plan.runId, resumeDigest: formulaDigest({ runPlan: plan, checkpoint }), checkpoint,
|
|
76
|
+
completedWorkItems: checkpoint.completedWorkItemIds, readyNow: dispatchable.map(task => task.workItemId), remaining: remaining.length,
|
|
77
|
+
pendingWorkItems: remaining, dispatch: { maxParallel: plan.limits.maxParallel, availableSlots, readyNow: dispatchable },
|
|
78
|
+
reconciliation: { required: uncertain.size > 0, workItemIds: checkpoint.uncertainWorkItemIds },
|
|
79
|
+
nextStep: remaining.length === 0 ? { operation: 'brain-complete', instruction: 'Merge the original run artifacts.' }
|
|
80
|
+
: uncertain.size ? { operation: 'brain-events', instruction: 'Reconcile original work item results before dispatching any continuation.' }
|
|
81
|
+
: { operation: 'brain-events', instruction: 'Continue only the returned dispatch tasks in the original run and record their results.' } };
|
|
82
|
+
}
|
|
83
|
+
export async function runCalctoolCoreOperation(core, operation, input) {
|
|
84
|
+
const request = { schemaVersion: 'calctool.skill.request/1.0', requestId: 'local-calctool', operation, input };
|
|
85
|
+
if (operation === 'brain-resume') return resumeCalctoolRun(core, request);
|
|
86
|
+
if (operation === 'brain-complete') {
|
|
87
|
+
const errors = core.validateRunPlan(input.runPlan);
|
|
88
|
+
if (errors.length) return core.blockedResponse(request.requestId, request, errors.map(message => ({ severity: 'P0', ruleId: 'RUN_PLAN_INVALID', entityRef: 'input.runPlan', message })));
|
|
89
|
+
return compiled(request, core.mergeSwarmArtifacts(input.runPlan, input.artifacts));
|
|
90
|
+
}
|
|
91
|
+
return core.run(request);
|
|
92
|
+
}
|