@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
package/dist/server.js
CHANGED
|
@@ -1,27 +1,155 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { aggregateCallAdmissionSnapshots } from "./call-admission.js";
|
|
2
3
|
import { isAdmittingExecutor } from "./executor-admission.js";
|
|
3
4
|
import { createMcpRoute, MCP_CORS_HEADERS } from "./routes/mcp.js";
|
|
4
5
|
import { routeOAuthCallback, } from "./routes/oauth.js";
|
|
6
|
+
import { runEdge } from "./runtime/run.js";
|
|
5
7
|
import { withSecurityHeaders, } from "./routes/shared.js";
|
|
8
|
+
const isArtifactPath = (path) => path === "/artifacts" || path.startsWith("/artifacts/");
|
|
6
9
|
/**
|
|
7
10
|
* Build the Web-standard fetch handler.
|
|
8
11
|
*
|
|
9
12
|
* Route ordering is the contract: private mutation routes precede wildcard
|
|
10
13
|
* OPTIONS, and the security wrapper is applied to every response, including 404s.
|
|
14
|
+
*
|
|
15
|
+
* Each request is one fiber tied to `request.signal`: when its caller leaves,
|
|
16
|
+
* the request stops waiting on whatever it was waiting on, and what it holds
|
|
17
|
+
* is released (routes/mcp.ts). It runs on no Connecta runtime, because
|
|
18
|
+
* `/health` and a closed deployment's 503 must keep answering after
|
|
19
|
+
* `close()` has disposed that runtime.
|
|
11
20
|
*/
|
|
12
21
|
export function createFetchHandler(opts) {
|
|
13
22
|
const { auth, publicUrl, registry } = opts;
|
|
14
23
|
const routeMcp = createMcpRoute(opts);
|
|
15
|
-
|
|
24
|
+
// The first provider's metadata answer for this request, if any has one.
|
|
25
|
+
const metadata = (request, baseUrl) => Effect.gen(function* () {
|
|
26
|
+
for (const provider of auth) {
|
|
27
|
+
const handleMetadata = provider.handleMetadata?.bind(provider);
|
|
28
|
+
if (handleMetadata) {
|
|
29
|
+
const response = yield* Effect.promise(async () => handleMetadata(request, baseUrl));
|
|
30
|
+
if (response)
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
});
|
|
36
|
+
const health = async () => {
|
|
37
|
+
// The executor is required, so code admission always has a shape to
|
|
38
|
+
// report: either the executor's own pool or the fallback controller
|
|
39
|
+
// wrapped around it at construction.
|
|
40
|
+
const codeAdmission = isAdmittingExecutor(opts.executor)
|
|
41
|
+
? opts.executor.admissionSnapshot?.()
|
|
42
|
+
: undefined;
|
|
43
|
+
return Response.json({
|
|
44
|
+
status: "ok",
|
|
45
|
+
connectors: registry.listConnectors().length,
|
|
46
|
+
server: opts.serverInfo,
|
|
47
|
+
// Which sandbox, not how it is tuned: `connecta doctor` reports the
|
|
48
|
+
// executor it just exercised rather than assuming one, and a
|
|
49
|
+
// deployment whose executor identifies as nothing omits the key
|
|
50
|
+
// instead of inviting a guess (#368).
|
|
51
|
+
...(opts.executorName !== undefined
|
|
52
|
+
? { executor: { name: opts.executorName } }
|
|
53
|
+
: {}),
|
|
54
|
+
// Whether a program's write pauses for resume_execution or fails E4.
|
|
55
|
+
// The tool is listed either way; this is the only place an operator
|
|
56
|
+
// can see which answer it gives (#565).
|
|
57
|
+
resumableWrites: opts.resumable !== undefined,
|
|
58
|
+
// Counts only, from refreshes that already happened — the endpoint
|
|
59
|
+
// asks no downstream anything, and `connecta doctor` reads it to
|
|
60
|
+
// report a stale allowlist without a probe of its own (#343).
|
|
61
|
+
// Stable 64-bit hashes preserve that shape without publishing ids.
|
|
62
|
+
catalogDrift: Object.fromEntries(await Promise.all(Object.entries(registry.catalogDriftSnapshot()).map(async ([id, report]) => {
|
|
63
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(id)));
|
|
64
|
+
const key = Array.from(hash.subarray(0, 8), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
65
|
+
return [key, report];
|
|
66
|
+
}))),
|
|
67
|
+
admission: {
|
|
68
|
+
policy: "global-fifo",
|
|
69
|
+
requests: opts.requestAdmission.snapshot(),
|
|
70
|
+
code: codeAdmission ?? { managedByExecutor: true },
|
|
71
|
+
downstreamCalls: {
|
|
72
|
+
policy: "connector-partitioned-per-runtime",
|
|
73
|
+
aggregate: aggregateCallAdmissionSnapshots(Object.values(registry.callAdmissionSnapshot())),
|
|
74
|
+
},
|
|
75
|
+
reservedRoutes: [
|
|
76
|
+
"/health",
|
|
77
|
+
...(opts.ui?.reservedPaths ?? []),
|
|
78
|
+
...(opts.ui && opts.activity?.list ? ["/activity"] : []),
|
|
79
|
+
...(opts.ui && opts.artifactsModule ? ["/artifacts", "/artifacts/*"] : []),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
...(opts.deploymentInfo ? { deployment: opts.deploymentInfo } : {}),
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const route = (context) => Effect.gen(function* () {
|
|
86
|
+
const { request, path, baseUrl } = context;
|
|
87
|
+
// Private mutations own OPTIONS so they never inherit wildcard CORS.
|
|
88
|
+
const ui = opts.ui;
|
|
89
|
+
if (ui) {
|
|
90
|
+
const uiResponse = yield* Effect.promise(() => ui.handle(context));
|
|
91
|
+
if (uiResponse)
|
|
92
|
+
return uiResponse;
|
|
93
|
+
}
|
|
94
|
+
if (request.method === "OPTIONS") {
|
|
95
|
+
const preflight = yield* routeMcp.handle(context);
|
|
96
|
+
if (preflight)
|
|
97
|
+
return preflight;
|
|
98
|
+
const response = yield* metadata(request, baseUrl);
|
|
99
|
+
if (response)
|
|
100
|
+
return response;
|
|
101
|
+
return new Response(null, {
|
|
102
|
+
status: 204,
|
|
103
|
+
headers: MCP_CORS_HEADERS,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (path.startsWith("/.well-known/")) {
|
|
107
|
+
const response = yield* metadata(request, baseUrl);
|
|
108
|
+
if (response)
|
|
109
|
+
return response;
|
|
110
|
+
return new Response("Not Found", { status: 404 });
|
|
111
|
+
}
|
|
112
|
+
if (path === "/health")
|
|
113
|
+
return yield* Effect.promise(health);
|
|
114
|
+
const oauthCallback = yield* routeOAuthCallback(context);
|
|
115
|
+
if (oauthCallback)
|
|
116
|
+
return oauthCallback;
|
|
117
|
+
const mcp = yield* routeMcp.handle(context);
|
|
118
|
+
if (mcp)
|
|
119
|
+
return mcp;
|
|
120
|
+
return new Response("Not Found", { status: 404 });
|
|
121
|
+
});
|
|
122
|
+
const serve = (request, runtimeContext) => Effect.suspend(() => {
|
|
16
123
|
const url = new URL(request.url);
|
|
17
124
|
const baseUrl = publicUrl ?? url.origin;
|
|
18
125
|
const path = url.pathname;
|
|
19
126
|
const defer = runtimeContext
|
|
20
127
|
? runtimeContext.waitUntil.bind(runtimeContext)
|
|
21
128
|
: undefined;
|
|
129
|
+
// The artifact hostname has no MCP, health, OAuth, or operator routes.
|
|
130
|
+
// Dispatch before MCP origin checks so even a hostile /mcp is a plain 404.
|
|
131
|
+
if (opts.artifactOrigin && url.origin === new URL(opts.artifactOrigin).origin) {
|
|
132
|
+
if (!isArtifactPath(path) || !opts.ui || !opts.artifactsModule) {
|
|
133
|
+
return Effect.succeed(withSecurityHeaders(new Response("Not Found", { status: 404 }), url, path));
|
|
134
|
+
}
|
|
135
|
+
const context = {
|
|
136
|
+
request, url, path, baseUrl: opts.artifactOrigin, opts, defer, runtimeContext,
|
|
137
|
+
};
|
|
138
|
+
return Effect.map(Effect.promise(() => opts.ui.handle(context)), response => withSecurityHeaders(response ?? new Response("Not Found", { status: 404 }), url, path));
|
|
139
|
+
}
|
|
140
|
+
if (opts.artifactOrigin && isArtifactPath(path) && opts.ui && opts.artifactsModule) {
|
|
141
|
+
const target = new URL(opts.artifactOrigin);
|
|
142
|
+
target.pathname = path;
|
|
143
|
+
target.search = url.search;
|
|
144
|
+
return Effect.succeed(withSecurityHeaders(new Response(null, {
|
|
145
|
+
status: 308,
|
|
146
|
+
headers: { Location: target.toString() },
|
|
147
|
+
}), url, path));
|
|
148
|
+
}
|
|
22
149
|
const originRefusal = routeMcp.rejectOrigin(request);
|
|
23
|
-
if (originRefusal)
|
|
24
|
-
return withSecurityHeaders(originRefusal, url, path);
|
|
150
|
+
if (originRefusal) {
|
|
151
|
+
return Effect.succeed(withSecurityHeaders(originRefusal, url, path));
|
|
152
|
+
}
|
|
25
153
|
// Container and orchestrator probes reach /health over plain HTTP on
|
|
26
154
|
// loopback, where no proxy has set X-Forwarded-Proto. Redirecting them to
|
|
27
155
|
// the public origin would make an internal liveness check depend on
|
|
@@ -43,10 +171,10 @@ export function createFetchHandler(opts) {
|
|
|
43
171
|
target.pathname = path === "/ui" ? "/" : url.pathname;
|
|
44
172
|
target.search = url.search;
|
|
45
173
|
target.hash = "";
|
|
46
|
-
return withSecurityHeaders(new Response(null, {
|
|
174
|
+
return Effect.succeed(withSecurityHeaders(new Response(null, {
|
|
47
175
|
status: 308,
|
|
48
176
|
headers: { Location: target.toString() },
|
|
49
|
-
}), url, path);
|
|
177
|
+
}), url, path));
|
|
50
178
|
}
|
|
51
179
|
const context = {
|
|
52
180
|
request,
|
|
@@ -57,89 +185,9 @@ export function createFetchHandler(opts) {
|
|
|
57
185
|
defer,
|
|
58
186
|
runtimeContext,
|
|
59
187
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return uiResponse;
|
|
65
|
-
if (request.method === "OPTIONS") {
|
|
66
|
-
const preflight = await routeMcp.handle(context);
|
|
67
|
-
if (preflight)
|
|
68
|
-
return preflight;
|
|
69
|
-
for (const provider of auth) {
|
|
70
|
-
if (provider.handleMetadata) {
|
|
71
|
-
const response = await provider.handleMetadata(request, baseUrl);
|
|
72
|
-
if (response)
|
|
73
|
-
return response;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return new Response(null, {
|
|
77
|
-
status: 204,
|
|
78
|
-
headers: MCP_CORS_HEADERS,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
if (path.startsWith("/.well-known/")) {
|
|
82
|
-
for (const provider of auth) {
|
|
83
|
-
if (provider.handleMetadata) {
|
|
84
|
-
const response = await provider.handleMetadata(request, baseUrl);
|
|
85
|
-
if (response)
|
|
86
|
-
return response;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return new Response("Not Found", { status: 404 });
|
|
90
|
-
}
|
|
91
|
-
if (path === "/health") {
|
|
92
|
-
// The executor is required, so code admission always has a shape to
|
|
93
|
-
// report: either the executor's own pool or the fallback controller
|
|
94
|
-
// wrapped around it at construction.
|
|
95
|
-
const codeAdmission = isAdmittingExecutor(opts.executor)
|
|
96
|
-
? opts.executor.admissionSnapshot?.()
|
|
97
|
-
: undefined;
|
|
98
|
-
return Response.json({
|
|
99
|
-
status: "ok",
|
|
100
|
-
connectors: registry.listConnectors().length,
|
|
101
|
-
server: opts.serverInfo,
|
|
102
|
-
// Which sandbox, not how it is tuned: `connecta doctor` reports the
|
|
103
|
-
// executor it just exercised rather than assuming one, and a
|
|
104
|
-
// deployment whose executor identifies as nothing omits the key
|
|
105
|
-
// instead of inviting a guess (#368).
|
|
106
|
-
...(opts.executorName !== undefined
|
|
107
|
-
? { executor: { name: opts.executorName } }
|
|
108
|
-
: {}),
|
|
109
|
-
// Counts only, from refreshes that already happened — the endpoint
|
|
110
|
-
// asks no downstream anything, and `connecta doctor` reads it to
|
|
111
|
-
// report a stale allowlist without a probe of its own (#343).
|
|
112
|
-
// Stable 64-bit hashes preserve that shape without publishing ids.
|
|
113
|
-
catalogDrift: Object.fromEntries(await Promise.all(Object.entries(registry.catalogDriftSnapshot()).map(async ([id, report]) => {
|
|
114
|
-
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(id)));
|
|
115
|
-
const key = Array.from(hash.subarray(0, 8), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
116
|
-
return [key, report];
|
|
117
|
-
}))),
|
|
118
|
-
admission: {
|
|
119
|
-
policy: "global-fifo",
|
|
120
|
-
requests: opts.requestAdmission.snapshot(),
|
|
121
|
-
code: codeAdmission ?? { managedByExecutor: true },
|
|
122
|
-
downstreamCalls: {
|
|
123
|
-
policy: "connector-partitioned-per-runtime",
|
|
124
|
-
aggregate: aggregateCallAdmissionSnapshots(Object.values(registry.callAdmissionSnapshot())),
|
|
125
|
-
},
|
|
126
|
-
reservedRoutes: [
|
|
127
|
-
"/health",
|
|
128
|
-
...(opts.ui?.reservedPaths ?? []),
|
|
129
|
-
...(opts.ui && opts.activity?.list ? ["/activity"] : []),
|
|
130
|
-
],
|
|
131
|
-
},
|
|
132
|
-
...(opts.deploymentInfo ? { deployment: opts.deploymentInfo } : {}),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
const oauthCallback = await routeOAuthCallback(context);
|
|
136
|
-
if (oauthCallback)
|
|
137
|
-
return oauthCallback;
|
|
138
|
-
const mcp = await routeMcp.handle(context);
|
|
139
|
-
if (mcp)
|
|
140
|
-
return mcp;
|
|
141
|
-
return new Response("Not Found", { status: 404 });
|
|
142
|
-
};
|
|
143
|
-
return withSecurityHeaders(await route(), url, path);
|
|
188
|
+
return Effect.map(route(context), (response) => withSecurityHeaders(response, url, path));
|
|
189
|
+
});
|
|
190
|
+
return function fetch(request, runtimeContext) {
|
|
191
|
+
return runEdge(serve(request, runtimeContext), { signal: request.signal });
|
|
144
192
|
};
|
|
145
193
|
}
|
package/dist/skills.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { boundedEchoText } from "./errors.js";
|
|
2
|
-
|
|
2
|
+
const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding.";
|
|
3
|
+
const RECOVERY = 'After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
|
|
4
|
+
/**
|
|
5
|
+
* The MCP instructions of a deployment whose programs pause at writes. The
|
|
6
|
+
* route sentence is its own, not ROUTE's: with writes in programs, "one known
|
|
7
|
+
* write" is the only work left for call_destructive_tool, and an agent told
|
|
8
|
+
* only that programs *may* write reads each write after a lookup as a known
|
|
9
|
+
* one and sends them one prompt at a time.
|
|
10
|
+
*/
|
|
11
|
+
export const CONNECTA_INSTRUCTIONS = `Choose a route before discovery. A known-address read needs only call_tool; one known write, call_destructive_tool. Everything else starts with execute_code to discover, call, and return the answer: unknown addresses, reduction, multiple or dependent calls, loops, joins, branches, and writes that follow reads. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding. A program's call to a tool not annotated readOnlyHint: true pauses unsent; resume_execution with the returned token, address, and args approves it and continues the program (approval "tool" covers that tool for the run). Keep catalog inspection top level with search_tools. ${RECOVERY}`;
|
|
12
|
+
/**
|
|
13
|
+
* The instructions of a deployment without resumable writes, whose programs
|
|
14
|
+
* refuse every write: the routing it always had.
|
|
15
|
+
*/
|
|
16
|
+
export const READ_ONLY_PROGRAM_INSTRUCTIONS = `${ROUTE} Only readOnlyHint: true tools run there. Keep catalog inspection and unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. ${RECOVERY}`;
|
|
3
17
|
const USAGE_SKILL_BASE = `# Connecta usage
|
|
4
18
|
|
|
5
19
|
## The surface
|
|
6
20
|
|
|
7
|
-
|
|
21
|
+
Eight tools: \`execute_code\`, \`resume_execution\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Discovery and multi-call work live in a program. Top-level search remains for catalog inspection and approval-required work.
|
|
8
22
|
|
|
9
23
|
Follow the MCP instructions for routing. Read at most once for syntax or repair.
|
|
10
24
|
|
|
@@ -25,7 +39,7 @@ Search and call in one run when schemas suffice. For unfamiliar result formats,
|
|
|
25
39
|
|
|
26
40
|
For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is 8), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An \`outputSchemaSource: "observed"\` shape is a hint, not a contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
27
41
|
|
|
28
|
-
- \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for
|
|
42
|
+
- \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for reads. These inputs filter discovery; they grant no authority.
|
|
29
43
|
- Use \`includeSchemas: "json"\` for programmatic schema inspection; compact schemas are text, not objects with \`.properties\`. Check connectorTitle for the account/environment, then address, purpose, annotations, inputs, and outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
30
44
|
- Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
|
|
31
45
|
- Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
|
|
@@ -34,27 +48,32 @@ For top-level catalog inspection or approval-required discovery, omit \`limit\`
|
|
|
34
48
|
- Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
|
|
35
49
|
- Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
|
|
36
50
|
|
|
37
|
-
Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and
|
|
51
|
+
Only tools explicitly annotated \`readOnlyHint: true\` are reachable unasked. Where the MCP instructions say programs pause, any other call pauses unsent and \`execute_code\` returns \`paused\` with the write and a token; repeat all three unchanged in top-level \`resume_execution\` to approve (\`approval: "tool"\` covers that tool for the run). Replay reruns the program against a journal: make the same calls in the same order. The catalog, credential, admission, and approval gates run below the sandbox; code cannot widen its authority.
|
|
38
52
|
|
|
39
53
|
## Errors and repair
|
|
40
54
|
|
|
41
55
|
Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Promise rejections retain these fields. Branch on fields, never prose. After a shared argument failure, repair one call before repeating it across other records. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
|
|
42
56
|
|
|
43
|
-
- \`destructive_tool_requires_approval\`: stop
|
|
57
|
+
- \`destructive_tool_requires_approval\`: stop and send the returned address through top-level \`call_destructive_tool\`.
|
|
58
|
+
- \`execution_paused\`: the run already stopped at a write; let it end.
|
|
59
|
+
- \`write_outcome_unknown\`: sent but unanswered, never re-sent; check the target.
|
|
60
|
+
- \`execution_expired\`, \`_diverged\`, \`_interrupted\`: check \`writes\`, then rerun.
|
|
44
61
|
- \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
|
|
45
62
|
- A truncated direct-call result: follow its \`get_result\` action. A truncated program result has no page handle; filter, map, or slice inside a new program.
|
|
46
63
|
- Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
|
|
47
64
|
|
|
48
65
|
For a direct call, \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. Every call makes one attempt; use the returned error classification and retry hint to decide whether to reissue. \`diagnostics: true\` adds timing.
|
|
49
66
|
|
|
50
|
-
\`get_result({ id, offset?, maxBytes? })\` returns \`{
|
|
67
|
+
\`get_result({ id, offset?, maxBytes? })\` returns a one-line JSON header \`{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }\`, a newline, and then the page as raw text, for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1, defaults to the result's cap, and is clamped to it; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the header reports the served offset. Follow \`nextAction\` until \`hasMore\` is false to reassemble pages. An unknown or expired id is an error.
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
The \`execute_code\` description states this deployment's host-call and write budgets and per-call deadline.
|
|
53
70
|
|
|
54
71
|
## Runtime portability
|
|
55
72
|
|
|
56
73
|
Portable code uses standard JavaScript builtins, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
|
|
57
74
|
|
|
75
|
+
\`Date.now()\` is the run's start and does not advance; \`Math.random()\` is seeded per run.
|
|
76
|
+
|
|
58
77
|
## Examples
|
|
59
78
|
|
|
60
79
|
One read-only call at a known address:
|
|
@@ -95,8 +114,8 @@ Connector guides appear in \`skills({})\` and discovery with an exact \`guide\`
|
|
|
95
114
|
/** Shared Connecta routing guidance, byte-identical across deployments. */
|
|
96
115
|
export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
|
|
97
116
|
/** The always-loaded MCP `instructions` string. */
|
|
98
|
-
export function instructionsFor() {
|
|
99
|
-
return CONNECTA_INSTRUCTIONS;
|
|
117
|
+
export function instructionsFor(resumableWrites) {
|
|
118
|
+
return resumableWrites ? CONNECTA_INSTRUCTIONS : READ_ONLY_PROGRAM_INSTRUCTIONS;
|
|
100
119
|
}
|
|
101
120
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
102
121
|
export function hasConnectorGuides(connectors) {
|
package/dist/storage/file.js
CHANGED
|
@@ -259,8 +259,9 @@ export function fileStorage(path, opts = {}) {
|
|
|
259
259
|
throw new Error(`[connecta] state file ${path} is closed.`);
|
|
260
260
|
};
|
|
261
261
|
const logger = opts.logger ?? console;
|
|
262
|
-
//
|
|
263
|
-
//
|
|
262
|
+
// With a sealing vault, downstream OAuth tokens here are ciphertext; without
|
|
263
|
+
// one they are cleartext, and OAuth flow state is plaintext either way. Keep
|
|
264
|
+
// the file owner-only regardless. Repair is best-effort: chmod is a no-op or throws on
|
|
264
265
|
// non-POSIX filesystems, and a loose mode must never keep the store from
|
|
265
266
|
// starting.
|
|
266
267
|
const tighten = () => {
|
|
@@ -377,5 +378,41 @@ export function fileStorage(path, opts = {}) {
|
|
|
377
378
|
.filter((key) => Boolean(fresh(key)) && key.startsWith(prefix))
|
|
378
379
|
.sort();
|
|
379
380
|
},
|
|
381
|
+
// Atomic twice over: the lock makes this process the only writer of the
|
|
382
|
+
// file, and nothing between the comparison and the persisted rename
|
|
383
|
+
// yields, so no other call on this store can interleave. A write that
|
|
384
|
+
// cannot be persisted is rolled back so memory never claims a value the
|
|
385
|
+
// file does not hold.
|
|
386
|
+
async compareAndSet(key, expected, next, opts) {
|
|
387
|
+
assertOpen();
|
|
388
|
+
lock.assertHeld();
|
|
389
|
+
const previous = fresh(key);
|
|
390
|
+
if ((previous?.value ?? null) !== expected)
|
|
391
|
+
return false;
|
|
392
|
+
if (next === null) {
|
|
393
|
+
if (!previous)
|
|
394
|
+
return true;
|
|
395
|
+
delete data[key];
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
data[key] = {
|
|
399
|
+
value: next,
|
|
400
|
+
...(opts?.ttlSeconds
|
|
401
|
+
? { exp: Date.now() + opts.ttlSeconds * 1000 }
|
|
402
|
+
: {}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
persist();
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (previous)
|
|
410
|
+
data[key] = previous;
|
|
411
|
+
else
|
|
412
|
+
delete data[key];
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
},
|
|
380
417
|
};
|
|
381
418
|
}
|
package/dist/storage/memory.js
CHANGED
|
@@ -12,27 +12,28 @@ export function memoryStorage() {
|
|
|
12
12
|
}
|
|
13
13
|
return e;
|
|
14
14
|
};
|
|
15
|
+
const write = (key, value, ttlSeconds) => {
|
|
16
|
+
// Rotate through at most 16 existing keys. Live entries cannot keep an
|
|
17
|
+
// expired tail resident forever, and no request starts a background job.
|
|
18
|
+
for (let i = 0; i < 16; i++) {
|
|
19
|
+
const next = sweep.next();
|
|
20
|
+
if (next.done) {
|
|
21
|
+
sweep = map.keys();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
fresh(next.value);
|
|
25
|
+
}
|
|
26
|
+
map.set(key, {
|
|
27
|
+
value,
|
|
28
|
+
...(ttlSeconds ? { exp: Date.now() + ttlSeconds * 1000 } : {}),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
15
31
|
return {
|
|
16
32
|
async get(key) {
|
|
17
33
|
return fresh(key)?.value ?? null;
|
|
18
34
|
},
|
|
19
35
|
async set(key, value, opts) {
|
|
20
|
-
|
|
21
|
-
// expired tail resident forever, and no request starts a background job.
|
|
22
|
-
for (let i = 0; i < 16; i++) {
|
|
23
|
-
const next = sweep.next();
|
|
24
|
-
if (next.done) {
|
|
25
|
-
sweep = map.keys();
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
fresh(next.value);
|
|
29
|
-
}
|
|
30
|
-
map.set(key, {
|
|
31
|
-
value,
|
|
32
|
-
...(opts?.ttlSeconds
|
|
33
|
-
? { exp: Date.now() + opts.ttlSeconds * 1000 }
|
|
34
|
-
: {}),
|
|
35
|
-
});
|
|
36
|
+
write(key, value, opts?.ttlSeconds);
|
|
36
37
|
},
|
|
37
38
|
async delete(key) {
|
|
38
39
|
map.delete(key);
|
|
@@ -45,5 +46,16 @@ export function memoryStorage() {
|
|
|
45
46
|
})
|
|
46
47
|
.sort();
|
|
47
48
|
},
|
|
49
|
+
// Atomic because nothing between the read and the write yields: the body
|
|
50
|
+
// runs to completion before any other call on this map can start.
|
|
51
|
+
async compareAndSet(key, expected, next, opts) {
|
|
52
|
+
if ((fresh(key)?.value ?? null) !== expected)
|
|
53
|
+
return false;
|
|
54
|
+
if (next === null)
|
|
55
|
+
map.delete(key);
|
|
56
|
+
else
|
|
57
|
+
write(key, next, opts?.ttlSeconds);
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
48
60
|
};
|
|
49
61
|
}
|
package/dist/timeout.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The deadline vocabulary shared by downstream discovery probes and tool calls.
|
|
2
2
|
// One definition keeps those waits bounded consistently.
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
3
5
|
/**
|
|
4
6
|
* Generous default bound for a single downstream probe/catalog call. High enough
|
|
5
7
|
* to trip only on a pathological hang, not a realistically slow probe.
|
|
@@ -12,54 +14,48 @@ export function normalizeTimeoutMs(value) {
|
|
|
12
14
|
}
|
|
13
15
|
return Math.max(1, Math.trunc(value));
|
|
14
16
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
const timer = setTimeout(() => finish(true), ms);
|
|
28
|
-
const cancel = () => finish(false);
|
|
29
|
-
signal?.addEventListener("abort", cancel, { once: true });
|
|
30
|
-
if (signal?.aborted)
|
|
31
|
-
cancel();
|
|
32
|
-
});
|
|
33
|
-
}
|
|
17
|
+
/**
|
|
18
|
+
* Run one operation under a deadline and the caller's signal, whichever ends
|
|
19
|
+
* it first.
|
|
20
|
+
*
|
|
21
|
+
* The operation gets a signal of its own: the caller's abort is forwarded to
|
|
22
|
+
* it, and the deadline aborts it with `timeoutError`. Either way the promise
|
|
23
|
+
* rejects with that signal's reason, without waiting for work that ignores
|
|
24
|
+
* the signal. A synchronous throw rejects like an asynchronous one. This is
|
|
25
|
+
* the Promise face of `withDeadlineEffect` (src/runtime/run.ts), which owns
|
|
26
|
+
* the contract; an Effect caller uses that directly.
|
|
27
|
+
*/
|
|
34
28
|
export function withDeadline(operation, options) {
|
|
29
|
+
return runEdge(withDeadlineEffect((signal) => Effect.tryPromise({
|
|
30
|
+
try: () => operation(signal),
|
|
31
|
+
catch: (error) => error,
|
|
32
|
+
}), options));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run `operation` under one signal that aborts when any of `signals` does —
|
|
36
|
+
* the MCP call's own and the inbound request's, for the program tools — and
|
|
37
|
+
* detach every listener afterwards, however it ends.
|
|
38
|
+
*/
|
|
39
|
+
export async function underAnySignal(signals, operation) {
|
|
35
40
|
const controller = new AbortController();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const timer = options.timeoutMs === undefined
|
|
47
|
-
? undefined
|
|
48
|
-
: setTimeout(() => controller.abort(options.timeoutError), options.timeoutMs);
|
|
49
|
-
let work;
|
|
41
|
+
const forwarders = signals
|
|
42
|
+
.filter((signal) => signal !== undefined)
|
|
43
|
+
.map((signal) => {
|
|
44
|
+
const forward = () => controller.abort(signal.reason);
|
|
45
|
+
if (signal.aborted)
|
|
46
|
+
forward();
|
|
47
|
+
else
|
|
48
|
+
signal.addEventListener("abort", forward, { once: true });
|
|
49
|
+
return { signal, forward };
|
|
50
|
+
});
|
|
50
51
|
try {
|
|
51
|
-
|
|
52
|
+
return await operation(controller.signal);
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
finally {
|
|
55
|
+
for (const { signal, forward } of forwarders) {
|
|
56
|
+
signal.removeEventListener("abort", forward);
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
|
-
return Promise.race([work, aborted]).finally(() => {
|
|
57
|
-
controller.abort();
|
|
58
|
-
if (timer)
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
controller.signal.removeEventListener("abort", onAbort);
|
|
61
|
-
options.signal?.removeEventListener("abort", forwardAbort);
|
|
62
|
-
});
|
|
63
59
|
}
|
|
64
60
|
/**
|
|
65
61
|
* Give one operation a caller-facing deadline and the matching cancellation
|
package/dist/tool-safety.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolDef } from "./types.js";
|
|
1
|
+
import type { Connector, ToolDef } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* The one fail-closed classification shared by discovery and invocation.
|
|
4
4
|
*
|
|
@@ -7,3 +7,29 @@ import type { ToolDef } from "./types.js";
|
|
|
7
7
|
* approval-visible call path.
|
|
8
8
|
*/
|
|
9
9
|
export declare function isExplicitlyReadOnly(definition: ToolDef): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* A deployment's approval exemptions, resolved once at construction from
|
|
12
|
+
* `execute.approval` (#566). Config is the only source: nothing an agent, the
|
|
13
|
+
* operator UI, or a downstream catalog says can add an entry.
|
|
14
|
+
*/
|
|
15
|
+
export interface ApprovalPolicy {
|
|
16
|
+
/** Connector id → the default for that connector's tools. */
|
|
17
|
+
readonly connectors: ReadonlyMap<string, "never" | "ask">;
|
|
18
|
+
/** Canonical `connector.tool` address → that tool's own setting. */
|
|
19
|
+
readonly tools: ReadonlyMap<string, "never" | "ask">;
|
|
20
|
+
}
|
|
21
|
+
export declare const NO_EXEMPTIONS: ApprovalPolicy;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a program may call this tool without pausing for approval.
|
|
24
|
+
*
|
|
25
|
+
* Only a tool that is *not* explicitly read-only can be exempt — a read-only
|
|
26
|
+
* tool needs no exemption, and reporting one would blur the two classes an
|
|
27
|
+
* exemption must never merge. The most specific setting wins: the tool's
|
|
28
|
+
* address, then its connector's entry, then the connector's own default,
|
|
29
|
+
* then `"ask"`. Annotations play no part beyond that first test: a downstream
|
|
30
|
+
* that could annotate its way out of approval would be granting itself the
|
|
31
|
+
* capability this exists to keep in config. Exempt never means read-only:
|
|
32
|
+
* discovery keeps it in the approval-required class, and `call_tool` still
|
|
33
|
+
* refuses it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isApprovalExempt(policy: ApprovalPolicy, connector: Pick<Connector, "id" | "approval">, toolName: string, definition: ToolDef): boolean;
|
package/dist/tool-safety.js
CHANGED
|
@@ -9,3 +9,34 @@ export function isExplicitlyReadOnly(definition) {
|
|
|
9
9
|
return (definition.annotations?.readOnlyHint === true &&
|
|
10
10
|
definition.annotations?.destructiveHint !== true);
|
|
11
11
|
}
|
|
12
|
+
export const NO_EXEMPTIONS = {
|
|
13
|
+
connectors: new Map(),
|
|
14
|
+
tools: new Map(),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Whether a program may call this tool without pausing for approval.
|
|
18
|
+
*
|
|
19
|
+
* Only a tool that is *not* explicitly read-only can be exempt — a read-only
|
|
20
|
+
* tool needs no exemption, and reporting one would blur the two classes an
|
|
21
|
+
* exemption must never merge. The most specific setting wins: the tool's
|
|
22
|
+
* address, then its connector's entry, then the connector's own default,
|
|
23
|
+
* then `"ask"`. Annotations play no part beyond that first test: a downstream
|
|
24
|
+
* that could annotate its way out of approval would be granting itself the
|
|
25
|
+
* capability this exists to keep in config. Exempt never means read-only:
|
|
26
|
+
* discovery keeps it in the approval-required class, and `call_tool` still
|
|
27
|
+
* refuses it.
|
|
28
|
+
*/
|
|
29
|
+
export function isApprovalExempt(policy, connector, toolName, definition) {
|
|
30
|
+
if (isExplicitlyReadOnly(definition))
|
|
31
|
+
return false;
|
|
32
|
+
// This tool starts another program. Letting a program call it while holding
|
|
33
|
+
// the only executor permit would deadlock; manual refresh is a top-level
|
|
34
|
+
// action, even though other artifact writes are exempt inside programs.
|
|
35
|
+
if (connector.id === "artifacts" && toolName === "run_refresh")
|
|
36
|
+
return false;
|
|
37
|
+
const setting = policy.tools.get(`${connector.id}.${toolName}`) ??
|
|
38
|
+
policy.connectors.get(connector.id) ??
|
|
39
|
+
connector.approval ??
|
|
40
|
+
"ask";
|
|
41
|
+
return setting === "never";
|
|
42
|
+
}
|