@rackbops/ac-plugin-contract 2.0.0-alpha.39 → 2.0.0-alpha.41

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
@@ -1,7 +1,7 @@
1
1
  # Writing an artifact-console plugin
2
2
 
3
- A plugin adds to the console without changing it: a nav **panel**, an Overview **card**, a confirm-gated
4
- **action**, a scheduled **job**, an HTTP **route**, and a **setting** are all contributed from data + a small
3
+ A plugin adds to the console without changing it: a nav **panel**, an Overview **card**, an
4
+ **action** (a button whose modal is its confirmation), a scheduled **job**, an HTTP **route**, and a **setting** are all contributed from data + a small
5
5
  server module. The host validates the plugin's manifest before it runs any of the plugin's code, loads it at
6
6
  startup, and surfaces it at `GET /api/plugins`.
7
7
 
@@ -119,6 +119,12 @@ Rules the host enforces, at validation or load time:
119
119
  doesn't (the schema itself requires only `id`, `hostApiVersion` and `kind`). `contributes` and every sub-field
120
120
  is optional in the schema — contribute only the points you use; each contribution's own required fields are
121
121
  schema-enforced.
122
+ - **Jobs** (#137): a job's **`timeoutSec`** must be at least `1` (the schema's `minimum`; a zero or negative
123
+ timeout would fire at once and time out any run that awaits real I/O), and a job **`id`** must be unique
124
+ *within its plugin* -- two jobs with
125
+ one id are rejected as `duplicate job id "<id>" within the plugin`, like a duplicate panel, card or action id.
126
+ Job ids are plugin-scoped (a job is addressed as `<pluginId>/<jobId>`), so two different plugins may declare the
127
+ same job id.
122
128
  - The manifest is the **declaration**; your server code supplies the runtime halves. An HTTP route and a job must
123
129
  be *declared here* or the host will refuse to serve/run them (see §2).
124
130
 
@@ -138,7 +144,7 @@ export const createPlugin: CreatePlugin = (host) => ({
138
144
  const greeting = (await host.settings.get("greeting")) ?? "hello"
139
145
  return Response.json({ greeting })
140
146
  })
