@zackbart/connecta 0.41.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 +61 -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 +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- 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 +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- 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 +5 -0
- package/dist/invocation.js +98 -34
- 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 +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -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 +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- 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 +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- 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/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- 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 +1 -1
- 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) {
|
package/dist/skills.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -16,44 +17,92 @@ export class SkillLookupError extends ConnectorCallError {
|
|
|
16
17
|
}
|
|
17
18
|
// The meta instructions and built-in guides use the meta names: built-in guides
|
|
18
19
|
// are byte-identical across deployments, and the eval code arm replaces usage.
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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`;
|
|
31
41
|
}
|
|
32
|
-
export const CONNECTA_INSTRUCTIONS = `${ROUTE} ${writePolicy(false, META_ROUTES)} Never repeat a write to recover its output. ${RECOVERY}`;
|
|
33
42
|
/**
|
|
34
|
-
* The instructions one endpoint serves: pool scope
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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.
|
|
39
49
|
*/
|
|
40
|
-
export function endpointInstructions(description, native, readOnly = false) {
|
|
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}. `}`;
|
|
41
52
|
const instructions = native
|
|
42
53
|
? nativeInstructions(native.executeCode, readOnly)
|
|
43
|
-
:
|
|
44
|
-
?
|
|
45
|
-
:
|
|
46
|
-
|
|
47
|
-
return instructions;
|
|
48
|
-
const routing = native
|
|
49
|
-
? instructions
|
|
50
|
-
: `Known read: ${META_ROUTES.read}${readOnly ? "" : `; known write: ${META_ROUTES.write}`}. Other work: ${META_ROUTES.program}; discover and call together, return answers, sample unfamiliar reads. ${writePolicy(readOnly, META_ROUTES)} Never repeat writes for output. Give auth handoffs to user/operator; ${META_ROUTES.authorize} refreshes links. Page resultId/format:"paged" with connecta.result(handleOrId,{page?}).`;
|
|
51
|
-
return `Use skills. ${description} ${routing}`;
|
|
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}`;
|
|
52
58
|
}
|
|
53
59
|
/** Instructions for pools whose host searches the listed native tools. */
|
|
54
60
|
export function nativeInstructions(executeCode, readOnly = false) {
|
|
55
61
|
const routes = routeNames(executeCode ? "native-code" : "native");
|
|
56
|
-
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
|
+
};
|
|
57
106
|
}
|
|
58
107
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
59
108
|
export function hasConnectorGuides(connectors) {
|
|
@@ -115,11 +164,16 @@ const CONNECTOR_SKILL_PREFIX = "connector:";
|
|
|
115
164
|
export function connectorSkillName(connectorId) {
|
|
116
165
|
return `${CONNECTOR_SKILL_PREFIX}${connectorId}`;
|
|
117
166
|
}
|
|
118
|
-
/** The
|
|
119
|
-
|
|
167
|
+
/** The guide as authored, or undefined when the connector declares none (or a blank one). */
|
|
168
|
+
function authoredGuide(connector) {
|
|
120
169
|
const guide = connector.usageGuide;
|
|
121
170
|
const content = typeof guide === "string" ? guide : guide?.content;
|
|
122
|
-
|
|
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)
|
|
123
177
|
return undefined;
|
|
124
178
|
return connector.readOnly === true
|
|
125
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}`
|
|
@@ -356,6 +410,11 @@ const PRIVATE = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
|
|
|
356
410
|
function localRecord(name, uri, description, content, aliases) {
|
|
357
411
|
return { entry: { uri, frontmatter: { name, description }, resources: "dynamic" }, content, aliases };
|
|
358
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
|
+
}
|
|
359
418
|
/** JSON frontmatter is YAML too; no runtime provider imports or filesystem reads. */
|
|
360
419
|
function validSkillName(name) {
|
|
361
420
|
return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
@@ -416,7 +475,11 @@ const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (
|
|
|
416
475
|
const guides = identities.map(({ connector, name, legacy }) => {
|
|
417
476
|
const description = connectorGuideSummary(connector);
|
|
418
477
|
const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
|
|
419
|
-
|
|
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}`;
|
|
420
483
|
return {
|
|
421
484
|
...localRecord(name, `${root}/SKILL.md`, description, content, [
|
|
422
485
|
connectorSkillName(connector.id),
|
|
@@ -666,6 +729,10 @@ export class SkillsRegistry {
|
|
|
666
729
|
return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
|
|
667
730
|
const record = yield* upwardStep(authInvoke(self, "lookup", [name]));
|
|
668
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);
|
|
669
736
|
const origin = record.files ? `${downstreamOrigin(record.connector.id)}\n\n` : "";
|
|
670
737
|
if (typeof content.text === "string")
|
|
671
738
|
return origin + content.text;
|
package/dist/storage/keys.js
CHANGED
|
@@ -58,7 +58,8 @@ export const scopes = {
|
|
|
58
58
|
export const RESULT_TTL_SECONDS = 900;
|
|
59
59
|
/**
|
|
60
60
|
* A stashed result: chunk 0 at `result:<id>`, chunk n at `result:<id>#<n>`.
|
|
61
|
-
* The value is the paging envelope `src/meta-tools.ts` writes and reads
|
|
61
|
+
* The value is the paging envelope `src/meta-tools.ts` writes and reads, or at
|
|
62
|
+
* chunk 0 the refusal `src/result-seal.ts` writes for a result it did not publish.
|
|
62
63
|
*/
|
|
63
64
|
export const resultKeys = {
|
|
64
65
|
family: {
|
|
@@ -191,7 +192,7 @@ export const responseCacheKeys = {
|
|
|
191
192
|
codec: textCodec,
|
|
192
193
|
ttl: {
|
|
193
194
|
kind: "configured",
|
|
194
|
-
by: "bounded downstream ttlMs; generations and hash refresh baselines expire after 48 hours",
|
|
195
|
+
by: "bounded downstream ttlMs; generations, refusal fences and hash refresh baselines expire after 48 hours",
|
|
195
196
|
},
|
|
196
197
|
durable: false,
|
|
197
198
|
},
|
|
@@ -356,19 +357,21 @@ export const oauthRefreshActiveKeys = {
|
|
|
356
357
|
prefix: "oauth:refresh-active:",
|
|
357
358
|
holder: (epoch, holder) => validateStorageKey(`oauth:refresh-active:${epoch}:${holder}`),
|
|
358
359
|
};
|
|
359
|
-
/** Single-use browser links (`used`/`started`/`failed
|
|
360
|
+
/** Single-use browser links and forced resets (`used`/`started`/`failed`), and MRTR rounds. */
|
|
360
361
|
export const oauthConnectKeys = {
|
|
361
362
|
family: {
|
|
362
363
|
name: "oauth-connect",
|
|
363
364
|
scope: "connector",
|
|
364
|
-
prefixes: ["oauth:connect-used:", "oauth:request-used:"],
|
|
365
|
+
prefixes: ["oauth:connect-used:", "oauth:connect-reset:", "oauth:request-used:"],
|
|
365
366
|
version: { number: 1, in: "untagged" },
|
|
366
367
|
codec: textCodec,
|
|
367
|
-
//
|
|
368
|
-
|
|
368
|
+
// A link's remaining lifetime, fifteen minutes for a round, or thirty for a
|
|
369
|
+
// reset, which must outlive every link its flow can issue.
|
|
370
|
+
ttl: { kind: "fixed", seconds: 30 * 60 },
|
|
369
371
|
durable: false,
|
|
370
372
|
},
|
|
371
373
|
used: (nonce) => validateStorageKey(`oauth:connect-used:${nonce}`),
|
|
374
|
+
reset: (flow) => validateStorageKey(`oauth:connect-reset:${flow}`),
|
|
372
375
|
retry: (nonce) => validateStorageKey(`oauth:request-used:${nonce}`),
|
|
373
376
|
};
|
|
374
377
|
/**
|
|
@@ -134,7 +134,7 @@ export function nativeSqlStorage(driver) {
|
|
|
134
134
|
},
|
|
135
135
|
catalogSnapshot(keys, generationKey, prefix, suffixes) {
|
|
136
136
|
return Effect.gen(function* () {
|
|
137
|
-
if (keys.length < 1 || keys.length > 4 || suffixes.length >
|
|
137
|
+
if (keys.length < 1 || keys.length > 4 || suffixes.length > 3)
|
|
138
138
|
throw new RangeError("Catalog locator exceeds bounds");
|
|
139
139
|
for (const key of [...keys, generationKey, prefix, ...suffixes])
|
|
140
140
|
validateStorageKey(key);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ToolDef } from "./types.js";
|
|
2
|
+
/** What deployment code may say about one tool. */
|
|
3
|
+
export type ClassificationOverride = "read" | "read-repeatable" | "write" | "hidden";
|
|
4
|
+
export declare function sourceClassification(tool: ToolDef): "read" | "write";
|
|
5
|
+
/** The source said this tool writes, as opposed to saying nothing. */
|
|
6
|
+
export declare function sourceDeclaresWrite(tool: ToolDef): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Deployment override, then fail-closed annotations. `"read"` speaks only where
|
|
9
|
+
* the source is silent: a declared write stays a write, because a read annotation
|
|
10
|
+
* lets a host resend the call. `"read-repeatable"` is the deployment accepting
|
|
11
|
+
* that resend for a tool it has checked, so it serves as a read regardless. Either
|
|
12
|
+
* way a source write stays write-like, so Connecta itself never replays it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function classifyTool(definition: ToolDef, override?: Exclude<ClassificationOverride, "hidden">): "read" | "write";
|
|
15
|
+
export declare function carryToolClassification(source: ToolDef, served: ToolDef, acceptRepeat?: boolean): ToolDef;
|
|
16
|
+
/** Replay and outcome policy only; admission and presentation use the served class. */
|
|
17
|
+
export declare function isWriteLike(tool: ToolDef): boolean;
|
|
18
|
+
/** The surface that runs a read-only program: it never dispatches a write-like call. */
|
|
19
|
+
export declare const READ_PROGRAM_SOURCE: "execute_read_code";
|
|
20
|
+
/**
|
|
21
|
+
* What a read program refuses: anything write-like, unless the deployment accepted
|
|
22
|
+
* repeats. A plain `"read"` over a source write stays out, because only
|
|
23
|
+
* `"read-repeatable"` says a host's resend of the program is acceptable.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readProgramRefuses(tool: ToolDef): boolean;
|
|
26
|
+
export declare function cloneTool(tool: ToolDef): ToolDef;
|
|
27
|
+
/**
|
|
28
|
+
* The only permission decision for a connector call. Approval is client-owned.
|
|
29
|
+
* A read-only endpoint refuses every write on every surface; a writable one
|
|
30
|
+
* sends writes through call_destructive_tool and writing programs, never call_tool
|
|
31
|
+
* or a read program.
|
|
32
|
+
*/
|
|
33
|
+
export declare function writeRefusal(classification: ToolDef["classification"], surface: string, readOnly?: boolean): "read_only_endpoint" | "destructive_tool_requires_approval" | undefined;
|
package/dist/tool-safety.js
CHANGED
|
@@ -1,33 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
function sourceFactsOf(annotations) {
|
|
2
|
+
const readOnly = annotations?.readOnlyHint;
|
|
3
|
+
const destructive = annotations?.destructiveHint;
|
|
4
|
+
if (readOnly === true && destructive !== true)
|
|
5
|
+
return "read";
|
|
6
|
+
// Anything but a clean read or silence counts as declared, including contradictory
|
|
7
|
+
// or malformed hints: an override must not turn an unclear statement into a read.
|
|
8
|
+
return readOnly === undefined && destructive !== true ? "silent" : "declared_write";
|
|
9
|
+
}
|
|
10
|
+
// Source policy is fail-closed annotations, with silence kept apart from a
|
|
11
|
+
// declared write. Deployment overrides never enter it. It is private metadata,
|
|
12
|
+
// not catalog data. Keep it through host-owned clones without exposing it to
|
|
13
|
+
// tools or storage.
|
|
14
|
+
const sourceFacts = new WeakMap();
|
|
15
|
+
const factsFor = (tool) => sourceFacts.get(tool) ?? sourceFactsOf(tool.annotations);
|
|
16
|
+
export function sourceClassification(tool) {
|
|
17
|
+
return factsFor(tool) === "read" ? "read" : "write";
|
|
18
|
+
}
|
|
19
|
+
/** The source said this tool writes, as opposed to saying nothing. */
|
|
20
|
+
export function sourceDeclaresWrite(tool) {
|
|
21
|
+
return factsFor(tool) === "declared_write";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Deployment override, then fail-closed annotations. `"read"` speaks only where
|
|
25
|
+
* the source is silent: a declared write stays a write, because a read annotation
|
|
26
|
+
* lets a host resend the call. `"read-repeatable"` is the deployment accepting
|
|
27
|
+
* that resend for a tool it has checked, so it serves as a read regardless. Either
|
|
28
|
+
* way a source write stays write-like, so Connecta itself never replays it.
|
|
29
|
+
*/
|
|
2
30
|
export function classifyTool(definition, override) {
|
|
3
|
-
if (override
|
|
31
|
+
if (override === "read-repeatable")
|
|
32
|
+
return "read";
|
|
33
|
+
if (override === "write" || (override === "read" && !sourceDeclaresWrite(definition)))
|
|
4
34
|
return override;
|
|
5
|
-
return definition
|
|
6
|
-
? "read"
|
|
7
|
-
: "write";
|
|
8
|
-
}
|
|
9
|
-
// Source policy is fail-closed annotations.
|
|
10
|
-
// Deployment overrides never enter it. It is private metadata, not catalog data.
|
|
11
|
-
// Keep it through host-owned clones without exposing it to tools or storage.
|
|
12
|
-
const sourceClassifications = new WeakMap();
|
|
13
|
-
export function sourceClassification(tool) {
|
|
14
|
-
return sourceClassifications.get(tool) ?? classifyTool(tool);
|
|
35
|
+
return sourceClassification(definition);
|
|
15
36
|
}
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
// Tools a deployment set to `"read-repeatable"`, private like the source facts.
|
|
38
|
+
const repeatAccepted = new WeakSet();
|
|
39
|
+
export function carryToolClassification(source, served, acceptRepeat = repeatAccepted.has(source)) {
|
|
40
|
+
sourceFacts.set(served, factsFor(source));
|
|
41
|
+
if (acceptRepeat)
|
|
42
|
+
repeatAccepted.add(served);
|
|
18
43
|
return served;
|
|
19
44
|
}
|
|
20
45
|
/** Replay and outcome policy only; admission and presentation use the served class. */
|
|
21
46
|
export function isWriteLike(tool) {
|
|
22
47
|
return tool.classification !== "read" || sourceClassification(tool) === "write";
|
|
23
48
|
}
|
|
49
|
+
/** The surface that runs a read-only program: it never dispatches a write-like call. */
|
|
50
|
+
export const READ_PROGRAM_SOURCE = "execute_read_code";
|
|
51
|
+
/**
|
|
52
|
+
* What a read program refuses: anything write-like, unless the deployment accepted
|
|
53
|
+
* repeats. A plain `"read"` over a source write stays out, because only
|
|
54
|
+
* `"read-repeatable"` says a host's resend of the program is acceptable.
|
|
55
|
+
*/
|
|
56
|
+
export function readProgramRefuses(tool) {
|
|
57
|
+
return isWriteLike(tool) && !repeatAccepted.has(tool);
|
|
58
|
+
}
|
|
24
59
|
export function cloneTool(tool) {
|
|
25
60
|
return carryToolClassification(tool, structuredClone(tool));
|
|
26
61
|
}
|
|
27
62
|
/**
|
|
28
63
|
* The only permission decision for a connector call. Approval is client-owned.
|
|
29
64
|
* A read-only endpoint refuses every write on every surface; a writable one
|
|
30
|
-
* sends writes through call_destructive_tool and programs, never call_tool
|
|
65
|
+
* sends writes through call_destructive_tool and writing programs, never call_tool
|
|
66
|
+
* or a read program.
|
|
31
67
|
*/
|
|
32
68
|
export function writeRefusal(classification, surface, readOnly = false) {
|
|
33
69
|
if (classification === "read")
|
package/dist/types.d.ts
CHANGED
|
@@ -335,6 +335,12 @@ export type ConnectorSkillResourceContents = {
|
|
|
335
335
|
text?: never;
|
|
336
336
|
});
|
|
337
337
|
/** The whole plugin contract — the one open seam. */
|
|
338
|
+
/** A complete cached catalog and the times it was fetched and expires, in epoch milliseconds. */
|
|
339
|
+
export interface ConnectorCatalogPeek {
|
|
340
|
+
tools: ToolDef[];
|
|
341
|
+
fetchedAt: number;
|
|
342
|
+
expiresAt: number;
|
|
343
|
+
}
|
|
338
344
|
export interface Connector {
|
|
339
345
|
/** Serve only read-classified tools on every endpoint. Defaults to false. */
|
|
340
346
|
readOnly?: boolean;
|
|
@@ -399,6 +405,16 @@ export interface Connector {
|
|
|
399
405
|
* fetched lazily over the network and are not known at construction time.
|
|
400
406
|
*/
|
|
401
407
|
staticTools?: ToolDef[];
|
|
408
|
+
/**
|
|
409
|
+
* Optional: the newest complete catalog this context could be served, fresh
|
|
410
|
+
* or stale, read from storage alone. No transport, no authorization and no
|
|
411
|
+
* writes. Discovery lists its result and marks it; call resolution never
|
|
412
|
+
* reads it. `remoteMcp()` implements it. A custom connector omits it and
|
|
413
|
+
* discovery falls back to the catalog core retains for an authenticated owner.
|
|
414
|
+
* A wrapper that replaces `listTools` must replace or remove this as well, or
|
|
415
|
+
* discovery lists what the wrapper would have changed.
|
|
416
|
+
*/
|
|
417
|
+
peekCatalog?(ctx: ConnectorContext): Promise<ConnectorCatalogPeek | undefined>;
|
|
402
418
|
/**
|
|
403
419
|
* Optional: what this connector is, for `Connecta.describeConfig()` and the
|
|
404
420
|
* operator surfaces built on it — where it points, how it authenticates,
|
|
@@ -616,6 +632,21 @@ export interface Executor {
|
|
|
616
632
|
* rewrite; set this to say the same thing minification-proof.
|
|
617
633
|
*/
|
|
618
634
|
readonly name?: string;
|
|
635
|
+
/**
|
|
636
|
+
* The run's wall-clock deadline, in whole milliseconds, when this sandbox has
|
|
637
|
+
* one: time from `execute()` to its own timeout result. Connecta ends a
|
|
638
|
+
* program's reads just before it, so a slow read becomes a catchable
|
|
639
|
+
* `timeout` rather than the whole run's failure. Omit it if the sandbox
|
|
640
|
+
* has no fixed deadline. See documentation/code-mode.md#cancellation-and-limits.
|
|
641
|
+
*/
|
|
642
|
+
readonly deadlineMs?: number;
|
|
643
|
+
/**
|
|
644
|
+
* The largest stored result, in UTF-8 bytes, that one program may read whole
|
|
645
|
+
* with `connecta.result(id, { all: true })` and still have budget left to use
|
|
646
|
+
* it. Default 1.5 MiB. Set it lower when a tighter CPU or memory limit would
|
|
647
|
+
* end the run first.
|
|
648
|
+
*/
|
|
649
|
+
readonly maxReadBytes?: number;
|
|
619
650
|
execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
|
|
620
651
|
/** Release runtime resources. Node's built-in pool implements this. */
|
|
621
652
|
close?(): void | Promise<void>;
|
package/dist/usage-guide.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { META_ROUTES } from "./agent-routes.js";
|
|
2
|
+
import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
|
|
2
3
|
import { UNCOUNTED_GUEST_FUNCTIONS } from "./guest-functions.js";
|
|
3
4
|
/** Self-contained content for the usage skill; independent of skill registration. */
|
|
4
5
|
export const USAGE_SKILL = `---
|
|
@@ -14,7 +15,7 @@ Route as the server instructions and ${META_ROUTES.program}'s first sentence say
|
|
|
14
15
|
Read one account with ${META_ROUTES.read}: \`{ "address": "crm.get_account", "args": { "id": "acct_42" } }\`.
|
|
15
16
|
On a read-only endpoint search omits writes (guides may name them) and each
|
|
16
17
|
fails read_only_endpoint; tell the user. Unannotated tools fail closed to writes.
|
|
17
|
-
|
|
18
|
+
Where listed, ${READ_PROGRAM_SOURCE} refuses writes, marked \`callable: false\`.
|
|
18
19
|
|
|
19
20
|
## The global and the envelopes
|
|
20
21
|
|
|
@@ -28,7 +29,7 @@ Parse \`format: "text"\` only when documented as JSON. Verify collection roots.
|
|
|
28
29
|
\`connecta.read("resource://id/"+encodeURIComponent(uri))\` returns \`{ contents }\` for advertised URIs.
|
|
29
30
|
|
|
30
31
|
Search/describe return \`{ tools }\`, never an array. Check search's
|
|
31
|
-
\`catalogErrors\` and \`absence\` before selecting a match. Search defaults to a
|
|
32
|
+
\`catalogErrors\` and \`absence\` before selecting a match; a \`stale: true\` error's tools are listed from an older catalog, and a call still checks the live one. Search defaults to a
|
|
32
33
|
TypeScript \`signature\`; text schemas have \`schemaFormat: "text"\` and no .properties.
|
|
33
34
|
A \`readyCalls\` entry is a complete read: \`connecta.call(entry.address, entry.args)\`. Select the exact tool and
|
|
34
35
|
account/environment, inspect its schema, and supply its required keys and types.
|
|
@@ -85,14 +86,14 @@ Otherwise fetch for units, pagination, aliases, or sequences.
|
|
|
85
86
|
|
|
86
87
|
## Large results and recovery
|
|
87
88
|
|
|
88
|
-
Large
|
|
89
|
-
|
|
90
|
-
returned id in place of result-id below; this example assumes documented JSON.
|
|
89
|
+
Large results carry resultId. Follow nextOffset in UTF-8 bytes, reassemble and reduce
|
|
90
|
+
inside one program. Use that id for result-id below; this example assumes documented JSON.
|
|
91
91
|
|
|
92
|
-
Calls
|
|
93
|
-
\`{ format: "paged", valueFormat, resultId?, totalBytes, hint }\`.
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
Calls inline up to 1 MiB; larger return
|
|
93
|
+
\`{ format: "paged", valueFormat, resultId?, totalBytes, hint }\`. Follow its hint:
|
|
94
|
+
\`connecta.result(handle, { all: true })\` gives \`{ data, format }\` for one host call,
|
|
95
|
+
or throws result_too_large; then page. IDs last 15 minutes; only explicit
|
|
96
|
+
paging spends extra host calls.
|
|
96
97
|
Use zero-based \`{ page: 0 }\` or UTF-8 \`{ offset }\`, never both. Keep maxBytes
|
|
97
98
|
fixed across pages; connector caps and the 32 KiB bridge ceiling apply.
|
|
98
99
|
|
|
@@ -100,7 +101,7 @@ fixed across pages; connector caps and the 32 KiB bridge ceiling apply.
|
|
|
100
101
|
async () => {
|
|
101
102
|
let text = "", offset = 0, page;
|
|
102
103
|
do {
|
|
103
|
-
page = await connecta.result("result-id", { offset
|
|
104
|
+
page = await connecta.result("result-id", { offset });
|
|
104
105
|
text += page.text;
|
|
105
106
|
offset = page.nextOffset;
|
|
106
107
|
} while (page.hasMore);
|
|
@@ -108,13 +109,13 @@ async () => {
|
|
|
108
109
|
}
|
|
109
110
|
\`\`\`
|
|
110
111
|
|
|
111
|
-
Oversized returns lead with nextAction. Never rerun writes for output. write_outcome_unknown or writeOutcome forbids retry, even if caught.
|
|
112
|
+
Oversized returns lead with nextAction. Never rerun writes for output. write_outcome_unknown or writeOutcome forbids retry, even if caught. Give the user any approval link in the tool's words; a call after approval is new.
|
|
112
113
|
Recovery args may be partial (argsRedacted) or absent (argsOmitted). Reconcile
|
|
113
114
|
before retrying with the original args or repeating a program that sent writes.
|
|
114
115
|
Non-retryable errors need repair.
|
|
115
116
|
host_auth_required needs host connection repair; provider_permission_denied needs
|
|
116
|
-
resource-owner access. Auth failures carry the handoff in error.data
|
|
117
|
-
|
|
117
|
+
resource-owner access. Auth failures carry the handoff in error.data; give it to the
|
|
118
|
+
user or operator.
|
|
118
119
|
|
|
119
120
|
## Output
|
|
120
121
|
|