@zackbart/connecta 0.10.0 → 0.10.2
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 +113 -0
- package/CHANGELOG.md +83 -0
- package/README.md +62 -12
- package/bin/connecta.mjs +272 -0
- package/dist/access-tokens.d.ts +32 -0
- package/dist/access-tokens.d.ts.map +1 -0
- package/dist/access-tokens.js +225 -0
- package/dist/access-tokens.js.map +1 -0
- package/dist/catalog-service.d.ts +39 -1
- package/dist/catalog-service.d.ts.map +1 -1
- package/dist/catalog-service.js +133 -11
- package/dist/catalog-service.js.map +1 -1
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +113 -13
- package/dist/catalog.js.map +1 -1
- package/dist/execute.d.ts +45 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +265 -68
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +1 -5
- package/dist/invocation.js.map +1 -1
- package/dist/meta-tools.d.ts +1 -0
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +410 -12
- package/dist/meta-tools.js.map +1 -1
- package/dist/operator-ui/generated.d.ts +2 -2
- package/dist/operator-ui/generated.d.ts.map +1 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/generated.js.map +1 -1
- package/dist/operator-ui/model.d.ts +2 -0
- package/dist/operator-ui/model.d.ts.map +1 -1
- package/dist/operator-ui/model.js.map +1 -1
- package/dist/routes/access-tokens.d.ts +7 -0
- package/dist/routes/access-tokens.d.ts.map +1 -0
- package/dist/routes/access-tokens.js +84 -0
- package/dist/routes/access-tokens.js.map +1 -0
- package/dist/routes/shared.d.ts +3 -0
- package/dist/routes/shared.d.ts.map +1 -1
- package/dist/routes/shared.js.map +1 -1
- package/dist/routes/ui.d.ts.map +1 -1
- package/dist/routes/ui.js +9 -1
- package/dist/routes/ui.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +5 -0
- package/dist/storage/file.js.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +8 -0
- package/dist/storage/memory.js.map +1 -1
- package/dist/tool-safety.d.ts +10 -0
- package/dist/tool-safety.d.ts.map +1 -0
- package/dist/tool-safety.js +12 -0
- package/dist/tool-safety.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +44 -1
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -0
- package/documentation/auth.md +58 -0
- package/documentation/call-admission.md +7 -0
- package/documentation/code-first-exploration.md +292 -0
- package/documentation/code-mode.md +697 -0
- package/documentation/connector-guides.md +7 -0
- package/documentation/connectors.md +63 -0
- package/documentation/mcp-2026-07-28.md +46 -0
- package/documentation/meta-tools.md +167 -0
- package/documentation/operations.md +7 -0
- package/documentation/operator-ui.md +7 -0
- package/documentation/request-admission.md +7 -0
- package/documentation/storage-and-credentials.md +54 -0
- package/ethos.md +132 -0
- package/examples/node/README.md +53 -0
- package/examples/node/src/index.ts +73 -0
- package/examples/worker/README.md +160 -0
- package/examples/worker/src/cloudflare-kv.ts +43 -0
- package/examples/worker/src/d1-activity-row.ts +100 -0
- package/examples/worker/src/d1-activity.ts +144 -0
- package/examples/worker/src/index.ts +136 -0
- package/examples/worker/wrangler.jsonc +26 -0
- package/package.json +11 -1
- package/src/access-tokens.ts +289 -0
- package/src/catalog-service.ts +177 -15
- package/src/catalog.ts +143 -12
- package/src/execute.ts +372 -96
- package/src/index.ts +33 -1
- package/src/invocation.ts +1 -8
- package/src/meta-tools.ts +504 -11
- package/src/operator-ui/browser.css +63 -0
- package/src/operator-ui/browser.ts +288 -2
- package/src/operator-ui/generated.ts +2 -2
- package/src/operator-ui/model.ts +6 -0
- package/src/routes/access-tokens.ts +115 -0
- package/src/routes/shared.ts +3 -0
- package/src/routes/ui.ts +9 -0
- package/src/server.ts +5 -0
- package/src/skills.ts +1 -1
- package/src/storage/file.ts +5 -0
- package/src/storage/memory.ts +8 -0
- package/src/tool-safety.ts +15 -0
- package/src/types.ts +5 -0
- package/src/ui.ts +50 -1
- package/src/version.ts +1 -1
- package/templates/node/.env.example +5 -0
- package/templates/node/AGENTS.md +19 -0
- package/templates/node/README.md +33 -0
- package/templates/node/package.json +23 -0
- package/templates/node/src/index.ts +43 -0
- package/templates/node/tsconfig.json +12 -0
package/src/execute.ts
CHANGED
|
@@ -34,6 +34,145 @@ import type {
|
|
|
34
34
|
const EXECUTE_MAX_HOST_CALLS = 20;
|
|
35
35
|
export const EXECUTE_MAX_BATCH_CALLS = 10;
|
|
36
36
|
const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
|
|
37
|
+
const diagnosticsEncoder = new TextEncoder();
|
|
38
|
+
|
|
39
|
+
type ExecuteDiagnosticOperation = "search" | "describe" | "call" | "batch";
|
|
40
|
+
|
|
41
|
+
interface ExecuteOperationDiagnostics {
|
|
42
|
+
operation: ExecuteDiagnosticOperation;
|
|
43
|
+
count: number;
|
|
44
|
+
failures: number;
|
|
45
|
+
durationMs: number;
|
|
46
|
+
resultBytes: number;
|
|
47
|
+
catalogMs: number;
|
|
48
|
+
connectorMs: number;
|
|
49
|
+
calls?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type MutableOperationDiagnostics = ExecuteOperationDiagnostics;
|
|
53
|
+
|
|
54
|
+
class ExecuteDiagnostics {
|
|
55
|
+
private readonly started = Date.now();
|
|
56
|
+
private readonly operations = new Map<
|
|
57
|
+
ExecuteDiagnosticOperation,
|
|
58
|
+
MutableOperationDiagnostics
|
|
59
|
+
>();
|
|
60
|
+
admissionMs = 0;
|
|
61
|
+
setupMs = 0;
|
|
62
|
+
executorWallMs = 0;
|
|
63
|
+
|
|
64
|
+
private stats(operation: ExecuteDiagnosticOperation) {
|
|
65
|
+
let stats = this.operations.get(operation);
|
|
66
|
+
if (!stats) {
|
|
67
|
+
stats = {
|
|
68
|
+
operation,
|
|
69
|
+
count: 0,
|
|
70
|
+
failures: 0,
|
|
71
|
+
durationMs: 0,
|
|
72
|
+
resultBytes: 0,
|
|
73
|
+
catalogMs: 0,
|
|
74
|
+
connectorMs: 0,
|
|
75
|
+
};
|
|
76
|
+
this.operations.set(operation, stats);
|
|
77
|
+
}
|
|
78
|
+
return stats;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
recordCatalog(
|
|
82
|
+
operation: "search" | "describe",
|
|
83
|
+
durationMs: number,
|
|
84
|
+
ok: boolean,
|
|
85
|
+
result?: unknown,
|
|
86
|
+
): void {
|
|
87
|
+
const stats = this.stats(operation);
|
|
88
|
+
stats.count++;
|
|
89
|
+
stats.failures += ok ? 0 : 1;
|
|
90
|
+
stats.durationMs += durationMs;
|
|
91
|
+
stats.catalogMs += durationMs;
|
|
92
|
+
if (ok) stats.resultBytes += serializedDiagnosticBytes(result);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recordCall(
|
|
96
|
+
operation: "call" | "batch",
|
|
97
|
+
outcome: {
|
|
98
|
+
ok: boolean;
|
|
99
|
+
durationMs: number;
|
|
100
|
+
timing: { catalogMs: number; connectorMs: number };
|
|
101
|
+
value?: unknown;
|
|
102
|
+
},
|
|
103
|
+
): void {
|
|
104
|
+
const stats = this.stats(operation);
|
|
105
|
+
if (operation === "call") {
|
|
106
|
+
stats.count++;
|
|
107
|
+
if (outcome.ok) {
|
|
108
|
+
stats.resultBytes += serializedDiagnosticBytes(outcome.value);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
stats.calls = (stats.calls ?? 0) + 1;
|
|
112
|
+
}
|
|
113
|
+
stats.failures += outcome.ok ? 0 : 1;
|
|
114
|
+
if (operation === "call") stats.durationMs += outcome.durationMs;
|
|
115
|
+
stats.catalogMs += outcome.timing.catalogMs;
|
|
116
|
+
stats.connectorMs += outcome.timing.connectorMs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
recordBatch(
|
|
120
|
+
durationMs: number,
|
|
121
|
+
ok: boolean,
|
|
122
|
+
calls: number,
|
|
123
|
+
result?: unknown,
|
|
124
|
+
): void {
|
|
125
|
+
const stats = this.stats("batch");
|
|
126
|
+
stats.count++;
|
|
127
|
+
// Calls normally accrue while each child runs. Invalid batch input never
|
|
128
|
+
// starts children, so retain the attempted cardinality here.
|
|
129
|
+
if (!ok) stats.calls = Math.max(stats.calls ?? 0, calls);
|
|
130
|
+
stats.durationMs += durationMs;
|
|
131
|
+
if (!ok) stats.failures++;
|
|
132
|
+
else stats.resultBytes += serializedDiagnosticBytes(result);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
finish(): {
|
|
136
|
+
timing: {
|
|
137
|
+
totalMs: number;
|
|
138
|
+
admissionMs: number;
|
|
139
|
+
setupMs: number;
|
|
140
|
+
executorWallMs: number;
|
|
141
|
+
catalogMs: number;
|
|
142
|
+
connectorMs: number;
|
|
143
|
+
};
|
|
144
|
+
operations: ExecuteOperationDiagnostics[];
|
|
145
|
+
} {
|
|
146
|
+
const operations = [...this.operations.values()];
|
|
147
|
+
return {
|
|
148
|
+
timing: {
|
|
149
|
+
totalMs: Date.now() - this.started,
|
|
150
|
+
admissionMs: this.admissionMs,
|
|
151
|
+
setupMs: this.setupMs,
|
|
152
|
+
executorWallMs: this.executorWallMs,
|
|
153
|
+
catalogMs: operations.reduce((sum, item) => sum + item.catalogMs, 0),
|
|
154
|
+
connectorMs: operations.reduce(
|
|
155
|
+
(sum, item) => sum + item.connectorMs,
|
|
156
|
+
0,
|
|
157
|
+
),
|
|
158
|
+
},
|
|
159
|
+
operations,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function serializedDiagnosticBytes(value: unknown): number {
|
|
165
|
+
try {
|
|
166
|
+
const text = JSON.stringify(value);
|
|
167
|
+
return text === undefined
|
|
168
|
+
? 0
|
|
169
|
+
: diagnosticsEncoder.encode(text).byteLength;
|
|
170
|
+
} catch {
|
|
171
|
+
// The executor's normal result guard owns the error. Diagnostics must
|
|
172
|
+
// never turn measurement into a second failure path.
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
37
176
|
|
|
38
177
|
// deno-fmt-ignore
|
|
39
178
|
const RESERVED = new Set([
|
|
@@ -154,6 +293,7 @@ export async function buildSandboxProviders(
|
|
|
154
293
|
hostCallTimeoutMs?: number;
|
|
155
294
|
discoveryConcurrency?: number;
|
|
156
295
|
onInvocationFailure?: (failure: InvocationFailure) => void;
|
|
296
|
+
diagnostics?: ExecuteDiagnostics;
|
|
157
297
|
} = {},
|
|
158
298
|
): Promise<ExecutorProvider[]> {
|
|
159
299
|
// All host calls made by one execute_code invocation share a downstream
|
|
@@ -231,12 +371,17 @@ export async function buildSandboxProviders(
|
|
|
231
371
|
throw err;
|
|
232
372
|
}
|
|
233
373
|
};
|
|
234
|
-
const callAddress = async (
|
|
374
|
+
const callAddress = async (
|
|
375
|
+
address: unknown,
|
|
376
|
+
args: unknown,
|
|
377
|
+
diagnosticOperation: "call" | "batch" = "call",
|
|
378
|
+
) => {
|
|
235
379
|
const outcome = await invocation.invoke(
|
|
236
380
|
String(address),
|
|
237
381
|
args ?? {},
|
|
238
382
|
invocationContext(),
|
|
239
383
|
);
|
|
384
|
+
limits.diagnostics?.recordCall(diagnosticOperation, outcome);
|
|
240
385
|
if (!outcome.ok) {
|
|
241
386
|
const failure = new InvocationFailure(outcome.error);
|
|
242
387
|
limits.onInvocationFailure?.(failure);
|
|
@@ -256,6 +401,7 @@ export async function buildSandboxProviders(
|
|
|
256
401
|
args ?? {},
|
|
257
402
|
invocationContext(),
|
|
258
403
|
);
|
|
404
|
+
limits.diagnostics?.recordCall("call", outcome);
|
|
259
405
|
if (!outcome.ok) {
|
|
260
406
|
const failure = new InvocationFailure(outcome.error);
|
|
261
407
|
limits.onInvocationFailure?.(failure);
|
|
@@ -270,83 +416,143 @@ export async function buildSandboxProviders(
|
|
|
270
416
|
prelude: lazyNamespacePrelude(namespaces),
|
|
271
417
|
fns: {
|
|
272
418
|
__callNamespace: callNamespace,
|
|
273
|
-
call:
|
|
419
|
+
call: (address: unknown, args: unknown) =>
|
|
420
|
+
callAddress(address, args),
|
|
274
421
|
batch: async (calls: unknown) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
422
|
+
const started = Date.now();
|
|
423
|
+
const callCount = Array.isArray(calls) ? calls.length : 0;
|
|
424
|
+
try {
|
|
425
|
+
if (!Array.isArray(calls)) throw new Error("calls must be an array");
|
|
426
|
+
if (calls.length > EXECUTE_MAX_BATCH_CALLS) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`connecta.batch accepts at most ${EXECUTE_MAX_BATCH_CALLS} calls`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const result = await Promise.all(
|
|
432
|
+
calls.map(async (call) => {
|
|
433
|
+
const item = call as { address?: unknown; args?: unknown };
|
|
434
|
+
try {
|
|
435
|
+
return {
|
|
436
|
+
address: String(item.address),
|
|
437
|
+
ok: true,
|
|
438
|
+
data: await callAddress(
|
|
439
|
+
item.address,
|
|
440
|
+
item.args,
|
|
441
|
+
"batch",
|
|
442
|
+
),
|
|
443
|
+
};
|
|
444
|
+
} catch (err) {
|
|
445
|
+
// Same failure shape batch_call reports: the message a program
|
|
446
|
+
// can log, plus the typed details it must classify by. A
|
|
447
|
+
// thrown host error crosses the sandbox bridge as a bare
|
|
448
|
+
// message string in every executor, so this is the one place a
|
|
449
|
+
// program can tell a policy refusal from a transient failure.
|
|
450
|
+
const details =
|
|
451
|
+
err instanceof InvocationFailure
|
|
452
|
+
? err.details
|
|
453
|
+
: classifyCallError(err, "batch_call_failed");
|
|
454
|
+
return {
|
|
455
|
+
address: String(item.address),
|
|
456
|
+
ok: false,
|
|
457
|
+
error: details.message,
|
|
458
|
+
errorDetails: details,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}),
|
|
462
|
+
);
|
|
463
|
+
limits.diagnostics?.recordBatch(
|
|
464
|
+
Date.now() - started,
|
|
465
|
+
true,
|
|
466
|
+
callCount,
|
|
467
|
+
result,
|
|
468
|
+
);
|
|
469
|
+
return result;
|
|
470
|
+
} catch (err) {
|
|
471
|
+
limits.diagnostics?.recordBatch(
|
|
472
|
+
Date.now() - started,
|
|
473
|
+
false,
|
|
474
|
+
callCount,
|
|
279
475
|
);
|
|
476
|
+
throw err;
|
|
280
477
|
}
|
|
281
|
-
return await Promise.all(
|
|
282
|
-
calls.map(async (call) => {
|
|
283
|
-
const item = call as { address?: unknown; args?: unknown };
|
|
284
|
-
try {
|
|
285
|
-
return {
|
|
286
|
-
address: String(item.address),
|
|
287
|
-
ok: true,
|
|
288
|
-
data: await callAddress(item.address, item.args),
|
|
289
|
-
};
|
|
290
|
-
} catch (err) {
|
|
291
|
-
// Same failure shape batch_call reports: the message a program
|
|
292
|
-
// can log, plus the typed details it must classify by. A
|
|
293
|
-
// thrown host error crosses the sandbox bridge as a bare
|
|
294
|
-
// message string in every executor, so this is the one place a
|
|
295
|
-
// program can tell a policy refusal from a transient failure.
|
|
296
|
-
const details =
|
|
297
|
-
err instanceof InvocationFailure
|
|
298
|
-
? err.details
|
|
299
|
-
: classifyCallError(err, "batch_call_failed");
|
|
300
|
-
return {
|
|
301
|
-
address: String(item.address),
|
|
302
|
-
ok: false,
|
|
303
|
-
error: details.message,
|
|
304
|
-
errorDetails: details,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}),
|
|
308
|
-
);
|
|
309
478
|
},
|
|
310
|
-
search: async (raw: unknown) =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
479
|
+
search: async (raw: unknown) => {
|
|
480
|
+
const started = Date.now();
|
|
481
|
+
try {
|
|
482
|
+
const result = await typedDiscovery(async () => {
|
|
483
|
+
const args = (raw ?? {}) as {
|
|
484
|
+
query?: string;
|
|
485
|
+
connector?: string;
|
|
486
|
+
safety?: "readOnly" | "approvalRequired" | "all";
|
|
487
|
+
limit?: number;
|
|
488
|
+
offset?: number;
|
|
489
|
+
fullDescriptions?: boolean;
|
|
490
|
+
includeSchemas?: "compact" | "json";
|
|
491
|
+
includeSchemaKeys?: boolean;
|
|
492
|
+
};
|
|
493
|
+
const result = flatSearchResult(
|
|
494
|
+
await catalog.search({
|
|
495
|
+
...args,
|
|
496
|
+
// Key metadata rides along with schemas by default, since that
|
|
497
|
+
// is the whole point of it in code mode. It stays opt-out
|
|
498
|
+
// because it counts against the same discovery-byte ceiling.
|
|
499
|
+
includeSchemaKeys: args.includeSchemaKeys !== false,
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
boundedDiscoveryText(
|
|
503
|
+
result,
|
|
504
|
+
"Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.",
|
|
505
|
+
);
|
|
506
|
+
return result;
|
|
507
|
+
});
|
|
508
|
+
limits.diagnostics?.recordCatalog(
|
|
509
|
+
"search",
|
|
510
|
+
Date.now() - started,
|
|
511
|
+
true,
|
|
331
512
|
result,
|
|
332
|
-
"Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.",
|
|
333
513
|
);
|
|
334
514
|
return result;
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
515
|
+
} catch (err) {
|
|
516
|
+
limits.diagnostics?.recordCatalog(
|
|
517
|
+
"search",
|
|
518
|
+
Date.now() - started,
|
|
519
|
+
false,
|
|
520
|
+
);
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
describe: async (raw: unknown) => {
|
|
525
|
+
const started = Date.now();
|
|
526
|
+
try {
|
|
527
|
+
const result = await typedDiscovery(async () => {
|
|
528
|
+
const args = (raw ?? {}) as {
|
|
529
|
+
addresses?: unknown;
|
|
530
|
+
format?: "compact" | "json";
|
|
531
|
+
fullDescriptions?: boolean;
|
|
532
|
+
};
|
|
533
|
+
const result = { tools: await catalog.describe(args) };
|
|
534
|
+
boundedDiscoveryText(
|
|
535
|
+
result,
|
|
536
|
+
'Split the address list or use format: "compact".',
|
|
537
|
+
);
|
|
538
|
+
return result;
|
|
539
|
+
});
|
|
540
|
+
limits.diagnostics?.recordCatalog(
|
|
541
|
+
"describe",
|
|
542
|
+
Date.now() - started,
|
|
543
|
+
true,
|
|
345
544
|
result,
|
|
346
|
-
'Split the address list or use format: "compact".',
|
|
347
545
|
);
|
|
348
546
|
return result;
|
|
349
|
-
})
|
|
547
|
+
} catch (err) {
|
|
548
|
+
limits.diagnostics?.recordCatalog(
|
|
549
|
+
"describe",
|
|
550
|
+
Date.now() - started,
|
|
551
|
+
false,
|
|
552
|
+
);
|
|
553
|
+
throw err;
|
|
554
|
+
}
|
|
555
|
+
},
|
|
350
556
|
},
|
|
351
557
|
},
|
|
352
558
|
];
|
|
@@ -362,7 +568,10 @@ export function createExecuteTool(
|
|
|
362
568
|
config: { discoveryConcurrency?: number } = {},
|
|
363
569
|
) {
|
|
364
570
|
return async (
|
|
365
|
-
{ code
|
|
571
|
+
{ code, diagnostics: diagnosticsRequested }: {
|
|
572
|
+
code: string;
|
|
573
|
+
diagnostics?: boolean;
|
|
574
|
+
},
|
|
366
575
|
options: { signal?: AbortSignal } = {},
|
|
367
576
|
): Promise<ToolResult> => {
|
|
368
577
|
const controller = new AbortController();
|
|
@@ -373,42 +582,64 @@ export function createExecuteTool(
|
|
|
373
582
|
}
|
|
374
583
|
let lease;
|
|
375
584
|
let outcome;
|
|
585
|
+
const diagnostics = diagnosticsRequested ? new ExecuteDiagnostics() : undefined;
|
|
376
586
|
const invocationFailures: InvocationFailure[] = [];
|
|
377
587
|
try {
|
|
378
588
|
// Admission comes before provider construction: queued calls retain no
|
|
379
589
|
// catalogs, request scopes, or one-closure-per-tool provider arrays.
|
|
380
590
|
if (isAdmittingExecutor(executor)) {
|
|
381
|
-
|
|
591
|
+
const admissionStarted = Date.now();
|
|
592
|
+
try {
|
|
593
|
+
lease = await executor.acquire({ signal: controller.signal });
|
|
594
|
+
} finally {
|
|
595
|
+
if (diagnostics) {
|
|
596
|
+
diagnostics.admissionMs = Date.now() - admissionStarted;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
382
599
|
if ((lease.waitMs ?? 0) > 0) {
|
|
383
600
|
logger.debug("[connecta] execute_code admitted after queue wait", {
|
|
384
601
|
waitMs: lease.waitMs,
|
|
385
602
|
});
|
|
386
603
|
}
|
|
387
604
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
605
|
+
const setupStarted = Date.now();
|
|
606
|
+
let providers: ExecutorProvider[];
|
|
607
|
+
try {
|
|
608
|
+
providers = await buildSandboxProviders(
|
|
609
|
+
registry,
|
|
610
|
+
baseUrl,
|
|
611
|
+
logger,
|
|
612
|
+
activity,
|
|
613
|
+
{
|
|
614
|
+
signal: controller.signal,
|
|
615
|
+
onInvocationFailure: (failure) => {
|
|
616
|
+
invocationFailures.push(failure);
|
|
617
|
+
},
|
|
618
|
+
...(diagnostics ? { diagnostics } : {}),
|
|
619
|
+
...(config.discoveryConcurrency !== undefined
|
|
620
|
+
? { discoveryConcurrency: config.discoveryConcurrency }
|
|
621
|
+
: {}),
|
|
397
622
|
},
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
);
|
|
623
|
+
);
|
|
624
|
+
} finally {
|
|
625
|
+
if (diagnostics) diagnostics.setupMs = Date.now() - setupStarted;
|
|
626
|
+
}
|
|
403
627
|
if (controller.signal.aborted) {
|
|
404
628
|
throw new ExecutorAdmissionError(
|
|
405
629
|
"executor_cancelled",
|
|
406
630
|
"Execution was cancelled during sandbox setup.",
|
|
407
631
|
);
|
|
408
632
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
633
|
+
const executorStarted = Date.now();
|
|
634
|
+
try {
|
|
635
|
+
outcome = lease
|
|
636
|
+
? await lease.execute(code, providers)
|
|
637
|
+
: await executor.execute(code, providers);
|
|
638
|
+
} finally {
|
|
639
|
+
if (diagnostics) {
|
|
640
|
+
diagnostics.executorWallMs = Date.now() - executorStarted;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
412
643
|
} catch (err) {
|
|
413
644
|
if (err instanceof ExecutorAdmissionError) {
|
|
414
645
|
if (err.code === "executor_overloaded") {
|
|
@@ -426,6 +657,19 @@ export function createExecuteTool(
|
|
|
426
657
|
? { retryAfterMs: err.retryAfterMs }
|
|
427
658
|
: {}),
|
|
428
659
|
},
|
|
660
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
661
|
+
});
|
|
662
|
+
result.isError = true;
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
if (diagnostics) {
|
|
666
|
+
const result = jsonResult({
|
|
667
|
+
error: {
|
|
668
|
+
code: "executor_failed",
|
|
669
|
+
message: `Executor failed: ${msg(err)}`,
|
|
670
|
+
retryable: false,
|
|
671
|
+
},
|
|
672
|
+
diagnostics: diagnostics.finish(),
|
|
429
673
|
});
|
|
430
674
|
result.isError = true;
|
|
431
675
|
return result;
|
|
@@ -476,13 +720,26 @@ export function createExecuteTool(
|
|
|
476
720
|
const result = jsonResult({
|
|
477
721
|
error: invocationFailure.details,
|
|
478
722
|
...(logs ? { logs } : {}),
|
|
723
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
479
724
|
});
|
|
480
725
|
result.isError = true;
|
|
481
726
|
return result;
|
|
482
727
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
728
|
+
const message = `Error: ${outcome.error}`;
|
|
729
|
+
if (diagnostics) {
|
|
730
|
+
const result = jsonResult({
|
|
731
|
+
error: {
|
|
732
|
+
code: "executor_failed",
|
|
733
|
+
message,
|
|
734
|
+
retryable: false,
|
|
735
|
+
},
|
|
736
|
+
...(logs ? { logs } : {}),
|
|
737
|
+
diagnostics: diagnostics.finish(),
|
|
738
|
+
});
|
|
739
|
+
result.isError = true;
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}`);
|
|
486
743
|
}
|
|
487
744
|
// A result crossing back as a host BigInt (or otherwise unserializable
|
|
488
745
|
// value) makes JSON.stringify throw — keep that inside the structured
|
|
@@ -491,13 +748,26 @@ export function createExecuteTool(
|
|
|
491
748
|
try {
|
|
492
749
|
result = guardExecuteResultValue(outcome.result);
|
|
493
750
|
} catch (err) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
751
|
+
const message = `Error: result is not JSON-serializable: ${msg(err)}`;
|
|
752
|
+
if (diagnostics) {
|
|
753
|
+
const response = jsonResult({
|
|
754
|
+
error: {
|
|
755
|
+
code: "executor_failed",
|
|
756
|
+
message,
|
|
757
|
+
retryable: false,
|
|
758
|
+
},
|
|
759
|
+
...(logs ? { logs } : {}),
|
|
760
|
+
diagnostics: diagnostics.finish(),
|
|
761
|
+
});
|
|
762
|
+
response.isError = true;
|
|
763
|
+
return response;
|
|
764
|
+
}
|
|
765
|
+
return errorResult(`${message}${logs ? `\n\nLogs:\n${logs}` : ""}`);
|
|
497
766
|
}
|
|
498
767
|
return jsonResult({
|
|
499
768
|
result,
|
|
500
769
|
...(logs ? { logs } : {}),
|
|
770
|
+
...(diagnostics ? { diagnostics: diagnostics.finish() } : {}),
|
|
501
771
|
});
|
|
502
772
|
};
|
|
503
773
|
}
|
|
@@ -529,13 +799,13 @@ const executeDescription = (
|
|
|
529
799
|
Write an async arrow function. It runs with NO network, filesystem, timers, or imports — the only capabilities are:
|
|
530
800
|
- One global per connector: every address <connectorId>.<toolName> from search_tools is callable as <connectorId>.<toolName>(args) with a single args object matching the schema from ${EXECUTE_SCHEMA_SOURCE[surface]}. Names are sanitized to JS identifiers: characters outside [A-Za-z0-9_$] become "_" (e.g. my-service.get.thing → my_service.get_thing), leading digits get "_" prefixed, reserved words get "_" appended.
|
|
531
801
|
- connecta.call(address, args) and connecta.batch(calls) — call raw addresses.
|
|
532
|
-
- connecta.search(args) and connecta.describe(args) — load and inspect request-local catalogs on demand. Matches carrying schemas also list inputKeys, requiredInputKeys, and outputKeys — the same names the schema shows, ready to check against before building args. They are absent when a schema is not a plain object shape, so read the schema itself rather than assuming a missing list means no fields.
|
|
802
|
+
- connecta.search(args) and connecta.describe(args) — load and inspect request-local catalogs on demand. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute; the filter changes results, not authority. Matches carrying schemas also list inputKeys, requiredInputKeys, and outputKeys — the same names the schema shows, ready to check against before building args. They are absent when a schema is not a plain object shape, so read the schema itself rather than assuming a missing list means no fields.
|
|
533
803
|
- console.log(...) — captured and returned alongside the result.
|
|
534
804
|
|
|
535
805
|
Tool calls return plain values (MCP text content is JSON-parsed when possible) and throw on downstream errors — use try/catch to handle them. A thrown error carries only a message; connecta.batch reports each call as { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }, so use it when the program must tell a policy refusal from a transient failure. Never retry a failure whose retryable is false, and never retry a rate_limited one immediately — the sandbox has no timers. Return a JSON-serializable value; large results are truncated, so reduce data in code instead of returning raw payloads.
|
|
536
806
|
|
|
537
807
|
Plain JavaScript only — no TypeScript syntax. For unknown-address dependent work, use one execute_code call: search inside it, read the compact schemas, and continue to the dependent calls; do not return search results for a second execute_code call. Compact schemas are TypeScript-like strings, not JSON Schema objects: write the property names they display, never a positional guess or an invented alias.
|
|
538
|
-
Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((tool) => tool.address.endsWith(suffix)); if (!match) throw new Error("no tool matching " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }`;
|
|
808
|
+
Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((tool) => tool.address.endsWith(suffix)); if (!match) throw new Error("no tool matching " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }`;
|
|
539
809
|
|
|
540
810
|
/** Register the execute_code meta-tool. Only called when an executor is configured. */
|
|
541
811
|
export function registerExecuteTool(
|
|
@@ -570,6 +840,12 @@ export function registerExecuteTool(
|
|
|
570
840
|
code: z
|
|
571
841
|
.string()
|
|
572
842
|
.describe("A JavaScript async arrow function to execute."),
|
|
843
|
+
diagnostics: z
|
|
844
|
+
.boolean()
|
|
845
|
+
.optional()
|
|
846
|
+
.describe(
|
|
847
|
+
"Add request-local, payload-free timing and result-size summaries.",
|
|
848
|
+
),
|
|
573
849
|
}),
|
|
574
850
|
// The sandbox exposes only tools that are explicitly read-only, and the
|
|
575
851
|
// executor grants no network, filesystem, env, or timer capabilities.
|
|
@@ -591,7 +867,7 @@ export function registerExecuteTool(
|
|
|
591
867
|
return { signal, forward };
|
|
592
868
|
});
|
|
593
869
|
try {
|
|
594
|
-
return await handler(args as { code: string }, {
|
|
870
|
+
return await handler(args as { code: string; diagnostics?: boolean }, {
|
|
595
871
|
signal: controller.signal,
|
|
596
872
|
});
|
|
597
873
|
} finally {
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
credentialTestRule,
|
|
4
4
|
describeCredentialTestMismatch,
|
|
5
5
|
} from "./credentials.js";
|
|
6
|
+
import { AccessTokenManager } from "./access-tokens.js";
|
|
6
7
|
import { Registry } from "./registry.js";
|
|
7
8
|
import { createFetchHandler } from "./server.js";
|
|
8
9
|
import { droppedBrandingUrls, droppedUiAuthUrls } from "./ui.js";
|
|
@@ -49,6 +50,12 @@ export interface ConnectaCredentialsConfig {
|
|
|
49
50
|
encryptionKey?: string;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** Operator-issued credentials for clients connecting to this deployment. */
|
|
54
|
+
export interface ConnectaAccessTokensConfig {
|
|
55
|
+
/** Maximum simultaneously active access tokens. Defaults to 100. */
|
|
56
|
+
maxActive?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
53
60
|
export interface ConnectaDiscoveryConfig {
|
|
54
61
|
/**
|
|
@@ -148,6 +155,11 @@ export interface ConnectaConfig {
|
|
|
148
155
|
activity?: ConnectaActivityConfig;
|
|
149
156
|
/** Operator credential vault settings. */
|
|
150
157
|
credentials?: ConnectaCredentialsConfig;
|
|
158
|
+
/**
|
|
159
|
+
* Named, revocable Bearer tokens for MCP clients. Creation and mutation
|
|
160
|
+
* require an eligible Clerk operator; token secrets are returned once.
|
|
161
|
+
*/
|
|
162
|
+
accessTokens?: ConnectaAccessTokensConfig;
|
|
151
163
|
/** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
|
|
152
164
|
discovery?: ConnectaDiscoveryConfig;
|
|
153
165
|
/** Deployment-wide call deadlines and result paging threshold. */
|
|
@@ -468,6 +480,19 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
468
480
|
const credentialVault = encryptionKey
|
|
469
481
|
? new CredentialVault(storage, encryptionKey)
|
|
470
482
|
: undefined;
|
|
483
|
+
const configuredAuth = normalizeAuth(config.auth);
|
|
484
|
+
const accessTokens = config.accessTokens
|
|
485
|
+
? new AccessTokenManager(storage, config.accessTokens)
|
|
486
|
+
: undefined;
|
|
487
|
+
if (
|
|
488
|
+
accessTokens &&
|
|
489
|
+
!configuredAuth.some((provider) => provider.uiAuth?.kind === "clerk")
|
|
490
|
+
) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
"accessTokens requires a Clerk auth provider: only an eligible Clerk " +
|
|
493
|
+
"operator may create, rename, or revoke deployment access tokens",
|
|
494
|
+
);
|
|
495
|
+
}
|
|
471
496
|
const registry = new Registry(config.connectors, {
|
|
472
497
|
storage,
|
|
473
498
|
logger,
|
|
@@ -488,7 +513,9 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
488
513
|
? { maxBatchResultBytes: config.calls.maxBatchResultBytes }
|
|
489
514
|
: {}),
|
|
490
515
|
});
|
|
491
|
-
const inboundAuth = normalizeAuth(
|
|
516
|
+
const inboundAuth = normalizeAuth(
|
|
517
|
+
accessTokens ? [accessTokens.auth, ...configuredAuth] : configuredAuth,
|
|
518
|
+
);
|
|
492
519
|
warnInsecureConfig(config, inboundAuth, logger);
|
|
493
520
|
const requestAdmission = admissionController(
|
|
494
521
|
config.admission?.requests,
|
|
@@ -542,6 +569,7 @@ export function createConnecta(config: ConnectaConfig): Connecta {
|
|
|
542
569
|
? { discoveryConcurrency: config.discovery.concurrency }
|
|
543
570
|
: {}),
|
|
544
571
|
...(credentialVault !== undefined ? { credentialVault } : {}),
|
|
572
|
+
...(accessTokens !== undefined ? { accessTokens } : {}),
|
|
545
573
|
...(config.deploymentInfo !== undefined
|
|
546
574
|
? { deploymentInfo: config.deploymentInfo }
|
|
547
575
|
: {}),
|
|
@@ -580,6 +608,10 @@ export { validateToolInput } from "./validate.js";
|
|
|
580
608
|
export type { ValidateToolInputOptions } from "./validate.js";
|
|
581
609
|
export { bearerToken } from "./auth/bearer.js";
|
|
582
610
|
export type { BearerTokenOptions } from "./auth/bearer.js";
|
|
611
|
+
export type {
|
|
612
|
+
AccessTokenMetadata,
|
|
613
|
+
CreatedAccessToken,
|
|
614
|
+
} from "./access-tokens.js";
|
|
583
615
|
export { memoryStorage } from "./storage/memory.js";
|
|
584
616
|
export { CONNECTA_VERSION } from "./version.js";
|
|
585
617
|
// Registry is reachable through `Connecta.registry`, so its type is public;
|