@aexol/spectral 0.9.245 → 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 CHANGED
@@ -39,6 +39,7 @@ function printHeader() {
39
39
  " spectral desktop Start the agent in the background and open the UI",
40
40
  " spectral bind Link this directory to an Aexol Studio project",
41
41
  " spectral unbind Remove the Aexol Studio project binding",
42
+ " spectral secret Manage local secrets (set/list/rm)",
42
43
  " spectral update Check for and install the latest Spectral version",
43
44
  " spectral ports Diagnose port collisions across local projects",
44
45
  " spectral --version Print version and exit",
@@ -135,6 +136,11 @@ async function main() {
135
136
  await runUnbind();
136
137
  process.exit(0);
137
138
  }
139
+ if (first === "secret") {
140
+ const { runSecret } = await import("./commands/secret.js");
141
+ await runSecret(args.slice(1));
142
+ process.exit(0);
143
+ }
138
144
  if (first === "update") {
139
145
  const { runUpdate } = await import("./commands/update.js");
140
146
  await runUpdate(args.slice(1));
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `spectral secret` — manage the local encrypted secret store.
3
+ *
4
+ * Usage:
5
+ * spectral secret set NAME create/update a secret; value is read from
6
+ * STDIN (piped or hidden interactive prompt) and
7
+ * is NEVER passed as a command-line argument
8
+ * spectral secret list list names + timestamps — values are never shown
9
+ * spectral secret rm NAME delete a secret
10
+ *
11
+ * Shares the exact same store as the /api/secrets relay API and the
12
+ * `list_secrets` agent tool. Secrets set here are injected as env vars into
13
+ * the agent's bash tool (see src/secrets/store.ts).
14
+ */
15
+ export declare function runSecret(args: string[]): Promise<void>;
16
+ //# sourceMappingURL=secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../../src/commands/secret.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAsEH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyG7D"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `spectral secret` — manage the local encrypted secret store.
3
+ *
4
+ * Usage:
5
+ * spectral secret set NAME create/update a secret; value is read from
6
+ * STDIN (piped or hidden interactive prompt) and
7
+ * is NEVER passed as a command-line argument
8
+ * spectral secret list list names + timestamps — values are never shown
9
+ * spectral secret rm NAME delete a secret
10
+ *
11
+ * Shares the exact same store as the /api/secrets relay API and the
12
+ * `list_secrets` agent tool. Secrets set here are injected as env vars into
13
+ * the agent's bash tool (see src/secrets/store.ts).
14
+ */
15
+ import { createInterface } from "node:readline";
16
+ import { Writable } from "node:stream";
17
+ import pc from "picocolors";
18
+ import { SecretNameError, SecretStoreCorruptError, SecretValueError, deleteSecret, listSecrets, setSecret, } from "../secrets/index.js";
19
+ /**
20
+ * Read the secret value from STDIN. Never from argv — argv is visible in
21
+ * shell history and the process table.
22
+ *
23
+ * - Piped input: the entire stdin is consumed; a single trailing newline
24
+ * (from `echo "value" | …`) is stripped, everything else is preserved.
25
+ * - Interactive TTY: a single line is read with the typed characters hidden
26
+ * (readline with a swallowing output stream).
27
+ */
28
+ function readValueFromStdin(name) {
29
+ return new Promise((resolve, reject) => {
30
+ if (process.stdin.isTTY) {
31
+ // Hide input: swallow every echoed chunk; the prompt goes to real stdout.
32
+ const muted = new Writable({
33
+ write(_chunk, _enc, cb) {
34
+ cb();
35
+ },
36
+ });
37
+ const rl = createInterface({ input: process.stdin, output: muted, terminal: true });
38
+ process.stdout.write(`Enter value for ${name} (input hidden): `);
39
+ rl.question("", (answer) => {
40
+ rl.close();
41
+ process.stdout.write("\n");
42
+ resolve(answer);
43
+ });
44
+ rl.on("error", reject);
45
+ return;
46
+ }
47
+ const chunks = [];
48
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
49
+ process.stdin.on("error", reject);
50
+ process.stdin.on("end", () => {
51
+ const raw = Buffer.concat(chunks).toString("utf8");
52
+ // Strip exactly one trailing newline/CR from `echo`-style pipes; keep
53
+ // any other whitespace — the value is preserved verbatim otherwise.
54
+ const value = raw.replace(/\r?\n$/, "");
55
+ resolve(value);
56
+ });
57
+ });
58
+ }
59
+ function fail(err) {
60
+ if (err instanceof SecretNameError ||
61
+ err instanceof SecretValueError ||
62
+ err instanceof SecretStoreCorruptError) {
63
+ process.stderr.write(pc.red(`${err.message}\n`));
64
+ process.exit(1);
65
+ }
66
+ process.stderr.write(pc.red(`Secret operation failed: ${err instanceof Error ? err.message : String(err)}\n`));
67
+ process.exit(1);
68
+ }
69
+ export async function runSecret(args) {
70
+ const sub = args[0];
71
+ if (sub === "set") {
72
+ const nameArg = args[1];
73
+ if (!nameArg || nameArg.trim() === "") {
74
+ process.stderr.write(pc.red('Usage: spectral secret set NAME (value is read from STDIN, e.g. `echo -n "value" | spectral secret set NAME`)\n'));
75
+ process.exit(1);
76
+ }
77
+ let value;
78
+ try {
79
+ value = await readValueFromStdin(nameArg.trim());
80
+ }
81
+ catch (err) {
82
+ fail(err);
83
+ }
84
+ if (value.length === 0) {
85
+ process.stderr.write(pc.red("Secret value is empty — pipe a non-empty value on STDIN, e.g. `echo -n \"value\" | spectral secret set NAME`\n"));
86
+ process.exit(1);
87
+ }
88
+ try {
89
+ const meta = setSecret(nameArg.trim(), value);
90
+ process.stdout.write(pc.green(`✓ Secret ${meta.name} stored (encrypted at rest, injected as env var into the agent bash tool)\n`));
91
+ }
92
+ catch (err) {
93
+ fail(err);
94
+ }
95
+ return;
96
+ }
97
+ if (sub === "list") {
98
+ try {
99
+ const secrets = listSecrets();
100
+ if (secrets.length === 0) {
101
+ process.stdout.write("No secrets stored.\n");
102
+ return;
103
+ }
104
+ const nameW = Math.max(5, ...secrets.map((s) => s.name.length));
105
+ process.stdout.write([
106
+ `${secrets.length} stored secret(s) (values are never shown):`,
107
+ "",
108
+ `${"NAME".padEnd(nameW)} Created Updated`,
109
+ `${"─".repeat(nameW)} ${"─".repeat(19)} ${"─".repeat(19)}`,
110
+ ...secrets.map((s) => `${s.name.padEnd(nameW)} ${s.createdAt.padEnd(19)} ${s.updatedAt}`),
111
+ ].join("\n") + "\n");
112
+ }
113
+ catch (err) {
114
+ fail(err);
115
+ }
116
+ return;
117
+ }
118
+ if (sub === "rm") {
119
+ const nameArg = args[1];
120
+ if (!nameArg || nameArg.trim() === "") {
121
+ process.stderr.write(pc.red("Usage: spectral secret rm NAME\n"));
122
+ process.exit(1);
123
+ }
124
+ try {
125
+ const deleted = deleteSecret(nameArg.trim());
126
+ if (!deleted) {
127
+ process.stderr.write(pc.red(`Secret ${nameArg.trim().toUpperCase()} not found\n`));
128
+ process.exit(1);
129
+ }
130
+ process.stdout.write(pc.green(`✓ Secret ${nameArg.trim().toUpperCase()} removed\n`));
131
+ }
132
+ catch (err) {
133
+ fail(err);
134
+ }
135
+ return;
136
+ }
137
+ if (sub === "--help" || sub === "-h") {
138
+ process.stdout.write([
139
+ "Usage:",
140
+ " spectral secret set NAME create/update a secret (value read from STDIN, never argv)",
141
+ " spectral secret list list names + timestamps (values are never shown)",
142
+ " spectral secret rm NAME delete a secret",
143
+ "",
144
+ "Stored secrets are encrypted at rest (AES-256-GCM) and injected as env vars",
145
+ "into the agent's bash tool. Manage them remotely via the /api/secrets API.",
146
+ "",
147
+ ].join("\n"));
148
+ process.exit(0);
149
+ }
150
+ process.stderr.write(pc.red(`Unknown secret subcommand: ${sub ?? ""}\n`) +
151
+ pc.dim("Use `spectral secret --help` for usage.\n"));
152
+ process.exit(1);
153
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `list_secrets` tool extension.
3
+ *
4
+ * Lets the agent see which local secrets are stored (names + timestamps) so it
5
+ * can reason about what env vars will be injected into its bash tool.
6
+ *
7
+ * SECURITY: this tool returns ONLY names and timestamps. Secret values are
8
+ * never returned by this tool, by the relay GET API, or by anything else —
9
+ * values exist solely as env vars injected into the agent's bash environment
10
+ * (see src/secrets/store.ts `getSecretEnvVars`).
11
+ */
12
+ import type { ExtensionAPI } from "../sdk/coding-agent/index.js";
13
+ export default function listSecretsExtension(ext: ExtensionAPI): void;
14
+ //# sourceMappingURL=list-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-secrets.d.ts","sourceRoot":"","sources":["../../src/extensions/list-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,8BAA8B,CAAC;AAGnF,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CA4DpE"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `list_secrets` tool extension.
3
+ *
4
+ * Lets the agent see which local secrets are stored (names + timestamps) so it
5
+ * can reason about what env vars will be injected into its bash tool.
6
+ *
7
+ * SECURITY: this tool returns ONLY names and timestamps. Secret values are
8
+ * never returned by this tool, by the relay GET API, or by anything else —
9
+ * values exist solely as env vars injected into the agent's bash environment
10
+ * (see src/secrets/store.ts `getSecretEnvVars`).
11
+ */
12
+ import { getSecretEnvVars, listSecrets } from "../secrets/index.js";
13
+ export default function listSecretsExtension(ext) {
14
+ ext.registerTool({
15
+ name: "list_secrets",
16
+ label: "List Secrets",
17
+ description: "List the locally stored secret env vars that will be injected into your bash tool environment " +
18
+ "(e.g. GITHUB_TOKEN). Returns ONLY names and timestamps — secret values are never returned by " +
19
+ "this tool or any other; they exist solely as environment variables in your bash sessions. " +
20
+ "Manage secrets with the `spectral secret` CLI command or the /api/secrets relay API.",
21
+ parameters: {
22
+ type: "object",
23
+ properties: {},
24
+ },
25
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
26
+ try {
27
+ const secrets = listSecrets();
28
+ if (secrets.length === 0) {
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: "No secrets stored. Add one with `spectral secret set NAME` (value read from STDIN) " +
34
+ "or POST /api/secrets. Stored secrets are injected as env vars into your bash tool.",
35
+ },
36
+ ],
37
+ details: { isError: false },
38
+ };
39
+ }
40
+ const envNames = Object.keys(getSecretEnvVars());
41
+ const lines = [
42
+ `${secrets.length} stored secret(s) (values are never returned — they are injected as env vars):`,
43
+ "",
44
+ ...secrets.map((s) => ` ${s.name} created ${s.createdAt} updated ${s.updatedAt}` +
45
+ (envNames.includes(s.name) ? "" : " (not injected: reserved name)")),
46
+ "",
47
+ "These are available as environment variables in your bash tool sessions.",
48
+ ];
49
+ return {
50
+ content: [{ type: "text", text: lines.join("\n") }],
51
+ details: { isError: false },
52
+ };
53
+ }
54
+ catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err);
56
+ return {
57
+ content: [{ type: "text", text: `Error reading secrets: ${msg}` }],
58
+ details: { isError: true },
59
+ };
60
+ }
61
+ },
62
+ });
63
+ }
@@ -20,7 +20,7 @@ import { type WizardLoginResult } from "./wizard-login.js";
20
20
  * `id` is populated when the route had an `:id` placeholder.
