mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 侧证明共享集合被执行。
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<string> = 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<string> = 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<string> = 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
|
||||
|
||||
@@ -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<string, CodeBindingFunction>
|
||||
|
||||
51
packages/code-runtime/code-runtime/tests/reserved.spec.ts
Normal file
51
packages/code-runtime/code-runtime/tests/reserved.spec.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user