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
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../cli/index.js';
3
- const args = process.argv.slice(2);
4
- const personal = args.includes('--personal') ? ['--personal'] : [];
5
- run(['host-claude', ...personal, '--', ...args.filter((a) => a !== '--personal')]);
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/claude-host.ts
9
+ var args = process.argv.slice(2);
10
+ var personal = args.includes("--personal") ? ["--personal"] : [];
11
+ run(["host-claude", ...personal, "--", ...args.filter((a) => a !== "--personal")]);
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../cli/index.js';
3
- run(process.argv.slice(2), { harness: 'claude' });
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/claude-sbx.ts
9
+ run(process.argv.slice(2), { harness: "claude" });
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../cli/index.js';
3
- run(process.argv.slice(2), { harness: 'codex' });
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/codex-sbx.ts
9
+ run(process.argv.slice(2), { harness: "codex" });
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../cli/index.js';
3
- run(process.argv.slice(2), { harness: 'gemini' });
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/gemini-sbx.ts
9
+ run(process.argv.slice(2), { harness: "gemini" });
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // sandbox / windows-development-sandbox: the menu with no arguments, any command, or `sandbox <agent>`.
3
- import { run } from '../cli/index.js';
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/sandbox.ts
4
9
  run(process.argv.slice(2));
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../cli/index.js';
3
- run(process.argv.slice(2), { command: 'hide' });
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/sbx-hide.ts
9
+ run(process.argv.slice(2), { command: "hide" });
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { run } from '../cli/index.js';
3
- run(process.argv.slice(2), { command: 'show' });
2
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
+ import {
4
+ run
5
+ } from "./chunk-K4SU3MHJ.js";
6
+ import "./chunk-VENWSGX2.js";
7
+
8
+ // src/bin/sbx-show.ts
9
+ run(process.argv.slice(2), { command: "show" });
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
- import { AccountStore } from '../../../host/profile/index.js';
3
+ import { AccountsService } from '../../services/index.js';
4
4
  import { SandboxChannel } from '../../../host/channel/index.js';
5
5
  import { describePrune, hostDataFolder, pruneStorage } from '../../../host/storage/index.js';
6
6
  import { RepoGuardStore, readDataSize, saveMarkedProjects } from '../../../host/guard/index.js';
@@ -73,16 +73,33 @@ async function prepareWorkspaceView(ctx, run) {
73
73
  '; ' +
74
74
  plan.options.view);
75
75
  }
76
+ /**
77
+ * Every agent that keeps a login starts the new sandbox on its default (the launched agent on --account when given),
78
+ * so a session of any agent opened later uses the login you chose. Returns the launched agent's login.
79
+ */
80
+ function launchAccounts(ctx, launched, wanted) {
81
+ const accounts = new AccountsService(ctx);
82
+ let account = null;
83
+ for (const agent of accounts.agents().filter((a) => a.kept)) {
84
+ if (agent.name === launched) {
85
+ account = accounts.ensure(launched, wanted);
86
+ continue;
87
+ }
88
+ try {
89
+ accounts.ensure(agent.name, null);
90
+ }
91
+ catch (e) {
92
+ // Another agent's saved login could not be put in place: the launch goes on, and that agent says so.
93
+ console.warn(c.yellow('Could not switch ' + agent.name + ' to its default account: ' + errMsg(e)));
94
+ }
95
+ }
96
+ return account;
97
+ }
76
98
  /** Everything a real launch does before the sandbox starts (skipped by `generate`, which only writes files). */
