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