M7.7
Self-modification and its guardrails
1,264 words · about 8 min
Concept
If plugins are the extension mechanism and the agent can write code, the agent can write plugins. packages/extensions/README.md describes the result: "Model-facing tools over the live cordis runtime the agent itself runs inside: inspect the loaded plugins and service API, define and run model-written dynamic packages, and retract them again."
This is the most interesting thing in the repository and the most dangerous, and both deserve to be stated plainly.
Mechanism: five tools, two paired verbs and a report
MechanismFrom packages/extensions/tool-cordis/README.md:
cordis_inspect— read-only report over the current process: services, all live plugin fibers, registered tools, this session's dynamic packages, the reflection-backedapi/eventsreferences, and the client slot surface. An exactnamewithwhat: "api","events", or"client"narrows and adds the full contract.cordis_define— records a package (name,purpose, host halfcodeand/or browser halfclient) after syntax-checking both halves. Nothing runs; the user sees a card with a start control. The minteddyn-<n>id rides both the result value and the durable presentation metadata.cordis_run— evaluates the host half in the sandbox and delivers the browser half to open pages. Re-running an already-running package re-delivers the live version rather than failing.cordis_stop— disposes the host half to quiescence and withdraws the browser half; the definition survives.cordis_undefine— stops if needed and forgets the definition; its card remains in the conversation as an unloaded record.
The seam beneath them (docs/capability-seams.md): ctx.dynamicCordisRunner "owns the in-memory definition registry, the vm sandbox for host halves, and the request-run round trip," and ctx.cordisInspect routes inspection queries. Both are owned by packages/extensions/cordis-host-runner.
Composition, from examples/web-cordis/cordis.yml, is the two-row insert shown in M7.3. The capability is entirely opt-in and off by default.
The guardrails
Read them as a catalogue of what self-modification requires, because each answers a specific attack or accident.
Define and run are separate verbs. cordis_define runs nothing. A human sees the code as a card and starts it. The model cannot go from "wrote code" to "code executing" in one step.
Approval gating for client halves. run is documented as: "Start or update one Package for a model tool call. An unauthorized Client Package waits for approval; Plugin-wide authorization covers later versions." The runHostHalf remote signature carries a requestId: ApprovalRequestId | null and an approveFutureVersions: boolean — the approval is a real Module 5 approval flowing through the real approval service, and the scope of a blanket grant is explicit rather than implied.
Session ownership. The runner's methods repeat the precondition "Agent whose Session must own the Plugin," and the package README states it: "Every verb is session-scoped: a package is visible and controllable only in the session that defined it." One session cannot stop or inspect another's dynamic package.
Immutable, versioned packages. define appends a Package to a Plugin; stop is documented as stopping the active run "while retaining every Package version," and undefine removes "a Plugin, its active run, and all immutable Packages." Nothing is edited in place, so the audit trail is complete and a stop/start cannot silently swap code.
Stale-run rejection. invoke "Invoke an active Host method while rejecting stale Client runs" — a browser page holding a handle from a previous run cannot call into the current one.
Source-free inspection. inspectPlugin "Inspect one Plugin without returning Package source," and listPlugins/reference return "source-free" data. Inspection surfaces cannot be used to exfiltrate code.
No persistence, by construction. "Dynamic packages live only in the shared DSH process memory … They create no Plugin file, install no package, change no cordis.yml or personal/project configuration, do not survive restart, and cannot be promoted automatically. To keep an experiment, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow." Restart is a guaranteed clean slate, and promotion to permanence goes through the ordinary human-reviewed path.
Bounded evaluation. vmTimeoutMs and ackTimeoutMs live on the runner's config, so a runaway host half and an unresponsive browser both have deadlines.
Curated inspection surface. The api report shows only callable methods and "Only keys a host half can reach are named to a model" — src/curation.ts classifies each ctx key's reach as injectable, not-a-service, or other-face, and only injectable keys appear, because "naming a key a package cannot reach advertises a call that cannot be made." A verify-cordis-catalog gate pins the classified set so a newly declared key "stops the gate instead of quietly inviting a model to inject something that will never arrive." This is a quality guardrail rather than a security one, and it matters more than it looks: a model given a wrong API surface writes plausible code that fails at runtime, and then debugs in circles.
The stance: this is not a security boundary
packages/extensions/tool-cordis/README.md is unambiguous, and the honesty is the point:
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, andctx.bash, and writes toglobalThisstay 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.
examples/web-cordis/cordis.yml repeats it in the composition file itself: "Temporary Plugin code can reach every injected live capability; treat this deployment like shell access, not as a security boundary."
node:vm is well known not to be a security sandbox — host-realm object references reachable from inside the context are enough to escape. dsh uses it for isolation of accidents, not containment of adversaries, and labels it accordingly. That labelling is the professionally important part. A harness that shipped node:vm and called it a sandbox would be making a false security claim, and someone would deploy it facing untrusted input. The correct model: if the agent can run bash, self-modification adds capability but not privilege; if it cannot, self-modification is a privilege escalation and must not be enabled.
The remaining honest gap: because dynamic packages "may affect other sessions in that process," a package's effects are not session-scoped even though its control is. That is a real limitation, disclosed rather than papered over.
Tradeoffs
The upside is genuine: an agent that can inspect its own runtime can build the tool it needs mid-task instead of asking a human to ship one, and the inspect/define/run/stop cycle is a fast experimentation loop for harness developers. The downside is a large new surface — model-written code with live service access — whose containment is explicitly not a boundary. That is why the capability is opt-in, off by default, and confined to an example composition rather than the base bundle.
How others differ
Almost nobody ships this. Claude Code and Codex let an agent write plugin/extension files to disk, but loading requires a restart or an explicit human action — the same define/run split, with a much larger gap in the middle and no in-process evaluation at all. That is strictly safer and strictly slower.
The nearest precedents are outside the agent world: Emacs and Smalltalk images, where self-modification is the paradigm and the security model is "you trust the code." That analogy is the right one, and it is also the warning — those systems assume a single trusted author, which is exactly the assumption an agent processing untrusted input violates.
Practice here is genuinely unsettled. There is no accepted answer for how an agent should safely extend its own runtime, and the honest position is that dsh is exploring the space with explicit warnings rather than claiming to have solved it.
Check yourself
1 question on M7.7