77
99
  export async function prepare(ctx, run) {
78
100
  const { options, dataPath, storageRoot } = ctx;
79
101
  const { config } = run;
80
- const accounts = new AccountStore(storageRoot, config.claudeProfile.sharePluginsAcrossAccounts);
81
- const account = options.account ?? accounts.defaultName();
82
- if (account && run.harnessName === 'claude' && account !== accounts.activeFor(dataPath))
83
- accounts.activate(dataPath, account);
84
- else if (run.harnessName === 'claude')
85
- accounts.syncPlugins(dataPath);
102
+ const account = launchAccounts(ctx, run.harnessName, options.account ?? null);
86
103
  const terminal = config.provisioning.terminal.enabled
87
104
  ? await ensureTerminal(dataPath, config.provisioning.refresh, { pwsh: config.toolchains.pwsh.enabled !== false })
88
105
  : { exe: null, message: 'disabled' };
@@ -6,6 +6,7 @@ import { NeedsRestart } from '../../../host/projects/index.js';
6
6
  import { RunDir } from '../../../shared/channel/index.js';
7
7
  import { errMsg, fail, sleep } from '../../../core/util/index.js';
8
8
  import { c, follow, working, table } from '../../output/index.js';
9
+ import { AccountsService } from '../../services/index.js';
9
10
  import { bootLine, readyIn, stillWorking } from './boot-progress.js';
10
11
  import { hereDir, joinHere, readyHere, offerRestart } from '../projects/index.js';
11
12
  /** How long a session may take to start (its project's setup, the agent's install). */
@@ -65,6 +66,9 @@ export function waitForStart(ctx, requestId, what) {
65
66
  /** Asks the sandbox for a session; the agent's definition goes along (the sandbox may have booted with another). */
66
67
  function requestSession(ctx, channel, harness, args, joined) {
67
68
  const harnessSpec = ctx.loaded.config.harnesses[harness];
69
+ // --account into a running sandbox switches its login first (sessions already running switch on their restart).
70
+ if (ctx.options.account && !ctx.borrowed)
71
+ new AccountsService(ctx).ensure(harness, ctx.options.account);
68
72
  return channel.requestSession(harness, args, joined.guestDir, {
69
73
  ...(joined.spec ? { project: joined.spec } : {}),
70
74
  ...(harnessSpec ? { harnessSpec } : {}),
@@ -15,6 +15,7 @@ import { agentCommandsCommand } from './agent-commands.js';
15
15
  import { vpn } from './vpn.js';
16
16
  import { github } from './github.js';
17
17
  import { web, webServer } from './web.js';
18
+ import { remote } from './remote.js';
18
19
  export const HANDLERS = {
19
20
  launch: (ctx) => launch(ctx),
20
21
  generate: (ctx) => launch(ctx, true),
@@ -54,5 +55,6 @@ export const HANDLERS = {
54
55
  vpn: vpn,
55
56
  github: github,
56
57
  web: web,
58
+ remote: remote,
57
59
  _web: webServer,
58
60
  };
@@ -0,0 +1,2 @@
1
+ import { type Context } from './context.js';
2
+ export declare function remote(ctx: Context): Promise<number>;
@@ -0,0 +1,49 @@
1
+ // `sandbox remote [user] [--browser]`: another Windows user's program, used from here (docs/design/remote-access.md).
2
+ // Windows sign-in through the owner's pipe gives a one-time token; the menu then runs over the owner's web UI (every
3
+ // action checked against the role the owner gave, and written to their audit log), or --browser opens it signed in.
4
+ import { allows } from '../../app/kernel/index.js';
5
+ import { fail, safeText } from '../../core/util/index.js';
6
+ import { knockOnDoor } from '../../host/remote/index.js';
7
+ import { currentUserName, runningOwner } from '../../host/sharing/index.js';
8
+ import { openBrowser } from '../../host/web/index.js';
9
+ import { RemoteMenu } from '../menu/remote-menu.js';
10
+ import { remoteClient } from '../services/remote-client.js';
11
+ import { App } from '../tui/index.js';
12
+ import { requireOnPc } from './context.js';
13
+ /** Whose program: the user named, else the owner of the running sandbox when it is someone else's. */
14
+ function ownerOf(ctx) {
15
+ const named = ctx.options.positionals[0];
16
+ if (named)
17
+ return { name: named, id: named };
18
+ const owner = runningOwner();
19
+ if (owner.kind === 'other')
20
+ return { name: owner.user, id: owner.record.sid };
21
+ return fail('Whose web UI? No sandbox of another user is running.', 'Name them: sandbox remote <their Windows user name>');
22
+ }
23
+ export async function remote(ctx) {
24
+ requireOnPc('remote');
25
+ const owner = ownerOf(ctx);
26
+ const answer = await knockOnDoor(ctx.storageRoot, owner.id);
27
+ if (!answer.ok)
28
+ return fail(safeText(answer.reason), 'Ask ' +
29
+ safeText(owner.name) +
30
+ ' to turn on web.remote.windowsSignIn, add you to web.remote.users and start the web UI.');
31
+ if (ctx.options.raw.browser === true) {
32
+ openBrowser('http://127.0.0.1:' + answer.port + '/#w=' + answer.token);
33
+ console.log('Opened ' + safeText(owner.name) + "'s web UI in your browser (" + answer.role + '). It runs as them, not you.');
34
+ return 0;
35
+ }
36
+ const client = await remoteClient(answer.port, answer.token);
37
+ const caller = { kind: 'windows', user: currentUserName(), role: answer.role };
38
+ const app = new App();
39
+ try {
40
+ await new RemoteMenu(app, ctx, client, owner.name, caller).start();
41
+ }
42
+ finally {
43
+ app.close();
44
+ client.close();
45
+ }
46
+ if (!allows(caller, 'control'))
47
+ console.log('You had view access: nothing was changed.');
48
+ return 0;
49
+ }
@@ -8,6 +8,9 @@ import { c } from '../../output/index.js';
8
8
  /** Where each running session is on this PC, so it can be reopened after a restart. */
9
9
  export function sessionsToReopen(registry, sessions) {
10
10
  return sessions.flatMap((s) => {
11
+ // Shells are not reopened: a new one would not be the one that ended.
12
+ if (s.kind === 'shell')
13
+ return [];
11
14
  const project = projectForGuestDir([...registry.projects, ...registry.roots.map((r) => ({ ...r }))], s.cwd);
12
15
  return project
13
16
  ? [
@@ -88,7 +88,7 @@ export async function storageBeforeCommand(ctx, command) {
88
88
  ensureStorageRoot(ctx.storageRoot, protectPrivateFolder);
89
89
  // Contained storage (beta): a mount an earlier sandbox left behind is only mentioned here; repair fixes it.
90
90
  if (command !== 'launch' && !ctx.options.json) {
91
- const stale = staleMountNote(ctx.storageRoot, ctx.loaded.config.storage.dataName, isSandboxRunning());
91
+ const stale = staleMountNote(ctx.storageRoot, ctx.loaded.config.storage.dataName, isSandboxRunning);
92
92
  if (stale)
93
93
  console.error(c.yellow(stale));
94
94
  }
@@ -3,10 +3,16 @@
3
3
  // running when no sandbox does. startWebOnLaunch is the sandbox start's hook.
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
+ import { accessStoreOf } from '../../app/features/access/index.js';
6
7
  import { currentServer, START_WAIT_MS, webStatus } from '../../app/features/web/index.js';
7
8
  import { createRpcHost } from '../../app/registry.js';
9
+ import { loadConfig } from '../../core/config/index.js';
8
10
  import { packageVersion } from '../../core/package-info.js';
9
11
  import { sleep, fail } from '../../core/util/index.js';
12
+ import { AuditLog } from '../../host/audit/index.js';
13
+ import { doorPipeName, HostToolsHelper, networkProfile, RemoteGate, TlsStore, } from '../../host/remote/index.js';
14
+ import { SecretStore } from '../../host/secrets/index.js';
15
+ import { currentUserSid } from '../../host/sharing/index.js';
10
16
  import { IDLE_EXIT_MS, openBrowser, WebControl, WebRunner, WebStore } from '../../host/web/index.js';
11
17
  import { spawnDetached } from '../../host/sandbox/index.js';
12
18
  import { WebService } from '../services/index.js';
@@ -53,6 +59,65 @@ export async function startWebOnLaunch(ctx) {
53
59
  export async function web(ctx) {
54
60
  return (await definitionHandler(webStatus)(ctx)) ?? 0;
55
61
  }
62
+ /** The remote settings as in the file now (read again at most every 3 seconds; a broken file keeps the last). */
63
+ function remoteSettings(ctx) {
64
+ let last = ctx.loaded.config.web.remote;
65
+ let readAt = Date.now();
66
+ return () => {
67
+ if (Date.now() - readAt < 3000)
68
+ return last;
69
+ readAt = Date.now();
70
+ try {
71
+ last = loadConfig(ctx.options.config).config.web.remote;
72
+ }
73
+ catch {
74
+ // A settings file being edited or broken: keep what was read last.
75
+ }
76
+ return last;
77
+ };
78
+ }
79
+ /** Remote actions go to the audit log of this data set (k: remote), so the Activity page shows who did what. */
80
+ function remoteAudit(ctx) {
81
+ const { days, maxMb } = ctx.loaded.config.audit;
82
+ const log = new AuditLog(AuditLog.dirFor(ctx.storageRoot, ctx.dataPath), { days, maxMb });
83
+ return (entry) => {
84
+ try {
85
+ log.append([{ t: new Date().toISOString(), k: 'remote', p: entry.what, a: entry.outcome, au: entry.who }]);
86
+ }
87
+ catch {
88
+ // The audit log is a record, never a reason for a request to fail.
89
+ }
90
+ };
91
+ }
92
+ /**
93
+ * The gate for other Windows users and devices (docs/design/remote-access.md). The Windows helper is started when it
94
+ * can be built here: then the owner's own cookie is bound to the owner's connections too, and the sign-in pipe is
95
+ * served (it refuses everyone while web.remote.windowsSignIn is off).
96
+ */
97
+ function remoteGate(ctx) {
98
+ const secrets = SecretStore.standard();
99
+ let ownerSid = '';
100
+ try {
101
+ ownerSid = currentUserSid();
102
+ }
103
+ catch {
104
+ // Unknown: Windows sign-in stays off, the owner signs in with the token as before.
105
+ }
106
+ const helper = ownerSid ? HostToolsHelper.start(ctx.storageRoot, doorPipeName(ownerSid)) : null;
107
+ const audit = remoteAudit(ctx);
108
+ helper?.onProblem((text) => audit({ who: 'the web UI', what: 'Windows sign-in: ' + text, outcome: 'failed' }));
109
+ const store = new WebStore(ctx.storageRoot);
110
+ return new RemoteGate({
111
+ store: accessStoreOf(ctx),
112
+ settings: remoteSettings(ctx),
113
+ ownerSid,
114
+ helper,
115
+ audit,
116
+ tls: new TlsStore(store.dir, secrets),
117
+ profile: (address) => networkProfile(address),
118
+ port: () => ctx.loaded.config.web.port,
119
+ });
120
+ }
56
121
  /** Body of the background process (`sandbox _web`): runs until stopped. */
57
122
  export async function webServer(ctx) {
58
123
  const staticRoot = path.join(ctx.packageRoot, 'dist', 'web');
@@ -66,6 +131,7 @@ export async function webServer(ctx) {
66
131
  staticRoot,
67
132
  version: packageVersion(),
68
133
  idleExitMs: IDLE_EXIT_MS,
134
+ gate: remoteGate(ctx),
69
135
  }).run();
70
136
  process.exit(0);
71
137
  }
@@ -769,17 +769,76 @@ declare const COMMAND_LIST: readonly [{
769
769
  readonly interactive: true;
770
770
  }, {
771
771
  readonly name: "web";
772
- readonly summary: "(beta) your sandbox sessions in a browser on this PC: start, stop, status or open";
772
+ readonly summary: "(beta) your sandbox sessions in a browser on this PC (and, if you allow it, on your network)";
773
773
  readonly group: "Web";
774
- readonly examples: readonly ["sandbox web open start the web UI and open it in your browser"];
774
+ readonly examples: readonly ["sandbox web open start the web UI and open it in your browser", "sandbox web lan-on let devices you allow use it on your network", "sandbox web allow-device show a one-time code for a phone", "sandbox web devices who may use it; sandbox web revoke <id> signs one out"];
775
775
  readonly args: readonly [{
776
776
  readonly name: "action";
777
- readonly description: "start the server, stop it, show whether it runs, or open it in your browser";
777
+ readonly description: "start, stop, status, open; devices, allow-device, revoke, revoke-all, lan-on, lan-off (remote access)";
778
778
  readonly required: false;
779
779
  readonly choices: readonly ["start", "stop", "status", "open"];
780
+ }, {
781
+ readonly name: "id";
782
+ readonly description: "(revoke) the device or sign-in id from sandbox web devices";
783
+ readonly required: false;
784
+ readonly when: "revoke";
785
+ }];
786
+ readonly options: readonly [{
787
+ flag: string;
788
+ type: "string" | "number" | "boolean" | "list";
789
+ description: string;
790
+ short?: string | undefined;
791
+ value?: string | undefined;
792
+ label?: string | undefined;
793
+ default?: string | number | undefined;
794
+ choices?: string[] | "harnesses" | "sessions" | "accounts" | undefined;
795
+ advanced?: boolean | undefined;
796
+ when?: string | undefined;
797
+ noMenu?: boolean | undefined;
798
+ group?: string | undefined;
799
+ shownAs?: string | undefined;
800
+ }, {
801
+ flag: string;
802
+ type: "string" | "number" | "boolean" | "list";
803
+ description: string;
804
+ short?: string | undefined;
805
+ value?: string | undefined;
806
+ label?: string | undefined;
807
+ default?: string | number | undefined;
808
+ choices?: string[] | "harnesses" | "sessions" | "accounts" | undefined;
809
+ advanced?: boolean | undefined;
810
+ when?: string | undefined;
811
+ noMenu?: boolean | undefined;
812
+ group?: string | undefined;
813
+ shownAs?: string | undefined;
780
814
  }];
781
- readonly options: readonly [];
782
815
  readonly json: true;
816
+ }, {
817
+ readonly name: "remote";
818
+ readonly summary: "(beta) use another Windows user's web UI from here, signed in with your Windows account";
819
+ readonly group: "Web";
820
+ readonly examples: readonly ["sandbox remote the menu over the owner of the running sandbox (their program, your role)", "sandbox remote alice --browser open alice's web UI in your browser, signed in"];
821
+ readonly args: readonly [{
822
+ readonly name: "user";
823
+ readonly description: "whose web UI (default: the owner of the running sandbox)";
824
+ readonly required: false;
825
+ }];
826
+ readonly options: readonly [{
827
+ flag: string;
828
+ type: "string" | "number" | "boolean" | "list";
829
+ description: string;
830
+ short?: string | undefined;
831
+ value?: string | undefined;
832
+ label?: string | undefined;
833
+ default?: string | number | undefined;
834
+ choices?: string[] | "harnesses" | "sessions" | "accounts" | undefined;
835
+ advanced?: boolean | undefined;
836
+ when?: string | undefined;
837
+ noMenu?: boolean | undefined;
838
+ group?: string | undefined;
839
+ shownAs?: string | undefined;
840
+ }];
841
+ readonly interactive: true;
783
842
  }, {
784
843
  readonly name: "_watch";
785
844
  readonly summary: "background history sync";
@@ -540,20 +540,49 @@ const COMMAND_LIST = [
540
540
  },
541
541
  {
542
542
  name: 'web',
543
- summary: '(beta) your sandbox sessions in a browser on this PC: start, stop, status or open',
543
+ summary: '(beta) your sandbox sessions in a browser on this PC (and, if you allow it, on your network)',
544
544
  group: 'Web',
545
- examples: ['sandbox web open start the web UI and open it in your browser'],
545
+ examples: [
546
+ 'sandbox web open start the web UI and open it in your browser',
547
+ 'sandbox web lan-on let devices you allow use it on your network',
548
+ 'sandbox web allow-device show a one-time code for a phone',
549
+ 'sandbox web devices who may use it; sandbox web revoke <id> signs one out',
550
+ ],
546
551
  args: [
547
552
  {
548
553
  name: 'action',
549
- description: 'start the server, stop it, show whether it runs, or open it in your browser',
554
+ description: 'start, stop, status, open; devices, allow-device, revoke, revoke-all, lan-on, lan-off (remote access)',
550
555
  required: false,
551
556
  choices: ['start', 'stop', 'status', 'open'],
552
557
  },
558
+ {
559
+ name: 'id',
560
+ description: '(revoke) the device or sign-in id from sandbox web devices',
561
+ required: false,
562
+ when: 'revoke',
563
+ },
564
+ ],
565
+ options: [
566
+ text('--role', 'view|control', '(allow-device) what the device may do (default view)', {
567
+ when: 'allow-device',
568
+ choices: ['view', 'control'],
569
+ }),
570
+ bool('--yes', '(revoke-all, lan-off) do not ask first', { short: '-y', noMenu: true }),
553
571
  ],
554
- options: [],
555
572
  json,
556
573
  },
574
+ {
575
+ name: 'remote',
576
+ summary: "(beta) use another Windows user's web UI from here, signed in with your Windows account",
577
+ group: 'Web',
578
+ examples: [
579
+ 'sandbox remote the menu over the owner of the running sandbox (their program, your role)',
580
+ "sandbox remote alice --browser open alice's web UI in your browser, signed in",
581
+ ],
582
+ args: [{ name: 'user', description: 'whose web UI (default: the owner of the running sandbox)', required: false }],
583
+ options: [bool('--browser', 'open it in your browser instead of the menu')],
584
+ interactive: true,
585
+ },
557
586
  { name: '_watch', summary: 'background history sync', group: 'Data', args: [], options: [], hidden: true },
558
587
  {
559
588
  name: '_web',
@@ -1,4 +1,4 @@
1
- import { type Action, type AnyDefinition } from '../../app/kernel/index.js';
1
+ import { type Action, type AnyDefinition, type OperationIO } from '../../app/kernel/index.js';
2
2
  import type { Context } from '../commands/context.js';
3
3
  import { type App } from '../tui/index.js';
4
4
  import type { ShellExit } from './shell.js';
@@ -12,10 +12,12 @@ export interface Performed {
12
12
  readonly notice?: string;
13
13
  readonly reload?: boolean;
14
14
  }
15
+ /** How a definition is run: in this process (the menu), or on the owner's web UI (sandbox remote). */
16
+ export type DefinitionRunner = (id: string, input: unknown, io: OperationIO) => Promise<unknown>;
15
17
  export declare class MenuOperations {
16
18
  private readonly app;
17
- private readonly ctx;
18
- constructor(app: App, ctx: Context);
19
+ private readonly runner;
20
+ constructor(app: App, ctx: Context, runner?: DefinitionRunner);
19
21
  /** Does what an action of a page says: follows a link, or asks for its fields and runs its operation. */
20
22
  perform(action: Action, where: Where, busy?: ShowBusy, onOutput?: (output: unknown) => void): Promise<Performed>;
21
23
  /** The values of an action's fields, from the editors that fit them (undefined: one was backed out of). */
@@ -66,10 +66,10 @@ function follow(target) {
66
66
  }
67
67
  export class MenuOperations {
68
68
  app;
69
- ctx;
70
- constructor(app, ctx) {
69
+ runner;
70
+ constructor(app, ctx, runner = (id, input, io) => runDefinition(id, ctx, input, io)) {
71
71
  this.app = app;
72
- this.ctx = ctx;
72
+ this.runner = runner;
73
73
  }
74
74
  /** Does what an action of a page says: follows a link, or asks for its fields and runs its operation. */
75
75
  async perform(action, where, busy, onOutput) {
@@ -81,7 +81,14 @@ export class MenuOperations {
81
81
  const answers = await this.fields(action, def, where);
82
82
  if (!answers)
83
83
  return {};
84
- return { reload: true, notice: await this.run(def, { ...action.input, ...answers }, where, busy, onOutput) };
84
+ // An operation that opens a session's terminal (a new shell) leaves the menu to attach to it.
85
+ let opened;
86
+ const notice = await this.run(def, { ...action.input, ...answers }, where, busy, (output) => {
87
+ onOutput?.(output);
88
+ opened = def.kind === 'operation' ? def.opens?.(output) : undefined;
89
+ });
90
+ const exit = opened ? follow(opened).exit : undefined;
91
+ return { reload: true, notice, ...(exit ? { exit } : {}) };
85
92
  }
86
93
  /** The values of an action's fields, from the editors that fit them (undefined: one was backed out of). */
87
94
  async fields(action, def, where) {
@@ -110,7 +117,7 @@ export class MenuOperations {
110
117
  log: (text, tone) => said.push(tone === 'warn' || tone === 'danger' ? Theme.warn(text) : text),
111
118
  });
112
119
  try {
113
- const output = await runDefinition(def.id, this.ctx, input, io);
120
+ const output = await this.runner(def.id, input, io);
114
121
  onOutput?.(output);
115
122
  // The notice is one line: an operation that says several lines (accounts use) runs them together.
116
123
  const text = def.text?.(output)?.replace(/\n+/g, ' ');
@@ -5,20 +5,49 @@ export interface LiveOptions {
5
5
  readonly derive: () => string;
6
6
  /** The line changed: draw again. */
7
7
  readonly onChange: () => void;
8
+ /** How often the line is derived again (and the shortest gap between two process lists). */
8
9
  readonly intervalMs?: number;
10
+ /** The longest gap between two process lists while no sandbox runs and nobody presses a key. */
11
+ readonly idleMaxMs?: number;
12
+ /** Whether a process still runs (default: a signal-0 check, which starts no program). */
13
+ readonly alive?: (pid: number) => boolean;
14
+ /** Whether the process list is needed (default: not inside a sandbox, where "is it running?" is always yes). */
15
+ readonly needsList?: () => boolean;
16
+ /**
17
+ * A cheap fingerprint of what a sandbox start changes (the data folder's run folder): while none runs, a change
18
+ * fetches the list at the next tick instead of after the idle gap.
19
+ */
20
+ readonly signal?: () => string;
9
21
  }
22
+ /** The gap before the next process list after `unchanged` lists in a row found the same: doubling, up to `max`. */
23
+ export declare function listGap(unchanged: number, base: number, max: number): number;
10
24
  export declare class LiveStatus {
11
25
  private readonly o;
12
26
  private tasklist;
27
+ /** The sandbox's processes in the last list (none: no sandbox was running). */
28
+ private pids;
29
+ private fetchedAt;
30
+ /** Lists in a row that found the same answer (the gap to the next one grows with it). */
31
+ private unchanged;
32
+ /** The signal when the list was last fetched. */
33
+ private signalled;
13
34
  private text;
14
35
  private timer;
15
36
  private pending;
16
37
  private running;
17
38
  constructor(o: LiveOptions);
39
+ private get interval();
18
40
  /** The status line as of the last refresh. */
19
41
  line(): string;
20
42
  /** Reads the process list now and derives the line (one fetch at a time; a caller during one waits for it). */
21
43
  refresh(): Promise<void>;
44
+ private fetch;
45
+ private load;
46
+ /** Whether the process list may have changed since it was fetched (checked on every tick, cheaply). */
47
+ private listDue;
48
+ private tick;
49
+ /** A key was pressed: someone is looking, so a list older than one interval is fetched again (after the draw). */
50
+ touch(): void;
22
51
  private derive;
23
52
  /** Starts keeping the line fresh and answers the host's process questions from it (until stop). */
24
53
  start(): Promise<void>;