windows-development-sandbox 1.11.7 → 1.13.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 +13 -6
- package/config.example.json +15 -4
- package/dist/app/features/access/access.def.d.ts +190 -0
- package/dist/app/features/access/access.def.js +273 -0
- package/dist/app/features/access/access.run.d.ts +79 -0
- package/dist/app/features/access/access.run.js +129 -0
- package/dist/app/features/access/index.d.ts +2 -0
- package/dist/app/features/access/index.js +3 -0
- package/dist/app/features/agents/agent-entry.js +5 -0
- package/dist/app/features/agents/agents.def.js +1 -0
- package/dist/app/features/apps/apps-rows.d.ts +4 -1
- package/dist/app/features/apps/apps-rows.js +7 -2
- package/dist/app/features/apps/apps.def.d.ts +19 -0
- package/dist/app/features/apps/apps.def.js +50 -12
- package/dist/app/features/apps/apps.run.d.ts +2 -0
- package/dist/app/features/apps/apps.run.js +1 -1
- package/dist/app/features/audit/audit.def.d.ts +350 -0
- package/dist/app/features/audit/audit.def.js +457 -0
- package/dist/app/features/audit/audit.run.d.ts +110 -0
- package/dist/app/features/audit/audit.run.js +133 -0
- package/dist/app/features/audit/index.d.ts +2 -0
- package/dist/app/features/audit/index.js +3 -0
- package/dist/app/features/defs.d.ts +349 -4
- package/dist/app/features/defs.js +16 -1
- package/dist/app/features/files/files.def.js +1 -0
- package/dist/app/features/health/health.def.js +1 -0
- package/dist/app/features/sandbox/sandbox.def.js +3 -0
- package/dist/app/features/sessions/sessions.def.d.ts +36 -0
- package/dist/app/features/sessions/sessions.def.js +77 -9
- package/dist/app/features/sessions/sessions.run.d.ts +25 -2
- package/dist/app/features/sessions/sessions.run.js +71 -3
- package/dist/app/features/settings/settings.def.d.ts +8 -0
- package/dist/app/features/settings/settings.def.js +17 -0
- package/dist/app/features/settings/settings.run.d.ts +3 -0
- package/dist/app/features/settings/settings.run.js +16 -2
- package/dist/app/features/web/web.def.d.ts +9 -0
- package/dist/app/features/web/web.def.js +26 -1
- package/dist/app/features/web/web.run.d.ts +3 -0
- package/dist/app/features/web/web.run.js +11 -3
- package/dist/app/kernel/access.d.ts +33 -0
- package/dist/app/kernel/access.js +44 -0
- package/dist/app/kernel/define.d.ts +26 -2
- package/dist/app/kernel/index.d.ts +1 -0
- package/dist/app/kernel/index.js +1 -0
- package/dist/app/kernel/rpc.d.ts +5 -2
- package/dist/app/registry.d.ts +4 -1
- package/dist/app/registry.js +26 -4
- package/dist/cli/args.d.ts +0 -3
- package/dist/cli/args.js +3 -4
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch/prepare.js +1 -1
- package/dist/cli/commands/maintenance/index.d.ts +0 -1
- package/dist/cli/commands/maintenance/index.js +0 -1
- package/dist/cli/commands/registry.js +3 -2
- package/dist/cli/commands/remote.d.ts +2 -0
- package/dist/cli/commands/remote.js +49 -0
- package/dist/cli/commands/sessions/reopen.js +3 -0
- package/dist/cli/commands/web.js +66 -0
- package/dist/cli/manifest.d.ts +63 -45
- package/dist/cli/manifest.js +33 -18
- package/dist/cli/menu/io-menu.d.ts +5 -3
- package/dist/cli/menu/io-menu.js +12 -5
- package/dist/cli/menu/pages/apps-page.js +2 -15
- package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
- package/dist/cli/menu/pages/maintenance-page.js +51 -8
- package/dist/cli/menu/remote-menu.d.ts +19 -0
- package/dist/cli/menu/remote-menu.js +108 -0
- package/dist/cli/menu/views.js +4 -0
- package/dist/cli/operations/handler.js +15 -1
- package/dist/cli/services/remote-client.d.ts +16 -0
- package/dist/cli/services/remote-client.js +141 -0
- package/dist/cli/services/sessions-service.d.ts +5 -0
- package/dist/cli/services/sessions-service.js +17 -1
- package/dist/core/config/defaults.js +12 -2
- package/dist/core/local-config/file.js +16 -2
- package/dist/core/local-config/overrides.js +7 -0
- package/dist/core/run/versions.js +9 -8
- package/dist/core/schema/apps.d.ts +9 -0
- package/dist/core/schema/apps.js +9 -0
- package/dist/core/schema/config.d.ts +23 -3
- package/dist/core/schema/config.js +42 -5
- package/dist/core/schema/sandbox.d.ts +1 -0
- package/dist/core/schema/sandbox.js +11 -3
- package/dist/core/schema/web.d.ts +51 -0
- package/dist/core/schema/web.js +46 -1
- package/dist/core/util/fs.d.ts +5 -0
- package/dist/core/util/fs.js +23 -0
- package/dist/core/util/index.d.ts +1 -1
- package/dist/core/util/index.js +1 -1
- package/dist/guest/apps/app-installer.js +50 -4
- package/dist/guest/apps/index.d.ts +1 -1
- package/dist/guest/apps/index.js +1 -1
- package/dist/guest/apps/sources.js +1 -1
- package/dist/guest/audit/parse.d.ts +32 -2
- package/dist/guest/audit/parse.js +43 -5
- package/dist/guest/audit/process-audit.d.ts +3 -1
- package/dist/guest/audit/process-audit.js +9 -6
- package/dist/guest/boot/boot.js +12 -3
- package/dist/guest/boot/guide.d.ts +1 -1
- package/dist/guest/boot/guide.js +27 -3
- package/dist/guest/daemon/daemon.js +2 -0
- package/dist/guest/daemon/idle-shutdown.js +2 -1
- package/dist/guest/daemon/sessions.d.ts +23 -2
- package/dist/guest/daemon/sessions.js +151 -20
- package/dist/guest/entries/daemon.js +2 -1
- package/dist/guest/git/decision.js +7 -0
- package/dist/guest/sessions/cmdline.d.ts +14 -0
- package/dist/guest/sessions/cmdline.js +10 -0
- package/dist/guest/setup/addon-mcp.d.ts +2 -0
- package/dist/guest/setup/addon-mcp.js +49 -0
- package/dist/guest/setup/agent-addons.d.ts +9 -0
- package/dist/guest/setup/agent-addons.js +22 -0
- package/dist/guest/setup/headroom.d.ts +7 -0
- package/dist/guest/setup/headroom.js +60 -0
- package/dist/guest/setup/managed-policy.d.ts +2 -1
- package/dist/guest/setup/managed-policy.js +15 -6
- package/dist/guest/toolchains/plan.js +8 -3
- package/dist/guest/toolchains/setup.js +3 -0
- package/dist/host/audit/attribute.d.ts +46 -0
- package/dist/host/audit/attribute.js +76 -0
- package/dist/host/audit/collect.d.ts +29 -3
- package/dist/host/audit/collect.js +256 -61
- package/dist/host/audit/git-commits.d.ts +29 -0
- package/dist/host/audit/git-commits.js +157 -0
- package/dist/host/audit/index.d.ts +5 -0
- package/dist/host/audit/index.js +6 -0
- package/dist/host/audit/log.d.ts +45 -7
- package/dist/host/audit/log.js +148 -45
- package/dist/host/audit/redact.d.ts +2 -0
- package/dist/host/audit/redact.js +67 -0
- package/dist/host/channel/channel.d.ts +5 -1
- package/dist/host/channel/channel.js +12 -0
- package/dist/host/guard/agent-files.js +7 -1
- package/dist/host/guard/commands.js +13 -1
- package/dist/host/guard/git-config.js +27 -7
- package/dist/host/guard/git-dirs.js +78 -3
- package/dist/host/guard/mark-of-the-web.js +3 -1
- package/dist/host/guard/nested-repos.js +7 -2
- package/dist/host/guard/store.js +1 -0
- package/dist/host/guard/types.d.ts +2 -0
- package/dist/host/launch/mappings.js +11 -1
- package/dist/host/launch/project-scan.js +2 -21
- package/dist/host/launch/stage.js +4 -0
- package/dist/host/life/session-ledger.js +2 -1
- package/dist/host/remote/access-store.d.ts +156 -0
- package/dist/host/remote/access-store.js +308 -0
- package/dist/host/remote/cert.d.ts +20 -0
- package/dist/host/remote/cert.js +103 -0
- package/dist/host/remote/door-client.d.ts +13 -0
- package/dist/host/remote/door-client.js +54 -0
- package/dist/host/remote/firewall.d.ts +44 -0
- package/dist/host/remote/firewall.js +118 -0
- package/dist/host/remote/gate.d.ts +125 -0
- package/dist/host/remote/gate.js +391 -0
- package/dist/host/remote/index.d.ts +8 -0
- package/dist/host/remote/index.js +10 -0
- package/dist/host/remote/lan.d.ts +24 -0
- package/dist/host/remote/lan.js +88 -0
- package/dist/host/remote/tls-store.d.ts +22 -0
- package/dist/host/remote/tls-store.js +63 -0
- package/dist/host/remote/windows-helper.d.ts +67 -0
- package/dist/host/remote/windows-helper.js +129 -0
- package/dist/host/web/bridge.d.ts +5 -1
- package/dist/host/web/bridge.js +6 -2
- package/dist/host/web/browser.js +1 -1
- package/dist/host/web/control.d.ts +4 -1
- package/dist/host/web/control.js +28 -4
- package/dist/host/web/rpc.d.ts +16 -2
- package/dist/host/web/rpc.js +37 -4
- package/dist/host/web/runner.d.ts +4 -0
- package/dist/host/web/runner.js +12 -0
- package/dist/host/web/server.d.ts +18 -2
- package/dist/host/web/server.js +97 -24
- package/dist/host/web/state.d.ts +5 -0
- package/dist/host/web/state.js +8 -0
- package/dist/runtime/agent.js +184 -18
- package/dist/runtime/daemon.js +772 -338
- package/dist/runtime/db.js +184 -18
- package/dist/runtime/gitguard.js +6 -0
- package/dist/runtime/main.js +966 -553
- package/dist/runtime/netproxy.js +184 -18
- package/dist/runtime/view.js +186 -18
- package/dist/selftest/app.test.js +66 -7
- package/dist/selftest/audit.test.js +354 -47
- package/dist/selftest/channel.test.js +16 -0
- package/dist/selftest/cmdline.test.js +16 -0
- package/dist/selftest/git-access.test.js +8 -0
- package/dist/selftest/hcs.test.js +35 -0
- package/dist/selftest/hostile-input.test.js +12 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/live-daemon.test.js +25 -0
- package/dist/selftest/local-config.test.js +30 -0
- package/dist/selftest/menu-screens.js +1 -1
- package/dist/selftest/menu.test.js +3 -3
- package/dist/selftest/remote.test.d.ts +2 -0
- package/dist/selftest/remote.test.js +536 -0
- package/dist/selftest/repo-guard.test.js +56 -2
- package/dist/selftest/resume.test.js +2 -1
- package/dist/selftest/sharing.test.js +9 -2
- package/dist/selftest/support/engine-golden.js +9 -0
- package/dist/selftest/support/fixtures.d.ts +14 -0
- package/dist/selftest/support/web-preview.js +157 -0
- package/dist/selftest/toolchains.test.js +147 -2
- package/dist/selftest/web.test.js +2 -0
- package/dist/shared/apps/agent-addons.d.ts +64 -0
- package/dist/shared/apps/agent-addons.js +95 -0
- package/dist/shared/apps/catalogue.d.ts +33 -2
- package/dist/shared/apps/catalogue.js +102 -11
- package/dist/shared/apps/index.d.ts +3 -2
- package/dist/shared/apps/index.js +3 -2
- package/dist/shared/apps/selection.d.ts +2 -0
- package/dist/shared/apps/selection.js +7 -0
- package/dist/shared/channel/schema.d.ts +23 -0
- package/dist/shared/channel/schema.js +8 -0
- package/dist/shared/pty/protocol.d.ts +2 -0
- package/dist/shared/pty/protocol.js +2 -0
- package/dist/sync/watcher.js +9 -1
- package/dist/web/index.html +2 -2
- package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
- package/dist/web/main-Z2WYAE2Z.js +221 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
- package/native/csharp/HcsHost/Hcs.cs +9 -0
- package/native/csharp/HcsHost/Program.cs +26 -1
- package/native/csharp/HcsHost/Share.cs +173 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Program.cs +17 -1
- package/native/csharp/HostTools/WebHelper.cs +499 -0
- package/native/scripts/get-network-profile.ps1 +19 -0
- package/native/scripts/set-folder-access.ps1 +11 -0
- package/native/scripts/set-web-firewall.ps1 +103 -0
- package/package.json +1 -1
- package/sandbox/addons/ccusage/package-lock.json +111 -0
- package/sandbox/addons/ccusage/package.json +7 -0
- package/sandbox/addons/context7/package-lock.json +1313 -0
- package/sandbox/addons/context7/package.json +7 -0
- package/sandbox/addons/headroom/requirements.in +1 -0
- package/sandbox/addons/headroom/requirements.txt +2922 -0
- package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
- package/sandbox/addons/playwright-mcp/package.json +7 -0
- package/sandbox/csharp/PtyHost.cs +20 -0
- package/schemas/config.schema.json +78 -4
- package/schemas/sandbox-config.schema.json +2 -20
- package/dist/cli/commands/maintenance/audit.d.ts +0 -3
- package/dist/cli/commands/maintenance/audit.js +0 -58
- package/dist/web/main-K2JMCAZI.js +0 -219
|
@@ -48,6 +48,14 @@ export const gitAccessSuite = {
|
|
|
48
48
|
'--git-dir=x status',
|
|
49
49
|
'clean -fd',
|
|
50
50
|
'apply p.diff',
|
|
51
|
+
// reading commands with options that run a program or write a file
|
|
52
|
+
'grep -Ocalc.exe x',
|
|
53
|
+
'grep --open-files-in-pager=calc.exe x',
|
|
54
|
+
'ls-remote --upload-pack=calc.exe origin',
|
|
55
|
+
'ls-remote -ucalc.exe origin',
|
|
56
|
+
'diff --output=C:/x.txt',
|
|
57
|
+
'log --output C:/x.txt',
|
|
58
|
+
'diff --ext-diff',
|
|
51
59
|
];
|
|
52
60
|
t.equal(writing.filter((command) => allowed(command.split(' '))), [], 'readonly: anything that writes (or overrides config) is refused');
|
|
53
61
|
t.section('none and full');
|
|
@@ -570,6 +570,41 @@ export const hcsSuite = {
|
|
|
570
570
|
t.check(refused(['grant-vm-access', 'agent-sandbox-1', notDisk]) &&
|
|
571
571
|
refused(['grant-vm-access', 'agent-sandbox-1', missing]) &&
|
|
572
572
|
refused(['grant-vm-access', 'agent-sandbox-1', 'relative.vhdx']), 'grant-vm-access refuses anything but an existing .vhdx at a full path');
|
|
573
|
+
// share (prototype, docs/design/hcs-live-folders.md): refused before HCS is asked, so no Hyper-V is needed.
|
|
574
|
+
const folder = t.dir('hcs-share-folder');
|
|
575
|
+
const share = (over) => ({
|
|
576
|
+
id: 'agent-sandbox-1',
|
|
577
|
+
action: 'add',
|
|
578
|
+
name: 'agentsbx-0123abcd',
|
|
579
|
+
path: folder,
|
|
580
|
+
...over,
|
|
581
|
+
});
|
|
582
|
+
let link = '';
|
|
583
|
+
try {
|
|
584
|
+
link = path.join(tempRoot, 'share-link');
|
|
585
|
+
fs.symlinkSync(folder, link, 'junction');
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
link = ''; // a folder that refuses links (a mapped folder): that case is skipped
|
|
589
|
+
}
|
|
590
|
+
t.check(refused(['share'], share({ id: '0b9a3c2e-1f00-4a4c-9d1e-6a7b8c9d0e1f' })) &&
|
|
591
|
+
refused(['share'], share({ id: 'WindowsSandbox' })) &&
|
|
592
|
+
refused(['share'], share({ name: '3ad483dec1a233132da4197c9790dcda06668793230cd6d9e94b1882e0f7f29c' })) &&
|
|
593
|
+
refused(['share'], share({ name: 'agentsbx-XYZ' })) &&
|
|
594
|
+
refused(['share'], share({ action: 'update' })) &&
|
|
595
|
+
refused(['share'], share({ path: 'relative\\folder' })) &&
|
|
596
|
+
refused(['share'], share({ path: 'C:\\' })) &&
|
|
597
|
+
refused(['share'], share({ path: path.join(tempRoot, 'missing-folder') })) &&
|
|
598
|
+
refused(['share'], share({ path: folder + '\\..\\' + path.basename(folder) })) &&
|
|
599
|
+
refused(['share'], share({ path: folder + '/../' + path.basename(folder) })) &&
|
|
600
|
+
refused(['share'], share({ path: folder + ':stream' })) &&
|
|
601
|
+
refused(['share'], share({ path: folder + '.' })) &&
|
|
602
|
+
refused(['share'], share({ path: folder + ' ' })) &&
|
|
603
|
+
refused(['share'], share({ path: 'C:\\PROGRA~1' })) &&
|
|
604
|
+
refused(['share'], share({ readOnly: 'yes' })) &&
|
|
605
|
+
(!link || refused(['share'], share({ path: link }))), 'share refuses a Windows Sandbox or other foreign id, a foreign share name, an update, a drive, a relative, ' +
|
|
606
|
+
"missing, dotted, '/', stream, trailing-dot or -space, short-name or linked folder" +
|
|
607
|
+
(link ? '' : ' (link case skipped)'));
|
|
573
608
|
const existing = t.write(path.join(tempRoot, 'existing.vhdx'), 'keep me');
|
|
574
609
|
const over = raw.call(['create-vhdx', existing, '1']);
|
|
575
610
|
t.check(!over.ok && fs.readFileSync(existing, 'utf8') === 'keep me', 'create-vhdx never touches an existing file', over.message);
|
|
@@ -12,7 +12,7 @@ import { writeJsonAtomic } from '../shared/io/index.js';
|
|
|
12
12
|
export const hostileInputSuite = {
|
|
13
13
|
name: 'hostile-input',
|
|
14
14
|
title: 'file names, deletes, printed text, mappings and state files from an untrusted sandbox',
|
|
15
|
-
run: async (t) => {
|
|
15
|
+
run: async (t, f) => {
|
|
16
16
|
t.section('file names');
|
|
17
17
|
const unsafe = ['CON.jsonl', 'nul', 'aux.json', 'COM1.txt', 'a:b', 'x.jsonl.', 'x ', '..', 'a\\b', 'LPT3'];
|
|
18
18
|
t.equal(unsafe.filter(isSafeFileName), [], 'device names, streams and trailing dots are not valid names to copy into this PC');
|
|
@@ -52,5 +52,16 @@ export const hostileInputSuite = {
|
|
|
52
52
|
t.equal(fs.readdirSync(path.dirname(atomicFile)).length, 1, '... with no temp files left behind');
|
|
53
53
|
await t.throws(() => assertCmdSafePath('C:\\a\\%TEMP%\\agent.wsb'), 'cannot be started safely', 'a launch path with %VAR% (which cmd.exe would expand) is refused');
|
|
54
54
|
await t.throws(() => assertCmdSafePath('C:\\a"&calc&"\\agent.wsb'), 'cannot be started safely', 'a launch path with quotes and & is refused');
|
|
55
|
+
t.section('repo files read on this PC (security review 2026-10)');
|
|
56
|
+
const repoDir = t.dir('hostile-versions');
|
|
57
|
+
fs.writeFileSync(path.join(repoDir, '.nvmrc'), '\u001b]52;c;ZXZpbA==\u0007\u001b[2J20\n');
|
|
58
|
+
fs.writeFileSync(path.join(repoDir, '.python-version'), '3.12.1\n');
|
|
59
|
+
const versionsRun = await f.run({ cwd: repoDir });
|
|
60
|
+
t.check(!versionsRun.versions.node.spec.includes('\u001b') &&
|
|
61
|
+
versionsRun.versions.node.source !== '.nvmrc' &&
|
|
62
|
+
versionsRun.versions.python.spec === '3.12.1', 'a version file holding escape codes is ignored (it would reach the launch table); a plain one is used', JSON.stringify(versionsRun.versions));
|
|
63
|
+
fs.writeFileSync(path.join(repoDir, '.sandbox-config.json'), '{"readOnly": true}' + ' '.repeat(1100 * 1024));
|
|
64
|
+
await t.throws(() => f.run({ cwd: repoDir, trustConfig: true }), 'over 1 MB', 'a .sandbox-config.json over 1 MB is refused before it is parsed');
|
|
65
|
+
fs.rmSync(path.join(repoDir, '.sandbox-config.json'));
|
|
55
66
|
},
|
|
56
67
|
};
|
package/dist/selftest/index.js
CHANGED
|
@@ -57,6 +57,7 @@ import { syncSuite } from './sync.test.js';
|
|
|
57
57
|
import { testGuardSuite } from './test-guard.test.js';
|
|
58
58
|
import { toolchainsSuite } from './toolchains.test.js';
|
|
59
59
|
import { webSuite } from './web.test.js';
|
|
60
|
+
import { remoteSuite } from './remote.test.js';
|
|
60
61
|
import { workspaceSuite } from './workspace.test.js';
|
|
61
62
|
import { workspaceViewSuite } from './workspace-view.test.js';
|
|
62
63
|
import { liveNetworkSuite } from './live-network.test.js';
|
|
@@ -107,6 +108,7 @@ export const SUITES = [
|
|
|
107
108
|
secretsSuite,
|
|
108
109
|
hostileInputSuite,
|
|
109
110
|
webSuite,
|
|
111
|
+
remoteSuite,
|
|
110
112
|
testGuardSuite,
|
|
111
113
|
// pure helpers and the command line
|
|
112
114
|
layoutSuite,
|
|
@@ -19,6 +19,8 @@ export const liveDaemonSuite = {
|
|
|
19
19
|
Object.assign(session.harness, {
|
|
20
20
|
arguments: ['/d', '/k', 'echo LIVE-AGENT-READY'],
|
|
21
21
|
resumeArguments: ['/d', '/k', 'echo LIVE-AGENT-RESUMED'],
|
|
22
|
+
// The agent's own environment (where its keys live): a shell session must not get it.
|
|
23
|
+
environment: { LIVE_AGENT_KEY: 'sk-live-agent' },
|
|
22
24
|
});
|
|
23
25
|
t.writeJson(gen.sessionPath, session);
|
|
24
26
|
const daemon = startDaemon(t, f, { launchPath: gen.launchPath, data, workspace });
|
|
@@ -82,6 +84,29 @@ export const liveDaemonSuite = {
|
|
|
82
84
|
t.check(Boolean(ping?.ok) &&
|
|
83
85
|
!fs.existsSync(path.join(daemon.run, 'responses', 'evil.json')) &&
|
|
84
86
|
!fs.existsSync(path.join(daemon.run, 'requests', 'evil.json')), 'ping is answered; an unknown request kind gets no answer and is deleted', JSON.stringify(ping));
|
|
87
|
+
t.section('live: a plain shell session');
|
|
88
|
+
daemon.request({ kind: 'session', id: 'shell1', harness: 'shell', args: [], cwd: workspace, shell: 'cmd' });
|
|
89
|
+
const shell = await daemon.started('shell1');
|
|
90
|
+
t.check(shell?.phase === 'running' && shell.harness === 'cmd' && shell.kind === 'shell' && Boolean(shell.attach), 'a shell session runs through ptyhost and is listed by its shell, marked as a shell', JSON.stringify(shell) + '\n' + daemon.diagnostics());
|
|
91
|
+
if (shell?.attach) {
|
|
92
|
+
const shellView = await openViewer(shell.attach);
|
|
93
|
+
viewers.push(shellView);
|
|
94
|
+
// Typing before cmd has drawn its prompt can lose the keystrokes: wait for the prompt first.
|
|
95
|
+
await sees(t, shellView, '>');
|
|
96
|
+
shellView.type('echo SHELL-[%LIVE_AGENT_KEY%]-%USERNAME:~0,0%OK');
|
|
97
|
+
const sawShell = await sees(t, shellView, 'SHELL-[%LIVE_AGENT_KEY%]-OK');
|
|
98
|
+
t.check(Boolean(sawShell), "keystrokes reach the shell, and it has none of the agent's own environment", 'screen so far: ' + tail(shellView.output()) + '\n' + daemon.diagnostics());
|
|
99
|
+
daemon.request({ kind: 'close', id: 'close1', session: 'shell1' });
|
|
100
|
+
const closed = await daemon.response('close1');
|
|
101
|
+
const shellEnded = await t.waitFor(() => {
|
|
102
|
+
const s = daemon.status('shell1');
|
|
103
|
+
return s?.phase === 'exited' ? s : null;
|
|
104
|
+
}, WAIT.exit);
|
|
105
|
+
t.check(Boolean(closed?.ok) && shellEnded !== null && shellView.exitCode() !== null, 'close ends the shell: the sandbox answers, the status says exited and the attached client gets the exit', JSON.stringify(closed) + ' ' + JSON.stringify(daemon.status('shell1')));
|
|
106
|
+
daemon.request({ kind: 'close', id: 'close2', session: 'shell1' });
|
|
107
|
+
const again = await daemon.response('close2');
|
|
108
|
+
t.check(again?.ok === false, 'closing a session that has ended says it is not running', JSON.stringify(again));
|
|
109
|
+
}
|
|
85
110
|
t.section('live: restart and exit');
|
|
86
111
|
daemon.request({ kind: 'restart', id: 'restart1', harness: 'claude' });
|
|
87
112
|
const sawResumed = await sees(t, viewer, 'LIVE-AGENT-RESUMED');
|
|
@@ -61,6 +61,28 @@ export const localConfigSuite = {
|
|
|
61
61
|
await rejectedBecause({ claudeProfile: { hostDir: 'C:\\x' } }, 'claudeProfile.hostDir is not allowed', 'a project file cannot point at the Claude profile folder');
|
|
62
62
|
await rejectedBecause({ toolchains: { node: { hostDir: 'x' } } }, 'toolchains.node.hostDir is not allowed', 'a project file cannot point at host tool folders');
|
|
63
63
|
await rejectedBecause({ sandbox: { memoryMb: 12 } }, 'sandbox.memoryMb:', 'values are type-checked (memory too small)');
|
|
64
|
+
// Security review 2026-10: what a repo could reach through fields that looked harmless.
|
|
65
|
+
const secretFile = path.join(f.shared, 'id_ed25519');
|
|
66
|
+
fs.writeFileSync(secretFile, 'PRIVATE KEY');
|
|
67
|
+
await rejectedBecause({ scripts: [secretFile] }, 'only files inside the project', 'a project file cannot name a script outside the project (it would copy that PC file into the launch folder)');
|
|
68
|
+
await rejectedBecause({ scripts: [path.relative(dirC, secretFile)] }, 'only files inside the project', 'a relative ..\\ script path outside the project is refused too');
|
|
69
|
+
fs.symlinkSync(f.shared, path.join(dirC, 'linked'), 'junction');
|
|
70
|
+
await rejectedBecause({ scripts: ['./linked/id_ed25519'] }, 'only files inside the project', 'a script reached through a link to a folder outside the project is refused');
|
|
71
|
+
fs.rmSync(path.join(dirC, 'linked'));
|
|
72
|
+
for (const name of [
|
|
73
|
+
'AGENT_GIT_ACCESS',
|
|
74
|
+
'GIT_CONFIG_COUNT',
|
|
75
|
+
'ANTHROPIC_BASE_URL',
|
|
76
|
+
'NODE_OPTIONS',
|
|
77
|
+
'Path',
|
|
78
|
+
'https_proxy',
|
|
79
|
+
])
|
|
80
|
+
await rejectedBecause({ toolchains: { environment: { [name]: 'x' } } }, 'cannot be set by a project', 'a project file cannot set ' + name + ' in toolchains.environment');
|
|
81
|
+
await rejectedBecause({ sandbox: { reserveHostMb: 0 } }, 'sandbox.reserveHostMb is not allowed', 'a project file cannot lower the RAM kept for this PC');
|
|
82
|
+
f.writeLocal(dirC, { toolchains: { environment: { NPM_CONFIG_REGISTRY: 'https://registry.example' } } });
|
|
83
|
+
t.equal((await f.run({ cwd: dirC, trustConfig: true })).config.toolchains.environment.NPM_CONFIG_REGISTRY, 'https://registry.example', 'a registry mirror in toolchains.environment is still fine');
|
|
84
|
+
f.writeLocal(dirC, { provisioning: { copyFromHost: true } });
|
|
85
|
+
t.equal((await f.run({ cwd: dirC, trustConfig: true }, { ...f.baseConfig, provisioning: { ...f.baseConfig.provisioning, copyFromHost: false } })).config.provisioning.copyFromHost, false, 'a project file cannot turn on copying Node, Git and the agent from this PC when you turned it off');
|
|
64
86
|
await rejectedBecause('{\n // a comment\n "readOnly": tru\n}', 'line 3', 'broken JSON is reported with its line');
|
|
65
87
|
await rejectedBecause({ _comment: 'old style' }, '_comment is not allowed', 'unknown keys are rejected');
|
|
66
88
|
f.writeLocal(dirC, '// comments and trailing commas are fine\n{\n "readOnly": true, /* inline */\n}\n');
|
|
@@ -109,6 +131,14 @@ export const localConfigSuite = {
|
|
|
109
131
|
t.check(viaVariable.includes('outside the project') &&
|
|
110
132
|
viaLink.includes('outside the project') &&
|
|
111
133
|
(await f.run({ cwd: dirD, trustConfig: true }, withOutside(false))).config.mappings.some((m) => m.host === path.join(dirD, 'assets')), 'outside means where the folder really is (%VAR% expanded, links followed); a folder inside needs no permission');
|
|
134
|
+
// A second mapping of part of the project must not undo readOnly or the workspace filter.
|
|
135
|
+
fs.mkdirSync(path.join(dirD, 'assets'), { recursive: true });
|
|
136
|
+
f.writeLocal(dirD, { mappings: [{ host: '.', guest: 'C:\\Data', readOnly: false }] });
|
|
137
|
+
const readOnlyUser = { ...f.baseConfig, readOnly: true };
|
|
138
|
+
const remapped = (await f.launch({ cwd: dirD, trustConfig: true }, { config: readOnlyUser })).mappings.find((m) => m.guest.toLowerCase() === 'c:\\data');
|
|
139
|
+
const filtered = await t.thrownMessage(() => f.launch({ cwd: dirD, trustConfig: true }, { config: { ...f.baseConfig, workspaceFilter: { ...f.baseConfig.workspaceFilter, enabled: true } } }));
|
|
140
|
+
t.check(remapped?.readOnly === true && filtered.includes('workspaceFilter.enabled'), 'a project folder mapped again is read-only when the project is, and refused while the workspace filter is on', JSON.stringify(remapped) + ' / ' + filtered);
|
|
141
|
+
fs.rmSync(path.join(dirD, '.sandbox-config.json'));
|
|
112
142
|
t.section('a project can only tighten protections');
|
|
113
143
|
// Settings that would loosen a protection are listed before approval, then ignored.
|
|
114
144
|
f.writeLocal(dirC, {
|
|
@@ -329,7 +329,7 @@ export function renderScreens(dir) {
|
|
|
329
329
|
web: { running: true, port: 47800, address: 'http://127.0.0.1:47800/' },
|
|
330
330
|
more: [
|
|
331
331
|
{ name: 'sync', summary: 'sync history now' },
|
|
332
|
-
{ name: '
|
|
332
|
+
{ name: 'config', summary: 'your settings file' },
|
|
333
333
|
],
|
|
334
334
|
};
|
|
335
335
|
// Two unsaved changes in Settings.
|
|
@@ -624,12 +624,12 @@ export const menuSuite = {
|
|
|
624
624
|
t.section('apps in a few groups');
|
|
625
625
|
const groupIds = appsCategories(user2).map((c) => c.id);
|
|
626
626
|
const appItems = appsItems(user2, t.dir('apps-groups'), new Draft()).filter((i) => i.value !== REQUEST_APP);
|
|
627
|
-
t.check(groupIds.length <=
|
|
628
|
-
appsCategories(ConfigFile.project(t.dir('apps-groups', 'project'))).length <=
|
|
627
|
+
t.check(groupIds.length <= 7 &&
|
|
628
|
+
appsCategories(ConfigFile.project(t.dir('apps-groups', 'project'))).length <= 6 &&
|
|
629
629
|
APP_GROUPS.every((g) => appItems.some((i) => i.category === g.id)) &&
|
|
630
630
|
appItems.every((i) => groupIds.includes(i.category ?? '')) &&
|
|
631
631
|
appItems.length === appRows(user2, t.dir('apps-groups')).length &&
|
|
632
|
-
APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most
|
|
632
|
+
APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most 7 groups (with your own programs); every catalogue category belongs to one and every app is listed', groupIds.join());
|
|
633
633
|
t.section('dialog box');
|
|
634
634
|
const box = dialogBox({ title: 'Unsaved changes', lines: ['You have 2 unsaved changes.'], buttons: 'Save' }, 61);
|
|
635
635
|
const base = Array.from({ length: 12 }, () => 'x'.repeat(60));
|