@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +123 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/catalog-cache.js +2 -2
  9. package/dist/catalog-service.js +103 -63
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +12 -7
  12. package/dist/connectors/guarded-fetch.js +3 -3
  13. package/dist/connectors/negotiation-cache.js +1 -1
  14. package/dist/downstream-input-context.js +9 -0
  15. package/dist/downstream-input.js +13 -11
  16. package/dist/errors.d.ts +36 -12
  17. package/dist/errors.js +19 -9
  18. package/dist/execute.js +375 -43
  19. package/dist/executor-result.js +11 -9
  20. package/dist/executors/quickjs-child.js +12 -5
  21. package/dist/executors/quickjs-protocol.js +5 -6
  22. package/dist/executors/quickjs-runtime.js +6 -1
  23. package/dist/executors/quickjs.js +20 -4
  24. package/dist/guest-runtime.js +52 -2
  25. package/dist/guest-types.d.ts +47 -5
  26. package/dist/index.d.ts +1 -1
  27. package/dist/invocation.js +40 -14
  28. package/dist/meta-output.js +0 -15
  29. package/dist/meta-tools.js +166 -95
  30. package/dist/oauth-handoff.js +88 -11
  31. package/dist/program-result.js +5 -0
  32. package/dist/program-writes.js +5 -1
  33. package/dist/provider.d.ts +1 -1
  34. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  35. package/dist/providers/_shared/request-token.js +83 -0
  36. package/dist/providers/infisical/index.d.ts +7 -0
  37. package/dist/providers/infisical/index.js +713 -0
  38. package/dist/providers/infisical/skill.generated.js +8 -0
  39. package/dist/registry.d.ts +8 -6
  40. package/dist/registry.js +134 -61
  41. package/dist/resource-read.js +4 -1
  42. package/dist/routes/mcp.js +11 -2
  43. package/dist/sent-secrets.d.ts +54 -13
  44. package/dist/sent-secrets.js +770 -231
  45. package/dist/skills.js +26 -16
  46. package/dist/sqlite.js +21 -6
  47. package/dist/storage/keys.js +20 -5
  48. package/dist/storage/memory.js +9 -4
  49. package/dist/storage/sql.js +18 -9
  50. package/dist/types.d.ts +17 -3
  51. package/dist/usage-guide.js +49 -40
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/dist/worker.js +2 -1
  55. package/documentation/README.md +63 -0
  56. package/documentation/architecture.md +121 -89
  57. package/documentation/auth.md +138 -5
  58. package/documentation/code-mode.md +224 -107
  59. package/documentation/deploying.md +146 -0
  60. package/documentation/integrating.md +159 -0
  61. package/documentation/meta-tools.md +135 -30
  62. package/documentation/operating.md +135 -0
  63. package/documentation/operator-tests.md +4 -0
  64. package/documentation/operator-ui.md +132 -0
  65. package/documentation/provider-migration-0.29.md +5 -0
  66. package/examples/worker/README.md +7 -0
  67. package/package.json +7 -1
  68. package/templates/node/README.md +7 -0
  69. 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 = 'After auth_required use authorize_connector. After a truncated direct result use connecta.result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
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 localRecord(name, `${root}/SKILL.md`, description, content, [
352
- connectorSkillName(connector.id),
353
- `skill://connecta/connectors/${encodeURIComponent(connector.id)}`,
354
- root,
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 available = [
587
- "usage",
588
- ...this.registry
589
- .listConnectors()
590
- .filter((connector) => connectorGuide(connector) !== undefined)
591
- .map((connector) => connectorSkillName(connector.id)),
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
- return { ...PRIVATE, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text: record.content }] };
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
- return { ...PRIVATE, contents: [{ ...content, uri }] };
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
- const db = typeof database === "string" ? openSqlite(database) : database;
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 (error) {
162
- db.exec("ROLLBACK");
163
- throw error;
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
  }
@@ -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 partition
79
- * key, byte charge, and expiry. One record, swapped by compare-and-set, so the
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
- // Durable as a record, self-pruning by content: an entry leaves the
90
- // ledger once its result's TTL has passed.
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,
@@ -13,11 +13,15 @@ export function memoryStorage() {
13
13
  }
14
14
  return e;
15
15
  };
