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,13 +1,17 @@
1
- // The sessions feature's work: the list (as this user may see it under the sharing rules) and restarting the agents.
2
- import { safeText } from '../../../core/util/index.js';
1
+ // The sessions feature's work: the list (as this user may see it under the sharing rules), restarting the agents,
2
+ // opening a shell in a project of the running sandbox and closing a session.
3
+ import { fail, safeText } from '../../../core/util/index.js';
4
+ import { RunDir } from '../../../shared/channel/index.js';
3
5
  import { hostEndpoint } from '../../../host/channel/index.js';
4
6
  import { isSandboxRunning } from '../../../host/sandbox/index.js';
5
7
  import { requireOnPc, requireRunning } from '../../../cli/commands/context.js';
8
+ import { locateHere } from '../../../cli/commands/projects/here.js';
6
9
  import { lookAtSandbox, useSharedSandbox } from '../../../cli/commands/shared-use.js';
7
10
  import { SessionsService } from '../../../cli/services/index.js';
11
+ import { runningProjects } from '../../../cli/services/web-service.js';
8
12
  import { implement, typedConfirm } from '../../context.js';
9
13
  import { cancelled } from '../../kernel/index.js';
10
- import { listSessions, restartSessions } from './sessions.def.js';
14
+ import { closeSession, listSessions, restartSessions, startShell } from './sessions.def.js';
11
15
  /** A listed session as the surfaces see it: no attach address or token, text from the sandbox made safe. */
