@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
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
|
+
}
|
package/dist/relay/routing.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAKvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAmDzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/routing.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
257
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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":"
|
|
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
|
-
|
|
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";
|