@rackbops/ac-plugin-contract 2.0.0-alpha.58 → 2.0.0-alpha.60

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/AUTHORING.md CHANGED
@@ -39,6 +39,7 @@ key is itself a hard validation error, not a silently-ignored field.
39
39
  "kind": "in-process",
40
40
  "server": "dist/server.js",
41
41
  "frontend": "dist/ui.js",
42
+ "capabilities": ["agent.git.read"],
42
43
  "contributes": {
43
44
  "panels": [{ "id": "hello-panel", "title": "Hello", "route": "/hello", "export": "HelloPanel" }],
44
45
  "cards": [{ "id": "hello-card", "title": "Hello", "sizes": ["2x1"], "export": "HelloCard" }],
@@ -56,8 +57,7 @@ key is itself a hard validation error, not a silently-ignored field.
56
57
  { "id": "hello.machines", "schedule": "17 3 * * *", "timeoutSec": 30, "runOn": "agent" }
57
58
  ],
58
59
  "routes": [{ "method": "GET", "path": "/ping" }],
59
- "settings": [{ "key": "greeting", "type": "string", "default": "hello" }],
60
- "attention": true
60
+ "settings": [{ "key": "greeting", "type": "string", "default": "hello" }]
61
61
  }
62
62
  }
63
63
  ```
@@ -69,24 +69,28 @@ missing any of them fails `pnpm run check`. The complete shape (the `acPlugin` b
69
69
  ```jsonc
