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:
Chinesezjc
2026-08-07 11:20:05 +08:00
parent 70937db8a0
commit 5d4cea9dc1
8 changed files with 256 additions and 19 deletions
@@ -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 () => {