@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. package/templates/node/src/provision-token.ts +0 -17
@@ -0,0 +1,47 @@
1
+ // Code-declared agents (#837): the names deployment code lists in `agents`,
2
+ // the identity an admitted agent carries, and the one provider trusted to
3
+ // assert it. Pure, so core reads it without reaching the token module.
4
+ import { ConfigError } from "./config-schema.js";
5
+ /** Agent names: lowercase letters, digits, `_` and `-`. */
6
+ const AGENT_NAME_RE = /^[a-z0-9_-]+$/u;
7
+ /** Longest accepted agent name, so a name always fits a storage key. */
8
+ const MAX_AGENT_NAME_LENGTH = 64;
9
+ /** Directory namespace of agent subjects and activity actors. */
10
+ export const AGENT_NAMESPACE = "connecta:access-tokens:v1";
11
+ /** Whether `value` is an acceptable agent name. */
12
+ export function isAgentName(value) {
13
+ return typeof value === "string" && value.length <= MAX_AGENT_NAME_LENGTH && AGENT_NAME_RE.test(value);
14
+ }
15
+ /**
16
+ * The subject id that partitions an agent's per-caller state, such as result
17
+ * handles. The `agent:` prefix keeps it apart from the UUID subjects older
18
+ * tokens used in the same namespace.
19
+ */
20
+ export function agentSubjectId(name) {
21
+ return `agent:${name}`;
22
+ }
23
+ /** Validate a declared agent list: an array of unique, well-formed names. */
24
+ export function assertAgentNames(value, path) {
25
+ if (!Array.isArray(value))
26
+ throw new ConfigError(`${path} must be an array of agent names.`);
27
+ const seen = new Set();
28
+ value.forEach((name, index) => {
29
+ if (!isAgentName(name)) {
30
+ throw new ConfigError(`${path}[${index}] must be an agent name: 1 to ${MAX_AGENT_NAME_LENGTH} characters of ` +
31
+ "lowercase letters, digits, _ and -.");
32
+ }
33
+ if (seen.has(name))
34
+ throw new ConfigError(`${path}[${index}] repeats the agent name "${name}".`);
35
+ seen.add(name);
36
+ });
37
+ }
38
+ const agentProviders = new WeakSet();
39
+ /** Mark the built-in access-token adapter as the one provider whose `agent` is trusted. */
40
+ export function trustAgentProvider(provider) {
41
+ agentProviders.add(provider);
42
+ return provider;
43
+ }
44
+ /** Whether `provider` is the built-in access-token adapter. */
45
+ export function assertsAgents(provider) {
46
+ return agentProviders.has(provider);
47
+ }
@@ -1,3 +1,4 @@
1
+ import { withDeadline } from "../timeout.js";
1
2
  import { authorizationCredential } from "../inbound-credential.js";
2
3
  import { validIdentityReference } from "../identity.js";
