@synoikia/create-plugin 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synoikia/create-plugin",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Create Synoikia plugin repositories and plugins, then build, check and publish them",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,17 +2,18 @@
2
2
 
3
3
  {{description}}
4
4
 
5
- The sandbox calls `{{namespace}}.{{fn}}(…)`. Every call goes through Synoikia's permission gate, approvals, redaction and audit log; this plugin only describes {{name}}'s API.
5
+ The sandbox calls `{{namespace}}.{{fn}}(…)`. Every call goes through Synoikia's permission gate, approvals, redaction and audit log. This plugin only describes {{name}}'s API.
6
6
 
7
7
  ## Files
8
8
 
9
- | File | What |
10
- | --------------- | ---------------------------------------------------------------------------------------- |
11
- | `manifest.json` | Binding, connection form, `sensitiveKeys` and network hosts |
12
- | `plugin.yaml` | Locked operations, split twins, sensitive params and results, confirmation literals |
13
- | `src/plugin.ts` | Discovery and anything that needs logic |
14
- | `src/auth.ts` | How requests authenticate |
15
- | `test/` | A fake {{name}}, unit tests, and e2e tests on Synoikia's harness |
9
+ | File | What |
10
+ | --------------- | ------------------------------------------------------------------------------------------------------- |
11
+ | `manifest.json` | Binding, connection form, `sensitiveKeys` and network hosts |
12
+ | `plugin.yaml` | Locked operations, split twins, sensitive params and results, confirmation literals |
13
+ | `src/plugin.ts` | Discovery and anything that needs logic |
14
+ | `src/auth.ts` | How requests authenticate |
15
+ | `test/` | `compliance.yaml`, the profile that core's compliance suite certifies, a fake {{name}}, and `unit/` tests |
16
+ | `features/` | `{{id}}.feature`: plugin-specific behavior, written with the compliance suite's steps |
16
17
 
17
18
  ## Connection
18
19
 
@@ -2,16 +2,25 @@
2
2
 
3
3
  Plugins for [Synoikia](https://github.com/via-justa/synoikia-core), the self-hosted MCP control plane.
4
4
 
5
- A plugin only describes its upstream API: the operations it has, which ones read and which ones write, which are destructive, and which fields hold secrets. Synoikia's core decides what is allowed and runs every call through its sandbox, permission gate, approvals, redaction and audit log. See the [plugin authoring guide](https://github.com/via-justa/synoikia-core/blob/main/docs/plugin-authoring.md).
5
+ A plugin only describes its upstream API: its operations, which ones read or write, which are destructive, and which fields hold secrets. Synoikia's core decides what is allowed and runs every call through its sandbox, permission gate, approvals, redaction and audit log. See the [plugin authoring guide](https://github.com/via-justa/synoikia-core/blob/main/docs/plugin-authoring.md).
6
6
 
7
7
  ## Layout
8
8
 
9
- | Path | What |
10
- | ---------------------------- | ---------------------------------------------------------------------------------------- |
11
- | `plugins/<id>/manifest.json` | Binding, connection form, `sensitiveKeys`, network hosts |
9
+ | Path | What |
10
+ | ---------------------------- | ----------------------------------------------------------------------------------------- |
11
+ | `plugins/<id>/manifest.json` | Binding, connection form, `sensitiveKeys`, network hosts |
12
12
  | `plugins/<id>/plugin.yaml` | What the plugin tells core about its operations: locks, splits, secrets, confirm literals |
13
- | `plugins/<id>/src/` | Discovery, auth and anything that needs logic |
14
- | `plugins/<id>/test/` | A fake upstream, unit tests and e2e tests on Synoikia's harness |
13
+ | `plugins/<id>/src/` | Discovery, auth and anything that needs logic |
14
+ | `plugins/<id>/test/` | `compliance.yaml` (the compliance profile), a fake upstream, and unit tests in `unit/` |
15
+ | `plugins/<id>/features/` | `<id>.feature`: plugin-specific behavior, written with the compliance suite's steps |
16
+ | `tsconfig.base.json` | Shared compiler options. Each plugin's `tsconfig.json` extends it |
17
+ | `vitest.shared.ts` | Shared Vitest config. Each plugin's `vitest.config.ts` builds on it |
18
+
19
+ Plugins build against Synoikia's published npm packages:
20
+
21
+ - `@synoikia/plugin-sdk`: the manifest schema, RPC contract and `runPlugin()` runtime. The bundle includes it.
22
+ - `@synoikia/create-plugin` (dev only): `synoikia-plugin new`, `build`, `check` and the `repo` release steps.
23
+ - `@synoikia/core` (dev only): `@synoikia/core/testing/compliance` is core's compliance suite. It boots the real core on the built bundle as a permission-confined child, as in production. Then it drives the plugin against its own fake upstream.
15
24
 
16
25
  ## Development
17
26
 
@@ -20,8 +29,25 @@ Requires Node ≥ 22.12 and pnpm 10 (`corepack enable`).
20
29
  ```sh
21
30
  pnpm install
22
31
  pnpm new # add a plugin
23
- pnpm test # check, build and test every plugin
32
+ pnpm test # each plugin: check, build, unit tests, compliance suite and its features
24
33
  pnpm typecheck && pnpm lint && pnpm format:check
25
34
  ```
26
35
 
27
- Work on one plugin with `pnpm --filter ./plugins/<id> test`.
36
+ Work on one plugin with `pnpm --filter ./plugins/<id> test`. The run must end with "<Name> <version> is fit for production." and no failed test.
37
+
38
+ Each plugin's `test` script runs `synoikia-plugin check` (manifest, `plugin.yaml`, versions) and `synoikia-plugin build` (the self-contained bundle, `plugin.yaml` validated and inlined). Then it runs the unit tests, core's compliance suite and the plugin's features.
39
+
40
+ `test/compliance.yaml` is the profile that core's compliance suite certifies the plugin against. It names the reads, writes, rejects, a lock case for each locked operation, the secrets and the connection test. A change there is a security change. Review its diff like a `plugin.yaml` diff. The format is in the [plugin authoring guide](https://github.com/via-justa/synoikia-core/blob/main/docs/plugin-authoring.md) › The compliance suite.
41
+
42
+ ## Adding a plugin
43
+
44
+ `pnpm new` asks for an id, a name, an archetype and the auth. It writes `plugins/<id>/` with a manifest, a `plugin.yaml`, `src/`, `test/compliance.yaml` and a fake upstream. Then:
45
+
46
+ - Describe the upstream's operations in `plugin.yaml`: locks, split twins, classification overrides, `sensitiveParams`, `sensitiveResult`, confirmation literals.
47
+ - Keep code in `src/` for what needs logic: discovery, a split predicate, a custom confirmation source.
48
+ - Extend the fake upstream and `test/compliance.yaml`. Add a lock case for each locked operation.
49
+ - Write plugin-specific behavior in `features/<id>.feature`, and pure logic tests in `test/unit/`.
50
+
51
+ ## Updating Synoikia
52
+
53
+ Each plugin's `package.json` ranges and the lockfile pin the SDK and core versions. To take a newer release, bump the ranges and run `pnpm install`.
@@ -22,6 +22,5 @@ jobs:
22
22
  - run: pnpm lint
23
23
  - run: pnpm format:check
24
24
  - run: pnpm typecheck
25
- # Each plugin's test checks its manifest and plugin.yaml, builds its bundle, then runs its unit tests and
26
- # its e2e tests on core's harness under the permission model.
25
+ # Each plugin's test: check, build, unit tests, then core's compliance suite and its features on the bundle.
27
26
  - run: pnpm test
@@ -0,0 +1,3 @@
1
+ # The default rules. Allowlist a fake credential of a test by its exact value only.
2
+ [extend]
3
+ useDefault = true
@@ -1,3 +1,5 @@
1
1
  dist
2
2
  coverage
3
3
  pnpm-lock.yaml
4
+ # Guide text is hashed into attestation keys; keep it byte-for-byte.
5
+ plugins/*/guides
@@ -26,8 +26,23 @@
26
26
  "plugins/*/manifest.json",
27
27
  "plugins/*/plugin.yaml",
28
28
  "plugins/*/src/**",
29
+ "plugins/*/test/compliance.yaml",
30
+ "plugins/*/test/fake*.ts",
31
+ "plugins/*/test/fixtures/**",
32
+ "plugins/*/features/**",
33
+ "plugins/*/vitest.config.ts",
34
+ "plugins/*/instructions.md",
35
+ "plugins/*/guides/**",
36
+ "vitest.shared.ts",
29
37
  "**/package.json",
30
- ".github/workflows/**"
38
+ ".github/workflows/**",
39
+ ".gitleaks.toml",
40
+ ".claude/agents/**",
41
+ ".claude/hooks/**",
42
+ ".claude/settings.json",
43
+ "minisign.pub",
44
+ "pnpm-workspace.yaml",
45
+ "pnpm-lock.yaml"
31
46
  ]
