From 65f313178cfc65e7bbd0cf076e96962e639952b8 Mon Sep 17 00:00:00 2001 From: Alex Oliveira <4482374+aroff@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:23:03 +0000 Subject: [PATCH] docs(managed): the managed state format, hardening and the terms for the managed client The documentation ADR-0016 still called for, now that every part of it has landed: a format reference for the signed managed state (envelope, payload, refusals, binding, expiry, how a signer produces one), the hardening page (what FastSkill enforces and cannot, the system file, keys, the credential command, administrator-level hooks, the machine-wide timer, agent facts), the five CONTEXT.md terms, a CHANGELOG entry, and the README, cheatsheet, index and managed command cross-links. ADR-0016 is marked implemented. --- CHANGELOG | 16 ++ CONTEXT.md | 42 ++++- README.md | 2 + ...-machines-follow-a-signed-managed-state.md | 2 +- webdocs/cheatsheet.mdx | 12 ++ webdocs/cli-reference/managed-command.mdx | 4 + webdocs/index.mdx | 4 + webdocs/security/managed-machines.mdx | 169 ++++++++++++++++++ webdocs/security/meta.json | 3 +- webdocs/skill-management/managed-state.mdx | 165 +++++++++++++++++ webdocs/skill-management/meta.json | 3 +- 11 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 webdocs/security/managed-machines.mdx create mode 100644 webdocs/skill-management/managed-state.mdx 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" }