diff --git a/CHANGELOG b/CHANGELOG
index 91fbba05..30567d69 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -70,6 +70,22 @@
### Added
+- **Machines can follow a signed managed state (`fastskill managed`).** A managed state is a
+ signed, resolved list of the skills one user's agents should have, decided somewhere else. The
+ source and the Ed25519 verification keys come only from trusted settings (the administrator's
+ system file or the user's `managed.toml`), never from a project. `managed enroll` and
+ `managed apply` verify the state, deploy the listed skills into each agent's user skills folder,
+ remove the ones FastSkill placed that are no longer listed, and move blocked content into a
+ quarantine instead of deleting it. While a user follows a state, every command that adds or
+ replaces skill content is checked by digest first; `skill list` reports `managed-blocked` and
+ `managed-not-allowed`, and `--check` fails on them. An `https://` source can name a credential
+ command for sign-in, receive a fixed, inspectable report after each apply, honor `editable` and
+ point refusals at a `request_url`. Session-start hooks in Claude Code and Gemini CLI and a
+ per-user timer (at login, then hourly) keep machines up to date, and `managed hooks --system`
+ prints what an administrator installs machine-wide. Nothing changes until a source is
+ configured. See [ADR-0016](docs/adr/0016-machines-follow-a-signed-managed-state.md), the
+ [managed state](webdocs/skill-management/managed-state.mdx) format and
+ [Hardening managed machines](webdocs/security/managed-machines.mdx).
- **`storage::git_isolated::clone_isolated` for cloning repositories you do not control.** The
library clone reads no system, global or user git configuration and inherits no environment
beyond `PATH`, proxy and CA settings, so a host's credential helper, `insteadOf` rewrite or
diff --git a/CONTEXT.md b/CONTEXT.md
index 56289f8d..0744703d 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -147,6 +147,39 @@ A preset skill selection that a user may replace within the preset's customizati
**Permitted addition**:
A skill outside the preset's selections that its customization rules allow a user to add.
+### Managed machines
+
+**Managed state**:
+A signed, resolved list of the skills one user's agents on one machine should have, decided
+somewhere else and never by FastSkill. It travels in a DSSE envelope signed with Ed25519, is
+verified against keys pinned in trusted settings, is bound to its **managed source** and its
+`subject`, never goes backwards and expires. Resolved content like a Lock, not intent like a
+Manifest. See [ADR-0016](./docs/adr/0016-machines-follow-a-signed-managed-state.md).
+_Avoid_: policy, profile (the state is what a machine follows, not why).
+
+**Managed source**:
+The one location a machine fetches its managed state from: an `https://` URL or a local file path,
+set only in trusted settings (the administrator's system file or the user's `managed.toml`), never
+by a project. `report_url`, `request_url` and `editable` take effect only with an `https://`
+source.
+
+**Managed store**:
+FastSkill's private, per-user copy of the skills a managed state lists, from which `managed apply`
+deploys into each **Agent target** through a staging area. The cached state, the enrollment and
+ownership records and the quarantine live next to it.
+
+**Agent target**:
+One agent's per-user skills folder, as `aikit-sdk` describes it, detected on every apply.
+`managed apply` deploys to the fewest folders each agent reads and changes only the entries it
+placed. The current project's skills folder is never an Agent target.
+
+**Quarantine**:
+Where `managed apply` moves content aside instead of deleting it: content whose digest is blocked,
+content the state doesn't allow when it claims the Agent targets exclusively, and a deployed copy
+that was changed after FastSkill placed it. Each item keeps its former location, its digest and
+the reason; `managed unenroll` keeps the quarantine.
+_Avoid_: delete, remove (quarantined content is kept).
+
### Discovery axes
These two axes — not the verb names — are what actually distinguish the read-side commands. The verbs should expose them, not hide them.
@@ -291,9 +324,10 @@ A proposed selection of behaviors and boundaries to evaluate for a skill, before
## Resolved decisions
-- **Historical `sync` is removed.** Modern targets read skills directly. Propagation has exactly two
- members: `project install` (Manifest → skills directory) and `index rebuild` (skills directory →
- Vector index, conditional on an Embedding provider).
+- **Historical `sync` is removed.** Modern targets read skills directly. Propagation has exactly
+ three members: `project install` (Manifest → skills directory), `index rebuild` (skills directory
+ → Vector index, conditional on an Embedding provider) and `managed apply` (managed state →
+ managed store → Agent targets).
- **`index rebuild` is conditional.** It runs only when an **Embedding provider** is configured.
Mutating commands may auto-run it only when embeddings are enabled; `--reindex` and
`--no-reindex` override configuration. `cli doctor` surfaces provider state.
@@ -308,3 +342,5 @@ A proposed selection of behaviors and boundaries to evaluate for a skill, before
and explicit latest-stable resolution.
- [ADR-0010](./docs/adr/0010-command-taxonomy.md) defines the command namespaces and breaking
migration policy.
+- [ADR-0016](./docs/adr/0016-machines-follow-a-signed-managed-state.md) defines the managed
+ state, how a machine follows it, and what FastSkill never decides.
diff --git a/README.md b/README.md
index bccf76c2..cebf8379 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ agents — there is no metadata-file sync step. FastSkill manages the files; you
- **Install skills** from a local folder, a git repo (branch/tag/subdirectory), a zip URL, or a registry ID.
- **Keep installs reproducible** with `skill-project.toml` + `skills.lock`; split optional vs production skills into groups.
- **Ship a complete team setup** as a verified, versioned bundle containing its full skill dependency closure.
+- **Follow a managed state** on managed machines: `fastskill managed` verifies a signed skill list against pinned keys, deploys it to each agent, quarantines blocked content and gates installs by digest.
- **Discover skills** by meaning with semantic search (remote catalogs by default, `--local` for installed skills).
- **Test skill quality** with eval suites (`fastskill eval`) before you ship — each case runs isolated in a scratch workspace with only your skill, so trigger rates are reproducible across machines.
- **Improve skills automatically** with the text-gradient optimizer (`fastskill optimization`).
@@ -202,6 +203,7 @@ exports. It refuses a `tools/call` naming one with JSON-RPC
| `fastskill optimization` | `run`, `resume`, `status`, `inspect`, `export` | Optimize a skill document and inspect runs. |
| `fastskill index` | `rebuild` | Rebuild the local semantic index. |
| `fastskill cache` | `info`, `clean` | Inspect and reclaim cached content. |
+| `fastskill managed` | `enroll`, `apply`, `status`, `unenroll`, `hooks` | Follow a signed managed state on this machine. |
| `fastskill server` | `serve` | Run the local HTTP API and web UI. |
| `fastskill mcp` | `serve`, `install`, `list` | Run and configure the MCP server. |
| `fastskill cli` | `doctor`, `completion`, `spec` | Diagnose FastSkill and export CLI metadata. |
diff --git a/docs/adr/0016-machines-follow-a-signed-managed-state.md b/docs/adr/0016-machines-follow-a-signed-managed-state.md
index 82ec7634..afc1e5cb 100644
--- a/docs/adr/0016-machines-follow-a-signed-managed-state.md
+++ b/docs/adr/0016-machines-follow-a-signed-managed-state.md
@@ -1,7 +1,7 @@
# Machines can follow a signed managed state; FastSkill applies it and never decides it
Status: accepted. Date: 2026-09-24. Revised: 2026-09-24. Amended: 2026-09-24. Accepted:
-2026-10-09.
+2026-10-09. Implemented: 2026-10-10.
Related: [ADR-0001](0001-remove-sync-command.md),
[ADR-0003](0003-serve-trust-boundary-and-edge-auth.md),
diff --git a/webdocs/cheatsheet.mdx b/webdocs/cheatsheet.mdx
index 84e04327..2a139d60 100644
--- a/webdocs/cheatsheet.mdx
+++ b/webdocs/cheatsheet.mdx
@@ -111,6 +111,18 @@ Modern agents (Claude Code, Cursor, …) read skills directly from the skills di
See [Registry overview](/registry/overview) and [`repo` commands](/cli-reference/repository-command).
+## Managed machines
+
+| Operation | Command | What it does |
+|-----------|---------|--------------|
+| Enroll | `fastskill managed enroll` | Follows the source in trusted settings, applies its first state, registers session-start hooks and the timer |
+| Apply | `fastskill managed apply` | Fetches and applies the newest signed state; `--json` for the result |
+| Status | `fastskill managed status` | Source, subject, expiry, sign-in, targets and quarantine; `--report` adds what the last apply did and the report body |
+| Hooks | `fastskill managed hooks --system` | What an administrator installs machine-wide: the hook entry per agent and the timer files |
+| Unenroll | `fastskill managed unenroll` | Removes what the state put here for this user; the quarantine stays |
+
+See [`managed`](/cli-reference/managed-command), the [managed state](/skill-management/managed-state) format and [Hardening managed machines](/security/managed-machines).
+
## Local server
| Operation | Command | What it does |
diff --git a/webdocs/cli-reference/managed-command.mdx b/webdocs/cli-reference/managed-command.mdx
index 1082b4dd..876f4717 100644
--- a/webdocs/cli-reference/managed-command.mdx
+++ b/webdocs/cli-reference/managed-command.mdx
@@ -16,6 +16,10 @@ Only FastSkill's own entries are changed. A skill folder that FastSkill didn't p
holds other content than the state lists is reported as a collision and left alone.
+The envelope and the payload are documented in [Managed state](/skill-management/managed-state).
+What an administrator pins through device management, and what FastSkill can't enforce, is in
+[Hardening managed machines](/security/managed-machines).
+
## Usage
```bash
diff --git a/webdocs/index.mdx b/webdocs/index.mdx
index 0655aadb..ecadd597 100644
--- a/webdocs/index.mdx
+++ b/webdocs/index.mdx
@@ -41,6 +41,10 @@ The project is designed around a **CLI-first workflow**; local **`fastskill serv
`repo` for remote origins and catalog browsing. See [Registry overview](/registry/overview).
+
+
+ `fastskill managed` follows a signed managed state: verified against pinned keys, deployed to each agent, blocked content quarantined, installs gated by digest. See [managed command](/cli-reference/managed-command).
+
## Start with one local skill
diff --git a/webdocs/security/managed-machines.mdx b/webdocs/security/managed-machines.mdx
new file mode 100644
index 00000000..852c8811
--- /dev/null
+++ b/webdocs/security/managed-machines.mdx
@@ -0,0 +1,169 @@
+---
+title: "Hardening managed machines"
+description: "What FastSkill enforces on a machine that follows a managed state, what it cannot, and what an administrator pins through device management: the system file, keys, hooks and the machine-wide timer."
+---
+
+# Hardening managed machines
+
+[`fastskill managed`](/cli-reference/managed-command) is cooperative configuration management
+with integrity checks. It is not a sandbox. This page says where FastSkill enforces a
+[managed state](/skill-management/managed-state), where it can't, and what an administrator pins so
+that following the state is the default path on every machine.
+
+## What FastSkill enforces
+
+FastSkill enforces the state at its own operations, and nowhere else:
+
+- **`managed apply`** deploys the listed skills into each agent's user skills folder, removes the
+ ones it placed that are no longer listed, and moves blocked content into the quarantine. With
+ `allowed = listed` and `exclusive_targets = true`, content in an agent folder that the state
+ doesn't allow is quarantined too.
+- **Installs** are checked by digest before anything changes: `skill add`, `skill update`,
+ `bundle add`, `bundle update`, `bundle override`, `project install` and restoring personal
+ overrides, and the same through `server serve` and `mcp serve`. Blocked content is refused;
+ with `allowed = listed`, so is anything the state doesn't list.
+- **`skill list --check`** fails on `managed-blocked` and `managed-not-allowed`.
+
+What this protects against:
+
+- a state that was altered, replayed, or issued for another source or another subject, whether
+ it arrives over the network, from a cache, from a file or git source, or from a mirror
+- a repository that tries to change the managed source, or run a command, through project
+ configuration: managed settings in a project file are refused, with an error naming the file
+- other users on the same machine who aren't its administrator
+- a user installing blocked content by mistake, directly or through a project
+
+What it doesn't protect against:
+
+- **The machine's administrator.**
+- **The user acting deliberately.** They can copy files into an agent folder between applies,
+ move the clock, or start an agent without hooks. `required = true` makes following the state
+ the default path and refuses FastSkill's own operations until a valid state is cached. It
+ doesn't stop a determined user. Hard enforcement means configuring the agent itself, through
+ device management, to load only the folders FastSkill manages; few agents can be configured that
+ way today (see [Agents](#agents)).
+- **Whoever holds a pinned signing key.** A key holder decides what enrolled machines install.
+
+## How fast a block lands
+
+A blocked digest reaches a machine at its next successful apply: at login, hourly, and at every
+agent session start where a hook exists. A machine that is on and can sign in applies it within
+about 70 minutes, and its agents drop the skill from their next session. A machine that stops
+applying keeps what it has, however long that lasts: off, offline, or waiting for a sign-in.
+Expiry limits how long FastSkill keeps allowing installs under an old state, not how long
+installed content stays. The [report](/cli-reference/managed-command#the-report) tells a source
+which machines haven't applied recently.
+
+## The system file
+
+Device management deploys the system file at a fixed location:
+
+| Platform | Path |
+| --- | --- |
+| Linux | `/etc/fastskill/managed.toml` |
+| macOS | `/Library/Application Support/FastSkill/managed.toml` |
+| Windows | `%ProgramData%\FastSkill\managed.toml` |
+
+FastSkill honors it only when the file and its folder are owned by the administrator account
+(root, or Administrators or SYSTEM) and nobody else can write to them; otherwise it refuses to run
+and names the problem. A value in the system file wins over the same value in the user file, and
+no managed setting is ever read from a project, an environment variable or a flag.
+
+```toml
+source = "https://skills.example.com/state"
+credential_command = ["/usr/local/bin/example-login", "token"]
+
+[[keys]]
+id = "k1"
+public_key = "BASE64-ED25519-PUBLIC-KEY"
+
+# Refuse to unenroll, and refuse commands that read or change skills until a valid state is cached.
+required = true
+
+# Optional: cover these agents instead of the ones detected for each user.
+targets = ["claude", "gemini"]
+```
+
+When the system file names the source, the first apply enrolls each user: nobody has to run
+`fastskill managed enroll`. With `required = true`, `fastskill managed unenroll` is refused.
+
+## Keys
+
+Pin the verification keys in the system file, so that no user can add one. Pin a new key before
+the source signs with it, and unpin a key to revoke it: FastSkill verifies the cached state every
+time it uses it, so a state signed only by an unpinned key stops counting at the next command.
+Keep the private key where the machines never reach it. FastSkill never needs it; only the source
+signs.
+
+## The credential command
+
+When the source needs a sign-in, name the credential command in the system file by absolute
+path, pointing at a program only the administrator can change. It runs without a shell, and the
+token it prints goes only to the source's origin. From a hook or the timer there is no terminal,
+so `FASTSKILL_INTERACTIVE=0` is set and the command should fail at once instead of prompting; the
+user's next `fastskill managed apply` from a terminal signs in again.
+
+## Administrator-level hooks
+
+`fastskill managed hooks --system` prints, for each agent that runs a command at session start,
+the entry to merge into the agent's administrator-level settings. Device management places those
+files; FastSkill never writes them. When an administrator-level FastSkill hook is present for an
+agent, apply registers no user-level one, so there is no hook for the user to approve.
+
+| Agent | Linux | macOS | Windows |
+| --- | --- | --- | --- |
+| Claude Code | `/etc/claude-code/managed-settings.json` | `/Library/Application Support/ClaudeCode/managed-settings.json` | `C:\Program Files\ClaudeCode\managed-settings.json` |
+| Gemini CLI | `/etc/gemini-cli/settings.json` | `/Library/Application Support/GeminiCli/settings.json` | `C:\ProgramData\gemini-cli\settings.json` |
+
+```bash
+fastskill managed hooks --system
+fastskill managed hooks --system --json
+```
+
+## The machine-wide timer
+
+The same command prints the machine-wide timer: the files to place, and the commands to run
+after placing them. When a machine-wide timer is present, apply installs no per-user one.
+
+| Platform | What device management places |
+| --- | --- |
+| Linux | `/etc/systemd/user/fastskill-managed-apply.timer` and `.service`, a systemd user unit for every user |
+| macOS | `/Library/LaunchAgents/dev.fastskill.managed-apply.plist`, loaded at each user's next login |
+| Windows | `C:\ProgramData\fastskill\managed-apply-task.xml`, registered with `schtasks` |
+
+Each run applies at login and then every hour, after a random wait of up to ten minutes, so a
+fleet doesn't fetch all at once.
+
+## Agents
+
+Hard enforcement lives in the agent: one configured, through device management, to load skills
+only from the folders FastSkill manages. What each agent allows, as checked against its
+documentation on 2026-09-24. "Shared folder" says whether the agent reads `~/.agents/skills`;
+"same session" says whether a skill written by its session-start hook shows up in that session.
+
+| Agent | Shared folder | Command hook at session start | Administrator-level hooks | Same session |
+| --- | --- | --- | --- | --- |
+| Claude Code | no | yes; output reaches the model | managed settings | no |
+| Cursor (IDE and CLI) | yes | yes, not awaited; output not shown | `/etc/cursor/hooks.json` | no |
+| Codex CLI | yes | yes; user hooks need approval | managed hooks, no approval | later |
+| Gemini CLI | yes | yes; output must be JSON | `/etc/gemini-cli/settings.json` | no |
+| Qwen Code | yes | yes | `/etc/qwen-code` | later |
+| GitHub Copilot (VS Code) | yes | yes; only JSON context reaches the model | GitHub policy | no |
+| GitHub Copilot CLI | yes | yes; 30 s timeout | `/etc/github-copilot/policy.d` | no |
+| Devin Desktop | yes | yes | `/etc/devin/hooks.json` | no |
+| Auggie | yes | yes | `/etc/augment/settings.json` | no |
+| Kiro, CodeBuddy, Qoder | no | yes | not checked | no |
+| IBM Bob | no | yes; 10 s timeout | enforced-hooks policy | no |
+| OpenCode, Amp, Pi, Kilo | yes | only through a plugin | not checked | no |
+| Roo Code (archived) | yes | none | none | no |
+
+"Later" means the agent watches its skills folders and picks the skill up during the session.
+FastSkill registers session-start hooks where it has verified support (today Claude Code and
+Gemini CLI); every other agent is kept up to date by the timer. Agents add hooks and folders
+often, so check the agent's current documentation before relying on a row.
+
+## Several users on one machine
+
+The system file is machine-wide. Everything else is per user: the managed store, the machine id,
+the cached state, the quarantine, the hooks and the timer. Each user enrolls and reports
+separately.
diff --git a/webdocs/security/meta.json b/webdocs/security/meta.json
index 9e40831f..c8ea0ed8 100644
--- a/webdocs/security/meta.json
+++ b/webdocs/security/meta.json
@@ -1,7 +1,8 @@
{
"pages": [
"model",
- "best-practices"
+ "best-practices",
+ "managed-machines"
],
"title": "Security"
}
diff --git a/webdocs/skill-management/managed-state.mdx b/webdocs/skill-management/managed-state.mdx
new file mode 100644
index 00000000..c2dc632c
--- /dev/null
+++ b/webdocs/skill-management/managed-state.mdx
@@ -0,0 +1,165 @@
+---
+title: "Managed state"
+description: "The format of the signed managed state a machine follows: the DSSE envelope, the payload fields, what FastSkill refuses, and how a state is bound to its source and subject."
+---
+
+# Managed state
+
+A **managed state** is a signed, resolved list of the skills one user's agents on one machine
+should have. Something outside FastSkill decides it; FastSkill verifies it, applies it and never
+decides it. This page is the format reference. The commands are under
+[`managed`](/cli-reference/managed-command), and what an administrator pins is in
+[Hardening managed machines](/security/managed-machines).
+
+The envelope, the payload and the [report](/cli-reference/managed-command#the-report) are a
+compatibility surface, like the [reconciliation statuses](/skill-management/reconciliation): each
+carries a format version, and within a version fields are only added, never changed or removed.
+
+## The envelope
+
+A state travels in a [DSSE](https://github.com/secure-systems-lab/dsse) envelope: one JSON object.
+
+```json
+{
+ "payloadType": "application/vnd.fastskill.managed-state+json",
+ "payload": "eyJmb3JtYXRfdmVyc2lvbiI6MSwgLi4ufQ",
+ "signatures": [
+ { "keyid": "k1", "sig": "base64 Ed25519 signature" }
+ ]
+}
+```
+
+- `payloadType` is exactly `application/vnd.fastskill.managed-state+json`.
+- `payload` is the payload's exact bytes in base64: standard or URL-safe alphabet, padded or not.
+- Each signature is Ed25519 over DSSE's pre-authentication encoding of the payload type and the
+ payload bytes, `DSSEv1 `, with the lengths
+ in ASCII decimal. No JSON is canonicalized: the signer signs the bytes it puts in `payload`.
+- `keyid` names a pinned key. FastSkill accepts the envelope when at least one signature names a
+ pinned key id and verifies against that key. Signatures that name no pinned key are ignored.
+- An envelope larger than 8 MiB is refused.
+
+Keys are pinned in [trusted settings](/cli-reference/managed-command#settings) only; they are
+never fetched from the source. Several keys can be pinned at once, so pin a new key before the
+source starts signing with it, and unpin a key to revoke it. FastSkill verifies the cached state
+every time it uses it, so a state signed only by an unpinned key stops counting at once.
+
+## The payload
+
+The payload is a JSON object. Its `format_version` is `1`; a state with any other value is refused
+with a message to update FastSkill, before anything else is read.
+
+| Field | Required | Meaning |
+| --- | --- | --- |
+| `format_version` | yes | `1`. |
+| `issued_at` | yes | When the state was issued, RFC 3339. |
+| `expires_at` | yes | When it expires, RFC 3339. |
+| `source` | yes | The managed source it was issued for: the configured `source`, exactly. |
+| `subject` | yes | An opaque label. FastSkill displays it and compares it for equality, never interprets it. |
+| `skills` | no | The skills to install. Each entry has an `id`, a `digest` and an `artifact`. |
+| `allowed` | yes | `listed`: only digests in `skills` or `also_allowed` may be installed. `any`: anything that isn't blocked. |
+| `also_allowed` | no | Digests that may be installed but that the state doesn't install. |
+| `blocked` | no | Digests that must not be present, each with an optional `message` for the user. |
+| `exclusive_targets` | no | With `allowed = listed`, `true` makes apply quarantine content in an agent folder whose digest isn't allowed, instead of leaving it alone. Default `false`. |
+| `report_url` | no | Where each apply sends its report. |
+| `editable` | no | `blocked-only` (default) or `refused`: whether editable local skills skip the allow-list. |
+| `request_url` | no | A link template with one `{digest}`, shown when an install is refused. |
+
+`report_url`, `editable` and `request_url` take effect only with an `https://` source; see
+[the fields only an `https://` source honors](/cli-reference/managed-command#fields-only-an-https-source-honors).
+
+A `skills` entry has:
+
+- `id`: the skill's id, one safe path component. It must equal the identity of the content the
+ archive holds.
+- `digest`: the skill's content digest in the current `sha256-tree-v2:` form. It must equal the
+ digest of the extracted archive.
+- `artifact`: where the skill's ZIP archive is. An `https://` URL; for a file source, also a path
+ next to the state or an absolute path.
+
+Entries are resolved content, like a Lock, not intent, like a Manifest. Applying a state never
+resolves versions, reads repositories or runs dependency resolution: `managed apply` installs
+exactly `skills`. A skill whose declared dependencies aren't in the state is still installed, and
+apply reports the missing dependencies.
+
+### Example
+
+```json
+{
+ "format_version": 1,
+ "issued_at": "2026-10-10T12:00:00Z",
+ "expires_at": "2026-10-17T12:00:00Z",
+ "source": "https://skills.example.com/state",
+ "subject": "team-a",
+ "skills": [
+ {
+ "id": "code-review",
+ "digest": "sha256-tree-v2:3f0c2b7d9e41a6c8d5f0b2e7c1a94d6f8e3b5a7c9d1e2f4a6b8c0d2e4f6a8b1c",
+ "artifact": "https://skills.example.com/archives/code-review-3f0c2b7d.zip"
+ }
+ ],
+ "allowed": "any",
+ "also_allowed": [],
+ "blocked": [
+ {
+ "digest": "sha256-tree-v2:9a1e4c7b2d5f8a0c3e6b9d1f4a7c0e2b5d8f1a3c6e9b2d4f7a0c3e5b8d1f4a6c",
+ "message": "Withdrawn on 2026-10-09. Install code-review instead."
+ }
+ ],
+ "exclusive_targets": false,
+ "report_url": "https://skills.example.com/reports",
+ "request_url": "https://skills.example.com/request?digest={digest}"
+}
+```
+
+### What FastSkill refuses
+
+The whole state is refused when any of these is true:
+
+- its format version isn't `1`
+- a digest is in `blocked` and also in `skills` or `also_allowed`
+- two `skills` entries share an id
+- an id isn't a safe skill id: one path component, under the same rules as any skill id
+- a digest isn't in the current content digest form; the older bare-hex form is never accepted
+ here, because two different trees could share it
+- an `artifact`, `report_url` or `request_url` isn't `https://` (a file source may use local
+ paths for artifacts)
+- `editable` has a value other than `blocked-only` or `refused`
+
+When it installs a skill, apply also requires that the downloaded content's digest equals the
+entry's `digest` and that the content's own identity equals the entry's `id`.
+
+### Bound to its source and subject, and never backwards
+
+- **Source.** `source` must equal the configured managed source. A state issued for another source
+ is refused, even when a pinned key signed it.
+- **Subject.** FastSkill records the `subject` of the first state it accepts after enrollment.
+ Later states with a different subject are refused until `fastskill managed enroll` runs again,
+ so a state issued for one user can't be swapped in for another's.
+- **No rollback.** FastSkill records the highest `issued_at` it has accepted and refuses any state
+ issued earlier. Receiving the same state again is fine.
+- **Clock skew.** A state whose `issued_at` is more than five minutes ahead of the local clock is
+ refused.
+
+### Expiry
+
+FastSkill keeps the last valid state and uses it offline until it expires. After expiry, commands
+that add or change skill content are refused, and `managed apply` adds, changes and removes
+nothing, though blocked content is still quarantined. Nothing is removed because of expiry alone,
+and expiry is checked against the local clock. The next accepted state ends the condition.
+
+## Producing a state
+
+FastSkill ships no signer and serves no managed state. Any tool that writes JSON and signs with
+an Ed25519 key can produce one:
+
+1. Resolve the skills: for each, its id, its content digest, and a ZIP archive of its folder at
+ that digest, placed where `artifact` will point.
+2. Write the payload with `issued_at` now, an `expires_at` that leaves machines time to fetch the
+ next state, and the `source` the machines are configured with.
+3. Sign the pre-authentication encoding of the payload type and the payload bytes with the private
+ key, and wrap the payload and the signature in the envelope under the key's pinned id.
+4. Publish the envelope at the source, next to the archives for a file source, and pin the public
+ key in the machines' trusted settings before any machine fetches it.
+
+A signed state in a git repository or a storage bucket is a complete managed source. Nothing
+about it depends on the transport.
diff --git a/webdocs/skill-management/meta.json b/webdocs/skill-management/meta.json
index 39b29300..2106f255 100644
--- a/webdocs/skill-management/meta.json
+++ b/webdocs/skill-management/meta.json
@@ -2,7 +2,8 @@
"pages": [
"manifest-system",
"validation",
- "reconciliation"
+ "reconciliation",
+ "managed-state"
],
"title": "Skills and projects"
}