@zackbart/connecta 0.40.0 → 0.42.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 (195) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. package/templates/node/package.json +1 -1
@@ -1,15 +1,43 @@
1
+ import { Effect, Option } from "effect";
2
+ import { RequestScope } from "../runtime/request-scope.js";
3
+ import { hasTimingTraces, traceFor } from "../timing-trace.js";
4
+ import { utf8Length } from "../bytes.js";
5
+ // Request-wide totals include admission, credentials and catalog work. These
6
+ // I/O-spanning observations are not pure storage latency or CPU attribution.
7
+ function timedD1(family, statements, operation) {
8
+ return Effect.suspend(() => {
9
+ if (!hasTimingTraces())
10
+ return operation;
11
+ return Effect.flatMap(Effect.serviceOption(RequestScope), (owner) => {
12
+ const trace = Option.isSome(owner) ? traceFor(owner.value.identity) : undefined;
13
+ if (!trace)
14
+ return operation;
15
+ const start = performance.now();
16
+ let bytes = 0;
17
+ return operation.pipe(Effect.tap((value) => Effect.sync(() => {
18
+ // Count serialized application result bytes, not D1 wire bytes.
19
+ try {
20
+ bytes = utf8Length(JSON.stringify(value) ?? "");
21
+ }
22
+ catch {
23
+ /* no payload retained */
24
+ }
25
+ })), Effect.ensuring(Effect.sync(() => trace.record(family, performance.now() - start, bytes, statements))));
26
+ });
27
+ });
28
+ }
1
29
  import { effectSqlDriver, sqlDriverEdge, sqlPromise } from "./sql-driver.js";
2
30
  export function d1Driver(db) {
3
31
  const prepare = (statement) => statement.params.length > 0 ? db.prepare(statement.sql).bind(...statement.params) : db.prepare(statement.sql);
4
32
  return sqlDriverEdge(effectSqlDriver({
5
- all: (statement) => sqlPromise(() => prepare(statement)
33
+ all: (statement) => timedD1("d1.read", 1, sqlPromise(() => prepare(statement)
6
34
  .all()
7
- .then((result) => result.results)),
8
- run: (statement) => sqlPromise(() => prepare(statement)
35
+ .then((result) => result.results))),
36
+ run: (statement) => timedD1("d1.write", 1, sqlPromise(() => prepare(statement)
9
37
  .run()
10
- .then((result) => result.meta.changes ?? 0)),
38
+ .then((result) => result.meta.changes ?? 0))),
11
39
  // D1's public binding API preserves per-statement changes, including zero;
12
40
  // execute the whole array once. No withTransaction, RETURNING, or replay.
13
- batch: (statements) => sqlPromise(() => db.batch(statements.map(prepare)).then((results) => results.map((result) => result.meta.changes ?? 0))),
41
+ batch: (statements) => timedD1("d1.batch", statements.length, sqlPromise(() => db.batch(statements.map(prepare)).then((results) => results.map((result) => result.meta.changes ?? 0)))),
14
42
  }));
15
43
  }
@@ -58,7 +58,8 @@ export const scopes = {
58
58
  export const RESULT_TTL_SECONDS = 900;
59
59
  /**
60
60
  * A stashed result: chunk 0 at `result:<id>`, chunk n at `result:<id>#<n>`.
61
- * The value is the paging envelope `src/meta-tools.ts` writes and reads.
61
+ * The value is the paging envelope `src/meta-tools.ts` writes and reads, or at
62
+ * chunk 0 the refusal `src/result-seal.ts` writes for a result it did not publish.
62
63
  */
63
64
  export const resultKeys = {
64
65
  family: {
@@ -176,6 +177,8 @@ export const connectorHealthKeys = {
176
177
  discovery: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:discovery`),
177
178
  status: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:status`),
178
179
  };
180
+ const responseCacheLocatorPrefix = (id, config) => validateStorageKey(`response-cache:v1:${id}:${config}:`);
181
+ const responseCacheLocatorSeparator = ":";
179
182
  /**
180
183
  * SDK catalog response cache, complete manifests published after their chunks.
181
184
  * Secret-bearing inputs reach these keys only through `secretDigest`.
@@ -189,13 +192,17 @@ export const responseCacheKeys = {
189
192
  codec: textCodec,
190
193
  ttl: {
191
194
  kind: "configured",
192
- by: "bounded downstream ttlMs; generations and hash refresh baselines expire after 48 hours",
195
+ by: "bounded downstream ttlMs; generations, refusal fences and hash refresh baselines expire after 48 hours",
193
196
  },
194
197
  durable: false,
195
198
  },
196
199
  prefix: (id) => validateStorageKey(`response-cache:v1:${id}:`),
197
200
  generation: (id) => validateStorageKey(`response-cache:v1:${id}:generation`),
198
- namespace: (id, config, generation) => validateStorageKey(`response-cache:v1:${id}:${config}:${generation}:`),
201
+ namespace: (id, config, generation) => validateStorageKey(`${responseCacheLocatorPrefix(id, config)}${generation}${responseCacheLocatorSeparator}`),
202
+ /** SQL binds these layout parts around the stored generation; it owns no delimiters. */
203
+ locatorPrefix: responseCacheLocatorPrefix,
204
+ locatorSeparator: responseCacheLocatorSeparator,
205
+ locatedEntry: (prefix, generation, partition) => validateStorageKey(`${prefix}${generation}${responseCacheLocatorSeparator}${partition}`),
199
206
  entry: (namespace, partition) => validateStorageKey(`${namespace}${partition}`),
200
207
  refreshDigest: (id, config, partition) => validateStorageKey(`response-cache:v1:${id}:refresh-digest:${config}:${partition}`),
201
208
  chunk: (entry, revision, index) => validateStorageKey(`${entry}:chunk:${revision}:${index}`),
@@ -350,19 +357,21 @@ export const oauthRefreshActiveKeys = {
350
357
  prefix: "oauth:refresh-active:",
351
358
  holder: (epoch, holder) => validateStorageKey(`oauth:refresh-active:${epoch}:${holder}`),
352
359
  };
353
- /** Single-use browser links (`used`/`started`/`failed`/`closed`) and MRTR rounds. */
360
+ /** Single-use browser links and forced resets (`used`/`started`/`failed`), and MRTR rounds. */
354
361
  export const oauthConnectKeys = {
355
362
  family: {
356
363
  name: "oauth-connect",
357
364
  scope: "connector",
358
- prefixes: ["oauth:connect-used:", "oauth:request-used:"],
365
+ prefixes: ["oauth:connect-used:", "oauth:connect-reset:", "oauth:request-used:"],
359
366
  version: { number: 1, in: "untagged" },
360
367
  codec: textCodec,
361
- // The link's remaining lifetime, or fifteen minutes when retiring it.
362
- ttl: { kind: "fixed", seconds: 15 * 60 },
368
+ // A link's remaining lifetime, fifteen minutes for a round, or thirty for a
369
+ // reset, which must outlive every link its flow can issue.
370
+ ttl: { kind: "fixed", seconds: 30 * 60 },
363
371
  durable: false,
364
372
  },
365
373
  used: (nonce) => validateStorageKey(`oauth:connect-used:${nonce}`),
374
+ reset: (flow) => validateStorageKey(`oauth:connect-reset:${flow}`),
366
375
  retry: (nonce) => validateStorageKey(`oauth:request-used:${nonce}`),
367
376
  };
368
377
  /**
@@ -27,7 +27,7 @@ import { Effect } from "effect";
27
27
  import { activityBehaviorFacts, activityPackageVersion, activityClientFact, InvalidActivityCursorError, } from "../activity.js";
28
28
  import { assertKnownOptions, ConfigError, keys, optionsOf } from "../config-schema.js";
29
29
  import { agentFrictionForCode } from "../activity-friction.js";
30
- import { accessTokenKeys, validateStorageKey } from "./keys.js";
30
+ import { accessTokenKeys, responseCacheKeys, validateStorageKey } from "./keys.js";
31
31
  const sql = (text, ...params) => ({
32
32
  sql: text,
33
33
  params,
@@ -132,6 +132,34 @@ export function nativeSqlStorage(driver) {
132
132
  return new Map(rows.map((row) => [row.key, textOf(row.value, row.value_bytes, "Stored access token metadata")]));
133
133
  });
134
134
  },
135
+ catalogSnapshot(keys, generationKey, prefix, suffixes) {
136
+ return Effect.gen(function* () {
137
+ if (keys.length < 1 || keys.length > 4 || suffixes.length > 3)
138
+ throw new RangeError("Catalog locator exceeds bounds");
139
+ for (const key of [...keys, generationKey, prefix, ...suffixes])
140
+ validateStorageKey(key);
141
+ yield* ensure();
142
+ const rows = yield* driver.all(sql(`WITH initial AS (
143
+ SELECT key, value FROM connecta_kv WHERE key IN (${keys.map(() => "?").join(",")}) AND ${live}
144
+ ), suffixes(suffix) AS (VALUES ${suffixes.map(() => "(?)").join(",") || "(NULL)"}), wanted(key) AS (
145
+ SELECT key FROM initial UNION ALL
146
+ SELECT ? || (SELECT value FROM initial WHERE key = ?) || ? || suffix FROM suffixes
147
+ ) SELECT key, ${textColumn("value")} FROM connecta_kv
148
+ WHERE key IN (SELECT key FROM wanted) AND ${live}`, ...keys, ...suffixes, prefix, generationKey, responseCacheKeys.locatorSeparator));
149
+ const values = new Map(rows.map((row) => [row.key, textOf(row.value, row.value_bytes, "Stored catalog locator")]));
150
+ for (const key of keys)
151
+ if (!values.has(key))
152
+ values.set(key, null);
153
+ const generation = values.get(generationKey);
154
+ if (generation)
155
+ for (const suffix of suffixes) {
156
+ const key = responseCacheKeys.locatedEntry(prefix, generation, suffix);
157
+ if (!values.has(key))
158
+ values.set(key, null);
159
+ }
160
+ return values;
161
+ });
162
+ },
135
163
  getMany(keys) {
136
164
  return Effect.gen(function* () {
137
165
  for (const key of keys)
@@ -157,6 +185,25 @@ export function nativeSqlStorage(driver) {
157
185
  return values;
158
186
  });
159
187
  },
188
+ setMany(entries, opts) {
189
+ return Effect.gen(function* () {
190
+ if (entries.length > 64)
191
+ throw new RangeError("setMany accepts at most 64 rows");
192
+ for (const [key] of entries)
193
+ validateStorageKey(key);
194
+ const expiry = expiryOptions(opts);
195
+ if (!entries.length)
196
+ return;
197
+ yield* ensure();
198
+ const changes = yield* driver.batch([
199
+ expirySweep(),
200
+ ...entries.map(([key, value]) => sql(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ${expiry.expression})
201
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at_ms = excluded.expires_at_ms`, key, value, expiry.value)),
202
+ ]);
203
+ if (changes.length !== entries.length + 1 || changes.slice(1).some((changed) => changed !== 1))
204
+ throw new Error("SQL multi-set confirmation is incomplete");
205
+ });
206
+ },
160
207
  set(key, value, opts) {
161
208
  return Effect.gen(function* () {
162
209
  validateStorageKey(key);
@@ -334,11 +381,20 @@ function activityRow(read) {
334
381
  }
335
382
  return row;
336
383
  }
384
+ // Operation metadata shares the existing kind-tagged text slot; no new columns.
385
+ function parseOperation(raw) {
386
+ try {
387
+ return raw === null ? undefined : JSON.parse(raw);
388
+ }
389
+ catch {
390
+ return undefined;
391
+ }
392
+ }
337
393
  function rowToEvent(row) {
338
394
  // Rows written before `friction` had a column derive it from the code.
339
395
  // Friction without a code — an oversized but successful result — exists
340
396
  // only in the column, which is why the column exists.
341
- const friction = row.friction ?? agentFrictionForCode(row.error_code ?? undefined);
397
+ const friction = row.event_kind === "operation" ? undefined : (row.friction ?? agentFrictionForCode(row.error_code ?? undefined));
342
398
  const packageVersion = activityPackageVersion(row.package_version);
343
399
  const clientName = activityClientFact(row.client_name, "name");
344
400
  const clientVersion = activityClientFact(row.client_version, "version");
@@ -351,6 +407,7 @@ function rowToEvent(row) {
351
407
  classification: row.classification,
352
408
  resultBytes: row.result_bytes,
353
409
  kind: row.event_kind,
410
+ operation: row.event_kind === "operation" ? parseOperation(row.drift_kind) : undefined,
354
411
  pool: row.pool_name,
355
412
  actorBasis: row.actor_basis,
356
413
  drift: {
@@ -461,7 +518,7 @@ export function nativeSqlActivityStore(driver, kind, options = {}) {
461
518
  // Stored beside the code rather than derived from it: a truncated
462
519
  // result is friction on a call that succeeded, and
463
520
  // `error_code IS NOT NULL` stays an honest count of failures.
464
- event.friction ?? null, event.approval ?? null, activityPackageVersion(event.packageVersion) ?? null, event.serverName, event.serverVersion, activityClientFact(event.clientName, "name") ?? null, activityClientFact(event.clientVersion, "version") ?? null, event.deploymentId ?? null, facts.classification ?? null, facts.resultBytes ?? null, facts.kind ?? null, facts.drift?.kind ?? null, facts.drift?.addedTools ?? null, facts.drift?.removedTools ?? null, facts.drift?.changedTools ?? null, facts.pool ?? null, facts.actorBasis ?? null),
521
+ event.kind === "operation" ? null : (event.friction ?? null), event.approval ?? null, activityPackageVersion(event.packageVersion) ?? null, event.serverName, event.serverVersion, activityClientFact(event.clientName, "name") ?? null, activityClientFact(event.clientVersion, "version") ?? null, event.deploymentId ?? null, facts.classification ?? null, facts.resultBytes ?? null, facts.kind ?? null, facts.kind === "operation" ? JSON.stringify(facts.operation ?? null) : (facts.drift?.kind ?? null), facts.drift?.addedTools ?? null, facts.drift?.removedTools ?? null, facts.drift?.changedTools ?? null, facts.pool ?? null, facts.actorBasis ?? null),
465
522
  sql(`DELETE FROM tool_call_activity WHERE id IN (
466
523
  SELECT id FROM tool_call_activity
467
524
  WHERE occurred_at_ms < ?
@@ -10,6 +10,12 @@ export function sqlStorage(driver, kind) {
10
10
  const root = {};
11
11
  const native = {
12
12
  ...store,
13
+ setMany: (entries, options) => {
14
+ let write = store.setMany(entries, options);
15
+ for (const [key] of entries)
16
+ write = observedWrite(root, key, write);
17
+ return write;
18
+ },
13
19
  set: (key, value, options) => observedWrite(root, key, store.set(key, value, options)),
14
20
  delete: (key) => observedWrite(root, key, store.delete(key)),
15
21
  compareAndSet: (key, expected, next, options) => observedWrite(root, key, store.compareAndSet(key, expected, next, options)),
@@ -0,0 +1,84 @@
1
+ const families = new Set([
2
+ "catalog.cache.lookup",
3
+ "catalog.cache.hit",
4
+ "catalog.cache.expired",
5
+ "catalog.chunks",
6
+ "catalog.validated.hit",
7
+ "catalog.validated.miss",
8
+ "catalog.intake",
9
+ "catalog.decode",
10
+ "catalog.validate",
11
+ "catalog.accepted",
12
+ "catalog",
13
+ "admission",
14
+ "connector",
15
+ "result",
16
+ "d1.read",
17
+ "d1.write",
18
+ "d1.batch",
19
+ "memo.hit",
20
+ "memo.miss",
21
+ "memo.fresh",
22
+ ]);
23
+ const traces = new WeakMap();
24
+ let active = 0;
25
+ let firstRequest = true;
26
+ export const hasTimingTraces = () => active > 0;
27
+ export const traceFor = (identity) => identity ? traces.get(identity) : undefined;
28
+ const finite = (value) => (Number.isFinite(value) && value >= 0 ? value : 0);
29
+ export class TimingTrace {
30
+ first;
31
+ started = performance.now();
32
+ totals = new Map();
33
+ closed = false;
34
+ constructor(first) {
35
+ this.first = first;
36
+ }
37
+ record(family, durationMs = 0, bytes = 0, statements = 0, absent = 0, items = 0) {
38
+ if (this.closed || !families.has(family))
39
+ return;
40
+ const total = this.totals.get(family) ?? { count: 0, durationMs: 0, bytes: 0, statements: 0, absent: 0, items: 0 };
41
+ total.count++;
42
+ total.durationMs += finite(durationMs);
43
+ total.bytes += finite(bytes);
44
+ total.statements += finite(statements);
45
+ total.absent += finite(absent);
46
+ total.items += finite(items);
47
+ this.totals.set(family, total);
48
+ }
49
+ finish(logger) {
50
+ if (this.closed)
51
+ return;
52
+ this.closed = true;
53
+ try {
54
+ logger.info("connecta.timing", {
55
+ version: 1,
56
+ isolateFirstRequest: this.first,
57
+ durationMs: performance.now() - this.started,
58
+ weakReferences: typeof WeakRef === "function" && typeof FinalizationRegistry === "function",
59
+ operations: Object.fromEntries(this.totals),
60
+ });
61
+ }
62
+ catch {
63
+ /* Observability cannot change a request's outcome. */
64
+ }
65
+ this.totals.clear();
66
+ }
67
+ }
68
+ /** Called for each MCP request, including those not sampled. */
69
+ export function startTimingTrace(identity, sampleRate, logger) {
70
+ const first = firstRequest;
71
+ firstRequest = false;
72
+ if (sampleRate <= 0 || (sampleRate < 1 && Math.random() >= sampleRate))
73
+ return undefined;
74
+ const trace = new TimingTrace(first);
75
+ traces.set(identity, trace);
76
+ active++;
77
+ return () => {
78
+ if (traces.get(identity) !== trace)
79
+ return;
80
+ traces.delete(identity);
81
+ active--;
82
+ trace.finish(logger);
83
+ };
84
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolDef } from "./types.js";
2
+ /** What deployment code may say about one tool. */
3
+ export type ClassificationOverride = "read" | "read-repeatable" | "write" | "hidden";
4
+ export declare function sourceClassification(tool: ToolDef): "read" | "write";
5
+ /** The source said this tool writes, as opposed to saying nothing. */
6
+ export declare function sourceDeclaresWrite(tool: ToolDef): boolean;
7
+ /**
8
+ * Deployment override, then fail-closed annotations. `"read"` speaks only where
9
+ * the source is silent: a declared write stays a write, because a read annotation
10
+ * lets a host resend the call. `"read-repeatable"` is the deployment accepting
11
+ * that resend for a tool it has checked, so it serves as a read regardless. Either
12
+ * way a source write stays write-like, so Connecta itself never replays it.
13
+ */
14
+ export declare function classifyTool(definition: ToolDef, override?: Exclude<ClassificationOverride, "hidden">): "read" | "write";
15
+ export declare function carryToolClassification(source: ToolDef, served: ToolDef, acceptRepeat?: boolean): ToolDef;
16
+ /** Replay and outcome policy only; admission and presentation use the served class. */
17
+ export declare function isWriteLike(tool: ToolDef): boolean;
18
+ /** The surface that runs a read-only program: it never dispatches a write-like call. */
19
+ export declare const READ_PROGRAM_SOURCE: "execute_read_code";
20
+ /**
21
+ * What a read program refuses: anything write-like, unless the deployment accepted
22
+ * repeats. A plain `"read"` over a source write stays out, because only
23
+ * `"read-repeatable"` says a host's resend of the program is acceptable.
24
+ */
25
+ export declare function readProgramRefuses(tool: ToolDef): boolean;
26
+ export declare function cloneTool(tool: ToolDef): ToolDef;
27
+ /**
28
+ * The only permission decision for a connector call. Approval is client-owned.
29
+ * A read-only endpoint refuses every write on every surface; a writable one
30
+ * sends writes through call_destructive_tool and writing programs, never call_tool
31
+ * or a read program.
32
+ */
33
+ export declare function writeRefusal(classification: ToolDef["classification"], surface: string, readOnly?: boolean): "read_only_endpoint" | "destructive_tool_requires_approval" | undefined;
@@ -1,33 +1,69 @@
1
- /** Deployment override, then fail-closed annotations. */
1
+ function sourceFactsOf(annotations) {
2
+ const readOnly = annotations?.readOnlyHint;
3
+ const destructive = annotations?.destructiveHint;
4
+ if (readOnly === true && destructive !== true)
5
+ return "read";
6
+ // Anything but a clean read or silence counts as declared, including contradictory
7
+ // or malformed hints: an override must not turn an unclear statement into a read.
8
+ return readOnly === undefined && destructive !== true ? "silent" : "declared_write";
9
+ }
10
+ // Source policy is fail-closed annotations, with silence kept apart from a
11
+ // declared write. Deployment overrides never enter it. It is private metadata,
12
+ // not catalog data. Keep it through host-owned clones without exposing it to
13
+ // tools or storage.
14
+ const sourceFacts = new WeakMap();
15
+ const factsFor = (tool) => sourceFacts.get(tool) ?? sourceFactsOf(tool.annotations);
16
+ export function sourceClassification(tool) {
17
+ return factsFor(tool) === "read" ? "read" : "write";
18
+ }
19
+ /** The source said this tool writes, as opposed to saying nothing. */
20
+ export function sourceDeclaresWrite(tool) {
21
+ return factsFor(tool) === "declared_write";
22
+ }
23
+ /**
24
+ * Deployment override, then fail-closed annotations. `"read"` speaks only where
25
+ * the source is silent: a declared write stays a write, because a read annotation
26
+ * lets a host resend the call. `"read-repeatable"` is the deployment accepting
27
+ * that resend for a tool it has checked, so it serves as a read regardless. Either
28
+ * way a source write stays write-like, so Connecta itself never replays it.
29
+ */
2
30
  export function classifyTool(definition, override) {
3
- if (override !== undefined)
31
+ if (override === "read-repeatable")
32
+ return "read";
33
+ if (override === "write" || (override === "read" && !sourceDeclaresWrite(definition)))
4
34
  return override;
5
- return definition.annotations?.readOnlyHint === true && definition.annotations?.destructiveHint !== true
6
- ? "read"
7
- : "write";
8
- }
9
- // Source policy is fail-closed annotations.
10
- // Deployment overrides never enter it. It is private metadata, not catalog data.
11
- // Keep it through host-owned clones without exposing it to tools or storage.
12
- const sourceClassifications = new WeakMap();
13
- export function sourceClassification(tool) {
14
- return sourceClassifications.get(tool) ?? classifyTool(tool);
35
+ return sourceClassification(definition);
15
36
  }