70
70
  {
71
71
  "name": "@ac/plugin-hello",
72
- "version": "0.1.0",
72
+ "version": "0.1.1",
73
73
  "private": true,
74
74
  "type": "module",
75
75
  "exports": { ".": { "default": "./dist/server.js" } },
76
76
  "acPlugin": { /* the block shown above */ },
77
77
  "scripts": {
78
78
  "typecheck": "tsc --noEmit && tsc -p tsconfig.ui.json --noEmit",
79
- "build": "tsc && tsc -p tsconfig.ui.json",
79
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.build.json",
80
80
  "test": "vitest run"
81
81
  },
82
82
  "dependencies": {
83
83
  "@ac/contract": "workspace:*",
84
- "@rackbops/ui-react": "^0.2.36",
84
+ "@rackbops/ui-react": "^0.2.43",
85
85
  "react": "^19.2.0"
86
86
  },
87
87
  "devDependencies": {
88
+ "@testing-library/dom": "^10.4.2",
89
+ "@testing-library/react": "^16.1.0",
88
90
  "@types/node": "catalog:",
89
91
  "@types/react": "catalog:",
92
+ "@types/react-dom": "catalog:",
93
+ "jsdom": "^30.1.1",
90
94
  "typescript": "catalog:",
91
95
  "vitest": "catalog:"
92
96
  }
@@ -144,12 +148,26 @@ no host I/O, so the host can construct every plugin before any side effect. All
144
148
 
145
149
  ```ts
146
150
  import type { CreatePlugin } from "@ac/contract"
151
+ import { checkRow } from "./stored-row.js"
152
+
153
+ export const DEFAULT_GREETING = "hello"
154
+
155
+ function isString(value: unknown): value is string {
156
+ return typeof value === "string"
157
+ }
147
158
 
148
159
  export const createPlugin: CreatePlugin = (host) => ({
149
160
  activate() {
150
161
  // GET /api/x/hello/ping → the greeting setting (its default comes from the manifest `settings[]`).
162
+ // A stored greeting that is not a string reads as unset (#900; see "Setting" below).
151
163
  host.routes.register("GET", "/ping", async () => {
152
- const greeting = (await host.settings.get("greeting")) ?? "hello"
164
+ const greeting =
165
+ checkRow(await host.settings.get("greeting"), isString, {
166
+ log: host.log,
167
+ store: "plugin-settings",
168
+ key: "greeting",
169
+ expected: "string",
170
+ }) ?? DEFAULT_GREETING
153
171
  return Response.json({ greeting })
154
172
  })
155
173
  // An action: the button, its flags and the fields come from the manifest; the handler is here.
@@ -180,7 +198,12 @@ is never left open). Register last whatever must not exist when a later step fai
180
198
 
181
199
  - **Route** — `host.routes.register(method, path, handler)`. The handler is a web-standard
182
200
  `(request: Request) => Response | Promise<Response>`, mounted at `/api/x/<id><path>`. You may only register a
183
- `(method, path)` you declared in `contributes.routes` — an undeclared route throws. **Never answer `502` or
201
+ `(method, path)` you declared in `contributes.routes` — an undeclared route throws.
202
+ `operator: true` on the route (#867): With the admin kill switch off (`admin_enabled = false`), the host
203
+ refuses this route for its declared method with `403 admin disabled`, as it refuses the admin plugin's own
204
+ surface. Mark an action or route an operator would expect the switch to shut: one that runs a job the job-run
205
+ route refuses, writes to GitHub or the store on the operator's behalf, or changes the console's configuration.
206
+ Additive; a host released before this field refuses a manifest that declares it. **Never answer `502` or
184
207
  `504` yourself** (#474): the production console is reached through Cloudflare, and Cloudflare's own
185
208
  documentation says it "returns a Cloudflare-branded HTTP 502 or 504 error when your origin web server responds
186
209
  with a standard HTTP 502 bad gateway or 504 gateway timeout error" — so on the production console (default
@@ -219,6 +242,11 @@ is never left open). Register last whatever must not exist when a later step fai
219
242
  same action again while that locked modal is up reuses it and does not re-run it. After a failed result the
220
243
  primary is enabled again and a retry runs, so a double-click whose first run fails fast can run the action
221
244
  twice (the second a retry): an action whose failure may have partly applied should tolerate that.
245
+ `operator: true` (#867): With the admin kill switch off (`admin_enabled = false`), the host refuses this
246
+ action (`POST /api/actions/<plugin>/<id>`) with `403 admin disabled`, as it refuses the admin plugin's own
247
+ surface. Mark an action or route an operator would expect the switch to shut: one that runs a job the job-run
248
+ route refuses, writes to GitHub or the store on the operator's behalf, or changes the console's configuration.
249
+ Additive; a host released before this field refuses a manifest that declares it.
222
250
  `confirm` stays a valid manifest flag, so an existing manifest keeps loading, but neither the modal nor the
223
251
  palette acts on it.
224
252
  - **Job** — implement `runJob(ctx)` and dispatch on `ctx.jobId`. The host runs only jobs you declared in
@@ -254,6 +282,27 @@ is never left open). Register last whatever must not exist when a later step fai
254
282
  carries it. See §7.
255
283
  - **Setting** — read/write with `host.settings.get(key)` / `host.settings.set(key, value)`. Both are async;
256
284
  `get` returns the manifest `default` when unset.
285
+ - **Storage** — `host.storage.get(key)` / `host.storage.set(key, value)` / `host.storage.delete(key)`, async,
286
+ any JSON value, scoped to your plugin.
287
+ - **Check the shape of what you read back** (#900). `get<T>` on either store is only a cast: the host
288
+ guarantees a row is valid JSON (a row that is not reads as unset, #879), never that it is the shape your
289
+ code expects. A hand edit, a restore from a damaged copy or an older version of your plugin can leave a
290
+ number where you wrote an object. So read untyped (`get(key)` answers `unknown`), check the fields you
291
+ dereference at the read site, and treat a row of the wrong shape as unset: your default applies (for a
292
+ setting, the manifest default), and log a warn line naming the key, never the value. Two refinements:
293
+ - if unset would **widen** what your plugin acts on (a deny list, a guard, a limit), refuse the row
294
+ instead and fail closed: an unreadable exclusion list must never mean "exclude nothing";
295
+ - for a row that keeps a list (a queue, a history), keep the good entries and drop only the bad ones,
296
+ and before you write such a row back over a value you could not read whole, keep the raw value
297
+ somewhere (`<key>/unreadable`), or one malformed entry, or a history saved in another shape, is lost
298
+ for good. Never write over a kept copy: if it already holds a different value, refuse the write
299
+ (fail the job or action, naming both keys) until the operator repairs the row or deletes the copy.
300
+
301
+ `plugins/hello`'s `/ping` is the worked example, and its `src/stored-row.ts` (`checkRow`, `requireRow`,
302
+ `checkList`, `checkMap`, `preserveUnreadable`) is the helper every bundled plugin copies: copy it into
303
+ yours, since plugins share no runtime code. It writes each line at most once an hour per row, so a
304
+ polled route over a bad row does not flood the log. The bundled plugins' lines and fields are in
305
+ `docs/identifiers.md` (#900's entry).
257
306
  - **Console settings** (#506) — `host.consoleSettings?.get(key)`, read-only, optional. Unlike `Setting`
258
307
  above (per-plugin), this reads the console-WIDE settings an operator edits in Admin's Settings card
259
308
  (`repo_owners`, `layout_file`, `tab_order`, `plan_default_machine`, `repo_pins`) -- but only a key on
@@ -261,7 +310,7 @@ is never left open). Register last whatever must not exist when a later step fai
261
310
  is the reference consumer (`repo_owners`/`repo_pins`, its own repo-set resolution).
262
311
 
263
312
  `createPlugin` must import from `@ac/contract` **type-only** (`import type`), so the compiled `dist/server.js`
264
- has no runtime imports — see §6.
313
+ has no runtime import of it — see §6 (a sibling module of your own, such as `./stored-row.js`, is fine).
265
314
 
266
315
  ### Rejecting a bad action input — `ActionInputError` (#440)
267
316
 
@@ -616,6 +665,16 @@ A `hint` (optional, e.g. a category or the plugin id) is searched the same as `l
616
665
  `>hel` finds an item whose label doesn't contain "hel" but whose hint does — and renders as muted secondary
617
666
  text in the row.
618
667
 
668
+ **What the chrome tolerates (#761).** A plugin bundle is built separately, so the shell does not trust the
669
+ shapes it hands to the chrome. A quickcast source that throws or returns a non-array is skipped, and an item
670
+ without a string `label` (or a `custom` item without a function `run`) is dropped, each logged with
671
+ `console.warn` at most once a minute per source. A toast message that is not a string is shown as its
672
+ `message` (an `Error`) or its `String()` form, and an unknown `severity` shows as `info`. A header button
673
+ label is shown as `String(label)`, and a button whose `onClick` is not a function is not added. If one of the
674
+ command palette, the toasts or the header buttons still fails to render, only that piece is replaced by a
675
+ one-line note and the rest of the console stays. The types in `@ac/contract` remain the contract; this is a
676
+ backstop, not a license.
677
+
619
678
  **Ctrl/Cmd+K from inside a same-origin iframe (#369).** A key pressed while an iframe has focus lands in the
620
679
  iframe's own document, never the shell's, so the palette does not open by itself. A panel that hosts a
621
680
  same-origin iframe (the Artifacts report preview does) forwards the hotkey: bind a `keydown` listener on the
@@ -695,34 +754,70 @@ Build it with `tsc` and `"jsx": "react-jsx"` — `tsc` under `nodenext` preserve
695
754
  import and injects `import { jsx } from "react/jsx-runtime"`, emitting exactly the servable ESM the import map
696
755
  resolves. Do **not** bundle (no rollup/esbuild bundling of React) — that would ship a second React copy.
697
756
 
698
- hello uses two tsconfigs so the server (no JSX) and the frontend (JSX) build separately:
757
+ hello uses four tsconfigs. The server (no JSX) and the frontend (JSX) build separately, and each has a typecheck config that covers its tests and a build-only config that does not, so `build` never emits a compiled test into `dist/` (#350; `pnpm run check`'s `check-no-test-emit.mjs` fails otherwise, and `check-typecheck-covers-tests.mjs` fails if a test file is in no typecheck config):
699
758
 
700
- `tsconfig.json` (server):
759
+ `tsconfig.json` (typecheck, server):
701
760
 
702
761
  ```json
703
762
  {
704
763
  "extends": "../../tsconfig.base.json",
705
764
  "compilerOptions": { "rootDir": "src", "outDir": "dist" },
706
- "include": ["src/server.ts", "src/server.test.ts"]
765
+ "include": [
766
+ "src/server.ts",
767
+ "src/server.test.ts",
768
+ "src/stored-row.ts",
769
+ "src/stored-row.test.ts",
770
+ "src/ac-host-drift.test.ts"
771
+ ]
707
772
  }
708
773
  ```
709
774
 
710
- `tsconfig.ui.json` (frontend):
775
+ `tsconfig.ui.json` (typecheck, frontend):
711
776
 
712
777
  ```json
713
778
  {
714
779
  "extends": "../../tsconfig.base.json",
715
- "compilerOptions": { "rootDir": "src", "outDir": "dist", "jsx": "react-jsx" },
716
- "include": ["src/ui.tsx", "src/ui.test.tsx"]
780
+ "compilerOptions": {
781
+ "rootDir": "src",
782
+ "outDir": "dist",
783
+ "jsx": "react-jsx",
784
+ "lib": ["ES2023", "DOM", "DOM.Iterable"]
785
+ },
786
+ "include": ["src/ui.tsx", "src/ui.test.tsx", "src/quickcast.test.tsx", "src/ac-host.d.ts"]
717
787
  }
718
788
  ```
719
789
 
720
- and `package.json` builds + typechecks both:
790
+ `tsconfig.build.json` (build, server):
791
+
792
+ ```json
793
+ {
794
+ "extends": "../../tsconfig.base.json",
795
+ "compilerOptions": { "rootDir": "src", "outDir": "dist" },
796
+ "include": ["src/server.ts", "src/stored-row.ts"]
797
+ }
798
+ ```
799
+
800
+ `tsconfig.ui.build.json` (build, frontend):
801
+
802
+ ```json
803
+ {
804
+ "extends": "../../tsconfig.base.json",
805
+ "compilerOptions": {
806
+ "rootDir": "src",
807
+ "outDir": "dist",
808
+ "jsx": "react-jsx",
809
+ "lib": ["ES2023", "DOM", "DOM.Iterable"]
810
+ },
811
+ "include": ["src/ui.tsx", "src/ac-host.d.ts"]
812
+ }
813
+ ```
814
+
815
+ and `package.json` builds with the build-only pair and typechecks with the other two:
721
816
 
722
817
  ```json
723
818
  "scripts": {
724
819
  "typecheck": "tsc --noEmit && tsc -p tsconfig.ui.json --noEmit",
725
- "build": "tsc && tsc -p tsconfig.ui.json",
820
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.build.json",
726
821
  "test": "vitest run"
727
822
  }
728
823
  ```
@@ -874,12 +969,21 @@ if (outcome.ok) {
874
969
  }
875
970
  ```
876
971
 
972
+ **Treat every string union in the contract as open** (`error.kind` here, `ToastSeverity`, `TaskStatus`,
973
+ `PreflightStatus`, `OverviewOwner`, `ActionFieldType`, a filter token's `kind`): a later minor release may add a
974
+ member, which the contract calls additive. Keep a default branch (no `assertNever`) in any `switch` over one.
975
+ The unions most likely to grow carry a `MAY GROW` tag in their doc comment (`ActionFieldType` has grown before, #463, and may again; its doc comment is in the published schema, so it carries no tag).
976
+
877
977
  `outcome.error.kind === "grant"` is the one worth showing to the person almost verbatim -- it
878
978
  carries the agent's own refusal text (`"<capability> is not granted to <plugin> on <machine>"`). The
879
979
  others are the console's problem to describe differently: `unknown-machine` (a stale machine id),
880
- `refused` (the path isn't a `/v1/*` verb, or is `/v1/pair`/`/v1/grants` -- the control plane, never
980
+ `refused` (the path isn't a `/v1/*` verb, or is `/v1/pair`/`/v1/grants`/`/v1/preflight`/`/v1/update` -- the control plane, never
881
981
  reachable this way, whatever the path spelling), `pin` (the agent's certificate changed since
882
- pairing), `network`, `http` (an unexpected status the console doesn't special-case).
982
+ pairing), `network`, `http` (an unexpected status the console doesn't special-case). `ok: true` means the
983
+ agent answered a 2xx and its body was read (#778): a 2xx whose body could not be read (a timeout or a reset
984
+ mid-body, a body that is not JSON) is `ok: false`, `kind: "http"`, with that 2xx as `status` and the reason
985
+ `agent answered <status> but its body could not be read: <cause>`. The agent may have acted on such a call,
986
+ so a verb that starts something should not be retried blindly. A `204` is `ok: true` with no body.
883
987
 
884
988
  An agent job declares the capabilities its own verbs need, in the job's `capabilities` (#843). Each one
885
989
  must also be in the top-level list and match the capability grammar (`agent.` and lowercase segments).
@@ -952,10 +1056,20 @@ drawer shows: `{ status: "done" }` only for the agent's `done` frame with `statu
952
1056
  `stream ended before done: <message>`, the drawer's last line without its brackets, when the stream
953
1057
  closed or errored first or the `done` frame did not parse, so the job's own failure and a lost stream
954
1058
  read differently. It never rejects. It never settles when the relay sees no end at all: the host
955
- stopping while the job runs (the relay stops with the process), or a hung job. It does not settle
956
- while the connection is silently gone, and never if it does not come back (a reboot, a lost NAT
957
- mapping): no FIN or RST reaches the host, and the stream has no body timeout, no keepalive and no
958
- agent heartbeat.
1059
+ stopping while the job runs (the relay stops with the process), or a hung job (the stream has no body
1060
+ timeout and the agent sends no heartbeat). A connection silently gone (no FIN or RST reaches the host:
1061
+ the machine asleep or off the network, a reboot, a lost NAT mapping) settles `failed` with
1062
+ `stream ended before done: terminated` once TCP keep-alive's probes go unanswered (#777), about 70 s
1063
+ after the last byte on the console's Linux host; a machine asleep for longer than that mid-job reads
1064
+ `failed` even if the job finishes after it wakes.
1065
+
1066
+ A host restart mid-job is the one end the relay cannot report from inside the new process (#911): the
1067
+ host records each relay it opens and forgets it when the relay sees its end, and
1068
+ `host.agent.unfinishedStreams?()` (optional, like `finished`) returns, once, the calling plugin's relays
1069
+ that an earlier host process opened and never saw end, each `{ taskId, jobId, machineId, machineName,
1070
+ label, openedAt }` (the `taskId` is the earlier process's and no longer exists). Call it once in
1071
+ `activate()` and report each run as ended with an unknown outcome; `plugins/claude-runner` emits
1072
+ `claude.run` `status: "unknown"` for each. `ac jobs run` keeps no record and returns none.
959
1073
 
960
1074
  ```ts
961
1075
  const { jobId } = await execResult // however the plugin's own verb call started the job
@@ -987,13 +1101,13 @@ by `${source}:${id}`.
987
1101
  interface AttentionItem {
988
1102
  id: string // unique within YOUR plugin; the rollup's dedupe key is `${source}:${id}`
989
1103
  severity: "danger" | "warn" | "info"
990
- source: string // your own plugin id -- the rollup shows it as the row's source
1104
+ source: string // your own plugin id -- the dedupe key and draft scope; the rollup does not display it
991
1105
  text: string // always a counted sentence: "3 open actions in the ledger", never "some actions"
992
1106
  badge?: string
993
1107
  href?: string // an external link; wins over `target` when both are set. Linked only
994
1108
  // when its scheme is http:, https:, mailto: or tel: (#722); any other
995
1109
  // href is not linked and `target` is used instead
996
- target?: string // an in-app destination: "widget:<cardId>" or "panel:<pluginId>"
1110
+ target?: string // an in-app destination: "widget:<cardId>" (a home card: scrolls to and flashes it; text when the card is not on the page) or "panel:<pluginId>"
997
1111
  hint?: string // the exact command to run, or the reason -- shown as a tooltip/expander
998
1112
  action?: { kind: "issue"; repo?: string; title: string; body: string } // opens provider-github's issue.create, seeded
999
1113
  }
@@ -1129,9 +1243,9 @@ what it's for.
1129
1243
  |---|---|---|
1130
1244
  | Panel | `panels[]` (`id`, `title`, `route`, `export`, `nav?`, `order?`, `isolation?`) | a component export in `ui.js` |
1131
1245
  | Card | `cards[]` (`id`, `title`, `sizes`, `export`) | a component export in `ui.js` |
1132
- | Action | `actions[]` (`id`, `scope`, `title?`, `confirm?`, `danger?`, `fields?`) | `host.actions.register({ id, run })` |
1246
+ | Action | `actions[]` (`id`, `scope`, `title?`, `confirm?`, `danger?`, `operator?`, `fields?`) | `host.actions.register({ id, run })` |
1133
1247
  | Job | `jobs[]` (`id`, `schedule`, `timeoutSec`, `runOn`, `minAgentVersion?`, `capabilities?`) | `runJob(ctx)` dispatching on `ctx.jobId` |
1134
- | Route | `routes[]` (`method`, `path`) | `host.routes.register(method, path, handler)` |
1248
+ | Route | `routes[]` (`method`, `path`, `operator?`) | `host.routes.register(method, path, handler)` |
1135
1249
  | Setting | `settings[]` (`key`, `type`, `default?`) | read via `host.settings.get/set` |
1136
1250
  | Attention | `attention: true` | `host.routes.register("GET", "/attention", handler)` -> `{ ok: true, items }` |
1137
1251
 
@@ -1197,6 +1311,9 @@ bound for a slot, or `null` when none is. It throws unless the plugin's own mani
1197
1311
  `{slot, version}` in `contributes.consumes`, and an installed plugin may not resolve `auth` at all. Call
1198
1312
  it at request time (inside a route handler or job), never in `activate()`: providers bind during
1199
1313
  activation in load order, so a consumer that activates first would read `null` there for good.
1314
+ `host.slots.providerId?.(slot, version)` (#866, optional: a host older than #866 has none) answers the id of the
1315
+ plugin bound to a slot, or `null`, without the provider itself and without a `consumes` entry; an installed
1316
+ plugin may not read `auth`'s.
1200
1317
  `ac-artifacts-weft` (`artifacts@1`) is the reference consumer: the second plugin to declare `consumes`
1201
1318
  for it, after `artifacts`, which reads `provider-artifacts`' HTTP routes from the browser, and the first to
1202
1319
  call `resolve`. `hello` contributes none of the six points above via a slot (nor `attention` -- its own manifest
package/README.md CHANGED
@@ -18,11 +18,20 @@ convention.
18
18
 
19
19
  - `import type { HostApi, Plugin, ... } from "@rackbops/ac-plugin-contract"` — the server-side types.
20
20
  - `@rackbops/ac-plugin-contract/schema.json` — the manifest JSON Schema, for a vendored drift check
21
- against the live console (§5 of AUTHORING.md).
21
+ (§9 of AUTHORING.md, "Vendoring `schema.json` with a drift check"). The check compares your
22
+ committed copy with the one in the installed package, so it detects a version bump that changed
23
+ the schema; it does not look at a running console.
22
24
  - `@rackbops/ac-plugin-contract/host` — a types-only ambient declaration of `@ac/host` (the browser
23
25
  API a plugin's frontend imports at runtime, resolved through the console's own import map). Add it
24
26
  to your tsconfig's `"types"` array or a triple-slash reference — it carries no runtime export.
25
27
 
28
+ ## Unions are open
29
+
30
+ Treat every string union this package exports (`AgentCallOutcome`'s `error.kind`, `ToastSeverity`,
31
+ `TaskStatus`, `PreflightStatus`, `OverviewOwner`, `ActionFieldType` and the like) as open: a later minor
32
+ release may add a member, so keep a default branch in any `switch` over one. The unions most likely to grow
33
+ carry a `MAY GROW` tag in their doc comment.
34
+
26
35
  ## Version
27
36
 
28
37
  This package's version always equals the published `artifact-console` host's own version — pin your
@@ -46,4 +46,3 @@ export type ActionOptionsResponse = {
46
46
  ok: false;
47
47
  error: string;
48
48
  };
49
- //# sourceMappingURL=action-options.d.ts.map
@@ -6,4 +6,3 @@
6
6
  * nothing here changes an existing field.
7
7
  */
8
8
  export {};
9
- //# sourceMappingURL=action-options.js.map
@@ -3,8 +3,8 @@
3
3
  * `contributes.attention: true` in its manifest and serves `GET /api/x/<id>/attention` ->
4
4
  * `{ ok: true, items: AttentionItem[] }`. The `overview` plugin's rollup card polls every
5
5
  * contributor (discovered via `GET /api/plugins`'s `PluginView.attention`), merges, sorts by
6
- * severity, and dedupes by `${source}:${id}`. Ported from 1.x's `attentionRows` (`R:\repos\
7
- * artifacts-console\artifact-console\frontend\src\overview\attention.ts`) -- "own the fact, own
6
+ * severity, and dedupes by `${source}:${id}`. Ported from 1.x's `attentionRows` (the `artifacts-console`
7
+ * repo's overview `attention.ts`) -- "own the fact, own
8
8
  * the row": each plugin knows its own facts best, so the rollup itself derives nothing, it only
9
9
  * merges what contributors already computed.
10
10
  */
@@ -28,7 +28,7 @@ export interface AttentionItem {
28
28
  /** Unique within the contributing plugin; the rollup's dedupe key is `${source}:${id}`. */
29
29
  id: string;
30
30
  severity: AttentionSeverity;
31
- /** The contributing plugin's id, e.g. "scans" -- the rollup shows it as the row's source. */
31
+ /** The contributing plugin's id, e.g. "scans" -- identifies the contributing plugin (dedupe key, draft scope); the rollup does not display it. */
32
32
  source: string;
33
33
  /** Always a counted sentence (1.x's `attCount`, e.g. "1 scheduled job run failed"). */
34
34
  text: string;
@@ -39,8 +39,9 @@ export interface AttentionItem {
39
39
  * custom scheme, one that does not parse) renders no link, and the row uses `target` instead, as
40
40
  * if `href` were absent. */
41
41
  href?: string;
42
- /** An in-app destination: `widget:<cardId>` flashes/scrolls to a sibling card on Overview,
43
- * `panel:<pluginId>` navigates to that plugin's panel (its first panel route, or `/<pluginId>`
42
+ /** An in-app destination: `widget:<cardId>` scrolls to the home card whose `data-card-id` is
43
+ * `<cardId>` or ends `:<cardId>`, and flashes it (when no such card is on the page the row is
44
+ * plain text), `panel:<pluginId>` navigates to that plugin's panel (its first panel route, or `/<pluginId>`
44
45
  * when the console knows none). A value that is not a string is ignored. */
45
46
  target?: string;
46
47
  /** The exact command to run, or the reason, shown as a tooltip/expander (1.x's `hint`). */
@@ -53,4 +54,3 @@ export interface AttentionResponse {
53
54
  ok: true;
54
55
  items: AttentionItem[];
55
56
  }
56
- //# sourceMappingURL=attention.d.ts.map
package/dist/attention.js CHANGED
@@ -3,10 +3,9 @@
3
3
  * `contributes.attention: true` in its manifest and serves `GET /api/x/<id>/attention` ->
4
4
  * `{ ok: true, items: AttentionItem[] }`. The `overview` plugin's rollup card polls every
5
5
  * contributor (discovered via `GET /api/plugins`'s `PluginView.attention`), merges, sorts by
6
- * severity, and dedupes by `${source}:${id}`. Ported from 1.x's `attentionRows` (`R:\repos\
7
- * artifacts-console\artifact-console\frontend\src\overview\attention.ts`) -- "own the fact, own
6
+ * severity, and dedupes by `${source}:${id}`. Ported from 1.x's `attentionRows` (the `artifacts-console`
7
+ * repo's overview `attention.ts`) -- "own the fact, own
8
8
  * the row": each plugin knows its own facts best, so the rollup itself derives nothing, it only
9
9
  * merges what contributors already computed.
10
10
  */
11
11
  export {};
12
- //# sourceMappingURL=attention.js.map
@@ -28,7 +28,9 @@ export interface SettingsService {
28
28
  export interface ConsoleSettingsService {
29
29
  get<T = unknown>(key: string): Promise<T | undefined>;
30
30
  }
31
- /** Per-plugin durable storage: a SQLite table scoped to the plugin, with atomic JSON helpers. */
31
+ /** Per-plugin durable storage: the plugin's rows in the host's shared `plugin_storage` table, keyed by
32
+ * (plugin id, key), with plain `get`/`set`/`delete` over JSON values (an `undefined` value is stored as
33
+ * `null`). */
32
34
  export interface StorageService {
33
35
  get<T = unknown>(key: string): Promise<T | undefined>;
34
36
  set(key: string, value: unknown): Promise<void>;
@@ -73,7 +75,9 @@ export interface JobDeclaration {
73
75
  id: string;
74
76
  runOn: "host" | "agent";
75
77
  }
76
- /** The typed event bus (e.g. `issue.changed`, `job.finished`, `artifact.rendered`). */
78
+ /** The event bus: `emit(type: string, payload: unknown)` is untyped, so a type name is a convention between
79
+ * emitter and subscriber (the host emits `job.finished`; core plugins emit `artifacts.rendered` and
80
+ * `claude.run`; `issue.changed` is the documented example of a plugin-to-plugin event). */
77
81
  export interface EventsService {
78
82
  /** Calls every subscriber of `type` synchronously, in subscription order, and never awaits one.
79
83
  * A subscriber that throws, or returns a promise that rejects, is logged by the host and stops
@@ -94,7 +98,10 @@ export interface GithubService {
94
98
  /** The one shared Octokit REST client — consumers make all GitHub calls through this. It
95
99
  * authenticates with the console's PAT (`AC_GITHUB_TOKEN`), and `octokit().auth()` returns that
96
100
  * token to whichever plugin asks: no `env[]` declaration is needed, and nothing hides it (#784;
97
- * in-process plugins are trusted code, not sandboxed). */
101
+ * in-process plugins are trusted code, not sandboxed). Since #753 the client caps requests at 10 in
102
+ * flight, retries a GET's 5xx (GraphQL and writes are POSTs and are not) and a short secondary limit once, gives each attempt 20 s, and rejects a
103
+ * request at once with `GitHub rate limit exhausted until <ISO>` while a 403/429 has reported that
104
+ * request's budget spent. */
98
105
  octokit(): Octokit;
99
106
  /** Remaining primary-rate-limit budget from the last response whose three `x-ratelimit-*` headers
100
107
  * are all finite numbers (and the reset a valid time), a failed response's included (#784: a 403 or
@@ -158,9 +165,11 @@ export interface AgentService {
158
165
  * tasks drawer's last line without its brackets, when the stream closed or errored before its
159
166
  * `done` frame, or that frame did not parse. It never rejects. It never settles when the relay sees
160
167
  * no end at all: when the host stops while the job runs (the relay stops with the process), or when
161
- * the job hangs. It does not settle while the connection is silently gone, and never if it does not
162
- * come back (a reboot, a lost NAT mapping): with the machine asleep or the network gone, no FIN or
163
- * RST reaches the host, and the stream has no body timeout, no keepalive and no agent heartbeat.
168
+ * the job hangs (the stream has no body timeout and the agent sends no heartbeat). A connection
169
+ * silently gone (the machine asleep or off the network, so no FIN or RST reaches the host) settles
170
+ * `failed` with `stream ended before done: terminated` once TCP keep-alive's probes go unanswered
171
+ * (#777), about 70 s after the last byte on the console's Linux host; a machine asleep for longer than
172
+ * that mid-job reads `failed` even if the job finishes after it wakes.
164
173
  */
165
174
  streamJob(machineId: string, jobId: string, opts: {
166
175
  label: string;
@@ -174,6 +183,22 @@ export interface AgentService {
174
183
  reason: string;
175
184
  }>;
176
185
  }>;
186
+ /**
187
+ * The relays this plugin opened with `streamJob` in an earlier host process whose end that process
188
+ * never saw (#911): a host restart mid-job. Each is returned once, then forgotten. A process that
189
+ * keeps no relay record (`ac jobs run`) and a host older than #911 return none or lack the method.
190
+ * `taskId` is the earlier process's task id, which no longer exists.
191
+ */
192
+ unfinishedStreams?(): Promise<UnfinishedStream[]>;
193
+ }
194
+ /** A `streamJob` relay whose end an earlier host process never saw (#911). */
195
+ export interface UnfinishedStream {
196
+ taskId: string;
197
+ jobId: string;
198
+ machineId: string;
199
+ machineName: string;
200
+ label: string;
201
+ openedAt: string;
177
202
  }
178
203
  /** Proxy to a sidecar plugin's HTTP surface (the host handles health + base URL). */
179
204
  export interface SidecarService {
@@ -223,7 +248,8 @@ export type ActionInputErrorName = "ActionInputError";
223
248
  * know their callers' payload shapes either. Two providers for the same `slot@version` is a boot
224
249
  * error (auth/notify are singletons) -- surfaced by the host-side registry `provide` throws from, not
225
250
  * this interface, since this is the plugin's OWN scoped view (pre-bound to its id, so the error can
226
- * name the real offending plugin without a pluginId parameter here). */
251
+ * name the real offending plugin without a pluginId parameter here). `providerId` (#866) reads which
252
+ * plugin holds a binding without exposing it (the admin Security card reads `auth@1`'s). */
227
253
  export interface SlotRegistry {
228
254
  /** Register this plugin's provider instance for `slot@version`. Throws if that slot already has a
229
255
  * provider (from this plugin or another) -- auth/notify are singletons, never a last-write-wins. */
@@ -238,6 +264,13 @@ export interface SlotRegistry {
238
264
  * during activation in load order, so a consumer that activates before its provider would read `null`
239
265
  * there for good (#97). */
240
266
  resolve<T = unknown>(slot: string, version: number): T | null;
267
+ /** The id of the plugin bound to `slot@version`, or `null` when nothing is bound (#866). Never the
268
+ * provider itself, so it opens no way to the impl. Unlike `resolve` it needs no `contributes.consumes`
269
+ * entry. An installed plugin may not read the `auth` slot's (it throws, naming the plugin), as it may
270
+ * not resolve it. Optional: a host older than #866 has no such method, so check for it before calling
271
+ * (`host.slots.providerId?.(slot, version)`). Call it at request time, never in `activate()`, for the
272
+ * same load-order reason as `resolve`. */
273
+ providerId?(slot: string, version: number): string | null;
241
274
  }
242
275
  /** A logger prefixed with the plugin id. A plain-object first arg is the line's structured fields; an
243
276
  * `Error` first arg is logged as its own enumerable properties (`code`, `status`, ...) plus
@@ -288,6 +321,11 @@ export interface HostApi {
288
321
  /** #506: see `ConsoleSettingsService`. Optional so an out-of-repo plugin's own test fake still
289
322
  * compiles. */
290
323
  consoleSettings?: ConsoleSettingsService;
324
+ /** #875: `true` only in the process that serves HTTP and owns the plugins' on-disk state (`main.ts`);
325
+ * absent in a one-shot CLI process (`ac jobs run`), which loads every plugin too. Work that must run
326
+ * once per server boot, and must never run beside a live server (clearing a stale lock file under a
327
+ * git clone, say), is gated on it. Optional so an out-of-repo plugin's own test fake still compiles. */
328
+ serving?: boolean;
291
329
  }
292
330
  /**
293
331
  * The context the host passes to `Plugin.runJob` for one job run (#31). `jobId` selects which
@@ -326,7 +364,7 @@ export interface Plugin {
326
364
  }
327
365
  /** A plugin's server entry: `dist/server.js` exports this as `createPlugin`. */
328
366
  export type CreatePlugin = (host: HostApi) => Plugin;
329
- /** Toast severity, matching notify. */
367
+ /** Toast severity, matching notify. MAY GROW: keep a default branch. */
330
368
  export type ToastSeverity = "info" | "warning" | "error";
331
369
  /**
332
370
  * The browser-side `@ac/host` a frontend bundle imports. v1 (implemented in #32) is `theme` tokens +
@@ -454,4 +492,3 @@ export interface ActionSettledEvent {
454
492
  ok: boolean;
455
493
  error?: string;
456
494
  }
457
- //# sourceMappingURL=host-api.d.ts.map
package/dist/host-api.js CHANGED
@@ -7,4 +7,3 @@
7
7
  * `HOST_API_VERSION`.
8
8
  */
9
9
  export {};
10
- //# sourceMappingURL=host-api.js.map
@@ -33,4 +33,3 @@ export interface PluginIndex {
33
33
  generatedAt?: string;
34
34
  plugins: PluginIndexEntry[];
35
35
  }
36
- //# sourceMappingURL=index-envelope.d.ts.map
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=index-envelope.js.map
package/dist/index.d.ts CHANGED
@@ -13,4 +13,3 @@ export * from "./ov-widgets.js";
13
13
  export type * from "./slots.js";
14
14
  export { HOST_API_VERSION, SLOTS } from "./version.js";
15
15
  export type * from "./views.js";
16
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -6,4 +6,3 @@
6
6
  export * from "./issue-filter.js";
7
7
  export * from "./ov-widgets.js";
8
8
  export { HOST_API_VERSION, SLOTS } from "./version.js";
9
- //# sourceMappingURL=index.js.map
@@ -76,8 +76,9 @@ export interface AgeFilter {
76
76
  * Every axis is optional; an absent axis imposes no constraint. `priority`/`effort`/`type`/`tags` are
77
77
  * all "the record has a label whose name is in the set" -- the UI groups labels into these ladders
78
78
  * (from `labels.json`, #58), but for a plain value the predicate treats all four identically: a plain
79
- * label-name test. A token (#464a) adds `@any` and `@upto:<rung>` on the ladder axes, resolved against
80
- * `MatchContext.ladders`; `tags` has no ladder, so a token there matches nothing.
79
+ * label-name test. A token (#464a) adds `@any` (any ladder axis) and `@upto:<rung>` (priority and effort
80
+ * only; it matches nothing on `type`), resolved against `MatchContext.ladders`; `tags` has no ladder, so
81
+ * a token there matches nothing.
81
82
  */
82
83
  export interface IssueFilter {
83
84
  priority?: SetFilter;
@@ -119,4 +120,3 @@ export interface SavedView {
119
120
  * exactly as it always did, while a token that needs `ctx` matches nothing.
120
121
  */
121
122
  export declare function matchesIssue(record: IssueRecord, filter: IssueFilter, now: number, ctx?: MatchContext): boolean;
122
- //# sourceMappingURL=issue-filter.d.ts.map
@@ -149,4 +149,3 @@ export function matchesIssue(record, filter, now, ctx) {
149
149
  matchesAge(ageBucket(record.updatedAt, now), filter.activity) &&
150
150
  matchesSearch(record, filter.search));
151
151
  }
152
- //# sourceMappingURL=issue-filter.js.map
@@ -103,6 +103,14 @@ export interface ActionContribution {
103
103
  * asks for a second click. */
104
104
  confirm?: boolean;
105
105
  danger?: boolean;
106
+ /**
107
+ * With the admin kill switch off (`admin_enabled = false`), the host refuses this action (`POST /api/actions/<plugin>/<id>`) with
108
+ * `403 admin disabled`, as it refuses the admin plugin's own surface. Mark an action or route an operator
109
+ * would expect the switch to shut: one that runs a job the job-run route refuses, writes to GitHub or the
110
+ * store on the operator's behalf, or changes the console's configuration. Additive; a host released before
111
+ * this field refuses a manifest that declares it (#867).
112
+ */
113
+ operator?: true;
106
114
  /** The form the shell builds the action's modal from; omit for a no-input action. */
107
115
  fields?: ActionField[];
108
116
  }
@@ -149,6 +157,14 @@ export interface RouteContribution {
149
157
  method: HttpMethod;
150
158
  /** Path under the plugin's namespace, e.g. "/feed". */
151
159
  path: string;
160
+ /**
161
+ * With the admin kill switch off (`admin_enabled = false`), the host refuses this route for its declared method with
162
+ * `403 admin disabled`, as it refuses the admin plugin's own surface. Mark an action or route an operator
163
+ * would expect the switch to shut: one that runs a job the job-run route refuses, writes to GitHub or the
164
+ * store on the operator's behalf, or changes the console's configuration. Additive; a host released before
165
+ * this field refuses a manifest that declares it (#867).
166
+ */
167
+ operator?: true;
152
168
  }
153
169
  /** JSON scalar type of a plugin setting (rendered by Admin). */
154
170
  export type SettingType = "string" | "integer" | "number" | "boolean";
@@ -253,4 +269,3 @@ export interface PluginStateEntry {
253
269
  state: "healthy" | "degraded";
254
270
  };
255
271
  }
256
- //# sourceMappingURL=manifest.d.ts.map
package/dist/manifest.js CHANGED
@@ -6,4 +6,3 @@
6
6
  * (`hostApiVersion`, `env[]`).
7
7
  */
8
8
  export {};
9
- //# sourceMappingURL=manifest.js.map
@@ -8,6 +8,7 @@
8
8
  * to round-trip. RENAMING an `id` here orphans every stored layout that references it, the same as
9
9
  * 1.x's own comment warns; adding/reordering rows is safe.
10
10
  */
11
+ /** MAY GROW: keep a default branch. */
11
12
  export type OverviewOwner = "overview" | "metrics-claude" | "repos" | "artifacts" | "provider-github";
12
13
  export interface OverviewWidget {
13
14
  id: string;
@@ -18,4 +19,3 @@ export interface OverviewWidget {
18
19
  export declare const OV_WIDGETS: readonly OverviewWidget[];
19
20
  /** The registry id `HomeCards` actually renders for one `OV_WIDGETS` row: `plugin:<owner>:<id>`. */
20
21
  export declare function ovRegistryId(widget: OverviewWidget): string;
21
- //# sourceMappingURL=ov-widgets.d.ts.map
@@ -30,4 +30,3 @@ export const OV_WIDGETS = [
30
30
  export function ovRegistryId(widget) {
31
31
  return `plugin:${widget.owner}:${widget.id}`;
32
32
  }
33
- //# sourceMappingURL=ov-widgets.js.map
package/dist/slots.d.ts CHANGED
@@ -214,4 +214,3 @@ export interface NotifyProvider {
214
214
  signal?: AbortSignal;
215
215
  }): Promise<void>;
216
216
  }
217
- //# sourceMappingURL=slots.d.ts.map
package/dist/slots.js CHANGED
@@ -14,4 +14,3 @@
14
14
  * built later in E5), so completing the v1 shape here is not a breaking change.
15
15
  */
16
16
  export {};
17
- //# sourceMappingURL=slots.js.map
package/dist/version.d.ts CHANGED
@@ -28,4 +28,3 @@ export declare const SLOTS: {
28
28
  readonly version: 1;
29
29
  };
30
30
  };
31
- //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -16,4 +16,3 @@ export const SLOTS = {
16
16
  auth: { name: "auth", version: 1 },
17
17
  notify: { name: "notify", version: 1 },
18
18
  };
19
- //# sourceMappingURL=version.js.map
package/dist/views.d.ts CHANGED
@@ -82,6 +82,10 @@ export type PluginOrigin = "bundled" | "installed";
82
82
  export interface PluginView {
83
83
  id: string;
84
84
  enabled: boolean;
85
+ /** The enable state the next restart applies, when it differs from `enabled` (#767): an operator's persisted
86
+ * choice (`POST /api/plugins/:id/enabled`) not yet applied, with `AC_PLUGINS_FORCE_ENABLED` winning as it does
87
+ * at boot. Absent when the two agree. Additive. */
88
+ pendingEnabled?: boolean;
85
89
  /** Required even on an errored entry — it is a property of where the plugin came from, not of loading. */
86
90
  origin: PluginOrigin;
87
91
  error?: string;
@@ -142,7 +146,8 @@ export interface DeclarativeCardDef {
142
146
  }
143
147
  /** What started a task: a manually-triggered job (#31), an agent run (E4), or a sidecar run (E4/E6). */
144
148
  export type TaskKind = "job" | "agent" | "sidecar";
145
- /** `queued` (#51): an agent-job task waiting on an unreachable machine, badged red in the drawer. */
149
+ /** `queued` (#51): an agent-job task waiting on an unreachable machine, badged red in the drawer.
150
+ * MAY GROW: keep a default branch. */
146
151
  export type TaskStatus = "running" | "done" | "failed" | "queued";
147
152
  /**
148
153
  * The `GET /api/tasks` list item (#42) — the host's in-memory task registry's view of one run. Not a
@@ -180,9 +185,11 @@ export interface JobMachineRun {
180
185
  * plugin every capability the job declares (#843; reason `not granted: <capability>[, ...]`, the
181
186
  * missing ones in declared order), or its paired agent version compares lower than the job's
182
187
  * declared `minAgentVersion` (#372, #80; reason `agent <version> < <min>`). A missing grant wins
183
- * when both hold. Computed live every call from the machine's CURRENT grants and version, never
184
- * persisted, so it always reflects "would a dispatch right now skip this machine", not a stale
185
- * historical fact. Independent of `lastRun`/`queued`. Either can be set from before the skip
188
+ * when both hold. Computed on every read, never persisted: the grant half from the machine's
189
+ * CURRENT grants, the version half from the LAST RECORDED agent version (what the last probe, ping,
190
+ * preflight or update poll stored). A dispatch re-probes the agent and judges the floor on the version
191
+ * that probe just reported, so after an upgrade or downgrade this field can differ from what a dispatch
192
+ * right now would do until the stored version is refreshed. Independent of `lastRun`/`queued`. Either can be set from before the skip
186
193
  * applied, and a `queued` row with a version cause can also be written WHILE the skip applies: an
187
194
  * unreachable machine is queued before any floor is judged, since the floor is judged only on a
188
195
  * version a probe just reported. A grant skip is judged before the unreachable branch, so it never
@@ -209,7 +216,8 @@ export interface DashLayout {
209
216
  /**
210
217
  * `GET /api/agents` (#48): one paired machine, as the console's `machines` table sees it. Never
211
218
  * carries the bearer token the console sends it (`packages/host/src/agents/store.ts`'s `list`/`get`
212
- * don't select it) -- the row is safe to render straight into the Agents panel.
219
+ * read the whole row, but `toView` is the allow-list that builds this shape and leaves the token out)
220
+ * -- the row is safe to render straight into the Agents panel.
213
221
  */
214
222
  export interface MachineView {
215
223
  id: string;
@@ -237,7 +245,10 @@ export interface MachineView {
237
245
  * The outcome of `HostApi.agent.call` (#50). `kind` tells a plugin which refusals are its own to
238
246
  * show verbatim (`grant`: the agent's 403 text) from ones that are the console's problem to
239
247
  * describe differently (`unknown-machine`; `refused`: not a verb the service will forward; `pin`:
240
- * the agent's certificate no longer matches; `network`; `http`: an unexpected status).
248
+ * the agent's certificate no longer matches; `network`; `http`: an unexpected status). `ok: true` means
249
+ * the agent answered a 2xx and its body was read (#778): a 2xx whose body could not be read (a timeout
250
+ * or reset mid-body, a body that is not JSON) is `ok: false` with `kind: "http"` and that 2xx `status`,
251
+ * since the agent may have acted on the call. A `204` is `ok: true` with `body` undefined.
241
252
  */
242
253
  export type AgentCallOutcome<T> = {
243
254
  ok: true;
@@ -246,6 +257,7 @@ export type AgentCallOutcome<T> = {
246
257
  } | {
247
258
  ok: false;
248
259
  error: {
260
+ /** MAY GROW: a new kind is an additive change, so keep a default branch. */
249
261
  kind: "unknown-machine" | "refused" | "grant" | "pin" | "network" | "http";
250
262
  status?: number;
251
263
  reason: string;
@@ -260,7 +272,7 @@ export type AgentCallOutcome<T> = {
260
272
  * reinstall. `unauthenticated` is `gh`-only: present and running, but `gh auth status` failed.
261
273
  * Independently defined here from the agent's own (identically-shaped) internal type -- the host
262
274
  * never depends on `@rackbops/ac-agent` at runtime, only as a devDependency for its integration
263
- * tests, matching `grants.ts`'s own id/capability pattern.
275
+ * tests, matching `grants.ts`'s own id/capability pattern. MAY GROW: keep a default branch.
264
276
  */
265
277
  export type PreflightStatus = "ok" | "missing" | "broken" | "off-path" | "unauthenticated";
266
278
  export interface PreflightRow {
@@ -279,4 +291,3 @@ export interface PreflightReport {
279
291
  searchPath: "registry" | "process";
280
292
  rows: PreflightRow[];
281
293
  }
282
- //# sourceMappingURL=views.d.ts.map
package/dist/views.js CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=views.js.map
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@rackbops/ac-plugin-contract",
3
- "version": "2.0.0-alpha.58",
3
+ "version": "2.0.0-alpha.60",
4
4
  "private": false,
5
+ "license": "Apache-2.0",
5
6
  "type": "module",
6
7
  "description": "The artifact-console plugin ABI for out-of-repo plugin authors: HostApi, the acPlugin manifest contribution points, the generated manifest JSON Schema, and the @ac/host ambient browser declaration.",
7
8
  "repository": {
package/schema.json CHANGED
@@ -235,6 +235,11 @@
235
235
  "danger": {
236
236
  "type": "boolean"
237
237
  },
238
+ "operator": {
239
+ "type": "boolean",
240
+ "const": true,
241
+ "description": "With the admin kill switch off (`admin_enabled = false`), the host refuses this action (`POST /api/actions/<plugin>/<id>`) with `403 admin disabled`, as it refuses the admin plugin's own surface. Mark an action or route an operator would expect the switch to shut: one that runs a job the job-run route refuses, writes to GitHub or the store on the operator's behalf, or changes the console's configuration. Additive; a host released before this field refuses a manifest that declares it (#867)."
242
+ },
238
243
  "fields": {
239
244
  "type": "array",
240
245
  "items": {
@@ -394,6 +399,11 @@
394
399
  "path": {
395
400
  "type": "string",
396
401
  "description": "Path under the plugin's namespace, e.g. \"/feed\"."
402
+ },
403
+ "operator": {
404
+ "type": "boolean",
405
+ "const": true,
406
+ "description": "With the admin kill switch off (`admin_enabled = false`), the host refuses this route for its declared method with `403 admin disabled`, as it refuses the admin plugin's own surface. Mark an action or route an operator would expect the switch to shut: one that runs a job the job-run route refuses, writes to GitHub or the store on the operator's behalf, or changes the console's configuration. Additive; a host released before this field refuses a manifest that declares it (#867)."
397
407
  }
398
408
  },
399
409
  "required": [