Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/byoh.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ and fallbacks per enhancement are in

### Named capabilities: real code for free

Everything that is genuinely code — transcript stitching, guard hooks, slug sanitization, shadow
scanning — is a **named capability** a descriptor references. If your harness emits a compatible
stream, you get the full feature from configuration alone:
Everything that is genuinely code — transcript stitching, slug sanitization, shadow scanning —
is a **named capability** a descriptor references. If your harness emits a compatible stream,
you get the full feature from configuration alone:

- `transcript.parser = "claude-stream-json"` — Claude Code `-p --output-format stream-json` events.
- `transcript.parser = "codex-items"` — Codex `item.started`/`item.completed` JSONL.
Expand All @@ -149,11 +149,12 @@ An unknown capability name fails the schema gate listing the allowed values.
### The guard restriction

User-supplied descriptors may **not** declare `[guard]` or set `run.supports_guard = true`: the
write guard installs native hook config into dispatch environments and stays restricted to
built-in descriptors until the guard engine is opened up (fail-open safety). Unguarded runs fall
back to the `detect-stray-writes` audit. A project-local overlay of a guarded built-in is fine —
the restriction applies to the user file's own content, and the embedded guard merges through
underneath it.
write guard **fails open** — a mistyped guard block would silently disarm it — so guard data
stays restricted to the embedded built-in descriptors. Unguarded runs fall back to the
`detect-stray-writes` audit, and `run --guard` with a harness defined only by user descriptors is
rejected in preflight (the run stops rather than continuing silently unguarded). A project-local
overlay of a guarded built-in is fine — the restriction applies to the user file's own content,
and the embedded guard merges through underneath it.

## The workflow

Expand Down
5 changes: 4 additions & 1 deletion docs/claude-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ descriptor references:
| `stream_json.rs` | `-p --output-format stream-json` transcript parsing (`claude-stream-json`) |
| `transcript.rs` | JSONL record shapes + shared tool-call extractors |
| `plugin_shadow.rs` | plugin-shadow detection + isolation banner (`claude-plugins`) |
| `guard.rs` | write-guard hook install + `hookSpecificOutput` deny verdict (`claude-hooks`) |

The write guard has no per-harness code: the descriptor's `[guard]` block (hook file, matcher,
hook-entry and `hookSpecificOutput` verdict templates) is rendered by the generic engine in
`src/adapters/guard.rs`; the hidden `guard` subcommand is its frozen hook entry-point alias.

## Dispatch quirks (all forced by the `claude` CLI)

Expand Down
6 changes: 5 additions & 1 deletion docs/codex-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ references:
|------|--------------|
| `harnesses/codex.toml` | the descriptor — every declarative value + capability references |
| `transcript.rs` | `item.completed` event-stream parsing (`codex-items`) |
| `guard.rs` | write-guard hook install + `{"decision": "block"}` verdict (`codex-hooks`) |

The write guard has no per-harness code: the descriptor's `[guard]` block (hook file, matcher,
hook-entry and `{"decision": "block"}` verdict templates) is rendered by the generic engine in
`src/adapters/guard.rs`; the hidden `guard-codex` subcommand is its frozen hook entry-point
alias.

## Dispatch (`codex exec`)

Expand Down
37 changes: 22 additions & 15 deletions docs/progressive-enhancements.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ only). Only genuinely contradictory flag combinations stay errors.
`src/adapters/descriptor_adapter.rs` — the one generic `DescriptorAdapter` implementing the
trait from a descriptor.
- `src/adapters/capabilities.rs` — the **named capabilities**: closed enums a descriptor references
by kebab-case name for everything that is real code (transcript parsers, guard engines, slug
generation, shadow preflight).
- `src/adapters/<harness>/` — only the code behind those capabilities: transcript parsers, guard
hooks, the plugin-shadow scan, the OpenCode slug sanitizer.
by kebab-case name for everything that is real code (transcript parsers, slug generation,
shadow preflight). The write guard needs no named capability: it is pure `[guard]` data
rendered by the one generic engine in `src/adapters/guard.rs`.
- `src/adapters/<harness>/` — only the code behind those capabilities: transcript parsers, the
plugin-shadow scan, the OpenCode slug sanitizer.
- `run_capabilities()` (descriptor table `[run]`) + `harness_run_preflight()`
(`src/cli/run/util.rs`) — the `run` preflight: undeclared enhancements warn naming their
fallback and adjust the options (`--guard` forced off, missing `skills_dir` forces
Expand Down Expand Up @@ -147,18 +148,24 @@ afterwards.
arm whose subagent read the live skill source instead of its staged copy, which contaminates the
arm; fatal in revision mode, where the `old_skill` arm then sees new-skill content.)

