@zackbart/connecta 0.31.0 → 0.33.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 +97 -0
- package/README.md +30 -20
- package/bin/connecta.mjs +25 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +14 -9
- package/dist/config.d.ts +58 -78
- package/dist/config.js +143 -47
- package/dist/connector-access.d.ts +4 -33
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +32 -7
- package/dist/describe-config.js +9 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +12 -7
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/meta-tools.js +17 -19
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -2
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +151 -29
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +15 -7
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +83 -21
- package/dist/page-styles.js +1 -1
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +63 -11
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +31 -9
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +5 -5
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +5 -5
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +279 -26
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +116 -11
- package/dist/providers/notion/rest.js +183 -24
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +287 -25
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +4 -1
- package/dist/providers/sheets/index.js +9 -6
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +6 -6
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +155 -85
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +25 -17
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +8 -0
- package/dist/skills.js +5 -4
- package/dist/storage/keys.js +9 -1
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +12 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +17 -15
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +378 -58
- package/documentation/auth.md +806 -215
- package/documentation/code-mode.md +93 -81
- package/documentation/deploying.md +128 -26
- package/documentation/integrating.md +145 -22
- package/documentation/meta-tools.md +58 -46
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +14 -9
- package/documentation/operator-tests.md +117 -31
- package/documentation/operator-ui.md +326 -55
- package/documentation/provider-auth-migration.md +42 -27
- package/documentation/provider-migration-0.29.md +3 -3
- package/examples/worker/AGENTS.md +35 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +41 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +7 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -10
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +71 -44
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +33 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/dist/tool-safety.d.ts +0 -9
- package/templates/node/src/provision-token.ts +0 -17
package/dist/meta-tools.js
CHANGED
|
@@ -210,12 +210,6 @@ async function stashResult(bytes, results, preview, rawText) {
|
|
|
210
210
|
totalBytes,
|
|
211
211
|
hint: `${results.write ? WRITE_ALREADY_RAN + " " : ""}Paging is unavailable: text contains unpaired surrogates and can't be paged as text; request the value as JSON (value mode or a JSON-returning call) to recover it.`,
|
|
212
212
|
};
|
|
213
|
-
if (!results.pageable)
|
|
214
|
-
return {
|
|
215
|
-
truncated: true,
|
|
216
|
-
totalBytes,
|
|
217
|
-
hint: `${WRITE_ALREADY_RAN} Paging is unavailable for write results on read-only pools.`,
|
|
218
|
-
};
|
|
219
213
|
const id = crypto.randomUUID();
|
|
220
214
|
try {
|
|
221
215
|
// Base64 permits byte-range decoding, and splitting the envelope across
|
|
@@ -270,7 +264,7 @@ async function stashResult(bytes, results, preview, rawText) {
|
|
|
270
264
|
};
|
|
271
265
|
}
|
|
272
266
|
/** Program calls use the direct-call envelope, ledger, expiry and authority. */
|
|
273
|
-
export async function pageProgramResult(registry, baseUrl, resolved, value, format, secrets,
|
|
267
|
+
export async function pageProgramResult(registry, baseUrl, resolved, value, format, secrets, requestScope, wireBytes = PROGRAM_RESULT_WIRE_BYTES) {
|
|
274
268
|
const data = secrets.redact(value);
|
|
275
269
|
const wire = JSON.stringify({ data, format });
|
|
276
270
|
if (enc.encode(wire).length <= wireBytes)
|
|
@@ -279,7 +273,6 @@ export async function pageProgramResult(registry, baseUrl, resolved, value, form
|
|
|
279
273
|
const bytes = enc.encode(text);
|
|
280
274
|
const write = resolved.definition.classification !== "read";
|
|
281
275
|
const notice = await stashResult(bytes, {
|
|
282
|
-
pageable: !write || trust === "trusted",
|
|
283
276
|
binding: {
|
|
284
277
|
identity: registry.resultIdentity(),
|
|
285
278
|
baseUrl,
|
|
@@ -304,7 +297,7 @@ export async function pageProgramResult(registry, baseUrl, resolved, value, form
|
|
|
304
297
|
};
|
|
305
298
|
}
|
|
306
299
|
/** Program returns share the direct stash, with all host-admitted call dependencies. */
|
|
307
|
-
export async function guardProgramReturn(value, registry, baseUrl, dependencies, secrets,
|
|
300
|
+
export async function guardProgramReturn(value, registry, baseUrl, dependencies, secrets, logger, maxChars, completedWrite) {
|
|
308
301
|
const serialized = serializeResultText(value);
|
|
309
302
|
if (serialized.length <= maxChars)
|
|
310
303
|
return value;
|
|
@@ -314,7 +307,6 @@ export async function guardProgramReturn(value, registry, baseUrl, dependencies,
|
|
|
314
307
|
const cap = Math.min(registry.maxResultBytes, ...dependencies.map((dependency) => resolveMaxResultBytes(registry.getConnector(dependency.connector)?.maxResultBytes, registry.maxResultBytes)));
|
|
315
308
|
const results = {
|
|
316
309
|
secrets,
|
|
317
|
-
pageable: !write || trust === "trusted",
|
|
318
310
|
binding: {
|
|
319
311
|
identity: registry.resultIdentity(),
|
|
320
312
|
baseUrl,
|
|
@@ -522,6 +514,8 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
522
514
|
});
|
|
523
515
|
const catalog = new CatalogService(registry, baseUrl, {
|
|
524
516
|
requestScope,
|
|
517
|
+
// search_tools never shows a read-only endpoint's writes.
|
|
518
|
+
readOnly: opts.readOnly,
|
|
525
519
|
probeTimeoutMs,
|
|
526
520
|
concurrency: discoveryConcurrency,
|
|
527
521
|
defer: opts.defer,
|
|
@@ -536,6 +530,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
536
530
|
const outcome = await invocation.invoke(call.address, call.args ?? {}, {
|
|
537
531
|
source,
|
|
538
532
|
sentSecrets,
|
|
533
|
+
readOnly: opts.readOnly,
|
|
539
534
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
540
535
|
...(opts.requestSignal !== undefined ? { requestSignal: opts.requestSignal } : {}),
|
|
541
536
|
unwrapResult: call.resultMode === "value",
|
|
@@ -557,7 +552,6 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
557
552
|
const cap = resolveMaxResultBytes(resolved.connector.maxResultBytes, globalCap);
|
|
558
553
|
const results = {
|
|
559
554
|
secrets,
|
|
560
|
-
pageable: resolved.definition.classification === "read" || opts.trust === "trusted",
|
|
561
555
|
binding: {
|
|
562
556
|
identity: registry.resultIdentity(),
|
|
563
557
|
baseUrl,
|
|
@@ -788,7 +782,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
788
782
|
if (binding.program &&
|
|
789
783
|
!(await registry.recheckResultAccess(undefined, binding.classification, options.signal)))
|
|
790
784
|
return false;
|
|
791
|
-
if (binding.classification === "write" && opts.
|
|
785
|
+
if (binding.classification === "write" && opts.readOnly === true)
|
|
792
786
|
return false;
|
|
793
787
|
const dependencies = binding.program ?? [binding];
|
|
794
788
|
for (const dependency of dependencies) {
|
|
@@ -809,7 +803,7 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
809
803
|
if (!tool ||
|
|
810
804
|
!(dependency.access === "catalog" ||
|
|
811
805
|
(dependency.classification === "read" && tool.classification === "read") ||
|
|
812
|
-
opts.
|
|
806
|
+
opts.readOnly !== true))
|
|
813
807
|
return false;
|
|
814
808
|
}
|
|
815
809
|
return true;
|
|
@@ -915,9 +909,11 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
|
|
|
915
909
|
},
|
|
916
910
|
};
|
|
917
911
|
}
|
|
918
|
-
const SEARCH_DESC =
|
|
919
|
-
const CALL_DESC =
|
|
912
|
+
const SEARCH_DESC = (readOnly) => `${readOnly ? "Use top-level search for catalog inspection; this read-only endpoint omits write tools." : "Use top-level search for catalog inspection or approval-required work before call_destructive_tool."} Unknown-address read-only work belongs in one execute_code program that searches, 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" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses. Returns { catalogErrors, tools, total, offset, limit, hasMore }; read catalogErrors and absence before selecting a tool. Compact schemas have schemaFormat="text".`;
|
|
913
|
+
const CALL_DESC = (readOnly) => `Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. ${readOnly ? "This endpoint is read-only and refuses every write." : "Unannotated or write-capable tools fail closed to call_destructive_tool."} A truncated result carries a connecta.result action.`;
|
|
920
914
|
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.";
|
|
915
|
+
/** A read-only endpoint keeps the six-tool surface, so this tool says why it never writes here. */
|
|
916
|
+
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 call_tool or execute_code for them.";
|
|
921
917
|
const AUTHORIZE_DESC = "Use after auth_required. Returns an OAuth or operator-credential handoff, or reports required deployment configuration. force=true requests an OAuth restart when the verified user opens the /connect URL; this tool never accepts credentials.";
|
|
922
918
|
const SKILLS_DESC = "List or fetch on-demand guidance. Fetch usage only when the always-loaded instructions are insufficient or a program needs repair.";
|
|
923
919
|
/**
|
|
@@ -1001,7 +997,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1001
997
|
const mt = createMetaTools(registry, ctx.baseUrl, {
|
|
1002
998
|
downstreamInput: Boolean(ctx.authElicitation),
|
|
1003
999
|
requestScope: ctx.requestScope,
|
|
1004
|
-
|
|
1000
|
+
readOnly: ctx.readOnly,
|
|
1005
1001
|
defaultToolTimeoutMs: ctx.defaultToolTimeoutMs,
|
|
1006
1002
|
probeTimeoutMs: ctx.probeTimeoutMs,
|
|
1007
1003
|
discoveryConcurrency: ctx.discoveryConcurrency,
|
|
@@ -1028,7 +1024,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1028
1024
|
return mt.skills(args);
|
|
1029
1025
|
});
|
|
1030
1026
|
server.registerTool("search_tools", {
|
|
1031
|
-
description: describedFor(registry, SEARCH_DESC, "search"),
|
|
1027
|
+
description: describedFor(registry, SEARCH_DESC(ctx.readOnly === true), "search"),
|
|
1032
1028
|
inputSchema: SEARCH_INPUT,
|
|
1033
1029
|
outputSchema: SEARCH_OUTPUT,
|
|
1034
1030
|
annotations: READ_ONLY_REMOTE,
|
|
@@ -1037,7 +1033,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1037
1033
|
return mt.searchTools(args);
|
|
1038
1034
|
});
|
|
1039
1035
|
server.registerTool("call_tool", {
|
|
1040
|
-
description: CALL_DESC,
|
|
1036
|
+
description: CALL_DESC(ctx.readOnly === true),
|
|
1041
1037
|
inputSchema: CALL_INPUT,
|
|
1042
1038
|
// Default mode returns native content, so no structured result is promised.
|
|
1043
1039
|
// call_tool admits only tools that are themselves explicitly read-only;
|
|
@@ -1050,7 +1046,9 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
1050
1046
|
: mt.callTool(args);
|
|
1051
1047
|
});
|
|
1052
1048
|
server.registerTool("call_destructive_tool", {
|
|
1053
|
-
description:
|
|
1049
|
+
description: ctx.readOnly === true
|
|
1050
|
+
? CALL_DESTRUCTIVE_READ_ONLY_DESC
|
|
1051
|
+
: describedFor(registry, CALL_DESTRUCTIVE_DESC, "destructive"),
|
|
1054
1052
|
inputSchema: CALL_DESTRUCTIVE_INPUT,
|
|
1055
1053
|
annotations: {
|
|
1056
1054
|
destructiveHint: true,
|
|
@@ -17,13 +17,30 @@ export interface ActivityModule {
|
|
|
17
17
|
readonly recordTool: typeof recordToolActivity;
|
|
18
18
|
readonly recordChange: typeof recordCatalogChangeActivity;
|
|
19
19
|
}
|
|
20
|
-
/** Optional
|
|
20
|
+
/** Optional agent-token authentication and operator lifecycle. */
|
|
21
21
|
export interface AccessTokensModule {
|
|
22
22
|
readonly auth: import("./types.js").InboundAuth;
|
|
23
23
|
handle(context: RouteContext): Promise<Response | null>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Every declared agent's token slot, in declaration order: the list GET
|
|
26
|
+
* /ui/access-tokens serves, which the maintainer's operator contract
|
|
27
|
+
* repeats field by field. Never secret material.
|
|
28
|
+
*/
|
|
29
|
+
slots(): Promise<Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
state: "none";
|
|
32
|
+
revision: string;
|
|
33
|
+
} | {
|
|
34
|
+
name: string;
|
|
35
|
+
state: "issued";
|
|
36
|
+
issuedAt: string;
|
|
37
|
+
revision: string;
|
|
38
|
+
} | {
|
|
39
|
+
name: string;
|
|
40
|
+
state: "revoked";
|
|
41
|
+
revokedAt: string;
|
|
42
|
+
revision: string;
|
|
43
|
+
}>>;
|
|
44
|
+
/** The same module bound to the deployment's declared `agents`; createConnecta calls it once. */
|
|
45
|
+
forAgents(agents: readonly string[]): AccessTokensModule;
|
|
29
46
|
}
|
|
@@ -8,7 +8,7 @@ type FailureStep = "MCP handshake" | "tools/list" | "tools/call" | "resources/re
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const OAUTH_ERROR_CODES: ReadonlySet<string>;
|
|
10
10
|
/** Inbound denial facts, never identity text or provider diagnostics. */
|
|
11
|
-
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "
|
|
11
|
+
export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "not_maintainer" | "pool_not_declared" | "not_pool_member";
|
|
12
12
|
/**
|
|
13
13
|
* Name `ctor`'s instances `label` in records and fixed text. Call once, at
|
|
14
14
|
* module scope, beside the class. An error whose prototype chain reaches no
|
|
@@ -104,7 +104,7 @@ export declare function failureRecord(subject: FailureSubject, failure?: unknown
|
|
|
104
104
|
* The events an operator log reports failures as. Closed: a new one is a
|
|
105
105
|
* reviewed line here, never a string assembled at the call site.
|
|
106
106
|
*/
|
|
107
|
-
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "
|
|
107
|
+
export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "pool tool grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP default endpoint request denied" | "MCP pool request denied" | "MCP agent endpoint request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "operator agent slots read failed" | "result paging unavailable" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
|
|
108
108
|
/**
|
|
109
109
|
* Log one failure as `[connecta] <event>` and its record. A record
|
|
110
110
|
* `failureRecord` did not build is replaced, whatever its shape, by a fixed
|
package/dist/operator-record.js
CHANGED
|
@@ -112,9 +112,9 @@ const AUTH_DENIAL_REASONS = new Set([
|
|
|
112
112
|
"email_domain_denied",
|
|
113
113
|
"gate_denied",
|
|
114
114
|
"gate_failed",
|
|
115
|
+
"not_maintainer",
|
|
115
116
|
"pool_not_declared",
|
|
116
|
-
"
|
|
117
|
-
"pool_grant_threw",
|
|
117
|
+
"not_pool_member",
|
|
118
118
|
]);
|
|
119
119
|
/** The registry's connector id grammar (src/registry.ts). */
|
|
120
120
|
const CONNECTOR_ID_RE = /^[a-z0-9_-]{1,64}$/;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { OperatorUiContractV2 } from "./contract.js";
|
|
2
|
+
import { type Notice, type RequestFailureFacts, type Tone } from "./view.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Tokens page's rows, computed from the slot list and contract v2 alone.
|
|
5
|
+
* Agents are declared in code: a row's only controls are its token's issue,
|
|
6
|
+
* rotate and revoke, so nothing here creates, renames or rebinds one.
|
|
7
|
+
*/
|
|
8
|
+
/** One declared agent's token slot, as `GET /ui/access-tokens` returns it. */
|
|
9
|
+
export type AgentSlot = ({
|
|
10
|
+
name: string;
|
|
11
|
+
state: "none";
|
|
12
|
+
} | {
|
|
13
|
+
name: string;
|
|
14
|
+
state: "issued";
|
|
15
|
+
issuedAt: string;
|
|
16
|
+
} | {
|
|
17
|
+
name: string;
|
|
18
|
+
state: "revoked";
|
|
19
|
+
revokedAt: string;
|
|
20
|
+
}) & {
|
|
21
|
+
/**
|
|
22
|
+
* The route's opaque slot revision; it changes on every slot commit (P3b,
|
|
23
|
+
* #837). A slot read without one is unresolved and never acted on.
|
|
24
|
+
*/
|
|
25
|
+
revision?: string;
|
|
26
|
+
};
|
|
27
|
+
/** Issue and rotate are the same request; the slot's state names it. */
|
|
28
|
+
export type SlotAction = "issue" | "rotate" | "revoke";
|
|
29
|
+
export interface AgentRow {
|
|
30
|
+
name: string;
|
|
31
|
+
/** Pools it belongs to, from `agents[].pools`; empty when the contract does not list it. */
|
|
32
|
+
pools: string[];
|
|
33
|
+
slot: AgentSlot;
|
|
34
|
+
/**
|
|
35
|
+
* Whether `slot` comes from the live slot list. Only a live slot can be
|
|
36
|
+
* acted on: one known only from the contract may be stale, so its controls
|
|
37
|
+
* are disabled until a refresh lists it.
|
|
38
|
+
*/
|
|
39
|
+
live: boolean;
|
|
40
|
+
/** Why its controls are off, or null when they work. */
|
|
41
|
+
blocked: string | null;
|
|
42
|
+
/** Its latest activity row; null is "never" (or history that is off). */
|
|
43
|
+
lastSeenAt: string | null;
|
|
44
|
+
actions: SlotAction[];
|
|
45
|
+
}
|
|
46
|
+
export declare function slotActions(slot: Pick<AgentSlot, "state">): SlotAction[];
|
|
47
|
+
/**
|
|
48
|
+
* One agent's slot, resolved once for both what the row shows and what an
|
|
49
|
+
* action may do. The live slot list is authoritative; the contract's copy is
|
|
50
|
+
* shown only for an agent the list does not have, and is never acted on.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveSlot(contract: OperatorUiContractV2 | null, slots: AgentSlot[], name: string): {
|
|
53
|
+
slot: AgentSlot;
|
|
54
|
+
live: boolean;
|
|
55
|
+
} | null;
|
|
56
|
+
/**
|
|
57
|
+
* One row per declared agent, in the slot list's order, then any agent only
|
|
58
|
+
* the contract lists. The contract adds pools and last seen.
|
|
59
|
+
*/
|
|
60
|
+
export declare function agentRows(contract: OperatorUiContractV2 | null, slots: AgentSlot[], unresolved?: readonly string[]): AgentRow[];
|
|
61
|
+
export declare function slotBadge(slot: Pick<AgentSlot, "state">): {
|
|
62
|
+
label: string;
|
|
63
|
+
tone: Tone;
|
|
64
|
+
};
|
|
65
|
+
/** When the slot last changed, as "Issued <date>" or "Revoked <date>"; a dash when it never has. */
|
|
66
|
+
export declare function slotSince(slot: AgentSlot, format: (value: string) => string): string;
|
|
67
|
+
export declare function lastSeenLabel(lastSeenAt: string | null, format: (value: string) => string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Why a token operation did not land, in fixed sentences keyed by status.
|
|
70
|
+
* `outcome_unknown` (503) and `aborted` (499) are failures like any other and
|
|
71
|
+
* are never retried for the operator: an unknown rotation returned no secret,
|
|
72
|
+
* and only a new, deliberate rotation produces one.
|
|
73
|
+
*/
|
|
74
|
+
export declare function tokenFailureNotice(action: SlotAction, name: string, facts: RequestFailureFacts, productName: string): Notice;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { failure } from "./view.js";
|
|
2
|
+
/** Why a row's controls are disabled when its slot is not in the live list. */
|
|
3
|
+
const SLOT_UNAVAILABLE = "Token state unavailable, refresh";
|
|
4
|
+
/** Why a row's controls are disabled until a fresh list read resolves its slot. */
|
|
5
|
+
const SLOT_UNRESOLVED = "Refresh to continue";
|
|
6
|
+
export function slotActions(slot) {
|
|
7
|
+
return slot.state === "issued" ? ["rotate", "revoke"] : ["issue"];
|
|
8
|
+
}
|
|
9
|
+
function contractSlot(agent) {
|
|
10
|
+
return { name: agent.name, ...agent.token };
|
|
11
|
+
}
|
|
12
|
+
function maintainerAgents(contract) {
|
|
13
|
+
return contract?.role === "maintainer" ? contract.agents : [];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One agent's slot, resolved once for both what the row shows and what an
|
|
17
|
+
* action may do. The live slot list is authoritative; the contract's copy is
|
|
18
|
+
* shown only for an agent the list does not have, and is never acted on.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveSlot(contract, slots, name) {
|
|
21
|
+
const listed = slots.find((slot) => slot.name === name);
|
|
22
|
+
if (listed)
|
|
23
|
+
return { slot: listed, live: true };
|
|
24
|
+
const agent = maintainerAgents(contract).find((candidate) => candidate.name === name);
|
|
25
|
+
return agent ? { slot: contractSlot(agent), live: false } : null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One row per declared agent, in the slot list's order, then any agent only
|
|
29
|
+
* the contract lists. The contract adds pools and last seen.
|
|
30
|
+
*/
|
|
31
|
+
export function agentRows(contract, slots, unresolved = []) {
|
|
32
|
+
const agents = maintainerAgents(contract);
|
|
33
|
+
const declared = new Map(agents.map((agent) => [agent.name, agent]));
|
|
34
|
+
const names = [...slots.map((slot) => slot.name), ...agents.map((agent) => agent.name)];
|
|
35
|
+
return [...new Set(names)].map((name) => {
|
|
36
|
+
const { slot, live } = resolveSlot(contract, slots, name);
|
|
37
|
+
const agent = declared.get(name);
|
|
38
|
+
return {
|
|
39
|
+
name,
|
|
40
|
+
pools: agent?.pools ?? [],
|
|
41
|
+
slot,
|
|
42
|
+
live,
|
|
43
|
+
blocked: !live ? SLOT_UNAVAILABLE : unresolved.includes(name) || !slot.revision ? SLOT_UNRESOLVED : null,
|
|
44
|
+
lastSeenAt: agent?.lastSeenAt ?? null,
|
|
45
|
+
actions: slotActions(slot),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function slotBadge(slot) {
|
|
50
|
+
if (slot.state === "issued")
|
|
51
|
+
return { label: "Issued", tone: "ok" };
|
|
52
|
+
if (slot.state === "revoked")
|
|
53
|
+
return { label: "Revoked", tone: "warn" };
|
|
54
|
+
return { label: "No token", tone: "neutral" };
|
|
55
|
+
}
|
|
56
|
+
/** When the slot last changed, as "Issued <date>" or "Revoked <date>"; a dash when it never has. */
|
|
57
|
+
export function slotSince(slot, format) {
|
|
58
|
+
if (slot.state === "issued")
|
|
59
|
+
return `Issued ${format(slot.issuedAt)}`;
|
|
60
|
+
if (slot.state === "revoked")
|
|
61
|
+
return `Revoked ${format(slot.revokedAt)}`;
|
|
62
|
+
return "—";
|
|
63
|
+
}
|
|
64
|
+
export function lastSeenLabel(lastSeenAt, format) {
|
|
65
|
+
return lastSeenAt ? format(lastSeenAt) : "never";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Why a token operation did not land, in fixed sentences keyed by status.
|
|
69
|
+
* `outcome_unknown` (503) and `aborted` (499) are failures like any other and
|
|
70
|
+
* are never retried for the operator: an unknown rotation returned no secret,
|
|
71
|
+
* and only a new, deliberate rotation produces one.
|
|
72
|
+
*/
|
|
73
|
+
export function tokenFailureNotice(action, name, facts, productName) {
|
|
74
|
+
if (facts.kind === "session")
|
|
75
|
+
return failure("Your session has ended. Sign in again, then retry.");
|
|
76
|
+
if (facts.kind === "forbidden")
|
|
77
|
+
return failure("You don't have permission to manage agent tokens.");
|
|
78
|
+
if (facts.kind === "stale") {
|
|
79
|
+
return failure(`${name}'s token changed before the request was sent, so nothing was sent. Check it and try again.`);
|
|
80
|
+
}
|
|
81
|
+
if (facts.kind === "network") {
|
|
82
|
+
return failure(`Couldn't reach ${productName}. Refresh to check ${name}'s token before trying again.`);
|
|
83
|
+
}
|
|
84
|
+
if (facts.status === 503) {
|
|
85
|
+
return failure(action === "revoke"
|
|
86
|
+
? `Revocation outcome unknown for ${name}: revoke again to be sure its token is stopped.`
|
|
87
|
+
: `${action === "issue" ? "Issue" : "Rotation"} outcome unknown for ${name}: no secret was returned, so rotate again for a replacement.`);
|
|
88
|
+
}
|
|
89
|
+
if (facts.status === 499) {
|
|
90
|
+
return failure(action === "revoke"
|
|
91
|
+
? `The request ended before ${name}'s revocation finished. Refresh to check its token before trying again.`
|
|
92
|
+
: `The request ended before ${name}'s token was delivered, so no secret was returned. Refresh to check its token before trying again.`);
|
|
93
|
+
}
|
|
94
|
+
// The route's compare-and-swap refused it: the slot moved after the list was read.
|
|
95
|
+
if (facts.status === 409 && facts.code === "slot_changed")
|
|
96
|
+
return failure("Changed since you looked, refresh.");
|
|
97
|
+
if (facts.status === 409)
|
|
98
|
+
return failure(`${name}'s token changed meanwhile. Reload and try again.`);
|
|
99
|
+
if (facts.status === 404)
|
|
100
|
+
return failure(`${name} is no longer declared in code. Reload the page.`);
|
|
101
|
+
return action === "revoke"
|
|
102
|
+
? failure(`${name}'s token could not be revoked. Reload and try again.`)
|
|
103
|
+
: failure(`A token could not be issued for ${name}. Reload and try again.`);
|
|
104
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { ActivityOutcome } from "../activity.js";
|
|
|
2
2
|
import type { ConnectaConfigDescription } from "../describe-config.js";
|
|
3
3
|
import type { ConnectorStatus, ConnectorToolDescription } from "../types.js";
|
|
4
4
|
import type { UiProblem } from "./model.js";
|
|
5
|
-
/** The registry's final verdict, independent of the viewer's
|
|
5
|
+
/** The registry's final verdict, independent of whether the viewer's endpoint is read-only. */
|
|
6
6
|
export interface OperatorTool extends ConnectorToolDescription {
|
|
7
7
|
address: string;
|
|
8
8
|
classification: "read" | "write";
|
|
@@ -25,7 +25,7 @@ export interface OperatorConnectorOverlay {
|
|
|
25
25
|
/** Latest visible call in the bounded activity window, or null when unknown. */
|
|
26
26
|
lastCall: OperatorLastCall | null;
|
|
27
27
|
}
|
|
28
|
-
/** Whole connector access, or exact tool grants.
|
|
28
|
+
/** Whole connector access, or exact tool grants. `requireReadOnly` is always false since guarded grants were removed (#837). */
|
|
29
29
|
export interface OperatorGrant {
|
|
30
30
|
connectorId: string;
|
|
31
31
|
tools: "all" | Array<{
|
|
@@ -33,40 +33,162 @@ export interface OperatorGrant {
|
|
|
33
33
|
requireReadOnly: boolean;
|
|
34
34
|
}>;
|
|
35
35
|
}
|
|
36
|
-
export
|
|
36
|
+
export type OperatorRole = "maintainer" | "member";
|
|
37
|
+
export type OperatorMember = {
|
|
38
|
+
kind: "human";
|
|
39
|
+
namespace: string;
|
|
40
|
+
id: string;
|
|
41
|
+
label: string | null;
|
|
42
|
+
} | {
|
|
43
|
+
kind: "agent";
|
|
44
|
+
name: string;
|
|
45
|
+
};
|
|
46
|
+
export interface OperatorPoolV2 {
|
|
37
47
|
name: string;
|
|
48
|
+
/** `/mcp/<name>`. */
|
|
49
|
+
humanPath: string;
|
|
50
|
+
/** `/agent/<name>`. */
|
|
51
|
+
agentPath: string;
|
|
52
|
+
readOnly: boolean;
|
|
53
|
+
/** Workspace address agents act as; null in the member view. */
|
|
54
|
+
owner: string | null;
|
|
55
|
+
/** The pool's declared tools, unfiltered. */
|
|
56
|
+
tools: OperatorGrant[];
|
|
57
|
+
/** Maintainer view only; [] for members. */
|
|
58
|
+
members: OperatorMember[];
|
|
59
|
+
}
|
|
60
|
+
/** Plain /mcp. Maintainer view only; null for members. */
|
|
61
|
+
export interface OperatorRootEndpoint {
|
|
62
|
+
/** `/mcp`. */
|
|
38
63
|
path: string;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
64
|
+
readOnly: boolean;
|
|
65
|
+
/** Humans only. */
|
|
66
|
+
maintainers: OperatorMember[];
|
|
67
|
+
}
|
|
68
|
+
/** What a member may see about one connector in their pools. */
|
|
69
|
+
export interface OperatorMemberConnector {
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
pools: string[];
|
|
73
|
+
authScope: "shared" | "personal";
|
|
74
|
+
/** Tools their pools grant. */
|
|
75
|
+
tools: Array<{
|
|
76
|
+
address: string;
|
|
77
|
+
classification: "read" | "write";
|
|
78
|
+
}>;
|
|
79
|
+
/** Own connection state for personal connectors; null for shared ones. */
|
|
80
|
+
connection: {
|
|
81
|
+
state: ConnectorStatus["state"];
|
|
82
|
+
problem?: UiProblem;
|
|
83
|
+
} | null;
|
|
84
|
+
/** What the member may do. Always "none" for shared connectors. */
|
|
85
|
+
manage: OperatorMemberManage;
|
|
42
86
|
}
|
|
43
|
-
export
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
export type OperatorMemberManage = {
|
|
88
|
+
kind: "none";
|
|
89
|
+
} | {
|
|
90
|
+
kind: "oauth";
|
|
91
|
+
actions: Array<"connect" | "restart" | "disconnect">;
|
|
92
|
+
} | {
|
|
93
|
+
/** /ui/credentials/<id>, the member's own partition. */
|
|
94
|
+
kind: "credential";
|
|
95
|
+
label: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
/** Labels and names, never values. */
|
|
98
|
+
fields?: Array<{
|
|
99
|
+
name: string;
|
|
100
|
+
label: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
}>;
|
|
103
|
+
actions: Array<"save" | "test" | "clear">;
|
|
104
|
+
};
|
|
105
|
+
export interface OperatorAgent {
|
|
106
|
+
name: string;
|
|
107
|
+
pools: string[];
|
|
108
|
+
/**
|
|
109
|
+
* The agent's token slot. `revision` is the same opaque value GET
|
|
110
|
+
* /ui/access-tokens lists for the slot, on every state including "none":
|
|
111
|
+
* it changes on every commit to the slot, and a token mutation sends it as
|
|
112
|
+
* `expect`, so one decided from a stale view refuses with 409 `slot_changed`.
|
|
113
|
+
*/
|
|
114
|
+
token: {
|
|
115
|
+
state: "none";
|
|
116
|
+
revision: string;
|
|
117
|
+
} | {
|
|
118
|
+
state: "issued";
|
|
119
|
+
issuedAt: string;
|
|
120
|
+
revision: string;
|
|
121
|
+
} | {
|
|
122
|
+
state: "revoked";
|
|
123
|
+
revokedAt: string;
|
|
124
|
+
revision: string;
|
|
58
125
|
};
|
|
126
|
+
/** Latest activity row for this agent, null when activity is off or none is visible. */
|
|
127
|
+
lastSeenAt: string | null;
|
|
59
128
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Where a shared connector's credential comes from:
|
|
131
|
+
* - "vault": a declared operator credential slot that holds a saved value;
|
|
132
|
+
* - "missing": a declared slot, or a shared OAuth connector, with nothing saved yet;
|
|
133
|
+
* - "oauth": a shared OAuth grant, even when the grant is vault-sealed;
|
|
134
|
+
* - "config": supplied by deployment code or environment (literal headers, env values,
|
|
135
|
+
* request-token callbacks, `api()` handlers that hold their own credentials), never shown;
|
|
136
|
+
* - "unauthenticated": the connector declares no auth at all.
|
|
137
|
+
* A token minted per request follows its underlying credential (a GitHub App key from a
|
|
138
|
+
* slot is "vault", from code is "config"). Absence of a slot alone never means
|
|
139
|
+
* "unauthenticated": that needs a connector that declares no auth.
|
|
140
|
+
*/
|
|
141
|
+
export type OperatorSharedSource = "vault" | "missing" | "oauth" | "config" | "unauthenticated";
|
|
142
|
+
export type OperatorRunsAs = {
|
|
143
|
+
kind: "shared";
|
|
144
|
+
source: OperatorSharedSource;
|
|
145
|
+
savedAt: string | null;
|
|
146
|
+
} | {
|
|
147
|
+
kind: "personal";
|
|
148
|
+
connectedMembers: number;
|
|
149
|
+
members: number;
|
|
150
|
+
} | {
|
|
151
|
+
kind: "workspace";
|
|
152
|
+
humans: "self";
|
|
153
|
+
agents: string | null;
|
|
154
|
+
} | {
|
|
155
|
+
kind: "workspaceFixed";
|
|
156
|
+
subject: string;
|
|
157
|
+
};
|
|
158
|
+
export interface OperatorConnectorPoolCell {
|
|
159
|
+
connectorId: string;
|
|
160
|
+
/** Null is plain /mcp. */
|
|
161
|
+
pool: string | null;
|
|
162
|
+
/** Connector-relative tool names, as in `OperatorGrant`, not addresses. */
|
|
163
|
+
tools: "all" | string[];
|
|
164
|
+
runsAs: OperatorRunsAs;
|
|
165
|
+
}
|
|
166
|
+
export interface OperatorUiContractV2 {
|
|
167
|
+
schemaVersion: 2;
|
|
168
|
+
role: OperatorRole;
|
|
169
|
+
/** Maintainer only; null for members. */
|
|
170
|
+
config: ConnectaConfigDescription | null;
|
|
171
|
+
/** Leaf provenance for Settings, as in v1. Maintainer only; null for members. */
|
|
172
|
+
configSources: Record<string, "default" | "config"> | null;
|
|
66
173
|
live: {
|
|
174
|
+
/** Maintainer only; [] for members. */
|
|
67
175
|
connectors: OperatorConnectorOverlay[];
|
|
68
|
-
/**
|
|
176
|
+
/** "forbidden" for members. */
|
|
69
177
|
activity: "available" | "unconfigured" | "forbidden" | "unavailable";
|
|
70
178
|
};
|
|
71
|
-
|
|
179
|
+
/** Maintainer only. */
|
|
180
|
+
root: OperatorRootEndpoint | null;
|
|
181
|
+
/** Maintainer: all; member: their pools, owner null, members []. */
|
|
182
|
+
pools: OperatorPoolV2[];
|
|
183
|
+
/** Maintainer only; [] for members. */
|
|
184
|
+
agents: OperatorAgent[];
|
|
185
|
+
/** Maintainer only; [] for members. */
|
|
186
|
+
matrix: OperatorConnectorPoolCell[];
|
|
187
|
+
/** Member only; [] for maintainers, who read live.connectors. */
|
|
188
|
+
connectors: OperatorMemberConnector[];
|
|
189
|
+
you: {
|
|
190
|
+
interactive: boolean;
|
|
191
|
+
};
|
|
72
192
|
}
|
|
193
|
+
/** The operator contract `GET /ui/api/config` serves; schema version 1 was removed (#837). */
|
|
194
|
+
export type OperatorUiContract = OperatorUiContractV2;
|