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