12
16
  function sessionRow(s) {
13
17
  const { attach: _attach, ...rest } = s;
@@ -56,3 +60,67 @@ export const restartSessionsRun = implement(restartSessions, async (ctx, input,
56
60
  io.progress('Restarting the agents' + (scope.guest ? ' of ' + scope.projectName : ''));
57
61
  return { message: safeText(await service.restart(input.harness ?? null, scope.guest)) };
58
62
  });
63
+ /** Where a new shell opens: the project named, else the one of the current folder, else the only one, else asked. */
64
+ async function shellPlace(ctx, project, io) {
65
+ const projects = runningProjects({ ctx, line: null, limited: false, owner: null });
66
+ if (project !== undefined) {
67
+ const named = projects.find((p) => p.name.toLowerCase() === project.toLowerCase());
68
+ if (!named)
69
+ fail('The running sandbox has no project "' + project + '".', 'sandbox status lists its projects.');
70
+ return { name: named.name, guest: named.guest };
71
+ }
72
+ const here = locateHere(ctx, new RunDir(ctx.dataPath).readSandboxStatus());
73
+ if (here.guestDir)
74
+ return { name: here.project?.name, guest: here.guestDir };
75
+ if (!projects.length)
76
+ fail('The running sandbox has no projects.');
77
+ if (projects.length === 1)
78
+ return projects[0];
79
+ const chosen = await io.choose({
80
+ title: 'Which project?',
81
+ choices: projects.map((p) => ({ value: p.name, label: p.name, description: p.guest })),
82
+ });
83
+ const picked = projects.find((p) => p.name === chosen);
84
+ if (!picked)
85
+ cancelled();
86
+ return picked;
87
+ }
88
+ export const startShellRun = implement(startShell, async (asked, input, io) => {
89
+ requireOnPc('shell');
90
+ requireRunning();
91
+ const ctx = await useSharedSandbox(asked, { kind: 'use' }, typedConfirm(io));
92
+ const place = await shellPlace(ctx, input.project, io);
93
+ io.progress('Opening ' + (input.shell === 'auto' ? 'a shell' : input.shell) + ' in ' + (place.name ?? place.guest));
94
+ const session = await new SessionsService(ctx).openShell(input.shell, place.guest);
95
+ const shell = safeText(session.harness);
96
+ return {
97
+ session: safeText(session.id),
98
+ shell,
99
+ message: 'Opened ' + shell + ' in ' + safeText(place.name ?? place.guest) + ' (session ' + safeText(session.id) + ').',
100
+ };
101
+ });
102
+ export const closeSessionRun = implement(closeSession, async (asked, input, io) => {
103
+ requireOnPc('close');
104
+ requireRunning();
105
+ const find = (ctx) => new SessionsService(ctx).running(null).find((s) => s.id === input.session);
106
+ // Anyone the sandbox is shared with may close a shell; an agent session is its owner's to end.
107
+ const ctx = await useSharedSandbox(asked, (shared) => find(shared)?.kind === 'shell' ? { kind: 'use' } : { kind: 'owner-only', what: 'close an agent session' }, typedConfirm(io));
108
+ const session = find(ctx);
109
+ if (!session)
110
+ fail('No running session ' + input.session + '.', 'sandbox sessions lists the running ones.');
111
+ const what = safeText(session.harness) + (session.kind === 'shell' ? ' (shell)' : '');
112
+ const yes = await io.confirm({
113
+ title: 'Close ' + what + ' session ' + input.session + '?',
114
+ lines: [
115
+ 'It ends, with every program it started.' +
116
+ (session.kind === 'shell' ? '' : ' The agent can pick its conversation up again in a new session.'),
117
+ ],
118
+ yes: 'Close',
119
+ danger: true,
120
+ implied: true,
121
+ });
122
+ if (!yes)
123
+ cancelled();
124
+ io.progress('Closing session ' + input.session);
125
+ return { message: safeText(await new SessionsService(ctx).close(input.session)) };
126
+ });
@@ -22,6 +22,8 @@ export declare class ConfigFile {
22
22
  readonly target: SettingsTarget;
23
23
  readonly file: string;
24
24
  readonly schema: SchemaNode;
25
+ /** The file as read in this synchronous run (see read). */
26
+ private snapshot;
25
27
  constructor(target: SettingsTarget, file: string);
26
28
  static user(configPath: string | null): ConfigFile;
27
29
  static project(dir: string): ConfigFile;
@@ -30,6 +32,11 @@ export declare class ConfigFile {
30
32
  text(): string;
31
33
  /** The value written in this file (undefined = not set here). */
32
34
  valueInFile(key: string[]): unknown;
35
+ /**
36
+ * The file's text and what it parses to, read once per synchronous run: the settings list asks for every setting's
37
+ * value (hundreds of reads), and the next run (after a key, a save or a change outside) reads the file again.
38
+ */
39
+ private read;
33
40
  /** Your own settings with the built-in defaults filled in (what a sandbox uses without a project file). */
34
41
  userConfig(): Config;
35
42
  /** What is in effect: for your file the merged config, for a project file only what the file sets. */
@@ -34,6 +34,8 @@ export class ConfigFile {
34
34
  target;
35
35
  file;
36
36
  schema;
37
+ /** The file as read in this synchronous run (see read). */
38
+ snapshot;
37
39
  constructor(target, file) {
38
40
  this.target = target;
39
41
  this.file = file;
@@ -52,11 +54,25 @@ export class ConfigFile {
52
54
  return fs.existsSync(this.file);
53
55
  }
54
56
  text() {
55
- return this.exists ? fs.readFileSync(this.file, 'utf8') : '';
57
+ return this.read().text;
56
58
  }
57
59
  /** The value written in this file (undefined = not set here). */
58
60
  valueInFile(key) {
59
- return fileValue(this.text(), key);
61
+ const value = _.get(this.read().parsed, key);
62
+ // A copy: the parsed file is shared by every read in this run.
63
+ return value !== null && typeof value === 'object' ? structuredClone(value) : value;
64
+ }
65
+ /**
66
+ * The file's text and what it parses to, read once per synchronous run: the settings list asks for every setting's
67
+ * value (hundreds of reads), and the next run (after a key, a save or a change outside) reads the file again.
68
+ */
69
+ read() {
70
+ if (!this.snapshot) {
71
+ const text = this.exists ? fs.readFileSync(this.file, 'utf8') : '';
72
+ this.snapshot = { text, parsed: parse(text) ?? {} };
73
+ queueMicrotask(() => (this.snapshot = undefined));
74
+ }
75
+ return this.snapshot;
60
76
  }
61
77
  /** Your own settings with the built-in defaults filled in (what a sandbox uses without a project file). */
62
78
  userConfig() {
@@ -100,6 +116,7 @@ export class ConfigFile {
100
116
  return 'unchanged';
101
117
  fs.mkdirSync(path.dirname(this.file), { recursive: true });
102
118
  fs.writeFileSync(this.file, text, 'utf8');
119
+ this.snapshot = undefined;
103
120
  return 'saved';
104
121
  }
105
122
  }
@@ -46,9 +46,17 @@ export function masked(key, value) {
46
46
  return undefined;
47
47
  return _.get(redactSecrets(_.set({}, key, structuredClone(value))), key);
48
48
  }
49
+ /** The keys a project file's schema can set, worked out once per schema (a list asks for it on every row). */
50
+ const projectKeysOf = new WeakMap();
49
51
  /** The keys a project file can set. */
50
52
  function projectKeys(files) {
51
- return new Set(files.project.schema.leaves().map((l) => l.key.join('.')));
53
+ const schema = files.project.schema;
54
+ let keys = projectKeysOf.get(schema);
55
+ if (!keys) {
56
+ keys = new Set(schema.leaves().map((l) => l.key.join('.')));
57
+ projectKeysOf.set(schema, keys);
58
+ }
59
+ return keys;
52
60
  }
53
61
  /** One setting's row in a scope (`config` and `inProject` can be passed when making many rows). */
54
62
  export function settingRow(s, files, scope, source = SAVED, config = files.user.userConfig(), inProject = projectKeys(files)) {
@@ -94,6 +94,14 @@ export declare const getSetting: import("../../kernel/define.js").QueryDef<z.Zod
94
94
  key: z.ZodString;
95
95
  value: z.ZodUnknown;
96
96
  }, z.core.$strip>, "settings.get">;
97
+ /**
98
+ * The settings someone with control (not the owner) may change: how attaching and reopening sessions behave, in your
99
+ * own settings file. Everything else (folders, agents, secrets, network, storage, sharing, the audit log, remote
100
+ * access) changes what the sandbox can reach or who can come in, so it is the owner's (docs/design/remote-access.md).
101
+ */
102
+ export declare const CONTROL_SETTINGS: readonly string[];
103
+ /** settings.set / settings.reset: control for CONTROL_SETTINGS in the user file, owner for everything else. */
104
+ export declare function controlSettingAccess(input: unknown): 'control' | 'owner';
97
105
  export declare const setSetting: import("../../kernel/define.js").OperationDef<z.ZodObject<{
98
106
  scope: z.ZodDefault<z.ZodEnum<{
99
107
  project: "project";
@@ -65,8 +65,24 @@ export const getSetting = defineQuery({
65
65
  surfaces: { except: { menu: 'Settings shows every value', web: 'the Settings page shows every value' } },
66
66
  cli: { name: 'config', action: 'get', positional: ['key'] },
67
67
  });
68
+ /**
69
+ * The settings someone with control (not the owner) may change: how attaching and reopening sessions behave, in your
70
+ * own settings file. Everything else (folders, agents, secrets, network, storage, sharing, the audit log, remote
71
+ * access) changes what the sandbox can reach or who can come in, so it is the owner's (docs/design/remote-access.md).
72
+ */
73
+ export const CONTROL_SETTINGS = ['attach.', 'resume.'];
74
+ /** settings.set / settings.reset: control for CONTROL_SETTINGS in the user file, owner for everything else. */
75
+ export function controlSettingAccess(input) {
76
+ const o = (input ?? {});
77
+ const key = typeof o.key === 'string' ? o.key : '';
78
+ const userFile = o.scope === undefined || o.scope === 'user';
79
+ return userFile && CONTROL_SETTINGS.some((prefix) => key.startsWith(prefix) && key.length > prefix.length)
80
+ ? 'control'
81
+ : 'owner';
82
+ }
68
83
  export const setSetting = defineOperation({
69
84
  id: 'settings.set',
85
+ access: controlSettingAccess,
70
86
  title: 'Change a setting',
71
87
  summary: 'change one setting (text is read like the command line: true/on, numbers, JSON)',
72
88
  input: z.object({
@@ -80,6 +96,7 @@ export const setSetting = defineOperation({
80
96
  });
81
97
  export const resetSetting = defineOperation({
82
98
  id: 'settings.reset',
99
+ access: controlSettingAccess,
83
100
  title: 'Reset a setting',
84
101
  summary: 'put one setting back to its default (in a project file: remove it)',
85
102
  input: z.object({ scope: ScopeSchema.default('user'), key: KeySchema }),
@@ -4,12 +4,18 @@ export declare const WebStatusSchema: z.ZodObject<{
4
4
  running: z.ZodBoolean;
5
5
  port: z.ZodNullable<z.ZodNumber>;
6
6
  address: z.ZodNullable<z.ZodString>;
7
+ lanAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
8
+ lanFingerprint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
+ lanProblem: z.ZodOptional<z.ZodNullable<z.ZodString>>;
7
10
  }, z.core.$strip>;
8
11
  export type WebStatus = z.output<typeof WebStatusSchema>;
9
12
  export declare const webStatus: import("../../kernel/define.js").QueryDef<z.ZodObject<{}, z.core.$strip>, z.ZodObject<{
10
13
  running: z.ZodBoolean;
11
14
  port: z.ZodNullable<z.ZodNumber>;
12
15
  address: z.ZodNullable<z.ZodString>;
16
+ lanAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
17
+ lanFingerprint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
+ lanProblem: z.ZodOptional<z.ZodNullable<z.ZodString>>;
13
19
  }, z.core.$strip>, "web.status">;
14
20
  export declare const startWeb: import("../../kernel/define.js").OperationDef<z.ZodObject<{}, z.core.$strip>, z.ZodObject<{
15
21
  port: z.ZodNumber;
@@ -29,5 +35,8 @@ export declare const webView: import("../../kernel/define.js").ViewDef<{
29
35
  running: z.ZodBoolean;
30
36
  port: z.ZodNullable<z.ZodNumber>;
31
37
  address: z.ZodNullable<z.ZodString>;
38
+ lanAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ lanFingerprint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
40
+ lanProblem: z.ZodOptional<z.ZodNullable<z.ZodString>>;
32
41
  }, z.core.$strip>, "web.status">;
33
42
  }, Record<string, never>, "web">;
@@ -8,6 +8,13 @@ export const WebStatusSchema = z.object({
8
8
  running: z.boolean().describe('The web UI server answers'),
9
9
  port: z.number().nullable().describe('Its port on 127.0.0.1, when it runs'),
10
10
  address: z.string().nullable().describe('Its address (without the login token), when it runs'),
11
+ lanAddress: z
12
+ .string()
13
+ .nullable()
14
+ .optional()
15
+ .describe('Its address on your network for paired devices (LAN mode), when it listens there'),
16
+ lanFingerprint: z.string().nullable().optional().describe("The LAN certificate's SHA-256 fingerprint"),
17
+ lanProblem: z.string().nullable().optional().describe('Why LAN mode is on but not listening'),
11
18
  });
12
19
  const MessageSchema = z.object({ message: z.string().describe('What happened') });
13
20
  export const webStatus = defineQuery({
@@ -18,7 +25,11 @@ export const webStatus = defineQuery({
18
25
  output: WebStatusSchema,
19
26
  surfaces: NOT_ON_WEB,
20
27
  text: (o) => o.running && o.port
21
- ? 'The web UI is running at http://127.0.0.1:' + o.port + '/ (sandbox web open signs you in).'
28
+ ? 'The web UI is running at http://127.0.0.1:' +
29
+ o.port +
30
+ '/ (sandbox web open signs you in).' +
31
+ (o.lanAddress ? '\nOn your network: ' + o.lanAddress + ' (certificate ' + o.lanFingerprint + ')' : '') +
32
+ (o.lanProblem ? '\nLAN mode is on but not listening: ' + o.lanProblem : '')
22
33
  : 'The web UI is not running (sandbox web start).',
23
34
  cli: { name: 'web', action: 'status', json: true },
24
35
  });
@@ -73,6 +84,20 @@ export function webPage(o) {
73
84
  tone: o.running ? 'ok' : 'muted',
74
85
  },
75
86
  ...(o.address ? [{ id: 'address', label: 'Address', value: o.address + ' (this PC only)' }] : []),
87
+ ...(o.lanAddress
88
+ ? [
89
+ {
90
+ id: 'lan',
91
+ label: 'On your network',
92
+ value: o.lanAddress,
93
+ description: 'Type this into the phone, then enter the code from Remote access > Allow a new device.',
94
+ details: o.lanFingerprint ? ['Certificate ' + o.lanFingerprint] : undefined,
95
+ },
96
+ ]
97
+ : []),
98
+ ...(o.lanProblem
99
+ ? [{ id: 'lan-problem', label: 'LAN mode', value: o.lanProblem, tone: 'warn' }]
100
+ : []),
76
101
  ],
77
102
  actions: o.running ? [open, stop] : [start, open],
78
103
  notes: [{ text: 'Loopback only, behind a token. Open signs you in.', tone: 'muted' }],
@@ -10,6 +10,9 @@ export declare const webStatusRun: import("../../kernel/define.js").Implementati
10
10
  running: import("zod").ZodBoolean;
11
11
  port: import("zod").ZodNullable<import("zod").ZodNumber>;
12
12
  address: import("zod").ZodNullable<import("zod").ZodString>;
13
+ lanAddress: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
14
+ lanFingerprint: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
15
+ lanProblem: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
13
16
  }, import("zod/v4/core").$strip>, "web.status">, import("../../../cli/commands/context.js").Context>;
14
17
  export declare const startWebRun: import("../../kernel/define.js").Implementation<import("../../kernel/define.js").OperationDef<import("zod").ZodObject<{}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
15
18
  port: import("zod").ZodNumber;
@@ -60,7 +60,15 @@ export const webStatusRun = implement(webStatus, async (ctx) => {
60
60
  requireOnPc('web');
61
61
  const { running, state } = await controlOf(ctx).status();
62
62
  const port = running && state ? state.port : null;
63
- return { running, port, address: port ? 'http://127.0.0.1:' + port + '/' : null };
63
+ const lan = running ? (state?.lan ?? null) : null;
64
+ return {
65
+ running,
66
+ port,
67
+ address: port ? 'http://127.0.0.1:' + port + '/' : null,
68
+ lanAddress: lan?.url ?? null,
69
+ lanFingerprint: lan?.fingerprint ?? null,
70
+ lanProblem: lan?.problem ?? null,
71
+ };
64
72
  });
65
73
  export const startWebRun = implement(startWeb, async (ctx, _input, io) => {
66
74
  requireOnPc('web');
@@ -73,8 +81,8 @@ export const openWebRun = implement(openWeb, async (ctx, _input, io) => {
73
81
  const started = await startServer(ctx, io, control);
74
82
  if (started.wasRunning)
75
83
  io.log(started.message);
76
- const { state } = await control.status();
77
- if (!state)
84
+ const { running, state } = await control.status();
85
+ if (!running || !state)
78
86
  return fail('The web UI stopped right away.', 'Run sandbox web start again.');
79
87
  openBrowser(control.url(state));
80
88
  return { message: 'Opened in your browser.' };
@@ -0,0 +1,33 @@
1
+ /** What a definition needs. Default (fails closed): a query needs view, an operation owner. */
2
+ export type Access = 'view' | 'control' | 'owner';
3
+ /** A role the owner can give: view only, or full control (everything but the owner's own things). */
4
+ export type RemoteRole = 'view' | 'control';
5
+ /** Who runs a definition: the owner, or someone the owner let in. */
6
+ export type Caller = {
7
+ readonly kind: 'owner';
8
+ } | {
9
+ readonly kind: 'windows';
10
+ readonly user: string;
11
+ readonly role: RemoteRole;
12
+ } | {
13
+ readonly kind: 'device';
14
+ readonly id: string;
15
+ readonly name: string;
16
+ readonly role: RemoteRole;
17
+ };
18
+ export declare const OWNER: Caller;
19
+ /** A definition's need, as written: fixed, or from its input (settings.set on a web.* key needs owner). */
20
+ export type AccessRule = Access | ((input: unknown) => Access);
21
+ /** The most a caller may do. */
22
+ export declare function callerAccess(caller: Caller): Access;
23
+ /** The caller may run something that needs `need`. */
24
+ export declare function allows(caller: Caller, need: Access): boolean;
25
+ /** What a definition needs for this input. A rule that throws on a strange input needs owner. */
26
+ export declare function accessNeeded(def: {
27
+ readonly kind: 'operation' | 'query';
28
+ readonly access?: AccessRule;
29
+ }, input: unknown): Access;
30
+ /** Who the caller is, in a few words (the audit log, the page's header). */
31
+ export declare function callerLabel(caller: Caller): string;
32
+ /** The refusal text for a caller that may not. */
33
+ export declare function refusalText(caller: Caller, need: Access): string;
@@ -0,0 +1,44 @@
1
+ // Who may run what (docs/design/remote-access.md): every definition needs view, control or owner; the caller of a run
2
+ // has a role. The owner (at the PC, or through the token link) may do everything; another Windows user of this PC or a
3
+ // paired device has the role the owner gave them, never owner. Pure (the web bundle shows what the role allows).
4
+ export const OWNER = { kind: 'owner' };
5
+ const RANK = { view: 0, control: 1, owner: 2 };
6
+ /** The most a caller may do. */
7
+ export function callerAccess(caller) {
8
+ return caller.kind === 'owner' ? 'owner' : caller.role;
9
+ }
10
+ /** The caller may run something that needs `need`. */
11
+ export function allows(caller, need) {
12
+ return RANK[callerAccess(caller)] >= RANK[need];
13
+ }
14
+ /** What a definition needs for this input. A rule that throws on a strange input needs owner. */
15
+ export function accessNeeded(def, input) {
16
+ const rule = def.access;
17
+ if (rule === undefined)
18
+ return def.kind === 'query' ? 'view' : 'owner';
19
+ if (typeof rule === 'string')
20
+ return rule;
21
+ try {
22
+ return rule(input);
23
+ }
24
+ catch {
25
+ return 'owner';
26
+ }
27
+ }
28
+ /** Who the caller is, in a few words (the audit log, the page's header). */
29
+ export function callerLabel(caller) {
30
+ switch (caller.kind) {
31
+ case 'owner':
32
+ return 'you';
33
+ case 'windows':
34
+ return 'Windows user ' + caller.user;
35
+ case 'device':
36
+ return 'device ' + caller.name;
37
+ }
38
+ }
39
+ /** The refusal text for a caller that may not. */
40
+ export function refusalText(caller, need) {
41
+ if (need === 'owner')
42
+ return 'Only the owner of this PC can do that, at the PC.';
43
+ return callerAccess(caller) === 'view' ? 'Your access is view only.' : 'You may not do that.';
44
+ }
@@ -1,6 +1,7 @@
1
1
  import type { z } from 'zod';
2
+ import type { AccessRule } from './access.js';
2
3
  import type { OperationIO } from './io.js';
3
- import type { Page } from './view.js';
4
+ import type { Page, Target } from './view.js';
4
5
  export type SurfaceName = 'cli' | 'menu' | 'web';
5
6
  export declare const SURFACE_NAMES: readonly SurfaceName[];
6
7
  /** Where a definition is offered: everywhere, or everywhere but these surfaces (each with the reason). */
@@ -63,6 +64,11 @@ interface Definition<Id extends string, I extends z.ZodType, O extends z.ZodType
63
64
  readonly output: O;
64
65
  /** Default: every surface. */
65
66
  readonly surfaces?: Surfaces;
67
+ /**
68
+ * What a caller other than the owner needs (docs/design/remote-access.md). Default (fails closed): view for a query,
69
+ * owner for an operation; an operation says 'control' to be open to someone the owner gave control.
70
+ */
71
+ readonly access?: AccessRule;
66
72
  readonly cli?: CliHint;
67
73
  /** The output in one text: what an operation did, or a value a query read (CLI output, menu notice, web note). */
68
74
  text?(output: z.output<O>): string;
@@ -77,6 +83,13 @@ export interface OperationDef<I extends z.ZodType = z.ZodType, O extends z.ZodTy
77
83
  readonly kind: 'operation';
78
84
  /** Shown as dangerous (red); it asks for its own confirmation through io.confirm. */
79
85
  readonly danger?: boolean;
86
+ /** Its result must stay on screen until dismissed (a one-time code to type elsewhere): the web keeps it open. */
87
+ readonly keepResult?: boolean;
88
+ /**
89
+ * Where every surface goes once it succeeded: a new session's terminal is attached on the command line and in the
90
+ * menu, and opened in the web UI.
91
+ */
92
+ opens?(output: z.output<O>): Target | undefined;
80
93
  }
81
94
  export interface QueryDef<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType, Id extends string = string> extends Definition<Id, I, O> {
82
95
  readonly kind: 'query';
@@ -1,3 +1,4 @@
1
+ export * from './access.js';
1
2
  export * from './define.js';
2
3
  export * from './errors.js';
3
4
  export * from './fields.js';
@@ -1,5 +1,6 @@
1
1
  // The kernel of the shared core: pure (zod and nothing of Node, the terminal or the DOM), so the CLI, the menu, the
2
2
  // web server and the browser all import the same definitions, view model, form fields, questions and RPC messages.
3
+ export * from './access.js';
3
4
  export * from './define.js';
4
5
  export * from './errors.js';
5
6
  export * from './fields.js';
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { Access, Caller } from './access.js';
2
3
  import { type OperationIO } from './io.js';
3
4
  /** One call or subscription on the socket (chosen by the browser, unique while it runs). */
4
5
  export declare const RpcIdSchema: z.ZodString;
@@ -168,6 +169,8 @@ export interface RpcHost {
168
169
  kind(id: string): 'operation' | 'query' | undefined;
169
170
  /** How often a query is read again while subscribed (undefined: once). */
170
171
  liveMs(id: string): number | undefined;
171
- /** Runs an allowed definition (input and output checked). */
172
- run(id: string, input: unknown, io: OperationIO): Promise<unknown>;
172
+ /** What a caller other than the owner needs to run it with this input (undefined: not here). */
173
+ need(id: string, input: unknown): Access | undefined;
174
+ /** Runs an allowed definition (input and output checked); refused when `caller` (default: the owner) may not. */
175
+ run(id: string, input: unknown, io: OperationIO, caller?: Caller): Promise<unknown>;
173
176
  }
@@ -6,5 +6,8 @@ export declare function implementationOf(id: string): Implementation<AnyDefiniti
6
6
  export declare function runDefinition(id: string, ctx: AppContext, input: unknown, io: OperationIO): Promise<unknown>;
7
7
  /** Reads a query in-process (the menu): typed output, and no questions. */
8
8
  export declare function readQuery<D extends QueryDef>(def: D, ctx: AppContext, input?: unknown): Promise<OutputOf<D>>;
9
- /** What the web server may run: the registered definitions whose surfaces include the web. */
9
+ /**
10
+ * What the web server may run: the registered definitions whose surfaces include the web, each refused to a caller
11
+ * whose role does not reach what it needs (docs/design/remote-access.md).
12
+ */
10
13
  export declare function createRpcHost(ctx: AppContext): RpcHost;
@@ -1,4 +1,5 @@
1
- import { invoke, NO_QUESTIONS, onSurface, } from './kernel/index.js';
1
+ import { fail } from '../core/util/errors.js';
2
+ import { accessNeeded, allows, invoke, OWNER, refusalText, NO_QUESTIONS, onSurface, } from './kernel/index.js';
2
3
  import { addAgentRun, listAgentsRun, makeDefaultAgentRun, removeAgentRun, startAgentRun, } from './features/agents/index.js';
3
4
  import { defaultAccountRun, listAccountsRun, removeAccountRun, renameAccountRun, saveAccountRun, useAccountRun, } from './features/accounts/index.js';
4
5
  import { listAppsRun, requestAppRun, setCustomAppsRun, toggleAppRun } from './features/apps/index.js';
@@ -10,13 +11,14 @@ import { checkHealthRun, enableSandboxRun, lastHealthRun, repairHealthRun } from
10
11
  import { hideWindowRun, sandboxStatusRun, showWindowRun, stopSandboxRun } from './features/sandbox/index.js';
11
12
  import { listLogsRun, tailLogRun } from './features/logs/index.js';
12
13
  import { listAuditRun } from './features/audit/index.js';
13
- import { listSessionsRun, restartSessionsRun } from './features/sessions/index.js';
14
+ import { closeSessionRun, listSessionsRun, restartSessionsRun, startShellRun } from './features/sessions/index.js';
14
15
  import { exportConfigRun, getSettingRun, importConfigRun, listSettingsRun, resetSettingRun, setSettingRun, } from './features/settings/index.js';
15
16
  import { migrateSettingsRun, runUpdateRun, updatesStatusRun } from './features/updates/index.js';
16
17
  import { checkEngineRun, cleanEngineRun, engineLeftoversRun, engineStatusRun } from './features/engine/index.js';
17
18
  import { openWebRun, startWebRun, stopWebRun, webStatusRun } from './features/web/index.js';
18
19
  import { vpnLoginRun, vpnLogoutRun, vpnStatusRun } from './features/vpn/index.js';
19
20
  import { githubLoginRun, githubLogoutRun, githubStatusRun } from './features/github/index.js';
21
+ import { accessStatusRun, allowDeviceRun, lanOffRun, lanOnRun, revokeAccessRun, revokeAllAccessRun, } from './features/access/index.js';
20
22
  export const IMPLEMENTATIONS = [
21
23
  sandboxStatusRun,
22
24
  showWindowRun,
@@ -24,6 +26,8 @@ export const IMPLEMENTATIONS = [
24
26
  stopSandboxRun,
25
27
  listSessionsRun,
26
28
  restartSessionsRun,
29
+ startShellRun,
30
+ closeSessionRun,
27
31
  startAgentRun,
28
32
  addAgentRun,
29
33
  makeDefaultAgentRun,
@@ -88,6 +92,12 @@ export const IMPLEMENTATIONS = [
88
92
  githubLoginRun,
89
93
  githubLogoutRun,
90
94
  webStatusRun,
95
+ accessStatusRun,
96
+ allowDeviceRun,
97
+ revokeAccessRun,
98
+ revokeAllAccessRun,
99
+ lanOnRun,
100
+ lanOffRun,
91
101
  ];
92
102
  export function implementationOf(id) {
93
103
  return IMPLEMENTATIONS.find((i) => i.def.id === id);
@@ -103,7 +113,10 @@ export function runDefinition(id, ctx, input, io) {
103
113
  export async function readQuery(def, ctx, input = {}) {
104
114
  return (await runDefinition(def.id, ctx, input, NO_QUESTIONS));
105
115
  }
106
- /** What the web server may run: the registered definitions whose surfaces include the web. */
116
+ /**
117
+ * What the web server may run: the registered definitions whose surfaces include the web, each refused to a caller
118
+ * whose role does not reach what it needs (docs/design/remote-access.md).
119
+ */
107
120
  export function createRpcHost(ctx) {
108
121
  const allowed = (id) => {
109
122
  const impl = implementationOf(id);
@@ -115,10 +128,17 @@ export function createRpcHost(ctx) {
115
128
  const def = allowed(id)?.def;
116
129
  return def?.kind === 'query' ? def.live?.everyMs : undefined;
117
130
  },
118
- run: (id, input, io) => {
131
+ need: (id, input) => {
132
+ const impl = allowed(id);
133
+ return impl ? accessNeeded(impl.def, input) : undefined;
134
+ },
135
+ run: async (id, input, io, caller = OWNER) => {
119
136
  const impl = allowed(id);
120
137
  if (!impl)
121
138
  throw new Error('Not allowed on the web: ' + id);
139
+ const need = accessNeeded(impl.def, input);
140
+ if (!allows(caller, need))
141
+ fail(refusalText(caller, need));
122
142
  return invoke(impl, ctx, input, io);
123
143
  },
124
144
  };
@@ -1,3 +1,9 @@
1
1
  #!/usr/bin/env node
2
- 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/agent-sandbox.ts
3
9
  run(process.argv.slice(2));
@@ -0,0 +1,7 @@
1
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
2
+ import {
3
+ attach
4
+ } from "./chunk-VENWSGX2.js";
5
+ export {
6
+ attach
7
+ };