@zackbart/connecta 0.38.0 → 0.40.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 +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- 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 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- 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 +7 -4
- 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 +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -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 +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- 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/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 +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- 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/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- 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 +2 -4
- 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 +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- 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 +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- 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/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 +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- 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/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
package/dist/invocation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routePhrases } from "./agent-routes.js";
|
|
1
2
|
import { authCall, registerAuthMethod } from "./auth/native.js";
|
|
2
3
|
import { withAuthorizationHandoffEffect } from "./authorization-handoff.js";
|
|
3
4
|
import { hasControlCharacters } from "./tool-name.js";
|
|
@@ -9,13 +10,13 @@ import { downstreamValue } from "./mcp-result.js";
|
|
|
9
10
|
import { carryFailureFacts, classifiedFailure, failureRecord, logFailure, recordedToolName, } from "./operator-record.js";
|
|
10
11
|
import { splitAddress } from "./registry.js";
|
|
11
12
|
import { causeError, runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
12
|
-
import { validateCatalogToolInput } from "./validate.js";
|
|
13
|
+
import { schemaExample, validateCatalogToolInput } from "./validate.js";
|
|
13
14
|
import { sentSecretsFor, sentSecretsForRequest } from "./sent-secrets.js";
|
|
14
15
|
import { recoveryRecord, recordRecoveryFacts } from "./call-recovery.js";
|
|
15
16
|
import { callFromContext, contextView, viewServices, Credentials } from "./runtime/call.js";
|
|
16
17
|
import { chargeCallBudget } from "./runtime/call-admission.js";
|
|
17
18
|
import { callEntry } from "./runtime/call-entry.js";
|
|
18
|
-
import { forgetProviderFailure, isKnownProviderFailure, isSettledWrite, keyProtectsCall, knownCallFailure, onlyAuxiliaryRequests, knownProviderFailure, openLogicalCall, } from "./provider-call.js";
|
|
19
|
+
import { forgetProviderFailure, isKnownProviderFailure, isSettledWrite, keyProtectsCall, knownCallFailure, refusalRequest, onlyAuxiliaryRequests, knownProviderFailure, openLogicalCall, } from "./provider-call.js";
|
|
19
20
|
import { classifyWriteOutcome, settleWrite, settleWriteResult } from "./write-outcome.js";
|
|
20
21
|
import { classificationDigest, recordAuthFailure, recordCallEntry, replayClassificationDigest, resolveInvocationAuthEffect, } from "./invocation-auth.js";
|
|
21
22
|
import { downstreamContinuation, recordDownstreamArgumentEcho, isDownstreamInputResult, assertDownstreamOutputSafe, } from "./downstream-input-context.js";
|
|
@@ -141,6 +142,7 @@ export class InvocationFailure extends Error {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
keepErrorName(InvocationFailure, "InvocationFailure");
|
|
145
|
+
const withoutExample = ({ example: _example, ...stated }) => stated;
|
|
144
146
|
/**
|
|
145
147
|
* Shared downstream invocation engine. Address/catalog resolution is delegated
|
|
146
148
|
* to the request-local CatalogService; every remaining call semantic lives
|
|
@@ -286,10 +288,13 @@ export class InvocationService {
|
|
|
286
288
|
}
|
|
287
289
|
if (mayHaveWritten &&
|
|
288
290
|
(error.code === "auth_required" || error.code === "downstream_oauth_required") &&
|
|
289
|
-
!logicalCall?.sent.some((request) => request.dispatched)
|
|
291
|
+
!logicalCall?.sent.some((request) => request.dispatched) &&
|
|
292
|
+
!(answered && refusalRequest(error)?.dispatched === false)) {
|
|
290
293
|
// An empty resource ledger cannot settle this auth failure. Keep
|
|
291
294
|
// actual resource-refusal provenance, but withdraw an empty ledger's
|
|
292
295
|
// known mark before composing advice and counting program writes.
|
|
296
|
+
// A boundary-owned physical pre-dispatch refusal is positive proof;
|
|
297
|
+
// its private provenance cannot be supplied by error fields.
|
|
293
298
|
answered = false;
|
|
294
299
|
forgetProviderFailure(error);
|
|
295
300
|
}
|
|
@@ -406,6 +411,14 @@ export class InvocationService {
|
|
|
406
411
|
return error;
|
|
407
412
|
return {
|
|
408
413
|
...error,
|
|
414
|
+
// An example comes only from the published schema, never a connector's copy of the
|
|
415
|
+
// call, unless the connector states why there is none.
|
|
416
|
+
validation: {
|
|
417
|
+
...withoutExample(error.validation),
|
|
418
|
+
...(error.validation.exampleUnavailable === undefined && resolved?.definition.inputSchema
|
|
419
|
+
? schemaExample(resolved.definition.inputSchema)
|
|
420
|
+
: {}),
|
|
421
|
+
},
|
|
409
422
|
connector: target.connector.id,
|
|
410
423
|
operation: `${target.connector.id}.${target.toolName}`,
|
|
411
424
|
nextAction: this.catalog.searchRecovery({
|
|
@@ -552,7 +565,7 @@ export class InvocationService {
|
|
|
552
565
|
return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and ${connectorReadOnly ? `connector "${target.connector.id}"` : "this endpoint"} is read-only. Nothing here can send it.`);
|
|
553
566
|
}
|
|
554
567
|
if (refusal === "destructive_tool_requires_approval") {
|
|
555
|
-
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write.
|
|
568
|
+
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. ${routePhrases(context).writeNeedsApproval}`);
|
|
556
569
|
}
|
|
557
570
|
// Remote MCP tools advertise their input schema in the catalog. Validate
|
|
558
571
|
// against that same request-local definition before admission or provider
|
|
@@ -580,7 +593,12 @@ export class InvocationService {
|
|
|
580
593
|
return yield* Effect.fail(callSignal.reason);
|
|
581
594
|
attempts = 1;
|
|
582
595
|
const call = (permit) => Effect.gen({ self: this }, function* () {
|
|
583
|
-
const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({
|
|
596
|
+
const operationContext = this.registry.contextFor(target.connector.id, this.catalog.baseUrl, this.catalog.requestScope, defined({
|
|
597
|
+
signal: callSignal,
|
|
598
|
+
timeoutMs: context.timeoutMs,
|
|
599
|
+
defer: this.catalog.defer,
|
|
600
|
+
routes: context.routes,
|
|
601
|
+
}));
|
|
584
602
|
const lease = callFromContext(operationContext, write ? "write" : "read", {
|
|
585
603
|
admitted: true,
|
|
586
604
|
// An invocation deadline aborts when its call finishes. A
|
|
@@ -604,11 +622,11 @@ export class InvocationService {
|
|
|
604
622
|
yield* Effect.gen(function* () {
|
|
605
623
|
if (target.connector.credential) {
|
|
606
624
|
if (!connectorContext.credential) {
|
|
607
|
-
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured.
|
|
608
|
-
|
|
625
|
+
throw new ConnectorCallError("auth_required", "Operator-managed credential storage is not configured. " +
|
|
626
|
+
routePhrases(connectorContext).authorizeCall(target.connector.id));
|
|
609
627
|
}
|
|
610
628
|
if (!(yield* Context.get(lease.services, Credentials).values)) {
|
|
611
|
-
throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential.
|
|
629
|
+
throw new ConnectorCallError("auth_required", `Connector "${target.connector.id}" has no stored credential. ${routePhrases(connectorContext).authorizeCall()}`);
|
|
612
630
|
}
|
|
613
631
|
}
|
|
614
632
|
yield* resolveInvocationAuthEffect(connector, callTool, connectorContext);
|
package/dist/membership.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ResolvedPool {
|
|
|
22
22
|
readOnly: boolean;
|
|
23
23
|
/** Exactly the pool's `tools`; nothing intersects or widens it. */
|
|
24
24
|
access: ConnectorAccess;
|
|
25
|
+
/** Non-empty and homogeneous: all people or all agents. */
|
|
25
26
|
members: readonly Member[];
|
|
26
27
|
/** Trimmed `owner`: the Workspace address agents in this pool act as. */
|
|
27
28
|
owner?: string;
|
|
@@ -40,11 +41,8 @@ export interface MemberContext {
|
|
|
40
41
|
}
|
|
41
42
|
/** The namespaces bare ids may resolve in, read from the resolved providers. */
|
|
42
43
|
export declare function memberContext(auth: readonly InboundAuth[], agents: readonly string[]): MemberContext;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
* ambiguous, unknown, or repeated (INV-11).
|
|
46
|
-
*/
|
|
47
|
-
export declare function resolveMembers(value: unknown, path: string, context: MemberContext): Member[];
|
|
44
|
+
/** Resolve a pool's members, which are all people or all agents (INV-11). */
|
|
45
|
+
export declare function resolvePoolMembers(value: unknown, path: string, context: MemberContext): Member[];
|
|
48
46
|
/**
|
|
49
47
|
* `maintainers`: people only. Required whenever an inbound provider can admit
|
|
50
48
|
* a person, which is any provider but the built-in agent-token adapter. With
|
|
@@ -53,5 +51,15 @@ export declare function resolveMembers(value: unknown, path: string, context: Me
|
|
|
53
51
|
export declare function resolveMaintainers(value: unknown, admitsPeople: boolean, context: MemberContext): readonly IdentityReference[];
|
|
54
52
|
/** Whether an admitted identity is this member. An agent never matches a person. */
|
|
55
53
|
export declare function isMember(member: Member, identity: Readonly<AuthenticatedIdentity>): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Non-maintainer people listed in several pools, in declaration order. Plain
|
|
56
|
+
* `/mcp` routes a person by their single pool, so it refuses each of these.
|
|
57
|
+
* An open deployment admits no people, so it has none.
|
|
58
|
+
*/
|
|
59
|
+
export declare function peopleInSeveralPools(pools: ReadonlyMap<string, ResolvedPool>, maintainers: readonly IdentityReference[]): Array<{
|
|
60
|
+
namespace: string;
|
|
61
|
+
id: string;
|
|
62
|
+
pools: string[];
|
|
63
|
+
}>;
|
|
56
64
|
/** Whether an admitted identity is a listed maintainer. */
|
|
57
65
|
export declare function isMaintainer(maintainers: readonly IdentityReference[], identity: Readonly<AuthenticatedIdentity>): boolean;
|
package/dist/membership.js
CHANGED
|
@@ -65,7 +65,7 @@ const memberKey = (member) => member.kind === "agent" ? `agent\0${member.name}`
|
|
|
65
65
|
* Resolve one list of references, throwing on any that is malformed,
|
|
66
66
|
* ambiguous, unknown, or repeated (INV-11).
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
function resolveMembers(value, path, context) {
|
|
69
69
|
if (!Array.isArray(value)) {
|
|
70
70
|
throw new ConfigError(`${path} must be an array of principal ids, { namespace, id }, or "agent:<name>".`);
|
|
71
71
|
}
|
|
@@ -79,6 +79,14 @@ export function resolveMembers(value, path, context) {
|
|
|
79
79
|
return Object.freeze(member);
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
/** Resolve a pool's members, which are all people or all agents (INV-11). */
|
|
83
|
+
export function resolvePoolMembers(value, path, context) {
|
|
84
|
+
const members = resolveMembers(value, path, context);
|
|
85
|
+
if (!members.every((member) => member.kind === members[0].kind)) {
|
|
86
|
+
throw new ConfigError(`${path} mixes people and agents; list only one kind.`);
|
|
87
|
+
}
|
|
88
|
+
return members;
|
|
89
|
+
}
|
|
82
90
|
/**
|
|
83
91
|
* `maintainers`: people only. Required whenever an inbound provider can admit
|
|
84
92
|
* a person, which is any provider but the built-in agent-token adapter. With
|
|
@@ -88,7 +96,7 @@ export function resolveMaintainers(value, admitsPeople, context) {
|
|
|
88
96
|
const path = "ConnectaConfig.maintainers";
|
|
89
97
|
if (value === undefined || (Array.isArray(value) && value.length === 0)) {
|
|
90
98
|
if (admitsPeople) {
|
|
91
|
-
throw new ConfigError(`${path} is required with inbound auth: list the people who
|
|
99
|
+
throw new ConfigError(`${path} is required with inbound auth: list the people who receive the whole catalog at plain /mcp, administer shared ` +
|
|
92
100
|
"credentials, manage agent tokens, and read activity.");
|
|
93
101
|
}
|
|
94
102
|
return Object.freeze([]);
|
|
@@ -115,6 +123,30 @@ export function isMember(member, identity) {
|
|
|
115
123
|
principal.namespace === member.namespace &&
|
|
116
124
|
principal.id === member.id);
|
|
117
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Non-maintainer people listed in several pools, in declaration order. Plain
|
|
128
|
+
* `/mcp` routes a person by their single pool, so it refuses each of these.
|
|
129
|
+
* An open deployment admits no people, so it has none.
|
|
130
|
+
*/
|
|
131
|
+
export function peopleInSeveralPools(pools, maintainers) {
|
|
132
|
+
if (maintainers.length === 0)
|
|
133
|
+
return [];
|
|
134
|
+
const people = new Map();
|
|
135
|
+
for (const [name, pool] of pools) {
|
|
136
|
+
for (const member of pool.members) {
|
|
137
|
+
if (member.kind !== "human")
|
|
138
|
+
continue;
|
|
139
|
+
if (maintainers.some((m) => m.namespace === member.namespace && m.id === member.id))
|
|
140
|
+
continue;
|
|
141
|
+
const key = JSON.stringify([member.namespace, member.id]);
|
|
142
|
+
const person = people.get(key) ?? { namespace: member.namespace, id: member.id, pools: [] };
|
|
143
|
+
if (!person.pools.includes(name))
|
|
144
|
+
person.pools.push(name);
|
|
145
|
+
people.set(key, person);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...people.values()].filter((person) => person.pools.length > 1);
|
|
149
|
+
}
|
|
118
150
|
/** Whether an admitted identity is a listed maintainer. */
|
|
119
151
|
export function isMaintainer(maintainers, identity) {
|
|
120
152
|
return maintainers.some((maintainer) => isMember({ kind: "human", ...maintainer }, identity));
|
package/dist/meta-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { META_ROUTES } from "./agent-routes.js";
|
|
1
2
|
import { toolError } from "./tool-error.js";
|
|
2
3
|
import { upwardStep } from "./upward.js";
|
|
3
4
|
import { authCall, authInvoke, authFlow, authMethod, authStorage, registerAuth } from "./auth/native.js";
|
|
@@ -178,7 +179,7 @@ function isWellFormedText(text) {
|
|
|
178
179
|
* reported the decoy near the top instead. A write never gets this: repeating
|
|
179
180
|
* it is the one thing its notice forbids.
|
|
180
181
|
*/
|
|
181
|
-
const
|
|
182
|
+
const readReduceFirst = (routes) => `To find something specific, repeat this read inside ${routes.program} with connecta.call and filter or search the result there, returning only what matters`;
|
|
182
183
|
function pagingHint(results, totalBytes, preview) {
|
|
183
184
|
if (results.pagingTool === "read_result") {
|
|
184
185
|
const shown = preview.kind === "prefix"
|
|
@@ -190,13 +191,13 @@ function pagingHint(results, totalBytes, preview) {
|
|
|
190
191
|
}
|
|
191
192
|
if (!results.write) {
|
|
192
193
|
if (results.binding.program)
|
|
193
|
-
return
|
|
194
|
+
return `filter/map/slice data inside ${results.routes.program} and return only what you need; page this completed return with connecta.result using nextAction.`;
|
|
194
195
|
const [shown, full] = preview.kind === "prefix"
|
|
195
196
|
? [`Bytes 0-${preview.bytes} of ${totalBytes} follow.`, "to read it in full, page the rest"]
|
|
196
197
|
: preview.kind === "text-of-envelope"
|
|
197
198
|
? ["Its text follows.", `to read the full ${totalBytes}-byte content array as JSON, page it`]
|
|
198
199
|
: [`The result is ${totalBytes} bytes.`, "to read it in full, page it"];
|
|
199
|
-
return `${shown} ${
|
|
200
|
+
return `${shown} ${readReduceFirst(results.routes)}; ${full} with connecta.result using nextAction.`;
|
|
200
201
|
}
|
|
201
202
|
const body = preview.kind === "prefix"
|
|
202
203
|
? `Bytes 0-${preview.bytes} of ${totalBytes} follow; page the rest with connecta.result using nextAction.`
|
|
@@ -263,7 +264,7 @@ const stashResult = function (bytes, results, preview, rawText) {
|
|
|
263
264
|
? `Paging is unavailable. ${WRITE_ALREADY_RAN}`
|
|
264
265
|
: results.pagingTool
|
|
265
266
|
? "Paging is unavailable."
|
|
266
|
-
:
|
|
267
|
+
: `Paging is unavailable. Use ${results.routes.program} to reduce read-only results before returning them.`,
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
270
|
return {
|
|
@@ -302,6 +303,8 @@ const pageProgramResultNative = /* @__PURE__ */ registerAuth(pageProgramResult,
|
|
|
302
303
|
const bytes = enc.encode(text);
|
|
303
304
|
const write = resolved.definition.classification !== "read";
|
|
304
305
|
const notice = yield* upwardStep(stashResult(bytes, {
|
|
306
|
+
// Program-call pages name only execute_code, listed wherever programs run.
|
|
307
|
+
routes: META_ROUTES,
|
|
305
308
|
binding: {
|
|
306
309
|
identity: registry.resultIdentity(),
|
|
307
310
|
baseUrl,
|
|
@@ -341,6 +344,8 @@ const guardProgramReturnNative = /* @__PURE__ */ registerAuth(guardProgramReturn
|
|
|
341
344
|
const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
|
|
342
345
|
const results = {
|
|
343
346
|
secrets,
|
|
347
|
+
// A program return names only execute_code, which every surface running programs lists.
|
|
348
|
+
routes: META_ROUTES,
|
|
344
349
|
binding: {
|
|
345
350
|
identity: registry.resultIdentity(),
|
|
346
351
|
baseUrl,
|
|
@@ -541,12 +546,13 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
541
546
|
// identity lets remote connectors reuse one downstream client inside that
|
|
542
547
|
// request without leaking request-bound I/O into the next one.
|
|
543
548
|
const requestScope = opts.requestScope ?? {};
|
|
549
|
+
const routes = opts.routes ?? META_ROUTES;
|
|
544
550
|
if (opts.canManageAuth || opts.oauthConnectUrl || opts.credentialHandoffUrl || opts.oauthConnectUnavailable) {
|
|
545
551
|
bindAuthorizationHandoffs(requestScope, authMethod((connector) => authCall(authorizationHandoff, [
|
|
546
552
|
registry,
|
|
547
553
|
baseUrl,
|
|
548
554
|
requestScope,
|
|
549
|
-
opts,
|
|
555
|
+
{ ...opts, routes },
|
|
550
556
|
{ connector },
|
|
551
557
|
])));
|
|
552
558
|
}
|
|
@@ -567,6 +573,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
567
573
|
requestSignal: opts.requestSignal,
|
|
568
574
|
// searchRoute keeps its top-level default. In-program callers use a
|
|
569
575
|
// separate CatalogService configured for connecta.search.
|
|
576
|
+
routes,
|
|
570
577
|
});
|
|
571
578
|
const invocation = new InvocationService(registry, catalog, opts.activity);
|
|
572
579
|
/** MCP adapter: shared invocation semantics plus MCP-only result shaping. */
|
|
@@ -584,6 +591,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
584
591
|
activitySource: opts.activitySource,
|
|
585
592
|
sentSecrets,
|
|
586
593
|
readOnly: opts.readOnly,
|
|
594
|
+
routes,
|
|
587
595
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
588
596
|
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
589
597
|
unwrapResult: call.resultMode === "value",
|
|
@@ -612,6 +620,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
612
620
|
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
613
621
|
const results = {
|
|
614
622
|
secrets,
|
|
623
|
+
routes,
|
|
615
624
|
binding: {
|
|
616
625
|
identity: registry.resultIdentity(),
|
|
617
626
|
baseUrl,
|
|
@@ -836,6 +845,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
836
845
|
binding.identity.subject !== identity.subject ||
|
|
837
846
|
binding.identity.principal !== identity.principal ||
|
|
838
847
|
binding.identity.endpoint !== identity.endpoint ||
|
|
848
|
+
binding.identity.pool !== identity.pool ||
|
|
839
849
|
binding.identity.origin !== identity.origin ||
|
|
840
850
|
(binding.program === undefined
|
|
841
851
|
? typeof binding.connector !== "string" || typeof binding.tool !== "string"
|
|
@@ -990,11 +1000,15 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
990
1000
|
})),
|
|
991
1001
|
};
|
|
992
1002
|
}
|
|
993
|
-
|
|
994
|
-
|
|
1003
|
+
// Each description states only its own tool's scope; the full routing rule
|
|
1004
|
+
// lives once in the server instructions. A client that drops instructions
|
|
1005
|
+
// still routes correctly from these scopes plus execute_code's terse lead.
|
|
1006
|
+
// Only the meta surface lists these tools, so they use its names.
|
|
1007
|
+
const SEARCH_DESC = (readOnly) => `${readOnly ? "Catalog inspection; this read-only endpoint omits write tools." : `Catalog inspection, or finding a write before ${META_ROUTES.write}.`} For read work, search inside one ${META_ROUTES.program} program that also calls and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" selects reads; "approvalRequired" selects the fail-closed write complement; neither grants authority. Empty query browses. Read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
|
|
1008
|
+
const CALL_DESC = (readOnly) => `Call one known-address read: a tool explicitly annotated readOnlyHint: true. Other read work: ${META_ROUTES.program}. ${readOnly ? "This endpoint is read-only and refuses every write." : `Unannotated or write-capable tools fail closed to ${META_ROUTES.write}.`} A truncated result carries a connecta.result action.`;
|
|
995
1009
|
const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
|
|
996
1010
|
/** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
|
|
997
|
-
const CALL_DESTRUCTIVE_READ_ONLY_DESC =
|
|
1011
|
+
const CALL_DESTRUCTIVE_READ_ONLY_DESC = `This endpoint is read-only: search and describe omit write tools, and every write call here is refused. Reads still run; prefer ${META_ROUTES.read} or ${META_ROUTES.program} for them.`;
|
|
998
1012
|
const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or required deployment configuration. A configured credential reports presence only: if a call just failed with an authentication or operator-configuration error, give the retained handoff to the operator; otherwise proceed with the call. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
|
|
999
1013
|
const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
|
|
1000
1014
|
/**
|
|
@@ -1084,6 +1098,7 @@ const AUTHORIZE_INPUT = advertisedSchema(z.object({
|
|
|
1084
1098
|
export function registerMetaTools(server, registry, ctx) {
|
|
1085
1099
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1086
1100
|
maxProgramResultChars: ctx.maxProgramResultChars,
|
|
1101
|
+
routes: ctx.routes,
|
|
1087
1102
|
...(ctx.native && !ctx.native.executeCode ? { pagingTool: "read_result" } : {}),
|
|
1088
1103
|
...(ctx.native ? { activitySource: "native" } : {}),
|
|
1089
1104
|
downstreamInput: Boolean(ctx.authElicitation),
|
|
@@ -1115,6 +1130,37 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1115
1130
|
bindRequest(request);
|
|
1116
1131
|
return yield* upwardStep(authInvoke(mt, "skills", [args]));
|
|
1117
1132
|
})));
|
|
1133
|
+
server.registerTool("authorize_connector", {
|
|
1134
|
+
description: ctx.native
|
|
1135
|
+
? AUTHORIZE_DESC +
|
|
1136
|
+
" Use when a connector’s tools are missing. Granted: " +
|
|
1137
|
+
registry
|
|
1138
|
+
.listConnectors()
|
|
1139
|
+
.map((connector) => connector.id)
|
|
1140
|
+
.join(", ") +
|
|
1141
|
+
"."
|
|
1142
|
+
: AUTHORIZE_DESC,
|
|
1143
|
+
inputSchema: AUTHORIZE_INPUT,
|
|
1144
|
+
outputSchema: AUTHORIZE_OUTPUT,
|
|
1145
|
+
// Starts (or with force, resets) a downstream OAuth flow — it changes
|
|
1146
|
+
// stored connector auth state, so it is deliberately not read-only.
|
|
1147
|
+
annotations: {
|
|
1148
|
+
readOnlyHint: false,
|
|
1149
|
+
destructiveHint: false,
|
|
1150
|
+
openWorldHint: true,
|
|
1151
|
+
},
|
|
1152
|
+
}, authMethod((args, request) => authFlow(function* () {
|
|
1153
|
+
bindRequest(request);
|
|
1154
|
+
const run = authMethod(() => authInvoke(mt, "authorizeConnector", [args]));
|
|
1155
|
+
return yield* upwardStep(ctx.authElicitation
|
|
1156
|
+
? authInvoke(ctx.authElicitation, "run", [
|
|
1157
|
+
"authorize_connector",
|
|
1158
|
+
args,
|
|
1159
|
+
request,
|
|
1160
|
+
run,
|
|
1161
|
+
])
|
|
1162
|
+
: authCall(run, []));
|
|
1163
|
+
})));
|
|
1118
1164
|
if (ctx.native) {
|
|
1119
1165
|
if (!ctx.native.executeCode) {
|
|
1120
1166
|
server.registerTool("read_result", { description: READ_RESULT_DESC, inputSchema: READ_RESULT_INPUT, annotations: READ_ONLY_LOCAL }, authMethod((args, request) => authFlow(function* () {
|
|
@@ -1186,28 +1232,5 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1186
1232
|
])
|
|
1187
1233
|
: authInvoke(mt, "callDestructiveTool", [call]));
|
|
1188
1234
|
})));
|
|
1189
|
-
server.registerTool("authorize_connector", {
|
|
1190
|
-
description: AUTHORIZE_DESC,
|
|
1191
|
-
inputSchema: AUTHORIZE_INPUT,
|
|
1192
|
-
outputSchema: AUTHORIZE_OUTPUT,
|
|
1193
|
-
// Starts (or with force, resets) a downstream OAuth flow — it changes
|
|
1194
|
-
// stored connector auth state, so it is deliberately not read-only.
|
|
1195
|
-
annotations: {
|
|
1196
|
-
readOnlyHint: false,
|
|
1197
|
-
destructiveHint: false,
|
|
1198
|
-
openWorldHint: true,
|
|
1199
|
-
},
|
|
1200
|
-
}, authMethod((args, request) => authFlow(function* () {
|
|
1201
|
-
bindRequest(request);
|
|
1202
|
-
const run = authMethod(() => authInvoke(mt, "authorizeConnector", [args]));
|
|
1203
|
-
return yield* upwardStep(ctx.authElicitation
|
|
1204
|
-
? authInvoke(ctx.authElicitation, "run", [
|
|
1205
|
-
"authorize_connector",
|
|
1206
|
-
args,
|
|
1207
|
-
request,
|
|
1208
|
-
run,
|
|
1209
|
-
])
|
|
1210
|
-
: authCall(run, []));
|
|
1211
|
-
})));
|
|
1212
1235
|
return undefined;
|
|
1213
1236
|
}
|
package/dist/native-recovery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Adapt only Connecta error envelopes; successful downstream payloads stay untouched. */
|
|
2
|
-
export function nativeRecovery(result,
|
|
2
|
+
export function nativeRecovery(result, wireName) {
|
|
3
3
|
const response = result;
|
|
4
4
|
const body = response?.structuredContent;
|
|
5
5
|
if (!response?.isError || !body || typeof body.error !== "object" || !body.error)
|
|
@@ -28,22 +28,10 @@ export function nativeRecovery(result, executeCode, wireName) {
|
|
|
28
28
|
}
|
|
29
29
|
: { host: "search", purpose: action.purpose };
|
|
30
30
|
}
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
? "the returned authorization handoff"
|
|
34
|
-
: tool.startsWith("call_")
|
|
35
|
-
? "the original native tool"
|
|
36
|
-
: tool === "execute_code" && executeCode
|
|
37
|
-
? tool
|
|
38
|
-
: "your host's tool search");
|
|
39
|
-
for (const key of ["message", "instructions", "retry"])
|
|
40
|
-
if (typeof error[key] === "string")
|
|
41
|
-
error[key] = rewrite(error[key]);
|
|
31
|
+
// Recovery text already uses the native wording: native pools build it with
|
|
32
|
+
// native route names. Only the address in retry text becomes the wire name.
|
|
42
33
|
if (wireName && typeof error.operation === "string" && typeof error.retry === "string")
|
|
43
|
-
error.retry = error.retry.replaceAll(
|
|
44
|
-
if (!executeCode && typeof error.instructions === "string") {
|
|
45
|
-
error.instructions = error.instructions.replace(/; connecta\.search[\s\S]*$/, ". Retry the original native call after recovery.");
|
|
46
|
-
}
|
|
34
|
+
error.retry = error.retry.replaceAll(error.operation, wireName);
|
|
47
35
|
return {
|
|
48
36
|
...response,
|
|
49
37
|
structuredContent: { ...body, error },
|
package/dist/native-tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bytesToHex } from "./bytes.js";
|
|
2
|
+
import { routeNames } from "./agent-routes.js";
|
|
2
3
|
import { duringViewEffect } from "./runtime/call.js";
|
|
3
4
|
import { RequestScope, requestOwner } from "./runtime/request-scope.js";
|
|
4
5
|
import { Effect } from "effect";
|
|
@@ -28,7 +29,7 @@ const SEPARATOR = "__";
|
|
|
28
29
|
const PLAIN_TOOL = /^[A-Za-z0-9-](?:[A-Za-z0-9-]|_(?!_))*$/;
|
|
29
30
|
const NATIVE_NAME = new RegExp(`^[A-Za-z][A-Za-z0-9_-]{0,${NATIVE_NAME_MAX - 1}}$`);
|
|
30
31
|
/** Connecta's own tools on a native endpoint. Their names never contain `__`. */
|
|
31
|
-
const NATIVE_OWN_TOOLS = ["execute_code", "skills", "read_result"];
|
|
32
|
+
const NATIVE_OWN_TOOLS = ["execute_code", "skills", "read_result", "authorize_connector"];
|
|
32
33
|
const enc = new TextEncoder();
|
|
33
34
|
function shortHash(text) {
|
|
34
35
|
return runEdge(shortHashNative(text));
|
|
@@ -149,6 +150,8 @@ export class NativeSurface {
|
|
|
149
150
|
// Listing omits a read-only endpoint's writes, as search does.
|
|
150
151
|
readOnly: options.readOnly,
|
|
151
152
|
defer: options.defer,
|
|
153
|
+
// Declared schema descriptions read as this pool serves them.
|
|
154
|
+
routes: routeNames(options.executeCode ? "native-code" : "native"),
|
|
152
155
|
});
|
|
153
156
|
}
|
|
154
157
|
/**
|
|
@@ -183,7 +186,7 @@ export class NativeSurface {
|
|
|
183
186
|
...(entry ? { _meta: { "dev.connecta/address": `${entry.connector.id}.${entry.tool.name}` } } : {}),
|
|
184
187
|
}, authMethod((args, request) => Effect.gen({ self: this }, function* () {
|
|
185
188
|
const result = yield* authInvoke(this, "call", [name, args ?? {}, request]);
|
|
186
|
-
return nativeRecovery(result,
|
|
189
|
+
return nativeRecovery(result, name);
|
|
187
190
|
})));
|
|
188
191
|
}
|
|
189
192
|
/**
|
package/dist/oauth-sealing.js
CHANGED
|
@@ -46,3 +46,13 @@ export async function deriveRequestStateKey(raw) {
|
|
|
46
46
|
info: new TextEncoder().encode("auth-elicitation"),
|
|
47
47
|
}, material, 256));
|
|
48
48
|
}
|
|
49
|
+
/** A non-extractable HMAC key for secret-bearing stored cache keys; nothing is stored. */
|
|
50
|
+
export async function deriveStorageDigestKey(raw) {
|
|
51
|
+
const material = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveKey"]);
|
|
52
|
+
return crypto.subtle.deriveKey({
|
|
53
|
+
name: "HKDF",
|
|
54
|
+
hash: "SHA-256",
|
|
55
|
+
salt: new TextEncoder().encode("connecta:storage-digest:v1"),
|
|
56
|
+
info: new TextEncoder().encode("cache-keys"),
|
|
57
|
+
}, material, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign"]);
|
|
58
|
+
}
|