141
- // A confirm-gated action: the button, confirm and fields come from the manifest; the handler is here.
147
+ // An action: the button, its flags and the fields come from the manifest; the handler is here.
142
148
  host.actions.register({
143
149
  id: "hello.ping",
144
150
  run: async (input) => ({ echoed: input }),
@@ -174,10 +180,25 @@ export const createPlugin: CreatePlugin = (host) => ({
174
180
  fails naming the file and line (an installed third-party plugin is not covered).
175
181
  - **Action** — `host.actions.register({ id, run })`. The button, `confirm`/`danger` and the modal `fields` all
176
182
  come from the manifest; here you supply only `run(input)`. (The action's manifest metadata drives the shell
177
- UI; registering a `run` for an id the manifest omits simply leaves it without a button.)
183
+ UI; registering a `run` for an id the manifest omits simply leaves it without a button.) Opening the modal is
184
+ the confirmation (#530): its primary button, labelled with the action's `title`, fires `run` on the **first**
185
+ click, whether or not the manifest sets `confirm` -- there is no second "Confirm" click. `danger: true` gives
186
+ that primary button the danger style, gives its Ctrl/Cmd+K palette row a danger badge, and stops Enter pressed in
187
+ one of the form's fields from submitting it (a click on the button, or Enter or Space on the focused button,
188
+ still fires; Enter in a text area is a newline). A non-danger action still submits on Enter in any field,
189
+ including a checkbox or a radio (browsers treat that Enter as a submit): mark an action `danger` if it must
190
+ not fire from a stray Enter. The primary disables only while a request is in flight, so an action that
191
+ answers faster than an operator's second click can be run twice: make its `run` tolerate an overlapping call.
192
+ `confirm` stays a valid manifest flag, so an existing manifest keeps loading, but neither the modal nor the
193
+ palette acts on it.
178
194
  - **Job** — implement `runJob(ctx)` and dispatch on `ctx.jobId`. The host runs only jobs you declared in
179
- `contributes.jobs`, on their 5-field cron `schedule`. `ctx.log` appends to that run's recorded log;
180
- `ctx.signal` aborts at `timeoutSec`. `runOn: "host"` runs your `runJob` once, in the host process.
195
+ `contributes.jobs`, on their 5-field cron `schedule`. `ctx.log` appends to that run's recorded log (its last
196
+ 50 lines are kept); the host prunes the run history when a run is recorded (#139): it keeps at most the
197
+ newest 200 runs of that job (per machine, for a `runOn: "agent"` job) and drops runs older than 30 days,
198
+ though never a job's (or machine's) newest run, so `host.jobs.lastRun` still answers for a job that runs
199
+ less often than monthly. A job that runs every minute therefore no longer grows the run history without
200
+ bound. `ctx.signal` aborts at `timeoutSec` seconds (at least 1, #137). `runOn: "host"` runs your `runJob`
201
+ once, in the host process.
181
202
  `runOn: "agent"` (#51) also runs your `runJob` in the host process — the agent itself runs no plugin
182
203
  code — but once per currently paired machine, with `ctx.machine` set to `{ id, name }`; do the
183
204
  machine's own work through `host.agent.call(ctx.machine.id, …)`. A machine that doesn't answer when
@@ -392,6 +413,25 @@ cards -- reads when it is opened and after its own actions. Only Home is still r
392
413
  which remounts it for Refresh, the heartbeat and a return after a minute; Home's open-issues card keeps
393
414
  its count across those remounts, so within 15 minutes of its last load none of them re-reads it (#550).
394
415
 
416
+ **A panel that must fit the viewport reads the shell header's height (#368).** The shell header is in normal
417
+ flow and wraps, so its height changes with the viewport width and with what plugins put in it. The shell
418
+ measures it (`offsetHeight`) and publishes it in whole px as the CSS custom property `--ac-shell-header-h` on
419
+ `document.documentElement`: set before the first paint, kept current on a `ResizeObserver` on the header's
420
+ border box and on window `resize`, and removed when the shell unmounts. A panel's CSS reads
421
+ `var(--ac-shell-header-h, 0px)`; the `0px` fallback is what applies wherever the shell has not published it.
422
+ The Artifacts panel at <=720 px is the example:
423
+ `height: calc(100dvh - var(--ac-shell-header-h, 0px) - var(--rb-space-4))` with
424
+ `margin-bottom: calc(-1 * var(--rb-space-4))`, because the shell's `<main>` pads every side by
425
+ `var(--rb-space-4)` (the top is subtracted, the bottom cancelled), and its `position: fixed` drawer and scrim use
426
+ `top: var(--ac-shell-header-h, 0px)` so the header's controls stay reachable with them open. Limits: the
427
+ value is a whole number of px, so a header with a fractional real height leaves a panel sized against it up
428
+ to half a pixel over or under; `dvh` follows a phone's collapsing address bar but the shell root's own
429
+ `min-height` is `100vh`, so on a phone with the bar showing the page can scroll by the difference, and once
430
+ it has scrolled by some amount the in-flow header moves up while a `fixed` overlay stays put, leaving that
431
+ strip of the panel uncovered between the header and the overlay; and the panel itself resizes with `dvh` as
432
+ the bar collapses. The layout arithmetic and the whole-pixel rounding were measured in desktop Chromium at
433
+ 375 px; the phone behaviours in this list are derived from the CSS, not observed on a phone.
434
+
395
435
  ### The `@ac/host` browser module
396
436
 
397
437
  `@ac/host` (typed as `BrowserHostApi` in `@ac/contract`) is the one shared module instance the shell and every
@@ -435,6 +475,7 @@ const unregister = header.registerButton({ id: "hello-scan", label: "Scan", onCl
435
475
  `actions.open` opens the universal confirm modal for a registered action — the plugin's own, or another
436
476
  plugin's (`pluginId` names it; the Board and the Artifacts action bar open `provider-github`'s and others').
437
477
  `target` scopes the draft to a specific object, e.g. an issue number, when the action isn't `scope: "global"`.
478
+ Opening the modal is the confirmation: the operator's first click on its primary button runs the action.
438
479
  It is fire-and-forget, held until the shell mounts, and dropped if it never does:
439
480
 
440
481
  ```ts
@@ -465,7 +506,7 @@ the listener is called once for every action the modal dispatched and got a resu
465
506
  and whoever opened it (your own `actions.open`, or the Ctrl/Cmd+K palette) — with the action's `pluginId`,
466
507
  `actionId`, `ok`, the `error` message on a failure, and the opened ref's `target` (absent when the ref had none,
467
508
  as from the palette) — what the action was opened *for*, not what the operator finally typed into the form. It
468
- carries no input. Cancelling, or closing before the confirm, calls nothing. Use it to
509
+ carries no input. Cancelling, or closing the modal without clicking its primary button, calls nothing. Use it to
469
510
  refresh what an action changed — the Board refetches the card an `issue.*` action named, so a closed issue's row
470
511
  goes at once instead of at the next poll. It returns the unsubscribe; a listener that throws is logged and
471
512
  skipped, and never reaches the modal or another listener. It needs no shell to be mounted: it simply hears
@@ -507,6 +548,17 @@ A `hint` (optional, e.g. a category or the plugin id) is searched the same as `l
507
548
  `>hel` finds an item whose label doesn't contain "hel" but whose hint does — and renders as muted secondary
508
549
  text in the row.
509
550
 
551
+ **Ctrl/Cmd+K from inside a same-origin iframe (#369).** A key pressed while an iframe has focus lands in the
552
+ iframe's own document, never the shell's, so the palette does not open by itself. A panel that hosts a
553
+ same-origin iframe (the Artifacts report preview does) forwards the hotkey: bind a `keydown` listener on the
554
+ iframe's `contentDocument` (skip a frame whose `contentDocument` is null or throws: cross-origin), and on Ctrl
555
+ or Cmd plus `k` with no Alt and no Shift, `preventDefault()` and dispatch `new CustomEvent("ac:quickcast-key")`
556
+ on the parent `window`. The shell opens the palette under the keystroke's own guard: only when no other dialog
557
+ is open, and an event while the palette is already open does nothing (closing stays with Ctrl/Cmd+K and
558
+ Escape). Remove the listener when the frame goes away. Closing the palette with Escape leaves focus on the
559
+ shell page rather than back inside the frame (observed in Chromium), so the user clicks the frame to type in
560
+ it again.
561
+
510
562
  `fetch` makes a same-origin, credentialed, CSRF-aware request to a host/plugin route — a relative `path`
511
563
  resolves against the console origin; a cross-origin absolute URL throws rather than being followed. A panel
512
564
  reads a provider's routes through this rather than a bare `window.fetch`; a card should reach for `cached`
@@ -558,7 +610,9 @@ the guide is written against hello, so this is exactly what it does:
558
610
  2. **The Vitest alias.** `@ac/host` only resolves for real through the host page's import map, which a plain
559
611
  Node test runner has no equivalent of. If you test the frontend through Vitest, alias the specifier to
560
612
  `packages/ui-shell/src/vendor/ac-host-entry.mjs` (the shell's own implementation — reuse it rather than
561
- mocking), the way [`plugins/hello/vitest.config.ts`](../../plugins/hello/vitest.config.ts) does.
613
+ mocking), the way [`plugins/hello/vitest.config.ts`](../../plugins/hello/vitest.config.ts) does. (Leave out
614
+ that file's `setupFiles` line: it names this repo's own shared test-timeout script, which your plugin does not
615
+ have; set your own `waitFor` / test timeouts if your tests need more than Vitest's defaults.)
562
616
  3. **Leaving it external in the build.** Nothing extra is needed here as long as you build with plain `tsc`
563
617
  (§3, no bundler) — `tsc` only transpiles syntax, so `import { toast } from "@ac/host"` is emitted into
564
618
  `dist/ui.js` verbatim, unresolved, for the browser's import map to satisfy at runtime, exactly like the
@@ -915,7 +969,7 @@ what it's for.
915
969
 
916
970
  | Point | Declared in `contributes` | Supplied by |
917
971
  |---|---|---|
918
- | Panel | `panels[]` (`id`, `title`, `route`, `export`, `nav?`, `order?`) | a component export in `ui.js` |
972
+ | Panel | `panels[]` (`id`, `title`, `route`, `export`, `nav?`, `order?`, `isolation?`) | a component export in `ui.js` |
919
973
  | Card | `cards[]` (`id`, `title`, `sizes`, `export`) | a component export in `ui.js` |
920
974
  | Action | `actions[]` (`id`, `scope`, `title?`, `confirm?`, `danger?`, `fields?`) | `host.actions.register({ id, run })` |
921
975
  | Job | `jobs[]` (`id`, `schedule`, `timeoutSec`, `runOn`) | `runJob(ctx)` dispatching on `ctx.jobId` |
@@ -943,6 +997,10 @@ Ties break the same way, so the order is total. `order` is a hint the operator c
943
997
  which puts them right after Home). `Home` is always first and is not orderable. A panel that lost a
944
998
  contested nav slot still has no tab, and still appears in quickcast at its ranked position.
945
999
 
1000
+ **`isolation` (#124).** A panel may declare `isolation: "iframe"`: the value is reserved (design §4) and the
1001
+ manifest accepts it, but nothing implements it in 2.0 and the host ignores it -- panels run in-process today,
1002
+ declared or not. `"iframe"` is the only accepted value, and it belongs on a panel, not on a card or the manifest.
1003
+
946
1004
  A card never renders its own heading -- the wrapper supplies it from the manifest `title`
947
1005
  (`HomeCards.tsx`'s `PluginCard`, `<Card><h3>{title}</h3>...</Card>`); a card rendering one of its
948
1006
  own duplicates it on screen (#399).
@@ -27,6 +27,11 @@ export interface PanelContribution {
27
27
  order?: number;
28
28
  /** The export name in the plugin's frontend bundle. */
29
29
  export: string;
30
+ /**
31
+ * Reserved (design §4, #124): `"iframe"` asks the host to mount this panel in an iframe. Not
32
+ * implemented in 2.0; the host accepts and ignores it, so a panel declaring it still loads in-process.
33
+ */
34
+ isolation?: "iframe";
30
35
  }
31
36
  /** A contributed Overview card. */
32
37
  export interface CardContribution {
@@ -105,7 +110,11 @@ export interface JobContribution {
105
110
  id: string;
106
111
  /** 5-field cron expression. */
107
112
  schedule: string;
108
- /** Per-run timeout in seconds. */
113
+ /**
114
+ * Per-run timeout in seconds. Must be at least 1: a zero or negative value would fire the timeout at
115
+ * once, timing out any run that awaits real I/O (#137).
116
+ * @minimum 1
117
+ */
109
118
  timeoutSec: number;
110
119
  runOn: JobRunOn;
111
120
  /** A `runOn: "agent"` job is not dispatched to a machine whose paired agent version compares lower
package/dist/slots.d.ts CHANGED
@@ -118,6 +118,10 @@ export interface Identity {
118
118
  id: string;
119
119
  email?: string;
120
120
  name?: string;
121
+ /** `"machine"` for a non-person credential (a Cloudflare Access service token); absent or `"user"` for a
122
+ * person. The host admits a machine identity on `GET`/`HEAD /api/readyz` only and refuses it on every other gated
123
+ * route; any other value is treated as a non-person too (#552). */
124
+ kind?: "user" | "machine";
121
125
  }
122
126
  /** `auth@1`: resolve the identity for a request, or null in open mode. */
123
127
  export interface AuthProvider {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rackbops/ac-plugin-contract",
3
- "version": "2.0.0-alpha.39",
3
+ "version": "2.0.0-alpha.41",
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
@@ -168,6 +168,11 @@
168
168
  "export": {
169
169
  "type": "string",
170
170
  "description": "The export name in the plugin's frontend bundle."
171
+ },
172
+ "isolation": {
173
+ "type": "string",
174
+ "const": "iframe",
175
+ "description": "Reserved (design §4, #124): `\"iframe\"` asks the host to mount this panel in an iframe. Not implemented in 2.0; the host accepts and ignores it, so a panel declaring it still loads in-process."
171
176
  }
172
177
  },
173
178
  "required": [
@@ -343,7 +348,8 @@
343
348
  },
344
349
  "timeoutSec": {
345
350
  "type": "number",
346
- "description": "Per-run timeout in seconds."
351
+ "description": "Per-run timeout in seconds. Must be at least 1: a zero or negative value would fire the timeout at once, timing out any run that awaits real I/O (#137).",
352
+ "minimum": 1
347
353
  },
348
354
  "runOn": {
349
355
  "$ref": "#/definitions/JobRunOn"