@zackbart/connecta 0.25.0 → 0.26.1

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.
Files changed (100) hide show
  1. package/AGENTS.md +6 -5
  2. package/CHANGELOG.md +387 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/artifacts/connector.d.ts +55 -0
  9. package/dist/artifacts/connector.js +724 -0
  10. package/dist/artifacts/document.js +80 -0
  11. package/dist/artifacts/guide.js +140 -0
  12. package/dist/artifacts/html-scan.js +329 -0
  13. package/dist/artifacts/json.js +68 -0
  14. package/dist/artifacts/kv-store.d.ts +20 -0
  15. package/dist/artifacts/kv-store.js +140 -0
  16. package/dist/artifacts/markdown.js +478 -0
  17. package/dist/artifacts/module.d.ts +37 -0
  18. package/dist/artifacts/module.js +60 -0
  19. package/dist/artifacts/operations.d.ts +195 -0
  20. package/dist/artifacts/operations.js +944 -0
  21. package/dist/artifacts/refresh.d.ts +53 -0
  22. package/dist/artifacts/refresh.js +235 -0
  23. package/dist/artifacts/routes.js +217 -0
  24. package/dist/artifacts/types.d.ts +214 -0
  25. package/dist/artifacts/types.js +22 -0
  26. package/dist/artifacts/validate.d.ts +62 -0
  27. package/dist/artifacts/validate.js +653 -0
  28. package/dist/artifacts.d.ts +6 -0
  29. package/dist/artifacts.js +9 -0
  30. package/dist/auth/downstream-oauth.js +397 -48
  31. package/dist/auth/oauth-start-reset.js +20 -0
  32. package/dist/branding.d.ts +87 -0
  33. package/dist/branding.js +170 -1
  34. package/dist/catalog-service.js +39 -29
  35. package/dist/catalog.js +42 -8
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connectors/remote-mcp.js +48 -9
  41. package/dist/errors.d.ts +16 -1
  42. package/dist/errors.js +28 -0
  43. package/dist/execute.js +431 -75
  44. package/dist/executor-admission.d.ts +11 -1
  45. package/dist/executor-admission.js +79 -14
  46. package/dist/executors/quickjs.js +4 -2
  47. package/dist/index.d.ts +62 -6
  48. package/dist/index.js +230 -6
  49. package/dist/invocation.js +56 -10
  50. package/dist/meta-tools.js +7 -2
  51. package/dist/module-contracts.d.ts +21 -1
  52. package/dist/oauth-callback-outcome.js +43 -18
  53. package/dist/operator-ui/generated.js +2 -2
  54. package/dist/operator-ui/model.d.ts +62 -5
  55. package/dist/operator-ui/view.js +372 -28
  56. package/dist/page-styles.js +7 -0
  57. package/dist/program-source.js +116 -0
  58. package/dist/registry.d.ts +42 -6
  59. package/dist/registry.js +164 -60
  60. package/dist/resumable.js +1291 -0
  61. package/dist/routes/activity.js +6 -69
  62. package/dist/routes/actor-labels.js +114 -0
  63. package/dist/routes/credentials.js +27 -5
  64. package/dist/routes/mcp.js +120 -59
  65. package/dist/routes/oauth-management.js +87 -20
  66. package/dist/routes/oauth.js +54 -87
  67. package/dist/routes/operator.js +1 -0
  68. package/dist/routes/shared.d.ts +23 -1
  69. package/dist/routes/shared.js +20 -2
  70. package/dist/routes/ui-data.js +2 -1
  71. package/dist/routes/ui.js +19 -4
  72. package/dist/run-journal.js +347 -0
  73. package/dist/runtime/shared-read.js +106 -0
  74. package/dist/server.js +30 -2
  75. package/dist/skills.js +30 -11
  76. package/dist/timeout.js +26 -0
  77. package/dist/tool-safety.d.ts +35 -0
  78. package/dist/tool-safety.js +31 -0
  79. package/dist/types.d.ts +23 -0
  80. package/dist/ui.d.ts +17 -5
  81. package/dist/ui.js +51 -68
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/documentation/architecture.md +214 -27
  85. package/documentation/auth.md +186 -2
  86. package/documentation/code-mode.md +236 -63
  87. package/documentation/meta-tools.md +55 -29
  88. package/ethos.md +12 -10
  89. package/examples/worker/AGENTS.md +6 -0
  90. package/examples/worker/README.md +174 -10
  91. package/examples/worker/src/d1-activity-row.ts +14 -2
  92. package/examples/worker/src/d1-activity.ts +4 -2
  93. package/examples/worker/src/index.ts +36 -1
  94. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  95. package/examples/worker/wrangler.jsonc +5 -1
  96. package/package.json +6 -2
  97. package/templates/node/AGENTS.md +9 -2
  98. package/templates/node/README.md +31 -3
  99. package/templates/node/package.json +1 -1
  100. package/templates/node/src/index.ts +27 -0
@@ -6,13 +6,16 @@ import type { CatalogAccessObservation, CatalogDriftReport } from "../types.js";
6
6
  * the rule is a second place for it to be wrong.
