@zackbart/connecta 0.1.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SECURITY.md +25 -0
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/activity.d.ts +82 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +51 -0
- package/dist/activity.js.map +1 -0
- package/dist/auth/bearer.d.ts +10 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +47 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/clerk.d.ts +20 -0
- package/dist/auth/clerk.d.ts.map +1 -0
- package/dist/auth/clerk.js +167 -0
- package/dist/auth/clerk.js.map +1 -0
- package/dist/auth/downstream-oauth.d.ts +78 -0
- package/dist/auth/downstream-oauth.d.ts.map +1 -0
- package/dist/auth/downstream-oauth.js +180 -0
- package/dist/auth/downstream-oauth.js.map +1 -0
- package/dist/catalog.d.ts +11 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +144 -0
- package/dist/catalog.js.map +1 -0
- package/dist/connectors/api.d.ts +34 -0
- package/dist/connectors/api.d.ts.map +1 -0
- package/dist/connectors/api.js +36 -0
- package/dist/connectors/api.js.map +1 -0
- package/dist/connectors/remote-mcp.d.ts +33 -0
- package/dist/connectors/remote-mcp.d.ts.map +1 -0
- package/dist/connectors/remote-mcp.js +255 -0
- package/dist/connectors/remote-mcp.js.map +1 -0
- package/dist/credentials.d.ts +33 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +177 -0
- package/dist/credentials.js.map +1 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +424 -0
- package/dist/execute.js.map +1 -0
- package/dist/executors/quickjs.d.ts +26 -0
- package/dist/executors/quickjs.d.ts.map +1 -0
- package/dist/executors/quickjs.js +267 -0
- package/dist/executors/quickjs.js.map +1 -0
- package/dist/favicon.d.ts +2 -0
- package/dist/favicon.d.ts.map +1 -0
- package/dist/favicon.js +49 -0
- package/dist/favicon.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-result.d.ts +9 -0
- package/dist/mcp-result.d.ts.map +1 -0
- package/dist/mcp-result.js +35 -0
- package/dist/mcp-result.js.map +1 -0
- package/dist/meta-tools.d.ts +100 -0
- package/dist/meta-tools.d.ts.map +1 -0
- package/dist/meta-tools.js +797 -0
- package/dist/meta-tools.js.map +1 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +57 -0
- package/dist/node.js.map +1 -0
- package/dist/registry.d.ts +78 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +299 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +543 -0
- package/dist/server.js.map +1 -0
- package/dist/skills.d.ts +8 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +60 -0
- package/dist/skills.js.map +1 -0
- package/dist/storage/file.d.ts +8 -0
- package/dist/storage/file.d.ts.map +1 -0
- package/dist/storage/file.js +53 -0
- package/dist/storage/file.js.map +1 -0
- package/dist/storage/memory.d.ts +4 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +29 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +91 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1031 -0
- package/dist/ui.js.map +1 -0
- package/package.json +96 -0
- package/src/activity.ts +144 -0
- package/src/auth/bearer.ts +54 -0
- package/src/auth/clerk.ts +226 -0
- package/src/auth/downstream-oauth.ts +202 -0
- package/src/catalog.ts +166 -0
- package/src/connectors/api.ts +80 -0
- package/src/connectors/remote-mcp.ts +324 -0
- package/src/credentials.ts +259 -0
- package/src/execute.ts +550 -0
- package/src/executors/quickjs.ts +321 -0
- package/src/favicon.ts +53 -0
- package/src/index.ts +197 -0
- package/src/mcp-result.ts +43 -0
- package/src/meta-tools.ts +1120 -0
- package/src/node.ts +62 -0
- package/src/registry.ts +400 -0
- package/src/server.ts +758 -0
- package/src/skills.ts +63 -0
- package/src/storage/file.ts +62 -0
- package/src/storage/memory.ts +34 -0
- package/src/types.ts +254 -0
- package/src/ui.ts +1121 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// QuickJS-in-WebAssembly executor — the Node-side sandbox for execute_code.
|
|
2
|
+
//
|
|
3
|
+
// Model-written code runs inside a QuickJS interpreter compiled to WASM:
|
|
4
|
+
// memory-safe, no ambient authority — the guest has no fetch, filesystem,
|
|
5
|
+
// env, timers, or imports. The only bridge to the host is `__call`, which
|
|
6
|
+
// returns a QuickJS promise that the host resolves with a JSON payload after
|
|
7
|
+
// running the provider function; `executePendingJobs` drives guest
|
|
8
|
+
// continuations from a host-side loop. Values cross the boundary as JSON, so
|
|
9
|
+
// provider args/results must be JSON-serializable.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getQuickJS,
|
|
13
|
+
shouldInterruptAfterDeadline,
|
|
14
|
+
type QuickJSContext,
|
|
15
|
+
type QuickJSDeferredPromise,
|
|
16
|
+
} from "quickjs-emscripten";
|
|
17
|
+
import type { ExecuteResult, Executor, ExecutorProvider } from "../types.js";
|
|
18
|
+
|
|
19
|
+
export interface QuickJsExecutorOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Wall-clock budget for the whole execution, host tool calls included
|
|
22
|
+
* (same wall-clock-budget semantics as codemode's DynamicWorkerExecutor).
|
|
23
|
+
* Default 30s — intentionally tighter than codemode's 60s default: connecta
|
|
24
|
+
* sandbox code is tool-call glue, not compute, so a shorter leash surfaces
|
|
25
|
+
* hung downstreams sooner. Raise it here if a workload legitimately needs it.
|
|
26
|
+
*/
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
/** Guest heap limit. Default 64 MiB. */
|
|
29
|
+
memoryLimitBytes?: number;
|
|
30
|
+
/** Guest stack limit. Default 1 MiB. */
|
|
31
|
+
maxStackSizeBytes?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_LOG_ENTRIES = 200;
|
|
35
|
+
|
|
36
|
+
function msg(err: unknown): string {
|
|
37
|
+
return err instanceof Error ? err.message : String(err);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
|
|
41
|
+
export function normalizeCode(code: string): string {
|
|
42
|
+
let c = code.trim();
|
|
43
|
+
const fence = /^```[\w-]*\s*\n([\s\S]*?)\n?```$/.exec(c);
|
|
44
|
+
if (fence) c = fence[1].trim();
|
|
45
|
+
// Sniff for an existing function expression past any leading comments/blank
|
|
46
|
+
// lines — models routinely prefix their code with a `//` or `/* */` note,
|
|
47
|
+
// which would otherwise defeat the detection and wrap the whole thing.
|
|
48
|
+
const head = c.replace(/^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)+/, "");
|
|
49
|
+
if (/^async\b/.test(head) || head.startsWith("(")) return c;
|
|
50
|
+
return `async () => {\n${c}\n}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Guest-side prelude: console capture, the JSON call bridge, one frozen global per provider. */
|
|
54
|
+
function setupScript(providers: ExecutorProvider[]): string {
|
|
55
|
+
const lines: string[] = [
|
|
56
|
+
`globalThis.console = (() => {
|
|
57
|
+
const fmt = (x) => { if (typeof x === "string") return x; try { return JSON.stringify(x); } catch { return String(x); } };
|
|
58
|
+
const emit = (...a) => __log(a.map(fmt).join(" "));
|
|
59
|
+
return { log: emit, info: emit, warn: emit, error: emit, debug: emit };
|
|
60
|
+
})();`,
|
|
61
|
+
`globalThis.__invoke = async (ns, fn, args) => {
|
|
62
|
+
const r = JSON.parse(await __call(ns, fn, JSON.stringify(args)));
|
|
63
|
+
if (!r.ok) throw new Error(r.error);
|
|
64
|
+
return r.value;
|
|
65
|
+
};`,
|
|
66
|
+
];
|
|
67
|
+
for (const p of providers) {
|
|
68
|
+
const ns = JSON.stringify(p.name);
|
|
69
|
+
const fields = Object.keys(p.fns).map((fn) => {
|
|
70
|
+
const f = JSON.stringify(fn);
|
|
71
|
+
// Forward every argument verbatim, positionally — same as codemode's
|
|
72
|
+
// sandbox proxy (`(...args) => call(fn, args)`). The host wrappers in
|
|
73
|
+
// buildSandboxProviders already guard the no-arg case, so no coercion.
|
|
74
|
+
return ` [${f}]: (...a) => __invoke(${ns}, ${f}, a),`;
|
|
75
|
+
});
|
|
76
|
+
lines.push(`globalThis[${ns}] = Object.freeze({\n${fields.join("\n")}\n});`);
|
|
77
|
+
}
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when a dumped error is QuickJS's deadline-interrupt signal. */
|
|
82
|
+
function isInterrupt(dumped: unknown): boolean {
|
|
83
|
+
if (!dumped || typeof dumped !== "object") return false;
|
|
84
|
+
const e = dumped as { name?: unknown; message?: unknown };
|
|
85
|
+
return e.name === "InternalError" && e.message === "interrupted";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatGuestError(dumped: unknown): string {
|
|
89
|
+
if (dumped && typeof dumped === "object") {
|
|
90
|
+
const e = dumped as { name?: unknown; message?: unknown };
|
|
91
|
+
if (e.message != null) {
|
|
92
|
+
return e.name != null && e.name !== "Error"
|
|
93
|
+
? `${String(e.name)}: ${String(e.message)}`
|
|
94
|
+
: String(e.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return typeof dumped === "string" ? dumped : JSON.stringify(dumped);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface HostBridge {
|
|
101
|
+
pending: number;
|
|
102
|
+
aborted: boolean;
|
|
103
|
+
wake: () => void;
|
|
104
|
+
waitForSettle: Promise<void>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function armWake(bridge: HostBridge): void {
|
|
108
|
+
bridge.waitForSettle = new Promise((resolve) => {
|
|
109
|
+
bridge.wake = resolve;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function installBridge(
|
|
114
|
+
ctx: QuickJSContext,
|
|
115
|
+
providers: ExecutorProvider[],
|
|
116
|
+
logs: string[],
|
|
117
|
+
): HostBridge {
|
|
118
|
+
const bridge: HostBridge = {
|
|
119
|
+
pending: 0,
|
|
120
|
+
aborted: false,
|
|
121
|
+
wake: () => {},
|
|
122
|
+
waitForSettle: Promise.resolve(),
|
|
123
|
+
};
|
|
124
|
+
armWake(bridge);
|
|
125
|
+
|
|
126
|
+
const logFn = ctx.newFunction("__log", (h) => {
|
|
127
|
+
if (logs.length < MAX_LOG_ENTRIES) {
|
|
128
|
+
logs.push(ctx.getString(h));
|
|
129
|
+
} else if (logs.length === MAX_LOG_ENTRIES) {
|
|
130
|
+
logs.push(`[log truncated after ${MAX_LOG_ENTRIES} entries]`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
ctx.setProp(ctx.global, "__log", logFn);
|
|
134
|
+
logFn.dispose();
|
|
135
|
+
|
|
136
|
+
const byName = new Map(providers.map((p) => [p.name, p]));
|
|
137
|
+
const invoke = async (
|
|
138
|
+
ns: string,
|
|
139
|
+
fn: string,
|
|
140
|
+
argsJson: string,
|
|
141
|
+
): Promise<string> => {
|
|
142
|
+
try {
|
|
143
|
+
const provider = byName.get(ns);
|
|
144
|
+
const f =
|
|
145
|
+
provider && Object.hasOwn(provider.fns, fn)
|
|
146
|
+
? provider.fns[fn]
|
|
147
|
+
: undefined;
|
|
148
|
+
if (!f) throw new Error(`Unknown function ${ns}.${fn}`);
|
|
149
|
+
const args = JSON.parse(argsJson) as unknown[];
|
|
150
|
+
const value = await f(...args);
|
|
151
|
+
return JSON.stringify({ ok: true, value });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return JSON.stringify({ ok: false, error: msg(err) });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const callFn = ctx.newFunction("__call", (nsH, fnH, argsH) => {
|
|
158
|
+
const ns = ctx.getString(nsH);
|
|
159
|
+
const fn = ctx.getString(fnH);
|
|
160
|
+
const argsJson = ctx.getString(argsH);
|
|
161
|
+
const deferred: QuickJSDeferredPromise = ctx.newPromise();
|
|
162
|
+
bridge.pending++;
|
|
163
|
+
void invoke(ns, fn, argsJson).then((json) => {
|
|
164
|
+
bridge.pending--;
|
|
165
|
+
if (bridge.aborted) {
|
|
166
|
+
// Context is (about to be) torn down — settle nothing, free our handles.
|
|
167
|
+
deferred.dispose();
|
|
168
|
+
} else {
|
|
169
|
+
const h = ctx.newString(json);
|
|
170
|
+
deferred.resolve(h);
|
|
171
|
+
h.dispose();
|
|
172
|
+
}
|
|
173
|
+
bridge.wake();
|
|
174
|
+
});
|
|
175
|
+
return deferred.handle;
|
|
176
|
+
});
|
|
177
|
+
ctx.setProp(ctx.global, "__call", callFn);
|
|
178
|
+
callFn.dispose();
|
|
179
|
+
|
|
180
|
+
return bridge;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Sandbox executor for Node (or any JS runtime) built on quickjs-emscripten.
|
|
185
|
+
* Suits connecta's execute_code: code is tool-call glue, so an interpreter's
|
|
186
|
+
* speed is irrelevant while its isolation (plain WASM, zero native deps) is
|
|
187
|
+
* the point. One fresh QuickJS context per execution; the WASM module itself
|
|
188
|
+
* is cached across calls by quickjs-emscripten.
|
|
189
|
+
*/
|
|
190
|
+
export function quickJsExecutor(
|
|
191
|
+
options: QuickJsExecutorOptions = {},
|
|
192
|
+
): Executor {
|
|
193
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
194
|
+
const memoryLimitBytes = options.memoryLimitBytes ?? 64 * 1024 * 1024;
|
|
195
|
+
const maxStackSizeBytes = options.maxStackSizeBytes ?? 1024 * 1024;
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
async execute(
|
|
199
|
+
code: string,
|
|
200
|
+
providers: ExecutorProvider[],
|
|
201
|
+
): Promise<ExecuteResult> {
|
|
202
|
+
const QuickJS = await getQuickJS();
|
|
203
|
+
const ctx = QuickJS.newContext();
|
|
204
|
+
const deadline = Date.now() + timeoutMs;
|
|
205
|
+
const timeoutError = `Execution timed out after ${timeoutMs}ms.`;
|
|
206
|
+
ctx.runtime.setMemoryLimit(memoryLimitBytes);
|
|
207
|
+
ctx.runtime.setMaxStackSize(maxStackSizeBytes);
|
|
208
|
+
ctx.runtime.setInterruptHandler(shouldInterruptAfterDeadline(deadline));
|
|
209
|
+
|
|
210
|
+
const logs: string[] = [];
|
|
211
|
+
const bridge = installBridge(ctx, providers, logs);
|
|
212
|
+
const finish = (r: ExecuteResult): ExecuteResult => {
|
|
213
|
+
bridge.aborted = true;
|
|
214
|
+
// Outstanding host calls still hold deferred-promise handles; their
|
|
215
|
+
// callbacks free them and wake the drain, which disposes the context
|
|
216
|
+
// once the last straggler settles. Re-arm before every wait so the
|
|
217
|
+
// deferred never resolves-and-stays-resolved between settles — that
|
|
218
|
+
// would spin the microtask queue and starve the very timers the
|
|
219
|
+
// stragglers are waiting on. Arming before the pending check (and
|
|
220
|
+
// synchronously, before the first await) closes the settle-before-arm
|
|
221
|
+
// window: any settle after this point resolves the promise we await.
|
|
222
|
+
if (bridge.pending === 0) ctx.dispose();
|
|
223
|
+
else {
|
|
224
|
+
void (async () => {
|
|
225
|
+
armWake(bridge);
|
|
226
|
+
while (bridge.pending > 0) {
|
|
227
|
+
await bridge.waitForSettle;
|
|
228
|
+
armWake(bridge);
|
|
229
|
+
}
|
|
230
|
+
ctx.dispose();
|
|
231
|
+
})();
|
|
232
|
+
}
|
|
233
|
+
return logs.length > 0 ? { ...r, logs } : r;
|
|
234
|
+
};
|
|
235
|
+
const fail = (error: string): ExecuteResult =>
|
|
236
|
+
finish({ result: undefined, error });
|
|
237
|
+
|
|
238
|
+
const setup = ctx.evalCode(setupScript(providers));
|
|
239
|
+
if (setup.error) {
|
|
240
|
+
const detail = formatGuestError(ctx.dump(setup.error));
|
|
241
|
+
setup.error.dispose();
|
|
242
|
+
return fail(`Sandbox setup failed: ${detail}`);
|
|
243
|
+
}
|
|
244
|
+
setup.value.dispose();
|
|
245
|
+
|
|
246
|
+
const evaluated = ctx.evalCode(
|
|
247
|
+
`Promise.resolve((\n${normalizeCode(code)}\n)())`,
|
|
248
|
+
);
|
|
249
|
+
if (evaluated.error) {
|
|
250
|
+
const dumped = ctx.dump(evaluated.error);
|
|
251
|
+
evaluated.error.dispose();
|
|
252
|
+
// A synchronous runaway (no await to yield on) trips the deadline
|
|
253
|
+
// interrupt here; surface the friendly timeout, not "interrupted".
|
|
254
|
+
if (isInterrupt(dumped) && Date.now() >= deadline) return fail(timeoutError);
|
|
255
|
+
return fail(formatGuestError(dumped));
|
|
256
|
+
}
|
|
257
|
+
const promiseHandle = evaluated.value;
|
|
258
|
+
|
|
259
|
+
// Drive the guest: run microtask jobs, inspect the result promise,
|
|
260
|
+
// sleep until a host call settles (or the budget runs out), repeat.
|
|
261
|
+
// Returns without touching the context lifecycle; disposal happens below.
|
|
262
|
+
const drive = async (): Promise<ExecuteResult> => {
|
|
263
|
+
for (;;) {
|
|
264
|
+
const jobs = ctx.runtime.executePendingJobs();
|
|
265
|
+
if (jobs.error) {
|
|
266
|
+
const dumped = ctx.dump(jobs.error);
|
|
267
|
+
jobs.error.dispose();
|
|
268
|
+
// A runaway inside a resumed continuation trips the interrupt here.
|
|
269
|
+
if (isInterrupt(dumped) && Date.now() >= deadline) {
|
|
270
|
+
return { result: undefined, error: timeoutError };
|
|
271
|
+
}
|
|
272
|
+
return { result: undefined, error: formatGuestError(dumped) };
|
|
273
|
+
}
|
|
274
|
+
const state = ctx.getPromiseState(promiseHandle);
|
|
275
|
+
if (state.type === "fulfilled") {
|
|
276
|
+
const result: unknown = ctx.dump(state.value);
|
|
277
|
+
state.value.dispose();
|
|
278
|
+
return { result };
|
|
279
|
+
}
|
|
280
|
+
if (state.type === "rejected") {
|
|
281
|
+
const dumped = ctx.dump(state.error);
|
|
282
|
+
state.error.dispose();
|
|
283
|
+
// A synchronous runaway rejects the async fn's promise with the
|
|
284
|
+
// interrupt; surface the friendly timeout, not "interrupted".
|
|
285
|
+
if (isInterrupt(dumped) && Date.now() >= deadline) {
|
|
286
|
+
return { result: undefined, error: timeoutError };
|
|
287
|
+
}
|
|
288
|
+
return { result: undefined, error: formatGuestError(dumped) };
|
|
289
|
+
}
|
|
290
|
+
if (bridge.pending === 0) {
|
|
291
|
+
return {
|
|
292
|
+
result: undefined,
|
|
293
|
+
error:
|
|
294
|
+
"Execution stalled: code awaits something that can never settle.",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const remaining = deadline - Date.now();
|
|
298
|
+
if (remaining <= 0) {
|
|
299
|
+
return { result: undefined, error: timeoutError };
|
|
300
|
+
}
|
|
301
|
+
const settled = await Promise.race([
|
|
302
|
+
bridge.waitForSettle.then(() => true),
|
|
303
|
+
new Promise<boolean>((r) => setTimeout(() => r(false), remaining)),
|
|
304
|
+
]);
|
|
305
|
+
if (settled) armWake(bridge);
|
|
306
|
+
else {
|
|
307
|
+
return { result: undefined, error: timeoutError };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let outcome: ExecuteResult;
|
|
313
|
+
try {
|
|
314
|
+
outcome = await drive();
|
|
315
|
+
} finally {
|
|
316
|
+
promiseHandle.dispose();
|
|
317
|
+
}
|
|
318
|
+
return finish(outcome);
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
package/src/favicon.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Build a dependency-free 32px ICO matching the default SVG "C" mark. */
|
|
2
|
+
function buildIco(): Uint8Array {
|
|
3
|
+
const size = 32;
|
|
4
|
+
const directoryLength = 22;
|
|
5
|
+
const bitmapHeaderLength = 40;
|
|
6
|
+
const colorLength = size * size * 4;
|
|
7
|
+
const maskRowLength = 4;
|
|
8
|
+
const maskLength = maskRowLength * size;
|
|
9
|
+
const imageLength = bitmapHeaderLength + colorLength + maskLength;
|
|
10
|
+
const ico = new Uint8Array(directoryLength + imageLength);
|
|
11
|
+
const view = new DataView(ico.buffer);
|
|
12
|
+
|
|
13
|
+
view.setUint16(0, 0, true); // reserved
|
|
14
|
+
view.setUint16(2, 1, true); // image type: icon
|
|
15
|
+
view.setUint16(4, 1, true); // image count
|
|
16
|
+
ico[6] = size;
|
|
17
|
+
ico[7] = size;
|
|
18
|
+
view.setUint16(10, 1, true); // color planes
|
|
19
|
+
view.setUint16(12, 32, true); // bits per pixel
|
|
20
|
+
view.setUint32(14, imageLength, true);
|
|
21
|
+
view.setUint32(18, directoryLength, true);
|
|
22
|
+
|
|
23
|
+
const dib = directoryLength;
|
|
24
|
+
view.setUint32(dib, bitmapHeaderLength, true);
|
|
25
|
+
view.setInt32(dib + 4, size, true);
|
|
26
|
+
view.setInt32(dib + 8, size * 2, true); // color image + transparency mask
|
|
27
|
+
view.setUint16(dib + 12, 1, true);
|
|
28
|
+
view.setUint16(dib + 14, 32, true);
|
|
29
|
+
view.setUint32(dib + 20, colorLength, true);
|
|
30
|
+
|
|
31
|
+
const pixels = dib + bitmapHeaderLength;
|
|
32
|
+
const mask = pixels + colorLength;
|
|
33
|
+
for (let y = 0; y < size; y += 1) {
|
|
34
|
+
for (let x = 0; x < size; x += 1) {
|
|
35
|
+
const dx = x - 15.5;
|
|
36
|
+
const dy = y - 15.5;
|
|
37
|
+
const radius = Math.sqrt(dx * dx + dy * dy);
|
|
38
|
+
const opaque = radius >= 8 && radius <= 13 &&
|
|
39
|
+
(x <= 18 || Math.abs(dy) >= 7);
|
|
40
|
+
const row = size - 1 - y;
|
|
41
|
+
const pixel = pixels + (row * size + x) * 4;
|
|
42
|
+
ico[pixel + 3] = opaque ? 255 : 0;
|
|
43
|
+
if (!opaque) {
|
|
44
|
+
ico[mask + row * maskRowLength + Math.floor(x / 8)] |=
|
|
45
|
+
0x80 >> (x % 8);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return ico;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const CONNECTA_FAVICON_ICO = buildIco();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { CredentialVault } from "./credentials.js";
|
|
2
|
+
import { Registry } from "./registry.js";
|
|
3
|
+
import { createFetchHandler } from "./server.js";
|
|
4
|
+
import { memoryStorage } from "./storage/memory.js";
|
|
5
|
+
import type { ActivityReadGate, ActivityStore } from "./activity.js";
|
|
6
|
+
import type {
|
|
7
|
+
Connector,
|
|
8
|
+
ConnectaBranding,
|
|
9
|
+
Executor,
|
|
10
|
+
InboundAuth,
|
|
11
|
+
KVStorage,
|
|
12
|
+
Logger,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
export interface ConnectaConfig {
|
|
16
|
+
connectors: Connector[];
|
|
17
|
+
/**
|
|
18
|
+
* Privacy-minimal downstream tool activity storage. Writes are best-effort
|
|
19
|
+
* and never change tool results. Implement `list` to enable the Activity UI.
|
|
20
|
+
*/
|
|
21
|
+
activity?: ActivityStore;
|
|
22
|
+
/**
|
|
23
|
+
* Optional authorization gate for the Activity read API. MCP authentication
|
|
24
|
+
* is still required first. Omit to admit every authenticated actor.
|
|
25
|
+
*/
|
|
26
|
+
activityReadGate?: ActivityReadGate;
|
|
27
|
+
/** Stable deployment label included in activity events, e.g. "production". */
|
|
28
|
+
activityDeploymentId?: string;
|
|
29
|
+
/** Inbound auth adapters. Includes bearerToken(...); omit for open (dev). */
|
|
30
|
+
auth?: InboundAuth | InboundAuth[];
|
|
31
|
+
/** KVStorage impl. Defaults to memoryStorage(). */
|
|
32
|
+
storage?: KVStorage;
|
|
33
|
+
/**
|
|
34
|
+
* Public base URL. Defaults to the request origin per-request. Configuring an
|
|
35
|
+
* HTTPS URL also redirects matching inbound HTTP requests to HTTPS.
|
|
36
|
+
*/
|
|
37
|
+
publicUrl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Base64-encoded 32-byte AES key for connector credentials managed in /ui.
|
|
40
|
+
* Keep this in the runtime's secret store, never in KV or source control.
|
|
41
|
+
*/
|
|
42
|
+
credentialEncryptionKey?: string;
|
|
43
|
+
/** Optional browser UI and OAuth result-page labels. */
|
|
44
|
+
branding?: ConnectaBranding;
|
|
45
|
+
logger?: Logger;
|
|
46
|
+
/** Tool-list cache TTL (seconds). Default 300. */
|
|
47
|
+
toolCacheTtlSeconds?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Persist serializable remote tool catalogs in storage so cold isolates can
|
|
50
|
+
* discover tools without a downstream handshake. Default true.
|
|
51
|
+
*/
|
|
52
|
+
persistToolCatalog?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* How long an expired persisted catalog remains available as a fallback
|
|
55
|
+
* when a live refresh fails. Default 3600 seconds.
|
|
56
|
+
*/
|
|
57
|
+
toolCatalogStaleSeconds?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Max inline result size (bytes) before call_tool/batch_call truncate and
|
|
60
|
+
* stash the full text for get_result paging. Default 50_000.
|
|
61
|
+
*/
|
|
62
|
+
maxResultBytes?: number;
|
|
63
|
+
serverInfo?: { name?: string; version?: string };
|
|
64
|
+
/** Deployment metadata exposed by /health (for example a Worker version). */
|
|
65
|
+
deploymentInfo?: Record<string, unknown>;
|
|
66
|
+
/**
|
|
67
|
+
* Sandbox for the optional execute_code meta-tool (code mode). Omit → the
|
|
68
|
+
* tool is not registered and connecta serves the nine base tools. Workers:
|
|
69
|
+
* `new DynamicWorkerExecutor({ loader: env.LOADER })` from
|
|
70
|
+
* `@cloudflare/codemode`. Node: `quickJsExecutor()` from "@zackbart/connecta/quickjs".
|
|
71
|
+
*/
|
|
72
|
+
executor?: Executor;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Connecta {
|
|
76
|
+
/** Web-standard fetch handler. Usable as `export default { fetch: connecta.fetch }`. */
|
|
77
|
+
fetch: (request: Request, env?: unknown, ctx?: unknown) => Promise<Response>;
|
|
78
|
+
registry: Registry;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function defaultLogger(): Logger {
|
|
82
|
+
return {
|
|
83
|
+
debug: (...a) => console.debug("[connecta]", ...a),
|
|
84
|
+
info: (...a) => console.info("[connecta]", ...a),
|
|
85
|
+
warn: (...a) => console.warn("[connecta]", ...a),
|
|
86
|
+
error: (...a) => console.error("[connecta]", ...a),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Bearer providers are checked before Clerk (per spec). */
|
|
91
|
+
function normalizeAuth(auth: ConnectaConfig["auth"]): InboundAuth[] {
|
|
92
|
+
const list = auth ? (Array.isArray(auth) ? auth : [auth]) : [];
|
|
93
|
+
return [...list].sort((a, b) => {
|
|
94
|
+
const rank = (x: InboundAuth) => (x.kind === "bearer" ? 0 : 1);
|
|
95
|
+
return rank(a) - rank(b);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createConnecta(config: ConnectaConfig): Connecta {
|
|
100
|
+
const storage = config.storage ?? memoryStorage();
|
|
101
|
+
const logger = config.logger ?? defaultLogger();
|
|
102
|
+
const credentialConnectors = config.connectors.filter((c) => c.credential);
|
|
103
|
+
if (credentialConnectors.length > 0 && !config.credentialEncryptionKey) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`credentialEncryptionKey is required by connector credentials: ${credentialConnectors.map((c) => c.id).join(", ")}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const credentialVault = config.credentialEncryptionKey
|
|
109
|
+
? new CredentialVault(storage, config.credentialEncryptionKey)
|
|
110
|
+
: undefined;
|
|
111
|
+
const registry = new Registry(config.connectors, {
|
|
112
|
+
storage,
|
|
113
|
+
logger,
|
|
114
|
+
credentialVault,
|
|
115
|
+
toolCacheTtlSeconds: config.toolCacheTtlSeconds,
|
|
116
|
+
persistToolCatalog: config.persistToolCatalog,
|
|
117
|
+
toolCatalogStaleSeconds: config.toolCatalogStaleSeconds,
|
|
118
|
+
maxResultBytes: config.maxResultBytes,
|
|
119
|
+
});
|
|
120
|
+
const handler = createFetchHandler({
|
|
121
|
+
registry,
|
|
122
|
+
auth: normalizeAuth(config.auth),
|
|
123
|
+
publicUrl: config.publicUrl,
|
|
124
|
+
serverInfo: {
|
|
125
|
+
name: config.serverInfo?.name ?? "connecta",
|
|
126
|
+
version: config.serverInfo?.version ?? "0.1.0",
|
|
127
|
+
},
|
|
128
|
+
logger,
|
|
129
|
+
activity: config.activity,
|
|
130
|
+
activityReadGate: config.activityReadGate,
|
|
131
|
+
activityDeploymentId: config.activityDeploymentId,
|
|
132
|
+
executor: config.executor,
|
|
133
|
+
credentialVault,
|
|
134
|
+
deploymentInfo: config.deploymentInfo,
|
|
135
|
+
branding: config.branding,
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
fetch: (request, _env, ctx) =>
|
|
139
|
+
handler(
|
|
140
|
+
request,
|
|
141
|
+
ctx && typeof (ctx as { waitUntil?: unknown }).waitUntil === "function"
|
|
142
|
+
? (ctx as { waitUntil(promise: Promise<unknown>): void })
|
|
143
|
+
: undefined,
|
|
144
|
+
),
|
|
145
|
+
registry,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { remoteMcp } from "./connectors/remote-mcp.js";
|
|
150
|
+
export { api } from "./connectors/api.js";
|
|
151
|
+
export { bearerToken } from "./auth/bearer.js";
|
|
152
|
+
export { memoryStorage } from "./storage/memory.js";
|
|
153
|
+
export { CredentialVault } from "./credentials.js";
|
|
154
|
+
export { Registry } from "./registry.js";
|
|
155
|
+
export { createMetaTools } from "./meta-tools.js";
|
|
156
|
+
export {
|
|
157
|
+
buildSandboxProviders,
|
|
158
|
+
createExecuteTool,
|
|
159
|
+
sanitizeIdentifier,
|
|
160
|
+
} from "./execute.js";
|
|
161
|
+
|
|
162
|
+
export type { RemoteMcpOptions, RemoteMcpAuth } from "./connectors/remote-mcp.js";
|
|
163
|
+
export type { ApiOptions, ApiTool } from "./connectors/api.js";
|
|
164
|
+
export type {
|
|
165
|
+
Connector,
|
|
166
|
+
ConnectaBranding,
|
|
167
|
+
ConnectorCredentialAccess,
|
|
168
|
+
ConnectorCredentialConfig,
|
|
169
|
+
ConnectorCredentialFieldConfig,
|
|
170
|
+
ConnectorCredentialValues,
|
|
171
|
+
ConnectorContext,
|
|
172
|
+
ConnectorStatus,
|
|
173
|
+
CredentialTestResult,
|
|
174
|
+
ExecuteResult,
|
|
175
|
+
Executor,
|
|
176
|
+
ExecutorProvider,
|
|
177
|
+
InboundAuth,
|
|
178
|
+
UiAuthConfig,
|
|
179
|
+
AuthResult,
|
|
180
|
+
JsonSchema,
|
|
181
|
+
KVStorage,
|
|
182
|
+
Logger,
|
|
183
|
+
ToolDef,
|
|
184
|
+
ToolAnnotations,
|
|
185
|
+
} from "./types.js";
|
|
186
|
+
export type {
|
|
187
|
+
ActivityActor,
|
|
188
|
+
ActivityCallSource,
|
|
189
|
+
ActivityOutcome,
|
|
190
|
+
ActivityPage,
|
|
191
|
+
ActivityReader,
|
|
192
|
+
ActivityReadGate,
|
|
193
|
+
ActivitySink,
|
|
194
|
+
ActivityStore,
|
|
195
|
+
ToolCallActivityEvent,
|
|
196
|
+
} from "./activity.js";
|
|
197
|
+
export { InvalidActivityCursorError } from "./activity.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Connector } from "./types.js";
|
|
2
|
+
|
|
3
|
+
interface McpishResult {
|
|
4
|
+
content?: { type?: string; text?: string }[];
|
|
5
|
+
isError?: boolean;
|
|
6
|
+
structuredContent?: unknown;
|
|
7
|
+
toolResult?: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unwrap an MCP CallToolResult into an ordinary JavaScript value:
|
|
12
|
+
* `toolResult` wins when present, then `structuredContent`; all-text content is
|
|
13
|
+
* JSON-parsed when possible. Downstream `isError` results become exceptions.
|
|
14
|
+
* Non-MCP connectors already return plain values.
|
|
15
|
+
*/
|
|
16
|
+
export function unwrapMcpResult(
|
|
17
|
+
kind: Connector["kind"],
|
|
18
|
+
result: unknown,
|
|
19
|
+
): unknown {
|
|
20
|
+
if (kind !== "mcp" || result == null || typeof result !== "object") {
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
const r = result as McpishResult;
|
|
24
|
+
if ("toolResult" in r) return r.toolResult;
|
|
25
|
+
const content = Array.isArray(r.content) ? r.content : [];
|
|
26
|
+
if (r.isError) {
|
|
27
|
+
const text = content
|
|
28
|
+
.filter((c) => c.type === "text")
|
|
29
|
+
.map((c) => c.text ?? "")
|
|
30
|
+
.join("\n");
|
|
31
|
+
throw new Error(text || "Tool call failed");
|
|
32
|
+
}
|
|
33
|
+
if (r.structuredContent != null) return r.structuredContent;
|
|
34
|
+
if (content.length > 0 && content.every((c) => c.type === "text")) {
|
|
35
|
+
const text = content.map((c) => c.text ?? "").join("\n");
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|