kritya 0.8.40-beta → 0.8.42-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/doctor.js +8 -1
- package/dist/index.js +13 -9
- package/dist/shell/sandbox.js +276 -16
- package/dist/ui/useAgent.js +2 -1
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { BUILTIN_PROVIDERS, CONFIG_DIR, CONFIG_FILE, legacyGlobalModel, loadConfig, privacyModeFor, resolveProvider, } from "../config/config.js";
|
|
5
|
+
import { BUILTIN_PROVIDERS, CONFIG_DIR, CONFIG_FILE, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "../config/config.js";
|
|
6
6
|
import { DEFAULT_MODEL, contextWindowFor } from "../config/models.js";
|
|
7
7
|
import { retentionDaysFor } from "../config/retention.js";
|
|
8
8
|
import { assertJsonWithinLimits } from "../config/jsonSafety.js";
|
|
@@ -368,6 +368,13 @@ export async function runDoctorCli(argv, options = {}) {
|
|
|
368
368
|
const offline = argv.includes("--offline");
|
|
369
369
|
const dirArg = argv.find((a) => !a.startsWith("-"));
|
|
370
370
|
const workspace = path.resolve(options.workspace ?? dirArg ?? ".");
|
|
371
|
+
// The user's own global .env is unconditionally trusted and is where the
|
|
372
|
+
// interactive and headless paths both pick up provider keys (see
|
|
373
|
+
// `runInteractive` in index.tsx and `runHeadless` in headless.ts). Without
|
|
374
|
+
// loading it here the provider check below sees only the ambient
|
|
375
|
+
// environment, so a key that lives solely in ~/.kritya/.env is reported as
|
|
376
|
+
// missing — a false "no API key" that also flips the exit code to 1.
|
|
377
|
+
loadDotEnv([path.join(CONFIG_DIR, ".env")]);
|
|
371
378
|
const sections = await collectDiagnostics({ ...options, workspace, offline });
|
|
372
379
|
const report = summarize(sections, workspace);
|
|
373
380
|
if (json) {
|
package/dist/index.js
CHANGED
|
@@ -221,17 +221,21 @@ if (args.version) {
|
|
|
221
221
|
// pipe never pays for it and never sees it), cached for a day, bounded, and
|
|
222
222
|
// written to stderr.
|
|
223
223
|
console.log(VERSION);
|
|
224
|
+
// Awaited, not fired off behind a deferred process.exit(). This is
|
|
225
|
+
// top-level module code with no `return`, so a detached exit does not stop
|
|
226
|
+
// the statements below it: on a TTY the whole interactive path ran anyway
|
|
227
|
+
// (the TTY check, then main() and its AI-disclosure banner) and was then
|
|
228
|
+
// killed mid-frame when the notice resolved, which trips a libuv assertion
|
|
229
|
+
// on Windows — "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
|
|
230
|
+
// file src\win\async.c". Awaiting keeps the notice and makes the exit
|
|
231
|
+
// synchronous. checkForUpdate is capped by a 2s AbortSignal timeout, cached
|
|
232
|
+
// for a day, and never throws, so this cannot hang or fail.
|
|
224
233
|
if (process.stdout.isTTY) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
console.error(notice);
|
|
229
|
-
})
|
|
230
|
-
.finally(() => process.exit(0));
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
process.exit(0);
|
|
234
|
+
const notice = await updateNoticeForUser();
|
|
235
|
+
if (notice)
|
|
236
|
+
console.error(notice);
|
|
234
237
|
}
|
|
238
|
+
process.exit(0);
|
|
235
239
|
}
|
|
236
240
|
const workspace = path.resolve(args.dir);
|
|
237
241
|
if (!fs.existsSync(workspace) || !fs.statSync(workspace).isDirectory()) {
|
package/dist/shell/sandbox.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { classifyDanger } from "../permissions/danger.js";
|
|
6
7
|
let cachedTool;
|
|
8
|
+
let cachedMxcExec;
|
|
7
9
|
function commandExists(bin) {
|
|
8
10
|
const finder = os.platform() === "win32" ? "where" : "which";
|
|
9
11
|
try {
|
|
@@ -13,7 +15,66 @@ function commandExists(bin) {
|
|
|
13
15
|
return false;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* MXC ships `wxc-exec.exe` per architecture (`bin/x64/`, `bin/arm64/`), and its
|
|
20
|
+
* own resolver picks the same way — see `sdk/node/src/v1/platform.ts::
|
|
21
|
+
* findWxcExecutable`, which this mirrors.
|
|
22
|
+
*/
|
|
23
|
+
function mxcArch() {
|
|
24
|
+
return os.arch() === "arm64" ? "arm64" : "x64";
|
|
25
|
+
}
|
|
26
|
+
/** The `@microsoft/mxc-sdk` install directory, or null when it isn't installed. */
|
|
27
|
+
function mxcPackageRoot() {
|
|
28
|
+
try {
|
|
29
|
+
// `./package.json` is in the SDK's exports map, so this resolves whether or
|
|
30
|
+
// not the package's main entry is importable.
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
return path.dirname(require.resolve("@microsoft/mxc-sdk/package.json"));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Locate `wxc-exec.exe`, in the order a user would expect to override it: an
|
|
40
|
+
* explicit path, then MXC's own `MXC_BIN_DIR`, then `PATH`, then an optional
|
|
41
|
+
* `@microsoft/mxc-sdk` install. Null means MXC isn't present, which leaves
|
|
42
|
+
* Windows behaving exactly as it did before MXC support existed.
|
|
43
|
+
*/
|
|
44
|
+
function locateMxcExecutable() {
|
|
45
|
+
const explicit = process.env.KRITYA_MXC_EXEC;
|
|
46
|
+
if (explicit && fs.existsSync(explicit))
|
|
47
|
+
return explicit;
|
|
48
|
+
// MXC's own override: <MXC_BIN_DIR>/<arch>/wxc-exec.exe.
|
|
49
|
+
const binDir = process.env.MXC_BIN_DIR;
|
|
50
|
+
if (binDir) {
|
|
51
|
+
const candidate = path.join(binDir, mxcArch(), "wxc-exec.exe");
|
|
52
|
+
if (fs.existsSync(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
// On PATH — kept as a bare name so the OS resolves it at spawn time.
|
|
56
|
+
if (commandExists("wxc-exec.exe"))
|
|
57
|
+
return "wxc-exec.exe";
|
|
58
|
+
const root = mxcPackageRoot();
|
|
59
|
+
if (root) {
|
|
60
|
+
const candidate = path.join(root, "bin", mxcArch(), "wxc-exec.exe");
|
|
61
|
+
if (fs.existsSync(candidate))
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** Cached after the first check — locating it can shell out to `where`. */
|
|
67
|
+
function mxcExecutable() {
|
|
68
|
+
if (cachedMxcExec === undefined)
|
|
69
|
+
cachedMxcExec = locateMxcExecutable();
|
|
70
|
+
return cachedMxcExec;
|
|
71
|
+
}
|
|
72
|
+
/** Test-only: drops the memoized backend and MXC path between cases. */
|
|
73
|
+
export function resetSandboxToolCache() {
|
|
74
|
+
cachedTool = undefined;
|
|
75
|
+
cachedMxcExec = undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Which sandbox backend (if any) is usable on this platform, cached after the first check. */
|
|
17
78
|
function sandboxTool() {
|
|
18
79
|
if (cachedTool !== undefined)
|
|
19
80
|
return cachedTool;
|
|
@@ -24,6 +85,12 @@ function sandboxTool() {
|
|
|
24
85
|
else if (platform === "darwin") {
|
|
25
86
|
cachedTool = commandExists("sandbox-exec") ? "sandbox-exec" : null;
|
|
26
87
|
}
|
|
88
|
+
else if (platform === "win32") {
|
|
89
|
+
// MXC (Microsoft eXecution Containers) is Windows' only containment
|
|
90
|
+
// backend — AppContainer + DACLs, enforced by the kernel. It is an
|
|
91
|
+
// optional install, so "not there" is the common case and stays null.
|
|
92
|
+
cachedTool = mxcExecutable() ? "mxc" : null;
|
|
93
|
+
}
|
|
27
94
|
else {
|
|
28
95
|
cachedTool = null;
|
|
29
96
|
}
|
|
@@ -35,10 +102,11 @@ export function sandboxAvailable() {
|
|
|
35
102
|
/**
|
|
36
103
|
* Default `sandboxMode` when the config leaves `sandboxExec` unset. "auto" on
|
|
37
104
|
* Linux/macOS, where bwrap/sandbox-exec can actually confine writes to the
|
|
38
|
-
* workspace. On Windows
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
105
|
+
* workspace. On Windows "strict" is kept even though MXC can now back it,
|
|
106
|
+
* because "strict" is the mode that fails closed: with MXC installed every
|
|
107
|
+
* command runs *inside* the container instead of being refused, and on a host
|
|
108
|
+
* where MXC isn't present (or can't be served) commands are refused rather
|
|
109
|
+
* than silently falling back to an unconfined run.
|
|
42
110
|
*/
|
|
43
111
|
export function defaultSandboxMode() {
|
|
44
112
|
return os.platform() === "win32" ? "strict" : "auto";
|
|
@@ -46,8 +114,10 @@ export function defaultSandboxMode() {
|
|
|
46
114
|
/** One-line reason sandboxing can't run here, for a fallback warning. */
|
|
47
115
|
export function sandboxUnavailableReason() {
|
|
48
116
|
const platform = os.platform();
|
|
49
|
-
if (platform === "win32")
|
|
50
|
-
return "
|
|
117
|
+
if (platform === "win32") {
|
|
118
|
+
return ("wxc-exec.exe not found (install @microsoft/mxc-sdk, set MXC_BIN_DIR, " +
|
|
119
|
+
"or point KRITYA_MXC_EXEC at the binary)");
|
|
120
|
+
}
|
|
51
121
|
if (platform === "linux")
|
|
52
122
|
return "bwrap (bubblewrap) not found on PATH";
|
|
53
123
|
if (platform === "darwin")
|
|
@@ -58,18 +128,18 @@ export function sandboxUnavailableReason() {
|
|
|
58
128
|
export function shouldSandbox(mode, command) {
|
|
59
129
|
if (!mode || mode === "off")
|
|
60
130
|
return false;
|
|
61
|
-
// "always"/"strict" mean always, on every platform
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// maximum enforcement/visibility even without a real sandbox backing it.
|
|
131
|
+
// "always"/"strict" mean always, on every platform. Where no backend can be
|
|
132
|
+
// found that falls back to the "[sandbox unavailable]" note ("always") or a
|
|
133
|
+
// refusal ("strict") — deliberate, since these are the modes for someone who
|
|
134
|
+
// wants maximum enforcement/visibility even without a sandbox behind them.
|
|
66
135
|
if (mode === "always" || mode === "strict")
|
|
67
136
|
return true;
|
|
68
|
-
// "auto"
|
|
69
|
-
//
|
|
70
|
-
//
|
|
137
|
+
// "auto" on Windows without a backend: falling back to "only flagged
|
|
138
|
+
// commands" avoids a spurious fallback note on every single shell call,
|
|
139
|
+
// which "sandbox everything" would otherwise cause. With MXC installed
|
|
140
|
+
// there *is* a backend, so "auto" means here what it means elsewhere.
|
|
71
141
|
if (os.platform() === "win32")
|
|
72
|
-
return classifyDanger(command) !== null;
|
|
142
|
+
return sandboxAvailable() || classifyDanger(command) !== null;
|
|
73
143
|
return true;
|
|
74
144
|
}
|
|
75
145
|
/**
|
|
@@ -365,6 +435,169 @@ ${writeExtra}
|
|
|
365
435
|
${readExtra}
|
|
366
436
|
`;
|
|
367
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Windows' system-drive root, e.g. `C:\` — the read-everything analogue of
|
|
440
|
+
* bwrap's `--ro-bind / /`. It is granted through MXC's *readwrite* list rather
|
|
441
|
+
* than its readonly one; see `mxcPolicyJson` for why that is not a loosening.
|
|
442
|
+
*/
|
|
443
|
+
function systemDriveRoot() {
|
|
444
|
+
const drive = process.env.SystemDrive || "C:";
|
|
445
|
+
return drive.endsWith("\\") ? drive : `${drive}\\`;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* `command` as a `cmd.exe` command line, the way `buildSandboxedCommand`'s
|
|
449
|
+
* other two branches get theirs from `sh -c`.
|
|
450
|
+
*
|
|
451
|
+
* `wxc-exec` takes one command-line string and hands it to CreateProcess, which
|
|
452
|
+
* reads it as an image plus arguments — no shell, so `a && b` arrives at the
|
|
453
|
+
* first program as literal arguments and `|`, `>`, `cd …` and the like do
|
|
454
|
+
* nothing at all. This mirrors what Node's own `child_process.exec` passes on
|
|
455
|
+
* Windows (`cmd.exe /d /s /c "…"`), so a sandboxed command and an unsandboxed
|
|
456
|
+
* one behave the same: `/d` skips the registry AutoRun hooks, and `/s` makes
|
|
457
|
+
* cmd strip only the outermost quotes so a command carrying its own quoted
|
|
458
|
+
* arguments survives. Verified through wxc-exec: `&&`, `|`, `>` and nested
|
|
459
|
+
* quotes (`node -e "…"`) all work, and `dir`/`echo` resolve as cmd builtins.
|
|
460
|
+
*/
|
|
461
|
+
function windowsShellCommand(command) {
|
|
462
|
+
return `cmd.exe /d /s /c "${command}"`;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* `PATH` directories the container must be able to read for `cmd.exe` to
|
|
466
|
+
* resolve bare tool names such as `git`, `npm` and `node`.
|
|
467
|
+
*
|
|
468
|
+
* The container's `PATH` is the real, registry-derived one, so it lists
|
|
469
|
+
* directories like `C:\Program Files\nodejs` — but those are `EPERM` to the
|
|
470
|
+
* container until granted, so cmd.exe's lookup fails inside them and reports
|
|
471
|
+
* `'node' is not recognized as an internal or external command`. Granting them
|
|
472
|
+
* read-only fixes it. This is the same job MXC's own
|
|
473
|
+
* `policy.filesystem.getAvailableToolsPolicy` does, and the filters below
|
|
474
|
+
* follow it.
|
|
475
|
+
*
|
|
476
|
+
* Filtered rather than passed through wholesale:
|
|
477
|
+
* - only paths that exist and are directories,
|
|
478
|
+
* - nothing under `%SystemRoot%` — the container already has a baseline over
|
|
479
|
+
* the system directories, and MXC's helper excludes them for that reason,
|
|
480
|
+
* - never a drive root, and never a path that contains the home directory or
|
|
481
|
+
* the workspace: granting an ancestor of either aborts container creation
|
|
482
|
+
* outright (see `mxcPolicyJson`), and a `PATH` entry that contains the
|
|
483
|
+
* workspace would also contradict the read-write grant it already has.
|
|
484
|
+
*/
|
|
485
|
+
function windowsToolPaths(workspace) {
|
|
486
|
+
const home = os.homedir();
|
|
487
|
+
const systemRoot = (process.env.SystemRoot || process.env.WINDIR || "C:\\Windows").replace(/\\+$/, "");
|
|
488
|
+
// True when `parent` is `child` or contains it.
|
|
489
|
+
const contains = (parent, child) => {
|
|
490
|
+
const rel = path.relative(parent, child);
|
|
491
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
492
|
+
};
|
|
493
|
+
const seen = new Set();
|
|
494
|
+
const out = [];
|
|
495
|
+
for (const raw of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
496
|
+
const entry = raw.trim();
|
|
497
|
+
if (!entry || !path.isAbsolute(entry))
|
|
498
|
+
continue;
|
|
499
|
+
const abs = path.resolve(entry);
|
|
500
|
+
const key = abs.toLowerCase();
|
|
501
|
+
if (seen.has(key))
|
|
502
|
+
continue;
|
|
503
|
+
seen.add(key);
|
|
504
|
+
// `C:\` (and a bare `C:`) is the read-everything entry, handled separately.
|
|
505
|
+
if (abs === path.parse(abs).root)
|
|
506
|
+
continue;
|
|
507
|
+
if (abs.toLowerCase() === systemRoot.toLowerCase())
|
|
508
|
+
continue;
|
|
509
|
+
if (abs.toLowerCase().startsWith(systemRoot.toLowerCase() + "\\"))
|
|
510
|
+
continue;
|
|
511
|
+
if (contains(abs, home) || contains(abs, workspace))
|
|
512
|
+
continue;
|
|
513
|
+
try {
|
|
514
|
+
if (!fs.statSync(abs).isDirectory())
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
out.push(abs);
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* The MXC request for one command, as JSON. Mirrors the posture of the bwrap
|
|
526
|
+
* and sandbox-exec branches below: reads stay open, writes are confined to the
|
|
527
|
+
* workspace plus the same short allowlist of tool caches.
|
|
528
|
+
*
|
|
529
|
+
* MXC's schema is versioned and the version marker is part of the contract, so
|
|
530
|
+
* this targets 1.0.0 (`schemas/stable/mxc-config.schema.1.0.0.json`).
|
|
531
|
+
* `wxc-exec` validates natively and refuses a request it cannot serve, so a
|
|
532
|
+
* policy that is wrong here fails the command rather than running it unconfined.
|
|
533
|
+
*
|
|
534
|
+
* Deliberate choices, all to keep Windows consistent with the other platforms
|
|
535
|
+
* rather than stricter by accident:
|
|
536
|
+
* - The network is left open. Neither bwrap nor sandbox-exec restricts it, and
|
|
537
|
+
* a contained command that cannot reach the network cannot `npm install`,
|
|
538
|
+
* `git push`, or run a dev server. Tightening it is a cross-platform call.
|
|
539
|
+
* - The temp dir is not isolated. Windows has no tmpfs equivalent, so scratch
|
|
540
|
+
* state already persists across calls without a shared-dir bind. MXC hands
|
|
541
|
+
* the container its own writable temp dir regardless (it rewrites `TEMP` to
|
|
542
|
+
* `%LOCALAPPDATA%\Packages\sandbox.{…}\AC\Temp`), so nothing needs granting
|
|
543
|
+
* here either.
|
|
544
|
+
* - UI access stays enabled. MXC's default blocks the Win32k subsystem, and a
|
|
545
|
+
* contained process that cannot reach it dies before running a line of its
|
|
546
|
+
* own code, with STATUS_DLL_INIT_FAILED (0xC0000142) — wxc-exec names this
|
|
547
|
+
* exact setting when it reports that. bwrap and sandbox-exec do not restrict
|
|
548
|
+
* the UI either, so this is the consistent posture rather than a loosening.
|
|
549
|
+
* - The system drive root is granted as read-write, and `readonlyPaths` is
|
|
550
|
+
* omitted entirely. Both spellings are meant to say "reads stay open over
|
|
551
|
+
* the whole drive", and MXC's own `policy.filesystem.getPowerShellPolicy`
|
|
552
|
+
* emits the readonly one — but on Windows 11 24H2 that spelling aborts
|
|
553
|
+
* container creation. wxc-exec logs `process created (PID …)` and the child
|
|
554
|
+
* then exits 1 with no output, so the command silently never runs; it is
|
|
555
|
+
* reproducible with a bare `readonlyPaths: ["C:\\"]` and with the drive root
|
|
556
|
+
* added to a policy that already works. Every ancestor of the profile chain
|
|
557
|
+
* behaves the same way (`C:\Users`, `%USERPROFILE%`, `…\AppData`,
|
|
558
|
+
* `…\AppData\Local`), which is consistent with the drive root needing to be
|
|
559
|
+
* DACL-brokered for the contained token. Listing it under `readwritePaths`
|
|
560
|
+
* instead is harmless and grants what was actually intended: the command can
|
|
561
|
+
* stat `C:\` — which `node.exe`, `cmd.exe` and `pwsh.exe` all do at startup,
|
|
562
|
+
* and without which they die with `EPERM: lstat 'C:\'` — while writes to the
|
|
563
|
+
* root, to the profile and to `%USERPROFILE%` are still denied. Verified
|
|
564
|
+
* against `wxc-exec.exe` 1.0.0 on Windows 11 24H2 with a policy of exactly
|
|
565
|
+
* this shape: reads of `C:\`, `System32` and `Program Files` succeed, the
|
|
566
|
+
* workspace and the container's own temp dir are writable, and the profile
|
|
567
|
+
* root, the drive root and `C:\Windows\Temp` all stay unwritable.
|
|
568
|
+
* `readonlyPaths` carries the `PATH` tool directories from
|
|
569
|
+
* `windowsToolPaths`. MXC already gives the container a baseline over the
|
|
570
|
+
* system directories and over anything whose ACL carries an AppContainer
|
|
571
|
+
* capability SID, so most of `PATH` needs nothing said about it — but a
|
|
572
|
+
* directory outside that baseline (per-user installs such as
|
|
573
|
+
* `%APPDATA%\npm`) is `EPERM` until granted, and `cmd.exe` cannot resolve a
|
|
574
|
+
* bare `npm` or `node` that lives in one. The key is omitted rather than
|
|
575
|
+
* sent as `[]` when there is nothing to add, so a policy built without it
|
|
576
|
+
* is byte-identical to what this function produced before.
|
|
577
|
+
*/
|
|
578
|
+
export function mxcPolicyJson(command, workspace, writable, readable = []) {
|
|
579
|
+
return JSON.stringify({
|
|
580
|
+
version: "1.0.0",
|
|
581
|
+
containment: "processcontainer",
|
|
582
|
+
process: { commandLine: command, cwd: workspace },
|
|
583
|
+
filesystem: {
|
|
584
|
+
// Existing paths only: the bwrap branch skips missing binds for the same
|
|
585
|
+
// reason, and naming a path that is not there is a needless way to have
|
|
586
|
+
// the whole request rejected. The drive root closes the list because it
|
|
587
|
+
// is the read-everything entry — see the note above for why it belongs
|
|
588
|
+
// here rather than in `readonlyPaths`.
|
|
589
|
+
readwritePaths: [...new Set([...writable, workspace, systemDriveRoot()])],
|
|
590
|
+
// Never the drive root or the profile chain — see the note above; both
|
|
591
|
+
// are fatal under `readonlyPaths`.
|
|
592
|
+
...(readable.length > 0 ? { readonlyPaths: [...new Set(readable)] } : {}),
|
|
593
|
+
},
|
|
594
|
+
network: { egress: { default: "allow" }, ingress: { default: "allow" } },
|
|
595
|
+
ui: { disable: false },
|
|
596
|
+
// MXC telemetry is off unless the run opts in, the user consents, and
|
|
597
|
+
// administrative policy permits it. Kritya opts out on the user's behalf.
|
|
598
|
+
telemetry: { enabled: false },
|
|
599
|
+
});
|
|
600
|
+
}
|
|
368
601
|
/**
|
|
369
602
|
* Wraps `command` (run via `sh -c`) so it's confined to `workspace`: writes
|
|
370
603
|
* are blocked everywhere else, network and (outside the real temp-dir roots)
|
|
@@ -382,6 +615,33 @@ export function buildSandboxedCommand(command, workspace) {
|
|
|
382
615
|
return null;
|
|
383
616
|
// Linked worktrees / submodules keep their real git dir outside the workspace.
|
|
384
617
|
const gitDir = externalGitCommonDir(workspace);
|
|
618
|
+
if (tool === "mxc") {
|
|
619
|
+
// Windows: hand the whole request to wxc-exec, which creates the
|
|
620
|
+
// AppContainer and runs the command inside it. The policy travels as
|
|
621
|
+
// base64 rather than a file — the same way MXC's own SDK passes it — so
|
|
622
|
+
// there is no profile file to create, guard, or clean up. Compare the
|
|
623
|
+
// symlink/TOCTOU defences the macOS branch needs for its profile: never
|
|
624
|
+
// writing a policy to disk removes that whole class of problem.
|
|
625
|
+
//
|
|
626
|
+
// Deliberately ahead of the shared-temp-dir setup below, which creates a
|
|
627
|
+
// directory as a side effect: Windows has no tmpfs to carve it out of, so
|
|
628
|
+
// there is nothing for it to do here.
|
|
629
|
+
const exe = mxcExecutable();
|
|
630
|
+
if (!exe)
|
|
631
|
+
return null;
|
|
632
|
+
const writable = [...extraWritablePaths(), ...(gitDir ? [gitDir] : [])].filter((p) => fs.existsSync(p));
|
|
633
|
+
// Unlike the branches below there is no `sh -c` here to give the command a
|
|
634
|
+
// shell, so `windowsShellCommand` supplies the `cmd.exe` one — see its
|
|
635
|
+
// note. `windowsToolPaths` supplies the read-only grants cmd.exe needs to
|
|
636
|
+
// resolve bare tool names inside the container.
|
|
637
|
+
return {
|
|
638
|
+
cmd: exe,
|
|
639
|
+
args: [
|
|
640
|
+
"--config-base64",
|
|
641
|
+
Buffer.from(mxcPolicyJson(windowsShellCommand(command), workspace, writable, windowsToolPaths(workspace)), "utf8").toString("base64"),
|
|
642
|
+
],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
385
645
|
// null if the path exists but isn't a real, self-owned directory (e.g.
|
|
386
646
|
// another local user squatted it as a symlink) — see safeSandboxSharedTmpDir.
|
|
387
647
|
const shared = safeSandboxSharedTmpDir();
|
package/dist/ui/useAgent.js
CHANGED
|
@@ -125,7 +125,8 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
125
125
|
const hasConfirmedAcceptEdits = useRef(false);
|
|
126
126
|
const hasConfirmedBypassMode = useRef(false);
|
|
127
127
|
/** Whether auto/bypass mode is even reachable — requires a real sandbox
|
|
128
|
-
* backend on this platform (Windows
|
|
128
|
+
* backend on this platform (on Windows that means MXC is installed; see
|
|
129
|
+
* sandboxAvailable). */
|
|
129
130
|
const sandboxActive = (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable();
|
|
130
131
|
const abortRef = useRef(null);
|
|
131
132
|
/** Tool calls currently running, keyed by call id — more than one when a
|