windows-development-sandbox 1.12.0 → 1.13.2

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.
Files changed (206) hide show
  1. package/README.md +1 -0
  2. package/config.example.json +11 -1
  3. package/dist/app/features/access/access.def.d.ts +190 -0
  4. package/dist/app/features/access/access.def.js +273 -0
  5. package/dist/app/features/access/access.run.d.ts +79 -0
  6. package/dist/app/features/access/access.run.js +129 -0
  7. package/dist/app/features/access/index.d.ts +2 -0
  8. package/dist/app/features/access/index.js +3 -0
  9. package/dist/app/features/accounts/accounts.def.d.ts +1 -0
  10. package/dist/app/features/accounts/accounts.def.js +12 -3
  11. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  12. package/dist/app/features/accounts/accounts.run.js +20 -11
  13. package/dist/app/features/agents/agent-entry.js +5 -0
  14. package/dist/app/features/agents/agents.def.js +1 -0
  15. package/dist/app/features/apps/apps.def.js +1 -0
  16. package/dist/app/features/audit/audit.def.d.ts +11 -2
  17. package/dist/app/features/audit/audit.def.js +7 -4
  18. package/dist/app/features/audit/audit.run.d.ts +3 -0
  19. package/dist/app/features/defs.d.ts +156 -3
  20. package/dist/app/features/defs.js +12 -1
  21. package/dist/app/features/files/files.def.js +1 -0
  22. package/dist/app/features/health/health.def.js +1 -0
  23. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  24. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  25. package/dist/app/features/sessions/sessions.def.js +77 -9
  26. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  27. package/dist/app/features/sessions/sessions.run.js +71 -3
  28. package/dist/app/features/settings/settings-file.d.ts +7 -0
  29. package/dist/app/features/settings/settings-file.js +19 -2
  30. package/dist/app/features/settings/settings-rows.js +9 -1
  31. package/dist/app/features/settings/settings.def.d.ts +8 -0
  32. package/dist/app/features/settings/settings.def.js +17 -0
  33. package/dist/app/features/web/web.def.d.ts +9 -0
  34. package/dist/app/features/web/web.def.js +26 -1
  35. package/dist/app/features/web/web.run.d.ts +3 -0
  36. package/dist/app/features/web/web.run.js +11 -3
  37. package/dist/app/kernel/access.d.ts +33 -0
  38. package/dist/app/kernel/access.js +44 -0
  39. package/dist/app/kernel/define.d.ts +14 -1
  40. package/dist/app/kernel/index.d.ts +1 -0
  41. package/dist/app/kernel/index.js +1 -0
  42. package/dist/app/kernel/rpc.d.ts +5 -2
  43. package/dist/app/registry.d.ts +4 -1
  44. package/dist/app/registry.js +24 -4
  45. package/dist/bin/agent-sandbox.js +7 -1
  46. package/dist/bin/chunk-IL7BXJF2.js +7 -0
  47. package/dist/bin/chunk-K4SU3MHJ.js +10432 -0
  48. package/dist/bin/chunk-VENWSGX2.js +78437 -0
  49. package/dist/bin/claude-host.js +10 -4
  50. package/dist/bin/claude-sbx.js +8 -2
  51. package/dist/bin/codex-sbx.js +8 -2
  52. package/dist/bin/gemini-sbx.js +8 -2
  53. package/dist/bin/sandbox.js +7 -2
  54. package/dist/bin/sbx-hide.js +8 -2
  55. package/dist/bin/sbx-show.js +8 -2
  56. package/dist/cli/commands/launch/prepare.js +24 -7
  57. package/dist/cli/commands/launch/running.js +4 -0
  58. package/dist/cli/commands/registry.js +2 -0
  59. package/dist/cli/commands/remote.d.ts +2 -0
  60. package/dist/cli/commands/remote.js +49 -0
  61. package/dist/cli/commands/sessions/reopen.js +3 -0
  62. package/dist/cli/commands/storage/offer.js +1 -1
  63. package/dist/cli/commands/web.js +66 -0
  64. package/dist/cli/manifest.d.ts +63 -4
  65. package/dist/cli/manifest.js +33 -4
  66. package/dist/cli/menu/io-menu.d.ts +5 -3
  67. package/dist/cli/menu/io-menu.js +12 -5
  68. package/dist/cli/menu/live.d.ts +29 -0
  69. package/dist/cli/menu/live.js +92 -21
  70. package/dist/cli/menu/menu.d.ts +2 -1
  71. package/dist/cli/menu/menu.js +16 -2
  72. package/dist/cli/menu/pages/list-page.d.ts +5 -0
  73. package/dist/cli/menu/pages/list-page.js +19 -0
  74. package/dist/cli/menu/remote-menu.d.ts +19 -0
  75. package/dist/cli/menu/remote-menu.js +108 -0
  76. package/dist/cli/menu/shell.d.ts +2 -0
  77. package/dist/cli/menu/shell.js +1 -1
  78. package/dist/cli/menu/views.js +2 -0
  79. package/dist/cli/operations/handler.js +15 -1
  80. package/dist/cli/services/accounts-service.d.ts +7 -0
  81. package/dist/cli/services/accounts-service.js +20 -0
  82. package/dist/cli/services/remote-client.d.ts +16 -0
  83. package/dist/cli/services/remote-client.js +141 -0
  84. package/dist/cli/services/sessions-service.d.ts +5 -0
  85. package/dist/cli/services/sessions-service.js +17 -1
  86. package/dist/cli/tui/app.d.ts +4 -0
  87. package/dist/cli/tui/app.js +5 -0
  88. package/dist/cli/tui/terminal.d.ts +6 -0
  89. package/dist/cli/tui/terminal.js +8 -1
  90. package/dist/cli/validate.js +8 -4
  91. package/dist/core/config/defaults.js +11 -1
  92. package/dist/core/local-config/file.js +16 -2
  93. package/dist/core/local-config/overrides.js +7 -0
  94. package/dist/core/package-info.js +1 -1
  95. package/dist/core/run/versions.js +9 -8
  96. package/dist/core/schema/config.d.ts +22 -3
  97. package/dist/core/schema/config.js +39 -2
  98. package/dist/core/schema/web.d.ts +51 -0
  99. package/dist/core/schema/web.js +46 -1
  100. package/dist/core/util/fs.d.ts +5 -0
  101. package/dist/core/util/fs.js +23 -0
  102. package/dist/core/util/index.d.ts +1 -1
  103. package/dist/core/util/index.js +1 -1
  104. package/dist/guest/daemon/daemon.js +2 -0
  105. package/dist/guest/daemon/idle-shutdown.js +2 -1
  106. package/dist/guest/daemon/sessions.d.ts +16 -2
  107. package/dist/guest/daemon/sessions.js +126 -20
  108. package/dist/guest/git/decision.js +7 -0
  109. package/dist/guest/sessions/cmdline.d.ts +14 -0
  110. package/dist/guest/sessions/cmdline.js +10 -0
  111. package/dist/guest/toolchains/setup.js +3 -0
  112. package/dist/host/audit/log.d.ts +3 -2
  113. package/dist/host/audit/log.js +3 -3
  114. package/dist/host/channel/channel.d.ts +5 -1
  115. package/dist/host/channel/channel.js +12 -0
  116. package/dist/host/guard/agent-files.js +7 -1
  117. package/dist/host/guard/commands.js +13 -1
  118. package/dist/host/guard/git-config.js +27 -7
  119. package/dist/host/guard/git-dirs.js +78 -3
  120. package/dist/host/guard/mark-of-the-web.js +3 -1
  121. package/dist/host/guard/nested-repos.js +7 -2
  122. package/dist/host/guard/store.js +1 -0
  123. package/dist/host/guard/types.d.ts +2 -0
  124. package/dist/host/launch/mappings.js +11 -1
  125. package/dist/host/launch/project-scan.js +2 -21
  126. package/dist/host/launch/stage.js +4 -0
  127. package/dist/host/life/session-ledger.js +2 -1
  128. package/dist/host/remote/access-store.d.ts +156 -0
  129. package/dist/host/remote/access-store.js +308 -0
  130. package/dist/host/remote/cert.d.ts +20 -0
  131. package/dist/host/remote/cert.js +103 -0
  132. package/dist/host/remote/door-client.d.ts +13 -0
  133. package/dist/host/remote/door-client.js +54 -0
  134. package/dist/host/remote/firewall.d.ts +44 -0
  135. package/dist/host/remote/firewall.js +118 -0
  136. package/dist/host/remote/gate.d.ts +125 -0
  137. package/dist/host/remote/gate.js +391 -0
  138. package/dist/host/remote/index.d.ts +8 -0
  139. package/dist/host/remote/index.js +10 -0
  140. package/dist/host/remote/lan.d.ts +24 -0
  141. package/dist/host/remote/lan.js +88 -0
  142. package/dist/host/remote/tls-store.d.ts +22 -0
  143. package/dist/host/remote/tls-store.js +63 -0
  144. package/dist/host/remote/windows-helper.d.ts +67 -0
  145. package/dist/host/remote/windows-helper.js +129 -0
  146. package/dist/host/sandbox/platform.d.ts +2 -0
  147. package/dist/host/sandbox/platform.js +19 -4
  148. package/dist/host/storage/contained/state.d.ts +3 -2
  149. package/dist/host/storage/contained/state.js +5 -2
  150. package/dist/host/web/bridge.d.ts +5 -1
  151. package/dist/host/web/bridge.js +6 -2
  152. package/dist/host/web/browser.js +1 -1
  153. package/dist/host/web/control.d.ts +4 -1
  154. package/dist/host/web/control.js +28 -4
  155. package/dist/host/web/rpc.d.ts +16 -2
  156. package/dist/host/web/rpc.js +37 -4
  157. package/dist/host/web/runner.d.ts +4 -0
  158. package/dist/host/web/runner.js +12 -0
  159. package/dist/host/web/server.d.ts +18 -2
  160. package/dist/host/web/server.js +97 -24
  161. package/dist/host/web/state.d.ts +5 -0
  162. package/dist/host/web/state.js +8 -0
  163. package/dist/runtime/agent.js +74 -3
  164. package/dist/runtime/daemon.js +205 -24
  165. package/dist/runtime/db.js +74 -3
  166. package/dist/runtime/gitguard.js +6 -0
  167. package/dist/runtime/main.js +77 -3
  168. package/dist/runtime/netproxy.js +74 -3
  169. package/dist/runtime/view.js +76 -3
  170. package/dist/selftest/app.test.js +41 -6
  171. package/dist/selftest/channel.test.js +16 -0
  172. package/dist/selftest/cmdline.test.js +16 -0
  173. package/dist/selftest/git-access.test.js +8 -0
  174. package/dist/selftest/hostile-input.test.js +12 -1
  175. package/dist/selftest/index.js +2 -0
  176. package/dist/selftest/layout.test.js +10 -1
  177. package/dist/selftest/live-daemon.test.js +25 -0
  178. package/dist/selftest/local-config.test.js +30 -0
  179. package/dist/selftest/menu-perf.test.js +198 -12
  180. package/dist/selftest/menu.test.js +4 -0
  181. package/dist/selftest/remote.test.d.ts +2 -0
  182. package/dist/selftest/remote.test.js +536 -0
  183. package/dist/selftest/repo-guard.test.js +56 -2
  184. package/dist/selftest/resume.test.js +2 -1
  185. package/dist/selftest/sharing.test.js +9 -2
  186. package/dist/selftest/support/fixtures.d.ts +13 -0
  187. package/dist/selftest/support/web-preview.js +63 -0
  188. package/dist/selftest/web.test.js +2 -0
  189. package/dist/shared/channel/schema.d.ts +23 -0
  190. package/dist/shared/channel/schema.js +8 -0
  191. package/dist/shared/pty/protocol.d.ts +2 -0
  192. package/dist/shared/pty/protocol.js +2 -0
  193. package/dist/web/index.html +1 -1
  194. package/dist/web/main-NBQ3LF4E.js +221 -0
  195. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  196. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  197. package/native/csharp/HostTools/Program.cs +17 -1
  198. package/native/csharp/HostTools/WebHelper.cs +499 -0
  199. package/native/scripts/get-network-profile.ps1 +19 -0
  200. package/native/scripts/set-folder-access.ps1 +11 -0
  201. package/native/scripts/set-web-firewall.ps1 +103 -0
  202. package/package.json +1 -1
  203. package/sandbox/csharp/PtyHost.cs +20 -0
  204. package/schemas/config.schema.json +65 -0
  205. package/schemas/sandbox-config.schema.json +0 -18
  206. package/dist/web/main-UD4QCXZR.js +0 -219