7
7
  *
8
8
  * - `runs_in_programs` — explicitly read-only, so `execute_code` may call it.
9
- * - `needs_approval` — everything else, which crosses `call_destructive_tool`.
9
+ * - `exempt` — not read-only, but the deployment's `execute.approval` lets a
10
+ * program call it without pausing (#566). Still approval-required
11
+ * everywhere else.
12
+ * - `needs_approval` — everything else: a program pauses for
13
+ * `resume_execution`, and a direct call crosses `call_destructive_tool`.
10
14
  *
11
- * Planned resumable writes add a third value for a config `approval: "never"`
12
- * exemption; the badge renders from a table keyed by this union so that value
13
- * is one entry, not a new branch.
15
+ * The badge renders from a table keyed by this union, so each value is one
16
+ * entry, not a branch.
14
17
  */
15
- export type UiToolSafety = "runs_in_programs" | "needs_approval";
18
+ export type UiToolSafety = "runs_in_programs" | "exempt" | "needs_approval";
16
19
  export interface UiTool {
17
20
  name: string;
18
21
  address: string;
@@ -122,6 +125,60 @@ export interface UiData {
122
125
  * it with a 404, so the page never enumerates a pool its reader cannot open.
123
126
  */
124
127
  pools?: string[];
128
+ /** True when artifact pages are mounted and this identity may open them. */
129
+ artifactsEnabled?: boolean;
130
+ }
131
+ /**
132
+ * One row of the artifact library. Every field is a fact the store keeps or
133
+ * an enum; nothing here is a message, a log line, or a downstream's words.
134
+ */
135
+ export interface UiArtifactRow {
136
+ id: string;
137
+ title: string;
138
+ kind: "html" | "markdown";
139
+ viewVersion: number;
140
+ updatedAt: string;
141
+ updatedBy: {
142
+ label: string;
143
+ };
144
+ archived: boolean;
145
+ freshness?: UiArtifactFreshness;
146
+ }
147
+ interface UiArtifactFreshness {
148
+ state: "unconfigured" | "current" | "stale";
149
+ schedule?: "manual" | "daily" | "weekly";
150
+ document?: string;
151
+ dueAt?: string;
152
+ last?: {
153
+ at: string;
154
+ status: "succeeded" | "unchanged" | "failed" | "superseded";
155
+ };
156
+ running?: boolean;
157
+ }
158
+ /** The viewer's payload: the page's facts and the one document its frame loads. */
159
+ export interface UiArtifactView {
160
+ id: string;
161
+ title: string;
162
+ kind: "html" | "markdown";
163
+ archived: boolean;
164
+ snapshot: boolean;
165
+ latestViewVersion: number;
166
+ view: {
167
+ version: number;
168
+ at: string;
169
+ by: {
170
+ label: string;
171
+ };
172
+ };
173
+ documents: {
174
+ name: string;
175
+ version: number;
176
+ updatedAt: string;
177
+ }[];
178
+ url: string;
179
+ snapshotUrl: string;
180
+ document: string;
181
+ freshness?: UiArtifactFreshness;
125
182
  }
126
183
  export interface FilteredUiConnector {
127
184
  connector: UiConnector;
@@ -1,21 +1,203 @@
1
+ /** Pages the nav lists. A single artifact is reached from the library, not the nav. */
1
2
  export const OPERATOR_PAGES = [
2
3
  "connections",
3
4
  "activity",
5
+ "artifacts",
4
6
  ];
5
7
  export const PAGE_META = {
6
8
  connections: { path: "/", label: "Connections" },
7
9
  activity: { path: "/activity", label: "Activity" },
10
+ artifacts: { path: "/artifacts", label: "Artifacts" },
11
+ artifact: { path: "/artifacts", label: "Artifact" },
8
12
  };
13
+ /**
14
+ * What each page is for, in one line under its heading — on the gate as well
15
+ * as once signed in, so the page an operator lands on reads the same before
16
+ * and after the session is checked.
17
+ */
18
+ export function pageDescription(page, productDescription) {
19
+ if (page === "activity") {
20
+ return "Every connector tool call, by who made it and how it ended. Arguments and results are never stored.";
21
+ }
22
+ if (isArtifactPage(page)) {
23
+ return "Pages agents published for the team. Each one keeps every version.";
24
+ }
25
+ return productDescription;
26
+ }
27
+ /** What the gate says while the session is checked, page by page. */
28
+ export function checkingCopy(page) {
29
+ if (page === "artifact")
30
+ return "Loading artifact…";
31
+ if (page === "artifacts")
32
+ return "Loading artifacts…";
33
+ return "Checking your session…";
34
+ }
9
35
  export function pageForPath(path) {
36
+ if (path.startsWith("/artifacts/"))
37
+ return "artifact";
10
38
  const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === path);
11
39
  return match ?? "connections";
12
40
  }
41
+ /** Artifact pages load their own data and never ask `/ui/data` whether to open. */
42
+ export function isArtifactPage(page) {
43
+ return page === "artifacts" || page === "artifact";
44
+ }
45
+ /**
46
+ * Where a viewer's frame gets its page: `/artifacts/<id>` or a snapshot,
47
+ * `/artifacts/<id>/v/<version>?d=<name>:<version>…`, mapped to its API call.
48
+ */
49
+ export function artifactViewRequest(pathname, search) {
50
+ const match = /^\/artifacts\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\/v\/(\d{1,9}))?$/.exec(pathname);
51
+ if (!match?.[1])
52
+ return undefined;
53
+ const params = new URLSearchParams();
54
+ if (match[2]) {
55
+ params.set("v", match[2]);
56
+ for (const pin of new URLSearchParams(search).getAll("d"))
57
+ params.append("d", pin);
58
+ }
59
+ const query = params.toString();
60
+ return `/artifacts/_api/view/${match[1]}${query ? `?${query}` : ""}`;
61
+ }
13
62
  export function info(message) {
14
63
  return { message, tone: "info" };
15
64
  }
16
65
  export function failure(message, fix) {
17
66
  return fix ? { message, tone: "error", fix } : { message, tone: "error" };
18
67
  }
68
+ const REFUSED_COPY = {
69
+ oauth_disconnect: "Disconnect didn't finish. If the service refused it, the deployment's log has its reply.",
70
+ oauth_reconnect: "Authorization couldn't start. If the service refused it, the deployment's log has its reply.",
71
+ credential_test: "The credential test couldn't run.",
72
+ };
73
+ /**
74
+ * The route's answer to a finished OAuth action. Its state picks the sentence;
75
+ * `opened` says whether the authorization page is already in a new tab.
76
+ */
77
+ export function oauthDoneNotice(action, answer, opened = true) {
78
+ if (action === "oauth_disconnect") {
79
+ return info("Disconnected. Connect again whenever you are ready.");
80
+ }
81
+ if (answer?.state === "ok")
82
+ return info("Connected.");
83
+ return info(opened
84
+ ? "Finish authorizing in the new tab. This page updates when you come back."
85
+ : "Your browser blocked the new tab. Open the authorization page from the link here.");
86
+ }
87
+ /** A credential Test's result. `ok` is the only part of the answer it reads. */
88
+ export function credentialTestNotice(connectorId, answer) {
89
+ return answer?.ok === true
90
+ ? info("Credential is valid.")
91
+ : failure("Credential test failed: the service rejected the stored credential, or the test couldn't reach it. The deployment's log has the service's reply.", { kind: "credential_test_failed", connectorId });
92
+ }
93
+ /**
94
+ * A route that refused one of these actions. A credential Test that found
95
+ * nothing usable to test names the problem, which picks the same copy and fix
96
+ * prompt the Connections page uses for it; every other refusal gets the
97
+ * action's one sentence. `problem` is whatever the response carried, so it is
98
+ * checked, not trusted.
99
+ */
100
+ export function refusedNotice(action, connectorId, problem) {
101
+ if (action === "credential_test" &&
102
+ (problem === "credential_required" || problem === "credential_mismatch")) {
103
+ return failure(PROBLEM_COPY[problem], { kind: problem, connectorId });
104
+ }
105
+ return failure(REFUSED_COPY[action], {
106
+ kind: action === "credential_test" ? "credential_test_failed" : "oauth_action_failed",
107
+ connectorId,
108
+ });
109
+ }
110
+ /**
111
+ * A credential save or removal the route refused, by status. The route's
112
+ * `error` can name a vault failure or echo validation detail, so none of it
113
+ * is shown; the status is enough to say what to do next.
114
+ */
115
+ function credentialRefusedCopy(action, status, problem) {
116
+ if (problem === "credential_mismatch")
117
+ return PROBLEM_COPY.credential_mismatch;
118
+ const verb = action === "credential_save" ? "saved" : "removed";
119
+ if (status === 400 || status === 413 || status === 415) {
120
+ return action === "credential_save"
121
+ ? "The credential wasn't saved: a value is empty or not in the expected format. Check it and save again."
122
+ : "The credential wasn't removed. Refresh the page and try again.";
123
+ }
124
+ if (status === 404) {
125
+ return "This connector no longer has a credential slot. Refresh the page to see its current setup.";
126
+ }
127
+ if (status === 503) {
128
+ return `Credential storage isn't configured on this deployment, so nothing was ${verb}.`;
129
+ }
130
+ return `The credential wasn't ${verb}. Try again; if it keeps failing, the deployment's log has the reason.`;
131
+ }
132
+ /**
133
+ * Every row action's failure, in one voice. The page's own failures — a
134
+ * lapsed session, a denied identity, a dropped connection — read the same for
135
+ * every action; a route's refusal picks the action's fixed sentence.
136
+ */
137
+ export function actionFailedNotice(action, connectorId, facts, productName) {
138
+ if (facts.kind === "session") {
139
+ return failure("Your session has ended. Sign in again, then retry.");
140
+ }
141
+ if (facts.kind === "forbidden") {
142
+ return failure("You don't have permission to change this connection's authentication.");
143
+ }
144
+ if (facts.kind === "network") {
145
+ return failure(`Couldn't reach ${productName}. Check your connection and try again.`);
146
+ }
147
+ if (action === "credential_save" || action === "credential_remove") {
148
+ return failure(credentialRefusedCopy(action, facts.status, facts.problem));
149
+ }
150
+ return refusedNotice(action, connectorId, facts.problem);
151
+ }
152
+ export function connectorLoadFailureCopy(failure, productName) {
153
+ return failure === "session"
154
+ ? "Your session wasn't accepted while loading this connector. Sign in again to see its status."
155
+ : `Couldn't reach ${productName} to load this connector. Check your connection, then refresh it.`;
156
+ }
157
+ /** The signed-in page's answer when `/ui/data` could not be read. */
158
+ export function loadFailureCopy(failure, productName) {
159
+ return {
160
+ title: `Couldn't reach ${productName}`,
161
+ body: failure === "network"
162
+ ? "Your browser couldn't connect. Check your connection, then retry."
163
+ : "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.",
164
+ };
165
+ }
166
+ /** A collection page's load failure: fixed words, whatever the route said. */
167
+ export function collectionFailureCopy(collection, facts, productName) {
168
+ if (facts.kind === "network") {
169
+ return `Couldn't reach ${productName}. Check your connection, then retry.`;
170
+ }
171
+ if (facts.kind === "session")
172
+ return "Your session has ended. Sign in again to see this page.";
173
+ if (facts.kind === "forbidden" || facts.status === 404) {
174
+ if (collection === "artifact")
175
+ return "There is no artifact here, or this identity can't open it.";
176
+ return collection === "activity"
177
+ ? "Activity isn't available to this identity."
178
+ : "Artifacts aren't available to this identity.";
179
+ }
180
+ return "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.";
181
+ }
182
+ /** The confirm's question, naming the connector by its title. */
183
+ export function confirmCopy(action, name) {
184
+ if (action === "oauth_disconnect") {
185
+ return {
186
+ question: `Disconnect ${name}? Its stored grant and any pending authorization are removed, and its tools stop working until it is connected again.`,
187
+ confirm: "Disconnect",
188
+ };
189
+ }
190
+ if (action === "oauth_restart") {
191
+ return {
192
+ question: `Reconnect ${name}? Its current grant stops working until you finish authorizing in the new tab.`,
193
+ confirm: "Reconnect",
194
+ };
195
+ }
196
+ return {
197
+ question: `Remove ${name}'s credential? The connector stops authenticating until a replacement is added.`,
198
+ confirm: "Remove",
199
+ };
200
+ }
19
201
  export function initialState(page) {
20
202
  return {
21
203
  page,
@@ -23,7 +205,9 @@ export function initialState(page) {
23
205
  session: "loading",
24
206
  gate: null,
25
207
  refreshing: false,
208
+ loadFailure: null,
26
209
  pendingFocus: null,
210
+ focusIfLost: null,
27
211
  ...identityScopedState(),
28
212
  };
29
213
  }
@@ -37,8 +221,13 @@ function identityScopedState() {
37
221
  data: null,
38
222
  connectorFilter: "",
39
223
  oauthNotice: null,
224
+ oauthNoticeFor: null,
40
225
  oauthBusy: null,
226
+ oauthBlocked: null,
227
+ confirming: null,
228
+ connectorFailures: {},
41
229
  credentialNotice: null,
230
+ credentialNoticeFor: null,
42
231
  credentialEditing: null,
43
232
  credentialBusy: null,
44
233
  activityPhase: "idle",
@@ -46,6 +235,13 @@ function identityScopedState() {
46
235
  activityEvents: [],
47
236
  activityCursor: null,
48
237
  activitySearch: "",
238
+ artifactPhase: "idle",
239
+ artifactNotice: null,
240
+ artifactRows: [],
241
+ artifactCursor: null,
242
+ artifactQuery: "",
243
+ artifactArchived: false,
244
+ artifactView: null,
49
245
  };
50
246
  }
51
247
  /**
@@ -60,7 +256,9 @@ export function resetIdentity(state, gate = null) {
60
256
  session: "gated",
61
257
  gate,
62
258
  refreshing: false,
259
+ loadFailure: null,
63
260
  pendingFocus: null,
261
+ focusIfLost: null,
64
262
  ...identityScopedState(),
65
263
  };
66
264
  }
@@ -74,6 +272,8 @@ export function withPage(state, page) {
74
272
  page,
75
273
  credentialEditing: null,
76
274
  credentialNotice: null,
275
+ credentialNoticeFor: null,
276
+ confirming: null,
77
277
  };
78
278
  }
79
279
  export function credentialUnavailableCopy(capability) {
@@ -85,13 +285,18 @@ export function credentialUnavailableCopy(capability) {
85
285
  }
86
286
  return "Credential management requires an interactive user. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
87
287
  }
88
- export function connectorStatusLabel(status) {
288
+ /**
289
+ * The status badge. An auth-needed connector is named by what it needs, so a
290
+ * row whose fix is "Add credential" does not say "Authorization needed".
291
+ */
292
+ export function connectorStatusLabel(status, problem) {
89
293
  if (status === "loading")
90
294
  return "Loading details";
91
295
  if (status === "ok")
92
296
  return "Connected";
93
- if (status === "auth_required")
94
- return "Authorization needed";
297
+ if (status === "auth_required") {
298
+ return problem === "credential_required" ? "Credential needed" : "Authorization needed";
299
+ }
95
300
  return "Unavailable";
96
301
  }
97
302
  export function toolCountLabel(count) {
@@ -111,16 +316,24 @@ export function summarizeConnectors(connectors) {
111
316
  total: connectors.length,
112
317
  connected: 0,
113
318
  attention: 0,
319
+ credentials: 0,
114
320
  unavailable: 0,
321
+ loading: 0,
115
322
  tools: 0,
116
323
  drifting: 0,
117
324
  };
118
325
  for (const connector of connectors) {
119
326
  if (connector.status === "ok")
120
327
  summary.connected += 1;
121
- else if (connector.status === "auth_required")
122
- summary.attention += 1;
123
- else if (connector.status !== "loading")
328
+ else if (connector.status === "auth_required") {
329
+ if (connector.problem === "credential_required")
330
+ summary.credentials += 1;
331
+ else
332
+ summary.attention += 1;
333
+ }
334
+ else if (connector.status === "loading")
335
+ summary.loading += 1;
336
+ else
124
337
  summary.unavailable += 1;
125
338
  summary.tools += connector.toolCount || 0;
126
339
  if (driftState(connector.catalogDrift) === "warning")
@@ -134,6 +347,16 @@ export function summarizeConnectors(connectors) {
134
347
  * zero, so a healthy deployment stays short.
135
348
  */
136
349
  export function connectorSummaryParts(summary) {
350
+ // Nothing has answered yet: "0 connected · 0 tools" would be a claim, and
351
+ // a false one.
352
+ if (summary.total > 0 && summary.loading === summary.total) {
353
+ return [
354
+ {
355
+ text: `Checking ${summary.total} connector${summary.total === 1 ? "" : "s"}…`,
356
+ tone: "neutral",
357
+ },
358
+ ];
359
+ }
137
360
  return [
138
361
  { text: `${summary.connected} connected`, tone: "neutral" },
139
362
  ...(summary.attention
@@ -144,15 +367,26 @@ export function connectorSummaryParts(summary) {
144
367
  },
145
368
  ]
146
369
  : []),
370
+ ...(summary.credentials
371
+ ? [
372
+ {
373
+ text: `${summary.credentials} need${summary.credentials === 1 ? "s" : ""} a credential`,
374
+ tone: "warn",
375
+ },
376
+ ]
377
+ : []),
147
378
  ...(summary.unavailable
148
379
  ? [{ text: `${summary.unavailable} unavailable`, tone: "danger" }]
149
380
  : []),
150
381
  { text: toolCountLabel(summary.tools), tone: "neutral" },
382
+ ...(summary.loading
383
+ ? [{ text: `${summary.loading} still loading`, tone: "neutral" }]
384
+ : []),
151
385
  ];
152
386
  }
153
387
  /**
154
- * The badge for each call path, keyed by the server's classification so the
155
- * planned third state is a row here rather than a new branch in a component.
388
+ * The badge for each call path, keyed by the server's classification so each
389
+ * state is a row here rather than a branch in a component.
156
390
  */
157
391
  export const TOOL_SAFETY_BADGE = {
158
392
  runs_in_programs: {
@@ -160,10 +394,15 @@ export const TOOL_SAFETY_BADGE = {
160
394
  tone: "ok",
161
395
  title: "Explicitly read-only: execute_code programs may call it without asking.",
162
396
  },
397
+ exempt: {
398
+ label: "exempt from approval",
399
+ tone: "neutral",
400
+ title: "Not read-only, but this deployment's config lets programs call it without pausing. Each call still counts against the write budget and appears in activity; call_tool still refuses it.",
401
+ },
163
402
  needs_approval: {
164
- label: "needs approval",
403
+ label: "asks for approval",
165
404
  tone: "warn",
166
- title: "Not explicitly read-only: calls cross call_destructive_tool, where the host asks first.",
405
+ title: "Not explicitly read-only: a program pauses for resume_execution, and a direct call crosses call_destructive_tool — either way the host asks first.",
167
406
  },
168
407
  };
169
408
  /**
@@ -186,6 +425,18 @@ const PROBLEM_COPY = {
186
425
  export function problemCopy(problem) {
187
426
  return problem ? PROBLEM_COPY[problem] ?? null : null;
188
427
  }
428
+ /**
429
+ * A problem someone can fix by authorizing is a warning, matching the amber
430
+ * "Authorization needed" badge above it; one where something broke is an
431
+ * error. The two used to share the red box, so the row contradicted itself.
432
+ */
433
+ export function problemTone(problem) {
434
+ return problem === "oauth_required" ||
435
+ problem === "credential_required" ||
436
+ problem === "auth_required"
437
+ ? "warn"
438
+ : "danger";
439
+ }
189
440
  /** Who owns this connector's downstream credentials, in two words. */
190
441
  export function authScopeLabel(scope) {
191
442
  return scope === "personal" ? "personal auth" : "shared auth";
@@ -272,14 +523,31 @@ export function formatDate(value) {
272
523
  if (!value)
273
524
  return "";
274
525
  const date = new Date(value);
275
- return Number.isNaN(date.valueOf()) ? "" : date.toLocaleString();
526
+ // Minutes, not seconds: nothing on these pages is read to the second, and
527
+ // the seconds were most of the noise in every row that carried a date.
528
+ return Number.isNaN(date.valueOf())
529
+ ? ""
530
+ : date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
531
+ }
532
+ const ACTOR_KINDS = {
533
+ clerk: "Clerk",
534
+ bearer: "Bearer token",
535
+ "cloudflare-access": "Cloudflare Access",
536
+ anonymous: "Anonymous",
537
+ };
538
+ /** How a caller signed in, as a person reads it. An unknown kind is shown as sent. */
539
+ function actorKindLabel(kind) {
540
+ if (!kind)
541
+ return "Unknown caller";
542
+ return ACTOR_KINDS[kind] ?? kind;
276
543
  }
544
+ /** Who made a call: their name when there is one, and how they signed in. */
277
545
  export function actorLabel(actor) {
278
546
  if (!actor?.kind)
279
- return "unknown";
280
- if (actor.label)
281
- return `${actor.kind} · ${actor.label}`;
282
- return actor.id ? `${actor.kind} · ${actor.id}` : actor.kind;
547
+ return "Unknown caller";
548
+ const who = actor.label || actor.id;
549
+ const kind = actorKindLabel(actor.kind);
550
+ return who ? `${who} (${kind})` : kind;
283
551
  }
284
552
  /**
285
553
  * The stable id behind a friendly label, shown only when a label or namespace
@@ -308,6 +576,9 @@ function activityMatches(event, query) {
308
576
  event.actor?.id,
309
577
  event.actor?.namespace,
310
578
  event.actor?.label,
579
+ activityOutcomeBadge(event.outcome).label,
580
+ activityDetail(event),
581
+ actorKindLabel(event.actor?.kind),
311
582
  ].some((value) => String(value ?? "").toLowerCase().includes(q));
312
583
  }
313
584
  export function filterActivity(events, query) {
@@ -316,29 +587,102 @@ export function filterActivity(events, query) {
316
587
  /** Counts only. What the deployment ran, never what it sent or received. */
317
588
  export function activitySummary(events) {
318
589
  if (events.length === 0)
319
- return "Arguments and results are never stored.";
590
+ return "";
320
591
  const tools = new Set(events.map((event) => event.address)).size;
321
- return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"} · no arguments or results stored`;
592
+ return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}`;
322
593
  }
323
- const ACTIVITY_OUTCOMES = ["success", "error", "timeout", "cancelled"];
594
+ // A pause and an approval are neither success nor failure: each gets its own
595
+ // class, and the stylesheet paints neither as an error.
596
+ const ACTIVITY_OUTCOMES = [
597
+ "success",
598
+ "error",
599
+ "timeout",
600
+ "cancelled",
601
+ "paused",
602
+ "approved",
603
+ ];
324
604
  export function activityOutcomeClass(outcome) {
325
605
  return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : "error";
326
606
  }
327
- /** The one-line detail under an address: source, retries, and friction. */
607
+ /** Each outcome as a badge: a word a person reads, and a tone that is never alone. */
608
+ const OUTCOME_BADGE = {
609
+ success: { label: "Succeeded", tone: "ok" },
610
+ error: { label: "Failed", tone: "danger" },
611
+ timeout: { label: "Timed out", tone: "danger" },
612
+ cancelled: { label: "Cancelled", tone: "neutral" },
613
+ paused: { label: "Waiting for approval", tone: "warn" },
614
+ approved: { label: "Approved", tone: "ok" },
615
+ };
616
+ export function activityOutcomeBadge(outcome) {
617
+ return OUTCOME_BADGE[outcome] ?? { label: "Failed", tone: "danger" };
618
+ }
619
+ const SOURCE_LABELS = {
620
+ execute_code: "In a program",
621
+ call_tool: "Direct call",
622
+ call_destructive_tool: "Direct call, approved by the host",
623
+ resume_execution: "Resumed program",
624
+ batch_call: "Batch call",
625
+ };
626
+ /**
627
+ * Friction classes and the error codes an operator meets most, in words. An
628
+ * unknown code falls back to itself with the underscores taken out, which is
629
+ * still more readable than the wire form and never a guess at its meaning.
630
+ */
631
+ const REASON_LABELS = {
632
+ tool_not_found: "tool not found",
633
+ unknown_address: "tool not found",
634
+ unknown_tool: "tool not found",
635
+ ambiguous_tool_alias: "ambiguous tool name",
636
+ schema_retry: "arguments didn't match the schema",
637
+ invalid_args: "arguments didn't match the schema",
638
+ destructive_reroute: "needed host approval",
639
+ destructive_tool_requires_approval: "needed host approval",
640
+ approval_required: "needed approval",
641
+ auth_required: "needed authorization",
642
+ result_too_large: "result too large to return inline",
643
+ timeout: "timed out",
644
+ };
645
+ function reasonLabel(code) {
646
+ return REASON_LABELS[code] ?? code.replaceAll("_", " ");
647
+ }
648
+ /** The one-line detail under an address: where it ran, retries, and why it stalled. */
328
649
  export function activityDetail(event) {
329
- const parts = [event.source];
650
+ const parts = [SOURCE_LABELS[event.source] ?? event.source];
651
+ if (event.approval === "tool")
652
+ parts.push("approved for the rest of the run");
653
+ if (event.approval === "call")
654
+ parts.push("approved for this call");
330
655
  if (event.attempts > 1)
331
656
  parts.push(`${event.attempts} attempts`);
332
- if (event.friction)
333
- parts.push(event.friction);
334
- // The friction class and the code coincide for auth_required and
335
- // result_too_large. Printing "· auth_required · auth_required" says nothing
336
- // twice, so the coarse class stands in for both when they agree.
337
- if (event.errorCode && event.errorCode !== event.friction) {
338
- parts.push(event.errorCode);
339
- }
657
+ const reasons = [event.friction, event.errorCode]
658
+ .filter((code) => Boolean(code))
659
+ .map(reasonLabel);
660
+ // The friction class and the code often say the same thing in two
661
+ // vocabularies ("auth_required" twice); once is enough.
662
+ for (const reason of new Set(reasons))
663
+ parts.push(reason);
340
664
  return parts.join(" · ");
341
665
  }
666
+ /**
667
+ * An artifact's last refresh, when it is worth a badge. Only a failure is: a
668
+ * success is what "Current data" already says, and a superseded run is
669
+ * nobody's concern.
670
+ */
671
+ export function artifactRefreshBadge(last) {
672
+ return last?.status === "failed" ? { label: "Refresh failed", tone: "danger" } : null;
673
+ }
674
+ /**
675
+ * A stored credential that cannot be used, by cause. The payload's `error`
676
+ * beside it is not shown: it can carry a vault's own words.
677
+ */
678
+ export function credentialProblemCopy(problem) {
679
+ if (problem === "credential_mismatch")
680
+ return PROBLEM_COPY.credential_mismatch;
681
+ if (problem === "credential_unreadable") {
682
+ return "The stored credential can't be read, so it can't be used. Replace it, or remove it and add a new one.";
683
+ }
684
+ return "The stored credential can't be used. Replace it, or remove it and add a new one.";
685
+ }
342
686
  export function credentialStateLabel(credential) {
343
687
  if (!credential.configured)
344
688
  return "not configured";
@@ -0,0 +1,7 @@
1
+ // Generated by scripts/build-operator-ui.mjs. Do not edit.
2
+ // Source: src/operator-ui/tokens.css and src/operator-ui/page.css, which the
3
+ // operator UI's browser.css imports too, so every page reads one token layer.
4
+ /** Custom properties and both palettes; styles no element. */
5
+ export const TOKENS_CSS = ":root{color-scheme:light;--accent: #2f5fe0;--radius: 10px;--sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;--mono: ui-monospace, \"SF Mono\", Menlo, Monaco, \"Cascadia Code\", Consolas, monospace;--bg: #f6f7f9;--surface: #ffffff;--surface-2: #f1f3f6;--border: #e2e5ea;--border-strong: #cdd2da;--text: #131820;--muted: #5b6472;--ok: #12734a;--warn: #9a5b06;--danger: #bb3226;--on-accent: #ffffff;--link: var(--accent);--tint: color-mix(in srgb, var(--accent) 8%, var(--surface));--shell: 68rem;--pad: 1.25rem;--gap: 1rem}@media(prefers-color-scheme:dark){html:not([data-scheme=light]){color-scheme:dark}}html[data-scheme=dark]{color-scheme:dark}@media(prefers-color-scheme:dark){html:not([data-scheme=light]){--bg: #0d1016;--surface: #151a21;--surface-2: #1c222c;--border: #262d39;--border-strong: #38414f;--text: #e6eaf1;--muted: #97a1b2;--ok: #4cc48c;--warn: #e2a33f;--danger: #f4776c;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}}html[data-scheme=dark]{--bg: #0d1016;--surface: #151a21;--surface-2: #1c222c;--border: #262d39;--border-strong: #38414f;--text: #e6eaf1;--muted: #97a1b2;--ok: #4cc48c;--warn: #e2a33f;--danger: #f4776c;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}";
6
+ /** Base typography, the shell, masthead, buttons, badges, and messages. */
7
+ export const PAGE_CSS = "*{box-sizing:border-box}html{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:16px;line-height:1.5;-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh}:is(h1,h2,h3,p,ul,ol){margin:0;padding:0}:is(h1,h2,h3){font-weight:600;line-height:1.3}h1{font-size:1.5rem;letter-spacing:-.01em}h2{font-size:1rem}:is(ul,ol){list-style:none}a{color:var(--link);text-decoration-thickness:1px;text-underline-offset:2px}button,input{font:inherit}:is(a,button,input,summary):focus-visible{border-radius:calc(var(--radius) / 2);outline:2px solid var(--link);outline-offset:2px}.skip-link{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);left:var(--pad);padding:.5rem .75rem;position:fixed;top:-4rem;z-index:10}.skip-link:focus{top:var(--pad)}.shell{margin:0 auto;max-width:var(--shell);padding-left:var(--pad);padding-right:var(--pad)}.cap,.meta{color:var(--muted);font-size:.875rem}.mono{font-family:var(--mono);font-size:.8125rem}.visually-hidden{clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.masthead{align-items:center;background:var(--surface);border-bottom:1px solid var(--border);display:flex;gap:var(--gap);justify-content:space-between;max-width:none;padding-bottom:.75rem;padding-top:.75rem;position:sticky;top:0;z-index:5}.masthead-inner{align-items:center;display:flex;gap:var(--gap);justify-content:space-between;margin:0 auto;max-width:var(--shell);width:100%}.mast-nav,.mast-actions,.page-nav,.session-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.mast-nav,.mast-actions{gap:var(--gap)}.navlink{background:none;border:0;border-radius:calc(var(--radius) - 2px);color:var(--muted);cursor:pointer;font-size:.9375rem;padding:.35rem .6rem;text-decoration:none}.navlink:hover{background:var(--surface-2);color:var(--text)}.navlink[aria-current=page]{background:var(--tint);color:var(--link);font-weight:500}.brand,.product{padding-left:0;padding-right:0;text-decoration:none}.brand{color:var(--text);font-weight:600}.brand:hover,.product:hover{background:none}.product{color:var(--muted)}.page{padding-bottom:4rem;padding-top:2rem}.lead{margin-bottom:1.5rem}.lead-copy{color:var(--muted);display:grid;gap:.5rem;margin-top:.35rem}.lead-copy p{max-width:60ch}.btn{background:var(--surface);border:1px solid var(--border-strong);border-radius:calc(var(--radius) - 2px);color:var(--text);cursor:pointer;display:inline-flex;align-items:center;font-size:.875rem;gap:.35rem;padding:.35rem .7rem;text-decoration:none;white-space:nowrap}.btn:hover{background:var(--surface-2)}.btn:disabled{cursor:not-allowed;opacity:.55}.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}.btn.primary:hover{background:color-mix(in srgb,var(--accent) 88%,black)}.btn.danger{color:var(--danger)}.btn.danger:hover{background:color-mix(in srgb,var(--danger) 10%,var(--surface))}.btn.quiet{background:none;border-color:var(--border);color:var(--muted)}.btn.quiet:hover{background:var(--surface-2);color:var(--text)}.badge{align-items:center;background:var(--surface-2);border-radius:999px;color:var(--muted);display:inline-flex;font-size:.75rem;gap:.3rem;line-height:1.6;padding:.1rem .5rem;white-space:nowrap}.badge.ok{background:color-mix(in srgb,var(--ok) 12%,var(--surface));color:var(--ok)}.badge.warn{background:color-mix(in srgb,var(--warn) 14%,var(--surface));color:var(--warn)}.badge.danger{background:color-mix(in srgb,var(--danger) 12%,var(--surface));color:var(--danger)}.badge.accent{background:var(--tint);color:var(--link)}.msg{background:color-mix(in srgb,var(--danger) 8%,var(--surface));border:1px solid color-mix(in srgb,var(--danger) 28%,var(--surface));border-radius:calc(var(--radius) - 2px);color:var(--danger);font-size:.875rem;padding:.5rem .75rem}.status-page{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);display:grid;gap:.75rem;margin:1.5rem auto 0;max-width:34rem;padding:1.5rem}.status-head{align-items:center;display:flex;gap:.6rem}.status-mark{align-items:center;background:var(--surface-2);border-radius:50%;color:var(--muted);display:inline-flex;flex:none;height:2rem;justify-content:center;width:2rem}.status-mark svg{height:1.25rem;width:1.25rem}.status-mark.ok{background:color-mix(in srgb,var(--ok) 14%,var(--surface));color:var(--ok)}.status-mark.danger{background:color-mix(in srgb,var(--danger) 12%,var(--surface));color:var(--danger)}.status-label{color:var(--muted);font-size:.875rem;font-weight:600}.status-label.ok{color:var(--ok)}.status-label.danger{color:var(--danger)}.status-page h1{overflow-wrap:anywhere}.status-copy{color:var(--muted)}.status-actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:.25rem}.status-details{border-top:1px solid var(--border);color:var(--muted);font-size:.875rem;margin-top:.25rem;padding-top:.75rem}.status-details>summary{cursor:pointer;width:fit-content}.status-details>p{margin-top:.5rem}.status-details pre{background:var(--surface-2);border:1px solid var(--border);border-radius:calc(var(--radius) / 2);color:var(--text);font-family:var(--mono);font-size:.75rem;line-height:1.55;margin:.5rem 0 0;max-height:18rem;overflow:auto;overflow-wrap:anywhere;padding:.5rem .6rem;white-space:pre-wrap}@media(max-width:40rem){:root{--pad: 1rem}.status-page{margin-top:0;padding:1.25rem var(--pad)}}";