@logicseek/hello-cc 1.1.0-rc.3 → 1.1.0-rc.4
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/CHANGELOG.md +39 -0
- package/docs/native.md +81 -2
- package/docs/native.zh-CN.md +68 -2
- package/lib/cli/commands/native.mjs +42 -8
- package/lib/integrations/native/claude.mjs +29 -3
- package/lib/integrations/native/codex.mjs +38 -9
- package/lib/integrations/native/dsh-acp.mjs +10 -0
- package/lib/integrations/native/interactions.mjs +16 -3
- package/lib/runtime/native/client.mjs +8 -1
- package/lib/runtime/native/service.mjs +189 -28
- package/lib/runtime/native/store.mjs +28 -4
- package/lib/web/browser/core.mjs +9 -1
- package/lib/web/http-routes.mjs +1 -1
- package/lib/web/native-sessions.mjs +53 -13
- package/lib/web/ui-i18n.mjs +6 -0
- package/lib/web/ui-native.mjs +22 -2
- package/lib/web/ui-template.mjs +2 -2
- package/lib/web/ui-workspace.mjs +6 -2
- package/package.json +1 -1
- package/scripts/helpers/gc-clock-observation.mjs +33 -0
- package/scripts/native-installed-acceptance.mjs +80 -19
- package/scripts/regression.mjs +18 -2
- package/scripts/web-native-retry-checks.mjs +177 -0
- package/scripts/web-workbench-acceptance.mjs +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,45 @@ the release description from the current changelog section. Use
|
|
|
16
16
|
`npm run release:github` with `GH_TOKEN` or `GITHUB_TOKEN` only for local
|
|
17
17
|
backfills.
|
|
18
18
|
|
|
19
|
+
## 1.1.0-rc.4
|
|
20
|
+
|
|
21
|
+
### Summary
|
|
22
|
+
|
|
23
|
+
This preview adds guarded forks for owned Codex and Claude sessions, durable
|
|
24
|
+
submission retries, and a persisted Codex read-only policy. It retains the
|
|
25
|
+
session isolation and shutdown fixes from rc.3.
|
|
26
|
+
|
|
27
|
+
### Highlights
|
|
28
|
+
|
|
29
|
+
- Fork an idle, owned Codex or Claude session into a distinct worker and provider
|
|
30
|
+
session. Pending approvals, unresolved deliveries and changing parent bindings
|
|
31
|
+
prevent a fork. DeepSeek Harness does not currently support native fork.
|
|
32
|
+
- Create Codex workers with `--sandbox read-only`. Resumes and forks retain the
|
|
33
|
+
saved policy; approval escalation and HCC scoped MCP are disabled for these
|
|
34
|
+
workers. Codex must confirm the policy before the worker becomes available.
|
|
35
|
+
This is a provider filesystem sandbox, not an all-tools or network guarantee.
|
|
36
|
+
- Retry an unconfirmed Web submission with its original ID and message after
|
|
37
|
+
reviewing its receipts. A later edited draft remains intact. Retries require
|
|
38
|
+
current control and the original executor identity. Older pending records
|
|
39
|
+
without the required identity remain available for manual review only.
|
|
40
|
+
- Persist the message and its retry identity together; repair the delivery
|
|
41
|
+
record after interruption without creating a second message. Uncertain
|
|
42
|
+
provider execution is never automatically replayed.
|
|
43
|
+
- Refresh late DeepSeek Harness tool input on the existing approval request,
|
|
44
|
+
preserving its identity and oversized-input restrictions.
|
|
45
|
+
- Disable split views when the page lacks the browser session required by its
|
|
46
|
+
embedded pane. Keep saved layouts for a later authenticated page.
|
|
47
|
+
- Distinguish the active runtime's clock observation from manual GC mutations
|
|
48
|
+
in regression checks while requiring all other metadata to remain unchanged.
|
|
49
|
+
|
|
50
|
+
### Validation
|
|
51
|
+
|
|
52
|
+
Validation covers policy persistence, legacy-runtime refusal, fork inheritance,
|
|
53
|
+
submission recovery and browser retry behavior with later draft edits. Source,
|
|
54
|
+
installed-package, CI and real-provider results retain separate receipts; prior
|
|
55
|
+
historical connection-reset and provider failures are not reclassified by a
|
|
56
|
+
passing retry. Existing frozen release archives remain unchanged.
|
|
57
|
+
|
|
19
58
|
## 1.1.0-rc.3
|
|
20
59
|
|
|
21
60
|
### Summary
|
package/docs/native.md
CHANGED
|
@@ -16,6 +16,41 @@ multiple pending inbox messages into the same turn. ACP resume and session close
|
|
|
16
16
|
depend on the extensions the provider advertises. HCC does not claim that an ACP
|
|
17
17
|
resume replays the original conversation transcript.
|
|
18
18
|
|
|
19
|
+
## Session fork and retries with the original ID
|
|
20
|
+
|
|
21
|
+
`hcc native fork --parent codex-reviewer --peer codex-branch` invokes the provider's
|
|
22
|
+
session fork. Codex uses `thread/fork`; Claude uses the optional SDK's
|
|
23
|
+
`forkSession`. The child receives a distinct session ID, executor, delivery records
|
|
24
|
+
and subsequent history, plus its own MCP scope when the sandbox permits HCC tools.
|
|
25
|
+
The parent keeps its identity. After
|
|
26
|
+
closing the child, `native start --peer codex-branch --provider codex --resume
|
|
27
|
+
last` resumes the child session. Fork inherits the parent's working directory
|
|
28
|
+
and verifies its identity; pending inbox messages and permission grants are not
|
|
29
|
+
copied.
|
|
30
|
+
|
|
31
|
+
Only saved HCC-owned native sessions can fork. The parent must be idle or
|
|
32
|
+
explicitly closed, without an active turn, pending approval, queued message or
|
|
33
|
+
unresolved delivery. Parent send, close and inbox dispatch are fenced while
|
|
34
|
+
forking. Existing TUI/Desktop sessions are outside this scope. A Claude SDK
|
|
35
|
+
without `forkSession` reports unsupported capability. DeepSeek Harness
|
|
36
|
+
0.2.0-rc.2 ACP does not implement fork; HCC does not replay a transcript to
|
|
37
|
+
simulate native fork.
|
|
38
|
+
|
|
39
|
+
A new message may use a unique `--submission-id` containing 8–100 letters,
|
|
40
|
+
digits, underscores or hyphens. After a lost receipt, inspect `native
|
|
41
|
+
deliveries`, then retry the same worker, sender, task and body with the original
|
|
42
|
+
ID. Matching retries return the original durable receipt; changed payloads or
|
|
43
|
+
targets are rejected. The project database commits the message and submission
|
|
44
|
+
ID together. If the native delivery projection was not yet saved, retry or
|
|
45
|
+
worker recovery repairs the original delivery while retaining local user
|
|
46
|
+
origin. Retrying an existing `uncertain` delivery does not replay execution.
|
|
47
|
+
|
|
48
|
+
Web offers **Retry same submission** for an unconfirmed send. Explicit
|
|
49
|
+
confirmation follows receipt inspection, and the retry must retain the original
|
|
50
|
+
executor and body. Later draft edits are preserved. Executor or project changes
|
|
51
|
+
disable the old retry. Missing durable pending records or expired messages are
|
|
52
|
+
rejected rather than sent as new work.
|
|
53
|
+
|
|
19
54
|
## Requirements
|
|
20
55
|
|
|
21
56
|
Use Node.js 24 or later and a provider installed and authenticated in the same
|
|
@@ -121,12 +156,56 @@ message bus even when `--cwd DIR` selects another working directory. `--model
|
|
|
121
156
|
MODEL` selects a provider-supported model. `--binary PATH` overrides the Codex
|
|
122
157
|
or dsh executable. Claude uses the SDK and rejects `--binary`.
|
|
123
158
|
|
|
159
|
+
### Codex read-only file sandbox
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
hcc native start --peer codex-readonly --provider codex --sandbox read-only
|
|
163
|
+
hcc native close --peer codex-readonly
|
|
164
|
+
hcc native start --peer codex-readonly --provider codex --resume last
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Only Codex accepts `--sandbox read-only|workspace-write`. New sessions default to
|
|
168
|
+
the existing `workspace-write` behavior. Resume inherits the stored policy when
|
|
169
|
+
omitted and rejects a different explicit policy; use a new worker for a different
|
|
170
|
+
policy. Legacy saved Codex workers migrate to their original workspace-write mode.
|
|
171
|
+
Retries before a session exists inherit the selected policy unless explicitly changed. Before
|
|
172
|
+
creating/resuming a Codex worker, both CLI and Web require the runtime's sandbox-policy
|
|
173
|
+
protocol version. It refuses an older daemon before sending a create request and
|
|
174
|
+
does not restart it automatically. Creation is bound to the just-verified runtime
|
|
175
|
+
generation; a missing identity or instance replacement rejects the request without
|
|
176
|
+
following the successor or retrying. After existing project work finishes, stop the
|
|
177
|
+
old runtime and start it with this version. Older runtime code cannot enforce the
|
|
178
|
+
new stored policy; downgrade execution is outside this read-only guarantee.
|
|
179
|
+
Web can still inspect or close a legacy worker, but refuses Codex send/respond
|
|
180
|
+
when its fresh snapshot has no sandbox policy. Read-only workers must also report
|
|
181
|
+
provider verification before those actions. Web mutations are bound to the runtime
|
|
182
|
+
generation observed immediately before admission.
|
|
183
|
+
|
|
184
|
+
Read-only fixes approval policy to `never`, requests `readOnly` and disabled
|
|
185
|
+
sandbox network for each new turn, and denies command/file/permission escalation
|
|
186
|
+
and MCP elicitation. Ordinary user questions remain available. Start/resume fail
|
|
187
|
+
closed if the provider does not report the requested policy, or if a resumed
|
|
188
|
+
thread already has an active turn. Forks require the same checks. `sandboxVerified` means matching provider
|
|
189
|
+
readback, not independent proof of operating-system enforcement.
|
|
190
|
+
|
|
191
|
+
HCC does not inject its scoped mutation MCP or command instructions into a
|
|
192
|
+
read-only worker. Its control plane still records messages, replies, delivery
|
|
193
|
+
receipts and audit events. The provider file sandbox does not make every external
|
|
194
|
+
service, user-configured MCP server or hook read-only; strict acceptance still
|
|
195
|
+
needs a private HOME without external tools. This policy is separate from the Web
|
|
196
|
+
viewer/control lease: a controller may still submit a review to a read-only worker.
|
|
197
|
+
|
|
198
|
+
Available since `1.1.0-rc.4`. Native Web snapshots retain `sandbox` and
|
|
199
|
+
`sandboxVerified`. Choose the read-only policy with the CLI. Native forks inherit the saved parent
|
|
200
|
+
policy and cannot override it; the child is independently verified before use.
|
|
201
|
+
|
|
124
202
|
| Command | Options / behavior |
|
|
125
203
|
| --- | --- |
|
|
126
204
|
| `hcc native up` | Start or reuse the project's background runtime |
|
|
127
205
|
| `hcc native status` | Inspect persisted worker state and runtime identity |
|
|
128
|
-
| `hcc native start` | Required `--peer NAME --provider codex\|claude\|dsh`; optional `--cwd DIR --model MODEL --resume last`; `--binary PATH` is available only for Codex and dsh |
|
|
129
|
-
| `hcc native
|
|
206
|
+
| `hcc native start` | Required `--peer NAME --provider codex\|claude\|dsh`; optional `--cwd DIR --model MODEL --resume last`; `--binary PATH` is available only for Codex and dsh; `--sandbox read-only\|workspace-write` is Codex-only |
|
|
207
|
+
| `hcc native fork` | Required `--parent NAME --peer NEW_NAME`; optional `--model MODEL --binary PATH`; forks an HCC-owned Codex or Claude session into a new worker |
|
|
208
|
+
| `hcc native send` | Required `--peer NAME --body TEXT`; optional `--from NAME --task ID --submission-id ID`; returns a durable message/submission receipt |
|
|
130
209
|
| `hcc native deliveries` | Optional `--peer NAME`; inspect delivery receipts |
|
|
131
210
|
| `hcc native events` | Required `--peer NAME`; optional `--after ID`; inspect a bounded event history |
|
|
132
211
|
| `hcc native requests` | Requires `--peer NAME`; inspect pending requests bound to this executor, session and turn |
|
package/docs/native.zh-CN.md
CHANGED
|
@@ -14,6 +14,34 @@ Native 模式运行由 HCC 创建并持有的后台 worker。共享 peer、任
|
|
|
14
14
|
不代表 host 会把多条待处理消息塞进同一个 turn。ACP 的 resume 和 session close
|
|
15
15
|
取决于 provider 声明的扩展;不能把 ACP resume 等同于重放原始会话历史。
|
|
16
16
|
|
|
17
|
+
## 会话 fork 与原 ID 重试
|
|
18
|
+
|
|
19
|
+
`hcc native fork --parent codex-reviewer --peer codex-branch` 使用 provider 自身的
|
|
20
|
+
会话 fork。Codex 通过 `thread/fork`,Claude 通过可选 SDK 的 `forkSession` 创建
|
|
21
|
+
新的会话 ID;子 worker 有独立 executor、消息投递记录和后续历史,策略允许 HCC
|
|
22
|
+
工具时也有独立 MCP scope。
|
|
23
|
+
父会话仍保留原身份;关闭子 worker 后可用 `native start --peer codex-branch
|
|
24
|
+
--provider codex --resume last` 恢复子会话。fork 继承父会话的工作目录,并复核目录
|
|
25
|
+
身份,不复制父 worker 的未决 inbox 或权限批准。
|
|
26
|
+
|
|
27
|
+
只能 fork HCC 持有且保存了会话 ID 的 native worker。父 worker 必须空闲或已明确
|
|
28
|
+
关闭,无活动 turn、待审批请求、排队或结果不明的投递;fork 期间暂停父 worker
|
|
29
|
+
的发送、关闭和 inbox 分发。已有 TUI/Desktop 会话不在此范围内。Claude SDK
|
|
30
|
+
缺少 `forkSession` 时明确报不支持。DeepSeek Harness 0.2.0-rc.2 的 ACP 实现未提供
|
|
31
|
+
fork;HCC 不用 transcript 重放冒充原生 fork。
|
|
32
|
+
|
|
33
|
+
每条新消息可指定唯一的 `--submission-id`,格式为 8–100 个英文字母、数字、下划线
|
|
34
|
+
或连字符。遇到回执丢失时先查 `native deliveries`,然后对同一 worker、sender、task
|
|
35
|
+
和正文沿用原 ID 重试。相同 ID 返回原持久回执,正文或目标变化会被拒绝。项目
|
|
36
|
+
数据库同时保存消息和 submission ID;若消息已提交而 native 投递记录尚未写入,
|
|
37
|
+
重试或 worker 恢复会补齐原记录,保留用户请求来源。已有 `uncertain` 投递不会
|
|
38
|
+
因为重试被自动重放。
|
|
39
|
+
|
|
40
|
+
Web 遇到未确认提交时提供“沿用原提交重试”。此操作需核对持久回执后明确确认,
|
|
41
|
+
并保持原执行器身份与原正文;之后编辑的草稿会保留。执行器或项目变化时禁用原
|
|
42
|
+
提交重试。没有匹配的持久 pending 记录、或原消息已被清理时拒绝重试;不会把
|
|
43
|
+
原 ID 当作新工作发送。
|
|
44
|
+
|
|
17
45
|
## 环境准备
|
|
18
46
|
|
|
19
47
|
需要 Node.js 24 或更新版本,并在运行 HCC 的同一环境中安装、登录对应 provider。
|
|
@@ -103,12 +131,50 @@ hcc msg inbox --peer coordinator
|
|
|
103
131
|
`--binary PATH` 覆盖 Codex 或 dsh 可执行文件。Claude 使用 SDK,传入 `--binary`
|
|
104
132
|
会直接报错。
|
|
105
133
|
|
|
134
|
+
### Codex 文件只读沙箱
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
hcc native start --peer codex-readonly --provider codex --sandbox read-only
|
|
138
|
+
# 关闭后恢复原会话时继承已保存的策略;不要改成 workspace-write。
|
|
139
|
+
hcc native close --peer codex-readonly
|
|
140
|
+
hcc native start --peer codex-readonly --provider codex --resume last
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`--sandbox` 仅支持 Codex 的 `read-only` 和 `workspace-write`。新会话未指定时保持
|
|
144
|
+
`workspace-write`;恢复时未指定则继承存储策略,显式不同的策略会被拒绝,需要另建
|
|
145
|
+
worker。旧版本记录迁移为原有 `workspace-write` 行为;无权限推断或自动升级。
|
|
146
|
+
首次初始化尚未建立会话时,重试默认继承已选择策略,也可显式更改。CLI 与 Web 创建/恢复 Codex worker
|
|
147
|
+
前检查后台服务的沙箱协议版本;旧服务不支持时在发送创建请求前拒绝,不自动重启。
|
|
148
|
+
创建请求绑定刚核实的 runtime generation;期间实例被替换或身份缺失时拒绝发送,
|
|
149
|
+
不会跟随新实例自动重试。
|
|
150
|
+
应等项目任务结束后由用户停止旧 runtime,再以新版本启动。不要用旧版本 runtime
|
|
151
|
+
读取新版本只读记录:旧代码不了解该策略,回退执行不属于只读保障。
|
|
152
|
+
Web 仍可查看或关闭旧 worker;其最新快照不含沙箱策略时,拒绝 Codex 发送和回答审批/问题。
|
|
153
|
+
只读 worker 还必须回报 provider 已校验策略,才能执行这两类操作。Web 写请求绑定准入前
|
|
154
|
+
刚读取的 runtime generation,不会自动跟随替换实例。
|
|
155
|
+
|
|
156
|
+
只读模式固定 `approvalPolicy: never`,每个新 turn 显式请求 `readOnly`、关闭沙箱网络,
|
|
157
|
+
拒绝命令/文件/权限提升和 MCP 授权表单;普通用户问题仍可回答。启动/恢复要求
|
|
158
|
+
provider 回报相符策略,否则关闭自有连接并报错;fork 子会话执行相同校验。已有活动 turn 的只读恢复也拒绝,
|
|
159
|
+
不能用新策略为正在执行的旧 turn 作保证。`sandboxVerified` 仅表示 provider 回报匹配,
|
|
160
|
+
不是一份操作系统行为证明。
|
|
161
|
+
|
|
162
|
+
HCC 不为只读 worker 注入自有的任务/锁/结果写操作 MCP,也不提示模型执行 HCC
|
|
163
|
+
写命令。HCC 控制面仍保存消息、回复、投递和审计。Codex 的文件沙箱不等于所有外部
|
|
164
|
+
服务或用户配置的 MCP/hooks 都只读;严格隔离验收仍须独立 HOME、无外部工具。
|
|
165
|
+
它也不同于网页“观察/控制”权限:控制者仍可向只读 worker 发送审阅请求。
|
|
166
|
+
|
|
167
|
+
此功能自 `1.1.0-rc.4` 起提供。Web 的 native 快照会保留 `sandbox` 与 `sandboxVerified`。
|
|
168
|
+
请通过 CLI 选择只读策略。
|
|
169
|
+
原生 fork 继承父会话的持久化策略,不能覆盖或提升;子会话启用前会独立校验。
|
|
170
|
+
|
|
106
171
|
| 命令 | 参数与作用 |
|
|
107
172
|
| --- | --- |
|
|
108
173
|
| `hcc native up` | 启动或复用本项目后台 runtime |
|
|
109
174
|
| `hcc native status` | 查看持久化 worker 状态和 runtime 身份 |
|
|
110
|
-
| `hcc native start` | 必须 `--peer NAME --provider codex\|claude\|dsh`;可选 `--cwd DIR --model MODEL --resume last`;`--binary PATH` 仅适用于 Codex 和 dsh |
|
|
111
|
-
| `hcc native
|
|
175
|
+
| `hcc native start` | 必须 `--peer NAME --provider codex\|claude\|dsh`;可选 `--cwd DIR --model MODEL --resume last`;`--binary PATH` 仅适用于 Codex 和 dsh;`--sandbox read-only\|workspace-write` 仅适用于 Codex |
|
|
176
|
+
| `hcc native fork` | 必须 `--parent NAME --peer NEW_NAME`;可选 `--model MODEL --binary PATH`;复制 HCC 持有的 Codex 或 Claude 会话到新 worker |
|
|
177
|
+
| `hcc native send` | 必须 `--peer NAME --body TEXT`;可选 `--from NAME --task ID --submission-id ID`;返回持久消息/submission 回执 |
|
|
112
178
|
| `hcc native deliveries` | 可选 `--peer NAME`,查看投递回执 |
|
|
113
179
|
| `hcc native events` | 必须 `--peer NAME`;可选 `--after ID`,查看有界事件历史 |
|
|
114
180
|
| `hcc native requests` | 必须 `--peer NAME`;查看与该执行器、会话、turn 绑定的待处理请求 |
|
|
@@ -9,8 +9,9 @@ import { startNativeService } from '../../runtime/native/service.mjs';
|
|
|
9
9
|
|
|
10
10
|
export function nativeHelp() {
|
|
11
11
|
console.log(`Usage: hcc native up|status|down
|
|
12
|
-
hcc native start --peer NAME --provider codex|claude|dsh [--cwd DIR] [--model MODEL] [--binary PATH] [--resume last]
|
|
13
|
-
hcc native
|
|
12
|
+
hcc native start --peer NAME --provider codex|claude|dsh [--cwd DIR] [--model MODEL] [--binary PATH] [--resume last] [--sandbox read-only|workspace-write]
|
|
13
|
+
hcc native fork --parent NAME --peer NEW_NAME [--model MODEL] [--binary PATH]
|
|
14
|
+
hcc native send --peer NAME --body TEXT [--from NAME] [--task ID] [--submission-id ID]
|
|
14
15
|
hcc native events --peer NAME [--after ID]
|
|
15
16
|
hcc native deliveries [--peer NAME]
|
|
16
17
|
hcc native requests --peer NAME
|
|
@@ -20,29 +21,48 @@ export function nativeHelp() {
|
|
|
20
21
|
|
|
21
22
|
Native workers share the project task/message/lock bus. Only HCC-owned saved
|
|
22
23
|
sessions can resume. Existing TUI/Desktop sessions keep their own transport.
|
|
24
|
+
Use a unique --submission-id for each new message and reuse it only for an
|
|
25
|
+
exact retry after an uncertain receipt.
|
|
23
26
|
Providers retain their permission checks; interactive requests await an explicit Web or local response.
|
|
27
|
+
Codex alone accepts --sandbox. New sessions default to workspace-write; resumed and forked sessions inherit their saved policy.
|
|
28
|
+
read-only disables permission escalation and HCC scoped MCP tools. It is a provider file sandbox, not an all-tools/network guarantee.
|
|
24
29
|
Claude workers require the optional @anthropic-ai/claude-agent-sdk package.
|
|
25
30
|
dsh workers use the ACP profile, not its Web-service terminal.`);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function createNativeCommands(deps) {
|
|
34
|
+
function requireSandboxRuntime(runtime) {
|
|
35
|
+
if (runtime?.sandboxPolicyVersion !== 1) {
|
|
36
|
+
throw new CliError('NATIVE_SANDBOX_RUNTIME_UNSUPPORTED',
|
|
37
|
+
'This native runtime cannot preserve Codex sandbox policies. Stop the project native runtime after its work has finished, then start it with this HCC version. No worker was created.');
|
|
38
|
+
}
|
|
39
|
+
if (typeof runtime?.generation !== 'string' ||
|
|
40
|
+
!runtime.generation.trim() || runtime.generation.length > 128) {
|
|
41
|
+
throw new CliError('NATIVE_OWNER_CHANGED', 'Native runtime did not report a verifiable generation. No worker was created.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
29
44
|
async function cmdNative(ctx, args) {
|
|
30
45
|
const command = args[0];
|
|
31
46
|
if (!command || wantsHelp(args)) return nativeHelp();
|
|
32
47
|
const opts = parseOpts(args.slice(1));
|
|
33
48
|
const allowed = {
|
|
34
49
|
up: [], serve: [], down: [], status: [],
|
|
35
|
-
start: ['peer', 'provider', 'cwd', 'model', 'binary', 'resume'],
|
|
36
|
-
send: ['peer', 'body', 'from', 'task'],
|
|
50
|
+
start: ['peer', 'provider', 'cwd', 'model', 'binary', 'resume', 'sandbox'],
|
|
51
|
+
send: ['peer', 'body', 'from', 'task', 'submission-id'],
|
|
37
52
|
events: ['peer', 'after'], deliveries: ['peer'], requests: ['peer'],
|
|
38
53
|
respond: ['peer', 'request', 'decision', 'response-file'],
|
|
54
|
+
fork: ['peer', 'parent', 'model', 'binary'],
|
|
39
55
|
interrupt: ['peer', 'turn'], close: ['peer']
|
|
40
56
|
};
|
|
41
57
|
if (!Object.hasOwn(allowed, command)) throw new CliError('BAD_ARGS', `Unknown native command: ${command}`);
|
|
42
58
|
validateOpts(`native ${command}`, opts, allowed[command]);
|
|
43
|
-
if (['start', 'send', 'events', 'interrupt', 'close', 'requests', 'respond'].includes(command) && !opts.peer) throw new CliError('BAD_ARGS', 'Missing --peer');
|
|
59
|
+
if (['start', 'fork', 'send', 'events', 'interrupt', 'close', 'requests', 'respond'].includes(command) && !opts.peer) throw new CliError('BAD_ARGS', 'Missing --peer');
|
|
44
60
|
if (command === 'start' && !opts.provider) throw new CliError('BAD_ARGS', 'Missing --provider');
|
|
45
61
|
if (command === 'start' && opts.provider === 'claude' && opts.binary !== undefined) throw new CliError('BAD_ARGS', 'Claude workers use the Agent SDK; --binary is available for Codex and dsh');
|
|
62
|
+
if (command === 'start' && opts.sandbox !== undefined &&
|
|
63
|
+
(opts.provider !== 'codex' || !['read-only', 'workspace-write'].includes(opts.sandbox))) {
|
|
64
|
+
throw new CliError('BAD_ARGS', '--sandbox accepts read-only or workspace-write for Codex only');
|
|
65
|
+
}
|
|
46
66
|
if (command === 'send' && !opts.body?.trim()) throw new CliError('BAD_ARGS', 'Missing --body');
|
|
47
67
|
let result;
|
|
48
68
|
if (command === 'up') result = await ensureNativeRuntime(ctx);
|
|
@@ -53,14 +73,28 @@ export function createNativeCommands(deps) {
|
|
|
53
73
|
process.once('SIGINT', shutdown);
|
|
54
74
|
return;
|
|
55
75
|
} else if (command === 'start') {
|
|
56
|
-
await ensureNativeRuntime(ctx);
|
|
76
|
+
const runtime = await ensureNativeRuntime(ctx);
|
|
77
|
+
if (opts.provider === 'codex') requireSandboxRuntime(runtime);
|
|
57
78
|
result = await nativeRequest(ctx, 'POST', '/workers', { peer: opts.peer, provider: opts.provider,
|
|
58
|
-
cwd: opts.cwd ? path.resolve(ctx.cwd, opts.cwd) : ctx.root, model: opts.model, binary: opts.binary, resume: opts.resume
|
|
79
|
+
cwd: opts.cwd ? path.resolve(ctx.cwd, opts.cwd) : ctx.root, model: opts.model, binary: opts.binary, resume: opts.resume,
|
|
80
|
+
...(opts.sandbox !== undefined ? { sandbox: opts.sandbox } : {}) },
|
|
81
|
+
opts.provider === 'codex' ? { expectedGeneration: runtime.generation } : undefined);
|
|
82
|
+
} else if (command === 'fork') {
|
|
83
|
+
if (!opts.parent) throw new CliError('BAD_ARGS', 'Missing --parent');
|
|
84
|
+
const runtime = await nativeRequest(ctx, 'GET', '/status');
|
|
85
|
+
requireSandboxRuntime(runtime);
|
|
86
|
+
result = await nativeRequest(ctx, 'POST', '/fork', { parent: opts.parent, peer: opts.peer, model: opts.model, binary: opts.binary },
|
|
87
|
+
{ expectedGeneration: runtime.generation });
|
|
59
88
|
} else if (command === 'send') {
|
|
60
89
|
const taskId = intOpt(opts, 'task', null);
|
|
61
90
|
if (taskId !== null && taskId < 1) throw new CliError('BAD_ARGS', 'Task ID must be positive');
|
|
91
|
+
const submissionId = opts['submission-id'];
|
|
92
|
+
if (submissionId !== undefined && !/^[A-Za-z0-9_-]{8,100}$/.test(submissionId)) {
|
|
93
|
+
throw new CliError('BAD_ARGS', 'Submission ID must contain 8 to 100 letters, digits, underscores, or hyphens');
|
|
94
|
+
}
|
|
62
95
|
const sender = deps.resolveCurrentPeer(ctx, opts, 'from', 'shell').id;
|
|
63
|
-
result = await nativeRequest(ctx, 'POST', '/send', { peer: opts.peer, from: sender, body: opts.body, taskId
|
|
96
|
+
result = await nativeRequest(ctx, 'POST', '/send', { peer: opts.peer, from: sender, body: opts.body, taskId,
|
|
97
|
+
...(submissionId ? { submissionId } : {}) });
|
|
64
98
|
} else if (command === 'events') {
|
|
65
99
|
const after = intOpt(opts, 'after', 0);
|
|
66
100
|
if (after < 0) throw new CliError('BAD_ARGS', 'Event cursor must be nonnegative');
|
|
@@ -89,7 +89,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
89
89
|
const cwdBinding = options.cwd ? captureSelectedCwdIdentity(options.cwd, options.expectedCwdIdentity) : null;
|
|
90
90
|
const capabilities = Object.freeze({
|
|
91
91
|
create: true, resume: true, send: true, observe: true,
|
|
92
|
-
interrupt: true, close: true, steer: false, fork:
|
|
92
|
+
interrupt: true, close: true, steer: false, fork: true, approvals: Boolean(options.interactive), mcp: Boolean(options.mcpServers)
|
|
93
93
|
});
|
|
94
94
|
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
95
95
|
const env = isolatedEnv(options.env || process.env, options.coordinationEnv || options.env || {});
|
|
@@ -99,6 +99,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
99
99
|
let model;
|
|
100
100
|
let metrics = null, runtimeMetadata = {};
|
|
101
101
|
let queryFunction = options.query;
|
|
102
|
+
let forkFunction = options.forkSession;
|
|
102
103
|
let sdkQuery = null;
|
|
103
104
|
let pump = null;
|
|
104
105
|
let activeTurn = null;
|
|
@@ -251,13 +252,17 @@ export function createClaudeAdapter(options = {}) {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
async function open(request = {}) {
|
|
254
|
-
|
|
255
|
+
let resumeId = request.sessionId;
|
|
256
|
+
const forkSessionId = request.forkSessionId;
|
|
257
|
+
if (forkSessionId !== undefined && (typeof forkSessionId !== 'string' || !forkSessionId.trim() || resumeId !== undefined)) {
|
|
258
|
+
throw new CliError('BAD_ARGS', 'A fork needs one nonempty source session ID and cannot also resume');
|
|
259
|
+
}
|
|
255
260
|
const selectedModel = request.model;
|
|
256
261
|
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
257
262
|
cwdBinding?.assertUnchanged();
|
|
258
263
|
if (opening) return opening;
|
|
259
264
|
if (status !== 'created') {
|
|
260
|
-
if ((resumeId && resumeId !== requestedSessionId) ||
|
|
265
|
+
if (forkSessionId || (resumeId && resumeId !== requestedSessionId) ||
|
|
261
266
|
(selectedModel && selectedModel !== model)) {
|
|
262
267
|
throw new CliError('NATIVE_ALREADY_OPEN', 'Claude adapter already owns a different session');
|
|
263
268
|
}
|
|
@@ -282,6 +287,7 @@ export function createClaudeAdapter(options = {}) {
|
|
|
282
287
|
}
|
|
283
288
|
const sdk = await import(entry);
|
|
284
289
|
queryFunction = sdk.query;
|
|
290
|
+
forkFunction = sdk.forkSession;
|
|
285
291
|
} catch (error) {
|
|
286
292
|
// A found SDK with broken exports/dependencies is not a missing SDK.
|
|
287
293
|
throw error;
|
|
@@ -291,6 +297,26 @@ export function createClaudeAdapter(options = {}) {
|
|
|
291
297
|
throw new CliError('NATIVE_SDK_INVALID', 'Claude Agent SDK must expose a query function');
|
|
292
298
|
}
|
|
293
299
|
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
300
|
+
if (forkSessionId) {
|
|
301
|
+
if (typeof forkFunction !== 'function') throw new CliError('NATIVE_CAPABILITY_UNSUPPORTED', 'This Claude SDK does not export forkSession');
|
|
302
|
+
// SDK transcript mutations use the host config directory, unlike query()
|
|
303
|
+
// which also accepts an environment override. Never fork in another home.
|
|
304
|
+
for (const key of ['HOME', 'CLAUDE_CONFIG_DIR']) {
|
|
305
|
+
if (env[key] !== undefined && env[key] !== process.env[key]) {
|
|
306
|
+
throw new CliError('NATIVE_FORK_ENV_MISMATCH', 'Claude fork requires the runtime and worker to use the same provider home');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
cwdBinding?.assertUnchanged();
|
|
310
|
+
const forked = await forkFunction(forkSessionId, { dir: options.cwd });
|
|
311
|
+
cwdBinding?.assertUnchanged();
|
|
312
|
+
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session was closed while forking');
|
|
313
|
+
if (typeof forked?.sessionId !== 'string' || !forked.sessionId.trim() ||
|
|
314
|
+
/[\x00-\x1f\x7f]/.test(forked.sessionId) || forked.sessionId === forkSessionId) {
|
|
315
|
+
throw new CliError('NATIVE_SESSION_MISMATCH', 'Claude fork did not create a distinct valid session');
|
|
316
|
+
}
|
|
317
|
+
resumeId = forked.sessionId;
|
|
318
|
+
sessionId = resumeId;
|
|
319
|
+
}
|
|
294
320
|
requestedSessionId = resumeId || null;
|
|
295
321
|
model = selectedModel;
|
|
296
322
|
// The SDK initializes after the first prompt. A requested resume ID is
|
|
@@ -6,7 +6,7 @@ import { captureSelectedCwdIdentity } from '../../process/selected-cwd-identity.
|
|
|
6
6
|
import { CODEX_INTERACTION_METHODS, codexInteractionKind, codexInteractionResponse, cancelCodexInteraction, codexInteractiveConfig } from '../codex-interactions.mjs';
|
|
7
7
|
|
|
8
8
|
const CAPABILITIES = Object.freeze({ create: true, resume: true, send: true, observe: true,
|
|
9
|
-
steer: true, interrupt: true, close: true, fork:
|
|
9
|
+
steer: true, interrupt: true, close: true, fork: true, accountRead: true });
|
|
10
10
|
const TERMINAL_TURN_STATUSES = new Set(['completed', 'interrupted', 'failed']);
|
|
11
11
|
|
|
12
12
|
function adapterError(code, message) {
|
|
@@ -18,7 +18,8 @@ export function createCodexAdapter(options = {}) {
|
|
|
18
18
|
const { binary = 'codex', cwd, env, timeoutMs = 30_000, onEvent = () => {},
|
|
19
19
|
rpcFactory = (config) => new JsonRpcProcess(config) } = options;
|
|
20
20
|
const cwdBinding = cwd ? captureSelectedCwdIdentity(cwd, options.expectedCwdIdentity) : null;
|
|
21
|
-
const state = { provider: 'codex', sessionId: null, turnId: null, status: 'new', owned: true
|
|
21
|
+
const state = { provider: 'codex', sessionId: null, turnId: null, status: 'new', owned: true,
|
|
22
|
+
sandbox: null, sandboxVerified: false };
|
|
22
23
|
const terminalTurns = new Set();
|
|
23
24
|
const interruptedTurns = new Set();
|
|
24
25
|
const turnSubmissions = new Map();
|
|
@@ -112,7 +113,10 @@ export function createCodexAdapter(options = {}) {
|
|
|
112
113
|
|
|
113
114
|
function serverRequest(method, params = {}, requestId) {
|
|
114
115
|
if (!CODEX_INTERACTION_METHODS.has(method)) throw adapterError('NATIVE_SERVER_REQUEST_UNSUPPORTED', `Unsupported Codex server request: ${method}`);
|
|
115
|
-
|
|
116
|
+
// A read-only session can answer questions, but no human response can grant
|
|
117
|
+
// command/file/permission or MCP approval outside its requested policy.
|
|
118
|
+
const canInteract = state.sandbox !== 'read-only' || method === 'item/tool/requestUserInput';
|
|
119
|
+
if (canInteract && options.interactive && params.threadId === state.sessionId && params.turnId && !terminalTurns.has(params.turnId)) {
|
|
116
120
|
// A server request can precede the turn/start admission response.
|
|
117
121
|
if (!state.turnId && pendingSubmissionId) { state.turnId = params.turnId; status('busy'); }
|
|
118
122
|
return interactions.request({ requestId, sessionId: params.threadId, turnId: params.turnId,
|
|
@@ -146,13 +150,22 @@ export function createCodexAdapter(options = {}) {
|
|
|
146
150
|
turnId: state.turnId, extra: { ...error.extra } });
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
async function open({ sessionId, model } = {}) {
|
|
153
|
+
async function open({ sessionId, forkSessionId, model, sandbox = 'workspace-write' } = {}) {
|
|
150
154
|
if (closed || state.status !== 'new') {
|
|
151
155
|
throw adapterError('NATIVE_SESSION_ALREADY_OPEN', 'This Codex adapter can open only one owned session.');
|
|
152
156
|
}
|
|
153
157
|
if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId.trim())) {
|
|
154
158
|
throw adapterError('BAD_ARGS', 'A Codex resume sessionId must be a nonempty string.');
|
|
155
159
|
}
|
|
160
|
+
if (forkSessionId !== undefined && (typeof forkSessionId !== 'string' || !forkSessionId.trim() || sessionId !== undefined)) {
|
|
161
|
+
throw adapterError('BAD_ARGS', 'A fork needs one nonempty source session ID and cannot also resume.');
|
|
162
|
+
}
|
|
163
|
+
if (sandbox !== 'read-only' && sandbox !== 'workspace-write') {
|
|
164
|
+
throw adapterError('BAD_ARGS', 'A Codex sandbox must be read-only or workspace-write.');
|
|
165
|
+
}
|
|
166
|
+
state.sandbox = sandbox;
|
|
167
|
+
const readOnly = sandbox === 'read-only';
|
|
168
|
+
capabilities.approvals = Boolean(options.interactive) && !readOnly;
|
|
156
169
|
status('opening');
|
|
157
170
|
try {
|
|
158
171
|
cwdBinding?.assertUnchanged();
|
|
@@ -166,20 +179,32 @@ export function createCodexAdapter(options = {}) {
|
|
|
166
179
|
await rpc.notify('initialized', {});
|
|
167
180
|
if (closed) throw adapterError('NATIVE_SESSION_CLOSED', 'The Codex session was closed while opening.');
|
|
168
181
|
const config = codexInteractiveConfig(options.mcpServers ? { mcp_servers: options.mcpServers } : {}, Boolean(options.interactive));
|
|
169
|
-
|
|
182
|
+
if (readOnly) config['features.request_permissions_tool'] = false;
|
|
183
|
+
const params = { sandbox, approvalPolicy: readOnly ? 'never' : 'on-request', approvalsReviewer: 'user',
|
|
170
184
|
...(Object.keys(config).length ? { config } : {}),
|
|
171
|
-
...(cwd ? { cwd } : {}), ...(model ? { model } : {}), ...(sessionId ? { threadId: sessionId } : {}) };
|
|
185
|
+
...(cwd ? { cwd } : {}), ...(model ? { model } : {}), ...(sessionId || forkSessionId ? { threadId: sessionId || forkSessionId } : {}) };
|
|
172
186
|
cwdBinding?.assertUnchanged();
|
|
173
|
-
const response = await rpc.request(sessionId ? 'thread/resume' : 'thread/start', params);
|
|
174
|
-
if (
|
|
187
|
+
const response = await rpc.request(forkSessionId ? 'thread/fork' : sessionId ? 'thread/resume' : 'thread/start', params);
|
|
188
|
+
if (typeof response?.thread?.id !== 'string' || !response.thread.id.trim() ||
|
|
189
|
+
(sessionId && response.thread.id !== sessionId) || (forkSessionId && response.thread.id === forkSessionId)) {
|
|
175
190
|
throw adapterError('NATIVE_PROTOCOL_ERROR', 'Codex returned an invalid native thread identity.');
|
|
176
191
|
}
|
|
177
192
|
if (closed) throw adapterError('NATIVE_SESSION_CLOSED', 'The Codex session was closed while opening.');
|
|
193
|
+
const activeTurn = response.thread.turns?.findLast((turn) => turn.status === 'inProgress');
|
|
194
|
+
// Readback must confirm policy before exposing a resumed or forked thread.
|
|
195
|
+
if (readOnly && (response.sandbox?.type !== 'readOnly' ||
|
|
196
|
+
(response.sandbox.networkAccess !== undefined && response.sandbox.networkAccess !== false) ||
|
|
197
|
+
response.approvalPolicy !== 'never')) {
|
|
198
|
+
throw adapterError('NATIVE_SANDBOX_UNVERIFIED', 'Codex did not confirm the requested read-only sandbox and never-approve policy.');
|
|
199
|
+
}
|
|
200
|
+
if (readOnly && (activeTurn || response.thread.status?.type === 'active')) {
|
|
201
|
+
throw adapterError('NATIVE_READ_ONLY_BUSY', 'An existing active Codex turn cannot be resumed or forked as verified read-only. Wait for it to finish first.');
|
|
202
|
+
}
|
|
203
|
+
state.sandboxVerified = readOnly || (response.sandbox?.type === 'workspaceWrite' && response.approvalPolicy === 'on-request');
|
|
178
204
|
state.sessionId = response.thread.id;
|
|
179
205
|
state.runtimeMetadata = { ...(typeof response.model === 'string' ? { model: response.model } : {}),
|
|
180
206
|
...(typeof response.approvalPolicy === 'string' ? { permissionMode: response.approvalPolicy } : {}) };
|
|
181
207
|
emit({ type: 'metadata', runtimeMetadata: state.runtimeMetadata });
|
|
182
|
-
const activeTurn = response.thread.turns?.findLast((turn) => turn.status === 'inProgress');
|
|
183
208
|
state.turnId = activeTurn?.id ?? null;
|
|
184
209
|
status(state.turnId ? 'busy' : 'idle');
|
|
185
210
|
return snapshot();
|
|
@@ -196,6 +221,7 @@ export function createCodexAdapter(options = {}) {
|
|
|
196
221
|
function send({ text, submissionId, expectedTurnId } = {}) {
|
|
197
222
|
return serial(async () => {
|
|
198
223
|
requireOpen();
|
|
224
|
+
cwdBinding?.assertUnchanged();
|
|
199
225
|
if (typeof text !== 'string' || !text.trim()) throw adapterError('BAD_ARGS', 'A native message needs nonempty text.');
|
|
200
226
|
const turnId = state.turnId;
|
|
201
227
|
if (expectedTurnId !== undefined && expectedTurnId !== turnId) {
|
|
@@ -207,6 +233,9 @@ export function createCodexAdapter(options = {}) {
|
|
|
207
233
|
const method = turnId ? 'turn/steer' : 'turn/start';
|
|
208
234
|
const params = { threadId: state.sessionId, input: [{ type: 'text', text }],
|
|
209
235
|
...(submissionId ? { clientUserMessageId: submissionId } : {}),
|
|
236
|
+
...(!turnId && state.sandbox === 'read-only' ? {
|
|
237
|
+
sandboxPolicy: { type: 'readOnly', networkAccess: false }, approvalPolicy: 'never'
|
|
238
|
+
} : {}),
|
|
210
239
|
...(turnId ? { expectedTurnId: expectedTurnId ?? turnId } : {}) };
|
|
211
240
|
try {
|
|
212
241
|
pendingSubmissionId = submissionId;
|
|
@@ -81,6 +81,16 @@ export function createDshAcpAdapter(options = {}) {
|
|
|
81
81
|
if (toolCalls.size > 128) toolCalls.delete(toolCalls.keys().next().value);
|
|
82
82
|
if (Object.hasOwn(bounded, 'rawInput') || bounded.contextTruncated) {
|
|
83
83
|
for (const waiter of [...toolContextWaiters]) if (waiter.toolCallId === update.toolCallId) waiter.finish(bounded);
|
|
84
|
+
// Context may arrive after the bounded initial wait. Refresh the same
|
|
85
|
+
// unresolved permission rather than leaving its acceptance blocked.
|
|
86
|
+
for (const request of interactions.snapshot()) {
|
|
87
|
+
if (request.sessionId !== sessionId || request.turnId !== active.turnId ||
|
|
88
|
+
request.params.toolCall?.toolCallId !== update.toolCallId || !request.params.toolCall.contextPending) continue;
|
|
89
|
+
const toolCall = { ...request.params.toolCall, ...bounded };
|
|
90
|
+
delete toolCall.contextPending;
|
|
91
|
+
interactions.update({ requestId: request.requestId, sessionId, turnId: active.turnId,
|
|
92
|
+
params: { ...request.params, toolCall } });
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
|
|
@@ -23,8 +23,7 @@ export function createNativeInteractions({ executorId = randomUUID(), isActive,
|
|
|
23
23
|
if (signal?.aborted || !isActive(sessionId, turnId)) return Promise.resolve(cancelled);
|
|
24
24
|
if (used.has(key) || pending.size >= 64) throw Object.assign(new Error('Duplicate or excessive native interaction requests'), { code: 'NATIVE_PROTOCOL_ERROR' });
|
|
25
25
|
used.add(key); if (used.size > 256) used.delete(used.values().next().value);
|
|
26
|
-
|
|
27
|
-
if (JSON.stringify(preview).length > 65536) { truncated = true; preview = { preview: JSON.stringify(params).slice(0, 65536) }; }
|
|
26
|
+
const { preview, truncated } = previewParams(params);
|
|
28
27
|
const promise = new Promise(resolve => {
|
|
29
28
|
const entry = { executorId, requestId: id, sessionId, turnId, kind, method, params: preview,
|
|
30
29
|
truncated, status: 'pending', createdAt: Date.now(), resolve, validate, cancelled };
|
|
@@ -38,6 +37,20 @@ export function createNativeInteractions({ executorId = randomUUID(), isActive,
|
|
|
38
37
|
});
|
|
39
38
|
return promise;
|
|
40
39
|
}
|
|
40
|
+
function previewParams(params) {
|
|
41
|
+
let truncated = false, preview = structuredClone(params);
|
|
42
|
+
const serialized = JSON.stringify(preview);
|
|
43
|
+
if (serialized.length > 65536) { truncated = true; preview = { preview: serialized.slice(0, 65536) }; }
|
|
44
|
+
return { preview, truncated };
|
|
45
|
+
}
|
|
46
|
+
function update({ requestId, sessionId, turnId, params }) {
|
|
47
|
+
const key = JSON.stringify([typeof requestId, requestId]), entry = pending.get(key);
|
|
48
|
+
if (!entry || entry.sessionId !== sessionId || entry.turnId !== turnId || !isActive(sessionId, turnId)) return false;
|
|
49
|
+
const { preview, truncated } = previewParams(params);
|
|
50
|
+
entry.params = preview; entry.truncated = truncated;
|
|
51
|
+
onChange?.(redactMcpUrlInteraction({ type: 'approval', ...snapshot().find(value => value.requestId === requestId) }));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
41
54
|
function respond(input) {
|
|
42
55
|
const key = JSON.stringify([typeof input.requestId, input.requestId]), entry = pending.get(key);
|
|
43
56
|
if (!entry || input.executorId !== executorId || input.sessionId !== entry.sessionId || input.turnId !== entry.turnId || !isActive(entry.sessionId, entry.turnId)) {
|
|
@@ -47,7 +60,7 @@ export function createNativeInteractions({ executorId = randomUUID(), isActive,
|
|
|
47
60
|
settle(key, result, 'answered');
|
|
48
61
|
return { executorId, requestId: entry.requestId, sessionId: entry.sessionId, turnId: entry.turnId, status: 'submitted' };
|
|
49
62
|
}
|
|
50
|
-
return { executorId, request, snapshot, respond, expire, resolved(requestId) {
|
|
63
|
+
return { executorId, request, update, snapshot, respond, expire, resolved(requestId) {
|
|
51
64
|
const key = JSON.stringify([typeof requestId, requestId]);
|
|
52
65
|
if (pending.has(key)) settle(key, pending.get(key).cancelled, 'resolved-by-provider');
|
|
53
66
|
} };
|
|
@@ -2,9 +2,16 @@ import http from 'node:http';
|
|
|
2
2
|
import { CliError } from '../../shared/errors.mjs';
|
|
3
3
|
import { readNativePointer } from './store.mjs';
|
|
4
4
|
|
|
5
|
-
export function nativeRequest(ctx, method, route, body = null, { timeoutMs = 35000 } = {}) {
|
|
5
|
+
export function nativeRequest(ctx, method, route, body = null, { timeoutMs = 35000, expectedGeneration } = {}) {
|
|
6
6
|
const pointer = readNativePointer(ctx);
|
|
7
7
|
if (!pointer) return Promise.reject(new CliError('NATIVE_RUNTIME_OFFLINE', 'Native runtime is offline. Run hcc native up.'));
|
|
8
|
+
// Keep a policy-checked request bound to that runtime. This same pointer's
|
|
9
|
+
// endpoint and credential are used below; never reread and follow a new owner.
|
|
10
|
+
if (expectedGeneration !== undefined && (typeof expectedGeneration !== 'string' ||
|
|
11
|
+
!expectedGeneration.trim() || expectedGeneration.length > 128 || pointer.generation !== expectedGeneration)) {
|
|
12
|
+
return Promise.reject(new CliError('NATIVE_OWNER_CHANGED',
|
|
13
|
+
'Native runtime changed after policy verification. Inspect its current status before retrying.', { uncertain: false }));
|
|
14
|
+
}
|
|
8
15
|
return new Promise((resolve, reject) => {
|
|
9
16
|
const payload = body === null ? null : JSON.stringify(body);
|
|
10
17
|
let timer;
|