@vellumai/credential-executor 0.10.7-dev.202607102035.64f07ea → 0.10.7-staging.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 (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +2 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +686 -0
  5. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +5 -3
  6. package/node_modules/@vellumai/service-contracts/src/grants.ts +184 -0
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +4 -2
  8. package/node_modules/@vellumai/service-contracts/src/rendering.ts +135 -0
  9. package/node_modules/@vellumai/service-contracts/src/rpc.ts +447 -4
  10. package/package.json +3 -2
  11. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  12. package/src/__tests__/command-executor.test.ts +1879 -0
  13. package/src/__tests__/command-validator.test.ts +1405 -0
  14. package/src/__tests__/command-workspace.test.ts +1050 -0
  15. package/src/__tests__/grant-store.test.ts +689 -0
  16. package/src/__tests__/http-executor.test.ts +1336 -0
  17. package/src/__tests__/http-policy.test.ts +1069 -0
  18. package/src/__tests__/local-materializers.test.ts +860 -0
  19. package/src/__tests__/local-standalone.test.ts +36 -5
  20. package/src/__tests__/local-token-refresh.test.ts +361 -0
  21. package/src/__tests__/manage-secure-command-tool.test.ts +134 -0
  22. package/src/__tests__/managed-integration.test.ts +91 -112
  23. package/src/__tests__/managed-lazy-getters.test.ts +359 -0
  24. package/src/__tests__/managed-materializers.test.ts +1028 -0
  25. package/src/__tests__/managed-reconnect.test.ts +2 -2
  26. package/src/__tests__/managed-rejection.test.ts +43 -0
  27. package/src/__tests__/toolstore.test.ts +773 -0
  28. package/src/__tests__/transport.test.ts +27 -23
  29. package/src/audit/store.ts +188 -0
  30. package/src/cli.ts +1 -1
  31. package/src/commands/auth-adapters.ts +169 -0
  32. package/src/commands/egress-hooks.ts +203 -0
  33. package/src/commands/executor.ts +1155 -0
  34. package/src/commands/output-scan.ts +157 -0
  35. package/src/commands/profiles.ts +286 -0
  36. package/src/commands/validator.ts +702 -0
  37. package/src/commands/workspace.ts +550 -0
  38. package/src/grants/index.ts +17 -0
  39. package/src/grants/persistent-store.ts +309 -0
  40. package/src/grants/rpc-handlers.ts +293 -0
  41. package/src/grants/temporary-store.ts +289 -0
  42. package/src/http/audit.ts +84 -0
  43. package/src/http/executor.ts +684 -0
  44. package/src/http/path-template.ts +245 -0
  45. package/src/http/policy.ts +238 -0
  46. package/src/http/response-filter.ts +233 -0
  47. package/src/index.ts +88 -8
  48. package/src/main.ts +340 -228
  49. package/src/managed-errors.ts +9 -0
  50. package/src/managed-lazy-getters.ts +106 -0
  51. package/src/managed-main.ts +822 -0
  52. package/src/materializers/local-oauth-lookup.ts +98 -0
  53. package/src/materializers/local-token-refresh.ts +287 -0
  54. package/src/materializers/local.ts +316 -0
  55. package/src/materializers/managed-platform.ts +295 -0
  56. package/src/paths.ts +20 -4
  57. package/src/server.ts +469 -52
  58. package/src/subjects/local.ts +177 -0
  59. package/src/subjects/managed.ts +311 -0
  60. package/src/subjects/policy.ts +79 -0
  61. package/src/toolstore/integrity.ts +94 -0
  62. package/src/toolstore/manifest.ts +154 -0
  63. package/src/toolstore/publish.ts +571 -0
  64. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +0 -104
  65. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +0 -118
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Managed platform OAuth materializer.
3
+ *
4
+ * Materializes a `platform_oauth` handle into a short-lived access token
5
+ * by calling the platform's CES token-materialization endpoint. The
6
+ * materialized token is returned to the caller for immediate use (e.g.
7
+ * injection into an HTTP request or command environment) but is **never**
8
+ * persisted to any local storage — it exists only in memory for the
9
+ * duration of the execution.
10
+ *
11
+ * Security invariants:
12
+ * - Materialized tokens are never written to disk.
13
+ * - Materialized tokens are never logged (not even partially).
14
+ * - Platform errors are surfaced as structured errors without leaking secrets.
15
+ * - If the platform cannot be reached, materialization fails closed.
16
+ *
17
+ * The materializer expects a resolved `ManagedSubject` from
18
+ * `subjects/managed.ts`. It does not perform handle parsing or catalog
19
+ * lookup — that is the resolver's responsibility.
20
+ */
21
+
22
+ import type { ManagedSubject } from "../subjects/managed.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Materialization result
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /**
29
+ * Successful materialization result.
30
+ *
31
+ * The `accessToken` field contains the short-lived token obtained from
32
+ * the platform. Callers MUST NOT persist this value — it should be used
33
+ * immediately for request injection and then discarded.
34
+ */
35
+ export interface MaterializedToken {
36
+ /** The short-lived access token. */
37
+ accessToken: string;
38
+ /** Token type (typically "Bearer"). */
39
+ tokenType: string;
40
+ /** Epoch ms when the token expires (null if the platform didn't report expiry). */
41
+ expiresAt: number | null;
42
+ /** Provider key (mirrored from the subject for convenience). */
43
+ provider: string;
44
+ /** Connection ID (mirrored from the subject for convenience). */
45
+ connectionId: string;
46
+ }
47
+
48
+ export type MaterializeResult =
49
+ | { ok: true; token: MaterializedToken }
50
+ | { ok: false; error: MaterializationError };
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Materialization errors
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export class MaterializationError extends Error {
57
+ readonly code: string;
58
+
59
+ constructor(code: string, message: string) {
60
+ super(message);
61
+ this.name = "MaterializationError";
62
+ this.code = code;
63
+ }
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Platform token response shape
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Shape of the platform's CES token-materialization response.
72
+ *
73
+ * Field names match the platform's ManagedTokenMaterializeResponseSerializer:
74
+ * access_token, token_type, expires_at, provider, handle
75
+ *
76
+ * The platform issues a short-lived access token for the specified
77
+ * connection. The token is pre-authorized for the scopes granted on
78
+ * the connection.
79
+ */
80
+ interface PlatformTokenResponse {
81
+ access_token: string;
82
+ token_type?: string;
83
+ /** ISO-8601 datetime when the token expires (null if no expiry). */
84
+ expires_at?: string | null;
85
+ provider?: string;
86
+ handle?: string;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Materializer options
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export interface ManagedMaterializerOptions {
94
+ /**
95
+ * Platform base URL (without trailing slash).
96
+ */
97
+ platformBaseUrl: string;
98
+ /**
99
+ * Assistant API key for authenticating with the platform.
100
+ */
101
+ assistantApiKey: string;
102
+ /**
103
+ * Platform-assigned assistant UUID. Required for building the
104
+ * platform materialize URL: /v1/assistants/<id>/oauth/managed/materialize/
105
+ */
106
+ assistantId: string;
107
+ /**
108
+ * Optional custom fetch implementation (for testing).
109
+ */
110
+ fetch?: typeof globalThis.fetch;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Materializer implementation
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Materialize a managed OAuth subject into a short-lived access token
119
+ * by calling the platform's token-materialization endpoint.
120
+ *
121
+ * The endpoint is:
122
+ * POST {platformBaseUrl}/v1/assistants/{assistantId}/oauth/managed/materialize/
123
+ *
124
+ * The request body contains `{ connection_id: <uuid> }`.
125
+ *
126
+ * The platform validates the assistant API key, checks that the connection
127
+ * is active, and returns a fresh access token (refreshing upstream if
128
+ * needed).
129
+ *
130
+ * Fail-closed: any error results in a structured `MaterializationError`
131
+ * rather than a partial or fallback result.
132
+ */
133
+ export async function materializeManagedToken(
134
+ subject: ManagedSubject,
135
+ options: ManagedMaterializerOptions
136
+ ): Promise<MaterializeResult> {
137
+ // -- Validate prerequisites -----------------------------------------------
138
+ if (!options.platformBaseUrl) {
139
+ return {
140
+ ok: false,
141
+ error: new MaterializationError(
142
+ "MISSING_PLATFORM_URL",
143
+ "Platform base URL is required for managed token materialization"
144
+ ),
145
+ };
146
+ }
147
+
148
+ if (!options.assistantApiKey) {
149
+ return {
150
+ ok: false,
151
+ error: new MaterializationError(
152
+ "MISSING_API_KEY",
153
+ "Assistant API key is required for managed token materialization"
154
+ ),
155
+ };
156
+ }
157
+
158
+ if (!options.assistantId) {
159
+ return {
160
+ ok: false,
161
+ error: new MaterializationError(
162
+ "MISSING_ASSISTANT_ID",
163
+ "Assistant ID is required for managed token materialization"
164
+ ),
165
+ };
166
+ }
167
+
168
+ // -- Call platform token endpoint -----------------------------------------
169
+ const fetchFn = options.fetch ?? globalThis.fetch;
170
+ const materializeUrl = `${
171
+ options.platformBaseUrl
172
+ }/v1/assistants/${encodeURIComponent(
173
+ options.assistantId
174
+ )}/oauth/managed/materialize/`;
175
+
176
+ let response: Response;
177
+ try {
178
+ response = await fetchFn(materializeUrl, {
179
+ method: "POST",
180
+ headers: {
181
+ Authorization: `Api-Key ${options.assistantApiKey}`,
182
+ Accept: "application/json",
183
+ "Content-Type": "application/json",
184
+ },
185
+ body: JSON.stringify({ connection_id: subject.connectionId }),
186
+ });
187
+ } catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ return {
190
+ ok: false,
191
+ error: new MaterializationError(
192
+ "PLATFORM_UNREACHABLE",
193
+ `Failed to reach platform token endpoint: ${sanitizeError(message)}`
194
+ ),
195
+ };
196
+ }
197
+
198
+ // -- Handle error responses -----------------------------------------------
199
+ if (!response.ok) {
200
+ return {
201
+ ok: false,
202
+ error: mapPlatformError(response.status, subject.connectionId),
203
+ };
204
+ }
205
+
206
+ // -- Parse token response -------------------------------------------------
207
+ let body: PlatformTokenResponse;
208
+ try {
209
+ body = (await response.json()) as PlatformTokenResponse;
210
+ } catch {
211
+ return {
212
+ ok: false,
213
+ error: new MaterializationError(
214
+ "INVALID_TOKEN_RESPONSE",
215
+ "Platform token endpoint returned invalid JSON"
216
+ ),
217
+ };
218
+ }
219
+
220
+ if (!body.access_token || typeof body.access_token !== "string") {
221
+ return {
222
+ ok: false,
223
+ error: new MaterializationError(
224
+ "INVALID_TOKEN_RESPONSE",
225
+ "Platform token response missing access_token"
226
+ ),
227
+ };
228
+ }
229
+
230
+ // -- Build materialized token ---------------------------------------------
231
+ const expiresAt = parseExpiresAt(body.expires_at);
232
+
233
+ const token: MaterializedToken = {
234
+ accessToken: body.access_token,
235
+ tokenType: body.token_type ?? "Bearer",
236
+ expiresAt,
237
+ provider: subject.provider,
238
+ connectionId: subject.connectionId,
239
+ };
240
+
241
+ return { ok: true, token };
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Helpers
246
+ // ---------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Map a platform HTTP error status to a structured MaterializationError.
250
+ */
251
+ function mapPlatformError(
252
+ status: number,
253
+ connectionId: string
254
+ ): MaterializationError {
255
+ switch (status) {
256
+ case 401:
257
+ return new MaterializationError(
258
+ "PLATFORM_AUTH_FAILED",
259
+ "Assistant API key is invalid or expired (HTTP 401)"
260
+ );
261
+ case 403:
262
+ return new MaterializationError(
263
+ "PLATFORM_FORBIDDEN",
264
+ "Assistant is not authorized to materialize this connection (HTTP 403)"
265
+ );
266
+ case 404:
267
+ return new MaterializationError(
268
+ "CONNECTION_NOT_FOUND",
269
+ `Connection ${connectionId} not found on the platform (HTTP 404)`
270
+ );
271
+ default:
272
+ return new MaterializationError(
273
+ `PLATFORM_HTTP_${status}`,
274
+ `Platform token endpoint returned HTTP ${status}`
275
+ );
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Parse an ISO-8601 `expires_at` datetime string into epoch milliseconds.
281
+ * Returns null if the value is missing or invalid.
282
+ */
283
+ function parseExpiresAt(expiresAt: string | null | undefined): number | null {
284
+ if (expiresAt == null) return null;
285
+ const ts = new Date(expiresAt).getTime();
286
+ if (Number.isNaN(ts)) return null;
287
+ return ts;
288
+ }
289
+
290
+ /**
291
+ * Sanitize error messages to avoid leaking secrets.
292
+ */
293
+ function sanitizeError(message: string): string {
294
+ return message.replace(/Api-Key\s+\S+/gi, "Api-Key [REDACTED]");
295
+ }
package/src/paths.ts CHANGED
@@ -84,6 +84,21 @@ export function getCesDataRoot(mode?: CesMode): string {
84
84
  // Subdirectory layout
85
85
  // ---------------------------------------------------------------------------
86
86
 
87
+ /** Directory for CES grant persistence. */
88
+ export function getCesGrantsDir(mode?: CesMode): string {
89
+ return join(getCesDataRoot(mode), "grants");
90
+ }
91
+
92
+ /** Directory for CES audit log persistence. */
93
+ export function getCesAuditDir(mode?: CesMode): string {
94
+ return join(getCesDataRoot(mode), "audit");
95
+ }
96
+
97
+ /** Directory for CES secure tool store (registered secure command tools). */
98
+ export function getCesToolStoreDir(mode?: CesMode): string {
99
+ return join(getCesDataRoot(mode), "toolstore");
100
+ }
101
+
87
102
  /** Directory for CES log files. */
88
103
  export function getCesLogDir(mode?: CesMode): string {
89
104
  return join(getCesDataRoot(mode), "logs");
@@ -126,7 +141,7 @@ export function getBootstrapSocketPath(): string {
126
141
  }
127
142
 
128
143
  // ---------------------------------------------------------------------------
129
- // Local-mode CES socket
144
+ // Local-mode standalone socket (temporary — CES_STANDALONE)
130
145
  // ---------------------------------------------------------------------------
131
146
 
132
147
  /** Default local-mode CES socket filename (under the local data root). */
@@ -135,9 +150,10 @@ const LOCAL_SOCKET_NAME = "ces.sock";
135
150
  /**
136
151
  * Return the path to the local-mode CES Unix socket.
137
152
  *
138
- * Used when local CES runs as a CLI-launched sibling process. The socket
139
- * lives under the CES-private local data root, whose directory permissions
140
- * are the access boundary.
153
+ * Used when local CES runs as a standalone sibling (`CES_STANDALONE=1`, the
154
+ * CLI-launched opt-in) rather than as the assistant's stdio
155
+ * child. The socket lives under the CES-private local data root, whose
156
+ * directory permissions are the access boundary.
141
157
  *
142
158
  * Priority:
143
159
  * 1. `CES_LOCAL_SOCKET` env var (full file path override; the CLI sets this