windows-development-sandbox 1.12.0 → 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.
Files changed (160) 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/agents/agent-entry.js +5 -0
  10. package/dist/app/features/agents/agents.def.js +1 -0
  11. package/dist/app/features/apps/apps.def.js +1 -0
  12. package/dist/app/features/audit/audit.def.d.ts +11 -2
  13. package/dist/app/features/audit/audit.def.js +7 -4
  14. package/dist/app/features/audit/audit.run.d.ts +3 -0
  15. package/dist/app/features/defs.d.ts +155 -3
  16. package/dist/app/features/defs.js +12 -1
  17. package/dist/app/features/files/files.def.js +1 -0
  18. package/dist/app/features/health/health.def.js +1 -0
  19. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  20. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  21. package/dist/app/features/sessions/sessions.def.js +77 -9
  22. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  23. package/dist/app/features/sessions/sessions.run.js +71 -3
  24. package/dist/app/features/settings/settings.def.d.ts +8 -0
  25. package/dist/app/features/settings/settings.def.js +17 -0
  26. package/dist/app/features/web/web.def.d.ts +9 -0
  27. package/dist/app/features/web/web.def.js +26 -1
  28. package/dist/app/features/web/web.run.d.ts +3 -0
  29. package/dist/app/features/web/web.run.js +11 -3
  30. package/dist/app/kernel/access.d.ts +33 -0
  31. package/dist/app/kernel/access.js +44 -0
  32. package/dist/app/kernel/define.d.ts +14 -1
  33. package/dist/app/kernel/index.d.ts +1 -0
  34. package/dist/app/kernel/index.js +1 -0
  35. package/dist/app/kernel/rpc.d.ts +5 -2
  36. package/dist/app/registry.d.ts +4 -1
  37. package/dist/app/registry.js +24 -4
  38. package/dist/cli/commands/registry.js +2 -0
  39. package/dist/cli/commands/remote.d.ts +2 -0
  40. package/dist/cli/commands/remote.js +49 -0
  41. package/dist/cli/commands/sessions/reopen.js +3 -0
  42. package/dist/cli/commands/web.js +66 -0
  43. package/dist/cli/manifest.d.ts +63 -4
  44. package/dist/cli/manifest.js +33 -4
  45. package/dist/cli/menu/io-menu.d.ts +5 -3
  46. package/dist/cli/menu/io-menu.js +12 -5
  47. package/dist/cli/menu/remote-menu.d.ts +19 -0
  48. package/dist/cli/menu/remote-menu.js +108 -0
  49. package/dist/cli/menu/views.js +2 -0
  50. package/dist/cli/operations/handler.js +15 -1
  51. package/dist/cli/services/remote-client.d.ts +16 -0
  52. package/dist/cli/services/remote-client.js +141 -0
  53. package/dist/cli/services/sessions-service.d.ts +5 -0
  54. package/dist/cli/services/sessions-service.js +17 -1
  55. package/dist/core/config/defaults.js +11 -1
  56. package/dist/core/local-config/file.js +16 -2
  57. package/dist/core/local-config/overrides.js +7 -0
  58. package/dist/core/run/versions.js +9 -8
  59. package/dist/core/schema/config.d.ts +22 -3
  60. package/dist/core/schema/config.js +39 -2
  61. package/dist/core/schema/web.d.ts +51 -0
  62. package/dist/core/schema/web.js +46 -1
  63. package/dist/core/util/fs.d.ts +5 -0
  64. package/dist/core/util/fs.js +23 -0
  65. package/dist/core/util/index.d.ts +1 -1
  66. package/dist/core/util/index.js +1 -1
  67. package/dist/guest/daemon/daemon.js +2 -0
  68. package/dist/guest/daemon/idle-shutdown.js +2 -1
  69. package/dist/guest/daemon/sessions.d.ts +16 -2
  70. package/dist/guest/daemon/sessions.js +126 -20
  71. package/dist/guest/git/decision.js +7 -0
  72. package/dist/guest/sessions/cmdline.d.ts +14 -0
  73. package/dist/guest/sessions/cmdline.js +10 -0
  74. package/dist/guest/toolchains/setup.js +3 -0
  75. package/dist/host/audit/log.d.ts +3 -2
  76. package/dist/host/audit/log.js +3 -3
  77. package/dist/host/channel/channel.d.ts +5 -1
  78. package/dist/host/channel/channel.js +12 -0
  79. package/dist/host/guard/agent-files.js +7 -1
  80. package/dist/host/guard/commands.js +13 -1
  81. package/dist/host/guard/git-config.js +27 -7
  82. package/dist/host/guard/git-dirs.js +78 -3
  83. package/dist/host/guard/mark-of-the-web.js +3 -1
  84. package/dist/host/guard/nested-repos.js +7 -2
  85. package/dist/host/guard/store.js +1 -0
  86. package/dist/host/guard/types.d.ts +2 -0
  87. package/dist/host/launch/mappings.js +11 -1
  88. package/dist/host/launch/project-scan.js +2 -21
  89. package/dist/host/launch/stage.js +4 -0
  90. package/dist/host/life/session-ledger.js +2 -1
  91. package/dist/host/remote/access-store.d.ts +156 -0
  92. package/dist/host/remote/access-store.js +308 -0
  93. package/dist/host/remote/cert.d.ts +20 -0
  94. package/dist/host/remote/cert.js +103 -0
  95. package/dist/host/remote/door-client.d.ts +13 -0
  96. package/dist/host/remote/door-client.js +54 -0
  97. package/dist/host/remote/firewall.d.ts +44 -0
  98. package/dist/host/remote/firewall.js +118 -0
  99. package/dist/host/remote/gate.d.ts +125 -0
  100. package/dist/host/remote/gate.js +391 -0
  101. package/dist/host/remote/index.d.ts +8 -0
  102. package/dist/host/remote/index.js +10 -0
  103. package/dist/host/remote/lan.d.ts +24 -0
  104. package/dist/host/remote/lan.js +88 -0
  105. package/dist/host/remote/tls-store.d.ts +22 -0
  106. package/dist/host/remote/tls-store.js +63 -0
  107. package/dist/host/remote/windows-helper.d.ts +67 -0
  108. package/dist/host/remote/windows-helper.js +129 -0
  109. package/dist/host/web/bridge.d.ts +5 -1
  110. package/dist/host/web/bridge.js +6 -2
  111. package/dist/host/web/browser.js +1 -1
  112. package/dist/host/web/control.d.ts +4 -1
  113. package/dist/host/web/control.js +28 -4
  114. package/dist/host/web/rpc.d.ts +16 -2
  115. package/dist/host/web/rpc.js +37 -4
  116. package/dist/host/web/runner.d.ts +4 -0
  117. package/dist/host/web/runner.js +12 -0
  118. package/dist/host/web/server.d.ts +18 -2
  119. package/dist/host/web/server.js +97 -24
  120. package/dist/host/web/state.d.ts +5 -0
  121. package/dist/host/web/state.js +8 -0
  122. package/dist/runtime/agent.js +74 -3
  123. package/dist/runtime/daemon.js +205 -24
  124. package/dist/runtime/db.js +74 -3
  125. package/dist/runtime/gitguard.js +6 -0
  126. package/dist/runtime/main.js +77 -3
  127. package/dist/runtime/netproxy.js +74 -3
  128. package/dist/runtime/view.js +76 -3
  129. package/dist/selftest/app.test.js +41 -6
  130. package/dist/selftest/channel.test.js +16 -0
  131. package/dist/selftest/cmdline.test.js +16 -0
  132. package/dist/selftest/git-access.test.js +8 -0
  133. package/dist/selftest/hostile-input.test.js +12 -1
  134. package/dist/selftest/index.js +2 -0
  135. package/dist/selftest/live-daemon.test.js +25 -0
  136. package/dist/selftest/local-config.test.js +30 -0
  137. package/dist/selftest/remote.test.d.ts +2 -0
  138. package/dist/selftest/remote.test.js +536 -0
  139. package/dist/selftest/repo-guard.test.js +56 -2
  140. package/dist/selftest/resume.test.js +2 -1
  141. package/dist/selftest/sharing.test.js +9 -2
  142. package/dist/selftest/support/fixtures.d.ts +13 -0
  143. package/dist/selftest/support/web-preview.js +63 -0
  144. package/dist/selftest/web.test.js +2 -0
  145. package/dist/shared/channel/schema.d.ts +23 -0
  146. package/dist/shared/channel/schema.js +8 -0
  147. package/dist/shared/pty/protocol.d.ts +2 -0
  148. package/dist/shared/pty/protocol.js +2 -0
  149. package/dist/web/index.html +1 -1
  150. package/dist/web/main-Z2WYAE2Z.js +221 -0
  151. package/native/csharp/HostTools/Program.cs +17 -1
  152. package/native/csharp/HostTools/WebHelper.cs +499 -0
  153. package/native/scripts/get-network-profile.ps1 +19 -0
  154. package/native/scripts/set-folder-access.ps1 +11 -0
  155. package/native/scripts/set-web-firewall.ps1 +103 -0
  156. package/package.json +1 -1
  157. package/sandbox/csharp/PtyHost.cs +20 -0
  158. package/schemas/config.schema.json +65 -0
  159. package/schemas/sandbox-config.schema.json +0 -18
  160. package/dist/web/main-UD4QCXZR.js +0 -219
