@zackbart/connecta 0.37.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/http/hatch.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// entries (8 at most, 16 KiB of JSON at most, with an explicit continuation
|
|
9
9
|
// cursor) and details return at most 32 KiB, cutting only optional parts and
|
|
10
10
|
// listing them in `truncated`; a required parameter or body field is never cut.
|
|
11
|
+
import { Routes } from "../agent-routes.js";
|
|
11
12
|
import { Effect, Schema } from "effect";
|
|
12
13
|
import { lexicalSearchContent } from "../catalog.js";
|
|
13
14
|
import { registerOperationVocabulary } from "../operation-vocabulary.js";
|
|
@@ -18,7 +19,8 @@ import { providerFailureMessage } from "../write-outcome.js";
|
|
|
18
19
|
import { redactionAltered } from "../redaction-marker.js";
|
|
19
20
|
import { refuseWithheldValues } from "../providers/_shared/rest/value-safety.js";
|
|
20
21
|
import { Refusal, toConnectorCallError } from "./errors.js";
|
|
21
|
-
import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
|
|
22
|
+
import { boundWarnings, fieldsDescription, fieldsInput, fieldsMisses, fieldsProblem, selectFields } from "./fields.js";
|
|
23
|
+
import { RESPONSE_BUDGET, responseContract } from "./hatch-response.js";
|
|
22
24
|
import { Integer, isMaxChars, isMinChars } from "./json-schema.js";
|
|
23
25
|
import { boundCursor, StandardPage } from "./paging.js";
|
|
24
26
|
import { Classification, Dispatches, IdempotencyKey, RequestSecrets, Signal } from "./services.js";
|
|
@@ -48,7 +50,6 @@ const Envelope = Schema.Struct({
|
|
|
48
50
|
status: Integer.annotate({ description: "HTTP status." }),
|
|
49
51
|
data: Schema.Unknown.pipe(Schema.annotateKey({ description: "Response body after value safety and the connection's privacy filtering." })),
|
|
50
52
|
});
|
|
51
|
-
const HATCH_FIELDS = fieldsInput("Dot paths to keep in data, applied to each item of an array, after value safety and privacy filtering.");
|
|
52
53
|
function invalid(message) {
|
|
53
54
|
throw new ConnectorCallError("invalid_args", message);
|
|
54
55
|
}
|
|
@@ -88,7 +89,7 @@ function parameters(values) {
|
|
|
88
89
|
if (typeof item !== "string" &&
|
|
89
90
|
typeof item !== "boolean" &&
|
|
90
91
|
!(typeof item === "number" && Number.isFinite(item))) {
|
|
91
|
-
invalid(
|
|
92
|
+
invalid("Each query parameter takes a string, finite number, boolean, or a list of them.");
|
|
92
93
|
}
|
|
93
94
|
return [name, String(item)];
|
|
94
95
|
});
|
|
@@ -160,9 +161,6 @@ export function hatch(options) {
|
|
|
160
161
|
];
|
|
161
162
|
}));
|
|
162
163
|
const Headers = Object.keys(headerFields).length ? Schema.Struct(headerFields) : undefined;
|
|
163
|
-
const Fields = options.fieldsRoot
|
|
164
|
-
? fieldsInput(`Dot paths to keep in data. Envelope fields begin with ${options.fieldsRoot}, e.g. ["${options.fieldsRoot}.id"] or ["${options.fieldsRoot}[].id"], where declared.`)
|
|
165
|
-
: HATCH_FIELDS;
|
|
166
164
|
const Body = options.textBodies
|
|
167
165
|
? Schema.Union([ObjectValue, Schema.Array(Schema.Unknown), Schema.String])
|
|
168
166
|
: undefined;
|
|
@@ -191,6 +189,22 @@ export function hatch(options) {
|
|
|
191
189
|
if (!known(key) || !tool.trim())
|
|
192
190
|
throw new Error(`Hatch named tool for ${key} needs an indexed operation.`);
|
|
193
191
|
}
|
|
192
|
+
// Paths start at the vendor body. The example is a root some operation the
|
|
193
|
+
// tool calls declares: the envelope root, else `id`, else the first declared.
|
|
194
|
+
const fieldsFor = (writes) => {
|
|
195
|
+
const preferred = options.fieldsRoot ?? "id";
|
|
196
|
+
let first;
|
|
197
|
+
for (const [key, verdict] of verdicts) {
|
|
198
|
+
if (verdict === "refuse" || (verdict === "read") === writes)
|
|
199
|
+
continue;
|
|
200
|
+
const roots = index.responseRoots(operations.get(key));
|
|
201
|
+
if (roots?.has(preferred))
|
|
202
|
+
return fieldsInput(fieldsNote(preferred));
|
|
203
|
+
first ??= roots && [...roots][0];
|
|
204
|
+
}
|
|
205
|
+
return fieldsInput(fieldsNote(first ?? preferred));
|
|
206
|
+
};
|
|
207
|
+
const fieldsNote = (example) => fieldsDescription("data", example, `Copy declared paths from ${name}_api_details response.fields. Applied after value safety and privacy filtering.`);
|
|
194
208
|
const verdictOf = (op) => verdicts.get(`${op.method} ${op.path}`) ?? "refuse";
|
|
195
209
|
const refusalOf = (op) => connection.safety.refusal?.({ method: op.method, template: op.path }) ?? options.binding?.refusal?.(op);
|
|
196
210
|
const discoveryRefusals = new Map();
|
|
@@ -244,13 +258,9 @@ export function hatch(options) {
|
|
|
244
258
|
truncated: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Dot paths cut for size, including the data prefix." })),
|
|
245
259
|
}
|
|
246
260
|
: {}),
|
|
247
|
-
|
|
248
|
-
?
|
|
249
|
-
|
|
250
|
-
description: "Reviewed warnings preserved after fields selection.",
|
|
251
|
-
})),
|
|
252
|
-
}
|
|
253
|
-
: {}),
|
|
261
|
+
warnings: Schema.optionalKey(Schema.Array(Schema.String).annotate({
|
|
262
|
+
description: `${options.binding?.warnings ? "Reviewed warnings preserved after fields selection, and " : ""}fields entries that matched nothing in this nonempty response, with the properties declared where matching stopped. At most 10 warnings of 512 characters, then an omitted count.`,
|
|
263
|
+
})),
|
|
254
264
|
});
|
|
255
265
|
const hasTotals = [...pages.values()].some((adapters) => adapters.some((adapter) => adapter.total !== undefined));
|
|
256
266
|
const envelope = pages.size
|
|
@@ -637,6 +647,29 @@ export function hatch(options) {
|
|
|
637
647
|
}));
|
|
638
648
|
const searchTool = tool("search");
|
|
639
649
|
const detailsTool = tool("details");
|
|
650
|
+
// The response contract is fixed for a connection, so each operation's is built once.
|
|
651
|
+
const responses = new Map();
|
|
652
|
+
const responseOf = (op) => {
|
|
653
|
+
if (!responses.has(op.row))
|
|
654
|
+
responses.set(op.row, responseContract(index, op, connection.safety));
|
|
655
|
+
return responses.get(op.row);
|
|
656
|
+
};
|
|
657
|
+
const Response = Schema.optionalKey(Schema.Struct({
|
|
658
|
+
anchor: Schema.Literals(["data", "data[]"]).annotate({
|
|
659
|
+
description: "Paths start at the read result's data; data[] means each item of a list body.",
|
|
660
|
+
}),
|
|
661
|
+
fields: Schema.Array(Schema.String).annotate({
|
|
662
|
+
description: "Selectable declared success-response paths, valid as fields inputs unchanged. Not a readability promise: value safety may still withhold or redact any value at read time.",
|
|
663
|
+
}),
|
|
664
|
+
withheld: Schema.optionalKey(Schema.Array(Schema.String).annotate({
|
|
665
|
+
description: "Best-effort hint: listed paths value safety always withholds or redacts. An unlisted path may still be withheld or redacted.",
|
|
666
|
+
})),
|
|
667
|
+
omitted: Schema.optionalKey(Integer.annotate({
|
|
668
|
+
description: `Declared paths not listed: deeper ones past ${RESPONSE_BUDGET.paths} paths or ${RESPONSE_BUDGET.chars} chars, or over ${RESPONSE_BUDGET.pathChars} chars.`,
|
|
669
|
+
})),
|
|
670
|
+
}).annotate({
|
|
671
|
+
description: "Where declared response values are, not whether they are readable. Absent when the operation declares no response properties or is refused.",
|
|
672
|
+
}));
|
|
640
673
|
const tools = [
|
|
641
674
|
read(connection, {
|
|
642
675
|
name: searchTool,
|
|
@@ -756,7 +789,7 @@ export function hatch(options) {
|
|
|
756
789
|
}),
|
|
757
790
|
read(connection, {
|
|
758
791
|
name: detailsTool,
|
|
759
|
-
description: `Read one ${title} API operation's request contract and refusal, including methods outside the call schemas. Reads no account data.`,
|
|
792
|
+
description: `Read one ${title} API operation's request contract, response fields and refusal, including methods outside the call schemas. Reads no account data.`,
|
|
760
793
|
input: Schema.Struct({
|
|
761
794
|
method: Method.annotate({ description: "The operation's HTTP method." }),
|
|
762
795
|
path: Path.annotate({ description: "The path template from search, or a concrete path." }),
|
|
@@ -776,6 +809,7 @@ export function hatch(options) {
|
|
|
776
809
|
body: Schema.optionalKey(ObjectValue.annotate({
|
|
777
810
|
description: "Body content type and schema. When paged: this page's properties, the required names it delivers or chunks, and the schema's other keywords on the page that carries them.",
|
|
778
811
|
})),
|
|
812
|
+
response: Response,
|
|
779
813
|
verdict: Schema.Literals(["read", "write", "destructive", "refuse"]),
|
|
780
814
|
...Routing,
|
|
781
815
|
revision: Schema.String,
|
|
@@ -798,8 +832,12 @@ export function hatch(options) {
|
|
|
798
832
|
const refusal = discoveryRefusal(op);
|
|
799
833
|
return { op, contract: { ...index.contract(op, args.param), ...(refusal ? { refusal } : {}) } };
|
|
800
834
|
});
|
|
801
|
-
const
|
|
802
|
-
const
|
|
835
|
+
const verdict = discoveryVerdictOf(op);
|
|
836
|
+
const tail = { verdict, ...route(op), revision: index.revision };
|
|
837
|
+
// The first page of a whole-operation answer carries the response contract; param pages do not.
|
|
838
|
+
const response = args.param === undefined && args.cursor === undefined && verdict !== "refuse" ? responseOf(op) : undefined;
|
|
839
|
+
const head = response ? { response } : {};
|
|
840
|
+
const whole = { ...contract, ...head, ...tail };
|
|
803
841
|
if (args.cursor === undefined && bytes(whole) <= DETAILS_BUDGET_BYTES)
|
|
804
842
|
return whole;
|
|
805
843
|
// Bounded pages: every parameter and body property arrives whole on some page, and a
|
|
@@ -874,6 +912,7 @@ export function hatch(options) {
|
|
|
874
912
|
},
|
|
875
913
|
},
|
|
876
914
|
}),
|
|
915
|
+
...head,
|
|
877
916
|
...tail,
|
|
878
917
|
...(chunk ? { chunk } : {}),
|
|
879
918
|
...(truncated.length ? { truncated } : {}),
|
|
@@ -1105,7 +1144,7 @@ export function hatch(options) {
|
|
|
1105
1144
|
const roots = index.responseRoots(op);
|
|
1106
1145
|
if (!roots)
|
|
1107
1146
|
invalid(`${method} ${op.path} declares no response properties, so fields cannot narrow it.`);
|
|
1108
|
-
const problem = fieldsProblem(args.fields, roots, "data");
|
|
1147
|
+
const problem = fieldsProblem(args.fields, roots, "data", `Examples are withheld for API operations; read ${tool("details")} for the contract.`);
|
|
1109
1148
|
if (problem) {
|
|
1110
1149
|
if (options.fieldsRoot && roots.has(options.fieldsRoot))
|
|
1111
1150
|
invalid(`${problem.message} Use fields:["${options.fieldsRoot}.id"] or fields:["${options.fieldsRoot}[].id"] for declared record or list fields.`);
|
|
@@ -1141,6 +1180,7 @@ export function hatch(options) {
|
|
|
1141
1180
|
dispatches,
|
|
1142
1181
|
withholds: connection.safety.withholdsErrors,
|
|
1143
1182
|
signal: yield* Signal,
|
|
1183
|
+
routes: yield* Routes,
|
|
1144
1184
|
});
|
|
1145
1185
|
const diagnostic = providerFailureMessage(error, `${error.message} ${idempotency.header}: ${key}.`);
|
|
1146
1186
|
return yield* Effect.fail(new Refusal({
|
|
@@ -1177,16 +1217,24 @@ export function hatch(options) {
|
|
|
1177
1217
|
in: adapter.in,
|
|
1178
1218
|
};
|
|
1179
1219
|
}
|
|
1220
|
+
const answered = answer.data === undefined ? null : answer.data;
|
|
1221
|
+
// Roots were checked before dispatch; the pinned index cannot say whether a
|
|
1222
|
+
// deeper level is closed, so a nested miss is reported, never refused.
|
|
1223
|
+
const warnings = boundWarnings([
|
|
1224
|
+
...(options.binding?.warnings && "warnings" in answer ? (answer.warnings ?? []) : []),
|
|
1225
|
+
...fieldsMisses(answered, args.fields, index
|
|
1226
|
+
.fields(op)
|
|
1227
|
+
.filter((field) => field.in === "response")
|
|
1228
|
+
.map((field) => field.path)),
|
|
1229
|
+
]);
|
|
1180
1230
|
return {
|
|
1181
1231
|
status: answer.status,
|
|
1182
|
-
data: selectFields(
|
|
1232
|
+
data: selectFields(answered, args.fields),
|
|
1183
1233
|
...(page ? { page } : {}),
|
|
1184
1234
|
...(options.binding?.truncated && "truncated" in answer && answer.truncated?.length
|
|
1185
1235
|
? { truncated: answer.truncated }
|
|
1186
1236
|
: {}),
|
|
1187
|
-
...(
|
|
1188
|
-
? { warnings: answer.warnings }
|
|
1189
|
-
: {}),
|
|
1237
|
+
...(warnings.length ? { warnings } : {}),
|
|
1190
1238
|
...(key ? { idempotencyKey: key } : {}),
|
|
1191
1239
|
};
|
|
1192
1240
|
});
|
|
@@ -1208,7 +1256,7 @@ export function hatch(options) {
|
|
|
1208
1256
|
if (nonGetReads.length)
|
|
1209
1257
|
readFields["body"] = Schema.optionalKey((Body ?? ObjectValue).annotate({ description: "Body for a reviewed read-only POST." }));
|
|
1210
1258
|
readFields["query"] = Schema.optionalKey(Query);
|
|
1211
|
-
readFields["fields"] = Schema.optionalKey(
|
|
1259
|
+
readFields["fields"] = Schema.optionalKey(fieldsFor(false));
|
|
1212
1260
|
if (Headers)
|
|
1213
1261
|
readFields["headers"] = Schema.optionalKey(Headers);
|
|
1214
1262
|
if (options.pageCursor === "argument")
|
|
@@ -1235,7 +1283,7 @@ export function hatch(options) {
|
|
|
1235
1283
|
body: Schema.optionalKey((Body ?? Schema.Union([ObjectValue, Schema.Array(Schema.Unknown)])).annotate({
|
|
1236
1284
|
description: "Request body as JSON fields, named as details lists them; form operations are encoded for you.",
|
|
1237
1285
|
})),
|
|
1238
|
-
fields: Schema.optionalKey(
|
|
1286
|
+
fields: Schema.optionalKey(fieldsFor(true)),
|
|
1239
1287
|
};
|
|
1240
1288
|
if (Headers)
|
|
1241
1289
|
writeFields["headers"] = Schema.optionalKey(Headers);
|
|
@@ -1265,6 +1313,22 @@ export function hatch(options) {
|
|
|
1265
1313
|
run: (args) => run("write", args),
|
|
1266
1314
|
}));
|
|
1267
1315
|
}
|
|
1316
|
+
// A read search may hand back as a ready call: GET by default, no path
|
|
1317
|
+
// template, no required parameter or body, and served by the read tool
|
|
1318
|
+
// itself rather than a named tool. Anything an agent must choose or fill in
|
|
1319
|
+
// stays behind search and details.
|
|
1320
|
+
const readTool = tool("read");
|
|
1321
|
+
const ready = (op) => {
|
|
1322
|
+
if (op.method !== "GET" || defaultRead !== "GET" || op.path.includes("{"))
|
|
1323
|
+
return undefined;
|
|
1324
|
+
const routed = route(op);
|
|
1325
|
+
if (routed.tool !== readTool || routed.named)
|
|
1326
|
+
return undefined;
|
|
1327
|
+
const contract = index.contract(op);
|
|
1328
|
+
return contract.parameters.some((param) => param.required) || contract.body?.required
|
|
1329
|
+
? undefined
|
|
1330
|
+
: { path: op.path, summary: op.summary.slice(0, SEARCH_BUDGET.summaryChars), operationId: op.operationId };
|
|
1331
|
+
};
|
|
1268
1332
|
registerOperationVocabulary(tools[0], function* () {
|
|
1269
1333
|
for (const op of index.ops) {
|
|
1270
1334
|
const verdict = discoveryVerdictOf(op);
|
|
@@ -1272,6 +1336,7 @@ export function hatch(options) {
|
|
|
1272
1336
|
text: `${op.path.slice(0, 384)} ${op.operationId.slice(0, 128)} ${op.summary.slice(0, 384)} ${op.tag.slice(0, 128)}`,
|
|
1273
1337
|
route: verdict === "refuse" ? searchTool : route(op).tool,
|
|
1274
1338
|
write: verdict !== "read",
|
|
1339
|
+
ready: verdict === "read" ? ready(op) : undefined,
|
|
1275
1340
|
};
|
|
1276
1341
|
}
|
|
1277
1342
|
});
|
package/dist/http/json-schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
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
|
package/dist/http/oauth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Refresh-once middleware
|
|
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
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// The grant itself (storage, coordination across isolates, the token
|
|
25
25
|
// request, the request-scope latch) stays in src/auth/.
|
|
26
|
+
import { Routes } from "../agent-routes.js";
|
|
26
27
|
import { Cause, Context, Effect, Option } from "effect";
|
|
27
28
|
import * as HttpClient from "effect/http/HttpClient";
|
|
28
29
|
import * as HttpClientResponse from "effect/http/HttpClientResponse";
|
|
@@ -70,7 +71,7 @@ export function refreshOnce(guarded, options) {
|
|
|
70
71
|
const error = new ConnectorCallError("connector_call_failed", "The OAuth challenge exceeded its response ceiling.", { retryable: false });
|
|
71
72
|
const refusal = new Refusal({
|
|
72
73
|
error: !readRequest(entry ?? {}) && options?.writeUnauthorized
|
|
73
|
-
? options.writeUnauthorized({ _tag: "Failed", failure: error })
|
|
74
|
+
? options.writeUnauthorized({ _tag: "Failed", failure: error }, yield* Routes)
|
|
74
75
|
: error,
|
|
75
76
|
});
|
|
76
77
|
const dispatch = entry ? (yield* Dispatches).find((item) => item.request === entry) : undefined;
|
|
@@ -125,7 +126,7 @@ export function refreshOnce(guarded, options) {
|
|
|
125
126
|
};
|
|
126
127
|
// This is still the resource request's own 401. The common failure
|
|
127
128
|
// mapper attaches its provenance; settleWrite decides the outcome.
|
|
128
|
-
return yield* Effect.fail(new Refusal({ error: options.writeUnauthorized(facts) }));
|
|
129
|
+
return yield* Effect.fail(new Refusal({ error: options.writeUnauthorized(facts, yield* Routes) }));
|
|
129
130
|
}
|
|
130
131
|
return HttpClientResponse.fromWeb(request, new Response(reply.text ?? "", { status: reply.status, headers: reply.headers }));
|
|
131
132
|
}
|
package/dist/http/paging.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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) =>
|
|
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
|
-
}
|
package/dist/http/provider.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// Internal provider kit
|
|
1
|
+
// Internal provider kit. Unreachable from src/index.ts.
|
|
2
|
+
import { routePhrases, routesOf } from "../agent-routes.js";
|
|
3
|
+
import { credentialFailureClass } from "../credential-verification.js";
|
|
2
4
|
import { Context, Effect, Option } from "effect";
|
|
3
5
|
import { api } from "../connectors/api.js";
|
|
4
6
|
import { OAUTH_CHALLENGE_MAX_BYTES } from "../auth/renewable-grant.js";
|
|
@@ -12,6 +14,7 @@ import { guardedClient } from "./client.js";
|
|
|
12
14
|
import { errorMeanings, Refusal, toConnectorCallError, withheldDefect, } from "./errors.js";
|
|
13
15
|
import { hatch } from "./hatch.js";
|
|
14
16
|
import { takesIdempotencyKey } from "./tools.js";
|
|
17
|
+
import { registerSearchSynonyms } from "../operation-vocabulary.js";
|
|
15
18
|
import { Grant, grantFromOAuth, refreshOnce } from "./oauth.js";
|
|
16
19
|
import { resultSafety } from "./safety.js";
|
|
17
20
|
import { Classification, Dispatches, Signal, WriteCapacity, fromConnectorContext, } from "./services.js";
|
|
@@ -43,7 +46,19 @@ const missingGrant = {
|
|
|
43
46
|
};
|
|
44
47
|
/** An ordinary Promise connector factory; Effect remains inside the kit. */
|
|
45
48
|
export function defineApiProvider(definition) {
|
|
46
|
-
const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, ...metadata } = definition;
|
|
49
|
+
const { displayTitle, description, authScope, vendor, baseUrl, maxResponseBytes, timeoutMs, headers, admit, transport, defaultCallAdmission, pageSize: declaredPageSize, idempotencyKey, credential, oauth, oauthWrite401, grant, grantRenewal, authenticate, errors, safety, result, inBandRefusals, probe, guide, transformGuide, hatch: declaredHatch, tools, tagConnector, searchSynonyms, ...metadata } = definition;
|
|
50
|
+
if (searchSynonyms !== undefined &&
|
|
51
|
+
!(Array.isArray(searchSynonyms) &&
|
|
52
|
+
searchSynonyms.length > 0 &&
|
|
53
|
+
searchSynonyms.length <= 32 &&
|
|
54
|
+
searchSynonyms.every((group) => Array.isArray(group) &&
|
|
55
|
+
group.length > 1 &&
|
|
56
|
+
group.length <= 8 &&
|
|
57
|
+
new Set(group).size === group.length &&
|
|
58
|
+
group.every((word) => /^[a-z][a-z0-9]{0,31}$/.test(word))))) {
|
|
59
|
+
throw new Error(`${metadata.title} searchSynonyms must be 1–32 groups of 2–8 distinct lowercase words.`);
|
|
60
|
+
}
|
|
61
|
+
const synonyms = searchSynonyms?.map((group) => [...group]);
|
|
47
62
|
if (credential !== undefined && oauth !== undefined) {
|
|
48
63
|
throw new Error(`${metadata.title} declares both credential and oauth; declare one authentication method.`);
|
|
49
64
|
}
|
|
@@ -125,11 +140,12 @@ export function defineApiProvider(definition) {
|
|
|
125
140
|
? refreshOnce(guarded, oauthWrite401
|
|
126
141
|
? {
|
|
127
142
|
challengeMaxBytes: OAUTH_CHALLENGE_MAX_BYTES,
|
|
128
|
-
writeUnauthorized: (renewal) => unsentAfterUnauthorized(`Connector "${id}"'s API`, renewal._tag === "Renewed"
|
|
143
|
+
writeUnauthorized: (renewal, routes) => unsentAfterUnauthorized(`Connector "${id}"'s API`, renewal._tag === "Renewed"
|
|
129
144
|
? undefined
|
|
130
145
|
: renewal._tag === "Failed"
|
|
131
146
|
? renewal.failure
|
|
132
|
-
:
|
|
147
|
+
: // Configured with the connection, worded during the call it refuses.
|
|
148
|
+
new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — ${routePhrases(routes).authorizeAndOpen(id)}.`), "downstream_oauth_required"),
|
|
133
149
|
}
|
|
134
150
|
: renewalModes)
|
|
135
151
|
: guarded, {
|
|
@@ -164,6 +180,7 @@ export function defineApiProvider(definition) {
|
|
|
164
180
|
dispatches: Context.get(lease.services, Dispatches),
|
|
165
181
|
withholds: connection.safety.withholdsErrors,
|
|
166
182
|
signal,
|
|
183
|
+
routes: routesOf(ctx),
|
|
167
184
|
};
|
|
168
185
|
return runEdge(probeProgram.pipe(Effect.provideService(WriteCapacity, new Map()), Effect.flatMap((data) => Effect.try({
|
|
169
186
|
try: () => {
|
|
@@ -173,7 +190,16 @@ export function defineApiProvider(definition) {
|
|
|
173
190
|
catch: (error) => error,
|
|
174
191
|
}).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)))), Effect.catch((failure) => Effect.sync(() => {
|
|
175
192
|
const error = toConnectorCallError(failure, errors, map);
|
|
176
|
-
|
|
193
|
+
const described = probe.failure?.(error, options);
|
|
194
|
+
// A provider's own verdict wins; a failure it leaves
|
|
195
|
+
// unclassified takes the class of the typed error behind it.
|
|
196
|
+
if (described?.ok || described?.failure)
|
|
197
|
+
return described;
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
message: described?.message ?? error.message,
|
|
201
|
+
failure: credentialFailureClass(error),
|
|
202
|
+
};
|
|
177
203
|
})), Effect.catchDefect((defect) => Effect.die(withheldDefect(defect, map, !!probe.run))), Effect.provideContext(lease.services), Effect.ensuring(Effect.sync(lease.close))), { signal: ctx.signal });
|
|
178
204
|
};
|
|
179
205
|
const builtTools = tools({
|
|
@@ -214,6 +240,8 @@ export function defineApiProvider(definition) {
|
|
|
214
240
|
tools: [...builtTools, ...hatchTools],
|
|
215
241
|
}));
|
|
216
242
|
tagConnector?.(connector, options);
|
|
243
|
+
if (synonyms)
|
|
244
|
+
registerSearchSynonyms(connector, synonyms);
|
|
217
245
|
return connector;
|
|
218
246
|
},
|
|
219
247
|
});
|
package/dist/http/safety.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
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
|
|
@@ -50,6 +50,10 @@ export function resultSafety(options) {
|
|
|
50
50
|
return dropped.size > 0 ? strip(safe) : safe;
|
|
51
51
|
},
|
|
52
52
|
withholdsErrors: (operation) => reviewed?.withholdsErrors(operation) === true,
|
|
53
|
+
withholds: (operation) => {
|
|
54
|
+
const reviewedHint = reviewed?.withholds?.(operation);
|
|
55
|
+
return (path) => path.some((segment) => dropped.has(segment.replace(/\[\]$/, ""))) || reviewedHint?.(path) === true;
|
|
56
|
+
},
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
/** Bind the REST value-safety table to its pinned index; unknown operations never bypass its review. */
|
|
@@ -78,5 +82,9 @@ export function restSafety(reviewed, index) {
|
|
|
78
82
|
const op = indexed(operation);
|
|
79
83
|
return op ? reviewed.withholdsErrors(op) : true;
|
|
80
84
|
},
|
|
85
|
+
withholds: (operation) => {
|
|
86
|
+
const op = indexed(operation);
|
|
87
|
+
return op ? reviewed.withholds(op.method, op.path) : () => false;
|
|
88
|
+
},
|
|
81
89
|
};
|
|
82
90
|
}
|
package/dist/http/services.js
CHANGED
|
@@ -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,
|
|
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
|
-
}
|
package/dist/http/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Provider kit
|
|
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
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// argument validation, sent-secret redaction, withheld handler text. Its
|
|
14
14
|
// boundary also decides every write's outcome, from the core ledger the kit's
|
|
15
15
|
// transport records into, so a kit connector declares `tracksEveryRequest`.
|
|
16
|
+
import { routesOf } from "../agent-routes.js";
|
|
16
17
|
import { Context, Effect, Schema } from "effect";
|
|
17
18
|
import { inLogicalCall } from "../provider-call.js";
|
|
18
19
|
import { registerToolEntry } from "../runtime/call-entry.js";
|
|
@@ -20,11 +21,11 @@ import { ConnectorCallError } from "../errors.js";
|
|
|
20
21
|
import { refuseWithheldValues } from "../providers/_shared/rest/value-safety.js";
|
|
21
22
|
import { causeError, runEdge } from "../runtime/run.js";
|
|
22
23
|
import { recordUnauthorized, Refusal, RequestRefused, toConnectorCallError, withheldDefect, } from "./errors.js";
|
|
23
|
-
import {
|
|
24
|
+
import { ArgumentProblems } from "./argument-problems.js";
|
|
25
|
+
import { fieldsContract, selectFields } from "./fields.js";
|
|
24
26
|
import { catalogSchema, Integer } from "./json-schema.js";
|
|
25
27
|
import { boundCursor, Cursor, StandardPage } from "./paging.js";
|
|
26
28
|
import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
|
|
27
|
-
const inputs = new WeakMap();
|
|
28
29
|
const keyed = new WeakSet();
|
|
29
30
|
/** Whether a kit write builder declared an idempotency key for this tool. */
|
|
30
31
|
export function takesIdempotencyKey(tool) {
|
|
@@ -62,26 +63,20 @@ function originalOrder(decoded, original) {
|
|
|
62
63
|
};
|
|
63
64
|
return copy(decoded, original);
|
|
64
65
|
}
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
/** A Struct's own property names, for `fields`. */
|
|
70
|
-
function propertyNames(schema, what) {
|
|
71
|
-
const fields = schema.fields;
|
|
72
|
-
if (!fields)
|
|
66
|
+
/** Refuse `fields` on anything but a Schema.Struct. */
|
|
67
|
+
function struct(schema, what) {
|
|
68
|
+
if (!schema.fields)
|
|
73
69
|
throw new Error(`fields needs ${what} declared as a Schema.Struct.`);
|
|
74
|
-
return
|
|
70
|
+
return schema;
|
|
75
71
|
}
|
|
76
|
-
/**
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
/** A tool's own check plus every bad `fields` path, refused together before anything is sent. */
|
|
73
|
+
function withFields(contract, check) {
|
|
74
|
+
return (args, problems) => {
|
|
75
|
+
contract.issues(problems, args.fields);
|
|
76
|
+
const { fields: _fields, ...rest } = args;
|
|
77
|
+
check?.(rest, problems);
|
|
78
|
+
};
|
|
80
79
|
}
|
|
81
|
-
const fieldsExample = (names) => {
|
|
82
|
-
const first = [...names][0];
|
|
83
|
-
return first === undefined ? "" : `, e.g. ["${first}"]`;
|
|
84
|
-
};
|
|
85
80
|
/**
|
|
86
81
|
* A read: admitted to call_tool and programs. `fields: true` (decision 0011
|
|
87
82
|
* S5) appends a `fields` argument that narrows the returned record; the input
|
|
@@ -93,17 +88,17 @@ export function read(connection, spec) {
|
|
|
93
88
|
throw new Error("A read review must be nonempty.");
|
|
94
89
|
if (spec.fields === undefined)
|
|
95
90
|
return build(connection, spec, "read", { readOnlyHint: true }, undefined, spec.review);
|
|
96
|
-
const
|
|
97
|
-
|
|
91
|
+
const contract = fieldsContract("result", struct(spec.output, "the output"));
|
|
92
|
+
struct(spec.input, "the input");
|
|
98
93
|
const input = Schema.Struct({
|
|
99
94
|
...spec.input.fields,
|
|
100
|
-
fields: Schema.optionalKey(
|
|
95
|
+
fields: Schema.optionalKey(contract.input),
|
|
101
96
|
});
|
|
102
97
|
return build(connection, {
|
|
103
98
|
...spec,
|
|
104
99
|
input,
|
|
100
|
+
check: withFields(contract, spec.check),
|
|
105
101
|
run: (args) => Effect.gen(function* () {
|
|
106
|
-
yield* checkFields(args.fields, names, "the result");
|
|
107
102
|
const { fields, ...rest } = args;
|
|
108
103
|
return selectFields(yield* spec.run(rest), fields);
|
|
109
104
|
}),
|
|
@@ -141,7 +136,7 @@ export function list(connection, spec) {
|
|
|
141
136
|
if (name in spec.filters)
|
|
142
137
|
throw new Error(`${spec.name} filters cannot declare ${name}; the kit supplies it.`);
|
|
143
138
|
}
|
|
144
|
-
const
|
|
139
|
+
const contract = fieldsContract("item", struct(spec.row, "the row"));
|
|
145
140
|
const input = Schema.Struct({
|
|
146
141
|
...spec.filters,
|
|
147
142
|
limit: Schema.optionalKey(allowedLimit.annotate({
|
|
@@ -149,7 +144,7 @@ export function list(connection, spec) {
|
|
|
149
144
|
default: size,
|
|
150
145
|
})),
|
|
151
146
|
cursor: Schema.optionalKey(Cursor),
|
|
152
|
-
fields: Schema.optionalKey(
|
|
147
|
+
fields: Schema.optionalKey(contract.input),
|
|
153
148
|
});
|
|
154
149
|
return build(connection, {
|
|
155
150
|
name: spec.name,
|
|
@@ -157,8 +152,8 @@ export function list(connection, spec) {
|
|
|
157
152
|
input,
|
|
158
153
|
// `fields` narrows each item, so items render sparse; the envelope keeps items and page required.
|
|
159
154
|
output: Schema.Unknown,
|
|
155
|
+
check: withFields(contract, undefined),
|
|
160
156
|
run: (args) => Effect.gen(function* () {
|
|
161
|
-
yield* checkFields(args.fields, names, "an item");
|
|
162
157
|
// A cursor stays valid when only the projection changes between pages.
|
|
163
158
|
const { fields, cursor: supplied, ...rest } = args;
|
|
164
159
|
const scope = spec.cursorScope === undefined
|
|
@@ -314,7 +309,13 @@ rendered) {
|
|
|
314
309
|
const lease = fromConnectorContext(ctx, verdict);
|
|
315
310
|
const classification = Context.get(lease.services, Classification);
|
|
316
311
|
const dispatches = Context.get(lease.services, Dispatches);
|
|
317
|
-
const map = {
|
|
312
|
+
const map = {
|
|
313
|
+
classification,
|
|
314
|
+
dispatches,
|
|
315
|
+
withholds: connection.safety.withholdsErrors,
|
|
316
|
+
signal: ctx.signal,
|
|
317
|
+
routes: routesOf(ctx),
|
|
318
|
+
};
|
|
318
319
|
return Effect.try({
|
|
319
320
|
try: () => {
|
|
320
321
|
if (verdict === "write" || classification === "write")
|
|
@@ -323,7 +324,22 @@ rendered) {
|
|
|
323
324
|
catch: (error) => error,
|
|
324
325
|
}).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)),
|
|
325
326
|
// Its message can quote the argument; as a defect, api() withholds it.
|
|
326
|
-
Effect.flatMap(() => decode(args).pipe(Effect.orDie)), Effect.map((decoded) => (spec.inputOrder === "original" ? originalOrder(decoded, args) : decoded)), Effect.flatMap((decoded) =>
|
|
327
|
+
Effect.flatMap(() => decode(args).pipe(Effect.orDie)), Effect.map((decoded) => (spec.inputOrder === "original" ? originalOrder(decoded, args) : decoded)), Effect.flatMap((decoded) => {
|
|
328
|
+
if (!spec.check)
|
|
329
|
+
return Effect.succeed(decoded);
|
|
330
|
+
const check = spec.check;
|
|
331
|
+
return Effect.try({
|
|
332
|
+
try: () => {
|
|
333
|
+
const problems = new ArgumentProblems();
|
|
334
|
+
check(decoded, problems);
|
|
335
|
+
const refusal = problems.refusal("Invalid arguments", tool.inputSchema);
|
|
336
|
+
if (refusal)
|
|
337
|
+
throw refusal;
|
|
338
|
+
return decoded;
|
|
339
|
+
},
|
|
340
|
+
catch: (error) => error,
|
|
341
|
+
}).pipe(Effect.catch((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error)));
|
|
342
|
+
}), Effect.flatMap((decoded) => Effect.gen(function* () {
|
|
327
343
|
const key = idempotency?.key(decoded);
|
|
328
344
|
if (key !== undefined && unsendableKey(key)) {
|
|
329
345
|
return yield* Effect.fail(new RequestRefused({
|
|
@@ -350,7 +366,6 @@ rendered) {
|
|
|
350
366
|
})));
|
|
351
367
|
});
|
|
352
368
|
registerToolEntry(tool, (args, ctx) => program(args, ctx, inLogicalCall(ctx)));
|
|
353
|
-
inputs.set(tool, spec.input);
|
|
354
369
|
if (idempotency)
|
|
355
370
|
keyed.add(tool);
|
|
356
371
|
return tool;
|