3
4
  function identityString(identity, field) {
@@ -9,7 +10,7 @@ function identityString(identity, field) {
9
10
  function unauthorized() {
10
11
  return {
11
12
  ok: false,
12
- response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"' } }),
13
+ response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"', "Cache-Control": "no-store" } }),
13
14
  };
14
15
  }
15
16
  /**
@@ -34,17 +35,24 @@ export function cloudflareAccessAuth() {
34
35
  return unauthorized();
35
36
  let identity;
36
37
  try {
37
- identity = await access.getIdentity();
38
+ identity = await withDeadline(() => access.getIdentity(), {
39
+ timeoutMs: 5000,
40
+ signal: request.signal,
41
+ timeoutError: new Error("Access identity lookup timed out"),
42
+ });
38
43
  }
39
44
  catch {
40
- return unauthorized();
45
+ return {
46
+ ok: false,
47
+ response: Response.json({ error: "Cloudflare Access identity unavailable", problem: "identity_unavailable" }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }),
48
+ };
41
49
  }
42
50
  // Access binds ctx.access.aud to its Worker application at the edge.
43
51
  // Service credentials can pass that edge, but connecta machines must
44
52
  // present a cta_ token to the machine provider instead.
45
53
  const forbidden = () => ({
46
54
  ok: false,
47
- response: Response.json({ error: "Cloudflare Access human identity required" }, { status: 403 }),
55
+ response: Response.json({ error: "Cloudflare Access human identity required" }, { status: 403, headers: { "Cache-Control": "no-store" } }),
48
56
  });
49
57
  if (!identity || typeof identity !== "object" || Array.isArray(identity))
50
58
  return forbidden();
@@ -56,7 +64,7 @@ export function cloudflareAccessAuth() {
56
64
  if (!userId) {
57
65
  return {
58
66
  ok: false,
59
- response: Response.json({ error: "Cloudflare Access user identity required" }, { status: 403 }),
67
+ response: Response.json({ error: "Cloudflare Access user identity required" }, { status: 403, headers: { "Cache-Control": "no-store" } }),
60
68
  };
61
69
  }
62
70
  return { ok: true, userId, subjectId: userId };
@@ -148,7 +148,7 @@ export class AuthElicitation {
148
148
  requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
149
149
  });
150
150
  // Recheck every entered read before restarting any part of a program.
151
- // A trusted pool cannot authorize replay of a newly classified write.
151
+ // A writable endpoint cannot authorize replay of a newly classified write.
152
152
  for (const read of previous.reads) {
153
153
  const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
154
154
  if (!current.ok ||
@@ -0,0 +1,15 @@
1
+ import { chargeCallBudget } from "./runtime/call-admission.js";
2
+ import { runEdge } from "./runtime/run.js";
3
+ const permits = new WeakMap();
4
+ export function attachCallAdmission(ctx, permit) {
5
+ permits.set(ctx, permit);
6
+ }
7
+ /**
8
+ * Charge one sequential auxiliary request using the enclosing concurrency slot.
9
+ * Await before dispatch and propagate refusal. Contexts outside an invocation
10
+ * have no registry policy to charge.
11
+ */
12
+ export function chargeConnectorCall(ctx) {
13
+ const permit = permits.get(ctx);
14
+ return permit ? runEdge(chargeCallBudget(permit, ctx.signal)) : Promise.resolve();
15
+ }
@@ -1,6 +1,7 @@
1
1
  import { Deferred, Duration, Effect } from "effect";
2
+ import { CALL_ADMISSION } from "./provider.js";
2
3
  import { ConnectorCallError } from "./errors.js";
3
- import { provideCallAdmissionProgram, startCallAdmission } from "./runtime/call-admission.js";
4
+ import { provideCallAdmissionProgram, provideCallBudgetCharge, startCallAdmission } from "./runtime/call-admission.js";
4
5
  import { fromSignal, runEdge } from "./runtime/run.js";
5
6
  const DEFAULT_MAX_QUEUE_SIZE = 32;
6
7
  const DEFAULT_QUEUE_TIMEOUT_MS = 5_000;
@@ -50,18 +51,6 @@ export function aggregateCallAdmissionSnapshots(snapshots) {
50
51
  }
51
52
  return aggregate;
52
53
  }
53
- function positiveWhole(value, name) {
54
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
55
- throw new TypeError(`${name} must be a positive whole number.`);
56
- }
57
- return value;
58
- }
59
- function nonNegativeWhole(value, name) {
60
- if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
61
- throw new TypeError(`${name} must be a non-negative whole number.`);
62
- }
63
- return value;
64
- }
65
54
  /**
66
55
  * Per-runtime, per-connector call admission. State contains only bounded
67
56
  * partition keys, counters, timestamps, signals, and promise continuations;
@@ -88,36 +77,14 @@ export class ConnectorCallAdmissionController {
88
77
  queueWaitMaxMs = 0;
89
78
  constructor(connectorId, policy) {
90
79
  this.connectorId = connectorId;
91
- if (!Array.isArray(policy.rules) || policy.rules.length !== 1) {
92
- throw new TypeError(`connector "${connectorId}" callAdmission.rules must contain exactly one rule in this release.`);
93
- }
80
+ policy = CALL_ADMISSION.parse(policy, `connector "${connectorId}" callAdmission`);
94
81
  const rule = policy.rules[0];
95
- if (rule.maxConcurrency === undefined && rule.budget === undefined) {
96
- throw new TypeError(`connector "${connectorId}" callAdmission rule must declare maxConcurrency or budget.`);
97
- }
98
- this.maxConcurrency =
99
- rule.maxConcurrency === undefined
100
- ? undefined
101
- : positiveWhole(rule.maxConcurrency, `connector "${connectorId}" callAdmission maxConcurrency`);
102
- if (this.maxConcurrency === undefined &&
103
- (rule.maxQueueSize !== undefined || rule.queueTimeoutMs !== undefined || rule.retryAfterMs !== undefined)) {
104
- throw new TypeError(`connector "${connectorId}" callAdmission queue settings require maxConcurrency.`);
105
- }
106
- this.maxQueueSize = nonNegativeWhole(rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, `connector "${connectorId}" callAdmission maxQueueSize`);
107
- this.queueTimeoutMs = positiveWhole(rule.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS, `connector "${connectorId}" callAdmission queueTimeoutMs`);
108
- this.retryAfterMs = nonNegativeWhole(rule.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS, `connector "${connectorId}" callAdmission retryAfterMs`);
109
- this.maxPartitions = positiveWhole(policy.maxPartitions ?? DEFAULT_MAX_PARTITIONS, `connector "${connectorId}" callAdmission maxPartitions`);
110
- if (rule.budget) {
111
- if (rule.budget.kind !== "rolling-window") {
112
- throw new TypeError(`connector "${connectorId}" callAdmission budget kind must be "rolling-window".`);
113
- }
114
- const maxCalls = positiveWhole(rule.budget.maxCalls, `connector "${connectorId}" callAdmission budget.maxCalls`);
115
- const windowMs = positiveWhole(rule.budget.windowMs, `connector "${connectorId}" callAdmission budget.windowMs`);
116
- this.budget = { maxCalls, windowMs };
117
- }
118
- else {
119
- this.budget = undefined;
120
- }
82
+ this.maxConcurrency = rule.maxConcurrency;
83
+ this.maxQueueSize = rule.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
84
+ this.queueTimeoutMs = rule.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS;
85
+ this.retryAfterMs = rule.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS;
86
+ this.maxPartitions = policy.maxPartitions ?? DEFAULT_MAX_PARTITIONS;
87
+ this.budget = rule.budget ? { maxCalls: rule.budget.maxCalls, windowMs: rule.budget.windowMs } : undefined;
121
88
  this.partitionKey = rule.partitionKey;
122
89
  }
123
90
  acquire(input) {
@@ -179,7 +146,7 @@ export class ConnectorCallAdmissionController {
179
146
  this.queueWaitMaxMs = Math.max(this.queueWaitMaxMs, waitMs);
180
147
  }
181
148
  let released = false;
182
- return {
149
+ const permit = {
183
150
  waitMs,
184
151
  release: () => {
185
152
  if (released)
@@ -190,6 +157,26 @@ export class ConnectorCallAdmissionController {
190
157
  this.pump(state);
191
158
  },
192
159
  };
160
+ provideCallBudgetCharge(permit, (chargedAt, signal) => {
161
+ if (this.closed || released) {
162
+ return Effect.fail(new CallAdmissionError("closed", "unavailable", `Connector "${this.connectorId}" call admission ${this.closed ? "is closed" : "permit is released"}.`));
163
+ }
164
+ if (signal?.aborted) {
165
+ this.cancelledTotal++;
166
+ return Effect.fail(this.cancelled(signal));
167
+ }
168
+ this.pruneBudget(state, chargedAt);
169
+ const retryAfterMs = this.budgetRetryAfterMs(state, chargedAt);
170
+ if (retryAfterMs !== undefined) {
171
+ this.rateLimitedTotal++;
172
+ return Effect.fail(this.budgetLimited(retryAfterMs));
173
+ }
174
+ if (this.budget)
175
+ state.admittedAt.push(chargedAt);
176
+ this.admittedTotal++;
177
+ return Effect.void;
178
+ });
179
+ return permit;
193
180
  }
194
181
  pump(state) {
195
182
  if (this.closed || this.maxConcurrency === undefined)
@@ -1,18 +1,22 @@
1
1
  const MAX_FACTS = 4;
2
2
  const MAX_TEXT = 128;
3
+ // Shared with the REST write schema so every accepted key survives recovery.
4
+ export const IDEMPOTENCY_KEY_MAX_LENGTH = 255;
3
5
  const NAME = /^[A-Za-z][A-Za-z0-9]*$/;
4
6
  const facts = new WeakMap();
5
7
  /**
6
8
  * Record facts that let the caller reconcile or safely retry this call's
7
9
  * write. Bounded: at most four string facts with short camelCase names; a fact
8
- * outside the bound is dropped rather than truncated.
10
+ * outside the bound is dropped rather than truncated. Idempotency keys allow
11
+ * 255 characters to match the REST write schema; other values allow 128.
9
12
  */
