@zackbart/connecta 0.24.4 → 0.26.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.
Files changed (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -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/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  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/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -1,9 +1,44 @@
1
1
  import type { CatalogAccessObservation, CatalogDriftReport } from "../types.js";
2
+ /**
3
+ * Which call path a tool takes, decided on the server by the same
4
+ * `isExplicitlyReadOnly` predicate discovery and invocation use
5
+ * (`src/tool-safety.ts`) and never re-derived in the browser: a second copy of
6
+ * the rule is a second place for it to be wrong.
7
+ *
8
+ * - `runs_in_programs` — explicitly read-only, so `execute_code` may call it.
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`.
14
+ *
15
+ * The badge renders from a table keyed by this union, so each value is one
16
+ * entry, not a branch.
17
+ */
18
+ export type UiToolSafety = "runs_in_programs" | "exempt" | "needs_approval";
2
19
  export interface UiTool {
3
20
  name: string;
4
21
  address: string;
5
22
  description?: string;
23
+ /** Absent only in payloads older than the field; the page then shows no badge. */
24
+ safety?: UiToolSafety;
6
25
  }
26
+ /**
27
+ * What went wrong with a connector, as a closed set the server chooses from.
28
+ * The browser keys both its on-screen description and a fixed fix prompt off
29
+ * this. There is deliberately no message beside it: a connector's status
30
+ * message can quote a downstream error body, so it goes to the deployment's
31
+ * log and never into the operator payload.
32
+ *
33
+ * `auth_required` splits by who has to act: a downstream OAuth grant
34
+ * (`oauth_required`, which also covers an expired or revoked grant — the
35
+ * status seam cannot tell those apart), an operator-managed credential slot
36
+ * (`credential_required`), or a secret that lives in deployment configuration
37
+ * (`auth_required`).
38
+ */
39
+ export type UiProblem = "connector_unavailable" | "oauth_required" | "credential_required" | "auth_required" | "credential_mismatch" | "catalog_failed";
40
+ /** A stored credential that cannot be used, by cause. */
41
+ export type UiCredentialProblem = "credential_mismatch" | "credential_unreadable";
7
42
  interface UiCredentialField {
8
43
  name: string;
9
44
  label: string;
@@ -26,6 +61,8 @@ interface UiCredential {
26
61
  updatedAt?: string;
27
62
  testable: boolean;
28
63
  error?: string;
64
+ /** Present exactly when `error` is: the cause, for the fix prompt. */
65
+ problem?: UiCredentialProblem;
29
66
  /**
30
67
  * Something true and non-blocking about a working credential — today, that
31
68
  * the vault still holds fields the connector has stopped declaring.
@@ -45,7 +82,12 @@ export interface UiConnector {
45
82
  manageSharedAuth: boolean;
46
83
  connectPersonal: boolean;
47
84
  };
48
- message?: string;
85
+ /**
86
+ * Why this connector is not usable, when it is not. See `UiProblem`. The
87
+ * only failure detail the payload carries; the status message behind it is
88
+ * logged on the server, not shipped.
89
+ */
90
+ problem?: UiProblem;
49
91
  authorizationUrl?: string;
50
92
  toolCount: number;
51
93
  tools: UiTool[];
@@ -77,6 +119,66 @@ export interface UiData {
77
119
  credentialManagement: CredentialManagementCapability;
78
120
  /** True when this interactive human may manage any visible OAuth connector. */
79
121
  oauthManagement: boolean;
122
+ /**
123
+ * Names of the `/mcp/<name>` pools this identity's grant admits. A pool
124
+ * whose grant refuses or throws is absent, exactly as the endpoint answers
125
+ * it with a 404, so the page never enumerates a pool its reader cannot open.
126
+ */
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;
80
182
  }
81
183
  export interface FilteredUiConnector {
82
184
  connector: UiConnector;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Client setup snippets for one MCP endpoint: the deployment's `/mcp` or a
3
+ * `/mcp/<pool>`. Three formats and no more — Claude Code's CLI, Codex's CLI,
4
+ * and the `mcpServers` JSON most other clients read — because a longer list
5
+ * is a list to keep current.
6
+ *
7
+ * None of them carries a credential. A client authenticates through the
8
+ * deployment's inbound auth — OAuth discovery where that is configured — and
9
+ * a token pasted into a snippet on this page would be one rendered by it.
10
+ */
11
+ /**
12
+ * The name a client files this server under. Derived from the deployment's
13
+ * configured server name, reduced to the characters every client CLI accepts
14
+ * unquoted, with the pool appended so two endpoints never collide.
15
+ */
16
+ export function clientServerName(serverName, pool) {
17
+ const base = (serverName ?? "")
18
+ .toLowerCase()
19
+ .replace(/[^a-z0-9_-]+/g, "-")
20
+ .replace(/-{2,}/g, "-")
21
+ .replace(/^-+|-+$/g, "")
22
+ .slice(0, 48) || "connecta";
23
+ return pool ? `${base}-${pool}` : base;
24
+ }
25
+ /** The URL for a pool: `/mcp` plus its name, which config limits to `[a-z0-9_-]`. */
26
+ export function poolEndpointUrl(mcpUrl, pool) {
27
+ return `${mcpUrl.replace(/\/+$/, "")}/${encodeURIComponent(pool)}`;
28
+ }
29
+ /** POSIX single-quoting, only when the value needs it. */
30
+ function shellWord(value) {
31
+ return /^[A-Za-z0-9_./:@%+=,~-]+$/.test(value)
32
+ ? value
33
+ : `'${value.replace(/'/g, `'"'"'`)}'`;
34
+ }
35
+ export function clientSetupCommands(name, url) {
36
+ return [
37
+ {
38
+ id: "claude",
39
+ label: "Claude Code",
40
+ text: `claude mcp add --transport http ${shellWord(name)} ${shellWord(url)}`,
41
+ },
42
+ {
43
+ id: "codex",
44
+ label: "Codex",
45
+ text: `codex mcp add ${shellWord(name)} --url ${shellWord(url)}`,
46
+ },
47
+ {
48
+ id: "json",
49
+ label: "JSON config",
50
+ text: JSON.stringify({ mcpServers: { [name]: { type: "http", url } } }, null, 2),
51
+ },
52
+ ];
53
+ }
@@ -1,20 +1,84 @@
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
  };
9
13
  export function pageForPath(path) {
14
+ if (path.startsWith("/artifacts/"))
15
+ return "artifact";
10
16
  const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === path);
11
17
  return match ?? "connections";
12
18
  }
19
+ /** Artifact pages load their own data and never ask `/ui/data` whether to open. */
20
+ export function isArtifactPage(page) {
21
+ return page === "artifacts" || page === "artifact";
22
+ }
23
+ /**
24
+ * Where a viewer's frame gets its page: `/artifacts/<id>` or a snapshot,
25
+ * `/artifacts/<id>/v/<version>?d=<name>:<version>…`, mapped to its API call.
26
+ */
27
+ export function artifactViewRequest(pathname, search) {
28
+ const match = /^\/artifacts\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\/v\/(\d{1,9}))?$/.exec(pathname);
29
+ if (!match?.[1])
30
+ return undefined;
31
+ const params = new URLSearchParams();
32
+ if (match[2]) {
33
+ params.set("v", match[2]);
34
+ for (const pin of new URLSearchParams(search).getAll("d"))
35
+ params.append("d", pin);
36
+ }
37
+ const query = params.toString();
38
+ return `/artifacts/_api/view/${match[1]}${query ? `?${query}` : ""}`;
39
+ }
13
40
  export function info(message) {
14
41
  return { message, tone: "info" };
15
42
  }
16
- export function failure(message) {
17
- return { message, tone: "error" };
43
+ export function failure(message, fix) {
44
+ return fix ? { message, tone: "error", fix } : { message, tone: "error" };
45
+ }
46
+ const REFUSED_COPY = {
47
+ oauth_disconnect: "OAuth disconnect failed. If the downstream refused it, the deployment's log has its reply.",
48
+ oauth_reconnect: "OAuth authorization could not restart. If the downstream refused it, the deployment's log has its reply.",
49
+ credential_test: "The credential test could not run.",
50
+ };
51
+ /** The route's answer to a finished OAuth action. Its state picks the sentence. */
52
+ export function oauthDoneNotice(action, answer) {
53
+ if (action === "oauth_disconnect") {
54
+ return info("OAuth disconnected. Restart authorization when you are ready to reconnect.");
55
+ }
56
+ return info(answer?.state === "ok"
57
+ ? "OAuth reconnected."
58
+ : "Authorization restarted. Open the authorization link to reconnect.");
59
+ }
60
+ /** A credential Test's result. `ok` is the only part of the answer it reads. */
61
+ export function credentialTestNotice(connectorId, answer) {
62
+ return answer?.ok === true
63
+ ? info("Credential is valid.")
64
+ : failure("Credential test failed: the downstream rejected the stored credential, or the test could not reach it. The deployment's log has the downstream's reply.", { kind: "credential_test_failed", connectorId });
65
+ }
66
+ /**
67
+ * A route that refused one of these actions. A credential Test that found
68
+ * nothing usable to test names the problem, which picks the same copy and fix
69
+ * prompt the Connections page uses for it; every other refusal gets the
70
+ * action's one sentence. `problem` is whatever the response carried, so it is
71
+ * checked, not trusted.
72
+ */
73
+ export function refusedNotice(action, connectorId, problem) {
74
+ if (action === "credential_test" &&
75
+ (problem === "credential_required" || problem === "credential_mismatch")) {
76
+ return failure(PROBLEM_COPY[problem], { kind: problem, connectorId });
77
+ }
78
+ return failure(REFUSED_COPY[action], {
79
+ kind: action === "credential_test" ? "credential_test_failed" : "oauth_action_failed",
80
+ connectorId,
81
+ });
18
82
  }
19
83
  export function initialState(page) {
20
84
  return {
@@ -46,6 +110,13 @@ function identityScopedState() {
46
110
  activityEvents: [],
47
111
  activityCursor: null,
48
112
  activitySearch: "",
113
+ artifactPhase: "idle",
114
+ artifactNotice: null,
115
+ artifactRows: [],
116
+ artifactCursor: null,
117
+ artifactQuery: "",
118
+ artifactArchived: false,
119
+ artifactView: null,
49
120
  };
50
121
  }
51
122
  /**
@@ -150,6 +221,47 @@ export function connectorSummaryParts(summary) {
150
221
  { text: toolCountLabel(summary.tools), tone: "neutral" },
151
222
  ];
152
223
  }
224
+ /**
225
+ * The badge for each call path, keyed by the server's classification so each
226
+ * state is a row here rather than a branch in a component.
227
+ */
228
+ export const TOOL_SAFETY_BADGE = {
229
+ runs_in_programs: {
230
+ label: "runs in programs",
231
+ tone: "ok",
232
+ title: "Explicitly read-only: execute_code programs may call it without asking.",
233
+ },
234
+ exempt: {
235
+ label: "exempt from approval",
236
+ tone: "neutral",
237
+ 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.",
238
+ },
239
+ needs_approval: {
240
+ label: "asks for approval",
241
+ tone: "warn",
242
+ 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.",
243
+ },
244
+ };
245
+ /**
246
+ * What the page says about a connector that is not usable, one fixed sentence
247
+ * per problem kind the server classified. This is the only account of a
248
+ * failure the Connections page renders: a connector's status message can quote
249
+ * a downstream error body, and a downstream error body can quote the secret
250
+ * that was just rejected, so the raw text stays in the deployment's log and
251
+ * never reaches the payload or the DOM. The fix prompt beside it is keyed off
252
+ * the same kind.
253
+ */
254
+ const PROBLEM_COPY = {
255
+ connector_unavailable: "Unavailable: its status check or catalog load failed, or did not finish in time. The deployment's log has the downstream error.",
256
+ oauth_required: "Needs OAuth authorization: no grant is stored, or the stored grant expired or was revoked.",
257
+ credential_required: "Needs a credential: nothing usable is stored in its credential slot.",
258
+ auth_required: "Needs authorization. Its secret lives in deployment configuration, not on this page.",
259
+ credential_mismatch: "The stored credential does not match the fields this connector declares, so it cannot be used.",
260
+ catalog_failed: "Connected, but its tool catalog could not be loaded, so none of its tools are served.",
261
+ };
262
+ export function problemCopy(problem) {
263
+ return problem ? PROBLEM_COPY[problem] ?? null : null;
264
+ }
153
265
  /** Who owns this connector's downstream credentials, in two words. */
154
266
  export function authScopeLabel(scope) {
155
267
  return scope === "personal" ? "personal auth" : "shared auth";
@@ -284,13 +396,26 @@ export function activitySummary(events) {
284
396
  const tools = new Set(events.map((event) => event.address)).size;
285
397
  return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"} · no arguments or results stored`;
286
398
  }
287
- const ACTIVITY_OUTCOMES = ["success", "error", "timeout", "cancelled"];
399
+ // A pause and an approval are neither success nor failure: each gets its own
400
+ // class, and the stylesheet paints neither as an error.
401
+ const ACTIVITY_OUTCOMES = [
402
+ "success",
403
+ "error",
404
+ "timeout",
405
+ "cancelled",
406
+ "paused",
407
+ "approved",
408
+ ];
288
409
  export function activityOutcomeClass(outcome) {
289
410
  return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : "error";
290
411
  }
291
412
  /** The one-line detail under an address: source, retries, and friction. */
292
413
  export function activityDetail(event) {
293
414
  const parts = [event.source];
415
+ if (event.approval === "tool")
416
+ parts.push("approved for the rest of the run");
417
+ if (event.approval === "call")
418
+ parts.push("approved for this call");
294
419
  if (event.attempts > 1)
295
420
  parts.push(`${event.attempts} attempts`);
296
421
  if (event.friction)
@@ -0,0 +1,321 @@
1
+ // Program source handling shared by every executor. Web-API only: execute_code
2
+ // runs it before handing a program to QuickJS or a Dynamic Worker, and the
3
+ // QuickJS normalizer runs it too for callers that drive that executor directly.
4
+ /** A whole-program markdown fence, as QuickJS's normalizer recognizes one. */
5
+ const FENCE = /^```[\w-]*\s*\n([\s\S]*?)\n?```$/;
6
+ /** Intake accepts one JavaScript fence, optionally surrounded by prose. */
7
+ const PROGRAM_FENCE = /^```(?:js|javascript)?[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/gm;
8
+ /** Leading whitespace and comments, which may precede the arrow expression. */
9
+ const LEADING_TRIVIA = /^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)+/;
10
+ /** One character of an identifier, keyword, or number; `\\` for escapes. */
11
+ const WORD = /[\w$\\\u0080-\uffff]/;
12
+ /**
13
+ * Words after which a `/` begins a regular expression rather than dividing.
14
+ * Any other word is an identifier or a literal, which a `/` divides.
15
+ */
16
+ const REGEX_AFTER_WORD = new Set([
17
+ "await", "case", "delete", "do", "else", "in", "instanceof", "new", "of",
18
+ "return", "throw", "typeof", "void", "yield",
19
+ ]);
20
+ /**
21
+ * Remove the statement terminators after a program's arrow expression:
22
+ * `async () => { … };`, with any whitespace or comments around the trailing
23
+ * semicolons, becomes `async () => { … }` with those comments kept. Both
24
+ * executors evaluate a program as a parenthesized expression, where a
25
+ * terminator is a syntax error on QuickJS and, on a Dynamic Worker, either the
26
+ * same error or a program that silently returns `undefined`. Models often end
27
+ * a program with `};`, so P1 accepts it on both.
28
+ *
29
+ * Deliberately narrow, so it cannot broaden what a program is: only a program
30
+ * whose first token is `async` or `(` — what QuickJS treats as a function
31
+ * expression — and only when every semicolon at bracket depth zero trails it,
32
+ * with nothing but whitespace and comments after. `async () => 1; 2();` keeps
33
+ * both its semicolons, and a bare statement body is never touched, since its
34
+ * trailing `;` can be an empty loop body. A whole-program markdown fence is
35
+ * looked through, and dropped when a terminator came out from under it.
36
+ *
37
+ * A small lexer, not a parser: it skips strings, template literals (with
38
+ * `${}` nesting), comments, and regular expressions, telling a regex from a
39
+ * division by the token before it. Any source it cannot close — an
40
+ * unterminated string or comment, unbalanced brackets — comes back unchanged,
41
+ * so the executor reports the same error it always did.
42
+ */
43
+ export function withoutProgramTerminator(code) {
44
+ const fenced = FENCE.exec(code.trim())?.[1];
45
+ const source = fenced === undefined ? code : fenced.trim();
46
+ const stripped = stripTerminators(source);
47
+ return stripped === source ? code : stripped;
48
+ }
49
+ /**
50
+ * Recover wrappers that models put around a single program. This runs in the
51
+ * host, so QuickJS and Dynamic Workers receive the same source. It does not
52
+ * turn arbitrary statements into a program: an arrow remains the contract.
53
+ */
54
+ export function normalizeProgramSource(code) {
55
+ let source = code.trim();
56
+ const head = source.replace(LEADING_TRIVIA, "");
57
+ // A program may itself contain fenced Markdown in a comment or string. Once
58
+ // the source starts as a program, a fence inside it is data, never a wrapper.
59
+ if (!/^(?:async\b|\(|export\s+default\s+async\b)/.test(head)) {
60
+ const fences = [...source.matchAll(PROGRAM_FENCE)];
61
+ if (fences.length === 1) {
62
+ const fence = fences[0];
63
+ const outside = source.slice(0, fence.index) + source.slice(fence.index + fence[0].length);
64
+ const body = fence[1].trim();
65
+ const fencedHead = body.replace(LEADING_TRIVIA, "");
66
+ if (!outside.includes("```") && /^(?:async\b|\(|export\s+default\s+async\b)/.test(fencedHead)) {
67
+ source = body;
68
+ }
69
+ }
70
+ }
71
+ source = source.replace(/^export\s+default\s+(?=async\s*(?:\(|[A-Za-z_$]))/, "");
72
+ // The declaration must be the whole source. A scanner finds its own closing
73
+ // brace rather than mistaking a brace in a string or a second statement for
74
+ // the end of the function.
75
+ const named = /^async\s+function\s+([A-Za-z_$][\w$]*)\s*\(([^()]*)\)\s*\{/.exec(source);
76
+ if (named) {
77
+ const end = functionBodyEnd(source, named[0].length - 1);
78
+ if (end !== undefined && /^(?:\s|;|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*$/.test(source.slice(end + 1))) {
79
+ // Invoke a named function expression inside the arrow. Its own name and
80
+ // `arguments` binding then work exactly as in the original declaration.
81
+ source = `async (...args) => (async function ${named[1]}(${named[2]}) {${source.slice(named[0].length, end)}})(...args)${source.slice(end + 1)}`;
82
+ }
83
+ }
84
+ return withoutProgramTerminator(source);
85
+ }
86
+ /** Find the matching closing brace of a named function's body. */
87
+ function functionBodyEnd(source, open) {
88
+ let depth = 0;
89
+ let previous = "";
90
+ for (let i = open; i < source.length; i++) {
91
+ const c = source[i];
92
+ if (/\s/.test(c))
93
+ continue;
94
+ if (c === "/" && source[i + 1] === "/") {
95
+ const end = source.indexOf("\n", i + 2);
96
+ i = end === -1 ? source.length : end;
97
+ continue;
98
+ }
99
+ if (c === "/" && source[i + 1] === "*") {
100
+ const end = source.indexOf("*/", i + 2);
101
+ if (end === -1)
102
+ return undefined;
103
+ i = end + 1;
104
+ continue;
105
+ }
106
+ if (c === "'" || c === '"' || c === "`") {
107
+ let end = i + 1;
108
+ for (; end < source.length && source[end] !== c; end++) {
109
+ if (source[end] === "\\")
110
+ end++;
111
+ else if (source[end] === "\n" && c !== "`")
112
+ return undefined;
113
+ }
114
+ if (end >= source.length)
115
+ return undefined;
116
+ i = end;
117
+ previous = "0";
118
+ continue;
119
+ }
120
+ if (c === "/" && startsRegex(previous)) {
121
+ let end = i + 1;
122
+ let inClass = false;
123
+ for (; end < source.length; end++) {
124
+ const r = source[end];
125
+ if (r === "\n")
126
+ return undefined;
127
+ if (r === "\\")
128
+ end++;
129
+ else if (r === "[")
130
+ inClass = true;
131
+ else if (r === "]")
132
+ inClass = false;
133
+ else if (r === "/" && !inClass)
134
+ break;
135
+ }
136
+ if (end >= source.length)
137
+ return undefined;
138
+ i = end;
139
+ previous = "0";
140
+ continue;
141
+ }
142
+ if (WORD.test(c)) {
143
+ let end = i + 1;
144
+ while (end < source.length && WORD.test(source[end]))
145
+ end++;
146
+ previous = source.slice(i, end);
147
+ i = end - 1;
148
+ continue;
149
+ }
150
+ if (c === "{")
151
+ depth++;
152
+ else if (c === "}") {
153
+ depth--;
154
+ if (depth === 0)
155
+ return i;
156
+ if (depth < 0)
157
+ return undefined;
158
+ }
159
+ previous = c;
160
+ }
161
+ return undefined;
162
+ }
163
+ function stripTerminators(source) {
164
+ const head = source.replace(LEADING_TRIVIA, "");
165
+ if (!/^async\b/.test(head) && !head.startsWith("("))
166
+ return source;
167
+ const n = source.length;
168
+ let depth = 0;
169
+ /** Bracket depth at which each open template substitution resumes its template. */
170
+ const substitutions = [];
171
+ /** Positions of the top-level semicolons, all of which must trail. */
172
+ const terminators = [];
173
+ /**
174
+ * The previous significant token: a punctuator character or a word, with a
175
+ * string, template, or regex literal recorded as `0` — a value, like a number.
176
+ */
177
+ let previous = "";
178
+ let i = 0;
179
+ /** Scan template text from `from` to its closing backtick or next `${`. */
180
+ const template = (from) => {
181
+ for (let j = from; j < n; j++) {
182
+ const c = source[j];
183
+ if (c === "\\") {
184
+ j++;
185
+ }
186
+ else if (c === "`") {
187
+ return j + 1;
188
+ }
189
+ else if (c === "$" && source[j + 1] === "{") {
190
+ substitutions.push(depth);
191
+ depth++;
192
+ return j + 2;
193
+ }
194
+ }
195
+ return undefined;
196
+ };
197
+ while (i < n) {
198
+ const c = source[i];
199
+ if (/\s/.test(c)) {
200
+ i++;
201
+ continue;
202
+ }
203
+ if (c === "/" && source[i + 1] === "/") {
204
+ const end = source.indexOf("\n", i);
205
+ i = end === -1 ? n : end;
206
+ continue;
207
+ }
208
+ if (c === "/" && source[i + 1] === "*") {
209
+ const end = source.indexOf("*/", i + 2);
210
+ if (end === -1)
211
+ return source;
212
+ i = end + 2;
213
+ continue;
214
+ }
215
+ if (c === ";") {
216
+ if (depth === 0)
217
+ terminators.push(i);
218
+ previous = ";";
219
+ i++;
220
+ continue;
221
+ }
222
+ // Code after a top-level semicolon means the program is more than one
223
+ // expression; that is for the executor to refuse, not for us to repair.
224
+ if (terminators.length > 0)
225
+ return source;
226
+ if (c === '"' || c === "'") {
227
+ let j = i + 1;
228
+ while (j < n && source[j] !== c) {
229
+ if (source[j] === "\n")
230
+ return source;
231
+ j += source[j] === "\\" ? 2 : 1;
232
+ }
233
+ if (j >= n)
234
+ return source;
235
+ i = j + 1;
236
+ previous = "0";
237
+ continue;
238
+ }
239
+ if (c === "`") {
240
+ const end = template(i + 1);
241
+ if (end === undefined)
242
+ return source;
243
+ i = end;
244
+ previous = source[end - 1] === "`" ? "0" : "{";
245
+ continue;
246
+ }
247
+ if (c === "}" && substitutions.at(-1) === depth - 1) {
248
+ substitutions.pop();
249
+ depth--;
250
+ const end = template(i + 1);
251
+ if (end === undefined)
252
+ return source;
253
+ i = end;
254
+ previous = source[end - 1] === "`" ? "0" : "{";
255
+ continue;
256
+ }
257
+ if (c === "/" && startsRegex(previous)) {
258
+ let j = i + 1;
259
+ let inClass = false;
260
+ for (; j < n; j++) {
261
+ const r = source[j];
262
+ if (r === "\n")
263
+ return source;
264
+ if (r === "\\")
265
+ j++;
266
+ else if (r === "[")
267
+ inClass = true;
268
+ else if (r === "]")
269
+ inClass = false;
270
+ else if (r === "/" && !inClass)
271
+ break;
272
+ }
273
+ if (j >= n)
274
+ return source;
275
+ i = j + 1;
276
+ while (i < n && /[\w$]/.test(source[i]))
277
+ i++;
278
+ previous = "0";
279
+ continue;
280
+ }
281
+ if (WORD.test(c)) {
282
+ let j = i + 1;
283
+ while (j < n && WORD.test(source[j]))
284
+ j++;
285
+ previous = source.slice(i, j);
286
+ i = j;
287
+ continue;
288
+ }
289
+ if (c === "(" || c === "[" || c === "{")
290
+ depth++;
291
+ else if (c === ")" || c === "]" || c === "}") {
292
+ if (depth === 0)
293
+ return source;
294
+ depth--;
295
+ }
296
+ previous = c;
297
+ i++;
298
+ }
299
+ if (depth !== 0 || substitutions.length > 0 || terminators.length === 0) {
300
+ return source;
301
+ }
302
+ let out = "";
303
+ let from = 0;
304
+ for (const at of terminators) {
305
+ out += source.slice(from, at);
306
+ from = at + 1;
307
+ }
308
+ return out + source.slice(from);
309
+ }
310
+ /** Whether a `/` after `previous` opens a regular expression. */
311
+ function startsRegex(previous) {
312
+ if (previous === "")
313
+ return true;
314
+ if (WORD.test(previous[0])) {
315
+ // A number or an identifier divides; a keyword introduces an operand.
316
+ return REGEX_AFTER_WORD.has(previous);
317
+ }
318
+ // After a closing bracket a value precedes; after any other punctuator
319
+ // an operand is expected.
320
+ return previous !== ")" && previous !== "]" && previous !== "}";
321
+ }