@vellumai/credential-executor 0.6.6 → 0.7.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 (61) hide show
  1. package/Dockerfile +1 -1
  2. package/bun.lock +15 -7
  3. package/node_modules/@vellumai/credential-storage/src/__tests__/package-boundary.test.ts +32 -6
  4. package/node_modules/@vellumai/egress-proxy/src/__tests__/package-boundary.test.ts +32 -1
  5. package/node_modules/@vellumai/{ces-contracts → service-contracts}/bun.lock +1 -1
  6. package/node_modules/@vellumai/{ces-contracts → service-contracts}/package.json +4 -2
  7. package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/contracts.test.ts +5 -1
  8. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +155 -0
  9. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +23 -0
  10. package/node_modules/@vellumai/service-contracts/src/index.ts +25 -0
  11. package/node_modules/@vellumai/{ces-contracts/src/index.ts → service-contracts/src/transport.ts} +6 -28
  12. package/node_modules/@vellumai/service-contracts/src/trust-rules.ts +116 -0
  13. package/package.json +5 -4
  14. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  15. package/src/__tests__/ces-migrations-002-api-keys.test.ts +185 -0
  16. package/src/__tests__/ces-migrations-runner.test.ts +227 -0
  17. package/src/__tests__/cli.test.ts +139 -0
  18. package/src/__tests__/command-executor.test.ts +71 -42
  19. package/src/__tests__/http-executor.test.ts +1 -1
  20. package/src/__tests__/local-materializers.test.ts +1 -1
  21. package/src/__tests__/local-token-refresh.test.ts +65 -38
  22. package/src/__tests__/managed-integration.test.ts +1 -1
  23. package/src/__tests__/managed-lazy-getters.test.ts +1 -1
  24. package/src/__tests__/managed-materializers.test.ts +1 -1
  25. package/src/__tests__/managed-rejection.test.ts +1 -1
  26. package/src/__tests__/toolstore.test.ts +65 -20
  27. package/src/__tests__/transport.test.ts +13 -4
  28. package/src/audit/store.ts +2 -2
  29. package/src/cli.ts +158 -0
  30. package/src/commands/executor.ts +2 -2
  31. package/src/grants/rpc-handlers.ts +1 -1
  32. package/src/http/__tests__/credential-routes-normalization.test.ts +202 -0
  33. package/src/http/audit.ts +1 -1
  34. package/src/http/credential-routes.ts +53 -7
  35. package/src/http/executor.ts +2 -2
  36. package/src/http/policy.ts +1 -1
  37. package/src/main.ts +120 -50
  38. package/src/managed-errors.ts +2 -2
  39. package/src/managed-lazy-getters.ts +4 -4
  40. package/src/managed-main.ts +9 -3
  41. package/src/materializers/local-oauth-lookup.ts +7 -6
  42. package/src/materializers/local-token-refresh.ts +25 -15
  43. package/src/materializers/local.ts +1 -1
  44. package/src/migrations/001-no-op.ts +19 -0
  45. package/src/migrations/002-api-keys-to-credentials.ts +60 -0
  46. package/src/migrations/registry.ts +15 -0
  47. package/src/migrations/runner.ts +146 -0
  48. package/src/migrations/types.ts +54 -0
  49. package/src/paths.ts +15 -11
  50. package/src/server.ts +2 -2
  51. package/src/subjects/local.ts +2 -2
  52. package/src/subjects/managed.ts +1 -1
  53. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +0 -471
  54. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +0 -436
  55. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/grants.test.ts +0 -0
  56. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/error.ts +0 -0
  57. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/grants.ts +0 -0
  58. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/handles.ts +0 -0
  59. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rendering.ts +0 -0
  60. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rpc.ts +0 -0
  61. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/tsconfig.json +0 -0
