From f2501ab3bfe415f2748eb1fa015d459401757aa7 Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:22:41 +0000 Subject: [PATCH 1/6] docs(spec): add L0 store-layout spec + store.json schema ~/.agents coexistence rules: harness/ as the sole owned root key (packages/, data/, extensions.lock, config.toml, audit.log, store.json, tmp/, .lock inside), skills/ adopted per Agent Skills spec, plugins/ never touched, mcp.json merge-only, skills-lock read-only. Pins Extension/ExtensionKind/ManifestRef/Capabilities. Write mutex, atomic-write protocol, and skills.sh ownership reconciliation. --- spec/store-layout.md | 273 ++++++++++++++++++++++++++++++++++ spec/store-layout.schema.json | 55 +++++++ 2 files changed, 328 insertions(+) create mode 100644 spec/store-layout.md create mode 100644 spec/store-layout.schema.json diff --git a/spec/store-layout.md b/spec/store-layout.md new file mode 100644 index 0000000..83aecec --- /dev/null +++ b/spec/store-layout.md @@ -0,0 +1,273 @@ +# AnyHarness store layout (L0) + +**Spec version: 0.1.0-draft** + +This document defines how AnyHarness coexists inside `~/.agents/`: which paths +it owns, which paths it shares, and which paths it must never touch. It is a +delta on existing conventions — it codifies what already exists and reserves +exactly one root key, `harness/`. It does not assert a universal `~/.agents/` +layout. + +The machine-readable companion for the store descriptor is +`store-layout.schema.json`. + +## 1. Conformance language + +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and +OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and +only when, they appear in all capitals. + +## 2. Terminology + +| Term | Meaning | +| ---- | ------- | +| **Extension** | The installed unit of the store. One Extension occupies exactly one directory under `~/.agents/harness/packages//`, or — for `kind: "skill"` materialized into the shared root — one directory under `~/.agents/skills//`. An Extension's contents are described by its manifest (see `manifest.md`) and its installed state by `extensions.lock` (see `lockfile.md`). | +| **ExtensionKind** | The kind of an Extension or of a component inside it. Exactly one of: `skill`, `mcp`, `plugin`, `hook`, `command`, `agent`, `rule`. This enumeration is closed; new kinds require a spec minor release. | +| **ManifestRef** | A pointer to a manifest document plus the version resolved at install time: `{ name, version }`, where `name` equals the manifest's `name` field and `version` its `version` field. Used wherever the spec must reference "which package revision produced this." | +| **Capabilities** | The set of host-injected environment slots a component may request — e.g. `storage`, `secrets`, `exec`, `network`, `fs`. Hosts grant slots; absent slots are declared (via `capabilities.negotiate` on the bridge), never probed. Request syntax is defined in `manifest.md`; grant policy in `trust.md`. | +| **Store root** | The directory `~/.agents/` as resolved from the active user's home directory. | +| **Our store** | `~/.agents/harness/` — the only root-level key this spec claims. | + +## 3. The `~/.agents/` shared namespace + +`~/.agents/` is a shared namespace in the `/usr/local` sense: many tools drop +entries under self-owned subpaths, and no single tool owns the root. Verified +occupants as of this writing include the `skills/` directory (Codex, Cursor, +OpenCode, Gemini CLI, Copilot, Warp, Zed, pi, Kimi), Codex's +`plugins/marketplace.json` catalog, skills.sh's `.skill-lock.json`, and +rival claimants (agentstow "Commons", dotagents, agentsfolder/spec, others). + +AnyHarness claims **exactly one** root-level key: `harness/`. An AnyHarness +implementation: + +1. MUST NOT create files or directories at the `~/.agents/` root other than + `harness/` and the shared writes permitted in §5. +2. MUST NOT read, write, move, or delete `~/.agents/plugins/` or anything + under it (§5.3). +3. MUST ignore every other root-level entry it does not recognize, including + files placed there by other tools or rival `.agents/` specs. +4. MUST NOT write to the `~/.agents/skills/` root itself — only to skill + directories it manages (§5.1). +5. MAY honor an `ANYHARNESS_HOME` environment variable (or equivalent host + configuration) relocating the store root for testing or non-home + deployments. When set, all `~/.agents/` references below resolve against + it. Hosts MUST NOT silently mix store roots. + +## 4. `~/.agents/harness/` — our store + +Everything AnyHarness owns lives under `harness/`: + +```text +~/.agents/harness/ + store.json # store descriptor (this spec's machine-readable file) + packages// # installed Extensions (plugin roots, verbatim) + data// # per-Extension writable data dirs (PLUGIN_DATA) + extensions.lock # lockfile — see lockfile.md + config.toml # local configuration, incl. trust policy — see trust.md + audit.log # append-only audit log (JSONL) — see trust.md + tmp/ # staging area for atomic installs/writes + .lock # advisory write mutex — see §7 +``` + +Rules: + +1. `packages//` contains the Extension's package exactly as sourced: + `plugin.json` at its root, component directories, vendor namespaces. `` + MUST equal the manifest `name` field after normalization (§6.2). +2. `data//` is the per-Extension persistent data directory — the value a + host supplies as `PLUGIN_DATA` when launching an Extension's stdio MCP + servers, per Agent Plugins 1.0 §9. It MUST be created before first use, + MUST be writable by the Extension's subprocesses, and MUST survive updates. + It MAY be deleted on uninstall. +3. `tmp/` holds partially-extracted packages and pending writes. A directory + under `tmp/` MUST NOT be considered installed; installation completes by an + atomic rename into `packages/`. Readers MUST ignore `tmp/`. +4. `harness/store.json` is the store descriptor defined in §8. +5. Other implementations MUST NOT write under `harness/`; AnyHarness MUST NOT + interpret foreign files found there (except to report them via `doctor`). + +## 5. Shared and foreign paths + +### 5.1 `~/.agents/skills/` — adopted as-is + +The `skills/` subtree is the established shared location for the Agent Skills +specification (). AnyHarness treats it as +a read/write shared location with the following compatibility guarantees: + +1. An AnyHarness install MAY place skill components under `skills//` when + the install target is the shared skills root (materializing a `skill`-kind + Extension). Each such directory MUST be a conformant Agent Skills skill — + a directory containing `SKILL.md` — and MUST NOT contain files other than + skill content. Some readers scan exactly one level of `skills/` and ignore + stray files; polluting the root or nesting non-skill entries is unsafe. +2. AnyHarness MUST NOT modify or remove a skill directory it did not install. + Ownership is decided by `extensions.lock` (ours) vs. `.skill-lock.json` / + `skills-lock.json` (skills.sh) — see §7.3 for the reconciliation rules. +3. AnyHarness MUST tolerate foreign skills coexisting in `skills/` and MUST NOT + require it to contain only AnyHarness-managed entries. +4. AnyHarness MUST NOT treat `plugins/`, `harness/`, or any non-skill root + entry as a skill. + +### 5.2 `~/.agents/mcp.json` — shared MCP declarations + +`mcp.json` at the store root is a shared file in the de-facto +`{"mcpServers": {…}}` shape. AnyHarness: + +1. MAY read `mcp.json` to satisfy `mcp`-kind component requests. +2. When writing, MUST merge: add or replace only the `mcpServers` member names + it manages (recorded in `extensions.lock`), MUST preserve every other + member name and every other top-level field byte-for-byte semantics. +3. MUST NOT create `mcp.json` with any top-level field other than + `mcpServers`. +4. MUST NOT modify `mcp.json` when it is not a JSON object or lacks an object- + valued `mcpServers` member — a foreign dialect means "leave it alone," and + AnyHarness SHOULD report this via `doctor`. + +### 5.3 `~/.agents/plugins/` — never touched + +`plugins/` is claimed by OpenAI Codex for `marketplace.json` — a *catalog* of +plugin sources, not a store of installed plugins — and is additionally +asserted by rival `.agents/` proposals (agentstow, dotagents). Its semantics +are foreign, contested, and different from ours. AnyHarness: + +1. MUST NOT read, write, create, or delete `~/.agents/plugins/` or any path + under it. +2. MUST NOT interpret `plugins/marketplace.json` or assume `plugins//` + directories are installed packages. +3. MUST NOT rely on `plugins/` being empty, absent, or conformant to any + particular content. + +### 5.4 `~/.agents/.skill-lock.json` and project `skills-lock.json` + +Both belong to skills.sh (`npx skills`, +): the former is the global install +lock, the latter a per-project manifest. AnyHarness MUST NOT write either +file. AnyHarness SHOULD read `.skill-lock.json` when reconciling `skills/` +ownership (§7.3) and MAY read a project `skills-lock.json` when importing +skill provenance. + +### 5.5 Everything else + +Root entries not listed here — `AGENTS.md`, `commands/`, `agents/`, +`models.json`, `modes/`, `manifest.yaml`, other tools' dotfiles — belong to +other tools or other spec proposals. AnyHarness MUST ignore them and MUST NOT +create analogues at root. + +## 6. Naming and path rules + +### 6.1 Store paths + +All paths under `harness/` are UTF-8, POSIX-separator logical paths. +Implementations on non-POSIX filesystems translate separators; the logical +form above is canonical. + +### 6.2 Extension directory names + +`packages//` and `data//` names MUST satisfy the Agent Plugins 1.0 +§5.5 name constraints (1–64 chars, `a-z0-9.-`, alphanumeric ends, no `--` or +`..`). For `local`-source Extensions whose manifest name collides with an +installed one, the installer MUST reject rather than silently rename — +directory name equals manifest name, always. + +### 6.3 Containment + +Per Agent Plugins 1.0 §4.1: every package-relative path AnyHarness resolves +(`./`-prefixed manifest paths, `${PLUGIN_ROOT}` expansions, component +locations) MUST remain inside the filesystem-resolved package directory. +Symlinks resolving outside the package root MUST be rejected. The same rule +applies to `data//` paths escaping `data/`. + +## 7. Concurrency and coexistence + +### 7.1 Atomic writes + +Every write AnyHarness makes to a file under `~/.agents/` (its own or shared) +MUST be atomic: write to a sibling temp path, `fsync` where the platform +allows, then rename. A reader MUST be prepared to see either the complete old +file or the complete new file, never a partial one. Installs MUST stage under +`harness/tmp/` and complete by rename. + +### 7.2 Write mutex + +Mutating operations (install, remove, update, lockfile write, config write, +`mcp.json` merge) MUST hold an advisory lock at `harness/.lock` for the +mutation's duration. The mechanism is platform-defined (lockfile with +`O_EXCL` creation, `flock`, or host-provided equivalent); the contract is +mutual exclusion among cooperating AnyHarness processes. Readers MAY read +lock-free but MUST tolerate the atomic-rename protocol in §7.1. When multiple +harnesses share one store through the bridge server (`harness serve`), the +serving process is the only writer; direct-CLI access while a daemon holds +the store MUST honor the same `.lock` file. A stale lock MAY be broken after +proving the holder process is dead; the mechanism is implementation-defined. + +### 7.3 `skills/` reconciliation + +`~/.agents/skills/` is multi-manager: skills.sh installs and symlinks skill +dirs and tracks them in `.skill-lock.json`; harnesses and users also drop +dirs directly. Reconciliation rules: + +1. A skill directory is **AnyHarness-managed** iff `extensions.lock` contains + an Extension that materialized it. Otherwise it is foreign. +2. AnyHarness MUST NOT overwrite, update, or delete a foreign skill + directory — including one owned by skills.sh (present in + `.skill-lock.json`) or an unmanaged user dir. +3. If an install would create `skills//` and a foreign `/` + already exists, the installer MUST fail with a name-conflict diagnostic — + never merge into or shadow the foreign dir. The resolution is an explicit + user action (remove the foreign dir, or install under a different name), + never an automatic takeover. +4. When skills.sh and AnyHarness both legitimately manage *different* skill + dirs, no coordination is needed — the directories are independent. +5. AnyHarness SHOULD treat `.skill-lock.json`'s `skills` map as evidence of + skills.sh ownership when reporting `doctor` conflicts, and SHOULD surface + rather than silently coexist with a skills.sh-managed dir of the same + name. + +## 8. `store.json` — the store descriptor + +`harness/store.json` marks the layout version of the store and is the anchor +for future layout migrations. It is a single JSON object validated by +`store-layout.schema.json`: + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `$schema` | string | no | Canonical schema id for this document. | +| `layoutVersion` | integer | yes | Store layout version. This document defines `1`. | +| `createdAt` | string | yes | RFC 3339 / ISO 8601 UTC timestamp of store creation. | +| `createdBy` | string | no | Implementation identifier, e.g. `anyharness-cli 0.2.0`. | + +Rules: + +1. An implementation MUST write `store.json` on store creation and MUST NOT + rewrite it on ordinary operations. +2. An implementation encountering `layoutVersion` greater than its supported + version MUST refuse to write to the store (reads MAY proceed at its own + risk) and SHOULD report the required layout version. +3. An implementation encountering a missing or invalid `store.json` in a + non-empty `harness/` SHOULD run migration/`doctor` semantics before + writing; in an empty `harness/` it creates the file with + `layoutVersion: 1`. + +## 9. Compatibility guarantees (summary) + +| Path | We read | We write | Guarantee | +| ---- | ------- | -------- | --------- | +| `harness/` | yes | yes | Sole owner. Layout versioned by `store.json`. | +| `skills//` | yes | ours only | Conformant skills only; never foreign dirs (§5.1, §7.3). | +| `mcp.json` | yes | merge only | `mcpServers` member-level merge; foreign keys preserved (§5.2). | +| `plugins/` | no | no | Never touched (§5.3). | +| `.skill-lock.json`, `skills-lock.json` | yes | no | Read-only provenance/conflict evidence (§5.4). | +| other root entries | ignore | no | None created; none interpreted (§3, §5.5). | + +## 10. Normative references + +- Agent Plugins 1.0 — package model, manifest base, `PLUGIN_DATA`, + containment rules. / + +- Agent Skills specification — `SKILL.md` format for `skills//`. + +- skills.sh (`vercel-labs/skills`) — `.skill-lock.json` / `skills-lock.json` + conventions. +- Codex plugin docs — `plugins/marketplace.json` semantics. + +- `lockfile.md`, `manifest.md`, `trust.md` — sibling L0 docs. diff --git a/spec/store-layout.schema.json b/spec/store-layout.schema.json new file mode 100644 index 0000000..e5ce3e3 --- /dev/null +++ b/spec/store-layout.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://anyharness.dev/schemas/l0/0.1/store-layout.schema.json", + "title": "AnyHarness store descriptor (harness/store.json)", + "description": "Machine-readable schema for the ~/.agents/harness/store.json descriptor defined by spec/store-layout.md §8. The spec text is authoritative if it conflicts with this schema.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Canonical schema identifier for this document." + }, + "layoutVersion": { + "type": "integer", + "const": 1, + "description": "Store layout version. store-layout.md defines 1." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 / ISO 8601 UTC timestamp of store creation." + }, + "createdBy": { + "type": "string", + "description": "Implementation identifier, e.g. 'anyharness-cli 0.2.0'." + } + }, + "required": ["layoutVersion", "createdAt"], + "additionalProperties": false, + "$defs": { + "ExtensionKind": { + "type": "string", + "enum": ["skill", "mcp", "plugin", "hook", "command", "agent", "rule"], + "description": "Closed enumeration of extension/component kinds defined by spec/store-layout.md §2." + }, + "ManifestRef": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$", + "description": "The manifest's name field (Agent Plugins 1.0 §5.5 constraints)." + }, + "version": { + "type": "string", + "description": "The manifest's version field as resolved at install time." + } + }, + "required": ["name", "version"], + "additionalProperties": false, + "description": "Pointer to a manifest document plus the version resolved at install time (spec/store-layout.md §2)." + } + } +} From 7efa7e67c4702f02367017d3eb1a224d8b6b74a0 Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:22:41 +0000 Subject: [PATCH 2/6] docs(spec): add L0 manifest spec + schema Agent Plugins 1.0 plugin.json as closed base (fields adopted by reference), dev.anyharness vendor namespace in both forms: extensions["dev.anyharness"] manifest data (namespaceVersion, capabilities, engines.harness) and the dev.anyharness/ extension directory (hooks.json, commands/, agents/, rules/, setup). Component discovery maps to ExtensionKind; 4-layer versioning policy. --- spec/manifest.md | 290 ++++++++++++++++++++++++++++++++++++++ spec/manifest.schema.json | 134 ++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 spec/manifest.md create mode 100644 spec/manifest.schema.json diff --git a/spec/manifest.md b/spec/manifest.md new file mode 100644 index 0000000..fef01ca --- /dev/null +++ b/spec/manifest.md @@ -0,0 +1,290 @@ +# AnyHarness manifest (L0) + +**Spec version: 0.1.0-draft** + +This document defines the AnyHarness package manifest: an Agent Plugins 1.0 +`plugin.json` used verbatim as the base format, plus the `dev.anyharness` +vendor namespace that carries the behavioral component types Agent Plugins +deliberately leaves to clients (hooks, commands, agents, rules, setup). + +The machine-readable companion is `manifest.schema.json`. + +## 1. Conformance language + +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and +OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and +only when, they appear in all capitals. + +## 2. Base format: Agent Plugins 1.0 `plugin.json` + +An AnyHarness package manifest **is** an Agent Plugins 1.0 manifest as defined +by the Agent Plugins specification (, normative +text: `spec/1.0.0.md` in +). This document does not +redefine any base field; the following are adopted by reference: + +| Field | Source | AnyHarness use | +| ----- | ------ | -------------- | +| `$schema` | Agent Plugins §5.2 | Required; canonical id selects the base spec version. | +| `name` | Agent Plugins §5.3, §5.5 | Required; becomes the `packages//` directory name. | +| `version` | Agent Plugins §5.4 | Optional there; AnyHarness installers SHOULD require it (needed for `ManifestRef` and update checks). SemVer RECOMMENDED. | +| `description`, `author`, `homepage`, `repository`, `license`, `keywords` | Agent Plugins §5.4 | Metadata, unchanged semantics. | +| `extensions` | Agent Plugins §5.6, §8 | Namespace map; `dev.anyharness` is our key (§4). | + +Adopted behavioral rules: + +- The manifest schema is **closed** (Agent Plugins §5.2): unknown top-level + fields are reported-and-ignored, never given semantics. +- Component discovery is by **fixed location** (Agent Plugins §6): `skills/` + and `mcp.json` at the package root. `plugin.json` cannot relocate or + inline them. +- **Containment** (Agent Plugins §4.1): every package path resolves within + the package root; `./`-prefixed relative paths only. +- **Data, not code**: a manifest is JSON and MUST NOT be runtime-evaluated. + TypeScript authoring configs compile to `plugin.json` at build time; the + runtime path contains no JS evaluation. This removes the embedded-engine + requirement entirely. + +AnyHarness conformance additionally requires that the package be loadable by +an Agent Plugins 1.0 conformant client that implements no `dev.anyharness` +namespace: such a client sees a valid skills+MCP plugin and ignores our +namespace, per Agent Plugins §8.1. + +## 3. The `dev.anyharness` namespace + +Agent Plugins §8 defines the extension mechanism: client-specific manifest +data under `extensions[""]`, and client-specific files under +a top-level directory named for the namespace. The spec's own ecosystem +precedent is `com.github.copilot` (GitHub Copilot's vendor namespace). + +AnyHarness's namespace is **`dev.anyharness`**, used in both forms: + +```text +my-plugin/ +├── plugin.json # Agent Plugins 1.0 manifest +│ # extensions["dev.anyharness"] = manifest data +├── skills/ # Agent Plugins standard component dir +├── mcp.json # Agent Plugins standard component file +└── dev.anyharness/ # extension directory — behavioral components + ├── hooks.json # hook declarations + ├── commands/ # slash-command definitions (*.md) + ├── agents/ # agent definitions (*.md) + ├── rules/ # rule files (*.md) + └── setup # optional setup script (exec policy: trust.md) +``` + +Rules: + +1. Every file under `dev.anyharness/` is package data subject to Agent + Plugins §4.1 containment. Component definitions are **files**, never + function serialization. +2. A component type whose fixed location is absent is not an error (Agent + Plugins §6.2 semantics apply). +3. `dev.anyharness/` MUST NOT contain executable component formats other than + the fixed locations in §5 — new kinds are added by spec revision, not ad + hoc. +4. The manifest-data form (`extensions["dev.anyharness"]`) and the directory + form are independent; both MAY be present. Manifest data carries + declarations and policy requests; the directory carries component content. + +## 4. `extensions["dev.anyharness"]` — manifest data + +The namespace object is validated by +`manifest.schema.json#/$defs/AnyharnessNamespace`. Fields: + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `namespaceVersion` | integer | yes | Version of the `dev.anyharness` data format. This document defines `1`. | +| `capabilities` | string[] | no | **Capabilities** slots the package requests (§6). | +| `engines` | object | no | Compatibility ranges; the only defined member is `harness`, a SemVer range against the AnyHarness implementation version (§8). | +| `hooksFormat` | integer | no | Version of `hooks.json` used; defaults to `1`. | +| `componentsFormat` | integer | no | Frontmatter format version for `commands/`, `agents/`, `rules/` files; defaults to `1`. | + +Unknown fields inside `extensions["dev.anyharness"]` MUST be reported and +ignored (mirroring Agent Plugins §5.2's non-fatal rule for unknown top-level +fields). A missing or non-integer `namespaceVersion` makes the namespace +object invalid: the implementation MUST skip `dev.anyharness` processing for +that package while still loading Agent Plugins components. A +`namespaceVersion` greater than the implementation's supported version MUST +be treated as unsupported the same way. + +## 5. Component locations and kinds + +Components discovered under `dev.anyharness/` map onto **ExtensionKind** +values (`spec/store-layout.md` §2): + +| Fixed location | ExtensionKind | Format | +| -------------- | ------------- | ------ | +| `skills//SKILL.md` (package root) | `skill` | Agent Skills spec — not ours to redefine | +| `mcp.json` (package root) | `mcp` | Agent Plugins 1.0 §7.2 MCP configuration | +| package root (the manifest itself) | `plugin` | This document + Agent Plugins 1.0 | +| `dev.anyharness/hooks.json` | `hook` | §5.1 | +| `dev.anyharness/commands/.md` | `command` | §5.2 | +| `dev.anyharness/agents/.md` | `agent` | §5.3 | +| `dev.anyharness/rules/.md` | `rule` | §5.4 | +| `dev.anyharness/setup` | (script policy, not a component) | §5.5 | + +Discovery of each type follows Agent Plugins §6.1–6.2: fixed locations only, +absence is not an error, wrong filesystem kind invalidates that component +type only. + +### 5.1 `hooks.json` — hook declarations + +A single JSON object at `dev.anyharness/hooks.json` declaring host lifecycle +hooks. `hooksFormat` version 1: + +```json +{ + "hooks": { + "": [ + { "command": "./dev.anyharness/hooks/on-load.sh", "timeout": 30 } + ] + } +} +``` + +- `hooks` maps an event name to an ordered array of hook entries. The set of + event names is defined by the bridge/host contract (see `spec/bridge/`), + not by this document; implementations MUST ignore unknown event names. +- A hook entry's `command` follows Agent Plugins §7.2.1 `command` semantics: + one executable token — a bare executable name or a `./`-prefixed + package-relative path — never a shell string. `args` (string[]) MAY be + present and support `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` expansion per Agent + Plugins §9.2. +- `timeout` is seconds (integer, optional). +- Hook execution is governed by the script policy in `trust.md`; declaring a + hook is not authorization to run it. + +### 5.2 `commands/.md` — slash commands + +Markdown files with YAML frontmatter — the converged convention (same +shape as `SKILL.md` frontmatter and `.claude`/`codex` command files). +Frontmatter fields, `componentsFormat` version 1: + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `name` | string | yes | Command name as invoked; defaults to filename stem if equal after normalization. | +| `description` | string | no | One-line summary for command listings. | + +The body is the command's prompt/template content, passed to the host +verbatim. Files not matching `*.md` are ignored. + +### 5.3 `agents/.md` — agent definitions + +Same envelope as commands: markdown body plus YAML frontmatter, format +version 1: + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `name` | string | yes | Agent identifier. | +| `description` | string | no | When/why to invoke this agent. | +| `tools` | string[] | no | Capability or tool names the agent requests. | + +The body is the agent's instruction text. + +### 5.4 `rules/.md` — rules + +Plain markdown rule files. Frontmatter MAY carry `name` and `description` +(format version 1); bodies are appended or injected per host convention. +Rule files MUST NOT be treated as executable. + +### 5.5 `setup` — install-time script + +An optional executable file `dev.anyharness/setup` run at install time. +Whether it runs at all, and under what confirmation, is entirely a trust +decision (`trust.md` §Script policy). The manifest gives no way to bypass +that policy. + +## 6. Capabilities requests + +The `capabilities` array in `extensions["dev.anyharness"]` names the +**Capabilities** slots the package's components need at runtime — +host-injected environment slots such as `storage`, `secrets`, `exec`, +`network`, `fs`. The value is an array of slot-name strings +(`manifest.schema.json#/$defs/Capabilities`). + +1. Requesting a capability is a declaration, not a grant. The host decides + which slots exist (a browser host has no `exec`) and which are granted + (`trust.md`). +2. A component MUST be written to tolerate absent slots — absence is declared + at `capabilities.negotiate` time, never probed. +3. Unknown slot names are not an error; the host ignores slots it does not + define. + +## 7. `ManifestRef` + +Wherever the store or lockfile references "the manifest that produced this," +the reference is a **ManifestRef**: `{ name, version }` — the manifest's own +two fields as resolved at install time. `version` SHOULD be required by +installers because a manifest without it produces an incomplete ManifestRef. + +## 8. Versioning policy + +Harness APIs churn faster than package formats; the spec therefore versions +at four independent layers: + +1. **Base manifest version** — inherited from Agent Plugins: the `plugin.json` + `$schema` canonical identifier selects the base spec version (e.g. + `…/schemas/1.0.0/plugin.schema.json`). AnyHarness tracks published Agent + Plugins versions; a package declaring an unrecognized base version is + rejected per Agent Plugins §5.2. +2. **Namespace version** — `namespaceVersion` inside + `extensions["dev.anyharness"]` versions our manifest-data format. Bumped + on incompatible change; implementations reject higher versions (§4). +3. **Component-kind formats** — `hooksFormat` and `componentsFormat` version + `hooks.json` and the markdown frontmatter contract independently, so a + format change in one kind does not invalidate the rest. +4. **Package↔harness compat range** — `engines.harness` is a SemVer range + (SemVer 2.0.0, ; range syntax per npm semver) + evaluated against the installing AnyHarness implementation version. A + package whose range excludes the implementation MUST NOT be installed; + the installer reports the required range. Absence means unbounded. + +Evolution rules for this document: adding an optional field or component +location is a spec minor change; removing or reinterpreting a field, +narrowing a value set, or changing a fixed location is a major change. + +## 9. Examples + +Minimal AnyHarness package (identical to a valid Agent Plugins package — +conformant by construction): + +```json +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "hello-plugin", + "version": "1.0.0" +} +``` + +With `dev.anyharness` data: + +```json +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "deploy-tools", + "version": "2.1.0", + "extensions": { + "dev.anyharness": { + "namespaceVersion": 1, + "capabilities": ["exec", "storage"], + "engines": { "harness": ">=0.1.0 <1.0.0" }, + "hooksFormat": 1, + "componentsFormat": 1 + } + } +} +``` + +## 10. Normative references + +- Agent Plugins Specification 1.0 — base manifest, extensions mechanism + (§8), containment (§4.1), MCP config (§7.2), placeholder expansion (§9), + failure boundaries (§11.3). / + +- Agent Skills specification — `SKILL.md` component format. + +- Model Context Protocol — `mcp.json` wire targets. + +- SemVer 2.0.0 — `version` values and `engines.harness` ranges. + diff --git a/spec/manifest.schema.json b/spec/manifest.schema.json new file mode 100644 index 0000000..1b50b4f --- /dev/null +++ b/spec/manifest.schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://anyharness.dev/schemas/l0/0.1/manifest.schema.json", + "title": "AnyHarness manifest (plugin.json)", + "description": "Machine-readable schema for an AnyHarness package manifest: the Agent Plugins 1.0 plugin.json base plus the dev.anyharness namespace (spec/manifest.md). The spec text is authoritative if it conflicts with this schema.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Canonical Agent Plugins manifest schema identifier, e.g. https://agent-plugins.org/schemas/1.0.0/plugin.schema.json. Selects the base spec version." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$", + "description": "Plugin name (Agent Plugins 1.0 §5.5 constraints). Becomes the packages// directory name." + }, + "version": { + "type": "string", + "description": "Version string (Semantic Versioning RECOMMENDED). Required in practice for ManifestRef and update checks." + }, + "description": { "type": "string" }, + "author": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "email": { "type": "string" }, + "url": { "type": "string" } + }, + "additionalProperties": false + }, + "homepage": { "type": "string" }, + "repository": { "type": "string" }, + "license": { "type": "string" }, + "keywords": { + "type": "array", + "items": { "type": "string" } + }, + "extensions": { + "type": "object", + "description": "Client-specific manifest data keyed by reverse-domain namespace (Agent Plugins §8). Namespaces other than dev.anyharness are ignored without validation.", + "properties": { + "dev.anyharness": { "$ref": "#/$defs/AnyharnessNamespace" } + }, + "additionalProperties": { "type": "object" } + } + }, + "required": ["$schema", "name"], + "additionalProperties": false, + "$defs": { + "AnyharnessNamespace": { + "type": "object", + "description": "dev.anyharness namespace object (spec/manifest.md §4).", + "properties": { + "namespaceVersion": { + "type": "integer", + "const": 1, + "description": "Version of the dev.anyharness data format. spec/manifest.md defines 1." + }, + "capabilities": { + "type": "array", + "items": { "$ref": "#/$defs/CapabilitySlot" }, + "uniqueItems": true, + "description": "Capabilities slots the package's components request (declaration, not grant)." + }, + "engines": { + "type": "object", + "properties": { + "harness": { + "type": "string", + "description": "SemVer range (npm range syntax) evaluated against the AnyHarness implementation version." + } + }, + "additionalProperties": false + }, + "hooksFormat": { + "type": "integer", + "const": 1, + "description": "Version of dev.anyharness/hooks.json. Defaults to 1." + }, + "componentsFormat": { + "type": "integer", + "const": 1, + "description": "Frontmatter format version for commands/, agents/, rules/ files. Defaults to 1." + } + }, + "required": ["namespaceVersion"], + "additionalProperties": true + }, + "CapabilitySlot": { + "type": "string", + "description": "Name of a host-injected environment slot. Seed set defined by spec/manifest.md §6 (e.g. storage, secrets, exec, network, fs); unknown slot names are ignored by hosts.", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "HookEntry": { + "type": "object", + "description": "One hook entry in dev.anyharness/hooks.json (spec/manifest.md §5.1).", + "properties": { + "command": { + "type": "string", + "description": "One executable token: bare name or './'-prefixed package-relative path (Agent Plugins §7.2.1 semantics)." + }, + "args": { + "type": "array", + "items": { "type": "string" } + }, + "timeout": { + "type": "integer", + "minimum": 1, + "description": "Seconds." + } + }, + "required": ["command"], + "additionalProperties": false + }, + "HooksFile": { + "type": "object", + "description": "dev.anyharness/hooks.json, hooksFormat 1.", + "properties": { + "hooks": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "$ref": "#/$defs/HookEntry" } + }, + "description": "Event name -> ordered hook entries. Unknown event names are ignored." + } + }, + "required": ["hooks"], + "additionalProperties": false + } + } +} From 76c38f1a809bd03ad4faf2b1af3c4a39040d6f7b Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:22:41 +0000 Subject: [PATCH 3/6] docs(spec): add L0 extensions.lock spec + schema Per-Extension {kind, manifest: ManifestRef, source{type,uri,ref,path}, integrity sha256 (SRI), installedAt/updatedAt, targets[], components, capabilities, attestations}. Pinned-ref reproducibility, manifest-of- files digest construction, skills.sh field-interop mapping. --- spec/lockfile.md | 164 ++++++++++++++++++++++++++++++++++++++ spec/lockfile.schema.json | 145 +++++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+) create mode 100644 spec/lockfile.md create mode 100644 spec/lockfile.schema.json diff --git a/spec/lockfile.md b/spec/lockfile.md new file mode 100644 index 0000000..d87b139 --- /dev/null +++ b/spec/lockfile.md @@ -0,0 +1,164 @@ +# AnyHarness lockfile: `extensions.lock` (L0) + +**Spec version: 0.1.0-draft** + +This document defines `~/.agents/harness/extensions.lock`, the deterministic +record of installed Extensions. It exists so installs are reproducible, +`doctor` can verify integrity, and updates know exactly what they are +replacing. It is a state file, not user configuration — users edit +`config.toml`, never the lockfile. + +The machine-readable companion is `lockfile.schema.json`. + +## 1. Conformance language + +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and +OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and +only when, they appear in all capitals. + +## 2. Location and write semantics + +1. The lockfile lives at `~/.agents/harness/extensions.lock` — inside our + sole owned root key (`store-layout.md` §4). Nothing else may write it. +2. It is UTF-8 JSON, one top-level object, pretty-printed with two-space + indentation, extension keys sorted lexically. Deterministic output keeps + diffs reviewable and merges mechanical. +3. Writes follow `store-layout.md` §7: staged in `harness/tmp/` then renamed + atomically, while holding `harness/.lock`. Readers MUST tolerate reading + either the previous or next complete file. +4. A lockfile that is missing, unparseable, or fails schema validation is + treated as empty for reads and MUST NOT be overwritten silently — the + implementation preserves the corrupt file (e.g. renamed aside) before + writing a fresh one, and reports the event in `audit.log` (`trust.md`). + +## 3. Format + +```json +{ + "version": 1, + "extensions": { + "deploy-tools": { + "kind": "plugin", + "manifest": { "name": "deploy-tools", "version": "2.1.0" }, + "source": { + "type": "github", + "uri": "https://github.com/acme/deploy-tools", + "ref": "v2.1.0" + }, + "integrity": "sha256-9f2c…", + "installedAt": "2026-10-08T07:00:00Z", + "updatedAt": "2026-10-08T07:00:00Z", + "targets": ["codex", "claude-code"], + "components": [ + { "kind": "skill", "name": "deploy" }, + { "kind": "mcp", "name": "deploy-api" }, + { "kind": "hook", "name": "on-load" }, + { "kind": "command", "name": "release" }, + { "kind": "agent", "name": "reviewer" }, + { "kind": "rule", "name": "house-style" } + ] + } + } +} +``` + +### 3.1 Top-level object + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `version` | integer | yes | Lockfile format version. This document defines `1`. Readers MUST refuse a higher version. | +| `extensions` | object | yes | Map from Extension name (the `packages//` or `skills//` directory name) to an **Extension** entry. MAY be empty. | + +### 3.2 Extension entry + +Each value of `extensions` is an **Extension** — the installed unit +(`store-layout.md` §2): + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `kind` | ExtensionKind | yes | The installed unit's kind — typically `plugin` (a manifest-bearing package) or `skill` (a standalone materialized skill). Closed enum: `skill \| mcp \| plugin \| hook \| command \| agent \| rule`. | +| `manifest` | ManifestRef | yes | `{ name, version }` resolved from the extension's `plugin.json` (or equivalent manifest) at install time. `manifest.name` MUST equal the `extensions` map key. | +| `source` | object | yes | Where the extension came from; see §3.3. | +| `integrity` | string | yes | Content digest of the installed package tree; see §4. | +| `installedAt` | string | yes | RFC 3339 / ISO 8601 UTC timestamp of first install. | +| `updatedAt` | string | yes | Timestamp of last update; equals `installedAt` until first update. | +| `targets` | string[] | yes | Harness identifiers the extension has been materialized/emitted to (e.g. `"codex"`, `"claude-code"`, `"opencode"`). MAY be empty. Updated by emit operations. | +| `components` | object[] | no | Inventory of contributed components: `{ kind, name }` pairs, `kind` drawn from ExtensionKind. Informational; absence means "not inventoried," not "no components." | +| `capabilities` | string[] | no | Capability slots granted at install time (subset of the manifest's request; `manifest.md` §6, `trust.md`). | +| `attestations` | object[] | no | Reserved for signing/provenance attestations (sigstore-style). Defined by `trust.md` §5; entries are opaque to the lockfile. | + +### 3.3 Source object + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `type` | string | yes | One of `git`, `github`, `registry`, `local`. Closed enum; new source types are a spec minor change. | +| `uri` | string | yes | Source identifier in canonical form for its type: `git` → clone URL; `github` → `owner/repo` or repo URL (SHOULD normalize to `owner/repo`); `registry` → registry URL + package coordinates; `local` → absolute or store-relative path at install time. | +| `ref` | string | no | Resolved revision: commit SHA, tag, branch, or registry version. For `git`/`github` the installer MUST resolve floating refs to a commit SHA at install time and record the SHA — reproducibility depends on it. | +| `path` | string | no | Subpath within the source where the package lives (monorepo sources), e.g. `plugins/deploy-tools`. | + +Interop note (skills.sh conventions): `uri`/`ref`/`installedAt`/`updatedAt` +follow the same semantics as `sourceUrl`/`ref`/`installedAt`/`updatedAt` in +skills.sh's lock entries, and `path` mirrors `skillPath`. An importer mapping +a skills.sh entry keeps field meanings 1:1. skills.sh's `skillFolderHash` +(GitHub tree SHA) is a *remote-side* digest; our `integrity` (§4) is a +*local-content* digest equivalent to their `computedHash` — the two are +complementary, not interchangeable, and the lockfile keeps them in separate +fields. + +## 4. Integrity digests + +`integrity` uses Subresource Integrity syntax +(): the string `sha256-` followed by the +base64-encoded SHA-256 of the package manifest-of-files. + +The hashed manifest-of-files is computed over the installed tree +(`packages//`, or `skills//` for standalone skills): + +1. List every regular file under the package directory, paths relative to + the package root, `/`-separated, sorted by byte order. Symlinks are + hashed as their target string; dangling or root-escaping symlinks abort + the digest. +2. Emit the canonical lines ` `, + one per file, joined with `\n`. +3. SHA-256 that byte string; base64 the result. + +An optional sibling field `treeHash` MAY record the GitHub tree SHA for +`github` sources (skills.sh `skillFolderHash` convention) to enable cheap +remote update checks without cloning. + +## 5. Semantics + +1. `extensions` is the authoritative inventory: an Extension not listed is + not installed, and an entry whose directory is absent is a `doctor` + finding, not a silent skip. +2. `extensions.lock` owns `packages/` and our slice of `skills/`: entries + recorded with kind `skill` and a `skills//` materialization are how + §7.3 of `store-layout.md` decides foreign-vs-managed dirs. +3. The lockfile is append-evolving: removing an entry removes the managed + state; orphaned `packages/` dirs (no lock entry) are reported by `doctor`, + never auto-deleted. +4. AnyHarness never writes skills.sh's `.skill-lock.json` or + `skills-lock.json` (`store-layout.md` §5.4); ownership questions between + the two managers are resolved by reading, not writing. +5. Unknown fields anywhere in the file MUST be preserved on rewrite where + practical and MUST NOT be treated as errors — forward-compat for mixed + tool versions. + +## 6. Versioning + +`version` is an integer bumped only on incompatible layout change. +Compatible additions (new optional fields, new `source.type` values) reuse +the current version; readers tolerate them per §5.5. A reader encountering +`version > 1` MUST refuse to write and SHOULD refuse to read. + +## 7. Normative references + +- `store-layout.md` — store paths, locking, `skills/` reconciliation, + pinned types (Extension, ExtensionKind, ManifestRef). +- `manifest.md` — manifest fields behind ManifestRef, Capabilities. +- `trust.md` — integrity verification policy, attestations, audit log. +- W3C Subresource Integrity — `sha256-` digest syntax. + +- skills.sh lock conventions (`vercel-labs/skills`) — `sourceUrl`, `ref`, + `installedAt`, `skillPath`, `skillFolderHash`, `computedHash` precedent. + diff --git a/spec/lockfile.schema.json b/spec/lockfile.schema.json new file mode 100644 index 0000000..8a95c5c --- /dev/null +++ b/spec/lockfile.schema.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://anyharness.dev/schemas/l0/0.1/lockfile.schema.json", + "title": "AnyHarness lockfile (harness/extensions.lock)", + "description": "Machine-readable schema for ~/.agents/harness/extensions.lock (spec/lockfile.md). The spec text is authoritative if it conflicts with this schema.", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "description": "Canonical schema identifier for this document." + }, + "version": { + "type": "integer", + "const": 1, + "description": "Lockfile format version. lockfile.md defines 1." + }, + "extensions": { + "type": "object", + "description": "Map from Extension directory name to Extension entry.", + "propertyNames": { + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$" + }, + "additionalProperties": { "$ref": "#/$defs/Extension" } + } + }, + "required": ["version", "extensions"], + "additionalProperties": false, + "$defs": { + "ExtensionKind": { + "type": "string", + "enum": ["skill", "mcp", "plugin", "hook", "command", "agent", "rule"], + "description": "Closed kind enumeration (store-layout.md §2). Mirrored from store-layout.schema.json/$defs/ExtensionKind." + }, + "ManifestRef": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$" + }, + "version": { "type": "string" } + }, + "required": ["name", "version"], + "additionalProperties": false, + "description": "Pointer to the extension's manifest plus the version resolved at install time. Mirrored from store-layout.schema.json/$defs/ManifestRef." + }, + "Source": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["git", "github", "registry", "local"], + "description": "Source type (closed enum; new types are a spec minor change)." + }, + "uri": { + "type": "string", + "minLength": 1, + "description": "Canonical source identifier: clone URL, owner/repo, registry coordinates, or local path." + }, + "ref": { + "type": "string", + "description": "Resolved revision. For git/github sources the installer records the commit SHA, not a floating ref." + }, + "path": { + "type": "string", + "description": "Subpath within the source containing the package (monorepo sources)." + } + }, + "required": ["type", "uri"], + "additionalProperties": false + }, + "ComponentRef": { + "type": "object", + "properties": { + "kind": { "$ref": "#/$defs/ExtensionKind" }, + "name": { "type": "string", "minLength": 1 } + }, + "required": ["kind", "name"], + "additionalProperties": false, + "description": "Inventory record of one contributed component." + }, + "Extension": { + "type": "object", + "description": "One installed unit (store-layout.md §2).", + "properties": { + "kind": { "$ref": "#/$defs/ExtensionKind" }, + "manifest": { "$ref": "#/$defs/ManifestRef" }, + "source": { "$ref": "#/$defs/Source" }, + "integrity": { + "type": "string", + "pattern": "^sha256-[A-Za-z0-9+/=]+$", + "description": "SRI-syntax sha256 digest of the installed package tree's manifest-of-files (lockfile.md §4)." + }, + "treeHash": { + "type": "string", + "description": "Optional GitHub tree SHA for github sources (skills.sh skillFolderHash convention) for cheap remote update checks." + }, + "installedAt": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 / ISO 8601 UTC timestamp of first install." + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update; equals installedAt until first update." + }, + "targets": { + "type": "array", + "items": { "type": "string" }, + "description": "Harness identifiers the extension was materialized/emitted to." + }, + "components": { + "type": "array", + "items": { "$ref": "#/$defs/ComponentRef" }, + "description": "Optional inventory of contributed components." + }, + "capabilities": { + "type": "array", + "items": { "type": "string" }, + "description": "Capability slots granted at install time." + }, + "attestations": { + "type": "array", + "items": { "type": "object" }, + "description": "Reserved for signing/provenance attestations (trust.md §5); entries are opaque to the lockfile." + } + }, + "required": [ + "kind", + "manifest", + "source", + "integrity", + "installedAt", + "updatedAt", + "targets" + ], + "additionalProperties": false + } + } +} From 9866ce3fe9ea414acaba4da34552bb9c7cdc6ed9 Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:22:41 +0000 Subject: [PATCH 4/6] docs(spec): add L0 trust spec + audit-record schema Threat model covering agent-executed installs (prompt-injection to package install), executable components, provenance (git-pin + digest; sigstore attestations reserved), explicit descope (no sandbox, no central registry, no mandatory signing). Integrity verification at install and load, config.toml exec policy defaults (ask -> deny non-interactive), append-only audit.log JSONL. --- spec/trust.md | 251 +++++++++++++++++++++++++++++++++++++++++ spec/trust.schema.json | 84 ++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 spec/trust.md create mode 100644 spec/trust.schema.json diff --git a/spec/trust.md b/spec/trust.md new file mode 100644 index 0000000..a86911b --- /dev/null +++ b/spec/trust.md @@ -0,0 +1,251 @@ +# AnyHarness trust model (L0) + +**Spec version: 0.1.0-draft** + +This document defines what AnyHarness does and does not guarantee about the +code it installs: integrity verification at install and load time, the policy +governing extension-provided executables, and the audit log that records +every trust-relevant event. + +The machine-readable companion (`trust.schema.json`) describes one audit log +record. + +## 1. Conformance language + +The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and +OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and +only when, they appear in all capitals. + +## 2. Threat model + +### 2.1 Assets and adversaries + +Assets at stake when `harness` runs inside an agent environment: + +- the user's filesystem reachable from the store root and from spawned + processes (`~/.agents/`, project files, SSH keys, cloud credentials); +- host environment variables, frequently carrying secrets; +- agent behavior itself — an agent that can be told to run commands can be + told to install packages; +- downstream harnesses that consume `~/.agents/` (every reader inherits our + decisions). + +Adversary classes: + +- **Malicious package author** — ships hooks/MCP/commands that exfiltrate or + persist. Classic supply chain, now with an automated installer in the loop. +- **Prompt injection → install** — novel vector specific to agent harnesses: + content the *agent* reads (a webpage, an issue, a repo README) instructs it + to run `harness add evil/pkg`. The "user" approving the install may be the + compromised agent itself. +- **Repo takeover / tag move** — a once-good source turns hostile between + installs or updates. +- **Typosquat / lookalike source** — `acme/deploy-tools` vs `acrne/deploy-tools`. +- **Tampered local state** — writes to `packages/` or `extensions.lock` + outside our atomic-write protocol. + +### 2.2 What this spec addresses + +| Threat | Mechanism | Where | +| ------ | --------- | ----- | +| Tampering in transit / post-install mutation | Content integrity digest verified at install *and* load | §3 | +| Floating-source bait-and-switch | Floating refs MUST resolve to commit SHAs in the lockfile; updates re-resolve explicitly | §3.3 | +| Agent-executed installs | Actor-aware policy: non-interactive installs default to `ask`→`deny`, allowlists, review gates | §4.2 | +| Extension executables | Script policy: what may run, when, under whose confirmation | §4 | +| Silent policy/security state changes | Append-only audit log of every trust event | §6 | +| Lookalike/unvetted sources | Source allowlists + provenance record (uri + pinned ref + integrity) | §4.2, §5 | + +### 2.3 Explicitly out of scope (stated, not implied) + +v0 of this spec **descopes** to *installer + lockfile + integrity + audit*: + +- **No sandbox.** AnyHarness does not sandbox extension executables. Hook, + MCP, setup, and command processes run with the host's ambient privileges. + Containment rules (Agent Plugins §4.1) govern *which package paths may be + referenced*, not what spawned processes may touch. Sandboxing is a host + concern (and a roadmap item, not an L0 guarantee). +- **No mandatory signing.** Provenance in v0 is the + `(source.uri, source.ref→SHA, integrity)` triple — git-pin plus local + content digest. Sigstore-style attestations are reserved in the lockfile + (`attestations`) and MAY be added in a spec minor release; no extension is + required to carry one, and no registry is a root of trust. +- **No central registry.** There is no AnyHarness registry vouching for + packages. `registry`-type sources are treated exactly like `git` ones — + verified by pin + digest, never by the registry's say-so. +- **No runtime monitoring.** The audit log records what *we* did; it is not + a behavioral monitor of running extensions. + +An implementation that wants stronger guarantees MUST layer them on top +(e.g. running extension processes under a host sandbox); they are not +portable contract. + +## 3. Integrity verification + +### 3.1 At install time + +1. After staging a package in `harness/tmp/`, the installer computes the + SRI-syntax digest defined by `lockfile.md` §4 over the staged tree. +2. Re-installs and updates MUST compare the fresh digest against the recorded + `integrity`. A changed digest on an update path is normal (new version); + the event is logged, not blocked. A changed digest on a *non*-update read + path is a finding (§3.2). +3. For `git`/`github` sources the installer MUST verify the checked-out + commit equals `source.ref`'s recorded SHA before digesting. + +### 3.2 At load time + +Before a host materializes or executes an extension's components, it verifies +the installed tree: + +1. MUST verify the digest of every file the load will consume: `plugin.json`, + `mcp.json`, all of `dev.anyharness/`, and each `skills//` entry it + will expose. Implementations MAY verify the full package digest for + simplicity. +2. Implementations MAY cache a verification result keyed on (package path, + digest input set, file mtimes/sizes). A cache MUST be invalidated by any + change under the package root detectable via mtime/size — content digests + are the fallback when metadata is unreliable. +3. On digest mismatch: the extension is **untrusted** — the host MUST NOT + execute, materialize, or register its components, MUST write an + `integrity.fail` audit event, and SHOULD surface a `doctor` finding. The + remediation is reinstall (`harness add --reinstall`), never silent + acceptance. + +### 3.3 Update semantics + +An update is a re-resolve + reinstall: fetch `source.uri`, resolve the ref, +verify the commit/digest, atomically swap `packages//`, rewrite the +lock entry (`updatedAt`, new `integrity`, new `ref`). `data//` +persists across updates (store-layout.md §4). + +## 4. Script and executable policy + +Extension packages are not inert: `dev.anyharness/setup` runs at install, +hooks run at lifecycle events, MCP servers run as subprocesses, skill +`scripts/` run on agent invocation. This section fixes who may run what, +when — the policy is configuration (`harness/config.toml`), not per-package +negotiation that a package could lobby for. + +### 4.1 Policy surface + +`config.toml` carries a `[policy]` table; defaults below are the spec's +required shipped defaults (implementations MUST NOT ship laxer defaults): + +| Key | Values | Default | Governs | +| --- | ------ | ------- | ------- | +| `exec.setup` | `deny` \| `ask` \| `allow` | `ask` | `dev.anyharness/setup` at install | +| `exec.hooks` | `deny` \| `ask` \| `allow` | `ask` | hook entries declared in `hooks.json` | +| `exec.mcp` | `deny` \| `ask` \| `allow` | `ask` | stdio MCP server processes | +| `exec.skillScripts` | `deny` \| `ask` \| `allow` | `ask` | files under a skill's `scripts/` | +| `exec.nonInteractive` | `deny` \| `allow` | `deny` | what `ask` resolves to when no human can answer (agent/CI/daemon context) | +| `sources.allow` | string[] | `[]` | URI glob allowlist; empty = no restriction beyond other rules | +| `sources.deny` | string[] | `[]` | URI glob denylist, checked first | + +`ask` means: prompt the human on the controlling TTY before first run of +each executable, then cache the decision per (extension, executable class) +for the lock entry's lifetime. When no TTY exists or the invoker is an +agent/non-interactive process, `ask` resolves to `exec.nonInteractive`'s +value — `deny` by default. + +### 4.2 Agent-executed installs + +`harness add` may be invoked by an agent as easily as by a human. The +countermeasure is actor-awareness, not hoping agents are well-behaved: + +1. Every mutating operation records an `actor` in the audit event: + `user` (interactive TTY), `agent` (detected agent/non-interactive + context), or `daemon` (bridge server). +2. For `actor: agent`, install-time executable classes (`exec.setup`, + `exec.hooks`) resolve `ask` → `deny` unless the source matches + `sources.allow`. The package still installs; its executables stay + dormant pending human review — which is the correct default under prompt + injection, because the "approver" cannot be trusted. +3. `sources.allow` SHOULD be the recommended way to grant agents install + autonomy: allowlisted provenance (exact `owner/repo` or registry + coordinates, never broad globs) beats trusting the agent's judgment. +4. Policy changes are themselves audited (`policy.change` events) — an agent + editing `config.toml` leaves a record. +5. Implementations SHOULD additionally gate installs behind the host + harness's own tool-permission surface when embedded (the bridge declares + install as a mutating operation in `capabilities.negotiate`). + +### 4.3 Command discipline + +Every executable AnyHarness materializes follows Agent Plugins §7.2.1 +command semantics: one executable token (bare name or `./`-prefixed +package-relative path), `args` passed separately, `${PLUGIN_ROOT}` / +`${PLUGIN_DATA}` expansion only where defined, never shell strings. No +spec-defined field evaluates code. + +## 5. Provenance and signing + +The v0 provenance record is the lockfile triple per extension: +`source.uri` (canonical origin), `source.ref` (pinned commit SHA or +version), `integrity` (local content digest). Together they answer "what +did we install, from where, and is it still what we installed." + +Reserved-not-required: `attestations[]` on a lock entry MAY later carry +sigstore-style bundle references (Fulcio-issued identity, Rekor log index) +per . Verifying them is OPTIONAL; a v0 consumer +MUST ignore the field's contents (it MUST NOT fail the entry). This spec +makes no claim that registry or git hosts attest anything — provenance is +self-recorded. + +## 6. Audit log + +### 6.1 Location and form + +`~/.agents/harness/audit.log` — append-only JSON Lines: one JSON object per +line, UTF-8, appended under the same `harness/.lock` mutex as other +mutations (store-layout.md §7.2). `O_APPEND` semantics; partial lines from +crashed writers are tolerated by readers (skip and continue). + +### 6.2 Event record + +Each line validates against `trust.schema.json`: + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `ts` | string | yes | RFC 3339 / ISO 8601 UTC timestamp. | +| `event` | string | yes | Event type (enum below). | +| `actor` | string | yes | `user` \| `agent` \| `daemon`. | +| `extension` | ManifestRef | no | The affected extension, when applicable. | +| `decision` | string | no | Policy decision taken: `allow` \| `deny` \| `ask`. | +| `integrity` | string | no | SRI digest relevant to the event (installed/verified value). | +| `source` | object | no | The lockfile Source object for install/update events. | +| `details` | object | no | Free-form event payload (non-secret; MUST NOT contain env values, tokens, or file contents). | + +`event` values: `install`, `update`, `remove`, `integrity.verify`, +`integrity.fail`, `exec.allow`, `exec.deny`, `policy.change`, +`lockfile.corrupt`, `skills.conflict`. + +### 6.3 Rules + +1. Every trust decision (install, update, exec allow/deny, integrity + failure, policy change) MUST produce an audit event. Audit MUST NOT be + disabled by configuration. +2. Writers MUST NOT rewrite or truncate history; rotation, if any, is by + archiving whole files (`audit.log.1`, …), never in-place edits. +3. Audit records are user-visible data: implementations MUST NOT write + secrets, credential values, or env contents into `details`. +4. The audit log is evidence, not enforcement: a missing log does not block + operation, but a missing-or-unwritable log on a mutating operation SHOULD + produce a `doctor` finding. + +## 7. Versioning + +The audit record schema is versioned by the spec, not inline: additive +fields are a spec minor change (consumers MUST ignore unknown fields); +renaming/removing fields or changing `event` semantics is a spec major +change. Policy keys in `config.toml` follow the same rule. + +## 8. Normative references + +- `store-layout.md` — paths, `.lock` mutex, ManifestRef. +- `lockfile.md` — integrity digest construction, lock entry fields. +- `manifest.md` — `dev.anyharness` component formats, Capabilities. +- Agent Plugins 1.0 — containment (§4.1), command/env semantics + (§7.2.1, §9). +- W3C Subresource Integrity — digest syntax. +- Sigstore — attestation model referenced by §5. + diff --git a/spec/trust.schema.json b/spec/trust.schema.json new file mode 100644 index 0000000..c5b4284 --- /dev/null +++ b/spec/trust.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://anyharness.dev/schemas/l0/0.1/trust.schema.json", + "title": "AnyHarness audit log record (harness/audit.log line)", + "description": "Machine-readable schema for one JSONL record in ~/.agents/harness/audit.log (spec/trust.md §6). The spec text is authoritative if it conflicts with this schema.", + "type": "object", + "properties": { + "ts": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 / ISO 8601 UTC timestamp." + }, + "event": { + "type": "string", + "enum": [ + "install", + "update", + "remove", + "integrity.verify", + "integrity.fail", + "exec.allow", + "exec.deny", + "policy.change", + "lockfile.corrupt", + "skills.conflict" + ], + "description": "Trust event type (trust.md §6.2)." + }, + "actor": { + "type": "string", + "enum": ["user", "agent", "daemon"], + "description": "Who initiated the operation: interactive user, agent/non-interactive context, or bridge daemon." + }, + "extension": { "$ref": "#/$defs/ManifestRef" }, + "decision": { + "type": "string", + "enum": ["allow", "deny", "ask"], + "description": "Policy decision taken, for exec.* and policy events." + }, + "integrity": { + "type": "string", + "pattern": "^sha256-[A-Za-z0-9+/=]+$", + "description": "SRI-syntax sha256 digest relevant to the event." + }, + "source": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["git", "github", "registry", "local"] + }, + "uri": { "type": "string" }, + "ref": { "type": "string" }, + "path": { "type": "string" } + }, + "required": ["type", "uri"], + "additionalProperties": false, + "description": "Lockfile Source object for install/update events." + }, + "details": { + "type": "object", + "description": "Free-form event payload. MUST NOT contain secrets, credential values, env contents, or file contents." + } + }, + "required": ["ts", "event", "actor"], + "additionalProperties": true, + "$defs": { + "ManifestRef": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$" + }, + "version": { "type": "string" } + }, + "required": ["name", "version"], + "additionalProperties": false, + "description": "Pointer to an extension's manifest + version. Mirrored from store-layout.schema.json/$defs/ManifestRef." + } + } +} From 0c3da69a02f7f8ba8989399073cdebb12669577c Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:22:41 +0000 Subject: [PATCH 5/6] docs(spec): refresh spec README index for L0 docs --- spec/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/README.md b/spec/README.md index e6ee240..07c49dd 100644 --- a/spec/README.md +++ b/spec/README.md @@ -5,19 +5,21 @@ convergence layer. Everything here is a **delta on existing conventions** — we codify what exists or ship explicitly-namespaced extensions; we never assert a new universal `~/.agents/` layout. -## Documents (drafts — under construction) +## Documents | Doc | Covers | | --- | ------ | -| `store-layout.md` | `~/.agents/` coexistence rules: `skills/` (adopted), `harness/` (ours), `plugins/` (never touch — Codex's catalog lives there), `mcp.json`, `extensions.lock`, `anyharness.toml` | -| `manifest.md` | Agent Plugins 1.0 `plugin.json` base + `dev.anyharness/` vendor namespace for hooks/commands/agents/rules | -| `lockfile.md` | `extensions.lock`: versions, sources, integrity hashes | -| `trust.md` | Integrity verification, script policy, audit log | +| `store-layout.md` | `~/.agents/` coexistence rules: `harness/` is our ONLY root key (`packages/`, `data/`, `extensions.lock`, `config.toml`, `audit.log`, `store.json` inside); `skills/` adopted; `plugins/` never touched (Codex catalog + rival claimants); `mcp.json` merge-only; `skills-lock` files read-only; write mutex + skills.sh reconciliation | +| `manifest.md` | Agent Plugins 1.0 `plugin.json` base + `dev.anyharness/` vendor namespace (manifest data + extension directory: `hooks.json`, `commands/`, `agents/`, `rules/`, `setup`), Capabilities requests, 4-layer versioning policy | +| `lockfile.md` | `harness/extensions.lock`: per-Extension `{kind, manifest: ManifestRef, source{type,uri,ref}, integrity sha256, installedAt, targets[]}`; skills.sh interop mapping | +| `trust.md` | Threat model (agent-executed installs, executable components, provenance, explicit descope), integrity verification at install + load, script/exec policy in `config.toml`, `audit.log` JSONL format | | `bridge/` | Bridge protocol v0.1: operations, capability model, three transports (stdio / HTTP loopback / in-process), `protocolVersion` handshake | ## Schemas -JSON Schemas live next to each doc (`.schema.json` suffix). +JSON Schemas live next to each doc (`.schema.json` suffix): `store-layout.schema.json` (the `harness/store.json` descriptor), `manifest.schema.json` (Agent Plugins base + `dev.anyharness` namespace), `lockfile.schema.json` (`extensions.lock`), `trust.schema.json` (one audit JSONL record). + +Pinned interface names shared across specs: `Extension`, `ExtensionKind` (`skill | mcp | plugin | hook | command | agent | rule`), `ManifestRef`, `Capabilities` — defined in `store-layout.md` §2 and mirrored in the schema `$defs`. ## Normative references we build on From f1c0ee88540d3b13cb94f87ab070bea639e39a85 Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Thu, 8 Oct 2026 07:25:30 +0000 Subject: [PATCH 6/6] docs(spec): reserve serve.caller config namespace in store-layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [[serve.caller]] (caller identity + per-token allow/deny op lists for HTTP-loopback daemon mode) is owned by spec/bridge/transports.md §3 — this spec defines only the reservation and the key-partitioning rule for config.toml. --- spec/store-layout.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/spec/store-layout.md b/spec/store-layout.md index 83aecec..91a2636 100644 --- a/spec/store-layout.md +++ b/spec/store-layout.md @@ -146,7 +146,20 @@ file. AnyHarness SHOULD read `.skill-lock.json` when reconciling `skills/` ownership (§7.3) and MAY read a project `skills-lock.json` when importing skill provenance. -### 5.5 Everything else +### 5.5 `harness/config.toml` — namespaced configuration + +`config.toml` is ours but sectioned by spec document so sibling specs own +their keys without interleaving: + +| Key prefix | Owned by | Notes | +| ---------- | -------- | ----- | +| `[policy]` | `trust.md` §4.1 | Exec/script policy and source allowlists. | +| `[[serve.caller]]` | `spec/bridge/transports.md` §3 | Reserved. HTTP-loopback daemon caller table: caller identity + per-token `allow`/`deny` op lists. This document defines only the reservation; semantics live there. | +| everything else | unspecified | Future spec revisions allocate new top-level tables explicitly; implementations MUST ignore unknown tables. | + +This document defines no other `config.toml` keys. + +### 5.6 Everything else Root entries not listed here — `AGENTS.md`, `commands/`, `agents/`, `models.json`, `modes/`, `manifest.yaml`, other tools' dotfiles — belong to @@ -257,7 +270,7 @@ Rules: | `mcp.json` | yes | merge only | `mcpServers` member-level merge; foreign keys preserved (§5.2). | | `plugins/` | no | no | Never touched (§5.3). | | `.skill-lock.json`, `skills-lock.json` | yes | no | Read-only provenance/conflict evidence (§5.4). | -| other root entries | ignore | no | None created; none interpreted (§3, §5.5). | +| other root entries | ignore | no | None created; none interpreted (§3, §5.6). | ## 10. Normative references