@zackbart/connecta 0.32.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 +63 -0
- package/README.md +15 -7
- package/bin/connecta.mjs +24 -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/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/config-value-sources.js +11 -9
- package/dist/config.d.ts +37 -75
- package/dist/config.js +110 -36
- package/dist/connector-access.d.ts +4 -34
- 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/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +29 -7
- package/dist/describe-config.js +7 -17
- package/dist/index.d.ts +4 -3
- package/dist/index.js +29 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -4
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +149 -28
- 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 +14 -6
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +82 -21
- package/dist/page-styles.js +1 -1
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +30 -8
- 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/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 +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +3 -3
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +3 -3
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +3 -3
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +10 -8
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +100 -11
- package/dist/providers/notion/rest.js +45 -5
- package/dist/providers/notion/skill.generated.js +2 -2
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +6 -3
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +3 -3
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/value-safety.js +22 -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 +145 -95
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +24 -23
- 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 +5 -0
- package/dist/storage/keys.js +9 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +16 -14
- 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 +355 -50
- package/documentation/auth.md +775 -243
- package/documentation/code-mode.md +14 -11
- package/documentation/deploying.md +121 -24
- package/documentation/integrating.md +92 -21
- package/documentation/meta-tools.md +14 -9
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +115 -28
- package/documentation/operator-ui.md +326 -61
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +30 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +36 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +3 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +27 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +67 -42
- 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 +28 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -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,41 +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";
|
|
37
44
|
name: string;
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
};
|
|
46
|
+
export interface OperatorPoolV2 {
|
|
47
|
+
name: string;
|
|
48
|
+
/** `/mcp/<name>`. */
|
|
49
|
+
humanPath: string;
|
|
50
|
+
/** `/agent/<name>`. */
|
|
51
|
+
agentPath: string;
|
|
40
52
|
readOnly: boolean;
|
|
41
|
-
/**
|
|
42
|
-
|
|
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[];
|
|
43
59
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
/** Plain /mcp. Maintainer view only; null for members. */
|
|
61
|
+
export interface OperatorRootEndpoint {
|
|
62
|
+
/** `/mcp`. */
|
|
63
|
+
path: string;
|
|
48
64
|
readOnly: boolean;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
86
|
+
}
|
|
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;
|
|
59
125
|
};
|
|
126
|
+
/** Latest activity row for this agent, null when activity is off or none is visible. */
|
|
127
|
+
lastSeenAt: string | null;
|
|
60
128
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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;
|
|
67
173
|
live: {
|
|
174
|
+
/** Maintainer only; [] for members. */
|
|
68
175
|
connectors: OperatorConnectorOverlay[];
|
|
69
|
-
/**
|
|
176
|
+
/** "forbidden" for members. */
|
|
70
177
|
activity: "available" | "unconfigured" | "forbidden" | "unavailable";
|
|
71
178
|
};
|
|
72
|
-
|
|
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
|
+
};
|
|
73
192
|
}
|
|
193
|
+
/** The operator contract `GET /ui/api/config` serves; schema version 1 was removed (#837). */
|
|
194
|
+
export type OperatorUiContract = OperatorUiContractV2;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/** How many chips an endpoint row shows before "+N more". */
|
|
2
|
+
const ACCESS_CHIP_LIMIT = 4;
|
|
3
|
+
function initialsOf(text) {
|
|
4
|
+
const words = text
|
|
5
|
+
.replace(/^user_/, "")
|
|
6
|
+
.split(/[\s._@-]+/)
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
const letters = words.length > 1 ? words[0][0] + words[1][0] : (words[0] ?? text).slice(0, 2);
|
|
9
|
+
return letters.toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
function hueOf(key) {
|
|
12
|
+
let hash = 0;
|
|
13
|
+
for (const char of key)
|
|
14
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
15
|
+
return hash % 6;
|
|
16
|
+
}
|
|
17
|
+
/** A member as a chip. Labels are null on Access deployments, where the raw id shows instead. */
|
|
18
|
+
function accessChip(member) {
|
|
19
|
+
if (member.kind === "agent") {
|
|
20
|
+
const key = `agent:${member.name}`;
|
|
21
|
+
return { key, kind: "agent", text: key, initials: "", hue: hueOf(key) };
|
|
22
|
+
}
|
|
23
|
+
const key = `${member.namespace}:${member.id}`;
|
|
24
|
+
const text = member.label ?? member.id;
|
|
25
|
+
return { key, kind: "human", text, initials: initialsOf(text), hue: hueOf(key) };
|
|
26
|
+
}
|
|
27
|
+
/** Chips for one endpoint in contract order, people first, without repeats. */
|
|
28
|
+
export function accessChips(members) {
|
|
29
|
+
const unique = new Map();
|
|
30
|
+
for (const chip of members.map(accessChip))
|
|
31
|
+
if (!unique.has(chip.key))
|
|
32
|
+
unique.set(chip.key, chip);
|
|
33
|
+
const chips = [...unique.values()];
|
|
34
|
+
return [...chips.filter((chip) => chip.kind === "human"), ...chips.filter((chip) => chip.kind === "agent")];
|
|
35
|
+
}
|
|
36
|
+
/** The chips a collapsed row shows, and how many the "+N more" button holds back. */
|
|
37
|
+
export function visibleChips(chips, expanded) {
|
|
38
|
+
if (expanded || chips.length <= ACCESS_CHIP_LIMIT)
|
|
39
|
+
return { shown: chips, hidden: 0 };
|
|
40
|
+
return { shown: chips.slice(0, ACCESS_CHIP_LIMIT), hidden: chips.length - ACCESS_CHIP_LIMIT };
|
|
41
|
+
}
|
|
42
|
+
/** Matrix cells for one endpoint, keyed by connector id. */
|
|
43
|
+
function cellsFor(contract, pool) {
|
|
44
|
+
return new Map(contract.matrix.filter((cell) => cell.pool === pool).map((cell) => [cell.connectorId, cell]));
|
|
45
|
+
}
|
|
46
|
+
/** Plain /mcp declares no tool list; its matrix cells are its grants. */
|
|
47
|
+
function rootGrants(contract) {
|
|
48
|
+
return contract.matrix
|
|
49
|
+
.filter((cell) => cell.pool === null)
|
|
50
|
+
.map((cell) => ({
|
|
51
|
+
connectorId: cell.connectorId,
|
|
52
|
+
tools: cell.tools === "all" ? "all" : cell.tools.map((name) => ({ name, requireReadOnly: false })),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
/** Intersect declared grants with final live verdicts, in config order. */
|
|
56
|
+
function endpointGroups(contract, grants, readOnly, pool) {
|
|
57
|
+
const cells = cellsFor(contract, pool);
|
|
58
|
+
return (contract.config?.connectors ?? []).flatMap((config) => {
|
|
59
|
+
const live = contract.live.connectors.find((c) => c.id === config.id);
|
|
60
|
+
const grant = grants.find((g) => g.connectorId === config.id);
|
|
61
|
+
if (!live || !grant)
|
|
62
|
+
return [];
|
|
63
|
+
const tools = live.tools.filter((tool) => {
|
|
64
|
+
if (readOnly && tool.classification !== "read")
|
|
65
|
+
return false;
|
|
66
|
+
return (grant.tools === "all" ||
|
|
67
|
+
grant.tools.some((g) => g.name === tool.name && (!g.requireReadOnly || tool.classification === "read")));
|
|
68
|
+
});
|
|
69
|
+
// A granted connector with no complete catalog still needs a visible fix.
|
|
70
|
+
return [{ config, live, tools, runsAs: cells.get(config.id)?.runsAs ?? null }];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const toolCount = (groups) => groups.reduce((n, group) => n + group.tools.length, 0);
|
|
74
|
+
/** One row for plain /mcp, then one per pool, in contract order. Maintainer view only. */
|
|
75
|
+
export function endpointRows(contract) {
|
|
76
|
+
if (contract.role !== "maintainer")
|
|
77
|
+
return [];
|
|
78
|
+
const rows = [];
|
|
79
|
+
if (contract.root) {
|
|
80
|
+
const groups = endpointGroups(contract, rootGrants(contract), contract.root.readOnly, null);
|
|
81
|
+
rows.push({
|
|
82
|
+
pool: null,
|
|
83
|
+
label: "Default",
|
|
84
|
+
path: contract.root.path,
|
|
85
|
+
agentPath: null,
|
|
86
|
+
readOnly: contract.root.readOnly,
|
|
87
|
+
toolCount: toolCount(groups),
|
|
88
|
+
groups,
|
|
89
|
+
access: accessChips(contract.root.maintainers),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (const pool of contract.pools) {
|
|
93
|
+
const groups = endpointGroups(contract, pool.tools, pool.readOnly, pool.name);
|
|
94
|
+
rows.push({
|
|
95
|
+
pool: pool.name,
|
|
96
|
+
label: pool.name,
|
|
97
|
+
path: pool.humanPath,
|
|
98
|
+
agentPath: pool.members.some((member) => member.kind === "agent") ? pool.agentPath : null,
|
|
99
|
+
readOnly: pool.readOnly,
|
|
100
|
+
toolCount: toolCount(groups),
|
|
101
|
+
groups,
|
|
102
|
+
access: accessChips(pool.members),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return rows;
|
|
106
|
+
}
|
|
107
|
+
/** A calendar day, without the time: when a shared credential was saved. */
|
|
108
|
+
function formatDay(value) {
|
|
109
|
+
const date = new Date(value);
|
|
110
|
+
return Number.isNaN(date.valueOf()) ? "" : date.toLocaleDateString(undefined, { dateStyle: "medium" });
|
|
111
|
+
}
|
|
112
|
+
/** Where a shared credential lives; a vault or OAuth date when the contract has one. */
|
|
113
|
+
function sharedSourceLabel(source, date) {
|
|
114
|
+
switch (source) {
|
|
115
|
+
case "vault":
|
|
116
|
+
return date ? `saved ${date}` : "saved";
|
|
117
|
+
case "oauth":
|
|
118
|
+
return date ? `connected ${date}` : "connected";
|
|
119
|
+
case "missing":
|
|
120
|
+
return "not configured";
|
|
121
|
+
case "config":
|
|
122
|
+
return "set in deployment";
|
|
123
|
+
case "unauthenticated":
|
|
124
|
+
return "no credential needed";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Whose account a connector acts with on one endpoint, in one line. */
|
|
128
|
+
export function runsAsLabel(runsAs, day = formatDay) {
|
|
129
|
+
switch (runsAs.kind) {
|
|
130
|
+
case "shared":
|
|
131
|
+
return `Shared · ${sharedSourceLabel(runsAs.source, runsAs.savedAt ? day(runsAs.savedAt) : "")}`;
|
|
132
|
+
case "personal":
|
|
133
|
+
return `Personal · ${runsAs.connectedMembers} of ${runsAs.members} connected`;
|
|
134
|
+
case "workspace":
|
|
135
|
+
return `Workspace · people: self · agents: ${runsAs.agents ?? "none"}`;
|
|
136
|
+
case "workspaceFixed":
|
|
137
|
+
return `Everyone acts as ${runsAs.subject}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function matrixColumns(contract) {
|
|
141
|
+
if (contract.role !== "maintainer")
|
|
142
|
+
return [];
|
|
143
|
+
return [
|
|
144
|
+
...(contract.root ? [{ pool: null, path: contract.root.path, readOnly: contract.root.readOnly }] : []),
|
|
145
|
+
...contract.pools.map((pool) => ({ pool: pool.name, path: pool.humanPath, readOnly: pool.readOnly })),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
/** What one connector × endpoint cell holds, straight from the contract's matrix. */
|
|
149
|
+
export function matrixCell(contract, connectorId, column) {
|
|
150
|
+
const cell = contract.matrix.find((c) => c.connectorId === connectorId && c.pool === column.pool);
|
|
151
|
+
if (!cell)
|
|
152
|
+
return { kind: "none" };
|
|
153
|
+
const live = contract.live.connectors.find((c) => c.id === connectorId)?.tools ?? [];
|
|
154
|
+
const tools = cell.tools === "all"
|
|
155
|
+
? live.map(({ name, classification }) => ({ name, classification }))
|
|
156
|
+
: cell.tools.map((name) => ({ name, classification: live.find((t) => t.name === name)?.classification ?? null }));
|
|
157
|
+
return { kind: cell.tools === "all" ? "all" : "some", tools, runsAs: cell.runsAs };
|
|
158
|
+
}
|
|
159
|
+
/** The cell's visible text: "all", a count, or a dash. */
|
|
160
|
+
export function matrixCellText(cell) {
|
|
161
|
+
if (cell.kind === "none")
|
|
162
|
+
return "—";
|
|
163
|
+
return cell.kind === "all" ? "all" : String(cell.tools.length);
|
|
164
|
+
}
|
|
165
|
+
/** The cell's accessible name, e.g. "GitHub on /support: 3 tools". */
|
|
166
|
+
export function matrixCellLabel(connector, column, cell) {
|
|
167
|
+
const where = `${connector} on ${column.path}`;
|
|
168
|
+
if (cell.kind === "none")
|
|
169
|
+
return `${where}: no tools`;
|
|
170
|
+
const what = cell.kind === "all" ? "all tools" : `${cell.tools.length} ${cell.tools.length === 1 ? "tool" : "tools"}`;
|
|
171
|
+
return `${where}: ${what}${column.readOnly ? ", read-only" : ""}`;
|
|
172
|
+
}
|
|
173
|
+
/** The connector's subject for the matrix's Runs as column: the /mcp cell, else its first pool's. */
|
|
174
|
+
export function connectorRunsAs(contract, connectorId) {
|
|
175
|
+
const cells = contract.matrix.filter((cell) => cell.connectorId === connectorId);
|
|
176
|
+
const root = cells.find((cell) => cell.pool === null);
|
|
177
|
+
if (root)
|
|
178
|
+
return root.runsAs;
|
|
179
|
+
for (const pool of contract.pools) {
|
|
180
|
+
const cell = cells.find((c) => c.pool === pool.name);
|
|
181
|
+
if (cell)
|
|
182
|
+
return cell.runsAs;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
/** Who may use plain /mcp, for Settings: each maintainer by label, else by raw id. */
|
|
187
|
+
export function rootAccessSentence(root) {
|
|
188
|
+
const names = root.maintainers.map((member) => (member.kind === "human" ? (member.label ?? member.id) : member.name));
|
|
189
|
+
return names.length
|
|
190
|
+
? `Only maintainers can use ${root.path}: ${names.join(", ")}.`
|
|
191
|
+
: `Only maintainers can use ${root.path}, and none are listed.`;
|
|
192
|
+
}
|
|
@@ -19,8 +19,8 @@ const CATALOGUE = {
|
|
|
19
19
|
credential_required: {
|
|
20
20
|
problem: "A connector with an operator-managed credential slot has no usable credential stored.",
|
|
21
21
|
steps: [
|
|
22
|
-
"
|
|
23
|
-
"If nobody can,
|
|
22
|
+
"With a credential vault configured, a maintainer adds a shared credential on the operator page, and a pool member saves their own personal one from their view of it; neither needs a code change. Without a vault, configure one first.",
|
|
23
|
+
"If nobody can, list the person in the deployment's maintainers for shared auth, or in the members of a pool containing the connector for personal auth.",
|
|
24
24
|
"Check that the connector's credential declaration (label and fields) matches what the downstream actually needs.",
|
|
25
25
|
],
|
|
26
26
|
},
|
|
@@ -57,9 +57,9 @@ const CATALOGUE = {
|
|
|
57
57
|
oauth_action_failed: {
|
|
58
58
|
problem: "Restarting or disconnecting a connector's downstream OAuth from the operator page failed.",
|
|
59
59
|
steps: [
|
|
60
|
-
"Check that the connector implements startAuth and disconnectAuth, and that the storage holding its OAuth state is writable.",
|
|
60
|
+
"Check that the connector implements the hooks the action needs (startAuth, finishAuth, and verifyState to connect; disconnectAuth to disconnect), and that the storage holding its OAuth state is writable.",
|
|
61
61
|
"Check the connector's OAuth client configuration, including the authorization server it discovers or is given.",
|
|
62
|
-
"Confirm the
|
|
62
|
+
"Confirm the person is a maintainer for shared auth, or a member of a pool containing the connector for personal auth.",
|
|
63
63
|
],
|
|
64
64
|
},
|
|
65
65
|
catalog_failed: {
|