16
- const write = (key, value, ttlSeconds) => {
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?.ttlSeconds);
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?.ttlSeconds);
76
+ write(key, next, opts);
72
77
  return true;
73
78
  },
74
79
  };
@@ -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 expiryValue = `(${now} + ?)`;
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 = ttlMillis(opts?.ttlSeconds);
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 (?, ?, ${expiryValue})
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 = ttlMillis(opts?.ttlSeconds);
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 (?, ?, ${expiryValue})
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 = ${expiryValue}
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, not each caller's 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` is stored with `options.ttlSeconds`, or with no
28
- * expiry when that is omitted, exactly as `set` would. On `false` nothing
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
@@ -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
- For example, read account acct_42 with call_tool arguments
18
- \`{ "address": "crm.get_account", "args": { "id": "acct_42" } }\`.
19
- Discover and call in the same program when schemas suffice. Top-level search_tools
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 one plain JavaScript async arrow, \`async () => { ... }\`. It receives no
27
- arguments. Use the supplied \`connecta\` global; do not shadow it with a parameter.
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 })\` is positional. The object overload
31
- is \`connecta.call({ address, args, timeoutMs })\`. Both return \`{ data, format }\`.
32
- For \`format: "json"\`, data is the JSON value. For \`format: "text"\`, data is a
33
- string: parse only if the provider documents JSON text. Never guess collection roots.
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 and describe return \`{ tools }\`, never an array. Search also returns
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 run lookup, then reduce its JSON result:
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", safety: "readOnly" });
48
- if (page.catalogErrors.length || page.absence) return { gap: page.catalogErrors, absence: page.absence };
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
- if (result.format !== "json") return { gap: "Expected JSON" };
55
- return { status: result.data.status, jobId: result.data.failedJobId };
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
- Await Promise.allSettled for independent calls when one failure must not discard
62
- other answers. Each rejected reason keeps \`code\`, \`message\`, \`retryable\`, and
63
- \`details\`. Branch on fields, never error prose. Repair one argument failure
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
- After verifying these addresses and schemas, read JSON and plain text together:
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 into another run. Never replay writes for their output.
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 relevant units, pagination, aliases, or sequences.
89
+ Otherwise fetch for units, pagination, aliases, or sequences.
95
90
 
96
91
  ## Large results and recovery
97
92
 
98
- A truncated direct result carries a resultId when paging is available. Follow
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
- Program returns have no page handle. Reduce reads before returning. After a
115
- write, use its existing result, check the target, or report the gap.
116
- write_outcome_unknown means a write was sent without a known answer: check the
117
- target, never resend automatically. Non-retryable errors require repair.
118
- Follow typed auth recovery: host_auth_required needs host connection repair;
119
- downstream_oauth_required or auth_required carries an authorize_connector
120
- handoff; provider_permission_denied needs the resource owner to grant access.
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
- Return reduced JSON. console.log is captured. connecta.emit accepts text, image,
125
- or audio blocks, works without await, and delivers only after success. Images
126
- need canonical base64 and image/png, image/jpeg, image/gif, or image/webp.
127
- Text shares the result cap; the description states block and byte budgets.
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(id: string, options?: { offset?: number; maxBytes?: number }): Promise<GuestResultPage>;
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: GuestBlock): PromiseLike<void>;`;
146
+ emit(block: GuestNativeBlock | GuestResult): PromiseLike<void>;`;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CONNECTA_VERSION = "0.29.0";
1
+ export declare const CONNECTA_VERSION = "0.30.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.29.0";
3
+ export const CONNECTA_VERSION = "0.30.0";
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.