Merge remote-tracking branch 'origin/master' into worktree/web-file-session-references

This commit is contained in:
creatixchu
2026-08-17 20:46:35 +08:00
249 changed files with 1751 additions and 226 deletions
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-code-runtime-python-fd3-protocol.md
2026-07-31-code-runtime-python-fd3-protocol.md: 5f9600a3f658df907d68ae695d42154009947fbd
2026-07-31-code-runtime-python-fd3-protocol.zh.md: dc3ae7cdfe1daf6e2ab1326e353c1bdbf9833175
@@ -0,0 +1,43 @@
# Agent Note: the code-runtime-python fd-3 frame protocol
Status: implemented
English | [中文](2026-07-31-code-runtime-python-fd3-protocol.zh.md)
## Problem
The CPython code-runtime backend (`@deepseek-ai/dsh-code-runtime-python`, arriving across a PR stack) runs each model program in a fresh `python3 -I` subprocess and bridges binding calls and completion values over the child's fd 3. That channel needs a wire protocol both sides agree on, and the host cannot trust it: model code has full access to fd 3 and can forge any frame, so every inbound frame is hostile input the host must validate and rebuild before reading. The protocol also has to carry lossless JSON without the depth limit `JSON.stringify`/`json.dumps` impose, because the seam's `CodeJsonValue` is depth-unbounded.
This layer of the stack delivers only that protocol, so the large `PythonCodeRuntime` implementation and its real-subprocess integration suite land on a reviewed wire contract instead of arriving fused with it. The parent stack splits [#436](https://github.com/deepseek-harness/deepseek-harness/pull/436) — a 9000-line single PR — into reviewable layers; this is the protocol layer, based on the [seam extension](2026-07-31-code-runtime-portable-identifier-seam.md).
## Decision
`src/protocol.ts` is the host side of the wire vocabulary and its hostile-frame codec:
- **`validateChildFrame`** shape-validates and REBUILDS every inbound frame. The compile-time union means nothing on fd 3 — a forged frame can carry `null`, poisoned fields, or omit required ones — so each accepted frame is reconstructed field by field: forged extras never ride along, a non-finite call id can never be echoed into a reply, and junk returns `undefined` to be dropped rather than throwing in the host's message handler.
- **`encodeJsonPlain` / `checkDoneValue` / `hasUnsafeIntegerToken` / `hasNonLosslessNumber`** are the lossless-JSON codec and meters. They traverse iteratively (an explicit stack, not recursion) so a deep value below the byte budget crosses intact; `checkDoneValue` folds byte-metering and number-losslessness into one walk that rejects an over-budget payload before the INCREMENTAL work it would otherwise add — the enqueued children; strings and keys are metered by a non-allocating escaped-size scan (`jsonStringBytesUpTo`), so the escaped copy is never materialized. It does not re-bound the frame's own width: `done.value` is already `JSON.parse`'d when the check runs, so the payload's size is paid upstream and capped there by the host's fixed fd-3 receive buffer (a later stack layer), not here. Beyond-safe-range integral doubles serialize through `BigInt` digits so the exact integer crosses, not `String()`'s rounded form.
- **`logTruncationMarker`** produces the in-band marker text a log ledger emits when it exhausts its byte budget.
`py/protocol.py` mirrors the message shapes as `TypedDict`s and re-declares the two surfaces both sides EXECUTE against — `PROTOCOL_FD = 3` and `log_truncation_marker` — with byte-identical text.
The package skeleton (`package.json`, `tsconfig.json`, `tsdown.config.ts`, `src/index.ts`, `src/invariant.ts`, README triplet) ships here rather than in a later stack layer: `check-workspace-constraints` reads every `packages/<group>/<pkg>` package.json unconditionally, and the coverage and invariant-topology gates require the package to exist and build the moment its directory does. The later backend-core PR extends `src/index.ts` with `PythonCodeRuntime` and grows `package.json`'s dependencies; because it bases on this branch, those are edits, not conflicts.
## Wire contract
Frames are JSON-lines on fd 3, one object per line, leaving stdout/stderr free for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame), `run` (after `boot-ack`), and one `reply` per `call`. The `log` frame's `truncated` flag marks the frame that IS the child ledger's own truncation marker, so the host stops capturing at the same point the child did instead of inferring it from its own budget. `done.error.kind` is one of `exception`, `invalid-output`, `output-limit`; wall/CPU budgets, aborts, and substrate death are observed host-side, not carried as frames.
## Mirror alignment
Round-12 review of #436 found `py/protocol.py` stale against `src/protocol.ts` in three declarations — `LogMessage` lacked `truncated`, `DoneMessage.error` lacked `kind`, and `Namespace` lacked the optional `errorClass`. This PR aligns all three when lifting the file, so the stale mirror is not carried forward. To keep it aligned, `tests/protocol-mirror.e2e.ts` spawns a real `python3` and asserts, against `src/protocol.ts`: `PROTOCOL_FD` and `log_truncation_marker` (the two surfaces both sides execute), and each `TypedDict`'s required/optional wire field set — so a renamed or dropped field, or one side making a field optional the other requires (exactly the round-12 drift), fails the test. Field *types* are not compared across the language boundary; that residue stays with review.
## Alternatives considered
**Move the Python JSON codec (`_encode_json_plain` / `_decode_json_plain`) into `py/protocol.py` for cross-side symmetry with `protocol.ts`.** Rejected. The repository's "prefer symmetry for parallel values" rule points at genuinely parallel values; these are not. The host-side codec in `protocol.ts` validates HOSTILE input and is self-contained. The Python codec produces output on the TRUSTED side and is coupled to bootstrap-internal helpers (`_Emit`, `_dump_scalar`/`_dump_string`/`_dump_float`, `LogBuffer`'s cost accounting, `_check_done_value`, `_lossless_json_violation`); lifting only the two entry points would drag that web into `protocol.py` or create a `bootstrap.py``protocol.py` import cycle. The real cross-side parallel is "host validates inbound (`protocol.ts`) ↔ child trusts host and emits (`bootstrap.py`)", and that symmetry is preserved: `protocol.py` stays the pure wire-vocabulary mirror it is on the TS side. The Python codec stays in `bootstrap.py`, delivered by the backend-core PR.
**Defer the package skeleton to the backend-core PR that "owns" package.json.** Rejected: the workspace-constraint, coverage, and invariant-topology gates fail the instant the `code-runtime-python` directory exists without a buildable package. A stacked split cannot create source files in a package that does not yet compile.
## Consequences
Bought: the fd-3 protocol and its hostile-input codec land as a self-contained, fully unit-covered layer, and the py/ts mirror drift the round-12 review found is fixed with an executing guard against its recurrence. The backend-core PR builds on a reviewed wire contract.
Cost: `src/index.ts` and `package.json` are introduced minimally here and edited (not created) by the backend-core PR. The mirror e2e compares field NAMES and required/optional-ness across the two sides but not field TYPES — comparing type declarations across TypeScript and Python has no mechanical equivalent, so that residue stays with review plus the backend's real-subprocess suite.
@@ -0,0 +1,43 @@
# Agent Note: the code-runtime-python fd-3 frame protocol
Status: implemented
[English](2026-07-31-code-runtime-python-fd3-protocol.md) | 中文
## Problem
CPython code-runtime 后端(`@deepseek-ai/dsh-code-runtime-python`,分多个 PR 落地)在一个全新的 `python3 -I` 子进程里运行每个模型程序,并把 binding 调用和完成值通过子进程的 fd 3 桥接。这条通道需要两侧一致的 wire protocol,而 host 不能信任它:模型代码对 fd 3 有完全访问权、可以伪造任意帧,所以每个入站帧都是 host 必须先校验并重建才能读取的敌意输入。协议还必须承载无深度限制的 lossless JSON,因为 seam 的 `CodeJsonValue` 深度无界,而 `JSON.stringify`/`json.dumps` 都有递归深度限制。
本层只交付这个协议,使得庞大的 `PythonCodeRuntime` 实现及其真子进程集成测试能落在一个已 review 的 wire contract 之上,而不是与它揉在一起到达。父 stack 把 [#436](https://github.com/deepseek-harness/deepseek-harness/pull/436)——一个 9000 行的单一 PR——拆成可 review 的层;本 PR 是协议层,base 是 [seam 扩展](2026-07-31-code-runtime-portable-identifier-seam.md)。
## Decision
`src/protocol.ts` 是 wire vocabulary 的 host 侧及其敌意帧编解码:
- **`validateChildFrame`** 对每个入站帧做形状校验并重建。编译期 union 在 fd 3 上毫无意义——伪造帧可携带 `null`、被污染的字段,或省略必需字段——所以每个被接受的帧都逐字段重建:伪造的额外字段绝不随行,非有限的 call id 绝不会被回显进 reply,垃圾返回 `undefined` 被丢弃,而不是在 host 的 message handler 里抛错。
- **`encodeJsonPlain` / `checkDoneValue` / `hasUnsafeIntegerToken` / `hasNonLosslessNumber`** 是 lossless-JSON 编解码器与计量器。它们迭代遍历(显式栈,非递归),使低于字节预算的深层值能完整穿越;`checkDoneValue` 把字节计量和数字无损性折进一次遍历,在它本会新增的 INCREMENTAL 工作之前就拒绝超预算 payload——即入栈子节点;字符串与 key 由非分配的转义尺寸扫描(`jsonStringBytesUpTo`)计量,从不物化转义副本。它不会重新约束帧自身的宽度:`done.value` 在检查运行时已被 `JSON.parse`,故 payload 的尺寸是上游代价,由 host 固定的 fd-3 接收缓冲(后续 stack 层)在那里封顶,而非本函数。超出安全范围的整数型 double 通过 `BigInt` 数字序列化,穿越的是精确整数而非 `String()` 的舍入形式。
- **`logTruncationMarker`** 产出日志 ledger 耗尽字节预算时发出的带内标记文本。
`py/protocol.py``TypedDict` 镜像消息形状,并重新声明两侧都会 EXECUTE 的两个面——`PROTOCOL_FD = 3``log_truncation_marker`——文本逐字节一致。
包骨架(`package.json``tsconfig.json``tsdown.config.ts``src/index.ts``src/invariant.ts`、README 三件套)在此交付,而非放到后续 stack 层:`check-workspace-constraints` 无条件读取每个 `packages/<group>/<pkg>` 的 package.jsoncoverage 与 invariant-topology gate 也要求包在其目录出现的那一刻即存在且可构建。后续的 backend-core PR 会用 `PythonCodeRuntime` 扩展 `src/index.ts` 并增补 `package.json` 的依赖;因为它 base 在本分支上,那些是编辑,不是冲突。
## Wire contract
帧是 fd 3 上的 JSON-lines,每行一个对象,让 stdout/stderr 空出给程序自己的输出。Child → host:`boot-ack``call``log``done`。Host → child`boot`(首帧)、`run`(在 `boot-ack` 之后)、以及每个 `call` 对应一个 `reply``log` 帧的 `truncated` 标志标记那个本身就是子进程 ledger 截断标记的帧,使 host 在与子进程相同的点停止捕获,而不是从自己的预算去推断。`done.error.kind``exception``invalid-output``output-limit` 之一;wall/CPU 预算、abort、substrate 死亡都在 host 侧观测,不作为帧携带。
## Mirror alignment
#436 的 round-12 review 发现 `py/protocol.py` 相对 `src/protocol.ts` 有三处声明陈旧——`LogMessage` 缺 `truncated`、`DoneMessage.error` 缺 `kind`、`Namespace` 缺可选的 `errorClass`。本 PR 在搬运该文件时对齐了这三处,不把陈旧镜像带过来。为持续保持对齐,`tests/protocol-mirror.e2e.ts` 启动一个真实 `python3`,对照 `src/protocol.ts` 断言:`PROTOCOL_FD` 与 `log_truncation_marker`(两侧都会执行的面),以及每个 `TypedDict` 的必填/可选 wire 字段集——于是字段被重命名或删除、或一侧把另一侧要求的字段改成可选(正是 round-12 那类漂移),测试即失败。字段的*类型*不跨语言边界比较,那部分残留留给 review。
## Alternatives considered
**把 Python JSON codec`_encode_json_plain` / `_decode_json_plain`)挪进 `py/protocol.py` 以与 `protocol.ts` 跨侧对称。** 拒绝。仓库的 “prefer symmetry for parallel values” 规则指向真正平行的值;这两者不是。`protocol.ts` 里的 host 侧 codec 校验的是敌意输入,自包含。Python codec 在受信任侧产出输出,且耦合于 bootstrap 内部 helper`_Emit``_dump_scalar`/`_dump_string`/`_dump_float``LogBuffer` 的成本核算、`_check_done_value``_lossless_json_violation`);只把两个入口挪过去会把这一整片拖进 `protocol.py`,或制造 `bootstrap.py``protocol.py` 的 import 环。真正的跨侧平行是 “host 校验入站(`protocol.ts` ↔ child 信任 host 并发出(`bootstrap.py`)”,这个对称性被保留:`protocol.py` 保持它在 TS 侧一样的纯 wire-vocabulary 镜像定位。Python codec 留在 `bootstrap.py`,由 backend-core PR 交付。
**把包骨架推迟到“拥有” package.json 的 backend-core PR。** 拒绝:workspace-constraint、coverage、invariant-topology gate 会在 `code-runtime-python` 目录一存在而包不可构建时立即失败。stacked 拆分无法在一个尚不能编译的包里创建源文件。
## Consequences
收获:fd-3 协议及其敌意输入 codec 作为自包含、unit 全覆盖的一层落地,round-12 review 发现的 py/ts 镜像漂移被修复,并有一个执行中的 guard 防其复发。backend-core PR 建立在已 review 的 wire contract 之上。
代价:`src/index.ts``package.json` 在此以最小形态引入,并由 backend-core PR 编辑(而非创建)。mirror e2e 比较两侧的字段名与必填/可选性,但不比较字段类型——跨 TypeScript 与 Python 比较类型声明无机械等价物,那部分残留留给 review 加后端真子进程套件。
@@ -112,4 +112,12 @@ gh pr checks
Report pending checks as pending. Inspect failures before attributing them to the branch or the environment.
When `gh pr checks` reports "no checks reported" and `/actions/runs?head_sha=<sha>` returns `total_count: 0`, read mergeability before suspecting the push or a dropped GitHub event:
```sh
gh pr view <number> --json mergeable,mergeStateStatus
```
GitHub creates no `pull_request` workflow runs while a PR is `CONFLICTING`/`DIRTY`, so the absent signal is the conflict, not infrastructure. Resolving the conflict is the only fix; empty commits, `--allow-empty` pushes, draft/ready toggles, and revert-and-restore bounces all leave `total_count` at zero and add junk history. Confirm the conflicting paths with `git merge-tree --write-tree HEAD origin/<base>` when the branch cannot be merged locally yet.
For `gh stack sync`, use the post-sync validation sequence instead of pretending the ordinary order was possible.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-frontend",
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: b44d054f7d72b9410f92aa8c1b3edc189bde55a1
config-catalog.zh.md: 23f870cfd49c00a3e9174c07fcdcaef463008eea
config-catalog.md: 50d8719c7ed77994715046180f8446a75057fd80
config-catalog.zh.md: 65d10a86db2520ea541c3a055a67d7b2b9ae4832
+1
View File
@@ -3152,6 +3152,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
- `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-code-runtime-python` ([`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts))
- `@deepseek-ai/dsh-home-paths` ([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))
- `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts))
- `@deepseek-ai/dsh-launch-environment` ([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
+1
View File
@@ -3153,6 +3153,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-web`[`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts)
- `@deepseek-ai/dsh-client-web-react`[`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts)
- `@deepseek-ai/dsh-cmdline`[`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts)
- `@deepseek-ai/dsh-code-runtime-python`[`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts)
- `@deepseek-ai/dsh-home-paths`[`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts)
- `@deepseek-ai/dsh-hook-protocol`[`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts)
- `@deepseek-ai/dsh-launch-environment`[`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts)
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 8fdf13d9943e89b4527cba05cbaced35778a56ac
module-graph.zh.md: 10b46262e7df9ed076c204f77de9728082577f0f
module-graph.md: 2eb7c748ee0bcf6eb63d200841e35f2606958a24
module-graph.zh.md: 35a5615914711da1f52e2ecfb938c2e134f6afcb
+3
View File
@@ -160,6 +160,7 @@ flowchart TD
end
subgraph group_code_runtime["packages/code-runtime"]
pkg_code_runtime["code-runtime"]
pkg_code_runtime_python["code-runtime-python"]
pkg_code_runtime_worker_thread["code-runtime-worker-thread"]
end
subgraph group_compaction["packages/compaction"]
@@ -342,6 +343,7 @@ flowchart TD
pkg_client_web --> pkg_invariants
pkg_client_web_react --> pkg_invariants
pkg_code_runtime --> pkg_invariants
pkg_code_runtime_python --> pkg_invariants
pkg_e2b --> pkg_invariants
pkg_sdk_jsonrpc_demo --> pkg_invariants
pkg_host_directory_picker --> pkg_invariants
@@ -1439,6 +1441,7 @@ flowchart TD
| [`client-web`](../packages/client/web) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-web-react`](../packages/client/web-react) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`code-runtime-python`](../packages/code-runtime/code-runtime-python) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`sdk-jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
+3
View File
@@ -162,6 +162,7 @@ flowchart TD
end
subgraph group_code_runtime["packages/code-runtime"]
pkg_code_runtime["code-runtime"]
pkg_code_runtime_python["code-runtime-python"]
pkg_code_runtime_worker_thread["code-runtime-worker-thread"]
end
subgraph group_compaction["packages/compaction"]
@@ -344,6 +345,7 @@ flowchart TD
pkg_client_web --> pkg_invariants
pkg_client_web_react --> pkg_invariants
pkg_code_runtime --> pkg_invariants
pkg_code_runtime_python --> pkg_invariants
pkg_e2b --> pkg_invariants
pkg_sdk_jsonrpc_demo --> pkg_invariants
pkg_host_directory_picker --> pkg_invariants
@@ -1441,6 +1443,7 @@ flowchart TD
| [`client-web`](../packages/client/web) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`client-web-react`](../packages/client/web-react) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`code-runtime-python`](../packages/code-runtime/code-runtime-python) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`sdk-jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | [`invariants`](../packages/runtime-diagnostics/invariants) |
| [`host-directory-picker`](../packages/host/directory-picker) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
+10
View File
@@ -461,6 +461,16 @@
"tests/**/*.ts"
]
},
"packages/code-runtime/code-runtime-python": {
"entry": [
"tests/**/*.spec.ts",
"tests/**/*.e2e.ts"
],
"project": [
"src/**/*.ts",
"tests/**/*.ts"
]
},
"packages/llm/llm-deepseek": {
"entry": [
"tests/**/*.spec.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-root",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"license": "MIT",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-gateway",
"description": "Typert Remote Host dispatcher and Client API endpoint",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly and Host Agent/Session lookup policy",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-app-boot",
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-cmdline",
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-base",
"description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-headless",
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-connection",
"description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-hmr",
"description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-locale",
"description": "Locale plugin: Host-backed zh/en preference, browser-derived fallback, locale snapshots, and typed namespace dictionaries",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-modules",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-runtime",
"description": "Client core services: SlotRegistry, SessionRuntime (scope tree + object layer)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-schema-form",
"description": "Schema/draft model layer for settings editors: rehydrates a serialized schemastery schema, validates drafts, and edits them immutably by path",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-agent-preset",
"description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-attachment",
"description": "Pure React attachment atoms for the dsh web UI: draft-image rail, message image gallery, and original-image lightbox (zero cordis)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-commands",
"description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-conversation",
"description": "Conversation domain: skeleton, ordered chat flow, composer with the Host-backed busy-Enter preference, and details host",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-deliverables",
"description": "Produced-files turn tail and clickable final-response file references for Web",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-directory-picker-browse",
"description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-directory-picker-native",
"description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-goal",
"description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-input-trigger",
"description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-jobs",
"description": "Session-header background-job list: live registry state mirrored from session/jobs frames",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-layout",
"description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-message-feedback",
"description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-model-selection",
"description": "Model selection: the /model popupSelect over session.models / session.selectModel",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-permission-presets",
"description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-plan",
"description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-primitives",
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-general",
"description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-models",
"description": "Models settings and shared product-onboarding dialogs over existing settings and credential joins",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-plugin-inventory",
"description": "Read-only Cordis Loader inventory tab in Web Plugins settings",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-plugins",
"description": "Plugins settings section with feature-owned tabs and configurable host-plane plugin cards",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings",
"description": "Settings domain base plugin: the settings-namespace scope service and the canonical settings slot-type contract",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-sidebar",
"description": "Sidebar plugin: session multi-level tree, search, grouping, state dots",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-skill",
"description": "Web skill references and the dedicated skill tool row",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-slots",
"description": "Slot registry pure core: SlotMap declaration merging, single register composition API, four-share props types, store-seat types, renderer install seam",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-subagent",
"description": "Subagent conversation catalog, continuation routing UI, and '@' reference source",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-theme",
"description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-tool",
"description": "Client Tool call-tree renderer and keyed per-tool presentation slot",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-trajectory",
"description": "Trajectory event ledger with an interactive timing overview: pure-consumer plugin registering into the conversation ViewMap (no service)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-user-questions",
"description": "Web ask_user_question feature: host tool mount plus composer-takeover question UI",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-workflow-run",
"description": "Durable workflow-run Conversation Node and nested member disclosure for dsh web",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-workspace",
"description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-web-react",
"description": "Shell-side React glue: createSlotRenderer, SessionProvider, bindSnapshotSelector (uSES bridge), useInvoke",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-web",
"description": "Web shell kernel: bootWebShell (module system holding + seed table + two-stage boot + AppRoot gate + app-shell assembly entry), consumed by the apps/web vite entry",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-python/README.md
README.md: 1fc19b89b751ce5063d6937d588b96f2f36ae69e
README.zh.md: 0351001308541b41ba519b70a61d25c48de73a4b
@@ -0,0 +1,29 @@
# @deepseek-ai/dsh-code-runtime-python
English | [中文](README.zh.md)
CPython-subprocess implementation of the [`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.md) seam. Companion to [`@deepseek-ai/dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.md); trades the Node worker thread for a fresh `python3` subprocess so model code is Python instead of TypeScript.
The package owns the wire protocol for that seam: the host-side frame codec and the Python-side mirror of the same message vocabulary.
## Wire protocol
The host and the CPython subprocess exchange a versionless, JSON-lines protocol on the child's fd 3 — one JSON object per line, leaving stdout/stderr free for the program's own output. `src/protocol.ts` is the host side; `py/protocol.py` mirrors its message shapes and the shared truncation-marker text on the Python side.
- **fd 3, not stdout** — Node pins the channel positionally with `stdio: ['pipe','pipe','pipe','pipe']`; the Python bootstrap reads the same `PROTOCOL_FD` constant. JSON-lines framing.
- **Host treats every inbound frame as hostile** — model code has full access to fd 3 and can post anything through it, so `validateChildFrame` shape-validates and REBUILDS each frame before the host reads it: forged extra fields never ride along, a non-number call id can never be echoed into a reply, and junk drops to `undefined` rather than throwing in the host's message handler. The Python side trusts host replies (the host is not model-controlled).
- **Lossless-JSON crossing** — completion values and binding arguments cross as exact JSON. `encodeJsonPlain` serializes a `JSON.parse`-produced value without recursion, so a deep value below the byte budget crosses intact instead of dying on `JSON.stringify`'s stack limit; `checkDoneValue` meters a forged completion value's byte length AND number losslessness in one traversal that rejects an over-budget payload before the incremental work it would add (the enqueued children; strings and keys are metered by a non-allocating escaped-size scan, so the escaped copy is never materialized) — the frame's own width is already parsed and capped upstream by the host's fd-3 receive buffer, not re-bounded here; `hasUnsafeIntegerToken` reads the raw frame text to catch an integer token that `JSON.parse` would silently round; `hasNonLosslessNumber` rejects a non-finite or negative-zero number in unbounded `call.args`. Beyond-safe-range integral doubles serialize through `BigInt` digits so the exact integer crosses, not the rounded `String()` form.
- **Shared truncation marker** — `logTruncationMarker(maxBytes)` produces byte-identical text on both sides, so a truncated log run reads the same however the cap was hit. The `log` frame's `truncated` flag distinguishes the child ledger's own marker from program output.
## Model Experience
Indirectly, through Code Mode in [`dsh-tools`](../../core/tools/README.md), which renders this backend's exact completion value when it fits (or an explicit `invalid-output` / `output-limit` failure), plus the exact `[dsh-code-runtime-python] log capture truncated at <maxLogBytes> bytes` log marker, into a retained `run_code` result.
#### KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **The cross-language guard covers the runtime-executed surfaces and the frame field shapes** — `tests/protocol-mirror.e2e.ts` spawns a real `python3` and asserts, against `src/protocol.ts`, both `PROTOCOL_FD` / the log truncation marker text AND each `TypedDict`'s required/optional wire field set in `py/protocol.py`. What it does not compare is the field *types* (e.g. that `cpuSeconds` is an `int` on both sides): comparing type declarations across TypeScript and Python has no mechanical equivalent here, so a type-level drift is still caught by review plus the backend's real-subprocess suite rather than this package's tests.
- **`src/index.ts` exports the protocol vocabulary only** — the package carries no subprocess execution path and no Python-side JSON codec, so nothing here spawns `python3` outside the mirror test.
@@ -0,0 +1,29 @@
# @deepseek-ai/dsh-code-runtime-python
[English](README.md) | 中文
[`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.md) seam 的 CPython 子进程实现。与 [`@deepseek-ai/dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.md) 配套;以全新的 `python3` 子进程取代 Node worker 线程,让模型代码从 TypeScript 换成 Python。
本包持有该 seam 的 wire protocolhost 侧的帧编解码,以及 Python 侧对同一套消息词汇的镜像。
## Wire protocol
host 与 CPython 子进程在子进程的 fd 3 上交换一个无版本号的 JSON-lines 协议——每行一个 JSON 对象,让 stdout/stderr 空出给程序自己的输出。`src/protocol.ts` 是 host 侧;`py/protocol.py` 在 Python 侧镜像其帧词汇与共享的截断标记文本。
- **fd 3,而非 stdout** —— Node 通过 `stdio: ['pipe','pipe','pipe','pipe']` 按位置钉住通道;Python bootstrap 读取相同的 `PROTOCOL_FD` 常量。JSON-lines 帧。
- **host 把每个入站帧当作敌意输入** —— 模型代码对 fd 3 有完全访问权、可通过它发送任意内容,所以 `validateChildFrame` 在 host 读取前对每个帧做形状校验并重建:伪造的额外字段绝不随行,非数字的 call id 绝不会被回显进 reply,垃圾降为 `undefined` 被丢弃,而不是在 host 的 message handler 里抛错。Python 侧信任 host 回复(host 不受模型控制)。
- **lossless-JSON 穿越** —— 完成值与 binding 参数以精确 JSON 穿越。`encodeJsonPlain` 无递归地序列化一个 `JSON.parse` 产出的值,使低于字节预算的深层值能完整穿越,而不是死在 `JSON.stringify` 的栈限制上;`checkDoneValue` 在一次遍历中同时计量伪造完成值的字节长度与数字无损性,在它本会新增的增量工作之前就拒绝超预算 payload(即入栈子节点;字符串与 key 由非分配的转义尺寸扫描计量,从不物化转义副本)——帧自身的宽度已被上游 `JSON.parse` 支付、由 host 的 fd-3 接收缓冲封顶,并非在此重新约束;`hasUnsafeIntegerToken` 读取原始帧文本,捕获 `JSON.parse` 会静默舍入的整数 token`hasNonLosslessNumber` 拒绝无字节上限的 `call.args` 中的非有限数或负零。超出安全范围的整数型 double 通过 `BigInt` 数字序列化,穿越的是精确整数而非 `String()` 的舍入形式。
- **共享截断标记** —— `logTruncationMarker(maxBytes)` 在两侧产出逐字节一致的文本,使被截断的日志运行无论从哪侧触达上限都读起来一致。`log` 帧的 `truncated` 标志把子进程 ledger 自身的标记与程序输出区分开。
## Model Experience
经由 [`dsh-tools`](../../core/tools/README.md) 里的 Code Mode 间接生效:Code Mode 把本后端的精确完成值(放得下时)或一个明确的 `invalid-output` / `output-limit` 失败,连同精确的 `[dsh-code-runtime-python] log capture truncated at <maxLogBytes> bytes` 日志标记,渲染进一个保留的 `run_code` 结果。
#### KV Cache effect
无直接失效;具名消费者拥有任何请求前缀的变更。
## Known Limitations and Deferred Work
- **跨语言 guard 覆盖运行时执行的面与帧字段形状** —— `tests/protocol-mirror.e2e.ts` 启动一个真实 `python3`,对照 `src/protocol.ts` 断言 `PROTOCOL_FD` / 日志截断标记文本,以及 `py/protocol.py` 中每个 `TypedDict` 的必填/可选 wire 字段集。它不比较字段的*类型*(例如 `cpuSeconds` 两侧都是 `int`):跨 TypeScript 与 Python 比较类型声明在此无机械等价物,故类型级漂移仍由 review 加后端真子进程套件捕获,而非本包的测试。
- **`src/index.ts` 只导出协议词汇** —— 本包不含子进程执行路径,也不含 Python 侧的 JSON codec,因此除 mirror 测试之外没有任何地方会启动 `python3`
@@ -0,0 +1,42 @@
{
"name": "@deepseek-ai/dsh-code-runtime-python",
"description": "CPython subprocess implementation of the DeepSeek Harness code-execution seam",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/code-runtime/code-runtime-python"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"py/**/*.py",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}
@@ -0,0 +1,138 @@
"""Wire protocol vocabulary for the Python side of dsh-code-runtime-python.
Mirrors ``src/protocol.ts``. Frames travel on fd 3 as JSON-lines (one JSON
object per line). The host validates every inbound frame; this side trusts
host replies.
The wire uses the JSON key ``global`` (a Python keyword), so the frame
``TypedDict``s that carry it are declared with the functional syntax rather than
class bodies: a class attribute cannot be named ``global``, and a ``global_``
attribute would describe a key the wire never sends. Optional-field messages
pair a required base with a ``total=False`` subclass so a required field such as
``type`` cannot be dropped while ``value``/``error``/``truncated`` stay optional.
"""
from __future__ import annotations
from typing import Any, Literal, TypedDict, Union
# The protocol fd from the child's perspective. Node passes
# ``stdio: [pipe, pipe, pipe, pipe]`` so the fourth entry (fd 3) is the
# framed-JSON channel; stdout/stderr stay clear for the program's own output.
PROTOCOL_FD = 3
class ErrorClass(TypedDict):
"""A namespace's program-visible exception class: rejected calls raise its
instances carrying the failed member name on ``memberNameProperty``."""
name: str
memberNameProperty: str
# ``global`` is a Python keyword, so the required part is declared functionally
# to hold the real wire key; ``errorClass`` is optional per the TS `errorClass?`.
_NamespaceRequired = TypedDict("_NamespaceRequired", {"global": str, "names": "list[str]"})
class Namespace(_NamespaceRequired, total=False):
"""One binding namespace declaration: the ``global`` name, its function
``names``, and an optional program-visible ``errorClass`` for rejected calls."""
errorClass: ErrorClass
class BootMessage(TypedDict):
"""Host → child, first frame on fd 3. Carries every cap and the namespaces."""
type: Literal["boot"]
cpuSeconds: int
addressSpaceBytes: int
maxLogBytes: int
maxValueBytes: int
namespaces: "list[Namespace]"
class RunMessage(TypedDict):
"""Host → child, sent after ``boot-ack``. Carries only the program body."""
type: Literal["run"]
program: str
class BootAckMessage(TypedDict):
"""Child → host: resource limits applied, ready for the run message."""
type: Literal["boot-ack"]
# ``global`` wire key: whole message declared functionally, all fields required.
CallMessage = TypedDict(
"CallMessage",
{"type": Literal["call"], "id": int, "global": str, "name": str, "args": Any},
)
_LogMessageRequired = TypedDict("_LogMessageRequired", {"type": Literal["log"], "text": str})
class LogMessage(_LogMessageRequired, total=False):
"""Child → host: one captured text chunk, streamed eagerly.
``truncated`` is set only on the frame that IS the child ledger's truncation
marker (not program output), so the host stops capturing at the same point
the child did — mirrors the TS `truncated?`.
"""
truncated: bool
class DoneErrorField(TypedDict):
"""Child → host: the failure carried on a ``done`` frame. ``kind`` is one of
the three the host validates; ``message`` is the traceback or diagnostic."""
kind: Literal["exception", "invalid-output", "output-limit"]
message: str
_DoneMessageRequired = TypedDict("_DoneMessageRequired", {"type": Literal["done"]})
class DoneMessage(_DoneMessageRequired, total=False):
"""Child → host: the program settled. ``value`` and ``error`` are optional per the TS mirror."""
value: Any
error: DoneErrorField
ChildToHost = Union[BootAckMessage, CallMessage, LogMessage, DoneMessage]
class ReplyOk(TypedDict):
type: Literal["reply"]
id: int
ok: Literal[True]
value: Any
class ReplyErr(TypedDict):
type: Literal["reply"]
id: int
ok: Literal[False]
message: str
ReplyMessage = Union[ReplyOk, ReplyErr]
# The host sends ``boot`` and ``run`` before any ``reply``, so the child-facing
# inbound union covers all three, not replies alone.
HostToChild = Union[BootMessage, RunMessage, ReplyMessage]
def log_truncation_marker(max_bytes: int) -> str:
"""Return the in-band marker for a log ledger that exhausted its budget.
Byte-identical text on both sides of the wire so a truncated run reads the
same however the cap was hit.
"""
return f"[dsh-code-runtime-python] log capture truncated at {max_bytes} bytes"
@@ -0,0 +1,19 @@
/**
* CPython subprocess code runtime for the DeepSeek Harness code-execution seam.
*
* The package owns the versionless fd-3 wire protocol between the Node host and
* the CPython subprocess. The protocol's host-side codec and hostile-frame
* validators are re-exported so every consumer of the wire shares one
* vocabulary.
* @module @deepseek-ai/dsh-code-runtime-python
*/
export type { BootMessage, ChildToHost, ReplyMessage } from './protocol.ts'
export {
checkDoneValue,
encodeJsonPlain,
hasNonLosslessNumber,
hasUnsafeIntegerToken,
logTruncationMarker,
validateChildFrame,
} from './protocol.ts'
@@ -0,0 +1,31 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-code-runtime-python`.
* @module @deepseek-ai/dsh-code-runtime-python/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime-python'
/** Cordis companion plugin name. */
export const name = 'code-runtime-python-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this package ships only the fd-3 wire-protocol codec and its Python mirror,
* exposing no runtime event sequence or mutable data relation; `protocol.spec.ts` and
* `protocol-mirror.e2e.ts` cover the protocol's behavior.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,656 @@
/**
* Versionless, JSON-lines wire protocol between the Node host and the CPython subprocess. Frames
* travel on the child's fd 3 (one JSON object per line), leaving stdout/stderr free for the
* program's own output. Host treats every inbound frame as hostile because model code can post
* anything through the same fd; the Python bootstrap trusts host replies.
* @module @deepseek-ai/dsh-code-runtime-python/src/protocol
*/
/**
* The framed-JSON channel's file descriptor from the child's perspective. The
* host pins it positionally when it spawns the child (`stdio` index 3, i.e.
* `['pipe','pipe','pipe','pipe']`), and the Python bootstrap reads the same
* number from its own `protocol.py`. Exported as the single TS-side source of
* truth: the host wiring uses it, and the cross-language mirror test asserts the
* Python constant equals it, so a drift on either side breaks the boot channel
* loudly rather than silently.
*/
export const PROTOCOL_FD = 3
/**
* One binding namespace declaration inside a {@link BootMessage}. `global` is
* the program-visible name the namespace is materialized under; `errorClass`,
* when present, asks the bootstrap to mint a program-visible exception class.
*/
interface Namespace {
global: string
names: string[]
errorClass?: ErrorClass
}
/**
* A namespace's program-visible exception class: rejected calls raise its
* instances carrying the failed member name on `memberNameProperty`.
*/
interface ErrorClass {
name: string
memberNameProperty: string
}
/**
* What the host sends immediately after spawn, as the first line on fd 3. The
* Python bootstrap reads this, applies resource limits, then waits for the
* subsequent run frame. Separated from the run so the run message stays
* pure model input.
*/
export interface BootMessage {
type: 'boot'
/** RLIMIT_CPU seconds; the Python bootstrap sets this on itself before executing model code. */
cpuSeconds: number
/** RLIMIT_AS bytes; caps address space so a runaway allocation fails cleanly. */
addressSpaceBytes: number
/** Shared byte budget for captured log text (Python-side ledger). */
maxLogBytes: number
/** Byte cap for the rendered completion value. */
maxValueBytes: number
/**
* The namespaces to materialize inside the program (globals + names;
* functions stay host-side). See {@link Namespace}.
*/
namespaces: Namespace[]
}
/** Host → Python: sent after `boot-ack`; carries only the model's program body. */
interface RunMessage {
type: 'run'
program: string
}
/** Python → host: acknowledges boot completed and resource limits are in place. */
interface BootAckMessage {
type: 'boot-ack'
}
/** Python → host: one bridged binding call (`await tools.name(args)` inside the program). */
interface CallMessage {
type: 'call'
/** Python-issued correlation id; the host answers each id at most once and ignores duplicates. */
id: number
/** The namespace global the call targets. */
global: string
/** The function name within the namespace. */
name: string
/** The JSON-safe argument the model program passed. */
args: unknown
}
/**
* Python → host: captured text, streamed eagerly so output survives a
* mid-run termination (RLIMIT_CPU, SIGTERM/SIGKILL, host wall-timeout).
*/
interface LogMessage {
type: 'log'
text: string
/**
* Set when this frame IS the child ledger's truncation marker rather than
* program output. The two ledgers can exhaust at different points — one
* child entry larger than `maxLogBytes` sends only the marker while the host
* ledger is still nearly empty — so the host cannot infer the child's state
* from its own budget, and comparing the text against the marker string
* would also honour a program that printed that string itself. Carrying it
* as a field lets the host stop capturing at the same point the child did
* and keeps exactly one marker in `logs`.
*/
truncated?: boolean
}
/** The failure carried on a {@link DoneMessage}: one of three kinds plus text. */
interface DoneErrorField {
kind: 'exception' | 'invalid-output' | 'output-limit'
message: string
}
/**
* Python → host: the program settled. `error` carries a program exception
* (traceback text), an `invalid-output` (completion value was not lossless
* JSON), or an `output-limit` (serialized completion exceeded the configured
* cap); wall/CPU budgets, aborts, and substrate death are observed host-side.
* From the honest child `value` is present only on a clean completion that
* produced one, and crosses as exact lossless JSON — never substituted or
* truncated. A forged frame CAN carry both `value` and `error`;
* {@link validateChildFrame} preserves both rather than guessing which to drop,
* so a consumer MUST check `error` first and ignore `value` when it is set.
*/
interface DoneMessage {
type: 'done'
value?: unknown
error?: DoneErrorField
}
/**
* Every message the Python side sends. The member interfaces stay module-
* private: consumers match on the union's discriminant; the host sends the
* boot and run frames as inline literals.
*/
export type ChildToHost = BootAckMessage | CallMessage | LogMessage | DoneMessage
/** Host → Python: successful answer to one {@link CallMessage}. */
interface ReplyOk {
type: 'reply'
id: number
ok: true
value: unknown
}
/** Host → Python: failed answer to one {@link CallMessage}. */
interface ReplyErr {
type: 'reply'
id: number
ok: false
message: string
}
/** Host → Python: the answer to one {@link CallMessage}. */
export type ReplyMessage = ReplyOk | ReplyErr
/** The required (non-optional) keys of `T`, as string literals. */
type RequiredKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? never : K }[keyof T] & string
/** The optional keys of `T`, as string literals. */
type OptionalKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? K : never }[keyof T] & string
/**
* Whether each key of frame `T` is a `'required'` or `'optional'` wire field.
* Because it is `Record<keyof T, …>`, an entry MUST list every key — a field
* added to the interface without a corresponding entry fails typecheck — and
* `keyof T`-typed keys reject a name no frame declares. The `'required'` /
* `'optional'` tag must match the field's actual optionality (checked by the
* `satisfies FrameFieldRoles<…>` clause on {@link WIRE_FRAME_FIELD_ROLES}), so
* an optionality flip is caught too. This is the exhaustive counterpart the
* array form could not express (a subset array satisfied it silently).
*/
type FrameFieldRoles<T> = Record<RequiredKeys<T>, 'required'> & Record<OptionalKeys<T>, 'optional'>
interface WireFrameShapes {
BootMessage: BootMessage
Namespace: Namespace
RunMessage: RunMessage
BootAckMessage: BootAckMessage
CallMessage: CallMessage
LogMessage: LogMessage
DoneErrorField: DoneErrorField
DoneMessage: DoneMessage
ErrorClass: ErrorClass
ReplyOk: ReplyOk
ReplyErr: ReplyErr
}
/**
* The frames carried on a message union: everything the host and child send as
* a top-level frame (`ChildToHost`, the two reply variants, and the host→child
* boot/run frames). The nested shapes `Namespace`, `ErrorClass`, and
* `DoneErrorField` are fields of other frames, not frames themselves, so they
* are excluded here and covered only by the roles `satisfies` and the mirror e2e.
*/
type MessageFrames = ChildToHost | ReplyMessage | BootMessage | RunMessage
/** The roster's value types minus the three nested (non-frame) shapes. */
type RosterMessageFrames = Exclude<WireFrameShapes[keyof WireFrameShapes], Namespace | ErrorClass | DoneErrorField>
/**
* Compile-time proof that {@link WireFrameShapes}'s message-frame entries are
* EXACTLY the frames on the message unions — checked BOTH directions. Forward
* (`MessageFrames extends RosterMessageFrames`) catches a frame added to a union
* without a roster entry; reverse (`RosterMessageFrames extends MessageFrames`)
* catches a frame removed from a union while the roster still lists it (e.g.
* dropping `ReplyErr` from `ReplyMessage`). Either divergence makes an alias
* `false`, failing the assignment below. Type-only; the `const`s emit nothing
* meaningful at runtime.
*/
type UnionSubsetOfRoster = [MessageFrames] extends [RosterMessageFrames] ? true : false
type RosterSubsetOfUnion = [RosterMessageFrames] extends [MessageFrames] ? true : false
const _unionSubsetOfRoster: UnionSubsetOfRoster = true
const _rosterSubsetOfUnion: RosterSubsetOfUnion = true
void _unionSubsetOfRoster
void _rosterSubsetOfUnion
/**
* Each frame's wire fields tagged by required/optional, keyed by field name so
* the mapping is exhaustive over the frame interface (see {@link FrameFieldRoles})
* across the whole {@link WireFrameShapes} roster. Bound to the interfaces by
* `satisfies` below; {@link WIRE_FRAME_FIELDS} projects it to sorted
* required/optional arrays for the cross-language mirror comparison. `global` is
* the JSON key {@link CallMessage} and {@link Namespace} send (a reserved word
* the Python side carries via a functional `TypedDict`).
*/
const WIRE_FRAME_FIELD_ROLES = {
BootMessage: { type: 'required', cpuSeconds: 'required', addressSpaceBytes: 'required', maxLogBytes: 'required', maxValueBytes: 'required', namespaces: 'required' },
Namespace: { global: 'required', names: 'required', errorClass: 'optional' },
RunMessage: { type: 'required', program: 'required' },
BootAckMessage: { type: 'required' },
CallMessage: { type: 'required', id: 'required', global: 'required', name: 'required', args: 'required' },
LogMessage: { type: 'required', text: 'required', truncated: 'optional' },
DoneErrorField: { kind: 'required', message: 'required' },
DoneMessage: { type: 'required', value: 'optional', error: 'optional' },
ErrorClass: { name: 'required', memberNameProperty: 'required' },
ReplyOk: { type: 'required', id: 'required', ok: 'required', value: 'required' },
ReplyErr: { type: 'required', id: 'required', ok: 'required', message: 'required' },
} as const satisfies { [K in keyof WireFrameShapes]: FrameFieldRoles<WireFrameShapes[K]> }
/**
* The wire field names of each frame, split into sorted required and optional
* key arrays — the shape the cross-language mirror test compares against
* `py/protocol.py`'s `TypedDict` `__required_keys__`/`__optional_keys__`.
* Projected from {@link WIRE_FRAME_FIELD_ROLES}, so it inherits that mapping's
* exhaustive, optionality-checked binding to the frame interfaces: a TS-side
* field add, remove, rename, or optionality flip fails typecheck at the roles
* map, and a Python-side divergence fails the mirror test at runtime.
*/
export const WIRE_FRAME_FIELDS =
Object.fromEntries(
Object.entries(WIRE_FRAME_FIELD_ROLES).map(([frame, roles]) => {
const required = Object.keys(roles).filter(key => (roles as Record<string, string>)[key] === 'required').sort()
const optional = Object.keys(roles).filter(key => (roles as Record<string, string>)[key] === 'optional').sort()
return [frame, { required, optional }]
}),
) as Record<keyof typeof WIRE_FRAME_FIELD_ROLES, { required: string[]; optional: string[] }>
/**
* The in-band marker text announcing that log capture stopped at the byte
* budget. Shared wire vocabulary: the Python-side LogBuffer emits it when ITS
* ledger exhausts, and the host emits identical text when its own ledger drops
* a frame first (forged fd-3 traffic, stray stdout bytes) — a truncated run
* reads the same however the cap was hit.
* @param maxBytes - the configured `maxLogBytes` the marker names.
* @returns the marker line.
*/
export function logTruncationMarker(maxBytes: number): string {
return `[dsh-code-runtime-python] log capture truncated at ${maxBytes} bytes`
}
/**
* Serialize one JSON-parse-produced value without recursion. `JSON.stringify`
* recurses per nesting level and throws `RangeError` a few thousand levels
* deep, but the seam's `CodeJsonValue` has no depth limit — an honest deep
* completion or binding resolution below the byte budget must cross intact
* (the worker backend's wire is equally stack-safe). Callers must pass a value
* produced by `JSON.parse` (or equally JSON-plain): only `null`, finite
* numbers, booleans, strings, dense arrays, and plain objects — this encoder
* validates nothing. Output matches compact `JSON.stringify` byte for byte
* EXCEPT on an integral double beyond the safe range, where {@link scalarJson}
* emits the exact integer's BigInt digits rather than `JSON.stringify`'s rounded
* spelling (`1152921504606846976`, not `...847000`) so the seam's lossless-JSON
* promise holds across the wire.
* @param value - a JSON-plain value (e.g. straight from `JSON.parse`).
* @returns the compact JSON encoding.
*/
export function encodeJsonPlain(value: unknown): string {
type Task = { text: string } | { value: unknown }
const chunks: string[] = []
const tasks: Task[] = [{ value }]
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
if ('text' in task) {
chunks.push(task.text)
continue
}
const current = task.value
if (typeof current === 'string') {
chunks.push(JSON.stringify(current))
} else if (Array.isArray(current)) {
chunks.push('[')
tasks.push({ text: ']' })
for (let index = current.length - 1; index >= 0; index--) {
if (index < current.length - 1) tasks.push({ text: ',' })
tasks.push({ value: current[index] })
}
} else if (typeof current === 'object' && current !== null) {
const record = current as Record<string, unknown>
chunks.push('{')
tasks.push({ text: '}' })
const keys = Object.keys(record)
for (let index = keys.length - 1; index >= 0; index--) {
const key = keys[index] as string
if (index < keys.length - 1) tasks.push({ text: ',' })
tasks.push({ value: record[key] })
tasks.push({ text: `${JSON.stringify(key)}:` })
}
} else {
chunks.push(scalarJson(current))
}
}
return chunks.join('')
}
/**
* One scalar (null, boolean, finite number) as JSON text. A beyond-safe-range
* integral double needs BigInt digits: `String(2 ** 60)` emits the ROUNDED
* `...847000` form, and echoing that to the child would silently change the
* integer the seam promised to carry losslessly — `BigInt(2 ** 60)` prints the
* exact `...846976` the double actually holds.
* @param current - a JSON-plain scalar (JSON.parse emits nothing else).
* @returns its JSON encoding.
*/
function scalarJson(current: unknown): string {
if (typeof current === 'number' && Number.isInteger(current) && !Number.isSafeInteger(current)) {
return BigInt(current).toString()
}
return String(current)
}
/**
* Exact UTF-8 byte length of one string's compact JSON form (quotes + escapes),
* computed by a single non-allocating scan that stops the instant the running
* total exceeds `maxBytes`. Used instead of `Buffer.byteLength(JSON.stringify(s))`
* so a control-heavy forged string — whose escaped copy expands up to ~6x — is
* rejected BEFORE that copy is materialized: `JSON.stringify` would allocate the
* full escaped form first, the very hundreds-of-MB spike the metered traversal
* exists to avoid. Mirrors `JSON.stringify`'s escaping byte-for-byte: `"` and
* `\` and the five short C0 escapes cost 2, other C0 controls `\uXXXX` cost 6, a
* valid surrogate pair is one astral code point emitted as raw 4-byte UTF-8, a
* LONE surrogate becomes `\uXXXX` at 6, and any other code point costs its raw
* UTF-8 width.
* @param text - the string to meter.
* @param maxBytes - largest serialized size the caller can still admit.
* @returns the exact serialized byte length, or `undefined` once it exceeds `maxBytes`.
*/
function jsonStringBytesUpTo(text: string, maxBytes: number): number | undefined {
let bytes = 2 // the two quotes
if (bytes > maxBytes) return undefined
for (let index = 0; index < text.length; index++) {
const code = text.charCodeAt(index)
if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
bytes += 2 // `\"` `\\` `\b` `\t` `\n` `\f` `\r`
} else if (code < 0x20) {
bytes += 6 // other C0 controls: `\uXXXX`
} else if (code < 0x80) {
bytes += 1
} else if (code < 0x800) {
bytes += 2
} else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length) {
const next = text.charCodeAt(index + 1)
if (next >= 0xdc00 && next <= 0xdfff) {
bytes += 4 // valid high+low pair: one astral code point, raw 4-byte UTF-8
index++
} else {
bytes += 6 // lone high surrogate: `\uXXXX`
}
} else if (code >= 0xd800 && code <= 0xdfff) {
bytes += 6 // lone surrogate (unpaired high at end, or any low): `\uXXXX`
} else {
bytes += 3 // other BMP code point
}
if (bytes > maxBytes) return undefined
}
return bytes
}
/**
* Meter a `JSON.parse`-produced done value's compact-JSON byte length AND its
* number losslessness in one traversal, stopping the instant `maxBytes` is
* crossed. This bounds the INCREMENTAL allocation the check itself would add on
* top of the already-parsed value — the enqueued children; strings and keys are
* metered by {@link jsonStringBytesUpTo} without allocating an escaped copy —
* not the parse that produced `value`.
* That upstream width is bounded separately, by the host-side cap on inbound
* fd-3 frame size before `JSON.parse` runs (owned by the runtime that reads the
* channel), so `value` cannot be arbitrarily large when it reaches here. The
* budget is the `maxValueBytes` the boot frame carries — a required wire field
* with no default at this layer. The traversal rejects over-budget BEFORE
* materializing a string's escaped form or enqueuing an array's/object's
* children, so a forgery within that frame cap cannot force those secondary
* allocations. Object key COUNTING is
* unavoidably O(keys) — JS has no lazy own-key iterator, and the parse already
* built the key set — but the check still refuses the per-entry work before the
* enqueue loop. A non-lossless number (non-finite, negative zero) is caught only
* when the value fits the budget — an over-budget value is rejected regardless,
* so the distinction is moot. Same JSON-plain precondition and traversal shape
* as {@link encodeJsonPlain}; a number's byte length is measured through
* {@link scalarJson} (matching the encoder, so a beyond-safe-range integer
* meters its exact BigInt digits, not `JSON.stringify`'s rounded spelling) and
* a string's/key's through {@link jsonStringBytesUpTo} (the exact escaped size,
* scanned without allocating the escaped copy).
* @param value - a JSON-plain value (e.g. straight from `JSON.parse`).
* @param maxBytes - the completion-value budget in bytes.
* @returns `{ ok: true, bytes }` with the exact serialized size, or
* `{ ok: false, reason }` — `over-budget` once the size exceeds `maxBytes`,
* `non-lossless` on a non-finite or negative-zero number.
*/
export function checkDoneValue(value: unknown, maxBytes: number): { ok: true; bytes: number } | { ok: false; reason: 'over-budget' | 'non-lossless' } {
let bytes = 0
// A non-lossless number is recorded, not returned on sight: over-budget must
// win regardless of where in the value each violation sits, so the whole
// metering finishes first. Otherwise `["<huge>", 1e400]` and `[1e400,
// "<huge>"]` — the same over-budget value in two member orders — would
// classify differently (non-lossless vs over-budget), and the JSDoc promises
// an over-budget value is rejected as over-budget regardless.
let nonLossless = false
const stack: unknown[] = [value]
while (stack.length > 0) {
const current = stack.pop()
if (typeof current === 'number') {
// Flag a non-lossless number but keep counting its encoded bytes: a value
// that is BOTH non-lossless and over-budget must classify as over-budget
// (the loop's byte check below wins), so the byte count cannot skip the
// offending number. `scalarJson` gives the same spelling a legit scalar
// would meter.
if (!Number.isFinite(current) || Object.is(current, -0)) nonLossless = true
bytes += Buffer.byteLength(scalarJson(current), 'utf8')
} else if (typeof current === 'string') {
// Meter the escaped form WITHOUT allocating it: jsonStringBytesUpTo scans
// and bails the instant the running cost crosses the remaining budget, so
// a control-heavy forgery (escaped copy up to ~6x) never materializes that
// copy the way `JSON.stringify` would.
const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes)
if (stringBytes === undefined) return { ok: false, reason: 'over-budget' }
bytes += stringBytes
} else if (Array.isArray(current)) {
// Brackets plus one comma per gap; elements add themselves. Reject
// BEFORE enqueuing children: every element serializes to at least one
// byte, so a forged flat array far above the budget fails here without
// pushing its elements onto the host stack. (The array itself is already
// materialized by the upstream parse; this only bounds the extra stack.)
bytes += 2 + (current.length > 1 ? current.length - 1 : 0)
if (bytes + current.length > maxBytes) return { ok: false, reason: 'over-budget' }
for (const item of current) stack.push(item)
} else if (typeof current === 'object' && current !== null) {
const record = current as Record<string, unknown>
// Count own keys with for...in + hasOwn. This IS O(keys) — JS has no lazy
// own-key iterator and the parse already built the key set — so the count
// cannot be sublinear; what the bound below buys is refusing the per-entry
// work (key escaping, value enqueue) before it runs. Each entry costs at
// least a quoted key (>= 2 bytes) + colon + >= 1-byte value.
let count = 0
for (const key in record) if (Object.hasOwn(record, key)) count += 1
bytes += 2 + (count > 1 ? count - 1 : 0)
if (bytes + count * 4 > maxBytes) return { ok: false, reason: 'over-budget' }
for (const key in record) {
if (!Object.hasOwn(record, key)) continue
// Meter the key's escaped form without allocating it (same reason as the
// string branch), then add the colon separator. `+ 1` for the `:`.
const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes)
if (keyBytes === undefined) return { ok: false, reason: 'over-budget' }
bytes += keyBytes + 1
stack.push(record[key])
}
} else {
bytes += Buffer.byteLength(scalarJson(current), 'utf8')
}
if (bytes > maxBytes) return { ok: false, reason: 'over-budget' }
}
// The whole value fit the budget; a recorded number violation is the verdict.
if (nonLossless) return { ok: false, reason: 'non-lossless' }
return { ok: true, bytes }
}
/**
* Whether a raw JSON line contains an integer token that would lose precision
* as a JavaScript number. `JSON.parse` silently rounds such a token
* (`9007199254740993` becomes `...992`) BEFORE any validation can see it, so
* the check must read the source text; a beyond-safe-range token whose double
* parse round-trips exactly (`2**53`, `2**60`) is lossless and passes. The scan walks the line skipping string literals (a digit run
* inside a string is data, not a number token) and tests every number token
* in plain integer form — no fraction or exponent, which parse as doubles by
* intent. A reviver cannot do this job: the reviver walk recurses per nesting
* level and would reintroduce the depth limit `encodeJsonPlain` removes.
* @param line - the raw UTF-8 text of one JSON-lines frame.
* @returns true when an unsafe integer token is present outside strings.
*/
export function hasUnsafeIntegerToken(line: string): boolean {
for (let index = 0; index < line.length; index++) {
const char = line[index]
if (char === '"') {
// Skip the string literal, honoring backslash escapes.
for (index++; index < line.length; index++) {
if (line[index] === '\\') index++
else if (line[index] === '"') break
}
continue
}
if (char === '-' || (char !== undefined && char >= '0' && char <= '9')) {
let end = index + 1
while (end < line.length) {
const c = line[end] as string
if ((c >= '0' && c <= '9') || c === '.' || c === 'e' || c === 'E' || c === '+' || c === '-') end++
else break
}
const token = line.slice(index, end)
// Beyond the safe range an integer token is still lossless IFF the
// double parse round-trips exactly (2**53 does; 2**53+1 rounds) — the
// canonical boundary accepts every JS-double-exact value, so only a
// genuinely rounding token marks the frame as forged.
if (/^-?\d+$/.test(token)) {
const parsed = Number(token)
// A token that parses to Infinity is trivially lossy; a finite
// beyond-safe-range one is lossy only when the BigInt round-trip
// disagrees.
if (!Number.isFinite(parsed)) return true
if (!Number.isSafeInteger(parsed) && BigInt(token) !== BigInt(parsed)) return true
}
index = end - 1
}
}
return false
}
/**
* Lazily yield one plain object's own enumerable property values. A generator
* (not `Object.values`/`Object.entries`) because {@link hasNonLosslessNumber}
* walks breadth it cannot bound: those helpers copy the whole VALUE (or
* key/value pair) list into a fresh array up front, so a wide object would cost
* that second full-breadth allocation before a single value is examined. The
* `for...in` here does not make the walk sublinear — V8 still materializes the
* key-name enumeration when the loop starts — but it avoids the extra value
* array, yielding each value straight off the already-parsed object.
* @param record - a JSON-parse-produced object.
* @yields each own enumerable property value, in key order.
*/
function* ownValues(record: object): Generator {
for (const key in record) {
if (Object.hasOwn(record, key)) yield (record as Record<string, unknown>)[key]
}
}
/**
* Whether a JSON.parse-produced value contains a number outside lossless
* JSON: non-finite (`1e400` parses to `Infinity`) or negative zero (`-0.0`
* parses to JS `-0`, whose sign bit a re-serialization drops). The honest
* child's validator rejects these before sending, so a frame carrying one is
* forged.
*
* Runs on `call.args`, which — unlike a completion value — has NO seam byte
* cap, so there is no budget to reject a wide payload against the way
* {@link checkDoneValue} does. The traversal therefore holds ONE cursor per
* NESTING LEVEL (an array or {@link ownValues} iterator) instead of one entry
* per member: a forged flat `args` at the top of the host's inbound frame-size
* cap would
* otherwise push tens of millions of stack entries — and `Object.values` would
* copy each object's full breadth — allocating hundreds of megabytes beyond
* what `JSON.parse` already holds. Iterative either way, so a deep frame
* cannot overflow the host stack.
* @param value - a JSON-parse-produced value from an fd-3 frame.
* @returns true when any contained number is non-finite or negative zero.
*/
export function hasNonLosslessNumber(value: unknown): boolean {
const cursors: Iterator<unknown>[] = [[value].values()]
while (cursors.length > 0) {
// The loop condition guarantees a top cursor.
const cursor = cursors.at(-1) as Iterator<unknown>
const step = cursor.next()
if (step.done === true) {
cursors.pop()
continue
}
const current = step.value
if (typeof current === 'number') {
if (!Number.isFinite(current) || Object.is(current, -0)) return true
} else if (Array.isArray(current)) {
cursors.push((current as unknown[]).values())
} else if (typeof current === 'object' && current !== null) {
cursors.push(ownValues(current))
}
}
return false
}
/**
* Runtime shape gate for inbound fd-3 traffic. Model code has full access to
* fd 3 and can post anything — `null`, primitives, poisoned fields — so the
* compile-time union means nothing here: every field is validated and REBUILT
* before the host reads it (forged extras never ride along; a non-number id
* can never be echoed into a reply). Junk returns `undefined` and is dropped
* so a throw in the host's `message` handler cannot crash the host process.
* @param raw - one JSON-parsed frame from fd 3.
* @returns the rebuilt frame, or `undefined` to drop it silently.
*/
export function validateChildFrame(raw: unknown): ChildToHost | undefined {
if (typeof raw !== 'object' || raw === null) return undefined
const m = raw as Record<string, unknown>
switch (m.type) {
case 'boot-ack':
return { type: 'boot-ack' }
case 'log':
if (typeof m.text !== 'string') return undefined
// Rebuilt, not passed through: a forged `truncated` of any other type
// would reach the host as a truthy value and silence capture for the
// rest of the run. Only the literal `true` counts.
return { type: 'log', text: m.text, ...m.truncated === true ? { truncated: true } : {} }
case 'call': {
// The id must be a finite number: it is echoed verbatim into the reply
// frame, and a forged `1e400` id (Infinity after JSON.parse) would make
// the reply unencodable as strict JSON. Negative zero is rejected too:
// it passes `Number.isFinite`, but the reply re-serializes it as `0`
// (`JSON.stringify({id:-0})` is `{"id":0}`), colliding with a real call
// whose id is `0` — the honest child never issues `-0`.
if (typeof m.id !== 'number' || !Number.isFinite(m.id) || Object.is(m.id, -0) || typeof m.global !== 'string' || typeof m.name !== 'string') return undefined
// A forged frame can omit `args` entirely; rebuilding it as `undefined`
// would invoke the binding with a non-JSON value, bypassing the
// lossless-JSON argument boundary. Any PRESENT value is JSON-plain by
// construction (the frame came from JSON.parse), so presence is the
// whole check.
if (!Object.hasOwn(m, 'args')) return undefined
// JSON.parse yields Infinity for 1e400 and preserves -0; both are
// outside lossless JSON, and the honest child never sends them.
if (hasNonLosslessNumber(m.args)) return undefined
return { type: 'call', id: m.id, global: m.global, name: m.name, args: m.args }
}
case 'done': {
// The value passes through untouched here: scanning it for non-lossless
// numbers would push every member of a wide forged payload before any
// byte cap runs. The done handler's bounded `checkDoneValue` folds the
// losslessness check into the metered traversal, rejecting over-budget
// before it enqueues children.
const err = m.error
if (err === undefined) {
return m.value === undefined ? { type: 'done' } : { type: 'done', value: m.value }
}
if (typeof err !== 'object' || err === null) return undefined
const { kind, message } = err as Record<string, unknown>
if (typeof message !== 'string') return undefined
if (kind !== 'exception' && kind !== 'invalid-output' && kind !== 'output-limit') return undefined
return m.value === undefined
? { type: 'done', error: { kind, message } }
: { type: 'done', value: m.value, error: { kind, message } }
}
default:
return undefined
}
}
@@ -0,0 +1,103 @@
import { execFile } from 'node:child_process'
import { existsSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { describe, expect, it } from 'vitest'
import { logTruncationMarker, PROTOCOL_FD, WIRE_FRAME_FIELDS } from '../src/protocol.ts'
/**
* Cross-language mirror check between `src/protocol.ts` and `py/protocol.py`,
* spawning a real `python3` to read the Python side. Two things are asserted:
* the runtime surfaces both sides EXECUTE against — `PROTOCOL_FD` and the log
* truncation marker text, where a drift silently corrupts a live run — and the
* per-frame wire field sets (required/optional keys of each `TypedDict`), which
* turns the otherwise review-only shape mirror into an executable check that
* catches the round-12 kind of drift (a renamed/dropped field, or one side
* making a field optional the other requires). Self-skips when no `python3` is
* on PATH — CI provides one; the pure-TS `protocol.spec.ts` covers the host
* codec unconditionally.
*/
const execFileAsync = promisify(execFile)
const pyDir = fileURLToPath(new URL('../py', import.meta.url))
// `-B` blocks bytecode writes into the source tree (`py/__pycache__/*.pyc`);
// `-I` isolates the interpreter but does not imply it.
const python3Flags = ['-I', '-B']
async function hasPython3(): Promise<boolean> {
try {
await execFileAsync('python3', ['--version'])
return true
} catch {
return false
}
}
const python3Available = await hasPython3()
describe.skipIf(!python3Available)('protocol.py mirrors protocol.ts at runtime', () => {
it('agrees on PROTOCOL_FD and the log truncation marker across byte budgets', async () => {
const budgets = [1, 65536, 1048576]
const probe = [
'import json, sys',
`sys.path.insert(0, ${JSON.stringify(pyDir)})`,
'from protocol import PROTOCOL_FD, log_truncation_marker',
`budgets = ${JSON.stringify(budgets)}`,
'print(json.dumps({',
' "fd": PROTOCOL_FD,',
' "markers": [log_truncation_marker(b) for b in budgets],',
'}))',
].join('\n')
const { stdout } = await execFileAsync('python3', [...python3Flags, '-c', probe])
const seen = JSON.parse(stdout) as { fd: number; markers: string[] }
// Assert against the TS-side PROTOCOL_FD export (the value the host wires),
// not a bare literal, so a drift on either side of the wire is caught here.
expect(seen.fd).toBe(PROTOCOL_FD)
expect(seen.markers).toEqual(budgets.map(budget => logTruncationMarker(budget)))
})
it('agrees on every frame type\'s wire field set between the TS and Python declarations', async () => {
// Turn the TypedDict mirror from a review-only obligation into an executable
// check: enumerate EVERY TypedDict in py/protocol.py (public names carrying
// __required_keys__) and assert both the frame roster and each frame's
// required/optional key sets against WIRE_FRAME_FIELDS — projected from the
// WIRE_FRAME_FIELD_ROLES map that `satisfies` binds exhaustively to the
// frame interfaces in protocol.ts. Together this catches drift on EITHER
// side of the wire: a TS-side field add, remove, rename, or optionality flip
// fails typecheck at the roles map; a Python frame added, removed, or with a
// changed field set fails this comparison. `global` is the reserved-keyword
// wire key the Python side carries via a functional TypedDict.
const probe = [
'import json, sys',
`sys.path.insert(0, ${JSON.stringify(pyDir)})`,
'import protocol as p',
'def keys(td): return {"required": sorted(td.__required_keys__), "optional": sorted(td.__optional_keys__)}',
// Every public TypedDict in the module — not a name list from the TS side,
// so a Python-only extra frame is visible here.
'frames = {n: keys(v) for n, v in vars(p).items()'
+ ' if not n.startswith("_") and hasattr(v, "__required_keys__")}',
'print(json.dumps(frames))',
].join('\n')
const { stdout } = await execFileAsync('python3', [...python3Flags, '-c', probe])
const seen = JSON.parse(stdout) as Record<string, { required: string[]; optional: string[] }>
// Normalize the TS source of truth to the same sorted shape Python reports.
const expected = Object.fromEntries(
Object.entries(WIRE_FRAME_FIELDS).map(([name, sets]) => [
name,
{ required: [...sets.required].sort(), optional: [...sets.optional].sort() },
]),
)
// Same frame roster on both sides (catches a frame present on only one),
// then identical field sets per frame.
expect(Object.keys(seen).sort()).toEqual(Object.keys(expected).sort())
expect(seen).toEqual(expected)
})
})
it('names the py/ directory that ships with the package', () => {
// Resolves py/ relative to this test file; the same directory ships in the
// package.json `files` whitelist (`py/**/*.py`). The tests/ directory itself
// is not published — this asserts the source-tree layout the mirror test
// depends on, so it holds even when python3 is absent from the runner.
expect(existsSync(pyDir)).toBe(true)
})
@@ -0,0 +1,304 @@
import { describe, expect, it } from 'vitest'
import { checkDoneValue, encodeJsonPlain, hasNonLosslessNumber, hasUnsafeIntegerToken, logTruncationMarker, validateChildFrame } from '../src/index.ts'
describe('logTruncationMarker', () => {
it('names the configured byte budget', () => {
expect(logTruncationMarker(65536)).toBe('[dsh-code-runtime-python] log capture truncated at 65536 bytes')
expect(logTruncationMarker(1)).toBe('[dsh-code-runtime-python] log capture truncated at 1 bytes')
})
})
describe('validateChildFrame', () => {
it('rebuilds boot-ack frames without extra fields', () => {
expect(validateChildFrame({ type: 'boot-ack' })).toEqual({ type: 'boot-ack' })
// Forged extras never ride along.
expect(validateChildFrame({ type: 'boot-ack', extra: 'x' })).toEqual({ type: 'boot-ack' })
})
it('rebuilds log frames when the text field is a string', () => {
expect(validateChildFrame({ type: 'log', text: 'hi' })).toEqual({ type: 'log', text: 'hi' })
// Non-string text drops.
expect(validateChildFrame({ type: 'log', text: 42 })).toBeUndefined()
expect(validateChildFrame({ type: 'log' })).toBeUndefined()
})
it('carries a log frame truncation flag only for the literal true', () => {
// The child's own ledger marker sets `truncated: true`; the host rebuilds
// it so it stops capturing at the same point.
expect(validateChildFrame({ type: 'log', text: 'x', truncated: true }))
.toEqual({ type: 'log', text: 'x', truncated: true })
// Any other truthy or non-boolean value is a forgery and is dropped from
// the rebuild — otherwise it would silence capture for the rest of the run.
expect(validateChildFrame({ type: 'log', text: 'x', truncated: 1 })).toEqual({ type: 'log', text: 'x' })
expect(validateChildFrame({ type: 'log', text: 'x', truncated: 'yes' })).toEqual({ type: 'log', text: 'x' })
expect(validateChildFrame({ type: 'log', text: 'x', truncated: false })).toEqual({ type: 'log', text: 'x' })
})
it('rebuilds call frames with a numeric id, string global, and string name', () => {
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'echo', args: { x: 1 } }))
.toEqual({ type: 'call', id: 1, global: 'tools', name: 'echo', args: { x: 1 } })
// A frame with NO args key drops whole: rebuilding it as `undefined`
// would invoke the binding with a non-JSON value, bypassing the
// lossless-JSON argument boundary. Any present value is JSON-plain by
// construction (frames arrive via JSON.parse), so null passes.
expect(validateChildFrame({ type: 'call', id: 2, global: 'tools', name: 'echo' })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: 2, global: 'tools', name: 'echo', args: null }))
.toEqual({ type: 'call', id: 2, global: 'tools', name: 'echo', args: null })
// A missing/mistyped required field drops.
expect(validateChildFrame({ type: 'call', id: '1', global: 'tools', name: 'echo' })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: 1, global: 7, name: 'echo' })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools' })).toBeUndefined()
})
it('rebuilds done frames with optional value/error', () => {
expect(validateChildFrame({ type: 'done' })).toEqual({ type: 'done' })
expect(validateChildFrame({ type: 'done', value: 42 })).toEqual({ type: 'done', value: 42 })
expect(validateChildFrame({ type: 'done', error: { kind: 'exception', message: 'boom' } }))
.toEqual({ type: 'done', error: { kind: 'exception', message: 'boom' } })
expect(validateChildFrame({ type: 'done', error: { kind: 'invalid-output', message: 'lossy' } }))
.toEqual({ type: 'done', error: { kind: 'invalid-output', message: 'lossy' } })
expect(validateChildFrame({ type: 'done', error: { kind: 'output-limit', message: 'big' } }))
.toEqual({ type: 'done', error: { kind: 'output-limit', message: 'big' } })
expect(validateChildFrame({ type: 'done', value: 1, error: { kind: 'exception', message: 'boom' } }))
.toEqual({ type: 'done', value: 1, error: { kind: 'exception', message: 'boom' } })
// A `value: undefined` field is dropped (JSON never carries it, but a forged
// shape might; the rebuild coalesces to the absent case).
expect(validateChildFrame({ type: 'done', value: undefined })).toEqual({ type: 'done' })
// A missing or unrecognized kind drops the frame: the child always sends
// one of the three, so anything else is a forgery.
expect(validateChildFrame({ type: 'done', error: { message: 'boom' } })).toBeUndefined()
expect(validateChildFrame({ type: 'done', error: { kind: 'timeout', message: 'x' } })).toBeUndefined()
})
it('rejects malformed done frames', () => {
// error must be an object.
expect(validateChildFrame({ type: 'done', error: 'boom' })).toBeUndefined()
expect(validateChildFrame({ type: 'done', error: null })).toBeUndefined()
// error.message must be a string.
expect(validateChildFrame({ type: 'done', error: {} })).toBeUndefined()
expect(validateChildFrame({ type: 'done', error: { message: 42 } })).toBeUndefined()
})
it('drops non-object inputs and unknown types silently', () => {
expect(validateChildFrame(null)).toBeUndefined()
expect(validateChildFrame(undefined)).toBeUndefined()
expect(validateChildFrame(42)).toBeUndefined()
expect(validateChildFrame('str')).toBeUndefined()
expect(validateChildFrame({})).toBeUndefined()
expect(validateChildFrame({ type: 'unknown' })).toBeUndefined()
})
it('drops CALL frames whose args are non-finite or negative zero', () => {
// JSON.parse turns 1e400 into Infinity and preserves -0; the honest child
// rejects both before sending, so a call frame carrying one is forged.
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'x', args: { n: Infinity } })).toBeUndefined()
expect(validateChildFrame({ type: 'call', id: Infinity, global: 'tools', name: 'x', args: null })).toBeUndefined()
// Plain zero and ordinary floats pass.
expect(validateChildFrame({ type: 'call', id: 1, global: 'tools', name: 'x', args: [0, 1.5] }))
.toEqual({ type: 'call', id: 1, global: 'tools', name: 'x', args: [0, 1.5] })
})
it('drops a CALL frame whose id is negative zero', () => {
// `-0` passes Number.isFinite, but the reply re-serializes it as `0`
// (JSON.stringify({id:-0}) === '{"id":0}'), so a forged `-0` id would
// collide with a real call whose id is `0`. The honest child never sends it.
expect(validateChildFrame({ type: 'call', id: -0, global: 'tools', name: 'x', args: null })).toBeUndefined()
// Plain positive zero is a legitimate id and passes.
expect(validateChildFrame({ type: 'call', id: 0, global: 'tools', name: 'x', args: null }))
.toEqual({ type: 'call', id: 0, global: 'tools', name: 'x', args: null })
})
it('passes DONE values through untouched — losslessness is metered later', () => {
// validateChildFrame no longer scans done.value: an unbounded scan would
// push every member of a wide forged payload before any byte cap ran. The
// done handler's checkDoneValue folds losslessness into the metered walk.
expect(validateChildFrame({ type: 'done', value: Infinity })).toEqual({ type: 'done', value: Infinity })
expect(validateChildFrame({ type: 'done', value: [{ x: -0 }] })).toEqual({ type: 'done', value: [{ x: -0 }] })
expect(validateChildFrame({ type: 'done', value: [0, 1.5] })).toEqual({ type: 'done', value: [0, 1.5] })
})
})
describe('lossless-number scan', () => {
it('finds non-finite and negative-zero numbers at any depth, iteratively', () => {
expect(hasNonLosslessNumber(Infinity)).toBe(true)
expect(hasNonLosslessNumber(-Infinity)).toBe(true)
expect(hasNonLosslessNumber(NaN)).toBe(true)
expect(hasNonLosslessNumber(-0)).toBe(true)
expect(hasNonLosslessNumber({ a: [1, { b: -0 }] })).toBe(true)
expect(hasNonLosslessNumber({ a: [0, 1.5, 'x', null, true] })).toBe(false)
// Deep nesting must not overflow the stack.
let deep: unknown = 0
for (let i = 0; i < 100000; i++) deep = [deep]
expect(hasNonLosslessNumber(deep)).toBe(false)
})
it('walks wide arrays and objects one member at a time', () => {
// `call.args` carries no seam byte cap, so a wide forged payload has no
// budget to be rejected against — the walk must hold one cursor per
// NESTING LEVEL, not one entry per member, or a flat payload at the top of
// the host's inbound frame-size cap would allocate tens of millions of stack
// entries (and `Object.values` a second full-breadth copy). Observable
// through the boundary: a wide payload whose per-member cost the old shape
// would have paid still scans, and a violation ANYWHERE in it is found
// wherever it sits.
const wideArray = new Array(2_000_000).fill(0) as unknown[]
expect(hasNonLosslessNumber(wideArray)).toBe(false)
// Last element, so the cursor must run the whole breadth lazily.
wideArray[wideArray.length - 1] = -0
expect(hasNonLosslessNumber(wideArray)).toBe(true)
const wideObject: Record<string, unknown> = {}
for (let i = 0; i < 200_000; i++) wideObject[`k${i}`] = i
expect(hasNonLosslessNumber(wideObject)).toBe(false)
wideObject.last = Infinity
expect(hasNonLosslessNumber(wideObject)).toBe(true)
// Interleaved nesting: a per-level cursor must resume its parent after a
// child level ends, so a violation after a nested container is still seen.
expect(hasNonLosslessNumber([[1], { a: 2 }, NaN])).toBe(true)
})
it('scans only own enumerable properties', () => {
// The per-level cursor filters own keys (a prototype-carrying frame is
// impossible off JSON.parse, but the filter is what keeps the walk equal
// to what the encoder would serialize).
const withProto = Object.create({ inherited: -0 }) as Record<string, unknown>
withProto.own = 1
expect(hasNonLosslessNumber(withProto)).toBe(false)
})
})
describe('unsafe-integer token scan', () => {
it('flags integer tokens outside the safe range, skipping strings and float forms', () => {
expect(hasUnsafeIntegerToken('{"v":9007199254740993}')).toBe(true)
// Exact beyond-safe-range tokens are lossless and pass (2**53, 2**64).
expect(hasUnsafeIntegerToken('{"v":9007199254740992}')).toBe(false)
expect(hasUnsafeIntegerToken('{"v":18446744073709551616}')).toBe(false)
// A token that parses to Infinity is trivially lossy.
expect(hasUnsafeIntegerToken(`{"v":${'9'.repeat(400)}}`)).toBe(true)
expect(hasUnsafeIntegerToken('{"v":-9007199254740993}')).toBe(true)
expect(hasUnsafeIntegerToken('{"v":9007199254740991}')).toBe(false)
expect(hasUnsafeIntegerToken('{"v":"9007199254740993"}')).toBe(false)
expect(hasUnsafeIntegerToken(String.raw`{"v":"esc\"9007199254740993"}`)).toBe(false)
expect(hasUnsafeIntegerToken('{"v":9007199254740993.0}')).toBe(false)
expect(hasUnsafeIntegerToken('{"v":9e99}')).toBe(false)
})
})
describe('checkDoneValue', () => {
it('matches the exact encoded size and rejects one byte over', () => {
const cases: unknown[] = [null, true, false, 0, -1.5, 'a"b\\', [], {}, [1, 'x', null], { a: [1, 2], b: { c: 'd' } }]
for (const value of cases) {
const exact = Buffer.byteLength(JSON.stringify(value), 'utf8')
expect(checkDoneValue(value, exact), JSON.stringify(value)).toEqual({ ok: true, bytes: exact })
expect(checkDoneValue(value, exact - 1), JSON.stringify(value)).toEqual({ ok: false, reason: 'over-budget' })
expect(encodeJsonPlain(value)).toBe(JSON.stringify(value))
}
})
it('rejects an over-budget value before its secondary allocations', () => {
// A huge string is refused on the cheap length lower bound, before its
// escaped copy is built.
const huge = { data: 'x'.repeat(1_000_000), tail: 'y' }
expect(checkDoneValue(huge, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// A flat array far above the budget fails on the brackets+length bound,
// before its elements are pushed onto the traversal stack. (The array is
// already materialized by the upstream parse; this only avoids the extra
// per-element stack growth.)
const flat = new Array(10_000_000).fill(0)
expect(checkDoneValue(flat, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// A wide object: braces+commas fit the cap, but the per-entry lower bound
// (quoted key + colon + value = count*4) does not, so it fails before any
// key is escaped or any value enqueued.
const wide: Record<string, number> = {}
for (let i = 0; i < 10; i++) wide[`k${i}`] = i
expect(checkDoneValue(wide, 12)).toEqual({ ok: false, reason: 'over-budget' })
})
it('meters a string\'s exact escaped size without allocating it', () => {
// A control-heavy string that fits by DECODED length but not once escaped
// must still reject: 200 NULs are 200 UTF-16 units (would pass a naive
// length bound against cap 1024) but escape to 200*6 + 2 = 1202 bytes.
// jsonStringBytesUpTo scans and bails before the escaped copy is built.
expect(checkDoneValue('\0'.repeat(200), 1024)).toEqual({ ok: false, reason: 'over-budget' })
// Exact-size acceptance, no false rejection: one NUL serializes to a
// 6-char \\uXXXX escape, so with the two quotes = 8 bytes.
expect(checkDoneValue('\0', 8)).toEqual({ ok: true, bytes: 8 })
expect(checkDoneValue('\0', 7)).toEqual({ ok: false, reason: 'over-budget' })
// Multi-byte and astral characters meter at their raw UTF-8 width (a valid
// surrogate pair is 4 bytes, matching JSON.stringify), not a 6-byte escape.
expect(checkDoneValue('\u00e9', 4)).toEqual({ ok: true, bytes: 4 }) // 2 quotes + 2-byte UTF-8
expect(checkDoneValue('\u{1f600}', 6)).toEqual({ ok: true, bytes: 6 }) // 2 quotes + 4-byte UTF-8
expect(checkDoneValue('\u{1f600}', 5)).toEqual({ ok: false, reason: 'over-budget' })
// A lone surrogate escapes to \\uXXXX = 6, so with quotes = 8.
expect(checkDoneValue('\ud800', 8)).toEqual({ ok: true, bytes: 8 })
// A high surrogate followed by a NON-low character is a lone surrogate (6-byte
// escape) plus that character: `\ud800` + `a` = 2 quotes + 6 + 1 = 9.
expect(checkDoneValue('\ud800a', 9)).toEqual({ ok: true, bytes: 9 })
// A BMP 3-byte code point (CJK) meters at its raw UTF-8 width: 2 quotes + 3.
expect(checkDoneValue('中', 5)).toEqual({ ok: true, bytes: 5 })
// Same non-allocating meter for object keys, before the value is enqueued.
expect(checkDoneValue({ ['\0'.repeat(200)]: 1 }, 1024)).toEqual({ ok: false, reason: 'over-budget' })
// A string reached with less than the two quotes' worth of budget is refused
// immediately (even the empty escaped form does not fit).
expect(checkDoneValue('x', 1)).toEqual({ ok: false, reason: 'over-budget' })
})
it('meters only own enumerable keys', () => {
// The walk counts keys with a `for...in` + hasOwn pass rather than
// Object.keys/entries (which allocate per member before the bound). A
// prototype-carrying forgery is impossible off JSON.parse, but the own-key
// filter is what keeps the count equal to the encoder's.
const withProto = Object.create({ inherited: 'x' }) as Record<string, unknown>
withProto.own = 1
expect(checkDoneValue(withProto, 1024)).toEqual({ ok: true, bytes: Buffer.byteLength('{"own":1}', 'utf8') })
})
it('rejects non-finite and negative-zero numbers at any depth as non-lossless', () => {
expect(checkDoneValue(Infinity, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue(-Infinity, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue(NaN, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue(-0, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
expect(checkDoneValue({ a: [1, { b: -0 }] }, 1024)).toEqual({ ok: false, reason: 'non-lossless' })
// An ordinary finite value within budget passes with its exact byte count.
const clean = { a: [0, 1.5, 'x', null, true] }
expect(checkDoneValue(clean, 1024)).toEqual({ ok: true, bytes: Buffer.byteLength(JSON.stringify(clean), 'utf8') })
})
it('classifies an over-budget value as over-budget regardless of member order', () => {
// A value that is BOTH over-budget and non-lossless must reject as
// over-budget whichever member the walk reaches first — the non-lossless
// number is recorded and metering finishes, so the two orders below (the
// same value) cannot classify differently. Cap 100 with a 1000-char string.
const big = 'x'.repeat(1000)
expect(checkDoneValue([big, Infinity], 100)).toEqual({ ok: false, reason: 'over-budget' })
expect(checkDoneValue([Infinity, big], 100)).toEqual({ ok: false, reason: 'over-budget' })
// A non-lossless number that DOES fit the budget still rejects as
// non-lossless (the recorded violation is the verdict once the whole value
// is confirmed within budget).
expect(checkDoneValue([Infinity], 100)).toEqual({ ok: false, reason: 'non-lossless' })
// The non-lossless number's OWN encoded bytes still count toward the budget,
// so a value whose only over-budget contribution is the non-lossless number
// itself is classified over-budget, not non-lossless. `[Infinity]` encodes
// as the 10-byte `[Infinity]`; at cap 3 the byte check wins.
expect(checkDoneValue([Infinity], 3)).toEqual({ ok: false, reason: 'over-budget' })
expect(checkDoneValue(Infinity, 3)).toEqual({ ok: false, reason: 'over-budget' })
})
it('meters and encodes deep nesting iteratively without overflowing the stack', () => {
let deep: unknown = 0
for (let i = 0; i < 100_000; i++) deep = [deep]
// 100000 '[' + '0' + 100000 ']' = 200001 bytes.
expect(checkDoneValue(deep, 1_000_000)).toEqual({ ok: true, bytes: 200_001 })
// encodeJsonPlain's headline contract is the same stack-safety (JSON.stringify
// recurses per level and throws RangeError a few thousand deep), so exercise
// it on the same 100k-deep value — JSON.stringify would throw here.
expect(encodeJsonPlain(deep)).toBe(`${'['.repeat(100_000)}0${']'.repeat(100_000)}`)
})
it('emits exact digits for beyond-safe integral doubles', () => {
// String(2**60) prints the ROUNDED ...847000; echoing that to the child
// would change the integer. BigInt digits give the exact ...846976.
const v = JSON.parse('[1152921504606846976]') as unknown
expect(encodeJsonPlain(v)).toBe('[1152921504606846976]')
expect(checkDoneValue(v, 100)).toEqual({ ok: true, bytes: Buffer.byteLength('[1152921504606846976]', 'utf8') })
})
})
@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../runtime-diagnostics/invariants"
}
]
}
@@ -0,0 +1,16 @@
import { defineConfig } from 'tsdown'
/**
* Single ESM bundle. The Python-side code is not TypeScript and ships verbatim
* under `py/` (whitelisted in package.json `files`) — no build step needed.
*/
export default defineConfig({
entry: ['lib/types/index.js', 'lib/types/invariant.js'],
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
})
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-code-runtime-worker-thread",
"description": "Worker-thread implementation of the DeepSeek Harness code-execution seam",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-code-runtime",
"description": "Abstract code-execution seam (ctx.codeRuntime) for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-command-compact",
"description": "Human-facing slash command for explicit session compaction",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-compaction-basic",
"description": "Token-meter-driven compaction policy and LLM summarization backend for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-compaction-tool-result-pruner",
"description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-compaction",
"description": "Abstract compaction service seam (ctx.compaction) for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-instructions",
"description": "Workspace context loader for AGENTS.md/CLAUDE.md instruction files",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-session-reference",
"description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-time-context",
"description": "Opt-in durable per-step context with the current time and elapsed time",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tmux-context",
"description": "Opt-in durable per-step context with this agent's tmux pane and window location",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-default-model",
"description": "Default model selection shared by Agent entry points",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-loop",
"description": "The concrete agent loop plugin for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-tool-presentation",
"description": "Agent-plane presentation selector: composes one agent's tools as Code Mode, native, or both",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent",
"description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-scope",
"description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-session",
"description": "Event-sourced session store for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-system-prompt",
"description": "System prompt assembly registry for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-tools",
"description": "Tool registry and execution pipeline for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-credentials-local",
"description": "File-backed credentials provider ($DSH_HOME/.env under the live process environment) for the DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-credentials",
"description": "Abstract credential seam (ctx.credentials): settings carry references to secrets, providers own the values",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-e2b",
"description": "Shared E2B sandbox lifecycle for DeepSeek Harness provider adapters",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-fs-e2b",
"description": "E2B filesystem implementation for DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-subprocess-e2b",
"description": "E2B subprocess implementation for DeepSeek Harness",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-acp-demo",
"description": "ACP automation server app: agent spine + JSONL persistence + ACP transport, with a JSON-RPC stdio bin",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},
@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-agent-spine-demo",
"description": "The default executor-less/UI-less agent spine with fallback session titles, provider-routed retry, and optional persisted goals",
"version": "0.1.0-rc.6",
"version": "0.1.0-rc.7",
"publishConfig": {
"access": "public"
},

Some files were not shown because too many files have changed in this diff Show More