docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -12,7 +12,7 @@ Exact model-facing schemas: [the generated tool catalog](../../../docs/tool-cata
|
||||
|
||||
## Trust stance
|
||||
|
||||
The sandbox isolates globals but is not a security boundary. Node globals are absent or redirect to Cordis services, and writes to `globalThis` stay local, but host-realm helpers and the privileged context make escape possible. Mounted plugins receive a façade without framework internals, yet its allowed services affect the live runtime. Treat this toolset like bash access.
|
||||
The sandbox isolates globals but is not a security boundary. Node globals are absent or redirect to Cordis services such as `ctx.fs`, `ctx.web`, and `ctx.bash`, and writes to `globalThis` stay local, but host-realm helpers make escape possible. Mounted plugins receive a façade without framework internals, yet its allowed services affect the live runtime. Treat this toolset like bash access; see the [design and trust stance](../../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
key: 'bash',
|
||||
summary: 'Abstract bash execution service.',
|
||||
summary: 'Registers one `ctx.bash` implementation.',
|
||||
methods: [
|
||||
'abstract resolve(request: BashExecRequest): BashExecSpec',
|
||||
'abstract run(spec: BashExecSpec): Promise<BashRunResult>',
|
||||
@@ -99,7 +99,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
key: 'codeRuntime',
|
||||
summary: 'Abstract code-execution service.',
|
||||
summary: 'Registers one `ctx.codeRuntime` implementation.',
|
||||
methods: [
|
||||
'abstract run(request: CodeRunRequest): Promise<CodeRunResult>',
|
||||
],
|
||||
@@ -114,7 +114,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
key: 'fs',
|
||||
summary: 'Abstract filesystem provider service.',
|
||||
summary: 'Abstract filesystem provider.',
|
||||
methods: [
|
||||
'abstract resolve(path: string, opts?: { cwd?: string }): Promise<FsTarget>',
|
||||
'abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>',
|
||||
@@ -240,7 +240,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'agent/created',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/created\'(this: Scoped<Agent>, agent: Agent): void',
|
||||
summary: 'A fully configured agent and its session were published.',
|
||||
summary: 'A fully configured agent and live session were published.',
|
||||
},
|
||||
{
|
||||
name: 'agent/disposed',
|
||||
@@ -324,19 +324,19 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'fs/edit-intent',
|
||||
mode: 'waterfall',
|
||||
signature: '\'fs/edit-intent\'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>',
|
||||
summary: 'Single-slot decision: produce the optional version guard for the next FileSystem.editText.',
|
||||
summary: 'Single-slot decision for the next FileSystem.editText.',
|
||||
},
|
||||
{
|
||||
name: 'fs/observed',
|
||||
mode: 'emit',
|
||||
signature: '\'fs/observed\'(target: FsTarget, version: FsVersion, actor: object | undefined): void',
|
||||
summary: 'Record that an actor observed a target at a version, after a successful read/write/edit.',
|
||||
summary: 'Record a successful observation.',
|
||||
},
|
||||
{
|
||||
name: 'fs/write-intent',
|
||||
mode: 'waterfall',
|
||||
signature: '\'fs/write-intent\'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>',
|
||||
summary: 'Single-slot decision: produce the write intent for the next FileSystem.writeText.',
|
||||
summary: 'Single-slot decision for the next FileSystem.writeText.',
|
||||
},
|
||||
{
|
||||
name: 'llm/stream',
|
||||
@@ -348,25 +348,25 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'session/created',
|
||||
mode: 'emit',
|
||||
signature: '\'session/created\'(this: Scoped<Session>, session: Session): void',
|
||||
summary: 'Emitted after session publication.',
|
||||
summary: 'Creation announcement during session publication.',
|
||||
},
|
||||
{
|
||||
name: 'session/disposed',
|
||||
mode: 'emit',
|
||||
signature: '\'session/disposed\'(this: Scoped<Session>, session: Session): void',
|
||||
summary: 'Emitted once when an announced session leaves the store, including publication rollback.',
|
||||
summary: 'Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin.',
|
||||
},
|
||||
{
|
||||
name: 'session/event',
|
||||
mode: 'emit',
|
||||
signature: '\'session/event\'(this: Scoped<Session>, session: Session, event: SessionEvent): void',
|
||||
summary: 'Post-commit append feed.',
|
||||
summary: 'Post-commit, fire-and-forget append feed.',
|
||||
},
|
||||
{
|
||||
name: 'session/flush',
|
||||
mode: 'parallel',
|
||||
signature: '\'session/flush\'(this: Scoped<Session>, session: Session): Promise<void> | void',
|
||||
summary: 'Awaited parallel durability checkpoint; dispatch through SessionStore.flush.',
|
||||
summary: 'Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto.',
|
||||
},
|
||||
{
|
||||
name: 'skill/provider-added',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Runtime mirror of the cordis `FiberState` const enum plus human-readable labels, shared by
|
||||
* the mount lifecycle (state reporting) and the inspect renderers (plugin-list and mount-table
|
||||
* labels).
|
||||
* Runtime mirror and labels for Cordis's `FiberState` const enum. A const enum has no runtime
|
||||
* object to import, so these values mirror the pinned vendored definition while retaining its
|
||||
* type.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/fiber-state
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
* normalization + validation with teaching errors, the marker-guarded `harness.defineTool` /
|
||||
* `harness.registerTool` pair, the SANDBOX CONTEXT FAÇADE a mounted plugin's `apply` receives
|
||||
* in place of the real `ctx`, and the plugin-shape helpers the mount lifecycle narrows sandbox
|
||||
* return values with.
|
||||
* return values with. The façade is a whitelist of lifecycle-safe verbs and declared services;
|
||||
* framework internals and context-valued service returns are denied.
|
||||
*
|
||||
* VM-realm schemas are rebuilt as host objects, and tool results are JSON-round-tripped and
|
||||
* shape-checked before session logging. Common JSON-Schema spellings are normalized when they
|
||||
* have one meaning; invalid vocabulary fails during registration with a teaching error.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/guard
|
||||
*/
|
||||
|
||||
@@ -61,7 +66,7 @@ function normalizeSchemaProp(value: unknown, path: string, forceRequired = false
|
||||
}
|
||||
// On an object property a JSON-Schema-style `required` ARRAY names required
|
||||
// children (handled by the nested unwrap below); everywhere else `required`
|
||||
// must be a boolean, and `false` simply reads as optional.
|
||||
// must be a boolean, and `false` means optional.
|
||||
const nestedRequiredArray = type === 'object' && Array.isArray(value.required)
|
||||
if (value.required !== undefined && typeof value.required !== 'boolean' && !nestedRequiredArray) {
|
||||
throw new Error(`harness.defineTool ${path}.required must be a boolean when present`)
|
||||
@@ -157,8 +162,8 @@ function assertExecuteReturn(value: unknown): ToolExecuteReturn {
|
||||
* The `harness.defineTool` handed into the sandbox: the real DSL, with `parameters` normalized
|
||||
* into a fresh host-realm SchemaSpec (JSON-Schema wrapper unwrapped, `integer` mapped,
|
||||
* `required: false` dropped) and the tool's `execute` return normalized into the host realm
|
||||
* via a JSON round-trip (see the module doc).
|
||||
*
|
||||
* via a JSON round-trip. Non-JSON or wrong-shape output fails that call instead of poisoning
|
||||
* the session log.
|
||||
* @param options - the standard `defineTool` options; `parameters` may be the SchemaSpec DSL or a JSON-Schema-style wrapper.
|
||||
* @returns the marker-tagged definition `harness.registerTool` (and the guarded `ctx.tools.register`) accepts.
|
||||
*/
|
||||
@@ -266,7 +271,8 @@ function declaredInjects(ctx: Context): Set<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* The sandbox context façade handed to a mounted plugin's `apply` in place of the real `ctx`.
|
||||
* Whitelist context for mounted plugins: lifecycle-safe verbs, guarded tools, and only declared
|
||||
* injected services. Framework plumbing is denied, and service methods cannot return a Context.
|
||||
*/
|
||||
function sandboxContext(ctx: Context): Context {
|
||||
const tools = sandboxTools(ctx)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* The self-referential cordis toolset: three model-facing tools that let the agent inspect and
|
||||
* MODIFY the live cordis runtime it is running inside.
|
||||
* Self-referential runtime tools: inspect live services/plugins/tools, mount a returned plugin
|
||||
* under an owned dynamic fiber, and unmount it to quiescence. Registrations are fiber effects,
|
||||
* so plugin disposal removes the entire dynamic subtree. The VM and context façade prevent
|
||||
* accidental misuse, not hostile code: an allowed service such as `ctx.bash` reaches the real
|
||||
* runtime. Named exports preserve loader injection metadata.
|
||||
* @module @deepseek-ai/dsh-tool-cordis
|
||||
*/
|
||||
|
||||
|
||||
@@ -127,7 +127,9 @@ function typeClosure(seeds: string[], types: readonly TypeApiEntry[]): TypeApiEn
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the generated service catalog against the live runtime.
|
||||
* Render the generated catalog against the live runtime: live catalogued services with methods,
|
||||
* uncatalogued live services with owners, absent loadable services, referenced type shapes, and
|
||||
* inherited Context APIs.
|
||||
* @param ctx - the runtime to intersect the catalog with.
|
||||
* @param api - generated service entries, replaceable in tests.
|
||||
* @param inherited - inherited `ctx` entries, replaceable in tests.
|
||||
|
||||
@@ -21,8 +21,8 @@ export interface DynamicMount {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount a plugin under the group fiber and settle it.
|
||||
*
|
||||
* Await the group, mount and settle one guarded child, and dispose it before rethrowing any
|
||||
* startup failure so a failed mount never lingers. A valid unresolved inject may remain pending.
|
||||
* @param group - the `cordis-dynamic` group fiber every mount hangs under.
|
||||
* @param plugin - the plugin the sandbox returned; wrapped with the registration guard before mounting.
|
||||
* @returns the settled child fiber (possibly pending on unsatisfied `inject`).
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
* The `node:vm` sandbox `cordis_mount` code evaluates in: a fresh realm whose globals are a
|
||||
* tagged write-through console, the `harness` registration helpers, the encoding primitives a
|
||||
* bare vm context lacks, and callable traps over the Node APIs the sandbox deliberately
|
||||
* withholds.
|
||||
* withholds. Traps steer filesystem, network, process, and timer work to `ctx.fs`, `ctx.web`,
|
||||
* `ctx.bash`, and Cordis timers. This keeps cooperative mounts inspectable and disposable but
|
||||
* is not containment: host-realm helper functions remain an escape route.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/sandbox
|
||||
*/
|
||||
|
||||
@@ -23,8 +25,8 @@ function taggedConsole(id: string): Record<'log' | 'info' | 'warn' | 'error' | '
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-sandbox prelude: give the vm realm's own constructors a `Symbol.hasInstance` that checks
|
||||
* BOTH realms.
|
||||
* Patch only VM constructors so `instanceof` accepts both VM values and host values passed as
|
||||
* arguments, events, or service results; host intrinsics remain untouched.
|
||||
*/
|
||||
const DUAL_REALM_INSTANCEOF_PRELUDE = `
|
||||
(hostIntrinsics) => {
|
||||
@@ -137,8 +139,8 @@ export function syntaxErrorContext(error: Error): string {
|
||||
/**
|
||||
* Evaluate mount code as the body of an async function inside the sandbox. `vmTimeoutMs` only
|
||||
* bounds the SYNCHRONOUS portion; an async body escapes it — acceptable under the module's
|
||||
* trust stance.
|
||||
*
|
||||
* trust stance. Parse errors include the offending line and a TypeScript-removal or bracket-
|
||||
* balance hint.
|
||||
* @param sandbox - the contextified object from {@link createSandbox}.
|
||||
* @param code - the model-written function body; must `return` a plugin.
|
||||
* @param id - the mount id, used as the vm filename (`cordis-mount-<id>.js`).
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('cordis_mount', () => {
|
||||
})
|
||||
|
||||
it('normalizes a self-made tool\'s result into the host realm, so the session log accepts it', async () => {
|
||||
// Normalize vm-realm results into host JSON before session validation.
|
||||
// VM-realm objects fail the session prototype-identity check; normalize them into host JSON.
|
||||
const ctx = await setup()
|
||||
await call(ctx, 'cordis_mount', { code: REVERSE_TOOL_CODE })
|
||||
const reversed = await call(ctx, 'reverse_text', { text: 'harness' })
|
||||
@@ -89,9 +89,8 @@ describe('cordis_mount', () => {
|
||||
['object-form blocks missing the type tag', 'return { content: [{ text: \'hi\' }] }', '{"content":[{"text":"hi"}]}'],
|
||||
['undefined — a forgotten return', 'return undefined', 'undefined'],
|
||||
])('rejects an execute return of %s as that one call\'s teaching error', async (_label, returnStatement, preview) => {
|
||||
// The failure this prevents: the registry trusts the return shape (postExecute spreads
|
||||
// result.content), so an unvalidated { content: 'ok' } would enter the session log as
|
||||
// ['o','k'] and silently corrupt the next model request.
|
||||
// The registry spreads result.content, so { content: 'ok' } would become ['o','k']; reject
|
||||
// it as this call's error before it corrupts the next request.
|
||||
const ctx = await setup()
|
||||
await call(ctx, 'cordis_mount', {
|
||||
code: `
|
||||
@@ -143,8 +142,8 @@ describe('cordis_mount', () => {
|
||||
})
|
||||
|
||||
it('accepts a JSON-Schema-style parameters wrapper and normalizes it to the DSL', async () => {
|
||||
// The dialect models write by strong prior: the { type:'object', properties, required: […]
|
||||
// } wrapper, `type: 'integer'`, and `required: false`.
|
||||
// These common JSON-Schema spellings each have one DSL meaning, so normalize rather than
|
||||
// consume another model turn with a rejection.
|
||||
const ctx = await setup()
|
||||
const result = await call(ctx, 'cordis_mount', {
|
||||
code: `
|
||||
|
||||
@@ -2,13 +2,10 @@ import { describe, expect, it } from 'vitest'
|
||||
import { call, setup, text } from './helpers.ts'
|
||||
|
||||
/**
|
||||
* The sandbox context façade is a whitelist, not a pass-through proxy: mount
|
||||
* code reaches only the registration/eventing verbs, the timer helpers, a
|
||||
* guarded `tools`, and its injected services. Every framework-plumbing member
|
||||
* that could hand back an UNGUARDED context — through which a plugin could
|
||||
* `ctx.<escape>.tools.register({…})` to bypass the marker check and host-realm
|
||||
* normalization — is denied. These are the regression guards for that escape
|
||||
* class (the review finding on the original pass-through proxy).
|
||||
* The sandbox context façade is a whitelist, not a pass-through proxy. Mounted code reaches only
|
||||
* registration/eventing verbs, timer helpers, guarded tools, and injected services. Framework
|
||||
* members that expose an unguarded context are denied because they could bypass marker checks and
|
||||
* host-realm normalization; these tests pin that escape class.
|
||||
*/
|
||||
|
||||
/** Mount a plugin whose `apply` touches one framework member, and report the error text. */
|
||||
@@ -77,7 +74,8 @@ describe('sandbox context façade — escape surface is closed', () => {
|
||||
|
||||
it('denies a service whose method returns a Context (the .ctx escape), registering nothing', async () => {
|
||||
// A cordis Service instance carries `.ctx` (a real Context), so
|
||||
// `ctx.systemPrompt.ctx.root.tools.register(…)` would be a fresh unguarded handle.
|
||||
// `ctx.systemPrompt.ctx.root.tools.register(…)` would escape the façade; service-return
|
||||
// guards reject that Context before the registration lands.
|
||||
const ctx = await setup()
|
||||
const result = await call(ctx, 'cordis_mount', {
|
||||
code: `
|
||||
@@ -189,9 +187,8 @@ describe('sandbox context façade — inject gate on services', () => {
|
||||
})
|
||||
|
||||
it('a cross-mount consumer must declare the provider — the undeclared path is refused, not left as a zombie tool', async () => {
|
||||
// The finding's scenario: a consumer registers a tool built on a provider's service WITHOUT
|
||||
// declaring inject. cordis would then never park the consumer when the provider unmounts,
|
||||
// leaving a tool that fails only at execution.
|
||||
// Without declared inject, Cordis cannot park the consumer when its provider unmounts. The
|
||||
// façade refuses access up front instead of leaving a zombie tool.
|
||||
const ctx = await setup()
|
||||
await call(ctx, 'cordis_mount', {
|
||||
code: 'return { name: \'greeter-provider\', apply(ctx) { ctx.provide(\'greeter\', { greet: (n) => \'hi \' + n }) } }',
|
||||
|
||||
Reference in New Issue
Block a user