Skills Grow Up: Resources, Permissions, Code


kodr’s first take on skills was deliberately tiny: discover SKILL.md, show a compact index, load the chosen Markdown into the system prompt. Small trust boundary, easy to reason about. These three phases widen what a skill can do - resources, then a permission contract, then actual code execution - each one careful not to widen it by accident.

Phase 66: resources, referenced not loaded

Cramming every bit of specialized guidance into one Markdown file doesn’t scale. So skills can now declare resources in frontmatter:

---
name: project-review
resources:
  - path: docs/checklist.md
    description: Review checklist
  - templates/report.md
---
Use the checklist before writing the report.

The key restraint: kodr lists those resources in --show-skills and the prompt’s available-skills block, but does not load their bodies automatically. A tool-enabled model has to ask for a specific declared resource with read_skill_resource. The loader is strict - the resource must be declared by that skill, the path must stay inside the skill directory, missing resources fail clearly, content is byte-capped. Useful for checklists, templates and examples, while keeping the Markdown-only boundary intact. Still no resource fetching, still no code execution - that’s intentionally later, after stronger sandbox work.

Phase 67: an approval contract

Kodr already had a permission policy, but a denied action was just an error. Safe, but not enough for an interactive harness - the user should see what’s being asked and decide. Phase 67 adds the first shared approval contract. ToolRunner can take a permissionApprover; when policy denies a read, write, command, or network request, the runner builds a structured request:

{ "action": "run_command", "input": { "command": "npm install" },
  "reason": "Command is denied by policy: npm install", "status": "pending" }

Return { "decision": "allow" } and the action proceeds once; deny and the call fails. Crucially, without an approver, behaviour stays fail-closed - non-interactive runs default to deny, so nothing is loosened just because the plumbing now exists. The channel learns permission-request and permission-decision, and the TUI exposes /allow and /deny. It’s not a trust store or a retry engine - it’s the shared message shape and presentation path that later phases (dependency installs, git operations, web prompts, and the very next phase) all reuse.

Phase 68: skill code execution, on a short leash

Now the trust-boundary change, kept narrow. SKILL.md can declare executable helpers:

---
name: project-tools
commands:
  - name: summarize
    path: scripts/summarize.mjs
    description: Print a project summary
---

Kodr shows the command names but never loads or runs script bodies during discovery. A model has to ask for one with run_skill_command, and the execution path stacks every guard it can: the command must be declared by the selected skill, the script path is jailed to that skill’s directory, an active sandbox executor is required, an explicit approver is required, network is disabled, and the workspace is read-only (a Docker --network none read-only mount, or the stronger OpenShell sandbox when configured). Non-interactive paths still fail closed.

The design call I like most: it deliberately does not reuse verification execution. Verification can run package scripts and may use a writable workspace - fine for your code. Skill helpers are untrusted project-adjacent code, so they get the locked-down boundary instead. The goal, stated plainly, is to make helper execution possible without making workspace code execution accidental.

Links: