@zackbart/connecta 0.37.0 → 0.38.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 (183) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/cloudflare-access.d.ts +3 -1
  9. package/dist/auth/cloudflare-access.js +22 -11
  10. package/dist/auth/downstream-oauth.js +195 -270
  11. package/dist/auth/downstream-revocation.js +1 -1
  12. package/dist/auth/oauth-grant.js +11 -0
  13. package/dist/auth/oauth-token-response.js +1 -1
  14. package/dist/auth/vercel-agent.js +2 -1
  15. package/dist/auth-elicitation.js +2 -2
  16. package/dist/branding.d.ts +1 -15
  17. package/dist/branding.js +2 -2
  18. package/dist/bytes.js +42 -0
  19. package/dist/call-admission-context.js +2 -5
  20. package/dist/call-admission.js +5 -0
  21. package/dist/call-recovery.js +0 -3
  22. package/dist/catalog-cache.js +26 -7
  23. package/dist/catalog-fingerprint.js +2 -18
  24. package/dist/catalog-revision.js +9 -0
  25. package/dist/catalog-service.js +51 -20
  26. package/dist/config-defaults.d.ts +1 -0
  27. package/dist/config-defaults.js +1 -0
  28. package/dist/config-schema.d.ts +1 -1
  29. package/dist/config-schema.js +6 -2
  30. package/dist/config.d.ts +41 -14
  31. package/dist/config.js +21 -71
  32. package/dist/connector-caller.js +1 -1
  33. package/dist/connectors/api-connector.js +4 -3
  34. package/dist/connectors/guarded-fetch.d.ts +0 -6
  35. package/dist/connectors/guarded-fetch.js +1 -11
  36. package/dist/connectors/remote-mcp.js +93 -69
  37. package/dist/connectors/resource-uri.js +0 -4
  38. package/dist/credentials.js +3 -0
  39. package/dist/d1.d.ts +0 -89
  40. package/dist/d1.js +0 -34
  41. package/dist/downstream-input.js +2 -2
  42. package/dist/execute.js +161 -116
  43. package/dist/executor-result.js +2 -1
  44. package/dist/executors/quickjs-runtime.js +2 -25
  45. package/dist/executors/quickjs.js +1 -1
  46. package/dist/guest-runtime.js +5 -3
  47. package/dist/guest-types.d.ts +5 -5
  48. package/dist/http/client.js +1 -1
  49. package/dist/http/errors.js +2 -2
  50. package/dist/http/json-schema.js +1 -1
  51. package/dist/http/node-transport.js +0 -1
  52. package/dist/http/oauth.js +1 -1
  53. package/dist/http/paging.js +6 -9
  54. package/dist/http/provider.js +1 -1
  55. package/dist/http/safety.js +1 -1
  56. package/dist/http/services.js +1 -8
  57. package/dist/http/tools.js +1 -7
  58. package/dist/http/vendor.js +7 -7
  59. package/dist/identity.js +2 -6
  60. package/dist/index.js +1 -2
  61. package/dist/invocation-auth.js +2 -1
  62. package/dist/invocation.js +8 -2
  63. package/dist/meta-tools.js +27 -27
  64. package/dist/native-tools.js +2 -1
  65. package/dist/oauth-handoff.js +2 -2
  66. package/dist/operation-vocabulary.js +1 -1
  67. package/dist/operator-record.d.ts +8 -2
  68. package/dist/operator-record.js +18 -50
  69. package/dist/operator-ui/contract.d.ts +4 -2
  70. package/dist/operator-ui/generated.js +3 -3
  71. package/dist/operator-ui/model.d.ts +7 -2
  72. package/dist/program-writes.js +3 -8
  73. package/dist/provider-call.js +0 -7
  74. package/dist/provider.d.ts +1 -1
  75. package/dist/providers/_shared/google/kit.js +1 -6
  76. package/dist/providers/_shared/google/service-account.js +1 -3
  77. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  78. package/dist/providers/_shared/guide.js +3 -0
  79. package/dist/providers/_shared/json.js +12 -0
  80. package/dist/providers/_shared/rest/compact-index.js +1 -1
  81. package/dist/providers/_shared/rest/kit.js +1 -3
  82. package/dist/providers/_shared/rest/operation-index.js +1 -3
  83. package/dist/providers/_shared/rest/value-safety.js +26 -28
  84. package/dist/providers/breeze/api.js +2 -7
  85. package/dist/providers/breeze/index.js +4 -2
  86. package/dist/providers/breeze/tools.js +3 -4
  87. package/dist/providers/ccb/api.js +1 -6
  88. package/dist/providers/ccb/index.js +2 -1
  89. package/dist/providers/ccb/tools.js +1 -6
  90. package/dist/providers/cloudflare/api.js +2 -2
  91. package/dist/providers/cloudflare/index.js +2 -1
  92. package/dist/providers/cloudflare/safety.js +3 -3
  93. package/dist/providers/docs/tools.js +2 -10
  94. package/dist/providers/drive/tools.js +12 -25
  95. package/dist/providers/forms/tools.js +1 -9
  96. package/dist/providers/gmail/tools.js +5 -21
  97. package/dist/providers/overflow/api.js +1 -3
  98. package/dist/providers/overflow/index.js +2 -1
  99. package/dist/providers/overflow/tools.js +3 -7
  100. package/dist/providers/planning-center/api.js +2 -1
  101. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  102. package/dist/providers/planning-center/lib/json-api.js +1 -9
  103. package/dist/providers/planning-center/tools.js +2 -1
  104. package/dist/providers/sheets/tools.js +1 -9
  105. package/dist/providers/slides/tools.js +4 -13
  106. package/dist/providers/stripe/index.js +2 -1
  107. package/dist/providers/stripe/safety.js +2 -2
  108. package/dist/providers/tithely/api.js +2 -7
  109. package/dist/providers/tithely/index.js +3 -2
  110. package/dist/providers/tithely/tools.js +7 -5
  111. package/dist/providers/vercel/api.js +1 -6
  112. package/dist/providers/vercel/lib/pin.js +1 -1
  113. package/dist/providers/vercel/safety.js +1 -1
  114. package/dist/providers/vercel/tools.js +2 -1
  115. package/dist/redaction-marker.js +2 -2
  116. package/dist/registry.d.ts +4 -1
  117. package/dist/registry.js +35 -22
  118. package/dist/request-principal.js +15 -2
  119. package/dist/request-state.js +2 -2
  120. package/dist/routes/access-tokens.js +1 -1
  121. package/dist/routes/activity-disclosure.js +1 -1
  122. package/dist/routes/connect.js +2 -2
  123. package/dist/routes/credentials.js +1 -1
  124. package/dist/routes/dispatch.js +10 -0
  125. package/dist/routes/mcp.js +23 -21
  126. package/dist/routes/oauth-management.js +1 -1
  127. package/dist/routes/operator.js +53 -7
  128. package/dist/routes/shared.d.ts +8 -8
  129. package/dist/routes/shared.js +22 -15
  130. package/dist/routes/ui-config.js +6 -6
  131. package/dist/routes/ui-data.js +2 -2
  132. package/dist/routes/ui.js +9 -4
  133. package/dist/runtime/call-admission.js +1 -1
  134. package/dist/runtime/call.js +1 -1
  135. package/dist/runtime/settlement.js +1 -1
  136. package/dist/runtime/shared-read.js +2 -15
  137. package/dist/sent-secrets.d.ts +14 -6
  138. package/dist/sent-secrets.js +231 -85
  139. package/dist/server.js +4 -4
  140. package/dist/skills.js +8 -35
  141. package/dist/sqlite.d.ts +0 -19
  142. package/dist/sqlite.js +2 -83
  143. package/dist/storage/keys.js +29 -124
  144. package/dist/storage/sql-native.js +14 -111
  145. package/dist/storage/sql.d.ts +1 -24
  146. package/dist/storage/sql.js +1 -12
  147. package/dist/tool-error.js +4 -0
  148. package/dist/types.d.ts +4 -3
  149. package/dist/ui.js +3 -0
  150. package/dist/url-safety.js +0 -4
  151. package/dist/usage-guide.js +2 -2
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/dist/worker.js +1 -1
  155. package/documentation/README.md +6 -42
  156. package/documentation/architecture.md +190 -507
  157. package/documentation/auth.md +172 -58
  158. package/documentation/code-mode.md +98 -66
  159. package/documentation/deploying.md +19 -40
  160. package/documentation/integrating.md +8 -19
  161. package/documentation/meta-tools.md +23 -6
  162. package/documentation/migration-0.33.md +5 -5
  163. package/documentation/migration-0.34.md +2 -4
  164. package/documentation/migration-retired-layouts.md +74 -0
  165. package/documentation/operator-tests.md +19 -119
  166. package/documentation/operator-ui.md +46 -8
  167. package/documentation/provider-auth-migration.md +4 -10
  168. package/documentation/provider-migration-0.29.md +4 -15
  169. package/examples/worker/AGENTS.md +4 -6
  170. package/examples/worker/README.md +20 -115
  171. package/guides/connecting-agents.md +7 -30
  172. package/guides/how-it-works.md +2 -2
  173. package/guides/safety-and-trust.md +4 -1
  174. package/package.json +19 -19
  175. package/templates/node/README.md +9 -26
  176. package/templates/node/package.json +1 -1
  177. package/dist/auth/oauth-v2-migration.js +0 -135
  178. package/dist/provider-call-facts.d.ts +0 -115
  179. package/dist/provider-call.d.ts +0 -112
  180. package/dist/storage/d1-copy.js +0 -255
  181. package/dist/storage/sql-schema.d.ts +0 -6
  182. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  183. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -1,3 +1,4 @@
1
+ import { utf8Length } from "../bytes.js";
1
2
  // QuickJS-in-WebAssembly runtime used inside the disposable child process.
2
3
  //
3
4
  // Model-written code runs inside a QuickJS interpreter compiled to WASM:
@@ -37,30 +38,6 @@ function logTransportBytes(entry) {
37
38
  // encoded into ChildToParentMessage. Measure the units the IPC cap sees.
38
39
  return serializedBytes(JSON.stringify(JSON.stringify(entry)));
39
40
  }
40
- function exceedsUtf8ByteLimit(value, limit) {
41
- let bytes = 0;
42
- for (let index = 0; index < value.length; index += 1) {
43
- const code = value.charCodeAt(index);
44
- if (code <= 0x7f)
45
- bytes += 1;
46
- else if (code <= 0x7ff)
47
- bytes += 2;
48
- else if (code >= 0xd800 &&
49
- code <= 0xdbff &&
50
- index + 1 < value.length &&
51
- value.charCodeAt(index + 1) >= 0xdc00 &&
52
- value.charCodeAt(index + 1) <= 0xdfff) {
53
- bytes += 4;
54
- index += 1;
55
- }
56
- else {
57
- bytes += 3;
58
- }
59
- if (bytes > limit)
60
- return true;
61
- }
62
- return false;
63
- }
64
41
  /** Normalize model output into an async-arrow expression: strip markdown fences, wrap bare bodies. */
65
42
  export function normalizeCode(code) {
66
43
  let c = code.trim();
@@ -216,7 +193,7 @@ function installBridge(ctx, providers, logs, onLog) {
216
193
  catch (err) {
217
194
  throw new Error(`Host result from ${label} could not be serialized: ${msg(err)}`);
218
195
  }
219
- if (exceedsUtf8ByteLimit(json, MAX_HOST_RESULT_BYTES)) {
196
+ if (utf8Length(json, MAX_HOST_RESULT_BYTES) > MAX_HOST_RESULT_BYTES) {
220
197
  throw new Error(`Host result from ${label} exceeds the ${MAX_HOST_RESULT_BYTES}-byte serialized bridge limit.`);
221
198
  }
222
199
  return { json };
@@ -119,7 +119,7 @@ function terminate(child) {
119
119
  }
120
120
  let providePoolProgram;
121
121
  class QuickJsChildPool {
122
- /** What `/health` and `connecta doctor` call this sandbox (#368). */
122
+ /** What `/health` and `connecta doctor` call this sandbox. */
123
123
  name = "QuickJS";
124
124
  admission;
125
125
  slots;
@@ -160,6 +160,8 @@ export function guestInitializer() {
160
160
  read: provider.read,
161
161
  result: provider.result,
162
162
  skill: provider.skill,
163
+ // Undeclared alias: models write connecta.skills, the top-level tool's name.
164
+ skills: provider.skill,
163
165
  emit(block) {
164
166
  const ref = block && typeof block === "object" ? weakGet(nativeRefs, block) : undefined;
165
167
  return trackEmission(emit(ref || block));
@@ -224,10 +226,10 @@ export function programError(raw, code) {
224
226
  /(?:const|let|var|function|\()\s*connecta\b/.test(code)) {
225
227
  hint = "Do not shadow connecta. Use the host-provided connecta global.";
226
228
  }
227
- else if (/callTool|\bskills\b|connecta\.(?:guide|skills)/.test(message) ||
228
- (["TypeError", "ReferenceError"].includes(name) && /connecta\.(?:guide|skills)\b/.test(code))) {
229
+ else if (/callTool|\bskills\b|connecta\.guide/.test(message) ||
230
+ (["TypeError", "ReferenceError"].includes(name) && /connecta\.guide\b/.test(code))) {
229
231
  hint =
230
- "Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill(name). Services are addressed by connector.tool, never guest globals.";
232
+ 'Use connecta.call(address, args), connecta.search, connecta.describe, connecta.result, and connecta.skill("usage"). Services are addressed by connector.tool, never guest globals.';
231
233
  }
232
234
  return {
233
235
  code: "program_error",
@@ -103,7 +103,7 @@ export type GuestBlock = {
103
103
  mimeType: string;
104
104
  };
105
105
  type SchemaFormat = "json" | "compact" | "typescript";
106
- export interface CatalogSearchArgs {
106
+ interface CatalogSearchArgs {
107
107
  query?: string;
108
108
  connector?: string;
109
109
  safety?: "readOnly" | "approvalRequired" | "all";
@@ -113,7 +113,7 @@ export interface CatalogSearchArgs {
113
113
  includeSchemas?: SchemaFormat;
114
114
  includeSchemaKeys?: boolean;
115
115
  }
116
- export interface CatalogDescribeArgs {
116
+ interface CatalogDescribeArgs {
117
117
  address?: unknown;
118
118
  addresses?: unknown;
119
119
  format?: SchemaFormat;
@@ -140,7 +140,7 @@ interface CatalogSearchFailure extends CatalogFailureDetail {
140
140
  }>;
141
141
  retry?: string;
142
142
  }
143
- export interface CatalogSearchPage {
143
+ interface CatalogSearchPage {
144
144
  catalogErrors: CatalogSearchFailure[];
145
145
  absence?: {
146
146
  service: string;
@@ -170,14 +170,14 @@ export interface CatalogSearchPage {
170
170
  guidance?: string;
171
171
  };
172
172
  }
173
- export type CatalogSearchResult = CatalogSearchPage & {
173
+ type CatalogSearchResult = CatalogSearchPage & {
174
174
  tools: Array<CatalogSearchTool & {
175
175
  connectorTitle?: string;
176
176
  guide?: string;
177
177
  guideSummary?: string;
178
178
  }>;
179
179
  };
180
- export interface CatalogDescription {
180
+ interface CatalogDescription {
181
181
  address: string;
182
182
  schemaFormat?: "json" | "text";
183
183
  name?: string;
@@ -1,6 +1,6 @@
1
1
  import { confinementBase, REDIRECT_STATUSES, BODILESS_STATUSES } from "../http-policy.js";
2
2
  import { guardedTransport, sendGuarded } from "../guarded-transport.js";
3
- // Guarded HTTP transport (decision 0010, B.2). Unreachable from src/index.ts.
3
+ // Guarded HTTP transport. Unreachable from src/index.ts.
4
4
  //
5
5
  // The guarded transport as an Effect `HttpClient`, with the behaviour of
6
6
  // src/connectors/guarded-fetch.ts: requests confined beneath one base URL,
@@ -1,4 +1,4 @@
1
- // Internal HTTP failure contract (decision 0010, B.3). Unreachable from src/index.ts.
1
+ // Internal HTTP failure contract. Unreachable from src/index.ts.
2
2
  //
3
3
  // Every way a kit request can fail, as a tagged error, and the one mapping
4
4
  // from those to the wire `ConnectorCallError`. A provider supplies meaning
@@ -26,7 +26,7 @@
26
26
  // refusing status counts only for the request that received it. Advice to
27
27
  // send a call again is a segment of its own (`resendAdvice`), never part of
28
28
  // the message, so output composes it only where the outcome allows.
29
- // Q8 policy is decided once by the core's `settleWrite` (#917), from the
29
+ // Q8 policy is decided once by the core's `settleWrite`, from the
30
30
  // ledger entries this kit forwards its matched proof and renewals to. A
31
31
  // write answered 401 stays unknown; only renewal removes its handoff, and
32
32
  // only a reviewed authentication-before-execution declaration proves refusal.
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // One Schema is a tool's type, its argument decoder, and its catalog JSON
4
4
  // Schema. Effect's generator is correct but spells some things differently
@@ -238,4 +238,3 @@ export function createNodeGuardedTransport(proxy) {
238
238
  },
239
239
  };
240
240
  }
241
- export const nodeGuardedTransport = createNodeGuardedTransport();
@@ -1,4 +1,4 @@
1
- // Refresh-once middleware (decision 0010, B.2). Unreachable from src/index.ts.
1
+ // Refresh-once middleware. Unreachable from src/index.ts.
2
2
  //
3
3
  // Refresh-once for a connection that authenticates with a token it can
4
4
  // renew: a downstream OAuth grant, a service-account assertion, a machine
@@ -1,4 +1,5 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ import { bytesToHex, decodeBase64url } from "../bytes.js";
2
+ // Provider kit. Unreachable from src/index.ts.
2
3
  //
3
4
  // List tools take one page at a time. Lookahead detects an extra row; bound
4
5
  // cursors authenticate the tool, arguments, connection and credential owner.
@@ -52,7 +53,7 @@ async function signingKey(storage) {
52
53
  return cached;
53
54
  let raw = await storage.get(continuationKeys.signingKey);
54
55
  if (raw === null) {
55
- const candidate = Array.from(crypto.getRandomValues(new Uint8Array(32)), (byte) => byte.toString(16).padStart(2, "0")).join("");
56
+ const candidate = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
56
57
  raw = (await storage.compareAndSet(continuationKeys.signingKey, null, candidate))
57
58
  ? candidate
58
59
  : await storage.get(continuationKeys.signingKey);
@@ -92,7 +93,7 @@ export const boundCursor = {
92
93
  const expected = yield* digest(tool, args);
93
94
  let parsed;
94
95
  try {
95
- parsed = JSON.parse(new TextDecoder().decode(fromBase64url(token)));
96
+ parsed = JSON.parse(new TextDecoder().decode(decodeBase64url(token)));
96
97
  }
97
98
  catch {
98
99
  parsed = undefined;
@@ -108,7 +109,7 @@ export const boundCursor = {
108
109
  if (token !== base64url(JSON.stringify({ v: record.v, d: record.d, c: record.c, m: record.m })))
109
110
  return yield* Effect.fail(new Refusal({ error: new ConnectorCallError("invalid_args", STRAY_CURSOR) }));
110
111
  const key = yield* keyForCall;
111
- const mac = fromBase64url(record.m);
112
+ const mac = decodeBase64url(record.m);
112
113
  const binding = yield* ContinuationBinding;
113
114
  const claims = new TextEncoder().encode(canonicalJson([record.v, binding.connection, binding.principal, record.d, record.c]));
114
115
  const valid = yield* Effect.promise(() => crypto.subtle.verify("HMAC", key, mac, claims));
@@ -125,7 +126,7 @@ function digest(tool, args) {
125
126
  .sort()
126
127
  .map((key) => [key, args[key]]);
127
128
  const bytes = new TextEncoder().encode(canonicalJson([tool, shape]));
128
- return Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)).pipe(Effect.map((hash) => Array.from(new Uint8Array(hash).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")));
129
+ return Effect.promise(() => crypto.subtle.digest("SHA-256", bytes)).pipe(Effect.map((hash) => bytesToHex(new Uint8Array(hash).slice(0, 16))));
129
130
  }
130
131
  function base64url(text) {
131
132
  return base64urlBytes(new TextEncoder().encode(text));
@@ -136,7 +137,3 @@ function base64urlBytes(bytes) {
136
137
  binary += String.fromCharCode(byte);
137
138
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
138
139
  }
139
- function fromBase64url(token) {
140
- const binary = atob(token.replace(/-/g, "+").replace(/_/g, "/"));
141
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
142
- }
@@ -1,4 +1,4 @@
1
- // Internal provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Internal provider kit. Unreachable from src/index.ts.
2
2
  import { Context, Effect, Option } from "effect";
3
3
  import { api } from "../connectors/api.js";
4
4
  import { OAUTH_CHALLENGE_MAX_BYTES } from "../auth/renewable-grant.js";
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // The one value-safety boundary of a kit connection. `connect` requires it,
4
4
  // and `Send` hands a provider nothing it has not passed: a named tool, a
@@ -3,7 +3,7 @@ import { Context, Effect } from "effect";
3
3
  import { dispatchProviderRequest, providerRequest } from "../provider-call.js";
4
4
  import { callClassification } from "../call-classification.js";
5
5
  import { fromSignal } from "../runtime/run.js";
6
- import { callFromContext, ContinuationBinding, ContinuationStorage, openCall as openCoreCall, serviceFor, Signal, } from "../runtime/call.js";
6
+ import { callFromContext, ContinuationBinding, ContinuationStorage, serviceFor, Signal } from "../runtime/call.js";
7
7
  import { RequestRefused, ResendRefused } from "./errors.js";
8
8
  export { writeRequestKey } from "./write-request.js";
9
9
  export { Caller, Credentials, Secrets, RequestSecrets, Budget, Recovery, Signal, Classification, Ledger, } from "../runtime/call.js";
@@ -70,10 +70,3 @@ export function fromConnectorContext(ctx, fallback) {
70
70
  services: lease.services.pipe(Context.add(ContinuationStorage, serviceFor(ctx, ContinuationStorage) ?? ctx.storage), Context.add(ContinuationBinding, serviceFor(ctx, ContinuationBinding) ?? { connection: "", principal: null }), Context.add(Dispatches, []), Context.add(WriteCapacity, new Map([["write", new Map([[undefined, undefined]])]]))),
71
71
  };
72
72
  }
73
- export function openCall(inputs) {
74
- const lease = openCoreCall(inputs);
75
- return {
76
- ...lease,
77
- services: lease.services.pipe(Context.add(Dispatches, []), Context.add(WriteCapacity, new Map([["write", new Map([[undefined, undefined]])]]))),
78
- };
79
- }
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // Tool builders. A tool is a name, a description, an input Schema, an output
4
4
  // Schema, and a program; the builder renders both Schemas as catalog JSON
@@ -24,7 +24,6 @@ import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
24
24
  import { catalogSchema, Integer } from "./json-schema.js";
25
25
  import { boundCursor, Cursor, StandardPage } from "./paging.js";
26
26
  import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
27
- const inputs = new WeakMap();
28
27
  const keyed = new WeakSet();
29
28
  /** Whether a kit write builder declared an idempotency key for this tool. */
30
29
  export function takesIdempotencyKey(tool) {
@@ -62,10 +61,6 @@ function originalOrder(decoded, original) {
62
61
  };
63
62
  return copy(decoded, original);
64
63
  }
65
- /** The input Schema a kit tool decodes with, for the catalog/decoder agreement check. */
66
- export function kitInput(tool) {
67
- return inputs.get(tool);
68
- }
69
64
  /** A Struct's own property names, for `fields`. */
70
65
  function propertyNames(schema, what) {
71
66
  const fields = schema.fields;
@@ -350,7 +345,6 @@ rendered) {
350
345
  })));
351
346
  });
352
347
  registerToolEntry(tool, (args, ctx) => program(args, ctx, inLogicalCall(ctx)));
353
- inputs.set(tool, spec.input);
354
348
  if (idempotency)
355
349
  keyed.add(tool);
356
350
  return tool;
@@ -1,4 +1,4 @@
1
- // Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
1
+ // Provider kit. Unreachable from src/index.ts.
2
2
  //
3
3
  // A vendor is an `HttpApi`: endpoints with Schemas for path params, query,
4
4
  // and payload, from which requests are encoded and the endpoint manifest is
@@ -13,13 +13,13 @@
13
13
  // empty 200. Every answer, from a declared operation or a hatch's path, takes
14
14
  // one route: bounded read, JSON, closed public classifier facts captured,
15
15
  // the request's sent secrets redacted from
16
- // every value of the whole body before anything can cut or extract from it
17
- // (#911), its keys kept for value safety to match by name, status (with
16
+ // every value of the whole body before anything can cut or extract from it,
17
+ // its keys kept for value safety to match by name, status (with
18
18
  // its operation, so value safety decides what a failure may quote), the
19
19
  // vendor's `result` hook, then the connection's ResultSafety. A provider
20
20
  // cannot reach a body earlier. A Content-Type redaction altered selects the
21
21
  // format the vendor reads as JSON, so no altered type routes a body around
22
- // ResultSafety as text or bytes (#703).
22
+ // ResultSafety as text or bytes.
23
23
  import { Context, Effect } from "effect";
24
24
  import * as HttpClientRequest from "effect/http/HttpClientRequest";
25
25
  import * as HttpApi from "effect/http-api/HttpApi";
@@ -233,12 +233,12 @@ export function connect(vendor, guarded, options) {
233
233
  : defect)));
234
234
  });
235
235
  // Header-derived format decisions see only redacted Content-Type. Names are protocol
236
- // constants `Headers` must accept, so only the values are redacted, together (#703).
236
+ // constants `Headers` must accept, so only the values are redacted, together.
237
237
  const entries = [...reply.headers];
238
238
  const values = secrets.redact(entries.map(([, value]) => value));
239
239
  const headers = new Headers(entries.map(([name], index) => [name, values[index]]));
240
240
  // Whole, before a detail line trims it or a projection extracts from it. Values only:
241
- // result safety and projections match fields by their vendor names (#703).
241
+ // result safety and projections match fields by their vendor names.
242
242
  let classifierFacts;
243
243
  const parsed = reply.bytes !== undefined
244
244
  ? {
@@ -400,7 +400,7 @@ export function connect(vendor, guarded, options) {
400
400
  /**
401
401
  * The Content-Type a format decision reads. One redaction altered reads as
402
402
  * `application/json`: a vendor's text or binary choice for it would hand
403
- * result safety a body it cannot read field by field (#703).
403
+ * result safety a body it cannot read field by field.
404
404
  */
405
405
  function formatType(redacted) {
406
406
  return redactionAltered(redacted) ? "application/json" : redacted.toLowerCase();
package/dist/identity.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { sha256Hex } from "./bytes.js";
1
2
  const IDENTITY_PART_RE = /^[\x21-\x7e]{1,256}$/;
2
- const encoder = new TextEncoder();
3
3
  export function validIdentityReference(value) {
4
4
  return Boolean(value &&
5
5
  typeof value.namespace === "string" &&
@@ -7,11 +7,7 @@ export function validIdentityReference(value) {
7
7
  IDENTITY_PART_RE.test(value.namespace) &&
8
8
  IDENTITY_PART_RE.test(value.id));
9
9
  }
10
- function bytesToHex(bytes) {
11
- return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
12
- }
13
10
  /** Deterministic pseudonymous partition; raw emails and ids do not enter keys. */
14
11
  export async function identityStorageKey(identity) {
15
- const digest = await crypto.subtle.digest("SHA-256", encoder.encode(`${identity.namespace}\n${identity.id}`));
16
- return bytesToHex(new Uint8Array(digest));
12
+ return sha256Hex(`${identity.namespace}\n${identity.id}`);
17
13
  }
package/dist/index.js CHANGED
@@ -177,8 +177,7 @@ function warnInsecureConfig(config) {
177
177
  logger.warn(`[connecta] connector "${connector.id}" has an OAuth callback with no ` +
178
178
  `state/CSRF check: /oauth/callback/${connector.id} refuses every ` +
179
179
  "callback rather than exchange an unverified code. Implement " +
180
- "`verifyState` to complete authorization (the shipped remoteMcp " +
181
- "connector already does).");
180
+ "`verifyState` to complete authorization.");
182
181
  }
183
182
  }
184
183
  }
@@ -1,3 +1,4 @@
1
+ import { bytesToHex } from "./bytes.js";
1
2
  import { authCall, authFlow, registerAuth } from "./auth/native.js";
2
3
  import { upwardStep } from "./upward.js";
3
4
  import { runEdge } from "./runtime/run.js";
@@ -34,7 +35,7 @@ export async function classificationDigest(definition) {
34
35
  annotations,
35
36
  schema,
36
37
  }));
37
- return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
38
+ return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)));
38
39
  }
39
40
  catch {
40
41
  // A digest refusal blocks recovery, not an ordinary call.
@@ -766,7 +766,13 @@ export class InvocationService {
766
766
  : Effect.succeed(result);
767
767
  const processed = yield* Effect.exit(timed((elapsed) => {
768
768
  resultProcessingMs += elapsed;
769
- }, Effect.tap(processing, () => Effect.sync(() => {
769
+ }, Effect.tap(withDeadlineEffect(() => processing, {
770
+ ...defined({
771
+ timeoutMs: context.timeoutMs === undefined ? undefined : Math.max(1, context.timeoutMs - (Date.now() - started)),
772
+ signal: context.requestSignal,
773
+ }),
774
+ timeoutError: new ConnectorCallError("timeout", "Result processing exceeded the tool call deadline."),
775
+ }), () => Effect.sync(() => {
770
776
  if (inputRequiredValue)
771
777
  return;
772
778
  try {
@@ -777,7 +783,7 @@ export class InvocationService {
777
783
  }
778
784
  }))));
779
785
  // Past this point the call has happened, so nothing may invite a retry.
780
- const unprocessable = () => failed(framingError("result_processing_failed", "The downstream call completed, but its result could not be processed. Do not repeat the call to recover its result."));
786
+ const unprocessable = () => failed(framingError("result_processing_failed", "The downstream call completed, but its result could not be processed. Any write already happened. Do not repeat the call to recover its result."));
781
787
  if (Exit.isFailure(processed))
782
788
  return unprocessable();
783
789
  try {
@@ -1,3 +1,4 @@
1
+ import { toolError } from "./tool-error.js";
1
2
  import { upwardStep } from "./upward.js";
2
3
  import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
3
4
  import { registryProgram } from "./runtime/registry.js";
@@ -20,7 +21,7 @@ import { AUTHORIZE_OUTPUT, SEARCH_OUTPUT, SKILLS_OUTPUT } from "./meta-output.js
20
21
  import { RESULT_TTL_SECONDS, resultKeys } from "./storage/keys.js";
21
22
  import { agentOutputOperations, sentSecretsForRequest } from "./sent-secrets.js";
22
23
  import { captureDownstreamInput } from "./downstream-input.js";
23
- import { PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
24
+ import { PROGRAM_RESULT_PAGE_BYTES, PROGRAM_RESULT_WIRE_BYTES } from "./program-result.js";
24
25
  export { MAX_DESCRIBE_ADDRESSES, MAX_DISCOVERY_RESULT_BYTES, MAX_SEARCH_LIMIT };
25
26
  const enc = new TextEncoder();
26
27
  // Each page is a slice of payload bytes, so a leading U+FEFF is data.
@@ -37,15 +38,7 @@ function errorResult(message) {
37
38
  return { content: [{ type: "text", text: message }], isError: true };
38
39
  }
39
40
  function discoveryErrorResult(error) {
40
- const result = jsonResult({
41
- error: {
42
- code: error.code,
43
- message: error.message,
44
- retryable: false,
45
- },
46
- });
47
- result.isError = true;
48
- return result;
41
+ return toolError({ code: error.code, message: error.message, retryable: false });
49
42
  }
50
43
  const discoveryResult = function (operation, hint) {
51
44
  return authFlow(function* () {
@@ -86,8 +79,8 @@ function isValidResultOffset(value) {
86
79
  * Backwards, never forwards: re-serving a few bytes the caller already has is
87
80
  * recoverable, silently skipping the rest of a character is not. Offsets the
88
81
  * server itself produced (`nextOffset`) are already boundaries and come back
89
- * unchanged, so this only moves an offset a client computed on its own
90
- * (issue #38). An offset at or past `bytes.length` is left alone — there is no
82
+ * unchanged, so this only moves an offset a client computed on its own.
83
+ * An offset at or past `bytes.length` is left alone — there is no
91
84
  * character there to split.
92
85
  */
93
86
  function alignStartToCharBoundary(bytes, offset) {
@@ -149,6 +142,15 @@ function base64Of(bytes) {
149
142
  }
150
143
  return out;
151
144
  }
145
+ /**
146
+ * The page a notice's nextAction asks a program to read: it fits the program's
147
+ * return even if every byte escapes to six JSON characters, with room for the
148
+ * page envelope, and never asks for more than the stash or bridge will serve.
149
+ */
150
+ function programPageBytes(results) {
151
+ const fits = Math.floor(((results.programChars ?? MAX_EXECUTE_RESULT_CHARS) - 1024) / 6);
152
+ return Math.max(4, Math.min(fits, results.cap, PROGRAM_RESULT_PAGE_BYTES));
153
+ }
152
154
  /** Opens a write's notice, ahead of any paging instruction. */
153
155
  const WRITE_ALREADY_RAN = "This write already ran: do not call it again to see its result.";
154
156
  /** UTF-8 cannot represent lone UTF-16 surrogates without changing them. */
@@ -229,7 +231,7 @@ const stashResult = function (bytes, results, preview, rawText) {
229
231
  try {
230
232
  // Base64 permits byte-range decoding, and splitting the envelope across
231
233
  // keys keeps a page's storage read proportional to the page instead of to
232
- // the whole result (issue #540). Chunk 0 carries the header; the connecta.result
234
+ // the whole result. Chunk 0 carries the header; the connecta.result
233
235
  // reader below maps a byte offset back to chunk index and base64 quad.
234
236
  const chunkBytes = resultChunkBytes(totalBytes);
235
237
  const binding = base64Of(enc.encode(JSON.stringify(results.binding)));
@@ -280,7 +282,7 @@ const stashResult = function (bytes, results, preview, rawText) {
280
282
  // A byte may cost six JSON characters when escaped. Leave room for the
281
283
  // result id, page counters, and continuation metadata in the returned page.
282
284
  arguments: {
283
- code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${Math.floor((MAX_EXECUTE_RESULT_CHARS - 1024) / 6)} })`,
285
+ code: `async () => await connecta.result(${JSON.stringify(id)}, { offset: ${preview.kind === "prefix" ? preview.bytes : 0}, maxBytes: ${programPageBytes(results)} })`,
284
286
  },
285
287
  },
286
288
  };
@@ -325,10 +327,10 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
325
327
  });
326
328
  });
327
329
  /** Program returns share the direct stash, with all host-admitted call dependencies. */
328
- export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite) {
329
- return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite));
330
+ export function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
331
+ return runEdge(guardProgramReturnNative(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars));
330
332
  }
331
- const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite) {
333
+ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn, function (value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite, programChars) {
332
334
  return authFlow(function* () {
333
335
  const serialized = serializeResultText(value);
334
336
  if (serialized.length <= maxChars)
@@ -347,6 +349,7 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
347
349
  },
348
350
  write: completedWrite,
349
351
  cap,
352
+ ...(programChars !== undefined ? { programChars } : {}),
350
353
  set: authMethod((id, chunks, ttl) => registryProgram(registry, "stashResult", id, chunks, ttl)),
351
354
  warn: () => logFailure(logger, "result paging unavailable", failureRecord({})),
352
355
  };
@@ -453,16 +456,11 @@ function escapedHeadOf(bytes, cap) {
453
456
  * Bound a downstream MCP `content` array by `cap`.
454
457
  *
455
458
  * A lone text block is measured, stashed, previewed, and paged as its text:
456
- * its envelope adds nothing but a wrapper and a second layer of JSON escaping,
457
- * which made a JSON payload's preview unreadable and inflated every page.
459
+ * its envelope adds nothing but a wrapper and a second layer of JSON escaping.
458
460
  *
459
461
  * Anything else is measured as the serialized envelope — the same string that
460
462
  * gets stashed and paged, and the one that counts every block rather than only
461
- * the text ones. Both halves matter (issue #43). Measuring only text blocks
462
- * meant an oversized all-image result scored zero bytes and was returned inline
463
- * unbounded, with no `resultId` to page from; and measuring one string while
464
- * truncating another left `totalBytes` describing something the cap was never
465
- * compared against.
463
+ * the text ones. Measure the same representation that gets truncated and paged.
466
464
  *
467
465
  * Over the cap, what a client gets depends on whether a preview is usable.
468
466
  * Several text blocks preview as their text joined by newlines, while
@@ -624,6 +622,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
624
622
  write: resolved.definition.classification !== "read",
625
623
  cap,
626
624
  ...(opts.pagingTool ? { pagingTool: opts.pagingTool } : {}),
625
+ ...(opts.maxProgramResultChars !== undefined ? { programChars: opts.maxProgramResultChars } : {}),
627
626
  set: authMethod((id, value, ttlSeconds) => registryProgram(registry, "stashResult", id, value, ttlSeconds)),
628
627
  // The catalog entry, never `toolName`: a record names a tool
629
628
  // only through the grammar check (src/operator-record.ts).
@@ -670,7 +669,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
670
669
  const structuredRecovery = outcome.error.nextAction !== undefined;
671
670
  const recoveryRequired = structuredRecovery ||
672
671
  // Sanitized `unavailable` diagnostics ride the structured shape too;
673
- // the plain-text path would drop them (#539).
672
+ // the plain-text path would drop them.
674
673
  outcome.error.details !== undefined ||
675
674
  // So does a conflict's `current`: where things stand is the retry.
676
675
  outcome.error.current !== undefined ||
@@ -733,7 +732,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
733
732
  content: [
734
733
  {
735
734
  type: "text",
736
- text: 'Available skills. Fetch one with skills({ name: "<name>" }).\n\n' +
735
+ text: "Available skills. Fetch one by exact name with this tool.\n\n" +
737
736
  listing.map((skill) => `- \`${skill.name}\` — ${skill.description}`).join("\n"),
738
737
  },
739
738
  ],
@@ -949,7 +948,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
949
948
  // client that computes its own offsets can land inside a multi-byte
950
949
  // character, which would decode as U+FFFD; the offset actually served is
951
950
  // the boundary at or before it, and it is what the response reports back
952
- // as `offset` (issue #38).
951
+ // as `offset`.
953
952
  const offset = start + alignStartToCharBoundary(bytes, requestedOffset - start);
954
953
  // Both sides of `maxBytes` are validated by now — the argument above, the
955
954
  // cap at intake — so `offset + maxBytes` always reaches past `offset`.
@@ -1084,6 +1083,7 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
1084
1083
  */
1085
1084
  export function registerMetaTools(server, registry, ctx) {
1086
1085
  const mt = createMetaTools(registry, ctx.baseUrl, {
1086
+ maxProgramResultChars: ctx.maxProgramResultChars,
1087
1087
  ...(ctx.native && !ctx.native.executeCode ? { pagingTool: "read_result" } : {}),
1088
1088
  ...(ctx.native ? { activitySource: "native" } : {}),
1089
1089
  downstreamInput: Boolean(ctx.authElicitation),
@@ -1,3 +1,4 @@
1
+ import { bytesToHex } from "./bytes.js";
1
2
  import { duringViewEffect } from "./runtime/call.js";
2
3
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
3
4
  import { Effect } from "effect";
@@ -35,7 +36,7 @@ function shortHash(text) {
35
36
  const shortHashNative = /* @__PURE__ */ registerAuth(shortHash, function (text) {
36
37
  return authFlow(function* () {
37
38
  const digest = new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", enc.encode(text)])));
38
- return [...digest.subarray(0, HASH_CHARS / 2)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
39
+ return bytesToHex(digest.subarray(0, HASH_CHARS / 2));
39
40
  });
40
41
  });
41
42
  /**
@@ -1,3 +1,4 @@
1
+ import { decodeBase64url } from "./bytes.js";
1
2
  import { upwardStep } from "./upward.js";
2
3
  import { authCall, authInvoke, authFlow, authStorage, registerAuth } from "./auth/native.js";
3
4
  import { runEdge } from "./runtime/run.js";
@@ -58,8 +59,7 @@ function encodeSegment(value) {
58
59
  function decodeSegment(value) {
59
60
  if (!/^[A-Za-z0-9_-]+$/.test(value))
60
61
  throw new Error("Invalid handoff encoding");
61
- const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
62
- const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
62
+ const decoded = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(decodeBase64url(value));
63
63
  if (encodeSegment(decoded) !== value)
64
64
  throw new Error("Noncanonical handoff encoding");
65
65
  return decoded;
@@ -2,7 +2,7 @@
2
2
  import { lexicalSearchContent, lexicalQueryTerms } from "./catalog.js";
3
3
  const tools = new WeakMap();
4
4
  const connectors = new WeakMap();
5
- export const OPERATION_VOCABULARY_BOUNDS = {
5
+ const OPERATION_VOCABULARY_BOUNDS = {
6
6
  operations: 8192,
7
7
  rowChars: 1024,
8
8
  chars: 2_097_152,