Runtime generation
June 28, 2026 · View on GitHub
The agent extends itself with new capabilities mid-conversation — safely, audibly, and persistently.
The mental model
Most agent frameworks have a fixed capability surface: the tools you ship are the tools the agent gets. Corvin treats capability as runtime-extensible state. The agent — talking to you in a chat — can:
- forge a new tool: ship a fresh sandboxed Python (or bash) executable
that's callable by
mcp__forge__<name>in the same turn it was created. - create a new skill: ship a fresh markdown body that gets prompt-injected as instructions into the next subprocess turn.
Both feel like the agent saying "wait, I'd be more useful if I had X" and then having X. The difference between the two is the artifact class:
| Tool (Forge) | Skill (Skill-Forge) | |
|---|---|---|
| What it is | Executable code | Markdown knowledge |
| Where it runs | bwrap subprocess | LLM context |
| Safety primitive | Sandbox + policy | Linter |
| Call shape | mcp__forge__<name>(args) | Auto-injected text |
| Visible in turn | Same turn | Next turn |
| Best for | Deterministic computation | Workflow / convention / pattern |
The whole subsystem is opt-in per persona (via forge_enabled /
skill_forge_enabled JSON flags), but every persona that gets it
goes through the same safety substrate. There is no fast-path or
trusted bypass — neither for personas, nor for the operator, nor for
the agent itself.
The problem this solves
Without runtime generation, every reusable capability has to be forecasted at build time and shipped as code. That works for a small set of stable tools. It doesn't work when:
- Each user has their own micro-workflows (different APIs, different data shapes, different conventions)
- The agent discovers a useful pattern mid-session that should survive the session
- A reproducible computation needs deterministic re-execution with different parameters (and the LLM should not be the executor)
- A piece of know-how should compound: each session adds to a growing library rather than re-discovering from scratch
The traditional answer is "give the user a plugin SDK." Corvin's answer is: the agent itself authors the artifact, the substrate keeps it safe, and the scope ladder gives it a measured lifetime.
The forge pipeline (executable tools)
When an LLM in a forge-enabled persona decides a deterministic computation is needed (e.g. "compute statistics over a CSV the user keeps re-asking about"), it calls:
{
"tool": "mcp__forge__forge_tool",
"input": {
"name": "csv.stats", // namespace.name
"description": "Compute mean/median/stddev for one CSV column",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "x-bind": "ro"},
"column": {"type": "string"}
},
"required": ["path", "column"]
},
"impl": "import csv, statistics, json, sys\n…"
}
}
What happens then:
- Schema + static check — name regex (
alnum + . + _), namespace policy (the persona'stool_namespacemust allowcsv.*), import allowlist (nosubprocess, nosocket, noos.system). - Manifest write — entry into
<scope>/forge/registry.jsonwithsha256,created_at,scope,meta. - Body write —
<scope>/forge/tools/<name>.py. - Slot mirror — for namespace-discovery on next subprocess boot.
tools/list_changednotification → the LLM can call the new tool in the same turn.
When the tool actually runs (some subsequent mcp__forge__csv.stats
call):
- spawned under
bwrapwith--share-netonly when the persona policy allows /tmpis fresh (mount-namespace),/usris read-only,/home/<user>is unmountedx-bind: ropaths frominput_schemaare bound read-only into the sandbox;x-bind: rwpaths read-write- stdout is JSON-shaped (
{ok, status, data, error, meta}), capped at 4 MiB; overflow is preserved as an artifact - secrets declared via
meta.secretsare pulled from the per-tenant vault, validated against the persona's ACL, and injected as env vars only inside the bwrap; literal values are stripped from the result envelope before it's returned - every step lands in the audit chain (
tool.created,tool.invoked,tool.network_share,tool.secrets_injected)
The skill-forge pipeline (prompt-injected knowledge)
When the LLM decides a workflow / pattern / convention should survive into future sessions:
{
"tool": "mcp__skill_forge__skill_create",
"input": {
"name": "deploy-flow",
"body": "# Deploy flow\n\n1. Run pre-deploy tests…\n…",
"description": "How we deploy: tests → tag → docker push → blue/green",
"type": "workflow"
}
}
What happens then:
- Linter (fail-closed) — NFKC + cyrillic-confusable normalisation
(so a homoglyph like
аdmincannot bypass detection); rejects prompt-injection patterns ("ignore previous instructions", embeddedcodes), embedded secrets (high-entropy strings, bearer tokens), persona-boundary phrases ("you are now…"). - Canonical write —
<scope>/skill-forge/skills/<name>/SKILL.mdplusmeta.jsoncarrying provenance and grade history. - Slot mirror — projection into
<repo>/operator/skill-forge/skills/dyn/<sanitized>/SKILL.mdso the engine's plugin-skill loader picks it up at next subprocess boot. - Adapter inject — on the very next bridge turn,
skill_inject.collect_active_skills()reads the skill from the workspace and appends it to--append-system-prompt. No engine restart needed.
The next time the user types in that chat, the LLM sees the new skill body in its system prompt and can apply it.
Two-loop grading (skills only)
Skills go through a two-loop grading system that turns user reactions into a survival signal:
Inner loop — auto-grade. After each turn, the adapter scans the
LLM's reply for mentions / paraphrases of the skills that were
active. Each match writes a grade with score 0.7 × 0.3 = 0.3 into
the skill's meta.json (the 0.3 cap reflects "we know it was used,
not whether it helped").
Outer loop — outcome-grade. The next user turn is scanned for approval ("danke", "perfekt"), rejection ("falsch", "nochmal"), or rephrase signals (difflib similarity ≥ 0.6 against the previous user text). Each signal maps to an absolute score:
| Signal | Score | Mean with auto-grade (0.3) | Promotion gate (>0.5) |
|---|---|---|---|
| approval | 0.9 | 0.6 | eligible |
| rejection | 0.1 | 0.2 | blocked |
| rephrase | 0.3 | 0.3 | blocked, soft hint |
Skills that survive the two-loop grading get promoted up the scope ladder; skills that don't get TTL-purged after 7 days. Survival is relevance.
Tools don't auto-grade — they auto-save
Forged tools have a different lifecycle: they're either explicitly
saved by the user (Layer 27 — me.* namespace, surviving every
session reset) or they live and die in their session scope. Tools
that earn their keep through the operator's forge_promote move up
the scope ladder; the rest get auto-purged with their session.
The Layer-27 /tool save <name> flow is the explicit path:
operator says "I want this for me, full stop", the tool gets
re-shelved under <corvin_home>/global/forge/tools/me.<name>.py
with the grade-gate bypassed. See memory model
for the full personal-tools story.
The five safety layers
Every step above is wrapped by these. There is no trusted bypass path — neither for the operator, nor for the agent, nor for promoted artifacts.
A. Linter (fail-closed)
Skill bodies + tool source are NFKC-normalized, then folded against a
curated cyrillic-confusable map (so admin and аdmin collide before
detection runs). The linter then walks for:
- prompt-injection markers (
ignore previous, escape sequences) - embedded secrets (high-entropy substrings ≥ 32 chars, bearer-token shapes)
- persona-boundary phrases that try to redefine the agent
- (warning, non-blocking) code density ≥ 40 %
Rejection raises LinterError; the artifact never lands. The linter
is the only safety layer that runs before disk writes — it's what
keeps a hostile agent from planting a payload.
B. Sandbox (bwrap + loopback-deny)
Every tool execution runs in a bwrap mount-namespace + PID-namespace +
network-namespace. Default: no network at all, fresh /tmp, ro /usr,
no home. Personas with network: allow (browser, research) get
--share-net plus a sitecustomize.py shim that patches
socket.socket.connect to refuse 127.0.0.0/8, ::1, and the cloud
metadata IP (169.254.169.254). Loopback-allow is a separate opt-in.
C. Path-gate (PreToolUse hook)
A Claude PreToolUse hook intercepts every Write / Edit / Bash /
WebFetch and denies them when the target path matches forge /
skill-forge / audit / policy locations. Bash detection covers
redirects, tee, sed -i, heredocs, eval, command substitution,
and unbalanced quotes (fail-closed when unparseable + hint string).
This is what keeps "the LLM writing files directly" from being a
bypass of the MCP server.
D. Secret vault (Layer 16 v3)
Operator-owned vault at ~/.config/corvin-voice/secrets.json (mode
0600). Tools declare needed env-vars by name in meta.secrets.
At runtime: per-persona ACL check → vault lookup → env injection
inside the bwrap only. The result envelope walks recursively for
literal values and replaces leaks with <redacted>. The audit
records which secrets were injected, never what.
E. Audit chain
Every state-changing call (tool.created, skill.created,
tool.invoked, skill.outcome_graded, …) emits a hash-chained
record. Tampering is detected by voice-audit verify. See
audit and compliance for the chain
mechanics.
How it integrates with the rest of Corvin
| Adjacent layer | What it gets from runtime generation |
|---|---|
| Memory model | Personal tools (me.*) reuse the forge user-scope. User-style learner reads outcome-grade events emitted by skill-forge. |
| Adapter | skill_inject.collect_active_skills() and personal_tools.format_inject_block() are auto-prepended to the system prompt every turn. |
| Audit | All forge / skill-forge events feed the unified hash chain — same voice-audit verify covers them. |
| Compliance | Vault + path-gate are the structural answer to GDPR Art. 32 (security of processing). |
| Engine layer | Skills land via --append-system-prompt; OpenCode and HermesEngine fall back to a <SYSTEM> block prefix in the user prompt (neither has a system-prompt flag). Hermes M3 roadmap: skills via prompt-inject block. |
Concrete commands
| Action | Where |
|---|---|
| Forge a tool | LLM calls mcp__forge__forge_tool (forge persona) |
| List tools | mcp__forge__forge_list |
| Promote a tool | mcp__forge__forge_promote(name) |
| Save tool to user library | /tool save <name> (Layer 27) |
| Create a skill | mcp__skill_forge__skill_create (forge persona) |
| Grade a skill | automatic (auto-grade + outcome-grade) |
| Promote a skill | mcp__skill_forge__skill_promote(name) |
| Verify audit chain | voice-audit verify |
Where to look in the code
operator/forge/— Forge plugin (registry, runner, MCP server)operator/skill-forge/— Skill-Forge pluginoperator/bridges/shared/skill_inject.py— adapter-side prompt-injection + auto-grade + outcome-gradeoperator/bridges/shared/personal_tools.py— Layer 27 personal-tools registryoperator/voice/hooks/path_gate.py— PreToolUse hookoperator/forge/forge/security_events.py—EVENT_SEVERITYcatalog for all forge / skill-forge / personal-tool events
Adjacent docs
- Architecture — the five orthogonal axes
- Memory model — how personal tools and user-style fit on top of forge + skill-forge
- Audit and compliance — the unified hash chain that ties all five safety layers together
- docs/forge.md — full forge reference (the older, per-feature documentation)
- docs/skills.md — full skill-forge reference
- docs/security.md — six enforcement surfaces in detail