@zackbart/connecta 0.32.0 → 0.34.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 (158) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +95 -116
  3. package/SECURITY.md +1 -1
  4. package/bin/connecta.mjs +24 -6
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +167 -23
  7. package/dist/access-tokens.js +676 -140
  8. package/dist/activity.d.ts +3 -0
  9. package/dist/agent-identity.js +47 -0
  10. package/dist/auth/cloudflare-access.js +13 -5
  11. package/dist/branding.d.ts +7 -0
  12. package/dist/branding.js +76 -9
  13. package/dist/call-admission-context.js +15 -0
  14. package/dist/call-admission.js +30 -43
  15. package/dist/call-recovery.js +6 -2
  16. package/dist/catalog-service.js +15 -1
  17. package/dist/config-value-sources.js +16 -9
  18. package/dist/config.d.ts +48 -75
  19. package/dist/config.js +144 -36
  20. package/dist/connector-access.d.ts +4 -34
  21. package/dist/connector-access.js +15 -99
  22. package/dist/connector-caller.d.ts +7 -1
  23. package/dist/connector-caller.js +4 -2
  24. package/dist/connectors/api-connector.d.ts +27 -4
  25. package/dist/connectors/api-connector.js +2 -0
  26. package/dist/connectors/remote-mcp.js +2 -0
  27. package/dist/describe-config.d.ts +31 -7
  28. package/dist/describe-config.js +8 -17
  29. package/dist/execute.js +1 -11
  30. package/dist/fix-prompt.js +2 -2
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +5 -3
  34. package/dist/membership.d.ts +53 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/module-contracts.d.ts +23 -6
  37. package/dist/oauth-callback-outcome.js +2 -2
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -4
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/config-summary.js +562 -0
  43. package/dist/operator-ui/connection-map.js +375 -0
  44. package/dist/operator-ui/contract.d.ts +149 -28
  45. package/dist/operator-ui/endpoints.js +221 -0
  46. package/dist/operator-ui/fix-prompts.js +5 -5
  47. package/dist/operator-ui/generated.js +5 -4
  48. package/dist/operator-ui/member-page.js +97 -0
  49. package/dist/operator-ui/model.d.ts +14 -6
  50. package/dist/operator-ui/overview.js +134 -0
  51. package/dist/operator-ui/view.d.ts +107 -37
  52. package/dist/operator-ui/view.js +163 -86
  53. package/dist/page-styles.js +5 -3
  54. package/dist/provider.d.ts +61 -8
  55. package/dist/provider.js +46 -7
  56. package/dist/providers/_shared/google/workspace.d.ts +56 -17
  57. package/dist/providers/_shared/google/workspace.js +34 -8
  58. package/dist/providers/_shared/rest/tools.js +2 -2
  59. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  60. package/dist/providers/_shared/rest/value-safety.js +699 -7
  61. package/dist/providers/ccb/index.js +2 -2
  62. package/dist/providers/cloudflare/index.js +3 -3
  63. package/dist/providers/cloudflare/rest.js +25 -3
  64. package/dist/providers/cloudflare/skill.generated.js +1 -1
  65. package/dist/providers/cloudflare/value-safety.js +32 -0
  66. package/dist/providers/docs/index.js +4 -4
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.js +4 -4
  69. package/dist/providers/drive/skill.generated.js +2 -2
  70. package/dist/providers/forms/index.js +4 -4
  71. package/dist/providers/forms/skill.generated.js +2 -2
  72. package/dist/providers/gmail/index.js +11 -9
  73. package/dist/providers/gmail/skill.generated.js +2 -2
  74. package/dist/providers/mixpanel/index.js +1 -1
  75. package/dist/providers/mixpanel/skill.generated.js +3 -3
  76. package/dist/providers/notion/index.d.ts +7 -0
  77. package/dist/providers/notion/index.js +100 -11
  78. package/dist/providers/notion/rest.js +45 -5
  79. package/dist/providers/notion/skill.generated.js +2 -2
  80. package/dist/providers/notion/value-safety.js +30 -5
  81. package/dist/providers/overflow/index.js +2 -2
  82. package/dist/providers/overflow/skill.generated.js +2 -2
  83. package/dist/providers/planning-center/index.js +2 -2
  84. package/dist/providers/planning-center/skill.generated.js +4 -4
  85. package/dist/providers/revenuecat/skill.generated.js +2 -2
  86. package/dist/providers/sheets/index.d.ts +3 -0
  87. package/dist/providers/sheets/index.js +7 -4
  88. package/dist/providers/sheets/skill.generated.js +2 -2
  89. package/dist/providers/slides/index.js +4 -4
  90. package/dist/providers/slides/skill.generated.js +2 -2
  91. package/dist/providers/stripe/rest.js +9 -2
  92. package/dist/providers/stripe/skill.generated.js +1 -1
  93. package/dist/providers/stripe/value-safety.js +22 -0
  94. package/dist/providers/tithely/index.js +1 -1
  95. package/dist/providers/vercel/index.js +1 -1
  96. package/dist/providers/vercel/rest.js +20 -2
  97. package/dist/providers/vercel/skill.generated.js +2 -2
  98. package/dist/providers/vercel/value-safety.js +22 -0
  99. package/dist/registry.d.ts +2 -3
  100. package/dist/registry.js +23 -9
  101. package/dist/routes/access-tokens.js +82 -75
  102. package/dist/routes/activity-disclosure.js +17 -18
  103. package/dist/routes/activity.js +34 -54
  104. package/dist/routes/actor-labels.js +33 -4
  105. package/dist/routes/agent-path.js +43 -0
  106. package/dist/routes/connect.js +16 -12
  107. package/dist/routes/credentials.js +16 -40
  108. package/dist/routes/mcp.js +161 -104
  109. package/dist/routes/oauth-management.js +11 -5
  110. package/dist/routes/oauth.js +5 -11
  111. package/dist/routes/operator.js +24 -23
  112. package/dist/routes/shared.d.ts +60 -20
  113. package/dist/routes/shared.js +187 -96
  114. package/dist/routes/ui-config.js +437 -97
  115. package/dist/routes/ui-data.js +2 -2
  116. package/dist/routes/ui.js +9 -16
  117. package/dist/runtime/call-admission.js +9 -0
  118. package/dist/server.js +5 -0
  119. package/dist/skills.js +10 -0
  120. package/dist/storage/keys.js +9 -1
  121. package/dist/storage/sql.js +5 -1
  122. package/dist/types.d.ts +29 -7
  123. package/dist/ui.d.ts +1 -1
  124. package/dist/ui.js +23 -16
  125. package/dist/usage-guide.js +1 -1
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/dist/workspace-address.js +9 -0
  129. package/dist/workspace-subject.js +23 -0
  130. package/documentation/README.md +47 -23
  131. package/documentation/architecture.md +371 -61
  132. package/documentation/auth.md +897 -249
  133. package/documentation/code-mode.md +37 -16
  134. package/documentation/deploying.md +126 -25
  135. package/documentation/integrating.md +97 -23
  136. package/documentation/meta-tools.md +50 -31
  137. package/documentation/migration-0.33.md +352 -0
  138. package/documentation/migration-0.34.md +67 -0
  139. package/documentation/operating.md +3 -1
  140. package/documentation/operator-tests.md +147 -34
  141. package/documentation/operator-ui.md +519 -64
  142. package/documentation/provider-auth-migration.md +37 -23
  143. package/documentation/provider-migration-0.29.md +2 -2
  144. package/examples/worker/AGENTS.md +37 -9
  145. package/examples/worker/README.md +127 -61
  146. package/examples/worker/src/connecta.config.ts +40 -23
  147. package/examples/worker/wrangler.jsonc +4 -1
  148. package/package.json +19 -18
  149. package/templates/node/.env.example +3 -0
  150. package/templates/node/AGENTS.md +33 -6
  151. package/templates/node/Dockerfile +2 -2
  152. package/templates/node/README.md +80 -52
  153. package/templates/node/docker-compose.yml +3 -2
  154. package/templates/node/package.json +4 -3
  155. package/templates/node/src/adopt-token.ts +23 -0
  156. package/templates/node/src/connecta.config.ts +28 -12
  157. package/templates/node/src/rotate-token.ts +17 -0
  158. package/templates/node/src/provision-token.ts +0 -17