@@ -28,6 +28,29 @@ export async function fileExistsAsync(p) {
28
28
  return false;
29
29
  }
30
30
  }
31
+ /**
32
+ * A file the sandbox or a repo may have written, read through one handle and only if that handle is a regular file of
33
+ * at most `maxBytes` (null otherwise): a multi-gigabyte .nvmrc must not stop a launch.
34
+ */
35
+ export function readSmallFile(file, maxBytes = 256 * 1024) {
36
+ let fd = null;
37
+ try {
38
+ fd = fs.openSync(file, 'r');
39
+ const stat = fs.fstatSync(fd);
40
+ if (!stat.isFile() || stat.size > maxBytes)
41
+ return null;
42
+ const buffer = Buffer.alloc(stat.size);
43
+ fs.readSync(fd, buffer, 0, stat.size, 0);
44
+ return buffer.toString('utf8');
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ finally {
50
+ if (fd !== null)
51
+ fs.closeSync(fd);
52
+ }
53
+ }
31
54
  export function readJson(p) {
32
55
  return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, ''));
33
56
  }
@@ -1,7 +1,7 @@
1
1
  export { isWin, sleep, captureOutput } from './process.js';
2
2
  export { UserError, fail, errMsg } from './errors.js';
3
3
  export { parseJsonc, readJsonc } from './jsonc.js';