*Descriptor fields:* the `[guard]` table — `engine`, `armed_message` — plus `[tools]` (the
write/patch/shell/read vocabulary) and `run.supports_guard` (validated to stay in lockstep with
the `[guard]` table).
*Capability:* `guard.engine` names the hook installer (`claude-hooks`, `codex-hooks`); each engine
also exposes its hook matcher so validation can prove every hooked tool is declared in `[tools]`.
The guard arbiter and `detect-stray-writes` classify tool names against the cross-harness
vocabulary union (`all_tool_vocabulary`), so wiring a guard or transcript parser without declaring
the harness's tool names is rejected at descriptor load. The hidden `guard` / `guard-codex`
subcommands are the hook entry points — their names are a stable on-disk contract. Shared
*Descriptor fields:* the `[guard]` table — `hooks_file`, `matcher`, `command_template`,
`hook_entry`, `verdict_template`, `armed_message` — plus `[tools]` (the write/patch/shell/read
vocabulary) and `run.supports_guard` (validated to stay in lockstep with the `[guard]` table).
There is no guard code capability: one generic engine (`src/adapters/guard.rs`) renders the
install (hook entry merged into `hooks_file`) and the deny verdict from these templates, whose
authored JSON key order is serialized verbatim — the verdict bytes are the harness's on-disk
contract. Validation proves every hooked `matcher` tool is declared in `[tools]`, that the
templates parse as JSON, and that their `{command}`/`{matcher}`/`{reason}` placeholders sit in
string values. The guard arbiter and `detect-stray-writes` classify tool names against the
cross-harness vocabulary union (`all_tool_vocabulary`), so wiring a guard or transcript parser
without declaring the harness's tool names is rejected at descriptor load. The hidden `guard` /
`guard-codex` subcommands are frozen hook entry-point aliases (a stable on-disk contract); a
future guard-capable built-in uses the generic `guard-hook --harness <label>` entry point — a
descriptor `[guard]` block is all it takes, no bespoke install/verdict code. Shared
marker/manifest/teardown machinery lives in `src/sandbox/`. **User-supplied descriptors may not
declare `[guard]`** (fail-open safety — the engine installs native hook config); the restriction
lifts when the guard engine is opened up.
declare `[guard]`** — the guard fails open, so a mistyped user guard block would silently disarm
it; `--guard` with a harness defined only by user descriptors is rejected in preflight, naming
the `detect-stray-writes` fallback.

### Shadow preflight

Expand Down
10 changes: 9 additions & 1 deletion harnesses/claude-code.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ surfaces_skill_invocation = true
[model]
flag = "--model"

# Guard data rendered by the generic engine (src/adapters/guard.rs). The JSON
# templates' key order is written/serialized verbatim and is an on-disk
# contract with armed hooks from earlier releases — do not reorder keys. The
# `guard` subcommand in command_template is a frozen alias for the same reason.
[guard]
engine = "claude-hooks"
hooks_file = ".claude/settings.local.json"
matcher = "Write|Edit|MultiEdit|NotebookEdit|Bash"
command_template = '"{exe}" guard "{marker}"'
hook_entry = '{"matcher":"{matcher}","hooks":[{"type":"command","command":"{command}"}]}'
verdict_template = '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"{reason}"}}'
armed_message = '''

🛡 Write guard armed: a PreToolUse hook is staged in .claude/settings.local.json
Expand Down
11 changes: 10 additions & 1 deletion harnesses/codex.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ surfaces_skill_invocation = false
[model]
flag = "-m"

# Guard data rendered by the generic engine (src/adapters/guard.rs). The JSON
# templates' key order is written/serialized verbatim and is an on-disk
# contract with armed hooks from earlier releases — do not reorder keys. The
# `guard-codex` subcommand in command_template is a frozen alias for the same
# reason.
[guard]
engine = "codex-hooks"
hooks_file = ".codex/hooks.json"
matcher = "^Bash$|^apply_patch$|^Edit$|^Write$"
command_template = '"{exe}" guard-codex "{marker}"'
hook_entry = '{"matcher":"{matcher}","hooks":[{"type":"command","command":"{command}","timeout":30,"statusMessage":"Checking eval write boundary"}]}'
verdict_template = '{"decision":"block","reason":"{reason}"}'
armed_message = '''

