@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -1,16 +1,37 @@
1
- /** Pages the nav lists. A single artifact is reached from the library, not the nav. */
1
+ /**
2
+ * Everything the operator app knows, and every rule for changing it, with no
3
+ * DOM in sight. The components render this and nothing else, so the questions
4
+ * that matter — what a page shows while it loads, what an identity change
5
+ * erases, whether a value may become an href — are answered by functions a test
6
+ * can call directly rather than by reading rendered markup back out of a
7
+ * browser.
8
+ */
9
+ const REGISTRATION_LABELS = {
10
+ cimd: "Client metadata document (CIMD)",
11
+ dcr: "Dynamic registration (DCR)",
12
+ static: "Pre-registered client",
13
+ };
14
+ export function registrationPathLabel(path) {
15
+ return REGISTRATION_LABELS[path];
16
+ }
17
+ /** Pages the nav lists. */
2
18
  export const OPERATOR_PAGES = [
19
+ "overview",
3
20
  "connections",
4
- "tokens",
21
+ "tools",
22
+ "access",
5
23
  "activity",
6
- "artifacts",
24
+ "config",
7
25
  ];
8
26
  export const PAGE_META = {
27
+ overview: { path: "/", label: "Overview" },
28
+ connector: { path: "/connectors", label: "Connector" },
29
+ tools: { path: "/tools", label: "Tools" },
30
+ access: { path: "/access", label: "Access" },
31
+ config: { path: "/config", label: "Config" },
9
32
  tokens: { path: "/tokens", label: "Access tokens" },
10
- connections: { path: "/", label: "Connections" },
33
+ connections: { path: "/connectors", label: "Connectors" },
11
34
  activity: { path: "/activity", label: "Activity" },
12
- artifacts: { path: "/artifacts", label: "Artifacts" },
13
- artifact: { path: "/artifacts", label: "Artifact" },
14
35
  };
15
36
  /**
16
37
  * What each page is for, in one line under its heading — on the gate as well
@@ -21,46 +42,20 @@ export function pageDescription(page, productDescription) {
21
42
  if (page === "activity") {
22
43
  return "Every connector tool call, by who made it and how it ended. Arguments and results are never stored.";
23
44
  }
24
- if (isArtifactPage(page)) {
25
- return "Pages agents published for the team. Each one keeps every version.";
26
- }
27
45
  return productDescription;
28
46
  }
29
47
  /** What the gate says while the session is checked, page by page. */
30
- export function checkingCopy(page) {
31
- if (page === "artifact")
32
- return "Loading artifact…";
33
- if (page === "artifacts")
34
- return "Loading artifacts…";
48
+ export function checkingCopy() {
35
49
  return "Checking your session…";
36
50
  }
37
51
  export function pageForPath(path) {
38
- if (path.startsWith("/artifacts/"))
39
- return "artifact";
52
+ if (/^\/connectors\/[a-z0-9_-]+$/.test(path))
53
+ return "connector";
54
+ if (path === "/tokens")
55
+ return "tokens";
40
56
  const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === path);
41
57
  return match ?? "connections";
42
58
  }
