CorvinOS
July 29, 2026 · View on GitHub
CorvinOS is designed as a platform, not a product. You extend it by dropping files into a configuration tree — no forking, no patching core code, no restart in most cases. The system reads your additions on the next message, grades them against real usage, and promotes the ones that work.
This document is the entry point for five extension surfaces:
| Surface | What it is | Where it lives | Hot-reload |
|---|---|---|---|
| Personas | AI identity, system prompt, tool set, engine choice | operator/cowork/personas/<name>.json | Yes — next message |
| Forge Tools | Sandboxed, bwrap-isolated, MCP-callable Python tools | Chat request or JSON in forge workspace | Yes — MCP hot-register |
| Skills | Markdown instruction files injected into future turns | Chat request or mcp__skill_forge__skill_create | Yes — injected per turn |
| Bridge Adapters | New messaging channels (Teams, Matrix, Signal, custom) | operator/bridges/<channel>/ | Restart needed |
| Workflow Packages | Installable bundles of personas + tools + skills | corvin-pkg install <package.corvin-pkg> | N/A — one-time install |
Every extension event — a new persona loaded, a forge tool created, a skill promoted, a package installed — is written to the hash-chained audit log. Nothing happens silently.
The hot-reload boundary
Most of CorvinOS hot-reloads without a restart. The rule is simple:
| Change | Reload mechanism | Restart needed? |
|---|---|---|
| Persona JSON — any field | Re-read on next message dispatch | No |
chat_profiles in settings.json | Re-read on next message dispatch | No |
whitelist, rate_limit_per_hour | Re-read on every message via currentSettings() | No |
| Forge tool created or promoted | MCP server hot-registers the new tool | No |
| Skill created, graded, or promoted | Injected into the next turn automatically | No |
| Workflow package installed | Takes effect immediately (personas/tools/skills land in place) | No |
| New bridge adapter daemon code | Daemon must be started | Yes — bridge.sh restart |
| Bot token or HTTP port change | Structural config, read at startup | Yes — bridge.sh restart |
Core adapter code (adapter.py) | Python process reloads | Yes — bridge.sh restart |
1. Personas
A persona is a JSON file that defines how the AI behaves in a specific chat context: its identity, its system prompt additions, its tool access, and its engine choice. Drop a file in the right place and it is live on the next message — no restart, no service interruption.
Where persona files go
User override (your deployment only, gitignored, survives git pull):
~/.corvin/cowork/personas/<name>.json
Bundle personas (committed to the repo, visible to all deployments):
operator/cowork/personas/<name>.json
User override personas take priority over bundle personas with the same name.
Minimal example
{
"name": "customer-support",
"description": "First-line support agent for Acme Corp.",
"append_system": "You are a customer support agent for Acme Corp. Always be polite and patient. If the user asks about billing, refunds, or account deletion, say 'I will connect you with our billing team' and stop. Never invent policy details you are not certain of.",
"permission_mode": "bypassPermissions",
"forge_enabled": false,
"skill_forge_enabled": false,
"memory_recall_enabled": true,
"ldd_preset": "off"
}
Full field reference
| Field | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Unique identifier. Used in /pin <name> and in routing config. |
description | string | "" | One-line description. Shown in the console and /personas output. |
append_system | string | "" | Text appended to the system prompt after the base persona block and before the user context block. |
permission_mode | enum | "bypassPermissions" | bypassPermissions / acceptEdits / plan / default. All bundle personas use bypassPermissions — the path-gate hook is the structural security boundary, not this field. |
mcp_servers | object | {} | Additional MCP servers to start for this persona (same schema as --mcp-config). Shallow-merged with global MCP config. |
forge_enabled | boolean | true | Whether forge tools are available in this persona's sessions. |
skill_forge_enabled | boolean | false | Whether SkillForge (runtime skill creation) is enabled. Must be explicitly opted in. |
memory_recall_enabled | boolean | false | Whether recall indexing and user model injection are active. |
working_dir | string | repo root | The --add-dir working directory passed to the underlying engine. |
ldd_preset | enum | "off" | LDD preset (operator/bridges/shared/ldd.py::PRESETS): "off" / "quick" / "default" / "strict". Governs the 10 process-LDD skills only — adr_gate/e2e-wiring-proof are always-on regardless (ADR-0259), toggled via quality_layers.py instead. |
add_dirs | array | [] | Additional --add-dir paths passed to the engine for this persona. |
delegate_enabled | boolean | false | Enables the orchestrator delegation tools (delegate_claude_code, delegate_codex, etc.). |
delegate_inject_skills | boolean | false | Whether active skills are visible to delegated workers. |
delegate_forge_enabled | boolean | false | Whether forge/skill-forge MCP tools are passed to delegated workers. |
Adding a custom MCP server
The mcp_servers field lets you attach any MCP-compatible server to a persona
without touching the global config:
{
"name": "research",
"mcp_servers": {
"acme-kb": {
"command": "{{PYTHON}}",
"args": ["/opt/acme/kb_server.py"]
},
"acme-cloud": {
"command": "npx",
"args": ["-y", "@acme/mcp-server-example"],
"env": {
"ACME_API_KEY": "${ACME_API_KEY}"
}
}
}
}
Template variables are expanded in command/args/env values before the
config is materialized:
{{PYTHON}}—sys.executable, the interpreter that can import CorvinOS's own packages. Use this instead of a barepython3, which does not exist on Windows PATH and resolves via the spawning process's PATH on POSIX (usually a system interpreter without your dependencies).{{REPO_ROOT}}— the directory holdingoperator/(the repo checkout, or the vendored tree inside a wheel install).{{CORE_ROOT}}— the directory holding thecore/package tree (equals{{REPO_ROOT}}in a checkout;site-packagesin a wheel install).{{HOME}}— the running user's home directory.
Note: image generation no longer needs a persona-level mcp_servers entry —
the built-in imagegen-zero-config catalog tool (ADR-0191) is seeded and
active on every installation.
Pinning a persona to a chat
From within the chat:
/pin customer-support
To remove the pin and return to auto-routing:
/unpin
Assigning a persona to a chat profile
For permanent assignment without requiring users to type /pin, add a persona
field to the chat profile in operator/bridges/<channel>/settings.json:
{
"chat_profiles": {
"123456789": {
"persona": "customer-support"
}
}
}
This takes effect on the next message to that chat (hot-reload applies).
Audit trail
When a persona is loaded for the first time in a session, a persona.loaded event
is written to the audit chain with the persona name, channel, and chat key. No
system prompt content is written — metadata only.
2. Forge Tools
Forge tools are schema-bound, sandboxed tools that the AI can create at runtime and
call as MCP tools. They persist across sessions (at project or user scope) and
can do anything a subprocess can do — file I/O, HTTP calls, running scripts — within
the sandbox policy. No code changes to CorvinOS required.
Creating a tool in-chat
The easiest path is to ask:
Create a forge tool called "fetch_weather" that takes a city name and returns
the current weather by calling the Open-Meteo API (no API key needed). Store at
project scope.
The AI calls the forge MCP tool with the schema and implementation. The tool is
immediately available as mcp__forge__fetch_weather in the same session.
Creating a tool manually
Tools are JSON files. Minimal structure:
{
"name": "fetch_weather",
"description": "Fetch current weather for a city using the Open-Meteo API.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. Berlin"
}
},
"required": ["city"]
},
"implementation": {
"type": "python",
"code": "import urllib.request, json\nurl = f'https://geocoding-api.open-meteo.com/v1/search?name={inputs[\"city\"]}&count=1'\nwith urllib.request.urlopen(url) as r:\n geo = json.load(r)['results'][0]\nwurl = f'https://api.open-meteo.com/v1/forecast?latitude={geo[\"latitude\"]}&longitude={geo[\"longitude\"]}¤t=temperature_2m,wind_speed_10m'\nwith urllib.request.urlopen(wurl) as r:\n data = json.load(r)\nprint(json.dumps(data['current']))"
},
"meta": {
"scope": "project",
"secrets": [],
"network": "allow"
}
}
Place the file in the forge workspace, then register it:
mcp__forge__artifact_register path=<tool-file.json>
For the full schema reference, see forge.md.
Scopes
| Scope | Lifetime | Shared across |
|---|---|---|
task | Current turn only | This turn, this session |
session | Until /new or /clear | All turns in the current chat session |
project | Permanent | All sessions in the current project |
user | Permanent | All projects for this user |
To promote a tool to a wider scope:
mcp__forge__forge_promote name=fetch_weather target_scope=user
Sandbox policy
By default, forge tools run in a bwrap sandbox:
- No network access (unless
meta.network: "allow"is set and the persona permits it) - Fresh
/tmpper invocation - Read-only
/usrand/etc - No access to
~/.corvin/, audit chains, or policy files — enforced by the path-gate hook
The persona determines whether network: allow is respected. The browser and
research personas grant network access; all others deny it by default.
Secret injection
If a tool needs an API key or password:
- Store the secret in the vault:
/vault set MY_API_KEY sk-... - Declare the secret name in the tool's
meta.secrets:["MY_API_KEY"] - At execution time, the runner injects it as an environment variable inside the sandbox.
The secret value never appears in the tool definition, the AI context, or any log.
Audit trail
Every forge tool creation, promotion, and invocation writes an event to the audit chain: tool name, scope, persona, channel, and chat key. No implementation code or output content is written.
3. Skills (SkillForge)
Skills are Markdown documents injected into future conversation turns as additional system prompt context. Unlike forge tools (which do things), skills shape how the AI thinks and responds — they encode workflows, checklists, domain-specific reasoning patterns, or project conventions.
Skills are self-improving: the system grades each skill after every turn based on whether the AI applied it, and automatically promotes skills that earn consistent positive grades to wider scopes.
Enabling SkillForge on a persona
SkillForge must be explicitly opted in:
{
"skill_forge_enabled": true
}
The assistant and most bundle personas do not enable this by default.
Creating a skill in-chat
Create a skill called "code-review-checklist" that always checks for:
1. Missing error handling
2. Hardcoded secrets
3. Missing tests
Save at session scope.
The AI calls mcp__skill_forge__skill_create. The skill is immediately injected into
subsequent turns in the same session.
Creating a skill via MCP directly
mcp__skill_forge__skill_create(
name="code-review-checklist",
body="When reviewing code, always check:\n1. Missing error handling...",
scope="session"
)
Linter
All skills pass through a linter before being saved. The linter rejects:
- Prompt-injection patterns (instructions to ignore previous instructions, roleplay as a different AI, reveal system prompts)
- Embedded secrets (API key shapes, private key PEM blocks)
- Persona-boundary phrases (instructions that would override the persona definition)
- Oversized bodies (default limit: 8 KB)
A rejected skill returns the linter's rejection reason. Fix the content and retry.
Grading and promotion
Skills are promoted through a scope ladder based on usage grades:
| Promotion | Requirement |
|---|---|
| task → session | ≥ 1 positive grade |
| session → project | ≥ 3 grades with mean ≥ 0.5 |
| project → user | Explicit force=True in mcp__skill_forge__skill_promote |
Grades are assigned automatically after each turn:
- Mention/paraphrase grade (0.7): the adapter checks whether the active skill was mentioned or applied in the AI response.
- User approval (0.9): the user explicitly approves the behavior guided by the skill.
- User rejection (0.1): the user explicitly rejects the behavior.
- Rephrase (0.3): the user rephrases or corrects the behavior.
You can manually grade a skill:
mcp__skill_forge__skill_grade name=code-review-checklist score=0.9
Managing skills
mcp__skill_forge__skill_list # list all skills at all scopes
mcp__skill_forge__skill_get name=... # read the full skill body
mcp__skill_forge__skill_diff name=... # diff between scope levels
mcp__skill_forge__skill_purge name=... # delete a skill
Audit trail
Skill creation, promotion, grading, and purge all emit audit events with skill name, scope, grade score, and channel/chat context. No skill body content is written to the audit chain.
4. Bridge Adapters
A bridge is a pair: a Node.js daemon that speaks the channel's protocol, and a settings.json that the shared adapter reads to configure routing. Adding a new channel means writing one daemon file and one settings file — no changes to the shared adapter or any other part of CorvinOS.
Minimum requirements
A bridge daemon must:
- Accept messages from the channel and write them to the shared inbox.
- Accept reply envelopes from the inbox and send them back to the channel.
- Expose a
GET /statusendpoint on its assigned port that returns{"status": "ok"}. - Have a
settings.jsonwith at leastwhitelist(array), a token field, and optionallychat_profilesandrate_limit_per_hour. - Have an
install.shthat runsnpm install. - Have an entry in
operator/bridges/bridge.shsobridge.sh up/down/status/tailwork uniformly.
Reference implementation
The simplest complete bridge is the Telegram daemon:
operator/bridges/telegram/daemon.js
It is approximately 400 lines and covers: bot initialization, whitelist checking, rate limiting, the inbox protocol, media handling, command passthrough, and graceful shutdown. Use it as the template for a new channel.
Inbox protocol
The shared adapter communicates with daemons via a JSON newline-delimited inbox. Incoming message envelope (daemon → adapter):
{
"channel": "mybridge",
"chat_key": "mybridge:12345",
"user_id": "12345",
"text": "Hello",
"attachments": [],
"ts": 1716288000.0
}
Outgoing reply envelope (adapter → daemon):
{
"chat_key": "mybridge:12345",
"text": "Hello back",
"audio_path": "/tmp/corvin/tts_12345.ogg",
"is_voice": true
}
Settings hot-reload in your daemon
The daemon must read settings.json on every message — not once at boot — by
calling currentSettings() from operator/bridges/shared/js/settings.js. This
gives you hot-reload for whitelist changes, pin changes, rate limit tuning, and
chat profile updates while the daemon is running.
const { currentSettings } = require('../shared/js/settings');
// inside message handler:
const settings = currentSettings();
if (!settings.whitelist.includes(userId)) return;
Registering in bridge.sh
Add a block to operator/bridges/bridge.sh following the existing pattern for
Telegram or Discord. The script handles start, stop, status, and log tail uniformly
if you follow the naming conventions (<channel>/daemon.js, port on
BRIDGE_PORT_<CHANNEL>).
What needs a restart vs. what does not
| Change | Restart needed |
|---|---|
whitelist, rate_limit_per_hour, chat_profiles in settings.json | No — hot-reloaded |
| Daemon JavaScript code | Yes — bridge.sh restart <channel> |
| Bot token or webhook URL | Yes — bridge.sh restart <channel> |
| Port number | Yes — bridge.sh restart <channel> |
5. Workflow Packages
Workflow packages (.corvin-pkg files) bundle personas, forge tools, skills, and
configuration into a single signed archive. They are the distribution unit for
CorvinOS extensions — share a package and the recipient gets a complete, coherent
capability without manual file placement.
Installing a package
corvin-pkg install <name>.corvin-pkg
The installer:
- Verifies the package signature against the publisher's trusted public key.
- Extracts personas to
~/.corvin/cowork/personas/(user override location). - Registers forge tools at the scope declared in the package manifest.
- Installs skills at the declared scope.
- Emits a
corvin_pkg.installedevent to the audit chain.
Preview what a package contains before installing:
corvin-pkg inspect <name>.corvin-pkg
Creating a package
A package is a signed archive built from a manifest.json:
{
"name": "acme-support-workflow",
"version": "1.0.0",
"description": "Customer support workflow: persona, ticket tool, escalation skill.",
"publisher": "Acme Corp",
"min_corvin_version": "0.9.0",
"contents": {
"personas": ["personas/customer-support.json"],
"forge_tools": ["tools/fetch_ticket.json"],
"skills": ["skills/escalation-checklist.md"]
}
}
Build and sign the package:
corvin-pkg build manifest.json --out acme-support-workflow.corvin-pkg
Package security
Packages require a valid signature to install. The public key for each trusted publisher is pinned in operator config. To add a publisher:
corvin-pkg trust add <publisher-name> <public-key-file.pem>
Unsigned packages are rejected. Packages whose declared min_corvin_version
exceeds your installed version are also rejected with an upgrade prompt.
Package contents land in the user override locations — they are never committed to
the repo automatically and will not be overwritten by git pull.
Extension quick reference
| What you want | How to do it | Where it lives | Restart? |
|---|---|---|---|
| Custom agent behavior | Add or edit a persona JSON | ~/.corvin/cowork/personas/<name>.json | No |
| Attach an MCP server | Add mcp_servers to a persona | Same persona JSON | No |
| Runtime automation tool | Create a forge tool | Chat request or JSON in forge workspace | No |
| Reusable reasoning pattern | Create a skill | Chat request or mcp__skill_forge__skill_create | No |
| New messaging channel | Write a Node.js bridge daemon | operator/bridges/<channel>/ | Yes (daemon start) |
| Shareable bundle | Build a .corvin-pkg | corvin-pkg build | No (one-time install) |
Going further
- forge.md — full forge tool schema, policy fields, and sandbox options
- awpkg.md — workflow package manifest format and signing
- personas.md — auto-routing, LDD presets, and the full persona merge order
- bridges.md — the inbox protocol in detail, media handling, and bridge health checks