10
13
  export function recordRecovery(ctx, recovery) {
11
14
  const kept = { ...facts.get(ctx) };
12
15
  for (const [name, value] of Object.entries(recovery)) {
13
16
  if (Object.keys(kept).length >= MAX_FACTS && !Object.hasOwn(kept, name))
14
17
  break;
15
- if (!NAME.test(name) || name.length > 32 || typeof value !== "string" || value.length > MAX_TEXT)
18
+ const maxLength = name === "idempotencyKey" ? IDEMPOTENCY_KEY_MAX_LENGTH : MAX_TEXT;
19
+ if (!NAME.test(name) || name.length > 32 || typeof value !== "string" || value.length > maxLength)
16
20
  continue;
17
21
  kept[name] = value;
18
22
  }
@@ -377,6 +377,7 @@ export class CatalogService {
377
377
  concurrency;
378
378
  searchRoute;
379
379
  requestSignal;
380
+ readOnly;
380
381
  // The request-scoped catalog cache: one shared read per connector asked
381
382
  // about, started by the first asker and joined by every later one. A
382
383
  // success stays for the rest of the request, so a search and the call after
@@ -401,6 +402,7 @@ export class CatalogService {
401
402
  this.searchRoute = options.searchRoute ?? "search_tools";
402
403
  this.requestSignal = options.requestSignal;
403
404
  this.defer = options.defer;
405
+ this.readOnly = options.readOnly === true;
404
406
  }
405
407
  /**
406
408
  * Send a caller back to discovery through the surface it can actually reach.
@@ -460,9 +462,12 @@ export class CatalogService {
460
462
  // each settling in its input slot: an unavailable catalog is a Failure
461
463
  // there, not a failed discovery. `route` names the surface in a timeout.
462
464
  // A probe also ends with its request, so no listing outlives the caller
463
- // that asked for it.
465
+ // that asked for it. On a read-only endpoint a write is absent from search
466
+ // and describe, as if the connector never had it. Guides are not filtered.
464
467
  discoveryCatalogs(ids, route) {
465
- return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal }), {
468
+ return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.readOnly
469
+ ? Effect.map(this.catalog(id, { signal }), (tools) => tools.filter((tool) => tool.classification === "read"))
470
+ : this.catalog(id, { signal }), {
466
471
  ...(this.requestSignal ? { signal: this.requestSignal } : {}),
467
472
  timeoutMs: this.probeTimeoutMs,
468
473
  timeoutError: new ConnectorCallError("timeout", `${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
@@ -44,7 +44,6 @@ function factorySource(sources, parts) {
44
44
  export function describeConfigSources(snapshot, raw) {
45
45
  const sources = {};
46
46
  const connectorSources = new Map(raw.connectors.map((connector) => [connector.id, factorySources(connector)]));
47
- const moduleSources = { accessTokens: factorySources(raw.accessTokens) };
48
47
  const provided = (value) => (value === undefined ? "default" : "config");
49
48
  for (const path of configValuePaths(snapshot)) {
50
49
  const parts = path.split(".").slice(1);
@@ -56,10 +55,12 @@ export function describeConfigSources(snapshot, raw) {
56
55
  source = provided(raw.serverInfo?.[key]);
57
56
  else if (group === "urls")
58
57
  source = key === "mcpPath" ? "default" : provided(raw[key]);
59
- else if (group === "trust")
60
- source = provided(raw.trust);
61
- else if (group === "identity")
62
- source = provided(raw.identity?.[key]);
58
+ else if (group === "maintainers")
59
+ source = provided(raw.maintainers);
60
+ else if (group === "readOnly")
61
+ source = provided(raw.readOnly);
62
+ else if (group === "agents")
63
+ source = provided(raw.agents);
63
64
  else if (group === "storage")
64
65
  source = provided(raw.storage);
65
66
  else if (group === "deploymentInfo")
@@ -67,11 +68,15 @@ export function describeConfigSources(snapshot, raw) {
67
68
  else if (group === "classification")
68
69
  source = provided(raw.classification);
69
70
  else if (group === "pools")
70
- source = field === "trust" ? provided(raw.pools?.[key]?.trust) : provided(raw.pools);
71
+ // An inherited value comes from wherever the deployment's came from.
72
+ source =
73
+ field === "readOnly"
74
+ ? provided(raw.pools?.[key]?.readOnly ?? raw.readOnly)
75
+ : field === "owner"
76
+ ? provided(raw.pools?.[key]?.owner)
77
+ : provided(raw.pools);
71
78
  else if (group === "modules") {
72
- const module = key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens;
73
- const factory = moduleSources[key];
74
- source = (factory ? factorySource(factory, parts.slice(2)) : undefined) ?? provided(module);
79
+ source = provided(key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens);
75
80
  }
76
81
  else if (group === "branding") {
77
82
  const brand = raw.ui?.branding;
package/dist/config.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- import type { ConnectorGrant } from "./connector-access.js";
2
1
  import type { CredentialVault } from "./credential-contract.js";
3
2
  import { type ConfigInput, type ConfigOutput } from "./config-schema.js";
3
+ import type { MemberRef } from "./membership.js";
4
4
  import type { AccessTokensModule, ActivityModule, OperatorSurface } from "./module-contracts.js";
5
- import type { AuthenticatedIdentity, Connector, Executor, IdentityReference, InboundAuth, KVStorage, Logger } from "./types.js";
6
- /** Config-owned identity rules for one deployment and tenant. */
7
- export type ConnectorPermission = "all" | "none" | readonly string[];
5
+ import type { Connector, Executor, InboundAuth, KVStorage, Logger } from "./types.js";
8
6
  declare const discovery: {
9
7
  /**
10
8
  * Maximum connector catalogs/status probes fetched at once by discovery
@@ -76,7 +74,7 @@ declare const execute: {
76
74
  */
77
75
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
78
76
  /**
79
- * Trusted-pool writes one program may send, on top of the host-call
77
+ * Writes one program may send on a writable endpoint, on top of the host-call
80
78
  * budget every call already spends. Default 10.
81
79
  */
82
80
  maxWrites: import("./config-schema.js").Field<number, number, true>;
@@ -162,89 +160,64 @@ declare const admission: {
162
160
  };
163
161
  };
164
162
  };
165
- declare const identity: {
163
+ declare const pool: {
166
164
  /**
167
- * What this admitted identity may discover and call: `"all"`, or a list
168
- * whose entries are connector ids (the whole connector) and `connector.tool`
169
- * addresses (that tool only), or `{ tool: "connector.tool",
170
- * requireReadOnly: true }` for an exact tool only while its loaded catalog
171
- * explicitly classifies it read-only. Grants are additive: an unrestricted
172
- * connector or address grant wins over the guarded form. An address naming
173
- * a tool the catalog lacks is unreachable and warned once, never widened.
165
+ * Whether this pool's endpoint refuses every write: write tools are absent
166
+ * from search_tools, tool listings, and describe, and every write call is
167
+ * refused. Inherits the deployment's `readOnly` unless set here, in either
168
+ * direction.
174
169
  */
175
- connectorAccess: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>, ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined, true>;
176
- /** Global payload-free activity reads. Defaults to interactive humans. */
177
- activityAccess: import("./config-schema.js").Field<(principal: Readonly<IdentityReference>) => boolean | Promise<boolean>, ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined, true>;
178
- /** Shared credential and OAuth administration. Defaults to none. */
179
- credentialAdministration: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
180
- /** Client-token lifecycle management by interactive humans. Defaults to false. */
181
- accessTokenManagement: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
182
- /** Connecting or changing the caller's personal account. Defaults to none. */
183
- personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
184
- };
185
- declare const pool: {
186
- /** Programs may write only in trusted pools. Default read-only. */
187
- trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
170
+ readOnly: import("./config-schema.js").Field<boolean, boolean | undefined, true>;
188
171
  /** Connector ids and exact `connector.tool` addresses in this pool. */
189
172
  tools: import("./config-schema.js").Field<readonly string[], readonly string[], false>;
190
173
  /**
191
- * Whether this admitted identity may open the pool. Denied by default:
192
- * a pool with no grant serves nobody. A false return, a throw, and an
193
- * undeclared pool name are the same 404.
174
+ * Who may open `/mcp/<name>`: principal ids in the namespace of the one
175
+ * interactive auth provider, `{ namespace, id }`, or `"agent:<name>"` for a
176
+ * declared agent. Required and non-empty; the pool serves exactly its
177
+ * `tools` to these members and the same 404 to everyone else.
178
+ */
179
+ members: import("./config-schema.js").Field<readonly MemberRef[], readonly MemberRef[], false>;
180
+ /**
181
+ * The Google Workspace address agents in this pool act as, on a Workspace
182
+ * connector whose `subject` is a mapping function. Humans always act as
183
+ * themselves through that mapping, and a fixed `subject` address applies to
184
+ * everyone. Without an owner, an agent's Workspace calls fail
185
+ * `auth_required`.
194
186
  */
195
- grant: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
187
+ owner: import("./config-schema.js").Field<string, string | undefined, true>;
196
188
  };
197
189
  declare const connectaConfig: {
198
190
  connectors: import("./config-schema.js").Field<Connector[], readonly Connector[], false>;
199
191
  /** Inbound auth adapters, such as clerkAuth(...); omit for open (dev). */
200
192
  auth: import("./config-schema.js").Field<InboundAuth | InboundAuth[], readonly InboundAuth[], true>;
201
- /** Code-derived connection visibility and independent management permissions. */
202
- identity: import("./config-schema.js").Field<{
203
- connectorAccess?: ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined;
204
- activityAccess?: ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined;
205
- credentialAdministration?: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
206
- accessTokenManagement?: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
207
- personalConnection?: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
208
- }, {
209
- readonly connectorAccess: ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined;
210
- readonly activityAccess: ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined;
211
- readonly credentialAdministration: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
212
- readonly accessTokenManagement: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
213
- readonly personalConnection: ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined;
214
- }, true> & {
215
- readonly shape: {
216
- /**
217
- * What this admitted identity may discover and call: `"all"`, or a list
218
- * whose entries are connector ids (the whole connector) and `connector.tool`
219
- * addresses (that tool only), or `{ tool: "connector.tool",
220
- * requireReadOnly: true }` for an exact tool only while its loaded catalog
221
- * explicitly classifies it read-only. Grants are additive: an unrestricted
222
- * connector or address grant wins over the guarded form. An address naming
223
- * a tool the catalog lacks is unreachable and warned once, never widened.
224
- */
225
- connectorAccess: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>, ((identity: Readonly<AuthenticatedIdentity>) => "all" | readonly ConnectorGrant[] | Promise<"all" | readonly ConnectorGrant[]>) | undefined, true>;
226
- /** Global payload-free activity reads. Defaults to interactive humans. */
227
- activityAccess: import("./config-schema.js").Field<(principal: Readonly<IdentityReference>) => boolean | Promise<boolean>, ((principal: Readonly<IdentityReference>) => boolean | Promise<boolean>) | undefined, true>;
228
- /** Shared credential and OAuth administration. Defaults to none. */
229
- credentialAdministration: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
230
- /** Client-token lifecycle management by interactive humans. Defaults to false. */
231
- accessTokenManagement: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>, ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined, true>;
232
- /** Connecting or changing the caller's personal account. Defaults to none. */
233
- personalConnection: import("./config-schema.js").Field<(identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>, ((identity: Readonly<AuthenticatedIdentity>) => ConnectorPermission | Promise<ConnectorPermission>) | undefined, true>;
234
- };
235
- };
236
- /** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
193
+ /**
194
+ * Agents, declared by name: lowercase letters, digits, `_` and `-`, each
195
+ * unique. Each has one `cta_` token slot that operators issue, rotate, and
196
+ * revoke; nothing outside code adds, renames, or rebinds an agent. Removing
197
+ * a name stops its token on the next request. Requires `accessTokens`.
198
+ * An admitted agent carries `identity.agent` and has no principal, so
199
+ * personal connectors are absent for it. Defaults to none.
200
+ */
201
+ agents: import("./config-schema.js").Field<readonly string[], readonly string[], true>;
202
+ /**
203
+ * The people who may use plain `/mcp`, which serves the whole catalog with
204
+ * the deployment's `readOnly`, and who administer the deployment. Same
205
+ * reference forms as pool `members`, people only. Required with inbound
206
+ * auth; a maintainer opens no pool unless listed among its members.
207
+ */
208
+ maintainers: import("./config-schema.js").Field<readonly MemberRef[], readonly MemberRef[] | undefined, true>;
209
+ /** Named tool pools, each served at `/mcp/<name>` to its members only. */
237
210
  pools: import("./config-schema.js").Field<Record<string, {
238
- trust?: "trusted" | "read-only" | undefined;
239
- grant?: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
211
+ readOnly?: boolean | undefined;
212
+ owner?: string | undefined;
240
213
  tools: readonly string[];
214
+ members: readonly MemberRef[];
241
215
  }>, Readonly<Record<string, {
242
- readonly trust: "trusted" | "read-only";
216
+ readonly readOnly: boolean | undefined;
243
217
  readonly tools: readonly string[];
244
- readonly grant: ((identity: Readonly<AuthenticatedIdentity>) => boolean | Promise<boolean>) | undefined;
218
+ readonly members: readonly MemberRef[];
219
+ readonly owner: string | undefined;
245
220
  }>> | undefined, true>;
246
- /** Trust of the default /mcp endpoint. Default read-only. */
247
- trust: import("./config-schema.js").Field<"trusted" | "read-only", "trusted" | "read-only", true>;
248
221
  /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
249
222
  classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;
250
223
  /**
@@ -272,7 +245,7 @@ declare const connectaConfig: {
272
245
  vault: import("./config-schema.js").Field<CredentialVault, CredentialVault | undefined, true>;
273
246
  /** Optional connection UI, created by operatorUi() from /ui. */
274
247
  ui: import("./config-schema.js").Field<OperatorSurface, OperatorSurface | undefined, true>;
275
- /** Optional managed client tokens, created by accessTokens() from /auth/access-tokens. */
248
+ /** Optional agent tokens for the declared `agents`, created by accessTokens() from /auth/access-tokens. */
276
249
  accessTokens: import("./config-schema.js").Field<AccessTokensModule, AccessTokensModule | undefined, true>;
277
250
  /** Tool-catalog TTL bounds and probe deadlines. */
278
251
  discovery: import("./config-schema.js").Field<{
@@ -394,7 +367,7 @@ declare const connectaConfig: {
394
367
  */
395
368
  watchdogMs: import("./config-schema.js").Field<number, number, true>;
396
369
  /**
397
- * Trusted-pool writes one program may send, on top of the host-call
370
+ * Writes one program may send on a writable endpoint, on top of the host-call
398
371
  * budget every call already spends. Default 10.
399
372
  */
400
373
  maxWrites: import("./config-schema.js").Field<number, number, true>;
@@ -548,6 +521,15 @@ declare const connectaConfig: {
548
521
  * `customExecutor(myExecutor, { lifecycle: "self-managed" })`.
549
522
  */
550
523
  executor: import("./config-schema.js").Field<Executor, Executor, false>;
524
+ /**
525
+ * Required. Whether the default /mcp endpoint refuses every write. `true`:
526
+ * write tools are absent from search_tools, tool listings, and describe,
527
+ * and call_tool, call_destructive_tool, and program writes are refused.
528
+ * `false`: call_destructive_tool and programs may write, and the MCP
529
+ * client owns approval. Pools inherit it unless they set their own. It has
530
+ * no default while deployments migrate from `trust` (#820).
531
+ */
532
+ readOnly: import("./config-schema.js").Field<boolean, boolean, false>;
551
533
  };
552
534
  /** A deployment's whole configuration, passed to `createConnecta`. */
553
535
  export type ConnectaConfig = ConfigInput<typeof connectaConfig>;
@@ -563,11 +545,9 @@ export type AdmissionPoolConfig = ConfigInput<typeof codeAdmission>;
563
545
  export type RequestAdmissionConfig = ConfigInput<typeof requestAdmission>;
564
546
  /** Runtime-portable server-memory boundaries for `/mcp` and fallback code-mode work. */
565
547
  export type ConnectaAdmissionConfig = ConfigInput<typeof admission>;
566
- export type ConnectaIdentityConfig = ConfigInput<typeof identity>;
567
548
  /**
568
- * A named tool pool served at `/mcp/<name>`. The pool is the slice a client
569
- * pointed at that endpoint may see; the identity's own `connectorAccess`
570
- * remains its ceiling and the pool can only narrow it.
549
+ * A named tool pool served at `/mcp/<name>`: exactly its `tools`, to its
550
+ * `members` only.
571
551
  */
572
552
  export type ConnectaPoolConfig = ConfigInput<typeof pool>;
573
553
  type Parsed = ConfigOutput<typeof connectaConfig>;