@zackbart/connecta 0.29.0 → 0.30.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 +67 -0
- package/README.md +123 -134
- package/bin/connecta.mjs +13 -2
- package/bin/version.mjs +1 -1
- package/dist/argument-redaction.js +230 -0
- package/dist/auth-elicitation.js +14 -9
- package/dist/authorization-handoff.js +111 -0
- package/dist/catalog-cache.js +2 -2
- package/dist/catalog-service.js +103 -63
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -7
- package/dist/connectors/guarded-fetch.js +3 -3
- package/dist/connectors/negotiation-cache.js +1 -1
- package/dist/downstream-input-context.js +9 -0
- package/dist/downstream-input.js +13 -11
- package/dist/errors.d.ts +36 -12
- package/dist/errors.js +19 -9
- package/dist/execute.js +375 -43
- package/dist/executor-result.js +11 -9
- package/dist/executors/quickjs-child.js +12 -5
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +6 -1
- package/dist/executors/quickjs.js +20 -4
- package/dist/guest-runtime.js +52 -2
- package/dist/guest-types.d.ts +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/invocation.js +40 -14
- package/dist/meta-output.js +0 -15
- package/dist/meta-tools.js +166 -95
- package/dist/oauth-handoff.js +88 -11
- package/dist/program-result.js +5 -0
- package/dist/program-writes.js +5 -1
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/registry.d.ts +8 -6
- package/dist/registry.js +134 -61
- package/dist/resource-read.js +4 -1
- package/dist/routes/mcp.js +11 -2
- package/dist/sent-secrets.d.ts +54 -13
- package/dist/sent-secrets.js +770 -231
- package/dist/skills.js +26 -16
- package/dist/sqlite.js +21 -6
- package/dist/storage/keys.js +20 -5
- package/dist/storage/memory.js +9 -4
- package/dist/storage/sql.js +18 -9
- package/dist/types.d.ts +17 -3
- package/dist/usage-guide.js +49 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +2 -1
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +121 -89
- package/documentation/auth.md +138 -5
- package/documentation/code-mode.md +224 -107
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +135 -30
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +4 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +5 -0
- package/examples/worker/README.md +7 -0
- package/package.json +7 -1
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +1 -1
package/dist/skills.js
CHANGED
|
@@ -5,7 +5,7 @@ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
|
5
5
|
import { closeScopeOnExit } from "./runtime/connector-scope.js";
|
|
6
6
|
import { sentSecretsForRequest } from "./sent-secrets.js";
|
|
7
7
|
const ROUTE = "Choose a route before discovery. One known-address read uses call_tool; one known-address write uses call_destructive_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.";
|
|
8
|
-
const RECOVERY = '
|
|
8
|
+
const RECOVERY = 'When an auth error includes a handoff, give it to the user or operator. Use authorize_connector for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Fetch skills({ name: "usage" }) only for missing guidance or program repair.';
|
|
9
9
|
/**
|
|
10
10
|
* The always-loaded MCP `instructions` string. A program runs reads and the
|
|
11
11
|
* writes in trusted pools; a read-only pool routes every write to top-level
|
|
@@ -348,11 +348,14 @@ async function localRecords(connectors) {
|
|
|
348
348
|
const description = connectorGuideSummary(connector);
|
|
349
349
|
const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
|
|
350
350
|
const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${connectorGuide(connector)}`;
|
|
351
|
-
return
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
return {
|
|
352
|
+
...localRecord(name, `${root}/SKILL.md`, description, content, [
|
|
353
|
+
connectorSkillName(connector.id),
|
|
354
|
+
`skill://connecta/connectors/${encodeURIComponent(connector.id)}`,
|
|
355
|
+
root,
|
|
356
|
+
]),
|
|
357
|
+
connector,
|
|
358
|
+
};
|
|
356
359
|
}));
|
|
357
360
|
if (new Set(guides.map((guide) => guide.entry.uri)).size !== guides.length)
|
|
358
361
|
throw new ConnectorCallError("unavailable", "Connector skill slugs collide.");
|
|
@@ -583,18 +586,18 @@ export class SkillsRegistry {
|
|
|
583
586
|
return record;
|
|
584
587
|
}
|
|
585
588
|
missing(name) {
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
"investigate",
|
|
593
|
-
].join(", ");
|
|
589
|
+
const guides = this.registry.listConnectors().filter((connector) => connectorGuide(connector) !== undefined);
|
|
590
|
+
// The recovery message exposes these guide names even though no skill was
|
|
591
|
+
// read. Keep their visibility bindings if a guest catches and returns it.
|
|
592
|
+
for (const connector of guides)
|
|
593
|
+
this.options.onRead?.(connector, "local");
|
|
594
|
+
const available = ["usage", ...guides.map((connector) => connectorSkillName(connector.id)), "investigate"].join(", ");
|
|
594
595
|
// Caller-authored URI/error text never enters operator records. Preserve
|
|
595
596
|
// legacy guidance without allowing a missing name to probe a hidden view.
|
|
596
597
|
const id = name.startsWith(CONNECTOR_SKILL_PREFIX) ? name.slice(CONNECTOR_SKILL_PREFIX.length) : name;
|
|
597
598
|
const connector = this.registry.getConnector(id);
|
|
599
|
+
if (connector)
|
|
600
|
+
this.options.onRead?.(connector, "local");
|
|
598
601
|
const message = name.startsWith(CONNECTOR_SKILL_PREFIX)
|
|
599
602
|
? connector
|
|
600
603
|
? `Connector "${boundedEchoText(id)}" has no usage guide.`
|
|
@@ -638,7 +641,11 @@ export class SkillsRegistry {
|
|
|
638
641
|
if (local) {
|
|
639
642
|
const record = await withManifest(local);
|
|
640
643
|
this.assertSafe([record]);
|
|
641
|
-
|
|
644
|
+
if (record.connector)
|
|
645
|
+
this.options.onRead?.(record.connector, "local");
|
|
646
|
+
// A connector guide is operator-authored payload, like a downstream file; built-in guides are Connecta's own.
|
|
647
|
+
const text = record.connector ? sentSecretsForRequest(this.scope).redact(record.content) : record.content;
|
|
648
|
+
return { ...PRIVATE, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text }] };
|
|
642
649
|
}
|
|
643
650
|
if (!uri.startsWith("skill://downstream/"))
|
|
644
651
|
throw this.missing(uri);
|
|
@@ -664,7 +671,10 @@ export class SkillsRegistry {
|
|
|
664
671
|
}
|
|
665
672
|
if (bytes > MAX_SKILL_BYTES)
|
|
666
673
|
throw new ConnectorCallError("unavailable", "Downstream skill file exceeds the byte bound.");
|
|
667
|
-
|
|
674
|
+
this.options.onRead?.(record.connector, "downstream");
|
|
675
|
+
// Downstream file contents take the request's full payload rules before any reader sees them.
|
|
676
|
+
const clean = sentSecretsForRequest(this.scope).redact(content);
|
|
677
|
+
return { ...PRIVATE, contents: [{ ...clean, uri }] };
|
|
668
678
|
}
|
|
669
679
|
async text(name) {
|
|
670
680
|
const record = await this.lookup(name);
|
package/dist/sqlite.js
CHANGED
|
@@ -135,15 +135,22 @@ export function importStateFile(database, statePath, now = Date.now()) {
|
|
|
135
135
|
throw new TypeError(`${statePath} is not a connecta state file: entry ${index + 1} has no string value`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
let db;
|
|
139
|
+
let transaction = false;
|
|
140
|
+
let step = "open the SQLite database";
|
|
139
141
|
const result = { imported: 0, kept: 0, expired: 0 };
|
|
140
|
-
db.exec("BEGIN IMMEDIATE");
|
|
141
142
|
try {
|
|
143
|
+
db = typeof database === "string" ? openSqlite(database) : database;
|
|
144
|
+
step = "begin the import transaction";
|
|
145
|
+
db.exec("BEGIN IMMEDIATE");
|
|
146
|
+
transaction = true;
|
|
147
|
+
step = "prepare the key-value table";
|
|
142
148
|
for (const statement of KV_SCHEMA)
|
|
143
149
|
db.exec(statement);
|
|
144
150
|
const insert = db.prepare(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ?)
|
|
145
151
|
ON CONFLICT (key) DO NOTHING`);
|
|
146
|
-
for (const [key, entry] of entries) {
|
|
152
|
+
for (const [index, [key, entry]] of entries.entries()) {
|
|
153
|
+
step = `import entry ${index + 1} into the key-value table`;
|
|
147
154
|
const { value, exp } = entry;
|
|
148
155
|
if (exp !== undefined && exp <= now) {
|
|
149
156
|
result.expired += 1;
|
|
@@ -156,11 +163,19 @@ export function importStateFile(database, statePath, now = Date.now()) {
|
|
|
156
163
|
result.kept += 1;
|
|
157
164
|
}
|
|
158
165
|
}
|
|
166
|
+
step = "commit the import transaction";
|
|
159
167
|
db.exec("COMMIT");
|
|
160
168
|
}
|
|
161
|
-
catch
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
catch {
|
|
170
|
+
if (transaction) {
|
|
171
|
+
try {
|
|
172
|
+
db.exec("ROLLBACK");
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Rollback errors can contain stored text too. Report the failed step.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`State import failed: could not ${step}`);
|
|
164
179
|
}
|
|
165
180
|
return result;
|
|
166
181
|
}
|
package/dist/storage/keys.js
CHANGED
|
@@ -75,8 +75,8 @@ export const resultKeys = {
|
|
|
75
75
|
};
|
|
76
76
|
// --- root families --------------------------------------------------------
|
|
77
77
|
/**
|
|
78
|
-
* The runtime-wide result stash ledger: every live stash entry's
|
|
79
|
-
*
|
|
78
|
+
* The runtime-wide result stash ledger: every live stash entry's reservation
|
|
79
|
+
* id, byte charge, and expiry. One record, swapped by compare-and-set, so the
|
|
80
80
|
* stash bounds hold across isolates and processes sharing the store.
|
|
81
81
|
*/
|
|
82
82
|
export const stashLedgerKeys = {
|
|
@@ -86,13 +86,27 @@ export const stashLedgerKeys = {
|
|
|
86
86
|
prefixes: ["result-stash:v1:"],
|
|
87
87
|
version: { number: 1, in: "key" },
|
|
88
88
|
codec: jsonCodec,
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
ttl: { kind: "durable" },
|
|
89
|
+
// Every reservation is finite; the row expires with its latest charge.
|
|
90
|
+
ttl: { kind: "configured", by: "latest live stash reservation deadline" },
|
|
92
91
|
durable: false,
|
|
93
92
|
},
|
|
94
93
|
ledger: "result-stash:v1:ledger",
|
|
95
94
|
};
|
|
95
|
+
/** Storage-side proof that a stash charge's absolute deadline has passed. */
|
|
96
|
+
export const stashExpiryKeys = {
|
|
97
|
+
family: {
|
|
98
|
+
name: "result-stash-expiry",
|
|
99
|
+
scope: "root",
|
|
100
|
+
prefixes: ["result-stash-expiry:v1:"],
|
|
101
|
+
version: { number: 1, in: "key" },
|
|
102
|
+
codec: textCodec,
|
|
103
|
+
ttl: { kind: "configured", by: "probed stash charge deadline" },
|
|
104
|
+
durable: false,
|
|
105
|
+
},
|
|
106
|
+
// Different deadlines cannot overwrite one another's proof. Repeated probes
|
|
107
|
+
// of the same deadline always use the same value and absolute expiry.
|
|
108
|
+
deadline: (expiresAtMs) => `result-stash-expiry:v1:${expiresAtMs}`,
|
|
109
|
+
};
|
|
96
110
|
/**
|
|
97
111
|
* A persisted downstream catalog: the manifest at `catalog:<id>`, then its
|
|
98
112
|
* chunks at `catalog:<id>:chunk:<revision>:<index>`.
|
|
@@ -379,6 +393,7 @@ export const inputRetryKeys = {
|
|
|
379
393
|
export const KEY_FAMILIES = [
|
|
380
394
|
resultKeys.family,
|
|
381
395
|
stashLedgerKeys.family,
|
|
396
|
+
stashExpiryKeys.family,
|
|
382
397
|
catalogKeys.family,
|
|
383
398
|
responseCacheKeys.family,
|
|
384
399
|
oauthHandoffKeys.family,
|
package/dist/storage/memory.js
CHANGED
|
@@ -13,11 +13,15 @@ export function memoryStorage() {
|
|
|
13
13
|
}
|
|
14
14
|
return e;
|
|
15
15
|
};
|
|
16
|
-
const write = (key, value,
|
|
16
|
+
const write = (key, value, opts) => {
|
|
17
|
+
const { ttlSeconds, expiresAtMs } = opts ?? {};
|
|
18
|
+
if (expiresAtMs !== undefined && (!Number.isSafeInteger(expiresAtMs) || ttlSeconds !== undefined)) {
|
|
19
|
+
throw new RangeError("expiresAtMs must be a safe integer epoch timestamp without ttlSeconds");
|
|
20
|
+
}
|
|
17
21
|
if (ttlSeconds !== undefined && !Number.isFinite(ttlSeconds)) {
|
|
18
22
|
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
19
23
|
}
|
|
20
|
-
const exp = ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined;
|
|
24
|
+
const exp = expiresAtMs ?? (ttlSeconds ? Date.now() + ttlSeconds * 1000 : undefined);
|
|
21
25
|
if (exp !== undefined && !Number.isFinite(exp)) {
|
|
22
26
|
throw new RangeError("ttlSeconds must produce a finite expiration timestamp");
|
|
23
27
|
}
|
|
@@ -37,6 +41,7 @@ export function memoryStorage() {
|
|
|
37
41
|
});
|
|
38
42
|
};
|
|
39
43
|
return {
|
|
44
|
+
capabilities: { absoluteExpiry: true },
|
|
40
45
|
describe: () => ({ kind: "memory" }),
|
|
41
46
|
async get(key) {
|
|
42
47
|
validateStorageKey(key);
|
|
@@ -44,7 +49,7 @@ export function memoryStorage() {
|
|
|
44
49
|
},
|
|
45
50
|
async set(key, value, opts) {
|
|
46
51
|
validateStorageKey(key);
|
|
47
|
-
write(key, value, opts
|
|
52
|
+
write(key, value, opts);
|
|
48
53
|
},
|
|
49
54
|
async delete(key) {
|
|
50
55
|
validateStorageKey(key);
|
|
@@ -68,7 +73,7 @@ export function memoryStorage() {
|
|
|
68
73
|
if (next === null)
|
|
69
74
|
map.delete(key);
|
|
70
75
|
else
|
|
71
|
-
write(key, next, opts
|
|
76
|
+
write(key, next, opts);
|
|
72
77
|
return true;
|
|
73
78
|
},
|
|
74
79
|
};
|
package/dist/storage/sql.js
CHANGED
|
@@ -95,12 +95,21 @@ export function sqlStorage(driver, kind) {
|
|
|
95
95
|
// SQLite and D1 evaluate 'now' in the database, never in a caller isolate.
|
|
96
96
|
const now = "CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)";
|
|
97
97
|
const live = `(expires_at_ms IS NULL OR expires_at_ms > ${now})`;
|
|
98
|
-
const
|
|
98
|
+
const expiryOptions = (opts) => {
|
|
99
|
+
if (opts?.expiresAtMs !== undefined) {
|
|
100
|
+
if (!Number.isSafeInteger(opts.expiresAtMs) || opts.ttlSeconds !== undefined) {
|
|
101
|
+
throw new RangeError("expiresAtMs must be a safe integer epoch timestamp without ttlSeconds");
|
|
102
|
+
}
|
|
103
|
+
return { value: opts.expiresAtMs, expression: "?" };
|
|
104
|
+
}
|
|
105
|
+
return { value: ttlMillis(opts?.ttlSeconds), expression: `(${now} + ?)` };
|
|
106
|
+
};
|
|
99
107
|
const current = async (key) => {
|
|
100
108
|
const [row] = await driver.all(sql(`SELECT ${textColumn("value")} FROM connecta_kv WHERE key = ? AND ${live}`, key));
|
|
101
109
|
return row ? textOf(row.value, row.value_bytes, `the stored value of ${JSON.stringify(key)}`) : null;
|
|
102
110
|
};
|
|
103
111
|
return {
|
|
112
|
+
capabilities: { absoluteExpiry: true },
|
|
104
113
|
describe: () => ({ kind }),
|
|
105
114
|
async get(key) {
|
|
106
115
|
validateStorageKey(key);
|
|
@@ -110,15 +119,15 @@ export function sqlStorage(driver, kind) {
|
|
|
110
119
|
async set(key, value, opts) {
|
|
111
120
|
validateStorageKey(key);
|
|
112
121
|
await ensure();
|
|
113
|
-
const expiry =
|
|
122
|
+
const expiry = expiryOptions(opts);
|
|
114
123
|
await driver.batch([
|
|
115
124
|
sql(`DELETE FROM connecta_kv WHERE key IN (
|
|
116
125
|
SELECT key FROM connecta_kv WHERE expires_at_ms <= ${now} LIMIT ${SWEEP_ROWS}
|
|
117
126
|
)`),
|
|
118
127
|
sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
119
|
-
VALUES (?, ?, ${
|
|
128
|
+
VALUES (?, ?, ${expiry.expression})
|
|
120
129
|
ON CONFLICT (key) DO UPDATE SET
|
|
121
|
-
value = excluded.value, expires_at_ms = excluded.expires_at_ms`, key, value, expiry),
|
|
130
|
+
value = excluded.value, expires_at_ms = excluded.expires_at_ms`, key, value, expiry.value),
|
|
122
131
|
]);
|
|
123
132
|
},
|
|
124
133
|
async delete(key) {
|
|
@@ -149,7 +158,7 @@ export function sqlStorage(driver, kind) {
|
|
|
149
158
|
}
|
|
150
159
|
let expiry;
|
|
151
160
|
try {
|
|
152
|
-
expiry =
|
|
161
|
+
expiry = expiryOptions(opts);
|
|
153
162
|
}
|
|
154
163
|
catch (error) {
|
|
155
164
|
// Invalid write options do not change a failed comparison's result.
|
|
@@ -162,14 +171,14 @@ export function sqlStorage(driver, kind) {
|
|
|
162
171
|
// Insert, or take over a row that has expired. A live row makes the
|
|
163
172
|
// upsert's WHERE false, so nothing changes and the claim is refused.
|
|
164
173
|
return ((await driver.run(sql(`INSERT INTO connecta_kv (key, value, expires_at_ms)
|
|
165
|
-
VALUES (?, ?, ${
|
|
174
|
+
VALUES (?, ?, ${expiry.expression})
|
|
166
175
|
ON CONFLICT (key) DO UPDATE SET
|
|
167
176
|
value = excluded.value, expires_at_ms = excluded.expires_at_ms
|
|
168
177
|
WHERE connecta_kv.expires_at_ms IS NOT NULL
|
|
169
|
-
AND connecta_kv.expires_at_ms <= ${now}`, key, next, expiry))) > 0);
|
|
178
|
+
AND connecta_kv.expires_at_ms <= ${now}`, key, next, expiry.value))) > 0);
|
|
170
179
|
}
|
|
171
|
-
return ((await driver.run(sql(`UPDATE connecta_kv SET value = ?, expires_at_ms = ${
|
|
172
|
-
WHERE key = ? AND ${live} AND value = ?`, next, expiry, key, expected))) > 0);
|
|
180
|
+
return ((await driver.run(sql(`UPDATE connecta_kv SET value = ?, expires_at_ms = ${expiry.expression}
|
|
181
|
+
WHERE key = ? AND ${live} AND value = ?`, next, expiry.value, key, expected))) > 0);
|
|
173
182
|
},
|
|
174
183
|
};
|
|
175
184
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,15 +7,28 @@ export type JsonSchema = Record<string, unknown>;
|
|
|
7
7
|
* before accessing storage. SQL TEXT results truncate at NUL on Node 22.
|
|
8
8
|
*/
|
|
9
9
|
export interface KVStorage {
|
|
10
|
+
/**
|
|
11
|
+
* Explicit adapter opt-in: both `set` and `compareAndSet` honor
|
|
12
|
+
* `expiresAtMs`, including delayed commits. `createConnecta` rejects adapters
|
|
13
|
+
* without this capability. Declare it only after implementing that contract.
|
|
14
|
+
*/
|
|
15
|
+
readonly capabilities: {
|
|
16
|
+
readonly absoluteExpiry: true;
|
|
17
|
+
};
|
|
10
18
|
/** The live value at `key`; null when absent or expired. */
|
|
11
19
|
get(key: string): Promise<string | null>;
|
|
12
20
|
/**
|
|
13
21
|
* Write `value`. With `opts.ttlSeconds` the entry reads as absent once that
|
|
14
22
|
* many seconds pass; omitted or zero means no expiry. Shared stores must
|
|
15
|
-
* create and check expiry with a storage-owned clock
|
|
23
|
+
* create and check relative expiry with a storage-owned clock. `expiresAtMs`
|
|
24
|
+
* instead sets an absolute, safe-integer epoch-millisecond expiry, including
|
|
25
|
+
* for a delayed commit. Adapters must honor it without restarting a TTL at
|
|
26
|
+
* commit time. Supply at most one of these options; a past absolute expiry
|
|
27
|
+
* writes an already-expired value.
|
|
16
28
|
*/
|
|
17
29
|
set(key: string, value: string, opts?: {
|
|
18
30
|
ttlSeconds?: number;
|
|
31
|
+
expiresAtMs?: number;
|
|
19
32
|
}): Promise<void>;
|
|
20
33
|
delete(key: string): Promise<void>;
|
|
21
34
|
/** Live keys beginning with `prefix`, sorted by UTF-16 code unit. */
|
|
@@ -24,8 +37,8 @@ export interface KVStorage {
|
|
|
24
37
|
* Atomic compare-and-set: write `next` only if the key's current value is
|
|
25
38
|
* exactly `expected`, and report whether the write happened. `expected:
|
|
26
39
|
* null` means "absent", and an expired entry is absent. `next: null`
|
|
27
|
-
* deletes; otherwise `next`
|
|
28
|
-
*
|
|
40
|
+
* deletes; otherwise `next` takes the relative or absolute expiry options
|
|
41
|
+
* exactly as `set` would, or no expiry when both are omitted. On `false` nothing
|
|
29
42
|
* changed.
|
|
30
43
|
*
|
|
31
44
|
* Atomic means linearizable against every other operation on the same
|
|
@@ -41,6 +54,7 @@ export interface KVStorage {
|
|
|
41
54
|
*/
|
|
42
55
|
compareAndSet(key: string, expected: string | null, next: string | null, options?: {
|
|
43
56
|
ttlSeconds?: number;
|
|
57
|
+
expiresAtMs?: number;
|
|
44
58
|
}): Promise<boolean>;
|
|
45
59
|
/**
|
|
46
60
|
* Which adapter this is, for `Connecta.describeConfig()`. The shipped
|
package/dist/usage-guide.js
CHANGED
|
@@ -14,56 +14,51 @@ description: How to route work between one execute_code program and Connecta's e
|
|
|
14
14
|
| One known-address write | call_destructive_tool |
|
|
15
15
|
| Discovery, multiple or dependent calls, paging, reduction | execute_code |
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
covers catalog inspection and write discovery in read-only pools. Programs may
|
|
17
|
+
Read one account with call_tool: \`{ "address": "crm.get_account", "args": { "id": "acct_42" } }\`.
|
|
18
|
+
Discover and call together when schemas suffice. search_tools covers catalog
|
|
19
|
+
inspection and write discovery in read-only pools. Programs may
|
|
21
20
|
write only in trusted pools; the tool description states this endpoint's trust.
|
|
22
21
|
Unannotated tools fail closed to writes.
|
|
23
22
|
|
|
24
23
|
## The global and the envelopes
|
|
25
24
|
|
|
26
|
-
Pass
|
|
27
|
-
|
|
28
|
-
No imports, require, filesystem, fetch, or timers in portable programs.
|
|
25
|
+
Pass JavaScript \`async () => { ... }\`, no arguments. Do not shadow \`connecta\`.
|
|
26
|
+
No imports, require, filesystem, fetch, or timers.
|
|
29
27
|
|
|
30
|
-
\`connecta.call(address, args, { timeoutMs })\`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
Use \`connecta.call(address, args, { timeoutMs })\` or
|
|
29
|
+
\`connecta.call({ address, args, timeoutMs })\`; both return \`{ data, format }\`.
|
|
30
|
+
\`format: "json"\` carries JSON; \`format: "text"\` carries text. Parse text only
|
|
31
|
+
when documented as JSON. Verify collection roots.
|
|
34
32
|
|
|
35
33
|
\`connecta.read("resource://id/"+encodeURIComponent(uri))\` returns \`{ contents }\` for advertised URIs.
|
|
36
34
|
|
|
37
|
-
Search
|
|
35
|
+
Search/describe return \`{ tools }\`, never an array. Search also returns
|
|
38
36
|
\`catalogErrors\`, pagination, and possible \`absence\`; check failures before
|
|
39
37
|
selecting a match. JSON schemas are the program default. Compact schemas have
|
|
40
38
|
\`schemaFormat: "text"\`; do not read their .properties. Select the exact tool and
|
|
41
39
|
account/environment, inspect its schema, and supply its required keys and types.
|
|
42
40
|
|
|
43
|
-
Discover an unfamiliar
|
|
41
|
+
Discover an unfamiliar read and reduce its result:
|
|
44
42
|
|
|
45
43
|
\`\`\`js
|
|
46
44
|
async () => {
|
|
47
|
-
const page = await connecta.search({ connector: "ci", query: "get_run"
|
|
48
|
-
if (page.catalogErrors.length || page.absence) return
|
|
45
|
+
const page = await connecta.search({ connector: "ci", query: "get_run" });
|
|
46
|
+
if (page.catalogErrors.length || page.absence) return page;
|
|
49
47
|
const tool = page.tools.find(t => t.name === "get_run");
|
|
50
48
|
if (!tool) return { gap: "get_run unavailable" };
|
|
51
|
-
const { tools } = await connecta.describe({ address: tool.address });
|
|
52
|
-
if (tools[0].error) return { gap: tools[0].error };
|
|
53
49
|
const result = await connecta.call(tool.address, { runId: 42 });
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
return result.format === "json"
|
|
51
|
+
? { status: result.data.status, jobId: result.data.failedJobId } : result;
|
|
56
52
|
}
|
|
57
53
|
\`\`\`
|
|
58
54
|
|
|
59
55
|
## Keep partial successes
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
before repeating it across records. Every run reports hostCalls counts separately.
|
|
57
|
+
Use Promise.allSettled to keep independent successes. Rejections retain \`code\`,
|
|
58
|
+
\`message\`, \`retryable\`, and \`details\`. Branch on fields, not prose. Repair
|
|
59
|
+
arguments before batching. Runs report hostCalls separately.
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
Read JSON and text after verifying addresses and schemas:
|
|
67
62
|
|
|
68
63
|
\`\`\`js
|
|
69
64
|
async () => {
|
|
@@ -83,7 +78,7 @@ async () => {
|
|
|
83
78
|
Host-call budget exhaustion ends the run with budget_exceeded. Catch and
|
|
84
79
|
allSettled cannot recover it or return partial output. Bound batches before
|
|
85
80
|
starting them; search, describe, call, result, and skill spend host calls.
|
|
86
|
-
Split remaining reads
|
|
81
|
+
Split remaining reads. Never replay writes for their output.
|
|
87
82
|
|
|
88
83
|
## Per-connector guides
|
|
89
84
|
|
|
@@ -91,14 +86,21 @@ Fetch a listed guide with skills by its exact name, or \`connecta.skill(name)\`
|
|
|
91
86
|
inside a program; it returns \`{ name, text, format: "text" }\`.
|
|
92
87
|
\`guideRequired: true\` is a hard stop. connector_required and approval_required
|
|
93
88
|
remain after schema expansion; schema_truncated clears after exact describe.
|
|
94
|
-
Otherwise fetch for
|
|
89
|
+
Otherwise fetch for units, pagination, aliases, or sequences.
|
|
95
90
|
|
|
96
91
|
## Large results and recovery
|
|
97
92
|
|
|
98
|
-
|
|
93
|
+
Large direct results and program returns carry resultId when paging is available. Follow
|
|
99
94
|
nextOffset in UTF-8 bytes, reassemble and reduce inside one program. Use the
|
|
100
95
|
returned id in place of result-id below; this example assumes documented JSON.
|
|
101
96
|
|
|
97
|
+
Calls reconstruct up to 1 MiB through stash pages. Larger values return
|
|
98
|
+
\`{ format: "paged", valueFormat, resultId?, totalBytes, hint }\`. Pass the handle
|
|
99
|
+
to \`connecta.result\`. No resultId means no paging. IDs expire after
|
|
100
|
+
15 minutes; only explicit paging spends extra host calls.
|
|
101
|
+
Use zero-based \`{ page: 0 }\` or UTF-8 \`{ offset }\`, never both. Keep maxBytes
|
|
102
|
+
fixed across pages; connector caps and the 32 KiB bridge ceiling apply.
|
|
103
|
+
|
|
102
104
|
\`\`\`js
|
|
103
105
|
async () => {
|
|
104
106
|
let text = "", offset = 0, page;
|
|
@@ -111,20 +113,24 @@ async () => {
|
|
|
111
113
|
}
|
|
112
114
|
\`\`\`
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
Oversized returns lead with nextAction. Never rerun writes for output. write_outcome_unknown forbids retry, even if caught.
|
|
117
|
+
Recovery args may be partial (argsRedacted) or absent (argsOmitted). Reconcile
|
|
118
|
+
before retrying with the original args or repeating a program that sent writes.
|
|
119
|
+
Non-retryable errors need repair.
|
|
120
|
+
host_auth_required needs host connection repair; provider_permission_denied needs
|
|
121
|
+
resource-owner access. Auth failures carry the handoff in error.data. When an auth
|
|
122
|
+
error includes a handoff, give it to the user or operator.
|
|
121
123
|
|
|
122
124
|
## Output
|
|
123
125
|
|
|
124
|
-
|
|
125
|
-
or
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
Use \`connecta.emit(await connecta.call(address, args))\` for MCP rich output,
|
|
127
|
+
or \`for (const b of x.content ?? []) connecta.emit(b)\`. Both forward originals,
|
|
128
|
+
including resources and metadata; edits do not change them. Page oversized
|
|
129
|
+
native content with \`connecta.result(x.contentResult)\`.
|
|
130
|
+
Authored blocks accept text or base64 image/audio; images require canonical
|
|
131
|
+
base64 and PNG/JPEG/GIF/WebP MIME. Emit works without await, delivers on success,
|
|
132
|
+
and obeys byte/block budgets; text shares the return cap. Return reduced JSON.
|
|
133
|
+
|
|
128
134
|
`;
|
|
129
135
|
/** Kept in sync with GuestApi by the declaration test. These are type notation, not runnable JS. */
|
|
130
136
|
export const GUEST_API_DECLARATION = `search(args?: CatalogSearchArgs): Promise<CatalogSearchResult>;
|
|
@@ -132,6 +138,9 @@ describe(args?: CatalogDescribeArgs): Promise<{ tools: CatalogDescription[] }>;
|
|
|
132
138
|
call(address: string, args?: unknown, options?: { timeoutMs?: number }): Promise<GuestResult>;
|
|
133
139
|
call(request: { address: string; args?: unknown; timeoutMs?: number }): Promise<GuestResult>;
|
|
134
140
|
read(uri: string): Promise<GuestResourceResult>;
|
|
135
|
-
result(
|
|
141
|
+
result(
|
|
142
|
+
id: string | GuestResultHandle,
|
|
143
|
+
options?: { page?: number; offset?: number; maxBytes?: number },
|
|
144
|
+
): Promise<GuestResultPage>;
|
|
136
145
|
skill(name: string): Promise<{ name: string; text: string; format: "text" }>;
|
|
137
|
-
emit(block:
|
|
146
|
+
emit(block: GuestNativeBlock | GuestResult): PromiseLike<void>;`;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTA_VERSION = "0.
|
|
1
|
+
export declare const CONNECTA_VERSION = "0.30.0";
|
package/dist/version.js
CHANGED
package/dist/worker.js
CHANGED
|
@@ -160,7 +160,8 @@ export function workerExecutor(options) {
|
|
|
160
160
|
defineProperties(error, {
|
|
161
161
|
code: { value: reply.error.code, enumerable: true },
|
|
162
162
|
retryable: { value: reply.error.retryable, enumerable: true },
|
|
163
|
-
details: { value: freeze(clone(reply.error.details)), enumerable: true }
|
|
163
|
+
details: { value: freeze(clone(reply.error.details)), enumerable: true },
|
|
164
|
+
data: { value: freeze(clone(reply.error.details)), enumerable: true }
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
167
|
throw error;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Agent documentation
|
|
2
|
+
|
|
3
|
+
Choose a route by the work you are doing. Each route identifies the task guide
|
|
4
|
+
and contracts needed to operate an endpoint, set up a deployment, connect a
|
|
5
|
+
service, or change Connecta itself.
|
|
6
|
+
|
|
7
|
+
| Your task | Start here | Read next when needed |
|
|
8
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
9
|
+
| Use an existing endpoint to answer a question or perform a task | [Operating an endpoint](./operating.md) | [Meta-tools](./meta-tools.md), [guest API](./code-mode.md) |
|
|
10
|
+
| Set up, configure, upgrade, or diagnose a deployment | [Deploying Connecta](./deploying.md) | [Auth](./auth.md), [operator UI](./operator-ui.md) |
|
|
11
|
+
| Add a maintained integration, remote MCP, or HTTP API to a deployment | [Integrating services](./integrating.md) | Provider skill, [classification](./architecture.md#providers-and-reviewed-classification), [downstream auth](./auth.md#shared-and-personal-auth) |
|
|
12
|
+
| Change Connecta's source, tests, or published contracts | [AGENTS.md](https://github.com/zackbart/connecta/blob/main/AGENTS.md) | [Principles](https://github.com/zackbart/connecta/blob/main/PRINCIPLES.md), [architecture](./architecture.md), then the affected contract |
|
|
13
|
+
|
|
14
|
+
## Contract reference
|
|
15
|
+
|
|
16
|
+
| Guide | Owns |
|
|
17
|
+
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| [Meta-tools](./meta-tools.md) | Six top-level tools, discovery, direct-call results, paging, skills, and recovery |
|
|
19
|
+
| [Code mode](./code-mode.md) | JavaScript guest API, executor requirements, output, retries, cancellation, limits, and writes |
|
|
20
|
+
| [Auth](./auth.md) | Inbound identity, grants, pools, machine tokens, credential ownership, and downstream OAuth |
|
|
21
|
+
| [Architecture](./architecture.md) | Request lifetimes, routes, storage, configuration, import boundaries, and Effect implementation |
|
|
22
|
+
| [Operator UI](./operator-ui.md) | Optional UI setup, scoped configuration and live-data contract, activity disclosure, and UI implementation |
|
|
23
|
+
| [Operator UI tests](./operator-tests.md) | Browser state matrix, real-server flows, and snapshot maintenance |
|
|
24
|
+
| [Provider migration for 0.29](./provider-migration-0.29.md) | Upgrade actions for Notion, Vercel, and Cloudflare; provider reconciliation tables supply exact name mappings |
|
|
25
|
+
|
|
26
|
+
Each long contract starts with task routes. Follow those section links before
|
|
27
|
+
reading the whole guide. Source and test pointers identify where to check a
|
|
28
|
+
claim or implement a change.
|
|
29
|
+
|
|
30
|
+
## Documentation ownership
|
|
31
|
+
|
|
32
|
+
The root [README](../README.md) explains the product for people. It owns what
|
|
33
|
+
Connecta offers, how the pieces connect, deployment choices, and the setup
|
|
34
|
+
handoff. Keep JavaScript, imports, configuration, migrations, and internal
|
|
35
|
+
module maps here in agent documentation.
|
|
36
|
+
|
|
37
|
+
[AGENTS.md](https://github.com/zackbart/connecta/blob/main/AGENTS.md) is the
|
|
38
|
+
canonical repository policy, with `CLAUDE.md` as its symlink. Do not copy its
|
|
39
|
+
verification, review, or release rules into another policy document.
|
|
40
|
+
|
|
41
|
+
Deployment-local READMEs and agent instructions own runnable platform setup:
|
|
42
|
+
[Node and Docker](../templates/node/README.md), [Workers](../examples/worker/README.md).
|
|
43
|
+
Keep those aligned with their configuration and with this task router.
|
|
44
|
+
Repository [skills](https://github.com/zackbart/connecta/tree/main/.claude/skills)
|
|
45
|
+
own scoped change procedures. Each [provider folder](https://github.com/zackbart/connecta/tree/main/src/providers)
|
|
46
|
+
owns its maintained `SKILL.md`, options, tests, fixtures, and review data.
|
|
47
|
+
Use those details instead of copying provider catalogs into general guides.
|
|
48
|
+
The provider list in the human README is generated; preserve its marker pair
|
|
49
|
+
and run `npm run check:providers-generated` after edits.
|
|
50
|
+
|
|
51
|
+
When changing behavior, update its owning contract and task guide in the same
|
|
52
|
+
change. Check examples against current source and point to the tests that
|
|
53
|
+
defend the contract. Published Markdown links to repo-only files must use
|
|
54
|
+
canonical `https://github.com/zackbart/connecta/blob/main/…` or `tree/main/…`
|
|
55
|
+
links. Keep `documentation/` flat, with Markdown files only.
|
|
56
|
+
|
|
57
|
+
The [changelog](../CHANGELOG.md) records shipped releases.
|
|
58
|
+
[Decisions](https://github.com/zackbart/connecta/tree/main/decisions) explain
|
|
59
|
+
history and may be superseded. [Spec coverage](https://github.com/zackbart/connecta/blob/main/spec/coverage.json)
|
|
60
|
+
records implemented support, gaps, and test evidence. The
|
|
61
|
+
[0.29 plan of record](https://github.com/zackbart/connecta/issues/703) and other
|
|
62
|
+
issues describe planned work; do not turn their goals into claims of shipped
|
|
63
|
+
behavior.
|