Writing packs and skills
Last updated October 5, 2026
On this page
- Packs, modules and skills
- What a pack holds
- Collections
- Tools and their contracts
- Skills and playbooks
- Evals
- Rules
- Agents
- Triggers
- Views
- Thread templates
- The JSON Schemas
- Write a small pack
- Turn it into a company module
- Add a second admin in the demo
- Upload, review, enable
- See the agent and the page
- Describe a module instead
- When an upload is refused
- Installing a pack directly
- Sandboxed WASM skills
- Try one in the demo
- Not possible yet
- Known problems on this page
- For developers
- API
- CLI
- The WASM ABI
- Where it lives
Draft for review
A pack is one YAML file, pack.yaml, that describes a part of a company:
the records it keeps, the agents that work on them, what those agents may do,
the rules that hold them back and the pages people see. Every department in
the demo (CRM, Support, Support operations and the rest) is a pack that ships
with OrchKernel. You can write your own, have another admin review it, and
turn it on like any bundled module.
This page walks through what a pack holds, using the bundled crm and
support-ops packs as examples. Then you write a small pack, upload it as a
company module, have a second admin approve it, and see its agent and page
appear in the demo. What is not possible yet is listed
at the end.
This guide describes packs as they work today on the golive branch.
The steps below were run in the demo company with no model configured. In that case the demo's stub planner answers instead of a model: it knows only the walkthrough's asks and answers anything else with a "No model is configured" note (see Models). Steps that need a real model say so.
Packs, modules and skills#
| Word | What it is |
|---|---|
| Pack | The pack.yaml file: the definition. |
| Module | A pack a company has turned on and configured: bound to its own teams, people and connections. See Modules. |
| Catalog | The list of modules a company can turn on, on the Catalog tab of Modules: the bundled ones and the company's own. |
| Company module | A pack your company wrote, uploaded, had reviewed by a second admin and added to the catalog. |
| Skill | One thing an agent knows how to do, with its permissions, its playbook and its eval cases. Skills live inside a pack. |
| Playbook | A skill's instructions in fixed sections (goal, steps, guardrails and so on). See Agents. |
| Eval | A test case for a skill: a request and the plan the agent must make for it. See Changing a skill or playbook. |
| Proposal | A change waiting in the change pipeline under Governance > Changes. An uploaded pack becomes one. |
| Promote | The last step of the change pipeline: the reviewed change takes effect. For an uploaded pack, it adds the module to the catalog. |
The usual path is: write a pack, upload it, a second admin reviews and promotes it into the catalog, then an admin enables it. A pack can also be installed directly without becoming a module; see Installing a pack directly.
What a pack holds#
The bundled packs are in packs/<id>/pack.yaml in the source tree. Every
section except id and name is optional.
| Section | What it holds | Example in the bundled packs |
|---|---|---|
id, name, version, description |
Who the pack is. version starts at 1. |
id: crm, name: CRM |
requires |
Other catalog ids this pack builds on. It cannot be enabled until they are. | support-ops has requires: [support] |
collections |
The kinds of records it keeps, with fields, who may read and write them, and how sensitive they are. | CRM's leads and contacts |
tools |
Calls to outside systems, each a contract with argument and result schemas. | CRM's gmail.send, Support operations' support-ops.billing.refund |
skills |
What agents can do: instructions, a playbook, permissions and evals. | CRM's lead-management |
rules |
Policy that holds or refuses actions. | crm-agent-email-needs-rep |
agents |
The agents, each with its skills, team, budget and trust. | Sales Assistant |
triggers |
Schedules, webhooks and record changes that start work. | crm-morning-followups, weekdays at 08:00 |
views |
The pages in the sidebar: lists, record pages and dashboards. | CRM's Leads, Lead and Pipeline pages |
thread_templates |
Thread templates offered under New thread. | Product operations' Release |
Collections#
A collection is a kind of record, like a table. Each one has a schema with
a base type and fields, a policy and a data_class.
collections:
- name: leads
data_class: { label: pii.customer, sensitivity: restricted }
schema:
base: organization
fields:
- { name: name, type: string, required: true, synonyms: [company, account] }
- { name: stage, type: enum, values: [new, contacted, qualified, negotiation, won, lost] }
- { name: owner, type: actor_ref, description: Rep who owns the lead }
- { name: value, type: money }
policy:
read_teams: ["team:sales"]
write_teams: ["team:sales"]
read_rows: { rule: owner_or_role, owner_field: owner, roles: [sales_lead, admin] }
write_rows: { rule: owner_only, owner_field: owner }
semantics:
description: Sales leads and where each one is in the pipeline
example_questions: ["what are my follow ups today"]
| Setting | Values |
|---|---|
base |
person, organization, work_item, event, document, decision, place, money, or thing |
Field type |
string, text, int, float, bool, date, date_time, money, enum (with values), ref (with collection), refs, actor_ref, list (with of), json |
read_rows, write_rows |
any; owner_only (with owner_field); owner_or_role (with owner_field and roles); owner_or_team_field |
sensitivity |
public, internal, confidential, restricted |
synonyms, description and semantics are what agents read to understand
the collection, so write them in the words people use. The team names in
read_teams and write_teams are the pack's own; when the pack is enabled,
the admin binds each to a real team. See The brain for how row
rules play out.
A skill that reads a collection must also list the collection's data class
label under data_classes in its permissions when the collection is
confidential or restricted (CRM's skill lists pii.customer to read
leads). Without it the read is refused.
Tools and their contracts#
OrchKernel ships no connectors of its own. A pack declares each tool as a contract against a named server: the tool name on that server, the arguments it takes and the result it returns. Any server that meets the contract works, which is how the demo's local fakes stand in for Gmail or Stripe.
tools:
- id: support-ops.billing.refund
name: Issue a refund or credit
provider: { type: mcp, server: billing, tool: refund }
risk_tier: critical
data_class: { label: financial, sensitivity: confidential }
external_side_effect: true
rate_limit_per_min: 5
arg_rules:
- { rule: in_read_results, path: invoice }
args_schema:
type: object
properties:
invoice: { type: string }
amount: { type: number, minimum: 0 }
reason: { type: string }
required: [invoice, amount, reason]
additionalProperties: false
result_schema:
type: object
properties: { id: { type: string } }
required: [id]
Name new tools <pack>.<server>.<tool> so two modules on one server never
collide. arg_rules are checked before anyone is asked to approve a call:
| Rule | The argument must |
|---|---|
in_read_results |
Appear in a record or tool result the run has read (an invoice id, an email address). |
company_domain |
Be an address on one of the company's domains. |
listed_in_connection |
Be listed under key in the module's connection settings. |
not_repeated |
Not repeat an earlier call's value (one refund per invoice). message sets the refusal for a call that went through, pending for one still waiting for approval. |
When a tool has more than one of the first three rules on the same
argument, passing any one is enough. not_repeated must pass as well.
How a server is bound to a tool is in Connections.
Skills and playbooks#
A skill says what an agent may touch and how it should work.
| Field | What it does |
|---|---|
instructions |
Plain instructions, used when there is no playbook. |
playbook |
The structured instructions: goal, when, inputs, steps, guardrails, approval_points, outputs, measures and run. |
tools |
The pack's tools this skill may call. |
permissions |
read_collections, write_collections, data_classes, memory_write_scopes, may_delegate. A skill can name collections another pack owns; that pack's row rules still apply. |
risk_tier |
low, medium, high or critical. |
handles |
Words that route a request to this skill. |
code |
{ type: declarative }: a model plans each run. Or a sandboxed skill, below. |
evals |
Test cases for the skill, below. |
CRM's lead-management skill is a good model: one step per thing the rep
asks for, guardrails such as "Never change a stage to lost without an explicit
instruction from the rep", and an approval point for every email. The
playbook's sections, and how single and loop runs differ, are explained in
Agents.
Evals#
An eval case gives the skill a request and says which operations its plan should start with. The eval gate runs them whenever the skill or its playbook is changed; see Changing a skill or playbook.
evals:
- name: follow-ups-query-is-scoped-by-date
input: { ask: "what are my follow ups today" }
expect: { ops: [ { op: query, collection: leads } ] }
By default the expected operations must be the first ones, in order. Add
match: contains when they may come anywhere in the first round. Give every
skill at least one eval: a later change to its playbook is refused without
one. A model must answer for an eval to pass, so with the demo's stub planner
evals show as "not run".
Rules#
Rules are the pack's policy. In a company module they can only tighten.
rules:
- id: crm-agent-email-needs-rep
description: Agents never send email without the rep approving the draft
effect: require_approval
approver: { approver: acting_for }
matches: { tools: [gmail.send], agents_only: true }
priority: 10
effect is deny, require_approval or allow. approver is
acting_for (the person the agent works for), owner, manager,
role (with role) or actor (with id). matches can name tools,
actors, collections, actions (such as brain.upsert(leads)),
agents_only, min_risk and an amount cap (amount_over, in cents). See
Policy for how rules are weighed.
Agents#
agents:
- id: sales-assistant
name: Sales Assistant
kind: agent
role: sales_assistant
description: Works each rep's pipeline on their behalf
skills: [lead-management]
tools: [gmail.send, calendar.create]
budget: { limit: 5000 }
concurrency: 2
memory_scopes: [team]
team: team:sales
trust_tier: standard
budget.limit is in cents (5000 shows as $50.00). In a module, who the agent
acts for comes from the Enable sheet, not from the pack. In a pack installed
directly, acts_for: [alice] sets it.
Triggers#
kind |
Starts work when | Extra field |
|---|---|---|
cron |
A schedule comes round | schedule: every:30m, every:2h, hourly, daily@08:00, weekdays@08:00 |
webhook |
An outside system posts an event | source, named <pack>.<source> |
data_change |
A record in a collection is written | collection |
Each trigger has an id, a name and a template for the task it creates:
title, owner (required), assignment, priority, skill_hint, labels
and sla_hours. Support operations' churn watcher has a cron sweep and a
data_change trigger on tickets; Founder's hiring coordinator has a
webhook trigger with source founder.careers. In the demo, module triggers
start off; see Modules.
Views#
A view is a page. kind is list, detail (one record) or dashboard.
section and order place it in the sidebar of a pack installed directly; a
module's pages go in the section the Enable sheet sets. The spec is a list
of blocks:
| Block | Shows |
|---|---|
header |
Title, subtitle, tags, facts, and buttons such as ask: { agent: sales-assistant, text: "..." } |
table, list |
Rows from a query, optionally linking to a detail view |
fields |
A record's fields |
refs |
Everything that refers to a record: messages, runs, approvals, decisions |
tiles |
Numbers from aggregates (count, sum, avg) |
chart |
Bar or line charts over a grouped aggregate |
text, actions |
Text and buttons |
A view only shows to people who can read its collection. A detail view's
blocks can use $record.<field>, and filters can use $today. CRM's
crm.pipeline dashboard uses most of them.
Thread templates#
thread_templates:
- name: release
label: Release
description: One release, from drafted notes to publication
slots:
- { name: pm, role: owner, kind: person, label: Product manager }
- { name: eng_lead, role: approver, kind: person, label: Engineering lead }
- { name: release_notes_writer, role: contributor, kind: agent, default: release-notes-writer, label: Release-notes writer }
artifacts: [[release_notes, release_notes]]
requires_approval_to_close: true
agent_chatter_cap: 3
What each part means is in Threads. A module's template appears under New thread while the module is enabled.
The JSON Schemas#
ok schemas prints the JSON Schema of every definition type as one JSON
object, and GET /api/schemas returns the same. Point your editor's YAML
support at the relevant part to get completion and checks while you write.
ok schemas > schemas.json
Run it in a scratch folder: it also creates an empty ./.orchkernel/state.db
where you run it (a bug, see Known problems).
| Key | Covers |
|---|---|
collection |
A collection: schema, policy, semantics, data class |
skill |
A skill, its permissions, code, playbook and eval cases |
playbook |
A playbook on its own |
test_case |
One eval case |
tool |
A tool, its provider and argument rules |
rule |
A rule, its match and approver |
actor |
People, teams and agents |
| others | Tasks, threads, events, delegations, policy bundles and the gateway's types |
There is no schema for the pack file as a whole, for views, triggers or thread templates. The upload checks those and lists every problem it finds.
Write a small pack#
This pack adds an Equipment requests collection for the Engineering team,
an agent that logs requests, a rule and a page. Save it as pack.yaml in a
folder of its own, for example equipment/pack.yaml.
id: equipment
name: Equipment requests
version: 1
description: Laptops, monitors and other kit people ask for, with an agent that logs and sorts the requests.
collections:
- name: equipment_requests
storage: native
data_class: { label: internal, sensitivity: internal }
schema:
base: work_item
fields:
- { name: item, type: string, required: true, synonyms: [kit, device], description: What is being asked for }
- { name: requester, type: actor_ref, description: Who asked }
- { name: status, type: enum, values: [new, approved, ordered, delivered, declined] }
- { name: cost, type: money, synonyms: [price] }
- { name: needed_by, type: date }
policy:
read_teams: ["team:eng"]
write_teams: ["team:eng"]
read_rows: { rule: any }
write_rows: { rule: any }
semantics:
description: Equipment people have asked for and where each request stands
synonyms: [kit requests, hardware]
example_questions: ["what equipment is waiting", "log a monitor for eli"]
searchable: true
skills:
- id: equipment-intake
name: Equipment intake
version: 1
description: Log equipment requests and list the ones still waiting.
instructions: |
Log each equipment request in equipment_requests with status new.
List open requests when asked. Never set a request to approved.
permissions:
read_collections: [equipment_requests]
write_collections: [equipment_requests]
memory_write_scopes: [team]
risk_tier: low
handles: [equipment, laptop, monitor, kit]
code: { type: declarative }
playbook:
goal: Every equipment request is logged once, with who asked and by when, so nothing is lost in chat.
when:
prose: Someone asks for kit or asks what is waiting.
triggers: [mention]
inputs:
- The open equipment_requests
- What the person asked, in their words
steps:
- name: Check
do: Query equipment_requests for the same item and requester so a request is not logged twice.
- name: Log
do: Upsert the request with status new, the requester and needed_by.
- name: Report
do: Output the request, or the list of open requests when that was the question.
guardrails:
- Never set a request to approved, ordered or declined; a person does that.
approval_points:
- Approving a purchase is a person's decision, made on the record.
outputs:
- A logged request or the list of open requests
measures:
- name: waiting
description: Requests not yet approved or declined
source: { collection: equipment_requests, filter: { op: eq, field: status, value: new }, metrics: [{ op: count, as: n }] }
value: n
run: { mode: single }
evals:
- name: a-request-is-checked-first
input: { ask: "Eli needs a second monitor by Friday" }
expect: { ops: [ { op: query, collection: equipment_requests } ] }
rules:
- id: equipment-helper-never-deletes
description: The equipment helper never deletes a request
effect: deny
matches: { actors: [equipment-helper], actions: ["brain.delete"] }
priority: 10
agents:
- id: equipment-helper
name: Equipment helper
kind: agent
role: equipment_helper
description: Logs equipment requests and lists what is waiting
skills: [equipment-intake]
budget: { limit: 1000 }
concurrency: 1
memory_scopes: [team]
team: team:eng
trust_tier: sandboxed
views:
- id: equipment.requests
name: Equipment requests
kind: list
collection: equipment_requests
section: Engineering
order: 50
icon: list
spec:
blocks:
- type: header
title: Equipment requests
subtitle: What people have asked for, oldest need first
actions:
- { label: Ask Equipment helper, ask: { agent: equipment-helper } }
- type: table
source: { collection: equipment_requests, order: { field: needed_by, ascending: true } }
columns: [item, requester, status, cost, needed_by]
Two things a first pack often gets wrong:
- A rule in a company module must name one of the module's own agents, tools
or collections in
matches. A rule that only names an action such asbrain.upsert(equipment_requests)is refused with "match names none of the module's agents, tools or collections". - The agent's
teammust be a team the pack's collections or rules name (team:enghere). At enable time an admin binds it to a real team.
Turn it into a company module#
A company module needs two human admins: one uploads it, another reviews and promotes it. The demo has only Ada, so add a second admin first.
Add a second admin in the demo#
While ok demo serve runs, add people through the API. A command-line change
to the state file is overwritten by the running server; see
Setting up a company.
-
Copy Ada's token from the list
ok demo serveprinted, and the port from its Workspace line (8080 unless you started it with--bind). In a terminal:ADA=orchk_... # Ada's token OK=http://127.0.0.1:8080 -
Add Dana as an admin and give her a token:
curl -X POST $OK/api/actors \ -H "Authorization: Bearer $ADA" -H 'content-type: application/json' \ -d '{"actor":{"id":"dana","name":"Dana Ruiz","kind":"human","role":"admin"}}' curl -X POST $OK/api/tokens \ -H "Authorization: Bearer $ADA" -H 'content-type: application/json' \ -d '{"actor":"dana","label":"docs"}'The first call answers with Dana's record (
"role":"admin"). The second answers with her token in"token":"orchk_...", valid 30 days. Keep it for step 3 below.
Upload, review, enable#
-
Sign in as ada. Open Modules and choose Upload a module. The sheet says "A pack.yaml of at most 256 KB. Its skills must be declarative, its ids new, and its rules may only tighten. Trust and ownership in the file are ignored." Choose your
pack.yamlwith the file picker, or paste the YAML into the box below it, then choose Upload for review.The sheet now says "Filed module equipment v1 (uploaded): 1 collections, 1 agents, 0 tools as proposal . Another admin reviews the card below and promotes it from Governance › Changes; enabling it is a separate step." Below it is the Review card: the collection and its data class ("equipment_requests: internal, internal"), the tools ("none"), the skill's reads and writes, the agent and its team, the triggers ("none"), the rule ("deny on agents equipment-helper; actions brain.delete"), the page and the section it asks for ("in Engineering"), what it requires ("nothing"), and the file's sha256 fingerprint, so the reviewer can tell exactly which file they approved.
-
Still as Ada, open Governance, tab Changes. The card module equipment v1 (uploaded), Proposed, "proposed by Ada Park", shows Approve and Reject. Ada cannot approve her own upload: choose Approve, then Approve in the sheet, and the sheet shows "policy denied change.review: ada proposed this module; another admin reviews and promotes it". Choose Cancel.
-
In a private browser window, open the workspace and sign in with Dana's token. Open Governance, tab Changes. The same card is there, with a content section that opens to the full pack. Choose Approve. The sheet "Approve this change" asks for an optional Note; type one if you like and choose Approve. Within a few seconds the card reads Reviewed, shows "Dana Ruiz approved: ", and offers Promote.
-
Choose Promote. The card reads Promoted. The module is now in the catalog. Nothing is turned on yet.
-
Back as ada, open Modules. The Catalog tab shows Equipment requests with the badges Uploaded and Reviewed by your company, "1 collection · 1 agent · 1 page", and Enable.
-
Choose Enable. The sheet Enable Equipment requests has four parts:
Part What you see Teams A drop-down "Pick a team…" for team:eng. A company module's teams are not picked for you.Agents "Equipment helper · acts for nobody · $10.00 · sandboxed · on" Connections "This module calls no tool servers." Pages A tick box for Equipment requests and a box for its sidebar section, filled in with the module's name. Enable is greyed out and the sheet says "Bind team:eng to enable." Under Teams, pick Engineering (team:eng). The sheet then says "equipment-helper joins Engineering and can read dependencies, incidents, pull_requests, equipment_requests", and the agent line changes to "Equipment helper · acts for Engineering · $10.00 · sandboxed · on".
-
Choose Enable. The module page opens: state Enabled, health Ok, version v1, set up by Ada Park.
See the agent and the page#
-
Sign in as eli (or erin), who are on Engineering. The sidebar has a new section, Equipment requests, with the page Equipment requests. The section comes from the Enable sheet's Pages part; the pack's
section: Engineeringis not used for a module.Ada does not see the page: she is not on Engineering, and a page shows only to people who can read its collection.
-
Open the page. It shows the header, the subtitle, the Ask Equipment helper button and an empty table ("No rows.").
-
Choose Ask Equipment helper. The page New conversation with Equipment helper opens. Type "I need a second monitor by Friday" and choose Ask agent.
With the stub planner, the agent answers "No model is configured, so the demo's stub planner answered. ..." and the run shows Done. With a real model the agent plans from the playbook: it queries the collection, then logs the request.
-
To see the agent write without a model, add
?dev=1to the end of the conversation's address and reload. Choose Explicit plan next to the agent's name; a second box appears. Type a message in the message box ("Log a second monitor for me"), paste this plan into the second box, and choose Ask agent:{"ops":[{"op":"query","collection":"equipment_requests","bind":"existing"}, {"op":"upsert","collection":"equipment_requests","fields":{"item":"Second monitor","requester":"eli","status":"new","needed_by":"2026-10-09"}}, {"op":"output","value":"Logged: second monitor for Eli"}]}The agent answers "Logged: second monitor for Eli" and the run shows Done. Open the page again: the table lists "Second monitor", "Eli Novak", "New", no cost, "Fri, Oct 9". At phone width the table scrolls sideways inside its card.
-
Open Directory, click Equipment helper and choose Open agent page. The header shows the module Equipment requests and Active. The Overview lists trust Sandboxed, the skill "Equipment intake v1", acts for Eli Novak and Erin Walsh, "$0.00 of $10.00" spent, and the playbook's goal. Acts under shows one delegation per person, "Via Equipment requests". The Playbook tab shows the sections. See Agents.
From here the module behaves like a bundled one: pause it, change who the agent acts for, or roll back its record on its page. See Modules.
Describe a module instead#
Describe a module on the Modules page asks the Architect (a built-in agent) to draft a pack from a sentence and file it as a proposal; any human admin, including you, may review an Architect draft. It needs a real model. In the demo it fails today: after Draft it the sheet says "The Architect did not draft a module: no native skill registered as architect", with or without a model (a bug, see Known problems).
When an upload is refused#
The upload lists every problem at once. The ones you are most likely to see:
| Message | Fix |
|---|---|
version_not_higher: equipment v1 is not higher than the catalog's v1 |
Raise version to upload a new version. |
skill ...: wasm code is refused; company modules run declarative skills only |
Use code: { type: declarative }. |
rule ...: match names none of the module's agents, tools or collections |
Name one of the pack's own objects in matches. |
rule ...: approver role ...: module rules may name only the admin role, the owner or the manager |
Use role: admin, owner or manager. |
agent ... acts for ...; delegations come only from the modules record |
Remove acts_for; choose who it acts for on the Enable sheet. |
agent ... is on team ..., which none of the module's collections or rules name |
Use a team the collections or rules name. |
collection ... already exists (or tool, skill, rule, actor, trigger, thread template, page) |
Choose ids no other module uses. Prefix them with the pack id. |
trigger ...: webhook source ... is not namespaced <pack>.<source> |
Name the source <pack>.<source>. |
yaml: ...: missing field ... at line ... column ... |
The file does not match the pack format; the message names the field and line. |
too_large: the module is ... KB of YAML; the limit is 256 KB |
Keep the YAML under 256 KB. |
What the kernel changes on upload, whatever the file says:
| In the file | Becomes |
|---|---|
trust |
Verified ("Reviewed by your company") once promoted |
| Every tool | An external side effect, at least high risk. On the review card the reviewer can pick a lower risk and choose Lower before promoting. |
Every agent's trust_tier |
sandboxed; an admin changes it on the Enable sheet or the module's Agents tab |
| Each collection's owner | The pack |
A collection may be restricted; the card marks it so the reviewer sees it.
Installing a pack directly#
An admin can install a pack without making it a module. This skips the second admin's review, so use it for development and for sandboxed skills, and use a company module for anything people depend on.
| Company module | Direct install | |
|---|---|---|
| Reviewed by a second admin | Yes | No: any human admin installs it |
| Listed on the Modules page, pausable, configurable | Yes | No |
| Who an agent acts for | Chosen on the Enable sheet | The agent's acts_for in the file |
| Sandboxed (WASM) skills | Refused | Allowed |
Pack trust |
Verified | As written; unverified (the default) may not declare a confidential or restricted collection |
| A collection id already taken | Refused | Depends on --on-collision |
While the server runs, install over the API: POST /api/packs with
{ "path": "<folder holding pack.yaml, on the server's machine>" } or
{ "yaml": "<pack.yaml text>" }. It answers
{"pack":"equipment","collections":[...],"skills":1,"agents":1}.
From the command line, with the server stopped (the CLI writes the state file directly):
ok --as ada pack install path/to/equipment # the folder holding pack.yaml
ok --as ada pack list # id, name and version of every installed pack
Use your company's admin in --as (ada in the demo), and --state for a
state file other than ./.orchkernel/state.db (the demo's is
orchkernel-demo/state.db). It prints installed pack equipment (1 collections, 1 skills, 1 agents); pack list prints lines such as
equipment Equipment requests v1.
--on-collision is fail (the default: refused with "collection ...
already exists (owned by ...)" if another pack or the company owns the
collection), merge (adds the new fields), replace, or
namespace:<prefix>. Reinstalling the same pack replaces its own
collections. An unverified pack with a restricted collection is refused with
"pack invalid: unverified pack declares collection ... with data class ...".
Sandboxed WASM skills#
A skill can run code instead of asking a model to plan. The code is compiled to WebAssembly (WASM) and runs inside the kernel with a fuel limit, a memory limit and a cap on calls. It reaches the world only through one host function, and every call it makes goes through the same policy as an agent's plan: the skill's permissions, delegations, rules and approvals. It needs no model, so it works the same in the demo.
skills:
- id: hello-wasm
name: Hello in WASM
version: 1
description: Says hello and counts the leads it can read.
permissions:
read_collections: [leads]
data_classes: [pii.customer]
risk_tier: low
handles: [hello]
code: { type: wasm, path: hello.wasm } # relative to the pack folder
evals:
- name: greets-by-name
input: { name: "Ada", fixtures: { brain.query: [ { id: "l1" }, { id: "l2" } ] } }
expect: { greeting: "hello Ada", existing: 2 }
code can also be { type: wat, source: "..." }, WebAssembly text inline,
for tests and tiny skills. A missing .wasm file is refused at install
("skill hello-wasm references missing wasm ...").
Try one in the demo#
-
Build the example in
examples/skills/hello-wasmwithcargo build --release --target wasm32-unknown-unknown(this needs Rust with thewasm32-unknown-unknowntarget). The file istarget/wasm32-unknown-unknown/release/hello_wasm.wasm. -
Make a folder
hello, copy the file into it ashello.wasm, and save this ashello/pack.yaml: the skill above plus an agent that acts for Alice.id: hello name: Hello version: 1 skills: # the hello-wasm skill above, indented under skills: agents: - id: hello-bot name: Hello bot kind: agent role: hello_bot skills: [hello-wasm] acts_for: [alice] team: team:sales -
Install it with Ada's token, giving the folder's full path:
curl -X POST $OK/api/packs -H "Authorization: Bearer $ADA" \ -H 'content-type: application/json' -d '{"path":"/full/path/to/hello"}'It answers
{"agents":1,"collections":[],"pack":"hello","skills":1}. -
Ask the agent as Alice, with her token in
$ALICE:curl -X POST $OK/api/ask -H "Authorization: Bearer $ALICE" \ -H 'content-type: application/json' -d '{"agent":"hello-bot","text":"hello"}'The run finishes
"outcome":"done"with the output{"existing":4,"greeting":"hello stranger"}. Its steps show the skillhello-wasmselected, abrain_query, and a note "looking up leads" (the guest'slogcall).existingis how many leads the query returned: Alice's four. The guest greets "stranger" because it reads a top-levelname, and a real run's input has none; see the ABI below.Leave out
data_classes: [pii.customer]and the query is refused, because leads are restricted: the run still finishes, with"existing":0, and nothing in the run says the read was refused.
Evals for a sandboxed skill run the code against a stand-in host: the
case's whole input is passed to the guest, brain.query and
directory.find answer from input.fixtures, and any call that would change
something is refused. The case above passes when the skill is proposed as a
change.
Not possible yet#
- Upgrading an enabled company module to a newer version. Uploading v2 and promoting it works, and the catalog card shows Newer version available, but the module record's version cannot be changed ("equipment version 2 is not the installed version 1") and there is no Upgrade button.
- Sandboxed (WASM) skills in a company module.
- A JSON Schema for the whole pack file, or for views, triggers and thread templates.
- Running a module's skill evals when it is uploaded. The proposal shows no eval results, and a skill with a playbook and no evals is accepted.
- Retiring a company module that is enabled. It is refused with "catalog_in_use: a modules record uses equipment; pause the module instead".
Known problems on this page#
- Describe a module always fails in the demo. The Architect's skill is
registered only when the company is first created, so after any restart
(every
ok demo serveis one) Draft it fails with "no native skill registered as architect". ok schemascreates a state file. It writes./.orchkernel/state.dbin the current folder, although it only prints schemas.- Plural slips in the upload note. "1 collections, 1 agents, 0 tools",
in the UI and in
ok changes list. - A refused read in a sandboxed skill leaves no trace. The guest gets an error and the run shows no step for it.
- The unverified-pack refusal names the wrong thing. It gives the data class label ("with data class internal") rather than the sensitivity that caused it.
- The hello-wasm example always greets "stranger" in a run. Only an eval, which passes the case's input as it is, can give it a name.
For developers#
API#
All under /api, with Authorization: Bearer <token>. Every catalog route is
for human admins.
| Method and path | Body | Notes |
|---|---|---|
GET /schemas |
The same JSON as ok schemas. |
|
GET /packs |
Installed packs: id, name, version. |
|
POST /packs |
{ path?, yaml?, on_collision? } |
Direct install. Human admins only. Returns { pack, collections, skills, agents }. |
GET /catalog |
Every module: id, version, versions, source (bundled, uploaded, architect), trust, state, installed, newer_version_available, summary. |
|
POST /catalog/uploads |
{ yaml } |
Files a module proposal. Returns { proposal, state, summary, card }. 413 too_large, 422 invalid with problems, 409 version_not_higher. |
POST /catalog/drafts |
{ text } |
Asks the Architect to draft a module. |
GET /catalog/proposals/:id/card |
The review card. | |
POST /catalog/proposals/:id/lower |
{ tool, risk_tier, external_side_effect } |
Lower a tool's risk on the card before promotion; never above high. |
DELETE /catalog/:id |
Retire an unused company module. 409 catalog_in_use while enabled. |
|
POST /changes/:id/review |
{ approve, note? } |
A second admin reviews. The proposer is refused with "policy denied change.review". |
POST /changes/:id/reject |
{ note? } |
Reject the proposal. |
POST /changes/:id/promote |
Adds the reviewed module to the catalog. | |
POST /modules/:pack/enable |
{ record: { teams: { "team:eng": "team:eng" }, ... } } |
Enable; see Modules. |
GET /thread-templates |
Includes enabled modules' templates. |
CLI#
ok schemas # every definition's JSON Schema
ok pack install <dir> [--on-collision fail|merge|replace|namespace:<prefix>]
ok pack list
ok catalog list # id, version, source, trust, state, name
ok catalog upload <pack.yaml> # file a module proposal
ok catalog retire <id>
ok changes list | review | promote # the second admin's review
ok module enable <pack> --team team:eng=team:eng
These work on the state file; stop the server first or use the API.
The WASM ABI#
The guest exports:
memory
alloc(len: i32) -> ptr: i32
run(input_ptr: i32, input_len: i32) -> packed: i64 // (ptr << 32) | len
run receives JSON. In a run it is {"task": {"id", "title", "description", "inputs"}, "me": "<actor acted for>"}; in an eval it is the
eval case's input. It returns JSON, which becomes the run's output.
Returning {"await_approval": "<id>"}, or an error starting with
approval_required:, parks the run; once approved the guest runs again and
gets the approved result for the same call.
The guest imports one function from module ok:
call(op_ptr, op_len, arg_ptr, arg_len) -> packed: i64
It answers {"ok": value} or {"err": "message"}.
op |
Argument |
|---|---|
brain.query |
{ collection, query: { filter, order, limit } } |
brain.aggregate |
{ collection, filter, group_by, metrics, limit } |
brain.upsert |
{ collection, id?, fields } |
brain.remember |
{ scope, content, topics, confidence } |
tool.call |
{ tool, args } |
llm.complete |
{ system, prompt, json } |
directory.find |
{ skills: [...] } |
thread.post |
{ body } |
log |
Any string; becomes a note on the run |
Limits: 50 million fuel, 16 MiB of memory and 200 host calls per run.
Where it lives#
| What | Where |
|---|---|
| The bundled packs | packs/*/pack.yaml |
| Pack loading and checks | crates/ok-skills/src/pack.rs |
| The sandbox | crates/ok-skills/src/sandbox.rs, host calls in crates/ok-kernel/src/runner.rs |
| Company module rules and the review card | crates/ok-kernel/src/modules/catalog.rs |
| The JSON Schemas | ok_core::json_schemas in crates/ok-core/src/lib.rs |
| The WASM example | examples/skills/hello-wasm |