21
21
  */
22
22
  interface RouteMatch {
23
- route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server";
23
+ route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server" | "list_secrets" | "create_secret" | "delete_secret";
24
24
  id?: string;
25
25
  /** Parsed query params, if the path carried a `?...` suffix. */
26
26
  query?: URLSearchParams;
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,26 @@ import { handleGetAgentSettingsAsync, handleListAgents, handlePutAgentSettingsAs
25
25
  import { handleGetSettingsAsync, handlePutSettingsAsync, } from "../server/handlers/settings.js";
26
26
  import { handleGetPromptMutationSettings, handlePutPromptMutationSettings, } from "../server/handlers/prompt-mutation-settings.js";
27
27
  import { handleKillDevProcess, handleKillDetectedDevServer, handleListDevProcesses, handleRestartDevProcess, handleScanDevServers, handleStartDevProcess, } from "../server/handlers/dev-processes.js";
28
+ import { handleDeleteSecret, handleListSecrets, handleSetSecret, } from "../server/handlers/secrets.js";
28
29
  import { startWizardLogin } from "./wizard-login.js";
30
+ // ---------------------------------------------------------------------------
31
+ // Path matching
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * `decodeURIComponent` that never throws. Malformed percent-encoding (e.g.
35
+ * `%ZZ`) returns `null` instead of a URIError escaping `matchRoute` — the
36
+ * matcher sits OUTSIDE the REST try/catch, so a synchronous throw here would
37
+ * surface as an unhandled rejection and could take down `spectral serve`.
38
+ * Callers treat `null` as no-match → 404.
39
+ */
40
+ function decodeSegment(raw) {
41
+ try {
42
+ return decodeURIComponent(raw);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
29
48
  /**
30
49
  * Inline path matcher. Returns `null` for any path/method combination we
31
50
  * don't recognise; the caller turns that into a `404 Unknown route`.
@@ -134,7 +153,9 @@ export function matchRoute(method, path) {
134
153
  // /api/projects/:id/files/search
135
154
  const projectFilesMatch = /^\/api\/projects\/([^/]+)\/files\/search$/.exec(cleanPath);
136
155
  if (projectFilesMatch) {
137
- const id = decodeURIComponent(projectFilesMatch[1]);
156
+ const id = decodeSegment(projectFilesMatch[1]);
157
+ if (id === null)
158
+ return null;
138
159
  if (method === "GET")
139
160
  return { route: "search_project_files", id, query };
140
161
  return null;
@@ -142,14 +163,18 @@ export function matchRoute(method, path) {
142
163
  // /api/projects/:id/bind-studio and /api/projects/:id/unbind-studio
143
164
  const bindStudioMatch = /^\/api\/projects\/([^/]+)\/bind-studio$/.exec(cleanPath);
144
165
  if (bindStudioMatch) {
145
- const id = decodeURIComponent(bindStudioMatch[1]);
166
+ const id = decodeSegment(bindStudioMatch[1]);
167
+ if (id === null)
168
+ return null;
146
169
  if (method === "POST")
147
170
  return { route: "bind_studio", id };
148
171
  return null;
149
172
  }
150
173
  const unbindStudioMatch = /^\/api\/projects\/([^/]+)\/unbind-studio$/.exec(cleanPath);
151
174
  if (unbindStudioMatch) {
152
- const id = decodeURIComponent(unbindStudioMatch[1]);
175
+ const id = decodeSegment(unbindStudioMatch[1]);
176
+ if (id === null)
177
+ return null;
153
178
  if (method === "POST")
154
179
  return { route: "unbind_studio", id };
155
180
  return null;
@@ -157,7 +182,9 @@ export function matchRoute(method, path) {
157
182
  // /api/projects/:id/observations
158
183
  const projectObsMatch = /^\/api\/projects\/([^/]+)\/observations$/.exec(cleanPath);
159
184
  if (projectObsMatch) {
160
- const id = decodeURIComponent(projectObsMatch[1]);
185
+ const id = decodeSegment(projectObsMatch[1]);
186
+ if (id === null)
187
+ return null;
161
188
  if (method === "GET")
162
189
  return { route: "get_project_observations", id };
163
190
  return null;
@@ -165,7 +192,9 @@ export function matchRoute(method, path) {
165
192
  // /api/projects/:id and /api/projects/:id/sessions
166
193
  const projectMatch = /^\/api\/projects\/([^/]+)(\/sessions)?$/.exec(cleanPath);
167
194
  if (projectMatch) {
168
- const id = decodeURIComponent(projectMatch[1]);
195
+ const id = decodeSegment(projectMatch[1]);
196
+ if (id === null)
197
+ return null;
169
198
  const isSessions = projectMatch[2] === "/sessions";
170
199
  if (isSessions) {
171
200
  if (method === "GET")
@@ -181,7 +210,9 @@ export function matchRoute(method, path) {
181
210
  // /api/projects/:id/cleanup-sessions
182
211
  const projectCleanupMatch = /^\/api\/projects\/([^/]+)\/cleanup-sessions$/.exec(cleanPath);
183
212
  if (projectCleanupMatch) {
184
- const id = decodeURIComponent(projectCleanupMatch[1]);
213
+ const id = decodeSegment(projectCleanupMatch[1]);
214
+ if (id === null)
215
+ return null;
185
216
  if (method === "POST")
186
217
  return { route: "cleanup_project_sessions", id };
187
218
  return null;
@@ -196,14 +227,18 @@ export function matchRoute(method, path) {
196
227
  // /api/sessions/:id
197
228
  const sessionMessagesMatch = /^\/api\/sessions\/([^/]+)\/messages$/.exec(cleanPath);
198
229
  if (sessionMessagesMatch) {
199
- const id = decodeURIComponent(sessionMessagesMatch[1]);
230
+ const id = decodeSegment(sessionMessagesMatch[1]);
231
+ if (id === null)
232
+ return null;
200
233
  if (method === "GET")
201
234
  return { route: "get_session_messages", id, query };
202
235
  return null;
203
236
  }
204
237
  const sessionMatch = /^\/api\/sessions\/([^/]+)$/.exec(cleanPath);
205
238
  if (sessionMatch) {
206
- const id = decodeURIComponent(sessionMatch[1]);
239
+ const id = decodeSegment(sessionMatch[1]);
240
+ if (id === null)
241
+ return null;
207
242
  if (method === "GET")
208
243
  return { route: "get_session", id };
209
244
  if (method === "PATCH")
@@ -214,7 +249,9 @@ export function matchRoute(method, path) {
214
249
  }
215
250
  const sessionAgentMatch = /^\/api\/sessions\/([^/]+)\/active-agent$/.exec(cleanPath);
216
251
  if (sessionAgentMatch) {
217
- const id = decodeURIComponent(sessionAgentMatch[1]);
252
+ const id = decodeSegment(sessionAgentMatch[1]);
253
+ if (id === null)
254
+ return null;
218
255
  if (method === "GET")
219
256
  return { route: "get_session_active_agent", id };
220
257
  if (method === "PUT")
@@ -224,7 +261,9 @@ export function matchRoute(method, path) {
224
261
  // /api/sessions/:id/memory/details and /api/sessions/:id/memory and /api/sessions/:id/compact
225
262
  const sessionActionMatch = /^\/api\/sessions\/([^/]+)\/(memory(?:\/details)?|compact)$/.exec(cleanPath);
226
263
  if (sessionActionMatch) {
227
- const id = decodeURIComponent(sessionActionMatch[1]);
264
+ const id = decodeSegment(sessionActionMatch[1]);
265
+ if (id === null)
266
+ return null;
228
267
  const action = sessionActionMatch[2];
229
268
  if (action === "memory" && method === "GET")
230
269
  return { route: "get_session_memory", id };
@@ -237,7 +276,9 @@ export function matchRoute(method, path) {
237
276
  // /api/sessions/:id/remember-and-delete
238
277
  const rememberDeleteMatch = /^\/api\/sessions\/([^/]+)\/remember-and-delete$/.exec(cleanPath);
239
278
  if (rememberDeleteMatch) {
240
- const id = decodeURIComponent(rememberDeleteMatch[1]);
279
+ const id = decodeSegment(rememberDeleteMatch[1]);
280
+ if (id === null)
281
+ return null;
241
282
  if (method === "POST")
242
283
  return { route: "remember_and_delete_session", id };
243
284
  return null;
@@ -245,7 +286,9 @@ export function matchRoute(method, path) {
245
286
  // /api/sessions/:id/fork
246
287
  const forkMatch = /^\/api\/sessions\/([^/]+)\/fork$/.exec(cleanPath);
247
288
  if (forkMatch) {
248
- const id = decodeURIComponent(forkMatch[1]);
289
+ const id = decodeSegment(forkMatch[1]);
290
+ if (id === null)
291
+ return null;
249
292
  if (method === "POST")
250
293
  return { route: "fork_session", id };
251
294
  return null;
@@ -253,8 +296,12 @@ export function matchRoute(method, path) {
253
296
  // /api/sessions/:id/queue and /api/sessions/:id/queue/:itemId
254
297
  const queueMatch = /^\/api\/sessions\/([^/]+)\/queue(\/([^/]+))?$/.exec(cleanPath);
255
298
  if (queueMatch) {
256
- const id = decodeURIComponent(queueMatch[1]);
257
- const itemId = queueMatch[3] ? decodeURIComponent(queueMatch[3]) : undefined;
299
+ const id = decodeSegment(queueMatch[1]);
300
+ if (id === null)
301
+ return null;
302
+ const itemId = queueMatch[3] ? decodeSegment(queueMatch[3]) : undefined;
303
+ if (itemId === null)
304
+ return null;
258
305
  if (!itemId) {
259
306
  if (method === "GET")
260
307
  return { route: "get_prompt_queue", id };
@@ -279,14 +326,18 @@ export function matchRoute(method, path) {
279
326
  }
280
327
  const restartDevProcessMatch = /^\/api\/dev-processes\/([^/]+)\/restart$/.exec(cleanPath);
281
328
  if (restartDevProcessMatch) {
282
- const id = decodeURIComponent(restartDevProcessMatch[1]);
329
+ const id = decodeSegment(restartDevProcessMatch[1]);
330
+ if (id === null)
331
+ return null;
283
332
  if (method === "POST")
284
333
  return { route: "restart_dev_process", id };
285
334
  return null;
286
335
  }
287
336
  const devProcessMatch = /^\/api\/dev-processes\/([^/]+)$/.exec(cleanPath);
288
337
  if (devProcessMatch) {
289
- const id = decodeURIComponent(devProcessMatch[1]);
338
+ const id = decodeSegment(devProcessMatch[1]);
339
+ if (id === null)
340
+ return null;
290
341
  if (method === "DELETE")
291
342
  return { route: "kill_dev_process", id };
292
343
  return null;
@@ -299,7 +350,9 @@ export function matchRoute(method, path) {
299
350
  }
300
351
  const devServerMatch = /^\/api\/dev-servers\/([^/]+)$/.exec(cleanPath);
301
352
  if (devServerMatch) {
302
- const id = decodeURIComponent(devServerMatch[1]);
353
+ const id = decodeSegment(devServerMatch[1]);
354
+ if (id === null)
355
+ return null;
303
356
  if (method === "DELETE") {
304
357
  const rawPort = query?.get("port");
305
358
  const parsedPort = rawPort === null ? Number.NaN : Number(rawPort);
@@ -310,6 +363,23 @@ export function matchRoute(method, path) {
310
363
  }
311
364
  return null;
312
365
  }
366
+ // /api/secrets and /api/secrets/:name
367
+ if (cleanPath === "/api/secrets") {
368
+ if (method === "GET")
369
+ return { route: "list_secrets" };
370
+ if (method === "POST")
371
+ return { route: "create_secret" };
372
+ return null;
373
+ }
374
+ const secretMatch = /^\/api\/secrets\/([^/]+)$/.exec(cleanPath);
375
+ if (secretMatch) {
376
+ const id = decodeSegment(secretMatch[1]);
377
+ if (id === null)
378
+ return null;
379
+ if (method === "DELETE")
380
+ return { route: "delete_secret", id };
381
+ return null;
382
+ }
313
383
  return null;
314
384
  }
315
385
  /**
@@ -646,6 +716,12 @@ export async function dispatchRoute(match, body, deps) {
646
716
  return handleScanDevServers(store, deps.devProcessRegistry);
647
717
  case "kill_dev_server":
648
718
  return await handleKillDetectedDevServer(store, Number(id), match.port);
719
+ case "list_secrets":
720
+ return handleListSecrets();
721
+ case "create_secret":
722
+ return handleSetSecret(asObject(body));
723
+ case "delete_secret":
724
+ return handleDeleteSecret(id);
649
725
  }
650
726
  }
651
727
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../../../src/sdk/coding-agent/utils/shell.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CACf;AAyCD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAsB/C;AAED;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAU3D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD"}
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../../../src/sdk/coding-agent/utils/shell.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CACf;AAyCD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAmC/C;AAED;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAU3D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD"}
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { delimiter } from "node:path";
3
3
  import { spawn, spawnSync } from "child_process";
4
4
  import { getBinDir } from "../config.js";
5
+ import { getSecretEnvVars } from "../../../secrets/index.js";
5
6
  /**
6
7
  * Find bash executable on PATH (cross-platform)
7
8
  */
@@ -100,7 +101,7 @@ export function getShellEnv() {
100
101
  const pathEntries = currentPath.split(delimiter).filter(Boolean);
101
102
  const hasBinDir = pathEntries.includes(binDir);
102
103
  const updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);
103
- return {
104
+ const env = {
104
105
  ...process.env,
105
106
  [pathKey]: updatedPath,
106
107
  // Agent shell hygiene (mirrors mini-swe-agent / SWE-agent environments):
@@ -114,6 +115,19 @@ export function getShellEnv() {
114
115
  TQDM_DISABLE: "1",
115
116
  GIT_TERMINAL_PROMPT: "0",
116
117
  };
118
+ // Secret env-var injection (see src/secrets/store.ts). Synchronous,
119
+ // mtime-cached read of the encrypted store, so new/updated/deleted secrets
120
+ // are picked up on the next bash spawn without an agent restart. Secrets
121
+ // are applied on top of the base environment and intentionally win over any
122
+ // same-named variable from process.env — the store is the managed source.
123
+ // PATH is excluded by the store itself (a secret named PATH would override
124
+ // the rebuilt PATH above and break the bin dir injection). Failures degrade
125
+ // to the base env: spawning must never break, and no values are ever logged.
126
+ const secretEnv = getSecretEnvVars();
127
+ const secretKeys = Object.keys(secretEnv);
128
+ if (secretKeys.length === 0)
129
+ return env;
130
+ return { ...env, ...secretEnv };
117
131
  }
118
132
  /**
119
133
  * Platform-specific instructions injected into the system prompt.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public surface of the local encrypted secret store.
3
+ *
4
+ * Note what is deliberately NOT exported here: `getSecretValue()` (internal —
5
+ * used only for env-var injection) and `resetSecretStore()` (test/reset
6
+ * helper). Consumers importing from this index can never expose secret values:
7
+ * every export either returns metadata-only views or the raw env map consumed
8
+ * by the bash tool's shell environment.
9
+ */
10
+ export { MAX_SECRET_VALUE_BYTES, SECRET_NAME_PATTERN, SecretNameError, SecretStoreCorruptError, SecretValueError, deleteSecret, getSecretDataPath, getSecretEnvVars, getSecretKeyPath, listSecrets, normalizeSecretName, setSecret, type SecretMeta, } from "./store.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACN,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,mBAAmB,EACnB,SAAS,EACT,KAAK,UAAU,GACf,MAAM,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public surface of the local encrypted secret store.
3
+ *
4
+ * Note what is deliberately NOT exported here: `getSecretValue()` (internal —
5
+ * used only for env-var injection) and `resetSecretStore()` (test/reset
6
+ * helper). Consumers importing from this index can never expose secret values:
7
+ * every export either returns metadata-only views or the raw env map consumed
8
+ * by the bash tool's shell environment.
9
+ */
10
+ export { MAX_SECRET_VALUE_BYTES, SECRET_NAME_PATTERN, SecretNameError, SecretStoreCorruptError, SecretValueError, deleteSecret, getSecretDataPath, getSecretEnvVars, getSecretKeyPath, listSecrets, normalizeSecretName, setSecret, } from "./store.js";