@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
@@ -3,9 +3,8 @@ import type { Connector, ConnectorCallAdmissionPolicy, ConnectorUsageGuide, Tool
3
3
  /**
4
4
  * How a provider reaches its vendor: the vendor's hosted MCP server, an
5
5
  * `api()` connector over the vendor's HTTP API, one connector composing both
6
- * with an explicit operation ownership map, or `"dual"`: the configured
7
- * `auth.type` selects the implementation, OAuth reaching the hosted MCP server
8
- * and a key reaching Connecta's REST connector.
6
+ * with an explicit operation ownership map, or `"dual"`: configuration selects the implementation, usually by `auth.type`. Mixpanel
7
+ * uses explicit `via` because service accounts reach both implementations.
9
8
  */
10
9
  export type ProviderKind = "mcp" | "api" | "composed" | "dual";
11
10
  /** Options every provider accepts, validated before `create` runs. */
@@ -23,19 +22,71 @@ export interface ProviderOptions {
23
22
  /** Optional per-runtime downstream call-admission policy. */
24
23
  callAdmission?: ConnectorCallAdmissionPolicy;
25
24
  }
26
- export declare const CALL_ADMISSION: Field<unknown, unknown, true> & {
25
+ declare const admissionPolicy: Field<{
26
+ maxPartitions?: number | undefined;
27
+ rules: {
28
+ maxConcurrency?: number | undefined;
29
+ maxQueueSize?: number | undefined;
30
+ queueTimeoutMs?: number | undefined;
31
+ retryAfterMs?: number | undefined;
32
+ partitionKey?: ((input: Readonly<import("./types.js").ConnectorCallAdmissionInput>) => string) | undefined;
33
+ budget?: {
34
+ kind: "rolling-window";
35
+ maxCalls: number;
36
+ windowMs: number;
37
+ } | undefined;
38
+ }[];
39
+ }, {
40
+ readonly maxPartitions: number | undefined;
41
+ readonly rules: readonly {
42
+ readonly maxConcurrency: number | undefined;
43
+ readonly maxQueueSize: number | undefined;
44
+ readonly queueTimeoutMs: number | undefined;
45
+ readonly retryAfterMs: number | undefined;
46
+ readonly partitionKey: ((input: Readonly<import("./types.js").ConnectorCallAdmissionInput>) => string) | undefined;
47
+ readonly budget: {
48
+ readonly kind: "rolling-window";
49
+ readonly maxCalls: number;
50
+ readonly windowMs: number;
51
+ } | undefined;
52
+ }[];
53
+ }, true> & {
27
54
  readonly shape: {
28
- rules: Field<unknown[], readonly unknown[] | undefined, true>;
29
- maxPartitions: Field<unknown, unknown, true>;
55
+ maxPartitions: Field<number, number | undefined, true>;
56
+ rules: Field<{
57
+ maxConcurrency?: number | undefined;
58
+ maxQueueSize?: number | undefined;
59
+ queueTimeoutMs?: number | undefined;
60
+ retryAfterMs?: number | undefined;
61
+ partitionKey?: ((input: Readonly<import("./types.js").ConnectorCallAdmissionInput>) => string) | undefined;
62
+ budget?: {
63
+ kind: "rolling-window";
64
+ maxCalls: number;
65
+ windowMs: number;
66
+ } | undefined;
67
+ }[], readonly {
68
+ readonly maxConcurrency: number | undefined;
69
+ readonly maxQueueSize: number | undefined;
70
+ readonly queueTimeoutMs: number | undefined;
71
+ readonly retryAfterMs: number | undefined;
72
+ readonly partitionKey: ((input: Readonly<import("./types.js").ConnectorCallAdmissionInput>) => string) | undefined;
73
+ readonly budget: {
74
+ readonly kind: "rolling-window";
75
+ readonly maxCalls: number;
76
+ readonly windowMs: number;
77
+ } | undefined;
78
+ }[], false>;
30
79
  };
31
80
  };
81
+ export declare const CALL_ADMISSION: Omit<Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
82
+ parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
83
+ readonly shape: typeof admissionPolicy.shape;
84
+ };
32
85
  /** Options every maintained provider shares. */
33
86
  export declare const PROVIDER_COMMON: {
34
- callAdmission: Field<unknown, unknown, true> & {
35
- readonly shape: {
36
- rules: Field<unknown[], readonly unknown[] | undefined, true>;
37
- maxPartitions: Field<unknown, unknown, true>;
38
- };
87
+ callAdmission: Omit<Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
88
+ parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
89
+ readonly shape: typeof admissionPolicy.shape;
39
90
  };
40
91
  instructions: Field<unknown, unknown, true>;
41
92
  title: Field<unknown, unknown, true>;
@@ -142,3 +193,4 @@ export declare function defineProvider<O extends ProviderOptions>(definition: Pr
142
193
  export declare function asProviderFactory<O extends ProviderOptions>(definition: Omit<ProviderDefinition<O>, "create"> & {
143
194
  create(id: string, options: Readonly<O>): Connector;
144
195
  }): ProviderFactory<O>;
196
+ export {};
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,14 @@
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, and an agent
16
+ * in a pool without an owner fails `auth_required` (decisions/0006). A fixed
17
+ * `subject` address is every caller's, owners aside. No tool argument,
18
+ * header, or program can name the subject — every product's base URL is
19
+ * confined beneath `users/me`, or its product's equivalent, so the token's
20
+ * subject is the only account a request reaches.
16
21
  * A call with no admitted caller, one an open deployment admitted without
17
22
  * authentication (the mapping is never asked about it), or one the mapping
18
23
  * answers `undefined` for, fails `auth_required` before anything leaves the
@@ -41,7 +46,8 @@
41
46
  * that client ID and *exactly* the scopes each product provider lists, comma
42
47
  * separated. A grant can take up to 24 hours to apply.
43
48
  * 6. Pass the key to the provider from the deployment's secrets, and map the
44
- * admitted identity to a Workspace address in `subject`.
49
+ * admitted identity to a Workspace address in `subject`. Give each pool
50
+ * agents use an `owner`, the address they act as there.
45
51
  */
46
52
  import { type GuardedRequest } from "../../../connectors/guarded-fetch.js";
47
53
  import type { AuthenticatedIdentity, ConnectorCallAdmissionPolicy, ConnectorContext } from "../../../types.js";
@@ -53,13 +59,15 @@ export type { GoogleServiceAccount } from "./service-account.js";
53
59
  * A function receives the identity an inbound auth provider authenticated for
54
60
  * this request and returns that person's Workspace address, or `undefined`
55
61
  * 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`.
62
+ * for an open deployment's anonymous requests, for a call no request
63
+ * admitted, or for an agent: an agent acts as the `owner` of the pool it
64
+ * came through, and fails closed in a pool without one. It may be async, for
65
+ * a directory lookup (a Clerk user id to an email, say), and receives the
66
+ * call's cancellation signal for that; it runs once per tool call and should
67
+ * cache what it fetches. A string is one fixed account for every caller who
68
+ * can reach the connector, agents included and pool owners aside, for a
69
+ * shared mailbox or scheduled work with no caller; limit who reaches it with
70
+ * `maintainers` and the `members` of the pools that list it.
63
71
  */
64
72
  export type GoogleWorkspaceSubject = string | ((identity: Readonly<AuthenticatedIdentity>, context: GoogleSubjectContext) => string | undefined | Promise<string | undefined>);
65
73
  /** What a subject mapping is told about the call it resolves for. */
@@ -103,18 +111,41 @@ export declare const GOOGLE_WORKSPACE_OPTIONS: import("../../../config-schema.js
103
111
  readonly privateKey: import("../../../config-schema.js").Field<unknown, unknown, true>;
104
112
  };
105
113
  };
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>;
114
+ callAdmission: Omit<import("../../../config-schema.js").Field<unknown, ConnectorCallAdmissionPolicy | undefined, true>, "parse"> & {
115
+ parse(value: unknown, path: string): ConnectorCallAdmissionPolicy;
116
+ readonly shape: import("../../../config-schema.js").Shape & {
117
+ maxPartitions: import("../../../config-schema.js").Field<number, number | undefined, true>;
118
+ rules: import("../../../config-schema.js").Field<{
119
+ maxConcurrency?: number | undefined;
120
+ maxQueueSize?: number | undefined;
121
+ queueTimeoutMs?: number | undefined;
122
+ retryAfterMs?: number | undefined;
123
+ partitionKey?: ((input: Readonly<import("../../../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
124
+ budget?: {
125
+ kind: "rolling-window";
126
+ maxCalls: number;
127
+ windowMs: number;
128
+ } | undefined;
129
+ }[], readonly {
130
+ readonly maxConcurrency: number | undefined;
131
+ readonly maxQueueSize: number | undefined;
132
+ readonly queueTimeoutMs: number | undefined;
133
+ readonly retryAfterMs: number | undefined;
134
+ readonly partitionKey: ((input: Readonly<import("../../../types.js").ConnectorCallAdmissionInput>) => string) | undefined;
135
+ readonly budget: {
136
+ readonly kind: "rolling-window";
137
+ readonly maxCalls: number;
138
+ readonly windowMs: number;
139
+ } | undefined;
140
+ }[], false>;
110
141
  };
111
142
  };
112
143
  instructions: import("../../../config-schema.js").Field<unknown, unknown, true>;
113
144
  title: import("../../../config-schema.js").Field<unknown, unknown, true>;
114
145
  maxResultBytes: import("../../../config-schema.js").Field<unknown, unknown, true>;
146
+ purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
115
147
  baseUrl: import("../../../config-schema.js").Field<unknown, unknown, true>;
116
148
  subject: import("../../../config-schema.js").Field<unknown, unknown, true>;
117
- purpose: import("../../../config-schema.js").Field<unknown, unknown, true>;
118
149
  };
119
150
  };
120
151
  /** The checked, construction-time half of {@link GoogleWorkspaceOptions}. */
@@ -125,6 +156,12 @@ export interface WorkspaceConnection {
125
156
  }
126
157
  /** Validate the shared options at construction; throws on a structural mistake. */
127
158
  export declare function workspaceConnection(factory: string, options: GoogleWorkspaceOptions): WorkspaceConnection;
159
+ /**
160
+ * Tag a built Workspace connector with whose account it acts as, so the
161
+ * operator matrix can say "humans act as themselves, agents as the pool
162
+ * owner" for a mapping, or name a fixed subject (#837). Returns it unchanged.
163
+ */
164
+ export declare function workspaceConnector<C extends object>(connection: WorkspaceConnection, connector: C): C;
128
165
  /** How a product reads an HTTP 404 — see H11. */
129
166
  type NotFoundMeaning = "absent" | "ambiguous";
130
167
  /**
@@ -10,9 +10,14 @@
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, and an agent
16
+ * in a pool without an owner fails `auth_required` (decisions/0006). A fixed
17
+ * `subject` address is every caller's, owners aside. No tool argument,
18
+ * header, or program can name the subject — every product's base URL is
19
+ * confined beneath `users/me`, or its product's equivalent, so the token's
20
+ * subject is the only account a request reaches.
16
21
  * A call with no admitted caller, one an open deployment admitted without
17
22
  * authentication (the mapping is never asked about it), or one the mapping
18
23
  * answers `undefined` for, fails `auth_required` before anything leaves the
@@ -41,13 +46,16 @@
41
46
  * that client ID and *exactly* the scopes each product provider lists, comma
42
47
  * separated. A grant can take up to 24 hours to apply.
43
48
  * 6. Pass the key to the provider from the deployment's secrets, and map the
44
- * admitted identity to a Workspace address in `subject`.
49
+ * admitted identity to a Workspace address in `subject`. Give each pool
50
+ * agents use an `owner`, the address they act as there.
45
51
  */
46
52
  import { guardedFetch, retryAfterMs } from "../../../connectors/guarded-fetch.js";
47
53
  import { keys, optionsOf } from "../../../config-schema.js";
48
54
  import { callerOf } from "../../../connector-caller.js";
49
55
  import { CALL_ADMISSION } from "../../../connectors/option-shapes.js";
50
56
  import { ConnectorCallError } from "../../../errors.js";
57
+ import { isWorkspaceAddress } from "../../../workspace-address.js";
58
+ import { tagWorkspaceSubject } from "../../../workspace-subject.js";
51
59
  import { delegatedToken, forgetDelegatedToken, parseServiceAccount, } from "./service-account.js";
52
60
  /** The closed options every Workspace provider accepts; see `assertKnownOptions`. */
53
61
  export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
@@ -56,8 +64,6 @@ export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
56
64
  serviceAccount: optionsOf()(keys("clientEmail", "privateKey", "clientId")),
57
65
  callAdmission: CALL_ADMISSION,
58
66
  });
59
- /** A plausible mailbox: one `@`, no whitespace or control characters. */
60
- const ADDRESS = /^[^\s@<>()",;:\\[\]]+@[^\s@<>()",;:\\[\]]+\.[^\s@<>()",;:\\[\]]+$/;
61
67
  /** Validate the shared options at construction; throws on a structural mistake. */
62
68
  export function workspaceConnection(factory, options) {
63
69
  const purpose = options?.purpose?.trim();
@@ -66,7 +72,7 @@ export function workspaceConnection(factory, options) {
66
72
  const account = parseServiceAccount(`${factory}()`, options.serviceAccount);
67
73
  const subject = options.subject;
68
74
  if (typeof subject === "string") {
69
- if (!ADDRESS.test(subject.trim())) {
75
+ if (!isWorkspaceAddress(subject.trim())) {
70
76
  throw new Error(`${factory}() subject must be a Workspace email address or a function of the caller.`);
71
77
  }
72
78
  return { purpose, account, subject: subject.trim() };
@@ -76,6 +82,14 @@ export function workspaceConnection(factory, options) {
76
82
  }
77
83
  return { purpose, account, subject };
78
84
  }
85
+ /**
86
+ * Tag a built Workspace connector with whose account it acts as, so the
87
+ * operator matrix can say "humans act as themselves, agents as the pool
88
+ * owner" for a mapping, or name a fixed subject (#837). Returns it unchanged.
89
+ */
90
+ export function workspaceConnector(connection, connector) {
91
+ return tagWorkspaceSubject(connector, connection.subject);
92
+ }
79
93
  /**
80
94
  * The Workspace address this call acts as, resolved from config and the
81
95
  * admitted caller only. Throws before any network call when there is none.
@@ -92,6 +106,14 @@ async function subjectFor(provider, subject, ctx) {
92
106
  if (!caller.authenticated) {
93
107
  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
108
  }
109
+ // An agent has no Workspace account of its own. It acts as the owner of the
110
+ // pool it came through, which core copied from that pool's config, and the
111
+ // mapping, which answers for people, is never asked about it (decisions/0006).
112
+ if (caller.identity.agent !== undefined) {
113
+ if (caller.pool !== undefined && isWorkspaceAddress(caller.poolOwner))
114
+ return caller.poolOwner;
115
+ 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.`);
116
+ }
95
117
  let mapped;
96
118
  try {
97
119
  mapped = await subject(caller.identity, ctx.signal ? { signal: ctx.signal } : {});
@@ -104,7 +126,7 @@ async function subjectFor(provider, subject, ctx) {
104
126
  if (mapped === undefined) {
105
127
  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
128
  }
107
- if (typeof mapped !== "string" || !ADDRESS.test(mapped.trim())) {
129
+ if (typeof mapped !== "string" || !isWorkspaceAddress(mapped.trim())) {
108
130
  throw new ConnectorCallError("connector_call_failed", `The deployment's ${provider} subject mapping returned something that is not an email address.`, { retryable: false });
109
131
  }
110
132
  return mapped.trim();
@@ -311,7 +333,7 @@ export function googleWorkspaceClient(options) {
311
333
  * write Google accepted, or one that left with no answer, may have landed:
312
334
  * telling the agent to retry it invites a duplicate, so it is reported as
313
335
  * an uncertain outcome that is not retried — the same verdict core reaches
314
- * for a trusted-pool program write (`write_outcome_unknown`). A read is safe to
336
+ * for a program write (`write_outcome_unknown`). A read is safe to
315
337
  * repeat and keeps its retryable classification.
316
338
  */
317
339
  /**
@@ -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. */