@rackbops/ac-plugin-contract 2.0.0-alpha.56 → 2.0.0-alpha.58
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 +108 -11
- package/dist/attention.d.ts +15 -2
- package/dist/host-api.d.ts +88 -26
- package/dist/manifest.d.ts +35 -8
- package/dist/slots.d.ts +20 -3
- package/dist/views.d.ts +21 -11
- package/package.json +1 -1
- package/schema.json +19 -10
package/AUTHORING.md
CHANGED
|
@@ -121,7 +121,8 @@ Rules the host enforces, at validation or load time:
|
|
|
121
121
|
declares. Every other slot, `notify@1` included, is open to an installed plugin.
|
|
122
122
|
- **`hostApiVersion`** must equal the host's `HOST_API_VERSION` (currently `1`); a mismatch is rejected.
|
|
123
123
|
- **`kind: "in-process"`** must declare a `server` entry — the *loader* records an error and skips a plugin that
|
|
124
|
-
doesn't
|
|
124
|
+
doesn't, and the install gate (`ac plugins install`, the Admin Plugins card) refuses to stage one (the schema
|
|
125
|
+
itself requires only `id`, `hostApiVersion` and `kind`). `contributes` and every sub-field
|
|
125
126
|
is optional in the schema — contribute only the points you use; each contribution's own required fields are
|
|
126
127
|
schema-enforced.
|
|
127
128
|
- **Jobs** (#137, #580): a job's **`timeoutSec`** must be at least `1` and at most `2147483` (the schema's
|
|
@@ -170,6 +171,13 @@ export const createPlugin: CreatePlugin = (host) => ({
|
|
|
170
171
|
})
|
|
171
172
|
```
|
|
172
173
|
|
|
174
|
+
When loading fails, the plugin is errored and unwound (#785). A `createPlugin` that throws, or that returns
|
|
175
|
+
anything but an object with an `activate()` method, and an `activate()` that throws or has not settled within
|
|
176
|
+
30 seconds (as does an import whose top-level `await` has not), record the plugin's error in `GET /api/plugins`.
|
|
177
|
+
Everything the plugin had already registered is then undone: its routes answer 404, its actions and event
|
|
178
|
+
subscriptions are dropped, and a slot it provided is released (except `auth@1`, which stays bound so the host
|
|
179
|
+
is never left open). Register last whatever must not exist when a later step fails.
|
|
180
|
+
|
|
173
181
|
- **Route** — `host.routes.register(method, path, handler)`. The handler is a web-standard
|
|
174
182
|
`(request: Request) => Response | Promise<Response>`, mounted at `/api/x/<id><path>`. You may only register a
|
|
175
183
|
`(method, path)` you declared in `contributes.routes` — an undeclared route throws. **Never answer `502` or
|
|
@@ -185,6 +193,15 @@ export const createPlugin: CreatePlugin = (host) => ({
|
|
|
185
193
|
can act on any status from 400 up — so the check on the deployed console is what proves it. Otherwise a 4xx or
|
|
186
194
|
`500`. `packages/host/src/no-gateway-status.test.ts` scans every bundled plugin's `src/` and the host's, and
|
|
187
195
|
fails naming the file and line (an installed third-party plugin is not covered).
|
|
196
|
+
The host sends your `Response` as you built it, with one exception (#831): an HTML document (a `Content-Type`
|
|
197
|
+
of `text/html` or `application/xhtml+xml`) that states no framing policy of its own gets the console pages'
|
|
198
|
+
default, `X-Frame-Options: SAMEORIGIN` and a `Content-Security-Policy: frame-ancestors 'self'` policy, so no
|
|
199
|
+
other site can frame it. If you already send a `Content-Security-Policy` without a `frame-ancestors` directive,
|
|
200
|
+
yours is kept exactly as it is and the framing policy is added beside it as a second policy (the browser
|
|
201
|
+
enforces each). A route that wants a different framing policy states it, with a `frame-ancestors` directive in
|
|
202
|
+
its `Content-Security-Policy` or with its own `X-Frame-Options`, and the host then leaves the response exactly
|
|
203
|
+
as it is, as it does `provider-artifacts`' `GET /render/*` and its stricter policy (#700). Any other content
|
|
204
|
+
type is never touched.
|
|
188
205
|
- **Action** — `host.actions.register({ id, run })`. The button, `confirm`/`danger` and the modal `fields` all
|
|
189
206
|
come from the manifest; here you supply only `run(input)`. (The action's manifest metadata drives the shell
|
|
190
207
|
UI; registering a `run` for an id the manifest omits simply leaves it without a button.) Opening the modal is
|
|
@@ -224,6 +241,17 @@ export const createPlugin: CreatePlugin = (host) => ({
|
|
|
224
241
|
success, keeping a separate `.../error` key for a failure and throwing the agent's reason verbatim
|
|
225
242
|
so the run is honestly recorded failed -- store, log, throw, in that order, never masking the
|
|
226
243
|
agent's text with a storage error of your own.
|
|
244
|
+
An agent job that calls a verb declares the capabilities those verbs need in the job's own
|
|
245
|
+
`capabilities` (#843; each must also be in the plugin's top-level `capabilities` and match the
|
|
246
|
+
capability grammar `^agent(\.[a-z][a-z0-9-]*)+$`, or the manifest is refused). The host then never dispatches the job to a paired machine that has not granted the plugin
|
|
247
|
+
every one of them: that machine is still probed, but it is skipped, with no run recorded, no task, no
|
|
248
|
+
`job.finished` event and no failure notified. Its row on the Admin Scheduled tasks card reads
|
|
249
|
+
`skipped: not granted: <capability>`; the Overview sched card leaves it out of the job's badge.
|
|
250
|
+
Absent or empty means every paired machine, as before the field existed, which is right for a job
|
|
251
|
+
that calls no verb (`hello.machines`). A host released before #843 refuses a manifest that declares
|
|
252
|
+
the field, with `manifest schema: /contributes/jobs/<n> must NOT have additional properties`, and that
|
|
253
|
+
refuses the whole plugin, not just the job: declare it only when your plugin requires a host that
|
|
254
|
+
carries it. See §7.
|
|
227
255
|
- **Setting** — read/write with `host.settings.get(key)` / `host.settings.set(key, value)`. Both are async;
|
|
228
256
|
`get` returns the manifest `default` when unset.
|
|
229
257
|
- **Console settings** (#506) — `host.consoleSettings?.get(key)`, read-only, optional. Unlike `Setting`
|
|
@@ -853,11 +881,51 @@ others are the console's problem to describe differently: `unknown-machine` (a s
|
|
|
853
881
|
reachable this way, whatever the path spelling), `pin` (the agent's certificate changed since
|
|
854
882
|
pairing), `network`, `http` (an unexpected status the console doesn't special-case).
|
|
855
883
|
|
|
884
|
+
An agent job declares the capabilities its own verbs need, in the job's `capabilities` (#843). Each one
|
|
885
|
+
must also be in the top-level list and match the capability grammar (`agent.` and lowercase segments).
|
|
886
|
+
The host checks them against the machine's grants before it dispatches the job, so a machine that has not
|
|
887
|
+
granted them is skipped rather than refused: no failed run, no `job failed` notification, and the Admin
|
|
888
|
+
Scheduled tasks card reads `skipped: not granted: <capability>` for that machine (the Overview sched card
|
|
889
|
+
leaves the machine out of the job's badge). `plugins/scans` is the example: each of its four jobs calls
|
|
890
|
+
one verb, `POST /v1/scan/<kind>`, and declares exactly the capability that verb needs. A host released
|
|
891
|
+
before #843 refuses a manifest that declares the field, with
|
|
892
|
+
`manifest schema: /contributes/jobs/<n> must NOT have additional properties`, which refuses the whole
|
|
893
|
+
plugin.
|
|
894
|
+
|
|
895
|
+
```json
|
|
896
|
+
"capabilities": ["agent.scan.context-pressure", "agent.scan.subagent-cost"],
|
|
897
|
+
"contributes": {
|
|
898
|
+
"jobs": [
|
|
899
|
+
{
|
|
900
|
+
"id": "scan.context-pressure",
|
|
901
|
+
"schedule": "40 3 * * *",
|
|
902
|
+
"timeoutSec": 990,
|
|
903
|
+
"runOn": "agent",
|
|
904
|
+
"capabilities": ["agent.scan.context-pressure"]
|
|
905
|
+
}
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
List every capability any of the job's verbs needs (a job that reads with `agent.git.read` and pulls with
|
|
911
|
+
`agent.git.pull` lists both). A job that lists too few is dispatched to a machine that then refuses the
|
|
912
|
+
verb, which is the failure this field exists to prevent. A `grant` refusal can still reach your `runJob`
|
|
913
|
+
when a grant is revoked while the run is in flight, so keep handling it.
|
|
914
|
+
|
|
856
915
|
A verb that can genuinely run long (a scan, a build) needs a fifth, optional `call` argument:
|
|
857
916
|
`host.agent.call(machineId, method, path, body, { timeoutMs })` sets how long the console waits for
|
|
858
917
|
the agent's response *headers* (undici's `headersTimeout`; the default is 300s). Pass the verb's own
|
|
859
918
|
limit plus a margin -- `scans` passes `agent limit + 30s` -- so the agent's own `504` reaches the
|
|
860
|
-
console before the console gives up and reports a bare `network` timeout instead.
|
|
919
|
+
console before the console gives up and reports a bare `network` timeout instead. From a job, also
|
|
920
|
+
pass the run's signal, `{ signal: ctx.signal }` (#701): when the run is timed out the request is
|
|
921
|
+
cancelled, and `call` resolves `{ ok: false, error: { kind: "network", reason } }` instead of waiting
|
|
922
|
+
on the agent. The same goes for GitHub calls through `host.github.octokit()`: pass
|
|
923
|
+
`request: { signal: ctx.signal }` to each call, but not to `octokit.paginate`, which hands its pages
|
|
924
|
+
no `request` options; bind the signal to the route instead,
|
|
925
|
+
`octokit.paginate(route.defaults({ request: { signal: ctx.signal } }), params)` (#701). Unlike
|
|
926
|
+
`agent.call`, which resolves `{ ok: false }`, an aborted Octokit call rejects (the `AbortError`, with
|
|
927
|
+
`status: 500` set on it), and an abort that lands while the body is read resolves with an empty
|
|
928
|
+
`data`, so check `ctx.signal.aborted` after the call too.
|
|
861
929
|
|
|
862
930
|
### Streaming a job into the tasks drawer
|
|
863
931
|
|
|
@@ -868,20 +936,41 @@ job's stream is `GET /v1/toolchain/upgrade/:jobId/events`, under its own capabil
|
|
|
868
936
|
reaches through its optional `path`: `{ label, path: "/v1/toolchain/upgrade/<jobId>/events" }`, a route
|
|
869
937
|
of the shape `/v1/<segments>/<jobId>/events` naming that same `jobId`, anything else rejecting) through the same
|
|
870
938
|
pinned client, registers a new host task for
|
|
871
|
-
`label`, and relays every `line`/`done` frame into it as the job runs; it resolves
|
|
872
|
-
the agent has confirmed the stream is open (a `200`), or rejects
|
|
873
|
-
else -- an invalid `jobId`, an unknown machine, or a non-`200` from
|
|
874
|
-
refusal, ...), each surfaced the same way `call` surfaces them. The relay
|
|
875
|
-
that promise, so nothing in a plugin's own code touches the stream at all
|
|
939
|
+
`label`, and relays every `line`/`done` frame into it as the job runs; it resolves
|
|
940
|
+
`{ taskId, finished }` once the agent has confirmed the stream is open (a `200`), or rejects
|
|
941
|
+
(starting no task) on anything else -- an invalid `jobId`, an unknown machine, or a non-`200` from
|
|
942
|
+
the agent (a `404`, a `403` refusal, ...), each surfaced the same way `call` surfaces them. The relay
|
|
943
|
+
itself runs detached from that promise, so nothing in a plugin's own code touches the stream at all.
|
|
944
|
+
It resolves usually while the job is still running (a job that fails fast can end before its stream
|
|
945
|
+
is opened, and the agent then replays its `done` at once), so it says the job started, not that it
|
|
946
|
+
succeeded. `finished` (#733) is the job's end. It is **optional**: the first host release that
|
|
947
|
+
contains #733 (after 2.0.0-alpha.56) supplies it; 2.0.0-alpha.56 and older do not, so a consumer must
|
|
948
|
+
handle `undefined`. It settles with the end the relay finished the task with, the one the tasks
|
|
949
|
+
drawer shows: `{ status: "done" }` only for the agent's `done` frame with `status: "done"`, and
|
|
950
|
+
`{ status: "failed", reason }` for every other end. `reason` is `job ended with status failed` (or
|
|
951
|
+
`job ended with an unknown status`) when the agent said the job failed, and
|
|
952
|
+
`stream ended before done: <message>`, the drawer's last line without its brackets, when the stream
|
|
953
|
+
closed or errored first or the `done` frame did not parse, so the job's own failure and a lost stream
|
|
954
|
+
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.
|
|
876
959
|
|
|
877
960
|
```ts
|
|
878
961
|
const { jobId } = await execResult // however the plugin's own verb call started the job
|
|
879
|
-
const { taskId } = await host.agent.streamJob(machineId, jobId, { label: `code: ${prompt}` })
|
|
962
|
+
const { taskId, finished } = await host.agent.streamJob(machineId, jobId, { label: `code: ${prompt}` })
|
|
880
963
|
// taskId names the task now streaming in the tasks drawer -- the action can simply return it.
|
|
964
|
+
// Don't await `finished` in the action: the modal would wait for the whole job. It is absent on an
|
|
965
|
+
// older host, and a throw in the handler must not become an unhandled rejection.
|
|
966
|
+
void finished
|
|
967
|
+
?.then((end) => host.events.emit("my.run", { ...end, taskId }))
|
|
968
|
+
.catch((err) => host.log.warn(`my.run end not emitted: ${String(err)}`))
|
|
881
969
|
```
|
|
882
970
|
|
|
883
971
|
`plugins/claude-runner` (#54) is the reference: `task.code` and `routine.rerun` call `exec/claude`
|
|
884
|
-
then `streamJob` and return the task id, never touching SSE framing themselves
|
|
972
|
+
then `streamJob` and return the task id, never touching SSE framing themselves, and emit
|
|
973
|
+
`claude.run` `done` or `failed` from `finished` (#733). The `agents` plugin's
|
|
885
974
|
`toolchain.upgrade` action (#675) is the example of a verb with its own events route: it starts the
|
|
886
975
|
job with `host.agent.call`, remembers the job id per machine (the confirm modal's settled event carries no
|
|
887
976
|
result, so a panel cannot read it from there) and relays the stream with `path`.
|
|
@@ -901,13 +990,21 @@ interface AttentionItem {
|
|
|
901
990
|
source: string // your own plugin id -- the rollup shows it as the row's source
|
|
902
991
|
text: string // always a counted sentence: "3 open actions in the ledger", never "some actions"
|
|
903
992
|
badge?: string
|
|
904
|
-
href?: string // an external link; wins over `target` when both are set
|
|
993
|
+
href?: string // an external link; wins over `target` when both are set. Linked only
|
|
994
|
+
// when its scheme is http:, https:, mailto: or tel: (#722); any other
|
|
995
|
+
// href is not linked and `target` is used instead
|
|
905
996
|
target?: string // an in-app destination: "widget:<cardId>" or "panel:<pluginId>"
|
|
906
997
|
hint?: string // the exact command to run, or the reason -- shown as a tooltip/expander
|
|
907
998
|
action?: { kind: "issue"; repo?: string; title: string; body: string } // opens provider-github's issue.create, seeded
|
|
908
999
|
}
|
|
909
1000
|
```
|
|
910
1001
|
|
|
1002
|
+
The rollup is lenient with a row it cannot use as typed (#722): it drops a row only when the row is not
|
|
1003
|
+
an object or its `id` or `text` is not a string. Otherwise it normalises: a missing or non-string
|
|
1004
|
+
`source` becomes your plugin id, a `severity` other than the three ranks and renders as `"info"`, a
|
|
1005
|
+
number `badge` or `hint` becomes its string, and an object or array one is left out (the row stays).
|
|
1006
|
+
A `target` that is not a string is ignored.
|
|
1007
|
+
|
|
911
1008
|
**Own the fact, own the row.** The rollup itself derives nothing -- it only merges what contributors
|
|
912
1009
|
already computed. If your plugin is the one that KNOWS a fact is wrong (a failed job, a stale scan, a
|
|
913
1010
|
stuck push), your plugin is the one that turns it into a row, at the severity and with the hint that
|
|
@@ -1033,7 +1130,7 @@ what it's for.
|
|
|
1033
1130
|
| Panel | `panels[]` (`id`, `title`, `route`, `export`, `nav?`, `order?`, `isolation?`) | a component export in `ui.js` |
|
|
1034
1131
|
| Card | `cards[]` (`id`, `title`, `sizes`, `export`) | a component export in `ui.js` |
|
|
1035
1132
|
| Action | `actions[]` (`id`, `scope`, `title?`, `confirm?`, `danger?`, `fields?`) | `host.actions.register({ id, run })` |
|
|
1036
|
-
| Job | `jobs[]` (`id`, `schedule`, `timeoutSec`, `runOn`) | `runJob(ctx)` dispatching on `ctx.jobId` |
|
|
1133
|
+
| Job | `jobs[]` (`id`, `schedule`, `timeoutSec`, `runOn`, `minAgentVersion?`, `capabilities?`) | `runJob(ctx)` dispatching on `ctx.jobId` |
|
|
1037
1134
|
| Route | `routes[]` (`method`, `path`) | `host.routes.register(method, path, handler)` |
|
|
1038
1135
|
| Setting | `settings[]` (`key`, `type`, `default?`) | read via `host.settings.get/set` |
|
|
1039
1136
|
| Attention | `attention: true` | `host.routes.register("GET", "/attention", handler)` -> `{ ok: true, items }` |
|
package/dist/attention.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export interface AttentionAction {
|
|
|
18
18
|
title: string;
|
|
19
19
|
body: string;
|
|
20
20
|
}
|
|
21
|
+
/** One needs-attention row. The rollup card (`plugins/overview`) is lenient with a contributor's
|
|
22
|
+
* JSON (#722): it drops a row only when the row is not an object or its `id` or `text` is not a
|
|
23
|
+
* string. Otherwise it normalises: a `source` that is missing or not a string becomes the id of the
|
|
24
|
+
* plugin the row was fetched from; a `severity` other than the three ranks and renders as `"info"`;
|
|
25
|
+
* a number `badge` or `hint` becomes its string, and an object or array one is left out (the row
|
|
26
|
+
* is kept). */
|
|
21
27
|
export interface AttentionItem {
|
|
22
28
|
/** Unique within the contributing plugin; the rollup's dedupe key is `${source}:${id}`. */
|
|
23
29
|
id: string;
|
|
@@ -27,15 +33,22 @@ export interface AttentionItem {
|
|
|
27
33
|
/** Always a counted sentence (1.x's `attCount`, e.g. "1 scheduled job run failed"). */
|
|
28
34
|
text: string;
|
|
29
35
|
badge?: string;
|
|
30
|
-
/** An external link; wins over `target` when both are present (1.x's own precedence).
|
|
36
|
+
/** An external link; wins over `target` when both are present (1.x's own precedence). Resolved
|
|
37
|
+
* against the console's page, it is linked only when its scheme is `http:`, `https:`, `mailto:`
|
|
38
|
+
* or `tel:` (#722): any other `href` (`javascript:`, `data:`, `blob:`, `vbscript:`, `file:`, a
|
|
39
|
+
* custom scheme, one that does not parse) renders no link, and the row uses `target` instead, as
|
|
40
|
+
* if `href` were absent. */
|
|
31
41
|
href?: string;
|
|
32
42
|
/** An in-app destination: `widget:<cardId>` flashes/scrolls to a sibling card on Overview,
|
|
33
|
-
* `panel:<pluginId>` navigates to that plugin's panel
|
|
43
|
+
* `panel:<pluginId>` navigates to that plugin's panel (its first panel route, or `/<pluginId>`
|
|
44
|
+
* when the console knows none). A value that is not a string is ignored. */
|
|
34
45
|
target?: string;
|
|
35
46
|
/** The exact command to run, or the reason, shown as a tooltip/expander (1.x's `hint`). */
|
|
36
47
|
hint?: string;
|
|
37
48
|
action?: AttentionAction;
|
|
38
49
|
}
|
|
50
|
+
/** `GET /api/x/<id>/attention`'s answer. Each of `items` is read as `AttentionItem` describes,
|
|
51
|
+
* including which malformed rows the rollup drops and which it normalises (#722). */
|
|
39
52
|
export interface AttentionResponse {
|
|
40
53
|
ok: true;
|
|
41
54
|
items: AttentionItem[];
|
package/dist/host-api.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
import type { Octokit } from "@octokit/rest";
|
|
10
10
|
import type { HttpMethod } from "./manifest.js";
|
|
11
11
|
import type { AgentCallOutcome, JobMachineRun, MachineView } from "./views.js";
|
|
12
|
-
/** A typed, per-plugin settings accessor
|
|
12
|
+
/** A typed, per-plugin settings accessor: this plugin's own rows in the host's `plugin_settings` table, a
|
|
13
|
+
* key with no stored value reading as its manifest `default` (`contributes.settings[]`). Not the
|
|
14
|
+
* host-global settings, which `consoleSettings` reads. */
|
|
13
15
|
export interface SettingsService {
|
|
14
16
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
15
17
|
set(key: string, value: unknown): Promise<void>;
|
|
@@ -19,7 +21,9 @@ export interface SettingsService {
|
|
|
19
21
|
* distinct from `SettingsService` above, which is per-plugin. Only a key on the editable-settings
|
|
20
22
|
* whitelist (`settings-schema.ts`) is readable here; any other key resolves to `undefined`, so a
|
|
21
23
|
* plugin can never reach an arbitrary host-global key through this surface. The value returned is
|
|
22
|
-
* the EFFECTIVE one (an `AC_<KEY>` env override included), the same value the Settings card shows
|
|
24
|
+
* the EFFECTIVE one (an `AC_<KEY>` env override included), the same value the Settings card shows,
|
|
25
|
+
* except that a whitelisted key with no stored value and no env var resolves to `undefined` here
|
|
26
|
+
* while the card shows its declared default (an empty string), so treat the two alike.
|
|
23
27
|
*/
|
|
24
28
|
export interface ConsoleSettingsService {
|
|
25
29
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
@@ -33,7 +37,8 @@ export interface StorageService {
|
|
|
33
37
|
/** Register/trigger the plugin's contributed jobs and read their last result. */
|
|
34
38
|
export interface JobsService {
|
|
35
39
|
/** Run a contributed job now (the manual-trigger endpoint). For a `runOn: "agent"` job this resolves
|
|
36
|
-
* once every currently paired machine has
|
|
40
|
+
* once every currently paired machine has been run, queued, or skipped (a missing grant, #843, or a
|
|
41
|
+
* `minAgentVersion` floor). */
|
|
37
42
|
trigger(jobId: string): Promise<void>;
|
|
38
43
|
/** The last run of a job, if it has run — for an agent job, the newest recorded run across every
|
|
39
44
|
* machine. */
|
|
@@ -48,14 +53,14 @@ export interface JobsService {
|
|
|
48
53
|
* scoped to the calling plugin's own jobs; this is the needs-attention rollup's in-process read of
|
|
49
54
|
* every OTHER loaded plugin's job health, so `overview`'s `/attention` route never has to make an
|
|
50
55
|
* HTTP call back into its own host. Discloses nothing `GET /api/jobs/:pluginId/:jobId/runs`
|
|
51
|
-
* doesn't already serve publicly -- just a non-HTTP path to the same data. Optional:
|
|
52
|
-
* `
|
|
56
|
+
* doesn't already serve publicly -- just a non-HTTP path to the same data. Optional: `overview`'s
|
|
57
|
+
* rollup and `ac-notify-discord`'s digest call it. */
|
|
53
58
|
machineRunsOf?(pluginId: string, jobId: string): Promise<JobMachineRun[]>;
|
|
54
59
|
}
|
|
55
|
-
/** #78: every
|
|
56
|
-
* discover what to poll via `jobs.machineRunsOf` without a second
|
|
57
|
-
* self-loop. Host-internal discovery only (unlike `PluginView`, never served
|
|
58
|
-
* route); optional on `HostApi
|
|
60
|
+
/** #78: every running in-process plugin's own declared jobs -- lets the needs-attention rollup (and
|
|
61
|
+
* `ac-notify-discord`'s digest) discover what to poll via `jobs.machineRunsOf` without a second
|
|
62
|
+
* manifest fetch or an HTTP self-loop. Host-internal discovery only (unlike `PluginView`, never served
|
|
63
|
+
* over HTTP as its own route); optional on `HostApi`, since only those two use it. */
|
|
59
64
|
export interface PluginJobsService {
|
|
60
65
|
list(): {
|
|
61
66
|
pluginId: string;
|
|
@@ -70,19 +75,31 @@ export interface JobDeclaration {
|
|
|
70
75
|
}
|
|
71
76
|
/** The typed event bus (e.g. `issue.changed`, `job.finished`, `artifact.rendered`). */
|
|
72
77
|
export interface EventsService {
|
|
78
|
+
/** Calls every subscriber of `type` synchronously, in subscription order, and never awaits one.
|
|
79
|
+
* A subscriber that throws, or returns a promise that rejects, is logged by the host and stops
|
|
80
|
+
* neither the others nor the emitter (#728). */
|
|
73
81
|
emit(type: string, payload: unknown): void;
|
|
74
|
-
/** Subscribe; returns an unsubscribe function.
|
|
75
|
-
|
|
82
|
+
/** Subscribe; returns an unsubscribe function. The handler may be `async` (#728): when it
|
|
83
|
+
* returns a promise (or any thenable) the host attaches a rejection handler, so a rejection is
|
|
84
|
+
* logged, never unhandled; any other return value is ignored. The return type is `unknown`,
|
|
85
|
+
* not `void | Promise<void>`, because only `unknown` still admits every handler the earlier
|
|
86
|
+
* `=> void` did (`(p) => list.push(p)` returns a number). */
|
|
87
|
+
on(type: string, handler: (payload: unknown) => unknown): () => void;
|
|
76
88
|
}
|
|
77
89
|
/** One shared, rate-limit-aware GitHub client (Octokit; PAT from env, #127). Consumers make every
|
|
78
90
|
* GitHub call through `octokit()` rather than constructing a second client. */
|
|
79
91
|
export interface GithubService {
|
|
80
92
|
/** The authenticated login, cached. */
|
|
81
93
|
viewer(): Promise<string>;
|
|
82
|
-
/** The one shared Octokit REST client — consumers make all GitHub calls through this.
|
|
94
|
+
/** The one shared Octokit REST client — consumers make all GitHub calls through this. It
|
|
95
|
+
* authenticates with the console's PAT (`AC_GITHUB_TOKEN`), and `octokit().auth()` returns that
|
|
96
|
+
* token to whichever plugin asks: no `env[]` declaration is needed, and nothing hides it (#784;
|
|
97
|
+
* in-process plugins are trusted code, not sandboxed). */
|
|
83
98
|
octokit(): Octokit;
|
|
84
|
-
/** Remaining primary-rate-limit budget from the last response
|
|
85
|
-
*
|
|
99
|
+
/** Remaining primary-rate-limit budget from the last response whose three `x-ratelimit-*` headers
|
|
100
|
+
* are all finite numbers (and the reset a valid time), a failed response's included (#784: a 403 or
|
|
101
|
+
* 429 carrying `x-ratelimit-remaining: 0` reads as 0 here, not as the last successful count), or
|
|
102
|
+
* undefined before any such response. A response with a header missing or malformed changes nothing. */
|
|
86
103
|
rateLimit(): {
|
|
87
104
|
remaining: number;
|
|
88
105
|
limit: number;
|
|
@@ -107,27 +124,55 @@ export interface AgentService {
|
|
|
107
124
|
* 300 s). A long-running verb -- a scan -- passes the verb's own limit plus a margin, so the
|
|
108
125
|
* agent's own timeout answer arrives before the console gives up. */
|
|
109
126
|
timeoutMs?: number;
|
|
127
|
+
/** Abandons the call when it aborts (#701): a job passes its run's `ctx.signal`, so a run the
|
|
128
|
+
* host has timed out stops waiting on the agent. The request is cancelled on the wire and the
|
|
129
|
+
* call resolves `{ ok: false, error: { kind: "network", reason } }`, `reason` being the
|
|
130
|
+
* signal's `reason.message` when that reason is an `Error`, else `"aborted"`. */
|
|
131
|
+
signal?: AbortSignal;
|
|
110
132
|
}): Promise<AgentCallOutcome<T>>;
|
|
111
133
|
/**
|
|
112
134
|
* Open a paired machine's job event stream (`GET /v1/jobs/:jobId/events`, #54) and relay it into a
|
|
113
135
|
* new host task: every agent `line` frame becomes a `task.log`, the `done` frame finishes it --
|
|
114
136
|
* the tasks drawer streams the machine's job with no new UI (decision 12/20). Resolves with the new
|
|
115
|
-
* task's id once the stream is confirmed open (the agent answered `200`)
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
137
|
+
* task's id once the stream is confirmed open (the agent answered `200`), usually while the job is
|
|
138
|
+
* still running (a job that fails fast can end before its stream is opened, and the agent then
|
|
139
|
+
* replays its `done` at once); the relay itself then runs detached, and its failures never reject
|
|
140
|
+
* here (see `finished`). Rejects (starting no task) on an invalid `jobId`, an unknown machine, or any
|
|
141
|
+
* non-`200` response from the agent (a `404` unknown job, a `403` refusal, ...) -- the same failure
|
|
142
|
+
* shapes `call` throws for a bad path, surfaced the same way.
|
|
119
143
|
*
|
|
120
144
|
* `path` (#675) is the events route of a job that does not live under `/v1/jobs`: the toolchain
|
|
121
145
|
* upgrade's is `/v1/toolchain/upgrade/<jobId>/events`, readable only by a plugin holding that verb's own
|
|
122
146
|
* capability. Omitted, it is `/v1/jobs/<jobId>/events` exactly as before. It is not a free path: it must be
|
|
123
147
|
* `/v1/<one or more lowercase segments>/<jobId>/events` with this call's own `jobId`, and carry no query,
|
|
124
148
|
* fragment, escape or dot segment; anything else rejects (starting no task, making no request).
|
|
149
|
+
*
|
|
150
|
+
* `finished` (#733) says how the job ended. The first host release that contains #733 (after
|
|
151
|
+
* 2.0.0-alpha.56) supplies it; 2.0.0-alpha.56 and older do not, so a consumer must handle
|
|
152
|
+
* `undefined` (it is optional for that reason, and so an out-of-repo plugin's own test fake still
|
|
153
|
+
* compiles). It settles with the status the relay finished the task with, so it always agrees with
|
|
154
|
+
* the tasks drawer: `{ status: "done" }` for the agent's `done` frame carrying `status: "done"`, and
|
|
155
|
+
* `{ status: "failed", reason }` for any other end. `reason` tells the job's own failure apart from
|
|
156
|
+
* the console losing sight of it: `job ended with status failed` (or `job ended with an unknown
|
|
157
|
+
* status`) when the agent's `done` frame says so, and `stream ended before done: <message>`, the
|
|
158
|
+
* tasks drawer's last line without its brackets, when the stream closed or errored before its
|
|
159
|
+
* `done` frame, or that frame did not parse. It never rejects. It never settles when the relay sees
|
|
160
|
+
* 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.
|
|
125
164
|
*/
|
|
126
165
|
streamJob(machineId: string, jobId: string, opts: {
|
|
127
166
|
label: string;
|
|
128
167
|
path?: string;
|
|
129
168
|
}): Promise<{
|
|
130
169
|
taskId: string;
|
|
170
|
+
finished?: Promise<{
|
|
171
|
+
status: "done";
|
|
172
|
+
} | {
|
|
173
|
+
status: "failed";
|
|
174
|
+
reason: string;
|
|
175
|
+
}>;
|
|
131
176
|
}>;
|
|
132
177
|
}
|
|
133
178
|
/** Proxy to a sidecar plugin's HTTP surface (the host handles health + base URL). */
|
|
@@ -135,7 +180,8 @@ export interface SidecarService {
|
|
|
135
180
|
/** The base URL the host proxies to for a sidecar plugin. */
|
|
136
181
|
baseUrl(pluginId: string): string;
|
|
137
182
|
}
|
|
138
|
-
/** Register
|
|
183
|
+
/** Register an action's `run`; the shell builds its button + modal from the manifest data, and opening
|
|
184
|
+
* the modal is the action's one confirmation (#530). */
|
|
139
185
|
export interface ActionsService {
|
|
140
186
|
register(action: {
|
|
141
187
|
id: string;
|
|
@@ -193,13 +239,21 @@ export interface SlotRegistry {
|
|
|
193
239
|
* there for good (#97). */
|
|
194
240
|
resolve<T = unknown>(slot: string, version: number): T | null;
|
|
195
241
|
}
|
|
196
|
-
/** A logger prefixed with the plugin id.
|
|
242
|
+
/** A logger prefixed with the plugin id. A plain-object first arg is the line's structured fields; an
|
|
243
|
+
* `Error` first arg is logged as its own enumerable properties (`code`, `status`, ...) plus
|
|
244
|
+
* `{ error: <message>, stack }`; every other arg is kept, in order, under `args` (an `Error` there, or as a
|
|
245
|
+
* field's value, as its own properties plus `message` and `stack`); a field named `time`, `level`, `msg`
|
|
246
|
+
* or `plugin` is renamed `field_<key>`, so it cannot relabel the line. A call never throws: a value JSON
|
|
247
|
+
* cannot write (a `BigInt`, a circular structure, a throwing getter) is logged as a string (#786). */
|
|
197
248
|
export interface LogService {
|
|
198
249
|
info(message: string, ...args: unknown[]): void;
|
|
199
250
|
warn(message: string, ...args: unknown[]): void;
|
|
200
251
|
error(message: string, ...args: unknown[]): void;
|
|
201
252
|
}
|
|
202
|
-
/** Read declared env keys only (the manifest's `env[]`).
|
|
253
|
+
/** Read declared env keys only (the manifest's `env[]`). This scopes what a plugin reads; it is not a
|
|
254
|
+
* secrecy boundary (#784): the plugin writes its own `env[]`, nothing restricts which keys it declares
|
|
255
|
+
* (another plugin's, or a host-owned one like `AC_GITHUB_TOKEN`), and in-process plugin code can read
|
|
256
|
+
* `process.env` directly. */
|
|
203
257
|
export interface EnvService {
|
|
204
258
|
get(key: string): string | undefined;
|
|
205
259
|
}
|
|
@@ -226,7 +280,8 @@ export interface HostApi {
|
|
|
226
280
|
actions: ActionsService;
|
|
227
281
|
log: LogService;
|
|
228
282
|
env: EnvService;
|
|
229
|
-
/** #78: see `PluginJobsService`. Optional --
|
|
283
|
+
/** #78: see `PluginJobsService`. Optional -- `overview`'s rollup and `ac-notify-discord`'s digest use
|
|
284
|
+
* it. */
|
|
230
285
|
plugins?: PluginJobsService;
|
|
231
286
|
/** #84: see `SlotRegistry`. */
|
|
232
287
|
slots: SlotRegistry;
|
|
@@ -254,6 +309,11 @@ export interface JobRunContext {
|
|
|
254
309
|
}
|
|
255
310
|
/** A plugin instance. `createPlugin(host)` is pure; I/O only in `activate()` (design §4 lifecycle). */
|
|
256
311
|
export interface Plugin {
|
|
312
|
+
/** Register routes, actions, subscriptions and slot providers. A plugin whose `activate()` throws, or
|
|
313
|
+
* has not settled within the host's load deadline (30 s), is errored and everything it registered is
|
|
314
|
+
* undone: its routes answer 404, its actions and subscriptions are dropped, a slot it bound is
|
|
315
|
+
* released (except `auth`), #785. The same holds for a `createPlugin` that throws or returns no
|
|
316
|
+
* object with an `activate` method. */
|
|
257
317
|
activate(): void | Promise<void>;
|
|
258
318
|
/**
|
|
259
319
|
* Run one of the plugin's contributed jobs, dispatched by `ctx.jobId` (#31's scheduler). Optional —
|
|
@@ -323,10 +383,12 @@ export interface BrowserHostApi {
|
|
|
323
383
|
* rather than followed -- `credentials: "same-origin"` alone would not stop the request itself, or a
|
|
324
384
|
* write's `X-AC-CSRF` token, from reaching a third-party origin. Credentials are included
|
|
325
385
|
* (`same-origin`); a non-GET/HEAD/OPTIONS request carries the `X-AC-CSRF` token fetched once from
|
|
326
|
-
* `GET /api/config` (#85) and cached
|
|
327
|
-
*
|
|
328
|
-
* `/api/
|
|
329
|
-
* the
|
|
386
|
+
* `GET /api/config` (#85) and cached. The host requires that token only where its `requireCsrf`
|
|
387
|
+
* middleware is mounted (`POST /api/attachments`, `POST`/`DELETE /api/plugins/indexes`, the
|
|
388
|
+
* `/api/plugins/install/*` writes); every other write, a plugin's own `/api/x/<id>/…` routes
|
|
389
|
+
* included, has the Origin check #85 enforces on every mutating request as its CSRF defence.
|
|
390
|
+
* Panels read a provider's routes (e.g. `/api/x/provider-github/…`) through this rather than a
|
|
391
|
+
* bare `fetch`. Returns the raw `Response`; the caller parses it.
|
|
330
392
|
*/
|
|
331
393
|
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
332
394
|
/**
|
package/dist/manifest.d.ts
CHANGED
|
@@ -91,13 +91,16 @@ export interface ActionField {
|
|
|
91
91
|
*/
|
|
92
92
|
optionsFrom?: ActionOptionsSource;
|
|
93
93
|
}
|
|
94
|
-
/**
|
|
94
|
+
/** An action; the shell builds its button + modal from these fields (#34), and opening the modal is the
|
|
95
|
+
* action's one confirmation (#530). */
|
|
95
96
|
export interface ActionContribution {
|
|
96
97
|
id: string;
|
|
97
98
|
/** Button/label text; the shell falls back to `id` when omitted. */
|
|
98
99
|
title?: string;
|
|
99
100
|
/** What the action operates on, e.g. "issue" | "report" | "global". */
|
|
100
101
|
scope: string;
|
|
102
|
+
/** Accepted, but inert since #530: the modal is the one confirmation, so neither it nor the palette
|
|
103
|
+
* asks for a second click. */
|
|
101
104
|
confirm?: boolean;
|
|
102
105
|
danger?: boolean;
|
|
103
106
|
/** The form the shell builds the action's modal from; omit for a no-input action. */
|
|
@@ -108,7 +111,12 @@ export type JobRunOn = "host" | "agent";
|
|
|
108
111
|
/** A scheduled job. */
|
|
109
112
|
export interface JobContribution {
|
|
110
113
|
id: string;
|
|
111
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* 5-field cron expression (the host's `jobs/cron.ts` grammar). Not validated when the plugin loads
|
|
116
|
+
* (#784): a schedule that does not parse is logged at warn when the scheduler starts and the job is
|
|
117
|
+
* never scheduled, while the plugin itself loads as usual. Every bundled plugin's schedules are
|
|
118
|
+
* parsed in CI.
|
|
119
|
+
*/
|
|
112
120
|
schedule: string;
|
|
113
121
|
/**
|
|
114
122
|
* Per-run timeout in seconds. Must be at least 1: a zero or negative value would fire the timeout at
|
|
@@ -125,6 +133,14 @@ export interface JobContribution {
|
|
|
125
133
|
* `runOn: "host"` job. Additive; absent means every paired machine is always dispatched, as
|
|
126
134
|
* before this field existed. */
|
|
127
135
|
minAgentVersion?: string;
|
|
136
|
+
/** The host-agent capabilities this job's verbs need on a machine (#843). A `runOn: "agent"` job is
|
|
137
|
+
* not dispatched to a paired machine that has not granted this plugin every one of them. That
|
|
138
|
+
* machine's row records `skipped` with reason `not granted: <capability>[, ...]` instead: no run is
|
|
139
|
+
* recorded, no `job.finished` is emitted and no failure is notified. Each must be declared in the
|
|
140
|
+
* plugin's top-level `capabilities` (the host refuses the manifest otherwise). Ignored for a
|
|
141
|
+
* `runOn: "host"` job. Additive: absent or empty dispatches to every paired machine, as before this
|
|
142
|
+
* field existed. A host released before this field refuses a manifest that declares it. */
|
|
143
|
+
capabilities?: string[];
|
|
128
144
|
}
|
|
129
145
|
/** HTTP method for a contributed route. */
|
|
130
146
|
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
@@ -168,18 +184,27 @@ export interface Contributes {
|
|
|
168
184
|
/** #78: the plugin serves `GET /api/x/<id>/attention` -> `{ ok: true, items: AttentionItem[] }`
|
|
169
185
|
* (`attention.ts`). The `overview` plugin's rollup card discovers contributors via
|
|
170
186
|
* `PluginView.attention` (`views.ts`) rather than a second manifest read. Additive; always `true`
|
|
171
|
-
* when present (a boolean, not a route list, since the path is fixed).
|
|
187
|
+
* when present (a boolean, not a route list, since the path is fixed). The plugin must also declare
|
|
188
|
+
* that route in `routes` and register it; the host checks neither at load (#784; every bundled
|
|
189
|
+
* plugin's is checked in CI), and the rollup counts a contributor whose route fails as not loaded. */
|
|
172
190
|
attention?: true;
|
|
173
191
|
}
|
|
174
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* A declared environment variable (field names shared verbatim with the discord-bot index). Declaring
|
|
194
|
+
* a key is what lets `HostApi.env.get` read it. `format` and `required` are metadata only: the host
|
|
195
|
+
* checks a value against neither (#784), so a plugin that depends on one checks its own value.
|
|
196
|
+
*/
|
|
175
197
|
export interface EnvVar {
|
|
176
198
|
key: string;
|
|
177
|
-
/** Anchored regex the value
|
|
199
|
+
/** Anchored regex the value is expected to match, e.g. "^[0-9]+$". Not enforced by the host; every
|
|
200
|
+
* bundled plugin's compiles and is anchored (checked in CI). */
|
|
178
201
|
format?: string;
|
|
202
|
+
/** Whether the plugin needs a value. Not enforced by the host: a missing value reads `undefined`. */
|
|
179
203
|
required?: boolean;
|
|
180
204
|
secret?: boolean;
|
|
181
205
|
}
|
|
182
|
-
/** A sidecar plugin's image and health path
|
|
206
|
+
/** A sidecar plugin's image and health path, expected when `kind === "sidecar"`. The host reads only
|
|
207
|
+
* `healthPath` (its health poll) and never `image` (the compose file names the sidecar's image). */
|
|
183
208
|
export interface SidecarSpec {
|
|
184
209
|
image: string;
|
|
185
210
|
healthPath: string;
|
|
@@ -199,9 +224,11 @@ export interface AcPluginManifest {
|
|
|
199
224
|
server?: string;
|
|
200
225
|
/** Frontend ESM entry; omitted by a plugin that contributes no UI. */
|
|
201
226
|
frontend?: string;
|
|
202
|
-
/** Image + health path
|
|
227
|
+
/** Image + health path, expected when `kind: "sidecar"`. Not enforced at load (#784): without it the
|
|
228
|
+
* health poll uses `/healthz`. Every bundled sidecar plugin's is checked in CI. */
|
|
203
229
|
sidecar?: SidecarSpec;
|
|
204
|
-
/** Host-agent capabilities the plugin may use, each granted per machine in Admin (E4).
|
|
230
|
+
/** Host-agent capabilities the plugin may use, each granted per machine in Admin (E4). A job's own
|
|
231
|
+
* `capabilities` (#843) is a subset of this list. */
|
|
205
232
|
capabilities?: string[];
|
|
206
233
|
contributes?: Contributes;
|
|
207
234
|
env?: EnvVar[];
|
package/dist/slots.d.ts
CHANGED
|
@@ -84,7 +84,16 @@ export interface LabelStandard {
|
|
|
84
84
|
type: Label[];
|
|
85
85
|
exclusive: string[];
|
|
86
86
|
}
|
|
87
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* `issues@1`: list/search/labels/people/mutate over forge-agnostic records.
|
|
89
|
+
*
|
|
90
|
+
* Declared, not bound (#784): `provider-github` declares `provides: issues@1`, and that declaration is
|
|
91
|
+
* what lets its consumers (`issues-board`, `issues-classic`) activate, but it binds no implementation of
|
|
92
|
+
* this interface, so `host.slots.resolve("issues", 1)` answers `null`. The consumers read
|
|
93
|
+
* `provider-github`'s HTTP routes (`/api/x/provider-github/…`, through `host.fetch`) instead, which
|
|
94
|
+
* answer the record shapes above. A plugin that wants this interface in-process gets `null` until a
|
|
95
|
+
* provider binds one.
|
|
96
|
+
*/
|
|
88
97
|
export interface IssuesProvider {
|
|
89
98
|
/** List issues for a repo in a given state. */
|
|
90
99
|
listIssues(repo: RepoRef, state: "open" | "closed" | "all"): Promise<IssueRecord[]>;
|
|
@@ -180,7 +189,9 @@ export interface Identity {
|
|
|
180
189
|
* model roles (Cloudflare Access does not); an empty array is an identity that holds none (#102). */
|
|
181
190
|
roles?: string[];
|
|
182
191
|
}
|
|
183
|
-
/** `auth@1`: resolve the identity for a request
|
|
192
|
+
/** `auth@1`: resolve the identity for a request. A bound provider answering `null` makes the host refuse
|
|
193
|
+
* the request with a 401 (on every route but `/healthz` and `/api/config`); open mode is the host with
|
|
194
|
+
* NO provider bound, not a provider answering `null`. */
|
|
184
195
|
export interface AuthProvider {
|
|
185
196
|
/** Resolve the identity from a request's headers, or null when unauthenticated. */
|
|
186
197
|
identify(headers: Record<string, string | undefined>): Promise<Identity | null>;
|
|
@@ -195,6 +206,12 @@ export interface NotifyMessage {
|
|
|
195
206
|
}
|
|
196
207
|
/** `notify@1`: fan out a message to this provider's channel. */
|
|
197
208
|
export interface NotifyProvider {
|
|
198
|
-
|
|
209
|
+
/** `opts.signal` (#701) is the caller's abort: a job that posts through its own provider passes its
|
|
210
|
+
* run's `ctx.signal`, and a provider should then abandon the send and reject. The host's failure
|
|
211
|
+
* fan-out passes none. Optional, so a provider written against `send(message)` alone still
|
|
212
|
+
* satisfies this interface. */
|
|
213
|
+
send(message: NotifyMessage, opts?: {
|
|
214
|
+
signal?: AbortSignal;
|
|
215
|
+
}): Promise<void>;
|
|
199
216
|
}
|
|
200
217
|
//# sourceMappingURL=slots.d.ts.map
|
package/dist/views.d.ts
CHANGED
|
@@ -40,9 +40,11 @@ export interface PluginCardView {
|
|
|
40
40
|
export: string;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* An action the shell builds a button + form from (#34); opening its modal is its one confirmation (#530).
|
|
44
|
+
* Unlike panels/cards, an action is
|
|
44
45
|
* pure data (no `export`), so it is surfaced for any loadable plugin regardless of whether it ships a
|
|
45
|
-
* frontend bundle. `confirm`/`danger` are normalised to booleans and `fields` to an array for the shell
|
|
46
|
+
* frontend bundle. `confirm`/`danger` are normalised to booleans and `fields` to an array for the shell (`confirm` is inert
|
|
47
|
+
* since #530).
|
|
46
48
|
*/
|
|
47
49
|
export interface PluginActionView {
|
|
48
50
|
id: string;
|
|
@@ -59,9 +61,10 @@ export interface PluginActionView {
|
|
|
59
61
|
* :pluginId/:jobId/runs`'s response already exposes it per call, and this card's row model
|
|
60
62
|
* (`SchedRow.runOn`) plumbs it through end to end even though its current render doesn't surface it
|
|
61
63
|
* (found in review, round 2 — a candidate for a future small UI addition, not a bug).
|
|
62
|
-
* `timeoutSec`/`minAgentVersion` are deliberately left off instead, independent of
|
|
63
|
-
* route's `skipped` reason already reflects a `minAgentVersion` gate
|
|
64
|
-
* duplicating the threshold
|
|
64
|
+
* `timeoutSec`/`minAgentVersion`/`capabilities` are deliberately left off instead, independent of
|
|
65
|
+
* that: the runs route's `skipped` reason already reflects a `minAgentVersion` gate or a missing grant
|
|
66
|
+
* (#843) as a per-machine fact, so duplicating the threshold or the capability list here would have no
|
|
67
|
+
* reader.
|
|
65
68
|
*/
|
|
66
69
|
export interface PluginJobView {
|
|
67
70
|
id: string;
|
|
@@ -72,8 +75,8 @@ export interface PluginJobView {
|
|
|
72
75
|
/**
|
|
73
76
|
* Where a plugin was discovered from (#183): `bundled` under the image's own `plugins/` dir, `installed`
|
|
74
77
|
* under an operator-installed-plugins dir. Derived from the scan root a plugin was found under, never
|
|
75
|
-
* from the manifest — a plugin cannot declare itself first-party.
|
|
76
|
-
* #
|
|
78
|
+
* from the manifest — a plugin cannot declare itself first-party. `installed` is the operator-installed
|
|
79
|
+
* root (`AC_INSTALLED_PLUGINS_DIR`, #451), filled by the install path (#452).
|
|
77
80
|
*/
|
|
78
81
|
export type PluginOrigin = "bundled" | "installed";
|
|
79
82
|
export interface PluginView {
|
|
@@ -173,10 +176,17 @@ export interface JobMachineRun {
|
|
|
173
176
|
queuedAt: string;
|
|
174
177
|
reason: string;
|
|
175
178
|
};
|
|
176
|
-
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
179
|
+
/** A dispatch right now would skip this machine, for one of two causes: it has not granted the
|
|
180
|
+
* plugin every capability the job declares (#843; reason `not granted: <capability>[, ...]`, the
|
|
181
|
+
* missing ones in declared order), or its paired agent version compares lower than the job's
|
|
182
|
+
* 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
|
|
186
|
+
* applied, and a `queued` row with a version cause can also be written WHILE the skip applies: an
|
|
187
|
+
* unreachable machine is queued before any floor is judged, since the floor is judged only on a
|
|
188
|
+
* version a probe just reported. A grant skip is judged before the unreachable branch, so it never
|
|
189
|
+
* writes one and clears any it finds. */
|
|
180
190
|
skipped?: {
|
|
181
191
|
reason: string;
|
|
182
192
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rackbops/ac-plugin-contract",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.58",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"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.",
|
package/schema.json
CHANGED
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
},
|
|
27
27
|
"sidecar": {
|
|
28
28
|
"$ref": "#/definitions/SidecarSpec",
|
|
29
|
-
"description": "Image + health path
|
|
29
|
+
"description": "Image + health path, expected when `kind: \"sidecar\"`. Not enforced at load (#784): without it the health poll uses `/healthz`. Every bundled sidecar plugin's is checked in CI."
|
|
30
30
|
},
|
|
31
31
|
"capabilities": {
|
|
32
32
|
"type": "array",
|
|
33
33
|
"items": {
|
|
34
34
|
"type": "string"
|
|
35
35
|
},
|
|
36
|
-
"description": "Host-agent capabilities the plugin may use, each granted per machine in Admin (E4)."
|
|
36
|
+
"description": "Host-agent capabilities the plugin may use, each granted per machine in Admin (E4). A job's own `capabilities` (#843) is a subset of this list."
|
|
37
37
|
},
|
|
38
38
|
"contributes": {
|
|
39
39
|
"$ref": "#/definitions/Contributes"
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"healthPath"
|
|
77
77
|
],
|
|
78
78
|
"additionalProperties": false,
|
|
79
|
-
"description": "A sidecar plugin's image and health path
|
|
79
|
+
"description": "A sidecar plugin's image and health path, expected when `kind === \"sidecar\"`. The host reads only `healthPath` (its health poll) and never `image` (the compose file names the sidecar's image)."
|
|
80
80
|
},
|
|
81
81
|
"Contributes": {
|
|
82
82
|
"type": "object",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"attention": {
|
|
139
139
|
"type": "boolean",
|
|
140
140
|
"const": true,
|
|
141
|
-
"description": "#78: the plugin serves `GET /api/x/<id>/attention` -> `{ ok: true, items: AttentionItem[] }` (`attention.ts`). The `overview` plugin's rollup card discovers contributors via `PluginView.attention` (`views.ts`) rather than a second manifest read. Additive; always `true` when present (a boolean, not a route list, since the path is fixed)."
|
|
141
|
+
"description": "#78: the plugin serves `GET /api/x/<id>/attention` -> `{ ok: true, items: AttentionItem[] }` (`attention.ts`). The `overview` plugin's rollup card discovers contributors via `PluginView.attention` (`views.ts`) rather than a second manifest read. Additive; always `true` when present (a boolean, not a route list, since the path is fixed). The plugin must also declare that route in `routes` and register it; the host checks neither at load (#784; every bundled plugin's is checked in CI), and the rollup counts a contributor whose route fails as not loaded."
|
|
142
142
|
}
|
|
143
143
|
},
|
|
144
144
|
"additionalProperties": false,
|
|
@@ -229,7 +229,8 @@
|
|
|
229
229
|
"description": "What the action operates on, e.g. \"issue\" | \"report\" | \"global\"."
|
|
230
230
|
},
|
|
231
231
|
"confirm": {
|
|
232
|
-
"type": "boolean"
|
|
232
|
+
"type": "boolean",
|
|
233
|
+
"description": "Accepted, but inert since #530: the modal is the one confirmation, so neither it nor the palette asks for a second click."
|
|
233
234
|
},
|
|
234
235
|
"danger": {
|
|
235
236
|
"type": "boolean"
|
|
@@ -247,7 +248,7 @@
|
|
|
247
248
|
"scope"
|
|
248
249
|
],
|
|
249
250
|
"additionalProperties": false,
|
|
250
|
-
"description": "
|
|
251
|
+
"description": "An action; the shell builds its button + modal from these fields (#34), and opening the modal is the action's one confirmation (#530)."
|
|
251
252
|
},
|
|
252
253
|
"ActionField": {
|
|
253
254
|
"type": "object",
|
|
@@ -344,7 +345,7 @@
|
|
|
344
345
|
},
|
|
345
346
|
"schedule": {
|
|
346
347
|
"type": "string",
|
|
347
|
-
"description": "5-field cron expression."
|
|
348
|
+
"description": "5-field cron expression (the host's `jobs/cron.ts` grammar). Not validated when the plugin loads (#784): a schedule that does not parse is logged at warn when the scheduler starts and the job is never scheduled, while the plugin itself loads as usual. Every bundled plugin's schedules are parsed in CI."
|
|
348
349
|
},
|
|
349
350
|
"timeoutSec": {
|
|
350
351
|
"type": "number",
|
|
@@ -358,6 +359,13 @@
|
|
|
358
359
|
"minAgentVersion": {
|
|
359
360
|
"type": "string",
|
|
360
361
|
"description": "A `runOn: \"agent\"` job is not dispatched to a machine whose paired agent version compares lower than this (#372, #80); the run row records the skip with its reason instead. Ignored for a `runOn: \"host\"` job. Additive; absent means every paired machine is always dispatched, as before this field existed."
|
|
362
|
+
},
|
|
363
|
+
"capabilities": {
|
|
364
|
+
"type": "array",
|
|
365
|
+
"items": {
|
|
366
|
+
"type": "string"
|
|
367
|
+
},
|
|
368
|
+
"description": "The host-agent capabilities this job's verbs need on a machine (#843). A `runOn: \"agent\"` job is not dispatched to a paired machine that has not granted this plugin every one of them. That machine's row records `skipped` with reason `not granted: <capability>[, ...]` instead: no run is recorded, no `job.finished` is emitted and no failure is notified. Each must be declared in the plugin's top-level `capabilities` (the host refuses the manifest otherwise). Ignored for a `runOn: \"host\"` job. Additive: absent or empty dispatches to every paired machine, as before this field existed. A host released before this field refuses a manifest that declares it."
|
|
361
369
|
}
|
|
362
370
|
},
|
|
363
371
|
"required": [
|
|
@@ -483,10 +491,11 @@
|
|
|
483
491
|
},
|
|
484
492
|
"format": {
|
|
485
493
|
"type": "string",
|
|
486
|
-
"description": "Anchored regex the value
|
|
494
|
+
"description": "Anchored regex the value is expected to match, e.g. \"^[0-9]+$\". Not enforced by the host; every bundled plugin's compiles and is anchored (checked in CI)."
|
|
487
495
|
},
|
|
488
496
|
"required": {
|
|
489
|
-
"type": "boolean"
|
|
497
|
+
"type": "boolean",
|
|
498
|
+
"description": "Whether the plugin needs a value. Not enforced by the host: a missing value reads `undefined`."
|
|
490
499
|
},
|
|
491
500
|
"secret": {
|
|
492
501
|
"type": "boolean"
|
|
@@ -496,7 +505,7 @@
|
|
|
496
505
|
"key"
|
|
497
506
|
],
|
|
498
507
|
"additionalProperties": false,
|
|
499
|
-
"description": "A declared environment variable (field names shared verbatim with the discord-bot index)."
|
|
508
|
+
"description": "A declared environment variable (field names shared verbatim with the discord-bot index). Declaring a key is what lets `HostApi.env.get` read it. `format` and `required` are metadata only: the host checks a value against neither (#784), so a plugin that depends on one checks its own value."
|
|
500
509
|
}
|
|
501
510
|
}
|
|
502
511
|
}
|