@skill-map/spec 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/architecture.md +8 -3
- package/cli-contract.md +19 -12
- package/db-schema.md +8 -8
- package/index.json +7 -7
- package/package.json +1 -1
- package/plugin-author-guide.md +3 -3
- package/schemas/project-config.schema.json +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Spec changelog
|
|
2
2
|
|
|
3
|
+
## 0.63.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Split plugin enable (operational) from import trust (security). Enable/disable now persist to the config layers, not the DB; `config_plugins` becomes a per-plugin local trust store. New `sm plugins trust / untrust` verbs, a trust PATCH route, a Settings UI Trust control, and a `pluginTrust.projectEnabled` opt-in grant or revoke consent to run a project-local plugin. It runs only when enabled AND trusted, so disabling one no longer re-reads as untrusted.
|
|
8
|
+
|
|
9
|
+
## User-facing
|
|
10
|
+
|
|
11
|
+
Plugins now have two separate switches: enable (is it part of the project, shared) and trust (may its code run on your machine). New `sm plugins trust` / `untrust` plus a Trust button in Settings. A plugin you disabled stays disabled instead of nagging that it is untrusted.
|
|
12
|
+
|
|
13
|
+
## 0.62.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- Reworked the `sm tutorial` destination prompt to list providers by vendor name rather than their shared destination folder (several providers share `.agents/skills`), with the open standard shown aka-first. Reorganized the interactive tutorial book: the 'Connect the harness' part is merged into 'The project from zero' so building and wiring the harness is one continuous part, alongside a chapter-by-chapter copy pass across the Claude, Codex and open-standard tracks.
|
|
18
|
+
|
|
19
|
+
## User-facing
|
|
20
|
+
|
|
21
|
+
The `sm tutorial` picker now lists each agent by name (Claude, OpenAI Codex, Google's Antigravity) instead of its install folder. The guided tutorial is tighter: building and connecting your project's harness is now one continuous part, with clearer copy throughout.
|
|
22
|
+
|
|
3
23
|
## 0.62.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/architecture.md
CHANGED
|
@@ -411,7 +411,7 @@ In addition to the `emitLink` path, Extractors MAY emit **Signals** via `ctx.emi
|
|
|
411
411
|
|
|
412
412
|
The kernel's **resolver phase** runs after extraction completes and before analysis starts. For each Signal, the resolver:
|
|
413
413
|
|
|
414
|
-
1. (
|
|
414
|
+
1. (Signal-resolver candidate filter not yet wired) Filters candidates whose `extractorId` is disabled by a per-extension enable filter. The per-extension enable config surface now exists (`plugins.<id>.extensions.<ext>.enabled`, see [`project-config.schema.json`](./schemas/project-config.schema.json) and §Plugin enable vs import trust) and gates extension registration at load time; the Signal-resolver candidate filter that consults it per detection is not wired yet. When the filter empties every candidate, the Signal carries `resolution.outcome = 'rejected'` with `extractorDisabled = { extractorId }`.
|
|
415
415
|
2. Ranks surviving candidates inside the Signal by the active Provider's `resolverRules.kindPriority` (when declared), then `confidence` DESC, then `range` length (`end - start`) DESC, then `extractorId` declaration order. The chosen index is recorded as `resolution.winnerIndex` and (provisionally) `resolution.outcome = 'materialised'`.
|
|
416
416
|
3. For body-scoped Signals with a `range`, the resolver builds overlap clusters per source (transitive closure of range intersection). Size-1 clusters keep their winner. For size 2+ clusters, the resolver re-applies the same four-step tiebreak to each Signal's winning candidate to pick a cluster winner. Losers flip to `resolution.outcome = 'rejected'` with `rejectedBy = { source, range, extractorId, reason }`, where `reason` names the deciding tiebreak step: `kind-priority`, `higher-confidence`, `longer-range`, or `earlier-declaration`. External pseudo-link clusters (every member targets `http://` / `https://`) skip cross-cluster ranking, every member materialises (URL-targeted Signals never conflict with internal-target Signals or each other because they leave the local graph).
|
|
417
417
|
4. Materialises every Signal whose final `outcome === 'materialised'` as a Link, identical in shape to one emitted directly via `emitLink`. The materialised Link's `sources[]` carries the winning candidate's `extractorId` so attribution survives.
|
|
@@ -558,7 +558,11 @@ Hooks introduce no new persisted state and do NOT participate in the determinist
|
|
|
558
558
|
### Locality
|
|
559
559
|
|
|
560
560
|
- **Drop-in**: extensions live inside plugins, discovered at boot from `<cwd>/.skill-map/plugins/<id>/` only. The `--plugin-dir <path>` escape hatch on the `sm plugins …` verb family loads a custom directory per invocation when the user explicitly opts in.
|
|
561
|
-
- **
|
|
561
|
+
- **Plugin enable vs import trust (security boundary, two orthogonal axes).** A drop-in plugin discovered under the project-local `<cwd>/.skill-map/plugins/` is parsed (manifest read + surfaced in `sm plugins list`) but its extension CODE is imported and executed only when BOTH axes allow it:
|
|
562
|
+
- **Enabled (operational, shareable).** Whether the plugin / extension is part of the project. Lives in the config layers (`plugins.<id>.enabled`, `plugins.<id>.extensions.<ext>.enabled`), `settings.json` (committed team baseline) overlaid by `settings.local.json` (per-checkout override). Written by `sm plugins enable / disable` (defaults to the shared `settings.json`; `--local` targets `settings.local.json`) and the Settings toggle. Default: the installed default (`true` for `stable` / `beta`, `false` for `experimental` / `deprecated`).
|
|
563
|
+
- **Trusted (security, LOCAL, per-machine).** Whether THIS machine's operator consents to importing the plugin's code. A per-plugin boolean in the `config_plugins` (DB) trust store, written by `sm plugins trust / untrust <id>` and the per-plugin Trust control in the UI. The DB is structurally local (never committed, not a config layer), so trust cannot travel in a clone. A committed `settings.json` does NOT and CANNOT grant import trust: honouring a shared file would let a hostile repo auto-execute its plugins on the victim's first `sm scan`.
|
|
564
|
+
|
|
565
|
+
A plugin's code is imported iff it is **enabled** AND (it carries a local **trust** grant OR the local opt-in `pluginTrust.projectEnabled` is set). Per-extension enable is applied after import, at registration. A fresh clone has no DB trust row and no local opt-in, so its project-local plugins are discovered-but-unexecuted (`status: 'disabled'`, `untrusted: true`); the runtime emits a one-time notice naming how many were found and pointing at `sm plugins trust <id>`. The local escape hatch `pluginTrust.projectEnabled` (in `settings.local.json` only, stripped from the committed layer, gated behind a confirm because it expands the local execution surface) trusts every plugin the project enables, for teams that vet plugins in code review. The loader keeps the two not-loaded reasons distinct: `disabledByConfig` (the operator turned it off) vs `untrustedNotLoaded` (no local trust grant), so an explicit disable never re-reads as untrusted across a restart. Built-in extensions (compiled into the CLI) and an explicit `--plugin-dir <path>` (the operator pointed the loader at the code on purpose) are NOT trust-gated; `--no-plugins` skips discovery entirely. The `sm plugins` management family (`list` / `show` / `enable` / `disable` / `trust` / `untrust` / `doctor`) still imports discovered plugin code to enumerate extensions, running those verbs is itself the operator's explicit choice to work with the project's plugins.
|
|
562
566
|
- **Built-in**: the reference impl bundles a default extension set (one Provider, four extractors, five analyzers, one formatter, one hook). The fifth analyzer, `core/schema-violation`, replays every scanned node and link through the authoritative spec schemas via AJV, the kernel-side guard against persisting non-conforming graph rows. The first built-in Hook is `core/update-check`, subscribing to `shutdown` to run the once-per-day "update available" probe + banner that lived on the CLI entry path before the Hook kind had concrete consumers. Loaded from `src/extensions/`, these are indistinguishable from plugin-supplied extensions to the kernel.
|
|
563
567
|
|
|
564
568
|
---
|
|
@@ -654,8 +658,9 @@ One locality class constrains which layers a given key MAY live in. Enforced in
|
|
|
654
658
|
Members:
|
|
655
659
|
- `allowEditSmFiles`, per-project consent to create / modify `.sm` sidecars.
|
|
656
660
|
- `scan.referencePaths`, additional link-validation paths.
|
|
661
|
+
- `pluginTrust.projectEnabled`, the local opt-in that trusts every plugin the project enables (the import-trust escape hatch).
|
|
657
662
|
|
|
658
|
-
|
|
663
|
+
The first two describe disk access the local operator opted into, the third the local code-execution surface; sharing any of them via the repo would silently expand every collaborator's surface (scan paths, or auto-running the repo's plugins) in a way only the original author consented to. `pluginTrust.projectEnabled` in particular MUST stay local: honouring a committed `true` would let a cloned repo auto-execute its own plugins, the exact supply-chain attack the import-trust gate prevents.
|
|
659
664
|
|
|
660
665
|
Adding a new entry is a behaviour change for older installs that wrote the key into a committed file: the value gets stripped at read time. The changeset adding the entry MUST document the migration.
|
|
661
666
|
|
package/cli-contract.md
CHANGED
|
@@ -52,9 +52,13 @@ external location MAY be loaded via the `--plugin-dir <path>` escape
|
|
|
52
52
|
hatch on the `sm plugins …` verb family, user-explicit per invocation.
|
|
53
53
|
Project-local plugins are discovered but their code is NOT executed by
|
|
54
54
|
`sm scan` / `sm serve` (and the other runtime verbs) until the operator
|
|
55
|
-
grants
|
|
56
|
-
`
|
|
57
|
-
|
|
55
|
+
grants LOCAL trust via `sm plugins trust <id>` (a per-plugin row in the
|
|
56
|
+
`config_plugins` DB trust store, or the local opt-in
|
|
57
|
+
`pluginTrust.projectEnabled`); the committed `settings.json` does not and
|
|
58
|
+
cannot grant it. Enable / disable (`sm plugins enable / disable`, persisted
|
|
59
|
+
in the config layers) is the separate OPERATIONAL axis and grants no trust.
|
|
60
|
+
`--plugin-dir` and built-ins are not gated. See
|
|
61
|
+
[`architecture.md` §Locality](./architecture.md) (plugin enable vs import
|
|
58
62
|
trust) for the normative model.
|
|
59
63
|
|
|
60
64
|
### User-settings file (narrow, documented exception)
|
|
@@ -195,7 +199,7 @@ The destination is the selected Provider's `scaffold.skillDir` (e.g. `.claude/sk
|
|
|
195
199
|
|
|
196
200
|
- `--for <provider-id>` selects the Provider explicitly (e.g. `--for claude`, `--for agent-skills`). The id MUST be a registered Provider declaring `scaffold.skillDir`; any other value is a usage error.
|
|
197
201
|
- Without `--for`, the default is the first scaffold-capable Provider in catalog order (Claude). The verb requires an empty cwd (see below), so there is no marker to detect: provider auto-detection does not apply.
|
|
198
|
-
- Without `--for`, on interactive stdin the verb prompts with a numbered list of Providers declaring `scaffold.skillDir`, marking the default (Claude); an empty answer accepts it. Each option shows the Provider
|
|
202
|
+
- Without `--for`, on interactive stdin the verb prompts with a numbered list of Providers declaring `scaffold.skillDir`, marking the default (Claude); an empty answer accepts it. Each option shows the Provider's vendor name, NOT its destination folder (several Providers share `.agents/skills`, so the folder does not identify the lens). For a Provider that carries `scaffold.aka`, the aka vendor leads with the Provider label in parentheses (e.g. the open standard renders as `Google's Antigravity (Standard: Agent skills)`). The `aka` strings are display-only, NOT accepted by `--for`.
|
|
199
203
|
- Without `--for`, on non-interactive stdin (pipes, CI) the verb selects the default without prompting, staying scriptable.
|
|
200
204
|
- `--experimental` includes Providers flagged `stability: 'experimental'` as scaffold destinations and enables them in the seeded fixture so the demo scan classifies their nodes. Without it, experimental Providers are omitted from the prompt and `--for <experimental-id>` is a usage error (they ship disabled by default). Default behaviour offers the stable, ready destinations (today Claude, the rich-track anchor, and the open-standard `agent-skills`, the basic-track anchor).
|
|
201
205
|
|
|
@@ -295,9 +299,9 @@ Keys are dot-paths (`jobs.minimumTtlSeconds`, `scan.tokenize`). Unknown keys →
|
|
|
295
299
|
|
|
296
300
|
#### Privacy-sensitive config
|
|
297
301
|
|
|
298
|
-
Keys whose value
|
|
302
|
+
Keys whose value expands the project's surface, either disk access OUTSIDE the project root (today: `scan.extraFolders`, `scan.referencePaths`) or the local code-execution surface (`pluginTrust.projectEnabled`, which locally trusts every plugin the project enables), are gated behind `--yes` so the user never expands the surface by accident. The analyzer:
|
|
299
303
|
|
|
300
|
-
- `sm config set <privacy-key> <value>` (without `--yes`), when the new value would expand the surface (adding `extraFolders` / `referencePaths` paths resolving outside the project root), exits with code `2` and prints the
|
|
304
|
+
- `sm config set <privacy-key> <value>` (without `--yes`), when the new value would expand the surface (adding `extraFolders` / `referencePaths` paths resolving outside the project root, or setting `pluginTrust.projectEnabled` to `true`), exits with code `2` and prints the affected detail to stderr (the exposed paths, or the list of currently-untrusted plugins it would trust), suggesting `--yes` to confirm.
|
|
301
305
|
- `sm config set <privacy-key> <value> --yes`, proceeds and prints the same list as a confirmation receipt.
|
|
302
306
|
- Writes that NARROW the surface (removing paths) do not require `--yes`.
|
|
303
307
|
|
|
@@ -305,7 +309,7 @@ The Settings UI's Project section enforces the same analyzer via a confirm dialo
|
|
|
305
309
|
|
|
306
310
|
#### Project-local-only config
|
|
307
311
|
|
|
308
|
-
The three privacy-sensitive keys above PLUS `allowEditSmFiles` are members of `PROJECT_LOCAL_ONLY_KEYS` (see [`architecture.md` §Config layering · Per-key locality](./architecture.md#per-key-locality)). The values are per-user-per-project and MUST NOT travel via the committed repo:
|
|
312
|
+
The three privacy-sensitive keys above PLUS `allowEditSmFiles` and `pluginTrust.projectEnabled` are members of `PROJECT_LOCAL_ONLY_KEYS` (see [`architecture.md` §Config layering · Per-key locality](./architecture.md#per-key-locality)). The values are per-user-per-project and MUST NOT travel via the committed repo:
|
|
309
313
|
|
|
310
314
|
- `sm config set` writes them to `<cwd>/.skill-map/settings.local.json` (gitignored).
|
|
311
315
|
- The loader strips them (with a warning) when found in the committed `project` layer (`settings.json`). An older install that wrote one to `settings.json` keeps validating against the schema, but the value is ignored at read time and `sm config show --source` surfaces the warning.
|
|
@@ -561,8 +565,10 @@ Authentication: the nonce is the sole credential. An implementation MUST reject
|
|
|
561
565
|
|---|---|
|
|
562
566
|
| `sm plugins list [<id>]` | No id: auto-discovered plugins with status, one row per plugin (`--json` emits the aggregate discovered-plugin registry). With a bare plugin id: that plugin's manifest plus its extension detail (kind / version / per-extension status; `--json` emits the single `DiscoveredPlugin`). A qualified `<plugin>/<ext>` id is rejected with a redirect to `sm plugins show`. |
|
|
563
567
|
| `sm plugins show <plugin>/<ext>` | Single-extension detail (Kind / Version / Stability / Description / Preconditions / Entry; `--json` emits the single extension object). Accepts only a qualified `<plugin>/<ext>` id; a bare plugin id is rejected with a redirect to `sm plugins list <id>`. |
|
|
564
|
-
| `sm plugins enable <id>... \| --all` |
|
|
565
|
-
| `sm plugins disable <id>... \| --all` |
|
|
568
|
+
| `sm plugins enable <id>... \| --all [--local]` | Operational toggle ON. Persists the per-extension `enabled` in the config layers (`plugins.<id>.extensions.<ext>.enabled`), defaulting to the shared `settings.json`; `--local` writes `settings.local.json` instead. Does NOT grant import trust (use `sm plugins trust`). Accepts one or more ids; batches are all-or-nothing (any unknown / mismatched id aborts before any write) and repeated ids are deduped. `--all` applies to every discovered plugin. |
|
|
569
|
+
| `sm plugins disable <id>... \| --all [--local]` | Operational toggle OFF; does not delete the plugin directory and does not revoke trust. Persists `enabled: false` in the config layers (`--local` targets `settings.local.json`). Eagerly purges each id's rows from `scan_contributions` so its UI chips disappear before the next scan (plugin-managed state in `state_plugin_kvs` / dedicated tables is preserved, see `plugin-kv-api.md`). Accepts one or more ids; batches are all-or-nothing and repeated ids are deduped. `--all` applies to every discovered plugin. |
|
|
570
|
+
| `sm plugins trust <id>... \| --all` | Grant LOCAL import trust: the operator's security consent to import and run this plugin's code on this machine. Persists a per-plugin row in the `config_plugins` (DB) trust store, keyed by the bare plugin id (a qualified `<plugin>/<ext>` collapses to its plugin). Local only, never committed, so it cannot travel in a clone. Distinct from `enable`: a plugin runs only when it is both enabled (config) and trusted. Accepts one or more ids; batches are all-or-nothing and repeated ids are deduped. `--all` applies to every discovered plugin. |
|
|
571
|
+
| `sm plugins untrust <id>... \| --all` | Revoke local import trust: drops the plugin's `config_plugins` trust row, so it reverts to discovered-but-unexecuted on the next scan / restart. Does NOT change the enable state and does NOT delete the plugin directory. Same id / batch semantics as `trust`. |
|
|
566
572
|
| `sm plugins config <plugin>/<ext> [<settingId> [<value>]] [--reset]` | Read or write the operator-supplied values for an extension's declared `settings`. No `settingId`: table of each declared setting with its effective value and the layer that set it (`--json` emits the resolved set). With `<settingId> <value>`: coerce the shell string to the setting's input-type, validate, then write under `plugins.<pluginId>.extensions.<extId>.settings.<settingId>` (a normal setting lands in `settings.json`; a `secret`-typed one is forced into `settings.local.json`, gitignored, never committed); prints a "re-scan to apply" reminder. `--reset` drops the override back to the manifest default. Requires a qualified `<plugin>/<ext>` id; a bare plugin id is rejected with a redirect to `sm plugins list <id>`. `secret` values are redacted as `<redacted>` in output. |
|
|
567
573
|
| `sm plugins doctor` | Revalidate all plugins against current spec version; update `status` fields. `--json` emits the report shape declared by [`plugins-doctor.schema.json`](./schemas/plugins-doctor.schema.json): `{ ok: true, kind: 'plugins.doctor', counts, issues[], warnings[], elapsedMs }`. |
|
|
568
574
|
|
|
@@ -630,10 +636,11 @@ The reference implementation ships a Hono BFF rooted at `src/server/`. One Node
|
|
|
630
636
|
| `GET /api/branch?path=<prefix>&path=<prefix>&limit=<n>` | implemented | Branch projection for the map. `path` is **repeatable**: the response is the UNION of the subtrees under every given prefix (forward-slash; a node matches a prefix when its path equals it or starts with `<prefix>/`). No `path` (or a single empty one) = the whole corpus. The union is capped at `limit` nodes (default and effective max = the scan's `maxRenderNodes`), so the response stays bounded regardless of how many prefixes are sent. Direct shape (no envelope wrap, like `/api/scan`): `{ schemaVersion, kind: 'branch', branch: { paths, total, rendered, truncated, cap }, nodes: Node[], links: Link[], issues: Issue[] }`, where `paths` echoes the requested prefixes. `nodes` is the first `rendered` nodes of the union in stable path order; `links` carries only edges whose source AND **resolved target** are in `nodes` (the resolved target is `resolvedTarget`, the node a trigger-style `invokes` / `mentions` link points to, falling back to the raw `target` for path-style links; a genuinely-broken link whose target resolves to no node is excluded); `issues` carries those touching `nodes`. `truncated` is `total > cap`. Lets the SPA render a multi-folder selection without hydrating the full `ScanResult`. DB absent → empty branch (zero nodes). Validation: `limit` integer ≥ 1 else 400 `bad-query`. |
|
|
631
637
|
| `GET /api/graph?format=ascii\|json\|md` | implemented | formatter-rendered graph. `Content-Type` per format: `text/plain` (ascii), `application/json` (json), `text/markdown` (md / mermaid). Default `format=ascii`. Unknown format → 400 `bad-query`. |
|
|
632
638
|
| `GET /api/config` | implemented | `RestEnvelope` (`kind: 'config'`), merged effective config (defaults → user → user-local → project → project-local → override). |
|
|
633
|
-
| `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`), list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project', description?: string, locked?: boolean, startsAsDisabled?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, stability?: 'experimental'\|'beta'\|'stable'\|'deprecated', locked?: boolean }> }`. The plugin row has no granular toggle axis; its `status` aggregates the children (`'enabled'` when at least one extension is enabled, else `'disabled'`). The `description` carries the manifest-declared description (built-ins: hardcoded on `IBuiltInPlugin`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`), plus the optional `stability` lifecycle label per `extensions/base.schema.json#/properties/stability` (omitted when undeclared; missing means `stable`. The SPA badges only non-default values, `experimental` / `beta` / `deprecated`, next to the extension row; `stable` renders nothing. Presentation-only EXCEPT `experimental` and `deprecated`, which each flip the extension's installed default to disabled). The SPA's Settings list renders descriptions as muted secondary text and indexes them for substring search alongside the ids. The `extensions` array is present whenever the plugin declares any extension AND loaded successfully. Each entry's `enabled` reflects the per-extension
|
|
634
|
-
| `PATCH /api/plugins/:id` | implemented | **Bundle (aggregate) macro endpoint**: fans the toggle out across every extension inside the plugin. `:id` MUST be a top-level plugin id (no slash); qualified-id form is the sibling route below. Body `{ enabled: boolean }` (JSON).
|
|
639
|
+
| `GET /api/plugins` | implemented | `RestEnvelope` (`kind: 'plugins'`), list of installed plugins (built-in + drop-in) with status. Item shape: `{ id, version, kinds, status, reason, source: 'built-in'\|'project', description?: string, locked?: boolean, startsAsDisabled?: boolean, trusted?: boolean, extensions?: Array<{ id, kind, version, enabled, description?: string, stability?: 'experimental'\|'beta'\|'stable'\|'deprecated', locked?: boolean }> }`. The plugin row has no granular toggle axis; its `status` aggregates the children (`'enabled'` when at least one extension is enabled, else `'disabled'`). The `description` carries the manifest-declared description (built-ins: hardcoded on `IBuiltInPlugin`; drop-ins: `plugin.json#/description`); each `extensions[]` entry carries its manifest's `description` per `IExtensionBase` (`extensions/base.schema.json#/properties/description`), plus the optional `stability` lifecycle label per `extensions/base.schema.json#/properties/stability` (omitted when undeclared; missing means `stable`. The SPA badges only non-default values, `experimental` / `beta` / `deprecated`, next to the extension row; `stable` renders nothing. Presentation-only EXCEPT `experimental` and `deprecated`, which each flip the extension's installed default to disabled). The SPA's Settings list renders descriptions as muted secondary text and indexes them for substring search alongside the ids. The `extensions` array is present whenever the plugin declares any extension AND loaded successfully. Each entry's `enabled` reflects the per-extension config resolution (`settings.local.json` over `settings.json` over installed default, where the default is `false` for `experimental` and `deprecated` extensions and `true` otherwise); enable no longer reads from the DB. The optional `locked: true` flag is stamped when the plugin id (or qualified extension id) appears in the host's lock-list (`src/server/locked-plugins.ts`); locked items render the toggle disabled in the SPA and any `PATCH` returns `403 locked`. Omitted when false. The optional `trusted: true` flag is stamped on a drop-in plugin that carries a local import-trust grant (a `config_plugins` trust row or the `pluginTrust.projectEnabled` opt-in); omitted when false, so an untrusted project-local plugin reads `trusted` absent (built-ins omit it, they are never trust-gated). The optional `startsAsDisabled: true` flag is stamped on a drop-in plugin (never built-ins) that was config-disabled at `sm serve` boot (discovery-time `status: 'disabled'` for a reason OTHER than untrust), so the handlers were never bucketed into the runtime; an untrusted plugin carries the one-time untrusted boot notice instead and does NOT get `startsAsDisabled`. The SPA renders a per-row hint when `startsAsDisabled` is set AND the user re-enables at least one of the plugin's extensions in the buffered state, since re-enabling requires `sm serve` restart (the rest of the toggle pipeline applies live). Omitted when false. |
|
|
640
|
+
| `PATCH /api/plugins/:id` | implemented | **Bundle (aggregate) macro endpoint**: fans the toggle out across every extension inside the plugin. `:id` MUST be a top-level plugin id (no slash); qualified-id form is the sibling route below. Body `{ enabled: boolean }` (JSON). Writes the per-extension `enabled` (`plugins.<id>.extensions.<ext>.enabled`) for every child to the config layers (the shared `settings.json`); locked children are silently dropped, mirroring the CLI's bulk-mode lock semantics. Response is the canonical `RestEnvelope` (`kind: 'plugins'`) reflecting the post-write state. **Lock**, rejected with 403 `locked` when the plugin id itself is in the host lock-list (`src/server/locked-plugins.ts`). **Apply window**, the override applies on the next scan; both the BFF and the watcher build a fresh resolver from the config layers before composing extensions, so the toggle is honoured without restarting `sm serve`. The endpoint purges `scan_contributions` rows for each disabled extension immediately so the UI stops rendering its chips before the next scan. **Exception**, drop-in plugins whose discovery-time `status` was `'disabled'` (carried as `startsAsDisabled: true`) are NOT in the runtime extension buckets; re-enabling them via PATCH persists the override but requires `sm serve` restart for the handlers to load. The SPA surfaces this per-row. The endpoint does NOT broadcast a WS event today. |
|
|
635
641
|
| `PATCH /api/plugins/:pluginId/extensions/:extensionId` | implemented | Canonical per-extension toggle. Body `{ enabled: boolean }`. Both segments are URL-path-segment-encoded (no slash inside `:pluginId` or `:extensionId`). 404 `not-found` when the plugin id is unknown or the extension id does not belong to that plugin. **Lock**, rejected with 403 `locked` when either the plugin id or the qualified `pluginId/extensionId` appears in the host lock-list. Same persistence + apply-window semantics as the bundle macro form (including the `startsAsDisabled` exception). The SPA's buffered Settings modal posts here for every per-row flip. |
|
|
636
|
-
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare plugin id (bundle cascade macro, same semantics as `PATCH /api/plugins/:id`) or a qualified `<plugin>/<extension>` id. Empty `changes` is a no-op (returns the current `GET /api/plugins` envelope). The route validates the **entire batch** before writing; any invalid entry (`unknown-plugin` / `locked`) rejects the whole request with the offending id in `error.details.id`, DB untouched. Valid batches apply in **one SQLite transaction**: bare plugin ids expand to child qualified ids before persistence; `
|
|
642
|
+
| `PATCH /api/plugins` | implemented | Bulk toggle. Body `{ "changes": Array<{ "id": string, "enabled": boolean }> }` where each `id` is either a bare plugin id (bundle cascade macro, same semantics as `PATCH /api/plugins/:id`) or a qualified `<plugin>/<extension>` id. Empty `changes` is a no-op (returns the current `GET /api/plugins` envelope). The route validates the **entire batch** before writing; any invalid entry (`unknown-plugin` / `locked`) rejects the whole request with the offending id in `error.details.id`, DB untouched. Valid batches apply in **one SQLite transaction**: bare plugin ids expand to child qualified ids before persistence; the per-extension `enabled` is written to the config layers per resulting key, then one grouped `scan_contributions` purge per disabled extension (enables skip the purge). Response is the same `RestEnvelope` (`kind: 'plugins'`) shape as `GET /api/plugins`, reflecting the post-write state; the SPA replaces its modal state from it. Apply-window and `startsAsDisabled` exception semantics match the per-id routes. The single-id `PATCH /api/plugins/:id` and qualified-id sibling stay available for CLI / external automation; the bulk variant lets the SPA stage edits in a buffered modal and ship the final delta atomically. |
|
|
643
|
+
| `PATCH /api/plugins/:id/trust` | implemented | Plugin-level LOCAL import-trust toggle (the security axis, separate from enable). `:id` MUST be a bare plugin id (no slash). Body `{ trusted: boolean }` (JSON). Writes (true) or clears (false) the plugin's row in the `config_plugins` (DB) trust store. Built-ins and locked ids are rejected with 403 `locked` (they are never trust-gated). Response is the canonical `RestEnvelope` (`kind: 'plugins'`) reflecting the post-write `trusted` projection. Granting trust lets an enabled plugin's code import on the next scan / `sm serve` restart (handlers load on restart, like the `startsAsDisabled` case); revoking reverts the plugin to discovered-but-unexecuted. Does NOT touch the enable axis. The local opt-in `pluginTrust.projectEnabled` (a project-local-only config key, surface-expanding, gated by the `confirm-required` flow) is the bulk alternative, set through the project-preferences route, not here. |
|
|
637
644
|
| `ALL /api/*` (other) | reserved | structured 404 envelope (see below); future endpoints land in subsequent sub-steps. |
|
|
638
645
|
| `GET /ws` | implemented (v14.4.a) | accepts WebSocket upgrade and registers the client with the BFF broadcaster. Server-push only, the server fans `scan.*` (and forthcoming `issue.*`) events to every connected client. See **WebSocket protocol** below. |
|
|
639
646
|
| `GET *` | implemented | static asset from the resolved UI bundle, falling back to `index.html` for SPA deep links. |
|
package/db-schema.md
CHANGED
|
@@ -431,22 +431,22 @@ The BFF's `/api/nodes` route loads the full set of favorited paths once per requ
|
|
|
431
431
|
|
|
432
432
|
### `config_plugins`
|
|
433
433
|
|
|
434
|
-
|
|
434
|
+
Records the operator's LOCAL import-trust grants for project-local drop-in plugins. This is the **security** axis (may THIS machine import and run the plugin's code), NOT the operational enable/disable toggle, which lives in the config layers (`plugins.<id>.enabled` / `plugins.<id>.extensions.<ext>.enabled` in `settings.json` / `settings.local.json`). Discovery is still filesystem-based; this table records per-machine consent. The table name is retained for continuity.
|
|
435
435
|
|
|
436
436
|
| Column | Type | Constraint |
|
|
437
437
|
|---|---|---|
|
|
438
438
|
| `plugin_id` | TEXT | PRIMARY KEY |
|
|
439
|
-
| `
|
|
440
|
-
| `config_json` | TEXT | NULL |
|
|
439
|
+
| `trusted` | INTEGER | NOT NULL DEFAULT 0, CHECK (`trusted` IN (0,1)) |
|
|
441
440
|
| `updated_at` | INTEGER | NOT NULL |
|
|
442
441
|
|
|
443
|
-
**Effective
|
|
442
|
+
**Effective trust resolution.** A project-local plugin's code is imported iff it is **enabled** (config layers) AND it is **trusted** locally. A plugin is trusted iff either:
|
|
444
443
|
|
|
445
|
-
1. `config_plugins
|
|
446
|
-
2.
|
|
447
|
-
3. Installed default, every discovered plugin is enabled until told otherwise.
|
|
444
|
+
1. A `config_plugins` row with `trusted = 1` exists for its `plugin_id`, written by `sm plugins trust` (cleared by `sm plugins untrust`). Keyed by the **bare plugin id** (trust is per-plugin; a qualified `<plugin>/<ext>` collapses to its plugin), OR
|
|
445
|
+
2. the local opt-in `pluginTrust.projectEnabled` (project-local-only config, honoured only from `settings.local.json`) is set, which trusts every plugin the project enables.
|
|
448
446
|
|
|
449
|
-
The
|
|
447
|
+
The store is structurally LOCAL: the DB never travels in a commit and is not a config layer, so a cloned repo's committed `settings.json` can never grant import trust to its own plugins (the supply-chain guard). Built-ins and `--plugin-dir` are not trust-gated. See [`architecture.md` §Locality](./architecture.md) (plugin enable vs import trust).
|
|
448
|
+
|
|
449
|
+
Greenfield note: this table previously stored the enable/disable toggle (`enabled` plus a vestigial `config_json`); both were dropped when enable moved to the config layers. Per the pre-1.0 greenfield posture the redefinition is applied inline to `001_initial.sql` with no migration file; the `scan_meta.schema_fingerprint` drift path rebuilds the cache on the first scan.
|
|
450
450
|
|
|
451
451
|
### `config_preferences`
|
|
452
452
|
|
package/index.json
CHANGED
|
@@ -174,14 +174,14 @@
|
|
|
174
174
|
}
|
|
175
175
|
]
|
|
176
176
|
},
|
|
177
|
-
"specPackageVersion": "0.
|
|
177
|
+
"specPackageVersion": "0.63.0",
|
|
178
178
|
"integrity": {
|
|
179
179
|
"algorithm": "sha256",
|
|
180
180
|
"files": {
|
|
181
|
-
"CHANGELOG.md": "
|
|
181
|
+
"CHANGELOG.md": "3b206cc04092e6a60b720983546117e6a76ef564ae4cee9672d3428e58048d6e",
|
|
182
182
|
"README.md": "a790cd010b46d47883d1f37e3893cea9d7aa69ec4750c0202e6a0c99991e7980",
|
|
183
|
-
"architecture.md": "
|
|
184
|
-
"cli-contract.md": "
|
|
183
|
+
"architecture.md": "f536b1a660958f701fd356858ca352a0929ac7454c6d59de71644f3693d9f21a",
|
|
184
|
+
"cli-contract.md": "07a1b15ec07a9bbcbe9d1eccba3ca7234043e52bbb584c25470f66d571e6b2ae",
|
|
185
185
|
"conformance/README.md": "dcbef7249f161acf597552a05dcadc813cd0ced430dcd3f813fcf5e1c876335d",
|
|
186
186
|
"conformance/cases/backtick-path-extraction.json": "4620e7f8bc161fc57cb44001e9d99879c7e22b4865a0c27a20dc28969cd936d9",
|
|
187
187
|
"conformance/cases/extractor-collision-detection.json": "179a02c61892f0d26492de0c4e2c327fa6b4986d1265a8f119e871df6afe4658",
|
|
@@ -230,11 +230,11 @@
|
|
|
230
230
|
"conformance/fixtures/view-contribution-payloads/notes/example.md": "312b1919cd7fd0f233648b053acfb2975662ede3c65dd391cc508204b67ad6fb",
|
|
231
231
|
"conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/analyzers/everything/index.js": "ea0022fec7f0fd5a26ba12db1310335f434f2f820682206a3a9542d98db0d346",
|
|
232
232
|
"conformance/fixtures/view-slots-all/.skill-map/plugins/all-slots/plugin.json": "c48e8a0574947ade0b4eb189d6bc27a48e24f92f616aacdc177f2d22d472a599",
|
|
233
|
-
"db-schema.md": "
|
|
233
|
+
"db-schema.md": "05a4e323877d391180201f08eca9a226bd70f6ca593e1052602c54827685ef70",
|
|
234
234
|
"interfaces/security-scanner.md": "0996dd782e2d39d4791f2e290da4bb1a68a5b30c1f79187977188ec8e3fe6ef2",
|
|
235
235
|
"job-events.md": "2c7017f5f0003b19653424111a07043487173cbe88b51e961598bb1693987059",
|
|
236
236
|
"job-lifecycle.md": "ce33bc8bb5090ea183f860e495bfccc2a4a0ac2e23f6ebad83b9c28aad59124e",
|
|
237
|
-
"plugin-author-guide.md": "
|
|
237
|
+
"plugin-author-guide.md": "1b5980a76621c9aba7587b213d648e33f4a6c428ac8b40a5ee6e2378229c3fa4",
|
|
238
238
|
"plugin-kv-api.md": "5e095581020043af73ff028e272f56d42ca9eb6e506dd777d45703f9db796a5b",
|
|
239
239
|
"plugin-quickstart.md": "19092b278d80df357ea623dc3bd9f833d059582ee1356f317621913d91e50512",
|
|
240
240
|
"prompt-preamble.md": "5d0f836688aa23eafc32104c3174132340b268361f6060326eec84da17c6ad6d",
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
"schemas/node.schema.json": "1ebba38e0c0ae022fccbc0cdf7c298da1720a68d4cb375f0baf9f0847998a0d8",
|
|
262
262
|
"schemas/plugins-doctor.schema.json": "03e2dc51c052a09bf0198c80e2c26e6129734ada4a748e483245de3dd8576c42",
|
|
263
263
|
"schemas/plugins-registry.schema.json": "211d081691fc83526e1593c79ed9741ad8a5dbd4db1a756f72141b0cced2ea15",
|
|
264
|
-
"schemas/project-config.schema.json": "
|
|
264
|
+
"schemas/project-config.schema.json": "6b654c0aa5ad4cd950166804327b41059f0c5a1d84e1b62ee1015c22a1c100ba",
|
|
265
265
|
"schemas/refresh-report.schema.json": "47184d4f6b15e9b7671dc178b3b3886a64422da198898508ecdb2cb27876db04",
|
|
266
266
|
"schemas/report-base-deterministic.schema.json": "59785fe6f3ceb34814bbbd03d10fa7336a32835ce598946f2923d469b32aa32a",
|
|
267
267
|
"schemas/report-base.schema.json": "e4d25f055e24f18ae0f77c24661c1bddc87ff2e43b001b6a827fcb14f9753f44",
|
package/package.json
CHANGED
package/plugin-author-guide.md
CHANGED
|
@@ -60,7 +60,7 @@ The kernel scans one root: `<cwd>/.skill-map/plugins/`, committed-with-the-repo
|
|
|
60
60
|
|
|
61
61
|
A plugin is any direct child directory of that root containing a `plugin.json`. Nested directories are not searched recursively. Pass `--plugin-dir <path>` to replace the default root with a custom directory (mostly for testing, or a plugin set the operator opts into).
|
|
62
62
|
|
|
63
|
-
**Import trust.** A project-local plugin is discovered (manifest parsed, listed by `sm plugins list`) but its code is NOT imported by the runtime verbs (`sm scan`, `sm serve`, ...) until the operator trusts it locally with `sm plugins
|
|
63
|
+
**Import trust.** A project-local plugin is discovered (manifest parsed, listed by `sm plugins list`) but its code is NOT imported by the runtime verbs (`sm scan`, `sm serve`, ...) until the operator trusts it locally with `sm plugins trust <id>` (or the per-plugin Trust control in the Settings UI), which writes a per-plugin row in the `config_plugins` (DB) trust store. This is a security boundary, and a SEPARATE axis from enable/disable (the operational toggle, which lives in the config layers): a plugin runs only when it is both enabled and trusted. Cloning a repo and scanning it must not auto-execute the repo's plugins, so the committed `settings.json` baseline cannot grant import trust, only the local DB trust store (or the local opt-in `pluginTrust.projectEnabled`) can. Authors developing a plugin trust it once locally; `--plugin-dir` is not gated. See [`architecture.md` §Locality](./architecture.md).
|
|
64
64
|
|
|
65
65
|
After every change to `plugins/`, run `sm plugins list` to see each plugin's load status. The seven statuses are documented under [Diagnostics](#diagnostics).
|
|
66
66
|
|
|
@@ -115,7 +115,7 @@ Two id shapes resolve at the toggle surface:
|
|
|
115
115
|
|
|
116
116
|
`--all` is the cascade variant: expands to every extension in every discovered plugin under the same `--yes` / TTY-confirm gate.
|
|
117
117
|
|
|
118
|
-
Resolution order per id:
|
|
118
|
+
Resolution order per id (the operational ENABLE axis): per-extension config (`plugins.<id>.extensions.<ext>.enabled`) > plugin-level config (`plugins.<id>.enabled`) > installed default, resolved through the config layers (`settings.local.json` over `settings.json`). The installed default is `true` for ordinary extensions and `false` for extensions declaring `stability: 'experimental'` or `stability: 'deprecated'` (they ship disabled until the operator opts in; see [Extension manifests](#extension-manifests)). Enable no longer reads from the DB; the `config_plugins` table now holds only the per-plugin import-trust grant (the security axis, see Import trust above). Persisted enable keys are written per qualified `<plugin>/<ext>` (the bundle macro expands at write time).
|
|
119
119
|
|
|
120
120
|
### Extractor / Analyzer / Action `precondition`, narrow the pipeline
|
|
121
121
|
|
|
@@ -214,7 +214,7 @@ The kernel knows six categories. Each has a JSON Schema under [`schemas/extensio
|
|
|
214
214
|
|
|
215
215
|
The runtime instance you `export default` includes both the manifest fields (`version`, `description`, plus kind-specific metadata) AND the runtime method. The kernel strips function-typed properties before AJV-validating the manifest, so the method lives beside metadata.
|
|
216
216
|
|
|
217
|
-
Base manifest fields shared by every kind (normative shape in [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json)): `version` (required for external plugins), `description` (required), and the optional `stability`, `order`, `annotation`, `settings`. `stability` (`'experimental' | 'beta' | 'stable' | 'deprecated'`, default `stable`) is a lifecycle label: the non-default values render as a badge next to the extension in `sm plugins list <id>` / `sm plugins show` and the Settings plugins panel. Presentation-only for `beta` and `stable`, but `experimental` and `deprecated` additionally flip the extension's installed default to DISABLED: it does not load (does not run, does not register, toggle shows off) until the operator opts in via `sm plugins enable <plugin>/<ext>`, the Settings toggle, or a `settings.json` / `
|
|
217
|
+
Base manifest fields shared by every kind (normative shape in [`schemas/extensions/base.schema.json`](./schemas/extensions/base.schema.json)): `version` (required for external plugins), `description` (required), and the optional `stability`, `order`, `annotation`, `settings`. `stability` (`'experimental' | 'beta' | 'stable' | 'deprecated'`, default `stable`) is a lifecycle label: the non-default values render as a badge next to the extension in `sm plugins list <id>` / `sm plugins show` and the Settings plugins panel. Presentation-only for `beta` and `stable`, but `experimental` and `deprecated` additionally flip the extension's installed default to DISABLED: it does not load (does not run, does not register, toggle shows off) until the operator opts in via `sm plugins enable <plugin>/<ext>`, the Settings toggle, or a `settings.json` / `settings.local.json` enable override. The opt-in wins over the installed default, so a `deprecated` extension can be kept running during a migration. A stable extension omits the field; declaring `stability: 'stable'` is valid but renders nothing.
|
|
218
218
|
|
|
219
219
|
### Extractors
|
|
220
220
|
|
|
@@ -85,11 +85,15 @@
|
|
|
85
85
|
"enabled": { "type": "boolean" },
|
|
86
86
|
"extensions": {
|
|
87
87
|
"type": "object",
|
|
88
|
-
"description": "Per-extension overrides, keyed by extension id (the leaf folder name, NOT the qualified `<plugin>/<ext>` id, the plugin is already the parent key).
|
|
88
|
+
"description": "Per-extension overrides, keyed by extension id (the leaf folder name, NOT the qualified `<plugin>/<ext>` id, the plugin is already the parent key). Carries the per-extension `enabled` toggle (operational) and the operator-supplied `settings` values.",
|
|
89
89
|
"additionalProperties": {
|
|
90
90
|
"type": "object",
|
|
91
91
|
"additionalProperties": false,
|
|
92
92
|
"properties": {
|
|
93
|
+
"enabled": {
|
|
94
|
+
"type": "boolean",
|
|
95
|
+
"description": "Per-extension operational on/off, resolved over the plugin-level `enabled` and the extension's installed default (`false` for `experimental` / `deprecated`, `true` otherwise). Shareable: lands in `settings.json` (team baseline) or `settings.local.json` (per-checkout override) via the normal config layering. This is the OPERATIONAL axis only; it does NOT grant import trust for a project-local plugin (see the top-level `pluginTrust`)."
|
|
96
|
+
},
|
|
93
97
|
"settings": {
|
|
94
98
|
"type": "object",
|
|
95
99
|
"description": "Operator-supplied values for the extension's declared settings, keyed by settingId. Values are intentionally NOT validated by this schema: the kernel's settings resolver validates each value against the per-type value schema of the input-type the manifest declares (`input-types.schema.json#/$defs/ISettingDeclaration`), since this schema cannot know which type a given settingId picked. A non-`secret` setting lands here in `settings.json` (team-shared) or `settings.local.json` (per-checkout override) via the normal config layering; `secret` settings are NOT stored here, they ride the dedicated encrypted `state_secrets` path (see `input-types.schema.json#/$defs/Setting_Secret`).",
|
|
@@ -101,6 +105,17 @@
|
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
},
|
|
108
|
+
"pluginTrust": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"description": "Local, per-machine plugin import-trust preferences. NOT part of the shareable enable/disable axis. Project-local ONLY: the config loader strips `pluginTrust` from the committed `project` layer (it is honoured only from `settings.local.json`), so a cloned repo can never auto-grant import trust to its own project-local plugins. Setting `projectEnabled: true` expands the local execution surface and is gated behind a confirm (CLI `--yes`, UI confirm dialog), like `scan.referencePaths`.",
|
|
112
|
+
"properties": {
|
|
113
|
+
"projectEnabled": {
|
|
114
|
+
"type": "boolean",
|
|
115
|
+
"description": "When true, every plugin this project ENABLES is treated as locally trusted, so its code may be imported without an explicit per-plugin trust grant in the `config_plugins` (DB) trust store. Default false. The operator's local opt-in for teams that vet plugins in code review. Project-local only (stripped from the committed `project` layer)."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
104
119
|
"jobs": {
|
|
105
120
|
"type": "object",
|
|
106
121
|
"additionalProperties": false,
|