@zackbart/connecta 0.26.3 → 0.28.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 +11 -11
- package/CHANGELOG.md +128 -1
- package/README.md +15 -19
- package/bin/connecta.mjs +10 -6
- package/dist/access-tokens.js +13 -2
- package/dist/activity.d.ts +5 -4
- package/dist/activity.js +1 -10
- package/dist/artifacts/connector.d.ts +3 -0
- package/dist/artifacts/connector.js +6 -0
- package/dist/artifacts/document.js +6 -0
- package/dist/artifacts/json.js +10 -0
- package/dist/artifacts/module.js +7 -1
- package/dist/artifacts/operations.d.ts +3 -1
- package/dist/artifacts/operations.js +69 -11
- package/dist/artifacts/refresh.d.ts +2 -0
- package/dist/artifacts/refresh.js +3 -2
- package/dist/artifacts/routes.js +4 -15
- package/dist/artifacts/validate.js +2 -4
- package/dist/auth/downstream-oauth.js +95 -36
- package/dist/catalog-service.js +1 -1
- package/dist/connector-scope.d.ts +2 -0
- package/dist/connector-scope.js +27 -0
- package/dist/connectors/api.js +8 -2
- package/dist/connectors/remote-mcp.js +57 -17
- package/dist/credentials.js +2 -1
- package/dist/execute.js +107 -368
- package/dist/executors/quickjs.js +1 -1
- package/dist/exempt-writes.js +162 -0
- package/dist/index.d.ts +12 -31
- package/dist/index.js +15 -47
- package/dist/invocation.js +6 -19
- package/dist/meta-tools.js +1 -2
- package/dist/oauth-partition.js +32 -0
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -3
- package/dist/operator-ui/view.js +6 -4
- package/dist/providers/notion.js +4 -1
- package/dist/providers/vercel.js +13 -9
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +58 -25
- package/dist/routes/credentials.js +16 -8
- package/dist/routes/mcp.js +5 -21
- package/dist/routes/oauth.js +70 -64
- package/dist/routes/shared.d.ts +1 -6
- package/dist/runtime/run.js +7 -1
- package/dist/server.js +0 -4
- package/dist/skills.js +8 -23
- package/dist/storage/file.js +57 -20
- package/dist/storage/memory.js +8 -1
- package/dist/tool-safety.d.ts +1 -1
- package/dist/tool-safety.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +51 -14
- package/documentation/auth.md +7 -4
- package/documentation/code-mode.md +91 -194
- package/documentation/meta-tools.md +38 -41
- package/ethos.md +6 -7
- package/examples/worker/README.md +14 -17
- package/examples/worker/src/d1-activity-row.ts +4 -1
- package/examples/worker/src/d1-storage.ts +27 -5
- package/examples/worker/src/index.ts +1 -9
- package/package.json +4 -4
- package/templates/node/AGENTS.md +4 -3
- package/templates/node/README.md +3 -4
- package/templates/node/package.json +2 -2
- package/dist/resumable.js +0 -1291
- package/dist/run-journal.js +0 -347
package/dist/server.js
CHANGED
|
@@ -52,10 +52,6 @@ export function createFetchHandler(opts) {
|
|
|
52
52
|
...(opts.executorName !== undefined
|
|
53
53
|
? { executor: { name: opts.executorName } }
|
|
54
54
|
: {}),
|
|
55
|
-
// Whether a program's write pauses for resume_execution or fails E4.
|
|
56
|
-
// The tool is listed either way; this is the only place an operator
|
|
57
|
-
// can see which answer it gives (#565).
|
|
58
|
-
resumableWrites: opts.resumable !== undefined,
|
|
59
55
|
// Counts only, from refreshes that already happened — the endpoint
|
|
60
56
|
// asks no downstream anything, and `connecta doctor` reads it to
|
|
61
57
|
// report a stale allowlist without a probe of its own (#343).
|
package/dist/skills.js
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { boundedEchoText } from "./errors.js";
|
|
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.
|
|
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. Sample unfamiliar reads.";
|
|
3
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
4
|
/**
|
|
5
|
-
* The MCP instructions
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
5
|
+
* The always-loaded MCP `instructions` string. A program runs reads and the
|
|
6
|
+
* writes config exempts from approval; every other write is its own top-level
|
|
7
|
+
* call_destructive_tool, where the host's permission prompt is the approval.
|
|
10
8
|
*/
|
|
11
|
-
export const CONNECTA_INSTRUCTIONS =
|
|
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}`;
|
|
9
|
+
export const CONNECTA_INSTRUCTIONS = `${ROUTE} Only readOnlyHint: true tools and config-exempt writes run there. Keep catalog inspection and other unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. Never repeat a write to recover its output. ${RECOVERY}`;
|
|
17
10
|
const USAGE_SKILL_BASE = `# Connecta usage
|
|
18
11
|
|
|
19
12
|
## The surface
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
Seven tools: \`execute_code\`, \`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.
|
|
22
15
|
|
|
23
16
|
Follow the MCP instructions for routing. Read at most once for syntax or repair.
|
|
24
17
|
|
|
@@ -48,16 +41,14 @@ For top-level catalog inspection or approval-required discovery, omit \`limit\`
|
|
|
48
41
|
- Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
|
|
49
42
|
- Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
|
|
50
43
|
|
|
51
|
-
Only explicitly \`readOnlyHint: true\`
|
|
44
|
+
Only explicitly \`readOnlyHint: true\` calls and writes the deployment's config exempts from approval run in a program. Any other write is refused before it is sent: send it through top-level \`call_destructive_tool\`, where the host asks. Sandbox code cannot widen host gates.
|
|
52
45
|
|
|
53
46
|
## Errors and repair
|
|
54
47
|
|
|
55
48
|
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.
|
|
56
49
|
|
|
57
50
|
- \`destructive_tool_requires_approval\`: stop and send the returned address through top-level \`call_destructive_tool\`.
|
|
58
|
-
- \`
|
|
59
|
-
- \`write_outcome_unknown\`: sent but unanswered, never re-sent; check the target.
|
|
60
|
-
- \`execution_expired\`, \`_diverged\`, \`_interrupted\`: check \`writes\`, then rerun.
|
|
51
|
+
- \`write_outcome_unknown\`: an exempt write was sent but unanswered and is never re-sent; check the target.
|
|
61
52
|
- \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
|
|
62
53
|
- Truncated direct call: page with \`get_result\`, never repeat a write. A program result has no page handle; reduce a read in a new program. After a write, check the target or report the gap.
|
|
63
54
|
- Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
|
|
@@ -72,8 +63,6 @@ The \`execute_code\` description states this deployment's host-call and write bu
|
|
|
72
63
|
|
|
73
64
|
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.
|
|
74
65
|
|
|
75
|
-
\`Date.now()\` is the run's start and does not advance; \`Math.random()\` is seeded per run.
|
|
76
|
-
|
|
77
66
|
## Examples
|
|
78
67
|
|
|
79
68
|
One read-only call at a known address:
|
|
@@ -113,10 +102,6 @@ Connector guides appear in \`skills({})\` and discovery with an exact \`guide\`
|
|
|
113
102
|
`;
|
|
114
103
|
/** Shared Connecta routing guidance, byte-identical across deployments. */
|
|
115
104
|
export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
|
|
116
|
-
/** The always-loaded MCP `instructions` string. */
|
|
117
|
-
export function instructionsFor(resumableWrites) {
|
|
118
|
-
return resumableWrites ? CONNECTA_INSTRUCTIONS : READ_ONLY_PROGRAM_INSTRUCTIONS;
|
|
119
|
-
}
|
|
120
105
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
121
106
|
export function hasConnectorGuides(connectors) {
|
|
122
107
|
return connectors.some((connector) => connectorGuide(connector) !== undefined);
|
package/dist/storage/file.js
CHANGED
|
@@ -2,6 +2,23 @@ import { chmodSync, closeSync, existsSync, futimesSync, mkdirSync, openSync, rea
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
function validEntry(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) &&
|
|
7
|
+
"value" in value && typeof value.value === "string" &&
|
|
8
|
+
(!("exp" in value) || (typeof value.exp === "number" && Number.isFinite(value.exp)));
|
|
9
|
+
}
|
|
10
|
+
function makeEntry(value, ttlSeconds) {
|
|
11
|
+
if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
|
|
12
|
+
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
13
|
+
}
|
|
14
|
+
if (!ttlSeconds)
|
|
15
|
+
return { value };
|
|
16
|
+
const exp = Date.now() + ttlSeconds * 1000;
|
|
17
|
+
if (!Number.isFinite(exp)) {
|
|
18
|
+
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
19
|
+
}
|
|
20
|
+
return { value, exp };
|
|
21
|
+
}
|
|
5
22
|
const HEARTBEAT_MS = 15_000;
|
|
6
23
|
const STALE_LOCK_MS = 60_000;
|
|
7
24
|
const localLocks = new Map();
|
|
@@ -272,12 +289,19 @@ export function fileStorage(path, opts = {}) {
|
|
|
272
289
|
// Non-POSIX filesystem or a race on the file — leave the mode as-is.
|
|
273
290
|
}
|
|
274
291
|
};
|
|
275
|
-
let data =
|
|
292
|
+
let data = Object.create(null);
|
|
276
293
|
try {
|
|
277
294
|
if (existsSync(path)) {
|
|
278
295
|
tighten();
|
|
279
296
|
try {
|
|
280
|
-
|
|
297
|
+
const loaded = JSON.parse(readFileSync(path, "utf8"));
|
|
298
|
+
if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) {
|
|
299
|
+
throw new TypeError("Expected a state object");
|
|
300
|
+
}
|
|
301
|
+
if (!Object.values(loaded).every(validEntry)) {
|
|
302
|
+
throw new TypeError("Expected string values and finite expiry timestamps");
|
|
303
|
+
}
|
|
304
|
+
data = Object.assign(Object.create(null), loaded);
|
|
281
305
|
}
|
|
282
306
|
catch (error) {
|
|
283
307
|
// Never let a damaged state file be silently replaced by an empty one:
|
|
@@ -299,7 +323,7 @@ export function fileStorage(path, opts = {}) {
|
|
|
299
323
|
`moved to ${quarantine}, starting from empty state. Downstream ` +
|
|
300
324
|
`OAuth connectors must be re-authorized and stored credentials ` +
|
|
301
325
|
`re-entered.`);
|
|
302
|
-
data =
|
|
326
|
+
data = Object.create(null);
|
|
303
327
|
}
|
|
304
328
|
}
|
|
305
329
|
}
|
|
@@ -315,13 +339,14 @@ export function fileStorage(path, opts = {}) {
|
|
|
315
339
|
// Reads only prune memory; they do not rewrite the state file.
|
|
316
340
|
const now = Date.now();
|
|
317
341
|
for (const [key, entry] of Object.entries(data)) {
|
|
318
|
-
if (entry.exp && now
|
|
342
|
+
if (entry.exp !== undefined && now >= entry.exp)
|
|
319
343
|
delete data[key];
|
|
320
344
|
}
|
|
321
345
|
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
322
346
|
// 0o600 on the tmp file; the atomic rename below preserves it, so the live
|
|
323
347
|
// state file is never briefly world-readable.
|
|
324
348
|
const fd = openSync(tmp, "wx", 0o600);
|
|
349
|
+
let committed = false;
|
|
325
350
|
try {
|
|
326
351
|
try {
|
|
327
352
|
writeFileSync(fd, JSON.stringify(data));
|
|
@@ -331,9 +356,13 @@ export function fileStorage(path, opts = {}) {
|
|
|
331
356
|
}
|
|
332
357
|
lock.assertHeld();
|
|
333
358
|
renameSync(tmp, path);
|
|
359
|
+
committed = true;
|
|
334
360
|
}
|
|
335
361
|
finally {
|
|
336
|
-
|
|
362
|
+
// A successful rename already removed the temp path. Cleanup must not
|
|
363
|
+
// turn a committed snapshot into a failure that callers roll back.
|
|
364
|
+
if (!committed)
|
|
365
|
+
rmSync(tmp, { force: true });
|
|
337
366
|
}
|
|
338
367
|
tighten();
|
|
339
368
|
};
|
|
@@ -341,7 +370,7 @@ export function fileStorage(path, opts = {}) {
|
|
|
341
370
|
const e = data[key];
|
|
342
371
|
if (!e)
|
|
343
372
|
return null;
|
|
344
|
-
if (e.exp && Date.now()
|
|
373
|
+
if (e.exp !== undefined && Date.now() >= e.exp) {
|
|
345
374
|
delete data[key];
|
|
346
375
|
return null;
|
|
347
376
|
}
|
|
@@ -358,19 +387,32 @@ export function fileStorage(path, opts = {}) {
|
|
|
358
387
|
async set(key, value, opts) {
|
|
359
388
|
assertOpen();
|
|
360
389
|
lock.assertHeld();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
390
|
+
const previous = fresh(key);
|
|
391
|
+
data[key] = makeEntry(value, opts?.ttlSeconds);
|
|
392
|
+
try {
|
|
393
|
+
persist();
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (previous)
|
|
397
|
+
data[key] = previous;
|
|
398
|
+
else
|
|
399
|
+
delete data[key];
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
368
402
|
},
|
|
369
403
|
async delete(key) {
|
|
370
404
|
assertOpen();
|
|
371
405
|
lock.assertHeld();
|
|
406
|
+
const previous = fresh(key);
|
|
372
407
|
delete data[key];
|
|
373
|
-
|
|
408
|
+
try {
|
|
409
|
+
persist();
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
if (previous)
|
|
413
|
+
data[key] = previous;
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
374
416
|
},
|
|
375
417
|
async list(prefix) {
|
|
376
418
|
assertOpen();
|
|
@@ -395,12 +437,7 @@ export function fileStorage(path, opts = {}) {
|
|
|
395
437
|
delete data[key];
|
|
396
438
|
}
|
|
397
439
|
else {
|
|
398
|
-
data[key] =
|
|
399
|
-
value: next,
|
|
400
|
-
...(opts?.ttlSeconds
|
|
401
|
-
? { exp: Date.now() + opts.ttlSeconds * 1000 }
|
|
402
|
-
: {}),
|
|
403
|
-
};
|
|
440
|
+
data[key] = makeEntry(next, opts?.ttlSeconds);
|
|
404
441
|
}
|
|
405
442
|
try {
|
|
406
443
|
persist();
|
package/dist/storage/memory.js
CHANGED
|
@@ -13,6 +13,13 @@ export function memoryStorage() {
|
|
|
13
13
|
return e;
|
|
14
14
|
};
|
|
15
15
|
const write = (key, value, ttlSeconds) => {
|
|
16
|
+
if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
|
|
17
|
+
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
18
|
+
}
|
|
19
|
+
const exp = ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined;
|
|
20
|
+
if (exp !== undefined && !Number.isFinite(exp)) {
|
|
21
|
+
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
22
|
+
}
|
|
16
23
|
// Rotate through at most 16 existing keys. Live entries cannot keep an
|
|
17
24
|
// expired tail resident forever, and no request starts a background job.
|
|
18
25
|
for (let i = 0; i < 16; i++) {
|
|
@@ -25,7 +32,7 @@ export function memoryStorage() {
|
|
|
25
32
|
}
|
|
26
33
|
map.set(key, {
|
|
27
34
|
value,
|
|
28
|
-
...(
|
|
35
|
+
...(exp !== undefined ? { exp } : {}),
|
|
29
36
|
});
|
|
30
37
|
};
|
|
31
38
|
return {
|
package/dist/tool-safety.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface ApprovalPolicy {
|
|
|
20
20
|
}
|
|
21
21
|
export declare const NO_EXEMPTIONS: ApprovalPolicy;
|
|
22
22
|
/**
|
|
23
|
-
* Whether a program may call this tool without
|
|
23
|
+
* Whether a program may call this tool without asking for approval.
|
|
24
24
|
*
|
|
25
25
|
* Only a tool that is *not* explicitly read-only can be exempt — a read-only
|
|
26
26
|
* tool needs no exemption, and reporting one would blur the two classes an
|
package/dist/tool-safety.js
CHANGED
|
@@ -14,7 +14,7 @@ export const NO_EXEMPTIONS = {
|
|
|
14
14
|
tools: new Map(),
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Whether a program may call this tool without
|
|
17
|
+
* Whether a program may call this tool without asking for approval.
|
|
18
18
|
*
|
|
19
19
|
* Only a tool that is *not* explicitly read-only can be exempt — a read-only
|
|
20
20
|
* tool needs no exemption, and reporting one would blur the two classes an
|
package/dist/types.d.ts
CHANGED
|
@@ -253,8 +253,8 @@ export interface Connector {
|
|
|
253
253
|
/**
|
|
254
254
|
* This connector's own default for its tools that are not explicitly
|
|
255
255
|
* read-only, inside `execute_code`: `"never"` lets a program call them
|
|
256
|
-
* without
|
|
257
|
-
* whose writes are cheap to undo (the
|
|
256
|
+
* without asking for approval. Reserved for connectors connecta ships
|
|
257
|
+
* whose writes are cheap to undo (the artifacts connector, whose
|
|
258
258
|
* every write is a new immutable version); a deployment's own connectors
|
|
259
259
|
* leave it unset and exempt through `execute.approval`, which overrides
|
|
260
260
|
* this either way. Never read from a downstream catalog — a server cannot
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -61,7 +61,7 @@ top to bottom.
|
|
|
61
61
|
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
62
62
|
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
63
63
|
| 3 | `/.well-known/*` | Auth metadata, or 404. |
|
|
64
|
-
| 4 | `/health` | Open and payload-free: health, executor,
|
|
64
|
+
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata, with drift as stable short hashes. |
|
|
65
65
|
| 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state and personal-ownership checked, independent of the UI. |
|
|
66
66
|
| 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
|
|
67
67
|
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
@@ -94,7 +94,7 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
94
94
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
95
95
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
96
96
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
97
|
-
404 — then register the
|
|
97
|
+
404 — then register the seven meta-tools on a fresh `McpServer`
|
|
98
98
|
(`test/server.test.ts`, `test/code-first-surface.test.ts`).
|
|
99
99
|
|
|
100
100
|
## Layers below the meta-tools
|
|
@@ -109,7 +109,7 @@ or hands out, and a change usually belongs in exactly one of them:
|
|
|
109
109
|
| `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
|
|
110
110
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
|
|
111
111
|
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
112
|
-
| `src/
|
|
112
|
+
| `src/exempt-writes.ts` | The writes a program may send, which config exempted from asking: the write budget's default, what a dispatched write's outcome says about whether it landed, and the tracking that lets one outlive the program without hiding an unknown outcome. Every other program write is refused before it reaches this. |
|
|
113
113
|
|
|
114
114
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
|
|
115
115
|
services, `CatalogService` and `InvocationService`. That is the point: a
|
|
@@ -222,12 +222,10 @@ successful call's preview and a paging-unavailable notice rather than a result i
|
|
|
222
222
|
A second optional method, `compareAndSet(key, expected, next, options?)`, is an
|
|
223
223
|
atomic claim: `null` means absent (expired counts) on the way in and delete on
|
|
224
224
|
the way out. A successful write accepts the same optional `ttlSeconds` as
|
|
225
|
-
`set`.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
exactly when the store has it, and asking for it over a store without it
|
|
230
|
-
refuses to construct. Downstream OAuth uses it where the store has it, so
|
|
225
|
+
`set`. Artifacts require it, because every write commits by swapping a head,
|
|
226
|
+
and so does issuing access tokens; a read and a write standing in for the
|
|
227
|
+
claim would let two writers both win. Downstream OAuth uses it where the store
|
|
228
|
+
has it, so
|
|
231
229
|
resealing legacy plaintext and discarding a refused grant cannot overwrite a
|
|
232
230
|
consent that landed in between, and falls back to a read and a write where it
|
|
233
231
|
does not.
|
|
@@ -330,7 +328,7 @@ owner admitted to `set_refresh` is stored with the program. Every run rechecks
|
|
|
330
328
|
that identity's current `connectorAccess` and pool grant, including continued
|
|
331
329
|
access to `artifacts.set_refresh`, then intersects those grants with shared
|
|
332
330
|
connectors. Revoking either grant stops later runs.
|
|
333
|
-
|
|
331
|
+
Approval exemptions are off, so a refresh writes nothing. A refused call fails the
|
|
334
332
|
entire refresh even if its program catches the error. The head CAS claims one
|
|
335
333
|
run per artifact; the data commit checks the claim ID and program version in
|
|
336
334
|
the same CAS. The claim deadline starts before executor admission and aborts
|
|
@@ -407,7 +405,8 @@ shape from building, in someone else's repository rather than this one.
|
|
|
407
405
|
|
|
408
406
|
## Effect inside
|
|
409
407
|
|
|
410
|
-
The core runs on Effect v4
|
|
408
|
+
The core runs on stable Effect v4, exact-pinned to `4.0.0`; no API a deployment
|
|
409
|
+
touches does. `createConnecta`,
|
|
411
410
|
`remoteMcp()`, `api()`, the `Connector` contract, and every shipped `.d.ts`
|
|
412
411
|
are Promise-shaped and name no Effect type, so a connector author never meets a
|
|
413
412
|
second async paradigm. The reason for the rewrite is the first section of this
|
|
@@ -482,7 +481,6 @@ would be another partition's.
|
|
|
482
481
|
| Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
|
|
483
482
|
| One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
|
|
484
483
|
| `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
|
|
485
|
-
| Resumable writes (`resumable.ts`) | A play's stop is a Deferred its write gate completes, raced against the executor like the watchdog; the gate for call n awaits the decision Deferreds of every lower-numbered call; header writes are one compare-and-set at a time on a per-play turn. A paused run holds nothing request-bound — its journal is data, and the next play is a new request's. |
|
|
486
484
|
| Discovery (`catalog-service.ts`) | A request-scoped cache: one shared read per connector (`runtime/shared-read.ts`), settled by the read itself and carrying its own signal and the probe timeout whichever asker starts it. Each asker waits under its own deadline and signal, so one that times out or is cancelled fails alone, and the read is cancelled only once every asker has gone. Fan-out is `Effect.forEach` under the discovery concurrency. |
|
|
487
485
|
| Registry (`registry.ts`) | Catalog persistence and the result stash are programs over `Storage`. A refresh flight is a Deferred its publishing request completes, bounded by its owner's deadline (the default probe timeout when it has none); persisted-catalog writes take per-connector turns, each a Deferred its own request completes. Same-request loads share one read the way discovery's do. |
|
|
488
486
|
| Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
|
|
@@ -569,8 +567,47 @@ the operator and activity routes cost about +100 KB gzip on `./ui` and +130 KB
|
|
|
569
567
|
on `./activity`, and matching the wire format meant opting out of most of what
|
|
570
568
|
it does: unowned paths fall through rather than 404, a wrong method is a JSON
|
|
571
569
|
405, and the content-type and size checks run before a body is read. The root
|
|
572
|
-
entry may import only `effect` itself (`test/purity.test.ts`)
|
|
573
|
-
`
|
|
570
|
+
entry may import only `effect` itself (`test/purity.test.ts`). Effect v4's
|
|
571
|
+
area imports, such as `effect/http-api` and `effect/ai`, would have to stay
|
|
572
|
+
behind a subpath. APIs tagged `@stability unstable` can still change in minor
|
|
573
|
+
releases, so stable v4 keeps the exact pin.
|
|
574
|
+
|
|
575
|
+
The stable `4.0.0` release was re-evaluated on 2026-10-01 with
|
|
576
|
+
[`scripts/probes/effect-v4-evaluation.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/probes/effect-v4-evaluation.mjs).
|
|
577
|
+
Run `npm run build`, then `node scripts/probes/effect-v4-evaluation.mjs output.json`.
|
|
578
|
+
The [raw observations](https://github.com/zackbart/connecta/blob/main/scripts/probes/effect-v4-evaluation-2026-10-01.json)
|
|
579
|
+
are synthetic Node requests and neutral esbuild bundles, not live-client or
|
|
580
|
+
workerd lifetime verification. Bundle figures add representative prototypes
|
|
581
|
+
alongside the current code; they do not claim savings from removing old code.
|
|
582
|
+
|
|
583
|
+
Two native input schemas with validation and JSON Schema rendering add 62,148
|
|
584
|
+
bytes gzip to the root, taking it from 303,083 to 365,231 against a 339,526
|
|
585
|
+
cap. Effect can reject excess properties when both decoding and rendering use
|
|
586
|
+
`onExcessProperty: "error"`; its default strips them. The generated schemas
|
|
587
|
+
still differ from the exact meta-tool goldens, including record rendering and
|
|
588
|
+
composed numeric bounds. A Schema replacement would need to preserve that
|
|
589
|
+
contract and memoized rendering. Both MCP SDK packages still depend on Zod,
|
|
590
|
+
so changing our inputs alone does not remove Zod from an installation.
|
|
591
|
+
|
|
592
|
+
A one-endpoint `HttpApi` prototype adds 106,634 bytes gzip to `./ui` and 106,453
|
|
593
|
+
to `./activity`. Even plain `HttpRouter` adds 45,324 to `./ui`, taking it to
|
|
594
|
+
130,023 against a 128,983 cap. Both default handlers return an empty 404 for a
|
|
595
|
+
wrong method and an unowned path; our routes need a JSON 405 for the former
|
|
596
|
+
and module fallthrough for the latter. Their `toWebHandler` also builds its
|
|
597
|
+
layer immediately and owns a runner, so adopting it would require proving
|
|
598
|
+
global-scope safety and preserving our scheduler and response-lifetime rules.
|
|
599
|
+
The existing Effect route programs keep those rules without this routing layer.
|
|
600
|
+
|
|
601
|
+
Effect's MCP server has a separate compatibility gate. Its `2026-07-28`
|
|
602
|
+
adapter accepted a stateless `tools/list`, but its `2025-06-18` adapter refused
|
|
603
|
+
a fresh request with 400. Initialization issued a session, and that session's
|
|
604
|
+
next request returned 404 on a fresh handler; Connecta served the same fresh
|
|
605
|
+
legacy list with 200 JSON and no session. The stable package exports no MCP
|
|
606
|
+
client, so it also cannot replace our downstream SDK and OAuth implementation.
|
|
607
|
+
[Issue #622](https://github.com/zackbart/connecta/issues/622) records the server
|
|
608
|
+
parity requirements. These measurements support keeping the current MCP,
|
|
609
|
+
validation, and routing implementations; stable v4 alone does not establish a
|
|
610
|
+
replacement's benefit or compatibility.
|
|
574
611
|
|
|
575
612
|
The core's cost is recorded rather than guessed. Against 0.24.4 the root entry
|
|
576
613
|
grew from 235,346 to 279,526 bytes gzip, the Worker example from 263,949 to
|
package/documentation/auth.md
CHANGED
|
@@ -64,9 +64,10 @@ also work on older adapters without `compareAndSet`; **new issuance requires
|
|
|
64
64
|
atomic `compareAndSet`**, because counting records before writing admits too many
|
|
65
65
|
concurrent creates. Active capacity defaults to 100, configurable with
|
|
66
66
|
`accessTokens(storage, { maxActive: 200 })`, up to 1,000. A durable reservation
|
|
67
|
-
counts before a secret is written.
|
|
68
|
-
|
|
69
|
-
an
|
|
67
|
+
counts before a secret is written. Failures before lookup publication release
|
|
68
|
+
their reservation; an uncertain lookup write retains its metadata and capacity
|
|
69
|
+
so an operator can revoke it, even when creation returned no secret. Creation is
|
|
70
|
+
never automatically retried. Avoid creating new tokens through old-version instances once
|
|
70
71
|
new-version issuance has started, since those instances do not honor reservations.
|
|
71
72
|
|
|
72
73
|
Secrets contain 256 random bits and only their SHA-256 digests persist. Creation
|
|
@@ -285,7 +286,9 @@ remoteMcp("linear", { url: "https://mcp.linear.app/mcp", authScope: "personal",
|
|
|
285
286
|
A personal connector is absent — not refused — from any request without a
|
|
286
287
|
stable namespaced principal. For a principal that can see one, connecta
|
|
287
288
|
partitions connector storage, vault records, catalog caches, OAuth generations,
|
|
288
|
-
and observed result shapes under an opaque SHA-256 identity key.
|
|
289
|
+
and observed result shapes under an opaque SHA-256 identity key. Refreshes
|
|
290
|
+
coalesce within one owner; another owner's authorization epoch cannot retire
|
|
291
|
+
that refresh. Keep namespaces
|
|
289
292
|
and principal ids stable across upgrades; changing either selects different
|
|
290
293
|
partitions. Literal `auth: { type: "headers" }` cannot be personal, because its
|
|
291
294
|
secret lives in deployment code; `remoteMcp()` refuses that combination at
|