32
47
  }
33
48
  },
@@ -2,43 +2,66 @@
2
2
 
3
3
  Plugins for [Synoikia](https://github.com/via-justa/synoikia-core). Authoring guide: `https://github.com/via-justa/synoikia-core/blob/main/docs/plugin-authoring.md`.
4
4
 
5
- **A plugin only describes its upstream API; core decides what is allowed.** Core's sandbox, permission gate, approvals, redaction and audit apply to every plugin. A plugin supplies the catalog, the classification (read, write, locked), targets, summaries, confirmation literals and `sensitiveKeys`. It never adds its own allow/deny switch, approval prompt or secret handling.
5
+ **A plugin only describes its upstream API. Core decides what is allowed.** Core's sandbox, permission gate, approvals, redaction and audit apply to every plugin. A plugin supplies the catalog, the classification (read, write, locked), targets, summaries, confirmation literals and `sensitiveKeys`. It never adds its own allow/deny switch, approval prompt or secret handling. If a plugin needs something core doesn't offer, that is a change in Synoikia core, released first.
6
6
 
7
- ## Layout
8
-
9
- | Path | What |
10
- | ---------------------------- | ------------------------------------------------------------------------------------------ |
11
- | `plugins/<id>/manifest.json` | Validated by the SDK schema: binding, connection form, `sensitiveKeys`, network hosts |
12
- | `plugins/<id>/plugin.yaml` | Policy as data: locked operations, split twins, sensitive params/results, confirm literals |
13
- | `plugins/<id>/src/` | `index.ts` calls `runPlugin()`; `plugin.ts` holds discovery and logic; `auth.ts` the auth |
14
- | `plugins/<id>/test/` | A fake upstream, unit tests, and `e2e.test.ts` on core's harness |
7
+ Every task that changes the repo starts with the `change-workflow` skill (`.claude/skills/change-workflow/`). Changes are built spec first and test first, one slice for each pull request. The `requirements-engineer`, `architect` and `developer` skills run the steps, and prose follows the `technical-writing` skill. The rules for commits, branches, feature files, tests and prose are in `docs/workflow.md`. The git hooks (`pnpm hooks:install`, once per clone) and the `Workflow` CI job check them.
15
8
 
16
- Policy data (locks, splits, sensitive params and results, confirm literals, guidance) goes in `plugin.yaml`; only decisions that need logic go in `src/`. `plugin.yaml` is validated and inlined at build time, so rebuild before e2e tests.
9
+ Plugin code, `plugin.yaml`, manifest and test work follows the `plugin-development` skill (`.claude/skills/plugin-development/`). Its rules: policy in YAML, SDK helpers before new code, fail closed, and `test/compliance.yaml` diffs reviewed as security changes. Load it before writing or reviewing a plugin change.
17
10
 
18
- ## Workflow
11
+ ## Layout
19
12
 
20
- Every change is built spec first and test first, one slice at a time. The rules are in `docs/workflow.md`. Start every task that changes the repo with the `change-workflow` skill. Write prose by the `technical-writing` skill.
13
+ | Path | What |
14
+ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `plugins/<id>/manifest.json` | Validated by the SDK schema: binding, connection form, `sensitiveKeys`, network hosts |
16
+ | `plugins/<id>/plugin.yaml` | Policy as data: locks, split twins, classification overrides, `sensitiveParams`/`sensitiveResult`, confirm literals, plugin-specific settings |
17
+ | `plugins/<id>/src/` | `index.ts` calls `runPlugin()`. `plugin.ts` uses `definePlugin` for discovery and the decisions that need logic. `auth.ts` signs requests |
18
+ | `plugins/<id>/guides/` | Markdown guides bundled at build time. Their hash versions attestation keys, so Prettier skips them |
19
+ | `plugins/<id>/test/` | `compliance.yaml` (the compliance profile), `fake.ts` that starts the fake upstream `fake-upstream.ts`, and unit tests in `unit/` |
20
+ | `plugins/<id>/features/` | `<id>.feature`: plugin-specific behavior, written with the compliance suite's steps on the profile's named calls |
21
+ | `plugins/<id>/RUNBOOK.md` | Manual checklist against a real instance |
22
+ | `.github/workflows/release.yml` | Packs, signs, indexes, verifies and publishes releases with `synoikia-plugin repo` (CI only), when the repository has it |
21
23
 
22
24
  ## Commands
23
25
 
24
- Node 22.12+, pnpm 10.
26
+ Node 22.12+ (`.nvmrc` pins 22), pnpm 10.
25
27
 
26
28
  ```sh
27
- pnpm new # add a plugin (see the add-plugin skill)
29
+ pnpm new # add a plugin (see the add-plugin skill)
28
30
  pnpm lint && pnpm format:check && pnpm typecheck && pnpm test # what CI runs
29
- pnpm --filter ./plugins/<id> test # one plugin: check, build, unit + e2e
30
- pnpm check # manifests, plugin.yaml and versions
31
- pnpm hooks:install # once per clone: the workflow's git hooks
32
- pnpm workflow pre-push # the workflow checks CI runs
31
+ pnpm --filter ./plugins/<id> test # one plugin: check, build, unit tests, compliance suite, features
32
+ pnpm check # manifests, plugin.yaml and versions
33
+ pnpm --filter ./plugins/<id> exec vitest run test/unit/<file>.test.ts # one unit test file
34
+ pnpm format # fix formatting
35
+ pnpm hooks:install # once per clone: the workflow git hooks
36
+ pnpm workflow pre-push # the workflow checks (branch, phases, features, prose)
33
37
  ```
34
38
 
39
+ Each plugin's `vitest.config.ts` uses `complianceConfig` from `@synoikia/core/testing/compliance`. Core's compliance suite and the plugin's features run the **built** `dist/index.js` on the real core. The bundle runs as a permission-confined child against the fake upstream. A stale bundle gives stale results, so rebuild after you change `src/` or `plugin.yaml`. The `test` script does this (`synoikia-plugin build` validates and inlines `plugin.yaml`).
40
+
41
+ A plugin is ready when its `test` run ends with "<Name> <version> is fit for production." and no test fails. A failing own feature does not change that verdict line, but it fails the run. The suite and its steps are in core's `docs/plugin-authoring.md` › The compliance suite.
42
+
43
+ **Policy data goes in `plugin.yaml`, logic in `src/`.** Locks, splits, sensitive params and results, confirmation literals, guidance and upstream-specific lists are YAML that the SDK's `compileRules` applies. Only decisions that need code stay in TypeScript.
44
+
45
+ **`test/compliance.yaml` is policy too.** It gives the reads, writes, rejects, a lock case for each locked operation, the secrets and the connection test. A change to it is a security change, so review its diff like a `plugin.yaml` diff.
46
+
35
47
  ## Rules that break releases or installs
36
48
 
37
- - **Self-contained bundle.** The plugin reads nothing outside its own package directory at runtime; `synoikia-plugin build` bundles every dependency and data file.
38
- - **Network hosts.** `manifest.json` `network.hosts` lists exactly the hosts the plugin connects to. Admins review it before enabling the plugin, and an update that changes it installs disabled. Core doesn't enforce it (the plugin process can reach any host), so it must be accurate.
39
- - **Secrets.** Every credential field is `writeOnly: true`, uses the `secret` widget and is in `sensitiveKeys`, along with every secret field the upstream returns. `synoikia-plugin check` enforces the first part; an e2e `secrets` contract check covers the second.
40
- - **Versions.** `version` in `manifest.json` and `package.json` must match, and a released version never changes. Bump only when asked; the `release-plugin` skill covers it.
49
+ - **Self-contained bundle.** Under the Node permission model the plugin can read nothing outside its own package directory. So every runtime dependency and data file must be bundled into `dist/index.js`. `synoikia-plugin build` does it.
50
+ - **Network hosts.** `manifest.json` `network.hosts` lists every host the plugin connects to. A new upstream URL, such as a spec mirror, goes there too. Admins review it before enabling the plugin, and an update that changes it installs disabled. Core doesn't enforce it (the plugin process can reach any host), so it must be accurate.
51
+ - **Secrets.** Every credential field is `writeOnly: true` in the connection schema, uses the `secret` widget, and is listed in `sensitiveKeys`. So is any secret field the upstream returns. `synoikia-plugin check`, run by each plugin's `test` script, checks the connection schema. A `secrets` case in `test/compliance.yaml` checks each secret the upstream returns.
52
+ - **Versions.** `version` in `manifest.json` and `package.json` must match. A version that is released can never change, so any change to a released plugin needs a new version. Bump only when asked. The `release-plugin` skill covers it.
53
+ - **SDK and core ranges.** Plugins depend on published `@synoikia/plugin-sdk`, `@synoikia/core` and `@synoikia/create-plugin` from npm. To update, bump the ranges and run `pnpm install`.
54
+
55
+ ## Conventions
56
+
57
+ Strict TypeScript with `noUncheckedIndexedAccess` and `verbatimModuleSyntax`: use `import type`, and `.js` extensions on relative imports. Unused variables are allowed only with a `_` prefix. Keep comments minimal: one or two lines, only for what the code can't say (a reason, a constraint, a design § reference). Do not write multi-line comment blocks in code, YAML or workflows. Prettier formats everything, and a hook runs it after each edit. Behavior that crosses the plugin boundary goes in `features/<id>.feature` or `test/compliance.yaml`, with the fake upstream extended to match. Pure logic gets a unit test in `test/unit/`.
41
58
 
42
59
  ## Security review
43
60
 
44
- Run the `security-reviewer` subagent on the diff before a PR that touches these paths: a plugin's `manifest.json`, `plugin.yaml`, `src/` or dependencies, and `.github/workflows/`. Address its findings. Put its `Security audit:` line in the PR description.
61
+ Before you open a PR that touches a security path, run the `security-reviewer` subagent (`.claude/agents/security-reviewer.md`) on the diff. Address its findings. The security paths include a plugin's `manifest.json`, `plugin.yaml`, `src/`, dependencies and `test/compliance.yaml`, and `.github/workflows/`. The full list is in `synoikia.workflow.securityPaths` in the root `package.json`.
62
+
63
+ The reviewer checks classification, request building, approval summaries, secrets, network hosts and the release flow. It also runs `pnpm audit` and triages known CVEs by whether the dependency is bundled into a plugin. Put its `Security audit:` line in the PR description. The `Workflow` CI job checks for it.
64
+
65
+ ## Protected files
66
+
67
+ A PreToolUse hook (`.claude/hooks/guard-paths.mjs`) denies edits to `pnpm-lock.yaml` and `plugins/*/dist/`, and asks first for `minisign.pub`. Never touch the `MINISIGN_SECRET_KEY` flow in `.github/workflows/release.yml` unless asked.
@@ -1,33 +1,70 @@
1
1
  ---
2
2
  name: security-reviewer
3
- description: Reviews a change to a Synoikia plugin, or to this repository's release workflow, for security problems and known CVEs in its dependencies. Use proactively before opening or merging a PR that touches plugins/*/manifest.json, plugins/*/plugin.yaml, plugins/*/src, a plugin's dependencies or .github/workflows; or when asked for a security review or dependency audit.
3
+ description: Reviews a change to a Synoikia plugin, or to this repository's release tooling, for security problems and known CVEs in its dependencies. Use proactively before opening or merging a PR that touches a path in synoikia.workflow.securityPaths of the root package.json, such as plugins/*/manifest.json, plugins/*/plugin.yaml, plugins/*/src, plugins/*/test/compliance.yaml, a plugin's dependencies or .github/workflows; or when asked for a security review or dependency audit.
4
4
  tools: Read, Grep, Glob, Bash
5
5
  ---
6
6
 
7
- You review changes to Synoikia plugins for security problems. You are read-only: never edit files, commit, push or change the environment. Use Bash only for read-only commands (`git diff`, `git log`, `pnpm audit`, `pnpm why`, `pnpm ls`) and for running existing tests.
7
+ You review changes to Synoikia plugins for security problems. You are read-only: never edit files, commit, push or change the environment. Use Bash only for read-only commands such as `git diff`, `git log`, `git show`, `pnpm audit`, `pnpm why`, `pnpm ls`, and for running existing tests (`pnpm --filter ./plugins/<id> test`).
8
8
 
9
9
  ## The trust model
10
10
 
11
- Synoikia core runs every plugin as a permission-confined child and puts every call through its own sandbox, permission gate, human approvals, redaction and audit log. Core enforces policy based on what the plugin tells it. A plugin that labels a destructive call `read`, leaves a secret out of `sensitiveKeys`, or writes a misleading approval summary weakens every install, and core can't detect it. Most findings are cases where the plugin gave core wrong information.
11
+ Synoikia core runs every plugin instance as a child process under the Node permission model. It puts every call through its own sandbox, permission gate, human approvals, redaction and audit log. Core enforces policy, but it enforces it based on what the plugin tells it. A plugin that labels a destructive call `read`, leaves a secret out of `sensitiveKeys`, or writes a misleading approval summary weakens every install. Core can't detect it. Most findings in this repository are cases where the plugin gave core wrong information. Background: `CLAUDE.md`, `README.md`, and Synoikia's design §3–§5 and §12 (`https://github.com/via-justa/synoikia-core/blob/main/docs/design/`).
12
12
 
13
13
  ## Scope
14
14
 
15
- Start from the diff you were given, otherwise `git diff origin/main...HEAD` plus uncommitted changes. Read the plugin's `manifest.json` and `plugin.yaml` in full whenever its `src/` changes.
15
+ Start from the diff you were given. Otherwise use `git diff origin/main...HEAD` plus uncommitted changes. Follow each changed function to its callers and callees. Read the plugin's `manifest.json` and `plugin.yaml` in full whenever its `src/` changes.
16
16
 
17
17
  ## What to check
18
18
 
19
- - **Classification** (`plugin.yaml` rules, discovery in `src/`): no operation with side effects is labeled read, including GETs that act. Destructive or hard-to-undo operations are `locked`. Unknown operations fail closed. A `classification: read` override needs a reason; removing a `locked`, `sensitiveParams` or `sensitiveResult` rule, or widening `include` past an `exclude`, is a finding unless the PR justifies it. `splitWhen` predicates must fail closed (unknown means the locked twin).
20
- - **Request building**: model-supplied arguments cannot change the host, scheme or port, path parameters are encoded, model arguments cannot set auth headers, and the operation key core gated is the one invoked.
21
- - **Summaries and confirm literals**: the approval shows exactly what will be affected; a literal names the thing at risk, and crafted params can't make it misleading. Summaries never contain secrets.
22
- - **Secrets**: every credential field is `writeOnly`, uses the `secret` widget and is in `sensitiveKeys`; every secret field the upstream returns is in `sensitiveKeys` or a `sensitiveResult` rule, with an e2e check. Credentials never appear in errors, logs or catalog text, and are never sent to any host but the upstream.
23
- - **Network**: `network.hosts` is minimal; TLS verification is on by default.
24
- - **Staying inside core's model**: no plugin-side allow/deny lists, approval prompts or caching that serves data after core denied a call; no `eval`, `new Function` or dynamic imports; nothing loaded from disk at runtime (`plugin.yaml` is inlined at build time).
25
- - **Untrusted upstream data**: specs, JSON and frames are untrusted; look for prototype pollution, unbounded sizes and YAML beyond plain data.
19
+ - **`plugin.yaml` rules** (applied by the SDK's `compileRules`): precedence fails closed. The order is excluded, then locked or split twin, then a rule's `classification`, then the plugin's heuristic, else write. These are findings unless the PR justifies them:
20
+ - removing or narrowing a `locked`, `split`, `sensitiveParams` or `sensitiveResult` rule
21
+ - adding a `classification: read` override
22
+ - widening `include` past an `exclude`
26
23
 
27
- ## Known vulnerabilities
24
+ A `splitWhen` predicate must fail closed (unknown means the locked twin). `plugin.yaml` is parsed and inlined at build time only. Nothing reads files at runtime.
25
+ - **The compliance profile** (`test/compliance.yaml`): it is what core certifies the plugin against, so review its diff like a `plugin.yaml` diff. A removed or weakened `reads`, `writes`, `rejects`, `locks`, `secrets` or `connectionTest` case is a finding unless the PR justifies it. A read that moves to `writes` or out of the profile needs a reason. Each locked operation and split twin has a `locks` case with its literal. The fake upstream (`test/fake*.ts`) must answer as the real upstream does, so a fake that hides a secret or an error weakens the certification.
26
+ - **Classification** (discovery in `src/`): no operation with side effects is labeled read. That includes GETs with side effects, and RPC methods whose names sound like reads. Destructive or hard-to-undo operations are `locked` with `typedConfirmation`, for example delete, wipe, restart, unlock, regenerate a key, or change auth or users. Operations the plugin doesn't know yet fail closed: write or locked, never read. Values taken from the upstream's own spec or introspection are untrusted and must not be able to downgrade a classification.
27
+ - **Request building** (`src/`, `resolveOperation`/`invoke`): model-supplied arguments cannot change the host, scheme or port. Path parameters are encoded (`encodeURIComponent`), so `../`, `?`, `#` and `%2F` can't change which operation runs. Model arguments cannot set auth headers or impersonation headers. The operation key that core gated is the one that is invoked: no aliasing, no smuggling a second call through params.
28
+ - **Targets and summaries** (`resolveTargets`, `summarize`, `prepareWrite`): the approval page shows exactly what will be affected. A summary or target that understates the effect is an approval-integrity finding. So is one that the model can make misleading through crafted params (display names, markdown, control characters). Summaries never contain secrets. Core passes them redacted params, so the plugin must not add secrets back from its own state.
29
+ - **Secrets** (`manifest.json`, `src/`): every credential field is `writeOnly`, uses the `secret` widget and is in `sensitiveKeys`. Every secret field the upstream can **return** (API keys in settings, webhook URLs, tokens, passwords, push keys) is in `sensitiveKeys`. A `secrets` case in `test/compliance.yaml` checks that it is redacted, and the fake returns it. Credentials are never in error messages, logs, thrown `PluginError` text or catalog descriptions. They are never sent to any host except the upstream: for example, the auth header must not go to a spec mirror.
30
+ - **Network** (`manifest.json` `network.hosts`): as small as possible, with templated hosts only from connection fields. No wildcards or extra third-party hosts without a stated reason. TLS verification is on by default (`verifyTls !== false`), and turning it off is an explicit connection setting.
31
+ - **Staying inside core's model**: no plugin-side allow/deny lists, approval prompts, retries of denied calls, or caching that serves data after core denied a call. No `eval`, `new Function`, dynamic `import()` of computed paths, or filesystem access outside the plugin's own package directory. Everything is bundled and nothing is loaded at runtime.
32
+ - **Parsing untrusted upstream data**: YAML, JSON and WebSocket frames from the upstream are untrusted. Look for prototype pollution through merges, unbounded sizes and recursion, and YAML features beyond plain data.
33
+ - **Release tooling** (`.github/workflows/`, and the `synoikia-plugin repo` steps of the pinned `@synoikia/create-plugin` version):
34
+ - `MINISIGN_SECRET_KEY` is exposed only to the Sign step, never echoed, and written with `umask 077`.
35
+ - Packing is deterministic. Publishing happens only after the signature is verified against `minisign.pub` and the install check passes.
36
+ - Released versions are never overwritten. The index download fails on errors other than "release not found", and `repo publish` refuses an existing tag whose tarball differs.
37
+ - Workflow permissions are the minimum needed. A new third-party action is pinned by commit SHA.
38
+ - **Tests**: new secret fields, locked operations and classification rules come with a test that fails if they regress. That is a profile case, a feature scenario or a unit test.
28
39
 
29
- Run `pnpm audit --json` and `pnpm audit --prod`. For each advisory, `pnpm why <package>`: a plugin's `dependencies` are bundled and ship to every install, so judge reachability; `devDependencies` run only in CI. Give the advisory ID, affected and patched ranges, the dependency path and the smallest fix. If the audit can't reach the registry, say so.
40
+ ## Known vulnerabilities (CVEs)
41
+
42
+ Always run this section, even when the diff touches no dependencies, because new advisories appear against unchanged code.
43
+
44
+ 1. Run `pnpm audit --json` from the repository root, and `pnpm audit --prod` to separate shipped dependencies from dev-only ones.
45
+ 2. For each advisory, use `pnpm why <package>` to find which plugin pulls it in, and whether it is a `dependency` or a `devDependency` of that plugin:
46
+ - A plugin's `dependencies` are bundled into `dist/index.js` and **ship to every install**. Judge whether the vulnerable code is reachable from that plugin. Find what input reaches it, and whether the model, the upstream or the admin supplies it.
47
+ - `devDependencies` (`@synoikia/core`, esbuild, vitest) don't ship, but they run in CI, including the Release workflow that handles the signing key. Report them as lower severity unless the advisory is about code execution at install or build time.
48
+ 3. Also check new or bumped dependencies in the diff:
49
+ - whether the version is current and has advisories (the audit covers this once it is in the lockfile)
50
+ - whether it has install scripts (they need an `onlyBuiltDependencies` entry in `pnpm-workspace.yaml`)
51
+ - whether the package is a typosquat of a well-known name
52
+ 4. For each CVE finding, give the advisory ID (GHSA/CVE), the affected and patched version ranges, the dependency path, and the smallest fix. That is a direct bump, or a `pnpm.overrides` entry when only a transitive dependency is vulnerable.
53
+
54
+ If `pnpm audit` can't reach the registry, say so. Don't report the dependency surface as clean.
30
55
 
31
56
  ## Reporting
32
57
 
33
- Report only issues tied to concrete code or a concrete advisory: severity (critical, high, medium, low), location, what breaks, a scenario, and the smallest fix with the test that would catch a regression. If there are none, say so and list what you checked.
58
+ Report only issues you can tie to concrete code or a concrete advisory. For each finding give:
59
+
60
+ - **Severity**:
61
+ - critical: a write or secret leak core can't stop.
62
+ - high: realistic misuse, or a reachable CVE in shipped code.
63
+ - medium: defense in depth lost, or a dev-only CVE that affects CI.
64
+ - low: hardening.
65
+ - **Location**: `path:line`, or the dependency path for a CVE.
66
+ - **What breaks**: which promise to core or to the admin no longer holds.
67
+ - **Scenario**: who does what, with which input, and what they gain.
68
+ - **Fix**: the smallest change, plus the test that would catch a regression.
69
+
70
+ Rank findings by severity. If there are none, say so plainly, and list what you checked and the `pnpm audit` result. Say what you could not verify rather than guessing.
@@ -5,10 +5,12 @@ description: Scaffold a new Synoikia plugin under plugins/<id> with `pnpm new`,
5
5
 
6
6
  # Add a plugin
7
7
 
8
+ Run the change as the `change-workflow` skill and `docs/workflow.md` say. Write the code as the `plugin-development` skill says. Background: Synoikia's plugin authoring guide (`https://github.com/via-justa/synoikia-core/blob/main/docs/plugin-authoring.md`).
9
+
8
10
  ## 1. Settle the basics
9
11
 
10
- - **`<id>`**: lowercase, the directory name and manifest `id`; the sandbox namespace defaults to it in camelCase.
11
- - **Archetype**: `openapi-rest` (the upstream serves an OpenAPI spec), `static-rest` (a REST API without one; operations are declared in `plugin.yaml`), `websocket-rpc` (JSON-RPC over WebSocket), or `blank`.
12
+ - **`<id>`**: lowercase, the directory name and manifest `id`. The sandbox namespace defaults to it in camelCase.
13
+ - **Archetype**: `openapi-rest` (the upstream serves an OpenAPI spec), `static-rest` (a REST API without one, with operations declared in `plugin.yaml`), `websocket-rpc` (JSON-RPC over WebSocket), or `blank`.
12
14
  - **Auth**: `bearer`, `api-key` (with its header), `basic` or `none`.
13
15
  - **Destructive operations**: which ones are `locked` (always a human who types a confirmation literal), and what that literal is.
14
16
  - **Secrets**: which connection fields are credentials, and which fields the upstream returns hold secrets.
@@ -19,21 +21,26 @@ description: Scaffold a new Synoikia plugin under plugins/<id> with `pnpm new`,
19
21
  pnpm new --id <id> --name "<Name>" --archetype <archetype> --auth <auth> --yes
20
22
  ```
21
23
 
22
- This writes `plugins/<id>/` (manifest, `plugin.yaml`, `src/`, a fake upstream, unit and e2e tests) and runs `pnpm install`. The generated plugin already builds and passes its tests.
24
+ This writes `plugins/<id>/` and runs `pnpm install`. The plugin holds `manifest.json`, `plugin.yaml`, `src/`, `test/compliance.yaml`, a fake upstream, a README and a RUNBOOK. For the `openapi-rest`, `static-rest` and `websocket-rpc` archetypes, the plugin passes the compliance suite as generated.
23
25
 
24
26
  ## 3. Describe the upstream
25
27
 
26
- - `manifest.json`: connection fields and help, `sensitiveKeys` (add every secret field the upstream returns), `network.hosts`.
27
- - `plugin.yaml`: `rules` for locks, splits, classification overrides, `sensitiveParams`, `sensitiveResult`, `confirm` literals and summary notes; `exclude` for plumbing the model has no business calling. A rule's `classification` can never unlock a locked operation, and anything unclassified is a write.
28
- - `src/plugin.ts`: only what needs logic, such as a `splitWhen` predicate or a custom confirm source.
28
+ - `manifest.json`: connection fields and help, `sensitiveKeys` (add every secret field the upstream returns), `network.hosts` (every host the plugin calls).
29
+ - `plugin.yaml`: `rules` for locks, splits, classification overrides, `sensitiveParams`, `sensitiveResult`, `confirm` literals, summary notes and guidance. Use `exclude` for plumbing the model has no business calling, and `operations` for declared operations. A rule's `classification` never unlocks, and anything unclassified is a write.
30
+ - `src/plugin.ts`: only what needs logic, such as a `splitWhen` predicate, a custom confirm source, or discovery.
29
31
 
30
32
  ## 4. Tests
31
33
 
32
- Extend `test/fake-upstream.ts` to answer what the plugin calls, including responses with secrets. Fill in `test/compliance.yaml`: a case for each locked operation, and each secret the fake returns. Core's compliance suite must print that the plugin is fit for production. Specify plugin-specific behavior in `features/*.feature` with the suite's steps (`docs/plugin-authoring.md`).
34
+ - Extend the fake upstream (`test/fake-upstream.ts`, on `startFakeHttp` from `@synoikia/core/testing` for HTTP) to answer what the plugin calls. Include the responses that hold secrets.
35
+ - Fill in `test/compliance.yaml`: reads, writes, rejects, the secrets, and a `locks` case for each locked operation and split twin.
36
+ - Write plugin-specific behavior in `features/<id>.feature` with the suite's steps, on calls named in `calls`.
37
+ - Put tests of pure logic in `test/unit/`.
33
38
 
34
39
  ```sh
35
40
  pnpm --filter ./plugins/<id> test
36
41
  pnpm typecheck && pnpm lint && pnpm format:check
37
42
  ```
38
43
 
39
- Then run the `security-reviewer` subagent on the diff.
44
+ The plugin run must end with "<Name> 0.1.0 is fit for production." and no failed test.
45
+
46
+ Leave the version at `0.1.0`. Run the `security-reviewer` subagent on the diff before you open the PR.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: plugin-development
3
+ description: >-
4
+ How to write and review plugin code in this Synoikia plugin repository: plugins/<id>/src, plugin.yaml,
5
+ manifest.json and the plugin's tests. Use it for any task that changes or reviews a plugin, even when the
6
+ request names only an operation, an upstream endpoint, a lock, a secret or a bug and never says
7
+ "TypeScript" or "plugin.yaml". Examples: add or lock an operation; split an operation into twins; mark a
8
+ param or result field sensitive; change a summary or confirmation literal; fix a client, a transport or
9
+ discovery; add a connection field; extend a fake upstream; write or fix unit tests, the compliance profile
10
+ or a plugin feature; review a plugin diff. Not for scaffolding a new plugin (add-plugin, then this skill for
11
+ the code) or version bumps (release-plugin).
12
+ ---
13
+
14
+ # Plugin development
15
+
16
+ A plugin is a Node 22 ESM process that describes one upstream API to core: its catalog, classification, targets, summaries, confirmation literals and sensitive fields. Core does the rest. New code must look like the code around it. Make the smallest change that does the job, with one shared way to do each thing. Add no dependency for what a few lines or the SDK already do.
17
+
18
+ `CLAUDE.md` holds the repo rules (bundle, network hosts, secrets, versions, security review). This skill is how to write the change. Load a reference only when the task touches its area:
19
+
20
+ | Task touches | Read |
21
+ | --------------------------------------------------------------------------- | -------------------------- |
22
+ | Types, narrowing upstream data, errors, imports, module layout | `references/typescript.md` |
23
+ | Tests, the fake upstream, `test/compliance.yaml`, the plugin's feature file | `references/testing.md` |
24
+
25
+ ## Rules for every change
26
+
27
+ 1. **Reuse first.** Before you write a helper, look in the plugin's own `src/` and in `@synoikia/plugin-sdk`. The SDK has `definePlugin`, `compileRules`, `parsePluginSettings`, `requireString`, `requireField`, `isPlainObject`, `truncate` and `clip`. It also has `PluginError`/`ErrorCodes`, `upstreamError`, `HttpJsonClient`, `singleFlight`, `PendingRequests` and the OpenAPI and static catalog adapters. Use it, or extend it. If the SDK lacks it and another plugin would need it too, it belongs in core's SDK, released first.
28
+ 2. **Policy in `plugin.yaml`, logic in `src/`.** Locks, splits, classification overrides, `sensitiveParams`, `sensitiveResult`, confirm literals, guidance, `exclude`/`include` and upstream lists are YAML. Code is only for a decision YAML can't express (a path check, a target lookup, a `splitWhen` predicate).
29
+ 3. **Describe, don't decide.** No plugin-side allow/deny switch, approval prompt, rate limit or secret handling: core's gate, approvals and redaction own them.
30
+ 4. **Fail closed.** Anything unclassified is a write. A rule's `classification` never unlocks. An upstream shape the code doesn't know is redacted or refused, not passed through.
31
+ 5. **Narrow upstream data at the edge.** Responses, events and specs are `unknown` until checked with `isPlainObject`, `requireString` and friends. Past that point, trust the narrowed type.
32
+ 6. **Secrets never leave masked.** Credential fields are `writeOnly`, use the `secret` widget and are in `sensitiveKeys`. Secret fields the upstream returns go in `sensitiveKeys` and `sensitiveResult`. Summaries, errors and logs never carry a secret or a raw upstream body.
33
+ 7. **Every host is declared.** A new upstream URL goes in `manifest.json` `network.hosts`, and every runtime dependency and data file is bundled into `dist/index.js`.
34
+ 8. **Profile diffs are security diffs.** A change to `test/compliance.yaml` changes what core certifies. Change it only on purpose, and say in the PR what changed and why.
35
+ 9. **Small and direct.** No speculative options, wrappers or config for one caller. Comments are one or two lines, for a reason or a design `§` reference only.
36
+ 10. **Tests ship with the change.** Specify the behavior first, as `docs/workflow.md` says. Logic gets a unit test in `test/unit/`. Behavior at the plugin boundary gets a profile case or a scenario in `features/<id>.feature`. Extend the fake upstream to match.
37
+
38
+ ## Checks
39
+
40
+ Run from the repo root before finishing:
41
+
42
+ ```sh
43
+ pnpm --filter ./plugins/<id> test # check, build, unit tests, compliance suite, features
44
+ pnpm lint && pnpm format:check && pnpm typecheck
45
+ pnpm check # manifests, plugin.yaml and versions
46
+ pnpm workflow pre-push # branch, phases, features and prose
47
+ ```
48
+
49
+ The plugin run must end with "<Name> <version> is fit for production." and no failed test.
50
+
51
+ Then run the `security-reviewer` subagent when `CLAUDE.md` › Security review lists a path you touched. Don't bump the version unless the user asked (`release-plugin`).
52
+
53
+ ## Reviewing
54
+
55
+ Lead with what tools can't catch. Say why each one matters:
56
+
57
+ - A lock or sensitive rule dropped or narrowed.
58
+ - A classification that turns a write into a read.
59
+ - A secret that reaches a summary or an error.
60
+ - A host missing from `network.hosts`.
61
+ - Logic in `src/` that belongs in `plugin.yaml`, or a plugin-side check that duplicates core.
62
+ - An `as` cast that hides an upstream shape.
63
+ - A `test/compliance.yaml` change with no reason given, or a case removed or weakened.
64
+ - A new upstream secret with no `secrets` entry, or no fake response that holds it.
@@ -0,0 +1,60 @@
1
+ # Testing
2
+
3
+ Vitest, with quickpickle for feature files. The plugin's `vitest.config.ts` uses `complianceConfig` from `@synoikia/core/testing/compliance`. The `test` script runs these steps:
4
+
5
+ 1. `synoikia-plugin check`
6
+ 2. `synoikia-plugin build`, which rebuilds `dist/index.js`
7
+ 3. the unit tests in `test/unit/`
8
+ 4. core's compliance suite, from `test/compliance.yaml`
9
+ 5. the plugin's own `features/<id>.feature`
10
+
11
+ The suite and the features run the **built** bundle. Rebuild after any change to `src/` or `plugin.yaml` before you run one file.
12
+
13
+ The run must end with "<Name> <version> is fit for production." and no failed test. A failing own feature does not change the verdict line, but it fails the run.
14
+
15
+ ## Pick the place that proves the behavior
16
+
17
+ | What | Where |
18
+ | --------------------------------------------------------------------- | ------------------------------------------------------------- |
19
+ | A pure function (catalog building, classification, a transform) | A unit test in `test/unit/` that imports and calls it |
20
+ | Reads, writes, rejected calls, locks, secrets and the connection test | A case in `test/compliance.yaml` |
21
+ | Plugin-specific behavior through core: approvals, summaries, levels | A scenario in `features/<id>.feature`, with the suite's steps |
22
+
23
+ `docs/workflow.md` › Tests has the rules for test files. A new `*.test.ts` file is a pure-function unit test in `test/unit/`.
24
+
25
+ ## The compliance profile
26
+
27
+ `test/compliance.yaml` tells core's suite how to drive the plugin against its fake upstream. The format and the steps are in core's `docs/plugin-authoring.md` › The compliance suite.
28
+
29
+ | Key | What |
30
+ | ---------------- | ------------------------------------------------------------------------ |
31
+ | `fake` | `test/fake.ts`, the module that starts the fake upstream |
32
+ | `connection` | The connection values, with `${fake.<name>}` for the fake's values |
33
+ | `reads` | Calls that must classify as reads |
34
+ | `writes` | Calls that must classify as writes |
35
+ | `rejects` | Calls that core or the plugin must refuse |
36
+ | `locks` | One case for each locked operation and each split twin, with its literal |
37
+ | `secrets` | Calls whose upstream values must never reach the agent |
38
+ | `connectionTest` | Connection values that the connection test must reject |
39
+ | `calls` | Named calls that `features/<id>.feature` uses |
40
+
41
+ - A lock with no case fails the suite. Add a case when you add a lock or a split twin.
42
+ - A diff of `test/compliance.yaml` is a security change. Change a case only on purpose, and explain it in the PR.
43
+
44
+ ## The fake upstream
45
+
46
+ - Each plugin has one in `test/` (`fake-upstream.ts` as `pnpm new` writes it). Build it on `startFakeHttp` from `@synoikia/core/testing` for HTTP, or on a local server for WebSocket.
47
+ - Tests never reach the real network.
48
+ - `test/fake.ts` exports `start()`. It starts the fake and returns its URL, `close()` and the values that the profile names. It can also return `reset()`.
49
+ - Extend the fake to answer every call the change makes. Include the responses that hold secrets, the error statuses and the permission denials.
50
+
51
+ ## Feature files
52
+
53
+ - Write scenarios with the suite's steps on the profile's named calls. The step table is in core's `docs/plugin-authoring.md`.
54
+ - Follow `docs/workflow.md` › Feature files: a `Rule:` for each scenario, outcomes and not solutions, five steps at most.
55
+ - Assert on what the agent or the human sees: the result, the approval text, the literal to type, the error code.
56
+
57
+ ## Unit tests
58
+
59
+ - Assert on what core or the model sees: the descriptor, the summary, the confirm literal, the masked result, the error code. Not on private fields.
60
+ - Give each new error path a test that hits it and checks its `ErrorCodes` value.
@@ -0,0 +1,31 @@
1
+ # TypeScript
2
+
3
+ The compiler settings are in `tsconfig.base.json`: strict, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`, `NodeNext` modules. Don't change them per plugin.
4
+
5
+ ## Imports and modules
6
+
7
+ - Use `import type` for anything used only as a type. Put a `.js` extension on every relative import.
8
+ - Relative paths only, no path aliases. Node built-ins with the `node:` prefix.
9
+ - A plugin's `src/` keeps the layout of the others: `index.ts` calls `runPlugin()`, `plugin.ts` builds the handlers with `definePlugin`, and `auth.ts` signs the requests. A `client.ts` talks to the upstream, and a `catalog.ts` turns discovery and `plugin.yaml` into operations. Add a file only when a second concern appears.
10
+ - Plugin-facing types (`PluginHandlers`, `OperationDescriptor`, …) come from `@synoikia/plugin-sdk`. Don't redeclare them.
11
+
12
+ ## Upstream data is `unknown`
13
+
14
+ - Type responses, events and specs as `unknown` and narrow them with `isPlainObject`, `requireString`, `requireField` or a small type guard. No `any`.
15
+ - Avoid `as`. When a cast is needed (a library gap, a record already checked), keep it next to the check that makes it true. `x!` is fine only right after a check or for a test fixture.
16
+ - `satisfies` to check an object literal against a type without widening it.
17
+ - With `noUncheckedIndexedAccess`, handle the `undefined` from `arr[i]` and `record[key]` (`?.`, `??`, an early return) rather than asserting it away.
18
+ - A shape the code doesn't recognize fails closed: redact it or throw, don't pass it on.
19
+
20
+ ## Errors
21
+
22
+ - Throw `PluginError` with an `ErrorCodes` value for failures core should see. Use `upstreamError` and `errorMessage` to map upstream failures. Messages name the operation and the cause, never a secret or a raw upstream body.
23
+ - Return a discriminated union (`{ ok: true; … } | { ok: false; reason: … }`) for an expected outcome the caller branches on. Don't add a generic `Result<T, E>` type or library.
24
+ - `catch (err)` gives `unknown`: narrow with `instanceof` or use `errorMessage(err)`.
25
+
26
+ ## Functions and data
27
+
28
+ - Prefer plain functions and object literals. Use a class only for a client with state (a connection, a session, pending requests).
29
+ - Small pure helpers over clever generics. Add a type parameter only when two real callers need it.
30
+ - Fixed sets of values: an `as const` array and a union derived from it, never an `enum`.
31
+ - Constants that are policy (limits, timeouts) live in `plugin.yaml` when they are plugin settings, else `UPPER_SNAKE` at the top of the module that owns them.
@@ -5,14 +5,38 @@ description: Prepare a plugin release by bumping its version in manifest.json an
5
5
 
6
6
  # Release a plugin
7
7
 
8
- A release is a version bump merged to `main`; the Release workflow packs, signs, verifies and publishes every plugin whose version isn't in the published index yet (see `RELEASING.md`).
8
+ A release is a version bump merged to `main`. The Release workflow (`.github/workflows/release.yml`) then packs, signs, verifies and publishes every plugin whose manifest version isn't in the published index yet (see `RELEASING.md`). Nothing is published from a branch, and nothing here needs the signing key.
9
9
 
10
- 1. **Pick the version** with semver against the last release (`git log --oneline -- plugins/<id>`):
11
- - patch: fixes, catalog corrections, wording;
12
- - minor: new operations, connection fields or capabilities; also any `plugin.yaml` change that unlocks an operation, drops a `sensitiveParams`/`sensitiveResult` rule or overrides a classification to read (call it out in the PR);
13
- - major: a change that breaks existing instances, such as a renamed operation key or a removed connection field.
14
- 2. **Bump both files to the same version**: `plugins/<id>/manifest.json` and `plugins/<id>/package.json`.
15
- 3. **Minimum Synoikia version**, only when the change needs a newer core: `"synoikia": { "minCoreVersion": "x.y.z" }` in `package.json`. A newer SDK means bumping the manifest `sdk` range together with the `@synoikia/plugin-sdk` range, then `pnpm install`.
16
- 4. **Verify**: `pnpm --filter ./plugins/<id> test`, then `pnpm typecheck && pnpm lint && pnpm format:check`.
10
+ ## Steps
17
11
 
18
- Released versions are immutable. Never touch `minisign.pub` or the signing steps of the workflow.
12
+ 1. **Pick the version** with semver against the last release (`git log --oneline -- plugins/<id>` shows what changed):
13
+ - patch: fixes, catalog corrections, wording.
14
+ - minor: new operations, connection fields or capabilities, compatible with existing instances. Also any `plugin.yaml` change that unlocks an operation, drops a `sensitiveParams`/`sensitiveResult` rule or overrides a classification to read. Call it out in the PR.
15
+ - major: a change that breaks existing instances, such as a renamed operation key, a removed connection field or a changed binding.
16
+
17
+ Released versions are immutable, so never reuse one. If the published index already has the version, `synoikia-plugin repo pack` skips the plugin and nothing is released.
18
+
19
+ 2. **Bump both files to the same version.** `synoikia-plugin check` (in the plugin's tests) and `repo pack` fail if they differ.
20
+ - `plugins/<id>/manifest.json` → `version`
21
+ - `plugins/<id>/package.json` → `version`
22
+
23
+ 3. **Minimum Synoikia version.** Only when the change needs a core feature that older Synoikia releases lack, add it to `package.json`:
24
+
25
+ ```json
26
+ "synoikia": { "minCoreVersion": "x.y.z" }
27
+ ```
28
+
29
+ Otherwise the manifest's `sdk` range decides which Synoikia versions can install it. If the plugin now needs a newer SDK, bump these ranges together, then run `pnpm install`:
30
+ - the `sdk` range in `manifest.json`
31
+ - the `@synoikia/plugin-sdk`, `@synoikia/core` and `@synoikia/create-plugin` ranges in `package.json`
32
+
33
+ 4. **Verify** that what will be packed builds and passes:
34
+
35
+ ```sh
36
+ pnpm --filter ./plugins/<id> test
37
+ pnpm typecheck && pnpm lint && pnpm format:check
38
+ ```
39
+
40
+ 5. In the PR description, name the plugin, the version, the user-visible changes and any `minCoreVersion`. Merging it publishes the release.
41
+
42
+ Never touch `minisign.pub`, the `MINISIGN_SECRET_KEY` steps in the workflow, or the `index` release.