@zackbart/connecta 0.26.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/CHANGELOG.md +116 -0
- package/dist/artifacts/document.js +6 -2
- package/dist/artifacts/markdown.js +43 -14
- package/dist/artifacts/routes.js +27 -4
- package/dist/artifacts/validate.js +1 -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/connectors/remote-mcp.js +32 -7
- package/dist/execute.js +7 -8
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.js +288 -33
- package/dist/page-styles.js +7 -0
- package/dist/registry.js +16 -13
- package/dist/routes/mcp.js +5 -1
- package/dist/routes/oauth-management.js +66 -12
- package/dist/routes/oauth.js +54 -87
- package/dist/routes/ui.js +7 -1
- package/dist/server.js +7 -5
- package/dist/skills.js +6 -6
- package/dist/types.d.ts +5 -0
- package/dist/ui.d.ts +5 -0
- package/dist/ui.js +20 -63
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +18 -8
- package/documentation/auth.md +103 -0
- package/documentation/code-mode.md +15 -15
- package/documentation/meta-tools.md +12 -5
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/dist/operator-ui/view.js
CHANGED
|
@@ -10,6 +10,28 @@ export const PAGE_META = {
|
|
|
10
10
|
artifacts: { path: "/artifacts", label: "Artifacts" },
|
|
11
11
|
artifact: { path: "/artifacts", label: "Artifact" },
|
|
12
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
|
+
}
|
|
13
35
|
export function pageForPath(path) {
|
|
14
36
|
if (path.startsWith("/artifacts/"))
|
|
15
37
|
return "artifact";
|
|
@@ -44,24 +66,29 @@ export function failure(message, fix) {
|
|
|
44
66
|
return fix ? { message, tone: "error", fix } : { message, tone: "error" };
|
|
45
67
|
}
|
|
46
68
|
const REFUSED_COPY = {
|
|
47
|
-
oauth_disconnect: "
|
|
48
|
-
oauth_reconnect: "
|
|
49
|
-
credential_test: "The credential test
|
|
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.",
|
|
50
72
|
};
|
|
51
|
-
/**
|
|
52
|
-
|
|
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) {
|
|
53
78
|
if (action === "oauth_disconnect") {
|
|
54
|
-
return info("
|
|
79
|
+
return info("Disconnected. Connect again whenever you are ready.");
|
|
55
80
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.");
|
|
59
86
|
}
|
|
60
87
|
/** A credential Test's result. `ok` is the only part of the answer it reads. */
|
|
61
88
|
export function credentialTestNotice(connectorId, answer) {
|
|
62
89
|
return answer?.ok === true
|
|
63
90
|
? info("Credential is valid.")
|
|
64
|
-
: failure("Credential test failed: the
|
|
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 });
|
|
65
92
|
}
|
|
66
93
|
/**
|
|
67
94
|
* A route that refused one of these actions. A credential Test that found
|
|
@@ -80,6 +107,97 @@ export function refusedNotice(action, connectorId, problem) {
|
|
|
80
107
|
connectorId,
|
|
81
108
|
});
|
|
82
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
|
+
}
|
|
83
201
|
export function initialState(page) {
|
|
84
202
|
return {
|
|
85
203
|
page,
|
|
@@ -87,7 +205,9 @@ export function initialState(page) {
|
|
|
87
205
|
session: "loading",
|
|
88
206
|
gate: null,
|
|
89
207
|
refreshing: false,
|
|
208
|
+
loadFailure: null,
|
|
90
209
|
pendingFocus: null,
|
|
210
|
+
focusIfLost: null,
|
|
91
211
|
...identityScopedState(),
|
|
92
212
|
};
|
|
93
213
|
}
|
|
@@ -101,8 +221,13 @@ function identityScopedState() {
|
|
|
101
221
|
data: null,
|
|
102
222
|
connectorFilter: "",
|
|
103
223
|
oauthNotice: null,
|
|
224
|
+
oauthNoticeFor: null,
|
|
104
225
|
oauthBusy: null,
|
|
226
|
+
oauthBlocked: null,
|
|
227
|
+
confirming: null,
|
|
228
|
+
connectorFailures: {},
|
|
105
229
|
credentialNotice: null,
|
|
230
|
+
credentialNoticeFor: null,
|
|
106
231
|
credentialEditing: null,
|
|
107
232
|
credentialBusy: null,
|
|
108
233
|
activityPhase: "idle",
|
|
@@ -131,7 +256,9 @@ export function resetIdentity(state, gate = null) {
|
|
|
131
256
|
session: "gated",
|
|
132
257
|
gate,
|
|
133
258
|
refreshing: false,
|
|
259
|
+
loadFailure: null,
|
|
134
260
|
pendingFocus: null,
|
|
261
|
+
focusIfLost: null,
|
|
135
262
|
...identityScopedState(),
|
|
136
263
|
};
|
|
137
264
|
}
|
|
@@ -145,6 +272,8 @@ export function withPage(state, page) {
|
|
|
145
272
|
page,
|
|
146
273
|
credentialEditing: null,
|
|
147
274
|
credentialNotice: null,
|
|
275
|
+
credentialNoticeFor: null,
|
|
276
|
+
confirming: null,
|
|
148
277
|
};
|
|
149
278
|
}
|
|
150
279
|
export function credentialUnavailableCopy(capability) {
|
|
@@ -156,13 +285,18 @@ export function credentialUnavailableCopy(capability) {
|
|
|
156
285
|
}
|
|
157
286
|
return "Credential management requires an interactive user. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
|
|
158
287
|
}
|
|
159
|
-
|
|
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) {
|
|
160
293
|
if (status === "loading")
|
|
161
294
|
return "Loading details";
|
|
162
295
|
if (status === "ok")
|
|
163
296
|
return "Connected";
|
|
164
|
-
if (status === "auth_required")
|
|
165
|
-
return "Authorization needed";
|
|
297
|
+
if (status === "auth_required") {
|
|
298
|
+
return problem === "credential_required" ? "Credential needed" : "Authorization needed";
|
|
299
|
+
}
|
|
166
300
|
return "Unavailable";
|
|
167
301
|
}
|
|
168
302
|
export function toolCountLabel(count) {
|
|
@@ -182,16 +316,24 @@ export function summarizeConnectors(connectors) {
|
|
|
182
316
|
total: connectors.length,
|
|
183
317
|
connected: 0,
|
|
184
318
|
attention: 0,
|
|
319
|
+
credentials: 0,
|
|
185
320
|
unavailable: 0,
|
|
321
|
+
loading: 0,
|
|
186
322
|
tools: 0,
|
|
187
323
|
drifting: 0,
|
|
188
324
|
};
|
|
189
325
|
for (const connector of connectors) {
|
|
190
326
|
if (connector.status === "ok")
|
|
191
327
|
summary.connected += 1;
|
|
192
|
-
else if (connector.status === "auth_required")
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
337
|
summary.unavailable += 1;
|
|
196
338
|
summary.tools += connector.toolCount || 0;
|
|
197
339
|
if (driftState(connector.catalogDrift) === "warning")
|
|
@@ -205,6 +347,16 @@ export function summarizeConnectors(connectors) {
|
|
|
205
347
|
* zero, so a healthy deployment stays short.
|
|
206
348
|
*/
|
|
207
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
|
+
}
|
|
208
360
|
return [
|
|
209
361
|
{ text: `${summary.connected} connected`, tone: "neutral" },
|
|
210
362
|
...(summary.attention
|
|
@@ -215,10 +367,21 @@ export function connectorSummaryParts(summary) {
|
|
|
215
367
|
},
|
|
216
368
|
]
|
|
217
369
|
: []),
|
|
370
|
+
...(summary.credentials
|
|
371
|
+
? [
|
|
372
|
+
{
|
|
373
|
+
text: `${summary.credentials} need${summary.credentials === 1 ? "s" : ""} a credential`,
|
|
374
|
+
tone: "warn",
|
|
375
|
+
},
|
|
376
|
+
]
|
|
377
|
+
: []),
|
|
218
378
|
...(summary.unavailable
|
|
219
379
|
? [{ text: `${summary.unavailable} unavailable`, tone: "danger" }]
|
|
220
380
|
: []),
|
|
221
381
|
{ text: toolCountLabel(summary.tools), tone: "neutral" },
|
|
382
|
+
...(summary.loading
|
|
383
|
+
? [{ text: `${summary.loading} still loading`, tone: "neutral" }]
|
|
384
|
+
: []),
|
|
222
385
|
];
|
|
223
386
|
}
|
|
224
387
|
/**
|
|
@@ -262,6 +425,18 @@ const PROBLEM_COPY = {
|
|
|
262
425
|
export function problemCopy(problem) {
|
|
263
426
|
return problem ? PROBLEM_COPY[problem] ?? null : null;
|
|
264
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
|
+
}
|
|
265
440
|
/** Who owns this connector's downstream credentials, in two words. */
|
|
266
441
|
export function authScopeLabel(scope) {
|
|
267
442
|
return scope === "personal" ? "personal auth" : "shared auth";
|
|
@@ -348,14 +523,31 @@ export function formatDate(value) {
|
|
|
348
523
|
if (!value)
|
|
349
524
|
return "";
|
|
350
525
|
const date = new Date(value);
|
|
351
|
-
|
|
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;
|
|
352
543
|
}
|
|
544
|
+
/** Who made a call: their name when there is one, and how they signed in. */
|
|
353
545
|
export function actorLabel(actor) {
|
|
354
546
|
if (!actor?.kind)
|
|
355
|
-
return "
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return
|
|
547
|
+
return "Unknown caller";
|
|
548
|
+
const who = actor.label || actor.id;
|
|
549
|
+
const kind = actorKindLabel(actor.kind);
|
|
550
|
+
return who ? `${who} (${kind})` : kind;
|
|
359
551
|
}
|
|
360
552
|
/**
|
|
361
553
|
* The stable id behind a friendly label, shown only when a label or namespace
|
|
@@ -384,6 +576,9 @@ function activityMatches(event, query) {
|
|
|
384
576
|
event.actor?.id,
|
|
385
577
|
event.actor?.namespace,
|
|
386
578
|
event.actor?.label,
|
|
579
|
+
activityOutcomeBadge(event.outcome).label,
|
|
580
|
+
activityDetail(event),
|
|
581
|
+
actorKindLabel(event.actor?.kind),
|
|
387
582
|
].some((value) => String(value ?? "").toLowerCase().includes(q));
|
|
388
583
|
}
|
|
389
584
|
export function filterActivity(events, query) {
|
|
@@ -392,9 +587,9 @@ export function filterActivity(events, query) {
|
|
|
392
587
|
/** Counts only. What the deployment ran, never what it sent or received. */
|
|
393
588
|
export function activitySummary(events) {
|
|
394
589
|
if (events.length === 0)
|
|
395
|
-
return "
|
|
590
|
+
return "";
|
|
396
591
|
const tools = new Set(events.map((event) => event.address)).size;
|
|
397
|
-
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"}`;
|
|
398
593
|
}
|
|
399
594
|
// A pause and an approval are neither success nor failure: each gets its own
|
|
400
595
|
// class, and the stylesheet paints neither as an error.
|
|
@@ -409,25 +604,85 @@ const ACTIVITY_OUTCOMES = [
|
|
|
409
604
|
export function activityOutcomeClass(outcome) {
|
|
410
605
|
return ACTIVITY_OUTCOMES.includes(outcome) ? outcome : "error";
|
|
411
606
|
}
|
|
412
|
-
/**
|
|
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. */
|
|
413
649
|
export function activityDetail(event) {
|
|
414
|
-
const parts = [event.source];
|
|
650
|
+
const parts = [SOURCE_LABELS[event.source] ?? event.source];
|
|
415
651
|
if (event.approval === "tool")
|
|
416
652
|
parts.push("approved for the rest of the run");
|
|
417
653
|
if (event.approval === "call")
|
|
418
654
|
parts.push("approved for this call");
|
|
419
655
|
if (event.attempts > 1)
|
|
420
656
|
parts.push(`${event.attempts} attempts`);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
parts.push(
|
|
428
|
-
}
|
|
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);
|
|
429
664
|
return parts.join(" · ");
|
|
430
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
|
+
}
|
|
431
686
|
export function credentialStateLabel(credential) {
|
|
432
687
|
if (!credential.configured)
|
|
433
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)}}";
|
package/dist/registry.js
CHANGED
|
@@ -767,9 +767,10 @@ export class Registry {
|
|
|
767
767
|
});
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
|
-
// Remove the persisted catalog: the manifest
|
|
771
|
-
//
|
|
772
|
-
// read's error first, then the
|
|
770
|
+
// Remove the persisted catalog: the manifest and every chunk it names, all
|
|
771
|
+
// at once under the chunk I/O bound. Every delete is attempted; the effect
|
|
772
|
+
// fails with the manifest read's error first, then the manifest delete's,
|
|
773
|
+
// then the first chunk delete's by index.
|
|
773
774
|
deleteCatalog(id) {
|
|
774
775
|
return Effect.gen({ self: this }, function* () {
|
|
775
776
|
const read = yield* Effect.result(storageGet(this.catalogKey(id)));
|
|
@@ -777,18 +778,20 @@ export class Registry {
|
|
|
777
778
|
? this.parseCatalogManifest(read.success)
|
|
778
779
|
: null;
|
|
779
780
|
// The root is authoritative, so attempt its deletion even when the
|
|
780
|
-
// best-effort read needed for physical chunk cleanup failed.
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
781
|
+
// best-effort read needed for physical chunk cleanup failed. It need
|
|
782
|
+
// not land first: a reader that finds the root but not a chunk has an
|
|
783
|
+
// incomplete catalog, which is never served, and an eventually
|
|
784
|
+
// consistent store could show it that way whatever the order.
|
|
785
|
+
const keys = [
|
|
786
|
+
this.catalogKey(id),
|
|
787
|
+
...Array.from({ length: manifest?.chunkCount ?? 0 }, (_, index) => this.catalogChunkKey(id, manifest.revision, index)),
|
|
788
|
+
];
|
|
789
|
+
const deleted = yield* forEachChunk(keys, (key) => storageDelete(key));
|
|
788
790
|
if (Result.isFailure(read))
|
|
789
791
|
return yield* Effect.fail(read.failure);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
+
const failed = deleted.find(Result.isFailure);
|
|
793
|
+
if (failed)
|
|
794
|
+
return yield* Effect.fail(failed.failure);
|
|
792
795
|
});
|
|
793
796
|
}
|
|
794
797
|
catalogGeneration(id) {
|
package/dist/routes/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createMcpHandler, isLegacyRequest, McpServer, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
|
|
2
2
|
import { Duration, Effect, Exit, Option, Result, Scope } from "effect";
|
|
3
|
+
import { notFoundResponse } from "../branding.js";
|
|
3
4
|
import { registerExecuteTool } from "../execute.js";
|
|
4
5
|
import { ExecutorAdmissionError, } from "../executor-admission.js";
|
|
5
6
|
import { registerMetaTools } from "../meta-tools.js";
|
|
@@ -495,7 +496,10 @@ export function createMcpRoute(opts) {
|
|
|
495
496
|
if (!pool || reason !== "granted") {
|
|
496
497
|
opts.logger.warn(`[connecta] refused /mcp/${poolName} with 404: pool ${reason}` +
|
|
497
498
|
(authz.actor.id ? ` for ${loggableValue(authz.actor.id)}` : ""));
|
|
498
|
-
|
|
499
|
+
// The server's own 404, so a browser sees the page every unserved
|
|
500
|
+
// path shows and an MCP client (which never asks for text/html)
|
|
501
|
+
// still reads a plain "Not Found".
|
|
502
|
+
return cors(notFoundResponse(request, opts));
|
|
499
503
|
}
|
|
500
504
|
access = intersectAccess(authz, pool.access);
|
|
501
505
|
}
|