16
- export function carryToolClassification(source, served) {
17
- sourceClassifications.set(served, sourceClassification(source));
37
+ // Tools a deployment set to `"read-repeatable"`, private like the source facts.
38
+ const repeatAccepted = new WeakSet();
39
+ export function carryToolClassification(source, served, acceptRepeat = repeatAccepted.has(source)) {
40
+ sourceFacts.set(served, factsFor(source));
41
+ if (acceptRepeat)
42
+ repeatAccepted.add(served);
18
43
  return served;
19
44
  }
20
45
  /** Replay and outcome policy only; admission and presentation use the served class. */
21
46
  export function isWriteLike(tool) {
22
47
  return tool.classification !== "read" || sourceClassification(tool) === "write";
23
48
  }
49
+ /** The surface that runs a read-only program: it never dispatches a write-like call. */
50
+ export const READ_PROGRAM_SOURCE = "execute_read_code";
51
+ /**
52
+ * What a read program refuses: anything write-like, unless the deployment accepted
53
+ * repeats. A plain `"read"` over a source write stays out, because only
54
+ * `"read-repeatable"` says a host's resend of the program is acceptable.
55
+ */
56
+ export function readProgramRefuses(tool) {
57
+ return isWriteLike(tool) && !repeatAccepted.has(tool);
58
+ }
24
59
  export function cloneTool(tool) {
25
60
  return carryToolClassification(tool, structuredClone(tool));
26
61
  }
27
62
  /**
28
63
  * The only permission decision for a connector call. Approval is client-owned.
29
64
  * A read-only endpoint refuses every write on every surface; a writable one
30
- * sends writes through call_destructive_tool and programs, never call_tool.
65
+ * sends writes through call_destructive_tool and writing programs, never call_tool
66
+ * or a read program.
31
67
  */
32
68
  export function writeRefusal(classification, surface, readOnly = false) {
33
69
  if (classification === "read")
package/dist/types.d.ts CHANGED
@@ -335,6 +335,12 @@ export type ConnectorSkillResourceContents = {
335
335
  text?: never;
336
336
  });
337
337
  /** The whole plugin contract — the one open seam. */
338
+ /** A complete cached catalog and the times it was fetched and expires, in epoch milliseconds. */
339
+ export interface ConnectorCatalogPeek {
340
+ tools: ToolDef[];
341
+ fetchedAt: number;
342
+ expiresAt: number;
343
+ }
338
344
  export interface Connector {
339
345
  /** Serve only read-classified tools on every endpoint. Defaults to false. */
340
346
  readOnly?: boolean;
@@ -399,6 +405,16 @@ export interface Connector {
399
405
  * fetched lazily over the network and are not known at construction time.
400
406
  */
401
407
  staticTools?: ToolDef[];
408
+ /**
409
+ * Optional: the newest complete catalog this context could be served, fresh
410
+ * or stale, read from storage alone. No transport, no authorization and no
411
+ * writes. Discovery lists its result and marks it; call resolution never
412
+ * reads it. `remoteMcp()` implements it. A custom connector omits it and
413
+ * discovery falls back to the catalog core retains for an authenticated owner.
414
+ * A wrapper that replaces `listTools` must replace or remove this as well, or
415
+ * discovery lists what the wrapper would have changed.
416
+ */
417
+ peekCatalog?(ctx: ConnectorContext): Promise<ConnectorCatalogPeek | undefined>;
402
418
  /**
403
419
  * Optional: what this connector is, for `Connecta.describeConfig()` and the
404
420
  * operator surfaces built on it — where it points, how it authenticates,
@@ -616,6 +632,21 @@ export interface Executor {
616
632
  * rewrite; set this to say the same thing minification-proof.
617
633
  */
618
634
  readonly name?: string;
635
+ /**
636
+ * The run's wall-clock deadline, in whole milliseconds, when this sandbox has
637
+ * one: time from `execute()` to its own timeout result. Connecta ends a
638
+ * program's reads just before it, so a slow read becomes a catchable
639
+ * `timeout` rather than the whole run's failure. Omit it if the sandbox
640
+ * has no fixed deadline. See documentation/code-mode.md#cancellation-and-limits.
641
+ */
642
+ readonly deadlineMs?: number;
643
+ /**
644
+ * The largest stored result, in UTF-8 bytes, that one program may read whole
645
+ * with `connecta.result(id, { all: true })` and still have budget left to use
646
+ * it. Default 1.5 MiB. Set it lower when a tighter CPU or memory limit would
647
+ * end the run first.
648
+ */
649
+ readonly maxReadBytes?: number;
619
650
  execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
620
651
  /** Release runtime resources. Node's built-in pool implements this. */
621
652
  close?(): void | Promise<void>;
@@ -842,6 +873,11 @@ export interface InboundAuth {
842
873
  kind: string;
843
874
  /** This provider may admit a human identity to operator mutation routes. */
844
875
  interactiveOperator?: true;
876
+ /**
877
+ * This provider checks credentials against the deployment's own URL and was
878
+ * given none, so construction refuses a configuration without `publicUrl`.
879
+ */
880
+ requiresPublicUrl?: true;
845
881
  /**
846
882
  * Recognize credential syntax or trusted runtime context, without verifying
847
883
  * it or doing I/O. The first recognizing provider owns the verdict, including
@@ -13,11 +13,11 @@
13
13
  * legacy IPv4 spelling (`2130706433`, `0x7f.1`, `0177.0.0.1`) into dotted
14
14
  * decimal and every IPv6 literal into compressed hex, so one canonical form is
15
15
  * all this module reads. It never resolves a name: that needs DNS, which the
16
- * Workers-safe core cannot do, and a name that resolves somewhere private — or
17
- * rebinds after the check — is out of scope. Pure Web API, no `node:` import.
16
+ * Workers-safe core cannot do. The owned Node transport additionally resolves
17
+ * and pins learned destinations at connect time. Pure Web API, no `node:` import.
18
18
  */
19
19
  function classifyIpv4(octets) {
20
- const [a = 0, b = 0] = octets;
20
+ const [a = 0, b = 0, c = 0] = octets;
21
21
  if (a === 127)
22
22
  return "loopback";
23
23
  if (a === 0 ||
@@ -25,7 +25,14 @@ function classifyIpv4(octets) {
25
25
  (a === 172 && b >= 16 && b <= 31) ||
26
26
  (a === 192 && b === 168) ||
27
27
  (a === 169 && b === 254) ||
28
- (a === 100 && b >= 64 && b <= 127)) {
28
+ (a === 100 && b >= 64 && b <= 127) ||
29
+ (a === 192 && b === 0 && (c === 0 || c === 2)) || // IETF protocol, documentation
30
+ (a === 192 && b === 88 && c === 99) || // 6to4 relay anycast
31
+ (a === 198 && (b === 18 || b === 19)) || // benchmarking
32
+ (a === 198 && b === 51 && c === 100) || // documentation
33
+ (a === 203 && b === 0 && c === 113) || // documentation
34
+ a >= 224 // multicast, reserved, broadcast
35
+ ) {
29
36
  return "private";
30
37
  }
31
38
  return "public";
@@ -61,24 +68,35 @@ function ipv6Pieces(host) {
61
68
  return [...head, ...Array.from({ length: missing }, () => 0), ...tail];
62
69
  }
63
70
  function classifyIpv6(pieces) {
64
- const [first = 0] = pieces;
65
- const leadingZeros = pieces.slice(0, 5).every((piece) => piece === 0);
66
- if (leadingZeros && pieces[5] === 0 && pieces[6] === 0) {
67
- if (pieces[7] === 0)
68
- return "private"; // ::
69
- if (pieces[7] === 1)
70
- return "loopback"; // ::1
71
+ const [p0 = 0, p1 = 0, p2 = 0, p3 = 0, p4 = 0, p5 = 0, p6 = 0, p7 = 0] = pieces;
72
+ const v4 = (high, low) => classifyIpv4([high >> 8, high & 0xff, low >> 8, low & 0xff]);
73
+ // A transitional form reaches, at most, the IPv4 address it carries. Only a
74
+ // mapped address can reach this host, so only it stays `loopback`; every
75
+ // other non-public carrier is `private`, keeping it out of a loopback
76
+ // connector's learned URLs.
77
+ const carried = (high, low) => (v4(high, low) === "public" ? "public" : "private");
78
+ const zeros = p0 === 0 && p1 === 0 && p2 === 0 && p3 === 0;
79
+ if (zeros && p4 === 0) {
80
+ if (p5 === 0xffff)
81
+ return v4(p6, p7); // ::ffff:a.b.c.d mapped
82
+ if (p5 === 0) {
83
+ if (p6 === 0 && p7 === 1)
84
+ return "loopback"; // ::1
85
+ return carried(p6, p7); // :: and ::a.b.c.d compatible
86
+ }
71
87
  }
72
- // ::ffff:a.b.c.d reaches the mapped IPv4 destination; classify that.
73
- if (leadingZeros && pieces[5] === 0xffff) {
74
- const high = pieces[6] ?? 0;
75
- const low = pieces[7] ?? 0;
76
- return classifyIpv4([high >> 8, high & 0xff, low >> 8, low & 0xff]);
88
+ if (zeros && p4 === 0xffff && p5 === 0)
89
+ return carried(p6, p7); // ::ffff:0:a.b.c.d translated
90
+ if (p0 === 0x64 && p1 === 0xff9b && p2 === 0 && p3 === 0 && p4 === 0 && p5 === 0)
91
+ return carried(p6, p7); // NAT64
92
+ if (p0 === 0x2002)
93
+ return carried(p1, p2); // 6to4
94
+ // Global unicast is 2000::/3. Outside it: unique-local, link-local,
95
+ // site-local, multicast, local-use NAT64, reserved. Inside it, special-purpose
96
+ // 2001::/23 and documentation 2001:db8::/32 and 3fff::/20 are not destinations.
97
+ if ((p0 & 0xe000) !== 0x2000 || (p0 === 0x2001 && (p1 < 0x200 || p1 === 0xdb8)) || (p0 === 0x3fff && p1 < 0x1000)) {
98
+ return "private";
77
99
  }
78
- if ((first & 0xfe00) === 0xfc00)
79
- return "private"; // fc00::/7
80
- if ((first & 0xffc0) === 0xfe80)
81
- return "private"; // fe80::/10
82
100
  return "public";
83
101
  }
84
102
  /**