43
- /** Artifact pages load their own data and never ask `/ui/data` whether to open. */
44
- export function isArtifactPage(page) {
45
- return page === "artifacts" || page === "artifact";
46
- }
47
- /**
48
- * Where a viewer's frame gets its page: `/artifacts/<id>` or a snapshot,
49
- * `/artifacts/<id>/v/<version>?d=<name>:<version>…`, mapped to its API call.
50
- */
51
- export function artifactViewRequest(pathname, search) {
52
- const match = /^\/artifacts\/([a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)(?:\/v\/(\d{1,9}))?$/.exec(pathname);
53
- if (!match?.[1])
54
- return undefined;
55
- const params = new URLSearchParams();
56
- if (match[2]) {
57
- params.set("v", match[2]);
58
- for (const pin of new URLSearchParams(search).getAll("d"))
59
- params.append("d", pin);
60
- }
61
- const query = params.toString();
62
- return `/artifacts/_api/view/${match[1]}${query ? `?${query}` : ""}`;
63
- }
64
59
  export function info(message) {
65
60
  return { message, tone: "info" };
66
61
  }
@@ -78,6 +73,8 @@ const REFUSED_COPY = {
78
73
  */
79
74
  export function oauthDoneNotice(action, answer, opened = true) {
80
75
  if (action === "oauth_disconnect") {
76
+ if (answer?.code === "oauth_revocation_failed")
77
+ return info("Disconnected locally. Provider revocation could not be confirmed. Revoke the grant in the provider's console.");
81
78
  return info("Disconnected. Connect again whenever you are ready.");
82
79
  }
83
80
  if (answer?.state === "ok")
@@ -100,8 +97,7 @@ export function credentialTestNotice(connectorId, answer) {
100
97
  * checked, not trusted.
101
98
  */
102
99
  export function refusedNotice(action, connectorId, problem) {
103
- if (action === "credential_test" &&
104
- (problem === "credential_required" || problem === "credential_mismatch")) {
100
+ if (action === "credential_test" && (problem === "credential_required" || problem === "credential_mismatch")) {
105
101
  return failure(PROBLEM_COPY[problem], { kind: problem, connectorId });
106
102
  }
107
103
  return failure(REFUSED_COPY[action], {
@@ -166,18 +162,14 @@ export function loadFailureCopy(failure, productName) {
166
162
  };
167
163
  }
168
164
  /** A collection page's load failure: fixed words, whatever the route said. */
169
- export function collectionFailureCopy(collection, facts, productName) {
165
+ export function collectionFailureCopy(_collection, facts, productName) {
170
166
  if (facts.kind === "network") {
171
167
  return `Couldn't reach ${productName}. Check your connection, then retry.`;
172
168
  }
173
169
  if (facts.kind === "session")
174
170
  return "Your session has ended. Sign in again to see this page.";
175
171
  if (facts.kind === "forbidden" || facts.status === 404) {
176
- if (collection === "artifact")
177
- return "There is no artifact here, or this identity can't open it.";
178
- return collection === "activity"
179
- ? "Activity isn't available to this identity."
180
- : "Artifacts aren't available to this identity.";
172
+ return "Activity isn't available to this identity.";
181
173
  }
182
174
  return "The deployment answered with an error. Retry in a moment; if it keeps failing, the deployment's log has the reason.";
183
175
  }
@@ -226,6 +218,9 @@ export function initialState(page) {
226
218
  function identityScopedState() {
227
219
  return {
228
220
  data: null,
221
+ contract: null,
222
+ contractPhase: "idle",
223
+ health: "loading",
229
224
  connectorFilter: "",
230
225
  oauthNotice: null,
231
226
  oauthNoticeFor: null,
@@ -248,13 +243,6 @@ function identityScopedState() {
248
243
  activityEvents: [],
249
244
  activityCursor: null,
250
245
  activitySearch: "",
251
- artifactPhase: "idle",
252
- artifactNotice: null,
253
- artifactRows: [],
254
- artifactCursor: null,
255
- artifactQuery: "",
256
- artifactArchived: false,
257
- artifactView: null,
258
246
  };
259
247
  }
260
248
  /**
@@ -310,117 +298,22 @@ export function connectorStatusLabel(status, problem) {
310
298
  return "Loading details";
311
299
  if (status === "ok")
312
300
  return "Connected";
301
+ if (status === "credential_required")
302
+ return "Credential needed";
313
303
  if (status === "auth_required") {
314
304
  return problem === "credential_required" ? "Credential needed" : "Authorization needed";
315
305
  }
316
306
  return "Unavailable";
317
307
  }
318
- export function toolCountLabel(count) {
319
- return `${count} ${count === 1 ? "tool" : "tools"}`;
320
- }
321
308
  export function connectorStatusTone(status) {
322
309
  if (status === "ok")
323
310
  return "ok";
324
- if (status === "auth_required")
311
+ if (status === "auth_required" || status === "credential_required")
325
312
  return "warn";
326
313
  if (status === "loading")
327
314
  return "neutral";
328
315
  return "danger";
329
316
  }
330
- export function summarizeConnectors(connectors) {
331
- const summary = {
332
- total: connectors.length,
333
- connected: 0,
334
- attention: 0,
335
- credentials: 0,
336
- unavailable: 0,
337
- loading: 0,
338
- tools: 0,
339
- drifting: 0,
340
- };
341
- for (const connector of connectors) {
342
- if (connector.status === "ok")
343
- summary.connected += 1;
344
- else if (connector.status === "auth_required") {
345
- if (connector.problem === "credential_required")
346
- summary.credentials += 1;
347
- else
348
- summary.attention += 1;
349
- }
350
- else if (connector.status === "loading")
351
- summary.loading += 1;
352
- else
353
- summary.unavailable += 1;
354
- summary.tools += connector.toolCount || 0;
355
- if (driftState(connector.catalogDrift) === "warning")
356
- summary.drifting += 1;
357
- }
358
- return summary;
359
- }
360
- /**
361
- * The one line above the list. Connected and tool counts are always present;
362
- * the two counts an operator may have to act on appear only when they are not
363
- * zero, so a healthy deployment stays short.
364
- */
365
- export function connectorSummaryParts(summary) {
366
- // Nothing has answered yet: "0 connected · 0 tools" would be a claim, and
367
- // a false one.
368
- if (summary.total > 0 && summary.loading === summary.total) {
369
- return [
370
- {
371
- text: `Checking ${summary.total} connector${summary.total === 1 ? "" : "s"}…`,
372
- tone: "neutral",
373
- },
374
- ];
375
- }
376
- return [
377
- { text: `${summary.connected} connected`, tone: "neutral" },
378
- ...(summary.attention
379
- ? [
380
- {
381
- text: `${summary.attention} need${summary.attention === 1 ? "s" : ""} authorization`,
382
- tone: "warn",
383
- },
384
- ]
385
- : []),
386
- ...(summary.credentials
387
- ? [
388
- {
389
- text: `${summary.credentials} need${summary.credentials === 1 ? "s" : ""} a credential`,
390
- tone: "warn",
391
- },
392
- ]
393
- : []),
394
- ...(summary.unavailable
395
- ? [{ text: `${summary.unavailable} unavailable`, tone: "danger" }]
396
- : []),
397
- { text: toolCountLabel(summary.tools), tone: "neutral" },
398
- ...(summary.loading
399
- ? [{ text: `${summary.loading} still loading`, tone: "neutral" }]
400
- : []),
401
- ];
402
- }
403
- /**
404
- * The badge for each call path, keyed by the server's classification so each
405
- * state is a row here rather than a branch in a component.
406
- */
407
- export const TOOL_SAFETY_BADGE = {
408
- runs_in_programs: {
409
- label: "runs in programs",
410
- tone: "ok",
411
- title: "Explicitly read-only: execute_code programs may call it without asking.",
412
- },
413
- exempt: {
414
- label: "exempt from approval",
415
- tone: "neutral",
416
- title: "Not read-only, but this deployment's config lets programs call it without asking. Each call still counts against the write budget and appears in activity; call_tool still refuses it.",
417
- },
418
- needs_approval: {
419
- label: "asks for approval",
420
- tone: "warn",
421
- title: "Not explicitly read-only: programs refuse it, and it runs through call_destructive_tool, where the host asks first.",
422
- },
423
- };
424
317
  /**
425
318
  * What the page says about a connector that is not usable, one fixed sentence
426
319
  * per problem kind the server classified. This is the only account of a
@@ -439,7 +332,7 @@ const PROBLEM_COPY = {
439
332
  catalog_failed: "Connected, but its tool catalog could not be loaded, so none of its tools are served.",
440
333
  };
441
334
  export function problemCopy(problem) {
442
- return problem ? PROBLEM_COPY[problem] ?? null : null;
335
+ return problem ? (PROBLEM_COPY[problem] ?? null) : null;
443
336
  }
444
337
  /**
445
338
  * A problem someone can fix by authorizing is a warning, matching the amber
@@ -447,16 +340,10 @@ export function problemCopy(problem) {
447
340
  * error. The two used to share the red box, so the row contradicted itself.
448
341
  */
449
342
  export function problemTone(problem) {
450
- return problem === "oauth_required" ||
451
- problem === "credential_required" ||
452
- problem === "auth_required"
343
+ return problem === "oauth_required" || problem === "credential_required" || problem === "auth_required"
453
344
  ? "warn"
454
345
  : "danger";
455
346
  }
456
- /** Who owns this connector's downstream credentials, in two words. */
457
- export function authScopeLabel(scope) {
458
- return scope === "personal" ? "personal auth" : "shared auth";
459
- }
460
347
  /**
461
348
  * What this identity may do with a connector, in one line. Being able to use it
462
349
  * is implied by seeing it at all, so the sentence covers authentication, which
@@ -472,7 +359,7 @@ export function permissionLabel(connector) {
472
359
  return "Authentication for this connection is managed by your deployment.";
473
360
  }
474
361
  /**
475
- * The four categories, in the order an operator should read them: what the
362
+ * The categories, in the order an operator should read them: what the
476
363
  * deployment refuses to call, what it can no longer call, what contradicts a
477
364
  * vetted verdict, and what changed shape underneath a reviewed schema. Counts
478
365
  * only — a name or a schema on this path would be the payload leak the whole
@@ -483,8 +370,9 @@ const DRIFT_CATEGORIES = [
483
370
  { key: "unservedTools", label: "Unserved" },
484
371
  { key: "annotationConflicts", label: "Annotation conflicts" },
485
372
  { key: "schemaChanges", label: "Schema changes" },
373
+ { key: "droppedTools", label: "Dropped tool names" },
486
374
  ];
487
- export function driftTotal(drift) {
375
+ function driftTotal(drift) {
488
376
  if (!drift)
489
377
  return 0;
490
378
  return DRIFT_CATEGORIES.reduce((sum, { key }) => sum + (drift[key] || 0), 0);
@@ -498,7 +386,7 @@ export function driftState(drift) {
498
386
  export function driftCounts(drift) {
499
387
  if (!drift)
500
388
  return [];
501
- return DRIFT_CATEGORIES.map(({ key, label }) => ({
389
+ return DRIFT_CATEGORIES.filter(({ key }) => key !== "droppedTools" || drift.droppedTools !== undefined).map(({ key, label }) => ({
502
390
  key,
503
391
  label,
504
392
  count: drift[key] || 0,
@@ -595,7 +483,9 @@ function activityMatches(event, query) {
595
483
  activityOutcomeBadge(event.outcome).label,
596
484
  activityDetail(event),
597
485
  actorKindLabel(event.actor?.kind),
598
- ].some((value) => String(value ?? "").toLowerCase().includes(q));
486
+ ].some((value) => String(value ?? "")
487
+ .toLowerCase()
488
+ .includes(q));
599
489
  }
600
490
  export function filterActivity(events, query) {
601
491
  return events.filter((event) => activityMatches(event, query));
@@ -604,20 +494,15 @@ export function filterActivity(events, query) {
604
494
  export function activitySummary(events) {
605
495
  if (events.length === 0)
606
496
  return "";
607
- const tools = new Set(events.map((event) => event.address)).size;
608
- return `${events.length} loaded call${events.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}`;
497
+ const calls = events.filter((event) => event.kind !== "catalog_drift");
498
+ const changes = events.length - calls.length;
499
+ const tools = new Set(calls.map((event) => event.address)).size;
500
+ return `${calls.length} loaded call${calls.length === 1 ? "" : "s"} · ${tools} tool${tools === 1 ? "" : "s"}${changes ? ` · ${changes} catalog change${changes === 1 ? "" : "s"}` : ""}`;
609
501
  }
610
502
  // A pause and an approval are neither success nor failure: each gets its own
611
503
  // class, and the stylesheet paints neither as an error. Nothing has emitted
612
504
  // either since issue #672 removed program pauses, but older rows carry them.
613
- const ACTIVITY_OUTCOMES = [
614
- "success",
615
- "error",
616
- "timeout",
617
- "cancelled",
618
- "paused",
619
- "approved",
620
- ];
505
+ const ACTIVITY_OUTCOMES = ["success", "error", "timeout", "cancelled", "paused", "approved"];
621
506
  export function activityOutcomeClass(outcome) {
622
507
  return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : "error";
623
508
  }
@@ -665,6 +550,8 @@ function reasonLabel(code) {
665
550
  }
666
551
  /** The one-line detail under an address: where it ran, retries, and why it stalled. */
667
552
  export function activityDetail(event) {
553
+ if (event.kind === "catalog_drift")
554
+ return "Catalog changed";
668
555
  const parts = [SOURCE_LABELS[event.source] ?? event.source];
669
556
  if (event.approval === "tool")
670
557
  parts.push("approved for the rest of the run");
@@ -672,23 +559,13 @@ export function activityDetail(event) {
672
559
  parts.push("approved for this call");
673
560
  if (event.attempts > 1)
674
561
  parts.push(`${event.attempts} attempts`);
675
- const reasons = [event.friction, event.errorCode]
676
- .filter((code) => Boolean(code))
677
- .map(reasonLabel);
562
+ const reasons = [event.friction, event.errorCode].filter((code) => Boolean(code)).map(reasonLabel);
678
563
  // The friction class and the code often say the same thing in two
679
564
  // vocabularies ("auth_required" twice); once is enough.
680
565
  for (const reason of new Set(reasons))
681
566
  parts.push(reason);
682
567
  return parts.join(" · ");
683
568
  }
684
- /**
685
- * An artifact's last refresh, when it is worth a badge. Only a failure is: a
686
- * success is what "Current data" already says, and a superseded run is
687
- * nobody's concern.
688
- */
689
- export function artifactRefreshBadge(last) {
690
- return last?.status === "failed" ? { label: "Refresh failed", tone: "danger" } : null;
691
- }
692
569
  /**
693
570
  * A stored credential that cannot be used, by cause. The payload's `error`
694
571
  * beside it is not shown: it can carry a vault's own words.
@@ -704,12 +581,8 @@ export function credentialProblemCopy(problem) {
704
581
  export function credentialStateLabel(credential) {
705
582
  if (!credential.configured)
706
583
  return "not configured";
707
- const masked = credential.fields?.length
708
- ? "configured"
709
- : `configured · ••••${credential.lastFour ?? ""}`;
710
- return credential.updatedAt
711
- ? `${masked} · updated ${formatDate(credential.updatedAt)}`
712
- : masked;
584
+ const masked = credential.fields?.length ? "configured" : `configured · ••••${credential.lastFour ?? ""}`;
585
+ return credential.updatedAt ? `${masked} · updated ${formatDate(credential.updatedAt)}` : masked;
713
586
  }
714
587
  /** Gate copy for each browser-auth shape, so the sign-in state is never a blank page. */
715
588
  export function gateCopy(kind, signedIn) {
@@ -1,7 +1,6 @@
1
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.
2
+ // Source: src/operator-ui/tokens.css and src/operator-ui/page.css.
4
3
  /** 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))}";
4
+ export const TOKENS_CSS = ":root{color-scheme:light;--accent: #5263e8;--radius: 6px;--sans: \"Inter Variable\", 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: #ffffff;--surface: #ffffff;--surface-2: #f7f8fa;--border: #e8eaee;--border-strong: #cdd2da;--text: #20232b;--muted: #69707e;--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;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-6: 1.5rem;--space-8: 2rem;--text-xs: .75rem;--text-sm: .8125rem;--text-base: .875rem;--sidebar-width: 14rem}@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: #111216;--surface: #18191e;--surface-2: #202127;--border: #2a2b33;--border-strong: #3b3d48;--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: #111216;--surface: #18191e;--surface-2: #202127;--border: #2a2b33;--border-strong: #3b3d48;--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
5
  /** Base typography, the shell, masthead, buttons, badges, and messages. */
7
6
  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)}}";
@@ -14,8 +14,20 @@ const WORD = /[\w$\\\u0080-\uffff]/;
14
14
  * Any other word is an identifier or a literal, which a `/` divides.
15
15
  */
16
16
  const REGEX_AFTER_WORD = new Set([
17
- "await", "case", "delete", "do", "else", "in", "instanceof", "new", "of",
18
- "return", "throw", "typeof", "void", "yield",
17
+ "await",
18
+ "case",
19
+ "delete",
20
+ "do",
21
+ "else",
22
+ "in",
23
+ "instanceof",
24
+ "new",
25
+ "of",
26
+ "return",
27
+ "throw",
28
+ "typeof",
29
+ "void",
30
+ "yield",
19
31
  ]);
20
32
  /**
21
33
  * Remove the statement terminators after a program's arrow expression:
@@ -1,17 +1,10 @@
1
- // The writes a program may send: config-exempt ones (#566), and nothing else.
2
- //
3
- // Every other write a program attempts is refused
4
- // `destructive_tool_requires_approval` before it is validated or sent (E4),
5
- // so the host's prompt on `call_destructive_tool` stays the only approval.
6
- // An exempt write is still a write, though, and it gets what a write needs:
7
- // its own budget, a dispatch the run waits for rather than aborts, and a
8
- // result that never hides a write whose outcome is unknown.
9
- //
10
- // Web-API only, like everything reachable from the root entry.
1
+ // Budget and outcome accounting for writes dispatched by trusted programs.
2
+ // Await each dispatched write once; never conceal an unknown outcome.
3
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
11
4
  import { Effect } from "effect";
12
5
  import { jsonResult } from "./meta-tools.js";
13
6
  /** Writes one program may send (`execute.maxWrites`), unless configured. */
14
- export const DEFAULT_MAX_WRITES = 10;
7
+ export const DEFAULT_MAX_WRITES = CONFIG_DEFAULTS.execute.maxWrites;
15
8
  /**
16
9
  * Whether a write landed, as far as anyone can know.
17
10
  *
@@ -20,9 +13,9 @@ export const DEFAULT_MAX_WRITES = 10;
20
13
  * not sending it could leave it undone. That is a dispatched call that timed
21
14
  * out, was cancelled, found the service unavailable, or failed any way that
22
15
  * is not a verdict. A refusal code (`REFUSALS`) or a downstream tool's own
23
- * `isError` is an answer, so it is `failed`, except that an `isError` whose text
24
- * classifies as a timeout is a gateway reporting that it gave up, and stays
25
- * unknown. A call that was never dispatched is `failed`: nothing was sent.
16
+ * `isError` is an answer, so it is `failed`. Response text does not decide
17
+ * whether an outcome is unknown. A call that was never dispatched is
18
+ * `failed`: nothing was sent.
26
19
  * `result_processing_failed` means the downstream call completed.
27
20
  */
28
21
  export function classifyWriteOutcome(outcome) {
@@ -51,6 +44,8 @@ export function classifyWriteOutcome(outcome) {
51
44
  */
52
45
  const REFUSALS = new Set([
53
46
  "auth_required",
47
+ "downstream_oauth_required",
48
+ "provider_permission_denied",
54
49
  "invalid_args",
55
50
  "not_found",
56
51
  "rate_limited",
@@ -59,7 +54,7 @@ const REFUSALS = new Set([
59
54
  ]);
60
55
  /** What a dispatched write's outcome says about whether it landed. */
61
56
  export function writeStateOf(outcome) {
62
- return classifyWriteOutcome(outcome.ok
57
+ const state = classifyWriteOutcome(outcome.ok
63
58
  ? { ok: true, dispatched: outcome.dispatched }
64
59
  : {
65
60
  ok: false,
@@ -67,19 +62,44 @@ export function writeStateOf(outcome) {
67
62
  error: outcome.error,
68
63
  ...(outcome.answered !== undefined ? { answered: outcome.answered } : {}),
69
64
  });
65
+ return state === "unknown" && !outcome.ok && outcome.error.uncertainCall
66
+ ? {
67
+ state,
68
+ uncertainCall: outcome.error.uncertainCall,
69
+ ...(outcome.error.details?.stage !== undefined
70
+ ? {
71
+ deadline: {
72
+ ...(outcome.error.details.operation !== undefined
73
+ ? { operation: outcome.error.details.operation }
74
+ : {}),
75
+ stage: outcome.error.details.stage,
76
+ ...(outcome.error.details.elapsedMs !== undefined
77
+ ? { elapsedMs: outcome.error.details.elapsedMs }
78
+ : {}),
79
+ ...(outcome.error.details.deadlineMs !== undefined
80
+ ? { deadlineMs: outcome.error.details.deadlineMs }
81
+ : {}),
82
+ },
83
+ }
84
+ : {}),
85
+ }
86
+ : state;
70
87
  }
71
88
  /**
72
- * The exempt writes one program sends. The play closes this when the
89
+ * The trusted-pool writes one program sends. The play closes this when the
73
90
  * program settles — a write that reaches the gate after is not sent — and
74
91
  * drains what is on the wire before the run's scope aborts it, so the write
75
92
  * finishes and its activity says how it did. The result never hides a write
76
93
  * whose outcome is unknown, and a program that fails after writing reports
77
94
  * the counts.
78
95
  */
79
- export class ExemptWrites {
96
+ export class ProgramWrites {
80
97
  closed = false;
81
98
  inFlight = new Set();
82
99
  states = [];
100
+ uncertainCalls = [];
101
+ uncertainCallsTruncated = false;
102
+ deadlines = [];
83
103
  /** Whether the program has settled: a write gated now is not sent. */
84
104
  get isClosed() {
85
105
  return this.closed;
@@ -95,10 +115,19 @@ export class ExemptWrites {
95
115
  });
96
116
  this.inFlight.add(settled);
97
117
  let recorded = false;
98
- return (state) => {
118
+ return (completion) => {
99
119
  if (recorded)
100
120
  return;
101
121
  recorded = true;
122
+ const state = typeof completion === "string" ? completion : completion.state;
123
+ if (typeof completion !== "string") {
124
+ if (this.uncertainCalls.length < 10)
125
+ this.uncertainCalls.push(completion.uncertainCall);
126
+ else
127
+ this.uncertainCallsTruncated = true;
128
+ if (completion.deadline && this.deadlines.length < 10)
129
+ this.deadlines.push(completion.deadline);
130
+ }
102
131
  this.states.push(state);
103
132
  this.inFlight.delete(settled);
104
133
  done();
@@ -124,6 +153,14 @@ export class ExemptWrites {
124
153
  code: "write_outcome_unknown",
125
154
  message: "A write this program sent has no known outcome, so that is the result rather than what the program returned. It will not be sent again. Check its target before doing anything that depends on it.",
126
155
  retryable: false,
156
+ ...(this.deadlines.length > 0 ? { details: this.deadlines[0] } : {}),
157
+ ...(this.deadlines.length > 1 ? { timeouts: this.deadlines } : {}),
158
+ ...(this.uncertainCalls.length === 1
159
+ ? { uncertainCall: this.uncertainCalls[0] }
160
+ : this.uncertainCalls.length > 1
161
+ ? { uncertainCalls: this.uncertainCalls }
162
+ : {}),
163
+ ...(this.uncertainCallsTruncated ? { uncertainCallsTruncated: true } : {}),
127
164
  writes,
128
165
  });
129
166
  }