@entrorove/skill 0.1.33 → 0.1.35
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 +36 -5
- package/CHANGELOG.md +10 -0
- package/SKILL.md +35 -3
- package/automatic-feedback-client.mjs +45 -0
- package/cli.mjs +25 -1
- package/cloud-event-stream.mjs +27 -0
- package/cloud-task-client.mjs +28 -13
- package/cloud-watch-client.mjs +58 -0
- package/ide-presentation.mjs +64 -0
- package/local-autocoord17-controller.mjs +6 -4
- package/local-cli.mjs +2 -0
- package/local-peer-status.mjs +6 -0
- package/local-swarm-contracts.mjs +3 -0
- package/local-swarm.mjs +32 -1
- package/manifest.json +1 -1
- package/native-client.mjs +58 -0
- package/native-notification.mjs +76 -0
- package/package.json +9 -2
- package/release-state.json +21 -14
- package/release.mjs +9 -0
- package/releases.json +12 -0
- package/remote-client.mjs +30 -7
- package/scope-runner-client.mjs +2 -1
package/CAPABILITY-MAP.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# 能力对账图(WP-02 / AC-20)
|
|
2
2
|
|
|
3
|
+
## 2026-10-13 接入:协作观测、原生规划与任务通知
|
|
4
|
+
|
|
5
|
+
| 能力 | 正常公开入口 | 已执行验证与边界 |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| 协作状态 | `local peer-status` | 原签名授权和原协调绑定;等待、活动、过期、跨任务拒绝;只读查询不写日志、不发写入权限 |
|
|
8
|
+
| Aimlock 规划 | `native aimlock brain-plan/brain-check/brain-validate/brain-status` | 原账号计划入库,检查范围与依赖;重启后按原 request 恢复,跨账号及自造计划拒绝;只认证规划,不认证业务交付 |
|
|
9
|
+
| 八域直接调用 | `native <domain> <operation>` | 真实 discovery → schema 校验 → 单次原生调用 → 原回执核验;八域帮助和状态观察遵循服务端合同,不要求不存在的评价 |
|
|
10
|
+
| IDE 系统通知 | `notifications configure/status/test` | 默认关闭;账号绑定、状态去重、凭据变更失效;仅真实结果、失败与需要确认事件;不推送推广 |
|
|
11
|
+
| 网页与手机通知 | 登录后的任务通知按钮 | 同一加密 Web Push 协议,账号与设备隔离;真实任务事务队列、持久重试、失效注销与到期清理;手机可见投递仍需设备本人授权与验收 |
|
|
12
|
+
| 失败后接续 | `scope-connect` 返回 `nextInput`;`feedback <requestId> [generation]` | 范围门禁使用同一排序路径集;新执行保存新一代回执,原失败回执不可改,默认只读最新一代 |
|
|
13
|
+
|
|
14
|
+
本地公开 schema 已增加到 **101 项**(原 100 项加 `peer-status`)。Aimlock 运行时由 7.0.44 更新为 **7.0.45**,公开操作由 20 项增加到 24 项;其余七域的运行时版本保持原值。下方历史表保留原对账日期与当时验收边界,不表示当前仍缺上述入口。发布件与生产部署需独立核验,不能从仓库测试推断。
|
|
15
|
+
|
|
16
|
+
## 2026-10-12 基线:八域 IDE 调用链
|
|
17
|
+
|
|
18
|
+
逐项对照旧版八个技能的已安装说明、入口和服务实现,本次补回以下共同保证。后面的 AC-20~24 保留其历史基线与当时测试边界。
|
|
19
|
+
|
|
20
|
+
| 旧版保证 | 当前入口与行为 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| 每个任务选择云端/本地,原生确认不可用时仍须展示问题 | `cloud-task` 返回 Confirm Protocol 和 `chatFallback`;确认才提交,退出保留原任务并采用本地方案 |
|
|
23
|
+
| 实际进度必须进入 IDE | `cloud-watch` 订阅本人任务 SSE,输出独立 IDE 事件;调用方必须把公开消息和摘要展示到对话进度 |
|
|
24
|
+
| 执行提示与推广可见 | `智能执行中,请勿关闭!` 与服务端选中的真实推广独立输出;保留推广 HTTPS 链接,按事件 ID 去重 |
|
|
25
|
+
| 断线恢复原任务 | `cloud-watch` 带原 `taskId/afterId` 只读续接;`status` 与 `feedback` 查询原请求,不重发业务 POST |
|
|
26
|
+
| 服务端自动评价,无第二次评价写入 | 调用完成事务保存本人原 request 的评价与结果摘要;CLI 校验回执。旧式适配器使用幂等持久化并在同一 HTTP 响应返回 |
|
|
27
|
+
| 未验证不可记好评 | intake、模型摘要、reported、不完整证据无评分;阻断、失败、P0/P1 不产生好评;发现和状态查询不生成新评价 |
|
|
28
|
+
| 实际 IDE 与原调用绑定 | `ENTROROVE_IDE` 或宿主检测写入原调用标签;每个 OS 用户继续复用受限凭据存储 |
|
|
29
|
+
| 最新官方技能说明生效 | 新调用使用 `npx --yes --prefer-online @entrorove/skill@latest`;安装更新后重新读取 managed `SKILL.md` |
|
|
30
|
+
| 调用次数与沙箱次数分开 | 普通调用与旧式 UUID 记录技能用量;`cloud-task` 才创建沙箱。所有查询和评价按账号权限过滤 |
|
|
31
|
+
|
|
32
|
+
自动技术评价是私有调用记录。公开用户评价仍须使用本人原话、真实评分和明确公开同意。原生系统通知与手机推送已按 2026-10-13 的公开入口接入;通知权限由用户开启。确认继续遵循 IDE 的明确问题与答案。思考详情只展示服务端公开的规划摘要。
|
|
33
|
+
|
|
3
34
|
本表把旧八域的远端公开操作与当前本地执行器分开统计。旧版 `SKILL.md` 只规定入口、状态和边界,没有逐域列出操作名;远端操作名采用随包锁定的 v2 runtime catalog 交叉核对,本地操作名直接来自 `local-contracts.mjs` 的 `LOCAL_OPERATION_SCHEMAS`。两个分母不能合并。
|
|
4
35
|
|
|
5
36
|
## 边界
|
|
@@ -25,16 +56,16 @@ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快
|
|
|
25
56
|
|
|
26
57
|
| 域 | 旧 runtime 公开操作 | 当前映射 | 语义边界 |
|
|
27
58
|
| --- | --- | --- | --- |
|
|
28
|
-
| 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` | discovery、任务路由、快照、门控、状态、取消和交付证据 | `brain-plan/check/validate/status`
|
|
59
|
+
| 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` | discovery、任务路由、快照、门控、状态、取消和交付证据 | 新增 `brain-plan/check/validate/status` 已接入 Aimlock 7.0.45,见上方最新接入表 |
|
|
29
60
|
| blueprint | `capabilities`, `help`, `intake`, `validate`, `compile-inline`, `acceptance-report` | `blueprint` runtime;验收证据由调用方提供,不能自升 `verified` | `acceptance-report` 不是自报绿灯 |
|
|
30
61
|
| calctool | `capabilities`, `help`, `intake`, `validate`, `compile-inline` | `calctool` runtime;公式执行和 final-gate 在本地 Decimal 层 | `research` 是 pipeline 内部阶段,不是独立公开操作 |
|
|
31
62
|
| swarm | `capabilities`, `help`, `intake`, `org-chart`, `blueprint-bridge`, `dispatch`, `claim`, `report`, `accept`, `swarm-status`, `traffic-light`, `security-check`, `validate-json`, `heartbeat`, `reclaim` | 服务端路由、租约和 `reported`/`verified` 状态 | 本地 peer/coordinate 是统一本地层,不能冒充旧 swarm 远端 |
|
|
32
63
|
| validator | `capabilities`, `help`, `intake`, `plan`, `validate-structure`, `security-scan`, `compliance-audit`, `functional-verify`, `verdict`;local-runner:`sandbox-run`, `fuzz-input`, `perf-benchmark`, `intrusive-test` | pure validator 与受控 runner 分开 | 生产验证需独立回执;`verdict` 不能把 reported 变绿 |
|
|
33
64
|
| mergeguard | `capabilities`, `help`, `intake`, `resolve-propose`, `ruleguard-scan`, `ruleguard-compile`;local-runner:`branch-create`, `branch-list`, `branch-switch`, `diff-report`, `preflight`, `merge-verified`, `rollback`, `ledger-query` | pure mergeguard 与本地合并闸门分开 | 虚拟分支不会创建 Git 分支/worktree |
|
|
34
65
|
| archguard | `capabilities`, `help`, `intake`, `contract-create`, `contract-get`, `contract-update`, `template-list`, `checkpoint`, `rules-scan`, `complexity-report`, `drift-status`, `ledger-query` | `archguard-*` 本地合同、快照、范围和账本 | 本地写入仍受路径和基线绑定 |
|
|
35
|
-
| confirm-protocol | `capabilities`, `help`, `interaction-request`, `interaction-answer`, `chat-render`, `memory-set`, `memory-list`, `memory-clear`, `batch-request`, `audit-query` | 纯协议操作 |
|
|
66
|
+
| confirm-protocol | `capabilities`, `help`, `interaction-request`, `interaction-answer`, `chat-render`, `memory-set`, `memory-list`, `memory-clear`, `batch-request`, `audit-query` | 纯协议操作 | 通知由独立账号与设备适配层接入;高风险交互默认不批准、不记忆、不批量 |
|
|
36
67
|
|
|
37
|
-
##
|
|
68
|
+
## 本地历史基线(2026-10-07,`LOCAL_OPERATION_SCHEMAS` 100 项)
|
|
38
69
|
|
|
39
70
|
`scope-connect <absolute repositoryRoot>` 是 CLI 编排入口,不改变这 100 项本地操作。它串联远端 `scope-runner-prepare/register/challenge/attest` 与已有真实快照、签名检查,为原账号与任务提交范围证据;不授予执行证据签名或写入权限。全部远端操作以实时 discovery 为准。
|
|
40
71
|
|
|
@@ -67,11 +98,11 @@ Entrorove 只治理通过技能入口执行的动作:路径、通行证、快
|
|
|
67
98
|
|
|
68
99
|
| 疑点 | 结论 | 证据与理由 |
|
|
69
100
|
| --- | --- | --- |
|
|
70
|
-
| `peer-status` |
|
|
101
|
+
| `peer-status` | **已接入并验证** | 公开 schema/CLI 经 `local-peer-status.mjs` 查询原授权;`peer-status-public.test.mjs` 与 `item17-autocoord-behavior.test.mjs` 通过。 |
|
|
71
102
|
| `interrupt` | **已实现(仓库)** | `assets/aimlock-runtime-v7.0.44.mjs` operation catalog 与 policy 均有;发布件需 clean-install 后复测。 |
|
|
72
103
|
| `delivery-doc` | **已实现(仓库)** | 同一 aimlock runtime/source 有 operation、schema、entry;发布面保持未验证。 |
|
|
73
104
|
| calctool `research` | **有意不独立公开(理由)** | `calctool-runtime-v7.0.43.mjs` 将其作为 pipeline 内部 partition kind;它没有独立请求合同,拆成公开 operation 会重复 intake/验证边界。若需求改为独立调用,再开变更。 |
|
|
74
|
-
| aimlock 自有 `brain plan/check/validate/status` |
|
|
105
|
+
| aimlock 自有 `brain plan/check/validate/status` | **已接入并验证** | Aimlock 7.0.45 的四个公开操作、原计划持久化与账号隔离已由真实 HTTP 和重启回归验证;规划不授予执行或交付认证。 |
|
|
75
106
|
|
|
76
107
|
## 发布与校验边界
|
|
77
108
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.35 — 2026-10-13
|
|
4
|
+
|
|
5
|
+
- 新增协作状态查询、Aimlock 专用规划与八域原生调用;接入账号和设备隔离的任务通知,修复失败接续的评价回执冲突,并完善范围门禁接续。
|
|
6
|
+
- Add read-only peer status, Aimlock planning and native invocation across eight domains; support account/device-bound task notifications, preserve feedback receipts across authorized retries, and correct scope-gate continuation.
|
|
7
|
+
|
|
8
|
+
## 0.1.34 — 2026-10-12
|
|
9
|
+
|
|
10
|
+
- 完善 IDE 云端规划选择、实时执行进度与独立推广提示;服务端自动评价绑定原调用并返回可核验回执,支持只读恢复;修复 UUID 调用用量统计,保持账号隔离及沙箱/调用计数分离。
|
|
11
|
+
- Improve IDE cloud planning choices, live execution progress and separate promotion notices. Bind automatic server evaluations to their original invocations with verifiable receipts and read-only recovery; correct UUID invocation usage while keeping accounts and skill/sandbox counters separate.
|
|
12
|
+
|
|
3
13
|
## 0.1.33 — 2026-10-12
|
|
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.35.
|
|
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
|
|
|
@@ -23,11 +23,23 @@ On first use or after a material agreement revision, the CLI returns a Confirm P
|
|
|
23
23
|
|
|
24
24
|
## Work with a task
|
|
25
25
|
|
|
26
|
+
### IDE display and cloud planning entry
|
|
27
|
+
|
|
28
|
+
For a task using this skill's cloud planning, first run `npx --yes --prefer-online @entrorove/skill@latest cloud-task` with `{"goal":"<the user's actual goal>","ideTaskId":"<the current IDE task ID, when available>"}` on stdin. Render the returned choice with the host's question tool or native UI. If no native adapter exists, show `interaction.chatFallback` in the conversation and collect its answer. Tool output alone is not a displayed question. Offer **确认** and **退出,采用本地方案**. Exit or an explicit cancellation continues the original task locally; it must not stop the task or change account preferences. An unanswered question does not authorize cloud consumption. Reuse a prior explicit choice for this same task rather than asking again.
|
|
29
|
+
|
|
30
|
+
With `sandboxChoice: "use"` (or the protocol callback's `answer: "use"`), retain the real returned task ID and immediately run the returned `nextAction`, `cloud-watch`. It subscribes to that account's authoritative SSE stream, returns newline-delimited `entrorove.ide-event/1.0` events, then one `entrorove.cloud-watch/1.0` result. **Mirror each new event's public `message` and optional `detail` into visible IDE progress/commentary.** Deduplicate by task ID and event ID, including events repeated in the final result. The same rule applies to normal invocation `ideMessages` and notice events. A JSON field or collapsed command log does not satisfy this display requirement. Show `智能执行中,请勿关闭!` and the selected promotion on separate lines; preserve the promotion's actual HTTPS link. Use only the server's selected promotion; do not insert a local test advertisement.
|
|
31
|
+
|
|
32
|
+
Display the actual entering, allocation, thinking, public output and exit events as they arrive. Do not generate model reasoning, invent intermediate events or replace `reported` with verified completion. The public model summary is the planning output available to show. `云端沙箱输出完毕` means the current planning response has arrived; inspect its real task/verification status before dependent execution. Exit here closes the progress subscription and does not cancel the persisted task.
|
|
33
|
+
|
|
34
|
+
When a bounded watch returns `nextAction`, continue it with the returned `afterId`; reconnect only with GET and that cursor. Never repeat cloud-task POST to recover progress. For an available planning result, query `status <feedback.requestId>` to obtain the original accepted flow and required next step, then continue that flow. Do not create a second `start` for the same cloud goal. If the task is still running but no events arrive, say so and keep reading its real status. Missing credentials, blocked entitlement, expiry and stream errors must be visible.
|
|
35
|
+
|
|
36
|
+
Set `ENTROROVE_IDE` to the actual host name (for example `Codex`, `Cursor` or `Claude Code`) when host detection is unavailable. It carries only the IDE label; credentials remain in the restricted store. Use the latest official package with `--prefer-online` for new invocations, and reread the managed `SKILL.md` after installation reports an update. Recovering an uncertain request remains a read-only query of its original ID.
|
|
37
|
+
|
|
26
38
|
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.
|
|
27
39
|
|
|
28
40
|
`start` creates a normal durable skill flow. It does not create a cloud-sandbox task or contribute to cloud-sandbox usage. To create a real cloud-sandbox task, call `operation: "cloud-task"` with `input.goal`; the CLI returns a Confirm Protocol choice interaction before any cloud request. Continue the same request with `sandboxChoice: "use"` or `sandboxChoice: "local"`. `use` calls the authenticated `/api/cloud-task` endpoint and returns the real `taskId`; `local` skips only this task. The server is authoritative for account entitlement and the account-wide sandbox preference: a disabled or unavailable account returns `blocked` and creates no task. A `needs-input`, `blocked` or `failed` result is not a successful dispatch and must be shown to the user.
|
|
29
41
|
|
|
30
|
-
After dispatch,
|
|
42
|
+
After dispatch, use `cloud-watch` for live IDE progress and `cloud-status` for a read-only snapshot with `input.taskId`. Both are CLI-only routes, also accepted by `invoke`; they do not pass through `/api/skill/invoke`. Status includes public title/summary, actual model usage and the stored automatic evaluation when available. A missing task is explicit and never treated as completed. The IDE and website consume the same account-bound event stream.
|
|
31
43
|
|
|
32
44
|
### Per-task cloud sandbox choice
|
|
33
45
|
|
|
@@ -41,11 +53,15 @@ For `workType: "merge"`, include `baselineRef` with the concrete branch, tag, co
|
|
|
41
53
|
|
|
42
54
|
The v2 capability map is recorded in [`CAPABILITY-MAP.md`](CAPABILITY-MAP.md). It maps every legacy domain and operation to its v2 equivalent, narrower semantics, or an explicitly documented uncovered area; adapters must not silently substitute an uncovered operation.
|
|
43
55
|
|
|
56
|
+
For an explicit domain operation, use `npx @entrorove/skill native <domain> <operation>` with its input object on stdin. The supported domains are `aimlock`, `blueprint`, `calctool`, `swarm`, `validator`, `mergeguard`, `archguard` and `confirm-protocol`. This public adapter discovers that domain's actual server schema, validates the input, invokes the registered operation once, and verifies any required automatic feedback. Discovery, help and status observations follow the server's published feedback contract. A failed gate or unknown outcome must never be replaced with another executor or reported as local success; fix the original cause and recover the original request through its read-only status.
|
|
57
|
+
|
|
58
|
+
Aimlock exposes `brain-plan`, `brain-check`, `brain-validate` and `brain-status`. Create a plan with the actual goal, scope `contract`, `requirements` and ordered `steps`; retain its returned `requestId` and `plan.digest`. Subsequent operations take only that original `planRequestId` (plus `paths` for `brain-check`); the server loads the authenticated account's stored plan. Never submit a reconstructed plan or another account's request. Planning validation checks scope, requirements and dependencies; it grants no mutation authority and never certifies business delivery.
|
|
59
|
+
|
|
44
60
|
For a returned `flowId`, continue with `{"operation":"continue","input":{"flowId":"<returned ID>","operation":"<next operation>","payload":{}}}`. Fill `payload` with the next operation's required input. Keep the same task and evidence in the IDE. For later work, `input.task.phase` may be `plan`, `coordinate`, `architecture`, `verify`, `merge` or `decision`; it remains the same skill entry point. Never treat an unknown request outcome as permission to retry a paid or mutating action; query the original request ID when the workflow supports it.
|
|
45
61
|
|
|
46
62
|
The v2 progress sequence is durable and ordered: the first runtime step reports `intake`, then the applicability `gate` is recorded and verified, then the selected contract or plan steps run. Every step can be `pending`, `running`, `reported`, `verified`, `blocked` or `failed`; only a completed verdict with trusted evidence is green.
|
|
47
63
|
|
|
48
|
-
When intake returns `workflow-gate`, connect the current repository before invoking that gate: run `npx @entrorove/skill scope-connect <absolute repositoryRoot>` with JSON `{"flowId":"<original flowId>","paths":["<existing source file>"]}` on stdin. Use the concrete existing source files in the task's scope; new files belong in the later plan's declared artifacts. The adapter enrolls an OS-private Ed25519 scope signer for this accepted account and task, answers a server challenge, copies and verifies real source files, and submits the signed facts. It sends hashes and scope metadata rather than source contents or the private key. Enrollment grants scope inspection only; execution evidence and write passes retain their separate authority checks. An operator-configured signer cannot be replaced. After a successful connection,
|
|
64
|
+
When intake returns `workflow-gate`, connect the current repository before invoking that gate: run `npx @entrorove/skill scope-connect <absolute repositoryRoot>` with JSON `{"flowId":"<original flowId>","paths":["<existing source file>"]}` on stdin. Use the concrete existing source files in the task's scope; new files belong in the later plan's declared artifacts. The adapter enrolls an OS-private Ed25519 scope signer for this accepted account and task, answers a server challenge, copies and verifies real source files, and submits the signed facts. It sends hashes and scope metadata rather than source contents or the private key. Enrollment grants scope inspection only; execution evidence and write passes retain their separate authority checks. An operator-configured signer cannot be replaced. After a successful connection, use the returned `nextInput` for `continue`, or explicitly send `payload: {"targetFiles":["<same existing source file>"]}` to the original `workflow-gate`. The connector uses `paths`; the gate uses `targetFiles`. Refresh an expired snapshot by reconnecting the same flow and path set. Show an explicit blocked result; query the original request ID after an unknown transport outcome before considering another attempt.
|
|
49
65
|
|
|
50
66
|
Scope inspection resolves aliases from actual `tsconfig.json` or `jsconfig.json`, including relative `extends`. For plugin source compiled by a host project whose config is outside the source's ancestors, include `importContexts: [{"sourcePrefix":"<plugin source directory>","configPath":"<actual host tsconfig.json>"}]` in `scope-connect` input. Inspect the real build configuration first; never invent an alias or mark an unresolved import as external. These context paths remain inside the repository, their hashes enter the signed facts, and the local task binding freezes them.
|
|
51
67
|
|
|
@@ -59,6 +75,22 @@ The local execution layer is available through `npx @entrorove/skill local <oper
|
|
|
59
75
|
|
|
60
76
|
Local CLI failures use the machine-readable `entrorove.local-error/1.0` envelope `{schemaVersion,status,code,disposition,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`.
|
|
61
77
|
|
|
78
|
+
### Read-only peer status
|
|
79
|
+
|
|
80
|
+
`local peer-status` observes the original signed worker grant, task and lease, or the registered coordination task/agent/chain/intent binding. It reports only that worker's active, waiting or expired state, own blocked/ready paths and own checkpoint requests. Queries do not issue leases, change ownership, write a coordination journal or grant permission to write. Use `local capabilities` for the exact supported input variants.
|
|
81
|
+
|
|
82
|
+
### Optional task notifications
|
|
83
|
+
|
|
84
|
+
System notifications are disabled until this account explicitly enables them. `npx @entrorove/skill notifications configure` accepts only `{"enabled":true}` or `{"enabled":false}` on stdin; `notifications status` observes the account's setting and `notifications test` submits a generic test only when enabled. macOS and Linux use their existing notification service. Real IDE events notify output availability, verification, failure or required confirmation, deduplicated by original task/request and state. Promotion and keep-alive messages remain visible in the IDE and never trigger system notifications. Changing the stored account credential disables the previous account's notification setting. OS acceptance does not prove that permission allows a visible notification.
|
|
85
|
+
|
|
86
|
+
The website's signed-in bell control enables encrypted Web Push for the current account and device. Desktop browsers and supported phones use this same push protocol; iPhone requires a Home Screen web app. Ask permission only through the user's enable action. Logout clears the browser binding and subscription. Messages contain only generic task status, with no prompts, credentials, model output or promotion. Delivery retries keep the original event and device binding; expired endpoints and temporary tasks are cleaned up automatically. Never describe an unsupported platform or failed delivery as a successful notification.
|
|
87
|
+
|
|
88
|
+
### Automatic invocation evaluation and optional user reviews
|
|
89
|
+
|
|
90
|
+
Every completed or failed execution call receives a server-generated `feedback` receipt bound to the original account, request hash and result digest. Durable calls commit this evaluation in the same transaction as the result; legacy adapters return an idempotent receipt with the same HTTP response. The CLI verifies the original identity and evaluation digest. Preserve `receiptId`, `requestId` and `evaluation.digest`; never generate a score or comment locally. Display the returned evaluation acknowledgement in IDE progress, deduplicated by receipt ID. Read the saved receipt with `npx @entrorove/skill feedback <original requestId>` after a connection loss; this is GET only and never repeats execution or submits another evaluation. An authorized retry before side effects keeps the original request but gets a new server execution generation and immutable receipt. Default feedback reads the latest generation; `feedback <original requestId> <generation>` reads a preserved earlier receipt. Never overwrite a failed receipt or use it as the current retry result. Discovery/status queries create no evaluations.
|
|
91
|
+
|
|
92
|
+
Only server-verified progress or a validated pure operation earns a positive automatic evaluation. Reported steps, intake, model summaries and incomplete evidence remain `pending` with no score. Blocked/failed results and P0/P1 findings cannot become positive. These are private technical evaluations; they do not create a public user review or masquerade as the user's words. If a server advertising this extension returns no valid receipt, show the missing receipt and query its original ID. Do not silently claim that evaluation was reported.
|
|
93
|
+
|
|
62
94
|
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.
|
|
63
95
|
|
|
64
96
|
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):
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
3
|
+
|
|
4
|
+
function canonical(value) {
|
|
5
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
6
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`;
|
|
7
|
+
return JSON.stringify(value);
|
|
8
|
+
}
|
|
9
|
+
function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
|
|
10
|
+
export function verifyAutomaticFeedback(feedback, requestId, requestHash) {
|
|
11
|
+
const evaluation = feedback?.evaluation;
|
|
12
|
+
if (!feedback || feedback.schemaVersion !== 'entrorove.automatic-feedback/1.0' || feedback.stored !== true
|
|
13
|
+
|| !/^[0-9a-f-]{36}$/i.test(feedback.receiptId) || feedback.requestId !== requestId
|
|
14
|
+
|| !/^[a-f0-9]{64}$/.test(feedback.requestHash) || requestHash && feedback.requestHash !== requestHash
|
|
15
|
+
|| !/^[a-f0-9]{64}$/.test(feedback.resultDigest) || feedback.generation !== undefined
|
|
16
|
+
&& (!Number.isSafeInteger(feedback.generation) || feedback.generation < 1) || !evaluation
|
|
17
|
+
|| evaluation.schemaVersion !== 'entrorove.automatic-evaluation/1.0' || evaluation.requestId !== requestId
|
|
18
|
+
|| !['good','bad','pending'].includes(evaluation.disposition)
|
|
19
|
+
|| !['passed','failed','incomplete'].includes(evaluation.validation)
|
|
20
|
+
|| !Number.isSafeInteger(evaluation.durationMs) || evaluation.durationMs < 0
|
|
21
|
+
|| !['findingCount','p0Count','p1Count'].every(key => Number.isSafeInteger(evaluation[key]) && evaluation[key] >= 0)
|
|
22
|
+
|| typeof evaluation.comment !== 'string' || !evaluation.comment.trim()) {
|
|
23
|
+
throw new Error(`Invalid automatic feedback receipt; query feedback ${requestId} without repeating the invocation`);
|
|
24
|
+
}
|
|
25
|
+
const { digest: checksum,...core } = evaluation;
|
|
26
|
+
if (digest(core) !== checksum || !['succeeded','reported','blocked','failed','incomplete'].includes(evaluation.status)
|
|
27
|
+
|| evaluation.findingCount < evaluation.p0Count + evaluation.p1Count
|
|
28
|
+
|| evaluation.disposition === 'pending' && (evaluation.score !== null || evaluation.validation !== 'incomplete')
|
|
29
|
+
|| evaluation.disposition === 'good' && (evaluation.status !== 'succeeded' || evaluation.validation !== 'passed' || evaluation.p0Count || evaluation.p1Count)
|
|
30
|
+
|| evaluation.disposition === 'bad' && evaluation.validation !== 'failed'
|
|
31
|
+
|| evaluation.disposition !== 'pending' && (!Number.isInteger(evaluation.score) || evaluation.score < 0 || evaluation.score > 100)) {
|
|
32
|
+
throw new Error('Automatic feedback digest or validation state is inconsistent');
|
|
33
|
+
}
|
|
34
|
+
return feedback;
|
|
35
|
+
}
|
|
36
|
+
export async function queryAutomaticFeedback(requestId, dependencies, generation) {
|
|
37
|
+
if (typeof requestId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestId)) throw new Error('feedback requires its original request ID');
|
|
38
|
+
const token = await dependencies.readCredential(dependencies.home);
|
|
39
|
+
if (generation !== undefined && (!/^[1-9][0-9]{0,14}$/.test(generation) || !Number.isSafeInteger(Number(generation)))) throw new Error('Invalid original execution generation');
|
|
40
|
+
const response = await requestTransport(trustedOrigin(dependencies.apiOrigin()),`/api/skill/feedback/${requestId}${generation === undefined ? '' : `?generation=${generation}`}`,
|
|
41
|
+
{ headers: { authorization: `Bearer ${token}` } });
|
|
42
|
+
const result = parseTransportResponse(response,'automatic feedback');
|
|
43
|
+
if (!response.ok) throw new Error(`Automatic feedback unavailable: HTTP ${response.status}; requestId=${requestId}`);
|
|
44
|
+
return verifyAutomaticFeedback(result.feedback,requestId);
|
|
45
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -8,12 +8,16 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { compareVersions } from './version.mjs';
|
|
9
9
|
import { credentialPath, readCredential, writeCredential } from './local-credential.mjs';
|
|
10
10
|
import { runLocal } from './local-cli.mjs';
|
|
11
|
+
import { runNative } from './native-client.mjs';
|
|
12
|
+
import { configureNativeNotifications, notificationSettings, sendNativeNotification } from './native-notification.mjs';
|
|
11
13
|
import { runTaskMessageCli } from './task-message-cli.mjs';
|
|
12
14
|
import { runScopeConnect } from './scope-runner-client.mjs';
|
|
13
15
|
import { apiUrl, buildInvocation, invokeRemote, queryFlows, queryStatus } from './remote-client.mjs';
|
|
14
16
|
const packageDir = path.dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
import { buildReview, submitSkillFeedback, feedbackNoticeVersion } from './review-client.mjs';
|
|
16
18
|
import { queryCloudTask, runCloudTask as dispatchCloudTask } from './cloud-task-client.mjs';
|
|
19
|
+
import { watchCloudTask } from './cloud-watch-client.mjs';
|
|
20
|
+
import { queryAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
17
21
|
const markerName = '.entrorove-managed.json';
|
|
18
22
|
const installFiles = ['SKILL.md', 'CAPABILITY-MAP.md', 'AGREEMENT.md', 'manifest.json'];
|
|
19
23
|
const markerSchema = 'entrorove.managed-install/1.0';
|
|
@@ -396,6 +400,13 @@ export async function runCloudStatus(input, dependencies = remoteDependencies())
|
|
|
396
400
|
if (['blocked', 'failed', 'missing'].includes(result.status)) process.exitCode = 2;
|
|
397
401
|
return result;
|
|
398
402
|
}
|
|
403
|
+
export async function runCloudWatch(input, dependencies = remoteDependencies()) {
|
|
404
|
+
const result = await watchCloudTask(input,{ ...dependencies,
|
|
405
|
+
onIdeEvent: event => process.stdout.write(`${JSON.stringify(event)}\n`) });
|
|
406
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
407
|
+
if (['blocked','failed','missing'].includes(result.status) || ['blocked','failed'].includes(result.taskStatus)) process.exitCode = 2;
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
399
410
|
|
|
400
411
|
async function invoke(data) {
|
|
401
412
|
if (data?.operation === 'cloud-task') {
|
|
@@ -404,6 +415,7 @@ async function invoke(data) {
|
|
|
404
415
|
if (data?.operation === 'cloud-status') {
|
|
405
416
|
return runCloudStatus(data.input, remoteDependencies());
|
|
406
417
|
}
|
|
418
|
+
if (data?.operation === 'cloud-watch') return runCloudWatch(data.input);
|
|
407
419
|
return invokeRemote(data, remoteDependencies());
|
|
408
420
|
}
|
|
409
421
|
|
|
@@ -430,6 +442,14 @@ async function main() {
|
|
|
430
442
|
process.stdout.write(JSON.stringify({ accepted, version: agreementVersion }) + '\n');
|
|
431
443
|
} else if (command === 'invoke' && args.length === 0) {
|
|
432
444
|
await invoke(await stdinJson());
|
|
445
|
+
} else if (command === 'native' && args.length === 2) {
|
|
446
|
+
await runNative(args[0], args[1], await stdinJson(), remoteDependencies());
|
|
447
|
+
} else if (command === 'notifications' && args.length === 1) {
|
|
448
|
+
const result = args[0] === 'configure' ? await configureNativeNotifications(await stdinJson())
|
|
449
|
+
: args[0] === 'status' ? await notificationSettings()
|
|
450
|
+
: args[0] === 'test' && (await notificationSettings()).enabled ? await sendNativeNotification('test') : null;
|
|
451
|
+
if (!result) throw new Error('Use notifications configure|status|test; enable this account before sending a test');
|
|
452
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
433
453
|
} else if (command === 'scope-connect' && args.length === 1) {
|
|
434
454
|
const result = await runScopeConnect(args[0], await stdinJson(), remoteDependencies());
|
|
435
455
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
@@ -440,6 +460,10 @@ async function main() {
|
|
|
440
460
|
await runCloudTask(await stdinJson());
|
|
441
461
|
} else if (command === 'cloud-status' && args.length === 0) {
|
|
442
462
|
await runCloudStatus(await stdinJson());
|
|
463
|
+
} else if (command === 'cloud-watch' && args.length === 0) {
|
|
464
|
+
await runCloudWatch(await stdinJson());
|
|
465
|
+
} else if (command === 'feedback' && [1,2].includes(args.length)) {
|
|
466
|
+
process.stdout.write(`${JSON.stringify(await queryAutomaticFeedback(args[0],remoteDependencies(),args[1]))}\n`);
|
|
443
467
|
} else if (command === 'review' && args.length === 0) {
|
|
444
468
|
await submitReview(await stdinJson());
|
|
445
469
|
} else if (command === 'status' && args.length === 1) {
|
|
@@ -461,7 +485,7 @@ async function main() {
|
|
|
461
485
|
} else if (command === 'local') {
|
|
462
486
|
await runLocal(args);
|
|
463
487
|
} else {
|
|
464
|
-
throw new Error('Usage: entrorove install | configure | agreement | invoke | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-status | status <requestId> | flows | review | local (JSON on stdin)');
|
|
488
|
+
throw new Error('Usage: entrorove install | configure | agreement | invoke | native <domain> <operation> | notifications configure|status|test | scope-connect <repositoryRoot> | host-message [adapterModulePath] | cloud-task | cloud-watch | cloud-status | feedback <requestId> [generation] | status <requestId> | flows | review | local (JSON on stdin)');
|
|
465
489
|
}
|
|
466
490
|
}
|
|
467
491
|
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function consumeCloudStream(body, onFrame) {
|
|
2
|
+
if (!body) throw new Error('Cloud event response has no stream');
|
|
3
|
+
const reader = body.getReader(), decoder = new TextDecoder('utf-8',{ fatal: true });
|
|
4
|
+
let pending = '', size = 0;
|
|
5
|
+
try {
|
|
6
|
+
while (true) {
|
|
7
|
+
const { value,done } = await reader.read();
|
|
8
|
+
if (value) size += value.byteLength;
|
|
9
|
+
if (size > 16_777_216) throw new Error('Cloud event stream exceeded its bounded read');
|
|
10
|
+
pending += decoder.decode(value,{ stream: !done });
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = /\r?\n\r?\n/u.exec(pending))) {
|
|
13
|
+
const frame = pending.slice(0,match.index); pending = pending.slice(match.index + match[0].length);
|
|
14
|
+
if (frame.length > 262144) throw new Error('Cloud event frame exceeded its bounded read');
|
|
15
|
+
let event = 'message', id, data = [];
|
|
16
|
+
for (const line of frame.split(/\r?\n/u)) {
|
|
17
|
+
if (line.startsWith('event:')) event = line.slice(6).trim();
|
|
18
|
+
else if (line.startsWith('id:')) id = line.slice(3).trim();
|
|
19
|
+
else if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /u,''));
|
|
20
|
+
}
|
|
21
|
+
if (data.length && await onFrame({ event,id,data: JSON.parse(data.join('\n')) }) === false) return;
|
|
22
|
+
}
|
|
23
|
+
if (pending.length > 262144) throw new Error('Cloud event frame exceeded its bounded read');
|
|
24
|
+
if (done) { if (pending.trim()) throw new Error('Cloud event response ended with an incomplete frame'); return; }
|
|
25
|
+
}
|
|
26
|
+
} finally { await reader.cancel().catch(() => undefined); reader.releaseLock(); }
|
|
27
|
+
}
|
package/cloud-task-client.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { appendInvocation } from './local-invocations.mjs';
|
|
3
3
|
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
4
|
+
import { displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
5
|
+
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
4
6
|
|
|
5
7
|
const CLOUD_TASK_SCHEMA = 'entrorove.cloud-task/1.0';
|
|
6
8
|
const CLOUD_STATUS_SCHEMA = 'entrorove.cloud-status/1.0';
|
|
@@ -26,7 +28,10 @@ function validateInput(input) {
|
|
|
26
28
|
if (input.ideTaskId !== undefined && (typeof input.ideTaskId !== 'string' || !input.ideTaskId.trim() || input.ideTaskId.length > 200)) {
|
|
27
29
|
throw new Error('cloud-task ideTaskId must be a non-empty string of at most 200 characters');
|
|
28
30
|
}
|
|
29
|
-
if (input.sandboxChoice !== undefined &&
|
|
31
|
+
if (input.sandboxChoice !== undefined && input.answer !== undefined && input.sandboxChoice !== input.answer) {
|
|
32
|
+
throw new Error('Conflicting cloud sandbox choices');
|
|
33
|
+
}
|
|
34
|
+
if ((input.sandboxChoice ?? input.answer) !== undefined && !['use', 'local'].includes(input.sandboxChoice ?? input.answer)) {
|
|
30
35
|
throw new Error('cloud-task sandboxChoice must be use or local');
|
|
31
36
|
}
|
|
32
37
|
return { goal: input.goal, ...(input.ideTaskId === undefined ? {} : { ideTaskId: input.ideTaskId }) };
|
|
@@ -48,18 +53,19 @@ function authenticatedHeaders(token, headers = {}) {
|
|
|
48
53
|
return { ...headers, authorization: `Bearer ${token}` };
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
export function sandboxChoiceInteraction(id) {
|
|
56
|
+
export function sandboxChoiceInteraction(id, task = {}) {
|
|
52
57
|
return {
|
|
53
58
|
schemaVersion: 'confirm.interaction/1.0', requestId: id, type: 'choice',
|
|
54
|
-
question: '
|
|
59
|
+
question: '是否启用云端沙箱执行思考规划?',
|
|
55
60
|
options: [
|
|
56
|
-
{ id: 'use', label: '
|
|
57
|
-
{ id: 'local', label: '
|
|
61
|
+
{ id: 'use', label: '确认', hint: '为当前任务创建云端思考规划并显示真实进度' },
|
|
62
|
+
{ id: 'local', label: '退出,采用本地方案', hint: '继续当前任务,不改变账号开关' },
|
|
58
63
|
],
|
|
59
|
-
default: 'local', timeout:
|
|
64
|
+
default: 'local', timeout: null, timeoutAction: 'local', risk: 'low',
|
|
60
65
|
riskDescription: '这是当前任务的执行位置选择,不改变账号级云端沙箱开关。',
|
|
61
66
|
rememberable: false, memoryKey: '',
|
|
62
|
-
callback: { operation: 'cloud-task', payload: {
|
|
67
|
+
callback: { operation: 'cloud-task', payload: { ...task } },
|
|
68
|
+
chatFallback: '是否启用云端沙箱执行思考规划?\n1. 确认:当前任务使用云端沙箱\n2. 退出:自动采用本地方案,继续当前任务',
|
|
63
69
|
};
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -76,24 +82,30 @@ export async function runCloudTask(input, dependencies) {
|
|
|
76
82
|
const task = validateInput(input);
|
|
77
83
|
validateDependencies(dependencies);
|
|
78
84
|
const id = requestId(task);
|
|
85
|
+
const choice = input.sandboxChoice ?? input.answer;
|
|
79
86
|
await dependencies.install();
|
|
80
87
|
if (!await dependencies.agreementAccepted(dependencies.home)) {
|
|
81
88
|
return envelope(task, 'blocked', { code: 'agreement-required', interaction: dependencies.agreementRequired(dependencies.home) });
|
|
82
89
|
}
|
|
83
|
-
if (
|
|
84
|
-
task, interaction: sandboxChoiceInteraction(id),
|
|
90
|
+
if (choice === undefined) return envelope(task, 'needs-input', {
|
|
91
|
+
task, interaction: sandboxChoiceInteraction(id,task),
|
|
92
|
+
});
|
|
93
|
+
if (choice === 'local') return envelope(task, 'skipped', {
|
|
94
|
+
reason: 'task', executionIsolation: 'local', message: '已退出云端沙箱,采用本地方案继续当前任务。',
|
|
95
|
+
continuation: { goal: task.goal,...(task.ideTaskId ? { ideTaskId: task.ideTaskId } : {}) },
|
|
85
96
|
});
|
|
86
|
-
if (input.sandboxChoice === 'local') return envelope(task, 'skipped', { reason: 'task' });
|
|
87
97
|
|
|
88
98
|
const token = await dependencies.readCredential(dependencies.home);
|
|
89
99
|
const origin = trustedOrigin(dependencies.apiOrigin());
|
|
90
100
|
const metadata = { kind: 'remote', operation: 'cloud-task', requestId: id };
|
|
91
101
|
await appendInvocation({ ...metadata, event: 'started' }, dependencies.home);
|
|
102
|
+
displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',requestId: id,kind: 'status',
|
|
103
|
+
phase: 'enter',message: '正在进入云端沙箱',separateLine: true },dependencies.onIdeEvent);
|
|
92
104
|
let response;
|
|
93
105
|
try {
|
|
94
106
|
response = await requestTransport(origin, '/api/cloud-task', {
|
|
95
107
|
method: 'POST', timeoutMs: 30_000,
|
|
96
|
-
headers: authenticatedHeaders(token, { 'content-type': 'application/json' }),
|
|
108
|
+
headers: authenticatedHeaders(token, { 'content-type': 'application/json',...ideClientHeaders() }),
|
|
97
109
|
body: JSON.stringify(task),
|
|
98
110
|
});
|
|
99
111
|
} catch (cause) {
|
|
@@ -124,7 +136,8 @@ export async function runCloudTask(input, dependencies) {
|
|
|
124
136
|
await appendInvocation({ ...metadata, event: 'completed' }, dependencies.home);
|
|
125
137
|
return envelope(task, 'started', { taskId: result.taskId,
|
|
126
138
|
taskStatus: typeof result.status === 'string' ? result.status : 'running',
|
|
127
|
-
executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker'
|
|
139
|
+
executionIsolation: typeof result.executionIsolation === 'string' ? result.executionIsolation : 'worker',
|
|
140
|
+
nextAction: { operation: 'cloud-watch',input: { taskId: result.taskId,afterId: '0' } } });
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
function publicTask(task) {
|
|
@@ -162,6 +175,7 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
162
175
|
code: typeof taskBody?.error === 'string' ? taskBody.error : taskResponse.status === 404 ? 'cloud-task-not-found' : 'cloud-status-unavailable', httpStatus: taskResponse.status };
|
|
163
176
|
}
|
|
164
177
|
const task = taskBody.task;
|
|
178
|
+
const feedback = taskBody.feedback ? verifyAutomaticFeedback(taskBody.feedback,taskBody.feedback.requestId) : undefined;
|
|
165
179
|
let usage = [];
|
|
166
180
|
let usageError;
|
|
167
181
|
const usageResponse = await requestTransport(origin, `/api/cloud-sandbox/tasks/${id}/usage`, {
|
|
@@ -174,6 +188,7 @@ export async function queryCloudTask(taskId, dependencies) {
|
|
|
174
188
|
} catch { usageError = 'cloud-usage-invalid-response'; }
|
|
175
189
|
await appendInvocation({ ...metadata, event: 'status' }, dependencies.home);
|
|
176
190
|
return { schemaVersion: CLOUD_STATUS_SCHEMA, status: 'observed', taskId: id,
|
|
177
|
-
taskStatus: task.status, task: publicTask(task), usage, ...(
|
|
191
|
+
taskStatus: task.status, task: publicTask(task), usage, ...(feedback ? { feedback } : {}),
|
|
192
|
+
...(usageError ? { usageError } : {}) };
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { trustedOrigin } from './remote-transport.mjs';
|
|
2
|
+
import { consumeCloudStream } from './cloud-event-stream.mjs';
|
|
3
|
+
import { automaticFeedbackIdeEvent, cloudIdeEvent, displayIdeEvent, ideClientHeaders } from './ide-presentation.mjs';
|
|
4
|
+
import { queryCloudTask } from './cloud-task-client.mjs';
|
|
5
|
+
|
|
6
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7
|
+
function inputOptions(input) {
|
|
8
|
+
if (!input || !UUID.test(input.taskId) || Object.keys(input).some(key => !['taskId','afterId','waitMs'].includes(key))) {
|
|
9
|
+
throw new Error('cloud-watch requires its real taskId and optional afterId and waitMs');
|
|
10
|
+
}
|
|
11
|
+
const afterId = input.afterId ?? '0', waitMs = input.waitMs ?? 15000;
|
|
12
|
+
if (typeof afterId !== 'string' || !/^(0|[1-9][0-9]{0,18})$/u.test(afterId) || BigInt(afterId) > 9223372036854775807n
|
|
13
|
+
|| !Number.isInteger(waitMs) || waitMs < 1 || waitMs > 45000) throw new Error('Invalid cloud-watch cursor or bounded wait');
|
|
14
|
+
return { taskId: input.taskId,afterId,waitMs };
|
|
15
|
+
}
|
|
16
|
+
export async function watchCloudTask(input, dependencies) {
|
|
17
|
+
const options = inputOptions(input), { taskId,waitMs } = options;
|
|
18
|
+
await dependencies.install();
|
|
19
|
+
if (!await dependencies.agreementAccepted(dependencies.home)) return { status: 'blocked',code: 'agreement-required',
|
|
20
|
+
interaction: dependencies.agreementRequired(dependencies.home) };
|
|
21
|
+
const token = await dependencies.readCredential(dependencies.home);
|
|
22
|
+
const controller = new AbortController(), deadline = setTimeout(() => controller.abort(),waitMs);
|
|
23
|
+
const events = [], emit = event => { events.push(event); displayIdeEvent(event,dependencies.onIdeEvent); };
|
|
24
|
+
let afterId = options.afterId, taskStatus = 'running', outputAvailable = false, settled = false, flowId;
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`${trustedOrigin(dependencies.apiOrigin())}/api/cloud-sandbox/tasks/${taskId}/events`, {
|
|
27
|
+
redirect: 'error',signal: controller.signal,headers: { authorization: `Bearer ${token}`,
|
|
28
|
+
accept: 'text/event-stream','last-event-id': afterId,...ideClientHeaders() } });
|
|
29
|
+
if (!response.ok) throw new Error(`Cloud event stream unavailable: HTTP ${response.status}; taskId=${taskId}`);
|
|
30
|
+
if (!response.headers.get('content-type')?.startsWith('text/event-stream')) throw new Error('Cloud event response is not an event stream');
|
|
31
|
+
await consumeCloudStream(response.body,frame => {
|
|
32
|
+
if (frame.event === 'stream-error') throw new Error(`Cloud event stream stopped: ${frame.data?.code ?? 'stream-failed'}`);
|
|
33
|
+
if (frame.event === 'status') {
|
|
34
|
+
if (frame.data?.taskId !== taskId || !['running','completed','blocked','failed','cancelled'].includes(frame.data.status)) throw new Error('Invalid cloud task status event');
|
|
35
|
+
taskStatus = frame.data.status; return taskStatus === 'running';
|
|
36
|
+
}
|
|
37
|
+
if (!/^[1-9][0-9]{0,18}$/u.test(frame.id ?? '') || frame.data?.id !== frame.id) throw new Error('Invalid cloud event cursor');
|
|
38
|
+
if (BigInt(frame.id) <= BigInt(afterId)) return;
|
|
39
|
+
const event = cloudIdeEvent(frame.data,taskId); afterId = frame.id; emit(event);
|
|
40
|
+
if (typeof frame.data.metadata?.flowId === 'string') flowId = frame.data.metadata.flowId;
|
|
41
|
+
settled = event.phase === 'workflow' && ['reported','verified','blocked','failed'].includes(event.state);
|
|
42
|
+
outputAvailable = settled && ['reported','verified'].includes(event.state);
|
|
43
|
+
if (settled) return false;
|
|
44
|
+
});
|
|
45
|
+
} catch (cause) {
|
|
46
|
+
if (!controller.signal.aborted) throw cause;
|
|
47
|
+
} finally { clearTimeout(deadline); controller.abort(); }
|
|
48
|
+
if (outputAvailable) emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'output',
|
|
49
|
+
message: '云端沙箱输出完毕;实际验证状态以任务回执为准。',separateLine: true });
|
|
50
|
+
const current = await queryCloudTask(taskId,dependencies);
|
|
51
|
+
if (current.status === 'observed') taskStatus = current.taskStatus;
|
|
52
|
+
if (current.feedback) emit(automaticFeedbackIdeEvent(current.feedback));
|
|
53
|
+
emit({ schemaVersion: 'entrorove.ide-event/1.0',taskId,kind: 'status',phase: 'exit',
|
|
54
|
+
message: settled || taskStatus !== 'running' ? '云端沙箱正在退出本次进度订阅' : '云端沙箱仍在执行,下次从当前进度继续读取',separateLine: true });
|
|
55
|
+
return { schemaVersion: 'entrorove.cloud-watch/1.0',status: current.status,taskId,taskStatus,afterId,outputAvailable,
|
|
56
|
+
events,...(flowId ? { flowId } : {}),task: current.task,usage: current.usage,...(current.feedback ? { feedback: current.feedback } : {}),
|
|
57
|
+
nextAction: taskStatus === 'running' && !settled ? { operation: 'cloud-watch',input: { taskId,afterId,waitMs } } : null };
|
|
58
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const UUID = /^[0-9a-f-]{36}$/i;
|
|
2
|
+
import { notifyIdeEvent } from './native-notification.mjs';
|
|
3
|
+
export function ideClientHeaders(environment = process.env) {
|
|
4
|
+
const ide = environment.ENTROROVE_IDE ?? (environment.CODEX_THREAD_ID ? 'Codex'
|
|
5
|
+
: environment.CURSOR_TRACE_ID ? 'Cursor' : environment.CLAUDE_CODE_ENTRYPOINT ? 'Claude Code' : 'Entrorove CLI');
|
|
6
|
+
if (typeof ide !== 'string' || !/^[\p{L}\p{N} .+_-]{2,48}$/u.test(ide)) throw new Error('Invalid ENTROROVE_IDE client name');
|
|
7
|
+
return { 'x-entrorove-ide': ide };
|
|
8
|
+
}
|
|
9
|
+
export function displayIdeEvent(event, sink) {
|
|
10
|
+
if (sink) sink(event);
|
|
11
|
+
else process.stderr.write(`${event.message}\n${event.detail ? event.detail + '\n' : ''}`);
|
|
12
|
+
void notifyIdeEvent(event).catch(() => process.stderr.write('Entrorove system notification unavailable; IDE progress remains visible.\n'));
|
|
13
|
+
}
|
|
14
|
+
function text(value) {
|
|
15
|
+
return typeof value === 'string' ? value.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/gu, '').slice(0,12000) : '';
|
|
16
|
+
}
|
|
17
|
+
export function cloudIdeEvent(value, taskId) {
|
|
18
|
+
if (!value || value.taskId !== taskId || !UUID.test(taskId) || !/^[1-9][0-9]{0,18}$/.test(value.id)
|
|
19
|
+
|| !['thinking','execution','notice','error'].includes(value.kind) || typeof value.content !== 'string') {
|
|
20
|
+
throw new Error('Cloud event lost its original task identity');
|
|
21
|
+
}
|
|
22
|
+
const metadata = value.metadata ?? {};
|
|
23
|
+
let message = text(value.content), phase = String(metadata.stepId ?? 'workflow');
|
|
24
|
+
if (message === 'Cloud task accepted.') message = '已进入云端沙箱,正在分配沙箱任务';
|
|
25
|
+
else if (message === 'Cloud workflow started.') message = '云端沙箱任务已分配';
|
|
26
|
+
else if (message === 'Cloud model classification started.') message = '云端沙箱正在思考中';
|
|
27
|
+
else if (message === 'Task classification completed.') message = '云端沙箱思考正在输出';
|
|
28
|
+
const event = { schemaVersion: 'entrorove.ide-event/1.0',taskId,eventId: value.id,kind: value.kind,
|
|
29
|
+
state: metadata.state ?? 'running',phase,message,separateLine: true };
|
|
30
|
+
if (metadata.requiresUserInput === true || phase === 'confirm' && event.state === 'pending') event.requiresConfirmation = true;
|
|
31
|
+
if (typeof metadata.summary === 'string' && metadata.summary !== value.content
|
|
32
|
+
&& value.content !== 'Cloud model classification started.') event.detail = text(metadata.summary);
|
|
33
|
+
if (value.kind === 'notice' && metadata.url !== undefined) {
|
|
34
|
+
let url;
|
|
35
|
+
try { url = new URL(metadata.url); } catch { throw new Error('Cloud promotion link is invalid'); }
|
|
36
|
+
if (url.protocol !== 'https:' || url.username || url.password) throw new Error('Cloud promotion requires a public HTTPS link');
|
|
37
|
+
event.url = url.href;
|
|
38
|
+
event.message = `[${message.replace(/[\\[\]]/gu, '\\$&')}](<${url.href}>)`;
|
|
39
|
+
}
|
|
40
|
+
return event;
|
|
41
|
+
}
|
|
42
|
+
export function invocationIdeNotices(output) {
|
|
43
|
+
const strings = [output?.message,output?.result?.message,
|
|
44
|
+
...(Array.isArray(output?.progress) ? output.progress.filter(item => item.kind === 'notice').map(item => item.summary) : [])];
|
|
45
|
+
return [...new Set(strings.filter(value => typeof value === 'string').flatMap(value => value.split(/\r?\n/u))
|
|
46
|
+
.filter(value => value === '智能执行中,请勿关闭!' || /^【[^\n】]{1,32}】/u.test(value)).map(text))];
|
|
47
|
+
}
|
|
48
|
+
export function presentInvocationNotices(result, sink) {
|
|
49
|
+
const messages = invocationIdeNotices(result?.output);
|
|
50
|
+
for (const message of messages) displayIdeEvent({ schemaVersion: 'entrorove.ide-event/1.0',
|
|
51
|
+
requestId: result.output.requestId,kind: 'notice',message,separateLine: true },sink);
|
|
52
|
+
if (result?.feedback) {
|
|
53
|
+
const event = automaticFeedbackIdeEvent(result.feedback);
|
|
54
|
+
messages.push(event.message); displayIdeEvent(event,sink);
|
|
55
|
+
}
|
|
56
|
+
return messages.length ? { ...result,ideMessages: messages } : result;
|
|
57
|
+
}
|
|
58
|
+
export function automaticFeedbackIdeEvent(feedback) {
|
|
59
|
+
const disposition = feedback.evaluation.disposition;
|
|
60
|
+
const status = disposition === 'good' ? '当前调用验证通过' : disposition === 'bad'
|
|
61
|
+
? '当前调用被阻断或验证失败' : '交付验证尚未完成,暂不评分';
|
|
62
|
+
return { schemaVersion: 'entrorove.ide-event/1.0',requestId: feedback.requestId,receiptId: feedback.receiptId,
|
|
63
|
+
kind: 'evaluation',phase: 'feedback',disposition, message: `本次调用评价已回传至服务器:${status}。`,separateLine: true };
|
|
64
|
+
}
|
|
@@ -67,15 +67,17 @@ export async function coordinateLocalPeer17(rawInput) {
|
|
|
67
67
|
return scanIntent17(fresh.state, current, intent, await physicalPeers17(current));
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
export async function statusLocalPeer17(rawInput) {
|
|
70
|
+
export async function statusLocalPeer17(rawInput, options = {}) {
|
|
71
71
|
const input = input17(rawInput, ['intentId']); await storageBinding(input);
|
|
72
|
-
|
|
72
|
+
const observe = async loaded => {
|
|
73
73
|
const task = coordTask17(loaded.state, input), intent = loaded.state.intents.find(value => value.intentId === input.intentId && value.taskId === task.taskId);
|
|
74
74
|
if (!intent) coordFail17('ENTROROVE_COORD_INTENT_BINDING', 'peer status must name its original task intent');
|
|
75
75
|
await coordNative17(task.material, false);
|
|
76
76
|
const view = scanIntent17(loaded.state, task, intent, await physicalPeers17(task));
|
|
77
|
-
return { ...view,
|
|
78
|
-
|
|
77
|
+
return { ...view, ...(options.readOnly ? { readOnly: true, sequence: loaded.sequence, head: loaded.head } : {}),
|
|
78
|
+
spawnRequest: loaded.state.spawnRequests.find(value => value.spawnRequestId === view.spawnRequestId) ?? null };
|
|
79
|
+
};
|
|
80
|
+
return options.readOnly ? observe(await readCoordination17(input)) : withCoordination17(input, 'peer-status', observe);
|
|
79
81
|
}
|
|
80
82
|
export async function completeLocalPeer17(rawInput) {
|
|
81
83
|
const input = input17(rawInput, ['intentId']); await storageBinding(input);
|
package/local-cli.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withLocalInvocation } from './local-invocations.mjs';
|
|
2
|
+
import { readLocalPeerStatus } from './local-peer-status.mjs';
|
|
2
3
|
import { peerCoordinate, spawnBind, completePeer, verifyCoordinationLease } from './local-swarm.mjs';
|
|
3
4
|
import { attestMutationSnapshot14 } from './local-mutation-binding.mjs';
|
|
4
5
|
import { recordMutationDenied14 } from './local-mutation-audit.mjs';
|
|
@@ -42,6 +43,7 @@ const operations = {
|
|
|
42
43
|
'budget-extend': budgetExtend, 'peer-coordinate': peerCoordinate, coordinate: peerCoordinate,
|
|
43
44
|
'spawn-bind': spawnBind, 'peer-complete': completePeer, complete: completePeer,
|
|
44
45
|
'lease-verify': verifyCoordinationLease,
|
|
46
|
+
'peer-status': readLocalPeerStatus,
|
|
45
47
|
'evidence-sign': signEvidence, 'evidence-verify': verifyEvidence, 'merge-verified': mergeVerified,
|
|
46
48
|
rollback, 'merge-freeze': prepareMergeArtifacts, 'merge-preflight': produceMergeArchguardPreflight,
|
|
47
49
|
'merge-journal-head': produceMergeJournalHead, 'final-gate': finalGate,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { peerStatus } from './local-swarm.mjs';
|
|
2
|
+
import { statusLocalPeer17 } from './local-autocoord17-controller.mjs';
|
|
3
|
+
|
|
4
|
+
export function readLocalPeerStatus(input) {
|
|
5
|
+
return Object.hasOwn(input, 'coordinationHome') ? statusLocalPeer17(input, { readOnly: true }) : peerStatus(input);
|
|
6
|
+
}
|
|
@@ -33,9 +33,12 @@ const spawn = object({ ...common, priority, parentWorkerId: nullable(text) }, [.
|
|
|
33
33
|
const complete = object({ ...common, status: enumeration(['completed', 'failed', 'blocked']),
|
|
34
34
|
approvalPath: nullable(text), signerConfigPath: nullable(text) }, required);
|
|
35
35
|
const verify = object(common, [...required, 'paths']);
|
|
36
|
+
const peerQuery = object({ ...common, intentId: sha256, priority, workerCount: integer(0, 50) }, required);
|
|
37
|
+
const coordinationQuery = exact({ repositoryRoot: text, coordinationHome: text, taskId: id, agentId: id, chainId: id, intentId: id });
|
|
36
38
|
export const SWARM_LOCAL_OPERATION_SCHEMAS = Object.freeze({
|
|
37
39
|
'peer-coordinate': coordinate, coordinate, 'spawn-bind': spawn,
|
|
38
40
|
'peer-complete': complete, complete, 'lease-verify': verify,
|
|
41
|
+
'peer-status': { oneOf: [peerQuery, coordinationQuery] },
|
|
39
42
|
});
|
|
40
43
|
// Capture the authority before verification; the gate must verify this exact
|
|
41
44
|
// binding before signing it into the existing pass. This never issues a grant.
|
package/local-swarm.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { withStateLock } from './local-state-lock.mjs';
|
|
|
8
8
|
import { withTaskHandoffIssueFence, withTaskHandoffReadFence } from './local-task-handoff-fence.mjs';
|
|
9
9
|
import { captureSwarmCompletion, captureSwarmOwnershipCompletions, revalidateSwarmCompletion } from './local-swarm-completion.mjs';
|
|
10
10
|
import { claimSwarmWriteIntent, closeSwarmWriteIntent, readSwarmCheckpointRequests,
|
|
11
|
-
registerSwarmWriteIntent, validateSwarmWriteIntents } from './local-swarm-priority.mjs';
|
|
11
|
+
readSwarmWriteIntentHistory, registerSwarmWriteIntent, validateSwarmWriteIntents } from './local-swarm-priority.mjs';
|
|
12
12
|
|
|
13
13
|
function fail(code, message, details = {}) { throw Object.assign(new Error(message), { code, ...details }); }
|
|
14
14
|
function freshState(authority) {
|
|
@@ -173,6 +173,37 @@ async function operate(rawInput, operation) {
|
|
|
173
173
|
}));
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
|
+
|
|
177
|
+
export async function peerStatus(input) {
|
|
178
|
+
const original = await readSwarmCloudAuthority(input, 'report');
|
|
179
|
+
const storage = await swarmStorage(original, 'read');
|
|
180
|
+
return withStateLock(storage.ownershipFile, () => withStateLock(storage.stateFile, async () => {
|
|
181
|
+
const authority = await readSwarmCloudAuthority(input, 'report');
|
|
182
|
+
const ownership = await validateSwarmOwnership(await readSwarmOwnership(storage, authority, 'read'), authority);
|
|
183
|
+
const { index, verified } = await readSwarmWriteIntentHistory(ownership, authority);
|
|
184
|
+
const state = validateStoredWorkers(await readSwarmJson(storage.stateFile, null), authority);
|
|
185
|
+
const intentId = sha256(canonical(input.delegationGrant));
|
|
186
|
+
if (input.intentId !== undefined && input.intentId !== intentId) fail('ENTROROVE_SWARM_INTENT_BINDING', 'status must query the original signed intent');
|
|
187
|
+
const intent = index.intents.find(value => value.intentId === intentId);
|
|
188
|
+
if (!intent || !verified.has(intentId)) fail('ENTROROVE_SWARM_INTENT_BINDING', 'the original peer intent has not been coordinated');
|
|
189
|
+
const worker = state.workers.find(value => value.workerId === authority.grant.workerId && value.leaseId === authority.lease.leaseId);
|
|
190
|
+
if (worker && worker.grantDigest !== intentId) fail('ENTROROVE_SWARM_LEASE_INVALID', 'worker no longer matches the original intent');
|
|
191
|
+
const expired = Date.parse(intent.expiresAt) <= Date.now();
|
|
192
|
+
const conflicting = ownership.records.filter(value => value.grantDigest !== intentId);
|
|
193
|
+
const blockedPaths = intent.paths.filter(target => conflicting.some(value => value.paths.some(candidate =>
|
|
194
|
+
candidate === target || candidate.startsWith(`${target}/`) || target.startsWith(`${candidate}/`))));
|
|
195
|
+
const checkpoints = index.checkpointRequests.filter(value => value.targetGrantDigest === intentId);
|
|
196
|
+
await readSwarmCloudAuthority(input, 'report');
|
|
197
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', readOnly: true,
|
|
198
|
+
flowId: authority.plan.flowId, taskId: authority.grant.taskId, workerId: authority.grant.workerId,
|
|
199
|
+
leaseId: authority.lease.leaseId, intentId, intentStatus: intent.status,
|
|
200
|
+
executionStatus: expired && worker?.status === 'active' ? 'expired' : worker?.status ?? (expired ? 'expired' : 'waiting'), expiresAt: intent.expiresAt,
|
|
201
|
+
priority: intent.priority, paths: [...intent.paths], blockedPaths,
|
|
202
|
+
readyPaths: intent.paths.filter(target => !blockedPaths.includes(target)),
|
|
203
|
+
checkpointRequests: checkpoints.map(value => ({ requestId: value.requestId, paths: value.paths, reason: value.reason })),
|
|
204
|
+
leaseActive: !expired && worker?.status === 'active', writeAuthorized: false, taskCompletionVerified: false };
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
176
207
|
export async function peerCoordinate(input) { return operate(input, 'coordinate'); }
|
|
177
208
|
export async function spawnBind(input) { return operate(input, 'spawn'); }
|
|
178
209
|
export async function completePeer(input) { return operate(input, 'complete'); }
|
package/manifest.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { postInvocation } from './remote-client.mjs';
|
|
4
|
+
import { presentInvocationNotices } from './ide-presentation.mjs';
|
|
5
|
+
import { trustedOrigin } from './remote-transport.mjs';
|
|
6
|
+
|
|
7
|
+
const domains = new Set(['aimlock', 'blueprint', 'calctool', 'swarm', 'validator', 'mergeguard', 'archguard', 'confirm-protocol']);
|
|
8
|
+
const runtimeCodes = { aimlock: 'R3mQ8kWpXn', blueprint: 'wvz6zmRWmX', calctool: 'KKyA6xljUX', swarm: 'zj7fTPVh4p',
|
|
9
|
+
validator: 'Xx9ZkQmW3p', mergeguard: 'Mm7GnPqR2v', archguard: 'Ag4Ch8Rd2K', 'confirm-protocol': 'Cf8Pr7Tm2Q' };
|
|
10
|
+
export function buildNativeInvocation(domain, operation, input, origin = 'https://entrorove.com') {
|
|
11
|
+
if (!domains.has(domain) || typeof operation !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(operation)
|
|
12
|
+
|| !input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Invalid native domain, operation or input');
|
|
13
|
+
const requestId = sha256(`${domain}:${operation}:${canonical(input)}`);
|
|
14
|
+
const body = { schemaVersion: `${domain}.skill.request/${domain === 'aimlock' ? '1.1' : '1.0'}`, requestId, operation, input };
|
|
15
|
+
const runtimeCode = runtimeCodes[domain];
|
|
16
|
+
const transport = { flowId: `native:${runtimeCode}`, operation,
|
|
17
|
+
payload: { schemaVersion: 'entrorove.native-request-transport/1.0', runtimeCode, request: body } };
|
|
18
|
+
return { url: `${trustedOrigin(origin)}/api/skills/${domain}/invoke`, requestId, body,
|
|
19
|
+
feedbackHashes: [sha256('continue' + canonical(transport)), sha256(operation + canonical(input))] };
|
|
20
|
+
}
|
|
21
|
+
function discoveredContract(output, operation) {
|
|
22
|
+
const payload = output.output ?? output;
|
|
23
|
+
const capabilities = payload.capabilities ?? payload;
|
|
24
|
+
const operations = capabilities.operations ?? capabilities.operationStatus?.implementedPure;
|
|
25
|
+
const schemas = payload.operationSchemas ?? capabilities.operationSchemas;
|
|
26
|
+
const schema = Array.isArray(schemas) ? schemas.find(value => value.operation === operation) : schemas?.[operation];
|
|
27
|
+
if (!operations?.includes(operation) || !(schema?.input ?? schema?.inputSchema)) throw new Error(`Native capability does not support ${operation}`);
|
|
28
|
+
return schema.input ?? schema.inputSchema;
|
|
29
|
+
}
|
|
30
|
+
export async function invokeNativeResult(domain, operation, input, dependencies) {
|
|
31
|
+
const { home } = dependencies;
|
|
32
|
+
await dependencies.install();
|
|
33
|
+
if (!await dependencies.agreementAccepted(home)) throw Object.assign(new Error('Entrorove agreement requires explicit acceptance'),
|
|
34
|
+
{ interaction: dependencies.agreementRequired(home) });
|
|
35
|
+
const call = buildNativeInvocation(domain, operation, input, dependencies.apiOrigin());
|
|
36
|
+
const token = await dependencies.readCredential(home);
|
|
37
|
+
let feedbackRequired = operation !== 'capabilities';
|
|
38
|
+
if (operation !== 'capabilities') {
|
|
39
|
+
const discovery = await postInvocation(buildNativeInvocation(domain, 'capabilities', {}, dependencies.apiOrigin()), token, home);
|
|
40
|
+
if (!discovery.response.ok || discovery.result.output.status !== 'succeeded') throw new Error('Native capability discovery failed');
|
|
41
|
+
const found = discovery.result.output;
|
|
42
|
+
const extension = (found.capabilities ?? found).responseExtensions;
|
|
43
|
+
if (extension?.automaticFeedback === 'entrorove.automatic-feedback/1.0') feedbackRequired = !extension.automaticFeedbackObservations.includes(operation);
|
|
44
|
+
const ajv = new Ajv({ strict: true, strictRequired: false, allErrors: true });
|
|
45
|
+
ajv.addKeyword({ keyword: 'optional', schemaType: 'boolean', valid: true });
|
|
46
|
+
ajv.addFormat('uuid', /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
47
|
+
ajv.addFormat('date-time', value => Number.isFinite(Date.parse(value)));
|
|
48
|
+
const validate = ajv.compile(discoveredContract(discovery.result.output, operation));
|
|
49
|
+
if (!validate(input)) throw new Error(`Invalid native ${operation} input: ${ajv.errorsText(validate.errors)}`);
|
|
50
|
+
}
|
|
51
|
+
return postInvocation(call, token, home, feedbackRequired);
|
|
52
|
+
}
|
|
53
|
+
export async function runNative(domain, operation, input, dependencies) {
|
|
54
|
+
const { response, result } = await invokeNativeResult(domain, operation, input, dependencies);
|
|
55
|
+
process.stdout.write(`${JSON.stringify(presentInvocationNotices(result, dependencies.onIdeEvent))}\n`);
|
|
56
|
+
if (!response.ok || ['blocked', 'failed', 'incomplete'].includes(result.output.status)) process.exitCode = 2;
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { constants, promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { sha256 } from './local-integrity.mjs';
|
|
6
|
+
import { readCredential } from './local-credential.mjs';
|
|
7
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
8
|
+
|
|
9
|
+
const messageKinds = { confirmed: '任务需要你确认。', failed: '任务执行失败,请查看 IDE 中的状态。',
|
|
10
|
+
blocked: '任务已阻断,请查看 IDE 中的所需操作。',
|
|
11
|
+
reported: '任务输出已生成,请查看 IDE 中的结果。', verified: '任务验证已完成。', test: 'Entrorove 系统通知测试。' };
|
|
12
|
+
function controls(home) { return path.join(home, '.entrorove', 'notifications.json'); }
|
|
13
|
+
async function readControls(home) {
|
|
14
|
+
const file = controls(home);
|
|
15
|
+
let stat;
|
|
16
|
+
try { stat = await fs.lstat(file); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
17
|
+
if (!stat.isFile() || stat.isSymbolicLink() || process.getuid && stat.uid !== process.getuid()
|
|
18
|
+
|| process.platform !== 'win32' && stat.mode & 0o077) throw new Error('Notification settings must be private');
|
|
19
|
+
const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
20
|
+
try {
|
|
21
|
+
const state = JSON.parse(await handle.readFile('utf8'));
|
|
22
|
+
if (state.schemaVersion !== 'entrorove.native-notifications/1.0' || typeof state.enabled !== 'boolean'
|
|
23
|
+
|| !/^[a-f0-9]{64}$/.test(state.accountBinding) || !Array.isArray(state.seen) || state.seen.length > 100) throw new Error('Invalid notification settings');
|
|
24
|
+
return state;
|
|
25
|
+
} finally { await handle.close(); }
|
|
26
|
+
}
|
|
27
|
+
async function saveControls(home, state) {
|
|
28
|
+
const file = controls(home), temp = `${file}.tmp-${process.pid}`;
|
|
29
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
30
|
+
try { await fs.writeFile(temp, JSON.stringify(state) + '\n', { flag: 'wx', mode: 0o600 }); await fs.rename(temp, file); }
|
|
31
|
+
finally { await fs.rm(temp, { force: true }); }
|
|
32
|
+
}
|
|
33
|
+
export async function notificationSettings(home = os.homedir()) {
|
|
34
|
+
const state = await readControls(home), binding = sha256(await readCredential(home));
|
|
35
|
+
return { enabled: state?.enabled === true && state.accountBinding === binding,
|
|
36
|
+
platform: process.platform, accountBound: Boolean(state && state.accountBinding === binding) };
|
|
37
|
+
}
|
|
38
|
+
export async function configureNativeNotifications(input, home = os.homedir()) {
|
|
39
|
+
if (!input || Object.keys(input).length !== 1 || typeof input.enabled !== 'boolean') throw new Error('Expected {enabled:boolean}');
|
|
40
|
+
const state = { schemaVersion: 'entrorove.native-notifications/1.0', enabled: input.enabled,
|
|
41
|
+
accountBinding: sha256(await readCredential(home)), seen: [] };
|
|
42
|
+
await withStateLock(controls(home), () => saveControls(home, state));
|
|
43
|
+
return notificationSettings(home);
|
|
44
|
+
}
|
|
45
|
+
export async function sendNativeNotification(kind, { platform = process.platform, execute = execFile } = {}) {
|
|
46
|
+
const message = messageKinds[kind];
|
|
47
|
+
if (!message) throw new Error('Invalid notification kind');
|
|
48
|
+
let file, args;
|
|
49
|
+
if (platform === 'darwin') {
|
|
50
|
+
file = '/usr/bin/osascript';
|
|
51
|
+
args = ['-e', 'on run argv\ndisplay notification (item 1 of argv) with title "Entrorove"\nend run', message];
|
|
52
|
+
} else if (platform === 'linux') { file = 'notify-send'; args = ['--app-name=Entrorove', 'Entrorove', message]; }
|
|
53
|
+
else throw new Error('Native notifications require macOS or a Linux notification service; this platform is unsupported');
|
|
54
|
+
await new Promise((resolve, reject) => execute(file, args, { timeout: 5000, windowsHide: true }, error => {
|
|
55
|
+
if (error) reject(new Error('System notification command failed; check OS notification permissions'));
|
|
56
|
+
else resolve();
|
|
57
|
+
}));
|
|
58
|
+
return { submitted: true, displayVerified: false };
|
|
59
|
+
}
|
|
60
|
+
export async function notifyIdeEvent(event, { home = os.homedir(), send = sendNativeNotification } = {}) {
|
|
61
|
+
const kind = event.requiresConfirmation && event.kind !== 'notice' ? 'confirmed'
|
|
62
|
+
: event.kind === 'evaluation' ? event.disposition === 'bad' ? 'failed' : null
|
|
63
|
+
: event.state === 'blocked' ? event.requiresConfirmation ? 'confirmed' : 'blocked'
|
|
64
|
+
: event.phase === 'workflow' && ['reported', 'verified', 'failed'].includes(event.state) ? event.state : null;
|
|
65
|
+
if (!kind || event.kind === 'notice') return { submitted: false };
|
|
66
|
+
const id = event.taskId ?? event.requestId;
|
|
67
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9._:-]{1,128}$/.test(id)) return { submitted: false };
|
|
68
|
+
return withStateLock(controls(home), async () => {
|
|
69
|
+
const state = await readControls(home);
|
|
70
|
+
if (!state?.enabled || state.accountBinding !== sha256(await readCredential(home))) return { submitted: false };
|
|
71
|
+
const key = sha256(`${id}:${kind}`);
|
|
72
|
+
if (state.seen.includes(key)) return { submitted: false, duplicate: true };
|
|
73
|
+
state.seen = [...state.seen.slice(-99), key]; await saveControls(home, state);
|
|
74
|
+
return send(kind);
|
|
75
|
+
});
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"local-swarm-state.mjs",
|
|
25
25
|
"local-swarm-storage.mjs",
|
|
26
26
|
"local-swarm.mjs",
|
|
27
|
+
"local-peer-status.mjs",
|
|
27
28
|
"local-autocoord17-authority.mjs",
|
|
28
29
|
"local-autocoord17-controller.mjs",
|
|
29
30
|
"local-autocoord17-peers.mjs",
|
|
@@ -78,6 +79,10 @@
|
|
|
78
79
|
"remote-client.mjs",
|
|
79
80
|
"scope-runner-client.mjs",
|
|
80
81
|
"cloud-task-client.mjs",
|
|
82
|
+
"cloud-watch-client.mjs",
|
|
83
|
+
"cloud-event-stream.mjs",
|
|
84
|
+
"ide-presentation.mjs",
|
|
85
|
+
"automatic-feedback-client.mjs",
|
|
81
86
|
"remote-transport.mjs",
|
|
82
87
|
"local-credential.mjs",
|
|
83
88
|
"local-invocations.mjs",
|
|
@@ -185,7 +190,9 @@
|
|
|
185
190
|
"local-final-gate16-state.mjs",
|
|
186
191
|
"local-final-gate16-proof.mjs",
|
|
187
192
|
"local-final-gate16-contract.mjs",
|
|
188
|
-
"local-final-gate16-release.mjs"
|
|
193
|
+
"local-final-gate16-release.mjs",
|
|
194
|
+
"native-client.mjs",
|
|
195
|
+
"native-notification.mjs"
|
|
189
196
|
],
|
|
190
197
|
"scripts": {
|
|
191
198
|
"test": "node --test --test-concurrency=1 test/*.test.mjs",
|
package/release-state.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.35",
|
|
4
|
+
"digest": "a6c953df9972ceb589a3e0bbbd057a3db5fb30f6811962d6c984a8d2cf827f83",
|
|
5
5
|
"files": {
|
|
6
6
|
"local-final-gate16.mjs": "b30caab08441449ad556a6e9a3cefbf91190f0b3f7370f46d2a21156375aa8e3",
|
|
7
7
|
"local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
|
|
@@ -30,30 +30,30 @@
|
|
|
30
30
|
"local-swarm-authority.mjs": "3d4c149b82c465a81419da40e6edd93c88e68af7bc5aa09c76aac6516c4596eb",
|
|
31
31
|
"local-swarm-baseline.mjs": "e6c22f2ab0876ce43296147ae255343e37f728aac80afd4adb5e58cfc022b7d5",
|
|
32
32
|
"local-swarm-completion.mjs": "2ade4ebe7e7612a22630784c82a33b982143aa56243e7d006f2b3f96d8c5368b",
|
|
33
|
-
"local-swarm-contracts.mjs": "
|
|
33
|
+
"local-swarm-contracts.mjs": "f9fd409ab76da0a2ed0bee8a918fdd250b9368840ef0f5e797db6d1cc7dde0f6",
|
|
34
34
|
"local-swarm-mutation.mjs": "15b8d0c4c4ea5bd17b4653d84968fc02252d9ccdca2b137ec71b0f08717f90f5",
|
|
35
35
|
"local-swarm-priority.mjs": "7dc595177f6bcfadc83457e9603edcd731758bfe42e5d517e156610e2cddd732",
|
|
36
36
|
"local-swarm-state.mjs": "ff84c5f35ef3e4a0ad9887d145e404f5786ae0dc92496116699d031b98e90c6a",
|
|
37
37
|
"local-swarm-storage.mjs": "4b506481913fe22769f47a14a024a2cc4c0f1e986bb52ef606d14c7844226086",
|
|
38
|
-
"local-swarm.mjs": "
|
|
38
|
+
"local-swarm.mjs": "8bb6e22f96b7677be9cc24e7fbbd11dfea331d6d607868ebaa3399f8a519782b",
|
|
39
39
|
"local-autocoord17-authority.mjs": "86b79798f6019523b1260b819622070a5b0d39670f607da930fb6923259dff1d",
|
|
40
|
-
"local-autocoord17-controller.mjs": "
|
|
40
|
+
"local-autocoord17-controller.mjs": "ada914d07d8773e2eb1fb45a35378effd00def697eeb37f209c81834880a2e4b",
|
|
41
41
|
"local-autocoord17-peers.mjs": "103d6a6634bbd3b4b850c5db7b571e3f05736b94e59a8fd7da63741281c903a4",
|
|
42
42
|
"local-autocoord17-store.mjs": "b48847a4a40c3aeb16bc1431b6a16221fb5ffdc0a6ba5c263acf196279d80ed1",
|
|
43
43
|
"local-autocoord17-tasks.mjs": "44b9f2d326ab3060c36657685b48b50a98711644e5baf47d4e8cb72c4c45f2a7",
|
|
44
44
|
"local-autocoord17-waits.mjs": "2b0a713987822548a94e1ed3a9a891532db45383797dfc5d5bc8d1e35593cf25",
|
|
45
45
|
"local-validator-isolation17.mjs": "48e94604e28b8fddd567dfb35ff60b73fd094a5803bd49f076388b7f310f92aa",
|
|
46
46
|
"local-calctool-composition17.mjs": "ac89fcb41ab0d0d9b47c1b5696945281b0894eb064820775e36a011c76314e6a",
|
|
47
|
-
"cli.mjs": "
|
|
47
|
+
"cli.mjs": "f0a3104ef9f4431ded2a7049c08ae6435b09ed53c4cba486c2bd52db2728d999",
|
|
48
48
|
"review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
|
|
49
49
|
"version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
|
|
50
|
-
"SKILL.md": "
|
|
51
|
-
"CAPABILITY-MAP.md": "
|
|
50
|
+
"SKILL.md": "ffea399b940f9eedd82d2c5e2f9b3bce606f47b27020c2c1b3c8b128e3a2161a",
|
|
51
|
+
"CAPABILITY-MAP.md": "8e1d007d594ae82dd77f89842cf1aa8d17db56f9cac98691759390c28162d848",
|
|
52
52
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
53
|
-
"package.json": "
|
|
53
|
+
"package.json": "9a7f12414556bcc150d57e64881694aeddfe0b760e98454491ac5a6585ed0421",
|
|
54
54
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
55
|
-
"release.mjs": "
|
|
56
|
-
"local-cli.mjs": "
|
|
55
|
+
"release.mjs": "eca6032eae240fb90c7646e0b1509ea6b4c94c23b90aca87e4091996c5b9b5f8",
|
|
56
|
+
"local-cli.mjs": "9eca8aefcb086cdce792105f9558cc26e34a0c88befaf0a56f4c7bdbd35f0a05",
|
|
57
57
|
"local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
|
|
58
58
|
"local-execution.mjs": "cdada6df6dc8831cebb58a629b56c4b3f8e1bf9aae4e3fdc121521ba20839383",
|
|
59
59
|
"local-integrity.mjs": "4f325376e536999c3b00245f66aac9ef1ae762c33012b363c231622008f72805",
|
|
@@ -118,9 +118,16 @@
|
|
|
118
118
|
"local-scope-state.mjs": "8b600a91fc0068d7a8c0bb752ddba7ed30ada93daafb118d6cba2368fd94cb27",
|
|
119
119
|
"local-scope-workspaces.mjs": "a44c34537e5ed2dcaf193a985e5bc05957989fbb160a650f9e37ba7d66eb2de8",
|
|
120
120
|
"local-task-routing.mjs": "e26d6f1ba134ffe1da2a49b4fb6bb6dfa5617671f774a08e719d04f202cf5eca",
|
|
121
|
-
"remote-client.mjs": "
|
|
122
|
-
"cloud-task-client.mjs": "
|
|
123
|
-
"scope-runner-client.mjs": "
|
|
121
|
+
"remote-client.mjs": "f676d17c867e9abd59943528a7716d444a0cf2fa637d48a1e397cbce9f37c640",
|
|
122
|
+
"cloud-task-client.mjs": "ecb6124ea5e6249f31fb95cb68d13d821d2daea8768fe47ddf6dcf82fc3392cc",
|
|
123
|
+
"scope-runner-client.mjs": "646ab4f80d4a8209e922fd45f40efd968f0744d9dfc03188b222c1a2e11e83b8",
|
|
124
|
+
"native-client.mjs": "d0f8a52e1195dd72ba0ae1febe25f0d6c37eff6ecf010e568935cacdf3c975bb",
|
|
125
|
+
"native-notification.mjs": "f116772178140d1a2a7aac4a8e361a76543b1f47c84b3823a26a797f11a8bfbb",
|
|
126
|
+
"local-peer-status.mjs": "a971bcc69d60bb32b875ca5bb4297d948631d5d2821b812c279613ea059a8b4f",
|
|
127
|
+
"cloud-watch-client.mjs": "bfbe636c10af467855776f9d29008d9ce13420b66bcc91b5bf8f6ef79fb2aefe",
|
|
128
|
+
"cloud-event-stream.mjs": "e0eec9fc3e0613094523929634f25030df5ca5c5b5fe2c1e1a4586ab10574808",
|
|
129
|
+
"ide-presentation.mjs": "dcf4efaa546a35752247a64dc4a6d0c0feaf7c3c2c8222330288b1202c0484f9",
|
|
130
|
+
"automatic-feedback-client.mjs": "eeedc0344f91603ff8523fdbc43569a561ed10fec4178cc898db6d61f7663246",
|
|
124
131
|
"remote-transport.mjs": "5e5a8c0b490371f7f5a052cffbeee6202d4cf713dff1c43b6d910fe944f8429c",
|
|
125
132
|
"local-credential.mjs": "2a302a7f67b5ea8dc73453fc96ab33db349b59ca11fa9456e48ca1d9669d877c",
|
|
126
133
|
"local-invocations.mjs": "f6ac4c41c1a5ec34de358c4cca210ca8e403b3a913150a43bb55ec9be55d9998",
|
package/release.mjs
CHANGED
|
@@ -92,6 +92,8 @@ const sourceFiles = [
|
|
|
92
92
|
'local-scope-workspaces.mjs',
|
|
93
93
|
'local-task-routing.mjs', 'remote-client.mjs', 'cloud-task-client.mjs',
|
|
94
94
|
'scope-runner-client.mjs',
|
|
95
|
+
'native-client.mjs', 'native-notification.mjs', 'local-peer-status.mjs',
|
|
96
|
+
'cloud-watch-client.mjs', 'cloud-event-stream.mjs', 'ide-presentation.mjs', 'automatic-feedback-client.mjs',
|
|
95
97
|
'remote-transport.mjs', 'local-credential.mjs', 'local-invocations.mjs',
|
|
96
98
|
'local-task-handoff-fence.mjs',
|
|
97
99
|
'local-task-handoff-storage.mjs', 'local-task-handoff-overlay-wire.mjs', 'local-task-handoff-completion-wire.mjs',
|
|
@@ -351,9 +353,16 @@ function releaseLabels() {
|
|
|
351
353
|
'workflow-local-client.mjs': { zh: '工作流真实执行授权', en: 'workflow actual execution authorization' },
|
|
352
354
|
'workflow-local-request.mjs': { zh: '工作流原执行请求恢复', en: 'workflow original execution request recovery' },
|
|
353
355
|
'remote-client.mjs': { zh: '远端恢复客户端', en: 'remote recovery client' },
|
|
356
|
+
'native-client.mjs': { zh: '八域原生调用与评价核验', en: 'Eight-domain native invocation and evaluation verification' },
|
|
357
|
+
'native-notification.mjs': { zh: '账号独立的可选系统通知', en: 'Optional account-bound system notifications' },
|
|
358
|
+
'local-peer-status.mjs': { zh: '协作任务状态只读入口', en: 'Read-only coordination task status entry' },
|
|
354
359
|
'task-message-entry.mjs': { zh: '任务消息实际投递入口', en: 'task message host delivery entry' },
|
|
355
360
|
'task-message-cli.mjs': { zh: '任务消息命令与适配器', en: 'task message command and adapter' },
|
|
356
361
|
'cloud-task-client.mjs': { zh: '云端沙箱任务路由与逐任务选择', en: 'cloud sandbox task routing and per-task choice' },
|
|
362
|
+
'cloud-watch-client.mjs': { zh: 'IDE 实时云端进度与推广展示', en: 'live IDE cloud progress and promotion display' },
|
|
363
|
+
'cloud-event-stream.mjs': { zh: 'IDE 实时云端进度与推广展示', en: 'live IDE cloud progress and promotion display' },
|
|
364
|
+
'ide-presentation.mjs': { zh: 'IDE 实时云端进度与推广展示', en: 'live IDE cloud progress and promotion display' },
|
|
365
|
+
'automatic-feedback-client.mjs': { zh: '原调用自动评价回执核验', en: 'original invocation automatic evaluation receipt verification' },
|
|
357
366
|
})
|
|
358
367
|
}
|
|
359
368
|
|
package/releases.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.1.35",
|
|
4
|
+
"date": "2026-10-13",
|
|
5
|
+
"zh": "新增协作状态查询、Aimlock 专用规划与八域原生调用;接入账号和设备隔离的任务通知,修复失败接续的评价回执冲突,并完善范围门禁接续。",
|
|
6
|
+
"en": "Add read-only peer status, Aimlock planning and native invocation across eight domains; support account/device-bound task notifications, preserve feedback receipts across authorized retries, and correct scope-gate continuation."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.1.34",
|
|
10
|
+
"date": "2026-10-12",
|
|
11
|
+
"zh": "完善 IDE 云端规划选择、实时执行进度与独立推广提示;服务端自动评价绑定原调用并返回可核验回执,支持只读恢复;修复 UUID 调用用量统计,保持账号隔离及沙箱/调用计数分离。",
|
|
12
|
+
"en": "Improve IDE cloud planning choices, live execution progress and separate promotion notices. Bind automatic server evaluations to their original invocations with verifiable receipts and read-only recovery; correct UUID invocation usage while keeping accounts and skill/sandbox counters separate."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.1.33",
|
|
4
16
|
"date": "2026-10-12",
|
package/remote-client.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import Ajv from 'ajv';
|
|
3
3
|
import { parseTransportResponse, requestTransport, trustedOrigin } from './remote-transport.mjs';
|
|
4
4
|
import { appendInvocation } from './local-invocations.mjs';
|
|
5
|
+
import { verifyAutomaticFeedback } from './automatic-feedback-client.mjs';
|
|
6
|
+
import { ideClientHeaders, presentInvocationNotices } from './ide-presentation.mjs';
|
|
5
7
|
|
|
6
8
|
export const apiUrl = 'https://entrorove.com/api/skill/invoke';
|
|
7
9
|
|
|
@@ -39,6 +41,11 @@ export function buildInvocation(operation, input) {
|
|
|
39
41
|
const id = requestId(operation, input);
|
|
40
42
|
return { url: apiUrl, requestId: id, body: { schemaVersion: 'entrorove.skill.request/2.0', requestId: id, operation, input } };
|
|
41
43
|
}
|
|
44
|
+
function requiresAutomaticFeedback(operation, input, capabilities) {
|
|
45
|
+
const actual = operation === 'continue' ? input.operation : operation;
|
|
46
|
+
return capabilities?.responseExtensions?.automaticFeedback === 'entrorove.automatic-feedback/1.0'
|
|
47
|
+
&& !capabilities.responseExtensions.automaticFeedbackObservations?.includes(actual);
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
export function validateDiscoveredChecks(input, checks = []) {
|
|
44
51
|
for (const check of checks) {
|
|
@@ -65,7 +72,7 @@ export async function invokeRemote(data, dependencies) {
|
|
|
65
72
|
}
|
|
66
73
|
const token = await readCredential(home);
|
|
67
74
|
const { response, result } = await invokeDiscovered(data, token, { home, apiOrigin });
|
|
68
|
-
process.stdout.write(JSON.stringify(result) + '\n');
|
|
75
|
+
process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
|
|
69
76
|
if (!response.ok || result.output.status === 'blocked' || result.output.status === 'failed'
|
|
70
77
|
|| result.output.status === 'incomplete') process.exitCode = 2;
|
|
71
78
|
}
|
|
@@ -76,20 +83,22 @@ export async function invokeRemoteResult(data, dependencies) {
|
|
|
76
83
|
}
|
|
77
84
|
async function invokeDiscovered(data, token, { home, apiOrigin }) {
|
|
78
85
|
const call = { ...buildInvocation(data.operation, data.input), url: `${trustedOrigin(apiOrigin())}/api/skill/invoke` };
|
|
86
|
+
let feedbackRequired = false;
|
|
79
87
|
if (data.operation !== 'capabilities') {
|
|
80
88
|
const discovery = await postInvocation({ ...buildInvocation('capabilities', {}), url: call.url }, token, home);
|
|
81
89
|
if (!discovery.response.ok || discovery.result.output.status !== 'succeeded') throw new Error(`Capability discovery failed: HTTP ${discovery.response.status}`);
|
|
82
90
|
const capabilities = discovery.result.output.capabilities;
|
|
91
|
+
feedbackRequired = requiresAutomaticFeedback(data.operation,data.input,capabilities);
|
|
83
92
|
const schema = capabilities?.operationSchemas?.[data.operation];
|
|
84
93
|
if (!capabilities?.operations?.includes(data.operation) || !schema) throw new Error(`Capability discovery does not support ${data.operation}`);
|
|
85
94
|
const validate = discoveredAjv().compile(schema);
|
|
86
95
|
if (!validate(data.input)) throw new Error(`Invalid ${data.operation} input: ${validate.errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ')}`);
|
|
87
96
|
validateDiscoveredChecks(data.input, capabilities.inputChecks?.[data.operation]);
|
|
88
97
|
}
|
|
89
|
-
return postInvocation(call, token, home);
|
|
98
|
+
return postInvocation(call, token, home,feedbackRequired);
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
async function postInvocation(call, token, home) {
|
|
101
|
+
export async function postInvocation(call, token, home, feedbackRequired = false) {
|
|
93
102
|
const metadata = { kind: 'remote', operation: call.body.operation, requestId: call.requestId };
|
|
94
103
|
const logged = async value => {
|
|
95
104
|
try { return await appendInvocation({ ...metadata, ...value }, home); }
|
|
@@ -98,9 +107,9 @@ async function postInvocation(call, token, home) {
|
|
|
98
107
|
await appendInvocation({ ...metadata, event: 'started' }, home);
|
|
99
108
|
let response; let result;
|
|
100
109
|
try {
|
|
101
|
-
response = await requestTransport(new URL(call.url).origin,
|
|
110
|
+
response = await requestTransport(new URL(call.url).origin, new URL(call.url).pathname, {
|
|
102
111
|
method: 'POST', timeoutMs: 135_000,
|
|
103
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}
|
|
112
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}`,...ideClientHeaders() }, body: JSON.stringify(call.body),
|
|
104
113
|
});
|
|
105
114
|
} catch (cause) {
|
|
106
115
|
if (cause.established) await logged({ event: 'outcome-unknown' });
|
|
@@ -122,6 +131,14 @@ async function postInvocation(call, token, home) {
|
|
|
122
131
|
await logged({ event: 'outcome-unknown' }); throw unknownInvocation(call.requestId);
|
|
123
132
|
}
|
|
124
133
|
await logged({ event: response.ok ? 'completed' : 'rejected', usage: result.usage ?? [] });
|
|
134
|
+
if (result.feedback) {
|
|
135
|
+
const expected = call.feedbackHashes ?? [call.requestId];
|
|
136
|
+
if (!expected.includes(result.feedback.requestHash)) throw new Error('Automatic feedback lost its original invocation binding');
|
|
137
|
+
verifyAutomaticFeedback(result.feedback,call.requestId,result.feedback.requestHash);
|
|
138
|
+
}
|
|
139
|
+
else if (feedbackRequired && !['running','pending'].includes(result.output.status)) {
|
|
140
|
+
throw new Error(`Automatic feedback receipt is missing; query feedback ${call.requestId}. Do not repeat the invocation.`);
|
|
141
|
+
}
|
|
125
142
|
return { response, result };
|
|
126
143
|
}
|
|
127
144
|
|
|
@@ -158,13 +175,16 @@ export function createTaskMessageClient(dependencies) {
|
|
|
158
175
|
async invoke(operation, input) {
|
|
159
176
|
const original = buildInvocation(operation, input);
|
|
160
177
|
const token = await machineCredential(dependencies); const origin = machineOrigin(dependencies);
|
|
178
|
+
let feedbackRequired = false;
|
|
161
179
|
if (operation !== 'capabilities') {
|
|
162
180
|
const discovery = buildInvocation('capabilities', {});
|
|
163
181
|
const discovered = await postInvocation({ ...discovery, url: `${origin}/api/skill/invoke` }, token, dependencies.home);
|
|
164
182
|
if (!discovered.response.ok || discovered.result.output.status !== 'succeeded') throw new Error('Task message capability discovery failed');
|
|
165
183
|
validateMachineCapability(operation, input, discovered.result.output.capabilities);
|
|
184
|
+
feedbackRequired = requiresAutomaticFeedback(operation,input,discovered.result.output.capabilities);
|
|
166
185
|
}
|
|
167
|
-
const { response, result } = await postInvocation({ ...original, url: `${origin}/api/skill/invoke` }, token, dependencies.home);
|
|
186
|
+
const { response, result } = await postInvocation({ ...original, url: `${origin}/api/skill/invoke` }, token, dependencies.home,feedbackRequired);
|
|
187
|
+
presentInvocationNotices(result,dependencies.onIdeEvent);
|
|
168
188
|
if (stoppedOriginalResume(response, result.output, operation, input)) return result.output;
|
|
169
189
|
if (!response.ok || ['blocked', 'failed', 'incomplete'].includes(result.output.status)) {
|
|
170
190
|
throw new Error(`Task message invocation was rejected: HTTP ${response.status}; requestId=${original.requestId}`);
|
|
@@ -178,6 +198,8 @@ export function createTaskMessageClient(dependencies) {
|
|
|
178
198
|
{ authorization: `Bearer ${token}` }, 'original task message status');
|
|
179
199
|
const result = await readResponse(response, 'task message status');
|
|
180
200
|
if (!response.ok || !result?.output || result.output.requestId !== id) throw new Error('Original task message status is unavailable or invalid');
|
|
201
|
+
if (result.feedback) verifyAutomaticFeedback(result.feedback,id);
|
|
202
|
+
presentInvocationNotices(result,dependencies.onIdeEvent);
|
|
181
203
|
await appendInvocation({ kind: 'remote', operation: 'status', requestId: id, event: 'status', usage: result.usage ?? [] }, dependencies.home);
|
|
182
204
|
return result.output;
|
|
183
205
|
},
|
|
@@ -191,7 +213,8 @@ export async function queryStatus(requestId, dependencies) {
|
|
|
191
213
|
const result = await readResponse(response, 'status');
|
|
192
214
|
if (!response.ok || result?.output?.requestId !== requestId) throw new Error(`entrorove status failed: HTTP ${response.status}; requestId=${requestId}`);
|
|
193
215
|
await appendInvocation({ kind: 'remote', operation: 'status', requestId, event: 'status', usage: result.usage ?? [] }, dependencies.home);
|
|
194
|
-
|
|
216
|
+
if (result.feedback) verifyAutomaticFeedback(result.feedback,requestId);
|
|
217
|
+
process.stdout.write(JSON.stringify(presentInvocationNotices(result,dependencies.onIdeEvent)) + '\n');
|
|
195
218
|
}
|
|
196
219
|
|
|
197
220
|
export async function queryFlows(dependencies) {
|
package/scope-runner-client.mjs
CHANGED
|
@@ -84,7 +84,8 @@ async function connect(identity, directory, dependencies) {
|
|
|
84
84
|
const accepted = await remote('scope-runner-attest', { flowId: identity.flowId, attestation: observed.attestation }, dependencies);
|
|
85
85
|
return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'succeeded', flowId: identity.flowId,
|
|
86
86
|
runnerId: binding.runnerId, scopeEvidence: accepted.scopeEvidence, grantsExecutionAuthority: false,
|
|
87
|
-
nextOperation: 'workflow-gate'
|
|
87
|
+
nextOperation: 'workflow-gate', nextInput: { flowId: identity.flowId, operation: 'workflow-gate',
|
|
88
|
+
payload: { targetFiles: [...identity.targetPaths] } } };
|
|
88
89
|
}
|
|
89
90
|
export async function connectScopeRunner(repositoryRoot, input, dependencies) {
|
|
90
91
|
if (!input || Object.keys(input).some(field => !['flowId', 'paths', 'importContexts'].includes(field))
|