@@ -0,0 +1,60 @@
1
+ import type { CesMigration } from "./types.js";
2
+
3
+ /**
4
+ * Providers whose bare API-key entries (e.g. `anthropic`) must be moved to
5
+ * the canonical `credential/{provider}/api_key` namespace.
6
+ *
7
+ * Note: `elevenlabs` is intentionally omitted — it was already migrated by
8
+ * `migrateElevenLabsToCredential()` in the Swift layer before CES migrations
9
+ * were introduced.
10
+ */
11
+ const PROVIDERS_TO_MIGRATE = [
12
+ "anthropic",
13
+ "openai",
14
+ "gemini",
15
+ "ollama",
16
+ "fireworks",
17
+ "openrouter",
18
+ "brave",
19
+ "perplexity",
20
+ "deepgram",
21
+ "xai",
22
+ ] as const;
23
+
24
+ export const apiKeyToCredentialsMigration: CesMigration = {
25
+ id: "002-api-keys-to-credentials",
26
+ description:
27
+ "Rekey bare provider API keys to credential/{provider}/api_key namespace",
28
+
29
+ async run(backend): Promise<void> {
30
+ for (const provider of PROVIDERS_TO_MIGRATE) {
31
+ const bareValue = await backend.get(provider);
32
+ if (bareValue === undefined) continue; // nothing to migrate for this provider
33
+
34
+ const credKey = `credential/${provider}/api_key`;
35
+ const existingCred = await backend.get(credKey);
36
+ if (existingCred === undefined) {
37
+ // Write new key first — safe to re-run if we crash after this.
38
+ // Skip delete if the write fails so the bare key is preserved for retry.
39
+ const ok = await backend.set(credKey, bareValue);
40
+ if (!ok) continue;
41
+ }
42
+ // Always delete old bare key (idempotent: harmless if already absent)
43
+ await backend.delete(provider);
44
+ }
45
+ },
46
+
47
+ async down(backend): Promise<void> {
48
+ for (const provider of PROVIDERS_TO_MIGRATE) {
49
+ const credKey = `credential/${provider}/api_key`;
50
+ const credValue = await backend.get(credKey);
51
+ if (credValue === undefined) continue;
52
+
53
+ const existingBare = await backend.get(provider);
54
+ if (existingBare === undefined) {
55
+ await backend.set(provider, credValue);
56
+ }
57
+ await backend.delete(credKey);
58
+ }
59
+ },
60
+ };
@@ -0,0 +1,15 @@
1
+ import { apiKeyToCredentialsMigration } from "./002-api-keys-to-credentials.js";
2
+ import { noOpMigration } from "./001-no-op.js";
3
+ import type { CesMigration } from "./types.js";
4
+
5
+ /**
6
+ * Ordered list of all CES data migrations.
7
+ *
8
+ * New migrations are appended to the end. Never reorder or remove entries —
9
+ * the runner uses array position for ordering and the `id` field for
10
+ * checkpoint tracking.
11
+ */
12
+ export const CES_MIGRATIONS: CesMigration[] = [
13
+ noOpMigration,
14
+ apiKeyToCredentialsMigration,
15
+ ];
@@ -0,0 +1,146 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ import type { SecureKeyBackend } from "@vellumai/credential-storage";
5
+
6
+ import { getLogger } from "../logger.js";
7
+ import type { CesMigration, CesMigrationStatus } from "./types.js";
8
+
9
+ const log = getLogger("ces-migrations");
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Checkpoint file
13
+ // ---------------------------------------------------------------------------
14
+
15
+ type CheckpointFile = {
16
+ applied: Record<string, { appliedAt: string; status?: CesMigrationStatus }>;
17
+ };
18
+
19
+ function getCheckpointPath(cesDataRoot: string): string {
20
+ return join(cesDataRoot, ".ces-migrations.json");
21
+ }
22
+
23
+ function loadCheckpoints(cesDataRoot: string): CheckpointFile {
24
+ const path = getCheckpointPath(cesDataRoot);
25
+ if (!existsSync(path)) {
26
+ return { applied: {} };
27
+ }
28
+ try {
29
+ const raw = readFileSync(path, "utf-8");
30
+ const data = JSON.parse(raw);
31
+ if (
32
+ typeof data === "object" &&
33
+ data != null &&
34
+ typeof data.applied === "object" &&
35
+ data.applied != null
36
+ ) {
37
+ return data as CheckpointFile;
38
+ }
39
+ log.warn(
40
+ "CES migration checkpoint file has unexpected structure; treating as fresh state",
41
+ );
42
+ } catch {
43
+ log.warn(
44
+ "CES migration checkpoint file is malformed; treating as fresh state",
45
+ );
46
+ }
47
+ return { applied: {} };
48
+ }
49
+
50
+ function saveCheckpoints(
51
+ cesDataRoot: string,
52
+ checkpoints: CheckpointFile,
53
+ ): void {
54
+ const path = getCheckpointPath(cesDataRoot);
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ const tmpPath = path + ".tmp";
57
+ writeFileSync(tmpPath, JSON.stringify(checkpoints, null, 2) + "\n", "utf-8");
58
+ renameSync(tmpPath, path);
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Runner
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Run all pending CES migrations in registry order.
67
+ *
68
+ * - Skips migrations that already have a checkpoint entry (`"completed"` or
69
+ * `"failed"`). Only `"started"` and `"rolling_back"` entries are cleared
70
+ * and re-run on the next startup (crash recovery — migrations must be
71
+ * idempotent).
72
+ * - Marks failed migrations as `"failed"` and continues startup; a failed
73
+ * migration does not block the RPC server from starting.
74
+ *
75
+ * @param cesDataRoot The CES-private data root (from `getCesDataRoot(mode)`).
76
+ * The checkpoint file is stored here as `.ces-migrations.json`.
77
+ * @param backend The active `SecureKeyBackend` instance, passed directly to
78
+ * each migration's `run()` function.
79
+ * @param migrations Ordered list of migrations from the registry.
80
+ */
81
+ export async function runCesMigrations(
82
+ cesDataRoot: string,
83
+ backend: SecureKeyBackend,
84
+ migrations: CesMigration[],
85
+ ): Promise<void> {
86
+ // Validate uniqueness.
87
+ const seen = new Set<string>();
88
+ for (const m of migrations) {
89
+ if (seen.has(m.id)) {
90
+ throw new Error(`Duplicate CES migration id: "${m.id}"`);
91
+ }
92
+ seen.add(m.id);
93
+ }
94
+
95
+ const checkpoints = loadCheckpoints(cesDataRoot);
96
+
97
+ // Clear any interrupted checkpoints so they re-run.
98
+ for (const [id, entry] of Object.entries(checkpoints.applied)) {
99
+ if (entry.status === "started" || entry.status === "rolling_back") {
100
+ log.warn(
101
+ `CES migration "${id}" was interrupted during a previous run; will re-run`,
102
+ );
103
+ delete checkpoints.applied[id];
104
+ }
105
+ }
106
+
107
+ for (const migration of migrations) {
108
+ if (checkpoints.applied[migration.id]) {
109
+ continue;
110
+ }
111
+
112
+ log.info(
113
+ `Running CES migration: ${migration.id} — ${migration.description}`,
114
+ );
115
+
116
+ // Mark as started before executing (crash recovery observability).
117
+ checkpoints.applied[migration.id] = {
118
+ appliedAt: new Date().toISOString(),
119
+ status: "started",
120
+ };
121
+ saveCheckpoints(cesDataRoot, checkpoints);
122
+
123
+ try {
124
+ await migration.run(backend);
125
+ } catch (error) {
126
+ log.error(
127
+ { migrationId: migration.id, error },
128
+ `CES migration failed: ${migration.id} — marking as failed and continuing`,
129
+ );
130
+ checkpoints.applied[migration.id] = {
131
+ appliedAt: new Date().toISOString(),
132
+ status: "failed",
133
+ };
134
+ saveCheckpoints(cesDataRoot, checkpoints);
135
+ continue;
136
+ }
137
+
138
+ checkpoints.applied[migration.id] = {
139
+ appliedAt: new Date().toISOString(),
140
+ status: "completed",
141
+ };
142
+ saveCheckpoints(cesDataRoot, checkpoints);
143
+
144
+ log.info(`CES migration completed: ${migration.id}`);
145
+ }
146
+ }
@@ -0,0 +1,54 @@
1
+ import type { SecureKeyBackend } from "@vellumai/credential-storage";
2
+
3
+ /**
4
+ * A single CES data migration.
5
+ *
6
+ * Migrations run at CES startup — once per installation, tracked via a
7
+ * checkpoint file in the CES-private data root. They are the right place
8
+ * for one-time transformations of the credential store (key renames,
9
+ * format changes, etc.) that must happen before the RPC server accepts
10
+ * connections.
11
+ *
12
+ * **Idempotency**: `run` and `down` must both be safe to re-run. The
13
+ * runner re-executes any migration whose checkpoint was left in `"started"`
14
+ * state (i.e. the process crashed mid-migration).
15
+ *
16
+ * **Ordering**: Migrations are executed in registry order. Never reorder
17
+ * or remove an entry from the registry once it has been released.
18
+ */
19
+ export interface CesMigration {
20
+ /**
21
+ * Unique identifier used as the checkpoint key.
22
+ * Convention: `"NNN-short-description"`, e.g. `"001-no-op"`.
23
+ * Must be unique across all registered migrations.
24
+ */
25
+ id: string;
26
+
27
+ /** Human-readable description logged when the migration runs. */
28
+ description: string;
29
+
30
+ /**
31
+ * Apply the migration.
32
+ *
33
+ * Receives the active `SecureKeyBackend` so the migration can read,
34
+ * write, and delete credential store entries without re-opening the
35
+ * encrypted store.
36
+ */
37
+ run(backend: SecureKeyBackend): void | Promise<void>;
38
+
39
+ /**
40
+ * Reverse the migration (best-effort).
41
+ *
42
+ * Some migrations are forward-only (e.g. dropping a key that is no
43
+ * longer used). In those cases, implement `down` as a no-op and document
44
+ * why reversal is not meaningful.
45
+ */
46
+ down(backend: SecureKeyBackend): void | Promise<void>;
47
+ }
48
+
49
+ /** Checkpoint status values written to `.ces-migrations.json`. */
50
+ export type CesMigrationStatus =
51
+ | "started"
52
+ | "completed"
53
+ | "rolling_back"
54
+ | "failed";
package/src/paths.ts CHANGED
@@ -43,12 +43,19 @@ export function getCesMode(): CesMode {
43
43
  // ---------------------------------------------------------------------------
44
44
 
45
45
  /**
46
- * Resolve the Vellum root directory, respecting `BASE_DATA_DIR` for
47
- * multi-instance deployments. Mirrors the logic in `assistant/src/util/platform.ts`.
46
+ * Resolve the CES security directory.
47
+ *
48
+ * Priority:
49
+ * 1. `CREDENTIAL_SECURITY_DIR` env var (set by the platform template for the
50
+ * CES container — `/ces-security` in managed mode)
51
+ * 2. Default: `~/.vellum/protected` (local mode shares the filesystem with
52
+ * the gateway)
48
53
  */
49
- function getVellumRootDir(): string {
50
- const baseDataDir = process.env["BASE_DATA_DIR"]?.trim();
51
- return join(baseDataDir || homedir(), ".vellum");
54
+ export function getSecurityDir(): string {
55
+ return (
56
+ process.env["CREDENTIAL_SECURITY_DIR"]?.trim() ||
57
+ join(homedir(), ".vellum", "protected")
58
+ );
52
59
  }
53
60
 
54
61
  /**
@@ -62,18 +69,15 @@ const DEFAULT_MANAGED_CES_DATA_ROOT = "/ces-data";
62
69
  /**
63
70
  * Return the CES-private data root.
64
71
  *
65
- * - Local: `<vellumRoot>/protected/credential-executor/`
72
+ * - Local: `<securityDir>/credential-executor/`
66
73
  * - Managed: `CES_DATA_DIR` env var, or `/ces-data` by default
67
74
  */
68
75
  export function getCesDataRoot(mode?: CesMode): string {
69
76
  const resolvedMode = mode ?? getCesMode();
70
77
  if (resolvedMode === "managed") {
71
- return (
72
- process.env["CES_DATA_DIR"] ??
73
- DEFAULT_MANAGED_CES_DATA_ROOT
74
- );
78
+ return process.env["CES_DATA_DIR"] ?? DEFAULT_MANAGED_CES_DATA_ROOT;
75
79
  }
76
- return join(getVellumRootDir(), "protected", "credential-executor");
80
+ return join(getSecurityDir(), "credential-executor");
77
81
  }
78
82
 
79
83
  // ---------------------------------------------------------------------------
package/src/server.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * CES RPC server.
3
3
  *
4
4
  * Implements the server-side of the CES wire protocol defined in
5
- * `@vellumai/ces-contracts`. The server reads newline-delimited JSON
5
+ * `@vellumai/service-contracts`. The server reads newline-delimited JSON
6
6
  * messages from a readable stream, dispatches them through the RPC
7
7
  * contract, and writes responses back to a writable stream.
8
8
  *
@@ -32,7 +32,7 @@ import {
32
32
  type RunAuthenticatedCommandResponse,
33
33
  type TransportMessage,
34
34
  TransportMessageSchema,
35
- } from "@vellumai/ces-contracts";
35
+ } from "@vellumai/service-contracts/credential-rpc";
36
36
 
37
37
  import { resolve } from "node:path";
38
38
 
@@ -7,7 +7,7 @@
7
7
  * operates independently — it never imports from the assistant daemon.
8
8
  *
9
9
  * Subject resolution is the first phase of credential materialisation:
10
- * 1. Parse the handle (via `@vellumai/ces-contracts`)
10
+ * 1. Parse the handle (via `@vellumai/service-contracts`)
11
11
  * 2. Look up the metadata/connection record in local storage
12
12
  * 3. Return a resolved subject that the materialiser can consume
13
13
  *
@@ -26,7 +26,7 @@ import {
26
26
  parseHandle,
27
27
  type LocalOAuthHandle,
28
28
  type LocalStaticHandle,
29
- } from "@vellumai/ces-contracts";
29
+ } from "@vellumai/service-contracts/credential-rpc";
30
30
 
31
31
  // ---------------------------------------------------------------------------
32
32
  // Resolved subject types
@@ -24,7 +24,7 @@ import {
24
24
  HandleType,
25
25
  parseHandle,
26
26
  type PlatformOAuthHandle,
27
- } from "@vellumai/ces-contracts";
27
+ } from "@vellumai/service-contracts/credential-rpc";
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Common subject interface