@@ -26765,12 +26765,39 @@ var ResumeSchema = external_exports.strictObject({
26765
26765
  });
26766
26766
 
26767
26767
  // src/core/schema/web.ts
26768
+ var REMOTE_ROLES = ["view", "control"];
26769
+ var WindowsUserNameSchema = external_exports.string().trim().min(1).max(120).regex(
26770
+ /^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/,
26771
+ "a Windows user name (name or DOMAIN\\name)"
26772
+ );
26773
+ var RemoteUserSchema = external_exports.strictObject({
26774
+ name: WindowsUserNameSchema.describe("The Windows user (name, or DOMAIN\\name)"),
26775
+ role: external_exports.enum(REMOTE_ROLES).describe("view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.")
26776
+ });
26777
+ var WebLanSchema = external_exports.strictObject({
26778
+ enabled: external_exports.boolean().describe(
26779
+ "(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out."
26780
+ ),
26781
+ port: external_exports.number().int().min(1024).max(65535).describe("HTTPS port for paired devices on your network."),
26782
+ address: external_exports.string().trim().max(15).regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address or empty").describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one).")
26783
+ });
26784
+ var WebRemoteSchema = external_exports.strictObject({
26785
+ windowsSignIn: external_exports.boolean().describe(
26786
+ "(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link."
26787
+ ),
26788
+ users: external_exports.array(RemoteUserSchema).max(32).describe("Who else may sign in with Windows, and their role (view or control)."),
26789
+ rememberDays: external_exports.number().int().min(1).max(90).describe(
26790
+ "How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed)."
26791
+ ),
26792
+ lan: WebLanSchema
26793
+ });
26768
26794
  var WebSchema = external_exports.strictObject({
26769
26795
  enabled: external_exports.boolean().describe(
26770
26796
  "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26771
26797
  ),
26772
26798
  port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26773
- openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26799
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts."),
26800
+ remote: WebRemoteSchema.describe("(Beta) Access for other Windows users of this PC and for paired devices.")
26774
26801
  });
26775
26802
 
26776
26803
  // src/core/schema/vpn.ts
