@mycadet/credential-executor 0.12.6-dev.202610080102.d7d1bec → 0.12.6-dev.202610082233.54244ee

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/credential-executor",
3
- "version": "0.12.6-dev.202610080102.d7d1bec",
3
+ "version": "0.12.6-dev.202610082233.54244ee",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -179,10 +179,8 @@ describe("managed CES reconnection (real entrypoint)", () => {
179
179
  const socketPath = resolveIpcEndpoint("ces", {
180
180
  workspaceDir: socketDir,
181
181
  }).path;
182
- const assistantDataMount = join(tmpDir, "assistant-data-ro");
183
182
  mkdirSync(dataDir, { recursive: true });
184
183
  mkdirSync(socketDir, { recursive: true });
185
- mkdirSync(join(assistantDataMount, ".cadet"), { recursive: true });
186
184
 
187
185
  const healthPort = await pickFreePort();
188
186
  const managedMain = resolve(__dirname, "..", "main.ts");
@@ -195,7 +193,6 @@ describe("managed CES reconnection (real entrypoint)", () => {
195
193
  CES_DATA_DIR: dataDir,
196
194
  CES_BOOTSTRAP_SOCKET_DIR: socketDir,
197
195
  CES_HEALTH_PORT: String(healthPort),
198
- CES_ASSISTANT_DATA_MOUNT: assistantDataMount,
199
196
  },
200
197
  stdout: "ignore",
201
198
  stderr: "ignore",
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { describe, expect, test } from "bun:test";
16
16
  import { readFileSync } from "node:fs";
17
- import { resolve } from "node:path";
17
+ import { join, resolve } from "node:path";
18
18
  import { PassThrough } from "node:stream";
19
19
 
20
20
  import { resolveIpcEndpoint } from "@mycadet/ipc-server-utils";
@@ -355,25 +355,11 @@ describe("CesRpcServer", () => {
355
355
  // ---------------------------------------------------------------------------
356
356
 
357
357
  describe("CES data paths", () => {
358
- test("local mode data root includes 'protected/credential-executor'", () => {
359
- const root = getCesDataRoot("local");
360
- expect(root).toMatch(/protected[/\\]credential-executor$/);
361
- });
362
-
363
- test("managed mode data root defaults to /ces-data", () => {
364
- const savedDir = process.env["CES_DATA_DIR"];
365
- delete process.env["CES_DATA_DIR"];
366
- try {
367
- expect(getCesDataRoot("managed")).toBe("/ces-data");
368
- } finally {
369
- if (savedDir !== undefined) process.env["CES_DATA_DIR"] = savedDir;
370
- }
371
- });
372
-
373
- test("managed mode data root respects CES_DATA_DIR env var", () => {
358
+ test("data root resolves CES_DATA_DIR in any mode", () => {
374
359
  const savedDir = process.env["CES_DATA_DIR"];
375
360
  process.env["CES_DATA_DIR"] = "/custom/ces-data";
376
361
  try {
362
+ expect(getCesDataRoot("local")).toBe("/custom/ces-data");
377
363
  expect(getCesDataRoot("managed")).toBe("/custom/ces-data");
378
364
  } finally {
379
365
  if (savedDir !== undefined) {
@@ -384,11 +370,17 @@ describe("CES data paths", () => {
384
370
  }
385
371
  });
386
372
 
387
- test("local data root is under the Cadet root, not the workspace", () => {
388
- const root = getCesDataRoot("local");
389
- // Must be under .cadet/protected/, NOT .cadet/workspace/
390
- expect(root).toMatch(/\.cadet[/\\]protected[/\\]/);
391
- expect(root).not.toMatch(/workspace/);
373
+ test("data root falls back to ./ces-data (cwd convention)", () => {
374
+ const savedDir = process.env["CES_DATA_DIR"];
375
+ delete process.env["CES_DATA_DIR"];
376
+ try {
377
+ expect(getCesDataRoot("managed")).toBe(
378
+ join(process.cwd(), "ces-data"),
379
+ );
380
+ expect(getCesDataRoot("local")).toBe(join(process.cwd(), "ces-data"));
381
+ } finally {
382
+ if (savedDir !== undefined) process.env["CES_DATA_DIR"] = savedDir;
383
+ }
392
384
  });
393
385
 
394
386
  test("getBootstrapSocketPath defaults to /run/ces-bootstrap", () => {
package/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * CES CLI — lightweight credential CRUD for the CES container.
3
+ * CES CLI — lightweight credential CRUD for the encrypted key store.
4
4
  *
5
5
  * Operates directly on the encrypted key store (`keys.enc` + `store.key`)
6
6
  * without requiring the RPC server, HTTP routes, or a running assistant.
@@ -15,40 +15,11 @@
15
15
  *
16
16
  * Environment variables:
17
17
  * CREDENTIAL_SECURITY_DIR — directory containing `keys.enc` + `store.key`
18
- * CES_ASSISTANT_DATA_MOUNT — fallback root for `<mount>/.cadet/protected/`
19
- *
20
- * When neither is set, defaults to `~/.cadet/protected/` (local mode).
18
+ * (falls back to `./ces-security`)
21
19
  */
22
20
 
23
- import { join } from "node:path";
24
- import { homedir } from "node:os";
25
21
  import { createLocalSecureKeyBackend } from "./materializers/local-secure-key-backend.js";
26
-
27
- // ---------------------------------------------------------------------------
28
- // Path resolution (mirrors the managed-mode path in main.ts)
29
- // ---------------------------------------------------------------------------
30
-
31
- function resolveCadetRoot(): string {
32
- const secDir = process.env["CREDENTIAL_SECURITY_DIR"]?.trim();
33
- if (secDir) {
34
- // CREDENTIAL_SECURITY_DIR points directly at the dir containing
35
- // keys.enc, but createLocalSecureKeyBackend wants the parent
36
- // (.cadet root) and appends /protected/ itself — unless
37
- // CREDENTIAL_SECURITY_DIR is set, in which case the backend reads
38
- // from that dir directly. So we pass dirname(secDir) as cadetRoot.
39
- // Actually, looking at resolveSecurityDir(): if CREDENTIAL_SECURITY_DIR
40
- // is set it uses that directly, ignoring cadetRoot. So cadetRoot
41
- // can be anything — the env var takes precedence.
42
- return join(secDir, "..");
43
- }
44
-
45
- const mount = process.env["CES_ASSISTANT_DATA_MOUNT"]?.trim();
46
- if (mount) {
47
- return join(mount, ".cadet");
48
- }
49
-
50
- return join(homedir(), ".cadet");
51
- }
22
+ import { getSecurityDir } from "./paths.js";
52
23
 
53
24
  // ---------------------------------------------------------------------------
54
25
  // CLI
@@ -62,8 +33,7 @@ async function main(): Promise<void> {
62
33
  process.exit(0);
63
34
  }
64
35
 
65
- const cadetRoot = resolveCadetRoot();
66
- const backend = createLocalSecureKeyBackend(cadetRoot);
36
+ const backend = createLocalSecureKeyBackend(getSecurityDir());
67
37
 
68
38
  switch (command) {
69
39
  case "list": {
@@ -149,7 +119,7 @@ Account format:
149
119
 
150
120
  Environment:
151
121
  CREDENTIAL_SECURITY_DIR Directory containing keys.enc + store.key
152
- CES_ASSISTANT_DATA_MOUNT Fallback: <mount>/.cadet/protected/`);
122
+ (falls back to ./ces-security)`);
153
123
  }
154
124
 
155
125
  main().catch((err) => {
package/src/main.ts CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { mkdirSync } from "node:fs";
21
21
  import { createServer as createNetServer, type Socket } from "node:net";
22
- import { dirname, join } from "node:path";
22
+ import { dirname } from "node:path";
23
23
  import { Readable, Writable } from "node:stream";
24
24
 
25
25
  import {
@@ -345,19 +345,9 @@ async function main(): Promise<void> {
345
345
  process.on("SIGINT", shutdown);
346
346
 
347
347
  // -- Secure key backend + migrations --------------------------------------
348
- // The secure key backend path differs by mode:
349
- // - Local: dirname(getSecurityDir()) — the parent of ~/.cadet/protected
350
- // (or CREDENTIAL_SECURITY_DIR when set by the CLI).
351
- // - Managed: join(assistantDataMount, ".cadet") — the assistant data mount.
352
- const secureKeyBackend =
353
- mode === "managed"
354
- ? createLocalSecureKeyBackend(
355
- join(
356
- process.env["CES_ASSISTANT_DATA_MOUNT"] ?? "/assistant-data-ro",
357
- ".cadet",
358
- ),
359
- )
360
- : createLocalSecureKeyBackend(dirname(getSecurityDir()));
348
+ // Both modes resolve the store location through getSecurityDir() (env var,
349
+ // cwd fallback convention) — see paths.ts.
350
+ const secureKeyBackend = createLocalSecureKeyBackend(getSecurityDir());
361
351
 
362
352
  await runCesMigrations(
363
353
  getCesDataRoot(mode),
@@ -2,8 +2,8 @@
2
2
  * CES-native SecureKeyBackend for **local mode only**.
3
3
  *
4
4
  * In local mode, CES runs as a child process of the assistant on the same
5
- * machine as the same user and can read/write the assistant's encrypted key
6
- * store file at `<cadetRoot>/protected/keys.enc`.
5
+ * machine as the same user and can read/write the encrypted key store file
6
+ * at `<ces-security>/keys.enc`.
7
7
  *
8
8
  * This implementation replicates the encryption/decryption logic from the
9
9
  * assistant's `encrypted-store.ts` without importing assistant-internal
@@ -15,7 +15,7 @@
15
15
  * Two store formats are supported:
16
16
  *
17
17
  * - **v2 (primary):** AES-256-GCM with a random 32-byte key stored at
18
- * `<cadetRoot>/protected/store.key`. The key is machine-independent —
18
+ * `<ces-security>/store.key`. The key is machine-independent —
19
19
  * any process that can read the key file can decrypt the store.
20
20
  *
21
21
  * - **v1 (legacy):** AES-256-GCM with a key derived from machine-specific
@@ -106,13 +106,14 @@ const KEYS_ENC_FILENAME = "keys.enc";
106
106
  *
107
107
  * When `CREDENTIAL_SECURITY_DIR` is set, files are read from (and written to)
108
108
  * that directory directly. This allows Docker deployments to mount a separate
109
- * CES-only security volume.
109
+ * CES-only security volume, and the CLI to point CES at the per-instance
110
+ * security dir.
110
111
  *
111
- * When the env var is unset, falls back to `<cadetRoot>/protected/` for
112
- * backwards compatibility.
112
+ * When the env var is unset, falls back to the passed-in default
113
+ * (`./ces-security` under the cwd fallback convention).
113
114
  */
114
- function resolveSecurityDir(cadetRoot: string): string {
115
- return process.env.CREDENTIAL_SECURITY_DIR || join(cadetRoot, "protected");
115
+ function resolveSecurityDir(securityDir: string): string {
116
+ return process.env.CREDENTIAL_SECURITY_DIR?.trim() || securityDir;
116
117
  }
117
118
 
118
119
  /**
@@ -120,9 +121,9 @@ function resolveSecurityDir(cadetRoot: string): string {
120
121
  * Returns the raw 32-byte key buffer, or null if the file is missing,
121
122
  * wrong size, or unreadable.
122
123
  */
123
- function readStoreKey(cadetRoot: string): Buffer | null {
124
+ function readStoreKey(securityDir: string): Buffer | null {
124
125
  try {
125
- const keyPath = join(resolveSecurityDir(cadetRoot), STORE_KEY_FILENAME);
126
+ const keyPath = join(resolveSecurityDir(securityDir), STORE_KEY_FILENAME);
126
127
  if (!existsSync(keyPath)) return null;
127
128
  const buf = readFileSync(keyPath);
128
129
  if (buf.length !== KEY_LENGTH) return null;
@@ -140,15 +141,15 @@ function readStoreKey(cadetRoot: string): Buffer | null {
140
141
  * Read or generate the v2 store key. If the key file does not exist,
141
142
  * creates a new 32-byte random key and writes it atomically.
142
143
  */
143
- function getOrReadStoreKey(cadetRoot: string): Buffer {
144
- const existing = readStoreKey(cadetRoot);
144
+ function getOrReadStoreKey(securityDir: string): Buffer {
145
+ const existing = readStoreKey(securityDir);
145
146
  if (existing) return existing;
146
147
 
147
- const securityDir = resolveSecurityDir(cadetRoot);
148
+ const resolvedSecurityDir = resolveSecurityDir(securityDir);
148
149
  mkdirSync(securityDir, { recursive: true });
149
150
 
150
151
  const key = randomBytes(KEY_LENGTH);
151
- const keyPath = join(securityDir, STORE_KEY_FILENAME);
152
+ const keyPath = join(resolvedSecurityDir, STORE_KEY_FILENAME);
152
153
  const tmpPath = keyPath + `.tmp.${process.pid}`;
153
154
  writeFileSync(tmpPath, key, { mode: 0o600 });
154
155
  chmodSync(tmpPath, 0o600);
@@ -166,14 +167,14 @@ function getOrReadStoreKey(cadetRoot: string): Buffer {
166
167
  */
167
168
  function getOrCreateStore(
168
169
  storePath: string,
169
- cadetRoot: string,
170
+ securityDir: string,
170
171
  ): { store: StoreFile; aesKey: Buffer } {
171
172
  const existing = readStore(storePath);
172
173
  if (existing) {
173
174
  if (existing.version === 1) {
174
175
  throw new Error("v1 store cannot be auto-initialized");
175
176
  }
176
- const storeKey = readStoreKey(cadetRoot);
177
+ const storeKey = readStoreKey(securityDir);
177
178
  if (!storeKey) {
178
179
  throw new Error("v2 store exists but store.key is missing or corrupt");
179
180
  }
@@ -181,7 +182,7 @@ function getOrCreateStore(
181
182
  }
182
183
 
183
184
  // No store exists — create a new empty v2 store
184
- const aesKey = getOrReadStoreKey(cadetRoot);
185
+ const aesKey = getOrReadStoreKey(securityDir);
185
186
  const store: StoreFileV2 = { version: 2, entries: {} };
186
187
  writeStore(store, storePath);
187
188
  return { store, aesKey };
@@ -346,25 +347,27 @@ export class StoreUnavailableError extends Error {
346
347
  * persisting refreshed OAuth tokens. `delete` removes a key from the
347
348
  * encrypted store.
348
349
  *
349
- * @param cadetRoot - The Cadet root directory (e.g. `~/.cadet`).
350
+ * @param securityDir - CES security directory (holds keys.enc + store.key).
350
351
  * @param options.entropyOverride - If provided, used instead of local
351
352
  * machine entropy for key derivation. In managed mode the CES sidecar
352
353
  * runs in a different container with a different hostname/user, so it
353
354
  * must use the assistant's entropy (read from the shared data mount)
354
355
  * to derive the same AES key.
356
+ * @param securityDir Default CES security directory (used when
357
+ * CREDENTIAL_SECURITY_DIR is unset).
355
358
  * @param options.entropyGetter - If provided, called on each `get()`/`set()`
356
359
  * to lazily resolve entropy. This handles the startup race where the
357
360
  * entropy file may not exist at construction time but appears later.
358
361
  * Takes precedence over `entropyOverride`.
359
362
  */
360
363
  export function createLocalSecureKeyBackend(
361
- cadetRoot: string,
364
+ securityDir: string,
362
365
  options?: {
363
366
  entropyOverride?: string;
364
367
  entropyGetter?: () => string | undefined;
365
368
  },
366
369
  ): SecureKeyBackend {
367
- const storePath = join(resolveSecurityDir(cadetRoot), KEYS_ENC_FILENAME);
370
+ const storePath = join(resolveSecurityDir(securityDir), KEYS_ENC_FILENAME);
368
371
  const staticEntropy = options?.entropyOverride;
369
372
  const entropyGetter = options?.entropyGetter;
370
373
 
@@ -389,7 +392,7 @@ export function createLocalSecureKeyBackend(
389
392
 
390
393
  let aesKey: Buffer;
391
394
  if (store.version === 2) {
392
- const storeKey = readStoreKey(cadetRoot);
395
+ const storeKey = readStoreKey(securityDir);
393
396
  if (!storeKey) {
394
397
  // The entry exists but the key material to decrypt it is unavailable
395
398
  // (the cold-start race). Unavailable, NOT not-found.
@@ -427,7 +430,7 @@ export function createLocalSecureKeyBackend(
427
430
  let aesKey: Buffer;
428
431
 
429
432
  try {
430
- const result = getOrCreateStore(storePath, cadetRoot);
433
+ const result = getOrCreateStore(storePath, securityDir);
431
434
  store = result.store;
432
435
  aesKey = result.aesKey;
433
436
  } catch {
package/src/paths.ts CHANGED
@@ -2,25 +2,26 @@
2
2
  * CES private data-root layout.
3
3
  *
4
4
  * Defines the directory structure for CES-private durable state (grants,
5
- * audit logs, tool store). This state is never stored on the assistant-visible
6
- * workspace/data root — it lives in a CES-only path that the assistant process
7
- * cannot read or write.
5
+ * audit logs, tool store) and CES-private security material (the encrypted
6
+ * store, `keys.enc` + `store.key`). This state is never stored on the
7
+ * assistant-visible workspace/data root — it lives in CES-only paths that
8
+ * the assistant process cannot read or write.
8
9
  *
9
- * Two modes:
10
+ * Resolution convention (shared with the gateway and the assistant):
11
+ * every directory comes from its env var, falling back to
12
+ * `process.cwd()/<hardcoded-value>`:
10
13
  *
11
- * - **Local**: CES private state lives under the Cadet root's `protected/`
12
- * directory at `<rootDir>/protected/credential-executor/`.
14
+ * - `CREDENTIAL_SECURITY_DIR` → `./ces-security`
15
+ * - `CES_DATA_DIR` → `./ces-data`
13
16
  *
14
- * - **Managed**: CES private state lives at `/ces-data` by default
15
- * (overridable via `CES_DATA_DIR` env var). The assistant container never
16
- * sees this path.
17
- *
18
- * The assistant-visible data root (where workspace, embeddings, etc. live)
19
- * is a separate path and must never be used for CES-private writes.
17
+ * Managed containers set both explicitly (the platform template provisions
18
+ * `/ces-security` and `/ces-data` volumes); local mode sets them per
19
+ * instance dir via the CLI. The assistant-visible data root (where
20
+ * workspace, embeddings, etc. live) is a separate path and must never be
21
+ * used for CES-private writes.
20
22
  */
21
23
 
22
24
  import { join } from "node:path";
23
- import { homedir } from "node:os";
24
25
 
25
26
  import { resolveIpcEndpoint } from "@mycadet/ipc-server-utils";
26
27
 
@@ -45,41 +46,31 @@ export function getCesMode(): CesMode {
45
46
  // ---------------------------------------------------------------------------
46
47
 
47
48
  /**
48
- * Resolve the CES security directory.
49
+ * Resolve the CES security directory (contains `keys.enc` + `store.key`).
49
50
  *
50
51
  * Priority:
51
52
  * 1. `CREDENTIAL_SECURITY_DIR` env var (set by the platform template for the
52
- * CES container — `/ces-security` in managed mode)
53
- * 2. Default: `~/.cadet/protected` (local mode shares the filesystem with
54
- * the gateway)
53
+ * CES container — `/ces-security` in managed mode; per-instance
54
+ * `<instanceDir>/ces-security` in local mode)
55
+ * 2. Default: `./ces-security` (cwd fallback convention)
55
56
  */
56
57
  export function getSecurityDir(): string {
57
58
  return (
58
59
  process.env["CREDENTIAL_SECURITY_DIR"]?.trim() ||
59
- join(homedir(), ".cadet", "protected")
60
+ join(process.cwd(), "ces-security")
60
61
  );
61
62
  }
62
63
 
63
64
  /**
64
- * Default managed CES data root.
65
+ * Return the CES-private data root (grants, audit logs, tool store).
65
66
  *
66
- * Defaults to `/ces-data` — the platform template provisions this path
67
- * via the `CES_DATA_DIR` env var and a dedicated PVC.
68
- */
69
- const DEFAULT_MANAGED_CES_DATA_ROOT = "/ces-data";
70
-
71
- /**
72
- * Return the CES-private data root.
73
- *
74
- * - Local: `<securityDir>/credential-executor/`
75
- * - Managed: `CES_DATA_DIR` env var, or `/ces-data` by default
67
+ * Priority:
68
+ * 1. `CES_DATA_DIR` env var (set by the platform template — `/ces-data` in
69
+ * managed mode; per-instance `<instanceDir>/ces-data` in local mode)
70
+ * 2. Default: `./ces-data` (cwd fallback convention)
76
71
  */
77
- export function getCesDataRoot(mode?: CesMode): string {
78
- const resolvedMode = mode ?? getCesMode();
79
- if (resolvedMode === "managed") {
80
- return process.env["CES_DATA_DIR"] ?? DEFAULT_MANAGED_CES_DATA_ROOT;
81
- }
82
- return join(getSecurityDir(), "credential-executor");
72
+ export function getCesDataRoot(_mode?: CesMode): string {
73
+ return process.env["CES_DATA_DIR"]?.trim() || join(process.cwd(), "ces-data");
83
74
  }
84
75
 
85
76
  // ---------------------------------------------------------------------------