kritya 0.8.61-beta → 0.8.63-beta
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/agent/memory.js +9 -0
- package/dist/agent/toolExecutor.js +34 -0
- package/dist/permissions/recon.js +162 -0
- package/dist/tools/common.js +1 -1
- package/dist/tools/document.js +19 -0
- package/dist/tools/notebook.js +10 -0
- package/dist/tools/secretScan.js +28 -0
- package/dist/tools/shell.js +9 -1
- package/package.json +1 -1
package/dist/agent/memory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { scanForSecrets } from "../tools/secretScan.js";
|
|
3
4
|
const MEMORY_FILE = "KRITYA.md";
|
|
4
5
|
const AUTO_HEADING = "## Learned by kritya (auto-updated; edit or delete freely)";
|
|
5
6
|
const MAX_FACTS = 20;
|
|
@@ -132,6 +133,14 @@ export function mergeProjectMemory(workspace, newFacts) {
|
|
|
132
133
|
for (const fact of newFacts) {
|
|
133
134
|
if (seen.has(fact.toLowerCase()))
|
|
134
135
|
continue;
|
|
136
|
+
// Write-time secret guard: facts are extracted from session transcripts,
|
|
137
|
+
// which can include credentials the model saw in tool output, and
|
|
138
|
+
// KRITYA.md is a plain workspace file that is read back into future
|
|
139
|
+
// prompts. A fact that looks like it contains a real secret is dropped
|
|
140
|
+
// rather than persisted — silently, since there is no user-facing write
|
|
141
|
+
// to block here, and the fact's value is the secret itself.
|
|
142
|
+
if (scanForSecrets(fact).length > 0)
|
|
143
|
+
continue;
|
|
135
144
|
seen.add(fact.toLowerCase());
|
|
136
145
|
added.push(fact);
|
|
137
146
|
}
|
|
@@ -3,6 +3,7 @@ import { classifyLaterExec, classifyLaterExecInShell, recordLaterExec, workspace
|
|
|
3
3
|
import { acknowledgeUnsandboxedFallback, sandboxAvailable, sandboxFallbackWarning, } from "../shell/sandbox.js";
|
|
4
4
|
import { isPlanningDocWrite, loadProjectState } from "./workflow.js";
|
|
5
5
|
import { redactSecrets } from "../tools/secretScan.js";
|
|
6
|
+
import { classifyReconCall, ReconTripwire, RECON_TRIP_THRESHOLD, RECON_WINDOW_CALLS, } from "../permissions/recon.js";
|
|
6
7
|
/** How much tool output to hand the UI (it shows a preview and expands on toggle). */
|
|
7
8
|
const PREVIEW_CHARS = 4000;
|
|
8
9
|
/**
|
|
@@ -91,6 +92,12 @@ function fenceExternal(output) {
|
|
|
91
92
|
export class ToolExecutor {
|
|
92
93
|
tools;
|
|
93
94
|
host;
|
|
95
|
+
/**
|
|
96
|
+
* Per-session recon-pattern score (see permissions/recon.ts). One
|
|
97
|
+
* executor serves one agent session, so the tripwire's state naturally
|
|
98
|
+
* spans exactly that session's calls and resets with it.
|
|
99
|
+
*/
|
|
100
|
+
tripwire = new ReconTripwire();
|
|
94
101
|
constructor(tools, host) {
|
|
95
102
|
this.tools = tools;
|
|
96
103
|
this.host = host;
|
|
@@ -211,6 +218,33 @@ export class ToolExecutor {
|
|
|
211
218
|
};
|
|
212
219
|
if (host.kill.active)
|
|
213
220
|
return killBlocked();
|
|
221
|
+
// Recon-pattern tripwire. The gates below (and the tools' own checks)
|
|
222
|
+
// judge calls one at a time; this is the one place that sees the
|
|
223
|
+
// sequence. Every call is scored, including ones another gate is about
|
|
224
|
+
// to block anyway — an attempt at a credential file is a signal whether
|
|
225
|
+
// or not it succeeds. Recording happens in this synchronous stretch,
|
|
226
|
+
// before any await, so a parallel batch of read-only calls can't race
|
|
227
|
+
// the score. Once tripped, only recon-carrying calls are blocked;
|
|
228
|
+
// ordinary work flows through the normal gates below.
|
|
229
|
+
const recon = this.tripwire.record(classifyReconCall(name, args));
|
|
230
|
+
if (recon.blocked) {
|
|
231
|
+
host.audit?.logPermission({
|
|
232
|
+
tool: name,
|
|
233
|
+
summary,
|
|
234
|
+
verdict: "denied",
|
|
235
|
+
source: "recon-tripwire",
|
|
236
|
+
});
|
|
237
|
+
logToolOutcome("blocked");
|
|
238
|
+
finishSpan("ERROR", "blocked: recon-pattern tripwire");
|
|
239
|
+
handlers.onToolEnd(id, name, summary, "blocked: recon-pattern tripwire", true);
|
|
240
|
+
return (`Stopped by the recon-pattern tripwire: this session's recent calls form a ` +
|
|
241
|
+
`reconnaissance-like pattern (${this.tripwire.tripKinds.join(", ")}) — ${recon.score} ` +
|
|
242
|
+
`points across the last ${RECON_WINDOW_CALLS} calls, over the limit of ` +
|
|
243
|
+
`${RECON_TRIP_THRESHOLD}. This call was blocked, and further reconnaissance-like ` +
|
|
244
|
+
`calls stay blocked for the rest of the session. Ordinary work (reading and ` +
|
|
245
|
+
`editing project files, builds, tests) can continue. If this is legitimate ` +
|
|
246
|
+
`debugging, tell the user what you are doing and why; a new session resets the tripwire.`);
|
|
247
|
+
}
|
|
214
248
|
// The planning-doc exemption is scoped to the active project's own
|
|
215
249
|
// docs/<slug>/ folder, so plan mode can persist its artifact without
|
|
216
250
|
// becoming a way to edit unrelated documentation.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { commandTouchesSensitivePath, isSensitivePath } from "../tools/common.js";
|
|
2
|
+
const WEIGHTS = {
|
|
3
|
+
"credential-probe": 5,
|
|
4
|
+
"env-dump": 4,
|
|
5
|
+
enumeration: 2,
|
|
6
|
+
"system-probe": 2,
|
|
7
|
+
};
|
|
8
|
+
/** Score at or above which the tripwire latches for the rest of the session. */
|
|
9
|
+
export const RECON_TRIP_THRESHOLD = 10;
|
|
10
|
+
/** How many recent calls the rolling score covers. */
|
|
11
|
+
export const RECON_WINDOW_CALLS = 20;
|
|
12
|
+
function signal(kind, detail) {
|
|
13
|
+
return { kind, weight: WEIGHTS[kind], detail };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whole-environment dumps. Only the bare forms score: `env FOO=1 cmd` runs a
|
|
17
|
+
* command with one variable set and `printenv PATH` reads one variable —
|
|
18
|
+
* both ordinary. Dumping *everything* is the recon move.
|
|
19
|
+
*/
|
|
20
|
+
const ENV_DUMP_PATTERNS = [
|
|
21
|
+
{ re: /(^|[|;&]\s*)(env|printenv)(\s*$|\s*[|;&])/, detail: "bare env/printenv dump" },
|
|
22
|
+
{ re: /\bexport\s+-p\b/, detail: "export -p dump" },
|
|
23
|
+
{ re: /\bdeclare\s+-x\b/, detail: "declare -x dump" },
|
|
24
|
+
{ re: /\bcompgen\s+-e\b/, detail: "compgen -e env listing" },
|
|
25
|
+
{ re: /\/proc\/(\d+|self)\/environ\b/, detail: "reads /proc environ" },
|
|
26
|
+
{ re: /\b(dir|ls|gci|Get-ChildItem)\s+env:/i, detail: "PowerShell env: drive listing" },
|
|
27
|
+
];
|
|
28
|
+
/** Identity and network-shape questions — cheap to ask, classic to ask first. */
|
|
29
|
+
const SYSTEM_PROBE_PATTERNS = [
|
|
30
|
+
{ re: /\bwhoami\b/, detail: "whoami" },
|
|
31
|
+
{ re: /\bhostname\b/, detail: "hostname" },
|
|
32
|
+
{ re: /\buname\b/, detail: "uname" },
|
|
33
|
+
{ re: /\bsysteminfo\b/, detail: "systeminfo" },
|
|
34
|
+
{ re: /(^|[|;&]\s*)id\s*($|[|;&])/, detail: "bare id" },
|
|
35
|
+
{ re: /\bnetstat\b/, detail: "netstat" },
|
|
36
|
+
{ re: /\bifconfig\b/, detail: "ifconfig" },
|
|
37
|
+
{ re: /\bipconfig\b/, detail: "ipconfig" },
|
|
38
|
+
{ re: /\bip\s+(addr|address|link|route)\b/, detail: "ip addr/link/route" },
|
|
39
|
+
{ re: /\barp\s+-a\b/, detail: "arp -a" },
|
|
40
|
+
{ re: /\bps\s+(aux|ef|-ef|-aux)\b/, detail: "full process listing" },
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Listing the places a person keeps things, rather than the project: home,
|
|
44
|
+
* the filesystem root, /etc, other users' folders. A listing of the
|
|
45
|
+
* workspace itself (relative paths, /tmp, project dirs) scores nothing.
|
|
46
|
+
*/
|
|
47
|
+
const ENUMERATION_PATTERNS = [
|
|
48
|
+
{
|
|
49
|
+
re: /(?:^|[|;&]\s*)(?:ls|dir|find|tree)\b[^|;&]*?\s(?:~|\$HOME|\/etc|\/home|[A-Za-z]:\\Users)(?=[\s|;&]|$)/,
|
|
50
|
+
detail: "lists home/system directories",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
re: /(?:^|[|;&]\s*)(?:ls|dir|find|tree)\b[^|;&]*?\s\/(?=[\s|;&]|$)/,
|
|
54
|
+
detail: "lists the filesystem root",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
/** Tools whose path/pattern arguments can aim at a credential file or a system directory. */
|
|
58
|
+
const PATH_ARG_TOOLS = new Set([
|
|
59
|
+
"read_file",
|
|
60
|
+
"read_document",
|
|
61
|
+
"read_notebook",
|
|
62
|
+
"edit_file",
|
|
63
|
+
"write_file",
|
|
64
|
+
"grep",
|
|
65
|
+
"glob",
|
|
66
|
+
"list_dir",
|
|
67
|
+
]);
|
|
68
|
+
/**
|
|
69
|
+
* Classify one tool call into recon signals — at most one signal per kind,
|
|
70
|
+
* so a single busy command can't trip the wire on repetition alone; it
|
|
71
|
+
* takes either one heavy call (a credential probe plus a dump) or a genuine
|
|
72
|
+
* sequence across calls.
|
|
73
|
+
*/
|
|
74
|
+
export function classifyReconCall(toolName, args) {
|
|
75
|
+
const byKind = new Map();
|
|
76
|
+
const add = (s) => {
|
|
77
|
+
if (!byKind.has(s.kind))
|
|
78
|
+
byKind.set(s.kind, s);
|
|
79
|
+
};
|
|
80
|
+
if (toolName === "shell") {
|
|
81
|
+
const command = String(args.command ?? "");
|
|
82
|
+
if (command) {
|
|
83
|
+
const sensitive = commandTouchesSensitivePath(command);
|
|
84
|
+
if (sensitive)
|
|
85
|
+
add(signal("credential-probe", `command references "${sensitive}"`));
|
|
86
|
+
for (const { re, detail } of ENV_DUMP_PATTERNS) {
|
|
87
|
+
if (re.test(command)) {
|
|
88
|
+
add(signal("env-dump", detail));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const { re, detail } of ENUMERATION_PATTERNS) {
|
|
93
|
+
if (re.test(command)) {
|
|
94
|
+
add(signal("enumeration", detail));
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const { re, detail } of SYSTEM_PROBE_PATTERNS) {
|
|
99
|
+
if (re.test(command)) {
|
|
100
|
+
add(signal("system-probe", detail));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (PATH_ARG_TOOLS.has(toolName)) {
|
|
107
|
+
for (const key of ["path", "pattern"]) {
|
|
108
|
+
const value = args[key];
|
|
109
|
+
if (typeof value !== "string" || !value)
|
|
110
|
+
continue;
|
|
111
|
+
if (isSensitivePath(value)) {
|
|
112
|
+
add(signal("credential-probe", `${toolName} targets "${value}"`));
|
|
113
|
+
}
|
|
114
|
+
if ((toolName === "list_dir" || toolName === "glob") &&
|
|
115
|
+
/^(~|\$HOME|\/|\/etc|\/home)([\\/]|$)/.test(value)) {
|
|
116
|
+
add(signal("enumeration", `${toolName} targets "${value}"`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...byKind.values()];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The rolling score and the latch. One instance per agent session (the
|
|
124
|
+
* tool executor owns it). Blocked calls are recorded too — the executor
|
|
125
|
+
* records before it blocks — so continued probing keeps the score pinned
|
|
126
|
+
* above the threshold instead of decaying while the caller retries.
|
|
127
|
+
*/
|
|
128
|
+
export class ReconTripwire {
|
|
129
|
+
window = [];
|
|
130
|
+
latchedKinds = null;
|
|
131
|
+
get tripped() {
|
|
132
|
+
return this.latchedKinds !== null;
|
|
133
|
+
}
|
|
134
|
+
/** The signal kinds that contributed to the trip, for the block message. */
|
|
135
|
+
get tripKinds() {
|
|
136
|
+
return this.latchedKinds ?? [];
|
|
137
|
+
}
|
|
138
|
+
windowScore() {
|
|
139
|
+
return this.window.reduce((acc, w) => acc + w.score, 0);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Record one call's signals. Returns whether this call must be blocked:
|
|
143
|
+
* the call that pushes the score to the threshold, and — once latched —
|
|
144
|
+
* every later call that carries any recon signal. Signal-free calls are
|
|
145
|
+
* never blocked, tripped or not.
|
|
146
|
+
*/
|
|
147
|
+
record(signals) {
|
|
148
|
+
const score = signals.reduce((acc, s) => acc + s.weight, 0);
|
|
149
|
+
this.window.push({ score, kinds: signals.map((s) => s.kind) });
|
|
150
|
+
if (this.window.length > RECON_WINDOW_CALLS)
|
|
151
|
+
this.window.shift();
|
|
152
|
+
const total = this.windowScore();
|
|
153
|
+
if (this.latchedKinds) {
|
|
154
|
+
return { blocked: signals.length > 0, score: total };
|
|
155
|
+
}
|
|
156
|
+
if (total >= RECON_TRIP_THRESHOLD) {
|
|
157
|
+
this.latchedKinds = [...new Set(this.window.flatMap((w) => w.kinds))];
|
|
158
|
+
return { blocked: signals.length > 0, score: total };
|
|
159
|
+
}
|
|
160
|
+
return { blocked: false, score: total };
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/tools/common.js
CHANGED
|
@@ -45,7 +45,7 @@ const SENSITIVE_PATH_PATTERNS = [
|
|
|
45
45
|
/\.p12$/i,
|
|
46
46
|
/\.pfx$/i,
|
|
47
47
|
];
|
|
48
|
-
function isSensitivePath(relPath) {
|
|
48
|
+
export function isSensitivePath(relPath) {
|
|
49
49
|
return SENSITIVE_PATH_PATTERNS.some((re) => re.test(relPath));
|
|
50
50
|
}
|
|
51
51
|
/** Splits a shell command into path-like tokens, stripping quotes/metacharacters. */
|
package/dist/tools/document.js
CHANGED
|
@@ -6,6 +6,7 @@ import { readDocx, writeDocx } from "./document/docx.js";
|
|
|
6
6
|
import { readXlsx, writeXlsx, editXlsx } from "./document/xlsx.js";
|
|
7
7
|
import { readPptx, writePptx } from "./document/pptx.js";
|
|
8
8
|
import { readPdf, writePdf, editPdf } from "./document/pdf.js";
|
|
9
|
+
import { formatSecretWarning, scanForSecrets } from "./secretScan.js";
|
|
9
10
|
const READABLE_EXTENSIONS = [".docx", ".xlsx", ".pptx", ".pdf"];
|
|
10
11
|
/**
|
|
11
12
|
* Bound the raw file read before it's dispatched to any format-specific
|
|
@@ -142,6 +143,14 @@ export const writeDocumentTool = {
|
|
|
142
143
|
const abs = resolveSafe(ctx.workspace, relPath);
|
|
143
144
|
const ext = path.extname(abs).toLowerCase();
|
|
144
145
|
const content = extractContent(args);
|
|
146
|
+
// Same write-time secret guard as write_file: the blocks/sheets/slides
|
|
147
|
+
// payload is model-authored text, and a generated .docx/.xlsx is still
|
|
148
|
+
// a file a real key shouldn't be baked into. The payload is structured,
|
|
149
|
+
// so scan its JSON form — every user-visible string is in there.
|
|
150
|
+
const secrets = scanForSecrets(JSON.stringify(content));
|
|
151
|
+
if (secrets.length > 0) {
|
|
152
|
+
throw new Error(formatSecretWarning(secrets, relPath));
|
|
153
|
+
}
|
|
145
154
|
let buf;
|
|
146
155
|
switch (ext) {
|
|
147
156
|
case ".docx":
|
|
@@ -232,6 +241,16 @@ export const editSpreadsheetTool = {
|
|
|
232
241
|
const edits = (args.edits ?? []);
|
|
233
242
|
if (edits.length === 0)
|
|
234
243
|
throw new Error("edit_spreadsheet: `edits` must be non-empty");
|
|
244
|
+
// Write-time secret guard, same as write_file: a string cell value is
|
|
245
|
+
// model-authored content being persisted into the workbook.
|
|
246
|
+
for (const edit of edits) {
|
|
247
|
+
if (typeof edit.value !== "string")
|
|
248
|
+
continue;
|
|
249
|
+
const secrets = scanForSecrets(edit.value);
|
|
250
|
+
if (secrets.length > 0) {
|
|
251
|
+
throw new Error(formatSecretWarning(secrets, relPath));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
235
254
|
const buf = await fs.readFile(abs);
|
|
236
255
|
const { buf: out, applied } = await editXlsx(buf, edits);
|
|
237
256
|
ctx.undo?.snapshot(abs, relPath);
|
package/dist/tools/notebook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { writeFileAtomic } from "../atomicWrite.js";
|
|
3
3
|
import { resolveSafe, truncateResult } from "./common.js";
|
|
4
|
+
import { formatSecretWarning, scanForSecrets } from "./secretScan.js";
|
|
4
5
|
/** nbformat stores text as either a single string or an array of lines. */
|
|
5
6
|
function joinSource(source) {
|
|
6
7
|
if (Array.isArray(source))
|
|
@@ -170,6 +171,15 @@ export const editNotebookTool = {
|
|
|
170
171
|
}
|
|
171
172
|
const nb = await loadNotebook(abs);
|
|
172
173
|
const cells = nb.cells;
|
|
174
|
+
// Same write-time secret guard as write_file/edit_file: cell source is
|
|
175
|
+
// model-authored content persisted to disk, and a notebook is an
|
|
176
|
+
// ordinary file a key can hide in just as easily as a .ts file.
|
|
177
|
+
if (op !== "delete" && args.source !== undefined) {
|
|
178
|
+
const secrets = scanForSecrets(String(args.source));
|
|
179
|
+
if (secrets.length > 0) {
|
|
180
|
+
throw new Error(formatSecretWarning(secrets, relPath));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
173
183
|
switch (op) {
|
|
174
184
|
case "replace": {
|
|
175
185
|
if (index >= cells.length) {
|
package/dist/tools/secretScan.js
CHANGED
|
@@ -148,3 +148,31 @@ export function formatSecretWarning(matches, path) {
|
|
|
148
148
|
`match common key formats or lower its entropy — for example, using an obvious placeholder ` +
|
|
149
149
|
`like "sk-ant-EXAMPLE...".`);
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Shell commands are the one write path the file tools can't see: a command
|
|
153
|
+
* like `echo '<key>' > notes.txt` or a heredoc puts a literal secret on disk
|
|
154
|
+
* without going through write_file/edit_file. The command string itself is
|
|
155
|
+
* the only place to catch that — but a secret merely *appearing* in a
|
|
156
|
+
* command (a curl Authorization header, say) doesn't put it in a file, so
|
|
157
|
+
* this only fires when the command also has a file-writing construct: an
|
|
158
|
+
* output redirect to a real target (not `>&` fd duplication or a `>=`
|
|
159
|
+
* comparison), `tee`, or `dd of=`. Heredocs are covered through the
|
|
160
|
+
* redirect they almost always pair with (`cat > f <<EOF`); a heredoc fed
|
|
161
|
+
* to an interpreter that then writes the secret itself is out of reach for
|
|
162
|
+
* a string scan and remains a known gap, like novel key formats.
|
|
163
|
+
*/
|
|
164
|
+
const SHELL_WRITE_RE = />{1,2}\s*(?![&=>])|\btee\b|\bdd\b[^|;&]*\bof=/;
|
|
165
|
+
export function scanCommandForSecretWrite(command) {
|
|
166
|
+
if (!SHELL_WRITE_RE.test(command))
|
|
167
|
+
return [];
|
|
168
|
+
return scanForSecrets(command);
|
|
169
|
+
}
|
|
170
|
+
export function formatShellSecretWarning(matches) {
|
|
171
|
+
const lines = matches.map((m) => ` - ${m.kind}: ${m.snippet}`);
|
|
172
|
+
return (`Blocked shell command: it writes to a file and contains what looks like real secret(s):\n` +
|
|
173
|
+
lines.join("\n") +
|
|
174
|
+
`\n\nEmbedding a literal secret in a shell command writes it to disk and shell history. ` +
|
|
175
|
+
`Reference the value from the environment instead (e.g. $API_KEY), or if this is ` +
|
|
176
|
+
`genuinely not a secret (e.g. a test fixture), rewrite it so it doesn't match common ` +
|
|
177
|
+
`key formats — for example, using an obvious placeholder like "sk-ant-EXAMPLE...".`);
|
|
178
|
+
}
|
package/dist/tools/shell.js
CHANGED
|
@@ -4,7 +4,7 @@ import { gitDiffStat } from "../git/git.js";
|
|
|
4
4
|
import { backgroundManager } from "../shell/background.js";
|
|
5
5
|
import { buildSandboxedCommand, networkBlockedHint, requiresSandbox, sandboxUnavailableReason, shouldSandbox, } from "../shell/sandbox.js";
|
|
6
6
|
import { commandTouchesSensitivePath, truncateTail } from "./common.js";
|
|
7
|
-
import { redactSecrets } from "./secretScan.js";
|
|
7
|
+
import { redactSecrets, scanCommandForSecretWrite, formatShellSecretWarning, } from "./secretScan.js";
|
|
8
8
|
const DEFAULT_TIMEOUT_S = 120;
|
|
9
9
|
const MAX_TIMEOUT_S = 600;
|
|
10
10
|
/** git subcommands that rewrite the working tree, index, or history. */
|
|
@@ -63,6 +63,14 @@ export const shellTool = {
|
|
|
63
63
|
if (sensitivePath) {
|
|
64
64
|
throw new Error(`Command references "${sensitivePath}", which looks like a secret file, and is blocked from tool access`);
|
|
65
65
|
}
|
|
66
|
+
// A command can also *create* the secret leak: a literal key in the
|
|
67
|
+
// command string plus a redirect/tee/dd target writes it to a file
|
|
68
|
+
// without going through the scanned file tools. Block that here, for
|
|
69
|
+
// background commands too — this check sits above the background branch.
|
|
70
|
+
const secretWrite = scanCommandForSecretWrite(command);
|
|
71
|
+
if (secretWrite.length > 0) {
|
|
72
|
+
throw new Error(formatShellSecretWarning(secretWrite));
|
|
73
|
+
}
|
|
66
74
|
if (args.background) {
|
|
67
75
|
const { id } = backgroundManager.start(command, ctx.workspace, ctx.sandboxMode);
|
|
68
76
|
// Echoing the command back can leak a credential embedded in it (e.g. a
|