From 5d4cea9dc18ba1f2b8dde518b1e2b710f75b50da Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 31 Jul 2026 18:10:33 +0800 Subject: [PATCH 1/8] feat(code-runtime): own portable-identifier exclusions at the seam Move the reserved-word, reserved-global, reserved-error-member, and dunder exclusion sets from the worker backend up to the code-runtime seam package, and narrow the portable identifier subset to drop the JS-only `$`. Every backend now imports one contract so a binding namespace list valid on one backend is valid on all. Delivers only the seam extension and the worker's adoption; the Python backend, py-types renderer, and Code Mode language dispatch are later PRs in the stack that depend on these exports. --- ...runtime-portable-identifier-seam.i18n.yaml | 6 ++ ...1-code-runtime-portable-identifier-seam.md | 42 +++++++++++++ ...ode-runtime-portable-identifier-seam.zh.md | 42 +++++++++++++ .../code-runtime-worker/src/index.ts | 44 ++++++++----- .../code-runtime-worker/tests/runtime.spec.ts | 11 ++++ .../code-runtime/code-runtime/src/index.ts | 62 +++++++++++++++++++ .../code-runtime/code-runtime/src/types.ts | 17 ++++- .../code-runtime/tests/reserved.spec.ts | 51 +++++++++++++++ 8 files changed, 256 insertions(+), 19 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md create mode 100644 .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md create mode 100644 packages/code-runtime/code-runtime/tests/reserved.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml new file mode 100644 index 0000000000..73bf26efee --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml @@ -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-portable-identifier-seam.md +2026-07-31-code-runtime-portable-identifier-seam.md: a24a2c03c937a7d569528504b5bd5fba812009a2 +2026-07-31-code-runtime-portable-identifier-seam.zh.md: 1bef607e31a5820ba849f09eb7ae4cc781c45a84 diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md new file mode 100644 index 0000000000..a24a2c03c9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md @@ -0,0 +1,42 @@ +# Agent Note: the code-runtime seam owns portable-identifier exclusions + +Status: implemented + +English | [中文](2026-07-31-code-runtime-portable-identifier-seam.zh.md) + +## Problem + +The code-runtime seam promises that a binding-namespace list valid on one backend is valid on every backend, so a Code Mode consumer can hand the same bindings to any registered runtime without knowing its language. The first backend, `dsh-code-runtime-worker`, privately owned the identifier rules that enforce part of that promise: an `IDENTIFIER` regex that allowed the JS-only `$`, a `RESERVED_WORDS` set holding only ECMAScript keywords, and a `RESERVED_ERROR_PROPERTIES` set of three JS `Error` slots. Those rules described the worker's own language, not the seam's portability contract. + +A second backend written against a different language (CPython, arriving in a later PR of this stack) would either re-declare its own rules — letting `lambda` pass the worker and fail Python, or `$tools` pass the worker and fail every non-JS backend — or import the worker's, inverting the dependency so the seam's implementation reached into a sibling implementation. Neither keeps the portability promise real: it would hold only for the backend a caller happened to test against. + +## Decision + +The seam package (`@deepseek-ai/dsh-code-runtime`) exports the portable-identifier exclusion contract as four named constants, and every backend imports them rather than re-declaring: + +- `PORTABLE_RESERVED_WORDS` — the union of ECMAScript and Python reserved words. A namespace global or error-class name matching any is refused on all backends, so `lambda` is refused even though it is a legal JS parameter name. Adding a language widens this union, which is a deliberate breaking review of existing binding names. +- `RESERVED_BINDING_GLOBALS` — globals some backend owns in the program's namespace: `console` (the worker's log capture) and `__dsh_main__`/`__builtins__`/`__name__`/`__debug__` (the Python bootstrap's wrapper and seeded module globals). Refused everywhere so a namespace list cannot pick a name that works on one backend and collides on another. +- `RESERVED_ERROR_MEMBERS` — error-member names every backend refuses: the JS `Error` slots (`name`, `message`, `stack`) and Python's exception-protocol members (`args`, `with_traceback`, `add_note`). +- `DUNDER_MEMBER` — the dunder-form regex (`__*__`), refused as an error member wholesale because several are constrained CPython descriptors whose exact set is an interpreter-version detail. + +The seam also narrows the portable identifier subset to `[A-Za-z_][A-Za-z0-9_]*` (documented on `CodeBindingNamespace.global` and `CodeBindingErrorClass`), dropping the JS-only `$`. The worker consumes the shared constants: `RESERVED_WORDS = PORTABLE_RESERVED_WORDS`, `RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS`, its `IDENTIFIER` regex loses `$`, and its error-member check adds `DUNDER_MEMBER`. + +The constants live at the seam even though only one backend ships in this PR: the whole point is that the contract is language-agnostic and owned above any single language. A backend that violated it would be the bug, and the shared set is where a reviewer looks to see what "portable" means. + +## Scope + +This PR delivers only the seam extension and the worker's adoption of it. No Python backend, `py-types` renderer, or Code Mode language dispatch ships here — they are later PRs in the stack that depend on these exports. The seam README's worker-only wording is left unchanged for the same reason: linking to a `dsh-code-runtime-python` README that does not yet exist would break the dead-link gate. + +## Alternatives considered + +**Each backend declares its own exclusions.** Rejected: it makes the portability promise per-backend. A binding list the caller tested on the worker could be refused by Python, which is exactly the split the seam exists to prevent. + +**The Python backend imports the worker's constants.** Rejected: it inverts the dependency — the seam's implementations would reach into a sibling implementation for a contract neither owns. The contract belongs above both, at the seam. + +**Keep `$` in the portable identifier subset.** Rejected: `$` is JS-only spelling. Allowing it would let `$tools` pass the worker and fail every non-JS backend, breaking portability for a purely cosmetic gain. + +## Consequences + +Bought: one place — the seam package — defines what a portable binding name is, and every backend enforces the same contract by import. A namespace list valid on one backend is valid on all, verifiably, not by coincidence of which backend the caller tested. + +Cost: existing worker callers using a `$`-containing global now fail identifier validation. Under the pre-release stance this is a corrected foundation, not a compatibility break to shim. The worker's seam-misuse tests gain cases for `$tools`, Python exception members (`args`), dunders (`__dict__`), and a Python-owned global (`__dsh_main__`), proving the shared set is enforced from the worker side. diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md new file mode 100644 index 0000000000..1bef607e31 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md @@ -0,0 +1,42 @@ +# Agent Note:code-runtime seam 拥有可移植标识符排除集 + +Status: implemented + +[English](2026-07-31-code-runtime-portable-identifier-seam.md) | 中文 + +## Problem + +code-runtime seam 承诺:在一个后端上有效的绑定命名空间列表,在每个后端上都有效,因此 Code Mode 消费方可以把同一组绑定交给任何已注册的运行时,而不必知道它的语言。首个后端 `dsh-code-runtime-worker` 私自拥有了执行这项承诺一部分的标识符规则:一个允许 JS 专有 `$` 的 `IDENTIFIER` 正则、一个只含 ECMAScript 关键字的 `RESERVED_WORDS` 集合,以及一个含三个 JS `Error` 槽位的 `RESERVED_ERROR_PROPERTIES` 集合。这些规则描述的是 worker 自身的语言,而非 seam 的可移植性契约。 + +一个针对不同语言(CPython,将在本 stack 后续 PR 中到来)编写的第二后端,要么重新声明自己的规则——让 `lambda` 通过 worker 却在 Python 上失败,或让 `$tools` 通过 worker 却在每个非 JS 后端上失败——要么导入 worker 的规则,从而反转依赖,使 seam 的一个实现伸手进入另一个兄弟实现。二者都无法让可移植承诺成真:它只对调用方恰好测试过的那个后端成立。 + +## Decision + +seam 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量导出可移植标识符排除契约,每个后端导入它们而非重新声明: + +- `PORTABLE_RESERVED_WORDS`——ECMAScript 与 Python 保留字的联集。任何命名空间 global 或 error-class 名称匹配其中之一,都在所有后端上被拒绝,因此 `lambda` 即便是合法的 JS 参数名也被拒绝。新增一门语言即扩宽此联集,这是对现有绑定名称的一次有意的破坏性复审。 +- `RESERVED_BINDING_GLOBALS`——某个后端在程序命名空间中拥有的 global:`console`(worker 的日志捕获)与 `__dsh_main__`/`__builtins__`/`__name__`/`__debug__`(Python bootstrap 的包装器与预置模块 global)。在所有后端上被拒绝,使命名空间列表无法选到一个在某后端能用、在另一后端冲突的名称。 +- `RESERVED_ERROR_MEMBERS`——每个后端都拒绝的 error-member 名称:JS `Error` 槽位(`name`、`message`、`stack`)与 Python 异常协议成员(`args`、`with_traceback`、`add_note`)。 +- `DUNDER_MEMBER`——dunder 形式正则(`__*__`),作为 error member 被整体拒绝,因为其中若干是受约束的 CPython 描述符,其确切集合是解释器版本细节。 + +seam 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记录在 `CodeBindingNamespace.global` 与 `CodeBindingErrorClass` 上),去掉 JS 专有的 `$`。worker 消费这些共享常量:`RESERVED_WORDS = PORTABLE_RESERVED_WORDS`、`RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS`,其 `IDENTIFIER` 正则去掉 `$`,其 error-member 检查加上 `DUNDER_MEMBER`。 + +尽管本 PR 只交付一个后端,这些常量仍置于 seam:要点正是该契约与语言无关,且拥有权在任何单一语言之上。违反它的后端才是 bug,而共享集合正是复审者查看"可移植"含义的地方。 + +## Scope + +本 PR 只交付 seam 扩展与 worker 对它的采用。这里不交付任何 Python 后端、`py-types` 渲染器或 Code Mode 的语言分发——它们是本 stack 中依赖这些导出的后续 PR。seam README 中仅描述 worker 的措辞保持不变,理由相同:链接到一个尚不存在的 `dsh-code-runtime-python` README 会破坏死链 gate。 + +## Alternatives considered + +**每个后端声明自己的排除集。** 拒绝:这让可移植承诺变成逐后端成立。调用方在 worker 上测过的绑定列表可能被 Python 拒绝,而这正是 seam 存在要防止的分裂。 + +**Python 后端导入 worker 的常量。** 拒绝:这反转依赖——seam 的实现会为一个二者都不拥有的契约伸手进入兄弟实现。契约属于二者之上,即 seam。 + +**在可移植标识符子集中保留 `$`。** 拒绝:`$` 是 JS 专有拼写。允许它会让 `$tools` 通过 worker 却在每个非 JS 后端上失败,为纯粹表面的好处破坏可移植性。 + +## Consequences + +获得:一个地方——seam 包——定义什么是可移植绑定名称,每个后端通过导入执行同一契约。在一个后端上有效的命名空间列表在所有后端上都有效,这是可验证的,而非取决于调用方测试了哪个后端的巧合。 + +代价:现有使用含 `$` global 的 worker 调用方现在会在标识符校验时失败。在预发布立场下这是一次被纠正的地基,而非需要 shim 的兼容性破坏。worker 的 seam-misuse 测试新增了 `$tools`、Python 异常成员(`args`)、dunder(`__dict__`)与一个 Python 拥有的 global(`__dsh_main__`)等用例,从 worker 侧证明共享集合被执行。 diff --git a/packages/code-runtime/code-runtime-worker/src/index.ts b/packages/code-runtime/code-runtime-worker/src/index.ts index be156c85ba..3c198ca94e 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url' import { Context } from 'cordis' import z from 'schemastery' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' -import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' +import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from '@deepseek-ai/dsh-code-runtime' import type { CodeBindingNamespace, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' import { snapshotJsonValue } from '@deepseek-ai/dsh-session' import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts' @@ -65,20 +65,27 @@ const ELU_POLL_INTERVAL_MS = 25 /** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */ const MIN_OUTPUT_BYTES = 4 -/** ECMAScript reserved words that cannot be async-function parameter names — rejected as binding globals. */ -const RESERVED_WORDS = new Set([ - 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', - 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', - 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', - 'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package', - 'private', 'protected', 'public', 'arguments', 'eval', -]) +/** + * The seam's cross-language reserved-word union: the portable-identifier + * contract promises a namespace list valid here is valid on every backend, so + * a Python keyword like `lambda` is refused even though it is a legal JS + * parameter name. + */ +const RESERVED_WORDS = PORTABLE_RESERVED_WORDS -/** Valid async-function parameter name (the binding global becomes one). */ -const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/ +/** + * The seam's language-portable identifier subset (see + * `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same + * namespace list must be usable against every backend regardless of language. + */ +const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/ -/** Error properties whose binding-member replacement would destroy the promised Error contract. */ -const RESERVED_ERROR_PROPERTIES = new Set(['name', 'message', 'stack']) +/** + * The seam's shared error-member exclusions (plus the dunder rule below): + * enforced identically here and in the Python backend so an errorClass valid + * on one backend is valid on all. + */ +const RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS /** * The shell a program is wrapped in for the type-strip, matching the @@ -331,7 +338,11 @@ export class WorkerCodeRuntime extends CodeRuntime { if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`) } - if (namespace.global === 'console' || bindings.has(namespace.global)) { + // RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set: + // `console` is THIS backend's log-capture slot; the dunder entries are + // the Python bootstrap's — refused here too so the namespace list stays + // portable across backends. + if (RESERVED_BINDING_GLOBALS.has(namespace.global) || bindings.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: duplicate binding global ${JSON.stringify(namespace.global)}`) } bindings.set(namespace.global, namespace) @@ -344,10 +355,11 @@ export class WorkerCodeRuntime extends CodeRuntime { if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) { throw new Error(`dsh-code-runtime-worker: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`) } - if (descriptor.name === 'console' || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) { + if (RESERVED_BINDING_GLOBALS.has(descriptor.name) || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) { throw new Error(`dsh-code-runtime-worker: duplicate injected global ${JSON.stringify(descriptor.name)}`) } - if (descriptor.memberNameProperty.length === 0 || RESERVED_ERROR_PROPERTIES.has(descriptor.memberNameProperty)) { + const member = descriptor.memberNameProperty + if (member.length === 0 || RESERVED_ERROR_PROPERTIES.has(member) || DUNDER_MEMBER.test(member)) { throw new Error(`dsh-code-runtime-worker: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`) } errorClassNames.add(descriptor.name) diff --git a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts index b16e2b5671..ce97faadb3 100644 --- a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts @@ -787,6 +787,9 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { const cases: [string, RegExp][] = [ ['not valid!', /not a usable identifier/], ['await', /not a usable identifier/], + // `$tools` is legal JS but outside the seam's language-portable subset: + // the same namespace list must work against every backend's language. + ['$tools', /not a usable identifier/], ['console', /duplicate binding global/], ] for (const [global, message] of cases) { @@ -822,6 +825,14 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { ])).rejects.toThrow(/duplicate injected global/) await expect(run([namespace('tools', 'CallError', '')])).rejects.toThrow(/member property.*not usable/) await expect(run([namespace('tools', 'CallError', 'message')])).rejects.toThrow(/member property.*not usable/) + // The shared exclusion set covers Python's exception-protocol members and + // dunders too, so the same errorClass is valid (or not) on every backend. + await expect(run([namespace('tools', 'CallError', 'args')])).rejects.toThrow(/member property.*not usable/) + await expect(run([namespace('tools', 'CallError', '__dict__')])).rejects.toThrow(/member property.*not usable/) + // The Python bootstrap's owned globals are refused here too (shared + // RESERVED_BINDING_GLOBALS), keeping namespace lists backend-portable. + await expect(runtime.run({ program: 'return 1', bindings: [{ global: '__dsh_main__', functions: {} }] })) + .rejects.toThrow(/duplicate binding global/) }) it('rejects config values that are not positive numbers', async () => { diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index bd52b9ed29..681c0465a7 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -17,6 +17,68 @@ export type { CodeRunResult, } from './types.ts' +/** + * Binding globals EVERY backend refuses because SOME backend owns the slot in + * the program's namespace: `console` (the worker's log capture), and + * `__dsh_main__`/`__builtins__`/`__name__` (the Python bootstrap's wrapper + * and seeded module globals), and `__debug__`. One shared set — rather than each backend + * refusing only its own slots — keeps the portability promise real: a + * namespace list valid on one backend is valid on all, so a caller cannot + * pick a name that works on the worker and collides on Python (or vice + * versa). Dunder-form names are additionally covered by the identifier rule + * on `CodeBindingNamespace.global` only when they fail it; `__name__` et al. + * ARE valid identifiers, hence this explicit set. `__debug__` is listed for a + * different reason than a collision: CPython compiles a bare `__debug__` + * reference to the constant `True` and rejects any assignment to the name at + * COMPILE time, so an injected global under that name is unreachable from the + * program — accepted by validation, unusable on the Python backend, which is + * exactly the split the shared set exists to prevent. + */ +export const RESERVED_BINDING_GLOBALS: ReadonlySet = new Set([ + 'console', + '__dsh_main__', '__builtins__', '__name__', '__debug__', +]) + +/** + * `CodeBindingErrorClass.memberNameProperty` names EVERY backend refuses, as + * one shared contract so a request valid on one backend is valid on all. The + * JS `Error` exclusions (`name`, `message`, `stack`) and Python's + * exception-protocol members (`args`, `with_traceback`, `add_note`) are + * listed by name; dunder-form names (`__*__`) are refused wholesale — several + * are constrained CPython descriptors whose `setattr` raises while + * constructing the rejection, and the exact set is an interpreter version + * detail. Any other non-empty own property name is accepted everywhere. + */ +export const RESERVED_ERROR_MEMBERS: ReadonlySet = new Set([ + 'name', 'message', 'stack', + 'args', 'with_traceback', 'add_note', +]) + +/** Dunder form (`__*__`): object-protocol slots in Python, refused as {@link RESERVED_ERROR_MEMBERS | error members} on every backend. */ +export const DUNDER_MEMBER = /^__.*__$/ + +/** + * Reserved words of EVERY shipped backend language (ECMAScript ∪ Python), + * refused as {@link CodeBindingNamespace.global} / error-class names by all + * backends. The portable-identifier contract promises a namespace list valid + * on one backend is valid on every backend; a per-language check would let + * `lambda` pass the TypeScript backend and fail the Python one. Extending the + * seam with a new language means widening this union (a breaking review of + * existing binding names, by design). + */ +export const PORTABLE_RESERVED_WORDS: ReadonlySet = new Set([ + // ECMAScript reserved words and reserved-in-strict-mode names. + 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', + 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', + 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', + 'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package', + 'private', 'protected', 'public', 'arguments', 'eval', + // Python 3.x keywords and soft keywords not already above ('type' and '_' + // are soft keywords: legal names in practice, reserved here for safety). + 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'def', 'del', 'elif', 'except', 'from', + 'global', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'match', 'type', '_', +]) + declare module 'cordis' { interface Context { codeRuntime: CodeRuntime diff --git a/packages/code-runtime/code-runtime/src/types.ts b/packages/code-runtime/code-runtime/src/types.ts index a53353799b..e96d8bc959 100644 --- a/packages/code-runtime/code-runtime/src/types.ts +++ b/packages/code-runtime/code-runtime/src/types.ts @@ -28,9 +28,14 @@ export type CodeJsonValue = null | boolean | number | string | CodeJsonValue[] | * of a particular consumer such as Code Mode. */ export interface CodeBindingErrorClass { - /** Constructor global and resulting `Error.name` (must be a usable JS identifier). */ + /** Constructor global and resulting `Error.name`; same portable identifier rule as {@link CodeBindingNamespace.global}. */ name: string - /** Non-empty own property for the member name; cannot replace `name`, `message`, or `stack`. */ + /** + * Non-empty own property for the member name. The portable exclusion set is + * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced + * identically by every backend; any other name — identifiers or not — is + * accepted everywhere. + */ memberNameProperty: string } @@ -42,7 +47,13 @@ export interface CodeBindingErrorClass { * collisions. */ export interface CodeBindingNamespace { - /** The global identifier the program sees (must be a valid JS identifier). */ + /** + * The global identifier the program sees. Must match the LANGUAGE-PORTABLE + * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved + * words, so the same namespace list works against every backend regardless + * of `language` — a JS-only spelling like `$tools` is rejected by design, + * not just by the Python backend. + */ global: string /** The callable members, keyed by the exact name the program calls. */ functions: Record diff --git a/packages/code-runtime/code-runtime/tests/reserved.spec.ts b/packages/code-runtime/code-runtime/tests/reserved.spec.ts new file mode 100644 index 0000000000..31868252a1 --- /dev/null +++ b/packages/code-runtime/code-runtime/tests/reserved.spec.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import { + DUNDER_MEMBER, + PORTABLE_RESERVED_WORDS, + RESERVED_BINDING_GLOBALS, + RESERVED_ERROR_MEMBERS, +} from '@deepseek-ai/dsh-code-runtime' + +/** + * The seam owns the portable-identifier exclusion sets so every backend + * enforces one contract: a namespace list valid on one backend is valid on + * all. These assertions pin the shared membership backends import rather than + * re-declare. + */ +describe('seam-owned portable identifier exclusions', () => { + it('RESERVED_BINDING_GLOBALS covers each backend-owned slot', () => { + expect(RESERVED_BINDING_GLOBALS.has('console')).toBe(true) + expect(RESERVED_BINDING_GLOBALS.has('__dsh_main__')).toBe(true) + expect(RESERVED_BINDING_GLOBALS.has('__builtins__')).toBe(true) + expect(RESERVED_BINDING_GLOBALS.has('__name__')).toBe(true) + expect(RESERVED_BINDING_GLOBALS.has('__debug__')).toBe(true) + expect(RESERVED_BINDING_GLOBALS.has('tools')).toBe(false) + }) + + it('RESERVED_ERROR_MEMBERS covers the JS Error and Python exception-protocol members', () => { + for (const name of ['name', 'message', 'stack', 'args', 'with_traceback', 'add_note']) { + expect(RESERVED_ERROR_MEMBERS.has(name)).toBe(true) + } + expect(RESERVED_ERROR_MEMBERS.has('code')).toBe(false) + }) + + it('DUNDER_MEMBER matches dunder-form names only', () => { + expect(DUNDER_MEMBER.test('__dict__')).toBe(true) + expect(DUNDER_MEMBER.test('__init__')).toBe(true) + expect(DUNDER_MEMBER.test('_private')).toBe(false) + expect(DUNDER_MEMBER.test('name')).toBe(false) + expect(DUNDER_MEMBER.test('__mid')).toBe(false) + }) + + it('PORTABLE_RESERVED_WORDS is the union of ECMAScript and Python reserved words', () => { + // ECMAScript-only keyword. + expect(PORTABLE_RESERVED_WORDS.has('function')).toBe(true) + // Python-only keyword — refused here so the list stays portable. + expect(PORTABLE_RESERVED_WORDS.has('lambda')).toBe(true) + expect(PORTABLE_RESERVED_WORDS.has('nonlocal')).toBe(true) + // Shared keyword. + expect(PORTABLE_RESERVED_WORDS.has('class')).toBe(true) + // Ordinary identifier is not reserved. + expect(PORTABLE_RESERVED_WORDS.has('tools')).toBe(false) + }) +}) From b5578c026e2c00b2efc922dddfe16c61ffadf578 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 31 Jul 2026 18:17:51 +0800 Subject: [PATCH 2/8] docs(code-runtime): align derived docs with the portable-identifier JSDoc Update the type-equiv blocks in docs/core-data-structures/code-runtime (both languages, re-recorded) to mirror the new CodeBindingNamespace / CodeBindingErrorClass JSDoc, and regenerate the cordis services catalog for the shifted source line. Keeps the worker-only intro until the Python backend package exists. --- docs/cordis-catalog/services.md | 2 +- .../core-data-structures/code-runtime.i18n.yaml | 4 ++-- docs/core-data-structures/code-runtime.md | 17 ++++++++++++++--- docs/core-data-structures/code-runtime.zh.md | 17 ++++++++++++++--- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 2ded3e4f07..07ae7bf044 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -398,7 +398,7 @@ abstract run(request: CodeRunRequest): Promise Types: [CodeRunRequest](../core-data-structures/code-runtime.md) · [CodeRunResult](../core-data-structures/code-runtime.md) -Source: [`packages/code-runtime/code-runtime/src/index.ts:33`](../../packages/code-runtime/code-runtime/src/index.ts) +Source: [`packages/code-runtime/code-runtime/src/index.ts:95`](../../packages/code-runtime/code-runtime/src/index.ts) ## `ctx.commands` — `CommandService` diff --git a/docs/core-data-structures/code-runtime.i18n.yaml b/docs/core-data-structures/code-runtime.i18n.yaml index fbdee4c938..ac7dae06af 100644 --- a/docs/core-data-structures/code-runtime.i18n.yaml +++ b/docs/core-data-structures/code-runtime.i18n.yaml @@ -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/core-data-structures/code-runtime.md -code-runtime.md: 64de3c45d4f1d1d981daa6c6f074abb667e0aa52 -code-runtime.zh.md: daf07aaf613852a6c4a7b1aff152fcc61052fbca +code-runtime.md: 522fe29deae8cac01462a336a02d082502b61fb4 +code-runtime.zh.md: 6d16f8ed2ab3b99620f30a8e81dc3718cf1419d1 diff --git a/docs/core-data-structures/code-runtime.md b/docs/core-data-structures/code-runtime.md index 64de3c45d4..522fe29dea 100644 --- a/docs/core-data-structures/code-runtime.md +++ b/docs/core-data-structures/code-runtime.md @@ -72,9 +72,14 @@ Each `CodeBindingNamespace` becomes one global object of async callables inside * of a particular consumer such as Code Mode. */ interface CodeBindingErrorClass { - /** Constructor global and resulting `Error.name` (must be a usable JS identifier). */ + /** Constructor global and resulting `Error.name`; same portable identifier rule as {@link CodeBindingNamespace.global}. */ name: string - /** Non-empty own property for the member name; cannot replace `name`, `message`, or `stack`. */ + /** + * Non-empty own property for the member name. The portable exclusion set is + * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced + * identically by every backend; any other name — identifiers or not — is + * accepted everywhere. + */ memberNameProperty: string } ``` @@ -88,7 +93,13 @@ interface CodeBindingErrorClass { * collisions. */ interface CodeBindingNamespace { - /** The global identifier the program sees (must be a valid JS identifier). */ + /** + * The global identifier the program sees. Must match the LANGUAGE-PORTABLE + * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved + * words, so the same namespace list works against every backend regardless + * of `language` — a JS-only spelling like `$tools` is rejected by design, + * not just by the Python backend. + */ global: string /** The callable members, keyed by the exact name the program calls. */ functions: Record diff --git a/docs/core-data-structures/code-runtime.zh.md b/docs/core-data-structures/code-runtime.zh.md index daf07aaf61..6d16f8ed2a 100644 --- a/docs/core-data-structures/code-runtime.zh.md +++ b/docs/core-data-structures/code-runtime.zh.md @@ -72,9 +72,14 @@ interface CodeRunResult { * of a particular consumer such as Code Mode. */ interface CodeBindingErrorClass { - /** Constructor global and resulting `Error.name` (must be a usable JS identifier). */ + /** Constructor global and resulting `Error.name`; same portable identifier rule as {@link CodeBindingNamespace.global}. */ name: string - /** Non-empty own property for the member name; cannot replace `name`, `message`, or `stack`. */ + /** + * Non-empty own property for the member name. The portable exclusion set is + * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced + * identically by every backend; any other name — identifiers or not — is + * accepted everywhere. + */ memberNameProperty: string } ``` @@ -88,7 +93,13 @@ interface CodeBindingErrorClass { * collisions. */ interface CodeBindingNamespace { - /** The global identifier the program sees (must be a valid JS identifier). */ + /** + * The global identifier the program sees. Must match the LANGUAGE-PORTABLE + * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved + * words, so the same namespace list works against every backend regardless + * of `language` — a JS-only spelling like `$tools` is rejected by design, + * not just by the Python backend. + */ global: string /** The callable members, keyed by the exact name the program calls. */ functions: Record From eb03aa86fec91dd92cb303834596141c0ad1f566 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 31 Jul 2026 18:30:32 +0800 Subject: [PATCH 3/8] =?UTF-8?q?refactor(code-runtime):=20address=20seam=20?= =?UTF-8?q?review=20=E2=80=94=20drop=20worker=20aliases,=20tighten=20dunde?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Worker consumes PORTABLE_RESERVED_WORDS / RESERVED_ERROR_MEMBERS by their seam names directly, dropping the local re-alias (symmetry with the other two imported constants). - Split the reserved-vs-duplicate diagnostics: a backend-owned global now reports "reserved binding global", not the misleading "duplicate". - DUNDER_MEMBER uses `__.+__` so a bare `__` (empty middle, not a real CPython dunder) is not matched; add coverage. - Worker misuse tests add `a$b` (second-char `$`) and `lambda` (Python keyword) so the identifier narrowing and reserved-word adoption are each pinned directly, not only transitively. - Clarify the seam JSDoc (dunder-vs-explicit-set wording, Python backend is a later stack PR) and record in the Agent Note the obligation to widen RESERVED_BINDING_GLOBALS when the bootstrap seeds more globals. --- ...runtime-portable-identifier-seam.i18n.yaml | 4 +- ...1-code-runtime-portable-identifier-seam.md | 2 + ...ode-runtime-portable-identifier-seam.zh.md | 2 + .../code-runtime-worker/src/index.ts | 33 ++++++----------- .../code-runtime-worker/tests/runtime.spec.ts | 15 ++++++-- .../code-runtime/code-runtime/src/index.ts | 37 +++++++++++-------- .../code-runtime/tests/reserved.spec.ts | 3 ++ 7 files changed, 54 insertions(+), 42 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml index 73bf26efee..a2a0848eab 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md -2026-07-31-code-runtime-portable-identifier-seam.md: a24a2c03c937a7d569528504b5bd5fba812009a2 -2026-07-31-code-runtime-portable-identifier-seam.zh.md: 1bef607e31a5820ba849f09eb7ae4cc781c45a84 +2026-07-31-code-runtime-portable-identifier-seam.md: bd1876fc459dc85bb5d9f42ac04f4cc801d6398a +2026-07-31-code-runtime-portable-identifier-seam.zh.md: 2fd5e97c562d1e51cbad906cd37998b8ffccc58a diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md index a24a2c03c9..bd1876fc45 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md @@ -27,6 +27,8 @@ The constants live at the seam even though only one backend ships in this PR: th This PR delivers only the seam extension and the worker's adoption of it. No Python backend, `py-types` renderer, or Code Mode language dispatch ships here — they are later PRs in the stack that depend on these exports. The seam README's worker-only wording is left unchanged for the same reason: linking to a `dsh-code-runtime-python` README that does not yet exist would break the dead-link gate. +`RESERVED_BINDING_GLOBALS` currently encodes the not-yet-merged Python bootstrap's concrete design: it seeds exactly `__builtins__`/`__name__` and wraps the program under `__dsh_main__`. The Python-backend PR that seeds any additional module global (`__doc__`, `__loader__`, `__spec__`, `__file__`, `__package__`, …) MUST widen this set in the same change, exactly as adding a language widens `PORTABLE_RESERVED_WORDS` — a name the bootstrap seeds but the set omits is the portability split this contract exists to prevent. + ## Alternatives considered **Each backend declares its own exclusions.** Rejected: it makes the portability promise per-backend. A binding list the caller tested on the worker could be refused by Python, which is exactly the split the seam exists to prevent. diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md index 1bef607e31..2fd5e97c56 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md @@ -27,6 +27,8 @@ seam 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记 本 PR 只交付 seam 扩展与 worker 对它的采用。这里不交付任何 Python 后端、`py-types` 渲染器或 Code Mode 的语言分发——它们是本 stack 中依赖这些导出的后续 PR。seam README 中仅描述 worker 的措辞保持不变,理由相同:链接到一个尚不存在的 `dsh-code-runtime-python` README 会破坏死链 gate。 +`RESERVED_BINDING_GLOBALS` 当前编码了尚未合并的 Python bootstrap 的具体设计:它恰好 seed `__builtins__`/`__name__`,并把程序包装在 `__dsh_main__` 之下。任何 seed 额外模块 global(`__doc__`、`__loader__`、`__spec__`、`__file__`、`__package__` 等)的 Python 后端 PR 必须在同一改动中扩宽此集合,正如新增一门语言即扩宽 `PORTABLE_RESERVED_WORDS`——bootstrap 会 seed 却不在集合中的名称,正是本契约要防止的可移植性分裂。 + ## Alternatives considered **每个后端声明自己的排除集。** 拒绝:这让可移植承诺变成逐后端成立。调用方在 worker 上测过的绑定列表可能被 Python 拒绝,而这正是 seam 存在要防止的分裂。 diff --git a/packages/code-runtime/code-runtime-worker/src/index.ts b/packages/code-runtime/code-runtime-worker/src/index.ts index 3c198ca94e..9773d3c341 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -65,14 +65,6 @@ const ELU_POLL_INTERVAL_MS = 25 /** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */ const MIN_OUTPUT_BYTES = 4 -/** - * The seam's cross-language reserved-word union: the portable-identifier - * contract promises a namespace list valid here is valid on every backend, so - * a Python keyword like `lambda` is refused even though it is a legal JS - * parameter name. - */ -const RESERVED_WORDS = PORTABLE_RESERVED_WORDS - /** * The seam's language-portable identifier subset (see * `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same @@ -80,13 +72,6 @@ const RESERVED_WORDS = PORTABLE_RESERVED_WORDS */ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/ -/** - * The seam's shared error-member exclusions (plus the dunder rule below): - * enforced identically here and in the Python backend so an errorClass valid - * on one backend is valid on all. - */ -const RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS - /** * The shell a program is wrapped in for the type-strip, matching the * grammatical context it will execute in (an async function body, where @@ -335,14 +320,17 @@ export class WorkerCodeRuntime extends CodeRuntime { private validateBindings(request: CodeRunRequest): Map { const bindings = new Map() for (const namespace of request.bindings) { - if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) { + if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`) } // RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set: // `console` is THIS backend's log-capture slot; the dunder entries are - // the Python bootstrap's — refused here too so the namespace list stays + // the Python backend's — refused here too so the namespace list stays // portable across backends. - if (RESERVED_BINDING_GLOBALS.has(namespace.global) || bindings.has(namespace.global)) { + if (RESERVED_BINDING_GLOBALS.has(namespace.global)) { + throw new Error(`dsh-code-runtime-worker: reserved binding global ${JSON.stringify(namespace.global)}`) + } + if (bindings.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: duplicate binding global ${JSON.stringify(namespace.global)}`) } bindings.set(namespace.global, namespace) @@ -352,14 +340,17 @@ export class WorkerCodeRuntime extends CodeRuntime { for (const namespace of request.bindings) { const descriptor = namespace.errorClass if (!descriptor) continue - if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) { + if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) { throw new Error(`dsh-code-runtime-worker: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`) } - if (RESERVED_BINDING_GLOBALS.has(descriptor.name) || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) { + if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) { + throw new Error(`dsh-code-runtime-worker: reserved binding global ${JSON.stringify(descriptor.name)}`) + } + if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) { throw new Error(`dsh-code-runtime-worker: duplicate injected global ${JSON.stringify(descriptor.name)}`) } const member = descriptor.memberNameProperty - if (member.length === 0 || RESERVED_ERROR_PROPERTIES.has(member) || DUNDER_MEMBER.test(member)) { + if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) { throw new Error(`dsh-code-runtime-worker: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`) } errorClassNames.add(descriptor.name) diff --git a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts index ce97faadb3..54f58eb414 100644 --- a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts @@ -790,7 +790,14 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { // `$tools` is legal JS but outside the seam's language-portable subset: // the same namespace list must work against every backend's language. ['$tools', /not a usable identifier/], - ['console', /duplicate binding global/], + // `a$b` pins the second character class too: the old identifier regex + // `[A-Za-z0-9_$]*` would have accepted a `$` after the first character. + ['a$b', /not a usable identifier/], + // `lambda` is a Python keyword, refused here directly (not just + // transitively) so the worker's adoption of PORTABLE_RESERVED_WORDS is + // its own regression, symmetric with the `$tools` case. + ['lambda', /not a usable identifier/], + ['console', /reserved binding global/], ] for (const [global, message] of cases) { await expect(runtime.run({ program: 'return 1', bindings: [{ global, functions: {} }] })).rejects.toThrow(message) @@ -817,7 +824,7 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { await expect(run([namespace('tools', 'not valid!')])).rejects.toThrow(/error class.*not a usable identifier/) await expect(run([namespace('tools', 'await')])).rejects.toThrow(/error class.*not a usable identifier/) - await expect(run([namespace('tools', 'console')])).rejects.toThrow(/duplicate injected global/) + await expect(run([namespace('tools', 'console')])).rejects.toThrow(/reserved binding global/) await expect(run([namespace('tools', 'tools')])).rejects.toThrow(/duplicate injected global/) await expect(run([ namespace('tools', 'CallError'), @@ -829,10 +836,10 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { // dunders too, so the same errorClass is valid (or not) on every backend. await expect(run([namespace('tools', 'CallError', 'args')])).rejects.toThrow(/member property.*not usable/) await expect(run([namespace('tools', 'CallError', '__dict__')])).rejects.toThrow(/member property.*not usable/) - // The Python bootstrap's owned globals are refused here too (shared + // The Python backend's owned globals are refused here too (shared // RESERVED_BINDING_GLOBALS), keeping namespace lists backend-portable. await expect(runtime.run({ program: 'return 1', bindings: [{ global: '__dsh_main__', functions: {} }] })) - .rejects.toThrow(/duplicate binding global/) + .rejects.toThrow(/reserved binding global/) }) it('rejects config values that are not positive numbers', async () => { diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index 681c0465a7..2e4f18a2e2 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -20,19 +20,23 @@ export type { /** * Binding globals EVERY backend refuses because SOME backend owns the slot in * the program's namespace: `console` (the worker's log capture), and - * `__dsh_main__`/`__builtins__`/`__name__` (the Python bootstrap's wrapper - * and seeded module globals), and `__debug__`. One shared set — rather than each backend - * refusing only its own slots — keeps the portability promise real: a - * namespace list valid on one backend is valid on all, so a caller cannot - * pick a name that works on the worker and collides on Python (or vice - * versa). Dunder-form names are additionally covered by the identifier rule - * on `CodeBindingNamespace.global` only when they fail it; `__name__` et al. - * ARE valid identifiers, hence this explicit set. `__debug__` is listed for a - * different reason than a collision: CPython compiles a bare `__debug__` - * reference to the constant `True` and rejects any assignment to the name at - * COMPILE time, so an injected global under that name is unreachable from the - * program — accepted by validation, unusable on the Python backend, which is - * exactly the split the shared set exists to prevent. + * `__dsh_main__`/`__builtins__`/`__name__` (the Python backend's bootstrap + * wrapper and seeded module globals — that backend is a later PR in this + * stack, see the [portable-identifier Agent + * Note](../../../../.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md)), + * and `__debug__`. One shared set — rather than each backend refusing only its + * own slots — keeps the portability promise real: a namespace list valid on + * one backend is valid on all, so a caller cannot pick a name that works on + * the worker and collides on Python (or vice versa). `__name__` et al. ARE + * valid portable identifiers, so the identifier rule on + * `CodeBindingNamespace.global` never rejects them — hence this explicit set. + * (Error members differ: {@link DUNDER_MEMBER} refuses every dunder form + * wholesale; binding globals refuse only the names listed here.) `__debug__` + * is listed for a different reason than a collision: CPython compiles a bare + * `__debug__` reference to the constant `True` and rejects any assignment to + * the name at COMPILE time, so an injected global under that name is + * unreachable from the program — accepted by validation, unusable on the + * Python backend, which is exactly the split the shared set exists to prevent. */ export const RESERVED_BINDING_GLOBALS: ReadonlySet = new Set([ 'console', @@ -54,8 +58,11 @@ export const RESERVED_ERROR_MEMBERS: ReadonlySet = new Set([ 'args', 'with_traceback', 'add_note', ]) -/** Dunder form (`__*__`): object-protocol slots in Python, refused as {@link RESERVED_ERROR_MEMBERS | error members} on every backend. */ -export const DUNDER_MEMBER = /^__.*__$/ +/** + * Dunder form (`__x__`, non-empty middle): object-protocol slots in Python, + * refused as {@link RESERVED_ERROR_MEMBERS | error members} on every backend. + */ +export const DUNDER_MEMBER = /^__.+__$/ /** * Reserved words of EVERY shipped backend language (ECMAScript ∪ Python), diff --git a/packages/code-runtime/code-runtime/tests/reserved.spec.ts b/packages/code-runtime/code-runtime/tests/reserved.spec.ts index 31868252a1..ccd2c89211 100644 --- a/packages/code-runtime/code-runtime/tests/reserved.spec.ts +++ b/packages/code-runtime/code-runtime/tests/reserved.spec.ts @@ -35,6 +35,9 @@ describe('seam-owned portable identifier exclusions', () => { expect(DUNDER_MEMBER.test('_private')).toBe(false) expect(DUNDER_MEMBER.test('name')).toBe(false) expect(DUNDER_MEMBER.test('__mid')).toBe(false) + // `__` has an empty middle — not a real CPython dunder, so not matched. + expect(DUNDER_MEMBER.test('__')).toBe(false) + expect(DUNDER_MEMBER.test('____')).toBe(true) }) it('PORTABLE_RESERVED_WORDS is the union of ECMAScript and Python reserved words', () => { From 0e6fe760c1407ca19fb8b5c8d4e8d2346b5bf383 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 31 Jul 2026 21:54:57 +0800 Subject: [PATCH 4/8] docs(code-runtime): regenerate cordis services catalog for shifted source line --- docs/cordis-catalog/services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 07ae7bf044..6c670942b2 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -398,7 +398,7 @@ abstract run(request: CodeRunRequest): Promise Types: [CodeRunRequest](../core-data-structures/code-runtime.md) · [CodeRunResult](../core-data-structures/code-runtime.md) -Source: [`packages/code-runtime/code-runtime/src/index.ts:95`](../../packages/code-runtime/code-runtime/src/index.ts) +Source: [`packages/code-runtime/code-runtime/src/index.ts:102`](../../packages/code-runtime/code-runtime/src/index.ts) ## `ctx.commands` — `CommandService` From 4dc2b197d737a5468a40cb62c5f379d2fbf0762a Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sat, 1 Aug 2026 01:55:10 +0800 Subject: [PATCH 5/8] test(code-runtime): correct DUNDER_MEMBER edge cases for the __.+__ pattern `____` has an empty middle between the two `__` pairs and does not match `/^__.+__$/`; assert that (not a match) and add `__x__` as the shortest real dunder form that does. --- packages/code-runtime/code-runtime/tests/reserved.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/code-runtime/code-runtime/tests/reserved.spec.ts b/packages/code-runtime/code-runtime/tests/reserved.spec.ts index ccd2c89211..f93b63b5ef 100644 --- a/packages/code-runtime/code-runtime/tests/reserved.spec.ts +++ b/packages/code-runtime/code-runtime/tests/reserved.spec.ts @@ -37,7 +37,10 @@ describe('seam-owned portable identifier exclusions', () => { expect(DUNDER_MEMBER.test('__mid')).toBe(false) // `__` has an empty middle — not a real CPython dunder, so not matched. expect(DUNDER_MEMBER.test('__')).toBe(false) - expect(DUNDER_MEMBER.test('____')).toBe(true) + // `____` also has an empty middle between the two `__` pairs — not matched. + expect(DUNDER_MEMBER.test('____')).toBe(false) + // A single character between the pairs is the shortest real dunder form. + expect(DUNDER_MEMBER.test('__x__')).toBe(true) }) it('PORTABLE_RESERVED_WORDS is the union of ECMAScript and Python reserved words', () => { From c05db227d545ca5e70f8f6af1e29b42f7ffbcb58 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sat, 1 Aug 2026 05:54:04 +0800 Subject: [PATCH 6/8] docs(code-runtime): sync Agent Note and public JSDoc with the shipped seam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agent Note: correct the stale worker-adoption paragraph — the worker consumes the seam constants directly by name (no RESERVED_WORDS / RESERVED_ERROR_PROPERTIES re-alias) — and describe DUNDER_MEMBER as `__x__` (non-empty middle). - types.ts: document RESERVED_BINDING_GLOBALS on CodeBindingNamespace.global (names like `__dsh_main__` pass the identifier rule but are still refused) and the non-empty-middle dunder rule on memberNameProperty; propagate to the type-equiv derivative docs (both languages, re-recorded). --- ...31-code-runtime-portable-identifier-seam.i18n.yaml | 4 ++-- ...026-07-31-code-runtime-portable-identifier-seam.md | 4 ++-- ...-07-31-code-runtime-portable-identifier-seam.zh.md | 4 ++-- docs/core-data-structures/code-runtime.i18n.yaml | 4 ++-- docs/core-data-structures/code-runtime.md | 11 +++++++---- docs/core-data-structures/code-runtime.zh.md | 11 +++++++---- packages/code-runtime/code-runtime/src/types.ts | 11 +++++++---- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml index a2a0848eab..be124ffcbd 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md -2026-07-31-code-runtime-portable-identifier-seam.md: bd1876fc459dc85bb5d9f42ac04f4cc801d6398a -2026-07-31-code-runtime-portable-identifier-seam.zh.md: 2fd5e97c562d1e51cbad906cd37998b8ffccc58a +2026-07-31-code-runtime-portable-identifier-seam.md: 72796458d75dc04d094372f13d7253a6a4dad511 +2026-07-31-code-runtime-portable-identifier-seam.zh.md: ce709d5924d0835cd475af6d439c07e1dbceecd0 diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md index bd1876fc45..72796458d7 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md @@ -17,9 +17,9 @@ The seam package (`@deepseek-ai/dsh-code-runtime`) exports the portable-identifi - `PORTABLE_RESERVED_WORDS` — the union of ECMAScript and Python reserved words. A namespace global or error-class name matching any is refused on all backends, so `lambda` is refused even though it is a legal JS parameter name. Adding a language widens this union, which is a deliberate breaking review of existing binding names. - `RESERVED_BINDING_GLOBALS` — globals some backend owns in the program's namespace: `console` (the worker's log capture) and `__dsh_main__`/`__builtins__`/`__name__`/`__debug__` (the Python bootstrap's wrapper and seeded module globals). Refused everywhere so a namespace list cannot pick a name that works on one backend and collides on another. - `RESERVED_ERROR_MEMBERS` — error-member names every backend refuses: the JS `Error` slots (`name`, `message`, `stack`) and Python's exception-protocol members (`args`, `with_traceback`, `add_note`). -- `DUNDER_MEMBER` — the dunder-form regex (`__*__`), refused as an error member wholesale because several are constrained CPython descriptors whose exact set is an interpreter-version detail. +- `DUNDER_MEMBER` — the dunder-form regex (`__x__`, non-empty middle), refused as an error member wholesale because several are constrained CPython descriptors whose exact set is an interpreter-version detail. -The seam also narrows the portable identifier subset to `[A-Za-z_][A-Za-z0-9_]*` (documented on `CodeBindingNamespace.global` and `CodeBindingErrorClass`), dropping the JS-only `$`. The worker consumes the shared constants: `RESERVED_WORDS = PORTABLE_RESERVED_WORDS`, `RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS`, its `IDENTIFIER` regex loses `$`, and its error-member check adds `DUNDER_MEMBER`. +The seam also narrows the portable identifier subset to `[A-Za-z_][A-Za-z0-9_]*` (documented on `CodeBindingNamespace.global` and `CodeBindingErrorClass`), dropping the JS-only `$`. The worker consumes the shared constants directly by their seam names — `PORTABLE_RESERVED_WORDS` for both binding-global and error-class names, `RESERVED_BINDING_GLOBALS` for backend-owned slots, `RESERVED_ERROR_MEMBERS` plus `DUNDER_MEMBER` for error members — with no local re-alias; its `IDENTIFIER` regex loses `$`. The constants live at the seam even though only one backend ships in this PR: the whole point is that the contract is language-agnostic and owned above any single language. A backend that violated it would be the bug, and the shared set is where a reviewer looks to see what "portable" means. diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md index 2fd5e97c56..ce709d5924 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md @@ -17,9 +17,9 @@ seam 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量导出可移 - `PORTABLE_RESERVED_WORDS`——ECMAScript 与 Python 保留字的联集。任何命名空间 global 或 error-class 名称匹配其中之一,都在所有后端上被拒绝,因此 `lambda` 即便是合法的 JS 参数名也被拒绝。新增一门语言即扩宽此联集,这是对现有绑定名称的一次有意的破坏性复审。 - `RESERVED_BINDING_GLOBALS`——某个后端在程序命名空间中拥有的 global:`console`(worker 的日志捕获)与 `__dsh_main__`/`__builtins__`/`__name__`/`__debug__`(Python bootstrap 的包装器与预置模块 global)。在所有后端上被拒绝,使命名空间列表无法选到一个在某后端能用、在另一后端冲突的名称。 - `RESERVED_ERROR_MEMBERS`——每个后端都拒绝的 error-member 名称:JS `Error` 槽位(`name`、`message`、`stack`)与 Python 异常协议成员(`args`、`with_traceback`、`add_note`)。 -- `DUNDER_MEMBER`——dunder 形式正则(`__*__`),作为 error member 被整体拒绝,因为其中若干是受约束的 CPython 描述符,其确切集合是解释器版本细节。 +- `DUNDER_MEMBER`——dunder 形式正则(`__x__`,非空中缀),作为 error member 被整体拒绝,因为其中若干是受约束的 CPython 描述符,其确切集合是解释器版本细节。 -seam 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记录在 `CodeBindingNamespace.global` 与 `CodeBindingErrorClass` 上),去掉 JS 专有的 `$`。worker 消费这些共享常量:`RESERVED_WORDS = PORTABLE_RESERVED_WORDS`、`RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS`,其 `IDENTIFIER` 正则去掉 `$`,其 error-member 检查加上 `DUNDER_MEMBER`。 +seam 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记录在 `CodeBindingNamespace.global` 与 `CodeBindingErrorClass` 上),去掉 JS 专有的 `$`。worker 直接以 seam 名消费这些共享常量——binding-global 与 error-class 名称用 `PORTABLE_RESERVED_WORDS`、后端拥有槽位用 `RESERVED_BINDING_GLOBALS`、error member 用 `RESERVED_ERROR_MEMBERS` 加 `DUNDER_MEMBER`——不再本地起别名;其 `IDENTIFIER` 正则去掉 `$`。 尽管本 PR 只交付一个后端,这些常量仍置于 seam:要点正是该契约与语言无关,且拥有权在任何单一语言之上。违反它的后端才是 bug,而共享集合正是复审者查看"可移植"含义的地方。 diff --git a/docs/core-data-structures/code-runtime.i18n.yaml b/docs/core-data-structures/code-runtime.i18n.yaml index ac7dae06af..c4bbc64d27 100644 --- a/docs/core-data-structures/code-runtime.i18n.yaml +++ b/docs/core-data-structures/code-runtime.i18n.yaml @@ -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/core-data-structures/code-runtime.md -code-runtime.md: 522fe29deae8cac01462a336a02d082502b61fb4 -code-runtime.zh.md: 6d16f8ed2ab3b99620f30a8e81dc3718cf1419d1 +code-runtime.md: e7b5a86513abcab90eb81fc0b24296760bf3b926 +code-runtime.zh.md: 17ddbb0f685d97ba3c2dd6a23744771e88a40ed0 diff --git a/docs/core-data-structures/code-runtime.md b/docs/core-data-structures/code-runtime.md index 522fe29dea..e7b5a86513 100644 --- a/docs/core-data-structures/code-runtime.md +++ b/docs/core-data-structures/code-runtime.md @@ -76,9 +76,9 @@ interface CodeBindingErrorClass { name: string /** * Non-empty own property for the member name. The portable exclusion set is - * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced - * identically by every backend; any other name — identifiers or not — is - * accepted everywhere. + * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__x__`, non-empty + * middle), enforced identically by every backend; any other name — + * identifiers or not — is accepted everywhere. */ memberNameProperty: string } @@ -98,7 +98,10 @@ interface CodeBindingNamespace { * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved * words, so the same namespace list works against every backend regardless * of `language` — a JS-only spelling like `$tools` is rejected by design, - * not just by the Python backend. + * not just by the Python backend. Names that satisfy the identifier rule but + * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`: `console`, + * `__dsh_main__`, `__builtins__`, `__name__`, `__debug__`) are also refused + * everywhere, since some backend seeds that slot in the program's namespace. */ global: string /** The callable members, keyed by the exact name the program calls. */ diff --git a/docs/core-data-structures/code-runtime.zh.md b/docs/core-data-structures/code-runtime.zh.md index 6d16f8ed2a..17ddbb0f68 100644 --- a/docs/core-data-structures/code-runtime.zh.md +++ b/docs/core-data-structures/code-runtime.zh.md @@ -76,9 +76,9 @@ interface CodeBindingErrorClass { name: string /** * Non-empty own property for the member name. The portable exclusion set is - * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced - * identically by every backend; any other name — identifiers or not — is - * accepted everywhere. + * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__x__`, non-empty + * middle), enforced identically by every backend; any other name — + * identifiers or not — is accepted everywhere. */ memberNameProperty: string } @@ -98,7 +98,10 @@ interface CodeBindingNamespace { * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved * words, so the same namespace list works against every backend regardless * of `language` — a JS-only spelling like `$tools` is rejected by design, - * not just by the Python backend. + * not just by the Python backend. Names that satisfy the identifier rule but + * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`: `console`, + * `__dsh_main__`, `__builtins__`, `__name__`, `__debug__`) are also refused + * everywhere, since some backend seeds that slot in the program's namespace. */ global: string /** The callable members, keyed by the exact name the program calls. */ diff --git a/packages/code-runtime/code-runtime/src/types.ts b/packages/code-runtime/code-runtime/src/types.ts index e96d8bc959..e6c3b5ddc2 100644 --- a/packages/code-runtime/code-runtime/src/types.ts +++ b/packages/code-runtime/code-runtime/src/types.ts @@ -32,9 +32,9 @@ export interface CodeBindingErrorClass { name: string /** * Non-empty own property for the member name. The portable exclusion set is - * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced - * identically by every backend; any other name — identifiers or not — is - * accepted everywhere. + * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__x__`, non-empty + * middle), enforced identically by every backend; any other name — + * identifiers or not — is accepted everywhere. */ memberNameProperty: string } @@ -52,7 +52,10 @@ export interface CodeBindingNamespace { * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved * words, so the same namespace list works against every backend regardless * of `language` — a JS-only spelling like `$tools` is rejected by design, - * not just by the Python backend. + * not just by the Python backend. Names that satisfy the identifier rule but + * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`: `console`, + * `__dsh_main__`, `__builtins__`, `__name__`, `__debug__`) are also refused + * everywhere, since some backend seeds that slot in the program's namespace. */ global: string /** The callable members, keyed by the exact name the program calls. */ From efdc78cba3e9094a73b2e0433f806b466b7ffdcd Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sat, 1 Aug 2026 13:57:49 +0800 Subject: [PATCH 7/8] docs(code-runtime): fix remaining dunder/backend wording and document exports in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.ts: RESERVED_ERROR_MEMBERS JSDoc now says `__x__` (non-empty middle), matching DUNDER_MEMBER and the derivative docs; and PORTABLE_RESERVED_WORDS is described as covering portable *target* languages (Python is a later-PR backend, not yet shipped). - types.ts + type-equiv docs: CodeBindingNamespace.global points to RESERVED_BINDING_GLOBALS by name with an `e.g.` sample rather than enumerating the set (single home), no longer implying `__debug__` is a seeded slot. - Agent Note: separate `__debug__` (CPython compile-time constant) from the seeded bootstrap globals. - README (en/zh): document the four exported exclusion sets and the portable identifier/naming contract — no cross-package link needed. - Regenerate cordis services catalog for the shifted source line. --- ...code-runtime-portable-identifier-seam.i18n.yaml | 4 ++-- ...-07-31-code-runtime-portable-identifier-seam.md | 2 +- ...-31-code-runtime-portable-identifier-seam.zh.md | 2 +- docs/cordis-catalog/services.md | 2 +- docs/core-data-structures/code-runtime.i18n.yaml | 4 ++-- docs/core-data-structures/code-runtime.md | 6 +++--- docs/core-data-structures/code-runtime.zh.md | 6 +++--- .../code-runtime/code-runtime/README.i18n.yaml | 4 ++-- packages/code-runtime/code-runtime/README.md | 2 ++ packages/code-runtime/code-runtime/README.zh.md | 2 ++ packages/code-runtime/code-runtime/src/index.ts | 14 ++++++++------ packages/code-runtime/code-runtime/src/types.ts | 6 +++--- 12 files changed, 30 insertions(+), 24 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml index be124ffcbd..45436c96e6 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md -2026-07-31-code-runtime-portable-identifier-seam.md: 72796458d75dc04d094372f13d7253a6a4dad511 -2026-07-31-code-runtime-portable-identifier-seam.zh.md: ce709d5924d0835cd475af6d439c07e1dbceecd0 +2026-07-31-code-runtime-portable-identifier-seam.md: 9e9df50235b3505458e3645e2c6ff6e9bd439183 +2026-07-31-code-runtime-portable-identifier-seam.zh.md: 31d2410ee4809d0693f2e7897e61c50163bb0758 diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md index 72796458d7..9e9df50235 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md @@ -15,7 +15,7 @@ A second backend written against a different language (CPython, arriving in a la The seam package (`@deepseek-ai/dsh-code-runtime`) exports the portable-identifier exclusion contract as four named constants, and every backend imports them rather than re-declaring: - `PORTABLE_RESERVED_WORDS` — the union of ECMAScript and Python reserved words. A namespace global or error-class name matching any is refused on all backends, so `lambda` is refused even though it is a legal JS parameter name. Adding a language widens this union, which is a deliberate breaking review of existing binding names. -- `RESERVED_BINDING_GLOBALS` — globals some backend owns in the program's namespace: `console` (the worker's log capture) and `__dsh_main__`/`__builtins__`/`__name__`/`__debug__` (the Python bootstrap's wrapper and seeded module globals). Refused everywhere so a namespace list cannot pick a name that works on one backend and collides on another. +- `RESERVED_BINDING_GLOBALS` — globals some backend owns in the program's namespace: `console` (the worker's log capture), `__dsh_main__`/`__builtins__`/`__name__` (the Python bootstrap's wrapper and seeded module globals), and `__debug__` (not a seeded slot but a CPython compile-time constant that rejects assignment, so an injected global under that name is unreachable — the same portability split by a different mechanism). Refused everywhere so a namespace list cannot pick a name that works on one backend and collides on another. - `RESERVED_ERROR_MEMBERS` — error-member names every backend refuses: the JS `Error` slots (`name`, `message`, `stack`) and Python's exception-protocol members (`args`, `with_traceback`, `add_note`). - `DUNDER_MEMBER` — the dunder-form regex (`__x__`, non-empty middle), refused as an error member wholesale because several are constrained CPython descriptors whose exact set is an interpreter-version detail. diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md index ce709d5924..31d2410ee4 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md @@ -15,7 +15,7 @@ code-runtime seam 承诺:在一个后端上有效的绑定命名空间列表 seam 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量导出可移植标识符排除契约,每个后端导入它们而非重新声明: - `PORTABLE_RESERVED_WORDS`——ECMAScript 与 Python 保留字的联集。任何命名空间 global 或 error-class 名称匹配其中之一,都在所有后端上被拒绝,因此 `lambda` 即便是合法的 JS 参数名也被拒绝。新增一门语言即扩宽此联集,这是对现有绑定名称的一次有意的破坏性复审。 -- `RESERVED_BINDING_GLOBALS`——某个后端在程序命名空间中拥有的 global:`console`(worker 的日志捕获)与 `__dsh_main__`/`__builtins__`/`__name__`/`__debug__`(Python bootstrap 的包装器与预置模块 global)。在所有后端上被拒绝,使命名空间列表无法选到一个在某后端能用、在另一后端冲突的名称。 +- `RESERVED_BINDING_GLOBALS`——某个后端在程序命名空间中拥有的 global:`console`(worker 的日志捕获)、`__dsh_main__`/`__builtins__`/`__name__`(Python bootstrap 的包装器与预置模块 global),以及 `__debug__`(不是 seed 的槽位,而是 CPython 编译期常量,赋值会被拒,故以该名注入的 global 不可达——同一种可移植性分裂,只是机制不同)。在所有后端上被拒绝,使命名空间列表无法选到一个在某后端能用、在另一后端冲突的名称。 - `RESERVED_ERROR_MEMBERS`——每个后端都拒绝的 error-member 名称:JS `Error` 槽位(`name`、`message`、`stack`)与 Python 异常协议成员(`args`、`with_traceback`、`add_note`)。 - `DUNDER_MEMBER`——dunder 形式正则(`__x__`,非空中缀),作为 error member 被整体拒绝,因为其中若干是受约束的 CPython 描述符,其确切集合是解释器版本细节。 diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 6c670942b2..c2951fa9b2 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -398,7 +398,7 @@ abstract run(request: CodeRunRequest): Promise Types: [CodeRunRequest](../core-data-structures/code-runtime.md) · [CodeRunResult](../core-data-structures/code-runtime.md) -Source: [`packages/code-runtime/code-runtime/src/index.ts:102`](../../packages/code-runtime/code-runtime/src/index.ts) +Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts) ## `ctx.commands` — `CommandService` diff --git a/docs/core-data-structures/code-runtime.i18n.yaml b/docs/core-data-structures/code-runtime.i18n.yaml index c4bbc64d27..12e29f632a 100644 --- a/docs/core-data-structures/code-runtime.i18n.yaml +++ b/docs/core-data-structures/code-runtime.i18n.yaml @@ -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/core-data-structures/code-runtime.md -code-runtime.md: e7b5a86513abcab90eb81fc0b24296760bf3b926 -code-runtime.zh.md: 17ddbb0f685d97ba3c2dd6a23744771e88a40ed0 +code-runtime.md: 77e4438d16cf83f2a66fbe688c8aface3647e8cb +code-runtime.zh.md: ffd9b5583ee066b837408a03a595962786f192cf diff --git a/docs/core-data-structures/code-runtime.md b/docs/core-data-structures/code-runtime.md index e7b5a86513..77e4438d16 100644 --- a/docs/core-data-structures/code-runtime.md +++ b/docs/core-data-structures/code-runtime.md @@ -99,9 +99,9 @@ interface CodeBindingNamespace { * words, so the same namespace list works against every backend regardless * of `language` — a JS-only spelling like `$tools` is rejected by design, * not just by the Python backend. Names that satisfy the identifier rule but - * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`: `console`, - * `__dsh_main__`, `__builtins__`, `__name__`, `__debug__`) are also refused - * everywhere, since some backend seeds that slot in the program's namespace. + * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `console`, + * `__dsh_main__`) are also refused everywhere; see its declaration for the + * exact set and why each entry is reserved. */ global: string /** The callable members, keyed by the exact name the program calls. */ diff --git a/docs/core-data-structures/code-runtime.zh.md b/docs/core-data-structures/code-runtime.zh.md index 17ddbb0f68..ffd9b5583e 100644 --- a/docs/core-data-structures/code-runtime.zh.md +++ b/docs/core-data-structures/code-runtime.zh.md @@ -99,9 +99,9 @@ interface CodeBindingNamespace { * words, so the same namespace list works against every backend regardless * of `language` — a JS-only spelling like `$tools` is rejected by design, * not just by the Python backend. Names that satisfy the identifier rule but - * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`: `console`, - * `__dsh_main__`, `__builtins__`, `__name__`, `__debug__`) are also refused - * everywhere, since some backend seeds that slot in the program's namespace. + * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `console`, + * `__dsh_main__`) are also refused everywhere; see its declaration for the + * exact set and why each entry is reserved. */ global: string /** The callable members, keyed by the exact name the program calls. */ diff --git a/packages/code-runtime/code-runtime/README.i18n.yaml b/packages/code-runtime/code-runtime/README.i18n.yaml index 8e45c6265b..33f092d79b 100644 --- a/packages/code-runtime/code-runtime/README.i18n.yaml +++ b/packages/code-runtime/code-runtime/README.i18n.yaml @@ -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 packages/code-runtime/code-runtime/README.md -README.md: c7a2d519e47d160f5ab123bfc887e7e9f24ec602 -README.zh.md: 22d0b120d7cea50b578a184b3e40d77707ebc489 +README.md: fafaca85ac63e83e16882ef17f9f8995103866fd +README.zh.md: dc771bc16557d4b68529f2fde29eef1c09f299b5 diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index c7a2d519e4..fafaca85ac 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -20,6 +20,8 @@ Semantics every implementation must honor (contract details in the class JSDoc): `CodeRunRequest` (`program`, `bindings`, `signal?`) carries everything the runtime acts on — defaulting (time budgets and outer-output cap) is the implementation's validated config, never a hidden `??` inside `run()`. `bindings` is a list of `CodeBindingNamespace`s (`global` + `functions` + optional `errorClass`), each exposed to the program as one global object of async callables returning `CodeJsonValue`, the seam-local structural equivalent of canonical `JsonValue` that keeps this interface package independent of sessions. An `errorClass` descriptor names a real program-global constructor and the own property that receives the rejected member name; runtimes remain independent of consumer terms such as `ToolCallError`. `CodeRunResult` reports the lossless JSON completion `value?`, ordered `logs: string[]`, and the `error?` (`CodeRunFailure`: `kind` + model-feedable `message`). See `src/types.ts` for the full contracts. +Binding-global and error-class names are **language-portable**: they must match the identifier subset `[A-Za-z_][A-Za-z0-9_]*` (no JS-only `$`) and clear the seam-exported exclusion sets, so one `bindings` list is valid against every backend regardless of its `language`. The package exports the contract every backend enforces — `PORTABLE_RESERVED_WORDS` (ECMAScript ∪ Python reserved words), `RESERVED_BINDING_GLOBALS` (backend-owned globals such as `console`), `RESERVED_ERROR_MEMBERS` and `DUNDER_MEMBER` (error-member exclusions) — so a name like `$tools`, `lambda`, or `__dsh_main__` makes `run()` reject as seam misuse on any backend, not just some. See `src/index.ts` for the exact sets and rationale. + ## Model Experience Indirectly, through Code Mode in `dsh-tools`, which exposes `run_code` and returns program logs, values, or failures as retained tool-result tokens. diff --git a/packages/code-runtime/code-runtime/README.zh.md b/packages/code-runtime/code-runtime/README.zh.md index 22d0b120d7..dc771bc165 100644 --- a/packages/code-runtime/code-runtime/README.zh.md +++ b/packages/code-runtime/code-runtime/README.zh.md @@ -20,6 +20,8 @@ `CodeRunRequest`(`program`、`bindings`、`signal?`)携带运行时操作所需的全部内容;默认值解析(时间预算与外层输出上限)属于实现的已验证配置,绝不能是隐藏的 `??`,更不能藏在 `run()` 内部。`bindings` 是 `CodeBindingNamespace` 列表(`global` + `functions` + 可选 `errorClass`);每个命名空间会作为一个由异步可调用函数组成的全局对象公开给程序,这些函数返回 `CodeJsonValue`。后者是 seam 本地、与规范 `JsonValue` 结构等价的类型,使接口包保持独立于会话。`errorClass` 描述符点名真实的程序全局构造器,以及用于接收被拒绝成员名称的自有属性;运行时不依赖 `ToolCallError` 等消费方术语。`CodeRunResult` 报告无损 JSON 完成值 `value?`、有序的 `logs: string[]` 和 `error?`(`CodeRunFailure`:`kind` + 可反馈给模型的 `message`)。完整契约见 `src/types.ts`。 +binding-global 与 error-class 名称是**语言可移植**的:必须匹配标识符子集 `[A-Za-z_][A-Za-z0-9_]*`(不含 JS 专有的 `$`)并通过 seam 导出的排除集,因此同一份 `bindings` 列表对每个后端都有效,无论其 `language` 为何。本包导出每个后端都执行的契约——`PORTABLE_RESERVED_WORDS`(ECMAScript ∪ Python 保留字)、`RESERVED_BINDING_GLOBALS`(如 `console` 等后端拥有的 global)、`RESERVED_ERROR_MEMBERS` 与 `DUNDER_MEMBER`(error-member 排除)——因此 `$tools`、`lambda`、`__dsh_main__` 之类的名称会让 `run()` 在任何后端上作为 seam 误用而 reject,而非只在某些后端。确切集合与理由见 `src/index.ts`。 + ## 模型体验 通过 `dsh-tools` 中的 Code Mode 间接提供;后者公开 `run_code`,并将程序日志、值或失败作为保留的工具结果 token 返回。 diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index 2e4f18a2e2..3aeb6a39a8 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -48,10 +48,10 @@ export const RESERVED_BINDING_GLOBALS: ReadonlySet = new Set([ * one shared contract so a request valid on one backend is valid on all. The * JS `Error` exclusions (`name`, `message`, `stack`) and Python's * exception-protocol members (`args`, `with_traceback`, `add_note`) are - * listed by name; dunder-form names (`__*__`) are refused wholesale — several - * are constrained CPython descriptors whose `setattr` raises while - * constructing the rejection, and the exact set is an interpreter version - * detail. Any other non-empty own property name is accepted everywhere. + * listed by name; dunder-form names (`__x__`, non-empty middle) are refused + * wholesale — several are constrained CPython descriptors whose `setattr` + * raises while constructing the rejection, and the exact set is an interpreter + * version detail. Any other non-empty own property name is accepted everywhere. */ export const RESERVED_ERROR_MEMBERS: ReadonlySet = new Set([ 'name', 'message', 'stack', @@ -65,9 +65,11 @@ export const RESERVED_ERROR_MEMBERS: ReadonlySet = new Set([ export const DUNDER_MEMBER = /^__.+__$/ /** - * Reserved words of EVERY shipped backend language (ECMAScript ∪ Python), + * Reserved words of every portable target language (ECMAScript ∪ Python), * refused as {@link CodeBindingNamespace.global} / error-class names by all - * backends. The portable-identifier contract promises a namespace list valid + * backends. Python is a portability target here even though only the + * TypeScript worker ships in this PR (the CPython backend is a later PR in the + * stack). The portable-identifier contract promises a namespace list valid * on one backend is valid on every backend; a per-language check would let * `lambda` pass the TypeScript backend and fail the Python one. Extending the * seam with a new language means widening this union (a breaking review of diff --git a/packages/code-runtime/code-runtime/src/types.ts b/packages/code-runtime/code-runtime/src/types.ts index e6c3b5ddc2..6d83ace689 100644 --- a/packages/code-runtime/code-runtime/src/types.ts +++ b/packages/code-runtime/code-runtime/src/types.ts @@ -53,9 +53,9 @@ export interface CodeBindingNamespace { * words, so the same namespace list works against every backend regardless * of `language` — a JS-only spelling like `$tools` is rejected by design, * not just by the Python backend. Names that satisfy the identifier rule but - * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`: `console`, - * `__dsh_main__`, `__builtins__`, `__name__`, `__debug__`) are also refused - * everywhere, since some backend seeds that slot in the program's namespace. + * name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `console`, + * `__dsh_main__`) are also refused everywhere; see its declaration for the + * exact set and why each entry is reserved. */ global: string /** The callable members, keyed by the exact name the program calls. */ From e15e3db2b1d11ad2834ca3b81befeedaf2e17fda Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sat, 1 Aug 2026 15:53:48 +0800 Subject: [PATCH 8/8] docs(code-runtime-worker): correct __debug__ attribution in binding-global comment The inline comment called every dunder entry "the Python backend's" slot, but `__debug__` is a CPython compile-time constant, not a seeded slot; align the comment with the seam declaration, which is the single home for why each entry is reserved. --- packages/code-runtime/code-runtime-worker/src/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/code-runtime/code-runtime-worker/src/index.ts b/packages/code-runtime/code-runtime-worker/src/index.ts index 9773d3c341..43801899a2 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -324,9 +324,11 @@ export class WorkerCodeRuntime extends CodeRuntime { throw new Error(`dsh-code-runtime-worker: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`) } // RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set: - // `console` is THIS backend's log-capture slot; the dunder entries are - // the Python backend's — refused here too so the namespace list stays - // portable across backends. + // `console` is THIS backend's log-capture slot; the dunder entries exist + // for the Python side — its seeded/wrapped slots plus the `__debug__` + // compile-time constant — refused here too so the namespace list stays + // portable across backends. The seam declaration is the single home for + // why each entry is reserved. if (RESERVED_BINDING_GLOBALS.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: reserved binding global ${JSON.stringify(namespace.global)}`) }