@aexol/spectral 0.9.244 → 0.9.246
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/dist/cli.js +6 -0
- package/dist/commands/secret.d.ts +16 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +153 -0
- package/dist/extensions/list-secrets.d.ts +14 -0
- package/dist/extensions/list-secrets.d.ts.map +1 -0
- package/dist/extensions/list-secrets.js +63 -0
- package/dist/relay/routing.d.ts +1 -1
- package/dist/relay/routing.d.ts.map +1 -1
- package/dist/relay/routing.js +93 -17
- package/dist/sdk/coding-agent/utils/shell.d.ts.map +1 -1
- package/dist/sdk/coding-agent/utils/shell.js +15 -1
- package/dist/secrets/index.d.ts +11 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +10 -0
- package/dist/secrets/store.d.ts +109 -0
- package/dist/secrets/store.d.ts.map +1 -0
- package/dist/secrets/store.js +415 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +7 -0
- package/dist/server/handlers/secrets.d.ts +40 -0
- package/dist/server/handlers/secrets.d.ts.map +1 -0
- package/dist/server/handlers/secrets.js +78 -0
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local encrypted secret store.
|
|
3
|
+
*
|
|
4
|
+
* Storage layout (both files under `getConfigDir()`, honoring
|
|
5
|
+
* `SPECTRAL_CONFIG_DIR` for tests):
|
|
6
|
+
* - `<configDir>/secrets.key` 32 random bytes, mode 0600 — AES-256 key
|
|
7
|
+
* - `<configDir>/secrets.json.enc` AES-256-GCM encrypted JSON, mode 0600
|
|
8
|
+
*
|
|
9
|
+
* Encrypted file layout:
|
|
10
|
+
* - bytes 0-7: magic `"SPSECRT1"` (format tag)
|
|
11
|
+
* - bytes 8-19: 12-byte random IV
|
|
12
|
+
* - bytes 20-35: 16-byte GCM auth tag
|
|
13
|
+
* - bytes 36+: ciphertext (UTF-8 JSON)
|
|
14
|
+
*
|
|
15
|
+
* Security rules enforced here and by this module's consumers:
|
|
16
|
+
* - `listSecrets()` returns ONLY `{ name, createdAt, updatedAt }` — values are
|
|
17
|
+
* never part of any listing.
|
|
18
|
+
* - `getSecretValue()` is INTERNAL: it is consumed only by
|
|
19
|
+
* `getSecretEnvVars()` (env-var injection into the agent's bash tool). It
|
|
20
|
+
* is deliberately NOT re-exported from `./index.js`, so neither the relay
|
|
21
|
+
* handlers nor the LLM-facing tool surface can reach it.
|
|
22
|
+
* - No caller ever logs a value; error messages and listings carry names only.
|
|
23
|
+
*
|
|
24
|
+
* All I/O is synchronous by design: `getSecretEnvVars()` must be callable from
|
|
25
|
+
* the sync `getShellEnv()` in `src/sdk/coding-agent/utils/shell.ts`. The store
|
|
26
|
+
* is tiny (values are capped at 64 KiB each), so sync reads are negligible.
|
|
27
|
+
*
|
|
28
|
+
* Keep this module zero-side-effect on import: nothing touches the filesystem
|
|
29
|
+
* until one of the exported functions is called.
|
|
30
|
+
*/
|
|
31
|
+
/** Secret names: start with a letter, then letters/digits/underscores, max 64 chars. */
|
|
32
|
+
export declare const SECRET_NAME_PATTERN: RegExp;
|
|
33
|
+
/** Maximum UTF-8 byte size of a secret value (64 KiB). */
|
|
34
|
+
export declare const MAX_SECRET_VALUE_BYTES: number;
|
|
35
|
+
/** Metadata-safe view of a secret: NEVER includes the value. */
|
|
36
|
+
export interface SecretMeta {
|
|
37
|
+
name: string;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
}
|
|
41
|
+
/** Thrown when a secret name is missing/invalid after normalization. */
|
|
42
|
+
export declare class SecretNameError extends Error {
|
|
43
|
+
constructor(message: string);
|
|
44
|
+
}
|
|
45
|
+
/** Thrown when a secret value exceeds `MAX_SECRET_VALUE_BYTES`. */
|
|
46
|
+
export declare class SecretValueError extends Error {
|
|
47
|
+
constructor(message: string);
|
|
48
|
+
}
|
|
49
|
+
/** Thrown when the encrypted store is unreadable, corrupt, or key-mismatched. */
|
|
50
|
+
export declare class SecretStoreCorruptError extends Error {
|
|
51
|
+
constructor(message: string);
|
|
52
|
+
}
|
|
53
|
+
/** Path to the AES-256 key file (32 random bytes). Lazily computed — see `getConfigDir`. */
|
|
54
|
+
export declare function getSecretKeyPath(): string;
|
|
55
|
+
/** Path to the AES-256-GCM encrypted store. Lazily computed — see `getConfigDir`. */
|
|
56
|
+
export declare function getSecretDataPath(): string;
|
|
57
|
+
/**
|
|
58
|
+
* Normalize a secret name per the shared contract: trim surrounding
|
|
59
|
+
* whitespace, uppercase, then validate against `SECRET_NAME_PATTERN`.
|
|
60
|
+
* Throws `SecretNameError` with a generic message — the offending raw input
|
|
61
|
+
* is never echoed back, so it can't leak into logs.
|
|
62
|
+
*/
|
|
63
|
+
export declare function normalizeSecretName(raw: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Return the secret env vars (name → value) for injection into the agent
|
|
66
|
+
* shell environment. Synchronous, mtime-cached: new/updated/deleted secrets
|
|
67
|
+
* are picked up on the next call without a CLI restart.
|
|
68
|
+
*
|
|
69
|
+
* Failure modes degrade gracefully — a corrupt store, missing key, or unreadable
|
|
70
|
+
* file returns `{}` instead of throwing, because `getShellEnv()` is sync and
|
|
71
|
+
* must never break bash spawning. No values are ever logged.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getSecretEnvVars(): Record<string, string>;
|
|
74
|
+
/**
|
|
75
|
+
* Create or update a secret. Returns the metadata-only view (never the value).
|
|
76
|
+
* Throws `SecretNameError` for invalid names, `SecretValueError` for oversized
|
|
77
|
+
* values, and `SecretStoreCorruptError` when the store cannot be read/written.
|
|
78
|
+
*
|
|
79
|
+
* Fully synchronous (no awaits): the read-modify-write below is atomic per
|
|
80
|
+
* event-loop turn, so concurrent relay POSTs in one process can never
|
|
81
|
+
* interleave — no mutex is needed. Cross-process writers are isolated by the
|
|
82
|
+
* unique per-request tmp name + rename in `writeStoreData`; the last rename
|
|
83
|
+
* wins and every writer re-reads the store on its next call.
|
|
84
|
+
*/
|
|
85
|
+
export declare function setSecret(rawName: string, value: string): SecretMeta;
|
|
86
|
+
/**
|
|
87
|
+
* Delete a secret. Returns true when a record was removed, false when the
|
|
88
|
+
* name was not stored. Throws `SecretNameError` for invalid names.
|
|
89
|
+
*/
|
|
90
|
+
export declare function deleteSecret(rawName: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* List secrets as metadata only — `{ name, createdAt, updatedAt }`. Values are
|
|
93
|
+
* never part of the returned shape (this is the source for the relay GET
|
|
94
|
+
* response and the `list_secrets` agent tool).
|
|
95
|
+
*/
|
|
96
|
+
export declare function listSecrets(): SecretMeta[];
|
|
97
|
+
/**
|
|
98
|
+
* INTERNAL — read a single secret value. Used ONLY for env-var injection via
|
|
99
|
+
* `getSecretEnvVars()`. Deliberately not re-exported from `./index.js` so it
|
|
100
|
+
* cannot be reached from the relay handlers or the LLM-facing tool surface.
|
|
101
|
+
* Never log the return value.
|
|
102
|
+
*/
|
|
103
|
+
export declare function getSecretValue(rawName: string): string | undefined;
|
|
104
|
+
/**
|
|
105
|
+
* Remove the store + key (test helper and future "reset secrets" support).
|
|
106
|
+
* Returns true when either file was deleted.
|
|
107
|
+
*/
|
|
108
|
+
export declare function resetSecretStore(): boolean;
|
|
109
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/secrets/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAiBH,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,QAA2B,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,QAAY,CAAC;AAQhD,gEAAgE;AAChE,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAaD,wEAAwE;AACxE,qBAAa,eAAgB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI3B;AAED,mEAAmE;AACnE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI3B;AAED,iFAAiF;AACjF,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAI3B;AAMD,4FAA4F;AAC5F,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWvD;AA+ND;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAczD;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAcpE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAUrD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,UAAU,EAAE,CAS1C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIlE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAY1C"}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local encrypted secret store.
|
|
3
|
+
*
|
|
4
|
+
* Storage layout (both files under `getConfigDir()`, honoring
|
|
5
|
+
* `SPECTRAL_CONFIG_DIR` for tests):
|
|
6
|
+
* - `<configDir>/secrets.key` 32 random bytes, mode 0600 — AES-256 key
|
|
7
|
+
* - `<configDir>/secrets.json.enc` AES-256-GCM encrypted JSON, mode 0600
|
|
8
|
+
*
|
|
9
|
+
* Encrypted file layout:
|
|
10
|
+
* - bytes 0-7: magic `"SPSECRT1"` (format tag)
|
|
11
|
+
* - bytes 8-19: 12-byte random IV
|
|
12
|
+
* - bytes 20-35: 16-byte GCM auth tag
|
|
13
|
+
* - bytes 36+: ciphertext (UTF-8 JSON)
|
|
14
|
+
*
|
|
15
|
+
* Security rules enforced here and by this module's consumers:
|
|
16
|
+
* - `listSecrets()` returns ONLY `{ name, createdAt, updatedAt }` — values are
|
|
17
|
+
* never part of any listing.
|
|
18
|
+
* - `getSecretValue()` is INTERNAL: it is consumed only by
|
|
19
|
+
* `getSecretEnvVars()` (env-var injection into the agent's bash tool). It
|
|
20
|
+
* is deliberately NOT re-exported from `./index.js`, so neither the relay
|
|
21
|
+
* handlers nor the LLM-facing tool surface can reach it.
|
|
22
|
+
* - No caller ever logs a value; error messages and listings carry names only.
|
|
23
|
+
*
|
|
24
|
+
* All I/O is synchronous by design: `getSecretEnvVars()` must be callable from
|
|
25
|
+
* the sync `getShellEnv()` in `src/sdk/coding-agent/utils/shell.ts`. The store
|
|
26
|
+
* is tiny (values are capped at 64 KiB each), so sync reads are negligible.
|
|
27
|
+
*
|
|
28
|
+
* Keep this module zero-side-effect on import: nothing touches the filesystem
|
|
29
|
+
* until one of the exported functions is called.
|
|
30
|
+
*/
|
|
31
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
32
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
33
|
+
import { basename, dirname, join } from "node:path";
|
|
34
|
+
import { getConfigDir } from "../config.js";
|
|
35
|
+
/** Secret names: start with a letter, then letters/digits/underscores, max 64 chars. */
|
|
36
|
+
export const SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
37
|
+
/** Maximum UTF-8 byte size of a secret value (64 KiB). */
|
|
38
|
+
export const MAX_SECRET_VALUE_BYTES = 64 * 1024;
|
|
39
|
+
/** 8-byte format tag at the head of the encrypted store. */
|
|
40
|
+
const MAGIC = Buffer.from("SPSECRT1", "utf8");
|
|
41
|
+
const IV_LENGTH = 12;
|
|
42
|
+
const TAG_LENGTH = 16;
|
|
43
|
+
const KEY_LENGTH = 32;
|
|
44
|
+
/** Thrown when a secret name is missing/invalid after normalization. */
|
|
45
|
+
export class SecretNameError extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "SecretNameError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Thrown when a secret value exceeds `MAX_SECRET_VALUE_BYTES`. */
|
|
52
|
+
export class SecretValueError extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "SecretValueError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Thrown when the encrypted store is unreadable, corrupt, or key-mismatched. */
|
|
59
|
+
export class SecretStoreCorruptError extends Error {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "SecretStoreCorruptError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Paths
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
/** Path to the AES-256 key file (32 random bytes). Lazily computed — see `getConfigDir`. */
|
|
69
|
+
export function getSecretKeyPath() {
|
|
70
|
+
return join(getConfigDir(), "secrets.key");
|
|
71
|
+
}
|
|
72
|
+
/** Path to the AES-256-GCM encrypted store. Lazily computed — see `getConfigDir`. */
|
|
73
|
+
export function getSecretDataPath() {
|
|
74
|
+
return join(getConfigDir(), "secrets.json.enc");
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Validation / normalization
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Normalize a secret name per the shared contract: trim surrounding
|
|
81
|
+
* whitespace, uppercase, then validate against `SECRET_NAME_PATTERN`.
|
|
82
|
+
* Throws `SecretNameError` with a generic message — the offending raw input
|
|
83
|
+
* is never echoed back, so it can't leak into logs.
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeSecretName(raw) {
|
|
86
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
87
|
+
throw new SecretNameError("Secret name is required");
|
|
88
|
+
}
|
|
89
|
+
const name = raw.trim().toUpperCase();
|
|
90
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
91
|
+
throw new SecretNameError("Invalid secret name: must start with a letter, contain only letters, digits and underscores, max 64 characters (pattern ^[A-Z][A-Z0-9_]{0,63}$)");
|
|
92
|
+
}
|
|
93
|
+
return name;
|
|
94
|
+
}
|
|
95
|
+
function assertValueSize(value) {
|
|
96
|
+
if (typeof value !== "string") {
|
|
97
|
+
throw new SecretValueError("Secret value must be a string");
|
|
98
|
+
}
|
|
99
|
+
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_VALUE_BYTES) {
|
|
100
|
+
throw new SecretValueError(`Secret value too large: max ${MAX_SECRET_VALUE_BYTES} bytes (64 KiB)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Crypto + file I/O
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
function encrypt(data, key) {
|
|
107
|
+
const iv = randomBytes(IV_LENGTH);
|
|
108
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
109
|
+
const ciphertext = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
110
|
+
const tag = cipher.getAuthTag();
|
|
111
|
+
return Buffer.concat([MAGIC, iv, tag, ciphertext]);
|
|
112
|
+
}
|
|
113
|
+
function decrypt(buf, key) {
|
|
114
|
+
if (buf.length < MAGIC.length + IV_LENGTH + TAG_LENGTH ||
|
|
115
|
+
!buf.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
116
|
+
throw new SecretStoreCorruptError("Unrecognized secret store format");
|
|
117
|
+
}
|
|
118
|
+
const iv = buf.subarray(MAGIC.length, MAGIC.length + IV_LENGTH);
|
|
119
|
+
const tag = buf.subarray(MAGIC.length + IV_LENGTH, MAGIC.length + IV_LENGTH + TAG_LENGTH);
|
|
120
|
+
const ciphertext = buf.subarray(MAGIC.length + IV_LENGTH + TAG_LENGTH);
|
|
121
|
+
try {
|
|
122
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
123
|
+
decipher.setAuthTag(tag);
|
|
124
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new SecretStoreCorruptError("Secret store is corrupt or was encrypted with a different key");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read the EXISTING key file. Throws `SecretStoreCorruptError` when the key
|
|
132
|
+
* is missing or has the wrong size. Deliberately NEVER generates a key: key
|
|
133
|
+
* generation happens on write paths only (`ensureSecretKey`), so a read
|
|
134
|
+
* against encrypted data without a key surfaces the lost-key state instead of
|
|
135
|
+
* silently minting a fresh key that can never decrypt the store.
|
|
136
|
+
*/
|
|
137
|
+
function readSecretKey() {
|
|
138
|
+
const keyPath = getSecretKeyPath();
|
|
139
|
+
let key;
|
|
140
|
+
try {
|
|
141
|
+
key = readFileSync(keyPath);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (err.code === "ENOENT") {
|
|
145
|
+
throw new SecretStoreCorruptError("Secrets key file is missing but encrypted data exists: restore secrets.key or delete the store and re-enter your secrets");
|
|
146
|
+
}
|
|
147
|
+
throw new SecretStoreCorruptError(`Cannot read secrets key: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
if (key.length !== KEY_LENGTH) {
|
|
150
|
+
throw new SecretStoreCorruptError(`Secrets key file is corrupt (expected ${KEY_LENGTH} bytes): delete it and re-enter your secrets`);
|
|
151
|
+
}
|
|
152
|
+
return key;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Return the AES-256 key, generating a new one ONLY when the key file does
|
|
156
|
+
* not exist yet (write paths). An existing key with loose permissions is
|
|
157
|
+
* tightened back to 0600 (defense-in-depth).
|
|
158
|
+
*/
|
|
159
|
+
function ensureSecretKey() {
|
|
160
|
+
const keyPath = getSecretKeyPath();
|
|
161
|
+
mkdirSync(dirname(keyPath), { recursive: true, mode: 0o700 });
|
|
162
|
+
if (existsSync(keyPath)) {
|
|
163
|
+
try {
|
|
164
|
+
chmodSync(keyPath, 0o600);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Best-effort: some filesystems do not support chmod.
|
|
168
|
+
}
|
|
169
|
+
return readSecretKey();
|
|
170
|
+
}
|
|
171
|
+
const key = randomBytes(KEY_LENGTH);
|
|
172
|
+
writeFileSync(keyPath, key, { mode: 0o600 });
|
|
173
|
+
return key;
|
|
174
|
+
}
|
|
175
|
+
function emptyStoreData() {
|
|
176
|
+
return { version: 1, secrets: {} };
|
|
177
|
+
}
|
|
178
|
+
/** Read + decrypt the store. Returns an empty store when the file is missing. Throws on corruption. */
|
|
179
|
+
function readStoreData() {
|
|
180
|
+
const dataPath = getSecretDataPath();
|
|
181
|
+
if (!existsSync(dataPath))
|
|
182
|
+
return emptyStoreData();
|
|
183
|
+
let raw;
|
|
184
|
+
try {
|
|
185
|
+
raw = readFileSync(dataPath);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
throw new SecretStoreCorruptError(`Cannot read secret store: ${err instanceof Error ? err.message : String(err)}`);
|
|
189
|
+
}
|
|
190
|
+
// The read path NEVER generates a key: encrypted data without a key is a
|
|
191
|
+
// lost-key state, and minting a fresh key here would mask it (and touch
|
|
192
|
+
// the filesystem as a side effect of a read). Keys are generated on write
|
|
193
|
+
// paths only (ensureSecretKey via writeStoreData).
|
|
194
|
+
const key = readSecretKey();
|
|
195
|
+
const json = decrypt(raw, key);
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(json);
|
|
198
|
+
if (parsed.version !== 1 || typeof parsed.secrets !== "object" || parsed.secrets === null) {
|
|
199
|
+
throw new SecretStoreCorruptError("Unrecognized secret store payload");
|
|
200
|
+
}
|
|
201
|
+
return { version: 1, secrets: parsed.secrets };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
if (err instanceof SecretStoreCorruptError)
|
|
205
|
+
throw err;
|
|
206
|
+
throw new SecretStoreCorruptError("Unrecognized secret store payload");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Orphaned `.tmp-*` files older than this are removed on the next write. */
|
|
210
|
+
const STALE_TMP_MS = 60_000;
|
|
211
|
+
/**
|
|
212
|
+
* Remove orphaned `secrets.json.enc.tmp-*` files left behind by crashed
|
|
213
|
+
* writers. Only files older than `STALE_TMP_MS` are removed so a concurrent
|
|
214
|
+
* process's in-flight write (tmp file created moments ago) is never deleted.
|
|
215
|
+
*/
|
|
216
|
+
function cleanupStaleTmpFiles(dataPath) {
|
|
217
|
+
let entries;
|
|
218
|
+
try {
|
|
219
|
+
entries = readdirSync(dirname(dataPath));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const prefix = `${basename(dataPath)}.tmp-`;
|
|
225
|
+
const cutoff = Date.now() - STALE_TMP_MS;
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (!entry.startsWith(prefix))
|
|
228
|
+
continue;
|
|
229
|
+
const full = join(dirname(dataPath), entry);
|
|
230
|
+
try {
|
|
231
|
+
if (statSync(full).mtimeMs < cutoff)
|
|
232
|
+
unlinkSync(full);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Ignore: the file vanished (another writer renamed it away).
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Encrypt + atomically write the store (tmp file + rename), mode 0600. */
|
|
240
|
+
function writeStoreData(data) {
|
|
241
|
+
const dataPath = getSecretDataPath();
|
|
242
|
+
const key = ensureSecretKey();
|
|
243
|
+
const payload = encrypt(JSON.stringify(data), key);
|
|
244
|
+
// Unique per-request tmp name: pid alone is shared by every write this
|
|
245
|
+
// process makes, so a crashed write would make the next write silently
|
|
246
|
+
// reuse (and clobber) the orphan. A random hex suffix makes collisions
|
|
247
|
+
// between concurrent writers practically impossible.
|
|
248
|
+
const tmpPath = `${dataPath}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
249
|
+
writeFileSync(tmpPath, payload, { mode: 0o600 });
|
|
250
|
+
try {
|
|
251
|
+
renameSync(tmpPath, dataPath);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
// Best-effort: remove this request's tmp file so failed writes cannot
|
|
255
|
+
// accumulate orphans.
|
|
256
|
+
try {
|
|
257
|
+
unlinkSync(tmpPath);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Already gone.
|
|
261
|
+
}
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
// Defense-in-depth: a pre-existing file with loose permissions gets tightened.
|
|
265
|
+
chmodSync(dataPath, 0o600);
|
|
266
|
+
cleanupStaleTmpFiles(dataPath);
|
|
267
|
+
}
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// mtime-cached env injection
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
/**
|
|
272
|
+
* Cache keyed on the data file's `mtimeMs:size` so external writes (other CLI
|
|
273
|
+
* processes, the relay API) invalidate it without a restart. Our own write
|
|
274
|
+
* path (`setSecret`/`deleteSecret`) refreshes the cache explicitly, covering
|
|
275
|
+
* same-millisecond writes where the mtime would not change.
|
|
276
|
+
*/
|
|
277
|
+
let envCache = null;
|
|
278
|
+
function dataFileCacheKey() {
|
|
279
|
+
try {
|
|
280
|
+
const data = statSync(getSecretDataPath());
|
|
281
|
+
const key = statSync(getSecretKeyPath());
|
|
282
|
+
return `${data.mtimeMs}:${data.size}:${key.mtimeMs}:${key.size}`;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function refreshEnvCache() {
|
|
289
|
+
const cacheKey = dataFileCacheKey();
|
|
290
|
+
const data = readStoreData();
|
|
291
|
+
// PATH is reserved: a secret named PATH would override the shell PATH that
|
|
292
|
+
// getShellEnv() deliberately rebuilds (bin dir injection) and break the
|
|
293
|
+
// bash tool. Stored under that name, it is ignored for env injection only.
|
|
294
|
+
const env = {};
|
|
295
|
+
for (const [name, record] of Object.entries(data.secrets)) {
|
|
296
|
+
if (name.toLowerCase() === "path")
|
|
297
|
+
continue;
|
|
298
|
+
env[name] = record.value;
|
|
299
|
+
}
|
|
300
|
+
envCache = { key: cacheKey ?? "empty", env };
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Return the secret env vars (name → value) for injection into the agent
|
|
304
|
+
* shell environment. Synchronous, mtime-cached: new/updated/deleted secrets
|
|
305
|
+
* are picked up on the next call without a CLI restart.
|
|
306
|
+
*
|
|
307
|
+
* Failure modes degrade gracefully — a corrupt store, missing key, or unreadable
|
|
308
|
+
* file returns `{}` instead of throwing, because `getShellEnv()` is sync and
|
|
309
|
+
* must never break bash spawning. No values are ever logged.
|
|
310
|
+
*/
|
|
311
|
+
export function getSecretEnvVars() {
|
|
312
|
+
try {
|
|
313
|
+
const cacheKey = dataFileCacheKey();
|
|
314
|
+
if (cacheKey === null) {
|
|
315
|
+
return {};
|
|
316
|
+
}
|
|
317
|
+
if (envCache && envCache.key === cacheKey) {
|
|
318
|
+
return envCache.env;
|
|
319
|
+
}
|
|
320
|
+
refreshEnvCache();
|
|
321
|
+
return envCache?.env ?? {};
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Public API
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
/**
|
|
331
|
+
* Create or update a secret. Returns the metadata-only view (never the value).
|
|
332
|
+
* Throws `SecretNameError` for invalid names, `SecretValueError` for oversized
|
|
333
|
+
* values, and `SecretStoreCorruptError` when the store cannot be read/written.
|
|
334
|
+
*
|
|
335
|
+
* Fully synchronous (no awaits): the read-modify-write below is atomic per
|
|
336
|
+
* event-loop turn, so concurrent relay POSTs in one process can never
|
|
337
|
+
* interleave — no mutex is needed. Cross-process writers are isolated by the
|
|
338
|
+
* unique per-request tmp name + rename in `writeStoreData`; the last rename
|
|
339
|
+
* wins and every writer re-reads the store on its next call.
|
|
340
|
+
*/
|
|
341
|
+
export function setSecret(rawName, value) {
|
|
342
|
+
const name = normalizeSecretName(rawName);
|
|
343
|
+
assertValueSize(value);
|
|
344
|
+
const data = readStoreData();
|
|
345
|
+
const now = new Date().toISOString();
|
|
346
|
+
const existing = data.secrets[name];
|
|
347
|
+
data.secrets[name] = {
|
|
348
|
+
value,
|
|
349
|
+
createdAt: existing?.createdAt ?? now,
|
|
350
|
+
updatedAt: now,
|
|
351
|
+
};
|
|
352
|
+
writeStoreData(data);
|
|
353
|
+
refreshEnvCache();
|
|
354
|
+
return { name, createdAt: data.secrets[name].createdAt, updatedAt: now };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Delete a secret. Returns true when a record was removed, false when the
|
|
358
|
+
* name was not stored. Throws `SecretNameError` for invalid names.
|
|
359
|
+
*/
|
|
360
|
+
export function deleteSecret(rawName) {
|
|
361
|
+
const name = normalizeSecretName(rawName);
|
|
362
|
+
const data = readStoreData();
|
|
363
|
+
if (!Object.prototype.hasOwnProperty.call(data.secrets, name)) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
delete data.secrets[name];
|
|
367
|
+
writeStoreData(data);
|
|
368
|
+
refreshEnvCache();
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* List secrets as metadata only — `{ name, createdAt, updatedAt }`. Values are
|
|
373
|
+
* never part of the returned shape (this is the source for the relay GET
|
|
374
|
+
* response and the `list_secrets` agent tool).
|
|
375
|
+
*/
|
|
376
|
+
export function listSecrets() {
|
|
377
|
+
const data = readStoreData();
|
|
378
|
+
return Object.entries(data.secrets)
|
|
379
|
+
.map(([name, record]) => ({
|
|
380
|
+
name,
|
|
381
|
+
createdAt: record.createdAt,
|
|
382
|
+
updatedAt: record.updatedAt,
|
|
383
|
+
}))
|
|
384
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* INTERNAL — read a single secret value. Used ONLY for env-var injection via
|
|
388
|
+
* `getSecretEnvVars()`. Deliberately not re-exported from `./index.js` so it
|
|
389
|
+
* cannot be reached from the relay handlers or the LLM-facing tool surface.
|
|
390
|
+
* Never log the return value.
|
|
391
|
+
*/
|
|
392
|
+
export function getSecretValue(rawName) {
|
|
393
|
+
const name = normalizeSecretName(rawName);
|
|
394
|
+
const data = readStoreData();
|
|
395
|
+
return data.secrets[name]?.value;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Remove the store + key (test helper and future "reset secrets" support).
|
|
399
|
+
* Returns true when either file was deleted.
|
|
400
|
+
*/
|
|
401
|
+
export function resetSecretStore() {
|
|
402
|
+
let removed = false;
|
|
403
|
+
for (const path of [getSecretDataPath(), getSecretKeyPath()]) {
|
|
404
|
+
try {
|
|
405
|
+
unlinkSync(path);
|
|
406
|
+
removed = true;
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
if (err.code !== "ENOENT")
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
envCache = null;
|
|
414
|
+
return removed;
|
|
415
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAaH,OAAO,EAQL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACtB,MAAM,8BAA8B,CAAC;AAatC,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -63,6 +63,7 @@ import { BrowserService } from "../extensions/browser/browser-service.js";
|
|
|
63
63
|
import subagentExt from "../agent/index.js";
|
|
64
64
|
import designerExtension from "../designer/index.js";
|
|
65
65
|
import logicTraceExtension from "../extensions/logic-trace.js";
|
|
66
|
+
import listSecretsExtension from "../extensions/list-secrets.js";
|
|
66
67
|
import observationalMemory from "../memory/index.js";
|
|
67
68
|
import { OBSERVATIONAL_MEMORY_CONTEXT_CUSTOM_TYPE, OBSERVATIONAL_MEMORY_SNAPSHOT_CUSTOM_TYPE, } from "../memory/types.js";
|
|
68
69
|
import { fetchAllowedModels as defaultFetchAllowedModels, } from "../relay/models-fetch.js";
|
|
@@ -368,6 +369,12 @@ export class AgentBridge {
|
|
|
368
369
|
async (ext) => {
|
|
369
370
|
observationalMemory(ext);
|
|
370
371
|
},
|
|
372
|
+
// list_secrets: metadata-only view of the local encrypted secret store
|
|
373
|
+
// (names + timestamps). Values are never returned to the LLM — they are
|
|
374
|
+
// injected as env vars into the bash tool via getShellEnv().
|
|
375
|
+
async (ext) => {
|
|
376
|
+
listSecretsExtension(ext);
|
|
377
|
+
},
|
|
371
378
|
];
|
|
372
379
|
// Always register subagent tool — execute() checks settings from disk
|
|
373
380
|
// on every invocation, so toggling takes effect immediately (no restart).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST handlers for the local secret store.
|
|
3
|
+
*
|
|
4
|
+
* Contract (shared with the landing frontend):
|
|
5
|
+
* - `GET /api/secrets` → `{ secrets: SecretMeta[] }` — metadata only,
|
|
6
|
+
* values are NEVER returned by anything.
|
|
7
|
+
* - `POST /api/secrets` → `{ secret: SecretMeta }` — body
|
|
8
|
+
* `{ name, value }`.
|
|
9
|
+
* - `DELETE /api/secrets/:name` → `{ ok: true }`.
|
|
10
|
+
*
|
|
11
|
+
* Name normalization/validation and the 64 KiB value cap live in
|
|
12
|
+
* `src/secrets/store.ts`. Errors follow the existing handler error shape
|
|
13
|
+
* (`BadRequestError`/`NotFoundError` → 400/404 in the relay rest loop).
|
|
14
|
+
*
|
|
15
|
+
* SECURITY: no handler here (or anywhere in the relay loop) logs a request or
|
|
16
|
+
* response value. Only names appear in error messages, and only after they
|
|
17
|
+
* have passed `normalizeSecretName` (letters/digits/underscores).
|
|
18
|
+
*/
|
|
19
|
+
import { type SecretMeta } from "../../secrets/index.js";
|
|
20
|
+
/**
|
|
21
|
+
* `GET /api/secrets` — list secrets as metadata only.
|
|
22
|
+
*/
|
|
23
|
+
export declare function handleListSecrets(): {
|
|
24
|
+
secrets: SecretMeta[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* `POST /api/secrets` — create or update a secret. Body: `{ name, value }`.
|
|
28
|
+
* Returns the metadata-only view; the stored value is never echoed back.
|
|
29
|
+
*/
|
|
30
|
+
export declare function handleSetSecret(body: Record<string, unknown>): {
|
|
31
|
+
secret: SecretMeta;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* `DELETE /api/secrets/:name` — delete a secret. Throws `NotFoundError` (404)
|
|
35
|
+
* when the name is not stored.
|
|
36
|
+
*/
|
|
37
|
+
export declare function handleDeleteSecret(rawName: string): {
|
|
38
|
+
ok: true;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAON,KAAK,UAAU,EACf,MAAM,wBAAwB,CAAC;AAUhC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI;IAAE,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,CAE7D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAerF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAiBhE"}
|