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
@@ -131,6 +131,7 @@ export const lastHealth = defineQuery({
131
131
  });
132
132
  export const checkHealth = defineOperation({
133
133
  id: 'health.check',
134
+ access: 'control',
134
135
  title: 'Check this PC',
135
136
  summary: 'check this PC, the install, your settings and the sandbox (changes nothing)',
136
137
  input: z.object({ harness: z.string().optional().describe('agent to check') }),
@@ -91,6 +91,7 @@ export const sandboxStatus = defineQuery({
91
91
  });
92
92
  export const showWindow = defineOperation({
93
93
  id: 'sandbox.showWindow',
94
+ access: 'control',
94
95
  title: 'Show the window',
95
96
  summary: 'show the sandbox window',
96
97
  input: z.object({}),
@@ -100,6 +101,7 @@ export const showWindow = defineOperation({
100
101
  });
101
102
  export const hideWindow = defineOperation({
102
103
  id: 'sandbox.hideWindow',
104
+ access: 'control',
103
105
  title: 'Hide the window',
104
106
  summary: 'hide the sandbox window (it keeps running)',
105
107
  input: z.object({}),
@@ -109,6 +111,7 @@ export const hideWindow = defineOperation({
109
111
  });
110
112
  export const stopSandbox = defineOperation({
111
113
  id: 'sandbox.stop',
114
+ access: 'control',
112
115
  title: 'Stop the sandbox…',
113
116
  summary: "shut the sandbox down (every project's sessions end)",
114
117
  input: z.object({}),
@@ -5,6 +5,10 @@ export declare const SessionRowSchema: z.ZodObject<{
5
5
  id: z.ZodString;
6
6
  launchId: z.ZodString;
7
7
  harness: z.ZodString;
8
+ kind: z.ZodOptional<z.ZodEnum<{
9
+ shell: "shell";
10
+ agent: "agent";
11
+ }>>;
8
12
  cwd: z.ZodString;
9
13
  project: z.ZodOptional<z.ZodString>;
10
14
  phase: z.ZodEnum<{
@@ -36,6 +40,10 @@ export declare const SessionsListSchema: z.ZodObject<{
36
40
  id: z.ZodString;
37
41
  launchId: z.ZodString;
38
42
  harness: z.ZodString;
43
+ kind: z.ZodOptional<z.ZodEnum<{
44
+ shell: "shell";
45
+ agent: "agent";
46
+ }>>;
39
47
  cwd: z.ZodString;
40
48
  project: z.ZodOptional<z.ZodString>;
41
49
  phase: z.ZodEnum<{
@@ -74,6 +82,10 @@ export declare const listSessions: import("../../kernel/define.js").QueryDef<z.Z
74
82
  id: z.ZodString;
75
83
  launchId: z.ZodString;
76
84
  harness: z.ZodString;
85
+ kind: z.ZodOptional<z.ZodEnum<{
86
+ shell: "shell";
87
+ agent: "agent";
88
+ }>>;
77
89
  cwd: z.ZodString;
78
90
  project: z.ZodOptional<z.ZodString>;
79
91
  phase: z.ZodEnum<{
@@ -102,6 +114,26 @@ export declare const restartSessions: import("../../kernel/define.js").Operation
102
114
  }, z.core.$strip>, z.ZodObject<{
103
115
  message: z.ZodString;
104
116
  }, z.core.$strip>, "sessions.restart">;
117
+ /** The shells a shell session can run; auto: PowerShell 7 when the sandbox has it, else Windows PowerShell. */
118
+ export declare const SHELL_CHOICES: readonly ["auto", "pwsh", "powershell", "cmd"];
119
+ export declare const startShell: import("../../kernel/define.js").OperationDef<z.ZodObject<{
120
+ project: z.ZodOptional<z.ZodString>;
121
+ shell: z.ZodDefault<z.ZodEnum<{
122
+ pwsh: "pwsh";
123
+ auto: "auto";
124
+ powershell: "powershell";
125
+ cmd: "cmd";
126
+ }>>;
127
+ }, z.core.$strip>, z.ZodObject<{
128
+ session: z.ZodString;
129
+ shell: z.ZodString;
130
+ message: z.ZodString;
131
+ }, z.core.$strip>, "sessions.shell">;
132
+ export declare const closeSession: import("../../kernel/define.js").OperationDef<z.ZodObject<{
133
+ session: z.ZodString;
134
+ }, z.core.$strip>, z.ZodObject<{
135
+ message: z.ZodString;
136
+ }, z.core.$strip>, "sessions.close">;
105
137
  /** The sessions page: one list (a table on the command line), this folder's project first. */
106
138
  export declare function sessionsPage(o: SessionsList): Page;
107
139
  export declare const sessionsView: import("../../kernel/define.js").ViewDef<{
@@ -116,6 +148,10 @@ export declare const sessionsView: import("../../kernel/define.js").ViewDef<{
116
148
  id: z.ZodString;
117
149
  launchId: z.ZodString;
118
150
  harness: z.ZodString;
151
+ kind: z.ZodOptional<z.ZodEnum<{
152
+ shell: "shell";
153
+ agent: "agent";
154
+ }>>;
119
155
  cwd: z.ZodString;
120
156
  project: z.ZodOptional<z.ZodString>;
121
157
  phase: z.ZodEnum<{
@@ -1,6 +1,6 @@
1
- // The agent sessions of the running sandbox: the list (live) and restarting the agents. Pure: schemas, labels and the
2
- // view; sessions.run.ts does the work. A running session links to its terminal (attach in the menu, the terminal page
3
- // in the web UI).
1
+ // The sessions of the running sandbox, agents and plain shells: the list (live), restarting the agents, opening a shell
2
+ // and closing a session. Pure: schemas, labels and the view; sessions.run.ts does the work. A running session links to
3
+ // its terminal (attach in the menu and on the command line, the terminal page in the web UI).
4
4
  import { z } from 'zod';
5
5
  import { action, ago, defineOperation, defineQuery, defineView, link, } from '../../kernel/index.js';
6
6
  const ViewersSchema = z.object({ viewers: z.number(), remote: z.number() });
@@ -8,7 +8,9 @@ const ViewersSchema = z.object({ viewers: z.number(), remote: z.number() });
8
8
  export const SessionRowSchema = z.object({
9
9
  id: z.string().describe('The session'),
10
10
  launchId: z.string(),
11
- harness: z.string().describe('The agent'),
11
+ harness: z.string().describe('The agent, or the shell (pwsh, powershell, cmd)'),
12
+ /** A plain shell rather than an agent (absent: an agent). */
13
+ kind: z.enum(['agent', 'shell']).optional(),
12
14
  cwd: z.string().describe('Its folder in the sandbox'),
13
15
  project: z.string().optional(),
14
16
  phase: z.enum(['starting', 'running', 'exited', 'failed']).describe('What it is doing'),
@@ -50,7 +52,7 @@ export function orderedSessions(list) {
50
52
  export const listSessions = defineQuery({
51
53
  id: 'sessions.list',
52
54
  title: 'Sessions',
53
- summary: 'list agent sessions and who is attached',
55
+ summary: 'list sessions (agents and shells) and who is attached',
54
56
  input: z.object({ wide: z.boolean().default(false).describe("also show each session's folder") }),
55
57
  output: SessionsListSchema,
56
58
  live: { everyMs: 2500 },
@@ -65,6 +67,7 @@ export const listSessions = defineQuery({
65
67
  });
66
68
  export const restartSessions = defineOperation({
67
69
  id: 'sessions.restart',
70
+ access: 'control',
68
71
  title: 'Restart agents',
69
72
  summary: "restart this project's running agents in place; they resume their conversation",
70
73
  input: z.object({
@@ -82,19 +85,71 @@ export const restartSessions = defineOperation({
82
85
  },
83
86
  },
84
87
  });
88
+ /** Agent and project names as the web UI has always accepted them. */
89
+ const NameText = z.string().regex(/^[A-Za-z0-9_.-]{1,64}$/);
90
+ const SessionId = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
91
+ /** The shells a shell session can run; auto: PowerShell 7 when the sandbox has it, else Windows PowerShell. */
92
+ export const SHELL_CHOICES = ['auto', 'pwsh', 'powershell', 'cmd'];
93
+ /** The session the operation opened, as a link to its terminal. */
94
+ function openedSession(o) {
95
+ return { kind: 'terminal', session: o.session };
96
+ }
97
+ export const startShell = defineOperation({
98
+ id: 'sessions.shell',
99
+ title: 'New shell',
100
+ summary: 'open PowerShell or cmd in a project of the running sandbox, as a session you can attach to',
101
+ input: z.object({
102
+ project: NameText.optional().describe("The project of the running sandbox (default: this folder's, else asked)"),
103
+ shell: z
104
+ .enum(SHELL_CHOICES)
105
+ .default('auto')
106
+ .describe('The shell: pwsh, powershell or cmd (default: pwsh if present)'),
107
+ }),
108
+ output: z.object({
109
+ session: z.string().describe('The new session'),
110
+ shell: z.string().describe('The shell it runs'),
111
+ message: z.string().describe('What happened'),
112
+ }),
113
+ // A shell runs anything as you: someone you let in remotely may type into one you opened, never open one.
114
+ access: 'owner',
115
+ text: (o) => o.message,
116
+ opens: openedSession,
117
+ cli: {
118
+ name: 'shell',
119
+ group: 'Sessions',
120
+ positional: ['project'],
121
+ flags: { shell: { flag: '--shell', value: 'name', choices: ['pwsh', 'powershell', 'cmd'] } },
122
+ json: true,
123
+ examples: [
124
+ "sandbox shell PowerShell in this folder's project, in this terminal (Ctrl+] detaches)",
125
+ 'sandbox shell api --shell cmd cmd in the project api',
126
+ ],
127
+ },
128
+ });
129
+ export const closeSession = defineOperation({
130
+ id: 'sessions.close',
131
+ title: 'Close',
132
+ summary: 'end a running session (an agent or a shell) and every program it started',
133
+ input: z.object({ session: SessionId.describe('The session to close') }),
134
+ output: z.object({ message: z.string().describe("The sandbox's answer") }),
135
+ access: 'control',
136
+ danger: true,
137
+ text: (o) => o.message,
138
+ cli: { name: 'close', group: 'Sessions', positional: ['session'], examples: ['sandbox close 4f2c9a'] },
139
+ });
85
140
  function sessionItem(s, wide) {
86
141
  const state = s.phase + (s.exitCode !== undefined ? ' (' + s.exitCode + ')' : '');
87
142
  const running = s.phase === 'running';
88
143
  return {
89
144
  id: s.id,
90
- label: s.harness + ' ' + (s.projectName ?? '-'),
145
+ label: (s.kind === 'shell' ? s.harness + ' (shell)' : s.harness) + ' ' + (s.projectName ?? '-'),
91
146
  hint: state + ' ' + ago(s.startedAt),
92
147
  badges: [{ text: s.phase, tone: running ? 'ok' : s.phase === 'failed' ? 'danger' : 'muted' }],
93
148
  group: s.here ? "This folder's project" : 'Other sessions',
94
149
  marked: s.here,
95
150
  icon: s.harness,
96
151
  description: running
97
- ? "Opens this session's terminal (in a terminal, Ctrl+] detaches; the session keeps running)."
152
+ ? "Opens this session's terminal (in a terminal, Ctrl+] detaches; the session keeps running). x closes it."
98
153
  : undefined,
99
154
  details: ['Session ' + s.id, 'Folder ' + s.cwd, 'Attached ' + viewerText(s.viewers)],
100
155
  cells: {
@@ -108,6 +163,9 @@ function sessionItem(s, wide) {
108
163
  started: ago(s.startedAt),
109
164
  },
110
165
  ...(running && s.canOpen ? { primary: link({ kind: 'terminal', session: s.id }, 'Open', { verb: 'attach' }) } : {}),
166
+ ...(running
167
+ ? { actions: [action(closeSession, { session: s.id }, { key: 'x', verb: 'close', importance: 1 })] }
168
+ : {}),
111
169
  ...(running ? {} : { disabled: 'This session has ended.' }),
112
170
  ...(running && !s.canOpen ? { disabled: 'Not reachable from this PC.' } : {}),
113
171
  };
@@ -150,6 +208,16 @@ export function sessionsPage(o) {
150
208
  ],
151
209
  actions: [
152
210
  ...(o.sandboxRunning ? [{ ...start, label: 'New session', key: 'n', verb: 'new session', importance: 1 }] : []),
211
+ ...(o.sandboxRunning
212
+ ? [
213
+ action(startShell, { shell: 'auto' }, {
214
+ key: 's',
215
+ verb: 'new shell',
216
+ importance: 1,
217
+ description: 'Opens PowerShell in a project of the sandbox, as a session like the agents.',
218
+ }),
219
+ ]
220
+ : []),
153
221
  ...(sessions.length
154
222
  ? [action(restartSessions, { all: false }, { key: 'r', verb: 'restart agents', importance: 1 })]
155
223
  : []),
@@ -172,8 +240,8 @@ export function sessionsPage(o) {
172
240
  export const sessionsView = defineView({
173
241
  id: 'sessions',
174
242
  title: 'Sessions',
175
- summary: 'The agent sessions of the running sandbox: open one, restart the agents.',
243
+ summary: 'The sessions of the running sandbox: open one, restart the agents, open a shell, close one.',
176
244
  queries: { sessions: listSessions },
177
- operations: [restartSessions],
245
+ operations: [restartSessions, startShell, closeSession],
178
246
  build: ({ sessions }) => sessionsPage(sessions),
179
247
  });
@@ -1,3 +1,4 @@
1
+ import { type Context } from '../../../cli/commands/context.js';
1
2
  export declare const listSessionsRun: import("../../kernel/define.js").Implementation<import("../../kernel/define.js").QueryDef<import("zod").ZodObject<{
2
3
  wide: import("zod").ZodDefault<import("zod").ZodBoolean>;
3
4
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
@@ -9,6 +10,10 @@ export declare const listSessionsRun: import("../../kernel/define.js").Implement
9
10
  id: import("zod").ZodString;
10
11
  launchId: import("zod").ZodString;
11
12
  harness: import("zod").ZodString;
13
+ kind: import("zod").ZodOptional<import("zod").ZodEnum<{
14
+ shell: "shell";
15
+ agent: "agent";
16
+ }>>;
12
17
  cwd: import("zod").ZodString;
13
18
  project: import("zod").ZodOptional<import("zod").ZodString>;
14
19
  phase: import("zod").ZodEnum<{
@@ -30,10 +35,28 @@ export declare const listSessionsRun: import("../../kernel/define.js").Implement
30
35
  }, import("zod/v4/core").$strip>>>;
31
36
  canOpen: import("zod").ZodBoolean;
32
37
  }, import("zod/v4/core").$strip>>;
33
- }, import("zod/v4/core").$strip>, "sessions.list">, import("../../../cli/commands/context.js").Context>;
38
+ }, import("zod/v4/core").$strip>, "sessions.list">, Context>;
34
39
  export declare const restartSessionsRun: import("../../kernel/define.js").Implementation<import("../../kernel/define.js").OperationDef<import("zod").ZodObject<{
35
40
  harness: import("zod").ZodOptional<import("zod").ZodString>;
36
41
  all: import("zod").ZodDefault<import("zod").ZodBoolean>;
37
42
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
38
43
  message: import("zod").ZodString;
39
- }, import("zod/v4/core").$strip>, "sessions.restart">, import("../../../cli/commands/context.js").Context>;
44
+ }, import("zod/v4/core").$strip>, "sessions.restart">, Context>;
45
+ export declare const startShellRun: import("../../kernel/define.js").Implementation<import("../../kernel/define.js").OperationDef<import("zod").ZodObject<{
46
+ project: import("zod").ZodOptional<import("zod").ZodString>;
47
+ shell: import("zod").ZodDefault<import("zod").ZodEnum<{
48
+ pwsh: "pwsh";
49
+ auto: "auto";
50
+ powershell: "powershell";
51
+ cmd: "cmd";
52
+ }>>;
53
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
54
+ session: import("zod").ZodString;
55
+ shell: import("zod").ZodString;
56
+ message: import("zod").ZodString;
57
+ }, import("zod/v4/core").$strip>, "sessions.shell">, Context>;
58
+ export declare const closeSessionRun: import("../../kernel/define.js").Implementation<import("../../kernel/define.js").OperationDef<import("zod").ZodObject<{
59
+ session: import("zod").ZodString;
60
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
61
+ message: import("zod").ZodString;
62
+ }, import("zod/v4/core").$strip>, "sessions.close">, Context>;
@@ -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
+ });
@@ -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
+ }