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
@@ -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>
@@ -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)
})
})