@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isRecord } from "../_shared/json.js";
|
|
2
2
|
// Vercel's pinned contract, authentication, failures and value-safety boundary.
|
|
3
3
|
import { Effect, Schema } from "effect";
|
|
4
|
-
import { ConnectorCallError } from "../../errors.js";
|
|
4
|
+
import { ConnectorCallError, tokenPrefix } from "../../errors.js";
|
|
5
5
|
import { Refusal, StatusFailure, UnreadableBody } from "../../http/errors.js";
|
|
6
6
|
import { restSafety } from "../../http/safety.js";
|
|
7
7
|
import { Credentials, RequestSecrets } from "../../http/services.js";
|
|
@@ -93,7 +93,7 @@ export const vercelErrors = {
|
|
|
93
93
|
const code = text(error["code"]);
|
|
94
94
|
const message = text(error["message"])?.trim() ?? text(root["message"])?.trim() ?? `Vercel returned HTTP ${status}.`;
|
|
95
95
|
const reviewed = withheld ? facts["code"] : reviewedCode(code);
|
|
96
|
-
const detail = (code ? `Vercel ${code}: ${message}` : message
|
|
96
|
+
const detail = tokenPrefix(code ? `Vercel ${code}: ${message}` : message, 1_000) || `Vercel returned HTTP ${status}.`;
|
|
97
97
|
if (status >= 200 && status <= 299 && facts["envRejected"] === true)
|
|
98
98
|
return `Vercel rejected the environment-variable write${reviewed ? ` (${reviewed})` : ""}; its message is withheld because this operation handles secret values.`;
|
|
99
99
|
return withheld
|
|
@@ -414,6 +414,7 @@ export function usageGuide(options) {
|
|
|
414
414
|
options = pinOptions(options);
|
|
415
415
|
const team = defaultTeam(options);
|
|
416
416
|
return {
|
|
417
|
+
summary: "Load for Vercel team scope, project/deployment ids, build events, runtime logs and guarded writes.",
|
|
417
418
|
context: [
|
|
418
419
|
`Vercel REST access token is already configured. Account purpose: ${options.purpose}`,
|
|
419
420
|
options.pin
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generate-providers.mjs; do not edit.
|
|
2
2
|
export const skill = {
|
|
3
|
-
"content": "## Projects, deployments and domains\n\nResolve a team by slug, a project by name, and deployments by the observed\nproject id. Team ids start with `team_`, project ids with `prj_`, and deployment\nids with `dpl_`. A deployment hostname and a deployment id identify the same\ndeployment; an alias can later point to a different deployment.\n\nSome project paths accept a project name, but a deployment list's projectId\nfilter requires the observed `prj_` id. Resolve the name before listing its\ndeployments; an empty result for a guessed name does not prove absence.\nComplete each identifier lookup before starting the reads that depend on it.\n\n<!-- when: unpinned -->\nThe configured team is a routing default, not a restriction. Explicit teamId\nor slug selects another team the token can reach; teamId: null selects personal\nscope. Vercel token permissions determine access. Prefer an explicit team when\njoining resources across several calls.\n<!-- end -->\n\n<!-- when: pinned -->\nThe deployment confines this connection to one team. A supplied team slug must\nbelong to that team; personal scope and foreign teams are refused. Project and\ndeployment references require ownership reads. Global/account and transfer\noperations without a verifiable team boundary remain unavailable. A team list\nreturns only the pinned team.\n<!-- end -->\n\nRead deployment state before its build events. Runtime logs diagnose requests\nafter a deployment built; build logs explain a failed build. Narrow time windows\nbefore raising collection limits. Generic reads refuse live follow streams;\nuse the bounded build/runtime readers. Deployment timestamps are milliseconds.\n\nProject domains and deployment aliases are different joins: a project domain\nbelongs to the project, while an alias identifies its assigned deployment.\nRead a pending domain's verification instructions before changing DNS.\n\n## Environment changes\n\nResolve environment-variable ids using metadata, including target, branch and\ncustom environment. A key can have distinct production and preview values.\nExisting values cannot be retrieved. Supply new values only when the task asks\nfor a change; confirm the returned id and metadata.\n\nEnvironment changes apply to future deployments. A new deployment is a separate\nwrite. Promoting an existing deployment does not rebuild it or update embedded\nenvironment values. After an uncertain update, reconcile observable metadata\nsuch as id, targets and comment; reading metadata cannot verify a secret value.\n\n## Explicit files and credentials\n\nDeployment files can reference previously uploaded digests. File uploads accept\nexplicit bytes and never read a path on the agent's machine. Decrypted-value,\ncredential-minting, protection-bypass-link, signing and domain move-out operations\nare refused; use Vercel's dashboard for those workflows.\n\n## Conventions\n\n- **
|
|
3
|
+
"content": "## Projects, deployments and domains\n\nResolve a team by slug, a project by name, and deployments by the observed\nproject id. Team ids start with `team_`, project ids with `prj_`, and deployment\nids with `dpl_`. A deployment hostname and a deployment id identify the same\ndeployment; an alias can later point to a different deployment.\n\nSome project paths accept a project name, but a deployment list's projectId\nfilter requires the observed `prj_` id. Resolve the name before listing its\ndeployments; an empty result for a guessed name does not prove absence.\nComplete each identifier lookup before starting the reads that depend on it.\n\n<!-- when: unpinned -->\nThe configured team is a routing default, not a restriction. Explicit teamId\nor slug selects another team the token can reach; teamId: null selects personal\nscope. Vercel token permissions determine access. Prefer an explicit team when\njoining resources across several calls.\n<!-- end -->\n\n<!-- when: pinned -->\nThe deployment confines this connection to one team. A supplied team slug must\nbelong to that team; personal scope and foreign teams are refused. Project and\ndeployment references require ownership reads. Global/account and transfer\noperations without a verifiable team boundary remain unavailable. A team list\nreturns only the pinned team.\n<!-- end -->\n\nRead deployment state before its build events. Runtime logs diagnose requests\nafter a deployment built; build logs explain a failed build. Narrow time windows\nbefore raising collection limits. Generic reads refuse live follow streams;\nuse the bounded build/runtime readers. Deployment timestamps are milliseconds.\n\nProject domains and deployment aliases are different joins: a project domain\nbelongs to the project, while an alias identifies its assigned deployment.\nRead a pending domain's verification instructions before changing DNS.\n\n## Environment changes\n\nResolve environment-variable ids using metadata, including target, branch and\ncustom environment. A key can have distinct production and preview values.\nExisting values cannot be retrieved. Supply new values only when the task asks\nfor a change; confirm the returned id and metadata.\n\nEnvironment changes apply to future deployments. A new deployment is a separate\nwrite. Promoting an existing deployment does not rebuild it or update embedded\nenvironment values. After an uncertain update, reconcile observable metadata\nsuch as id, targets and comment; reading metadata cannot verify a secret value.\n\n## Explicit files and credentials\n\nDeployment files can reference previously uploaded digests. File uploads accept\nexplicit bytes and never read a path on the agent's machine. Decrypted-value,\ncredential-minting, protection-bypass-link, signing and domain move-out operations\nare refused; use Vercel's dashboard for those workflows.\n\n## Conventions\n\n- **Hatch:** `vercel_api_search` finds operations, `vercel_api_details` shows one contract, `vercel_api_read` calls a reviewed read and `vercel_api_write` a reviewed write. 29 operations are refused by design; use `vercel_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Errors:** `not_found` means the record does not exist; this API tells absence from a permission gap.",
|
|
4
4
|
"instructionsHeading": "Account instructions"
|
|
5
5
|
};
|
package/dist/registry.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { CatalogDriftActivityContext } from "./activity.js";
|
|
|
2
2
|
import { type CallAdmissionPermit, type ConnectorCallAdmissionSnapshot } from "./call-admission.js";
|
|
3
3
|
import { type ConnectorCaller } from "./connector-caller.js";
|
|
4
4
|
import type { CredentialVault } from "./credential-contract.js";
|
|
5
|
+
import { type ClassificationOverride } from "./tool-safety.js";
|
|
5
6
|
import type { Connector, ConnectorContext, ConnectorStatus, KVStorage, Logger, ToolDef } from "./types.js";
|
|
6
7
|
/**
|
|
7
8
|
* Split `"<connectorId>.<toolName>"` on the first dot. Connector ids contain
|
|
@@ -45,7 +46,7 @@ export declare function resolveMaxResultBytes(value: number | undefined, inherit
|
|
|
45
46
|
export interface RegistryOptions {
|
|
46
47
|
publicUrl?: string | undefined;
|
|
47
48
|
oauthClientName?: string | undefined;
|
|
48
|
-
classification?: Readonly<Record<string, Readonly<Record<string,
|
|
49
|
+
classification?: Readonly<Record<string, Readonly<Record<string, ClassificationOverride>>>> | undefined;
|
|
49
50
|
storage: KVStorage;
|
|
50
51
|
/** Internal deployment root, preserved when a personal registry partitions storage. */
|
|
51
52
|
rootStorage?: KVStorage;
|
|
@@ -99,6 +100,13 @@ export interface ProgramResultDependency {
|
|
|
99
100
|
tool?: string;
|
|
100
101
|
classification: "read" | "write";
|
|
101
102
|
}
|
|
103
|
+
/** A cached catalog as discovery may list it. */
|
|
104
|
+
export interface CatalogPeek {
|
|
105
|
+
tools: ToolDef[];
|
|
106
|
+
fetchedAt: number;
|
|
107
|
+
/** Whether the connector's own cache would serve this now, with no failure to report. */
|
|
108
|
+
fresh: boolean;
|
|
109
|
+
}
|
|
102
110
|
export interface RegistryView {
|
|
103
111
|
credentialUiAvailable(): boolean;
|
|
104
112
|
/** Deployment-wide result-size cap threaded to the meta-tools. */
|
|
@@ -114,6 +122,12 @@ export interface RegistryView {
|
|
|
114
122
|
toolName: string;
|
|
115
123
|
} | null;
|
|
116
124
|
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
|
|
125
|
+
/**
|
|
126
|
+
* The newest complete catalog this caller could be listed, classified and
|
|
127
|
+
* filtered as a listing is, from storage alone. For discovery; a call never
|
|
128
|
+
* resolves through it. Undefined for a static catalog or a miss.
|
|
129
|
+
*/
|
|
130
|
+
peekCatalog(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<CatalogPeek | undefined>;
|
|
117
131
|
contextFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): ConnectorContext;
|
|
118
132
|
/** Acquire the connector's shared downstream-call permit. */
|
|
119
133
|
admitCall(id: string, input: {
|
|
@@ -125,8 +139,19 @@ export interface RegistryView {
|
|
|
125
139
|
resultIdentity(): ResultIdentity;
|
|
126
140
|
/** One validation pass: admit once, then check every dependency under that scope. */
|
|
127
141
|
recheckResultAccess(dependencies: readonly ProgramResultDependency[], classification: "read" | "write", signal?: AbortSignal): Promise<boolean>;
|
|
128
|
-
/**
|
|
129
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Reserve deployment-wide capacity before writing a paging envelope's chunks.
|
|
144
|
+
* `holdFirst` charges the whole envelope but leaves chunk 0, which makes the
|
|
145
|
+
* result readable, for the caller to write once the result is sealed.
|
|
146
|
+
*/
|
|
147
|
+
stashResult(id: string, chunks: readonly string[], ttlSeconds: number, options?: {
|
|
148
|
+
readonly holdFirst?: boolean;
|
|
149
|
+
}): Promise<boolean>;
|
|
150
|
+
/**
|
|
151
|
+
* Give back a stashed result nobody can name any more. Best effort: a failure
|
|
152
|
+
* leaves it charged until its expiry.
|
|
153
|
+
*/
|
|
154
|
+
releaseResult(id: string, chunkCount: number): Promise<void>;
|
|
130
155
|
/**
|
|
131
156
|
* Local declared-vs-stored credential mismatch, with no downstream I/O. With
|
|
132
157
|
* `requestScope`, the read joins that request's read memo.
|
|
@@ -260,7 +285,11 @@ export declare class Registry implements RegistryView {
|
|
|
260
285
|
* unconfirmed and retain their charge. Cleanup/release and settlement each
|
|
261
286
|
* stop waiting after 15 seconds. Storage TTL reclaims the rows.
|
|
262
287
|
*/
|
|
263
|
-
stashResult(id: string, chunks: readonly string[], ttlSeconds: number,
|
|
288
|
+
stashResult(id: string, chunks: readonly string[], ttlSeconds: number, options?: {
|
|
289
|
+
readonly partition?: string;
|
|
290
|
+
readonly holdFirst?: boolean;
|
|
291
|
+
}): Promise<boolean>;
|
|
292
|
+
releaseResult(id: string, chunkCount: number, partition?: string): Promise<void>;
|
|
264
293
|
/**
|
|
265
294
|
* Storage namespaced to the root result partition, kept separate from any
|
|
266
295
|
* connector's namespace. Backs connecta.result.
|
|
@@ -280,6 +309,8 @@ export declare class Registry implements RegistryView {
|
|
|
280
309
|
/** Compare only the fingerprints accepted in this exact cache partition. */
|
|
281
310
|
private recordCatalogDrift;
|
|
282
311
|
private loadDownstreamTools;
|
|
312
|
+
/** See {@link RegistryView.peekCatalog}. Same classification and filtering as `loadTools`. */
|
|
313
|
+
private loadPeek;
|
|
283
314
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
284
315
|
private acceptToolNames;
|
|
285
316
|
/**
|
|
@@ -292,6 +323,7 @@ export declare class Registry implements RegistryView {
|
|
|
292
323
|
/** Construction-only view for the secret-free static config description. */
|
|
293
324
|
describeStaticTools(id: string): ToolDef[] | undefined;
|
|
294
325
|
private validateClassification;
|
|
326
|
+
peekCatalog(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, scope?: RegistryScope): Promise<CatalogPeek | undefined>;
|
|
295
327
|
getTools(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions, scope?: RegistryScope): Promise<ToolDef[]>;
|
|
296
328
|
credentialDriftFor(id: string, requestScope?: object): Promise<string | undefined>;
|
|
297
329
|
/** Age of a complete cache entry; reads never refresh it. */
|
package/dist/registry.js
CHANGED
|
@@ -6,21 +6,21 @@ import { authCall, authInvoke } from "./auth/native.js";
|
|
|
6
6
|
import { Clock, Duration, Effect, Random } from "effect";
|
|
7
7
|
import { activityRequest } from "./activity-request.js";
|
|
8
8
|
import { aggregateCallAdmissionSnapshots, ConnectorCallAdmissionController, } from "./call-admission.js";
|
|
9
|
-
import { attachCatalogCache, catalogExpiry, catalogFetchedAt, customCatalogFallback, invalidateCatalogCache, observeUncachedCatalogRefresh, retainedCatalogIdentity, storeCustomCatalogFallback, } from "./catalog-cache.js";
|
|
10
|
-
import { carryCatalogFreshness, markCatalogFreshness } from "./catalog-freshness.js";
|
|
9
|
+
import { attachCatalogCache, catalogExpiry, catalogFetchedAt, bindCustomCatalogListing, customCatalogFallback, customCatalogPeek, fenceCustomCatalog, invalidateCatalogCache, observeUncachedCatalogRefresh, retainedCatalogIdentity, storeCustomCatalogFallback, } from "./catalog-cache.js";
|
|
10
|
+
import { carryCatalogFreshness, markCatalogFreshness, markCatalogStale } from "./catalog-freshness.js";
|
|
11
11
|
import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
|
|
12
12
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
13
13
|
import { usesCallerIdentity } from "./connector-caller.js";
|
|
14
14
|
import { forwardsPrincipal } from "./request-principal.js";
|
|
15
15
|
import { canonicalJson } from "./canonical-json.js";
|
|
16
16
|
import { storedCredentialShape } from "./credential-rules.js";
|
|
17
|
-
import { ConnectorCallError } from "./errors.js";
|
|
17
|
+
import { ConnectorCallError, isAccessRefusal } from "./errors.js";
|
|
18
18
|
import { attachOAuthPartition, oauthPartitionIdle } from "./oauth-partition.js";
|
|
19
19
|
import { attachOAuthSealer, vaultOAuthSealer } from "./oauth-sealing.js";
|
|
20
20
|
import { boundedStatus, failureRecord, failureStatus, logFailure, ownStatus } from "./operator-record.js";
|
|
21
21
|
import { ObservedOutputSchemas } from "./result-shapes.js";
|
|
22
22
|
import { ContinuationBinding, OperationCredentials, ContinuationStorage, operationView, setViewService, duringViewEffect, } from "./runtime/call.js";
|
|
23
|
-
import { hasNativeStorage, registerNativeStorage } from "./runtime/native-storage.js";
|
|
23
|
+
import { getNativeStorage, hasNativeStorage, registerNativeStorage } from "./runtime/native-storage.js";
|
|
24
24
|
import { observeStorageWrites } from "./runtime/request-reads.js";
|
|
25
25
|
import { attachStorageDigestKey } from "./storage-digest.js";
|
|
26
26
|
import { requestReads } from "./runtime/request-scope.js";
|
|
@@ -31,10 +31,10 @@ import { Storage } from "./runtime/services.js";
|
|
|
31
31
|
import { SharedRead } from "./runtime/shared-read.js";
|
|
32
32
|
import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageGetMany, storageSet, storageList, } from "./runtime/storage.js";
|
|
33
33
|
import { redactCatalog, sentSecretsFor, sentSecretsForRequest } from "./sent-secrets.js";
|
|
34
|
-
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary } from "./skills.js";
|
|
34
|
+
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, connectorGuide, connectorGuideSummary } from "./skills.js";
|
|
35
35
|
import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashExpiryKeys, stashLedgerKeys, } from "./storage/keys.js";
|
|
36
36
|
import { assertStaticToolNames, hasControlCharacters } from "./tool-name.js";
|
|
37
|
-
import { carryToolClassification, classifyTool } from "./tool-safety.js";
|
|
37
|
+
import { carryToolClassification, classifyTool, sourceDeclaresWrite, } from "./tool-safety.js";
|
|
38
38
|
import { isWorkspaceAddress } from "./workspace-address.js";
|
|
39
39
|
import { workspaceSubjectOf } from "./workspace-subject.js";
|
|
40
40
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
@@ -145,12 +145,18 @@ const STASH_WRITE_TIMEOUT_MS = 30_000;
|
|
|
145
145
|
const STASH_COMPLETION_TIMEOUT_MS = 15_000;
|
|
146
146
|
/** Reservation, chunk writes and completion share one finite outer budget. */
|
|
147
147
|
const STASH_TIMEOUT_MS = STASH_WRITE_TIMEOUT_MS + STASH_COMPLETION_TIMEOUT_MS;
|
|
148
|
+
/** Chunks one release deletes at a time; a stash holds at most 32. */
|
|
149
|
+
const STASH_RELEASE_CONCURRENCY = 16;
|
|
148
150
|
/** Stop waiting without starting background storage work after the deadline. */
|
|
149
151
|
function completeStash(effect) {
|
|
150
152
|
return effect.pipe(Effect.asVoid, Effect.timeoutOrElse({ duration: Duration.millis(STASH_COMPLETION_TIMEOUT_MS), orElse: () => Effect.void }),
|
|
151
153
|
// onError finalizers are otherwise uninterruptible, including their I/O.
|
|
152
154
|
Effect.interruptible);
|
|
153
155
|
}
|
|
156
|
+
/** The reservation id of one result id, which is random and never reused. */
|
|
157
|
+
function stashCharge(id) {
|
|
158
|
+
return Effect.tryPromise({ try: () => sha256Hex(id), catch: (error) => error });
|
|
159
|
+
}
|
|
154
160
|
/**
|
|
155
161
|
* Live charges in a stored ledger. Anything malformed reads as empty: the
|
|
156
162
|
* ledger bounds an advisory cache, and the next successful swap replaces it.
|
|
@@ -298,6 +304,13 @@ export class Registry {
|
|
|
298
304
|
`normalization; the discovery bound is ${GUIDE_SUMMARY_LENGTH}. ` +
|
|
299
305
|
"Shorten it or omit it to derive one.");
|
|
300
306
|
}
|
|
307
|
+
// Advisory English-language heuristic: a task verb or operation noun
|
|
308
|
+
// distinguishes actionable guidance from credential/scope facts. Never
|
|
309
|
+
// log guide text, which may contain deployment-private information.
|
|
310
|
+
if (connectorGuide(c) !== undefined &&
|
|
311
|
+
!/\b(load|use|when|read|reads|write|writes|search|query|inspect|resolve|list|review|edit|create|update|deploy|paging|reconciliation|accounting)\b/i.test(connectorGuideSummary(c) ?? "")) {
|
|
312
|
+
opts.logger.warn(`Connector "${c.id}" guide needs an actionable summary; say when to load it with usageGuide.summary.`);
|
|
313
|
+
}
|
|
301
314
|
this.connectors.set(c.id, c);
|
|
302
315
|
if (c.callAdmission !== undefined) {
|
|
303
316
|
this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
|
|
@@ -310,8 +323,8 @@ export class Registry {
|
|
|
310
323
|
Object.freeze(this.classification);
|
|
311
324
|
for (const [id, overrides] of Object.entries(this.classification)) {
|
|
312
325
|
for (const [name, verdict] of Object.entries(overrides)) {
|
|
313
|
-
if (!name ||
|
|
314
|
-
throw new Error(`ConnectaConfig.classification.${id}.${name}: expected "read", "write", or "hidden"`);
|
|
326
|
+
if (!name || !["read", "read-repeatable", "write", "hidden"].includes(verdict)) {
|
|
327
|
+
throw new Error(`ConnectaConfig.classification.${id}.${name}: expected "read", "read-repeatable", "write", or "hidden"`);
|
|
315
328
|
}
|
|
316
329
|
}
|
|
317
330
|
const connector = this.connectors.get(id);
|
|
@@ -610,8 +623,11 @@ export class Registry {
|
|
|
610
623
|
* unconfirmed and retain their charge. Cleanup/release and settlement each
|
|
611
624
|
* stop waiting after 15 seconds. Storage TTL reclaims the rows.
|
|
612
625
|
*/
|
|
613
|
-
stashResult(id, chunks, ttlSeconds,
|
|
614
|
-
return runOnPartition(registryEdgeProgram(Registry.prototype, this, "stashResult", id, chunks, ttlSeconds,
|
|
626
|
+
stashResult(id, chunks, ttlSeconds, options = {}) {
|
|
627
|
+
return runOnPartition(registryEdgeProgram(Registry.prototype, this, "stashResult", id, chunks, ttlSeconds, options), this.opts);
|
|
628
|
+
}
|
|
629
|
+
releaseResult(id, chunkCount, partition = scopes.results) {
|
|
630
|
+
return runOnPartition(registryEdgeProgram(Registry.prototype, this, "releaseResult", id, chunkCount, partition), this.opts);
|
|
615
631
|
}
|
|
616
632
|
/**
|
|
617
633
|
* Storage namespaced to the root result partition, kept separate from any
|
|
@@ -674,7 +690,16 @@ export class Registry {
|
|
|
674
690
|
if (connector.staticTools)
|
|
675
691
|
return markCatalogFreshness(connector.staticTools, Infinity);
|
|
676
692
|
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
677
|
-
|
|
693
|
+
// `remoteMcp()` binds and fences its own attempts, knowing how each was made.
|
|
694
|
+
const customListing = connector.peekCatalog
|
|
695
|
+
? undefined
|
|
696
|
+
: yield* authCall(bindCustomCatalogListing, [ctx, id]).pipe(Effect.catch((failure) => Effect.sync(() => {
|
|
697
|
+
logFailure(this.opts.logger, "catalog read failed", failureRecord({ connector: id }, failure));
|
|
698
|
+
return undefined;
|
|
699
|
+
})));
|
|
700
|
+
const listed = yield* authInvoke(connector, "listTools", [ctx]).pipe(Effect.catch((error) => Effect.gen({ self: this }, function* () {
|
|
701
|
+
if (customListing && isAccessRefusal(error))
|
|
702
|
+
yield* authCall(fenceCustomCatalog, [ctx, id, customListing]).pipe(Effect.catch((failure) => Effect.sync(() => logFailure(this.opts.logger, "catalog fence failed", failureRecord({ connector: id }, failure)))));
|
|
678
703
|
const fallback = error instanceof ConnectorCallError && error.code === "unavailable"
|
|
679
704
|
? yield* authCall(customCatalogFallback, [ctx, id]).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
680
705
|
: undefined;
|
|
@@ -690,12 +715,18 @@ export class Registry {
|
|
|
690
715
|
const facts = frozenFacts(structuredClone(tools));
|
|
691
716
|
const accepted = this.acceptToolNames(facts);
|
|
692
717
|
if (catalogFetchedAt(ctx) === undefined) {
|
|
693
|
-
for (const observe of [
|
|
694
|
-
|
|
718
|
+
for (const observe of [
|
|
719
|
+
authCall(storeCustomCatalogFallback, [ctx, facts, customListing]),
|
|
720
|
+
authCall(observeUncachedCatalogRefresh, [ctx, id, facts]),
|
|
721
|
+
]) {
|
|
722
|
+
yield* observe.pipe(Effect.catch((error) => Effect.sync(() => logFailure(this.opts.logger, "catalog refresh observation failed", failureRecord({ connector: id }, error)))));
|
|
695
723
|
}
|
|
696
724
|
}
|
|
697
725
|
const provenance = catalogExpiry(ctx);
|
|
698
726
|
const freshUntil = provenance && !provenance.staleFallback ? provenance.expiresAt : 0;
|
|
727
|
+
// A transient-failure fallback answers the listing; discovery still reports its age.
|
|
728
|
+
if (provenance?.staleFallback)
|
|
729
|
+
markCatalogStale(accepted, { code: "unavailable", ageMs: Math.max(0, Date.now() - provenance.fetchedAt) });
|
|
699
730
|
// Principal-forwarded listings belong to one caller, never to the shared
|
|
700
731
|
// operator observation store. The actual catalog cache is identity-bound.
|
|
701
732
|
if (!forwardsPrincipal(connector)) {
|
|
@@ -708,6 +739,30 @@ export class Registry {
|
|
|
708
739
|
return markCatalogFreshness(accepted, freshUntil);
|
|
709
740
|
});
|
|
710
741
|
}
|
|
742
|
+
/** See {@link RegistryView.peekCatalog}. Same classification and filtering as `loadTools`. */
|
|
743
|
+
loadPeek(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
744
|
+
return Effect.gen({ self: this }, function* () {
|
|
745
|
+
const connector = this.connectors.get(id);
|
|
746
|
+
if (!connector || connector.staticTools)
|
|
747
|
+
return undefined;
|
|
748
|
+
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
749
|
+
const peek = connector.peekCatalog
|
|
750
|
+
? yield* authInvoke(connector, "peekCatalog", [ctx])
|
|
751
|
+
: yield* authCall(customCatalogPeek, [ctx, id]);
|
|
752
|
+
if (!peek)
|
|
753
|
+
return undefined;
|
|
754
|
+
const facts = redactCatalog(ctx, peek.tools).map(({ classification: _ignored, ...fact }) => fact);
|
|
755
|
+
const overrides = this.classification[id];
|
|
756
|
+
return {
|
|
757
|
+
tools: this.acceptToolNames(frozenFacts(structuredClone(facts)))
|
|
758
|
+
.filter((tool) => overrides?.[tool.name] !== "hidden")
|
|
759
|
+
.map((tool) => this.publishTool(id, tool)),
|
|
760
|
+
fetchedAt: peek.fetchedAt,
|
|
761
|
+
// A custom catalog is always listed anew, so its retained copy is never current.
|
|
762
|
+
fresh: connector.peekCatalog !== undefined && peek.expiresAt > Date.now(),
|
|
763
|
+
};
|
|
764
|
+
});
|
|
765
|
+
}
|
|
711
766
|
/** The served catalog excludes control-character names; raw caches stay complete. */
|
|
712
767
|
acceptToolNames(tools) {
|
|
713
768
|
const accepted = tools.filter((tool) => !hasControlCharacters(tool.name));
|
|
@@ -734,19 +789,20 @@ export class Registry {
|
|
|
734
789
|
}
|
|
735
790
|
publishTool(id, tool) {
|
|
736
791
|
const override = this.classification[id]?.[tool.name];
|
|
792
|
+
const classification = classifyTool(tool, override === "hidden" ? undefined : override);
|
|
737
793
|
return carryToolClassification(tool, {
|
|
738
794
|
...structuredClone(tool),
|
|
739
|
-
classification
|
|
795
|
+
classification,
|
|
740
796
|
...(override !== undefined
|
|
741
797
|
? {
|
|
742
798
|
annotations: {
|
|
743
799
|
...structuredClone(tool.annotations),
|
|
744
|
-
readOnlyHint: override === "read",
|
|
745
|
-
destructiveHint: override === "write",
|
|
800
|
+
readOnlyHint: override !== "hidden" && classification === "read",
|
|
801
|
+
destructiveHint: override !== "hidden" && classification === "write",
|
|
746
802
|
},
|
|
747
803
|
}
|
|
748
804
|
: {}),
|
|
749
|
-
});
|
|
805
|
+
}, override === "read-repeatable" || undefined);
|
|
750
806
|
}
|
|
751
807
|
/** Construction-only view for the secret-free static config description. */
|
|
752
808
|
describeStaticTools(id) {
|
|
@@ -764,19 +820,30 @@ export class Registry {
|
|
|
764
820
|
return;
|
|
765
821
|
this.overrideWarningCatalogs.set(partition, catalog);
|
|
766
822
|
}
|
|
767
|
-
const known = new
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
if (
|
|
823
|
+
const known = new Map(tools.map((tool) => [tool.name, tool]));
|
|
824
|
+
const staticCatalog = this.connectors.get(id)?.staticTools !== undefined;
|
|
825
|
+
for (const [name, override] of Object.entries(overrides ?? {})) {
|
|
826
|
+
const tool = known.get(name);
|
|
827
|
+
if (!tool && staticCatalog) {
|
|
772
828
|
throw new Error(`ConnectaConfig.classification: connector "${id}" has no tool "${name}"`);
|
|
773
829
|
}
|
|
774
830
|
// Only deployment-authored names, never downstream text. JSON quoting
|
|
775
831
|
// prevents control characters from forging operator log lines.
|
|
776
832
|
const quoted = JSON.stringify(`${id}.${name}`).replace(/[\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16)}`);
|
|
833
|
+
if (tool) {
|
|
834
|
+
// A static catalog cannot change, so the deployment's construction reports it once.
|
|
835
|
+
const reports = staticCatalog ? partition === undefined && this.opts.constructionChecks !== false : true;
|
|
836
|
+
if (override === "read" && reports && sourceDeclaresWrite(tool)) {
|
|
837
|
+
this.opts.logger.warn(`classification override ${quoted} to "read" is not applied because the connector declares this tool a write; it stays a write. "read-repeatable" serves it as a read and accepts that hosts may repeat it`);
|
|
838
|
+
}
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
777
841
|
this.opts.logger.warn(`classification override ${quoted} is dormant because the runtime catalog lists no such tool`);
|
|
778
842
|
}
|
|
779
843
|
}
|
|
844
|
+
peekCatalog(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
845
|
+
return runEdge(registryEdgeProgram(Registry.prototype, this, "peekCatalog", id, baseUrl, requestScope, callOptions, scope));
|
|
846
|
+
}
|
|
780
847
|
getTools(id, baseUrl, requestScope, callOptions = {}, scope) {
|
|
781
848
|
return runEdge(registryEdgeProgram(Registry.prototype, this, "getTools", id, baseUrl, requestScope, callOptions, scope));
|
|
782
849
|
}
|
|
@@ -811,7 +878,8 @@ export class Registry {
|
|
|
811
878
|
}
|
|
812
879
|
static {
|
|
813
880
|
provideRegistryPrograms(Registry.prototype, {
|
|
814
|
-
stashResult: (self, id, chunks, ttlSeconds,
|
|
881
|
+
stashResult: (self, id, chunks, ttlSeconds, options = {}) => Effect.gen({ self }, function* () {
|
|
882
|
+
const partition = options.partition ?? scopes.results;
|
|
815
883
|
const maxBytes = this.opts.results?.maxStashBytes ?? CONFIG_DEFAULTS.results.maxStashBytes;
|
|
816
884
|
const maxEntries = this.opts.results?.maxStashEntries ?? CONFIG_DEFAULTS.results.maxStashEntries;
|
|
817
885
|
// The paging envelope is ASCII, so its string length is its stored byte count.
|
|
@@ -819,8 +887,9 @@ export class Registry {
|
|
|
819
887
|
if (bytes > maxBytes || maxEntries === 0)
|
|
820
888
|
return false;
|
|
821
889
|
const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
|
|
822
|
-
//
|
|
823
|
-
|
|
890
|
+
// A digest finds this reservation again on release without putting
|
|
891
|
+
// the result's ID in the deployment-wide ledger.
|
|
892
|
+
const charge = yield* stashCharge(id);
|
|
824
893
|
const ttlMs = ttlSeconds * 1000;
|
|
825
894
|
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
826
895
|
return false;
|
|
@@ -874,16 +943,22 @@ export class Registry {
|
|
|
874
943
|
// ID after failure or timeout, including a late header commit.
|
|
875
944
|
const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
|
|
876
945
|
const written = yield* Effect.gen(function* () {
|
|
877
|
-
|
|
946
|
+
const multi = getNativeStorage(self.opts.storage)?.setMany;
|
|
947
|
+
const lowest = options.holdFirst ? 1 : 0;
|
|
948
|
+
for (let index = keys.length - 1; index >= lowest;) {
|
|
949
|
+
const first = multi && index > 0 ? Math.max(1, index - 63) : index;
|
|
878
950
|
const before = yield* Clock.currentTimeMillis;
|
|
879
951
|
if (before >= writeDeadline || before >= chunkExpiry)
|
|
880
952
|
return false;
|
|
881
953
|
pendingWrite = true;
|
|
882
|
-
yield*
|
|
954
|
+
yield* (multi && index > 0
|
|
955
|
+
? multi(Array.from({ length: index - first + 1 }, (_, offset) => [keys[index - offset], chunks[index - offset]]), { expiresAtMs: chunkExpiry })
|
|
956
|
+
: storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry })).pipe(Effect.tap(() => Effect.sync(() => {
|
|
883
957
|
pendingWrite = false;
|
|
884
958
|
})));
|
|
885
959
|
if ((yield* Clock.currentTimeMillis) >= writeDeadline)
|
|
886
960
|
return false;
|
|
961
|
+
index = first - 1;
|
|
887
962
|
}
|
|
888
963
|
return true;
|
|
889
964
|
}).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
|
|
@@ -893,6 +968,15 @@ export class Registry {
|
|
|
893
968
|
yield* completeStash(settle);
|
|
894
969
|
return written;
|
|
895
970
|
}).pipe(Effect.timeoutOrElse({ duration: Duration.millis(STASH_TIMEOUT_MS), orElse: () => Effect.succeed(false) }), Effect.provideService(Storage, self.opts.storage)),
|
|
971
|
+
releaseResult: (self, id, chunkCount, partition = scopes.results) => completeStash(Effect.gen(function* () {
|
|
972
|
+
const charge = yield* stashCharge(id);
|
|
973
|
+
// Bytes first: a failed delete keeps its charge, so the ledger never
|
|
974
|
+
// undercounts what storage holds.
|
|
975
|
+
yield* Effect.forEach(Array.from({ length: chunkCount }, (_, index) => partition + resultKeys.chunk(id, index)), (key) => storageDelete(key), { concurrency: STASH_RELEASE_CONCURRENCY, discard: true });
|
|
976
|
+
yield* swapStashLedger((live) => live.some((entry) => entry[0] === charge)
|
|
977
|
+
? { entries: live.filter((entry) => entry[0] !== charge), result: undefined }
|
|
978
|
+
: { result: undefined });
|
|
979
|
+
})).pipe(Effect.ignore, Effect.provideService(Storage, self.opts.storage)),
|
|
896
980
|
recheckResultAccess: () => Effect.succeed(true),
|
|
897
981
|
storeOAuthHandoff: (self, connectorId, state, principalKey) => Effect.gen(function* () {
|
|
898
982
|
const key = oauthHandoffKeys.handoff(connectorId, yield* Effect.tryPromise({ try: () => sha256Hex(state), catch: (error) => error }));
|
|
@@ -961,6 +1045,12 @@ export class Registry {
|
|
|
961
1045
|
return yield* load.join(signal);
|
|
962
1046
|
}));
|
|
963
1047
|
}),
|
|
1048
|
+
peekCatalog: (self, id, baseUrl, requestScope, callOptions = {}, scope) => Effect.suspend(() => {
|
|
1049
|
+
if (!self.connectors.has(id))
|
|
1050
|
+
return Effect.succeed(undefined);
|
|
1051
|
+
const owner = self.contextFor(id, baseUrl, requestScope, callOptions, scope);
|
|
1052
|
+
return duringViewEffect(owner, (signal) => self.loadPeek(id, baseUrl, owner.requestScope, { ...callOptions, signal }, scope));
|
|
1053
|
+
}),
|
|
964
1054
|
credentialDriftFor: (self, id, requestScope) => Effect.gen(function* () {
|
|
965
1055
|
const credential = self.connectors.get(id)?.credential;
|
|
966
1056
|
const vault = self.opts.credentialVault;
|
|
@@ -1085,6 +1175,9 @@ class ScopedRegistryView {
|
|
|
1085
1175
|
getTools(...args) {
|
|
1086
1176
|
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "getTools", ...args));
|
|
1087
1177
|
}
|
|
1178
|
+
peekCatalog(...args) {
|
|
1179
|
+
return runEdge(registryEdgeProgram(ScopedRegistryView.prototype, this, "peekCatalog", ...args));
|
|
1180
|
+
}
|
|
1088
1181
|
contextFor(...args) {
|
|
1089
1182
|
const registry = this.registryFor(args[0]);
|
|
1090
1183
|
if (!registry)
|
|
@@ -1098,8 +1191,14 @@ class ScopedRegistryView {
|
|
|
1098
1191
|
}
|
|
1099
1192
|
return registry.admitCall(...args);
|
|
1100
1193
|
}
|
|
1101
|
-
stashResult(id, chunks, ttlSeconds) {
|
|
1102
|
-
return this.root.stashResult(id, chunks, ttlSeconds,
|
|
1194
|
+
stashResult(id, chunks, ttlSeconds, options = {}) {
|
|
1195
|
+
return this.root.stashResult(id, chunks, ttlSeconds, {
|
|
1196
|
+
...options,
|
|
1197
|
+
partition: this.scope.subjectKey ? scopes.subject(this.scope.subjectKey) : scopes.results,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
releaseResult(id, chunkCount) {
|
|
1201
|
+
return this.root.releaseResult(id, chunkCount, this.scope.subjectKey ? scopes.subject(this.scope.subjectKey) : scopes.results);
|
|
1103
1202
|
}
|
|
1104
1203
|
resultIdentity() {
|
|
1105
1204
|
return {
|
|
@@ -1146,7 +1245,11 @@ class ScopedRegistryView {
|
|
|
1146
1245
|
}
|
|
1147
1246
|
static {
|
|
1148
1247
|
provideRegistryPrograms(ScopedRegistryView.prototype, {
|
|
1149
|
-
|
|
1248
|
+
releaseResult: (self, id, chunkCount) => registryProgram(self.root, "releaseResult", id, chunkCount, self.scope.subjectKey ? scopes.subject(self.scope.subjectKey) : scopes.results),
|
|
1249
|
+
stashResult: (self, id, chunks, ttlSeconds, options = {}) => registryProgram(self.root, "stashResult", id, chunks, ttlSeconds, {
|
|
1250
|
+
...options,
|
|
1251
|
+
partition: self.scope.subjectKey ? scopes.subject(self.scope.subjectKey) : scopes.results,
|
|
1252
|
+
}),
|
|
1150
1253
|
recheckResultAccess: (self, dependencies, classification, signal) => Effect.tryPromise({
|
|
1151
1254
|
try: () => self.scope.currentResultAccess?.(dependencies, classification, signal) ?? Promise.resolve(true),
|
|
1152
1255
|
catch: (error) => error,
|
|
@@ -1173,6 +1276,15 @@ class ScopedRegistryView {
|
|
|
1173
1276
|
}
|
|
1174
1277
|
return carryCatalogFreshness(tools, visible);
|
|
1175
1278
|
}),
|
|
1279
|
+
peekCatalog: (self, ...args) => Effect.gen(function* () {
|
|
1280
|
+
const registry = self.registryFor(args[0]);
|
|
1281
|
+
if (!registry)
|
|
1282
|
+
return undefined;
|
|
1283
|
+
const peek = yield* registryProgram(registry, "peekCatalog", args[0], args[1], args[2], args[3], self.scope);
|
|
1284
|
+
const granted = self.scope.toolAccess?.get(args[0]);
|
|
1285
|
+
// A pool grant binds a stale listing exactly as it binds a live one.
|
|
1286
|
+
return peek && granted ? { ...peek, tools: peek.tools.filter((tool) => granted.has(tool.name)) } : peek;
|
|
1287
|
+
}),
|
|
1176
1288
|
credentialDriftFor: (self, id, requestScope) => Effect.suspend(() => {
|
|
1177
1289
|
const registry = self.registryFor(id);
|
|
1178
1290
|
return registry
|
package/dist/resource-read.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { utilityRecovery } from "./utility-recovery.js";
|
|
1
2
|
import { Effect } from "effect";
|
|
2
3
|
import { classifyCallError, ConnectorCallError } from "./errors.js";
|
|
3
4
|
import { InvocationFailure, timed } from "./invocation.js";
|
|
4
5
|
import { withDeadlineEffect } from "./runtime/run.js";
|
|
5
6
|
import { sentSecretsFor } from "./sent-secrets.js";
|
|
7
|
+
import { downstreamOrigin, isSkillResource } from "./skill-origin.js";
|
|
6
8
|
import { hasControlCharacters } from "./tool-name.js";
|
|
7
9
|
const encoder = new TextEncoder();
|
|
8
10
|
const URI_HINT = "Use resource://<connectorId>/<encodeURIComponent(downstreamUri)> with a whole-connector grant.";
|
|
@@ -26,13 +28,26 @@ function resourceTarget(registry, raw) {
|
|
|
26
28
|
const connector = registry.getResourceConnector(match[1]);
|
|
27
29
|
if (!connector?.readResource) {
|
|
28
30
|
throw new InvocationFailure({
|
|
29
|
-
|
|
31
|
+
...utilityRecovery(connector ? "unsupported_capability" : "unknown_address", "No resource reader is available."),
|
|
32
|
+
code: connector ? "unsupported_capability" : "unknown_address",
|
|
30
33
|
message: `No resource reader is available for this address. ${URI_HINT}`,
|
|
31
34
|
retryable: false,
|
|
32
35
|
});
|
|
33
36
|
}
|
|
34
37
|
return { connector, uri };
|
|
35
38
|
}
|
|
39
|
+
/** Frame skill text for a program; native resources/read keeps exact bytes and never comes here. */
|
|
40
|
+
function labelSkillText(contents, connectorId, uri) {
|
|
41
|
+
const items = contents?.contents;
|
|
42
|
+
if (!Array.isArray(items))
|
|
43
|
+
return contents;
|
|
44
|
+
return {
|
|
45
|
+
...contents,
|
|
46
|
+
contents: items.map((item) => typeof item?.text === "string" && (isSkillResource(uri) || isSkillResource(String(item.uri ?? "")))
|
|
47
|
+
? { ...item, text: `${downstreamOrigin(connectorId)}\n\n${item.text}` }
|
|
48
|
+
: item),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
36
51
|
/** A protocol read, under the same request scope, call permit and deadline as tools. */
|
|
37
52
|
export function readResource(registry, raw, options) {
|
|
38
53
|
let connector;
|
|
@@ -68,7 +83,7 @@ export function readResource(registry, raw, options) {
|
|
|
68
83
|
},
|
|
69
84
|
catch: (error) => error,
|
|
70
85
|
}));
|
|
71
|
-
return options.sentSecrets.redact(contents);
|
|
86
|
+
return labelSkillText(options.sentSecrets.redact(contents), target.connector.id, target.uri);
|
|
72
87
|
})), {
|
|
73
88
|
timeoutMs: options.timeoutMs,
|
|
74
89
|
...(options.signal ? { signal: options.signal } : {}),
|
|
@@ -98,6 +113,10 @@ export function readResource(registry, raw, options) {
|
|
|
98
113
|
retry: "Retry connecta.read with the same resource URI after the operator completes recovery.",
|
|
99
114
|
};
|
|
100
115
|
}
|
|
116
|
+
if (["unavailable", "unsupported_capability", "input_required_unsupported"].includes(details.code) &&
|
|
117
|
+
!details.nextAction) {
|
|
118
|
+
details = { ...details, nextAction: utilityRecovery(details.code, details.message).nextAction };
|
|
119
|
+
}
|
|
101
120
|
return Effect.fail(new InvocationFailure(options.sentSecrets.redact(details)));
|
|
102
121
|
}));
|
|
103
122
|
}
|