@intentic/extension-api 1.337.0 → 1.339.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/runtime.ts CHANGED
@@ -5,6 +5,7 @@ import { createBackoff, sleep, within } from "@intentic/base/async";
5
5
  import { errorMessage, undefinedIfMissing } from "@intentic/base/errors";
6
6
  import { FetchJsonError, fetchJson } from "@intentic/base/fetch-json";
7
7
  import { queueOnFile, writeFileAtomic } from "@intentic/base/fs";
8
+ import { parseJsonWith } from "@intentic/base/json";
8
9
  import { sandboxRouteAllowed } from "@intentic/extension-manifest/permissions";
9
10
  import type * as SandboxContract from "@intentic/sandbox-contract";
10
11
  import { readDocument } from "@intentic/sandbox-contract/documents";
@@ -25,6 +26,7 @@ import type * as OrpcClient from "@orpc/client";
25
26
  import type * as OpenApiFetch from "@orpc/openapi-client/fetch";
26
27
  import type { Disposable } from "./disposable.js";
27
28
  import type {
29
+ ApprovedActionHandler,
28
30
  BackendRouteHandler,
29
31
  ExtensionHealth,
30
32
  ExtensionProcessApi,
@@ -43,6 +45,9 @@ import { extensionApiVersion } from "./version.js";
43
45
  // The backend half's types, from here as well as from the package root: the root also carries the browser half, whose
44
46
  // types name Vue's, which a gateway's program has no business loading.
45
47
  export type {
48
+ ApprovedAction,
49
+ ApprovedActionHandler,
50
+ ApprovedActionOutcome,
46
51
  BackendRouteHandler,
47
52
  ExtensionHealth,
48
53
  ExtensionHealthState,
@@ -198,18 +203,11 @@ const splitLines = (buffer: string) => {
198
203
  // The values a frame carries for its listeners, or undefined for a line this version cannot read (not JSON, or a kind a
199
204
  // newer daemon sends), which is skipped rather than fatal.
200
205
  const frameOf = (line: string): { readonly kind: ExtensionEventKind; readonly values: readonly string[] } | undefined => {
201
- let raw: unknown;
202
- try {
203
- raw = JSON.parse(line);
204
- } catch {
205
- // allow(silent-catch): a line that is not JSON is a frame this version cannot read, skipped like an unknown kind
206
- return undefined;
207
- }
208
- const parsed = ExtensionEventSchema.safeParse(raw);
209
- if (!parsed.success) {
206
+ // A line that is not JSON is a frame this version cannot read, skipped like an unknown kind.
207
+ const event = parseJsonWith(line, ExtensionEventSchema);
208
+ if (event === undefined) {
210
209
  return undefined;
211
210
  }
212
- const event = parsed.data;
213
211
  switch (event.kind) {
214
212
  case "heartbeat":
215
213
  return undefined;
@@ -412,12 +410,14 @@ export const createExtensionRuntime = (options: ExtensionRuntimeOptions): Extens
412
410
  };
413
411
  };
414
412
 
415
- // The backend api a `server` bundle is handed: the runtime plus the two slots only the host serves, its route namespace
416
- // and its tools. A second `mount` or `serve` replaces the first, per the api's contract.
413
+ // The backend api a `server` bundle is handed: the runtime plus the slots only the host serves, its route namespace, its
414
+ // tools and the approved actions it carries out. A second `mount`, `serve` or `perform` replaces the first, per the
415
+ // api's contract.
417
416
  export interface ServerApiSlots {
418
417
  readonly api: ExtensionServerApi;
419
418
  readonly handler: () => BackendRouteHandler | undefined;
420
419
  readonly tools: () => ToolSource | undefined;
420
+ readonly performer: () => ApprovedActionHandler | undefined;
421
421
  close(): void;
422
422
  }
423
423
 
@@ -425,6 +425,7 @@ export const createServerApi = (options: ExtensionRuntimeOptions): ServerApiSlot
425
425
  const runtime = createExtensionRuntime(options);
426
426
  let mounted: BackendRouteHandler | undefined;
427
427
  let tools: ToolSource | undefined;
428
+ let performer: ApprovedActionHandler | undefined;
428
429
  return {
429
430
  api: {
430
431
  ...runtime.api,
@@ -438,9 +439,15 @@ export const createServerApi = (options: ExtensionRuntimeOptions): ServerApiSlot
438
439
  tools = source;
439
440
  },
440
441
  },
442
+ approvals: {
443
+ perform: (handler) => {
444
+ performer = handler;
445
+ },
446
+ },
441
447
  },
442
448
  handler: () => mounted,
443
449
  tools: () => tools,
450
+ performer: () => performer,
444
451
  close: runtime.close,
445
452
  };
446
453
  };
package/src/server.ts CHANGED
@@ -57,6 +57,39 @@ export interface ToolDefinition {
57
57
  // the host answers 404.
58
58
  export type BackendRouteHandler = (request: Request) => Promise<Response | undefined>;
59
59
 
60
+ // An action in the approvals queue that names this extension as its `performer`, handed over by the daemon only after
61
+ // it found a person's yes on record matching the item as it stands, and spent that yes. The workspace file is never
62
+ // what decides: an item marked approved without a recorded yes, or changed since, never reaches the backend.
63
+ export interface ApprovedAction {
64
+ // The item's id in the approvals queue.
65
+ readonly id: string;
66
+ // `performer.digest` as the yes pinned it: the SHA-256 the extension wrote when it held the act. The extension
67
+ // recomputes it over what it is about to do and refuses on any difference.
68
+ readonly digest: string;
69
+ // The item's headline and details, exactly as they stood when the person said yes.
70
+ readonly summary: string;
71
+ readonly details: string;
72
+ // Who said yes (their sign-in) and when, as the daemon recorded it. No approver when the sandbox runs without
73
+ // sign-in. A yes given again (a retry) has a later `approvedAt`, so the pair names one yes.
74
+ readonly approver?: string | undefined;
75
+ readonly approvedAt: number;
76
+ // The sandbox's action rules as the daemon reads them now, organisation pins applied: a rule set to deny after the
77
+ // yes still refuses.
78
+ readonly actionRules: Readonly<Record<string, string>>;
79
+ }
80
+
81
+ // What carrying out an approved action came to, in the three states the queue keeps apart.
82
+ export type ApprovedActionOutcome =
83
+ // It happened. `result` is the row's one line: a registry number, a task id.
84
+ | { readonly outcome: "done"; readonly result: string }
85
+ // It did not happen and nothing reached the outside world, so a new yes may safely run it again.
86
+ | { readonly outcome: "refused"; readonly error: string }
87
+ // It may have happened: the request went out and its answer failed or was lost part-way. The queue never repeats
88
+ // it by itself; the person checks the outside system first.
89
+ | { readonly outcome: "uncertain"; readonly error: string };
90
+
91
+ export type ApprovedActionHandler = (action: ApprovedAction, context: { readonly signal: AbortSignal }) => Promise<ApprovedActionOutcome>;
92
+
60
93
  // An extension's own setting values, keyed as its manifest's `contributes.settings` declares them.
61
94
  export type ExtensionSettingValues = Readonly<Record<string, string | number | boolean>>;
62
95
 
@@ -157,6 +190,13 @@ export interface ExtensionServerApi {
157
190
  readonly tools: {
158
191
  serve(tools: (card: ToolCard | undefined) => readonly ToolDefinition[] | Promise<readonly ToolDefinition[]>): void;
159
192
  };
193
+ // The approved actions this extension carries out itself, in place of an agent turn: an approvals-queue item whose
194
+ // `performer` names this extension reaches `perform` from the daemon's approvals executor alone, through a door of
195
+ // the backend host that no /x route, panel or agent token reaches, after the daemon has matched and spent the yes.
196
+ // The credentials stay in the backend, read from the daemon. A second perform replaces the first.
197
+ readonly approvals: {
198
+ perform(handler: ApprovedActionHandler): void;
199
+ };
160
200
  // Authenticated transport to the daemon's own routes; every call is checked against the manifest's
161
201
  // `permissions.daemon` allowlist.
162
202
  readonly daemon: {
@@ -175,9 +215,10 @@ export interface ExtensionServerContext {
175
215
  }
176
216
 
177
217
  // What a declared process (`contributes.processes`) gets, from `connectExtensionProcess` in
178
- // `@intentic/extension-api/runtime`: the backend's api without the two halves only the backend host can serve. A
179
- // process answers on its own port, so it mounts no routes and serves tools there (`contributes.tools.process`).
180
- export type ExtensionProcessApi = Omit<ExtensionServerApi, "routes" | "tools">;
218
+ // `@intentic/extension-api/runtime`: the backend's api without the halves only the backend host can serve. A process
219
+ // answers on its own port, so it mounts no routes, serves tools there (`contributes.tools.process`) and carries out no
220
+ // approved action.
221
+ export type ExtensionProcessApi = Omit<ExtensionServerApi, "routes" | "tools" | "approvals">;
181
222
 
182
223
  // Shape of the manifest `server` bundle's default (or named) export; `activateServer` runs once per activation, which
183
224
  // is once per backend-host start unless the extension returns a `deactivate`, after which a change to it alone
package/src/stream.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { sseEvents, sseJson } from "@intentic/sandbox-contract";
1
+ import { sseEvents, sseJson } from "@intentic/sandbox-contract/protocol/sse";
2
2
 
3
3
  // Reads a daemon SSE/ndjson stream: reframes blank-line-separated `data: <JSON>` frames and parses each, through the
4
4
  // contract's one SSE reader, which ends a stream silent past its heartbeat window rather than hanging on it.