fix(tool-cordis): replace the pass-through ctx proxy with a whitelist façade

Review finding (#220): the guarded proxy only special-cased ctx.tools, so
mount code could reach an UNGUARDED context through ctx.root, ctx.extend(), or
a service instance's .ctx, then ctx.root.tools.register({…}) to bypass the
marker check and host-realm normalization — a raw vm-realm result would later
error a real agent turn at the session-log plainness check.

The sandbox ctx is now a whitelist façade, not a pass-through proxy: it exposes
only what a mount needs — tools.register (marker-guarded), on/once, provide, the
timer helpers, and injected services resolved through a guarded get — and denies
every framework-plumbing member (root, parent, fiber, reflect, registry, extend,
isolate, intercept, plugin, set, mixin, …) with a teaching error. Injected
services are wrapped so a method returning a Context is rejected on the way back
(the .ctx escape), closing the one indirect leak. There is no context-valued
member left to reach; cross-mount provide/inject is untouched (the plugin's own
inject and the fiber's pending/active gating are unchanged). ctx.plugin (child
plugins) and ctx.set are denied by design; ctx.effect is deferred (FIXME).

Adds tests/sandbox-context.spec.ts covering the escape class (root/extend/fiber/
plugin/set/… denied, the classic root.tools.register bypass, the .ctx escape,
read-only writes) plus the async-service and symbol/in-operator paths for 100%
coverage. RFC/README/tool-catalog/config-catalog updated; api-catalog.ts
regenerated (also picks up the codeRuntime service that entered on the master
merge and was left stale).
This commit is contained in:
imccyu
2026-07-09 01:37:40 +08:00
parent aed752a75d
commit 1b1ba96d4f
9 changed files with 348 additions and 45 deletions

View File

@@ -18,10 +18,14 @@
* this plugin. Design home:
* docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
*
* The vm sandbox guards against ACCIDENTAL global pollution only — it is not a
* security boundary. The `ctx` handed to the mounted plugin's `apply` is the
* real, fully privileged runtime handle; that is the point of the toolset, so
* a deployment loads this plugin as deliberately as it grants a bash tool.
* The vm sandbox guards against ACCIDENTAL global pollution only, and the `ctx`
* a mounted plugin's `apply` receives is a WHITELIST façade (register a tool,
* observe events, provide/consume services, use timers — framework internals
* withheld; see the guard module). Neither is a security boundary: the verbs
* the façade DOES expose reach the real runtime unsandboxed (a mounted tool can
* shell out through `ctx.bash`), so a deployment loads this plugin as
* deliberately as it grants a bash tool. Design home:
* docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
*
* Plugin export shape: named exports, NO default. The cordis Loader's
* `unwrapExports` does `exports.default ?? exports`, so a stray default would
@@ -159,8 +163,11 @@ export function apply(ctx: Context, config: Config): void {
+ 'VETOES the call; prefer plain notification events unless you intend to '
+ 'intercept. (2) Never await something that only resolves after the current '
+ 'turn (your code runs INSIDE a tool call of that turn — it would deadlock). '
+ '(3) The sandbox prevents accidental global pollution, not malice: `ctx` is '
+ 'the real, fully privileged runtime handle.',
+ '(3) Your `ctx` is a restricted façade: you can register tools, observe '
+ 'events, provide/consume services, and use timers, but framework internals '
+ '(ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a '
+ 'security boundary though — the services you inject (e.g. ctx.bash) reach the '
+ 'real runtime.',
parameters: {
code: {
type: 'string',