4
- export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, tryReadJson, writeJson, realPath } from './fs.js';
4
+ export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, readSmallFile, tryReadJson, writeJson, realPath, } from './fs.js';
5
5
  export { xmlEscape, safeText, plainText } from './text.js';
6
6
  export { homeDir, expandPath, expandVariables, isRootPath, isUserProfile, isInsideOrEqual, findOnPath, } from './paths.js';
7
7
  export { isLink, removeLink, assertNoLinks } from './links.js';
@@ -1,7 +1,7 @@
1
1
  export { isWin, sleep, captureOutput } from './process.js';
2
2
  export { UserError, fail, errMsg } from './errors.js';
3
3
  export { parseJsonc, readJsonc } from './jsonc.js';
4
- export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, tryReadJson, writeJson, realPath } from './fs.js';
4
+ export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, readSmallFile, tryReadJson, writeJson, realPath, } from './fs.js';
5
5
  export { xmlEscape, safeText, plainText } from './text.js';
6
6
  export { homeDir, expandPath, expandVariables, isRootPath, isUserProfile, isInsideOrEqual, findOnPath, } from './paths.js';
7
7
  export { isLink, removeLink, assertNoLinks } from './links.js';
@@ -54,6 +54,8 @@ export class Daemon {
54
54
  return execCommand(req);
55
55
  case 'restart':
56
56
  return sessions.restart(req);
57
+ case 'close':
58
+ return sessions.close(req);
57
59
  case 'pressure':
58
60
  // No response: the host repeats it every few seconds while its memory stays low.
59
61
  return governor.hostPressure(req.seconds, req.reason, req.level ?? 'slow');
@@ -1,4 +1,5 @@
1
- // A headless sandbox with nothing running (or starting) only wastes RAM, so it shuts itself down.
1
+ // A headless sandbox with nothing running (or starting) only wastes RAM, so it shuts itself down. Every open session
2
+ // counts, a plain shell as much as an agent: whether it is busy cannot be told from outside, and closing it says so.
2
3
  import { spawnSync } from 'node:child_process';
3
4
  import { SYSTEM } from '../../shared/system.js';
4
5
  import { saveClaudeMem } from '../setup/claude-mem.js';
@@ -23,7 +23,10 @@ export declare class SessionManager {
23
23
  roots(): SessionRoot[];
24
24
  private rootStarted;
25
25
  private rootEnded;
26
- /** `session` requests: an agent in its own ptyhost, in the launch folder's project or a registered one. */
26
+ /**
27
+ * `session` requests: an agent (or a plain shell) in its own ptyhost, in the launch folder's project or a registered
28
+ * one.
29
+ */
27
30
  start(req: SessionRequest): Promise<void>;
28
31
  /**
29
32
  * `project` requests: provision a registered project without an agent (sandbox start / exec / db from its folder).
@@ -31,10 +34,21 @@ export declare class SessionManager {
31
34
  prepare(req: Extract<GuestRequest, {
32
35
  kind: 'project';
33
36
  }>): Promise<void>;
34
- /** `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). */
37
+ /**
38
+ * `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). Shells
39
+ * are left alone: restarting one would only throw away what it was doing.
40
+ */
35
41
  restart(req: Extract<GuestRequest, {
36
42
  kind: 'restart';
37
43
  }>): Promise<void>;
44
+ /**
45
+ * `close` requests: the session's ptyhost ends its program and everything it started (ProcessTree), then exits as
46
+ * if the program had. One that does not end in time has its ptyhost ended instead (closing the pseudo console ends
47
+ * what is attached to it).
48
+ */
49
+ close(req: Extract<GuestRequest, {
50
+ kind: 'close';
51
+ }>): Promise<void>;
38
52
  /**
39
53
  * Finds out which of Claude's conversation files this session writes (it appears with the first message) and puts
40
54
  * it in the status, so the host can reopen this very conversation. A reopened session starts with the one it was
@@ -1,6 +1,8 @@
1
- // `session` and `restart` requests: agent sessions, each in its own ptyhost process. A session runs in the launch
2
- // folder's project (the boot environment) or in a project registered later, which is provisioned first
3
- // (daemon/project).
1
+ // `session`, `restart` and `close` requests: agent sessions and plain shell sessions, each in its own ptyhost process.
2
+ // A session runs in the launch folder's project (the boot environment) or in a project registered later, which is
3
+ // provisioned first (daemon/project). A shell gets the same environment as an agent, without the agent's own
4
+ // environment (its keys) and without the agent's install, hooks and conversation tracking.
5
+ import fs from 'node:fs';
4
6
  import path from 'node:path';
5
7
  import crypto from 'node:crypto';
6
8
  import os from 'node:os';
@@ -10,7 +12,8 @@ import { isWithin } from '../../shared/paths/projects.js';
10
12
  import { now } from '../../shared/channel/run-dir.js';
11
13
  import { SYSTEM } from '../../shared/system.js';
12
14
  import { buildJobs, cargoJobs, linkSlotsFor, usableCpus } from '../../shared/memory/index.js';
13
- import { commandLine } from '../sessions/cmdline.js';
15
+ import { commandLine, shellLaunch } from '../sessions/cmdline.js';
16
+ import { pwshDir } from '../toolchains/tools/pwsh.js';
14
17
  import { freePort } from '../runtime/ports.js';
15
18
  import { primaryProject } from '../boot/project-status.js';
16
19
  import { ensurePtyHost, ptyHostLogTail, startPtyHost, waitForPtyHost } from '../sessions/pty-host.js';
@@ -30,7 +33,9 @@ import { isConversationId } from '../../shared/paths/conversation.js';
30
33
  import { errMsg } from '../../core/util/errors.js';
31
34
  import { provisioner } from './project-provisioner.js';
32
35
  import { IdleShutdown } from './idle-shutdown.js';
33
- import { insidePrimary, respond, safely, saveStatus } from './state.js';
36
+ import { insidePrimary, insideWorkspace, respond, safely, saveStatus } from './state.js';
37
+ /** How long a close waits for the session to end before it ends its ptyhost instead. */
38
+ const CLOSE_WAIT_MS = 8000;
34
39
  /** An ended session's ptyhost is remembered this long: its last programs reach the audit up to 15 s later. */
35
40
  const ROOTS_KEPT_MS = 10 * 60 * 1000;
36
41
  function terminalTab(id, title, cwd) {
@@ -95,16 +100,45 @@ function writeBudget(cpus, memoryGb, jobs, env) {
95
100
  warn('budget.json could not be written: ' + errMsg(e));
96
101
  }
97
102
  }
98
- /** Starts ptyhost for the agent and waits until it listens; a failure carries the tail of ptyhost's own log. */
99
- const launchPtyHost = async (req, remote, env, say, sessions, onSpawn) => {
103
+ /** The agent, installed if it is not yet; its environment may hold its API keys. */
104
+ const agentProgram = async (req, env, say) => {
100
105
  const harness = harnessFor(req);
101
106
  // Never refresh here: main already did, and replacing a running agent's executable would fail.
102
107
  say('Preparing ' + req.harness);
103
108
  await agents.provision(req.harness, harness, env, false);
104
109
  const launch = agents.resolveLaunch(req.harness, harness);
105
110
  const base = [...launch.args, ...harness.arguments];
111
+ return {
112
+ command: commandLine(launch.file, [...base, ...req.args]),
113
+ restartCommand: commandLine(launch.file, [...base, ...(harness.resumeArguments ?? [])]),
114
+ environment: harness.environment,
115
+ };
116
+ };
117
+ /** Where the shells are: PowerShell 7 from the toolchains (or an installed copy), Windows' own PowerShell and cmd. */
118
+ function shellPaths() {
119
+ const candidates = [
120
+ path.join(pwshDir(), 'pwsh.exe'),
121
+ path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'PowerShell', '7', 'pwsh.exe'),
122
+ ];
123
+ return { pwsh: candidates.find((f) => fs.existsSync(f)) ?? null, powershell: SYSTEM.powershell(), cmd: SYSTEM.cmd() };
124
+ }
125
+ /** A plain shell: no install, no arguments of the request, nothing of the agent's environment. */
126
+ const shellProgram = (shell) => {
127
+ const line = commandLine(shell.file, shell.args);
128
+ return { command: line, restartCommand: line, environment: {} };
129
+ };
130
+ /** Starts ptyhost for the program and waits until it listens; a failure carries the tail of ptyhost's own log. */
131
+ const launchPtyHost = async (req, program, remote, env, sessions, onSpawn) => {
106
132
  const port = await freePort();
107
133
  const token = crypto.randomBytes(24).toString('base64url');
134
+ const sessionEnv = {
135
+ ...withRegistryPath(env),
136
+ ...program.environment,
137
+ ...memoryEnv(env, sessions),
138
+ };
139
+ // Each PowerShell builds its own module path; one inherited from another PowerShell breaks its modules.
140
+ if (req.shell)
141
+ delete sessionEnv.PSModulePath;
108
142
  const child = startPtyHost({
109
143
  exe: ensurePtyHost(),
110
144
  port,
@@ -113,10 +147,10 @@ const launchPtyHost = async (req, remote, env, say, sessions, onSpawn) => {
113
147
  rows: 32,
114
148
  id: req.id,
115
149
  cwd: req.cwd,
116
- env: { ...withRegistryPath(env), ...harness.environment, ...memoryEnv(env, sessions) },
150
+ env: sessionEnv,
117
151
  remote,
118
- command: commandLine(launch.file, [...base, ...req.args]),
119
- restartCommand: commandLine(launch.file, [...base, ...(harness.resumeArguments ?? [])]),
152
+ command: program.command,
153
+ restartCommand: program.restartCommand,
120
154
  });
121
155
  onSpawn(child);
122
156
  try {
@@ -134,6 +168,16 @@ const launchPtyHost = async (req, remote, env, say, sessions, onSpawn) => {
134
168
  /** The launch folder's project, or the registered project this session is for (provisioned first if needed). */
135
169
  const placeFor = async (req, say) => {
136
170
  if (!req.project) {
171
+ // A project set up earlier (the web UI and sandbox shell name it by its folder only): its own environment.
172
+ const known = insidePrimary(req.cwd) ? null : provisioner.provisionedFor(req.cwd);
173
+ if (known && exists(req.cwd))
174
+ return {
175
+ env: known.env,
176
+ hooks: known.spec.hooks ? projectHooks(known.spec.name, known.spec.workspace) : null,
177
+ project: known.spec.name,
178
+ };
179
+ if (!insidePrimary(req.cwd) && insideWorkspace(req.cwd))
180
+ throw new Error('The project in ' + req.cwd + ' is not set up in the sandbox yet: run sandbox start in its folder first.');
137
181
  if (!insidePrimary(req.cwd) || !exists(req.cwd))
138
182
  throw new Error('Folder is not inside the sandbox workspace ' + workspace + ': ' + req.cwd);
139
183
  return { env: process.env, hooks: null, project: primaryProject() };
@@ -187,16 +231,31 @@ export class SessionManager {
187
231
  if (r && r.to === undefined)
188
232
  this.ptyHosts.set(id, { ...r, to: Date.now() });
189
233
  }
190
- /** `session` requests: an agent in its own ptyhost, in the launch folder's project or a registered one. */
234
+ /**
235
+ * `session` requests: an agent (or a plain shell) in its own ptyhost, in the launch folder's project or a registered
236
+ * one.
237
+ */
191
238
  async start(req) {
239
+ let shell = null;
240
+ let problem = null;
241
+ if (req.shell)
242
+ try {
243
+ shell = shellLaunch(req.shell, shellPaths());
244
+ }
245
+ catch (e) {
246
+ problem = errMsg(e);
247
+ }
248
+ // A shell is listed by its own name (pwsh, powershell, cmd).
249
+ const name = shell?.name ?? (req.shell ? 'shell' : req.harness);
192
250
  let status = {
193
251
  id: req.id,
194
252
  launchId: session.launchId,
195
- harness: req.harness,
253
+ harness: name,
254
+ ...(req.shell ? { kind: 'shell' } : {}),
196
255
  cwd: req.cwd,
197
256
  ...(req.project ? { project: req.project.name } : {}),
198
257
  phase: 'starting',
199
- message: 'Starting ' + req.harness,
258
+ message: 'Starting ' + name,
200
259
  updatedAt: now(),
201
260
  startedAt: now(),
202
261
  };
@@ -209,30 +268,34 @@ export class SessionManager {
209
268
  this.starting++;
210
269
  // Sessions after the first get their own tab right away (it shows the setup progress until the agent starts).
211
270
  if (req.id !== session.launchId && process.env.AGENT_NO_TABS !== '1')
212
- safely('terminal tab', () => terminalTab(req.id, req.harness + ' ' + path.basename(req.cwd), req.cwd));
271
+ safely('terminal tab', () => terminalTab(req.id, name + ' ' + path.basename(req.cwd), req.cwd));
213
272
  try {
273
+ if (problem)
274
+ throw new Error(problem);
214
275
  const place = await placeFor(req, say);
276
+ const program = shell ? shellProgram(shell) : await agentProgram(req, place.env, say);
215
277
  const remote = session.networking && session.attach.enabled;
216
278
  const from = Date.now();
217
- const { child, port, token } = await launchPtyHost(req, remote, place.env, say, this.live.size + 1, (c) => this.rootStarted(req.id, c, from));
218
- const resumed = req.harness === 'claude' ? resumedConversation(req.args) : undefined;
279
+ const { child, port, token } = await launchPtyHost(req, program, remote, place.env, this.live.size + 1, (c) => this.rootStarted(req.id, c, from));
280
+ const agent = shell === null;
281
+ const resumed = agent && req.harness === 'claude' ? resumedConversation(req.args) : undefined;
219
282
  const running = {
220
283
  ...status,
221
284
  ...(resumed ? { conversation: resumed } : {}),
222
285
  project: place.project,
223
286
  phase: 'running',
224
- message: req.harness + ' is running',
287
+ message: name + ' is running',
225
288
  attach: { addresses: remote ? guestAddresses() : [], port, token },
226
289
  };
227
290
  this.live.set(req.id, { status: running, child });
228
291
  child.once('exit', (code) => this.onExit(running, place, code));
229
292
  saveStatus(running);
230
- if (req.harness === 'claude')
293
+ if (agent && req.harness === 'claude')
231
294
  this.trackConversation(running.id, resumed);
232
295
  log('Session ' +
233
296
  req.id +
234
297
  ' started (' +
235
- req.harness +
298
+ name +
236
299
  ' in ' +
237
300
  req.cwd +
238
301
  ', project ' +
@@ -271,15 +334,53 @@ export class SessionManager {
271
334
  this.idle.check();
272
335
  }
273
336
  }
274
- /** `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). */
337
+ /**
338
+ * `restart` requests: the matching sessions restart their agent (continuing the conversation where it can). Shells
339
+ * are left alone: restarting one would only throw away what it was doing.
340
+ */
275
341
  async restart(req) {
276
342
  const targets = [...this.live.values()].filter((s) => s.status.attach &&
343
+ s.status.kind !== 'shell' &&
277
344
  (req.harness === null || s.status.harness === req.harness) &&
278
345
  (!req.cwd || isWithin(req.cwd, s.status.cwd)));
279
346
  for (const s of targets)
280
347
  await sendControl({ addresses: ['127.0.0.1'], port: s.status.attach.port, token: s.status.attach.token }, FrameType.Restart);
281
348
  respond(req.id, { ok: true, message: 'restarted ' + targets.length + ' session(s)' });
282
349
  }
350
+ /**
351
+ * `close` requests: the session's ptyhost ends its program and everything it started (ProcessTree), then exits as
352
+ * if the program had. One that does not end in time has its ptyhost ended instead (closing the pseudo console ends
353
+ * what is attached to it).
354
+ */
355
+ async close(req) {
356
+ const entry = this.live.get(req.session);
357
+ if (!entry?.status.attach) {
358
+ respond(req.id, { ok: false, message: 'Session ' + req.session + ' is not running.' });
359
+ return;
360
+ }
361
+ const { child, status } = entry;
362
+ const ended = new Promise((resolve) => {
363
+ if (child.exitCode !== null || child.signalCode !== null)
364
+ return resolve(true);
365
+ const timer = setTimeout(() => resolve(false), CLOSE_WAIT_MS);
366
+ child.once('exit', () => {
367
+ clearTimeout(timer);
368
+ resolve(true);
369
+ });
370
+ });
371
+ try {
372
+ await sendControl({ addresses: ['127.0.0.1'], port: status.attach.port, token: status.attach.token }, FrameType.Close);
373
+ }
374
+ catch (e) {
375
+ warn('Session ' + req.session + ' did not take the close request: ' + errMsg(e));
376
+ }
377
+ if (!(await ended)) {
378
+ warn('Session ' + req.session + ' did not end in time; ending its session host.');
379
+ safely('ending a session host', () => child.kill());
380
+ }
381
+ log('Session ' + req.session + ' closed on request');
382
+ respond(req.id, { ok: true, message: 'Closed ' + status.harness + ' session ' + req.session + '.' });
383
+ }
283
384
  /**
284
385
  * Finds out which of Claude's conversation files this session writes (it appears with the first message) and puts
285
386
  * it in the status, so the host can reopen this very conversation. A reopened session starts with the one it was
@@ -324,6 +425,11 @@ export class SessionManager {
324
425
  attach: undefined,
325
426
  });
326
427
  log('Session ' + running.id + ' ended (exit ' + code + ')');
428
+ // A shell ran no agent: no post-agent hooks, nothing of an agent's to save.
429
+ if (running.kind === 'shell') {
430
+ this.idle.check();
431
+ return;
432
+ }
327
433
  if (place.hooks)
328
434
  void runHooksAsync('post-agent', place.env, place.hooks).catch((e) => warn('post-agent hooks failed: ' + errMsg(e)));
329
435
  else
@@ -57,6 +57,8 @@ const LISTING = {
57
57
  /** Global options that take a value (`git -C dir status`). Config overrides (-c, --config-env) are refused outright. */
58
58
  const WITH_VALUE = new Set(['-C', '--namespace']);
59
59
  const FORBIDDEN_GLOBAL = /^(-c|--config-env|--exec-path|--git-dir|--work-tree|--super-prefix)(=|$)/;
60
+ /** Options of reading commands that run a program or write a file (grep -O<cmd>, ls-remote --upload-pack, --output). */
61
+ const RUNS_OR_WRITES = /^(--open-files-in-pager|--upload-pack|--output|--ext-diff)(=|$)/;
60
62
  export function gitDecision(args, access) {
61
63
  if (access === 'full')
62
64
  return { allow: true };
@@ -72,6 +74,11 @@ export function gitDecision(args, access) {
72
74
  i += WITH_VALUE.has(args[i]) ? 2 : 1;
73
75
  }
74
76
  const [command, ...rest] = args.slice(i);
77
+ const runsOrWrites = rest.find((a) => RUNS_OR_WRITES.test(a) ||
78
+ (command === 'ls-remote' && /^-u(.|$)/.test(a)) ||
79
+ (command === 'grep' && /^-O/.test(a)));
80
+ if (runsOrWrites)
81
+ return { allow: false, reason: 'git ' + runsOrWrites + ' is not allowed: git is read-only in this sandbox.' };
75
82
  if (command === undefined || command === '--version' || READ_ONLY.has(command) || (LISTING[command]?.(rest) ?? false))
76
83
  return { allow: true };
77
84
  return {
@@ -10,3 +10,17 @@ export declare function quoteArg(arg: string): string;
10
10
  export declare function cmdArgs(args: string[]): string;
11
11
  /** The command line ptyhost starts; when the program is cmd.exe its arguments are quoted for cmd (cmdArgs). */
12
12
  export declare function commandLine(file: string, args: string[]): string;
13
+ /** A plain shell session's program: which shell, its absolute path and its arguments. */
14
+ export interface ShellLaunch {
15
+ readonly name: 'pwsh' | 'powershell' | 'cmd';
16
+ readonly file: string;
17
+ readonly args: string[];
18
+ }
19
+ /** Where the shells are in this sandbox (pwsh: null when PowerShell 7 is not installed). */
20
+ export interface ShellPaths {
21
+ readonly pwsh: string | null;
22
+ readonly powershell: string;
23
+ readonly cmd: string;
24
+ }
25
+ /** The shell a session asked for runs as (auto: PowerShell 7 when present, else Windows PowerShell). Pure. */
26
+ export declare function shellLaunch(asked: 'auto' | ShellLaunch['name'], paths: ShellPaths): ShellLaunch;
@@ -31,3 +31,13 @@ export function commandLine(file, args) {
31
31
  ? [quoteArg(file), cmdArgs(args)].filter(Boolean).join(' ')
32
32
  : [file, ...args].map(quoteArg).join(' ');
33
33
  }
34
+ /** The shell a session asked for runs as (auto: PowerShell 7 when present, else Windows PowerShell). Pure. */
35
+ export function shellLaunch(asked, paths) {
36
+ if (asked === 'cmd')
37
+ return { name: 'cmd', file: paths.cmd, args: [] };
38
+ if (asked === 'powershell' || (asked === 'auto' && !paths.pwsh))
39
+ return { name: 'powershell', file: paths.powershell, args: ['-NoLogo'] };
40
+ if (!paths.pwsh)
41
+ throw new Error('PowerShell 7 is not installed in this sandbox (toolchains.pwsh). Open powershell or cmd instead, or turn pwsh on.');
42
+ return { name: 'pwsh', file: paths.pwsh, args: ['-NoLogo'] };
43
+ }
@@ -17,6 +17,9 @@ export async function setupToolchains(config, versions, options, run = {}) {
17
17
  env.summary.push(...part.summary);
18
18
  }
19
19
  Object.assign(env.vars, config.environment);
20
+ // The git guard's variables (AGENT_GIT_ACCESS, GIT_DIR, GIT_CONFIG_*, BASH_ENV) always win over
21
+ // toolchains.environment.
22
+ Object.assign(env.vars, envs.get('Git')?.vars);
20
23
  log('Set up ' + steps.length + ' toolchains and databases.');
21
24
  return env;
22
25
  }
@@ -1,16 +1,17 @@
1
1
  import { z } from 'zod';
2
- export declare const AUDIT_KINDS: readonly ["proc", "file", "commit", "session", "guard", "note"];
2
+ export declare const AUDIT_KINDS: readonly ["proc", "file", "commit", "session", "guard", "note", "remote"];
3
3
  export type AuditKind = (typeof AUDIT_KINDS)[number];
4
4
  /** A session id as the sandbox's run folder names them. */
5
5
  export declare const AuditSessionIdSchema: z.ZodString;
6
6
  /**
7
7
  * One event. k: proc (a program started), file (changed in a project), commit (a git commit in a project), session,
8
- * guard (repo guard), note.
8
+ * guard (repo guard), note, remote (what another Windows user or a paired device did in the web UI).
9
9
  */
10
10
  export declare const AuditEventSchema: z.ZodObject<{
11
11
  t: z.ZodString;
12
12
  k: z.ZodEnum<{
13
13
  file: "file";
14
+ remote: "remote";
14
15
  session: "session";
15
16
  note: "note";
16
17
  commit: "commit";
@@ -6,12 +6,12 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import zlib from 'node:zlib';
8
8
  import { z } from 'zod';
9
- export const AUDIT_KINDS = ['proc', 'file', 'commit', 'session', 'guard', 'note'];
9
+ export const AUDIT_KINDS = ['proc', 'file', 'commit', 'session', 'guard', 'note', 'remote'];
10
10
  /** A session id as the sandbox's run folder names them. */
11
11
  export const AuditSessionIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
12
12
  /**
13
13
  * One event. k: proc (a program started), file (changed in a project), commit (a git commit in a project), session,
14
- * guard (repo guard), note.
14
+ * guard (repo guard), note, remote (what another Windows user or a paired device did in the web UI).
15
15
  */
16
16
  export const AuditEventSchema = z.object({
17
17
  /** ISO time. */
@@ -44,7 +44,7 @@ export const AuditEventSchema = z.object({
44
44
  .string()
45
45
  .regex(/^[0-9a-f]{7,64}$/)
46
46
  .optional(),
47
- /** commit: who git says made it ("Name <email>"). */
47
+ /** commit: who git says made it ("Name <email>"); remote: who did it ("device Josh's phone"). */
48
48
  au: z.string().max(256).optional(),
49
49
  });
50
50
  /** Each text field's limit (the schema's): text is cut to it before writing, so a line is never refused on reading. */
@@ -1,5 +1,5 @@
1
1
  import type { GuestRequest, GuestResponse, Harness, SandboxStatus, SessionStatus } from '../../core/types.js';
2
- import type { ProjectSpec } from '../../shared/channel/index.js';
2
+ import type { ProjectSpec, ShellChoice } from '../../shared/channel/index.js';
3
3
  /**
4
4
  * The boot script writes its first status once Windows has booted the sandbox and logged on (a minute or two, longer on
5
5
  * a slow PC or the very first start); none after this long means it never got going.
@@ -46,6 +46,10 @@ export declare class SandboxChannel {
46
46
  exec(command: string[], cwd: string, timeoutSeconds: number): Promise<GuestResponse>;
47
47
  /** Restarts running sessions in place (they resume via the harness's resumeArguments, e.g. claude --continue). */
48
48
  restart(harness: string | null, cwd?: string): Promise<GuestResponse>;
49
+ /** Asks for a plain shell session in a folder of the sandbox; returns the request id (waitForSession follows it). */
50
+ requestShell(shell: ShellChoice, cwd: string): string;
51
+ /** Ends a running session (agent or shell) and everything it started; waits for the sandbox's answer. */
52
+ closeSession(sessionId: string): Promise<GuestResponse>;
49
53
  /** The PC is low on memory or CPU: the sandbox slows its builds down for `seconds`. Never waits for an answer. */
50
54
  pressure(reason: string, seconds: number, level?: 'slow' | 'severe'): void;
51
55
  ping(): Promise<GuestResponse>;
@@ -179,6 +179,18 @@ export class SandboxChannel {
179
179
  this.send({ kind: 'restart', id: requestId, harness, ...(cwd ? { cwd } : {}), createdAt: now() });
180
180
  return this.waitForResponse(requestId, 30000);
181
181
  }
182
+ /** Asks for a plain shell session in a folder of the sandbox; returns the request id (waitForSession follows it). */
183
+ requestShell(shell, cwd) {
184
+ const requestId = newId();
185
+ this.send({ kind: 'session', id: requestId, harness: 'shell', args: [], cwd, shell, createdAt: now() });
186
+ return requestId;
187
+ }
188
+ /** Ends a running session (agent or shell) and everything it started; waits for the sandbox's answer. */
189
+ async closeSession(sessionId) {
190
+ const requestId = newId();
191
+ this.send({ kind: 'close', id: requestId, session: sessionId, createdAt: now() });
192
+ return this.waitForResponse(requestId, 30000);
193
+ }
182
194
  /** The PC is low on memory or CPU: the sandbox slows its builds down for `seconds`. Never waits for an answer. */
183
195
  pressure(reason, seconds, level = 'slow') {
184
196
  this.send({ kind: 'pressure', id: newId(), reason, seconds, level, createdAt: now() });
@@ -17,8 +17,11 @@ const CLAUDE_RISKY_KEYS = [
17
17
  'enableAllProjectMcpServers',
18
18
  'enabledMcpjsonServers',
19
19
  'disableAllHooks',
20
+ // Plugins bring their own hooks and MCP servers.
21
+ 'enabledPlugins',
22
+ 'extraKnownMarketplaces',
20
23
  ];
21
- const VSCODE_RISKY_KEY = /path|executable|command|interpreter|shell|profile|args|env|exec|program|server|launch|runtime|binary/i;
24
+ const VSCODE_RISKY_KEY = /path|executable|command|interpreter|shell|profile|args|env|exec|program|server|launch|runtime|binary|alternatetools|home|goroot|tsdk|sdk/i;
22
25
  const MAX_AGENT_FILE_BYTES = 1024 * 1024;
23
26
  function claudeRisk(d) {
24
27
  const s = (_.isPlainObject(d) ? d : {});
@@ -26,6 +29,9 @@ function claudeRisk(d) {
26
29
  return {
27
30
  ..._.pick(s, CLAUDE_RISKY_KEYS),
28
31
  permissions: _.pick(permissions, ['defaultMode', 'additionalDirectories']),
32
+ // A whole-tool rule ("Bash", "Bash(*)") lets Claude Code on this PC run any command without asking; narrow rules
33
+ // (Bash(npm test), what "always allow" writes) are ordinary edits.
34
+ broadAllow: (Array.isArray(permissions.allow) ? permissions.allow : []).filter((r) => typeof r !== 'string' || !r.includes('(') || /\(\s*:?\*\s*\)\s*$/.test(r)),
29
35
  };
30
36
  }
31
37
  /**
@@ -1,5 +1,15 @@
1
1
  import { filesIn } from './file-hash.js';
2
2
  import { quarantine } from './quarantine.js';
3
+ /** What cmd runs from the current folder: a fixed list plus this PC's PATHEXT (the Python installer adds .PY, .PYW). */
4
+ const EXTENSIONS = [
5
+ ...new Set([
6
+ ...['exe', 'com', 'bat', 'cmd', 'vbs', 'vbe', 'js', 'jse', 'wsf', 'wsh', 'msc', 'ps1', 'cpl'],
7
+ ...(process.env.PATHEXT ?? '')
8
+ .split(';')
9
+ .map((e) => e.replace(/^\./, '').toLowerCase())
10
+ .filter((e) => /^[a-z0-9]{1,8}$/.test(e)),
11
+ ]),
12
+ ];
3
13
  /** Names you are likely to run in a project folder; with any of these extensions Windows would pick the local copy. */
4
14
  export const COMMANDS = new RegExp('^(' +
5
15
  [
@@ -108,7 +118,9 @@ export const COMMANDS = new RegExp('^(' +
108
118
  'ssh',
109
119
  'scp',
110
120
  ].join('|') +
111
- ')\\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|ps1)$', 'i');
121
+ ')\\.(' +
122
+ EXTENSIONS.join('|') +
123
+ ')$', 'i');
112
124
  /** Fingerprints of the files in the project root named like one of COMMANDS. */
113
125
  export function plantedCommands(workspace) {
114
126
  return filesIn(workspace, (n) => COMMANDS.test(n));
@@ -5,13 +5,28 @@ import _ from 'lodash';
5
5
  import { writeFileAtomic } from '../../shared/io/index.js';
6
6
  import { fileExists, isLink } from '../../core/util/index.js';
7
7
  /** Config keys that make git run a program, or load more config. */
8
- const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool|external|difffilter|defaultkeycommand|alternaterefscommand)\s*=/i;
9
- const SECTION = /^\s*\[([^\]]*)\]\s*/;
8
+ const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|tocmd|cccmd|smtpserver|httpd|vcs|packobjectshook|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool|external|difffilter|defaultkeycommand|alternaterefscommand)\s*(=|$)/i;
9
+ /**
10
+ * A section header as git reads it: a name (the old [section.sub] form included), then an optional quoted subsection
11
+ * in which `]` and escaped quotes are allowed ([filter "a]b"] must not hide the key after it).
12
+ */
13
+ const SECTION = /^\s*\[\s*([A-Za-z0-9.-]+)(\s+"(?:[^"\\]|\\.)*")?\s*\]\s*/;
14
+ /** Sections where any key is risky: [protocol] turns on transports that run commands (ext::), [url] rewrites URLs. */
15
+ const RISKY_SECTIONS = new Set(['pager', 'protocol', 'url']);
10
16
  const MARK = '# [agent-sandbox] disabled, added inside the sandbox: ';
11
17
  const MAX_CONFIG_BYTES = 4 * 1024 * 1024;
12
- /** Sections where any key runs a program ([pager] log = x), or where a "!" value does ([alias] st = !x). */
18
+ /**
19
+ * Sections where any key runs a program ([pager] log = x), where a "!" value does ([alias] st = !x, also quoted or on a
20
+ * continued line: every new alias line with a "!" counts), or a "!" submodule update command.
21
+ */
13
22
  function dangerousInSection(section, keyPart) {
14
- return (section === 'pager' && keyPart.includes('=')) || (section === 'alias' && /^[^=]+=\s*"?\s*!/.test(keyPart));
23
+ if (keyPart === '' || /^[#;]/.test(keyPart))
24
+ return false;
25
+ if (RISKY_SECTIONS.has(section))
26
+ return true;
27
+ if (section === 'alias')
28
+ return keyPart.includes('!');
29
+ return section === 'submodule' && /^update\s*=/i.test(keyPart) && keyPart.includes('!');
15
30
  }
16
31
  /** The lines of <dir>\config (none for a link or a file over 4 MB). */
17
32
  export function configLines(dir) {
@@ -30,16 +45,21 @@ export function disableConfig(dir, before) {
30
45
  let section = '';
31
46
  const fixed = lines.map((line) => {
32
47
  const header = SECTION.exec(line);
48
+ // A line that starts like a header but is not one git would accept: treat it as unknown (risky) below.
49
+ const badHeader = !header && /^\s*\[/.test(line);
33
50
  if (header) {
34
- section = header[1].trim().toLowerCase().split(/\s/)[0];
35
- inInclude = /^include(if\s+"[^"]*")?$/i.test(header[1].trim());
51
+ section = header[1].toLowerCase().split('.')[0];
52
+ inInclude = section === 'include' || section === 'includeif';
36
53
  }
37
54
  seen[line] = (seen[line] ?? 0) + 1;
38
55
  if (line.startsWith(MARK) || seen[line] <= (known[line] ?? 0))
39
56
  return line;
40
57
  // git also accepts "[core] fsmonitor = x" on one line, so look past a leading section header.
41
58
  const keyPart = line.replace(SECTION, '').trim();
42
- if (!(DANGEROUS_KEY.test(keyPart) || (inInclude && keyPart.includes('=')) || dangerousInSection(section, keyPart)))
59
+ if (!(badHeader ||
60
+ DANGEROUS_KEY.test(keyPart) ||
61
+ (inInclude && keyPart.includes('=')) ||
62
+ dangerousInSection(section, keyPart)))
43
63
  return line;
44
64
  findings.push({ kind: 'config', detail: line.trim() });
45
65
  return MARK + line.trim();