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.
- package/README.md +1 -0
- package/config.example.json +11 -1
- package/dist/app/features/access/access.def.d.ts +190 -0
- package/dist/app/features/access/access.def.js +273 -0
- package/dist/app/features/access/access.run.d.ts +79 -0
- package/dist/app/features/access/access.run.js +129 -0
- package/dist/app/features/access/index.d.ts +2 -0
- package/dist/app/features/access/index.js +3 -0
- package/dist/app/features/agents/agent-entry.js +5 -0
- package/dist/app/features/agents/agents.def.js +1 -0
- package/dist/app/features/apps/apps.def.js +1 -0
- package/dist/app/features/audit/audit.def.d.ts +11 -2
- package/dist/app/features/audit/audit.def.js +7 -4
- package/dist/app/features/audit/audit.run.d.ts +3 -0
- package/dist/app/features/defs.d.ts +155 -3
- package/dist/app/features/defs.js +12 -1
- package/dist/app/features/files/files.def.js +1 -0
- package/dist/app/features/health/health.def.js +1 -0
- package/dist/app/features/sandbox/sandbox.def.js +3 -0
- package/dist/app/features/sessions/sessions.def.d.ts +36 -0
- package/dist/app/features/sessions/sessions.def.js +77 -9
- package/dist/app/features/sessions/sessions.run.d.ts +25 -2
- package/dist/app/features/sessions/sessions.run.js +71 -3
- package/dist/app/features/settings/settings.def.d.ts +8 -0
- package/dist/app/features/settings/settings.def.js +17 -0
- package/dist/app/features/web/web.def.d.ts +9 -0
- package/dist/app/features/web/web.def.js +26 -1
- package/dist/app/features/web/web.run.d.ts +3 -0
- package/dist/app/features/web/web.run.js +11 -3
- package/dist/app/kernel/access.d.ts +33 -0
- package/dist/app/kernel/access.js +44 -0
- package/dist/app/kernel/define.d.ts +14 -1
- package/dist/app/kernel/index.d.ts +1 -0
- package/dist/app/kernel/index.js +1 -0
- package/dist/app/kernel/rpc.d.ts +5 -2
- package/dist/app/registry.d.ts +4 -1
- package/dist/app/registry.js +24 -4
- package/dist/cli/commands/registry.js +2 -0
- package/dist/cli/commands/remote.d.ts +2 -0
- package/dist/cli/commands/remote.js +49 -0
- package/dist/cli/commands/sessions/reopen.js +3 -0
- package/dist/cli/commands/web.js +66 -0
- package/dist/cli/manifest.d.ts +63 -4
- package/dist/cli/manifest.js +33 -4
- package/dist/cli/menu/io-menu.d.ts +5 -3
- package/dist/cli/menu/io-menu.js +12 -5
- package/dist/cli/menu/remote-menu.d.ts +19 -0
- package/dist/cli/menu/remote-menu.js +108 -0
- package/dist/cli/menu/views.js +2 -0
- package/dist/cli/operations/handler.js +15 -1
- package/dist/cli/services/remote-client.d.ts +16 -0
- package/dist/cli/services/remote-client.js +141 -0
- package/dist/cli/services/sessions-service.d.ts +5 -0
- package/dist/cli/services/sessions-service.js +17 -1
- package/dist/core/config/defaults.js +11 -1
- package/dist/core/local-config/file.js +16 -2
- package/dist/core/local-config/overrides.js +7 -0
- package/dist/core/run/versions.js +9 -8
- package/dist/core/schema/config.d.ts +22 -3
- package/dist/core/schema/config.js +39 -2
- package/dist/core/schema/web.d.ts +51 -0
- package/dist/core/schema/web.js +46 -1
- package/dist/core/util/fs.d.ts +5 -0
- package/dist/core/util/fs.js +23 -0
- package/dist/core/util/index.d.ts +1 -1
- package/dist/core/util/index.js +1 -1
- package/dist/guest/daemon/daemon.js +2 -0
- package/dist/guest/daemon/idle-shutdown.js +2 -1
- package/dist/guest/daemon/sessions.d.ts +16 -2
- package/dist/guest/daemon/sessions.js +126 -20
- package/dist/guest/git/decision.js +7 -0
- package/dist/guest/sessions/cmdline.d.ts +14 -0
- package/dist/guest/sessions/cmdline.js +10 -0
- package/dist/guest/toolchains/setup.js +3 -0
- package/dist/host/audit/log.d.ts +3 -2
- package/dist/host/audit/log.js +3 -3
- package/dist/host/channel/channel.d.ts +5 -1
- package/dist/host/channel/channel.js +12 -0
- package/dist/host/guard/agent-files.js +7 -1
- package/dist/host/guard/commands.js +13 -1
- package/dist/host/guard/git-config.js +27 -7
- package/dist/host/guard/git-dirs.js +78 -3
- package/dist/host/guard/mark-of-the-web.js +3 -1
- package/dist/host/guard/nested-repos.js +7 -2
- package/dist/host/guard/store.js +1 -0
- package/dist/host/guard/types.d.ts +2 -0
- package/dist/host/launch/mappings.js +11 -1
- package/dist/host/launch/project-scan.js +2 -21
- package/dist/host/launch/stage.js +4 -0
- package/dist/host/life/session-ledger.js +2 -1
- package/dist/host/remote/access-store.d.ts +156 -0
- package/dist/host/remote/access-store.js +308 -0
- package/dist/host/remote/cert.d.ts +20 -0
- package/dist/host/remote/cert.js +103 -0
- package/dist/host/remote/door-client.d.ts +13 -0
- package/dist/host/remote/door-client.js +54 -0
- package/dist/host/remote/firewall.d.ts +44 -0
- package/dist/host/remote/firewall.js +118 -0
- package/dist/host/remote/gate.d.ts +125 -0
- package/dist/host/remote/gate.js +391 -0
- package/dist/host/remote/index.d.ts +8 -0
- package/dist/host/remote/index.js +10 -0
- package/dist/host/remote/lan.d.ts +24 -0
- package/dist/host/remote/lan.js +88 -0
- package/dist/host/remote/tls-store.d.ts +22 -0
- package/dist/host/remote/tls-store.js +63 -0
- package/dist/host/remote/windows-helper.d.ts +67 -0
- package/dist/host/remote/windows-helper.js +129 -0
- package/dist/host/web/bridge.d.ts +5 -1
- package/dist/host/web/bridge.js +6 -2
- package/dist/host/web/browser.js +1 -1
- package/dist/host/web/control.d.ts +4 -1
- package/dist/host/web/control.js +28 -4
- package/dist/host/web/rpc.d.ts +16 -2
- package/dist/host/web/rpc.js +37 -4
- package/dist/host/web/runner.d.ts +4 -0
- package/dist/host/web/runner.js +12 -0
- package/dist/host/web/server.d.ts +18 -2
- package/dist/host/web/server.js +97 -24
- package/dist/host/web/state.d.ts +5 -0
- package/dist/host/web/state.js +8 -0
- package/dist/runtime/agent.js +74 -3
- package/dist/runtime/daemon.js +205 -24
- package/dist/runtime/db.js +74 -3
- package/dist/runtime/gitguard.js +6 -0
- package/dist/runtime/main.js +77 -3
- package/dist/runtime/netproxy.js +74 -3
- package/dist/runtime/view.js +76 -3
- package/dist/selftest/app.test.js +41 -6
- package/dist/selftest/channel.test.js +16 -0
- package/dist/selftest/cmdline.test.js +16 -0
- package/dist/selftest/git-access.test.js +8 -0
- package/dist/selftest/hostile-input.test.js +12 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/live-daemon.test.js +25 -0
- package/dist/selftest/local-config.test.js +30 -0
- package/dist/selftest/remote.test.d.ts +2 -0
- package/dist/selftest/remote.test.js +536 -0
- package/dist/selftest/repo-guard.test.js +56 -2
- package/dist/selftest/resume.test.js +2 -1
- package/dist/selftest/sharing.test.js +9 -2
- package/dist/selftest/support/fixtures.d.ts +13 -0
- package/dist/selftest/support/web-preview.js +63 -0
- package/dist/selftest/web.test.js +2 -0
- package/dist/shared/channel/schema.d.ts +23 -0
- package/dist/shared/channel/schema.js +8 -0
- package/dist/shared/pty/protocol.d.ts +2 -0
- package/dist/shared/pty/protocol.js +2 -0
- package/dist/web/index.html +1 -1
- package/dist/web/main-Z2WYAE2Z.js +221 -0
- package/native/csharp/HostTools/Program.cs +17 -1
- package/native/csharp/HostTools/WebHelper.cs +499 -0
- package/native/scripts/get-network-profile.ps1 +19 -0
- package/native/scripts/set-folder-access.ps1 +11 -0
- package/native/scripts/set-web-firewall.ps1 +103 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +20 -0
- package/schemas/config.schema.json +65 -0
- package/schemas/sandbox-config.schema.json +0 -18
- package/dist/web/main-UD4QCXZR.js +0 -219
|
@@ -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';
|
package/dist/app/kernel/index.js
CHANGED
|
@@ -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';
|
package/dist/app/kernel/rpc.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
172
|
-
|
|
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
|
}
|
package/dist/app/registry.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/app/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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,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
|
? [
|
package/dist/cli/commands/web.js
CHANGED
|
@@ -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
|
}
|
package/dist/cli/manifest.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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";
|
package/dist/cli/manifest.js
CHANGED
|
@@ -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
|
|
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: [
|
|
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
|
|
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
|
|
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). */
|
package/dist/cli/menu/io-menu.js
CHANGED
|
@@ -66,10 +66,10 @@ function follow(target) {
|
|
|
66
66
|
}
|
|
67
67
|
export class MenuOperations {
|
|
68
68
|
app;
|
|
69
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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, ' ');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AnyView, type Caller } from '../../app/kernel/index.js';
|
|
2
|
+
import type { Context } from '../commands/context.js';
|
|
3
|
+
import type { RemoteClient } from '../services/remote-client.js';
|
|
4
|
+
import { type App } from '../tui/index.js';
|
|
5
|
+
/** The views of the owner's program this role may open (remote access itself is the owner's). */
|
|
6
|
+
export declare function remoteViews(role: Caller): AnyView[];
|
|
7
|
+
export declare class RemoteMenu {
|
|
8
|
+
private readonly app;
|
|
9
|
+
private readonly client;
|
|
10
|
+
private readonly owner;
|
|
11
|
+
private readonly caller;
|
|
12
|
+
private readonly operations;
|
|
13
|
+
constructor(app: App, ctx: Context, client: RemoteClient, owner: string, caller: Caller);
|
|
14
|
+
private title;
|
|
15
|
+
/** The list of areas, until Esc. */
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
private page;
|
|
18
|
+
private showView;
|
|
19
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// `sandbox remote`: the menu drawn over another Windows user's program (docs/design/remote-access.md). Every view the
|
|
2
|
+
// web UI has is drawn by the menu's own renderer (menuView) from the shared core's pure definitions; its queries and
|
|
3
|
+
// operations run on the owner's web UI over the RPC socket, which checks the role this user was given and writes each
|
|
4
|
+
// action to the owner's audit log. Nothing here runs on this user's own settings or data.
|
|
5
|
+
import { NAV, navViews, viewById } from '../../app/features/defs.js';
|
|
6
|
+
import { NO_QUESTIONS, allows, viewInputs, } from '../../app/kernel/index.js';
|
|
7
|
+
import { errMsg, safeText } from '../../core/util/index.js';
|
|
8
|
+
import { Theme } from '../tui/index.js';
|
|
9
|
+
import { MenuOperations } from './io-menu.js';
|
|
10
|
+
import { menuView } from './render-view.js';
|
|
11
|
+
/** The views of the owner's program this role may open (remote access itself is the owner's). */
|
|
12
|
+
export function remoteViews(role) {
|
|
13
|
+
return navViews(NAV)
|
|
14
|
+
.map((id) => viewById(id))
|
|
15
|
+
.filter((v) => v !== undefined)
|
|
16
|
+
.filter((v) => Object.values(v.queries).every((q) => allows(role, q.access ?? 'view')));
|
|
17
|
+
}
|
|
18
|
+
export class RemoteMenu {
|
|
19
|
+
app;
|
|
20
|
+
client;
|
|
21
|
+
owner;
|
|
22
|
+
caller;
|
|
23
|
+
operations;
|
|
24
|
+
constructor(app, ctx, client, owner, caller) {
|
|
25
|
+
this.app = app;
|
|
26
|
+
this.client = client;
|
|
27
|
+
this.owner = owner;
|
|
28
|
+
this.caller = caller;
|
|
29
|
+
this.operations = new MenuOperations(app, ctx, (id, input, io) => client.run(id, input, io));
|
|
30
|
+
}
|
|
31
|
+
title() {
|
|
32
|
+
return safeText(this.owner) + "'s sandbox";
|
|
33
|
+
}
|
|
34
|
+
/** The list of areas, until Esc. */
|
|
35
|
+
async start() {
|
|
36
|
+
const views = remoteViews(this.caller);
|
|
37
|
+
const role = this.caller.kind === 'owner' ? 'owner' : this.caller.role;
|
|
38
|
+
let at;
|
|
39
|
+
for (;;) {
|
|
40
|
+
const picked = await this.app.list({
|
|
41
|
+
path: [this.title()],
|
|
42
|
+
intro: [
|
|
43
|
+
'You are using ' + safeText(this.owner) + "'s program with the role " + role + '.',
|
|
44
|
+
'What you do runs as ' + safeText(this.owner) + ' and is written to their audit log.',
|
|
45
|
+
],
|
|
46
|
+
items: views.map((v) => ({ value: v.id, label: v.title, description: v.summary })),
|
|
47
|
+
startAt: at,
|
|
48
|
+
escapeLabel: 'quit',
|
|
49
|
+
enterLabel: 'open',
|
|
50
|
+
});
|
|
51
|
+
if (picked.kind !== 'pick')
|
|
52
|
+
return;
|
|
53
|
+
at = picked.value;
|
|
54
|
+
const view = views.find((v) => v.id === picked.value);
|
|
55
|
+
if (view)
|
|
56
|
+
await this.showView(view);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async page(view) {
|
|
60
|
+
const inputs = viewInputs(view, {});
|
|
61
|
+
const data = {};
|
|
62
|
+
for (const [name, query] of Object.entries(view.queries))
|
|
63
|
+
data[name] = await this.client.run(query.id, inputs[name], NO_QUESTIONS);
|
|
64
|
+
return view.build(data, {});
|
|
65
|
+
}
|
|
66
|
+
async showView(view) {
|
|
67
|
+
let notice;
|
|
68
|
+
for (;;) {
|
|
69
|
+
let page;
|
|
70
|
+
try {
|
|
71
|
+
page = await this.page(view);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
await this.app.message({ path: [this.title(), view.title], lines: [safeText(errMsg(e))], tone: 'error' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const mv = menuView(page);
|
|
78
|
+
const result = await this.app.list({
|
|
79
|
+
path: [this.title(), view.title],
|
|
80
|
+
intro: page.notice ? [safeText(page.notice.text)] : undefined,
|
|
81
|
+
items: mv.items,
|
|
82
|
+
actions: mv.actions,
|
|
83
|
+
enterLabel: mv.enterLabel,
|
|
84
|
+
notice,
|
|
85
|
+
empty: page.empty?.text,
|
|
86
|
+
});
|
|
87
|
+
if (result.kind === 'back')
|
|
88
|
+
return;
|
|
89
|
+
const chosen = mv.resolve(result);
|
|
90
|
+
notice = undefined;
|
|
91
|
+
if (!chosen || chosen.kind === 'refresh')
|
|
92
|
+
continue;
|
|
93
|
+
const action = chosen.action;
|
|
94
|
+
if (action.kind === 'link') {
|
|
95
|
+
notice =
|
|
96
|
+
action.target.kind === 'terminal'
|
|
97
|
+
? Theme.muted('Attach to it from your own terminal: sandbox attach ' + safeText(action.target.session))
|
|
98
|
+
: Theme.muted('That opens on ' + safeText(this.owner) + "'s side only.");
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const done = await this.operations.perform(action, [this.title(), view.title], (text) => {
|
|
102
|
+
notice = text;
|
|
103
|
+
this.app.redraw();
|
|
104
|
+
});
|
|
105
|
+
notice = done.notice;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|