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
14 changes: 8 additions & 6 deletions spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
164 changes: 164 additions & 0 deletions spec/lockfile.md
Original file line number Diff line number Diff line change
@@ -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/<name>/` or `skills/<name>/` 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
(<https://www.w3.org/TR/SRI/>): 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/<name>/`, or `skills/<name>/` 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 `<sha256-hex-of-file-contents> <relative-path>`,
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/<name>/` 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.
<https://www.w3.org/TR/SRI/>
- skills.sh lock conventions (`vercel-labs/skills`) — `sourceUrl`, `ref`,
`installedAt`, `skillPath`, `skillFolderHash`, `computedHash` precedent.
<https://github.com/vercel-labs/skills>
145 changes: 145 additions & 0 deletions spec/lockfile.schema.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading