@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.
- package/CHANGELOG.md +148 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +32 -5
- package/dist/activity.js +32 -6
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +33 -44
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +15 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +77 -39
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +352 -43
- package/dist/catalog-freshness.js +25 -0
- package/dist/catalog-service.js +408 -78
- package/dist/catalog.js +216 -62
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +53 -21
- package/dist/config.js +56 -17
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +113 -22
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +14 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +4 -2
- package/dist/describe-config.js +1 -0
- package/dist/discovery-health.js +26 -11
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +60 -4
- package/dist/errors.js +88 -3
- package/dist/execute.js +491 -202
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +3 -3
- package/dist/executors/quickjs-child.js +14 -5
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +37 -20
- package/dist/executors/quickjs.js +24 -7
- package/dist/guarded-transport.js +2 -0
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +14 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +11 -3
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +167 -64
- package/dist/membership.d.ts +1 -1
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +286 -73
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +177 -55
- package/dist/node.d.ts +7 -0
- package/dist/node.js +85 -4
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +104 -0
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +9 -1
- package/dist/operator-record.js +16 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +1 -14
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +2 -1
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +8 -3
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +2 -1
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +3 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +12 -6
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +3 -6
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +2 -1
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +2 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +3 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +142 -30
- package/dist/resource-read.js +21 -2
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +53 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +83 -35
- package/dist/routes/shared.d.ts +6 -0
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +38 -2
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +213 -57
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +16 -7
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +36 -0
- package/dist/url-safety.js +38 -20
- package/dist/usage-guide.js +14 -13
- package/dist/utility-recovery.js +33 -0
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +60 -15
- package/documentation/architecture.md +152 -43
- package/documentation/auth.md +160 -53
- package/documentation/code-mode.md +396 -119
- package/documentation/deploying.md +35 -10
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +761 -157
- package/documentation/operating.md +91 -4
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +34 -20
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +10 -8
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/sent-secrets.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ import type { ConnectorContext, JsonSchema, Logger } from "./types.js";
|
|
|
2
2
|
/** One payload-free warning per configured connector, regardless of requests. */
|
|
3
3
|
export declare function shortSecretWarning(): (value: string | undefined, logger: Logger) => void;
|
|
4
4
|
export declare class SentSecrets {
|
|
5
|
+
/** Rises whenever this set could match more. Text checked at one revision has not been
|
|
6
|
+
* checked against a later one. */
|
|
7
|
+
revision: number;
|
|
5
8
|
/** Normalized form to its strongest kind. */
|
|
6
9
|
private readonly forms;
|
|
7
10
|
/** A form's leading units that encode only public auth framing; absent is none. */
|
|
@@ -44,6 +47,10 @@ export declare class SentSecrets {
|
|
|
44
47
|
contains(value: string, ignoreCase?: boolean): boolean;
|
|
45
48
|
containsUrl(value: string): boolean;
|
|
46
49
|
header(value: string): void;
|
|
50
|
+
/** A header maintained provider code declares a public identifier: matched whole only. A name
|
|
51
|
+
* alone never establishes this. Whitespace means an auth scheme or other framing, so that value
|
|
52
|
+
* is a credential whatever its header, and the answer is false. */
|
|
53
|
+
publicIdentifierHeader(value: string): boolean;
|
|
47
54
|
request(input: RequestInfo | URL, init?: RequestInit): void;
|
|
48
55
|
/** Serialized wire text about to be returned, stored, or paged, under payload rules. */
|
|
49
56
|
text(value: string): string;
|
|
@@ -95,6 +102,13 @@ export declare class SentSecrets {
|
|
|
95
102
|
/** Credentials alone: later calls keep the private arguments they submit, and cached
|
|
96
103
|
* catalogs never depend on one request's arguments. */
|
|
97
104
|
redactCredentials<T>(value: T): T;
|
|
105
|
+
/** A program's outbound arguments, with what the {@link outbound} check matched replaced,
|
|
106
|
+
* and whether it matched anything: a write that did is refused rather than sent altered.
|
|
107
|
+
* Output keeps the stricter matching; a program never holds what output withheld. */
|
|
108
|
+
filterCredentials<T>(value: T): {
|
|
109
|
+
value: T;
|
|
110
|
+
changed: boolean;
|
|
111
|
+
};
|
|
98
112
|
private idle;
|
|
99
113
|
private scan;
|
|
100
114
|
private matcher;
|
|
@@ -104,6 +118,12 @@ export declare class SentSecrets {
|
|
|
104
118
|
private wholeMatcher;
|
|
105
119
|
private fragments;
|
|
106
120
|
private credentials;
|
|
121
|
+
/** Credentials as an outbound argument can hold them: a whole form, or eight units of what
|
|
122
|
+
* follows its public framing and label. Identifiers and submitted private values are not
|
|
123
|
+
* here. Telling a secret from a label by shape has a limit: fewer than eight units past a
|
|
124
|
+
* label pass, and a public value sharing eight units of the rest, such as a publishable key
|
|
125
|
+
* beside its secret key, still matches. */
|
|
126
|
+
private outbound;
|
|
107
127
|
/** Every form lowercased, for fields such as hosts that reach output lowercased. */
|
|
108
128
|
private folded;
|
|
109
129
|
/** The shortest text that might hold something this set cannot check, which is withheld: a
|
package/dist/sent-secrets.js
CHANGED
|
@@ -24,6 +24,11 @@ const MIN_SECRET_LENGTH = 8;
|
|
|
24
24
|
// units is fragment-indexed. Eight units keeps ordinary words, dates, counts and auth framing out
|
|
25
25
|
// of the index: a public run must repeat eight consecutive units of a secret's form to match.
|
|
26
26
|
const FRAGMENT_LENGTH = MIN_SECRET_LENGTH;
|
|
27
|
+
// A credential's public label, for the outbound argument check alone: a vendor prefix of up to
|
|
28
|
+
// three short segments (`sk_test_`, `github_pat_`, `xoxb-`) or a JWT's header segment. Documents
|
|
29
|
+
// name these and every credential of a kind shares them. Reading a secret's opening as a label
|
|
30
|
+
// only stops runs starting inside it from matching; the rest of the value still matches.
|
|
31
|
+
const PUBLIC_LABEL = /^(?:[Ee]yJ[\w-]*\.|(?:[A-Za-z][A-Za-z0-9]{1,6}[-_]){1,3})/;
|
|
27
32
|
// A private value shorter than this is matched only as a whole field value: a leaf, key, number,
|
|
28
33
|
// boolean or decoded view equal to one of its forms. One to three units recur inside ordinary
|
|
29
34
|
// words, counts and ids, so a substring match would rewrite them everywhere.
|
|
@@ -115,7 +120,7 @@ const TAGS = new Set(("type:text type:image type:audio type:resource type:resour
|
|
|
115
120
|
"resultType:input_required mode:form mode:url method:elicitation/create jsonrpc:2.0 writeOutcome:unknown").split(" "));
|
|
116
121
|
// Outcome flags: a boolean under these names is kept, since a boolean carries one bit whatever
|
|
117
122
|
// produced it. A number or string under them is matched like any other.
|
|
118
|
-
const FACTS = new Set(["isError", "ok", "retryable", "truncated", "hasMore"]);
|
|
123
|
+
const FACTS = new Set(["isError", "ok", "retryable", "truncated", "hasMore", "toolAnswered"]);
|
|
119
124
|
// An error's diagnostics. Its own accessors under these names, such as V8's stack, are read once
|
|
120
125
|
// into its copy, as is whatever its chain answers for them; any other own accessor is never
|
|
121
126
|
// invoked. Accessors its class chain defines are read once too.
|
|
@@ -1096,6 +1101,9 @@ function windows(layer, reach) {
|
|
|
1096
1101
|
return ranges;
|
|
1097
1102
|
}
|
|
1098
1103
|
export class SentSecrets {
|
|
1104
|
+
/** Rises whenever this set could match more. Text checked at one revision has not been
|
|
1105
|
+
* checked against a later one. */
|
|
1106
|
+
revision = 0;
|
|
1099
1107
|
/** Normalized form to its strongest kind. */
|
|
1100
1108
|
forms = new Map();
|
|
1101
1109
|
/** A form's leading units that encode only public auth framing; absent is none. */
|
|
@@ -1132,6 +1140,7 @@ export class SentSecrets {
|
|
|
1132
1140
|
this.widestForm = Math.max(this.widestForm, form.length);
|
|
1133
1141
|
this.smallForms ||= kind < PRIVATE;
|
|
1134
1142
|
this.unicodeForms ||= /[\u0080-\uffff]/.test(form);
|
|
1143
|
+
this.revision++;
|
|
1135
1144
|
this.views = {};
|
|
1136
1145
|
this.memo = new Map();
|
|
1137
1146
|
for (const recipient of this.recipients)
|
|
@@ -1141,6 +1150,7 @@ export class SentSecrets {
|
|
|
1141
1150
|
if (length >= this.unmatched)
|
|
1142
1151
|
return;
|
|
1143
1152
|
this.unmatched = length;
|
|
1153
|
+
this.revision++;
|
|
1144
1154
|
this.views = {};
|
|
1145
1155
|
this.memo = new Map();
|
|
1146
1156
|
for (const recipient of this.recipients)
|
|
@@ -1278,6 +1288,17 @@ export class SentSecrets {
|
|
|
1278
1288
|
else
|
|
1279
1289
|
this.secret(framed[1]);
|
|
1280
1290
|
}
|
|
1291
|
+
/** A header maintained provider code declares a public identifier: matched whole only. A name
|
|
1292
|
+
* alone never establishes this. Whitespace means an auth scheme or other framing, so that value
|
|
1293
|
+
* is a credential whatever its header, and the answer is false. */
|
|
1294
|
+
publicIdentifierHeader(value) {
|
|
1295
|
+
if (/\s/.test(value)) {
|
|
1296
|
+
this.header(value);
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
this.identifier(value);
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1281
1302
|
request(input, init) {
|
|
1282
1303
|
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
1283
1304
|
for (const [name, value] of headers) {
|
|
@@ -1412,6 +1433,16 @@ export class SentSecrets {
|
|
|
1412
1433
|
redactCredentials(value) {
|
|
1413
1434
|
return this.credentials().redact(value, "envelope");
|
|
1414
1435
|
}
|
|
1436
|
+
/** A program's outbound arguments, with what the {@link outbound} check matched replaced,
|
|
1437
|
+
* and whether it matched anything: a write that did is refused rather than sent altered.
|
|
1438
|
+
* Output keeps the stricter matching; a program never holds what output withheld. */
|
|
1439
|
+
filterCredentials(value) {
|
|
1440
|
+
const outbound = this.outbound();
|
|
1441
|
+
if (outbound.idle())
|
|
1442
|
+
return { value, changed: false };
|
|
1443
|
+
const filtered = outbound.project(value, outbound.scan(false));
|
|
1444
|
+
return { value: filtered.value, changed: filtered.changed };
|
|
1445
|
+
}
|
|
1415
1446
|
idle() {
|
|
1416
1447
|
return !this.forms.size && this.unmatched === Infinity;
|
|
1417
1448
|
}
|
|
@@ -1453,6 +1484,23 @@ export class SentSecrets {
|
|
|
1453
1484
|
}
|
|
1454
1485
|
return this.views.credentials;
|
|
1455
1486
|
}
|
|
1487
|
+
/** Credentials as an outbound argument can hold them: a whole form, or eight units of what
|
|
1488
|
+
* follows its public framing and label. Identifiers and submitted private values are not
|
|
1489
|
+
* here. Telling a secret from a label by shape has a limit: fewer than eight units past a
|
|
1490
|
+
* label pass, and a public value sharing eight units of the rest, such as a publishable key
|
|
1491
|
+
* beside its secret key, still matches. */
|
|
1492
|
+
outbound() {
|
|
1493
|
+
if (!this.views.outbound) {
|
|
1494
|
+
const view = new SentSecrets();
|
|
1495
|
+
for (const [form, kind] of this.forms) {
|
|
1496
|
+
const frame = this.frames.get(form) ?? 0;
|
|
1497
|
+
if (kind === CREDENTIAL && frame !== Infinity)
|
|
1498
|
+
view.store(form, kind, frame + (PUBLIC_LABEL.exec(form.slice(frame))?.[0].length ?? 0));
|
|
1499
|
+
}
|
|
1500
|
+
this.views.outbound = view;
|
|
1501
|
+
}
|
|
1502
|
+
return this.views.outbound;
|
|
1503
|
+
}
|
|
1456
1504
|
/** Every form lowercased, for fields such as hosts that reach output lowercased. */
|
|
1457
1505
|
folded() {
|
|
1458
1506
|
if (!this.views.folded) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed sentence that marks downstream guidance as third-party. A prefix has
|
|
3
|
+
* no closing delimiter to forge, and connector IDs are deployment-authored.
|
|
4
|
+
*/
|
|
5
|
+
export function downstreamOrigin(connectorId) {
|
|
6
|
+
return `Third-party guidance from connector ${JSON.stringify(connectorId.slice(0, 128))}.`;
|
|
7
|
+
}
|
|
8
|
+
/** Downstream description text on one line, so it cannot begin a new listing entry. */
|
|
9
|
+
export function labeledDownstreamDescription(connectorId, description) {
|
|
10
|
+
return `${downstreamOrigin(connectorId)} ${description.replace(/\s+/g, " ").trim()}`;
|
|
11
|
+
}
|
|
12
|
+
const SKILL_RESOURCE = /^skill:\/\/|\/SKILL\.md(?:[?#]|$)/i;
|
|
13
|
+
/** Whether a downstream resource URI addresses skill instructions rather than ordinary data. */
|
|
14
|
+
export function isSkillResource(uri) {
|
|
15
|
+
return SKILL_RESOURCE.test(uri);
|
|
16
|
+
}
|
package/dist/skills.js
CHANGED
|
@@ -1,41 +1,108 @@
|
|
|
1
1
|
import { META_ROUTES, routeNames } from "./agent-routes.js";
|
|
2
|
+
import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
|
|
2
3
|
import { bytesToHex, utf8Length } from "./bytes.js";
|
|
3
4
|
import { duringViewEffect } from "./runtime/call.js";
|
|
4
5
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
5
6
|
import { upwardStep } from "./upward.js";
|
|
6
7
|
import { authCall, authInvoke, authFlow, authMethod, registerAuth, registerAuthMethod, authOwnMethod, } from "./auth/native.js";
|
|
7
8
|
import { USAGE_SKILL } from "./usage-guide.js";
|
|
9
|
+
import { downstreamOrigin, labeledDownstreamDescription } from "./skill-origin.js";
|
|
8
10
|
import { boundedEchoText, ConnectorCallError } from "./errors.js";
|
|
9
11
|
import { Effect } from "effect";
|
|
10
12
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
11
13
|
import { closeScopeOnExit } from "./runtime/connector-scope.js";
|
|
12
14
|
import { sentSecretsForRequest } from "./sent-secrets.js";
|
|
15
|
+
/** Trusted diagnostics constructed from the caller-visible local registry. */
|
|
16
|
+
export class SkillLookupError extends ConnectorCallError {
|
|
17
|
+
}
|
|
13
18
|
// The meta instructions and built-in guides use the meta names: built-in guides
|
|
14
19
|
// are byte-identical across deployments, and the eval code arm replaces usage.
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const WRITE_POLICY = "This endpoint permits writes, subject to connector restrictions; the host owns approval. Never resend a write to see its output.";
|
|
21
|
+
const READ_ONLY_POLICY = "This endpoint can't perform writes; no write route exists here.";
|
|
22
|
+
const RECOVERY = "On errors and truncated results, follow nextAction; give auth links to the user or operator.";
|
|
23
|
+
/** A guide can name a write step, so its notice says what happens to one. */
|
|
24
|
+
const READ_ONLY_GUIDE_NOTICE = "This endpoint can't perform writes; guide steps that change data will be refused. No write route exists on this endpoint.";
|
|
25
|
+
const policy = (readOnly) => (readOnly ? READ_ONLY_POLICY : WRITE_POLICY);
|
|
26
|
+
/** Connector ids in instructions; execute_code's inventory carries the rest. */
|
|
27
|
+
const INSTRUCTION_CONNECTORS_MAX_BYTES = 120;
|
|
28
|
+
/** Ids in roster order, then an exact count once they pass the byte cap. Ids are ASCII. */
|
|
29
|
+
function connectorList(ids) {
|
|
30
|
+
if (ids.length === 0)
|
|
31
|
+
return "none";
|
|
32
|
+
const all = ids.join(", ");
|
|
33
|
+
if (all.length <= INSTRUCTION_CONNECTORS_MAX_BYTES)
|
|
34
|
+
return all;
|
|
35
|
+
for (let shown = ids.length - 1; shown > 0; shown--) {
|
|
36
|
+
const line = [...ids.slice(0, shown), `+${ids.length - shown} more`].join(", ");
|
|
37
|
+
if (line.length <= INSTRUCTION_CONNECTORS_MAX_BYTES)
|
|
38
|
+
return line;
|
|
39
|
+
}
|
|
40
|
+
return `+${ids.length} more`;
|
|
41
|
+
}
|
|
24
42
|
/**
|
|
25
|
-
* The instructions one endpoint serves:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
43
|
+
* The instructions one endpoint serves: pool scope, what the endpoint reaches,
|
|
44
|
+
* routing, write policy, recovery. The description comes from deployment config
|
|
45
|
+
* for the pool the request was admitted through. `connectorIds` is that
|
|
46
|
+
* request's admitted roster, the same one execute_code's inventory discloses:
|
|
47
|
+
* configured ids only, in registry order, with no catalog, call or auth state.
|
|
48
|
+
* A native pool omits them because its tool names already show them.
|
|
30
49
|
*/
|
|
31
|
-
export function endpointInstructions(description, native) {
|
|
32
|
-
const
|
|
33
|
-
|
|
50
|
+
export function endpointInstructions(description, native, readOnly, connectorIds, readRoute = false) {
|
|
51
|
+
const gateway = `Gateway to connected services: ${connectorList(connectorIds)}. One known read: ${META_ROUTES.read}. ${readOnly ? "" : `One known write: ${META_ROUTES.write}. `}`;
|
|
52
|
+
const instructions = native
|
|
53
|
+
? nativeInstructions(native.executeCode, readOnly)
|
|
54
|
+
: readRoute
|
|
55
|
+
? `${gateway}Other read work: one ${READ_PROGRAM_SOURCE} program that searches, calls and returns the answer, with no write approval. ${readOnly ? "" : `Programs that write: ${META_ROUTES.program}. ${META_ROUTES.search} finds a write's address. `}${policy(readOnly)} ${RECOVERY}`
|
|
56
|
+
: `${gateway}Everything else: one ${META_ROUTES.program} program that searches, calls and returns the answer. ${policy(readOnly)} ${RECOVERY}`;
|
|
57
|
+
return description === undefined ? instructions : `${description} ${instructions}`;
|
|
34
58
|
}
|
|
35
59
|
/** Instructions for pools whose host searches the listed native tools. */
|
|
36
|
-
export function nativeInstructions(executeCode) {
|
|
60
|
+
export function nativeInstructions(executeCode, readOnly = false) {
|
|
37
61
|
const routes = routeNames(executeCode ? "native-code" : "native");
|
|
38
|
-
return `
|
|
62
|
+
return `Call known tools directly; ${executeCode ? `combine several in ${routes.program} using each tool's Program address.` : "find others with your host's search."} ${policy(readOnly)} ${RECOVERY} Tools missing: ask the operator to repair, then reconnect.`;
|
|
63
|
+
}
|
|
64
|
+
/** The `capabilities.extensions` key carrying {@link endpointExtension}. */
|
|
65
|
+
export const ENDPOINT_EXTENSION = "dev.connecta/endpoint";
|
|
66
|
+
const inventoryWords = (text) => text
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
/** Text that only respells what its entry already shows ("Google Calendar" for google_calendar) adds nothing. */
|
|
71
|
+
export function addsInformation(shown, text) {
|
|
72
|
+
const words = inventoryWords(text);
|
|
73
|
+
const shownWords = inventoryWords(shown);
|
|
74
|
+
return words.join("") !== shownWords.join("") && words.some((word) => !shownWords.includes(word));
|
|
75
|
+
}
|
|
76
|
+
/** Clips on a whole token, so text with no token that fits leaves nothing to show. */
|
|
77
|
+
export const inventoryNote = (text, maxBytes) => {
|
|
78
|
+
const clipped = boundedEchoText(text, maxBytes);
|
|
79
|
+
return clipped === "…" ? undefined : clipped;
|
|
80
|
+
};
|
|
81
|
+
/** A connector's bounded title for inventories, or undefined when it adds nothing to the id or cannot fit. */
|
|
82
|
+
export function connectorInventoryTitle(connector) {
|
|
83
|
+
const title = connector.title?.replace(/\s+/g, " ").trim();
|
|
84
|
+
return title && addsInformation(connector.id, title) ? inventoryNote(title, 45) : undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Facts about the endpoint this request reached, for a client composing its own
|
|
88
|
+
* host-specific guidance. Built only from the admitted scope and configuration:
|
|
89
|
+
* `connectors` is the caller's registry view, the roster `execute_code` and
|
|
90
|
+
* `authorize_connector` already describe, and nothing here loads a catalog.
|
|
91
|
+
*/
|
|
92
|
+
export function endpointExtension(connectors, native, executeCode, readOnly) {
|
|
93
|
+
return {
|
|
94
|
+
surface: native ? "native" : "meta",
|
|
95
|
+
executeCode,
|
|
96
|
+
readOnly,
|
|
97
|
+
connectors: connectors.map((connector) => {
|
|
98
|
+
const title = connectorInventoryTitle(connector);
|
|
99
|
+
return {
|
|
100
|
+
id: connector.id,
|
|
101
|
+
...(title ? { title } : {}),
|
|
102
|
+
...(connectorGuideRequired(connector) ? { readFirst: connectorSkillName(connector.id) } : {}),
|
|
103
|
+
};
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
39
106
|
}
|
|
40
107
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
41
108
|
export function hasConnectorGuides(connectors) {
|
|
@@ -97,14 +164,19 @@ const CONNECTOR_SKILL_PREFIX = "connector:";
|
|
|
97
164
|
export function connectorSkillName(connectorId) {
|
|
98
165
|
return `${CONNECTOR_SKILL_PREFIX}${connectorId}`;
|
|
99
166
|
}
|
|
100
|
-
/** The
|
|
101
|
-
|
|
167
|
+
/** The guide as authored, or undefined when the connector declares none (or a blank one). */
|
|
168
|
+
function authoredGuide(connector) {
|
|
102
169
|
const guide = connector.usageGuide;
|
|
103
170
|
const content = typeof guide === "string" ? guide : guide?.content;
|
|
104
|
-
|
|
171
|
+
return !content || content.trim() === "" ? undefined : content;
|
|
172
|
+
}
|
|
173
|
+
/** The connector's guide with its connection's read-only notice, or undefined when it has none. */
|
|
174
|
+
export function connectorGuide(connector) {
|
|
175
|
+
const content = authoredGuide(connector);
|
|
176
|
+
if (content === undefined)
|
|
105
177
|
return undefined;
|
|
106
178
|
return connector.readOnly === true
|
|
107
|
-
? `
|
|
179
|
+
? `This connection can't perform writes; guide steps that change data will be refused. No write route exists for this connection.\n\n${content}`
|
|
108
180
|
: content;
|
|
109
181
|
}
|
|
110
182
|
/** Discovery budget for one connector-guide summary, including an ellipsis. */
|
|
@@ -338,49 +410,116 @@ const PRIVATE = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
|
|
|
338
410
|
function localRecord(name, uri, description, content, aliases) {
|
|
339
411
|
return { entry: { uri, frontmatter: { name, description }, resources: "dynamic" }, content, aliases };
|
|
340
412
|
}
|
|
413
|
+
/** A local record's text after the frontmatter block Connecta wrote ahead of it. */
|
|
414
|
+
function localSkillBody(text) {
|
|
415
|
+
const close = text.indexOf("\n---\n", 3);
|
|
416
|
+
return close === -1 ? text : text.slice(close + "\n---\n".length).replace(/^\n/, "");
|
|
417
|
+
}
|
|
341
418
|
/** JSON frontmatter is YAML too; no runtime provider imports or filesystem reads. */
|
|
342
419
|
function validSkillName(name) {
|
|
343
420
|
return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
344
421
|
}
|
|
345
|
-
|
|
346
|
-
|
|
422
|
+
/** Resolve guide names once from the admitted connector roster, including collisions. */
|
|
423
|
+
export function resolvedConnectorGuides(connectors) {
|
|
424
|
+
return runEdge(resolvedConnectorGuidesNative(connectors));
|
|
425
|
+
}
|
|
426
|
+
const resolvedConnectorGuidesNative = /* @__PURE__ */ registerAuth(resolvedConnectorGuides, function (connectors) {
|
|
427
|
+
return authFlow(function* () {
|
|
428
|
+
const allocated = new Set(connectors.map((connector) => connector.id));
|
|
429
|
+
return yield* upwardStep(Effect.forEach(connectors
|
|
430
|
+
.filter((connector) => connectorGuide(connector) !== undefined)
|
|
431
|
+
.sort((a, b) => Number(connectorGuideRequired(b)) - Number(connectorGuideRequired(a)) ||
|
|
432
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), (connector) => authFlow(function* () {
|
|
433
|
+
const digest = validSkillName(connector.id)
|
|
434
|
+
? ""
|
|
435
|
+
: bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)]))));
|
|
436
|
+
const legacy = `connector-${digest.slice(0, 48)}`;
|
|
437
|
+
const slug = connector.id
|
|
438
|
+
.toLowerCase()
|
|
439
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
440
|
+
.replace(/^-|-$/g, "")
|
|
441
|
+
.slice(0, 42)
|
|
442
|
+
.replace(/-$/g, "") || "connector";
|
|
443
|
+
let name = connector.id;
|
|
444
|
+
if (!validSkillName(name)) {
|
|
445
|
+
const base = `${slug}-${digest.slice(0, 12)}`;
|
|
446
|
+
name = base;
|
|
447
|
+
let suffix = 1;
|
|
448
|
+
while (allocated.has(name))
|
|
449
|
+
name = `${base}-${suffix++}`;
|
|
450
|
+
allocated.add(name);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
connector,
|
|
454
|
+
name,
|
|
455
|
+
legacy,
|
|
456
|
+
reference: AVAILABLE_SKILLS.some((skill) => skill.name === name)
|
|
457
|
+
? `skill://connecta/connectors/${name}`
|
|
458
|
+
: name,
|
|
459
|
+
};
|
|
460
|
+
}), { concurrency: 1 }));
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
function localRecords(connectors, readOnly = false) {
|
|
464
|
+
return runEdge(localRecordsNative(connectors, readOnly));
|
|
347
465
|
}
|
|
348
|
-
const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (connectors) {
|
|
466
|
+
const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (connectors, readOnly = false) {
|
|
349
467
|
return authFlow(function* () {
|
|
350
468
|
const builtIns = AVAILABLE_SKILLS.map((skill) => localRecord(skill.name, `skill://connecta/${skill.name}/SKILL.md`, skill.description, skill.content(), [
|
|
351
469
|
skill.name,
|
|
352
470
|
`skill://connecta/${skill.name}`,
|
|
353
471
|
]));
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const name = validSkillName(connector.id)
|
|
359
|
-
? connector.id
|
|
360
|
-
: `connector-${bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)])))).slice(0, 48)}`;
|
|
472
|
+
const identities = yield* upwardStep(authCall(resolvedConnectorGuides, [
|
|
473
|
+
connectors,
|
|
474
|
+
]));
|
|
475
|
+
const guides = identities.map(({ connector, name, legacy }) => {
|
|
361
476
|
const description = connectorGuideSummary(connector);
|
|
362
477
|
const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
|
|
363
|
-
|
|
478
|
+
// One read-only notice: the endpoint's already covers its connections.
|
|
479
|
+
const guide = readOnly
|
|
480
|
+
? `${READ_ONLY_GUIDE_NOTICE}\n\n${authoredGuide(connector)}`
|
|
481
|
+
: connectorGuide(connector);
|
|
482
|
+
const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${guide}`;
|
|
364
483
|
return {
|
|
365
484
|
...localRecord(name, `${root}/SKILL.md`, description, content, [
|
|
366
485
|
connectorSkillName(connector.id),
|
|
486
|
+
...(connector.id.includes(":") ? [] : [connector.id]),
|
|
487
|
+
...(name !== connector.id
|
|
488
|
+
? [legacy, `skill://connecta/connectors/${legacy}`, `skill://connecta/connectors/${legacy}/SKILL.md`]
|
|
489
|
+
: []),
|
|
367
490
|
`skill://connecta/connectors/${encodeURIComponent(connector.id)}`,
|
|
368
491
|
root,
|
|
369
492
|
]),
|
|
370
493
|
connector,
|
|
371
494
|
};
|
|
372
|
-
})
|
|
495
|
+
});
|
|
373
496
|
if (new Set(guides.map((guide) => guide.entry.uri)).size !== guides.length)
|
|
374
497
|
throw new ConnectorCallError("unavailable", "Connector skill slugs collide.");
|
|
375
498
|
return [builtIns[0], ...guides, ...builtIns.slice(1)];
|
|
376
499
|
});
|
|
377
500
|
});
|
|
378
|
-
/**
|
|
379
|
-
function
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
501
|
+
/** Canonical identities win; an alias shared by different guides is ambiguous. */
|
|
502
|
+
function localLookup(records, key) {
|
|
503
|
+
const canonical = new Map();
|
|
504
|
+
for (const record of records) {
|
|
505
|
+
canonical.set(record.entry.uri, record);
|
|
506
|
+
canonical.set(record.entry.uri.slice(0, -"/SKILL.md".length), record);
|
|
507
|
+
const name = record.entry.frontmatter.name;
|
|
508
|
+
if (!record.connector || !AVAILABLE_SKILLS.some((skill) => skill.name === name))
|
|
509
|
+
canonical.set(name, record);
|
|
510
|
+
}
|
|
511
|
+
const aliases = new Map();
|
|
512
|
+
for (const record of records) {
|
|
513
|
+
for (const alias of record.aliases) {
|
|
514
|
+
if (canonical.has(alias))
|
|
515
|
+
continue;
|
|
516
|
+
aliases.set(alias, aliases.has(alias) && aliases.get(alias) !== record ? null : record);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const result = canonical.get(key) ?? aliases.get(key);
|
|
520
|
+
if (result === null)
|
|
521
|
+
throw new ConnectorCallError("invalid_args", "Ambiguous skill alias; use its canonical URI.");
|
|
522
|
+
return result;
|
|
384
523
|
}
|
|
385
524
|
/** Keep the downstream path tree, so relative supporting-file references still resolve. */
|
|
386
525
|
export function downstreamSkillUri(connectorId, uri) {
|
|
@@ -485,6 +624,7 @@ export class SkillsRegistry {
|
|
|
485
624
|
baseUrl;
|
|
486
625
|
options;
|
|
487
626
|
scope;
|
|
627
|
+
cacheHints;
|
|
488
628
|
local;
|
|
489
629
|
snapshot;
|
|
490
630
|
constructor(registry, baseUrl, options = {}) {
|
|
@@ -493,7 +633,11 @@ export class SkillsRegistry {
|
|
|
493
633
|
this.options = options;
|
|
494
634
|
void SkillsRegistry.nativePrograms;
|
|
495
635
|
this.scope = options.requestScope ?? {};
|
|
496
|
-
this.
|
|
636
|
+
this.cacheHints = {
|
|
637
|
+
...PRIVATE,
|
|
638
|
+
ttlMs: registry.listConnectors().some((connector) => connector.downstreamSkills !== undefined) ? 0 : 300_000,
|
|
639
|
+
};
|
|
640
|
+
this.local = authCall(localRecords, [registry.listConnectors(), options.readOnly === true]);
|
|
497
641
|
}
|
|
498
642
|
operation(connector, read) {
|
|
499
643
|
return runEdge(authOwnMethod(SkillsRegistry.prototype, this, "operation", [connector, read]));
|
|
@@ -547,7 +691,7 @@ export class SkillsRegistry {
|
|
|
547
691
|
: name.startsWith(CONNECTOR_SKILL_PREFIX)
|
|
548
692
|
? `Unknown connector "${boundedEchoText(id)}".`
|
|
549
693
|
: `Unknown skill "${boundedEchoText(name)}".`;
|
|
550
|
-
return new
|
|
694
|
+
return new SkillLookupError("not_found", `${message} Available skills: ${available}.`);
|
|
551
695
|
}
|
|
552
696
|
list(cursor) {
|
|
553
697
|
return runEdge(authOwnMethod(SkillsRegistry.prototype, this, "list", [cursor]));
|
|
@@ -574,7 +718,9 @@ export class SkillsRegistry {
|
|
|
574
718
|
return (yield* upwardStep(authInvoke(self, "records", []))).map((record) => ({
|
|
575
719
|
name: record.aliases[0] ?? record.entry.uri,
|
|
576
720
|
uri: record.entry.uri,
|
|
577
|
-
description:
|
|
721
|
+
description: record.files
|
|
722
|
+
? labeledDownstreamDescription(record.connector.id, String(record.entry.frontmatter.description))
|
|
723
|
+
: String(record.entry.frontmatter.description),
|
|
578
724
|
}));
|
|
579
725
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
|
|
580
726
|
}, SkillsRegistry.prototype.summaries);
|
|
@@ -583,10 +729,16 @@ export class SkillsRegistry {
|
|
|
583
729
|
return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
|
|
584
730
|
const record = yield* upwardStep(authInvoke(self, "lookup", [name]));
|
|
585
731
|
const content = (yield* upwardStep(authInvoke(self, "read", [record.entry.uri]))).contents[0];
|
|
732
|
+
// Connecta prepends frontmatter to local guides for native SKILL.md; a
|
|
733
|
+
// tool reader wants only the body. Downstream files keep their bytes.
|
|
734
|
+
if (!record.files && typeof content.text === "string")
|
|
735
|
+
return localSkillBody(content.text);
|
|
736
|
+
const origin = record.files ? `${downstreamOrigin(record.connector.id)}\n\n` : "";
|
|
586
737
|
if (typeof content.text === "string")
|
|
587
|
-
return content.text;
|
|
738
|
+
return origin + content.text;
|
|
588
739
|
try {
|
|
589
|
-
return
|
|
740
|
+
return (origin +
|
|
741
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0))));
|
|
590
742
|
}
|
|
591
743
|
catch {
|
|
592
744
|
throw new ConnectorCallError("unavailable", "Skill instructions are not UTF-8 text.");
|
|
@@ -596,7 +748,7 @@ export class SkillsRegistry {
|
|
|
596
748
|
registerAuthMethod(SkillsRegistry.prototype, "read", function (uri) {
|
|
597
749
|
const self = this;
|
|
598
750
|
return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
|
|
599
|
-
const local = (yield* upwardStep(self.local)
|
|
751
|
+
const local = localLookup(yield* upwardStep(self.local), uri);
|
|
600
752
|
if (local) {
|
|
601
753
|
const record = yield* upwardStep(authCall(withManifest, [local]));
|
|
602
754
|
self.assertSafe([record]);
|
|
@@ -606,7 +758,7 @@ export class SkillsRegistry {
|
|
|
606
758
|
const text = record.connector
|
|
607
759
|
? sentSecretsForRequest(self.scope).redact(record.content)
|
|
608
760
|
: record.content;
|
|
609
|
-
return { ...
|
|
761
|
+
return { ...self.cacheHints, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text }] };
|
|
610
762
|
}
|
|
611
763
|
if (!uri.startsWith("skill://downstream/"))
|
|
612
764
|
throw self.missing(uri);
|
|
@@ -642,7 +794,7 @@ export class SkillsRegistry {
|
|
|
642
794
|
self.options.onRead?.(record.connector, "downstream");
|
|
643
795
|
// Downstream file contents take the request's full payload rules before any reader sees them.
|
|
644
796
|
const clean = sentSecretsForRequest(self.scope).redact(content);
|
|
645
|
-
return { ...
|
|
797
|
+
return { ...self.cacheHints, contents: [{ ...clean, uri }] };
|
|
646
798
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
|
|
647
799
|
}, SkillsRegistry.prototype.read);
|
|
648
800
|
registerAuthMethod(SkillsRegistry.prototype, "resources", function (cursor) {
|
|
@@ -650,7 +802,7 @@ export class SkillsRegistry {
|
|
|
650
802
|
return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
|
|
651
803
|
const page = yield* upwardStep(authInvoke(self, "list", [cursor]));
|
|
652
804
|
return {
|
|
653
|
-
...
|
|
805
|
+
...self.cacheHints,
|
|
654
806
|
resources: page.skills.flatMap((skill) => (skill.resources === "dynamic" ? [{ uri: skill.uri }] : skill.resources).map((file) => ({
|
|
655
807
|
uri: file.uri,
|
|
656
808
|
name: file.uri === skill.uri
|
|
@@ -668,7 +820,10 @@ export class SkillsRegistry {
|
|
|
668
820
|
registerAuthMethod(SkillsRegistry.prototype, "get", function (uri) {
|
|
669
821
|
const self = this;
|
|
670
822
|
return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
|
|
671
|
-
return {
|
|
823
|
+
return {
|
|
824
|
+
...self.cacheHints,
|
|
825
|
+
skill: (yield* upwardStep(authInvoke(self, "lookup", [uri]))).entry,
|
|
826
|
+
};
|
|
672
827
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
|
|
673
828
|
}, SkillsRegistry.prototype.get);
|
|
674
829
|
registerAuthMethod(SkillsRegistry.prototype, "list", function (cursor) {
|
|
@@ -679,7 +834,7 @@ export class SkillsRegistry {
|
|
|
679
834
|
if (!Number.isSafeInteger(offset) || offset % SKILL_PAGE_SIZE !== 0 || offset >= records.length)
|
|
680
835
|
throw new ConnectorCallError("invalid_args", "Invalid skills cursor.");
|
|
681
836
|
return {
|
|
682
|
-
...
|
|
837
|
+
...self.cacheHints,
|
|
683
838
|
skills: records.slice(offset, offset + SKILL_PAGE_SIZE).map((record) => record.entry),
|
|
684
839
|
...(offset + SKILL_PAGE_SIZE < records.length
|
|
685
840
|
? { nextCursor: `skills:${offset + SKILL_PAGE_SIZE}` }
|
|
@@ -691,8 +846,7 @@ export class SkillsRegistry {
|
|
|
691
846
|
const self = this;
|
|
692
847
|
return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
|
|
693
848
|
const records = yield* upwardStep(self.local);
|
|
694
|
-
const local = records
|
|
695
|
-
bareConnectorGuide(records, uri);
|
|
849
|
+
const local = localLookup(records, uri);
|
|
696
850
|
if (local) {
|
|
697
851
|
const record = yield* upwardStep(authCall(withManifest, [local]));
|
|
698
852
|
self.assertSafe([record]);
|
|
@@ -791,9 +945,11 @@ export class SkillsRegistry {
|
|
|
791
945
|
...(self.options.defer ? { defer: self.options.defer } : {}),
|
|
792
946
|
});
|
|
793
947
|
yield* closeScopeOnExit(connector, ctx, self.options.defer);
|
|
794
|
-
return yield* authCall(read, [ctx]).pipe(Effect.mapError((error) => error instanceof ConnectorCallError && error.code === "
|
|
795
|
-
? new ConnectorCallError("
|
|
796
|
-
:
|
|
948
|
+
return yield* authCall(read, [ctx]).pipe(Effect.mapError((error) => error instanceof ConnectorCallError && error.code === "unsupported_capability"
|
|
949
|
+
? new ConnectorCallError("unsupported_capability", "Downstream Skills capability is not supported.", { retryable: false })
|
|
950
|
+
: error instanceof ConnectorCallError && error.code === "auth_required"
|
|
951
|
+
? new ConnectorCallError("auth_required", "Downstream skills require authorization.")
|
|
952
|
+
: new ConnectorCallError("unavailable", "Downstream skills are unavailable.")));
|
|
797
953
|
})), {
|
|
798
954
|
timeoutMs,
|
|
799
955
|
...(self.options.requestSignal ? { signal: self.options.requestSignal } : {}),
|