codeden 0.6.3 → 0.7.0
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/README.md +12 -5
- package/dist/cli.js +1 -1
- package/dist/drizzle/20261113075317_transcript_search/migration.sql +29 -0
- package/dist/drizzle/20261113075317_transcript_search/snapshot.json +3635 -0
- package/dist/drizzle/20261113083655_runners/migration.sql +17 -0
- package/dist/drizzle/20261113083655_runners/snapshot.json +3792 -0
- package/dist/drizzle/20261113165404_search_newest_first/migration.sql +43 -0
- package/dist/drizzle/20261113165404_search_newest_first/snapshot.json +3785 -0
- package/dist/{main-Bx58CdV4.js → main-BITVD_Bx.js} +4781 -1305
- package/dist/main-BITVD_Bx.js.map +1 -0
- package/dist/{main-CMt4z1Z9.js → main-NN0iK2tB.js} +2 -2
- package/dist/{main-CMt4z1Z9.js.map → main-NN0iK2tB.js.map} +1 -1
- package/dist/{mediaType-BCz-hbZl.js → mediaType-N0hKQqG9.js} +2 -2
- package/dist/{mediaType-BCz-hbZl.js.map → mediaType-N0hKQqG9.js.map} +1 -1
- package/dist/{runner-CiaiUWOE.js → runner-CQYh087M.js} +1302 -304
- package/dist/runner-CQYh087M.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/{server-test-b0hHPqXb.js → server-test-BwyCFssm.js} +3 -3
- package/dist/{server-test-b0hHPqXb.js.map → server-test-BwyCFssm.js.map} +1 -1
- package/dist/{tree-31WAOm5k.js → tree-Bjv-rXir.js} +457 -21
- package/dist/tree-Bjv-rXir.js.map +1 -0
- package/dist/web/assets/{core-FqX7NQzW.js → core-EsMgAasL.js} +1 -1
- package/dist/web/assets/engine-oniguruma-C2eJcn5h.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-BMKFJRL-.js → highlighted-body-JLMTFACH-Co1oXN1b.js} +1 -1
- package/dist/web/assets/{highlights-9yuGk1pg.js → highlights-DUjJZrPQ.js} +1 -1
- package/dist/web/assets/index-29XyPQXD.css +1 -0
- package/dist/web/assets/index-DK4GsIYY.js +943 -0
- package/dist/web/assets/langs-BduZAjBk.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-D00b7K53.js +1 -0
- package/dist/web/assets/{shiki-Qa-y3xHo.js → shiki-DP8JfDZC.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-VL43-ily.js → stream-tokenizer-IR1cpBIQ.js} +1 -1
- package/dist/web/assets/{themes-Di4DO5zr.js → themes-D2aixyve.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-Bx58CdV4.js.map +0 -1
- package/dist/runner-CiaiUWOE.js.map +0 -1
- package/dist/tree-31WAOm5k.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-C5UP18m1.js +0 -1
- package/dist/web/assets/index-C3QkKvUn.js +0 -942
- package/dist/web/assets/index-CmllrYFx.css +0 -1
- package/dist/web/assets/langs-DfCgj44a.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-zTA5f8lD.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
|
|
2
|
-
import {
|
|
2
|
+
import { $n as ProjectConfigFile, Hn as SETUP_TAIL_LIMIT, Un as BROWSER_RELAY_SECRET_HEADER, Xn as ProviderCapabilities, Yn as CODEDEN_CREDENTIAL_FILES, Zn as RuntimeResponse, a as childEnv, d as RunnerEventMessage, dr as mergeItemDeltas, f as RunnerLock, gr as UserInput, h as ServerToRunnerMessage, hr as itemDeltaText, i as listDescendants, lr as AgentEvent, mr as winningCommands, n as killTree$1, o as CODEDEN_VERSION, pr as oneLineDescription, qn as MAX_FOCUS_TYPE_CHARS, t as isAlive$1, zr as ReasoningEffort } from "./tree-Bjv-rXir.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -4521,18 +4521,29 @@ const reasonOf = (error) => error instanceof Error ? error.message : String(erro
|
|
|
4521
4521
|
* Copy a Claude conversation with the SDK's `forkSession()`: the transcript is written to a new
|
|
4522
4522
|
* session file at once (fresh message ids, the original untouched) and the new session id is
|
|
4523
4523
|
* returned for resuming. With `upTo` (a turn's fork point: its last transcript message), the
|
|
4524
|
-
*
|
|
4525
|
-
*
|
|
4526
|
-
* `
|
|
4524
|
+
* copy ends with that message. A conversation that no longer holds it (replaced or deleted)
|
|
4525
|
+
* throws `FORK_POINT_MISSING` before anything is copied, so it never becomes a whole copy
|
|
4526
|
+
* instead: `confirmForkPoint` refuses it from the transcript read when it can, and otherwise
|
|
4527
|
+
* `forkSession()` does, as it slices the whole transcript file and refuses a message the file
|
|
4528
|
+
* doesn't hold before writing anything (SDK 0.3.318: `Message <uuid> not found in session <id>`).
|
|
4527
4529
|
*/
|
|
4528
4530
|
async function forkClaudeConversation(sdk, resumeToken, upTo) {
|
|
4529
4531
|
if (upTo) await confirmForkPoint(sdk, resumeToken, upTo);
|
|
4530
4532
|
try {
|
|
4531
4533
|
return (await sdk.forkSession(resumeToken, upTo ? { upToMessageId: upTo } : void 0)).sessionId;
|
|
4532
4534
|
} catch (error) {
|
|
4535
|
+
if (upTo && refusedForkPoint(error, upTo)) throw forkPointMissing();
|
|
4533
4536
|
throw new Error(`Could not fork the Claude conversation ${resumeToken}: ${reasonOf(error)}`);
|
|
4534
4537
|
}
|
|
4535
4538
|
}
|
|
4539
|
+
/**
|
|
4540
|
+
* Read the transcript (`getSessionMessages`) and refuse a fork point it doesn't hold. That read
|
|
4541
|
+
* follows `parentUuid` links back from the newest message, and a context compaction's boundary
|
|
4542
|
+
* has none, so in a compacted conversation it starts at the latest boundary (a `system`
|
|
4543
|
+
* message) and leaves out every earlier message, although the transcript file still holds
|
|
4544
|
+
* them. A fork point missing from such a read may be before the compaction, so `forkSession()`
|
|
4545
|
+
* decides then. An empty read (a conversation that was deleted) holds nothing.
|
|
4546
|
+
*/
|
|
4536
4547
|
async function confirmForkPoint(sdk, resumeToken, upTo) {
|
|
4537
4548
|
let messages;
|
|
4538
4549
|
try {
|
|
@@ -4540,7 +4551,15 @@ async function confirmForkPoint(sdk, resumeToken, upTo) {
|
|
|
4540
4551
|
} catch (error) {
|
|
4541
4552
|
throw new Error(`Could not read the Claude conversation ${resumeToken}: ${reasonOf(error)}`);
|
|
4542
4553
|
}
|
|
4543
|
-
if (!messages.some((m) => m.uuid === upTo)) throw
|
|
4554
|
+
if (!(messages[0]?.type === "system") && !messages.some((m) => m.uuid === upTo)) throw forkPointMissing();
|
|
4555
|
+
}
|
|
4556
|
+
/** Whether `forkSession()` refused `upTo` as a message its transcript doesn't hold. */
|
|
4557
|
+
function refusedForkPoint(error, upTo) {
|
|
4558
|
+
const reason = reasonOf(error);
|
|
4559
|
+
return reason.includes(upTo) && /\bnot found\b/i.test(reason);
|
|
4560
|
+
}
|
|
4561
|
+
function forkPointMissing() {
|
|
4562
|
+
return new ProviderSessionError(FORK_POINT_MISSING, "The conversation no longer holds the end of that turn (it was replaced or deleted), so it can't be forked there");
|
|
4544
4563
|
}
|
|
4545
4564
|
// ../../packages/providers/src/util/model.ts
|
|
4546
4565
|
/** The id the model pickers use for "the provider's own default model". */
|
|
@@ -4553,8 +4572,26 @@ function explicitModel(model) {
|
|
|
4553
4572
|
const id = model?.trim();
|
|
4554
4573
|
return id && id !== "default" ? id : void 0;
|
|
4555
4574
|
}
|
|
4575
|
+
// ../../packages/providers/src/policy/assessment.ts
|
|
4576
|
+
const SAFE = { risky: false };
|
|
4577
|
+
const risky = (action, reason) => ({
|
|
4578
|
+
risky: true,
|
|
4579
|
+
action,
|
|
4580
|
+
reason
|
|
4581
|
+
});
|
|
4582
|
+
/** True for a verdict on CodeDen's own credential files. */
|
|
4583
|
+
const touchesCodeden = (risk) => risk.risky && risk.codeden === true;
|
|
4584
|
+
/**
|
|
4585
|
+
* The verdict for several checks of one action: CodeDen's own credentials first (a level may
|
|
4586
|
+
* allow an ordinary risk that comes before them), then the first risky one, else safe.
|
|
4587
|
+
*/
|
|
4588
|
+
function mostSevere(risks) {
|
|
4589
|
+
return risks.find(touchesCodeden) ?? risks.find((r) => r.risky) ?? SAFE;
|
|
4590
|
+
}
|
|
4556
4591
|
// ../../packages/providers/src/policy/paths.ts
|
|
4592
|
+
/** True when the context's paths are Windows paths. */
|
|
4557
4593
|
const isWin = (ctx) => (ctx.platform ?? process.platform) === "win32";
|
|
4594
|
+
/** The path functions for the context's platform. */
|
|
4558
4595
|
function pathApi(ctx) {
|
|
4559
4596
|
return isWin(ctx) ? path.win32 : path.posix;
|
|
4560
4597
|
}
|
|
@@ -4571,6 +4608,8 @@ const HOME_VARS = /^(\$HOME|\$\{HOME\}|\$env:USERPROFILE|%USERPROFILE%)(?=$|[\\/
|
|
|
4571
4608
|
* printed) is not the temp directory; PowerShell's and cmd's names are not.
|
|
4572
4609
|
*/
|
|
4573
4610
|
const TEMP_VARS = [/^(\$TMPDIR|\$\{TMPDIR\}|\$TEMP|\$\{TEMP\}|\$TMP|\$\{TMP\})(?=$|[\\/])/, /^(\$env:TEMP|\$env:TMP|%TEMP%|%TMP%)(?=$|[\\/])/i];
|
|
4611
|
+
/** CodeDen's home variable, which agents inherit: case-sensitive in POSIX shells only. */
|
|
4612
|
+
const CODEDEN_HOME_VARS = [/^(\$CODEDEN_HOME|\$\{CODEDEN_HOME\})(?=$|[\\/])/, /^(\$env:CODEDEN_HOME|%CODEDEN_HOME%)(?=$|[\\/])/i];
|
|
4574
4613
|
/**
|
|
4575
4614
|
* Git Bash paths on Windows: `/c/Users/x` is `C:\Users\x`, and `/tmp` is the user's temp
|
|
4576
4615
|
* directory.
|
|
@@ -4582,9 +4621,9 @@ function fromGitBash(value, temp) {
|
|
|
4582
4621
|
return value;
|
|
4583
4622
|
}
|
|
4584
4623
|
/**
|
|
4585
|
-
* Resolve `target` (absolute, relative to `dir`, `~`-prefixed, or starting with a home or
|
|
4586
|
-
*
|
|
4587
|
-
* Bash form too: the shell reports `/c/Users/x/repo` once a command has run in it.
|
|
4624
|
+
* Resolve `target` (absolute, relative to `dir`, `~`-prefixed, or starting with a home, temp or
|
|
4625
|
+
* CodeDen home variable) to an absolute path. On Windows the directory it resolves from may be
|
|
4626
|
+
* in Git Bash form too: the shell reports `/c/Users/x/repo` once a command has run in it.
|
|
4588
4627
|
*/
|
|
4589
4628
|
function resolveAgainst(target, ctx) {
|
|
4590
4629
|
const p = pathApi(ctx);
|
|
@@ -4592,6 +4631,8 @@ function resolveAgainst(target, ctx) {
|
|
|
4592
4631
|
const home = homeOf(ctx);
|
|
4593
4632
|
if (value === "~" || value.startsWith("~/") || value.startsWith("~\\")) value = home + value.slice(1);
|
|
4594
4633
|
value = value.replace(HOME_VARS, home);
|
|
4634
|
+
const codeden = ctx.codedenHome;
|
|
4635
|
+
if (codeden) for (const vars of CODEDEN_HOME_VARS) value = value.replace(vars, codeden);
|
|
4595
4636
|
const temp = tempRootsOf(ctx)[0];
|
|
4596
4637
|
if (temp) for (const vars of TEMP_VARS) value = value.replace(vars, temp);
|
|
4597
4638
|
let base = ctx.dir ?? ctx.cwd;
|
|
@@ -4611,6 +4652,10 @@ function relative$1(root, target, ctx) {
|
|
|
4611
4652
|
const norm = (s) => isWin(ctx) ? p.resolve(s).toLowerCase() : p.resolve(s);
|
|
4612
4653
|
return p.relative(norm(root), norm(target));
|
|
4613
4654
|
}
|
|
4655
|
+
/** True when two absolute paths name the same file. Case-insensitive on Windows. */
|
|
4656
|
+
function samePath(a, b, ctx) {
|
|
4657
|
+
return relative$1(a, b, ctx) === "";
|
|
4658
|
+
}
|
|
4614
4659
|
/** True when `target` is inside the worktree or one of the extra roots. */
|
|
4615
4660
|
function isInWorkspace(target, ctx) {
|
|
4616
4661
|
const abs = resolveAgainst(target, ctx);
|
|
@@ -4727,13 +4772,6 @@ function isSecretPath(target) {
|
|
|
4727
4772
|
if (SECRET_EXTENSIONS.some((ext) => base.endsWith(ext))) return true;
|
|
4728
4773
|
return /(^|\/)\.(ssh|aws|gnupg|docker)\//i.test(normalized) && !base.endsWith(".pub");
|
|
4729
4774
|
}
|
|
4730
|
-
// ../../packages/providers/src/policy/assessment.ts
|
|
4731
|
-
const SAFE = { risky: false };
|
|
4732
|
-
const risky = (action, reason) => ({
|
|
4733
|
-
risky: true,
|
|
4734
|
-
action,
|
|
4735
|
-
reason
|
|
4736
|
-
});
|
|
4737
4775
|
// ../../packages/providers/src/policy/git.ts
|
|
4738
4776
|
/**
|
|
4739
4777
|
* Git rules of the risk classifier (§11.2): force pushes, deleting refs, global config. `args`
|
|
@@ -5684,6 +5722,590 @@ function unixToIso(value) {
|
|
|
5684
5722
|
const ms = value < 0xe8d4a51000 ? value * 1e3 : value;
|
|
5685
5723
|
return new Date(ms).toISOString();
|
|
5686
5724
|
}
|
|
5725
|
+
// ../../packages/providers/src/policy/codeden-home.ts
|
|
5726
|
+
/**
|
|
5727
|
+
* CodeDen's own credential files (§15): the install token, the runner lock and the remote
|
|
5728
|
+
* access store in its data directory. An agent holding the install token or the runner's token
|
|
5729
|
+
* could drive CodeDen itself and approve its own requests, so touching one asks at every level,
|
|
5730
|
+
* Full included, and is refused for read-only sessions (§11.2). The data directory comes from
|
|
5731
|
+
* the session (`SessionOptions.codedenHome`); without it, this rule is off.
|
|
5732
|
+
*/
|
|
5733
|
+
/** A risky verdict on CodeDen's own credentials, which every level asks about. */
|
|
5734
|
+
function credentialsRisk(reason) {
|
|
5735
|
+
return {
|
|
5736
|
+
risky: true,
|
|
5737
|
+
action: "secrets",
|
|
5738
|
+
reason,
|
|
5739
|
+
codeden: true
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
/** The credential files in the session's CodeDen home, as absolute paths (none without one). */
|
|
5743
|
+
function codedenCredentialFiles(ctx) {
|
|
5744
|
+
const home = ctx.codedenHome;
|
|
5745
|
+
if (!home) return [];
|
|
5746
|
+
const p = pathApi(ctx);
|
|
5747
|
+
return CODEDEN_CREDENTIAL_FILES.map((file) => p.join(home, file));
|
|
5748
|
+
}
|
|
5749
|
+
/** The credential files, then the folders that hold them. */
|
|
5750
|
+
function guardedPaths(ctx) {
|
|
5751
|
+
const files = codedenCredentialFiles(ctx);
|
|
5752
|
+
const p = pathApi(ctx);
|
|
5753
|
+
return [...files, ...new Set(files.map((file) => p.dirname(file)))];
|
|
5754
|
+
}
|
|
5755
|
+
const WILDCARD = /[*?[]/;
|
|
5756
|
+
/** `c` matched as itself in a regular expression. */
|
|
5757
|
+
const literalChar = (c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5758
|
+
/**
|
|
5759
|
+
* A shell wildcard (`*`, `?`, `[...]`, and `**`, which before a slash stands for zero or more
|
|
5760
|
+
* folders) over a `/`-separated path as a regular expression. Its first `literal` characters
|
|
5761
|
+
* match as themselves. Undefined when malformed.
|
|
5762
|
+
*/
|
|
5763
|
+
function wildcardPattern(pattern, ignoreCase, literal = 0) {
|
|
5764
|
+
let source = pattern.slice(0, literal).replace(/[\s\S]/g, literalChar);
|
|
5765
|
+
for (let i = literal; i < pattern.length; i += 1) {
|
|
5766
|
+
const c = pattern[i];
|
|
5767
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
5768
|
+
const folders = pattern[i + 2] === "/";
|
|
5769
|
+
source += folders ? "(?:.*/)?" : ".*";
|
|
5770
|
+
i += folders ? 2 : 1;
|
|
5771
|
+
} else if (c === "*") source += "[^/]*";
|
|
5772
|
+
else if (c === "?") source += "[^/]";
|
|
5773
|
+
else if (c === "[" && pattern.indexOf("]", i + 2) > 0) {
|
|
5774
|
+
const end = pattern.indexOf("]", i + 2);
|
|
5775
|
+
const body = pattern.slice(i + 1, end).replace(/^!/, "^").replace(/\\/g, "\\\\");
|
|
5776
|
+
source += `[${body}]`;
|
|
5777
|
+
i = end;
|
|
5778
|
+
} else source += literalChar(c);
|
|
5779
|
+
}
|
|
5780
|
+
try {
|
|
5781
|
+
return new RegExp(`^${source}$`, ignoreCase ? "i" : "");
|
|
5782
|
+
} catch {
|
|
5783
|
+
return;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
/** `/`-separated, for wildcard matching; Windows paths match in any case. */
|
|
5787
|
+
function slashes(value, ctx) {
|
|
5788
|
+
return isWin(ctx) ? value.replace(/\\/g, "/") : value;
|
|
5789
|
+
}
|
|
5790
|
+
/**
|
|
5791
|
+
* Variables left in a resolved path (`$f`, `${f}`, `$env:F`, `%F%`) could be any name, so they
|
|
5792
|
+
* match like `*`. `$CODEDEN_HOME`, `$HOME` and the temp variables at the start resolve first.
|
|
5793
|
+
*/
|
|
5794
|
+
const PATH_VARIABLE = /\$\{\w+\}|\$env:\w+|\$\w+|%\w+%/gi;
|
|
5795
|
+
/**
|
|
5796
|
+
* How many leading characters of `abs` are CodeDen's home or a folder above it, at a folder
|
|
5797
|
+
* boundary. That part comes from the session (`$CODEDEN_HOME`, `~`, the working directory), so it
|
|
5798
|
+
* is always literal: `[`, `*`, `?` or `$` in the home's own path (`CodeDen [dev]`) are never a
|
|
5799
|
+
* pattern. Compared like `samePath`, in any case on Windows.
|
|
5800
|
+
*/
|
|
5801
|
+
function literalPrefix(abs, ctx) {
|
|
5802
|
+
const home = ctx.codedenHome;
|
|
5803
|
+
if (!home) return 0;
|
|
5804
|
+
const p = pathApi(ctx);
|
|
5805
|
+
const value = slashes(abs, ctx);
|
|
5806
|
+
const fold = (s) => isWin(ctx) ? s.toLowerCase() : s;
|
|
5807
|
+
for (let dir = p.resolve(home);; dir = p.dirname(dir)) {
|
|
5808
|
+
const prefix = slashes(dir, ctx).replace(/\/$/, "");
|
|
5809
|
+
const n = prefix.length;
|
|
5810
|
+
const atBoundary = value.length === n || value[n] === "/";
|
|
5811
|
+
if (n > 0 && atBoundary && fold(value.slice(0, n)) === fold(prefix)) return n;
|
|
5812
|
+
if (p.dirname(dir) === dir) return 0;
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
/** `target` as an absolute path, the unknown variables it adds as wildcards. */
|
|
5816
|
+
function resolvePattern(target, ctx) {
|
|
5817
|
+
const abs = resolveAgainst(target, ctx);
|
|
5818
|
+
const literal = literalPrefix(abs, ctx);
|
|
5819
|
+
return abs.slice(0, literal) + abs.slice(literal).replace(PATH_VARIABLE, "*");
|
|
5820
|
+
}
|
|
5821
|
+
/**
|
|
5822
|
+
* The wildcards in `abs` (from `resolvePattern`) as a regular expression over `/`-separated
|
|
5823
|
+
* paths, or undefined when it has none (or is malformed). Only the agent's part can be a pattern:
|
|
5824
|
+
* CodeDen's home, and the folders above it, match literally (`literalPrefix`).
|
|
5825
|
+
*/
|
|
5826
|
+
function agentPattern(abs, ctx) {
|
|
5827
|
+
const value = slashes(abs, ctx);
|
|
5828
|
+
const literal = literalPrefix(abs, ctx);
|
|
5829
|
+
if (!WILDCARD.test(value.slice(literal))) return void 0;
|
|
5830
|
+
return wildcardPattern(value, isWin(ctx), literal);
|
|
5831
|
+
}
|
|
5832
|
+
/**
|
|
5833
|
+
* True when `target` reaches one of CodeDen's credential files: the file itself, a folder that
|
|
5834
|
+
* holds one (`<home>`, `<home>/runner`: copying or searching it reads them), or a wildcard that
|
|
5835
|
+
* matches either. `~`, `$HOME`, `$CODEDEN_HOME` (and its PowerShell and cmd forms) and relative
|
|
5836
|
+
* paths resolve as in the shell. The home itself is always compared literally.
|
|
5837
|
+
*/
|
|
5838
|
+
function touchesCodedenCredentials(target, ctx) {
|
|
5839
|
+
if (!ctx.codedenHome || !target.trim()) return false;
|
|
5840
|
+
const abs = resolvePattern(target, ctx);
|
|
5841
|
+
const guarded = guardedPaths(ctx);
|
|
5842
|
+
if (guarded.some((p) => samePath(p, abs, ctx))) return true;
|
|
5843
|
+
const pattern = agentPattern(abs, ctx);
|
|
5844
|
+
return pattern !== void 0 && guarded.some((p) => pattern.test(slashes(p, ctx)));
|
|
5845
|
+
}
|
|
5846
|
+
// ../../packages/providers/src/policy/search-words.ts
|
|
5847
|
+
/**
|
|
5848
|
+
* Search commands (`grep`, `rg`, `findstr`, `Select-String`, ...) read as the tool reads them, for
|
|
5849
|
+
* the risk classifier (§11.2). Each word gets one role, once: an option, an option's value or an
|
|
5850
|
+
* operand. The pattern is an option's pattern value, or else the first operand. Because roles are
|
|
5851
|
+
* fixed in one pass, a value that looks like an option (`rg -r-e`: replacement text `-e`) is never
|
|
5852
|
+
* read as one, and a word that looks like a pattern option is never skipped as a value.
|
|
5853
|
+
*/
|
|
5854
|
+
/** Search commands, whose first operand is the pattern (`grep -rn "process.env" src`). */
|
|
5855
|
+
const SEARCHES = /* @__PURE__ */ new Set([
|
|
5856
|
+
"grep",
|
|
5857
|
+
"egrep",
|
|
5858
|
+
"fgrep",
|
|
5859
|
+
"zgrep",
|
|
5860
|
+
"rg",
|
|
5861
|
+
"ag",
|
|
5862
|
+
"ack",
|
|
5863
|
+
"findstr",
|
|
5864
|
+
"select-string",
|
|
5865
|
+
"sls"
|
|
5866
|
+
]);
|
|
5867
|
+
/** Options that take the next word as their value, per search tool. */
|
|
5868
|
+
const GREP_VALUED = [
|
|
5869
|
+
"-A",
|
|
5870
|
+
"-B",
|
|
5871
|
+
"-C",
|
|
5872
|
+
"-m",
|
|
5873
|
+
"-d",
|
|
5874
|
+
"-D",
|
|
5875
|
+
"--after-context",
|
|
5876
|
+
"--before-context",
|
|
5877
|
+
"--context",
|
|
5878
|
+
"--max-count",
|
|
5879
|
+
"--include",
|
|
5880
|
+
"--exclude",
|
|
5881
|
+
"--exclude-dir",
|
|
5882
|
+
"--label",
|
|
5883
|
+
"--binary-files",
|
|
5884
|
+
"--devices",
|
|
5885
|
+
"--directories"
|
|
5886
|
+
];
|
|
5887
|
+
const RG_VALUED = [
|
|
5888
|
+
...GREP_VALUED,
|
|
5889
|
+
"-g",
|
|
5890
|
+
"--glob",
|
|
5891
|
+
"--iglob",
|
|
5892
|
+
"-t",
|
|
5893
|
+
"--type",
|
|
5894
|
+
"-T",
|
|
5895
|
+
"--type-not",
|
|
5896
|
+
"-j",
|
|
5897
|
+
"--threads",
|
|
5898
|
+
"--max-depth",
|
|
5899
|
+
"-M",
|
|
5900
|
+
"--max-columns",
|
|
5901
|
+
"--sort",
|
|
5902
|
+
"--sortr",
|
|
5903
|
+
"-r",
|
|
5904
|
+
"--replace",
|
|
5905
|
+
"--colors",
|
|
5906
|
+
"-E",
|
|
5907
|
+
"--encoding"
|
|
5908
|
+
];
|
|
5909
|
+
const SELECT_STRING_VALUED = [
|
|
5910
|
+
"-path",
|
|
5911
|
+
"-literalpath",
|
|
5912
|
+
"-include",
|
|
5913
|
+
"-exclude",
|
|
5914
|
+
"-encoding",
|
|
5915
|
+
"-context"
|
|
5916
|
+
];
|
|
5917
|
+
/** Tools whose option names are matched in any case (names are kept in lower case). */
|
|
5918
|
+
const ANY_CASE = /* @__PURE__ */ new Set([
|
|
5919
|
+
"findstr",
|
|
5920
|
+
"select-string",
|
|
5921
|
+
"sls"
|
|
5922
|
+
]);
|
|
5923
|
+
/**
|
|
5924
|
+
* Tools whose short options combine in one word, the getopt way (`-ne.` is `-n -e .`), and whose
|
|
5925
|
+
* `-e` and `-f` take a value. `ag -f` and `ack -f` take none, so their words stay as written.
|
|
5926
|
+
*/
|
|
5927
|
+
const CLUSTERING = /* @__PURE__ */ new Set([
|
|
5928
|
+
"grep",
|
|
5929
|
+
"egrep",
|
|
5930
|
+
"fgrep",
|
|
5931
|
+
"zgrep",
|
|
5932
|
+
"rg"
|
|
5933
|
+
]);
|
|
5934
|
+
/** Options whose value is the search pattern. */
|
|
5935
|
+
const PATTERN_OPTIONS = [
|
|
5936
|
+
"-e",
|
|
5937
|
+
"--regexp",
|
|
5938
|
+
"-pattern",
|
|
5939
|
+
"/c"
|
|
5940
|
+
];
|
|
5941
|
+
/** Options that give the pattern another way, a pattern file: then no operand is the pattern. */
|
|
5942
|
+
const isPatternFile = (name) => name === "-f" || name.startsWith("--file") || name === "/g";
|
|
5943
|
+
/** True when the option `name` takes the next word as its value. */
|
|
5944
|
+
function takesValue(bin, name) {
|
|
5945
|
+
if ([
|
|
5946
|
+
"-e",
|
|
5947
|
+
"--regexp",
|
|
5948
|
+
"-pattern",
|
|
5949
|
+
"-f",
|
|
5950
|
+
"--file"
|
|
5951
|
+
].includes(name)) return true;
|
|
5952
|
+
if (bin === "rg") return RG_VALUED.includes(name);
|
|
5953
|
+
if (bin === "select-string" || bin === "sls") return SELECT_STRING_VALUED.includes(name);
|
|
5954
|
+
return GREP_VALUED.includes(name);
|
|
5955
|
+
}
|
|
5956
|
+
/** An option word's value bound with `=` or `:` (`--glob=x`, `-Path:x`, `/c:x`). */
|
|
5957
|
+
const BOUND = /^(--?[A-Za-z][\w-]*|\/[A-Za-z])[=:]/;
|
|
5958
|
+
/** Parse a search command's arguments (after the program name) into roles. */
|
|
5959
|
+
function parseSearch(bin, args) {
|
|
5960
|
+
const words = [];
|
|
5961
|
+
const add = (text, role, arg, from = 0, option) => words.push({
|
|
5962
|
+
text,
|
|
5963
|
+
role,
|
|
5964
|
+
option,
|
|
5965
|
+
arg,
|
|
5966
|
+
from,
|
|
5967
|
+
pattern: false
|
|
5968
|
+
});
|
|
5969
|
+
const fold = (name) => ANY_CASE.has(bin) ? name.toLowerCase() : name;
|
|
5970
|
+
const isOption = (a) => a.startsWith("-") || bin === "findstr" && a.startsWith("/");
|
|
5971
|
+
let operandsOnly = false;
|
|
5972
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
5973
|
+
const a = args[i];
|
|
5974
|
+
if (operandsOnly || !isOption(a)) add(a, "operand", i);
|
|
5975
|
+
else if (a === "--") {
|
|
5976
|
+
operandsOnly = true;
|
|
5977
|
+
add(a, "option", i, 0, a);
|
|
5978
|
+
} else if (CLUSTERING.has(bin) && /^-[^-]./.test(a)) for (let j = 1; j < a.length; j += 1) {
|
|
5979
|
+
const flag = `-${a[j]}`;
|
|
5980
|
+
add(flag, "option", -1, 0, flag);
|
|
5981
|
+
if (!takesValue(bin, flag)) continue;
|
|
5982
|
+
if (j + 1 < a.length) add(a.slice(j + 1), "value", i, j + 1, flag);
|
|
5983
|
+
else if (i + 1 < args.length) add(args[++i], "value", i, 0, flag);
|
|
5984
|
+
break;
|
|
5985
|
+
}
|
|
5986
|
+
else {
|
|
5987
|
+
const bound = BOUND.exec(a);
|
|
5988
|
+
const name = fold(bound?.[1] ?? a);
|
|
5989
|
+
add(a, "option", i, 0, name);
|
|
5990
|
+
if (!bound && takesValue(bin, name) && i + 1 < args.length) add(args[++i], "value", i, 0, name);
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
markPatterns(words);
|
|
5994
|
+
return words;
|
|
5995
|
+
}
|
|
5996
|
+
/**
|
|
5997
|
+
* Mark the pattern words: the values of `-e`, `--regexp`, `-Pattern` and `findstr /c:`, or, when
|
|
5998
|
+
* no option gives the pattern (nor a pattern file, `-f`), the first operand.
|
|
5999
|
+
*/
|
|
6000
|
+
function markPatterns(words) {
|
|
6001
|
+
let explicit = false;
|
|
6002
|
+
for (const w of words) {
|
|
6003
|
+
const name = w.option ?? "";
|
|
6004
|
+
if (w.role === "value" && PATTERN_OPTIONS.includes(name)) {
|
|
6005
|
+
w.pattern = true;
|
|
6006
|
+
explicit = true;
|
|
6007
|
+
} else if (w.role === "option" && PATTERN_OPTIONS.includes(name)) {
|
|
6008
|
+
explicit = true;
|
|
6009
|
+
if (w.text.length > name.length) w.pattern = true;
|
|
6010
|
+
} else if (w.role === "option" && isPatternFile(name)) explicit = true;
|
|
6011
|
+
}
|
|
6012
|
+
if (explicit) return;
|
|
6013
|
+
const first = words.find((w) => w.role === "operand");
|
|
6014
|
+
if (first) first.pattern = true;
|
|
6015
|
+
}
|
|
6016
|
+
/** The file operands of a parsed search (`grep -r -e x src lib` -> `src`, `lib`). */
|
|
6017
|
+
function searchOperands(words) {
|
|
6018
|
+
return words.filter((w) => w.role === "operand" && !w.pattern);
|
|
6019
|
+
}
|
|
6020
|
+
/** The values given to the named options: `name X`, `name=X`, or in a cluster (`-gX`). */
|
|
6021
|
+
function searchOptionValues(words, names) {
|
|
6022
|
+
return words.flatMap((w) => {
|
|
6023
|
+
if (!w.option || !names.includes(w.option)) return [];
|
|
6024
|
+
if (w.role === "value") return [w.text];
|
|
6025
|
+
return w.role === "option" && w.text.length > w.option.length ? [w.text.slice(w.option.length + 1)] : [];
|
|
6026
|
+
});
|
|
6027
|
+
}
|
|
6028
|
+
// ../../packages/providers/src/policy/secrets.ts
|
|
6029
|
+
/**
|
|
6030
|
+
* Secrets rules of the risk classifier (§11.2): which words of a command read a credentials
|
|
6031
|
+
* file, and which commands dump the environment. Writing a secrets file inside the worktree is
|
|
6032
|
+
* not access (`cp .env.example .env`); reading one is.
|
|
6033
|
+
*/
|
|
6034
|
+
/**
|
|
6035
|
+
* The argument as a path, or undefined when it can't be one: inline code, multi-line text
|
|
6036
|
+
* or anything with shell or code punctuation (`selected={row.key`) is not a file name. A
|
|
6037
|
+
* variable in braces (`${HOME}/.env`) is a path, and so is an option's value: `--file=x`, or
|
|
6038
|
+
* PowerShell's colon-bound `-Path:x` and `-LiteralPath:x`.
|
|
6039
|
+
*/
|
|
6040
|
+
function asPath(arg) {
|
|
6041
|
+
const value = arg.replace(/^-+[a-z-]*=/i, "").replace(/^-[a-z][a-z-]*:(?=.)/i, "");
|
|
6042
|
+
if (!value || /[\n\r{}()<>|;=`]/.test(value.replace(/\$\{\w+\}/g, ""))) return void 0;
|
|
6043
|
+
return value;
|
|
6044
|
+
}
|
|
6045
|
+
/** Commands whose words are data or only checked for existence (`echo .env >> .gitignore`). */
|
|
6046
|
+
const DATA = /* @__PURE__ */ new Set([
|
|
6047
|
+
"echo",
|
|
6048
|
+
"printf",
|
|
6049
|
+
"write-output",
|
|
6050
|
+
"write-host",
|
|
6051
|
+
"test",
|
|
6052
|
+
"[",
|
|
6053
|
+
"[["
|
|
6054
|
+
]);
|
|
6055
|
+
/** Commands that list or describe files without reading them (`ls certs/*.pem`). */
|
|
6056
|
+
const LISTS = /* @__PURE__ */ new Set([
|
|
6057
|
+
"ls",
|
|
6058
|
+
"dir",
|
|
6059
|
+
"get-childitem",
|
|
6060
|
+
"gci",
|
|
6061
|
+
"find",
|
|
6062
|
+
"fd",
|
|
6063
|
+
"tree",
|
|
6064
|
+
"stat",
|
|
6065
|
+
"du",
|
|
6066
|
+
"test-path"
|
|
6067
|
+
]);
|
|
6068
|
+
/**
|
|
6069
|
+
* The first argument that reads a credentials file, if any. `written` are the paths the command
|
|
6070
|
+
* writes (a copy's destination), which classifyWrite judges instead. `isSecret` decides which
|
|
6071
|
+
* paths count (default: secrets files by name). `paths` gives what a word may stand for (each
|
|
6072
|
+
* element of a PowerShell list, `shell.wordPaths`). A search's patterns are skipped whole, list
|
|
6073
|
+
* elements included: the tool searches for them, not in them.
|
|
6074
|
+
*/
|
|
6075
|
+
function secretArgument(bin, args, written = [], isSecret = isSecretPath, paths = (word, from) => [word.slice(from)]) {
|
|
6076
|
+
if (DATA.has(bin)) return void 0;
|
|
6077
|
+
const runs = (bin === "find" || bin === "fd") && args.some((a) => /^-(exec|execdir|ok|okdir|x|X|-exec|-exec-batch)$/.test(a));
|
|
6078
|
+
if (LISTS.has(bin) && !runs) return void 0;
|
|
6079
|
+
return (SEARCHES.has(bin) ? parseSearch(bin, args) : args.map((text, arg) => ({
|
|
6080
|
+
text,
|
|
6081
|
+
arg,
|
|
6082
|
+
from: 0,
|
|
6083
|
+
pattern: false
|
|
6084
|
+
}))).find((w) => {
|
|
6085
|
+
if (w.pattern || written.includes(w.text)) return false;
|
|
6086
|
+
return (w.arg >= 0 ? paths(args[w.arg], w.from) : [w.text]).some((word) => braceAlternatives(word).some((alt) => {
|
|
6087
|
+
const value = asPath(alt);
|
|
6088
|
+
return value !== void 0 && isSecret(value);
|
|
6089
|
+
}));
|
|
6090
|
+
})?.text;
|
|
6091
|
+
}
|
|
6092
|
+
/** The most words one argument's brace expansion is followed to. */
|
|
6093
|
+
const MAX_ALTERNATIVES = 64;
|
|
6094
|
+
/**
|
|
6095
|
+
* The words a shell's brace expansion makes of `word` (`~/.ssh/{id_rsa,x}` -> `~/.ssh/id_rsa`,
|
|
6096
|
+
* `~/.ssh/x`), capped; just `word` when it has no `{a,b}` group.
|
|
6097
|
+
*/
|
|
6098
|
+
function braceAlternatives(word) {
|
|
6099
|
+
const group = /\{([^{}]*,[^{}]*)\}/.exec(word);
|
|
6100
|
+
if (!group?.[1]) return [word];
|
|
6101
|
+
const before = word.slice(0, group.index);
|
|
6102
|
+
const after = word.slice(group.index + group[0].length);
|
|
6103
|
+
const out = [];
|
|
6104
|
+
for (const part of group[1].split(",")) for (const alt of braceAlternatives(before + part + after)) {
|
|
6105
|
+
if (out.length >= MAX_ALTERNATIVES) return out;
|
|
6106
|
+
out.push(alt);
|
|
6107
|
+
}
|
|
6108
|
+
return out;
|
|
6109
|
+
}
|
|
6110
|
+
/** Files a tool writes through its own options (`openssl req -keyout key.pem -out cert.pem`). */
|
|
6111
|
+
function toolOutputs(bin, args) {
|
|
6112
|
+
if (bin !== "openssl" && bin !== "ssh-keygen") return [];
|
|
6113
|
+
const flags = bin === "openssl" ? ["-out", "-keyout"] : ["-f"];
|
|
6114
|
+
return args.flatMap((a, i) => flags.includes(a) && args[i + 1] ? [args[i + 1]] : []);
|
|
6115
|
+
}
|
|
6116
|
+
/** Variable names that may hold a credential. */
|
|
6117
|
+
const SECRET_VAR = /key|token|secret|passw|pwd|credential|auth|private|session|cookie/i;
|
|
6118
|
+
/**
|
|
6119
|
+
* True for `printenv` and `Get-ChildItem env:` listings. Printing named variables that don't
|
|
6120
|
+
* look like credentials (`printenv PATH`, `Get-ChildItem env:NODE_ENV`) is not a dump.
|
|
6121
|
+
*/
|
|
6122
|
+
function dumpsEnvironment(bin, args) {
|
|
6123
|
+
const named = (names) => names.length > 0 && names.every((n) => /^\w+$/.test(n) && !SECRET_VAR.test(n));
|
|
6124
|
+
if (bin === "printenv") return !named(args.filter((a) => !a.startsWith("-")));
|
|
6125
|
+
if (!/^(get-childitem|gci|ls|dir)$/.test(bin)) return false;
|
|
6126
|
+
const drives = args.filter((a) => /^env:/i.test(a));
|
|
6127
|
+
return drives.length > 0 && !named(drives.map((a) => a.slice(4)));
|
|
6128
|
+
}
|
|
6129
|
+
// ../../packages/providers/src/policy/codeden-search.ts
|
|
6130
|
+
/** The values of an option, given as `name X`, `name=X` or `name:X`, each time it appears. */
|
|
6131
|
+
function optionValues(args, names, anyCase) {
|
|
6132
|
+
const out = [];
|
|
6133
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
6134
|
+
const a = args[i];
|
|
6135
|
+
const word = anyCase ? a.toLowerCase() : a;
|
|
6136
|
+
if (names.includes(word)) {
|
|
6137
|
+
if (i + 1 < args.length) out.push(args[i + 1]);
|
|
6138
|
+
i += 1;
|
|
6139
|
+
continue;
|
|
6140
|
+
}
|
|
6141
|
+
const bound = names.find((n) => word.startsWith(`${n}=`) || word.startsWith(`${n}:`));
|
|
6142
|
+
if (bound) out.push(a.slice(bound.length + 1));
|
|
6143
|
+
}
|
|
6144
|
+
return out;
|
|
6145
|
+
}
|
|
6146
|
+
const GREPS = /* @__PURE__ */ new Set([
|
|
6147
|
+
"grep",
|
|
6148
|
+
"egrep",
|
|
6149
|
+
"fgrep",
|
|
6150
|
+
"zgrep"
|
|
6151
|
+
]);
|
|
6152
|
+
/** `grep -r`, `-R`, `-rn`, `--recursive`, `-d recurse`: grep reads folders only then. */
|
|
6153
|
+
function grepRecursive(words) {
|
|
6154
|
+
return words.some((w) => w.role === "option" && /^(-r|-R|--recursive|--dereference-recursive)$/.test(w.text)) || searchOptionValues(words, ["-d", "--directories"]).includes("recurse");
|
|
6155
|
+
}
|
|
6156
|
+
/**
|
|
6157
|
+
* The folders a search reads recursively, with its name filters. Roles come from `parseSearch`
|
|
6158
|
+
* (`grep -re. ~` is `grep -r -e . ~`; in `rg -r-g x ~`, `-g` is replacement text, not a filter),
|
|
6159
|
+
* and each root may be a PowerShell list (`paths`).
|
|
6160
|
+
*/
|
|
6161
|
+
function searchWalks(bin, args, ctx, paths) {
|
|
6162
|
+
const words = parseSearch(bin, args);
|
|
6163
|
+
const operands = searchOperands(words).flatMap((w) => paths(args[w.arg], w.from));
|
|
6164
|
+
if (bin === "rg" || bin === "ag" || bin === "ack") return [{
|
|
6165
|
+
roots: operands,
|
|
6166
|
+
recursive: true,
|
|
6167
|
+
names: bin === "rg" ? searchOptionValues(words, [
|
|
6168
|
+
"-g",
|
|
6169
|
+
"--glob",
|
|
6170
|
+
"--iglob"
|
|
6171
|
+
]).filter((n) => !n.startsWith("!")) : []
|
|
6172
|
+
}];
|
|
6173
|
+
if (GREPS.has(bin)) {
|
|
6174
|
+
if (!grepRecursive(words)) return [];
|
|
6175
|
+
return [{
|
|
6176
|
+
roots: operands,
|
|
6177
|
+
recursive: true,
|
|
6178
|
+
names: searchOptionValues(words, ["--include"])
|
|
6179
|
+
}];
|
|
6180
|
+
}
|
|
6181
|
+
const sub = words.some((w) => w.role === "option" && /^\/[a-z]*s[a-z]*$/i.test(w.text));
|
|
6182
|
+
if (bin === "findstr" && sub) {
|
|
6183
|
+
const p = pathApi(ctx);
|
|
6184
|
+
return operands.map((operand) => ({
|
|
6185
|
+
roots: [p.dirname(operand)],
|
|
6186
|
+
recursive: true,
|
|
6187
|
+
names: [p.basename(operand)]
|
|
6188
|
+
}));
|
|
6189
|
+
}
|
|
6190
|
+
return [];
|
|
6191
|
+
}
|
|
6192
|
+
const LISTINGS = /* @__PURE__ */ new Set([
|
|
6193
|
+
"get-childitem",
|
|
6194
|
+
"gci",
|
|
6195
|
+
"dir",
|
|
6196
|
+
"ls"
|
|
6197
|
+
]);
|
|
6198
|
+
const LISTING_VALUED = [
|
|
6199
|
+
"-filter",
|
|
6200
|
+
"-include",
|
|
6201
|
+
"-exclude",
|
|
6202
|
+
"-depth",
|
|
6203
|
+
"-attributes"
|
|
6204
|
+
];
|
|
6205
|
+
/** `find -exec cat {} +` and `fd -x cat` hand what they find to a command. */
|
|
6206
|
+
const RUNS = /^-(exec|execdir|ok|okdir|x|X|-exec|-exec-batch)$/;
|
|
6207
|
+
/** The folders a listing goes through, with its name filters. */
|
|
6208
|
+
function listingWalk(bin, args) {
|
|
6209
|
+
if (bin === "find") {
|
|
6210
|
+
const end = args.findIndex((a) => /^[-(!)]/.test(a));
|
|
6211
|
+
const either = args.some((a) => [
|
|
6212
|
+
"-o",
|
|
6213
|
+
"-or",
|
|
6214
|
+
"!",
|
|
6215
|
+
"-not"
|
|
6216
|
+
].includes(a));
|
|
6217
|
+
return {
|
|
6218
|
+
roots: end < 0 ? args : args.slice(0, end),
|
|
6219
|
+
recursive: true,
|
|
6220
|
+
names: either ? [] : optionValues(args, ["-name", "-iname"], false)
|
|
6221
|
+
};
|
|
6222
|
+
}
|
|
6223
|
+
if (bin === "fd") return {
|
|
6224
|
+
roots: args.filter((a) => !a.startsWith("-")).slice(1),
|
|
6225
|
+
recursive: true,
|
|
6226
|
+
names: []
|
|
6227
|
+
};
|
|
6228
|
+
if (!LISTINGS.has(bin)) return void 0;
|
|
6229
|
+
const recursive = args.some((a) => /^-rec/i.test(a) || a === "-r" || /^-[a-zA-Z]*R[a-zA-Z]*$/.test(a) || /^\/s$/i.test(a));
|
|
6230
|
+
const named = optionValues(args, ["-path", "-literalpath"], true);
|
|
6231
|
+
const positional = [];
|
|
6232
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
6233
|
+
const a = args[i];
|
|
6234
|
+
const lower = a.toLowerCase();
|
|
6235
|
+
if (LISTING_VALUED.includes(lower) || lower === "-path" || lower === "-literalpath") i += 1;
|
|
6236
|
+
else if (!a.startsWith("-") && !/^\/[a-z](:\S*)?$/i.test(a)) positional.push(a);
|
|
6237
|
+
}
|
|
6238
|
+
return {
|
|
6239
|
+
roots: [...named, ...positional].flatMap((root) => root.split(",")),
|
|
6240
|
+
recursive,
|
|
6241
|
+
names: optionValues(args, ["-filter", "-include"], true).flatMap((n) => n.split(","))
|
|
6242
|
+
};
|
|
6243
|
+
}
|
|
6244
|
+
/**
|
|
6245
|
+
* True when listing `dir` (and, recursively, its subfolders) reaches `folder`: by its path, or
|
|
6246
|
+
* through the wildcards the agent wrote in it (never ones in CodeDen's home path).
|
|
6247
|
+
*/
|
|
6248
|
+
function holds(dir, folder, recursive, ctx) {
|
|
6249
|
+
if (recursive ? isInside$1(folder, dir, ctx) : samePath(dir, folder, ctx)) return true;
|
|
6250
|
+
const pattern = agentPattern(dir, ctx);
|
|
6251
|
+
if (!pattern) return false;
|
|
6252
|
+
const p = pathApi(ctx);
|
|
6253
|
+
for (let at = folder;; at = p.dirname(at)) {
|
|
6254
|
+
if (pattern.test(slashes(at, ctx))) return true;
|
|
6255
|
+
if (!recursive || p.dirname(at) === at) return false;
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
/** True when `file`, found under `root`, passes one of the name filters (or there are none). */
|
|
6259
|
+
function keeps(names, file, root, ctx) {
|
|
6260
|
+
if (names.length === 0) return true;
|
|
6261
|
+
const p = pathApi(ctx);
|
|
6262
|
+
const base = p.basename(file);
|
|
6263
|
+
const below = slashes(p.relative(root, file), ctx);
|
|
6264
|
+
return names.flatMap((name) => braceAlternatives(name.trim().replace(/^\.[\\/]/, ""))).some((name) => {
|
|
6265
|
+
const pattern = wildcardPattern(slashes(name, ctx), true);
|
|
6266
|
+
if (!pattern) return true;
|
|
6267
|
+
return /[\\/]/.test(name) ? pattern.test(below) : pattern.test(base);
|
|
6268
|
+
});
|
|
6269
|
+
}
|
|
6270
|
+
/** The root of `walk` from which it reaches a credential file, if any. */
|
|
6271
|
+
function walkReaches(walk, ctx) {
|
|
6272
|
+
const files = codedenCredentialFiles(ctx);
|
|
6273
|
+
const p = pathApi(ctx);
|
|
6274
|
+
for (const root of walk.roots.length ? walk.roots : ["."]) for (const alt of braceAlternatives(root)) {
|
|
6275
|
+
const dir = resolvePattern(alt, ctx);
|
|
6276
|
+
if (files.some((file) => holds(dir, p.dirname(file), walk.recursive, ctx) && keeps(walk.names, file, dir, ctx))) return root;
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
/**
|
|
6280
|
+
* A recursive search from `roots` (Claude's Grep), keeping files whose names match `names`:
|
|
6281
|
+
* the root it reaches a credential file from, if any.
|
|
6282
|
+
*/
|
|
6283
|
+
function searchReaches(roots, names, ctx) {
|
|
6284
|
+
return ctx.codedenHome ? walkReaches({
|
|
6285
|
+
roots,
|
|
6286
|
+
recursive: true,
|
|
6287
|
+
names
|
|
6288
|
+
}, ctx) : void 0;
|
|
6289
|
+
}
|
|
6290
|
+
/**
|
|
6291
|
+
* How a command reaches CodeDen's credential files other than by naming one: a recursive search
|
|
6292
|
+
* from a folder above them, or a listing (or `echo`ed path) whose output `feeds` another command.
|
|
6293
|
+
* `paths` gives what a word may stand for (a PowerShell list's elements). Returns the word it
|
|
6294
|
+
* reaches them through.
|
|
6295
|
+
*/
|
|
6296
|
+
function reachesThroughFolder(bin, args, ctx, feeds, paths = (word, from) => [word.slice(from)]) {
|
|
6297
|
+
if (!ctx.codedenHome) return void 0;
|
|
6298
|
+
if (SEARCHES.has(bin)) for (const walk of searchWalks(bin, args, ctx, paths)) {
|
|
6299
|
+
const root = walkReaches(walk, ctx);
|
|
6300
|
+
if (root) return root;
|
|
6301
|
+
}
|
|
6302
|
+
if (feeds || (bin === "find" || bin === "fd") && args.some((a) => RUNS.test(a))) {
|
|
6303
|
+
const walk = listingWalk(bin, args);
|
|
6304
|
+
const root = walk && walkReaches(walk, ctx);
|
|
6305
|
+
if (root) return root;
|
|
6306
|
+
}
|
|
6307
|
+
if (feeds && DATA.has(bin)) return args.find((a) => paths(a, 0).some((path) => touchesCodedenCredentials(path, ctx)));
|
|
6308
|
+
}
|
|
5687
6309
|
// ../../packages/providers/src/util/git-head.ts
|
|
5688
6310
|
/**
|
|
5689
6311
|
* The branch checked out in the repository at `dir` (its `.git` folder, or the `gitdir:` a
|
|
@@ -5914,212 +6536,58 @@ const INTERPRETERS = new Map([
|
|
|
5914
6536
|
"mksh",
|
|
5915
6537
|
"ash",
|
|
5916
6538
|
"fish"
|
|
5917
|
-
].map((b) => [b, SHELL]),
|
|
5918
|
-
["node", NODE],
|
|
5919
|
-
["nodejs", NODE],
|
|
5920
|
-
["perl", PERL],
|
|
5921
|
-
["ruby", RUBY],
|
|
5922
|
-
["pwsh", POWERSHELL],
|
|
5923
|
-
["powershell", POWERSHELL],
|
|
5924
|
-
["source", SOURCE],
|
|
5925
|
-
[".", SOURCE]
|
|
5926
|
-
]);
|
|
5927
|
-
const interpreter = (bin) => INTERPRETERS.get(bin) ?? (/^(python[\d.]*|py|pypy3?)$/.test(bin) ? PYTHON : void 0);
|
|
5928
|
-
/** A program argument that means "read the program from stdin". */
|
|
5929
|
-
const isStdin = (arg) => arg === void 0 || arg === "-" || arg === "/dev/stdin" || arg === "/proc/self/fd/0";
|
|
5930
|
-
/**
|
|
5931
|
-
* True when the command runs what it reads on stdin as code: an interpreter given no program
|
|
5932
|
-
* (no `-c`/`-e` code, module or script file), or told to read it from stdin (`-`, `sh -s`).
|
|
5933
|
-
*/
|
|
5934
|
-
function runsStdin(cmd) {
|
|
5935
|
-
const { tokens } = unwrap(cmd.words);
|
|
5936
|
-
const bin = binName(tokens[0]);
|
|
5937
|
-
if (bin === "iex" || bin === "invoke-expression") return true;
|
|
5938
|
-
const spec = interpreter(bin);
|
|
5939
|
-
if (!spec) return false;
|
|
5940
|
-
const args = tokens.slice(1);
|
|
5941
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
5942
|
-
const arg = args[i];
|
|
5943
|
-
if (arg === "--") return isStdin(args[i + 1]);
|
|
5944
|
-
if (!/^[-+]./.test(arg)) return isStdin(arg);
|
|
5945
|
-
if (spec.stdin?.(arg)) return true;
|
|
5946
|
-
if (spec.program(arg)) return !arg.includes("=") && isStdin(args[i + 1]);
|
|
5947
|
-
if (spec.valued(arg)) i += 1;
|
|
5948
|
-
}
|
|
5949
|
-
return true;
|
|
5950
|
-
}
|
|
5951
|
-
// ../../packages/providers/src/policy/secrets.ts
|
|
5952
|
-
/**
|
|
5953
|
-
* Secrets rules of the risk classifier (§11.2): which words of a command read a credentials
|
|
5954
|
-
* file, and which commands dump the environment. Writing a secrets file inside the worktree is
|
|
5955
|
-
* not access (`cp .env.example .env`); reading one is.
|
|
5956
|
-
*/
|
|
5957
|
-
/**
|
|
5958
|
-
* The argument as a path, or undefined when it can't be one: inline code, multi-line text
|
|
5959
|
-
* or anything with shell or code punctuation (`selected={row.key`) is not a file name.
|
|
5960
|
-
*/
|
|
5961
|
-
function asPath(arg) {
|
|
5962
|
-
const value = arg.replace(/^-+[a-z-]*=/i, "");
|
|
5963
|
-
if (!value || /[\n\r{}()<>|;=`]/.test(value)) return void 0;
|
|
5964
|
-
return value;
|
|
5965
|
-
}
|
|
5966
|
-
/** Commands whose words are data or only checked for existence (`echo .env >> .gitignore`). */
|
|
5967
|
-
const DATA = /* @__PURE__ */ new Set([
|
|
5968
|
-
"echo",
|
|
5969
|
-
"printf",
|
|
5970
|
-
"write-output",
|
|
5971
|
-
"write-host",
|
|
5972
|
-
"test",
|
|
5973
|
-
"[",
|
|
5974
|
-
"[["
|
|
5975
|
-
]);
|
|
5976
|
-
/** Commands that list or describe files without reading them (`ls certs/*.pem`). */
|
|
5977
|
-
const LISTS = /* @__PURE__ */ new Set([
|
|
5978
|
-
"ls",
|
|
5979
|
-
"dir",
|
|
5980
|
-
"get-childitem",
|
|
5981
|
-
"gci",
|
|
5982
|
-
"find",
|
|
5983
|
-
"fd",
|
|
5984
|
-
"tree",
|
|
5985
|
-
"stat",
|
|
5986
|
-
"du",
|
|
5987
|
-
"test-path"
|
|
5988
|
-
]);
|
|
5989
|
-
/** Search commands, whose first operand is the pattern (`grep -rn "process.env" src`). */
|
|
5990
|
-
const SEARCHES = /* @__PURE__ */ new Set([
|
|
5991
|
-
"grep",
|
|
5992
|
-
"egrep",
|
|
5993
|
-
"fgrep",
|
|
5994
|
-
"zgrep",
|
|
5995
|
-
"rg",
|
|
5996
|
-
"ag",
|
|
5997
|
-
"ack",
|
|
5998
|
-
"findstr",
|
|
5999
|
-
"select-string",
|
|
6000
|
-
"sls"
|
|
6001
|
-
]);
|
|
6002
|
-
/** Options that take the next word as their value, per search tool. */
|
|
6003
|
-
const GREP_VALUED = [
|
|
6004
|
-
"-A",
|
|
6005
|
-
"-B",
|
|
6006
|
-
"-C",
|
|
6007
|
-
"-m",
|
|
6008
|
-
"-d",
|
|
6009
|
-
"-D",
|
|
6010
|
-
"--after-context",
|
|
6011
|
-
"--before-context",
|
|
6012
|
-
"--context",
|
|
6013
|
-
"--max-count",
|
|
6014
|
-
"--include",
|
|
6015
|
-
"--exclude",
|
|
6016
|
-
"--exclude-dir",
|
|
6017
|
-
"--label",
|
|
6018
|
-
"--binary-files",
|
|
6019
|
-
"--devices",
|
|
6020
|
-
"--directories"
|
|
6021
|
-
];
|
|
6022
|
-
const RG_VALUED = [
|
|
6023
|
-
...GREP_VALUED,
|
|
6024
|
-
"-g",
|
|
6025
|
-
"--glob",
|
|
6026
|
-
"--iglob",
|
|
6027
|
-
"-t",
|
|
6028
|
-
"--type",
|
|
6029
|
-
"-T",
|
|
6030
|
-
"--type-not",
|
|
6031
|
-
"-j",
|
|
6032
|
-
"--threads",
|
|
6033
|
-
"--max-depth",
|
|
6034
|
-
"-M",
|
|
6035
|
-
"--max-columns",
|
|
6036
|
-
"--sort",
|
|
6037
|
-
"--sortr",
|
|
6038
|
-
"-r",
|
|
6039
|
-
"--replace",
|
|
6040
|
-
"--colors",
|
|
6041
|
-
"-E",
|
|
6042
|
-
"--encoding"
|
|
6043
|
-
];
|
|
6044
|
-
const SELECT_STRING_VALUED = [
|
|
6045
|
-
"-path",
|
|
6046
|
-
"-literalpath",
|
|
6047
|
-
"-include",
|
|
6048
|
-
"-exclude",
|
|
6049
|
-
"-encoding",
|
|
6050
|
-
"-context"
|
|
6051
|
-
];
|
|
6052
|
-
function searchValued(bin, arg) {
|
|
6053
|
-
if (bin === "rg") return RG_VALUED.includes(arg);
|
|
6054
|
-
if (bin === "select-string" || bin === "sls") return SELECT_STRING_VALUED.includes(arg.toLowerCase());
|
|
6055
|
-
return GREP_VALUED.includes(arg);
|
|
6056
|
-
}
|
|
6539
|
+
].map((b) => [b, SHELL]),
|
|
6540
|
+
["node", NODE],
|
|
6541
|
+
["nodejs", NODE],
|
|
6542
|
+
["perl", PERL],
|
|
6543
|
+
["ruby", RUBY],
|
|
6544
|
+
["pwsh", POWERSHELL],
|
|
6545
|
+
["powershell", POWERSHELL],
|
|
6546
|
+
["source", SOURCE],
|
|
6547
|
+
[".", SOURCE]
|
|
6548
|
+
]);
|
|
6549
|
+
const interpreter = (bin) => INTERPRETERS.get(bin) ?? (/^(python[\d.]*|py|pypy3?)$/.test(bin) ? PYTHON : void 0);
|
|
6550
|
+
/** A program argument that means "read the program from stdin". */
|
|
6551
|
+
const isStdin = (arg) => arg === void 0 || arg === "-" || arg === "/dev/stdin" || arg === "/proc/self/fd/0";
|
|
6057
6552
|
/**
|
|
6058
|
-
*
|
|
6059
|
-
*
|
|
6060
|
-
* `/c:"text"` is a pattern too.
|
|
6553
|
+
* True when the command runs what it reads on stdin as code: an interpreter given no program
|
|
6554
|
+
* (no `-c`/`-e` code, module or script file), or told to read it from stdin (`-`, `sh -s`).
|
|
6061
6555
|
*/
|
|
6062
|
-
function
|
|
6063
|
-
const
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
const value = args[i + 1];
|
|
6070
|
-
if (value !== void 0) patterns.add(value);
|
|
6071
|
-
i += 1;
|
|
6072
|
-
} else if (a.startsWith("--regexp=") || /^\/c:/i.test(a)) {
|
|
6073
|
-
explicit = true;
|
|
6074
|
-
patterns.add(a);
|
|
6075
|
-
} else if (a === "-f" || a.startsWith("--file") || /^\/g:/i.test(a)) explicit = true;
|
|
6076
|
-
}
|
|
6077
|
-
if (explicit) return patterns;
|
|
6556
|
+
function runsStdin(cmd) {
|
|
6557
|
+
const { tokens } = unwrap(cmd.words);
|
|
6558
|
+
const bin = binName(tokens[0]);
|
|
6559
|
+
if (bin === "iex" || bin === "invoke-expression") return true;
|
|
6560
|
+
const spec = interpreter(bin);
|
|
6561
|
+
if (!spec) return false;
|
|
6562
|
+
const args = tokens.slice(1);
|
|
6078
6563
|
for (let i = 0; i < args.length; i += 1) {
|
|
6079
|
-
const
|
|
6080
|
-
if (
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6564
|
+
const arg = args[i];
|
|
6565
|
+
if (arg === "--") return isStdin(args[i + 1]);
|
|
6566
|
+
if (!/^[-+]./.test(arg)) return isStdin(arg);
|
|
6567
|
+
if (spec.stdin?.(arg)) return true;
|
|
6568
|
+
if (spec.program(arg)) return !arg.includes("=") && isStdin(args[i + 1]);
|
|
6569
|
+
if (spec.valued(arg)) i += 1;
|
|
6085
6570
|
}
|
|
6086
|
-
return
|
|
6087
|
-
}
|
|
6088
|
-
/**
|
|
6089
|
-
* The first argument that reads a credentials file, if any. `written` are the paths the command
|
|
6090
|
-
* writes (a copy's destination), which classifyWrite judges instead.
|
|
6091
|
-
*/
|
|
6092
|
-
function secretArgument(bin, args, written = []) {
|
|
6093
|
-
if (DATA.has(bin)) return void 0;
|
|
6094
|
-
const runs = (bin === "find" || bin === "fd") && args.some((a) => /^-(exec|execdir|ok|okdir|x|X|-exec|-exec-batch)$/.test(a));
|
|
6095
|
-
if (LISTS.has(bin) && !runs) return void 0;
|
|
6096
|
-
const patterns = SEARCHES.has(bin) ? searchPatterns(bin, args) : /* @__PURE__ */ new Set();
|
|
6097
|
-
return args.find((a) => {
|
|
6098
|
-
if (patterns.has(a) || written.includes(a)) return false;
|
|
6099
|
-
const value = asPath(a);
|
|
6100
|
-
return value !== void 0 && isSecretPath(value);
|
|
6101
|
-
});
|
|
6102
|
-
}
|
|
6103
|
-
/** Files a tool writes through its own options (`openssl req -keyout key.pem -out cert.pem`). */
|
|
6104
|
-
function toolOutputs(bin, args) {
|
|
6105
|
-
if (bin !== "openssl" && bin !== "ssh-keygen") return [];
|
|
6106
|
-
const flags = bin === "openssl" ? ["-out", "-keyout"] : ["-f"];
|
|
6107
|
-
return args.flatMap((a, i) => flags.includes(a) && args[i + 1] ? [args[i + 1]] : []);
|
|
6571
|
+
return true;
|
|
6108
6572
|
}
|
|
6109
|
-
|
|
6110
|
-
const SECRET_VAR = /key|token|secret|passw|pwd|credential|auth|private|session|cookie/i;
|
|
6573
|
+
// ../../packages/providers/src/policy/shell.ts
|
|
6111
6574
|
/**
|
|
6112
|
-
*
|
|
6113
|
-
*
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
const
|
|
6120
|
-
|
|
6575
|
+
* The paths a word of `cmd` may stand for, from `from` on: its text and, for a PowerShell list
|
|
6576
|
+
* (`a,b`), each element. The whole text stays: when the shell is unknown it may be one name, or a
|
|
6577
|
+
* brace expansion (`{a,b}`). Callers apply it after reading the word's role, so a list given as a
|
|
6578
|
+
* search pattern stays data.
|
|
6579
|
+
*/
|
|
6580
|
+
function wordPaths(cmd, word, from = 0) {
|
|
6581
|
+
const text = word.slice(from);
|
|
6582
|
+
const commas = cmd.lists?.get(word)?.filter((at) => at >= from) ?? [];
|
|
6583
|
+
if (!commas.length) return [text];
|
|
6584
|
+
const bounds = [
|
|
6585
|
+
from - 1,
|
|
6586
|
+
...commas,
|
|
6587
|
+
word.length
|
|
6588
|
+
];
|
|
6589
|
+
return [text, ...bounds.slice(1).map((end, k) => word.slice(bounds[k] + 1, end)).filter(Boolean)];
|
|
6121
6590
|
}
|
|
6122
|
-
// ../../packages/providers/src/policy/shell.ts
|
|
6123
6591
|
/** Parse a command line into its simple commands. */
|
|
6124
6592
|
function parseShell(src, dialect = "auto") {
|
|
6125
6593
|
return new Lexer(src, dialect).list(null);
|
|
@@ -6140,6 +6608,8 @@ var Builder = class {
|
|
|
6140
6608
|
target = "word";
|
|
6141
6609
|
heredocStrip = false;
|
|
6142
6610
|
heredocs = [];
|
|
6611
|
+
/** Where the word's unquoted commas are: a PowerShell list's element boundaries. */
|
|
6612
|
+
commas = [];
|
|
6143
6613
|
constructor(out) {
|
|
6144
6614
|
this.out = out;
|
|
6145
6615
|
}
|
|
@@ -6147,6 +6617,11 @@ var Builder = class {
|
|
|
6147
6617
|
this.word += text;
|
|
6148
6618
|
this.inWord = true;
|
|
6149
6619
|
}
|
|
6620
|
+
/** An unquoted comma, which separates a PowerShell list's elements (`a,b`). */
|
|
6621
|
+
addComma() {
|
|
6622
|
+
this.commas.push(this.word.length);
|
|
6623
|
+
this.add(",");
|
|
6624
|
+
}
|
|
6150
6625
|
endWord() {
|
|
6151
6626
|
if (!this.inWord) return;
|
|
6152
6627
|
if (this.target === "write") this.cmd.writes.push(this.word);
|
|
@@ -6154,10 +6629,24 @@ var Builder = class {
|
|
|
6154
6629
|
delim: this.word.replace(/\\/g, ""),
|
|
6155
6630
|
strip: this.heredocStrip
|
|
6156
6631
|
});
|
|
6157
|
-
else if (this.target === "word")
|
|
6632
|
+
else if (this.target === "word") {
|
|
6633
|
+
this.cmd.words.push(this.word);
|
|
6634
|
+
if (this.commas.length) this.addList();
|
|
6635
|
+
} else if (this.target === "skip") this.cmd.input = [...this.cmd.input ?? [], this.word];
|
|
6158
6636
|
this.target = "word";
|
|
6159
6637
|
this.word = "";
|
|
6160
6638
|
this.inWord = false;
|
|
6639
|
+
this.commas = [];
|
|
6640
|
+
}
|
|
6641
|
+
/**
|
|
6642
|
+
* Note where the current word's unquoted commas are. The same text written twice with different
|
|
6643
|
+
* quoting keeps every comma either one had, so its list is never missed.
|
|
6644
|
+
*/
|
|
6645
|
+
addList() {
|
|
6646
|
+
this.cmd.lists ??= /* @__PURE__ */ new Map();
|
|
6647
|
+
const seen = this.cmd.lists.get(this.word) ?? [];
|
|
6648
|
+
const commas = [.../* @__PURE__ */ new Set([...seen, ...this.commas])].sort((a, b) => a - b);
|
|
6649
|
+
this.cmd.lists.set(this.word, commas);
|
|
6161
6650
|
}
|
|
6162
6651
|
endCommand() {
|
|
6163
6652
|
this.endWord();
|
|
@@ -6234,6 +6723,9 @@ var Lexer = class {
|
|
|
6234
6723
|
this.i += 1;
|
|
6235
6724
|
b.cmd.nested.push(this.list(")"));
|
|
6236
6725
|
b.add(src.slice(start, this.i));
|
|
6726
|
+
} else if (c === "," && this.dialect !== "posix") {
|
|
6727
|
+
b.addComma();
|
|
6728
|
+
this.i += 1;
|
|
6237
6729
|
} else {
|
|
6238
6730
|
b.add(c);
|
|
6239
6731
|
this.i += 1;
|
|
@@ -6376,12 +6868,62 @@ const MAX_DEPTH = 4;
|
|
|
6376
6868
|
* Classify a shell command (any shell: POSIX, PowerShell or cmd). Downloads piped into an
|
|
6377
6869
|
* interpreter (`curl … | sh`, `irm … | iex`) are found on the parsed pipeline in
|
|
6378
6870
|
* `classifyList`; here, a download run through a substitution (`iex (irm …)`,
|
|
6379
|
-
* `bash -c "$(curl …)"`, `bash <(curl …)`).
|
|
6871
|
+
* `bash -c "$(curl …)"`, `bash <(curl …)`). CodeDen's own credentials are looked for first,
|
|
6872
|
+
* across the whole command, so an earlier ordinary risk (`cat .env; …`) can't hide them.
|
|
6380
6873
|
*/
|
|
6381
6874
|
function classifyCommand(command, ctx) {
|
|
6875
|
+
const list = parseShell(command, ctx.shell ?? "auto");
|
|
6876
|
+
const credentials = credentialAccess(list, ctx, 0);
|
|
6877
|
+
if (credentials) return credentials;
|
|
6382
6878
|
if (/\b(iex|invoke-expression)\b[^|]*\b(irm|iwr|invoke-restmethod|invoke-webrequest|downloadstring)\b/i.test(command)) return risky("network", "Executes a downloaded script");
|
|
6383
6879
|
if (/\b(bash|sh|zsh)\s+(-c\s+)?["']?\$\(\s*(curl|wget)\b/i.test(command) || /<\(\s*(curl|wget)\b/i.test(command)) return risky("network", "Executes a downloaded script");
|
|
6384
|
-
return classifyList(
|
|
6880
|
+
return classifyList(list, ctx, 0);
|
|
6881
|
+
}
|
|
6882
|
+
/**
|
|
6883
|
+
* The first access to CodeDen's own credential files anywhere in `list`: substitutions,
|
|
6884
|
+
* subshells and inline scripts (`bash -c "…"`) included, following `cd` like `classifyList`.
|
|
6885
|
+
* `feeds`: the list's output is used by another command (it sits in a substitution or a pipe).
|
|
6886
|
+
*/
|
|
6887
|
+
function credentialAccess(list, ctx, depth, feeds = false) {
|
|
6888
|
+
let here = ctx;
|
|
6889
|
+
for (const [i, cmd] of list.entries()) {
|
|
6890
|
+
for (const nested of cmd.nested) {
|
|
6891
|
+
const found = credentialAccess(nested, here, depth, true);
|
|
6892
|
+
if (found) return found;
|
|
6893
|
+
}
|
|
6894
|
+
const piped = list[i + 1]?.piped === true;
|
|
6895
|
+
const found = credentialCommand(cmd, here, depth, feeds || piped);
|
|
6896
|
+
if (found) return found;
|
|
6897
|
+
here = afterCd(cmd, here);
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
/** The paths each word of `cmd` may stand for: itself, and a PowerShell list's elements. */
|
|
6901
|
+
const pathsOf = (cmd) => (word, from) => wordPaths(cmd, word, from);
|
|
6902
|
+
/**
|
|
6903
|
+
* One simple command's access to CodeDen's credentials: a redirect onto one, any argument (or
|
|
6904
|
+
* here-string) that reaches one (read, written, copied or deleted), or a search or listing that
|
|
6905
|
+
* reaches one from a folder above it (`./codeden-search`). Moving into CodeDen's home with `cd`
|
|
6906
|
+
* reads nothing; the command that then reads is caught.
|
|
6907
|
+
*/
|
|
6908
|
+
function credentialCommand(cmd, ctx, depth, feeds) {
|
|
6909
|
+
const { tokens } = unwrap(cmd.words);
|
|
6910
|
+
const bin = binName(tokens[0]);
|
|
6911
|
+
const args = tokens.slice(1);
|
|
6912
|
+
const script = inlineScript(bin, args);
|
|
6913
|
+
if (script && depth < MAX_DEPTH) {
|
|
6914
|
+
const inner = credentialAccess(parseShell(script.text, script.dialect), ctx, depth + 1, feeds);
|
|
6915
|
+
if (inner) return inner;
|
|
6916
|
+
}
|
|
6917
|
+
for (const target of cmd.writes) if (!DISCARD.test(target) && touchesCodedenCredentials(target, ctx)) return credentialsRisk(`Writes CodeDen's own credentials: ${target}`);
|
|
6918
|
+
const given = (cmd.input ?? []).find((word) => touchesCodedenCredentials(word, ctx));
|
|
6919
|
+
if (given) return credentialsRisk(`Passes CodeDen's own credentials: ${given}`);
|
|
6920
|
+
if (CD.has(bin)) return void 0;
|
|
6921
|
+
const paths = pathsOf(cmd);
|
|
6922
|
+
const reads = (a) => touchesCodedenCredentials(a, ctx);
|
|
6923
|
+
const touched = secretArgument(bin, args, [], reads, paths);
|
|
6924
|
+
if (touched) return credentialsRisk(`Accesses CodeDen's own credentials: ${touched}`);
|
|
6925
|
+
const through = reachesThroughFolder(bin, args, ctx, feeds, paths);
|
|
6926
|
+
return through ? credentialsRisk(`Reaches CodeDen's own credentials from ${through}`) : void 0;
|
|
6385
6927
|
}
|
|
6386
6928
|
/**
|
|
6387
6929
|
* Classify commands in order. A `cd` moves where later relative paths resolve; commands in
|
|
@@ -6421,10 +6963,17 @@ function afterCd(cmd, ctx) {
|
|
|
6421
6963
|
dir: resolveAgainst(target, ctx)
|
|
6422
6964
|
};
|
|
6423
6965
|
}
|
|
6424
|
-
/**
|
|
6966
|
+
/**
|
|
6967
|
+
* The value of a PowerShell-style named parameter, if present: `-Path x`, or bound with a
|
|
6968
|
+
* colon, `-Path:x`.
|
|
6969
|
+
*/
|
|
6425
6970
|
function namedArg(args, names) {
|
|
6426
|
-
|
|
6427
|
-
|
|
6971
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
6972
|
+
const arg = args[i].toLowerCase();
|
|
6973
|
+
if (names.includes(arg)) return args[i + 1];
|
|
6974
|
+
const bound = names.find((name) => arg.startsWith(`${name}:`));
|
|
6975
|
+
if (bound) return args[i].slice(bound.length + 1);
|
|
6976
|
+
}
|
|
6428
6977
|
}
|
|
6429
6978
|
/** The script of an inline shell invocation (`bash -c "…"`, `pwsh -Command …`, `cmd /c …`). */
|
|
6430
6979
|
function inlineScript(bin, args) {
|
|
@@ -6507,7 +7056,7 @@ function classifySimple(cmd, ctx, depth) {
|
|
|
6507
7056
|
const write = classifyWrite(target, ctx);
|
|
6508
7057
|
if (write.risky) return write;
|
|
6509
7058
|
}
|
|
6510
|
-
const secret = secretArgument(bin, args, written);
|
|
7059
|
+
const secret = secretArgument(bin, args, written, isSecretPath, pathsOf(cmd));
|
|
6511
7060
|
if (secret) return risky("secrets", `Accesses the secrets file ${secret}`);
|
|
6512
7061
|
if (dumpsEnvironment(bin, args)) return risky("secrets", "Dumps environment variables");
|
|
6513
7062
|
return SAFE;
|
|
@@ -6564,18 +7113,36 @@ function copyTargets(bin, args, dest, ctx) {
|
|
|
6564
7113
|
* Classify a file write/edit. The worktree, extra roots and OS temp are fine, secrets files in
|
|
6565
7114
|
* the worktree included: writing one (`cp .env.example .env`) reads nothing. A secrets file in
|
|
6566
7115
|
* temp counts as outside the worktree, so Full autonomy (which only asks for writes outside the
|
|
6567
|
-
* worktree) still asks for it.
|
|
7116
|
+
* worktree) still asks for it. CodeDen's own credential files ask at every level.
|
|
6568
7117
|
*/
|
|
6569
7118
|
function classifyWrite(target, ctx) {
|
|
7119
|
+
if (touchesCodedenCredentials(target, ctx)) return credentialsRisk(`Writes CodeDen's own credentials: ${target}`);
|
|
6570
7120
|
if (!isWritable(target, ctx)) return risky("outside_worktree", `Writes ${target}, outside the worktree`);
|
|
6571
7121
|
if (isSecretPath(target) && !isInWorkspace(target, ctx)) return risky("outside_worktree", `Writes the secrets file ${target}, outside the worktree`);
|
|
6572
7122
|
return SAFE;
|
|
6573
7123
|
}
|
|
6574
|
-
/**
|
|
6575
|
-
|
|
7124
|
+
/**
|
|
7125
|
+
* Classify a file read or search. Reads anywhere are fine except credential files; CodeDen's own
|
|
7126
|
+
* (or a folder holding them) ask at every level.
|
|
7127
|
+
*/
|
|
7128
|
+
function classifyRead(target, ctx) {
|
|
7129
|
+
if (touchesCodedenCredentials(target, ctx)) return credentialsRisk(`Reads CodeDen's own credentials: ${target}`);
|
|
6576
7130
|
if (isSecretPath(target)) return risky("secrets", `Reads the secrets file ${target}`);
|
|
6577
7131
|
return SAFE;
|
|
6578
7132
|
}
|
|
7133
|
+
/**
|
|
7134
|
+
* Classify a file-name search: `pattern` (Claude's Glob, or a search's `--glob` filter) from
|
|
7135
|
+
* `base` (default: the current directory). Listing names reads nothing, so only a pattern that
|
|
7136
|
+
* reaches CodeDen's own credential files asks; `{a,b}` alternatives are followed.
|
|
7137
|
+
*/
|
|
7138
|
+
function classifyGlob(pattern, base, ctx) {
|
|
7139
|
+
if (!pattern.trim()) return SAFE;
|
|
7140
|
+
const dir = base ? resolveAgainst(base, ctx) : ctx.dir ?? ctx.cwd;
|
|
7141
|
+
return braceAlternatives(pattern).some((alt) => touchesCodedenCredentials(alt, {
|
|
7142
|
+
...ctx,
|
|
7143
|
+
dir
|
|
7144
|
+
})) ? credentialsRisk(`Lists CodeDen's own credential files: ${pattern}`) : SAFE;
|
|
7145
|
+
}
|
|
6579
7146
|
/** Classify a web fetch. Only flagged when the context has an allowlist of hosts. */
|
|
6580
7147
|
function classifyFetch(url, ctx) {
|
|
6581
7148
|
if (!ctx.allowedHosts) return SAFE;
|
|
@@ -6801,9 +7368,11 @@ function assessClaudeTool(name, input, ctx) {
|
|
|
6801
7368
|
if (name === "Read") return classifyRead(str(input, "file_path") ?? "", ctx);
|
|
6802
7369
|
if (name === "Grep") {
|
|
6803
7370
|
const target = str(input, "path");
|
|
6804
|
-
|
|
7371
|
+
const filter = str(input, "glob");
|
|
7372
|
+
const root = searchReaches(target ? [target] : [], filter ? [filter] : [], ctx);
|
|
7373
|
+
return mostSevere([target ? classifyRead(target, ctx) : SAFE, root ? credentialsRisk(`Reaches CodeDen's own credentials from ${root}`) : SAFE]);
|
|
6805
7374
|
}
|
|
6806
|
-
if (name === "Glob") return
|
|
7375
|
+
if (name === "Glob") return classifyGlob(str(input, "pattern") ?? "", str(input, "path"), ctx);
|
|
6807
7376
|
if (name === "WebFetch") return classifyFetch(str(input, "url") ?? "", ctx);
|
|
6808
7377
|
return { risky: false };
|
|
6809
7378
|
}
|
|
@@ -6835,7 +7404,8 @@ const FINISH_PLAN = "Finish the plan and present it with ExitPlanMode; once the
|
|
|
6835
7404
|
* - auto -> `acceptEdits`, plus the risky-action classifier
|
|
6836
7405
|
* - full -> `bypassPermissions`, asking only for writes outside the worktree (the OS
|
|
6837
7406
|
* temp directory counts as inside, except for secrets files there)
|
|
6838
|
-
* Plan mode overrides all of them with `plan`.
|
|
7407
|
+
* Plan mode overrides all of them with `plan`. CodeDen's own credential files ask at every
|
|
7408
|
+
* level and are refused for read-only sessions (`../policy/codeden-home`).
|
|
6839
7409
|
*/
|
|
6840
7410
|
function permissionModeFor(autonomy, planMode) {
|
|
6841
7411
|
if (planMode) return "plan";
|
|
@@ -6853,6 +7423,8 @@ var ClaudePolicy = class {
|
|
|
6853
7423
|
ctx;
|
|
6854
7424
|
readOnly;
|
|
6855
7425
|
sessionAllow = /* @__PURE__ */ new Set();
|
|
7426
|
+
/** `allow_session` decisions on calls that touched CodeDen's own credentials (`credentialKey`). */
|
|
7427
|
+
credentialAllow = /* @__PURE__ */ new Set();
|
|
6856
7428
|
/** Plan mode: edits outside Claude's notes and writing commands are refused. */
|
|
6857
7429
|
planning = false;
|
|
6858
7430
|
constructor(autonomy, ctx, readOnly = false) {
|
|
@@ -6881,8 +7453,12 @@ var ClaudePolicy = class {
|
|
|
6881
7453
|
setPlanning(on) {
|
|
6882
7454
|
this.planning = on;
|
|
6883
7455
|
}
|
|
6884
|
-
/**
|
|
6885
|
-
|
|
7456
|
+
/**
|
|
7457
|
+
* Why a call is refused outright (read-only sessions, plan mode), or undefined. `dir` is the
|
|
7458
|
+
* shell's current directory, as for `gate`.
|
|
7459
|
+
*/
|
|
7460
|
+
denial(toolName, input, dir) {
|
|
7461
|
+
if (this.isReadOnly && touchesCodeden(this.assess(toolName, input, dir))) return this.readOnly ? "This session is read-only: CodeDen's own credential files are off limits. Act through CodeDen tools instead." : "This session has read-only permissions: CodeDen's own credential files are off limits. Report what you found instead.";
|
|
6886
7462
|
if (this.planning && !this.readOnly && this.isNotesEdit(toolName, input)) return void 0;
|
|
6887
7463
|
if (!this.isReadOnly) return this.planning ? this.planDenial(toolName, input) : void 0;
|
|
6888
7464
|
if (EDIT_TOOLS$1.has(toolName)) return this.readOnly ? "This session is read-only: it cannot edit files. Act through CodeDen tools instead." : "This session has read-only permissions: it cannot edit files, commit or push. Report what you found instead.";
|
|
@@ -6923,11 +7499,8 @@ var ClaudePolicy = class {
|
|
|
6923
7499
|
action: "plan",
|
|
6924
7500
|
reason: "The agent finished planning"
|
|
6925
7501
|
};
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
...this.ctx,
|
|
6929
|
-
dir
|
|
6930
|
-
} : this.ctx);
|
|
7502
|
+
const risk = this.assess(toolName, input, dir);
|
|
7503
|
+
if (touchesCodeden(risk) ? this.credentialAllow.has(this.credentialKey(toolName, input, dir)) : this.sessionAllow.has(this.key(toolName, input))) return { ask: false };
|
|
6931
7504
|
if (this.autonomy === "supervised") {
|
|
6932
7505
|
if (risk.risky) return {
|
|
6933
7506
|
ask: true,
|
|
@@ -6942,13 +7515,19 @@ var ClaudePolicy = class {
|
|
|
6942
7515
|
return { ask: false };
|
|
6943
7516
|
}
|
|
6944
7517
|
if (!risk.risky) return { ask: false };
|
|
6945
|
-
if (this.autonomy === "full" && risk.action !== "outside_worktree") return { ask: false };
|
|
7518
|
+
if (this.autonomy === "full" && risk.action !== "outside_worktree" && !risk.codeden) return { ask: false };
|
|
6946
7519
|
return {
|
|
6947
7520
|
ask: true,
|
|
6948
7521
|
action: risk.action,
|
|
6949
7522
|
reason: risk.reason
|
|
6950
7523
|
};
|
|
6951
7524
|
}
|
|
7525
|
+
assess(toolName, input, dir) {
|
|
7526
|
+
return assessClaudeTool(toolName, input, dir ? {
|
|
7527
|
+
...this.ctx,
|
|
7528
|
+
dir
|
|
7529
|
+
} : this.ctx);
|
|
7530
|
+
}
|
|
6952
7531
|
/**
|
|
6953
7532
|
* Whether a `canUseTool` call (Claude's own permission prompt) needs the user. In supervised
|
|
6954
7533
|
* mode everything Claude asks about goes to the user; otherwise only gated calls do.
|
|
@@ -6963,9 +7542,17 @@ var ClaudePolicy = class {
|
|
|
6963
7542
|
reason: "Claude asked for permission"
|
|
6964
7543
|
};
|
|
6965
7544
|
}
|
|
6966
|
-
/**
|
|
6967
|
-
|
|
7545
|
+
/**
|
|
7546
|
+
* Remember an `allow_session` decision. Commands and edits match exactly; others by name. A
|
|
7547
|
+
* call that touches CodeDen's own credentials is remembered only as itself, in `dir`.
|
|
7548
|
+
*/
|
|
7549
|
+
allowForSession(toolName, input, dir) {
|
|
6968
7550
|
this.sessionAllow.add(this.key(toolName, input));
|
|
7551
|
+
if (touchesCodeden(this.assess(toolName, input, dir))) this.credentialAllow.add(this.credentialKey(toolName, input, dir));
|
|
7552
|
+
}
|
|
7553
|
+
/** The exact call, in the directory its relative paths resolve from. */
|
|
7554
|
+
credentialKey(toolName, input, dir) {
|
|
7555
|
+
return `${toolName}\n${dir ?? this.ctx.cwd}\n${JSON.stringify(input)}`;
|
|
6969
7556
|
}
|
|
6970
7557
|
key(toolName, input) {
|
|
6971
7558
|
if (COMMAND_TOOLS.has(toolName)) return `${toolName}:${str(input, "command") ?? ""}`;
|
|
@@ -7724,7 +8311,7 @@ function policyHook(policy, shell) {
|
|
|
7724
8311
|
const toolInput = hookInput.tool_input ?? {};
|
|
7725
8312
|
const cwd = str(hookInput, "cwd");
|
|
7726
8313
|
if (cwd && (path.isAbsolute(cwd) || path.win32.isAbsolute(cwd))) shell.dir = cwd;
|
|
7727
|
-
const denied = policy.denial(name, toolInput);
|
|
8314
|
+
const denied = policy.denial(name, toolInput, shell.dir);
|
|
7728
8315
|
if (denied) return { hookSpecificOutput: {
|
|
7729
8316
|
hookEventName: "PreToolUse",
|
|
7730
8317
|
permissionDecision: "deny",
|
|
@@ -8053,23 +8640,130 @@ function statsFromUsageTrailer(text) {
|
|
|
8053
8640
|
};
|
|
8054
8641
|
return stats(field("total_tokens"), field("tool_uses"), field("duration_ms"));
|
|
8055
8642
|
}
|
|
8643
|
+
// ../../packages/providers/src/claude/subagent-usage.ts
|
|
8644
|
+
/** Token counts of one call from a Messages API `usage` object; undefined when it has none. */
|
|
8645
|
+
function callTokens(usage) {
|
|
8646
|
+
if (!usage || typeof usage !== "object") return void 0;
|
|
8647
|
+
const u = usage;
|
|
8648
|
+
const details = u.output_tokens_details;
|
|
8649
|
+
const tokens = {
|
|
8650
|
+
input: count(u.input_tokens) ?? 0,
|
|
8651
|
+
output: count(u.output_tokens) ?? 0,
|
|
8652
|
+
cacheRead: count(u.cache_read_input_tokens) ?? 0,
|
|
8653
|
+
cacheWrite: count(u.cache_creation_input_tokens) ?? 0,
|
|
8654
|
+
reasoning: count(details?.thinking_tokens) ?? 0
|
|
8655
|
+
};
|
|
8656
|
+
return tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite > 0 ? tokens : void 0;
|
|
8657
|
+
}
|
|
8658
|
+
/**
|
|
8659
|
+
* Tokens per model across one subagent's own API calls, for its card's cost (§7.3). Claude
|
|
8660
|
+
* streams a call as several `assistant` frames sharing its message id (one per content block),
|
|
8661
|
+
* each with the call's usage as it stood when the call started: input and cache counts are
|
|
8662
|
+
* final, output is not. So calls are keyed by message id, frames of one call merge (the larger
|
|
8663
|
+
* figure wins), and the subagent's transcript, read once it has finished, supplies the final
|
|
8664
|
+
* figures (`applyTranscript`).
|
|
8665
|
+
*/
|
|
8666
|
+
var SubagentUsage = class {
|
|
8667
|
+
calls = /* @__PURE__ */ new Map();
|
|
8668
|
+
/** Fold in one streamed frame (`message` of an `assistant` message). */
|
|
8669
|
+
addFrame(message) {
|
|
8670
|
+
const id = str(message, "id");
|
|
8671
|
+
const tokens = callTokens(message.usage);
|
|
8672
|
+
if (!id || !tokens) return;
|
|
8673
|
+
const model = modelOf(message);
|
|
8674
|
+
const prev = this.calls.get(id);
|
|
8675
|
+
this.calls.set(id, {
|
|
8676
|
+
...model || prev?.model ? { model: model ?? prev?.model } : {},
|
|
8677
|
+
input: Math.max(tokens.input, prev?.input ?? 0),
|
|
8678
|
+
output: Math.max(tokens.output, prev?.output ?? 0),
|
|
8679
|
+
cacheRead: Math.max(tokens.cacheRead, prev?.cacheRead ?? 0),
|
|
8680
|
+
cacheWrite: Math.max(tokens.cacheWrite, prev?.cacheWrite ?? 0),
|
|
8681
|
+
reasoning: Math.max(tokens.reasoning, prev?.reasoning ?? 0)
|
|
8682
|
+
});
|
|
8683
|
+
}
|
|
8684
|
+
/**
|
|
8685
|
+
* Take the final figures of the calls the frames showed from the subagent's transcript
|
|
8686
|
+
* (`getSubagentMessages`): per message id, the transcript wins. Calls only the transcript
|
|
8687
|
+
* holds are left out (an earlier run of a resumed subagent); calls it doesn't hold yet keep
|
|
8688
|
+
* their streamed figures. Returns whether anything changed.
|
|
8689
|
+
*/
|
|
8690
|
+
applyTranscript(messages) {
|
|
8691
|
+
let changed = false;
|
|
8692
|
+
for (const entry of messages) {
|
|
8693
|
+
const raw = str(entry, "type") === "assistant" ? entry.message : void 0;
|
|
8694
|
+
if (!raw || typeof raw !== "object") continue;
|
|
8695
|
+
const message = raw;
|
|
8696
|
+
const id = str(message, "id");
|
|
8697
|
+
const prev = id ? this.calls.get(id) : void 0;
|
|
8698
|
+
const tokens = callTokens(message.usage);
|
|
8699
|
+
if (!id || !prev || !tokens) continue;
|
|
8700
|
+
const model = modelOf(message) ?? prev.model;
|
|
8701
|
+
const next = {
|
|
8702
|
+
...model ? { model } : {},
|
|
8703
|
+
...tokens
|
|
8704
|
+
};
|
|
8705
|
+
if (!sameCall(prev, next)) {
|
|
8706
|
+
this.calls.set(id, next);
|
|
8707
|
+
changed = true;
|
|
8708
|
+
}
|
|
8709
|
+
}
|
|
8710
|
+
return changed;
|
|
8711
|
+
}
|
|
8712
|
+
/** Tokens per model in the order the models were first used; undefined with no calls. */
|
|
8713
|
+
summary() {
|
|
8714
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
8715
|
+
for (const call of this.calls.values()) {
|
|
8716
|
+
const key = call.model ?? "";
|
|
8717
|
+
const sum = byModel.get(key);
|
|
8718
|
+
byModel.set(key, {
|
|
8719
|
+
...call.model ? { model: call.model } : {},
|
|
8720
|
+
input: (sum?.input ?? 0) + call.input,
|
|
8721
|
+
output: (sum?.output ?? 0) + call.output,
|
|
8722
|
+
cacheRead: (sum?.cacheRead ?? 0) + call.cacheRead,
|
|
8723
|
+
cacheWrite: (sum?.cacheWrite ?? 0) + call.cacheWrite,
|
|
8724
|
+
reasoning: (sum?.reasoning ?? 0) + call.reasoning
|
|
8725
|
+
});
|
|
8726
|
+
}
|
|
8727
|
+
if (byModel.size === 0) return void 0;
|
|
8728
|
+
return [...byModel.values()].map((c) => ({
|
|
8729
|
+
...c.model ? { model: c.model } : {},
|
|
8730
|
+
inputTokens: c.input,
|
|
8731
|
+
outputTokens: c.output,
|
|
8732
|
+
cacheReadTokens: c.cacheRead,
|
|
8733
|
+
cacheWriteTokens: c.cacheWrite,
|
|
8734
|
+
...c.reasoning > 0 ? { reasoningTokens: c.reasoning } : {}
|
|
8735
|
+
}));
|
|
8736
|
+
}
|
|
8737
|
+
};
|
|
8738
|
+
/** The model that served a call; Claude Code marks its own placeholder replies `<synthetic>`. */
|
|
8739
|
+
function modelOf(message) {
|
|
8740
|
+
const model = str(message, "model")?.trim();
|
|
8741
|
+
return model && model !== "<synthetic>" ? model : void 0;
|
|
8742
|
+
}
|
|
8743
|
+
function sameCall(a, b) {
|
|
8744
|
+
return a.model === b.model && a.input === b.input && a.output === b.output && a.cacheRead === b.cacheRead && a.cacheWrite === b.cacheWrite && a.reasoning === b.reasoning;
|
|
8745
|
+
}
|
|
8056
8746
|
// ../../packages/providers/src/claude/subagents.ts
|
|
8057
8747
|
/**
|
|
8058
8748
|
* Subagent (Agent/Task tool) runs as child threads (§7.3). The parent gets a `subagent` item;
|
|
8059
8749
|
* messages tagged with `parent_tool_use_id` go to the child thread. Background subagents
|
|
8060
|
-
* finish on `task_notification`, foreground ones on their `tool_result`.
|
|
8750
|
+
* finish on `task_notification`, foreground ones on their `tool_result`. The card's figures
|
|
8751
|
+
* include the run's tokens per model, from its streamed messages; `onFinished` lets the
|
|
8752
|
+
* session read the final figures from the run's transcript (`correctUsage`).
|
|
8061
8753
|
*/
|
|
8062
8754
|
var SubagentTracker = class {
|
|
8063
8755
|
sessionId;
|
|
8064
8756
|
emit;
|
|
8065
8757
|
newId;
|
|
8066
8758
|
onThreadEnd;
|
|
8759
|
+
onFinished;
|
|
8067
8760
|
runs = /* @__PURE__ */ new Map();
|
|
8068
|
-
constructor(sessionId, emit, newId, onThreadEnd = () => {}) {
|
|
8761
|
+
constructor(sessionId, emit, newId, onThreadEnd = () => {}, onFinished = () => {}) {
|
|
8069
8762
|
this.sessionId = sessionId;
|
|
8070
8763
|
this.emit = emit;
|
|
8071
8764
|
this.newId = newId;
|
|
8072
8765
|
this.onThreadEnd = onThreadEnd;
|
|
8766
|
+
this.onFinished = onFinished;
|
|
8073
8767
|
}
|
|
8074
8768
|
ctx(toolUseId) {
|
|
8075
8769
|
return this.runs.get(toolUseId)?.child;
|
|
@@ -8097,7 +8791,8 @@ var SubagentTracker = class {
|
|
|
8097
8791
|
child,
|
|
8098
8792
|
item,
|
|
8099
8793
|
background: input.run_in_background === true,
|
|
8100
|
-
done: false
|
|
8794
|
+
done: false,
|
|
8795
|
+
usage: new SubagentUsage()
|
|
8101
8796
|
});
|
|
8102
8797
|
this.emit({
|
|
8103
8798
|
type: "thread.started",
|
|
@@ -8121,11 +8816,43 @@ var SubagentTracker = class {
|
|
|
8121
8816
|
}
|
|
8122
8817
|
onTaskStarted(msg) {
|
|
8123
8818
|
const run = this.runs.get(str(msg, "tool_use_id") ?? "");
|
|
8124
|
-
if (run
|
|
8819
|
+
if (!run) return;
|
|
8820
|
+
if (msg.is_backgrounded === true) run.background = true;
|
|
8821
|
+
run.agentId ??= str(msg, "task_id");
|
|
8822
|
+
}
|
|
8823
|
+
/** One of the run's own `assistant` messages (`parent_tool_use_id` is its tool use). */
|
|
8824
|
+
onAssistant(toolUseId, msg) {
|
|
8825
|
+
const run = this.runs.get(toolUseId);
|
|
8826
|
+
if (!run || run.done) return;
|
|
8827
|
+
run.agentId ??= str(msg, "agent_id");
|
|
8828
|
+
if (msg.message && typeof msg.message === "object") run.usage.addFrame(msg.message);
|
|
8829
|
+
}
|
|
8830
|
+
/**
|
|
8831
|
+
* The final figures of a finished run's calls, from its transcript: the card is sent again
|
|
8832
|
+
* when they change what it shows (§7.3).
|
|
8833
|
+
*/
|
|
8834
|
+
correctUsage(toolUseId, transcript) {
|
|
8835
|
+
const run = this.runs.get(toolUseId);
|
|
8836
|
+
if (!run?.done || !run.usage.applyTranscript(transcript)) return;
|
|
8837
|
+
const usage = run.usage.summary();
|
|
8838
|
+
if (!usage) return;
|
|
8839
|
+
run.item = {
|
|
8840
|
+
...run.item,
|
|
8841
|
+
stats: {
|
|
8842
|
+
...run.item.stats,
|
|
8843
|
+
usage
|
|
8844
|
+
}
|
|
8845
|
+
};
|
|
8846
|
+
this.emit({
|
|
8847
|
+
type: "item.completed",
|
|
8848
|
+
...run.parent,
|
|
8849
|
+
item: run.item
|
|
8850
|
+
});
|
|
8125
8851
|
}
|
|
8126
8852
|
onToolResult(toolUseId, content, isError, toolUseResult) {
|
|
8127
8853
|
const run = this.runs.get(toolUseId);
|
|
8128
8854
|
if (!run || run.done) return;
|
|
8855
|
+
run.agentId ??= str(toolUseResult, "agentId");
|
|
8129
8856
|
const status = str(toolUseResult, "status");
|
|
8130
8857
|
if (!isError && (status === "async_launched" || toolUseResult?.isAsync === true)) {
|
|
8131
8858
|
run.background = true;
|
|
@@ -8138,6 +8865,8 @@ var SubagentTracker = class {
|
|
|
8138
8865
|
onTaskNotification(msg) {
|
|
8139
8866
|
const toolUseId = str(msg, "tool_use_id");
|
|
8140
8867
|
if (!toolUseId) return;
|
|
8868
|
+
const run = this.runs.get(toolUseId);
|
|
8869
|
+
if (run) run.agentId ??= str(msg, "task_id");
|
|
8141
8870
|
const status = str(msg, "status");
|
|
8142
8871
|
const summary = str(msg, "summary");
|
|
8143
8872
|
this.finish(toolUseId, status === "completed" ? "completed" : status === "failed" ? "failed" : "interrupted", summary, statsFromTaskNotification(msg));
|
|
@@ -8170,16 +8899,23 @@ var SubagentTracker = class {
|
|
|
8170
8899
|
reason: "user"
|
|
8171
8900
|
});
|
|
8172
8901
|
const status = outcome === "completed" ? "completed" : outcome === "failed" ? "failed" : "cancelled";
|
|
8902
|
+
const usage = run.usage.summary();
|
|
8903
|
+
const figures = usage ? {
|
|
8904
|
+
...stats,
|
|
8905
|
+
usage
|
|
8906
|
+
} : stats;
|
|
8907
|
+
run.item = {
|
|
8908
|
+
...run.item,
|
|
8909
|
+
status,
|
|
8910
|
+
...summary ? { summary } : {},
|
|
8911
|
+
...figures ? { stats: figures } : {}
|
|
8912
|
+
};
|
|
8173
8913
|
this.emit({
|
|
8174
8914
|
type: "item.completed",
|
|
8175
8915
|
...run.parent,
|
|
8176
|
-
item:
|
|
8177
|
-
...run.item,
|
|
8178
|
-
status,
|
|
8179
|
-
...summary ? { summary } : {},
|
|
8180
|
-
...stats ? { stats } : {}
|
|
8181
|
-
}
|
|
8916
|
+
item: run.item
|
|
8182
8917
|
});
|
|
8918
|
+
if (usage && run.agentId) this.onFinished(toolUseId, run.agentId);
|
|
8183
8919
|
}
|
|
8184
8920
|
};
|
|
8185
8921
|
/** Drop the CLI's internal trailer (agent ids, usage) from a subagent result. */
|
|
@@ -8475,7 +9211,7 @@ var ClaudeEventMapper = class {
|
|
|
8475
9211
|
this.rootThreadId = `${opts.sessionId}:thread-root`;
|
|
8476
9212
|
this.emit = opts.emit;
|
|
8477
9213
|
this.newId = opts.newId ?? randomUUID;
|
|
8478
|
-
this.subagents = new SubagentTracker(opts.sessionId, this.emit, this.newId, (ctx, ok) => this.tools.finishTurn(ctx, ok));
|
|
9214
|
+
this.subagents = new SubagentTracker(opts.sessionId, this.emit, this.newId, (ctx, ok) => this.tools.finishTurn(ctx, ok), opts.onSubagentFinished);
|
|
8479
9215
|
this.tools = new ToolTracker(opts.cwd, (ctx, phase, item) => this.item(ctx, phase, item), this.subagents, this.newId);
|
|
8480
9216
|
this.stream = new StreamAssembler({
|
|
8481
9217
|
rootCtx: () => this.rootCtx(),
|
|
@@ -8553,6 +9289,13 @@ var ClaudeEventMapper = class {
|
|
|
8553
9289
|
ctxForTool(toolUseId) {
|
|
8554
9290
|
return this.tools.ctx(toolUseId) ?? this.rootCtx();
|
|
8555
9291
|
}
|
|
9292
|
+
/**
|
|
9293
|
+
* A finished subagent's transcript (`getSubagentMessages`): its calls' final token counts
|
|
9294
|
+
* replace the streamed ones, and the card is sent again when that changes it.
|
|
9295
|
+
*/
|
|
9296
|
+
correctSubagentUsage(toolUseId, transcript) {
|
|
9297
|
+
this.subagents.correctUsage(toolUseId, transcript);
|
|
9298
|
+
}
|
|
8556
9299
|
/** Emit an item event in a context. */
|
|
8557
9300
|
item(ctx, phase, item) {
|
|
8558
9301
|
this.emit({
|
|
@@ -8614,6 +9357,7 @@ var ClaudeEventMapper = class {
|
|
|
8614
9357
|
const isRoot = !msg.parent_tool_use_id;
|
|
8615
9358
|
const messageId = str(message, "id") ?? this.newId();
|
|
8616
9359
|
if (isRoot) this.contextTokens = contextTokensFromMessageUsage(message.usage) ?? this.contextTokens;
|
|
9360
|
+
else if (typeof msg.parent_tool_use_id === "string") this.subagents.onAssistant(msg.parent_tool_use_id, msg);
|
|
8617
9361
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
8618
9362
|
for (const block of content) if (block.type === "text" || block.type === "thinking" || block.type === "redacted_thinking") {
|
|
8619
9363
|
const type = block.type === "text" ? "text" : "thinking";
|
|
@@ -9020,7 +9764,10 @@ var ClaudeSession = class {
|
|
|
9020
9764
|
this.mapper = new ClaudeEventMapper({
|
|
9021
9765
|
sessionId: opts.sessionId,
|
|
9022
9766
|
cwd: opts.cwd,
|
|
9023
|
-
emit
|
|
9767
|
+
emit,
|
|
9768
|
+
onSubagentFinished: (toolUseId, agentId) => {
|
|
9769
|
+
this.correctSubagentUsage(toolUseId, agentId);
|
|
9770
|
+
}
|
|
9024
9771
|
});
|
|
9025
9772
|
this.broker = new ApprovalBroker(emit);
|
|
9026
9773
|
this.autonomy = opts.autonomy;
|
|
@@ -9028,6 +9775,7 @@ var ClaudeSession = class {
|
|
|
9028
9775
|
cwd: opts.cwd,
|
|
9029
9776
|
extraRoots: opts.additionalDirectories,
|
|
9030
9777
|
...opts.ownBranch ? { ownBranch: opts.ownBranch } : {},
|
|
9778
|
+
...opts.codedenHome ? { codedenHome: opts.codedenHome } : {},
|
|
9031
9779
|
notesRoots: config.notesRoots ?? claudeNotesDirs({
|
|
9032
9780
|
cwd: opts.cwd,
|
|
9033
9781
|
...claudeHomes(opts.env)
|
|
@@ -9042,6 +9790,20 @@ var ClaudeSession = class {
|
|
|
9042
9790
|
return this.mapper.resumeToken ?? this.opts.resumeToken;
|
|
9043
9791
|
}
|
|
9044
9792
|
/**
|
|
9793
|
+
* A subagent's streamed messages don't carry their calls' final output counts; its
|
|
9794
|
+
* transcript does. Read it once the run finished and let the card take the final figures.
|
|
9795
|
+
* If it can't be read (another config folder, not written yet), the streamed ones stay.
|
|
9796
|
+
*/
|
|
9797
|
+
async correctSubagentUsage(toolUseId, agentId) {
|
|
9798
|
+
const read = this.config.getSubagentMessages;
|
|
9799
|
+
const claudeSessionId = this.resumeToken();
|
|
9800
|
+
if (!read || !claudeSessionId || this.closed) return;
|
|
9801
|
+
try {
|
|
9802
|
+
const transcript = await read(claudeSessionId, agentId, { dir: this.opts.cwd });
|
|
9803
|
+
if (!this.closed) this.mapper.correctSubagentUsage(toolUseId, transcript);
|
|
9804
|
+
} catch {}
|
|
9805
|
+
}
|
|
9806
|
+
/**
|
|
9045
9807
|
* Switch autonomy. The policy changes at once, so the next tool call (even in a running
|
|
9046
9808
|
* turn) follows the new level; approvals already waiting stay waiting. The CLI's own
|
|
9047
9809
|
* permission mode follows (`applyPermissionMode`), except while planning: the CLI stays in
|
|
@@ -9264,7 +10026,7 @@ var ClaudeSession = class {
|
|
|
9264
10026
|
mapper: this.mapper
|
|
9265
10027
|
}, input, options.toolUseID, options.signal);
|
|
9266
10028
|
if (toolName === "ExitPlanMode") return this.reviewPlan(input, options);
|
|
9267
|
-
const denied = this.policy.denial(toolName, input);
|
|
10029
|
+
const denied = this.policy.denial(toolName, input, this.shell.dir);
|
|
9268
10030
|
if (denied) return {
|
|
9269
10031
|
behavior: "deny",
|
|
9270
10032
|
message: denied
|
|
@@ -9302,7 +10064,7 @@ var ClaudeSession = class {
|
|
|
9302
10064
|
};
|
|
9303
10065
|
toPermissionResult(toolName, input, response) {
|
|
9304
10066
|
if (response.kind === "approval" && response.decision !== "deny") {
|
|
9305
|
-
if (response.decision === "allow_session") this.policy.allowForSession(toolName, input);
|
|
10067
|
+
if (response.decision === "allow_session") this.policy.allowForSession(toolName, input, this.shell.dir);
|
|
9306
10068
|
return {
|
|
9307
10069
|
behavior: "allow",
|
|
9308
10070
|
updatedInput: input
|
|
@@ -9446,10 +10208,11 @@ var ClaudeAdapter = class {
|
|
|
9446
10208
|
async openSession(opts) {
|
|
9447
10209
|
const executable = this.executable();
|
|
9448
10210
|
if (!executable) throw new Error("Claude Code is not installed (no `claude` on PATH)");
|
|
9449
|
-
const { query } = await (this.options.loadSdk ?? loadClaudeSdk)();
|
|
10211
|
+
const { query, getSubagentMessages } = await (this.options.loadSdk ?? loadClaudeSdk)();
|
|
9450
10212
|
return new ClaudeSession(await withFork(this, opts), {
|
|
9451
10213
|
executable,
|
|
9452
10214
|
query,
|
|
10215
|
+
...getSubagentMessages ? { getSubagentMessages } : {},
|
|
9453
10216
|
onModels: (models) => {
|
|
9454
10217
|
if (!models.length) return;
|
|
9455
10218
|
this.freshness.confirm(executableFingerprint(executable));
|
|
@@ -9986,22 +10749,23 @@ var CodexUtilityModel = class {
|
|
|
9986
10749
|
* same risk classifier as Claude's tool calls: what the level allows is approved by policy
|
|
9987
10750
|
* (recorded with `decidedBy: "policy"`), and only the rest reaches the user.
|
|
9988
10751
|
*/
|
|
9989
|
-
/**
|
|
10752
|
+
/**
|
|
10753
|
+
* True when the permission level allows an action the classifier assessed as `risk`. CodeDen's
|
|
10754
|
+
* own credential files are never allowed by policy. Commands Codex runs inside its sandbox
|
|
10755
|
+
* without asking (reads included) never reach this policy.
|
|
10756
|
+
*/
|
|
9990
10757
|
function levelAllows(autonomy, risk) {
|
|
9991
10758
|
if (autonomy === "auto") return !risk.risky;
|
|
9992
|
-
if (autonomy === "full") return !risk.risky || risk.action !== "outside_worktree";
|
|
10759
|
+
if (autonomy === "full") return !risk.risky || risk.action !== "outside_worktree" && !risk.codeden;
|
|
9993
10760
|
return false;
|
|
9994
10761
|
}
|
|
9995
|
-
/** The first risky assessment of `items`, or safe. */
|
|
9996
|
-
function first(items) {
|
|
9997
|
-
return items.find((r) => r.risky) ?? { risky: false };
|
|
9998
|
-
}
|
|
9999
10762
|
/**
|
|
10000
10763
|
* Risk of a command Codex asks to run: the command line as sent and each parsed action, so a
|
|
10001
|
-
* redirect or path the parsed form drops still counts.
|
|
10764
|
+
* redirect or path the parsed form drops still counts. Here and below, CodeDen's own
|
|
10765
|
+
* credentials win over an earlier ordinary risk (`mostSevere`).
|
|
10002
10766
|
*/
|
|
10003
10767
|
function commandRisk(texts, ctx) {
|
|
10004
|
-
return
|
|
10768
|
+
return mostSevere(texts.filter(Boolean).map((text) => classifyCommand(text, ctx)));
|
|
10005
10769
|
}
|
|
10006
10770
|
/**
|
|
10007
10771
|
* Risk of a patch Codex asks to apply: each file it changes, and the root it asks to write
|
|
@@ -10014,7 +10778,7 @@ function patchRisk(paths, grantRoot, ctx) {
|
|
|
10014
10778
|
action: "outside_worktree",
|
|
10015
10779
|
reason: "Changes files Codex did not name"
|
|
10016
10780
|
};
|
|
10017
|
-
return
|
|
10781
|
+
return mostSevere(targets.map((target) => classifyWrite(target, ctx)));
|
|
10018
10782
|
}
|
|
10019
10783
|
/**
|
|
10020
10784
|
* Risk of extra permissions Codex asks for: network access (not restricted, as for Claude,
|
|
@@ -10029,7 +10793,7 @@ function permissionsRisk(permissions, ctx) {
|
|
|
10029
10793
|
action: "outside_worktree",
|
|
10030
10794
|
reason: "Asks for access to folders by pattern"
|
|
10031
10795
|
};
|
|
10032
|
-
return
|
|
10796
|
+
return mostSevere([
|
|
10033
10797
|
...(fs.write ?? []).map((p) => classifyWrite(p, ctx)),
|
|
10034
10798
|
...(fs.read ?? []).map((p) => classifyRead(p, ctx)),
|
|
10035
10799
|
...(fs.entries ?? []).map((entry) => {
|
|
@@ -11256,6 +12020,21 @@ function tokenUsage(u) {
|
|
|
11256
12020
|
if (u.modelContextWindow && u.modelContextWindow > 0) usage.contextWindow = Math.round(u.modelContextWindow);
|
|
11257
12021
|
return usage;
|
|
11258
12022
|
}
|
|
12023
|
+
/**
|
|
12024
|
+
* A sub-agent's tokens for its card (§7.3), from its thread's running `total`, in the same
|
|
12025
|
+
* terms as `tokenUsage`. `model` is the one its spawn asked for; absent, it runs the session's.
|
|
12026
|
+
*/
|
|
12027
|
+
function subagentUsage(u, model) {
|
|
12028
|
+
const t = tokenUsage(u);
|
|
12029
|
+
return {
|
|
12030
|
+
...model ? { model } : {},
|
|
12031
|
+
inputTokens: t.inputTokens ?? 0,
|
|
12032
|
+
outputTokens: t.outputTokens ?? 0,
|
|
12033
|
+
cacheReadTokens: t.cachedInputTokens ?? 0,
|
|
12034
|
+
cacheWriteTokens: t.cacheWriteTokens ?? 0,
|
|
12035
|
+
...t.reasoningTokens ? { reasoningTokens: t.reasoningTokens } : {}
|
|
12036
|
+
};
|
|
12037
|
+
}
|
|
11259
12038
|
function windowId(minutes, fallback) {
|
|
11260
12039
|
if (minutes === 300) return "5h";
|
|
11261
12040
|
if (minutes === 10080) return "weekly";
|
|
@@ -11384,7 +12163,7 @@ var CodexThreadMapper = class {
|
|
|
11384
12163
|
this.updatePlan(n.params.turnId, n.params.plan);
|
|
11385
12164
|
return;
|
|
11386
12165
|
case "thread/tokenUsage/updated": {
|
|
11387
|
-
this.o.onTokens?.(n.params.tokenUsage
|
|
12166
|
+
this.o.onTokens?.(n.params.tokenUsage);
|
|
11388
12167
|
const usage = this.turnUsage.add(n.params.turnId, n.params.tokenUsage);
|
|
11389
12168
|
if (!this.o.root) {
|
|
11390
12169
|
delete usage.contextTokens;
|
|
@@ -11637,6 +12416,7 @@ var CodexSubagents = class {
|
|
|
11637
12416
|
return false;
|
|
11638
12417
|
}
|
|
11639
12418
|
const run = this.claim(from, turnId, item.id, spawnDetails(item));
|
|
12419
|
+
if (item.model) run.model ??= item.model;
|
|
11640
12420
|
const child = item.receiverThreadIds[0];
|
|
11641
12421
|
if (child) this.bind(run, child);
|
|
11642
12422
|
if (phase !== "completed") return true;
|
|
@@ -11775,8 +12555,8 @@ var CodexSubagents = class {
|
|
|
11775
12555
|
prompt: item.input.text
|
|
11776
12556
|
};
|
|
11777
12557
|
},
|
|
11778
|
-
onTokens: (
|
|
11779
|
-
run.
|
|
12558
|
+
onTokens: (usage) => {
|
|
12559
|
+
run.tokens = usage;
|
|
11780
12560
|
}
|
|
11781
12561
|
});
|
|
11782
12562
|
run.thread = thread;
|
|
@@ -11877,9 +12657,10 @@ var CodexSubagents = class {
|
|
|
11877
12657
|
stats(run) {
|
|
11878
12658
|
const end = run.endedAt ?? this.o.now();
|
|
11879
12659
|
return {
|
|
11880
|
-
...run.
|
|
12660
|
+
...run.tokens ? { totalTokens: Math.max(0, Math.round(run.tokens.total.totalTokens)) } : {},
|
|
11881
12661
|
toolUses: run.toolCalls.size,
|
|
11882
|
-
durationMs: Math.max(0, Math.round(end - run.startedAt))
|
|
12662
|
+
durationMs: Math.max(0, Math.round(end - run.startedAt)),
|
|
12663
|
+
...run.tokens ? { usage: [subagentUsage(run.tokens, run.model)] } : {}
|
|
11883
12664
|
};
|
|
11884
12665
|
}
|
|
11885
12666
|
};
|
|
@@ -12226,7 +13007,8 @@ var CodexSession = class {
|
|
|
12226
13007
|
risk: {
|
|
12227
13008
|
cwd: this.opts.cwd,
|
|
12228
13009
|
extraRoots: this.opts.additionalDirectories,
|
|
12229
|
-
...this.opts.ownBranch ? { ownBranch: this.opts.ownBranch } : {}
|
|
13010
|
+
...this.opts.ownBranch ? { ownBranch: this.opts.ownBranch } : {},
|
|
13011
|
+
...this.opts.codedenHome ? { codedenHome: this.opts.codedenHome } : {}
|
|
12230
13012
|
},
|
|
12231
13013
|
autonomy: () => this.opts.readOnly ? "read_only" : this.autonomy,
|
|
12232
13014
|
ctx: (threadId, turnId) => this.mapper.requestCtx(threadId, turnId),
|
|
@@ -14927,25 +15709,32 @@ var OpenCodePolicy = class {
|
|
|
14927
15709
|
};
|
|
14928
15710
|
}
|
|
14929
15711
|
}
|
|
14930
|
-
/** Remember an `allow_session` decision: commands and paths match exactly. */
|
|
15712
|
+
/** Remember an `allow_session` decision: commands (in their directory) and paths match exactly. */
|
|
14931
15713
|
allowForSession(call) {
|
|
14932
15714
|
this.sessionAllow.add(this.key(call));
|
|
14933
15715
|
}
|
|
14934
15716
|
shell(call) {
|
|
14935
15717
|
const command = commandOf(call);
|
|
15718
|
+
const risk = classifyCommand(command, {
|
|
15719
|
+
...this.ctx,
|
|
15720
|
+
dir: this.workdir(call)
|
|
15721
|
+
});
|
|
14936
15722
|
if (this.isReadOnly) {
|
|
15723
|
+
if (touchesCodeden(risk)) return this.credentialsDenial();
|
|
14937
15724
|
const reason = readOnlyCommandDenial(command, { allowChecks: !this.readOnly });
|
|
14938
15725
|
return reason ? {
|
|
14939
15726
|
kind: "deny",
|
|
14940
15727
|
message: `This session is read-only: ${reason}.`
|
|
14941
15728
|
} : ALLOW;
|
|
14942
15729
|
}
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
15730
|
+
return this.gate(risk, "command");
|
|
15731
|
+
}
|
|
15732
|
+
/** Read-only sessions never touch CodeDen's own credential files. */
|
|
15733
|
+
credentialsDenial() {
|
|
15734
|
+
return {
|
|
15735
|
+
kind: "deny",
|
|
15736
|
+
message: this.readOnly ? "This session is read-only: CodeDen's own credential files are off limits. Act through CodeDen tools instead." : "This session has read-only permissions: CodeDen's own credential files are off limits. Report what you found instead."
|
|
15737
|
+
};
|
|
14949
15738
|
}
|
|
14950
15739
|
edit(paths) {
|
|
14951
15740
|
if (this.isReadOnly) return {
|
|
@@ -14954,19 +15743,19 @@ var OpenCodePolicy = class {
|
|
|
14954
15743
|
};
|
|
14955
15744
|
return this.gate(this.firstRisk(paths, classifyWrite), "file_change");
|
|
14956
15745
|
}
|
|
15746
|
+
/** The verdict for every target: CodeDen's own credentials first, then the first risky one. */
|
|
14957
15747
|
firstRisk(targets, classify) {
|
|
14958
|
-
|
|
14959
|
-
const risk = classify(target, this.ctx);
|
|
14960
|
-
if (risk.risky) return risk;
|
|
14961
|
-
}
|
|
14962
|
-
return { risky: false };
|
|
15748
|
+
return mostSevere(targets.map((target) => classify(target, this.ctx)));
|
|
14963
15749
|
}
|
|
14964
15750
|
/**
|
|
14965
15751
|
* The level's gate (the same as Claude's): edits and commands (`supervisedAction`) always
|
|
14966
15752
|
* ask in supervised mode; `alwaysRisky` asks for flagged calls even at full autonomy
|
|
14967
|
-
* (secrets reads).
|
|
15753
|
+
* (secrets reads). CodeDen's own credential files ask at every level and are refused for
|
|
15754
|
+
* read-only sessions.
|
|
14968
15755
|
*/
|
|
14969
15756
|
gate(risk, supervisedAction, alwaysRisky = false) {
|
|
15757
|
+
const codeden = touchesCodeden(risk);
|
|
15758
|
+
if (codeden && this.isReadOnly) return this.credentialsDenial();
|
|
14970
15759
|
if (this.autonomy === "supervised") {
|
|
14971
15760
|
if (risk.risky) return {
|
|
14972
15761
|
kind: "ask",
|
|
@@ -14981,18 +15770,28 @@ var OpenCodePolicy = class {
|
|
|
14981
15770
|
return ALLOW;
|
|
14982
15771
|
}
|
|
14983
15772
|
if (!risk.risky) return ALLOW;
|
|
14984
|
-
|
|
15773
|
+
const always = alwaysRisky || codeden;
|
|
15774
|
+
if (this.autonomy === "full" && risk.action !== "outside_worktree" && !always) return ALLOW;
|
|
14985
15775
|
return {
|
|
14986
15776
|
kind: "ask",
|
|
14987
15777
|
action: risk.action,
|
|
14988
15778
|
reason: risk.reason
|
|
14989
15779
|
};
|
|
14990
15780
|
}
|
|
15781
|
+
/**
|
|
15782
|
+
* What an approval for the session covers: a command in its working directory (the same
|
|
15783
|
+
* relative path elsewhere is another file), or the exact paths.
|
|
15784
|
+
*/
|
|
14991
15785
|
key(call) {
|
|
14992
15786
|
const { action, resources } = call.request;
|
|
14993
|
-
if (action === "shell") return `shell:${commandOf(call)}`;
|
|
15787
|
+
if (action === "shell") return `shell:${this.workdir(call)}\n${commandOf(call)}`;
|
|
14994
15788
|
return `${action}:${[...resources].sort().join("\n")}`;
|
|
14995
15789
|
}
|
|
15790
|
+
/** Where a shell call runs: its `workdir`, else the worktree. */
|
|
15791
|
+
workdir(call) {
|
|
15792
|
+
const workdir = str(call.toolInput, "workdir");
|
|
15793
|
+
return workdir ? resolveAgainst(workdir, this.ctx) : this.ctx.cwd;
|
|
15794
|
+
}
|
|
14996
15795
|
};
|
|
14997
15796
|
/** The command an OpenCode `shell` ask is about: the tool input, else its parsed parts. */
|
|
14998
15797
|
function commandOf(call) {
|
|
@@ -16079,7 +16878,8 @@ var OpenCodeSession = class {
|
|
|
16079
16878
|
this.broker = new ApprovalBroker(emit);
|
|
16080
16879
|
this.policy = new OpenCodePolicy(opts.autonomy, {
|
|
16081
16880
|
cwd: opts.cwd,
|
|
16082
|
-
extraRoots: opts.additionalDirectories
|
|
16881
|
+
extraRoots: opts.additionalDirectories,
|
|
16882
|
+
...opts.codedenHome ? { codedenHome: opts.codedenHome } : {}
|
|
16083
16883
|
}, opts.readOnly ?? false);
|
|
16084
16884
|
this.model = explicitModel(opts.model);
|
|
16085
16885
|
this.effort = opts.effort;
|
|
@@ -22234,6 +23034,7 @@ async function executeCommand(core, command) {
|
|
|
22234
23034
|
if (added.length > 0) core.log?.(`PATH: picked up ${added.join(", ")}`);
|
|
22235
23035
|
const adapters = command.providerId ? registry.list().filter((a) => a.id === command.providerId) : registry.list();
|
|
22236
23036
|
const statuses = await Promise.all(adapters.map((a) => a.detect()));
|
|
23037
|
+
if (command.refreshModels === false) return { statuses };
|
|
22237
23038
|
const refreshed = registry.refreshModels(adapters.map((a) => a.id));
|
|
22238
23039
|
if (!command.waitForModels) return { statuses };
|
|
22239
23040
|
await refreshed;
|
|
@@ -22348,7 +23149,7 @@ async function executeCommand(core, command) {
|
|
|
22348
23149
|
case "checkpoint.restoreFiles": return restoreFiles(command.cwd, command.sessionId, command.index, command.paths);
|
|
22349
23150
|
case "ports.list": return { ports: await listPorts(command.probe) };
|
|
22350
23151
|
case "mcp.test": {
|
|
22351
|
-
const { testMcpServer } = await import("./server-test-
|
|
23152
|
+
const { testMcpServer } = await import("./server-test-BwyCFssm.js");
|
|
22352
23153
|
return testMcpServer({
|
|
22353
23154
|
spec: command.spec,
|
|
22354
23155
|
cwd: command.cwd,
|
|
@@ -23077,9 +23878,24 @@ async function click(session, ref, opts = {}) {
|
|
|
23077
23878
|
const options = opts.button ? { button: opts.button } : {};
|
|
23078
23879
|
await withRef(session.page(opts.tabId), ref, (locator) => opts.doubleClick ? locator.dblclick(options) : locator.click(options));
|
|
23079
23880
|
}
|
|
23080
|
-
/**
|
|
23881
|
+
/**
|
|
23882
|
+
* Replace the value of an editable element; `submit` presses Enter afterwards. Without a ref,
|
|
23883
|
+
* type the text as key presses into whatever has focus (a canvas editor, a game, a field
|
|
23884
|
+
* clicked at coordinates).
|
|
23885
|
+
*/
|
|
23081
23886
|
async function type(session, ref, text, opts = {}) {
|
|
23082
|
-
|
|
23887
|
+
const page = session.page(opts.tabId);
|
|
23888
|
+
if (ref === void 0) {
|
|
23889
|
+
if (text.length > 2e3) throw new CommandError("invalid_text", `Without a ref, type at most ${MAX_FOCUS_TYPE_CHARS} characters`);
|
|
23890
|
+
try {
|
|
23891
|
+
await page.keyboard.type(text);
|
|
23892
|
+
if (opts.submit) await page.keyboard.press("Enter");
|
|
23893
|
+
} catch (error) {
|
|
23894
|
+
throw new CommandError("action_failed", `Typing at the focused element failed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`);
|
|
23895
|
+
}
|
|
23896
|
+
return;
|
|
23897
|
+
}
|
|
23898
|
+
await withRef(page, ref, async (locator) => {
|
|
23083
23899
|
await locator.fill(text);
|
|
23084
23900
|
if (opts.submit) await locator.press("Enter");
|
|
23085
23901
|
});
|
|
@@ -23174,6 +23990,150 @@ async function wait(session, opts) {
|
|
|
23174
23990
|
throw new CommandError("action_failed", error instanceof Error ? error.message : String(error));
|
|
23175
23991
|
}
|
|
23176
23992
|
}
|
|
23993
|
+
// ../runner/src/browser/pointer.ts
|
|
23994
|
+
/** Drags move in about one step per this many CSS pixels, so drag thresholds see motion. */
|
|
23995
|
+
const DRAG_STEP_PX = 20;
|
|
23996
|
+
const MIN_DRAG_STEPS = 5;
|
|
23997
|
+
const MAX_DRAG_STEPS = 50;
|
|
23998
|
+
/** A scroll returns once no scroll event came for this long (smooth scrolling has ended). */
|
|
23999
|
+
const SCROLL_QUIET_MS = 100;
|
|
24000
|
+
/** ... or after this long in the page, whatever happens. */
|
|
24001
|
+
const SCROLL_SETTLE_MAX_MS = 1e3;
|
|
24002
|
+
/** The runner stops waiting for the page itself after this (a busy or hidden page). */
|
|
24003
|
+
const SCROLL_SETTLE_TIMEOUT_MS = 1500;
|
|
24004
|
+
/**
|
|
24005
|
+
* Resolves once the page has had no `scroll` event (on any scroller: captured on `window`) for
|
|
24006
|
+
* `SCROLL_QUIET_MS`, at most `SCROLL_SETTLE_MAX_MS` (a timer too: hidden tabs get no animation
|
|
24007
|
+
* frames). A string, so it runs in the page's realm.
|
|
24008
|
+
*/
|
|
24009
|
+
const SCROLL_SETTLED = `new Promise((resolve) => {
|
|
24010
|
+
const start = performance.now();
|
|
24011
|
+
let last = start;
|
|
24012
|
+
const onScroll = () => { last = performance.now(); };
|
|
24013
|
+
const done = () => {
|
|
24014
|
+
removeEventListener("scroll", onScroll, { capture: true });
|
|
24015
|
+
clearTimeout(timer);
|
|
24016
|
+
resolve(true);
|
|
24017
|
+
};
|
|
24018
|
+
const timer = setTimeout(done, ${SCROLL_SETTLE_MAX_MS});
|
|
24019
|
+
addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
24020
|
+
const tick = () => {
|
|
24021
|
+
const now = performance.now();
|
|
24022
|
+
if (now - last < ${SCROLL_QUIET_MS} && now - start < ${SCROLL_SETTLE_MAX_MS}) {
|
|
24023
|
+
requestAnimationFrame(tick);
|
|
24024
|
+
} else done();
|
|
24025
|
+
};
|
|
24026
|
+
requestAnimationFrame(tick);
|
|
24027
|
+
})`;
|
|
24028
|
+
/** Intermediate moves for a drag: about one per `DRAG_STEP_PX`, within 5 to 50. */
|
|
24029
|
+
function dragSteps(from, to) {
|
|
24030
|
+
const distance = Math.hypot(to.x - from.x, to.y - from.y);
|
|
24031
|
+
return Math.min(Math.max(Math.round(distance / DRAG_STEP_PX), MIN_DRAG_STEPS), MAX_DRAG_STEPS);
|
|
24032
|
+
}
|
|
24033
|
+
const fmt = (n) => String(Math.round(n * 100) / 100);
|
|
24034
|
+
/** Refuse a point outside the viewport: it would hit nothing, or the wrong thing. */
|
|
24035
|
+
function assertInViewport(point, viewport) {
|
|
24036
|
+
if (point.x >= 0 && point.y >= 0 && point.x < viewport.width && point.y < viewport.height) return;
|
|
24037
|
+
throw new CommandError("out_of_bounds", `Point (${fmt(point.x)}, ${fmt(point.y)}) is outside the viewport (${fmt(viewport.width)}x${fmt(viewport.height)} CSS pixels). Take a new browser_screenshot.`);
|
|
24038
|
+
}
|
|
24039
|
+
/** The points an action touches: its point, and a drag's end. */
|
|
24040
|
+
function pointsOf(command) {
|
|
24041
|
+
const points = [{
|
|
24042
|
+
x: command.x,
|
|
24043
|
+
y: command.y
|
|
24044
|
+
}];
|
|
24045
|
+
if (command.action === "drag") points.push({
|
|
24046
|
+
x: command.toX ?? NaN,
|
|
24047
|
+
y: command.toY ?? NaN
|
|
24048
|
+
});
|
|
24049
|
+
return points;
|
|
24050
|
+
}
|
|
24051
|
+
/**
|
|
24052
|
+
* Run `action` with `modifiers` held, releasing every key it tried to press, also on failure.
|
|
24053
|
+
* A key counts as held before its press resolves: Playwright marks it down before dispatching,
|
|
24054
|
+
* so a press that fails can still leave it held for the next action in the shared browser.
|
|
24055
|
+
*/
|
|
24056
|
+
async function withModifiers(page, modifiers, action) {
|
|
24057
|
+
const held = [];
|
|
24058
|
+
try {
|
|
24059
|
+
for (const key of new Set(modifiers)) {
|
|
24060
|
+
held.push(key);
|
|
24061
|
+
await page.keyboard.down(key);
|
|
24062
|
+
}
|
|
24063
|
+
await action();
|
|
24064
|
+
} finally {
|
|
24065
|
+
for (const key of held.reverse()) await page.keyboard.up(key).catch(() => void 0);
|
|
24066
|
+
}
|
|
24067
|
+
}
|
|
24068
|
+
/**
|
|
24069
|
+
* Run steps that press `button`, releasing it if any of them fails, the press included (like
|
|
24070
|
+
* keys, Playwright marks a button held before the press is dispatched). The error is kept.
|
|
24071
|
+
*/
|
|
24072
|
+
async function releasingOnFailure(page, button, steps) {
|
|
24073
|
+
try {
|
|
24074
|
+
await steps();
|
|
24075
|
+
} catch (error) {
|
|
24076
|
+
await page.mouse.up({ button }).catch(() => void 0);
|
|
24077
|
+
throw error;
|
|
24078
|
+
}
|
|
24079
|
+
}
|
|
24080
|
+
async function drag(page, from, to, button) {
|
|
24081
|
+
await page.mouse.move(from.x, from.y);
|
|
24082
|
+
await releasingOnFailure(page, button, async () => {
|
|
24083
|
+
await page.mouse.down({ button });
|
|
24084
|
+
await page.mouse.move(to.x, to.y, { steps: dragSteps(from, to) });
|
|
24085
|
+
});
|
|
24086
|
+
await page.mouse.up({ button });
|
|
24087
|
+
}
|
|
24088
|
+
/** Wait until a scroll has been rendered, bounded; a page that never answers is not an error. */
|
|
24089
|
+
async function scrollSettled(page) {
|
|
24090
|
+
let timer;
|
|
24091
|
+
const timeout = new Promise((resolve) => {
|
|
24092
|
+
timer = setTimeout(resolve, SCROLL_SETTLE_TIMEOUT_MS);
|
|
24093
|
+
});
|
|
24094
|
+
try {
|
|
24095
|
+
await Promise.race([page.evaluate(SCROLL_SETTLED).catch(() => void 0), timeout]);
|
|
24096
|
+
} finally {
|
|
24097
|
+
clearTimeout(timer);
|
|
24098
|
+
}
|
|
24099
|
+
}
|
|
24100
|
+
/** Run one pointer action on a page whose viewport is `viewport` (CSS pixels). */
|
|
24101
|
+
async function mouseAction(page, command, viewport) {
|
|
24102
|
+
const points = pointsOf(command);
|
|
24103
|
+
for (const point of points) assertInViewport(point, viewport);
|
|
24104
|
+
const [at, end] = points;
|
|
24105
|
+
try {
|
|
24106
|
+
await withModifiers(page, command.modifiers ?? [], async () => {
|
|
24107
|
+
switch (command.action) {
|
|
24108
|
+
case "click":
|
|
24109
|
+
await releasingOnFailure(page, command.button, () => page.mouse.click(at.x, at.y, {
|
|
24110
|
+
button: command.button,
|
|
24111
|
+
clickCount: command.clickCount ?? 1
|
|
24112
|
+
}));
|
|
24113
|
+
return;
|
|
24114
|
+
case "hover":
|
|
24115
|
+
await page.mouse.move(at.x, at.y);
|
|
24116
|
+
return;
|
|
24117
|
+
case "drag":
|
|
24118
|
+
await drag(page, at, end ?? at, command.button);
|
|
24119
|
+
return;
|
|
24120
|
+
case "scroll":
|
|
24121
|
+
await page.mouse.move(at.x, at.y);
|
|
24122
|
+
await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
|
|
24123
|
+
return;
|
|
24124
|
+
}
|
|
24125
|
+
});
|
|
24126
|
+
} catch (error) {
|
|
24127
|
+
if (error instanceof CommandError) throw error;
|
|
24128
|
+
const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
24129
|
+
throw new CommandError("action_failed", `Mouse ${command.action} at (${fmt(at.x)}, ${fmt(at.y)}) failed: ${message}`);
|
|
24130
|
+
}
|
|
24131
|
+
if (command.action === "scroll") await scrollSettled(page);
|
|
24132
|
+
}
|
|
24133
|
+
/** `browser.mouse`: act at coordinates in a session's tab, checked against its current viewport. */
|
|
24134
|
+
async function mouse(session, command) {
|
|
24135
|
+
await mouseAction(session.page(command.tabId), command, await session.currentViewport(command.tabId));
|
|
24136
|
+
}
|
|
23177
24137
|
// ../runner/src/browser/handlers.ts
|
|
23178
24138
|
/**
|
|
23179
24139
|
* Handlers for the `browser.*` runner commands (§4.3, §12.2). Wire `execute` into the
|
|
@@ -23264,6 +24224,10 @@ function createBrowserCommandHandlers(deps) {
|
|
|
23264
24224
|
await press(manager.get(c.sessionId), c.key, c.tabId);
|
|
23265
24225
|
return {};
|
|
23266
24226
|
},
|
|
24227
|
+
"browser.mouse": async ({ name: _name, sessionId, ...command }) => {
|
|
24228
|
+
await mouse(manager.get(sessionId), command);
|
|
24229
|
+
return {};
|
|
24230
|
+
},
|
|
23267
24231
|
"browser.screenshot": (c) => screenshot(manager.get(c.sessionId), c.path, {
|
|
23268
24232
|
tabId: c.tabId,
|
|
23269
24233
|
fullPage: c.fullPage,
|
|
@@ -24127,21 +25091,34 @@ var BrowserSession = class {
|
|
|
24127
25091
|
await (await this.cdp(state)).send("Emulation.setDeviceMetricsOverride", deviceMetricsParams(this.viewport));
|
|
24128
25092
|
} catch {}
|
|
24129
25093
|
}
|
|
24130
|
-
/**
|
|
25094
|
+
/**
|
|
25095
|
+
* A view's own size (CSS pixels and scale); undefined while it has none. Throws when the page
|
|
25096
|
+
* doesn't answer in time.
|
|
25097
|
+
*/
|
|
25098
|
+
async viewSize(page) {
|
|
25099
|
+
const [width, height, scale] = await withTimeout(page.evaluate("[window.innerWidth, window.innerHeight, window.devicePixelRatio]"), NATIVE_MEASURE_TIMEOUT_MS);
|
|
25100
|
+
return width && height ? {
|
|
25101
|
+
width,
|
|
25102
|
+
height,
|
|
25103
|
+
deviceScaleFactor: scale || 1
|
|
25104
|
+
} : void 0;
|
|
25105
|
+
}
|
|
25106
|
+
/** The shown native view's size, just measured: the session's viewport follows it. */
|
|
25107
|
+
adopt(next) {
|
|
25108
|
+
const changed = !sameViewport(next, this.viewport);
|
|
25109
|
+
this.viewport = next;
|
|
25110
|
+
this.measuredAt = Date.now();
|
|
25111
|
+
if (changed) this.screencast.setFrameSize(nativeFrame(), next);
|
|
25112
|
+
}
|
|
25113
|
+
/**
|
|
25114
|
+
* The shown native view's actual size, as the desktop panel laid it out. Tolerant, for viewer
|
|
25115
|
+
* input and viewport answers: a view that doesn't answer keeps its last size.
|
|
25116
|
+
*/
|
|
24131
25117
|
async measure() {
|
|
24132
25118
|
try {
|
|
24133
|
-
const
|
|
24134
|
-
if (
|
|
24135
|
-
|
|
24136
|
-
width,
|
|
24137
|
-
height,
|
|
24138
|
-
deviceScaleFactor: scale || 1
|
|
24139
|
-
};
|
|
24140
|
-
const changed = !sameViewport(next, this.viewport);
|
|
24141
|
-
this.viewport = next;
|
|
24142
|
-
if (changed) this.screencast.setFrameSize(nativeFrame(), next);
|
|
24143
|
-
}
|
|
24144
|
-
this.measuredAt = Date.now();
|
|
25119
|
+
const next = await this.viewSize(this.page());
|
|
25120
|
+
if (next) this.adopt(next);
|
|
25121
|
+
else this.measuredAt = Date.now();
|
|
24145
25122
|
} catch {}
|
|
24146
25123
|
return { ...this.viewport };
|
|
24147
25124
|
}
|
|
@@ -24152,6 +25129,22 @@ var BrowserSession = class {
|
|
|
24152
25129
|
async syncViewport() {
|
|
24153
25130
|
if (this.native) await this.measure();
|
|
24154
25131
|
}
|
|
25132
|
+
/**
|
|
25133
|
+
* The viewport of `tabId` (default: the active tab) that an agent's coordinates refer to, now:
|
|
25134
|
+
* the emulated size every tab shares, or a native view's own size measured fresh (agent actions
|
|
25135
|
+
* are few, and the panel may have just resized). A native view in the background can differ
|
|
25136
|
+
* from the shown one, so only the shown view's size updates the session's viewport. Strict,
|
|
25137
|
+
* unlike `measure`: a view that can't be measured (busy in a script, navigating) fails with a
|
|
25138
|
+
* retryable `viewport_unavailable` instead of borrowing another size, so no action is sent.
|
|
25139
|
+
*/
|
|
25140
|
+
async currentViewport(tabId) {
|
|
25141
|
+
const state = this.tabState(tabId);
|
|
25142
|
+
if (!this.native) return { ...this.viewport };
|
|
25143
|
+
const size = await this.viewSize(state.page).catch(() => void 0);
|
|
25144
|
+
if (!size) throw new CommandError("viewport_unavailable", `Could not measure the size of tab ${state.tabId} (the page may be busy or loading), so nothing was done. Try again in a moment.`);
|
|
25145
|
+
if (state.tabId === this.activeTabId) this.adopt(size);
|
|
25146
|
+
return { ...size };
|
|
25147
|
+
}
|
|
24155
25148
|
/** The native view's size for input, measured at most once a second (pointer moves are many). */
|
|
24156
25149
|
async nativeViewport() {
|
|
24157
25150
|
if (Date.now() - this.measuredAt > NATIVE_MEASURE_MS) return this.measure();
|
|
@@ -24692,7 +25685,12 @@ var SessionHost = class {
|
|
|
24692
25685
|
async send(cmd) {
|
|
24693
25686
|
const hosted = this.get(cmd.sessionId);
|
|
24694
25687
|
await hosted.checkpointChain;
|
|
24695
|
-
|
|
25688
|
+
try {
|
|
25689
|
+
await hosted.session.send(cmd.input);
|
|
25690
|
+
} catch (error) {
|
|
25691
|
+
if (error instanceof ProviderSessionError && error.code === "turn_running") throw new CommandError("turn_running", error.message);
|
|
25692
|
+
throw error;
|
|
25693
|
+
}
|
|
24696
25694
|
}
|
|
24697
25695
|
async steer(cmd) {
|
|
24698
25696
|
const { session } = this.get(cmd.sessionId);
|
|
@@ -25059,6 +26057,6 @@ async function startRunner(opts) {
|
|
|
25059
26057
|
close
|
|
25060
26058
|
};
|
|
25061
26059
|
}
|
|
25062
|
-
export {
|
|
26060
|
+
export { findExecutable as A, installClaudeSdk as C, acquireInstallLock as D, resolveNpm as E, ownNodeEnv as M, import_websocket_server as N, isSecretPath as O, wrapper_default as P, importClaudeSdkFrom as S, runNpm as T, claudeMemoryDir as _, bufferToFormData as a, claudeSdkSource as b, refreshProcessPath as c, ImageEngineRegistry as d, createImageEngineRegistry as f, ImageGenerationError as g, resolveImageOutput as h, tryDecodeURIComponent as i, isElectronRuntime as j, brokenInstallMessage as k, defaultShell as l, imageInfo as m, readLock as n, createAdaptorServer as o, IMAGE_ASPECT_RATIOS as p, Hono as r, upgradeWebSocket as s, startRunner as t, shellCommandLine as u, findGitRoot as v, isClaudeSdkInstalled as w, CLAUDE_SDK_VERSION as x, mainRepoRoot as y };
|
|
25063
26061
|
|
|
25064
|
-
//# sourceMappingURL=runner-
|
|
26062
|
+
//# sourceMappingURL=runner-CQYh087M.js.map
|