@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.
- package/Dockerfile +1 -1
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +686 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +5 -3
- package/node_modules/@vellumai/service-contracts/src/grants.ts +184 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +4 -2
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +447 -4
- package/package.json +3 -2
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/command-executor.test.ts +1879 -0
- package/src/__tests__/command-validator.test.ts +1405 -0
- package/src/__tests__/command-workspace.test.ts +1050 -0
- package/src/__tests__/grant-store.test.ts +689 -0
- package/src/__tests__/http-executor.test.ts +1336 -0
- package/src/__tests__/http-policy.test.ts +1069 -0
- package/src/__tests__/local-materializers.test.ts +860 -0
- package/src/__tests__/local-standalone.test.ts +36 -5
- package/src/__tests__/local-token-refresh.test.ts +361 -0
- package/src/__tests__/manage-secure-command-tool.test.ts +134 -0
- package/src/__tests__/managed-integration.test.ts +91 -112
- package/src/__tests__/managed-lazy-getters.test.ts +359 -0
- package/src/__tests__/managed-materializers.test.ts +1028 -0
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/managed-rejection.test.ts +43 -0
- package/src/__tests__/toolstore.test.ts +773 -0
- package/src/__tests__/transport.test.ts +27 -23
- package/src/audit/store.ts +188 -0
- package/src/cli.ts +1 -1
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/egress-hooks.ts +203 -0
- package/src/commands/executor.ts +1155 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +286 -0
- package/src/commands/validator.ts +702 -0
- package/src/commands/workspace.ts +550 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +309 -0
- package/src/grants/rpc-handlers.ts +293 -0
- package/src/grants/temporary-store.ts +289 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +684 -0
- package/src/http/path-template.ts +245 -0
- package/src/http/policy.ts +238 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +88 -8
- package/src/main.ts +340 -228
- package/src/managed-errors.ts +9 -0
- package/src/managed-lazy-getters.ts +106 -0
- package/src/managed-main.ts +822 -0
- package/src/materializers/local-oauth-lookup.ts +98 -0
- package/src/materializers/local-token-refresh.ts +287 -0
- package/src/materializers/local.ts +316 -0
- package/src/materializers/managed-platform.ts +295 -0
- package/src/paths.ts +20 -4
- package/src/server.ts +469 -52
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +311 -0
- package/src/subjects/policy.ts +79 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +571 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +0 -104
- 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
|
|
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
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|