@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895
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 +1 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
- package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
- package/package.json +2 -3
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/local-standalone.test.ts +5 -36
- package/src/__tests__/managed-integration.test.ts +112 -91
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/transport.test.ts +23 -27
- package/src/cli.ts +1 -1
- package/src/index.ts +8 -88
- package/src/main.ts +228 -340
- package/src/paths.ts +4 -20
- package/src/server.ts +52 -469
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
- package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
- package/src/__tests__/command-executor.test.ts +0 -1879
- package/src/__tests__/command-validator.test.ts +0 -1405
- package/src/__tests__/command-workspace.test.ts +0 -1050
- package/src/__tests__/grant-store.test.ts +0 -689
- package/src/__tests__/http-executor.test.ts +0 -1336
- package/src/__tests__/http-policy.test.ts +0 -1069
- package/src/__tests__/local-materializers.test.ts +0 -860
- package/src/__tests__/local-token-refresh.test.ts +0 -361
- package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
- package/src/__tests__/managed-lazy-getters.test.ts +0 -359
- package/src/__tests__/managed-materializers.test.ts +0 -1028
- package/src/__tests__/managed-rejection.test.ts +0 -43
- package/src/__tests__/toolstore.test.ts +0 -773
- package/src/audit/store.ts +0 -188
- package/src/commands/auth-adapters.ts +0 -169
- package/src/commands/egress-hooks.ts +0 -203
- package/src/commands/executor.ts +0 -1155
- package/src/commands/output-scan.ts +0 -157
- package/src/commands/profiles.ts +0 -286
- package/src/commands/validator.ts +0 -702
- package/src/commands/workspace.ts +0 -550
- package/src/grants/index.ts +0 -17
- package/src/grants/persistent-store.ts +0 -309
- package/src/grants/rpc-handlers.ts +0 -293
- package/src/grants/temporary-store.ts +0 -289
- package/src/http/audit.ts +0 -84
- package/src/http/executor.ts +0 -684
- package/src/http/path-template.ts +0 -245
- package/src/http/policy.ts +0 -238
- package/src/http/response-filter.ts +0 -233
- package/src/managed-errors.ts +0 -9
- package/src/managed-lazy-getters.ts +0 -106
- package/src/managed-main.ts +0 -822
- package/src/materializers/local-oauth-lookup.ts +0 -98
- package/src/materializers/local-token-refresh.ts +0 -287
- package/src/materializers/local.ts +0 -316
- package/src/materializers/managed-platform.ts +0 -295
- package/src/subjects/local.ts +0 -177
- package/src/subjects/managed.ts +0 -311
- package/src/subjects/policy.ts +0 -79
- package/src/toolstore/integrity.ts +0 -94
- package/src/toolstore/manifest.ts +0 -154
- package/src/toolstore/publish.ts +0 -571
package/src/toolstore/publish.ts
DELETED
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Immutable toolstore publisher.
|
|
3
|
-
*
|
|
4
|
-
* Downloads (or re-fetches) secure command bundles inside CES, verifies
|
|
5
|
-
* their digest against the declared expected value, and writes them into
|
|
6
|
-
* content-addressed directories within the CES-private data root.
|
|
7
|
-
*
|
|
8
|
-
* ## Invariants
|
|
9
|
-
*
|
|
10
|
-
* 1. **CES-only writes** — All bundle content is written to the CES
|
|
11
|
-
* toolstore directory, which lives under the CES-private data root.
|
|
12
|
-
* The assistant process cannot read or write this path.
|
|
13
|
-
*
|
|
14
|
-
* 2. **Immutable publications** — Once a digest directory exists, it is
|
|
15
|
-
* never overwritten. Re-publishing the same digest is a deduplicated
|
|
16
|
-
* no-op that returns success.
|
|
17
|
-
*
|
|
18
|
-
* 3. **Digest verification before write** — Downloaded bytes are verified
|
|
19
|
-
* against the expected digest before any file is created. A mismatch
|
|
20
|
-
* is a hard error; no partial writes occur.
|
|
21
|
-
*
|
|
22
|
-
* 4. **No credential grants** — Publishing a bundle into the toolstore
|
|
23
|
-
* is a pure content operation. It does not create, modify, or imply
|
|
24
|
-
* any credential-use grant.
|
|
25
|
-
*
|
|
26
|
-
* 5. **No workspace-origin bundles** — Source URLs that point to the
|
|
27
|
-
* workspace directory or use non-HTTPS schemes are rejected.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
31
|
-
import { join, resolve } from "node:path";
|
|
32
|
-
|
|
33
|
-
import { getCesToolStoreDir, type CesMode } from "../paths.js";
|
|
34
|
-
import type { SecureCommandManifest } from "../commands/profiles.js";
|
|
35
|
-
import { validateManifest } from "../commands/validator.js";
|
|
36
|
-
import { verifyDigest } from "./integrity.js";
|
|
37
|
-
import {
|
|
38
|
-
isValidSha256Hex,
|
|
39
|
-
isWorkspaceOriginPath,
|
|
40
|
-
validateSourceUrl,
|
|
41
|
-
type ToolstoreManifest,
|
|
42
|
-
} from "./manifest.js";
|
|
43
|
-
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
// Publication result
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
export interface PublishResult {
|
|
49
|
-
/** Whether the publication succeeded. */
|
|
50
|
-
success: boolean;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Whether this was a deduplicated no-op (the digest directory already
|
|
54
|
-
* existed from a previous publication).
|
|
55
|
-
*/
|
|
56
|
-
deduplicated: boolean;
|
|
57
|
-
|
|
58
|
-
/** The content-addressed directory path where the bundle is stored. */
|
|
59
|
-
bundlePath: string;
|
|
60
|
-
|
|
61
|
-
/** Error message if publication failed (undefined on success). */
|
|
62
|
-
error?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Publish request
|
|
67
|
-
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
69
|
-
export interface PublishRequest {
|
|
70
|
-
/** Raw bundle bytes to publish. */
|
|
71
|
-
bundleBytes: Buffer | Uint8Array;
|
|
72
|
-
|
|
73
|
-
/** Expected SHA-256 hex digest of the bundle bytes. */
|
|
74
|
-
expectedDigest: string;
|
|
75
|
-
|
|
76
|
-
/** Unique identifier for the command bundle. */
|
|
77
|
-
bundleId: string;
|
|
78
|
-
|
|
79
|
-
/** Semantic version of the bundle. */
|
|
80
|
-
version: string;
|
|
81
|
-
|
|
82
|
-
/** HTTPS URL from which the bundle was fetched. */
|
|
83
|
-
sourceUrl: string;
|
|
84
|
-
|
|
85
|
-
/** The secure command manifest for this bundle. */
|
|
86
|
-
secureCommandManifest: SecureCommandManifest;
|
|
87
|
-
|
|
88
|
-
/** CES mode override (defaults to auto-detection). */
|
|
89
|
-
cesMode?: CesMode;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// Content-addressed path helpers
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
/** Manifest filename within a content-addressed bundle directory. */
|
|
97
|
-
const MANIFEST_FILENAME = "toolstore-manifest.json";
|
|
98
|
-
|
|
99
|
-
/** Bundle content filename within a content-addressed bundle directory. */
|
|
100
|
-
const BUNDLE_FILENAME = "bundle.bin";
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Return the content-addressed directory path for a given digest.
|
|
104
|
-
*
|
|
105
|
-
* Layout: `<toolstoreDir>/<digest>/`
|
|
106
|
-
*
|
|
107
|
-
* @throws {Error} if digest is not a valid SHA-256 hex string.
|
|
108
|
-
*/
|
|
109
|
-
export function getBundleDir(
|
|
110
|
-
toolstoreDir: string,
|
|
111
|
-
digest: string,
|
|
112
|
-
): string {
|
|
113
|
-
if (!isValidSha256Hex(digest)) {
|
|
114
|
-
throw new Error(`Invalid digest "${digest}": must be a 64-character lowercase hex SHA-256 digest.`);
|
|
115
|
-
}
|
|
116
|
-
return join(toolstoreDir, digest);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Return the manifest file path for a given digest.
|
|
121
|
-
*
|
|
122
|
-
* @throws {Error} if digest is not a valid SHA-256 hex string.
|
|
123
|
-
*/
|
|
124
|
-
export function getBundleManifestPath(
|
|
125
|
-
toolstoreDir: string,
|
|
126
|
-
digest: string,
|
|
127
|
-
): string {
|
|
128
|
-
return join(getBundleDir(toolstoreDir, digest), MANIFEST_FILENAME);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Return the bundle content file path for a given digest.
|
|
133
|
-
*
|
|
134
|
-
* @throws {Error} if digest is not a valid SHA-256 hex string.
|
|
135
|
-
*/
|
|
136
|
-
export function getBundleContentPath(
|
|
137
|
-
toolstoreDir: string,
|
|
138
|
-
digest: string,
|
|
139
|
-
): string {
|
|
140
|
-
return join(getBundleDir(toolstoreDir, digest), BUNDLE_FILENAME);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// Publisher
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Publish a secure command bundle into the CES-private immutable
|
|
149
|
-
* toolstore.
|
|
150
|
-
*
|
|
151
|
-
* This function:
|
|
152
|
-
* 1. Validates the source URL (HTTPS only, no workspace origins).
|
|
153
|
-
* 2. Validates the expected digest format.
|
|
154
|
-
* 3. Validates the secure command manifest.
|
|
155
|
-
* 4. Verifies the bundle bytes match the expected digest.
|
|
156
|
-
* 5. Checks for deduplication (returns early if already published).
|
|
157
|
-
* 6. Writes bundle contents and manifest atomically.
|
|
158
|
-
*
|
|
159
|
-
* Returns a {@link PublishResult} describing the outcome.
|
|
160
|
-
*/
|
|
161
|
-
export function publishBundle(request: PublishRequest): PublishResult {
|
|
162
|
-
const {
|
|
163
|
-
bundleBytes,
|
|
164
|
-
expectedDigest,
|
|
165
|
-
bundleId,
|
|
166
|
-
version,
|
|
167
|
-
sourceUrl,
|
|
168
|
-
secureCommandManifest,
|
|
169
|
-
cesMode,
|
|
170
|
-
} = request;
|
|
171
|
-
|
|
172
|
-
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
173
|
-
|
|
174
|
-
// -- Validate source URL ------------------------------------------------
|
|
175
|
-
const urlError = validateSourceUrl(sourceUrl);
|
|
176
|
-
if (urlError) {
|
|
177
|
-
return {
|
|
178
|
-
success: false,
|
|
179
|
-
deduplicated: false,
|
|
180
|
-
bundlePath: "",
|
|
181
|
-
error: `Invalid source URL: ${urlError}`,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// -- Reject workspace-origin paths in the URL ---------------------------
|
|
186
|
-
try {
|
|
187
|
-
const parsedUrl = new URL(sourceUrl);
|
|
188
|
-
const rawPathname = parsedUrl.pathname;
|
|
189
|
-
|
|
190
|
-
// Always check the raw pathname — this must not be skipped.
|
|
191
|
-
if (isWorkspaceOriginPath(rawPathname)) {
|
|
192
|
-
return {
|
|
193
|
-
success: false,
|
|
194
|
-
deduplicated: false,
|
|
195
|
-
bundlePath: "",
|
|
196
|
-
error: `Source URL path "${rawPathname}" appears to be a workspace-origin path. ` +
|
|
197
|
-
`Workspace-origin binaries are never publishable.`,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Also check the decoded pathname for percent-encoded traversals.
|
|
202
|
-
try {
|
|
203
|
-
const decodedPathname = decodeURIComponent(rawPathname);
|
|
204
|
-
if (decodedPathname !== rawPathname && isWorkspaceOriginPath(decodedPathname)) {
|
|
205
|
-
return {
|
|
206
|
-
success: false,
|
|
207
|
-
deduplicated: false,
|
|
208
|
-
bundlePath: "",
|
|
209
|
-
error: `Source URL path "${decodedPathname}" (decoded from "${rawPathname}") appears to be a workspace-origin path. ` +
|
|
210
|
-
`Workspace-origin binaries are never publishable.`,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
} catch {
|
|
214
|
-
// decodeURIComponent threw URIError on malformed sequences (e.g. %C0%AF).
|
|
215
|
-
// The raw pathname was already checked above, so we're safe.
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
// URL parsing already validated above
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// -- Validate digest format ---------------------------------------------
|
|
222
|
-
if (!isValidSha256Hex(expectedDigest)) {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
deduplicated: false,
|
|
226
|
-
bundlePath: "",
|
|
227
|
-
error: `Invalid expectedDigest "${expectedDigest}". ` +
|
|
228
|
-
`Must be a 64-character lowercase hex SHA-256 digest.`,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// -- Validate secure command manifest -----------------------------------
|
|
233
|
-
const manifestValidation = validateManifest(secureCommandManifest);
|
|
234
|
-
if (!manifestValidation.valid) {
|
|
235
|
-
return {
|
|
236
|
-
success: false,
|
|
237
|
-
deduplicated: false,
|
|
238
|
-
bundlePath: "",
|
|
239
|
-
error: `Invalid secure command manifest: ${manifestValidation.errors.join("; ")}`,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// -- Validate manifest metadata matches request -------------------------
|
|
244
|
-
const metadataMismatches: string[] = [];
|
|
245
|
-
if (secureCommandManifest.bundleDigest !== expectedDigest) {
|
|
246
|
-
metadataMismatches.push(
|
|
247
|
-
`bundleDigest "${secureCommandManifest.bundleDigest}" does not match expectedDigest "${expectedDigest}"`,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
if (secureCommandManifest.bundleId !== bundleId) {
|
|
251
|
-
metadataMismatches.push(
|
|
252
|
-
`bundleId "${secureCommandManifest.bundleId}" does not match request bundleId "${bundleId}"`,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
if (secureCommandManifest.version !== version) {
|
|
256
|
-
metadataMismatches.push(
|
|
257
|
-
`version "${secureCommandManifest.version}" does not match request version "${version}"`,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
if (metadataMismatches.length > 0) {
|
|
261
|
-
return {
|
|
262
|
-
success: false,
|
|
263
|
-
deduplicated: false,
|
|
264
|
-
bundlePath: "",
|
|
265
|
-
error: `Manifest metadata mismatch: ${metadataMismatches.join("; ")}`,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// -- Verify bundle digest -----------------------------------------------
|
|
270
|
-
const digestResult = verifyDigest(bundleBytes, expectedDigest);
|
|
271
|
-
if (!digestResult.valid) {
|
|
272
|
-
return {
|
|
273
|
-
success: false,
|
|
274
|
-
deduplicated: false,
|
|
275
|
-
bundlePath: "",
|
|
276
|
-
error: digestResult.error!,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// -- Deduplication check ------------------------------------------------
|
|
281
|
-
const bundleDir = getBundleDir(toolstoreDir, expectedDigest);
|
|
282
|
-
const manifestPath = getBundleManifestPath(toolstoreDir, expectedDigest);
|
|
283
|
-
|
|
284
|
-
if (existsSync(bundleDir) && existsSync(manifestPath)) {
|
|
285
|
-
// Already published — deduplicated no-op
|
|
286
|
-
return {
|
|
287
|
-
success: true,
|
|
288
|
-
deduplicated: true,
|
|
289
|
-
bundlePath: bundleDir,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// -- Ensure toolstore directory exists -----------------------------------
|
|
294
|
-
mkdirSync(toolstoreDir, { recursive: true });
|
|
295
|
-
|
|
296
|
-
// -- Write bundle atomically --------------------------------------------
|
|
297
|
-
//
|
|
298
|
-
// Write to a staging directory first, then rename to the final
|
|
299
|
-
// content-addressed path. This prevents partial writes from being
|
|
300
|
-
// visible to readers.
|
|
301
|
-
const stagingDir = join(toolstoreDir, `.staging-${expectedDigest}-${Date.now()}`);
|
|
302
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
// Write bundle content to a unique staging filename so that if the
|
|
306
|
-
// archive itself contains a root-level "bundle.bin", extraction won't
|
|
307
|
-
// collide with the staging archive file.
|
|
308
|
-
const stagingArchiveName = `.ces-bundle-staging-${Date.now()}.tar.gz`;
|
|
309
|
-
const stagingBundlePath = join(stagingDir, stagingArchiveName);
|
|
310
|
-
writeFileSync(stagingBundlePath, bundleBytes, { mode: 0o444 });
|
|
311
|
-
|
|
312
|
-
// Extract the tar.gz archive into the staging directory.
|
|
313
|
-
// The archive is expected to contain the runnable bundle contents,
|
|
314
|
-
// including the entrypoint declared in the manifest.
|
|
315
|
-
const extractProc = Bun.spawnSync(
|
|
316
|
-
["tar", "xzf", stagingBundlePath, "-C", stagingDir],
|
|
317
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
318
|
-
);
|
|
319
|
-
if (extractProc.exitCode !== 0) {
|
|
320
|
-
const stderr = extractProc.stderr
|
|
321
|
-
? new TextDecoder().decode(extractProc.stderr).trim()
|
|
322
|
-
: "unknown error";
|
|
323
|
-
throw new Error(
|
|
324
|
-
`Bundle extraction failed (exit code ${extractProc.exitCode}): ${stderr}`,
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Remove the raw archive — the extracted contents replace it
|
|
329
|
-
try {
|
|
330
|
-
unlinkSync(stagingBundlePath);
|
|
331
|
-
} catch {
|
|
332
|
-
// Best effort — the file may have been overwritten by extraction
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Scan for symlinks that escape the bundle directory.
|
|
336
|
-
// A symlink pointing outside the staging directory could allow the
|
|
337
|
-
// entrypoint to execute arbitrary binaries on the host.
|
|
338
|
-
const escapingSymlinks = findEscapingSymlinks(stagingDir);
|
|
339
|
-
if (escapingSymlinks.length > 0) {
|
|
340
|
-
throw new Error(
|
|
341
|
-
`Bundle contains symlinks that point outside the bundle directory: ${escapingSymlinks.join(", ")}. ` +
|
|
342
|
-
`Symlink escape is not allowed.`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Validate that the declared entrypoint resolves inside the staging
|
|
347
|
-
// directory. A manifest with an absolute or `../`-traversal entrypoint
|
|
348
|
-
// could make chmod (or later execution) touch files outside the bundle.
|
|
349
|
-
const realStagingDir = realpathSync(stagingDir);
|
|
350
|
-
const extractedEntrypoint = resolve(realStagingDir, secureCommandManifest.entrypoint);
|
|
351
|
-
if (extractedEntrypoint !== realStagingDir && !extractedEntrypoint.startsWith(realStagingDir + "/")) {
|
|
352
|
-
throw new Error(
|
|
353
|
-
`Entrypoint "${secureCommandManifest.entrypoint}" resolves outside the bundle directory. ` +
|
|
354
|
-
`Path traversal in entrypoint values is not allowed.`,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Check that the entrypoint itself is not a symlink (use lstat to avoid following)
|
|
359
|
-
try {
|
|
360
|
-
const entrypointStat = lstatSync(extractedEntrypoint);
|
|
361
|
-
if (entrypointStat.isSymbolicLink()) {
|
|
362
|
-
throw new Error(
|
|
363
|
-
`Entrypoint "${secureCommandManifest.entrypoint}" is a symbolic link. ` +
|
|
364
|
-
`Symlink entrypoints are not allowed — the entrypoint must be a regular file.`,
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
} catch (err) {
|
|
368
|
-
if (err instanceof Error && err.message.includes("Symlink entrypoints")) {
|
|
369
|
-
throw err;
|
|
370
|
-
}
|
|
371
|
-
// lstat failed — the file doesn't exist, fall through to existsSync check
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (!existsSync(extractedEntrypoint)) {
|
|
375
|
-
throw new Error(
|
|
376
|
-
`Entrypoint "${secureCommandManifest.entrypoint}" not found in extracted bundle contents. ` +
|
|
377
|
-
`The archive must contain the declared entrypoint path.`,
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Make the entrypoint executable
|
|
382
|
-
chmodSync(extractedEntrypoint, 0o555);
|
|
383
|
-
|
|
384
|
-
// Build and write toolstore manifest
|
|
385
|
-
const toolstoreManifest: ToolstoreManifest = {
|
|
386
|
-
digest: expectedDigest,
|
|
387
|
-
bundleId,
|
|
388
|
-
version,
|
|
389
|
-
origin: {
|
|
390
|
-
sourceUrl,
|
|
391
|
-
fetchedAt: new Date().toISOString(),
|
|
392
|
-
},
|
|
393
|
-
declaredProfiles: Object.keys(secureCommandManifest.commandProfiles),
|
|
394
|
-
secureCommandManifest,
|
|
395
|
-
publishedAt: new Date().toISOString(),
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const stagingManifestPath = join(stagingDir, MANIFEST_FILENAME);
|
|
399
|
-
writeFileSync(
|
|
400
|
-
stagingManifestPath,
|
|
401
|
-
JSON.stringify(toolstoreManifest, null, 2) + "\n",
|
|
402
|
-
{ mode: 0o444 },
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
// Rename staging directory to final content-addressed path
|
|
406
|
-
//
|
|
407
|
-
// On POSIX, rename() is atomic within a filesystem. Since the
|
|
408
|
-
// staging dir is in the same parent as the final dir, this is
|
|
409
|
-
// a same-filesystem rename.
|
|
410
|
-
renameSync(stagingDir, bundleDir);
|
|
411
|
-
} catch (err) {
|
|
412
|
-
// Clean up staging directory on failure
|
|
413
|
-
try {
|
|
414
|
-
rmSync(stagingDir, { recursive: true, force: true });
|
|
415
|
-
} catch {
|
|
416
|
-
// Best effort cleanup
|
|
417
|
-
}
|
|
418
|
-
// Re-check: another process may have published the same digest concurrently
|
|
419
|
-
if (existsSync(bundleDir) && existsSync(manifestPath)) {
|
|
420
|
-
return {
|
|
421
|
-
success: true,
|
|
422
|
-
deduplicated: true,
|
|
423
|
-
bundlePath: bundleDir,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
success: false,
|
|
428
|
-
deduplicated: false,
|
|
429
|
-
bundlePath: "",
|
|
430
|
-
error: `Failed to write bundle to toolstore: ${err instanceof Error ? err.message : String(err)}`,
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
success: true,
|
|
436
|
-
deduplicated: false,
|
|
437
|
-
bundlePath: bundleDir,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// ---------------------------------------------------------------------------
|
|
442
|
-
// Reader helpers
|
|
443
|
-
// ---------------------------------------------------------------------------
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Read a published toolstore manifest by digest.
|
|
447
|
-
*
|
|
448
|
-
* Returns null if no bundle with the given digest is published or if the
|
|
449
|
-
* digest is not a valid SHA-256 hex string.
|
|
450
|
-
*/
|
|
451
|
-
export function readPublishedManifest(
|
|
452
|
-
digest: string,
|
|
453
|
-
cesMode?: CesMode,
|
|
454
|
-
): ToolstoreManifest | null {
|
|
455
|
-
if (!isValidSha256Hex(digest)) {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
460
|
-
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
461
|
-
|
|
462
|
-
if (!existsSync(manifestPath)) {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
try {
|
|
467
|
-
const raw = readFileSync(manifestPath, "utf-8");
|
|
468
|
-
return JSON.parse(raw) as ToolstoreManifest;
|
|
469
|
-
} catch {
|
|
470
|
-
return null;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Delete a published bundle from the toolstore by digest.
|
|
476
|
-
*
|
|
477
|
-
* Removes the entire content-addressed directory for the given digest.
|
|
478
|
-
* Returns true if the directory existed and was deleted, false if it
|
|
479
|
-
* did not exist (or the digest was invalid).
|
|
480
|
-
*
|
|
481
|
-
* This is used during tool unregistration to ensure that a previously
|
|
482
|
-
* published bundle cannot be executed after the tool is removed from
|
|
483
|
-
* the in-memory registry.
|
|
484
|
-
*/
|
|
485
|
-
export function deleteBundleFromToolstore(
|
|
486
|
-
digest: string,
|
|
487
|
-
cesMode?: CesMode,
|
|
488
|
-
): boolean {
|
|
489
|
-
if (!isValidSha256Hex(digest)) {
|
|
490
|
-
return false;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
494
|
-
const bundleDir = getBundleDir(toolstoreDir, digest);
|
|
495
|
-
|
|
496
|
-
if (!existsSync(bundleDir)) {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
rmSync(bundleDir, { recursive: true, force: true });
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Check if a bundle with the given digest is published in the toolstore.
|
|
506
|
-
*
|
|
507
|
-
* Returns false if the digest is not a valid SHA-256 hex string.
|
|
508
|
-
*/
|
|
509
|
-
export function isBundlePublished(
|
|
510
|
-
digest: string,
|
|
511
|
-
cesMode?: CesMode,
|
|
512
|
-
): boolean {
|
|
513
|
-
if (!isValidSha256Hex(digest)) {
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const toolstoreDir = getCesToolStoreDir(cesMode);
|
|
518
|
-
const bundleDir = getBundleDir(toolstoreDir, digest);
|
|
519
|
-
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
520
|
-
return existsSync(bundleDir) && existsSync(manifestPath);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ---------------------------------------------------------------------------
|
|
524
|
-
// Symlink escape detection
|
|
525
|
-
// ---------------------------------------------------------------------------
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Recursively scan a directory for symlinks that point outside of it.
|
|
529
|
-
*
|
|
530
|
-
* Returns an array of relative paths (from `rootDir`) of symlinks whose
|
|
531
|
-
* resolved target falls outside `rootDir`.
|
|
532
|
-
*/
|
|
533
|
-
function findEscapingSymlinks(rootDir: string): string[] {
|
|
534
|
-
const escaping: string[] = [];
|
|
535
|
-
const realRoot = realpathSync(rootDir);
|
|
536
|
-
|
|
537
|
-
function walk(dir: string): void {
|
|
538
|
-
let entries: string[];
|
|
539
|
-
try {
|
|
540
|
-
entries = readdirSync(dir);
|
|
541
|
-
} catch {
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
for (const entry of entries) {
|
|
546
|
-
const fullPath = join(dir, entry);
|
|
547
|
-
let stat;
|
|
548
|
-
try {
|
|
549
|
-
stat = lstatSync(fullPath);
|
|
550
|
-
} catch {
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (stat.isSymbolicLink()) {
|
|
555
|
-
// Resolve the symlink target relative to its parent directory
|
|
556
|
-
const linkTarget = readlinkSync(fullPath);
|
|
557
|
-
const resolvedTarget = resolve(dir, linkTarget);
|
|
558
|
-
// Check if the resolved target is outside the root directory
|
|
559
|
-
if (!resolvedTarget.startsWith(realRoot + "/") && resolvedTarget !== realRoot) {
|
|
560
|
-
const relativePath = fullPath.slice(realRoot.length + 1);
|
|
561
|
-
escaping.push(relativePath);
|
|
562
|
-
}
|
|
563
|
-
} else if (stat.isDirectory()) {
|
|
564
|
-
walk(fullPath);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
walk(realRoot);
|
|
570
|
-
return escaping;
|
|
571
|
-
}
|