@zackbart/connecta 0.31.0 → 0.33.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 (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. package/templates/node/src/provision-token.ts +0 -17
@@ -0,0 +1,112 @@
1
+ import { connectorStatusLabel, connectorStatusTone } from "./view.js";
2
+ /**
3
+ * Whether the member's own partition holds a credential, read from `actions`
4
+ * alone (P6b): `clear` is listed only when a value is stored. Connection
5
+ * state says whether it works, never whether it exists.
6
+ */
7
+ function credentialStored(manage) {
8
+ return manage.actions.includes("clear");
9
+ }
10
+ function controlsFor(manage, connection) {
11
+ const can = (action) => (manage.kind === "none" ? false : manage.actions.includes(action));
12
+ if (manage.kind === "oauth") {
13
+ const connected = connection.state === "ok";
14
+ const controls = [];
15
+ if (!connected && can("connect")) {
16
+ controls.push({ kind: "connect", label: connection.state === "auth_required" ? "Connect" : "Reconnect" });
17
+ }
18
+ if (connected && can("restart"))
19
+ controls.push({ kind: "restart", label: "Reconnect" });
20
+ if (connection.state !== "auth_required" && can("disconnect")) {
21
+ controls.push({ kind: "disconnect", label: "Disconnect" });
22
+ }
23
+ return controls;
24
+ }
25
+ if (manage.kind === "credential") {
26
+ // `save` without `clear`: nothing stored yet. `test` is listed only for a stored value.
27
+ const stored = credentialStored(manage);
28
+ return [
29
+ ...(can("save")
30
+ ? [{ kind: "save", label: stored ? "Replace" : "Add credential" }]
31
+ : []),
32
+ ...(can("test") ? [{ kind: "test", label: "Test" }] : []),
33
+ ...(can("clear") ? [{ kind: "clear", label: "Clear" }] : []),
34
+ ];
35
+ }
36
+ return [];
37
+ }
38
+ function summaryFor(manage, connection) {
39
+ if (manage.kind === "credential") {
40
+ if (!credentialStored(manage)) {
41
+ return manage.actions.includes("save") ? "Add your credential to use its tools." : "No credential saved.";
42
+ }
43
+ if (connection.problem === "credential_mismatch")
44
+ return "Your saved credential no longer fits. Replace it.";
45
+ return connection.state === "ok" ? "Your credential is saved." : "Your credential is saved, but it isn't working.";
46
+ }
47
+ if (connection.state === "ok")
48
+ return "Connected with your account.";
49
+ if (connection.state === "auth_required") {
50
+ return manage.kind === "oauth" && manage.actions.includes("connect")
51
+ ? "Not connected. Its tools won't work for you until you connect."
52
+ : "Not connected.";
53
+ }
54
+ return "Unavailable right now.";
55
+ }
56
+ const toolCount = (connector) => connector.tools.length;
57
+ export function memberPage(contract) {
58
+ if (contract?.role !== "member")
59
+ return { endpoints: [], accounts: [], shared: [] };
60
+ return {
61
+ endpoints: contract.pools.map((pool) => ({ pool: pool.name, path: pool.humanPath })),
62
+ accounts: contract.connectors
63
+ .filter((connector) => connector.authScope === "personal")
64
+ .map((connector) => {
65
+ const connection = connector.connection ?? { state: "auth_required" };
66
+ return {
67
+ id: connector.id,
68
+ title: connector.title || connector.id,
69
+ toolCount: toolCount(connector),
70
+ badge: connector.connection
71
+ ? {
72
+ label: connectorStatusLabel(connection.state, connection.problem),
73
+ tone: connectorStatusTone(connection.state),
74
+ }
75
+ : null,
76
+ summary: summaryFor(connector.manage, connection),
77
+ manage: connector.manage,
78
+ controls: controlsFor(connector.manage, connection),
79
+ };
80
+ }),
81
+ // Names and counts only: a shared connector's state is not the member's to see.
82
+ shared: contract.connectors
83
+ .filter((connector) => connector.authScope === "shared")
84
+ .map((connector) => ({
85
+ id: connector.id,
86
+ title: connector.title || connector.id,
87
+ toolCount: toolCount(connector),
88
+ })),
89
+ };
90
+ }
91
+ export function toolCountLabel(count) {
92
+ return `${count} ${count === 1 ? "tool" : "tools"}`;
93
+ }
94
+ /** The in-page question before a member's action that takes something away. */
95
+ export function memberConfirmCopy(action, name) {
96
+ if (action === "oauth_restart") {
97
+ return {
98
+ question: `Reconnect ${name}? Your current connection stops working until you finish authorizing in the new tab.`,
99
+ confirm: "Reconnect",
100
+ };
101
+ }
102
+ if (action === "oauth_disconnect") {
103
+ return {
104
+ question: `Disconnect your ${name} account? Its tools stop working for you until you connect again.`,
105
+ confirm: "Disconnect",
106
+ };
107
+ }
108
+ return {
109
+ question: `Clear your ${name} credential? Its tools stop working for you until you add one again.`,
110
+ confirm: "Clear",
111
+ };
112
+ }
@@ -1,6 +1,7 @@
1
- import type { OperatorConnectorOverlay, OperatorTool, OperatorUiContract, OperatorViewer } from "./contract.js";
1
+ import type { ConnectaConfigDescription } from "../describe-config.js";
2
+ import type { OperatorConnectorOverlay, OperatorTool } from "./contract.js";
2
3
  import type { CatalogAccessObservation, CatalogDriftReport, ResourceTemplateRefusalCode } from "../types.js";
3
- /** Tool verdicts, independent of the selected pool's trust. */
4
+ /** Tool verdicts, independent of whether the selected pool is read-only. */
4
5
  export type UiToolSafety = "runs_in_programs" | "needs_approval";
5
6
  export interface UiTool extends Pick<OperatorTool, "name" | "address" | "description"> {
6
7
  /** Absent only in payloads older than the field; the page then shows no badge. */
@@ -28,7 +29,8 @@ interface UiCredentialField {
28
29
  description?: string;
29
30
  placeholder?: string;
30
31
  inputType: "email" | "password" | "text";
31
- configured: boolean;
32
+ /** null when metadata could not be read. */
33
+ configured: boolean | null;
32
34
  lastFour?: string;
33
35
  updatedAt?: string;
34
36
  }
@@ -37,7 +39,8 @@ interface UiCredential {
37
39
  description?: string;
38
40
  placeholder?: string;
39
41
  fields?: UiCredentialField[];
40
- configured: boolean;
42
+ /** null when metadata could not be read. */
43
+ configured: boolean | null;
41
44
  /** A stored value exists and may be deleted even if it cannot be decrypted. */
42
45
  removable?: boolean;
43
46
  lastFour?: string;
@@ -60,7 +63,12 @@ export interface UiConnector {
60
63
  title?: string;
61
64
  description?: string;
62
65
  status: "loading" | OperatorConnectorOverlay["status"];
63
- permissions?: Omit<OperatorViewer["permissions"]["connectors"][number], "id">;
66
+ /** What this maintainer may do with the connector. */
67
+ permissions?: {
68
+ use: boolean;
69
+ manageSharedAuth: boolean;
70
+ connectPersonal: boolean;
71
+ };
64
72
  /**
65
73
  * Why this connector is not usable, when it is not. See `UiProblem`. The
66
74
  * only failure detail the payload carries; the status message behind it is
@@ -91,9 +99,9 @@ export interface UiConnector {
91
99
  export type CredentialManagementCapability = "available" | "requires_operator" | "vault_not_configured" | "no_slots";
92
100
  export interface UiData {
93
101
  accessTokenManagement?: "available" | "requires_operator";
94
- serverInfo: Pick<OperatorUiContract["config"]["server"], "name" | "version">;
102
+ serverInfo: Pick<ConnectaConfigDescription["server"], "name" | "version">;
95
103
  /** Version of the installed @zackbart/connecta package. */
96
- connectaVersion: OperatorUiContract["config"]["connectaVersion"];
104
+ connectaVersion: ConnectaConfigDescription["connectaVersion"];
97
105
  connectors: UiConnector[];
98
106
  activityEnabled: boolean;
99
107
  credentialManagement: CredentialManagementCapability;
@@ -1,7 +1,8 @@
1
- import type { OperatorUiContract } from "./contract.js";
1
+ import type { OperatorRole, OperatorUiContractV2 } from "./contract.js";
2
2
  import type { CatalogDriftReport } from "../types.js";
3
3
  import type { CredentialManagementCapability, UiConnector, UiData, UiProblem } from "./model.js";
4
4
  import type { FixPromptKind } from "./fix-prompts.js";
5
+ import type { AgentSlot } from "./agents.js";
5
6
  export declare function registrationPathLabel(path: NonNullable<UiConnector["registrationPath"]>): string;
6
7
  /**
7
8
  * A connector's source column: the maintained provider with the
@@ -12,19 +13,26 @@ export declare function connectorSourceLabel(source: {
12
13
  kind: string;
13
14
  provider?: string;
14
15
  }): string;
15
- export type OperatorPage = "overview" | "tools" | "access" | "config" | "connector" | "tokens" | "connections" | "activity";
16
- /** Pages the nav lists. */
16
+ export type OperatorPage = "endpoints" | "settings" | "connector" | "tokens" | "connections" | "activity";
17
+ /** Pages the nav lists, in dashboard order. */
17
18
  export declare const OPERATOR_PAGES: readonly OperatorPage[];
18
19
  export declare const PAGE_META: Readonly<Record<OperatorPage, {
19
20
  path: string;
20
21
  label: string;
21
22
  }>>;
23
+ /** Legacy links remain valid, with the browser replacing their URL. */
24
+ export declare function canonicalOperatorPath(path: string): string;
22
25
  /**
23
26
  * What each page is for, in one line under its heading — on the gate as well
24
27
  * as once signed in, so the page an operator lands on reads the same before
25
28
  * and after the session is checked.
26
29
  */
27
30
  export declare function pageDescription(page: OperatorPage, productDescription: string): string;
31
+ /**
32
+ * Signed in, but neither a maintainer nor a member of any endpoint: who to
33
+ * ask, and that this is not an error to retry.
34
+ */
35
+ export declare function noAccessCopy(kind: string, productName: string): string;
28
36
  /** What the gate says while the session is checked, page by page. */
29
37
  export declare function checkingCopy(): string;
30
38
  export declare function pageForPath(path: string): OperatorPage;
@@ -83,12 +91,16 @@ export declare function failure(message: string, fix?: NoticeFix): Notice;
83
91
  * mutation that is a permission, not an expired session, so it stays inline.
84
92
  * - `network` — the browser could not reach the deployment at all.
85
93
  * - `refused` — the route answered, and said no.
94
+ * - `stale` — never sent: what the action was checked against changed before
95
+ * dispatch (a refreshed slot list or member contract).
86
96
  */
87
- export type RequestFailureKind = "session" | "forbidden" | "network" | "refused";
97
+ export type RequestFailureKind = "session" | "forbidden" | "network" | "refused" | "stale";
88
98
  export interface RequestFailureFacts {
89
99
  kind: RequestFailureKind;
90
100
  status?: number | undefined;
91
101
  problem?: unknown;
102
+ /** A refusal's machine `code` from a closed set, such as `slot_changed`; checked, never shown. */
103
+ code?: unknown;
92
104
  }
93
105
  /**
94
106
  * What the notices for an OAuth action and a credential Test say. Each of
@@ -121,6 +133,8 @@ export declare function credentialTestNotice(connectorId: string, answer: {
121
133
  * checked, not trusted.
122
134
  */
123
135
  export declare function refusedNotice(action: DownstreamAction, connectorId: string, problem?: unknown): Notice;
136
+ /** An action whose permission or target changed before it was sent. */
137
+ export declare const STALE_COPY = "This changed before the request was sent, so nothing was sent. Check it and try again.";
124
138
  /**
125
139
  * Every row action's failure, in one voice. The page's own failures — a
126
140
  * lapsed session, a denied identity, a dropped connection — read the same for
@@ -133,7 +147,7 @@ export declare function actionFailedNotice(action: RowAction, connectorId: strin
133
147
  * server's `connector_unavailable` and keeps its own copy and fix prompt;
134
148
  * these two have nothing for a coding agent to fix.
135
149
  */
136
- export type ConnectorLoadFailure = "session" | "network";
150
+ export type ConnectorLoadFailure = "session" | "network" | "unavailable";
137
151
  export declare function connectorLoadFailureCopy(failure: ConnectorLoadFailure, productName: string): string;
138
152
  /** The signed-in page's answer when `/ui/data` could not be read. */
139
153
  export declare function loadFailureCopy(failure: "server" | "network", productName: string): {
@@ -158,20 +172,27 @@ export declare function confirmCopy(action: PendingConfirm["action"], name: stri
158
172
  * page fetch again after an identity change.
159
173
  */
160
174
  type LoadPhase = "idle" | "loading" | "ready" | "error";
161
- export interface UiAccessToken {
162
- id: string;
163
- name: string;
164
- tokenPrefix: string;
165
- createdAt: string;
166
- revokedAt?: string;
167
- }
168
175
  export declare function accessTokenUnavailableCopy(capability?: string): string;
169
176
  export interface OperatorState {
170
177
  tokenPhase: LoadPhase;
171
178
  tokenNotice: Notice | null;
172
- tokens: UiAccessToken[];
179
+ /** One slot per declared agent, from `/ui/access-tokens`. */
180
+ tokens: AgentSlot[];
181
+ /** A just-issued secret, shown once in its dialog and dropped when that closes. */
173
182
  createdToken: string | null;
174
- tokenRenaming: string | null;
183
+ /** The agent `createdToken` belongs to. */
184
+ createdTokenFor: string | null;
185
+ /** A rotation or revocation waiting on its in-row confirmation. */
186
+ tokenConfirm: {
187
+ name: string;
188
+ action: "rotate" | "revoke";
189
+ } | null;
190
+ /**
191
+ * Agents whose last token operation ended with an unknown outcome (503,
192
+ * 499, or a dropped connection). Their actions stay off until a slot list
193
+ * read that started after the failure succeeds.
194
+ */
195
+ unresolvedSlots: string[];
175
196
  tokenBusy: boolean;
176
197
  page: OperatorPage;
177
198
  /**
@@ -180,11 +201,17 @@ export interface OperatorState {
180
201
  * the fence that keeps one identity's data off another identity's screen.
181
202
  */
182
203
  generation: number;
183
- /** `loading` until the first /ui/data answer decides gated or ready. */
184
- session: "loading" | "gated" | "ready";
204
+ /**
205
+ * `loading` until `/ui/api/config` answers (and, for a maintainer,
206
+ * `/ui/data` too). `denied` is signed in as someone who is neither a
207
+ * maintainer nor a pool member.
208
+ */
209
+ session: "loading" | "gated" | "ready" | "denied";
185
210
  gate: Notice | null;
186
211
  /** True while a signed-in operator's /ui/data is in flight. */
187
212
  refreshing: boolean;
213
+ /** A bounded admission recovery is pending; Retry can supersede it. */
214
+ recoveryPending: boolean;
188
215
  /**
189
216
  * Why the last /ui/data read failed when the session itself was not the
190
217
  * reason: the page stays signed in, with its chrome, and offers a retry.
@@ -204,7 +231,14 @@ export interface OperatorState {
204
231
  */
205
232
  focusIfLost: string | null;
206
233
  data: UiData | null;
207
- contract: OperatorUiContract | null;
234
+ /** Operator contract v2, for either role; null until `/ui/api/config` answers. */
235
+ contract: OperatorUiContractV2 | null;
236
+ /**
237
+ * The role the last good contract confirmed. It outlives a failed refresh
238
+ * that clears `contract`, so the next answer with a different role is
239
+ * still recognized as a different viewer. Only an identity reset clears it.
240
+ */
241
+ role: OperatorRole | null;
208
242
  contractPhase: LoadPhase;
209
243
  health: "loading" | "ok" | "unavailable";
210
244
  connectorFilter: string;
@@ -218,6 +252,11 @@ export interface OperatorState {
218
252
  * new tab: its row offers the link instead of the button.
219
253
  */
220
254
  oauthBlocked: string | null;
255
+ /**
256
+ * The authorization link the route returned for `oauthBlocked`. A member has
257
+ * no `/ui/data` connector to keep it on, so it is held here.
258
+ */
259
+ oauthBlockedUrl: string | null;
221
260
  /** A row action waiting for its in-page confirmation. */
222
261
  confirming: PendingConfirm | null;
223
262
  /** Connectors whose details failed on this side of the deployment. */
@@ -235,6 +274,31 @@ export interface OperatorState {
235
274
  activityCursor: string | null;
236
275
  activitySearch: string;
237
276
  }
277
+ /** Who the contract says this viewer is, or null before it loads. */
278
+ export declare function operatorRole(state: Pick<OperatorState, "contract">): OperatorRole | null;
279
+ /**
280
+ * The role pages and navigation render for. The contract's, or, while a
281
+ * refresh that failed has left no contract on screen, maintainer when the
282
+ * last good contract confirmed one and that maintainer's data is still here:
283
+ * their page stays mounted on its own retry state instead of being torn down
284
+ * mid-recovery. Never a request's permission; the store sends maintainer
285
+ * reads only while the contract itself says maintainer.
286
+ */
287
+ export declare function displayRole(state: Pick<OperatorState, "contract" | "role" | "data">): OperatorRole | null;
288
+ /**
289
+ * A confirmed maintainer's page is mounted without a contract behind it: a
290
+ * refresh failed, or its read ran past the deadline. The shell shows one
291
+ * recovery banner with Retry above whichever page is open, and that page is
292
+ * stale until a contract confirms the role again.
293
+ */
294
+ export declare function contractRecovering(state: Pick<OperatorState, "contract" | "role" | "data">): boolean;
295
+ /**
296
+ * Whether Activity is open to this viewer: maintainers only, on a deployment
297
+ * that records history. Members always read "forbidden".
298
+ */
299
+ export declare function activityReadable(state: Pick<OperatorState, "contract" | "data">): boolean;
300
+ /** The contract's half of `activityReadable`: a maintainer, on a deployment that records. */
301
+ export declare function activityAllowed(state: Pick<OperatorState, "contract">): boolean;
238
302
  export declare function initialState(page: OperatorPage): OperatorState;
239
303
  /**
240
304
  * Drop to the gate and forget the previous operator. Bumping the generation is
@@ -254,7 +318,7 @@ export declare function credentialUnavailableCopy(capability?: CredentialManagem
254
318
  */
255
319
  export declare function connectorStatusLabel(status: string, problem?: UiProblem): string;
256
320
  /** The tone a status carries wherever it is rendered as a badge or a tile. */
257
- export type Tone = "ok" | "warn" | "danger" | "neutral";
321
+ export type Tone = "ok" | "warn" | "danger" | "neutral" | "accent";
258
322
  export declare function connectorStatusTone(status: string): Tone;
259
323
  export declare function problemCopy(problem: UiProblem | undefined): string | null;
260
324
  /**
@@ -322,7 +386,8 @@ export declare function activityDetail(event: UiActivityEvent): string;
322
386
  */
323
387
  export declare function credentialProblemCopy(problem: string | undefined): string;
324
388
  export declare function credentialStateLabel(credential: {
325
- configured: boolean;
389
+ configured: boolean | null;
390
+ problem?: string;
326
391
  fields?: unknown[];
327
392
  lastFour?: string;
328
393
  updatedAt?: string;
@@ -25,25 +25,20 @@ export function connectorSourceLabel(source) {
25
25
  const implementation = source.kind === "remote-mcp" ? "hosted MCP" : source.kind === "api" ? "REST" : source.kind;
26
26
  return `${source.provider} · ${implementation}`;
27
27
  }
28
- /** Pages the nav lists. */
29
- export const OPERATOR_PAGES = [
30
- "overview",
31
- "connections",
32
- "tools",
33
- "access",
34
- "activity",
35
- "config",
36
- ];
28
+ /** Pages the nav lists, in dashboard order. */
29
+ export const OPERATOR_PAGES = ["endpoints", "connections", "activity", "tokens", "settings"];
37
30
  export const PAGE_META = {
38
- overview: { path: "/", label: "Overview" },
31
+ endpoints: { path: "/", label: "Endpoints" },
39
32
  connector: { path: "/connectors", label: "Connector" },
40
- tools: { path: "/tools", label: "Tools" },
41
- access: { path: "/access", label: "Access" },
42
- config: { path: "/config", label: "Config" },
43
- tokens: { path: "/tokens", label: "Access tokens" },
33
+ settings: { path: "/settings", label: "Settings" },
34
+ tokens: { path: "/tokens", label: "Tokens" },
44
35
  connections: { path: "/connectors", label: "Connectors" },
45
36
  activity: { path: "/activity", label: "Activity" },
46
37
  };
38
+ /** Legacy links remain valid, with the browser replacing their URL. */
39
+ export function canonicalOperatorPath(path) {
40
+ return { "/access": "/", "/tools": "/connectors", "/config": "/settings" }[path] ?? path;
41
+ }
47
42
  /**
48
43
  * What each page is for, in one line under its heading — on the gate as well
49
44
  * as once signed in, so the page an operator lands on reads the same before
@@ -55,6 +50,18 @@ export function pageDescription(page, productDescription) {
55
50
  }
56
51
  return productDescription;
57
52
  }
53
+ /**
54
+ * Signed in, but neither a maintainer nor a member of any endpoint: who to
55
+ * ask, and that this is not an error to retry.
56
+ */
57
+ export function noAccessCopy(kind, productName) {
58
+ const who = kind === "clerk"
59
+ ? "This account is signed in"
60
+ : kind === "cloudflare-access"
61
+ ? "Cloudflare Access signed you in"
62
+ : "This token is valid";
63
+ return `${who}, but it isn't a maintainer or a member of any endpoint on ${productName}. Ask a maintainer of this deployment to add you to an endpoint, then reload.`;
64
+ }
58
65
  /** What the gate says while the session is checked, page by page. */
59
66
  export function checkingCopy() {
60
67
  return "Checking your session…";
@@ -64,7 +71,7 @@ export function pageForPath(path) {
64
71
  return "connector";
65
72
  if (path === "/tokens")
66
73
  return "tokens";
67
- const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === path);
74
+ const match = OPERATOR_PAGES.find((page) => PAGE_META[page].path === canonicalOperatorPath(path));
68
75
  return match ?? "connections";
69
76
  }
70
77
  export function info(message) {
@@ -138,6 +145,8 @@ function credentialRefusedCopy(action, status, problem) {
138
145
  }
139
146
  return `The credential wasn't ${verb}. Try again; if it keeps failing, the deployment's log has the reason.`;
140
147
  }
148
+ /** An action whose permission or target changed before it was sent. */
149
+ export const STALE_COPY = "This changed before the request was sent, so nothing was sent. Check it and try again.";
141
150
  /**
142
151
  * Every row action's failure, in one voice. The page's own failures — a
143
152
  * lapsed session, a denied identity, a dropped connection — read the same for
@@ -153,14 +162,18 @@ export function actionFailedNotice(action, connectorId, facts, productName) {
153
162
  if (facts.kind === "network") {
154
163
  return failure(`Couldn't reach ${productName}. Check your connection and try again.`);
155
164
  }
165
+ if (facts.kind === "stale")
166
+ return failure(STALE_COPY);
156
167
  if (action === "credential_save" || action === "credential_remove") {
157
168
  return failure(credentialRefusedCopy(action, facts.status, facts.problem));
158
169
  }
159
170
  return refusedNotice(action, connectorId, facts.problem);
160
171
  }
161
172
  export function connectorLoadFailureCopy(failure, productName) {
173
+ if (failure === "unavailable")
174
+ return "Session identity is temporarily unavailable. Retry this connection.";
162
175
  return failure === "session"
163
- ? "Your session wasn't accepted while loading this connector. Sign in again to see its status."
176
+ ? "Your session wasn't accepted while loading this connector. Retry to check again. Sign in again if the session keeps failing."
164
177
  : `Couldn't reach ${productName} to load this connector. Check your connection, then refresh it.`;
165
178
  }
166
179
  /** The signed-in page's answer when `/ui/data` could not be read. */
@@ -208,6 +221,46 @@ export function accessTokenUnavailableCopy(capability) {
208
221
  ? "Access tokens are not configured for this deployment."
209
222
  : "Token management requires an interactive sign-in and explicit permission.";
210
223
  }
224
+ /** Who the contract says this viewer is, or null before it loads. */
225
+ export function operatorRole(state) {
226
+ return state.contract?.role ?? null;
227
+ }
228
+ /**
229
+ * The role pages and navigation render for. The contract's, or, while a
230
+ * refresh that failed has left no contract on screen, maintainer when the
231
+ * last good contract confirmed one and that maintainer's data is still here:
232
+ * their page stays mounted on its own retry state instead of being torn down
233
+ * mid-recovery. Never a request's permission; the store sends maintainer
234
+ * reads only while the contract itself says maintainer.
235
+ */
236
+ export function displayRole(state) {
237
+ if (state.contract)
238
+ return state.contract.role;
239
+ return state.role === "maintainer" && state.data ? "maintainer" : null;
240
+ }
241
+ /**
242
+ * A confirmed maintainer's page is mounted without a contract behind it: a
243
+ * refresh failed, or its read ran past the deadline. The shell shows one
244
+ * recovery banner with Retry above whichever page is open, and that page is
245
+ * stale until a contract confirms the role again.
246
+ */
247
+ export function contractRecovering(state) {
248
+ return !state.contract && displayRole(state) === "maintainer";
249
+ }
250
+ /**
251
+ * Whether Activity is open to this viewer: maintainers only, on a deployment
252
+ * that records history. Members always read "forbidden".
253
+ */
254
+ export function activityReadable(state) {
255
+ return Boolean(state.data?.activityEnabled) && activityAllowed(state);
256
+ }
257
+ /** The contract's half of `activityReadable`: a maintainer, on a deployment that records. */
258
+ export function activityAllowed(state) {
259
+ const contract = state.contract;
260
+ return (contract?.role === "maintainer" &&
261
+ contract.live.activity !== "forbidden" &&
262
+ contract.live.activity !== "unconfigured");
263
+ }
211
264
  export function initialState(page) {
212
265
  return {
213
266
  page,
@@ -228,8 +281,10 @@ export function initialState(page) {
228
281
  */
229
282
  function identityScopedState() {
230
283
  return {
284
+ recoveryPending: false,
231
285
  data: null,
232
286
  contract: null,
287
+ role: null,
233
288
  contractPhase: "idle",
234
289
  health: "loading",
235
290
  connectorFilter: "",
@@ -237,6 +292,7 @@ function identityScopedState() {
237
292
  oauthNoticeFor: null,
238
293
  oauthBusy: null,
239
294
  oauthBlocked: null,
295
+ oauthBlockedUrl: null,
240
296
  confirming: null,
241
297
  connectorFailures: {},
242
298
  credentialNotice: null,
@@ -247,7 +303,9 @@ function identityScopedState() {
247
303
  tokenNotice: null,
248
304
  tokens: [],
249
305
  createdToken: null,
250
- tokenRenaming: null,
306
+ createdTokenFor: null,
307
+ tokenConfirm: null,
308
+ unresolvedSlots: [],
251
309
  tokenBusy: false,
252
310
  activityPhase: "idle",
253
311
  activityNotice: null,
@@ -283,7 +341,8 @@ export function withPage(state, page) {
283
341
  ...state,
284
342
  page,
285
343
  createdToken: null,
286
- tokenRenaming: null,
344
+ createdTokenFor: null,
345
+ tokenConfirm: null,
287
346
  tokenNotice: null,
288
347
  credentialEditing: null,
289
348
  credentialNotice: null,
@@ -551,6 +610,7 @@ const REASON_LABELS = {
551
610
  invalid_args: "arguments didn't match the schema",
552
611
  destructive_reroute: "needed host approval",
553
612
  destructive_tool_requires_approval: "needed host approval",
613
+ read_only_endpoint: "endpoint is read-only",
554
614
  approval_required: "needed approval",
555
615
  auth_required: "needed authorization",
556
616
  result_too_large: "result too large to return inline",
@@ -585,20 +645,22 @@ export function credentialProblemCopy(problem) {
585
645
  if (problem === "credential_mismatch")
586
646
  return PROBLEM_COPY.credential_mismatch;
587
647
  if (problem === "credential_unreadable") {
588
- return "The stored credential can't be read, so it can't be used. Replace it, or remove it and add a new one.";
648
+ return "Credential status is unavailable. Retry the status check before replacing or removing the credential.";
589
649
  }
590
650
  return "The stored credential can't be used. Replace it, or remove it and add a new one.";
591
651
  }
592
652
  export function credentialStateLabel(credential) {
653
+ if (credential.configured === null || credential.problem === "credential_unreadable")
654
+ return "unavailable";
593
655
  if (!credential.configured)
594
656
  return "not configured";
595
- const masked = credential.fields?.length ? "configured" : `configured · ••••${credential.lastFour ?? ""}`;
657
+ const masked = "configured";
596
658
  return credential.updatedAt ? `${masked} · updated ${formatDate(credential.updatedAt)}` : masked;
597
659
  }
598
660
  /** Gate copy for each browser-auth shape, so the sign-in state is never a blank page. */
599
661
  export function gateCopy(kind, signedIn) {
600
662
  if (kind === "cloudflare-access") {
601
- return "Cloudflare Access admitted this browser, but the current identity cannot open deployment-wide operator pages.";
663
+ return "Checking Cloudflare Access identity failed. Retry to check whether this browser can open operator pages.";
602
664
  }
603
665
  if (kind !== "clerk") {
604
666
  return "Paste an operator bearer token to open this page. Nothing is requested until you do.";
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/build-operator-ui.mjs. Do not edit.
2
2
  // Source: src/operator-ui/tokens.css and src/operator-ui/page.css.
3
3
  /** Custom properties and both palettes; styles no element. */
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))}";
4
+ export const TOKENS_CSS = ":root{color-scheme:light;--default-accent: #171717;--accent: var(--default-accent);--radius: 8px;--sans: \"Geist Sans\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;--mono: \"Geist Mono\", ui-monospace, \"SF Mono\", Menlo, Monaco, \"Cascadia Code\", Consolas, monospace;--bg: #fafafa;--surface: #ffffff;--surface-2: #f5f5f5;--border: #eaeaea;--border-strong: #d4d4d4;--text: #171717;--muted: #666666;--ok: #0a7d33;--warn: #a35200;--danger: #d8242c;--on-accent: #ffffff;--link: var(--accent);--blue: #0060df;--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}@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]){--default-accent: #ededed;--on-accent: #171717;--blue: #70b3ff;--bg: #0a0a0a;--surface: #111111;--surface-2: #1a1a1a;--border: #2e2e2e;--border-strong: #454545;--text: #ededed;--muted: #a1a1a1;--ok: #4cce83;--warn: #f5b85a;--danger: #ff777f;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}}html[data-scheme=dark]{--default-accent: #ededed;--on-accent: #171717;--blue: #70b3ff;--bg: #0a0a0a;--surface: #111111;--surface-2: #1a1a1a;--border: #2e2e2e;--border-strong: #454545;--text: #ededed;--muted: #a1a1a1;--ok: #4cce83;--warn: #f5b85a;--danger: #ff777f;--link: color-mix(in srgb, var(--accent) 55%, #ffffff);--tint: color-mix(in srgb, var(--accent) 16%, var(--surface))}";
5
5
  /** Base typography, the shell, masthead, buttons, badges, and messages. */
6
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)}}";
@@ -1,4 +1,4 @@
1
- // Budget and outcome accounting for writes dispatched by trusted programs.
1
+ // Budget and outcome accounting for writes dispatched by programs on writable endpoints.
2
2
  // Await each dispatched write once; never conceal an unknown outcome.
3
3
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
4
4
  import { Effect } from "effect";
@@ -41,7 +41,7 @@ export function writeStateOf(outcome) {
41
41
  : state;
42
42
  }
43
43
  /**
44
- * The trusted-pool writes one program sends. The play closes this when the
44
+ * The writes one program sends. The play closes this when the
45
45
  * program settles — a write that reaches the gate after is not sent — and
46
46
  * drains what is on the wire before the run's scope aborts it, so the write
47
47
  * finishes and its activity says how it did. The result never hides a write