package/dist/provider.js CHANGED
@@ -3,17 +3,53 @@
3
3
  // acquire the MCP client, OAuth, or Effect graph by using it, and a hosted
4
4
  // provider does not acquire `api()`. Its runtime imports validate plain
5
5
  // options and reviewed classification, with no I/O.
6
- import { array, assertKnownOptions, keys, optionsOf } from "./config-schema.js";
6
+ import { array, assertKnownOptions, ConfigError, fn, keys, object, opaque, required, whole, } from "./config-schema.js";
7
7
  import { reviewedClassification } from "./catalog-drift.js";
8
- const budget = optionsOf()(keys("kind", "maxCalls", "windowMs"));
9
- const admissionRule = optionsOf()({
10
- ...keys("maxConcurrency", "maxQueueSize", "queueTimeoutMs", "retryAfterMs", "partitionKey"),
8
+ const budget = {
9
+ ...object({
10
+ kind: required(opaque({
11
+ check(value, path) {
12
+ if (value !== "rolling-window")
13
+ throw new ConfigError(`${path} must be "rolling-window".`);
14
+ },
15
+ })),
16
+ maxCalls: required(whole({ min: 1 })),
17
+ windowMs: required(whole({ min: 1 })),
18
+ }),
19
+ absent: () => undefined,
20
+ };
21
+ const admissionRule = object({
22
+ maxConcurrency: whole({ min: 1 }),
23
+ maxQueueSize: whole({ min: 0 }),
24
+ queueTimeoutMs: whole({ min: 1 }),
25
+ retryAfterMs: whole({ min: 0 }),
26
+ partitionKey: fn(),
11
27
  budget,
12
28
  });
13
- export const CALL_ADMISSION = optionsOf()({
14
- ...keys("maxPartitions"),
15
- rules: array(admissionRule),
29
+ const admissionPolicy = object({
30
+ maxPartitions: whole({ min: 1 }),
31
+ rules: required(array(admissionRule)),
16
32
  });
33
+ export const CALL_ADMISSION = {
34
+ ...admissionPolicy,
35
+ absent: () => undefined,
36
+ parse(value, path) {
37
+ const policy = admissionPolicy.parse(assertKnownOptions(value, path, admissionPolicy), path);
38
+ if (policy.rules.length !== 1) {
39
+ throw new ConfigError(`${path}.rules must contain exactly one rule in this release.`);
40
+ }
41
+ const rule = policy.rules[0];
42
+ if (rule.maxConcurrency === undefined && rule.budget === undefined) {
43
+ throw new ConfigError(`${path} rule must declare maxConcurrency or budget.`);
44
+ }
45
+ if (rule.maxConcurrency === undefined &&
46
+ (rule.maxQueueSize !== undefined || rule.queueTimeoutMs !== undefined || rule.retryAfterMs !== undefined)) {
47
+ throw new ConfigError(`${path} queue settings require maxConcurrency.`);
48
+ }
49
+ // object() omits undefined children; its resolved type lists every key.
50
+ return policy;
51
+ },
52
+ };
17
53
  /** Options every maintained provider shares. */
18
54
  export const PROVIDER_COMMON = {
19
55
  ...keys("title", "authScope", "purpose", "instructions", "maxResultBytes"),
@@ -142,6 +178,9 @@ function asProvider(provider, shape, id, options, build) {
142
178
  let connector;
143
179
  try {
144
180
  options = assertKnownOptions(options, `${factory}(${JSON.stringify(id)})`, shape);
181
+ const admission = options == null ? undefined : options.callAdmission;
182
+ if (admission !== undefined)
183
+ CALL_ADMISSION.parse(admission, `${factory}(${JSON.stringify(id)}).callAdmission`);
145
184
  connector = build(id, options);
146
185
  }
147
186
  catch (error) {
@@ -10,9 +10,16 @@
10
10
  * channel (PRINCIPLES.md INV-3, #678): deployment config supplies
11
11
  * `subject`, a function from the admitted `AuthenticatedIdentity` to a
12
12
  * Workspace address, and the provider reads the identity core attached beside
13
- * the context. No tool argument, header, or program can name the subject —
14
- * every product's base URL is confined beneath `users/me`, or its product's
15
- * equivalent, so the token's subject is the only account a request reaches.
13
+ * the context. An agent, which has no Workspace account, acts instead as the
14
+ * `owner` of the pool it came through, which core copies from that pool's
15
+ * config beside the identity; the mapping never sees an agent. In a pool
16
+ * without an owner the connector is absent for agents (core's scoped
17
+ * registry view hides it), and this provider still fails `auth_required` if
18
+ * one reaches it anyway (decisions/0006). A fixed
19
+ * `subject` address is every caller's, owners aside. No tool argument,
20
+ * header, or program can name the subject — every product's base URL is
21
+ * confined beneath `users/me`, or its product's equivalent, so the token's
22
+ * subject is the only account a request reaches.
16
23
  * A call with no admitted caller, one an open deployment admitted without
17
24
  * authentication (the mapping is never asked about it), or one the mapping
18
25
  * answers `undefined` for, fails `auth_required` before anything leaves the
@@ -41,7 +48,8 @@
41
48
  * that client ID and *exactly* the scopes each product provider lists, comma
42
49
  * separated. A grant can take up to 24 hours to apply.
43
50
  * 6. Pass the key to the provider from the deployment's secrets, and map the
44
- * admitted identity to a Workspace address in `subject`.
51
+ * admitted identity to a Workspace address in `subject`. Give each pool
52
+ * agents use an `owner`, the address they act as there.
45
53
  */
46
54
  import { type GuardedRequest } from "../../../connectors/guarded-fetch.js";
47
55
  import type { AuthenticatedIdentity, ConnectorCallAdmissionPolicy, ConnectorContext } from "../../../types.js";
@@ -53,13 +61,15 @@ export type { GoogleServiceAccount } from "./service-account.js";
53
61
  * A function receives the identity an inbound auth provider authenticated for
54
62
  * this request and returns that person's Workspace address, or `undefined`
55
63
  * for a caller who has none — that call then fails closed. It is never called
56
- * for an open deployment's anonymous requests, nor for a call no request
57
- * admitted. It may be async, for a directory lookup (a Clerk user id to an
58
- * email, say), and receives the call's cancellation signal for that; it runs
59
- * once per tool call and should cache what it fetches. A string is one fixed
60
- * account for every caller who can reach the connector, for a shared mailbox
61
- * or scheduled work with no caller; limit who reaches it with
62
- * `identity.connectorAccess`.
64
+ * for an open deployment's anonymous requests, for a call no request
65
+ * admitted, or for an agent: an agent acts as the `owner` of the pool it
66
+ * came through, and never sees the connector in a pool without one. It may be async, for
67
+ * a directory lookup (a Clerk user id to an email, say), and receives the
68
+ * call's cancellation signal for that; it runs once per tool call and should
69
+ * cache what it fetches. A string is one fixed account for every caller who
70
+ * can reach the connector, agents included and pool owners aside, for a
71
+ * shared mailbox or scheduled work with no caller; limit who reaches it with
72
+ * `maintainers` and the `members` of the pools that list it.
63
73
  */
64
74
  export type GoogleWorkspaceSubject = string | ((identity: Readonly<AuthenticatedIdentity>, context: GoogleSubjectContext) => string | undefined | Promise<string | undefined>);
65
75
  /** What a subject mapping is told about the call it resolves for. */
@@ -103,18 +113,41 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
103
113
  readonly privateKey: import("../../../config-schema.js").Field<unknown, unknown, true>;
104
114
  };
105
115
  };
106
- callAdmission: import("../../../config-schema.js").Field<unknown, unknown, true> & {
107
- readonly shape: {
108
- rules: import("../../../config-schema.js").Field<unknown[], readonly unknown[] | undefined, true>;
109
- maxPartitions: import("../../../config-schema.js").Field<unknown, unknown, true>;
116
+ callAdmission: Omit<import("../../../config-schema.js").Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
117
+ parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
118
+ readonly shape: import("../../../config-schema.js").Shape & {
119
+ maxPartitions: import("../../../config-schema.js").Field<number, number | undefined, true>;
120
+ rules: import("../../../config-schema.js").Field<{
121
+ maxConcurrency?: number | undefined;
122
+ maxQueueSize?: number | undefined;
123
+ queueTimeoutMs?: number | undefined;
124
+ retryAfterMs?: number | undefined;
125
+ partitionKey?: ((input: Readonly<import("../../../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
126
+ budget?: {
127
+ kind: "rolling-window";
128
+ maxCalls: number;
129
+ windowMs: number;
130
+ } | undefined;
131
+ }[], readonly {
132
+ readonly maxConcurrency: number | undefined;
133
+ readonly maxQueueSize: number | undefined;
134
+ readonly queueTimeoutMs: number | undefined;
135
+ readonly retryAfterMs: number | undefined;
136
+ readonly partitionKey: ((input: Readonly<import("../../../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
137
+ readonly budget: {
138
+ readonly kind: "rolling-window";
139
+ readonly maxCalls: number;
140
+ readonly windowMs: number;
141
+ } | undefined;
142
+ }[], false>;
110
143
  };
111
144
  };
112
145
  instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
113
146
  title: import("../../../config-schema.js").Field<unknown, unknown, true>;
114
147
  maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
115
- baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
116
- subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
117
148
  purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
149
+ subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
150
+ baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
118
151
  };
119
152
  };
120
153
  /** The checked, construction-time half of {@link GoogleWorkspaceOptions}. */
@@ -125,6 +158,12 @@ export interface WorkspaceConnection {
125
158
  }
126
159
  /** Validate the shared options at construction; throws on a structural mistake. */
127
160
  export declare function workspaceConnection(factory: string, options: GoogleWorkspaceOptions): WorkspaceConnection;
161
+ /**
162
+ * Tag a built Workspace connector with whose account it acts as, so the
163
+ * operator matrix can say "humans act as themselves, agents as the pool
164
+ * owner" for a mapping, or name a fixed subject (#837). Returns it unchanged.
165
+ */
166
+ export declare function workspaceConnector<C extends object>(connection: WorkspaceConnection, connector: C): C;
128
167
  /** How a product reads an HTTP 404 — see H11. */
129
168
  type NotFoundMeaning = "absent" | "ambiguous";
130
169
  /**
@@ -10,9 +10,16 @@
10
10
  * channel (PRINCIPLES.md INV-3, #678): deployment config supplies
11
11
  * `subject`, a function from the admitted `AuthenticatedIdentity` to a
12
12
  * Workspace address, and the provider reads the identity core attached beside
13
- * the context. No tool argument, header, or program can name the subject —
14
- * every product's base URL is confined beneath `users/me`, or its product's
15
- * equivalent, so the token's subject is the only account a request reaches.
13
+ * the context. An agent, which has no Workspace account, acts instead as the
14
+ * `owner` of the pool it came through, which core copies from that pool's
15
+ * config beside the identity; the mapping never sees an agent. In a pool
16
+ * without an owner the connector is absent for agents (core's scoped
17
+ * registry view hides it), and this provider still fails `auth_required` if
18
+ * one reaches it anyway (decisions/0006). A fixed
19
+ * `subject` address is every caller's, owners aside. No tool argument,
20
+ * header, or program can name the subject — every product's base URL is
21
+ * confined beneath `users/me`, or its product's equivalent, so the token's
22
+ * subject is the only account a request reaches.
16
23
  * A call with no admitted caller, one an open deployment admitted without
17
24
  * authentication (the mapping is never asked about it), or one the mapping
18
25
  * answers `undefined` for, fails `auth_required` before anything leaves the
@@ -41,13 +48,16 @@
41
48
  * that client ID and *exactly* the scopes each product provider lists, comma
42
49
  * separated. A grant can take up to 24 hours to apply.
43
50
  * 6. Pass the key to the provider from the deployment's secrets, and map the
44
- * admitted identity to a Workspace address in `subject`.
51
+ * admitted identity to a Workspace address in `subject`. Give each pool
52
+ * agents use an `owner`, the address they act as there.
45
53
  */
46
54
  import { guardedFetch, retryAfterMs } from "../../../connectors/guarded-fetch.js";
47
55
  import { keys, optionsOf } from "../../../config-schema.js";
48
56
  import { callerOf } from "../../../connector-caller.js";
49
57
  import { CALL_ADMISSION } from "../../../connectors/option-shapes.js";
50
58
  import { ConnectorCallError } from "../../../errors.js";
59
+ import { isWorkspaceAddress } from "../../../workspace-address.js";
60
+ import { tagWorkspaceSubject } from "../../../workspace-subject.js";
51
61
  import { delegatedToken, forgetDelegatedToken, parseServiceAccount, } from "./service-account.js";
52
62
  /** The closed options every Workspace provider accepts; see `assertKnownOptions`. */
53
63
  export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
@@ -56,8 +66,6 @@ export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
56
66
  serviceAccount: optionsOf()(keys("clientEmail", "privateKey", "clientId")),
57
67
  callAdmission: CALL_ADMISSION,
58
68
  });
59
- /** A plausible mailbox: one `@`, no whitespace or control characters. */
60
- const ADDRESS = /^[^\s@<>()",;:\\[\]]+@[^\s@<>()",;:\\[\]]+\.[^\s@<>()",;:\\[\]]+$/;
61
69
  /** Validate the shared options at construction; throws on a structural mistake. */
62
70
  export function workspaceConnection(factory, options) {
63
71
  const purpose = options?.purpose?.trim();
@@ -66,7 +74,7 @@ export function workspaceConnection(factory, options) {
66
74
  const account = parseServiceAccount(`${factory}()`, options.serviceAccount);
67
75
  const subject = options.subject;
68
76
  if (typeof subject === "string") {
69
- if (!ADDRESS.test(subject.trim())) {
77
+ if (!isWorkspaceAddress(subject.trim())) {
70
78
  throw new Error(`${factory}() subject must be a Workspace email address or a function of the caller.`);
71
79
  }
72
80
  return { purpose, account, subject: subject.trim() };
@@ -76,6 +84,14 @@ export function workspaceConnection(factory, options) {
76
84
  }
77
85
  return { purpose, account, subject };
78
86
  }
87
+ /**
88
+ * Tag a built Workspace connector with whose account it acts as, so the
89
+ * operator matrix can say "humans act as themselves, agents as the pool
90
+ * owner" for a mapping, or name a fixed subject (#837). Returns it unchanged.
91
+ */
92
+ export function workspaceConnector(connection, connector) {
93
+ return tagWorkspaceSubject(connector, connection.subject);
94
+ }
79
95
  /**
80
96
  * The Workspace address this call acts as, resolved from config and the
81
97
  * admitted caller only. Throws before any network call when there is none.
@@ -92,6 +108,16 @@ async function subjectFor(provider, subject, ctx) {
92
108
  if (!caller.authenticated) {
93
109
  throw new ConnectorCallError("auth_required", `${provider} acts as the signed-in Workspace user, and this deployment admitted the call without authentication. Configure inbound auth, or a fixed subject for shared use.`);
94
110
  }
111
+ // An agent has no Workspace account of its own. It acts as the owner of the
112
+ // pool it came through, which core copied from that pool's config, and the
113
+ // mapping, which answers for people, is never asked about it (decisions/0006).
114
+ // Core's scoped view already hides the connector from an agent without an
115
+ // owner; this refusal is the fallback for any context built without it.
116
+ if (caller.identity.agent !== undefined) {
117
+ if (caller.pool !== undefined && isWorkspaceAddress(caller.poolOwner))
118
+ return caller.poolOwner;
119
+ throw new ConnectorCallError("auth_required", `${provider} acts as a pool's owner for agents, and the pool this agent reached it through has no owner. An operator sets owner on the pool in the deployment's config; nothing a call sends can choose one.`);
120
+ }
95
121
  let mapped;
96
122
  try {
97
123
  mapped = await subject(caller.identity, ctx.signal ? { signal: ctx.signal } : {});
@@ -104,7 +130,7 @@ async function subjectFor(provider, subject, ctx) {
104
130
  if (mapped === undefined) {
105
131
  throw new ConnectorCallError("auth_required", `This caller has no Google Workspace account mapped for ${provider}. An operator maps signed-in identities to Workspace addresses in the deployment's subject config; nothing a call sends can choose one.`);
106
132
  }
107
- if (typeof mapped !== "string" || !ADDRESS.test(mapped.trim())) {
133
+ if (typeof mapped !== "string" || !isWorkspaceAddress(mapped.trim())) {
108
134
  throw new ConnectorCallError("connector_call_failed", `The deployment's ${provider} subject mapping returned something that is not an email address.`, { retryable: false });
109
135
  }
110
136
  return mapped.trim();
@@ -1,5 +1,5 @@
1
1
  import { guardedFetch, } from "../../../connectors/guarded-fetch.js";
2
- import { recordRecovery } from "../../../call-recovery.js";
2
+ import { IDEMPOTENCY_KEY_MAX_LENGTH, recordRecovery } from "../../../call-recovery.js";
3
3
  import { ConnectorCallError, unavailableCallError } from "../../../errors.js";
4
4
  const READ = { readOnlyHint: true };
5
5
  const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
@@ -506,7 +506,7 @@ export function restTools(vendor) {
506
506
  idempotencyKey: {
507
507
  type: "string",
508
508
  minLength: 1,
509
- maxLength: 255,
509
+ maxLength: IDEMPOTENCY_KEY_MAX_LENGTH,
510
510
  description: "Reuse to retry this exact write; one is generated and returned when omitted.",
511
511
  },
512
512
  }
@@ -99,6 +99,16 @@ interface ResourceRules {
99
99
  export interface ValueSafetyTable {
100
100
  /** Display name in refusals. */
101
101
  readonly title: string;
102
+ /** Global signing names plus generic credential names only on these reviewed response paths. */
103
+ readonly embeddedUrls?: {
104
+ readonly operations?: Readonly<Record<string, readonly string[]>>;
105
+ /** Discriminated resources, wherever lists, expansions, or events embed them. */
106
+ readonly resources?: {
107
+ readonly key: string;
108
+ readonly partials?: readonly string[];
109
+ readonly paths: Readonly<Record<string, readonly string[]>>;
110
+ };
111
+ };
102
112
  /** The reviewed verdict for every flagged operation, keyed `METHOD /path template`. */
103
113
  readonly operations: Readonly<Record<string, ValueSafetyVerdict>>;
104
114
  /**
@@ -127,6 +137,46 @@ export declare function under(prefixes: readonly string[], paths: readonly strin
127
137
  * (Cloudflare's).
128
138
  */
129
139
  export declare function credentialName(name: string): boolean;
140
+ /** Where a vendor stores the files it signs. */
141
+ export interface FileHosts {
142
+ /** Hosts matched whole or as a parent domain: `file.notion.so` covers `img.file.notion.so`. */
143
+ readonly domains: readonly string[];
144
+ /** S3 buckets, virtual-hosted or as the first path segment on an S3 endpoint. Never all of S3. */
145
+ readonly buckets: readonly string[];
146
+ }
147
+ /**
148
+ * `text` with every pre-signed link withheld: at one of `hosts` (Notion's
149
+ * file storage, say), its whole query and fragment; anywhere, a signing
150
+ * parameter's value. Everything else comes back byte for byte.
151
+ */
152
+ export declare function withholdSignedLinks(text: string, hosts: FileHosts): string;
153
+ /**
154
+ * Reviewed REST text policy: only credential-bearing URL queries/fragments
155
+ * change. Ordinary links, including external proxies, retain their bytes.
156
+ * Literal AWS/Google signing names are withheld anywhere, as in #818.
157
+ */
158
+ export declare function withholdEmbeddedUrls(text: string): string;
159
+ /** Every string inside a JSON value through `withholdSignedLinks`; unchanged values keep their identity. */
160
+ export declare function withholdSignedLinksIn(value: unknown, hosts: FileHosts, depth?: number): unknown;
161
+ /** Apply the reviewed text policy to a selected value or vendor error body. */
162
+ export declare function withholdEmbeddedUrlsIn(value: unknown): unknown;
163
+ /**
164
+ * Whether a request value carries a link whose query or
165
+ * fragment holds the exact `[redacted]` placeholder, plain or encoded: a
166
+ * withheld read copied into a write, which would store the placeholder.
167
+ * Only the link's own destination is read: a Markdown link ends at its
168
+ * closing `)`, so `[f](…?a=1)[redacted]` is ordinary text after it.
169
+ * The word alone (`unredacted`) or the placeholder away from a file link is
170
+ * a person's text. When `hosts` is supplied, require its actual authority
171
+ * to match that policy, preserving Notion's external-proxy boundary.
172
+ */
173
+ export declare function carriesWithheldLink(value: unknown, hosts?: FileHosts): boolean;
174
+ /** Refuse a withheld destination copied into customer data, before any dispatch. */
175
+ export declare function refuseWithheldLinks(value: unknown, title: string): void;
176
+ /** Upload admission inspects actual bytes, including base64-decoded files and multipart blobs. */
177
+ export declare function refuseWithheldUpload(body: BodyInit, title: string): Promise<void>;
178
+ /** Only for documented JSON fields the vendor decodes as base64, never arbitrary strings. */
179
+ export declare function refuseWithheldBase64(value: unknown, title: string, maxDecodedBytes?: number): Promise<void>;
130
180
  /** Whether every segment of a reviewed path is in the path language; the harness checks each table. */
131
181
  export declare function validPath(raw: string): boolean;
132
182
  /** What a vendor's tools ask of its reviewed table. */