@zackbart/connecta 0.33.0 → 0.34.1

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 (131) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +100 -128
  3. package/SECURITY.md +1 -1
  4. package/bin/version.mjs +1 -1
  5. package/dist/activity.d.ts +3 -0
  6. package/dist/branding.d.ts +7 -0
  7. package/dist/branding.js +76 -9
  8. package/dist/catalog-drift.d.ts +30 -26
  9. package/dist/catalog-drift.js +169 -84
  10. package/dist/catalog-service.js +16 -2
  11. package/dist/config-value-sources.js +6 -1
  12. package/dist/config.d.ts +13 -2
  13. package/dist/config.js +36 -2
  14. package/dist/connector-caller.js +2 -1
  15. package/dist/connectors/api-connector.d.ts +3 -0
  16. package/dist/connectors/api-connector.js +5 -1
  17. package/dist/connectors/option-shapes.js +1 -1
  18. package/dist/connectors/remote-mcp.d.ts +2 -0
  19. package/dist/connectors/remote-mcp.js +5 -1
  20. package/dist/describe-config.d.ts +5 -1
  21. package/dist/describe-config.js +2 -0
  22. package/dist/execute.js +1 -11
  23. package/dist/fix-prompt.js +2 -2
  24. package/dist/index.js +1 -0
  25. package/dist/invocation.js +3 -2
  26. package/dist/membership.d.ts +2 -0
  27. package/dist/meta-tools.js +3 -2
  28. package/dist/oauth-callback-outcome.js +2 -2
  29. package/dist/operator-ui/agents.d.ts +1 -1
  30. package/dist/operator-ui/agents.js +16 -16
  31. package/dist/operator-ui/config-summary.js +565 -0
  32. package/dist/operator-ui/connection-map.js +376 -0
  33. package/dist/operator-ui/endpoints.js +45 -16
  34. package/dist/operator-ui/fix-prompts.js +1 -1
  35. package/dist/operator-ui/generated.js +4 -4
  36. package/dist/operator-ui/health.js +52 -0
  37. package/dist/operator-ui/member-page.js +13 -28
  38. package/dist/operator-ui/overview.js +123 -0
  39. package/dist/operator-ui/view.d.ts +26 -22
  40. package/dist/operator-ui/view.js +130 -96
  41. package/dist/page-styles.js +5 -3
  42. package/dist/provider.d.ts +6 -1
  43. package/dist/provider.js +18 -1
  44. package/dist/providers/_shared/google/workspace.d.ts +9 -4
  45. package/dist/providers/_shared/google/workspace.js +7 -3
  46. package/dist/providers/basecamp/index.d.ts +2 -0
  47. package/dist/providers/breeze/index.d.ts +2 -0
  48. package/dist/providers/ccb/index.d.ts +2 -0
  49. package/dist/providers/ccb/index.js +2 -2
  50. package/dist/providers/cloudflare/index.d.ts +2 -0
  51. package/dist/providers/cloudflare/index.js +5 -1
  52. package/dist/providers/cloudflare/openapi.generated.js +3296 -3206
  53. package/dist/providers/cloudflare/rest.js +19 -0
  54. package/dist/providers/cloudflare/skill.generated.js +2 -2
  55. package/dist/providers/cloudflare/value-safety.js +51 -18
  56. package/dist/providers/docs/index.js +1 -1
  57. package/dist/providers/docs/skill.generated.js +2 -2
  58. package/dist/providers/drive/index.js +1 -1
  59. package/dist/providers/drive/skill.generated.js +2 -2
  60. package/dist/providers/forms/index.js +1 -1
  61. package/dist/providers/forms/skill.generated.js +2 -2
  62. package/dist/providers/gmail/index.js +1 -1
  63. package/dist/providers/gmail/skill.generated.js +2 -2
  64. package/dist/providers/intercom/index.d.ts +24 -0
  65. package/dist/providers/intercom/index.js +121 -0
  66. package/dist/providers/intercom/skill.generated.js +8 -0
  67. package/dist/providers/linear/index.js +204 -19
  68. package/dist/providers/linear/skill.generated.js +2 -2
  69. package/dist/providers/mixpanel/index.d.ts +2 -0
  70. package/dist/providers/mixpanel/index.js +21 -17
  71. package/dist/providers/mixpanel/skill.generated.js +3 -3
  72. package/dist/providers/notion/index.d.ts +2 -0
  73. package/dist/providers/notion/index.js +1 -1
  74. package/dist/providers/notion/skill.generated.js +1 -1
  75. package/dist/providers/overflow/index.d.ts +2 -0
  76. package/dist/providers/overflow/index.js +1 -1
  77. package/dist/providers/overflow/skill.generated.js +2 -2
  78. package/dist/providers/planning-center/index.d.ts +2 -0
  79. package/dist/providers/planning-center/index.js +1 -1
  80. package/dist/providers/planning-center/skill.generated.js +4 -4
  81. package/dist/providers/revenuecat/index.d.ts +5 -3
  82. package/dist/providers/revenuecat/index.js +147 -7
  83. package/dist/providers/revenuecat/skill.generated.js +3 -3
  84. package/dist/providers/sheets/index.js +1 -1
  85. package/dist/providers/sheets/skill.generated.js +2 -2
  86. package/dist/providers/slides/index.js +1 -1
  87. package/dist/providers/slides/skill.generated.js +2 -2
  88. package/dist/providers/stripe/index.d.ts +2 -0
  89. package/dist/providers/stripe/index.js +50 -30
  90. package/dist/providers/stripe/skill.generated.js +2 -1
  91. package/dist/providers/tithely/index.d.ts +2 -0
  92. package/dist/providers/tithely/index.js +1 -1
  93. package/dist/providers/vercel/index.d.ts +2 -0
  94. package/dist/providers/vercel/index.js +2 -2
  95. package/dist/providers/vercel/openapi.generated.js +391 -374
  96. package/dist/providers/vercel/skill.generated.js +2 -2
  97. package/dist/providers/vercel/value-safety.js +14 -4
  98. package/dist/registry.d.ts +10 -14
  99. package/dist/registry.js +77 -62
  100. package/dist/routes/mcp.js +20 -13
  101. package/dist/routes/oauth.js +2 -2
  102. package/dist/routes/ui-data.js +8 -2
  103. package/dist/skills.js +18 -2
  104. package/dist/storage/sql.js +5 -1
  105. package/dist/types.d.ts +36 -11
  106. package/dist/ui.js +9 -4
  107. package/dist/usage-guide.js +1 -1
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/documentation/README.md +2 -2
  111. package/documentation/architecture.md +66 -18
  112. package/documentation/auth.md +183 -67
  113. package/documentation/code-mode.md +30 -10
  114. package/documentation/deploying.md +5 -1
  115. package/documentation/integrating.md +64 -7
  116. package/documentation/meta-tools.md +38 -24
  117. package/documentation/migration-0.33.md +21 -13
  118. package/documentation/migration-0.34.md +67 -0
  119. package/documentation/operating.md +1 -1
  120. package/documentation/operator-tests.md +52 -20
  121. package/documentation/operator-ui.md +341 -104
  122. package/documentation/provider-auth-migration.md +22 -0
  123. package/examples/worker/AGENTS.md +15 -9
  124. package/examples/worker/README.md +41 -14
  125. package/examples/worker/src/connecta.config.ts +5 -4
  126. package/examples/worker/wrangler.jsonc +1 -1
  127. package/package.json +23 -19
  128. package/templates/node/AGENTS.md +13 -7
  129. package/templates/node/README.md +14 -11
  130. package/templates/node/package.json +1 -1
  131. package/templates/node/src/connecta.config.ts +2 -2
