kritya 0.8.61-beta → 0.8.62-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.
@@ -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
  }
@@ -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);
@@ -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) {
@@ -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
+ }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kritya",
3
- "version": "0.8.61-beta",
3
+ "version": "0.8.62-beta",
4
4
  "description": "Kritya — Superintelligence at your command line",
5
5
  "type": "module",
6
6
  "publishConfig": {