@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.
- package/Dockerfile +1 -1
- package/bun.lock +15 -7
- package/node_modules/@vellumai/credential-storage/src/__tests__/package-boundary.test.ts +32 -6
- package/node_modules/@vellumai/egress-proxy/src/__tests__/package-boundary.test.ts +32 -1
- package/node_modules/@vellumai/{ces-contracts → service-contracts}/bun.lock +1 -1
- package/node_modules/@vellumai/{ces-contracts → service-contracts}/package.json +4 -2
- package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/contracts.test.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +155 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +23 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +25 -0
- package/node_modules/@vellumai/{ces-contracts/src/index.ts → service-contracts/src/transport.ts} +6 -28
- package/node_modules/@vellumai/service-contracts/src/trust-rules.ts +116 -0
- package/package.json +5 -4
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/ces-migrations-002-api-keys.test.ts +185 -0
- package/src/__tests__/ces-migrations-runner.test.ts +227 -0
- package/src/__tests__/cli.test.ts +139 -0
- package/src/__tests__/command-executor.test.ts +71 -42
- package/src/__tests__/http-executor.test.ts +1 -1
- package/src/__tests__/local-materializers.test.ts +1 -1
- package/src/__tests__/local-token-refresh.test.ts +65 -38
- package/src/__tests__/managed-integration.test.ts +1 -1
- package/src/__tests__/managed-lazy-getters.test.ts +1 -1
- package/src/__tests__/managed-materializers.test.ts +1 -1
- package/src/__tests__/managed-rejection.test.ts +1 -1
- package/src/__tests__/toolstore.test.ts +65 -20
- package/src/__tests__/transport.test.ts +13 -4
- package/src/audit/store.ts +2 -2
- package/src/cli.ts +158 -0
- package/src/commands/executor.ts +2 -2
- package/src/grants/rpc-handlers.ts +1 -1
- package/src/http/__tests__/credential-routes-normalization.test.ts +202 -0
- package/src/http/audit.ts +1 -1
- package/src/http/credential-routes.ts +53 -7
- package/src/http/executor.ts +2 -2
- package/src/http/policy.ts +1 -1
- package/src/main.ts +120 -50
- package/src/managed-errors.ts +2 -2
- package/src/managed-lazy-getters.ts +4 -4
- package/src/managed-main.ts +9 -3
- package/src/materializers/local-oauth-lookup.ts +7 -6
- package/src/materializers/local-token-refresh.ts +25 -15
- package/src/materializers/local.ts +1 -1
- package/src/migrations/001-no-op.ts +19 -0
- package/src/migrations/002-api-keys-to-credentials.ts +60 -0
- package/src/migrations/registry.ts +15 -0
- package/src/migrations/runner.ts +146 -0
- package/src/migrations/types.ts +54 -0
- package/src/paths.ts +15 -11
- package/src/server.ts +2 -2
- package/src/subjects/local.ts +2 -2
- package/src/subjects/managed.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +0 -471
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +0 -436
- /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/grants.test.ts +0 -0
- /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/error.ts +0 -0
- /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/grants.ts +0 -0
- /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/handles.ts +0 -0
- /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rendering.ts +0 -0
- /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rpc.ts +0 -0
- /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
|
|
47
|
-
*
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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: `<
|
|
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(
|
|
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/
|
|
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/
|
|
35
|
+
} from "@vellumai/service-contracts/credential-rpc";
|
|
36
36
|
|
|
37
37
|
import { resolve } from "node:path";
|
|
38
38
|
|
package/src/subjects/local.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
29
|
+
} from "@vellumai/service-contracts/credential-rpc";
|
|
30
30
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// Resolved subject types
|
package/src/subjects/managed.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
HandleType,
|
|
25
25
|
parseHandle,
|
|
26
26
|
type PlatformOAuthHandle,
|
|
27
|
-
} from "@vellumai/
|
|
27
|
+
} from "@vellumai/service-contracts/credential-rpc";
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Common subject interface
|