@@ -26800,6 +26827,36 @@ var GithubSchema = external_exports.strictObject({
26800
26827
  });
26801
26828
 
26802
26829
  // src/core/schema/config.ts
26830
+ var RESERVED_ENV_PREFIX = /^(AGENT_|GIT_|GH_|GITHUB_|NODE_|NPM_CONFIG_(USERCONFIG|GLOBALCONFIG|PREFIX|SCRIPT_SHELL)|SSL_|CLAUDE|ANTHROPIC_|OPENAI_|CODEX_|GEMINI_|GOOGLE_|COPILOT_|AWS_|AZURE_)/i;
26831
+ var RESERVED_ENV_SUFFIX = /(_PROXY|_BASE_URL|_ENDPOINT|_CA_BUNDLE|_CA_CERTS)$/i;
26832
+ var RESERVED_ENV_NAMES = new Set(
26833
+ [
26834
+ "PATH",
26835
+ "PATHEXT",
26836
+ "BASH_ENV",
26837
+ "ENV",
26838
+ "COMSPEC",
26839
+ "PSMODULEPATH",
26840
+ "PYTHONPATH",
26841
+ "PYTHONSTARTUP",
26842
+ "PERL5OPT",
26843
+ "RUBYOPT",
26844
+ "JAVA_TOOL_OPTIONS",
26845
+ "LD_PRELOAD",
26846
+ "SYSTEMROOT",
26847
+ "WINDIR",
26848
+ "HOME",
26849
+ "USERPROFILE",
26850
+ "APPDATA",
26851
+ "LOCALAPPDATA",
26852
+ "PROGRAMDATA",
26853
+ "TEMP",
26854
+ "TMP"
26855
+ ].map((n) => n.toUpperCase())
26856
+ );
26857
+ function reservedEnvName(name) {
26858
+ return RESERVED_ENV_PREFIX.test(name) || RESERVED_ENV_SUFFIX.test(name) || RESERVED_ENV_NAMES.has(name.toUpperCase());
26859
+ }
26803
26860
  var ConfigSchema = external_exports.strictObject({
26804
26861
  $schema: SchemaRefSchema.optional(),
26805
26862
  defaultHarness: NameSchema.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
@@ -26866,10 +26923,14 @@ var LocalOverridesSchema = external_exports.strictObject({
26866
26923
  scripts: StringsSchema.describe(
26867
26924
  "Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."
26868
26925
  ),
26869
- // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
26926
+ // This PC's resources are yours to share: CPU, paging file (this PC's disk) and the RAM kept for this PC are not a
26927
+ // project's to set.
26870
26928
  sandbox: SandboxSchema.omit({
26871
26929
  cpuLimit: true,
26872
26930
  pageFileMb: true,
26931
+ reserveHostMb: true,
26932
+ maxMemoryPercent: true,
26933
+ maxMemoryMb: true,
26873
26934
  engine: true,
26874
26935
  agentMemoryHeadroomMb: true,
26875
26936
  pressureResponse: true
@@ -26883,7 +26944,10 @@ var LocalOverridesSchema = external_exports.strictObject({
26883
26944
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
26884
26945
  toolchains: external_exports.strictObject({
26885
26946
  ...import_lodash.default.mapValues(import_lodash.default.omit(ToolchainsSchema.shape, "environment"), (shape) => shape.partial()),
26886
- environment: ToolchainsSchema.shape.environment
26947
+ environment: ToolchainsSchema.shape.environment.refine(
26948
+ (env) => !Object.keys(env).some(reservedEnvName),
26949
+ "names the sandbox sets or that carry logins (AGENT_*, GIT_*, PATH, BASH_ENV, NODE_*, proxies, ANTHROPIC_*, CLAUDE_* ...) cannot be set by a project"
26950
+ )
26887
26951
  }).partial(),
26888
26952
  databases: external_exports.strictObject(import_lodash.default.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
26889
26953
  attach: AttachSchema.partial(),
@@ -26953,6 +27017,7 @@ var ProjectSpecSchema = external_exports.strictObject({
26953
27017
  /** Its hooks were staged in C:\AgentLaunch\projects\<name>\hooks. */
26954
27018
  hooks: external_exports.boolean()
26955
27019
  });
27020
+ var ShellChoiceSchema = external_exports.enum(["auto", "pwsh", "powershell", "cmd"]);
26956
27021
  var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26957
27022
  external_exports.strictObject({
26958
27023
  kind: external_exports.literal("session"),
@@ -26962,8 +27027,12 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26962
27027
  cwd: Text(),
26963
27028
  project: ProjectSpecSchema.optional(),
26964
27029
  harnessSpec: HarnessSchema.optional(),
27030
+ /** A plain shell instead of an agent (harness is then "shell" and args are ignored). */
27031
+ shell: ShellChoiceSchema.optional(),
26965
27032
  createdAt: TimeSchema
26966
27033
  }),
27034
+ /** End a running session (an agent or a shell) and everything it started. */
27035
+ external_exports.strictObject({ kind: external_exports.literal("close"), id: IdSchema, session: IdSchema, createdAt: TimeSchema }),
26967
27036
  /** Provision a registered project without starting an agent (sandbox start / exec / db from its folder). */
26968
27037
  external_exports.strictObject({ kind: external_exports.literal("project"), id: IdSchema, project: ProjectSpecSchema, createdAt: TimeSchema }),
26969
27038
  external_exports.strictObject({
@@ -27029,6 +27098,8 @@ var SessionStatusSchema = external_exports.object({
27029
27098
  id: IdSchema,
27030
27099
  launchId: Text(64),
27031
27100
  harness: Text(64),
27101
+ /** A plain shell (harness is then the shell: pwsh, powershell or cmd); absent: an agent. */
27102
+ kind: external_exports.enum(["agent", "shell"]).optional(),
27032
27103
  cwd: Text(),
27033
27104
  /** The project it runs in: its hub name, or the launch folder's for the first project. */
27034
27105
  project: Text(64).optional(),
@@ -32651,6 +32722,7 @@ async function setupToolchains(config2, versions, options, run3 = {}) {
32651
32722
  env.summary.push(...part.summary);
32652
32723
  }
32653
32724
  Object.assign(env.vars, config2.environment);
32725
+ Object.assign(env.vars, envs.get("Git")?.vars);
32654
32726
  log("Set up " + steps.length + " toolchains and databases.");
32655
32727
  return env;
32656
32728
  }
@@ -34964,6 +35036,8 @@ var FrameType = {
34964
35036
  Resize: 3,
34965
35037
  Restart: 4,
34966
35038
  Ping: 5,
35039
+ /** Ends the program and everything it started, and with it the session. */
35040
+ Close: 6,
34967
35041
  Output: 16,
34968
35042
  Exit: 17,
34969
35043
  Error: 18,
@@ -26689,12 +26689,39 @@ var ResumeSchema = external_exports.strictObject({
26689
26689
  });
26690
26690
 
26691
26691
  // src/core/schema/web.ts
26692
+ var REMOTE_ROLES = ["view", "control"];
26693
+ var WindowsUserNameSchema = external_exports.string().trim().min(1).max(120).regex(
26694
+ /^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/,
26695
+ "a Windows user name (name or DOMAIN\\name)"
26696
+ );
26697
+ var RemoteUserSchema = external_exports.strictObject({
26698
+ name: WindowsUserNameSchema.describe("The Windows user (name, or DOMAIN\\name)"),
26699
+ role: external_exports.enum(REMOTE_ROLES).describe("view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.")
26700
+ });
26701
+ var WebLanSchema = external_exports.strictObject({
26702
+ enabled: external_exports.boolean().describe(
26703
+ "(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out."
26704
+ ),
26705
+ port: external_exports.number().int().min(1024).max(65535).describe("HTTPS port for paired devices on your network."),
26706
+ address: external_exports.string().trim().max(15).regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address or empty").describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one).")
26707
+ });
26708
+ var WebRemoteSchema = external_exports.strictObject({
26709
+ windowsSignIn: external_exports.boolean().describe(
26710
+ "(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link."
26711
+ ),
26712
+ users: external_exports.array(RemoteUserSchema).max(32).describe("Who else may sign in with Windows, and their role (view or control)."),
26713
+ rememberDays: external_exports.number().int().min(1).max(90).describe(
26714
+ "How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed)."
26715
+ ),
26716
+ lan: WebLanSchema
26717
+ });
26692
26718
  var WebSchema = external_exports.strictObject({
26693
26719
  enabled: external_exports.boolean().describe(
26694
26720
  "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26695
26721
  ),
26696
26722
  port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26697
- openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26723
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts."),
26724
+ remote: WebRemoteSchema.describe("(Beta) Access for other Windows users of this PC and for paired devices.")
26698
26725
  });
26699
26726
 
26700
26727
  // src/core/schema/vpn.ts
@@ -26724,6 +26751,36 @@ var GithubSchema = external_exports.strictObject({
26724
26751
  });
26725
26752
 
26726
26753
  // src/core/schema/config.ts
26754
+ var RESERVED_ENV_PREFIX = /^(AGENT_|GIT_|GH_|GITHUB_|NODE_|NPM_CONFIG_(USERCONFIG|GLOBALCONFIG|PREFIX|SCRIPT_SHELL)|SSL_|CLAUDE|ANTHROPIC_|OPENAI_|CODEX_|GEMINI_|GOOGLE_|COPILOT_|AWS_|AZURE_)/i;
26755
+ var RESERVED_ENV_SUFFIX = /(_PROXY|_BASE_URL|_ENDPOINT|_CA_BUNDLE|_CA_CERTS)$/i;
26756
+ var RESERVED_ENV_NAMES = new Set(
26757
+ [
26758
+ "PATH",
26759
+ "PATHEXT",
26760
+ "BASH_ENV",
26761
+ "ENV",
26762
+ "COMSPEC",
26763
+ "PSMODULEPATH",
26764
+ "PYTHONPATH",
26765
+ "PYTHONSTARTUP",
26766
+ "PERL5OPT",
26767
+ "RUBYOPT",
26768
+ "JAVA_TOOL_OPTIONS",
26769
+ "LD_PRELOAD",
26770
+ "SYSTEMROOT",
26771
+ "WINDIR",
26772
+ "HOME",
26773
+ "USERPROFILE",
26774
+ "APPDATA",
26775
+ "LOCALAPPDATA",
26776
+ "PROGRAMDATA",
26777
+ "TEMP",
26778
+ "TMP"
26779
+ ].map((n) => n.toUpperCase())
26780
+ );
26781
+ function reservedEnvName(name) {
26782
+ return RESERVED_ENV_PREFIX.test(name) || RESERVED_ENV_SUFFIX.test(name) || RESERVED_ENV_NAMES.has(name.toUpperCase());
26783
+ }
26727
26784
  var ConfigSchema = external_exports.strictObject({
26728
26785
  $schema: SchemaRefSchema.optional(),
26729
26786
  defaultHarness: NameSchema.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
@@ -26790,10 +26847,14 @@ var LocalOverridesSchema = external_exports.strictObject({
26790
26847
  scripts: StringsSchema.describe(
26791
26848
  "Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."
26792
26849
  ),
26793
- // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
26850
+ // This PC's resources are yours to share: CPU, paging file (this PC's disk) and the RAM kept for this PC are not a
26851
+ // project's to set.
26794
26852
  sandbox: SandboxSchema.omit({
26795
26853
  cpuLimit: true,
26796
26854
  pageFileMb: true,
26855
+ reserveHostMb: true,
26856
+ maxMemoryPercent: true,
26857
+ maxMemoryMb: true,
26797
26858
  engine: true,
26798
26859
  agentMemoryHeadroomMb: true,
26799
26860
  pressureResponse: true
@@ -26807,7 +26868,10 @@ var LocalOverridesSchema = external_exports.strictObject({
26807
26868
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
26808
26869
  toolchains: external_exports.strictObject({
26809
26870
  ...import_lodash.default.mapValues(import_lodash.default.omit(ToolchainsSchema.shape, "environment"), (shape) => shape.partial()),
26810
- environment: ToolchainsSchema.shape.environment
26871
+ environment: ToolchainsSchema.shape.environment.refine(
26872
+ (env) => !Object.keys(env).some(reservedEnvName),
26873
+ "names the sandbox sets or that carry logins (AGENT_*, GIT_*, PATH, BASH_ENV, NODE_*, proxies, ANTHROPIC_*, CLAUDE_* ...) cannot be set by a project"
26874
+ )
26811
26875
  }).partial(),
26812
26876
  databases: external_exports.strictObject(import_lodash.default.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
26813
26877
  attach: AttachSchema.partial(),
@@ -26877,6 +26941,7 @@ var ProjectSpecSchema = external_exports.strictObject({
26877
26941
  /** Its hooks were staged in C:\AgentLaunch\projects\<name>\hooks. */
26878
26942
  hooks: external_exports.boolean()
26879
26943
  });
26944
+ var ShellChoiceSchema = external_exports.enum(["auto", "pwsh", "powershell", "cmd"]);
26880
26945
  var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26881
26946
  external_exports.strictObject({
26882
26947
  kind: external_exports.literal("session"),
@@ -26886,8 +26951,12 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26886
26951
  cwd: Text(),
26887
26952
  project: ProjectSpecSchema.optional(),
26888
26953
  harnessSpec: HarnessSchema.optional(),
26954
+ /** A plain shell instead of an agent (harness is then "shell" and args are ignored). */
26955
+ shell: ShellChoiceSchema.optional(),
26889
26956
  createdAt: TimeSchema
26890
26957
  }),
26958
+ /** End a running session (an agent or a shell) and everything it started. */
26959
+ external_exports.strictObject({ kind: external_exports.literal("close"), id: IdSchema, session: IdSchema, createdAt: TimeSchema }),
26891
26960
  /** Provision a registered project without starting an agent (sandbox start / exec / db from its folder). */
26892
26961
  external_exports.strictObject({ kind: external_exports.literal("project"), id: IdSchema, project: ProjectSpecSchema, createdAt: TimeSchema }),
26893
26962
  external_exports.strictObject({
@@ -26953,6 +27022,8 @@ var SessionStatusSchema = external_exports.object({
26953
27022
  id: IdSchema,
26954
27023
  launchId: Text(64),
26955
27024
  harness: Text(64),
27025
+ /** A plain shell (harness is then the shell: pwsh, powershell or cmd); absent: an agent. */
27026
+ kind: external_exports.enum(["agent", "shell"]).optional(),
26956
27027
  cwd: Text(),
26957
27028
  /** The project it runs in: its hub name, or the launch folder's for the first project. */
26958
27029
  project: Text(64).optional(),
@@ -5538,6 +5538,8 @@ var FrameType = {
5538
5538
  Resize: 3,
5539
5539
  Restart: 4,
5540
5540
  Ping: 5,
5541
+ /** Ends the program and everything it started, and with it the session. */
5542
+ Close: 6,
5541
5543
  Output: 16,
5542
5544
  Exit: 17,
5543
5545
  Error: 18,
@@ -27088,12 +27090,39 @@ var ResumeSchema = external_exports.strictObject({
27088
27090
  });
27089
27091
 
27090
27092
  // src/core/schema/web.ts
27093
+ var REMOTE_ROLES = ["view", "control"];
27094
+ var WindowsUserNameSchema = external_exports.string().trim().min(1).max(120).regex(
27095
+ /^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/,
27096
+ "a Windows user name (name or DOMAIN\\name)"
27097
+ );
27098
+ var RemoteUserSchema = external_exports.strictObject({
27099
+ name: WindowsUserNameSchema.describe("The Windows user (name, or DOMAIN\\name)"),
27100
+ role: external_exports.enum(REMOTE_ROLES).describe("view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.")
27101
+ });
27102
+ var WebLanSchema = external_exports.strictObject({
27103
+ enabled: external_exports.boolean().describe(
27104
+ "(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out."
27105
+ ),
27106
+ port: external_exports.number().int().min(1024).max(65535).describe("HTTPS port for paired devices on your network."),
27107
+ address: external_exports.string().trim().max(15).regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address or empty").describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one).")
27108
+ });
27109
+ var WebRemoteSchema = external_exports.strictObject({
27110
+ windowsSignIn: external_exports.boolean().describe(
27111
+ "(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link."
27112
+ ),
27113
+ users: external_exports.array(RemoteUserSchema).max(32).describe("Who else may sign in with Windows, and their role (view or control)."),
27114
+ rememberDays: external_exports.number().int().min(1).max(90).describe(
27115
+ "How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed)."
27116
+ ),
27117
+ lan: WebLanSchema
27118
+ });
27091
27119
  var WebSchema = external_exports.strictObject({
27092
27120
  enabled: external_exports.boolean().describe(
27093
27121
  "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
27094
27122
  ),
27095
27123
  port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
27096
- openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
27124
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts."),
27125
+ remote: WebRemoteSchema.describe("(Beta) Access for other Windows users of this PC and for paired devices.")
27097
27126
  });
27098
27127
 
27099
27128
  // src/core/schema/vpn.ts
@@ -27123,6 +27152,36 @@ var GithubSchema = external_exports.strictObject({
27123
27152
  });
27124
27153
 
27125
27154
  // src/core/schema/config.ts
27155
+ var RESERVED_ENV_PREFIX = /^(AGENT_|GIT_|GH_|GITHUB_|NODE_|NPM_CONFIG_(USERCONFIG|GLOBALCONFIG|PREFIX|SCRIPT_SHELL)|SSL_|CLAUDE|ANTHROPIC_|OPENAI_|CODEX_|GEMINI_|GOOGLE_|COPILOT_|AWS_|AZURE_)/i;
27156
+ var RESERVED_ENV_SUFFIX = /(_PROXY|_BASE_URL|_ENDPOINT|_CA_BUNDLE|_CA_CERTS)$/i;
27157
+ var RESERVED_ENV_NAMES = new Set(
27158
+ [
27159
+ "PATH",
27160
+ "PATHEXT",
27161
+ "BASH_ENV",
27162
+ "ENV",
27163
+ "COMSPEC",
27164
+ "PSMODULEPATH",
27165
+ "PYTHONPATH",
27166
+ "PYTHONSTARTUP",
27167
+ "PERL5OPT",
27168
+ "RUBYOPT",
27169
+ "JAVA_TOOL_OPTIONS",
27170
+ "LD_PRELOAD",
27171
+ "SYSTEMROOT",
27172
+ "WINDIR",
27173
+ "HOME",
27174
+ "USERPROFILE",
27175
+ "APPDATA",
27176
+ "LOCALAPPDATA",
27177
+ "PROGRAMDATA",
27178
+ "TEMP",
27179
+ "TMP"
27180
+ ].map((n) => n.toUpperCase())
27181
+ );
27182
+ function reservedEnvName(name) {
27183
+ return RESERVED_ENV_PREFIX.test(name) || RESERVED_ENV_SUFFIX.test(name) || RESERVED_ENV_NAMES.has(name.toUpperCase());
27184
+ }
27126
27185
  var ConfigSchema = external_exports.strictObject({
27127
27186
  $schema: SchemaRefSchema.optional(),
27128
27187
  defaultHarness: NameSchema.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
@@ -27189,10 +27248,14 @@ var LocalOverridesSchema = external_exports.strictObject({
27189
27248
  scripts: StringsSchema.describe(
27190
27249
  "Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."
27191
27250
  ),
27192
- // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
27251
+ // This PC's resources are yours to share: CPU, paging file (this PC's disk) and the RAM kept for this PC are not a
27252
+ // project's to set.
27193
27253
  sandbox: SandboxSchema.omit({
27194
27254
  cpuLimit: true,
27195
27255
  pageFileMb: true,
27256
+ reserveHostMb: true,
27257
+ maxMemoryPercent: true,
27258
+ maxMemoryMb: true,
27196
27259
  engine: true,
27197
27260
  agentMemoryHeadroomMb: true,
27198
27261
  pressureResponse: true
@@ -27206,7 +27269,10 @@ var LocalOverridesSchema = external_exports.strictObject({
27206
27269
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
27207
27270
  toolchains: external_exports.strictObject({
27208
27271
  ...import_lodash.default.mapValues(import_lodash.default.omit(ToolchainsSchema.shape, "environment"), (shape) => shape.partial()),
27209
- environment: ToolchainsSchema.shape.environment
27272
+ environment: ToolchainsSchema.shape.environment.refine(
27273
+ (env) => !Object.keys(env).some(reservedEnvName),
27274
+ "names the sandbox sets or that carry logins (AGENT_*, GIT_*, PATH, BASH_ENV, NODE_*, proxies, ANTHROPIC_*, CLAUDE_* ...) cannot be set by a project"
27275
+ )
27210
27276
  }).partial(),
27211
27277
  databases: external_exports.strictObject(import_lodash.default.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
27212
27278
  attach: AttachSchema.partial(),
@@ -27276,6 +27342,7 @@ var ProjectSpecSchema = external_exports.strictObject({
27276
27342
  /** Its hooks were staged in C:\AgentLaunch\projects\<name>\hooks. */
27277
27343
  hooks: external_exports.boolean()
27278
27344
  });
27345
+ var ShellChoiceSchema = external_exports.enum(["auto", "pwsh", "powershell", "cmd"]);
27279
27346
  var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
27280
27347
  external_exports.strictObject({
27281
27348
  kind: external_exports.literal("session"),
@@ -27285,8 +27352,12 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
27285
27352
  cwd: Text(),
27286
27353
  project: ProjectSpecSchema.optional(),
27287
27354
  harnessSpec: HarnessSchema.optional(),
27355
+ /** A plain shell instead of an agent (harness is then "shell" and args are ignored). */
27356
+ shell: ShellChoiceSchema.optional(),
27288
27357
  createdAt: TimeSchema
27289
27358
  }),
27359
+ /** End a running session (an agent or a shell) and everything it started. */
27360
+ external_exports.strictObject({ kind: external_exports.literal("close"), id: IdSchema, session: IdSchema, createdAt: TimeSchema }),
27290
27361
  /** Provision a registered project without starting an agent (sandbox start / exec / db from its folder). */
27291
27362
  external_exports.strictObject({ kind: external_exports.literal("project"), id: IdSchema, project: ProjectSpecSchema, createdAt: TimeSchema }),
27292
27363
  external_exports.strictObject({
@@ -27352,6 +27423,8 @@ var SessionStatusSchema = external_exports.object({
27352
27423
  id: IdSchema,
27353
27424
  launchId: Text(64),
27354
27425
  harness: Text(64),
27426
+ /** A plain shell (harness is then the shell: pwsh, powershell or cmd); absent: an agent. */
27427
+ kind: external_exports.enum(["agent", "shell"]).optional(),
27355
27428
  cwd: Text(),
27356
27429
  /** The project it runs in: its hub name, or the launch folder's for the first project. */
27357
27430
  project: Text(64).optional(),
@@ -5,7 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { z } from 'zod';
8
- import { agentsPage, agentsView } from '../app/features/agents/index.js';
8
+ import { agentNameProblem, agentsPage, agentsView } from '../app/features/agents/index.js';
9
9
  import { DEFINITIONS, NAV, navSections, navViews, QUERIES, VIEWS, viewById } from '../app/features/defs.js';
10
10
  import { accountsPage, agentPage, listAccounts } from '../app/features/accounts/index.js';
11
11
  import { addProject, projectRoots, projectsPage } from '../app/features/projects/index.js';
@@ -15,13 +15,13 @@ import { insideSandbox, isSandboxRunning } from '../host/sandbox/index.js';
15
15
  import { CRASH, listLogs, logsPage, tailLog } from '../app/features/logs/index.js';
16
16
  import { DEFAULTS } from '../core/config/index.js';
17
17
  import { sandboxPage } from '../app/features/sandbox/index.js';
18
- import { sessionsPage } from '../app/features/sessions/index.js';
18
+ import { closeSession, sessionsPage, startShell, } from '../app/features/sessions/index.js';
19
19
  import { APP_GROUPS, appsPage, listApps } from '../app/features/apps/index.js';
20
20
  import { keepShownSecrets, settingsPage } from '../app/features/settings/index.js';
21
21
  import { checkEngine, checkText, enginePage } from '../app/features/engine/index.js';
22
22
  import { runUpdate, updatesPage } from '../app/features/updates/index.js';
23
23
  import { MenuOperations } from '../cli/menu/io-menu.js';
24
- import { ago, bytesText, Cancelled, ClientMessageSchema, defineOperation, exemption, fakeIO, fieldOf, fieldProblem, FieldSchema, fieldsOf, fromWireError, humanize, implementer, invoke, pageActions, pageHasContent, ServerMessageSchema, SURFACE_NAMES, toWireError, valueText, } from '../app/kernel/index.js';
24
+ import { accessNeeded, ago, allows, bytesText, Cancelled, ClientMessageSchema, defineOperation, exemption, fakeIO, fieldOf, fieldProblem, FieldSchema, fieldsOf, fromWireError, humanize, implementer, invoke, pageActions, pageHasContent, ServerMessageSchema, SURFACE_NAMES, toWireError, valueText, OWNER, } from '../app/kernel/index.js';
25
25
  import { createRpcHost, IMPLEMENTATIONS, readQuery, runDefinition } from '../app/registry.js';
26
26
  import { webPage } from '../app/features/web/index.js';
27
27
  import { parseCli } from '../cli/args.js';
@@ -326,11 +326,43 @@ export const appSuite = {
326
326
  const menu = menuView(sessions);
327
327
  const chosen = menu.resolve({ kind: 'pick', value: 'new' });
328
328
  t.check(menu.items.length === 2 &&
329
- menu.actions.map((a) => a.key).join() === 'n,r,o,l' &&
329
+ menu.actions.map((a) => a.key).join() === 'n,s,r,o,x,l' &&
330
330
  menu.enterLabel(undefined, 'new') === 'attach' &&
331
331
  chosen?.kind === 'action' &&
332
332
  chosen.action.kind === 'link' &&
333
- menu.resolve({ kind: 'action', key: 'l', value: undefined })?.kind === 'refresh', 'sessions in the menu: rows, the n/r/o/l keys, Enter attaches');
333
+ menu.resolve({ kind: 'action', key: 'l', value: undefined })?.kind === 'refresh', 'sessions in the menu: rows, the n/s/r/o/x/l keys, Enter attaches');
334
+ t.section('plain shell sessions');
335
+ const shellRow = { ...running('sh1', true), harness: 'pwsh', kind: 'shell' };
336
+ const shellPage = sessionsPage({ ...list, sessions: [shellRow] });
337
+ const shellItem = shellPage.sections[0].items?.[0];
338
+ const closeAction = shellItem?.actions?.[0];
339
+ t.check(shellItem?.label === 'pwsh (shell) api' &&
340
+ closeAction?.kind === 'operation' &&
341
+ closeAction.op === 'sessions.close' &&
342
+ closeAction.key === 'x' &&
343
+ closeAction.danger === true &&
344
+ JSON.stringify(closeAction.input) === '{"session":"sh1"}', 'a shell is listed by its name, marked as a shell, and can be closed (x)', JSON.stringify(shellItem));
345
+ const newShell = shellPage.actions?.find((a) => a.kind === 'operation' && a.op === 'sessions.shell');
346
+ t.check(newShell?.kind === 'operation' && newShell.key === 's' && JSON.stringify(newShell.input) === '{"shell":"auto"}', 'the sessions page offers New shell (s) next to New session');
347
+ t.check(!sessionsPage({ ...list, sandboxRunning: false, sessions: [] }).actions?.some((a) => a.kind === 'operation' && a.op === 'sessions.shell'), 'no New shell without a running sandbox');
348
+ t.equal(startShell.opens?.({ session: 'sh1', shell: 'pwsh', message: '' }), { kind: 'terminal', session: 'sh1' }, 'a new shell opens its terminal on every surface');
349
+ const controlCaller = { kind: 'windows', user: 'bob', role: 'control' };
350
+ const viewCaller = { kind: 'device', id: 'd', name: 'phone', role: 'view' };
351
+ t.check(accessNeeded(startShell, {}) === 'owner' &&
352
+ !allows(controlCaller, accessNeeded(startShell, {})) &&
353
+ allows(OWNER, accessNeeded(startShell, {})) &&
354
+ accessNeeded(closeSession, { session: 'x' }) === 'control' &&
355
+ allows(controlCaller, accessNeeded(closeSession, { session: 'x' })) &&
356
+ !allows(viewCaller, accessNeeded(closeSession, { session: 'x' })), 'only the owner opens a shell; control may close a session, view may not');
357
+ t.equal([
358
+ cliInput(definitionFor('shell', []), parseCli(['shell', 'api', '--shell', 'cmd'])),
359
+ cliInput(definitionFor('close', []), parseCli(['close', 'sh1'])),
360
+ ], [{ project: 'api', shell: 'cmd' }, { session: 'sh1' }], 'sandbox shell [project] --shell and sandbox close <id> become the input');
361
+ t.check(agentNameProblem('pwsh', []) !== true && agentNameProblem('Shell', []) !== true, 'an agent cannot be named after a shell or the shell command');
362
+ if (insideSandbox())
363
+ await t.throws(() => runDefinition('sessions.shell', {}, {}, fakeIO([]).io), 'has nothing to control', 'sandbox shell refuses inside the sandbox');
364
+ else
365
+ t.skip('sandbox shell refuses inside the sandbox (this PC is not a sandbox)');
334
366
  const menuStatus = menuView(statusPage, { category: 'status', prefix: 'status:' });
335
367
  t.check(menuStatus.items.every((i) => i.category === 'status' && i.value.startsWith('status:')) &&
336
368
  menuStatus.actions.some((a) => a.key === 'x' && a.label === 'stop'), 'a view inside one category of a page: rows in it, prefixed, with the keys of its actions');
@@ -963,6 +995,9 @@ export const appSuite = {
963
995
  t.check(definitionFor('web', ['open'])?.id === 'web.open' &&
964
996
  definitionFor('web', ['status'])?.id === 'web.status' &&
965
997
  definitionFor('web', []) === undefined &&
966
- commandSpec('web')?.args[0]?.choices?.join() === 'start,stop,status,open', 'the web actions are actions of the web command; sandbox web with no action is not one of them');
998
+ commandSpec('web')?.args[0]?.choices?.join() ===
999
+ 'start,stop,status,open,allow-device,revoke,revoke-all,lan-on,lan-off,devices' &&
1000
+ definitionFor('web', ['allow-device'])?.id === 'access.allowDevice' &&
1001
+ definitionFor('web', ['revoke', '0123456789abcdef'])?.id === 'access.revoke', 'the web actions (and the remote access ones) are actions of the web command; sandbox web alone is not one');
967
1002
  },
968
1003
  };
@@ -35,6 +35,22 @@ export const channelSuite = {
35
35
  t.check(validSession(goodSession) &&
36
36
  !validSession({ ...goodSession, attach: { ...goodSession.attach, port: 70000 } }) &&
37
37
  !validSession({ ...goodSession, phase: 'pwned' }), 'session status: a well-formed one passes; an impossible port or unknown phase is rejected');
38
+ const shellRequest = {
39
+ kind: 'session',
40
+ id: 'r1',
41
+ harness: 'shell',
42
+ args: [],
43
+ cwd: 'C:\\Workspace',
44
+ shell: 'pwsh',
45
+ createdAt: 'x',
46
+ };
47
+ t.check(validRequest(shellRequest) &&
48
+ validRequest({ ...shellRequest, shell: 'auto' }) &&
49
+ !validRequest({ ...shellRequest, shell: 'bash -c evil' }) &&
50
+ validRequest({ kind: 'close', id: 'c1', session: 's1', createdAt: 'x' }) &&
51
+ !validRequest({ kind: 'close', id: 'c1', session: '..\\s1', createdAt: 'x' }) &&
52
+ validSession({ ...goodSession, harness: 'pwsh', kind: 'shell' }) &&
53
+ !validSession({ ...goodSession, kind: 'daemon' }), 'shells: a session request names one of the known shells; close names a safe session id; a status says shell');
38
54
  t.section('boot progress');
39
55
  const goodStatus = {
40
56
  launchId: 'l1',
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import path from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import { isWin } from '../core/util/index.js';
7
+ import { commandLine, shellLaunch } from '../guest/sessions/cmdline.js';
7
8
  import { runNode } from './support/fixtures.js';
8
9
  // Odd quote counts flip cmd's quote state for every later argument: those come after them on purpose.
9
10
  const TRICKY_ARGUMENTS = [
@@ -71,5 +72,20 @@ export const cmdlineSuite = {
71
72
  }
72
73
  else
73
74
  t.skip('end-to-end cmd.exe quoting run (not Windows, or the temp path has spaces)');
75
+ t.section('plain shell sessions');
76
+ const paths = { pwsh: 'C:/T/pwsh/pwsh.exe', powershell: 'C:/W/powershell.exe', cmd: 'C:/W/cmd.exe' };
77
+ const noPwsh = { ...paths, pwsh: null };
78
+ t.equal([
79
+ shellLaunch('auto', paths),
80
+ shellLaunch('auto', noPwsh),
81
+ shellLaunch('powershell', paths),
82
+ shellLaunch('cmd', paths),
83
+ ].map((l) => l.name + ' ' + commandLine(l.file, l.args)), [
84
+ 'pwsh C:/T/pwsh/pwsh.exe -NoLogo',
85
+ 'powershell C:/W/powershell.exe -NoLogo',
86
+ 'powershell C:/W/powershell.exe -NoLogo',
87
+ 'cmd C:/W/cmd.exe',
88
+ ], 'a shell runs by its absolute path: auto is pwsh when present, else Windows PowerShell; cmd takes no arguments');
89
+ await t.throws(() => shellLaunch('pwsh', noPwsh), 'PowerShell 7 is not installed', 'asking for pwsh where it is missing says so');
74
90
  },
75
91
  };
@@ -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');
@@ -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
  };
@@ -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,