package/dist/config.js CHANGED
@@ -184,6 +184,31 @@ function assertFlag(value, path) {
184
184
  if (typeof value !== "boolean")
185
185
  throw new ConfigError(`${path} must be true or false.`);
186
186
  }
187
+ /** Longest pool `description`, in characters, after trimming. */
188
+ const MAX_POOL_DESCRIPTION_LENGTH = 500;
189
+ /**
190
+ * A pool description is one line of prose for the model. C0 and C1 controls
191
+ * (including NEL), plus Unicode line and paragraph separators, are refused.
192
+ * A scan, as in remote-mcp.ts,
193
+ * because a character class over the control range trips `no-control-regex`.
194
+ */
195
+ function hasLineBreakOrControlCharacter(value) {
196
+ for (let index = 0; index < value.length; index++) {
197
+ const code = value.charCodeAt(index);
198
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029)
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+ function assertPoolDescription(value, path) {
204
+ const trimmed = typeof value === "string" ? value.trim() : undefined;
205
+ if (trimmed === undefined ||
206
+ trimmed.length === 0 ||
207
+ [...trimmed].length > MAX_POOL_DESCRIPTION_LENGTH ||
208
+ hasLineBreakOrControlCharacter(trimmed)) {
209
+ throw new ConfigError(`${path} must be text of 1 to ${MAX_POOL_DESCRIPTION_LENGTH} characters after trimming, with no line breaks or control characters.`);
210
+ }
211
+ }
187
212
  /** Shape only; each reference resolves against auth and agents in createConnecta. */
188
213
  function assertMemberList(value, path) {
189
214
  if (!Array.isArray(value)) {
@@ -217,8 +242,9 @@ const pool = {
217
242
  * The Google Workspace address agents in this pool act as, on a Workspace
218
243
  * connector whose `subject` is a mapping function. Humans always act as
219
244
  * themselves through that mapping, and a fixed `subject` address applies to
220
- * everyone. Without an owner, an agent's Workspace calls fail
221
- * `auth_required`.
245
+ * everyone. Without an owner, a mapping-subject Workspace connector is
246
+ * absent for this pool's agents, as a personal connector is; people in the
247
+ * pool still see it.
222
248
  */
223
249
  owner: opaque({
224
250
  check: (value, path) => {
@@ -227,6 +253,14 @@ const pool = {
227
253
  }
228
254
  },
229
255
  }),
256
+ /**
257
+ * What this pool is for, in plain words for the model: appended to the MCP
258
+ * `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
259
+ * caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
260
+ * 500 characters, no line breaks or control characters. It grants and hides nothing; plain
261
+ * `/mcp` never shows it.
262
+ */
263
+ description: opaque({ check: assertPoolDescription }),
230
264
  };
231
265
  const icon = {
232
266
  src: required(text()),
@@ -9,7 +9,8 @@
9
9
  // Workspace through domain-wide delegation (`src/providers/_shared/google/`) — which
10
10
  // hand the identity to a deployment-config function that names the
11
11
  // downstream subject (PRINCIPLES.md INV-3, #678), or, for an agent, use the
12
- // admitted pool's configured owner (decisions/0006). The provider never
12
+ // admitted pool's configured owner (decisions/0006), and the scoped registry
13
+ // view hides the connector from an agent whose pool has none. The provider never
13
14
  // decides whose account to open; config does, from this.
14
15
  //
15
16
  // Core sets it from the authorization that admitted the request, on the scoped
@@ -112,6 +112,8 @@ export interface ApiOptions {
112
112
  * construct.
113
113
  */
114
114
  maxResultBytes?: number;
115
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
116
+ readOnly?: boolean;
115
117
  /** Optional per-runtime downstream call-admission policy. */
116
118
  callAdmission?: ConnectorCallAdmissionPolicy;
117
119
  /**
@@ -146,6 +148,7 @@ export interface ApiOptions {
146
148
  /** The closed options api() accepts; see `assertKnownOptions`. */
147
149
  export declare const API_OPTIONS: import("../config-schema.js").Field<unknown, unknown, true> & {
148
150
  readonly shape: {
151
+ readOnly: import("../config-schema.js").Field<boolean, boolean | undefined, true>;
149
152
  callAdmission: Omit<import("../config-schema.js").Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
150
153
  parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
151
154
  readonly shape: import("../config-schema.js").Shape & {
@@ -3,7 +3,7 @@ import { attachFailureFacts, carryFailureFacts, errorLabel } from "../operator-r
3
3
  import { compileValidator, validateCatalogToolInput } from "../validate.js";
4
4
  import { array, assertKnownOptions, instance, keys, optionsOf, strings } from "../config-schema.js";
5
5
  import { describedEndpoint, describedOrigin, describedTools } from "../described.js";
6
- import { CALL_ADMISSION, CREDENTIAL, USAGE_GUIDE } from "./option-shapes.js";
6
+ import { READ_ONLY, CALL_ADMISSION, CREDENTIAL, USAGE_GUIDE } from "./option-shapes.js";
7
7
  import { assertStaticToolNames } from "../tool-name.js";
8
8
  import { registerInvocationAuth } from "../invocation-auth.js";
9
9
  import { redactCatalog, redactSentSecrets, sentSecretsFetch, trackCredentialReads } from "../sent-secrets.js";
@@ -41,6 +41,7 @@ function checkToolContract(id, tool) {
41
41
  /** The closed options api() accepts; see `assertKnownOptions`. */
42
42
  export const API_OPTIONS = optionsOf()({
43
43
  ...keys("title", "description", "authScope", "maxResultBytes", "testCredential", "testCredentials", "validateArgs"),
44
+ readOnly: READ_ONLY,
44
45
  callAdmission: CALL_ADMISSION,
45
46
  usageGuide: USAGE_GUIDE,
46
47
  credential: CREDENTIAL,
@@ -90,6 +91,8 @@ function describedApiAuth(opts) {
90
91
  */
91
92
  export function apiConnector(id, opts, oauth) {
92
93
  opts = assertKnownOptions(opts, `api(${JSON.stringify(id)})`, API_OPTIONS);
94
+ if (opts.readOnly !== undefined)
95
+ READ_ONLY.parse(opts.readOnly, `Connector(${JSON.stringify(id)}).readOnly`);
93
96
  if (opts.callAdmission !== undefined)
94
97
  CALL_ADMISSION.parse(opts.callAdmission, `api(${JSON.stringify(id)}).callAdmission`);
95
98
  assertStaticToolNames(opts.tools, `api(${JSON.stringify(id)}).tools`);
@@ -124,6 +127,7 @@ export function apiConnector(id, opts, oauth) {
124
127
  ...defined({
125
128
  description: opts.description,
126
129
  authScope: opts.authScope,
130
+ readOnly: opts.readOnly,
127
131
  maxResultBytes: opts.maxResultBytes,
128
132
  callAdmission: opts.callAdmission,
129
133
  usageGuide: opts.usageGuide,
@@ -5,7 +5,7 @@
5
5
  // path instead of silently doing nothing (INV-11). Values stay the factory's
6
6
  // to validate; handlers, schemas, and loggers pass through as `opaque()`.
7
7
  import { array, keys, optionsOf, strings, variants } from "../config-schema.js";
8
- export { CALL_ADMISSION, PROVIDER_COMMON } from "../provider.js";
8
+ export { READ_ONLY, CALL_ADMISSION, PROVIDER_COMMON } from "../provider.js";
9
9
  export const CREDENTIAL = optionsOf()({
10
10
  ...keys("label", "description", "placeholder"),
11
11
  fields: array(optionsOf()(keys("name", "label", "description", "placeholder", "inputType"))),
@@ -82,6 +82,8 @@ export interface RemoteMcpOptions {
82
82
  * construct.
83
83
  */
84
84
  maxResultBytes?: number;
85
+ /** Serve only read-classified tools on every endpoint. Defaults to false. */
86
+ readOnly?: boolean;
85
87
  /** Optional per-runtime downstream call-admission policy. */
86
88
  callAdmission?: ConnectorCallAdmissionPolicy;
87
89
  /**
@@ -24,7 +24,7 @@ import { retryAfterMs } from "./guarded-fetch.js";
24
24
  import { resourceUriMatchesTemplates } from "./resource-uri.js";
25
25
  import { callerOf } from "../connector-caller.js";
26
26
  import { readNegotiation, storeNegotiation } from "./negotiation-cache.js";
27
- import { CALL_ADMISSION, REMOTE_MCP_AUTH, USAGE_GUIDE } from "./option-shapes.js";
27
+ import { READ_ONLY, CALL_ADMISSION, REMOTE_MCP_AUTH, USAGE_GUIDE } from "./option-shapes.js";
28
28
  /**
29
29
  * Apply a maintained provider's slot copy and header framing to credential
30
30
  * auth the deployment left bare.
@@ -46,6 +46,7 @@ export function withCredentialDefaults(auth, defaults) {
46
46
  /** The closed options remoteMcp() accepts; see `assertKnownOptions`. */
47
47
  const REMOTE_MCP_OPTIONS = optionsOf()({
48
48
  ...keys("url", "title", "description", "authScope", "maxResultBytes", "versionNegotiation", "redirects", "requireHttps", "logger", "_transportFactory", "classify", "skills"),
49
+ readOnly: READ_ONLY,
49
50
  callAdmission: CALL_ADMISSION,
50
51
  usageGuide: USAGE_GUIDE,
51
52
  auth: REMOTE_MCP_AUTH,
@@ -1024,6 +1025,8 @@ function registrationErrorCode(body) {
1024
1025
  */
1025
1026
  export function remoteMcp(id, opts) {
1026
1027
  opts = assertKnownOptions(opts, `remoteMcp(${JSON.stringify(id)})`, REMOTE_MCP_OPTIONS);
1028
+ if (opts.readOnly !== undefined)
1029
+ READ_ONLY.parse(opts.readOnly, `Connector(${JSON.stringify(id)}).readOnly`);
1027
1030
  if (opts.callAdmission !== undefined)
1028
1031
  CALL_ADMISSION.parse(opts.callAdmission, `remoteMcp(${JSON.stringify(id)}).callAdmission`);
1029
1032
  const skillsEnabled = opts.skills;
@@ -2194,6 +2197,7 @@ export function remoteMcp(id, opts) {
2194
2197
  },
2195
2198
  }),
2196
2199
  ...(opts.description !== undefined ? { description: opts.description } : {}),
2200
+ ...(opts.readOnly !== undefined ? { readOnly: opts.readOnly } : {}),
2197
2201
  ...(opts.authScope !== undefined ? { authScope: opts.authScope } : {}),
2198
2202
  ...(opts.maxResultBytes !== undefined ? { maxResultBytes: opts.maxResultBytes } : {}),
2199
2203
  ...(opts.callAdmission !== undefined ? { callAdmission: opts.callAdmission } : {}),
@@ -20,6 +20,8 @@ export type DescribedMember = {
20
20
  };
21
21
  /** One connector as `describeConfig()` reports it. */
22
22
  export interface DescribedConnector {
23
+ /** Connector policy, independent of endpoint policy; omitted when false. */
24
+ readOnly?: boolean;
23
25
  id: string;
24
26
  title?: string;
25
27
  description?: string;
@@ -115,7 +117,8 @@ export interface ConnectaConfigDescription {
115
117
  /**
116
118
  * `readOnly` is resolved: the pool's own value, else the deployment's.
117
119
  * `members` are resolved references in declaration order. `owner` is the
118
- * Workspace address agents act as, or null.
120
+ * Workspace address agents act as, or null. `description` is the trimmed
121
+ * text the pool's endpoints add to their MCP instructions, or null.
119
122
  */
120
123
  pools: Array<{
121
124
  name: string;
@@ -124,6 +127,7 @@ export interface ConnectaConfigDescription {
124
127
  members: DescribedMember[];
125
128
  readOnly: boolean;
126
129
  owner: string | null;
130
+ description: string | null;
127
131
  }>;
128
132
  modules: {
129
133
  ui: {
@@ -133,6 +133,7 @@ function describeConnector(connector, deploymentCap, registry) {
133
133
  const guideSummary = connectorGuideSummary(connector);
134
134
  const guided = connector.usageGuide !== undefined;
135
135
  return {
136
+ ...(connector.readOnly === true ? { readOnly: true } : {}),
136
137
  id: connector.id,
137
138
  ...(str(connector.title) !== undefined ? { title: connector.title } : {}),
138
139
  ...(str(connector.description) !== undefined ? { description: connector.description } : {}),
@@ -250,6 +251,7 @@ export function describeConfig(input) {
250
251
  ? { kind: "agent", name: member.name }
251
252
  : { kind: "human", namespace: member.namespace, id: member.id }),
252
253
  owner: input.pools.get(name)?.owner ?? null,
254
+ description: input.pools.get(name)?.description ?? null,
253
255
  })),
254
256
  modules: {
255
257
  ui: { enabled: config.ui !== undefined },
package/dist/execute.js CHANGED
@@ -798,18 +798,8 @@ function sandboxProvider(registry, baseUrl, activity, limits, requestScope = {})
798
798
  ...args,
799
799
  includeSchemas: args.includeSchemas ?? "json",
800
800
  includeSchemaKeys: args.includeSchemaKeys !== false,
801
- }));
801
+ }, catalogDependency));
802
802
  boundedDiscoveryText(result, "Request a smaller limit, omit fullDescriptions, use compact schemas, or pass includeSchemaKeys: false.");
803
- for (const tool of result.tools)
804
- catalogAddressDependency(tool.address);
805
- for (const error of result.catalogErrors)
806
- catalogDependency(error.connector);
807
- for (const connector of result.absence?.configuredConnectors ?? [])
808
- catalogDependency(connector);
809
- for (const connector of result.queryAnalysis?.configuredConnectors ?? [])
810
- catalogDependency(connector);
811
- if (result.queryAnalysis?.connectorScope)
812
- catalogDependency(result.queryAnalysis.connectorScope);
813
803
  return result;
814
804
  }),
815
805
  describe: (raw) => discovery("describe", async () => {
@@ -29,7 +29,7 @@ export function renderFixPrompt(spec, connectorId) {
29
29
  "own logs (lines prefixed [connecta]) or by reproducing the failure locally. Never put a secret in source, " +
30
30
  "a log line, or your reply: secrets belong in the deployment's environment or its credential vault.",
31
31
  "Make the smallest change that fixes it, then verify it by redeploying and refreshing the connection on the " +
32
- "operator Connections page. If the fix needs something you cannot do yourself — a provider console setting, " +
33
- "a secret only the operator holds — name that exact step instead.",
32
+ "operator Connectors page. If the fix needs a step you cannot do yourself, such as a provider console setting " +
33
+ "or a secret only the operator holds, name that exact step instead.",
34
34
  ].join("\n\n");
35
35
  }
package/dist/index.js CHANGED
@@ -67,6 +67,7 @@ function resolvePools(pools, readOnly, registry, members) {
67
67
  readOnly: pool.readOnly ?? readOnly,
68
68
  members: Object.freeze(resolveMembers(pool.members, path, members)),
69
69
  ...(pool.owner !== undefined ? { owner: pool.owner.trim() } : {}),
70
+ ...(pool.description !== undefined ? { description: pool.description.trim() } : {}),
70
71
  }));
71
72
  }
72
73
  return resolved;
@@ -464,9 +464,10 @@ export class InvocationService {
464
464
  reconciliationRequired: true,
465
465
  };
466
466
  }
467
- const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly);
467
+ const connectorReadOnly = target.connector.readOnly === true;
468
+ const refusal = writeRefusal(target.definition.classification, context.source, context.readOnly || connectorReadOnly);
468
469
  if (refusal === "read_only_endpoint") {
469
- return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and this endpoint is read-only. Nothing here can send it.`);
470
+ return framingError("read_only_endpoint", `Tool "${canonicalAddress}" is a write, and ${connectorReadOnly ? `connector "${target.connector.id}"` : "this endpoint"} is read-only. Nothing here can send it.`);
470
471
  }
471
472
  if (refusal === "destructive_tool_requires_approval") {
472
473
  return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is a write. Invoke it through call_destructive_tool so the MCP host can request approval.`);
@@ -25,6 +25,8 @@ export interface ResolvedPool {
25
25
  members: readonly Member[];
26
26
  /** Trimmed `owner`: the Workspace address agents in this pool act as. */
27
27
  owner?: string;
28
+ /** Trimmed `description`, appended to this pool's MCP instructions. */
29
+ description?: string;
28
30
  }
29
31
  /** What resolving a reference needs to know about the deployment. */
30
32
  export interface MemberContext {
@@ -796,14 +796,15 @@ function metaToolsForRequest(registry, baseUrl, opts = {}) {
796
796
  return false;
797
797
  continue;
798
798
  }
799
- if (!registry.getConnector(dependency.connector))
799
+ const connector = registry.getConnector(dependency.connector);
800
+ if (!connector)
800
801
  return false;
801
802
  const tools = await registry.getTools(dependency.connector, baseUrl, requestScope, options.signal ? { signal: options.signal } : {});
802
803
  const tool = tools.find((tool) => tool.name === dependency.tool);
803
804
  if (!tool ||
804
805
  !(dependency.access === "catalog" ||
805
806
  (dependency.classification === "read" && tool.classification === "read") ||
806
- opts.readOnly !== true))
807
+ (opts.readOnly !== true && connector.readOnly !== true)))
807
808
  return false;
808
809
  }
809
810
  return true;
@@ -11,7 +11,7 @@ const OUTCOMES = {
11
11
  status: 400,
12
12
  tone: "declined",
13
13
  heading: "Authorization was declined",
14
- message: "Consent was declined at the provider, so nothing was stored. Start authorization again whenever you are ready.",
14
+ message: "Consent was declined, so nothing was stored. Start again when ready.",
15
15
  fix: {
16
16
  problem: "Consent for a downstream OAuth connector was declined at the provider, so no grant was stored.",
17
17
  steps: [
@@ -72,7 +72,7 @@ const OUTCOMES = {
72
72
  fix: {
73
73
  problem: "A provider returned an authorization code, but exchanging it for tokens failed.",
74
74
  steps: [
75
- "Check the OAuth client credentials the connector uses — client id and secret in the deployment's environment — and that they belong to the same OAuth app that showed the consent screen.",
75
+ "Check the OAuth client id and secret the connector reads from the deployment's environment, and that they belong to the OAuth app that showed the consent screen.",
76
76
  "Check that the token endpoint and redirect URI the connector uses match the provider's registration exactly.",
77
77
  "Find the [connecta] warning for this callback in the deployment logs; it records the exchange error the page withholds.",
78
78
  ],
@@ -62,7 +62,7 @@ export declare function slotBadge(slot: Pick<AgentSlot, "state">): {
62
62
  label: string;
63
63
  tone: Tone;
64
64
  };
65
- /** When the slot last changed, as "Issued <date>" or "Revoked <date>"; a dash when it never has. */
65
+ /** When the slot last changed, issued or revoked (its badge says which); a dash when it never has. */
66
66
  export declare function slotSince(slot: AgentSlot, format: (value: string) => string): string;
67
67
  export declare function lastSeenLabel(lastSeenAt: string | null, format: (value: string) => string): string;
68
68
  /**
@@ -1,6 +1,6 @@
1
1
  import { failure } from "./view.js";
2
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";
3
+ const SLOT_UNAVAILABLE = "Token state unknown. Refresh.";
4
4
  /** Why a row's controls are disabled until a fresh list read resolves its slot. */
5
5
  const SLOT_UNRESOLVED = "Refresh to continue";
6
6
  export function slotActions(slot) {
@@ -53,12 +53,12 @@ export function slotBadge(slot) {
53
53
  return { label: "Revoked", tone: "warn" };
54
54
  return { label: "No token", tone: "neutral" };
55
55
  }
56
- /** When the slot last changed, as "Issued <date>" or "Revoked <date>"; a dash when it never has. */
56
+ /** When the slot last changed, issued or revoked (its badge says which); a dash when it never has. */
57
57
  export function slotSince(slot, format) {
58
58
  if (slot.state === "issued")
59
- return `Issued ${format(slot.issuedAt)}`;
59
+ return format(slot.issuedAt);
60
60
  if (slot.state === "revoked")
61
- return `Revoked ${format(slot.revokedAt)}`;
61
+ return format(slot.revokedAt);
62
62
  return "—";
63
63
  }
64
64
  export function lastSeenLabel(lastSeenAt, format) {
@@ -72,33 +72,33 @@ export function lastSeenLabel(lastSeenAt, format) {
72
72
  */
73
73
  export function tokenFailureNotice(action, name, facts, productName) {
74
74
  if (facts.kind === "session")
75
- return failure("Your session has ended. Sign in again, then retry.");
75
+ return failure("Session ended. Sign in again.");
76
76
  if (facts.kind === "forbidden")
77
- return failure("You don't have permission to manage agent tokens.");
77
+ return failure("You can't manage agent tokens.");
78
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.`);
79
+ return failure(`${name}'s token changed, so nothing was sent. Try again.`);
80
80
  }
81
81
  if (facts.kind === "network") {
82
- return failure(`Couldn't reach ${productName}. Refresh to check ${name}'s token before trying again.`);
82
+ return failure(`Couldn't reach ${productName}. Refresh to check ${name}'s token.`);
83
83
  }
84
84
  if (facts.status === 503) {
85
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.`);
86
+ ? `Revoke result unknown for ${name}. Revoke again to be sure.`
87
+ : `${action === "issue" ? "Issue" : "Rotate"} result unknown for ${name}. No secret came back, so rotate again.`);
88
88
  }
89
89
  if (facts.status === 499) {
90
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.`);
91
+ ? `The request ended before ${name}'s revoke finished. Refresh to check its token.`
92
+ : `The request ended before ${name}'s token arrived. Refresh to check its token.`);
93
93
  }
94
94
  // The route's compare-and-swap refused it: the slot moved after the list was read.
95
95
  if (facts.status === 409 && facts.code === "slot_changed")
96
- return failure("Changed since you looked, refresh.");
96
+ return failure("Changed since you looked. Refresh.");
97
97
  if (facts.status === 409)
98
- return failure(`${name}'s token changed meanwhile. Reload and try again.`);
98
+ return failure(`${name}'s token changed. Reload and try again.`);
99
99
  if (facts.status === 404)
100
100
  return failure(`${name} is no longer declared in code. Reload the page.`);
101
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.`);
102
+ ? failure(`Couldn't revoke ${name}'s token. Reload and try again.`)
103
+ : failure(`Couldn't issue a token for ${name}. Reload and try again.`);
104
104
  }