🛡 Write guard armed: a PreToolUse hook is staged in .codex/hooks.json
Expand Down
39 changes: 35 additions & 4 deletions schema/harness-descriptor.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://slow-powers.dev/schemas/harness-descriptor.schema.json",
"title": "Harness Descriptor",
"description": "The data half of a harness adapter: every declarative value a harness exposes, plus references to named code capabilities (transcript parsers, guard engines, slug generation, shadow preflight). Authored as TOML (harnesses/*.toml), validated after transcoding to JSON. Cross-field invariants (guard/banner lockstep, slug/naming-rule agreement, vocabulary coverage) are enforced by the Rust loader on top of this schema.",
"description": "The data half of a harness adapter: every declarative value a harness exposes, plus references to named code capabilities (transcript parsers, slug generation, shadow preflight). The write guard is pure data (the [guard] block) rendered by one generic engine. Authored as TOML (harnesses/*.toml), validated after transcoding to JSON. Cross-field invariants (guard/banner lockstep, slug/naming-rule agreement, vocabulary coverage, guard template contracts) are enforced by the Rust loader on top of this schema.",
"type": "object",
"required": ["label"],
"additionalProperties": false,
Expand Down Expand Up @@ -106,11 +106,42 @@
},
"guard": {
"type": "object",
"required": ["engine", "armed_message"],
"required": [
"hooks_file",
"matcher",
"command_template",
"hook_entry",
"verdict_template",
"armed_message"
],
"additionalProperties": false,
"description": "Write guard: the named hook engine and the post-arm banner.",
"description": "Write guard: the data the generic guard engine renders into a PreToolUse hook and a deny verdict. Restricted to embedded built-in descriptors (the guard fails open, so a mistyped user descriptor would silently disarm it).",
"properties": {
"engine": { "enum": ["claude-hooks", "codex-hooks"] },
"hooks_file": {
"type": "string",
"minLength": 1,
"description": "Hook-config file the entry is merged into, `/`-separated relative to the staged env root (e.g. \".claude/settings.local.json\")."
},
"matcher": {
"type": "string",
"minLength": 1,
"description": "Tool-name matcher the hook registers for; every hooked tool must be declared in [tools] write/patch/shell."
},
"command_template": {
"type": "string",
"minLength": 1,
"description": "Shell command the armed hook runs, with {exe} and {marker} placeholders (e.g. '\"{exe}\" guard \"{marker}\"')."
},
"hook_entry": {
"type": "string",
"minLength": 1,
"description": "JSON template of the hook entry appended to the hook config's hooks.PreToolUse array, with {matcher} and {command} placeholders in its string values; authored key order is written verbatim."
},
"verdict_template": {
"type": "string",
"minLength": 1,
"description": "JSON template of the deny verdict printed on stdout, with a {reason} placeholder; authored key order is the harness's on-disk contract."
},
"armed_message": { "type": "string", "minLength": 1 }
}
},
Expand Down
64 changes: 7 additions & 57 deletions src/adapters/capabilities.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
//! Named code capabilities a harness descriptor references.
//!
//! Everything a descriptor cannot express as data — transcript stitching,
//! guard hook installation, slug sanitization, plugin-shadow scanning — lives
//! behind one of these closed enums. A descriptor opts in by naming the
//! capability (`parser = "codex-items"`, `engine = "claude-hooks"`); a harness
//! whose stream or hooks are compatible with an existing capability gets the
//! full feature from configuration alone.
//! slug sanitization, plugin-shadow scanning — lives behind one of these
//! closed enums. A descriptor opts in by naming the capability
//! (`parser = "codex-items"`); a harness whose stream is compatible with an
//! existing capability gets the full feature from configuration alone. (The
//! write guard needs no named capability: its install and verdict render from
//! the descriptor's `[guard]` data via [`super::guard`].)
//!
//! The enums deserialize from the kebab-case capability names the
//! `harness-descriptor` schema also enumerates, so an unknown name fails the
//! schema gate with a listed-allowed-values message before ever reaching Rust.

use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::path::Path;

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -55,56 +55,6 @@ impl TranscriptParser {
}
}

/// Write-guard engines: install a PreToolUse-style hook under the staged env.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GuardEngine {
/// Hook merged into `.claude/settings.local.json`.
ClaudeHooks,
/// Hook merged into `.codex/hooks.json`.
CodexHooks,
}

impl GuardEngine {
/// The tool-name matcher the engine's hook registers for. Exposed so
/// descriptor validation can prove every hooked tool is declared in the
/// descriptor's `[tools]` vocabulary.
pub(crate) fn hook_matcher(self) -> &'static str {
match self {
GuardEngine::ClaudeHooks => super::claude_code::guard::HOOK_MATCHER,
GuardEngine::CodexHooks => super::codex::guard::HOOK_MATCHER,
}
}

/// Arm the write guard under `stage_root`, returning the staged marker
/// path.
pub(crate) fn install_guard(
self,
stage_root: &Path,
guard_exe: &Path,
ttl: Option<Duration>,
) -> io::Result<PathBuf> {
match self {
GuardEngine::ClaudeHooks => {
super::claude_code::guard::install_guard(stage_root, guard_exe, ttl)
}
GuardEngine::CodexHooks => {
super::codex::guard::install_guard(stage_root, guard_exe, ttl)
}
}
}

/// A hook-config dir the install created outside the skills dir, which
/// teardown prunes when restoring the original config leaves it empty.
pub(crate) fn hook_cleanup_dir(self, stage_root: &Path) -> Option<PathBuf> {
match self {
// The Claude hook lives in .claude/, which staging already owns.
GuardEngine::ClaudeHooks => None,
GuardEngine::CodexHooks => Some(super::codex::guard::hook_cleanup_dir(stage_root)),
}
}
}

/// Staged-slug generators, for harnesses whose naming rules need
/// sanitization/truncation beyond a format string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
Expand Down
Loading
Loading