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.
- 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/accounts/accounts.def.d.ts +1 -0
- package/dist/app/features/accounts/accounts.def.js +12 -3
- package/dist/app/features/accounts/accounts.run.d.ts +1 -0
- package/dist/app/features/accounts/accounts.run.js +20 -11
- 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 +156 -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-file.d.ts +7 -0
- package/dist/app/features/settings/settings-file.js +19 -2
- package/dist/app/features/settings/settings-rows.js +9 -1
- 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/bin/agent-sandbox.js +7 -1
- package/dist/bin/chunk-IL7BXJF2.js +7 -0
- package/dist/bin/chunk-K4SU3MHJ.js +10432 -0
- package/dist/bin/chunk-VENWSGX2.js +78437 -0
- package/dist/bin/claude-host.js +10 -4
- package/dist/bin/claude-sbx.js +8 -2
- package/dist/bin/codex-sbx.js +8 -2
- package/dist/bin/gemini-sbx.js +8 -2
- package/dist/bin/sandbox.js +7 -2
- package/dist/bin/sbx-hide.js +8 -2
- package/dist/bin/sbx-show.js +8 -2
- package/dist/cli/commands/launch/prepare.js +24 -7
- package/dist/cli/commands/launch/running.js +4 -0
- 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/storage/offer.js +1 -1
- 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/live.d.ts +29 -0
- package/dist/cli/menu/live.js +92 -21
- package/dist/cli/menu/menu.d.ts +2 -1
- package/dist/cli/menu/menu.js +16 -2
- package/dist/cli/menu/pages/list-page.d.ts +5 -0
- package/dist/cli/menu/pages/list-page.js +19 -0
- package/dist/cli/menu/remote-menu.d.ts +19 -0
- package/dist/cli/menu/remote-menu.js +108 -0
- package/dist/cli/menu/shell.d.ts +2 -0
- package/dist/cli/menu/shell.js +1 -1
- package/dist/cli/menu/views.js +2 -0
- package/dist/cli/operations/handler.js +15 -1
- package/dist/cli/services/accounts-service.d.ts +7 -0
- package/dist/cli/services/accounts-service.js +20 -0
- 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/cli/tui/app.d.ts +4 -0
- package/dist/cli/tui/app.js +5 -0
- package/dist/cli/tui/terminal.d.ts +6 -0
- package/dist/cli/tui/terminal.js +8 -1
- package/dist/cli/validate.js +8 -4
- 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/package-info.js +1 -1
- 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/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +19 -4
- package/dist/host/storage/contained/state.d.ts +3 -2
- package/dist/host/storage/contained/state.js +5 -2
- 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/layout.test.js +10 -1
- package/dist/selftest/live-daemon.test.js +25 -0
- package/dist/selftest/local-config.test.js +30 -0
- package/dist/selftest/menu-perf.test.js +198 -12
- package/dist/selftest/menu.test.js +4 -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-NBQ3LF4E.js +221 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- 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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SecretStore } from '../secrets/index.js';
|
|
2
|
+
export interface TlsMaterial {
|
|
3
|
+
readonly cert: string;
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly fingerprint: string;
|
|
6
|
+
readonly notAfter: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class TlsStore {
|
|
9
|
+
private readonly secrets;
|
|
10
|
+
private readonly now;
|
|
11
|
+
private readonly file;
|
|
12
|
+
constructor(dir: string, secrets: SecretStore, now?: () => number);
|
|
13
|
+
/** The kept certificate when there is a usable one (no key opened), else null. */
|
|
14
|
+
info(): {
|
|
15
|
+
fingerprint: string;
|
|
16
|
+
notAfter: string;
|
|
17
|
+
} | null;
|
|
18
|
+
/** The certificate to serve: the kept one, or a new one for these addresses when there is none or it ends soon. */
|
|
19
|
+
material(addresses: readonly string[]): TlsMaterial;
|
|
20
|
+
/** A new certificate (the old one is gone). */
|
|
21
|
+
renew(addresses: readonly string[]): TlsMaterial;
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The LAN listener's certificate, kept in <storage>\web\tls.json with its private key sealed by SecretStore (DPAPI).
|
|
2
|
+
// Made the first time LAN mode starts and then kept: a new certificate means a new warning on every phone, which
|
|
3
|
+
// teaches people to click through, so it is replaced only when it has expired or on request
|
|
4
|
+
// (sandbox web lan new-certificate, which also signs out every device).
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { JsonFile } from '../../shared/io/index.js';
|
|
9
|
+
import { makeCertificate } from './cert.js';
|
|
10
|
+
const TlsFileSchema = z.strictObject({
|
|
11
|
+
cert: z.string().max(8000),
|
|
12
|
+
key: z.string().max(8000),
|
|
13
|
+
fingerprint: z.string().regex(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/),
|
|
14
|
+
notAfter: z.string().max(40),
|
|
15
|
+
addresses: z.array(z.string().max(15)).max(16),
|
|
16
|
+
});
|
|
17
|
+
/** Renewed this long before it ends. */
|
|
18
|
+
const RENEW_BEFORE_MS = 7 * 86_400_000;
|
|
19
|
+
export class TlsStore {
|
|
20
|
+
secrets;
|
|
21
|
+
now;
|
|
22
|
+
file;
|
|
23
|
+
constructor(dir, secrets, now = Date.now) {
|
|
24
|
+
this.secrets = secrets;
|
|
25
|
+
this.now = now;
|
|
26
|
+
this.file = new JsonFile(TlsFileSchema, path.join(dir, 'tls.json'));
|
|
27
|
+
}
|
|
28
|
+
/** The kept certificate when there is a usable one (no key opened), else null. */
|
|
29
|
+
info() {
|
|
30
|
+
const saved = this.file.read();
|
|
31
|
+
return saved ? { fingerprint: saved.fingerprint, notAfter: saved.notAfter } : null;
|
|
32
|
+
}
|
|
33
|
+
/** The certificate to serve: the kept one, or a new one for these addresses when there is none or it ends soon. */
|
|
34
|
+
material(addresses) {
|
|
35
|
+
const saved = this.file.read();
|
|
36
|
+
if (saved && Date.parse(saved.notAfter) - this.now() > RENEW_BEFORE_MS) {
|
|
37
|
+
try {
|
|
38
|
+
return {
|
|
39
|
+
cert: saved.cert,
|
|
40
|
+
key: this.secrets.open(saved.key),
|
|
41
|
+
fingerprint: saved.fingerprint,
|
|
42
|
+
notAfter: saved.notAfter,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Sealed by another user or system: a new one is made.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return this.renew(addresses);
|
|
50
|
+
}
|
|
51
|
+
/** A new certificate (the old one is gone). */
|
|
52
|
+
renew(addresses) {
|
|
53
|
+
const made = makeCertificate({ addresses, dnsNames: [os.hostname()], now: this.now() });
|
|
54
|
+
this.file.write({
|
|
55
|
+
cert: made.cert,
|
|
56
|
+
key: this.secrets.seal(made.key),
|
|
57
|
+
fingerprint: made.fingerprint,
|
|
58
|
+
notAfter: made.notAfter,
|
|
59
|
+
addresses: [...addresses],
|
|
60
|
+
});
|
|
61
|
+
return made;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** A Windows account as the helper names it. */
|
|
2
|
+
export interface WindowsAccount {
|
|
3
|
+
readonly sid: string;
|
|
4
|
+
/** DOMAIN\name. */
|
|
5
|
+
readonly user: string;
|
|
6
|
+
}
|
|
7
|
+
/** Someone connected to the pipe: who their token says they are. */
|
|
8
|
+
export interface Knock extends WindowsAccount {
|
|
9
|
+
/** The token's integrity RID (0x2000 medium, 0x3000 high). */
|
|
10
|
+
readonly integrity: number;
|
|
11
|
+
readonly appContainer: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The two ends of a loopback TCP connection, our server's end being `local`. */
|
|
14
|
+
export interface Connection {
|
|
15
|
+
readonly localAddress: string;
|
|
16
|
+
readonly localPort: number;
|
|
17
|
+
readonly remoteAddress: string;
|
|
18
|
+
readonly remotePort: number;
|
|
19
|
+
}
|
|
20
|
+
export interface WindowsHelper {
|
|
21
|
+
/** The Windows user of the process at the other end, or null (unknown, a service, it changed while checking). */
|
|
22
|
+
peer(c: Connection): Promise<WindowsAccount | null>;
|
|
23
|
+
lookup(name: string): Promise<WindowsAccount | null>;
|
|
24
|
+
/** Who answers knocks on the pipe: the text written back ("ok <token> <port>" or "refused <reason>"). */
|
|
25
|
+
onKnock(answer: (k: Knock) => Promise<string>): void;
|
|
26
|
+
/** Problems worth a log line (the pipe could not be made). */
|
|
27
|
+
onProblem(listener: (text: string) => void): void;
|
|
28
|
+
stop(): void;
|
|
29
|
+
}
|
|
30
|
+
export type HelperLine = {
|
|
31
|
+
readonly kind: 'peer' | 'lookup';
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly account: WindowsAccount | null;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: 'door';
|
|
36
|
+
readonly number: number;
|
|
37
|
+
readonly knock: Knock;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'problem';
|
|
40
|
+
readonly text: string;
|
|
41
|
+
};
|
|
42
|
+
/** One line from the helper, or null when it is not one of its lines. */
|
|
43
|
+
export declare function parseHelperLine(line: string): HelperLine | null;
|
|
44
|
+
/** Why a knock is refused before the settings are asked (low integrity, a sandboxed app, a service), or null. */
|
|
45
|
+
export declare function knockProblem(k: Knock, ownerSid: string): string | null;
|
|
46
|
+
/** The pipe's name for an owner: agent-sandbox-web-<SID> (WebHelper.cs makes the same name on the client side). */
|
|
47
|
+
export declare function doorPipeName(ownerSid: string): string;
|
|
48
|
+
/** The real helper process. */
|
|
49
|
+
export declare class HostToolsHelper implements WindowsHelper {
|
|
50
|
+
private readonly child;
|
|
51
|
+
private readonly waiting;
|
|
52
|
+
private next;
|
|
53
|
+
private answer;
|
|
54
|
+
private problem;
|
|
55
|
+
private constructor();
|
|
56
|
+
/** Starts it (null when HostTools cannot be built here). `pipe` null: no Windows sign-in, only peer checks. */
|
|
57
|
+
static start(storageRoot: string, pipe: string | null): HostToolsHelper | null;
|
|
58
|
+
private failAll;
|
|
59
|
+
private receive;
|
|
60
|
+
private send;
|
|
61
|
+
private ask;
|
|
62
|
+
peer(c: Connection): Promise<WindowsAccount | null>;
|
|
63
|
+
lookup(name: string): Promise<WindowsAccount | null>;
|
|
64
|
+
onKnock(answer: (k: Knock) => Promise<string>): void;
|
|
65
|
+
onProblem(listener: (text: string) => void): void;
|
|
66
|
+
stop(): void;
|
|
67
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The web server's line to Windows for sign-in (docs/design/remote-access.md): one long-running `HostTools web-helper`
|
|
2
|
+
// that serves the named pipe other Windows users sign in through, tells who is at the other end of a loopback TCP
|
|
3
|
+
// connection, and turns account names into SIDs. Lines are tab-separated (WebHelper.cs); everything it prints is
|
|
4
|
+
// checked here. Tests use a fake WindowsHelper.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import readline from 'node:readline';
|
|
7
|
+
import { hostToolsExe } from '../native.js';
|
|
8
|
+
const SID = /^S-1-[0-9-]{1,180}$/;
|
|
9
|
+
const MEDIUM_INTEGRITY = 0x2000;
|
|
10
|
+
/** Most requests waiting for the helper at once; more are answered "unknown" (a flood cannot pile up). */
|
|
11
|
+
const MAX_WAITING = 64;
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
13
|
+
const clip = (text) => [...text]
|
|
14
|
+
.filter((c) => c >= ' ' && c !== '\u007f')
|
|
15
|
+
.join('')
|
|
16
|
+
.slice(0, 200);
|
|
17
|
+
/** One line from the helper, or null when it is not one of its lines. */
|
|
18
|
+
export function parseHelperLine(line) {
|
|
19
|
+
const f = line.split('\t');
|
|
20
|
+
if ((f[0] === 'peer' || f[0] === 'lookup') && /^\d{1,9}$/.test(f[1] ?? '')) {
|
|
21
|
+
const ok = f[2] === 'ok' && SID.test(f[3] ?? '') && (f[4] ?? '').length > 0;
|
|
22
|
+
return { kind: f[0], id: f[1], account: ok ? { sid: f[3], user: clip(f[4]) } : null };
|
|
23
|
+
}
|
|
24
|
+
if (f[0] === 'door' && f.length === 6 && /^\d{1,9}$/.test(f[1]) && SID.test(f[2]) && /^\d{1,6}$/.test(f[3]))
|
|
25
|
+
return {
|
|
26
|
+
kind: 'door',
|
|
27
|
+
number: Number(f[1]),
|
|
28
|
+
knock: { sid: f[2], integrity: Number(f[3]), appContainer: f[4] !== '0', user: clip(f[5]) },
|
|
29
|
+
};
|
|
30
|
+
if (f[0] === 'door-failed' || f[0] === 'error')
|
|
31
|
+
return { kind: 'problem', text: clip(f.slice(1).join(' ')) };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** Why a knock is refused before the settings are asked (low integrity, a sandboxed app, a service), or null. */
|
|
35
|
+
export function knockProblem(k, ownerSid) {
|
|
36
|
+
if (k.appContainer)
|
|
37
|
+
return 'a sandboxed app (AppContainer)';
|
|
38
|
+
if (k.integrity < MEDIUM_INTEGRITY)
|
|
39
|
+
return 'a low-integrity process';
|
|
40
|
+
if (/^S-1-5-(18|19|20)$/.test(k.sid))
|
|
41
|
+
return 'a Windows service account';
|
|
42
|
+
if (k.sid.toUpperCase() === ownerSid.toUpperCase())
|
|
43
|
+
return 'the owner (use sandbox web open)';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** The pipe's name for an owner: agent-sandbox-web-<SID> (WebHelper.cs makes the same name on the client side). */
|
|
47
|
+
export function doorPipeName(ownerSid) {
|
|
48
|
+
return 'agent-sandbox-web-' + ownerSid;
|
|
49
|
+
}
|
|
50
|
+
/** The real helper process. */
|
|
51
|
+
export class HostToolsHelper {
|
|
52
|
+
child;
|
|
53
|
+
waiting = new Map();
|
|
54
|
+
next = 1;
|
|
55
|
+
answer = async () => 'refused Windows sign-in is off.';
|
|
56
|
+
problem = () => undefined;
|
|
57
|
+
constructor(exe, pipe) {
|
|
58
|
+
this.child = spawn(exe, ['web-helper', '--pipe', pipe ?? '', '--server-pid', String(process.pid)], {
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
this.child.stderr.resume();
|
|
63
|
+
this.child.on('error', () => this.failAll());
|
|
64
|
+
this.child.on('exit', () => this.failAll());
|
|
65
|
+
readline.createInterface({ input: this.child.stdout }).on('line', (line) => this.receive(line));
|
|
66
|
+
}
|
|
67
|
+
/** Starts it (null when HostTools cannot be built here). `pipe` null: no Windows sign-in, only peer checks. */
|
|
68
|
+
static start(storageRoot, pipe) {
|
|
69
|
+
const exe = hostToolsExe(storageRoot);
|
|
70
|
+
return exe ? new HostToolsHelper(exe, pipe) : null;
|
|
71
|
+
}
|
|
72
|
+
failAll() {
|
|
73
|
+
for (const resolve of this.waiting.values())
|
|
74
|
+
resolve(null);
|
|
75
|
+
this.waiting.clear();
|
|
76
|
+
}
|
|
77
|
+
receive(line) {
|
|
78
|
+
const parsed = parseHelperLine(line);
|
|
79
|
+
if (!parsed)
|
|
80
|
+
return;
|
|
81
|
+
if (parsed.kind === 'problem')
|
|
82
|
+
return this.problem(parsed.text);
|
|
83
|
+
if (parsed.kind === 'door') {
|
|
84
|
+
void this.answer(parsed.knock)
|
|
85
|
+
.catch(() => 'refused Something went wrong.')
|
|
86
|
+
.then((text) => this.send(['answer', String(parsed.number), text.replace(/[\t\r\n]/g, ' ')]));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const resolve = this.waiting.get(parsed.id);
|
|
90
|
+
this.waiting.delete(parsed.id);
|
|
91
|
+
resolve?.(parsed.account);
|
|
92
|
+
}
|
|
93
|
+
send(fields) {
|
|
94
|
+
if (this.child.stdin.writable)
|
|
95
|
+
this.child.stdin.write(fields.join('\t') + '\n');
|
|
96
|
+
}
|
|
97
|
+
ask(fields) {
|
|
98
|
+
if (this.waiting.size >= MAX_WAITING || fields.some((f) => /[\t\r\n]/.test(f)))
|
|
99
|
+
return Promise.resolve(null);
|
|
100
|
+
const id = String(this.next++);
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
this.waiting.delete(id);
|
|
104
|
+
resolve(null);
|
|
105
|
+
}, REQUEST_TIMEOUT_MS);
|
|
106
|
+
this.waiting.set(id, (account) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve(account);
|
|
109
|
+
});
|
|
110
|
+
this.send([fields[0], id, ...fields.slice(1)]);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
peer(c) {
|
|
114
|
+
return this.ask(['peer', c.localAddress, String(c.localPort), c.remoteAddress, String(c.remotePort)]);
|
|
115
|
+
}
|
|
116
|
+
lookup(name) {
|
|
117
|
+
return this.ask(['lookup', name]);
|
|
118
|
+
}
|
|
119
|
+
onKnock(answer) {
|
|
120
|
+
this.answer = answer;
|
|
121
|
+
}
|
|
122
|
+
onProblem(listener) {
|
|
123
|
+
this.problem = listener;
|
|
124
|
+
}
|
|
125
|
+
stop() {
|
|
126
|
+
this.child.stdin.end();
|
|
127
|
+
this.failAll();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -7,6 +7,8 @@ export declare function insideSandbox(): boolean;
|
|
|
7
7
|
export declare const GUEST_DATA_DIR = "C:\\AgentData";
|
|
8
8
|
/** Whether a Windows Sandbox program is in this process list (tasklist /NH /FO CSV output). */
|
|
9
9
|
export declare function sandboxInTasklist(tasklist: string): boolean;
|
|
10
|
+
/** The process ids of the Windows Sandbox programs in this process list (tasklist /NH /FO CSV output). */
|
|
11
|
+
export declare function sandboxPids(tasklist: string): number[];
|
|
10
12
|
export declare function isSandboxRunning(): boolean;
|
|
11
13
|
export declare function sandboxFeatureInstalled(): boolean;
|
|
12
14
|
/** `owner`: whose sandbox runs, when the caller knows (host/sharing); it makes the message say who started it. */
|
|
@@ -10,15 +10,19 @@ import { launchBlocked } from '../sharing/rules.js';
|
|
|
10
10
|
const HELPER_TIMEOUT_MS = 15000;
|
|
11
11
|
const START_WAIT_MS = 20000;
|
|
12
12
|
const SANDBOX_PROCESSES = ['WindowsSandboxClient.exe', 'WindowsSandboxRemoteSession.exe', 'WindowsSandbox.exe'];
|
|
13
|
+
/** Whether the launch and data folders are mapped in (looked for once; see insideSandbox). */
|
|
14
|
+
let mappedIn;
|
|
13
15
|
/**
|
|
14
16
|
* Running INSIDE a Windows Sandbox this tool started (its launch folder and data folder are mapped in): then the CLI
|
|
15
17
|
* talks to the sandbox it is in, through C:\\AgentData and loopback, instead of looking for one on a PC.
|
|
16
18
|
*/
|
|
17
19
|
export function insideSandbox() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
if (!isWin || process.env.AGENT_SANDBOX_HOST_ONLY)
|
|
21
|
+
return false;
|
|
22
|
+
// The launch folder and the data folder are mapped in for the sandbox's whole life (and never exist on a PC), so
|
|
23
|
+
// they are looked for once: the menu asks this many times a second.
|
|
24
|
+
mappedIn ??= fs.existsSync('C:\\AgentLaunch\\session.json') && fs.existsSync('C:\\AgentData\\run');
|
|
25
|
+
return mappedIn;
|
|
22
26
|
}
|
|
23
27
|
export const GUEST_DATA_DIR = 'C:\\AgentData';
|
|
24
28
|
/** Whether a Windows Sandbox program is in this process list (tasklist /NH /FO CSV output). */
|
|
@@ -26,6 +30,17 @@ export function sandboxInTasklist(tasklist) {
|
|
|
26
30
|
const out = tasklist.toLowerCase();
|
|
27
31
|
return SANDBOX_PROCESSES.some((image) => out.includes('"' + image.toLowerCase() + '"'));
|
|
28
32
|
}
|
|
33
|
+
/** The process ids of the Windows Sandbox programs in this process list (tasklist /NH /FO CSV output). */
|
|
34
|
+
export function sandboxPids(tasklist) {
|
|
35
|
+
const images = new Set(SANDBOX_PROCESSES.map((image) => image.toLowerCase()));
|
|
36
|
+
const pids = [];
|
|
37
|
+
for (const line of tasklist.split('\n')) {
|
|
38
|
+
const row = /^"([^"]{1,260})","(\d{1,10})"/.exec(line);
|
|
39
|
+
if (row && images.has(row[1].toLowerCase()))
|
|
40
|
+
pids.push(Number(row[2]));
|
|
41
|
+
}
|
|
42
|
+
return pids;
|
|
43
|
+
}
|
|
29
44
|
export function isSandboxRunning() {
|
|
30
45
|
if (!isWin)
|
|
31
46
|
return false;
|
|
@@ -83,7 +83,8 @@ export declare class StateFile {
|
|
|
83
83
|
export declare function mountedSets(storageRoot: string): string[];
|
|
84
84
|
/**
|
|
85
85
|
* A line for the start of a command when the record says an image is mounted that no running process holds and no
|
|
86
|
-
* sandbox uses (nothing is looked up on the disk: this is only the record), or null.
|
|
86
|
+
* sandbox uses (nothing is looked up on the disk: this is only the record), or null. `sandboxRunning` can be a
|
|
87
|
+
* function: it is only asked (it runs tasklist) when the record says an image is mounted.
|
|
87
88
|
*/
|
|
88
|
-
export declare function staleMountNote(storageRoot: string, set: string, sandboxRunning: boolean, alive?: (pid: number) => boolean): string | null;
|
|
89
|
+
export declare function staleMountNote(storageRoot: string, set: string, sandboxRunning: boolean | (() => boolean), alive?: (pid: number) => boolean): string | null;
|
|
89
90
|
export {};
|
|
@@ -155,12 +155,15 @@ export function mountedSets(storageRoot) {
|
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
157
157
|
* A line for the start of a command when the record says an image is mounted that no running process holds and no
|
|
158
|
-
* sandbox uses (nothing is looked up on the disk: this is only the record), or null.
|
|
158
|
+
* sandbox uses (nothing is looked up on the disk: this is only the record), or null. `sandboxRunning` can be a
|
|
159
|
+
* function: it is only asked (it runs tasklist) when the record says an image is mounted.
|
|
159
160
|
*/
|
|
160
161
|
export function staleMountNote(storageRoot, set, sandboxRunning, alive = processAlive) {
|
|
161
162
|
const record = setStateOf(new JsonFile(ContainedStateSchema, path.join(path.resolve(storageRoot), 'mounts', 'state.json')).read() ??
|
|
162
163
|
EMPTY_STATE, set);
|
|
163
|
-
if (!record.mounted ||
|
|
164
|
+
if (!record.mounted || record.holders.some((h) => alive(h.pid)))
|
|
165
|
+
return null;
|
|
166
|
+
if (typeof sandboxRunning === 'function' ? sandboxRunning() : sandboxRunning)
|
|
164
167
|
return null;
|
|
165
168
|
return 'Contained storage is still mounted from an earlier sandbox. Run sandbox repair to dismount it (Windows asks for permission).';
|
|
166
169
|
}
|
|
@@ -7,6 +7,8 @@ export declare class TerminalBridge {
|
|
|
7
7
|
private readonly cols;
|
|
8
8
|
private readonly rows;
|
|
9
9
|
private readonly dial;
|
|
10
|
+
/** View only: keystrokes and resizes from the browser are dropped here, whatever the page sends. */
|
|
11
|
+
private readonly readOnly;
|
|
10
12
|
private socket;
|
|
11
13
|
private pinger;
|
|
12
14
|
private drain;
|
|
@@ -14,7 +16,9 @@ export declare class TerminalBridge {
|
|
|
14
16
|
private done;
|
|
15
17
|
private readonly filter;
|
|
16
18
|
private readonly reader;
|
|
17
|
-
constructor(ws: WebSocket, endpoint: Endpoint, cols: number, rows: number, dial?: (endpoint: Endpoint) => Promise<net.Socket
|
|
19
|
+
constructor(ws: WebSocket, endpoint: Endpoint, cols: number, rows: number, dial?: (endpoint: Endpoint) => Promise<net.Socket>,
|
|
20
|
+
/** View only: keystrokes and resizes from the browser are dropped here, whatever the page sends. */
|
|
21
|
+
readOnly?: boolean);
|
|
18
22
|
/** Connects to the session and starts passing data both ways. */
|
|
19
23
|
start(): Promise<void>;
|
|
20
24
|
private fromBrowser;
|
package/dist/host/web/bridge.js
CHANGED
|
@@ -12,6 +12,7 @@ export class TerminalBridge {
|
|
|
12
12
|
cols;
|
|
13
13
|
rows;
|
|
14
14
|
dial;
|
|
15
|
+
readOnly;
|
|
15
16
|
socket = null;
|
|
16
17
|
pinger = null;
|
|
17
18
|
drain = null;
|
|
@@ -19,12 +20,15 @@ export class TerminalBridge {
|
|
|
19
20
|
done = false;
|
|
20
21
|
filter = new OutputFilter();
|
|
21
22
|
reader = new FrameReader();
|
|
22
|
-
constructor(ws, endpoint, cols, rows, dial = connect
|
|
23
|
+
constructor(ws, endpoint, cols, rows, dial = connect,
|
|
24
|
+
/** View only: keystrokes and resizes from the browser are dropped here, whatever the page sends. */
|
|
25
|
+
readOnly = false) {
|
|
23
26
|
this.ws = ws;
|
|
24
27
|
this.endpoint = endpoint;
|
|
25
28
|
this.cols = cols;
|
|
26
29
|
this.rows = rows;
|
|
27
30
|
this.dial = dial;
|
|
31
|
+
this.readOnly = readOnly;
|
|
28
32
|
}
|
|
29
33
|
/** Connects to the session and starts passing data both ways. */
|
|
30
34
|
async start() {
|
|
@@ -54,7 +58,7 @@ export class TerminalBridge {
|
|
|
54
58
|
this.tell({ t: 'ready' });
|
|
55
59
|
}
|
|
56
60
|
fromBrowser(data, isBinary) {
|
|
57
|
-
if (!this.socket || this.done)
|
|
61
|
+
if (!this.socket || this.done || this.readOnly)
|
|
58
62
|
return;
|
|
59
63
|
const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
|
60
64
|
if (isBinary) {
|
package/dist/host/web/browser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { SYSTEM, system32 } from '../../shared/system.js';
|
|
|
4
4
|
import { refuseInSelfTest } from '../test-guard.js';
|
|
5
5
|
export function openBrowser(url) {
|
|
6
6
|
refuseInSelfTest('open a browser');
|
|
7
|
-
if (!/^http:\/\/127\.0\.0\.1:\d{1,5}\/(#
|
|
7
|
+
if (!/^http:\/\/127\.0\.0\.1:\d{1,5}\/(#[kw]=[0-9a-f]{64})?$/.test(url))
|
|
8
8
|
throw new Error('Not a web UI address.');
|
|
9
9
|
spawn(SYSTEM.cmd(), ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
10
10
|
}
|
|
@@ -6,7 +6,10 @@ export interface WebStatus {
|
|
|
6
6
|
export declare class WebControl {
|
|
7
7
|
private readonly store;
|
|
8
8
|
constructor(store: WebStore);
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Running means the state file exists and its port answers as OUR server: it proves it holds the token (an HMAC of
|
|
11
|
+
* a fresh challenge), so the token link is never handed to another program that took the port.
|
|
12
|
+
*/
|
|
10
13
|
status(): Promise<WebStatus>;
|
|
11
14
|
/** http://127.0.0.1:<port>/#k=<token>: the fragment never goes to the server. */
|
|
12
15
|
url(state: WebState): string;
|
package/dist/host/web/control.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Talking to a running web server from the CLI: is it up, its address (with the token fragment), and stopping it. The
|
|
2
2
|
// state file says where it is and holds the token, so only you (and SYSTEM / Administrators) can do any of this.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
import http from 'node:http';
|
|
4
5
|
import { CSRF_HEADER, CSRF_VALUE } from '../../shared/web/index.js';
|
|
5
6
|
function request(state, method, path, body, cookie) {
|
|
@@ -18,8 +19,13 @@ function request(state, method, path, body, cookie) {
|
|
|
18
19
|
...(cookie ? { Cookie: cookie } : {}),
|
|
19
20
|
},
|
|
20
21
|
}, (res) => {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
let text = '';
|
|
23
|
+
res.setEncoding('utf8');
|
|
24
|
+
res.on('data', (chunk) => {
|
|
25
|
+
if (text.length < 4096)
|
|
26
|
+
text += chunk;
|
|
27
|
+
});
|
|
28
|
+
res.on('end', () => resolve(Object.assign(res, { text })));
|
|
23
29
|
});
|
|
24
30
|
req.on('timeout', () => req.destroy(new Error('timed out')));
|
|
25
31
|
req.on('error', reject);
|
|
@@ -31,13 +37,23 @@ export class WebControl {
|
|
|
31
37
|
constructor(store) {
|
|
32
38
|
this.store = store;
|
|
33
39
|
}
|
|
34
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Running means the state file exists and its port answers as OUR server: it proves it holds the token (an HMAC of
|
|
42
|
+
* a fresh challenge), so the token link is never handed to another program that took the port.
|
|
43
|
+
*/
|
|
35
44
|
async status() {
|
|
36
45
|
const state = this.store.read();
|
|
37
46
|
if (!state)
|
|
38
47
|
return { running: false, state: null };
|
|
39
48
|
try {
|
|
40
|
-
|
|
49
|
+
const challenge = crypto.randomBytes(16).toString('hex');
|
|
50
|
+
const res = await request(state, 'GET', '/api/health?challenge=' + challenge);
|
|
51
|
+
const expected = crypto.createHmac('sha256', state.token).update(challenge).digest('hex');
|
|
52
|
+
const proof = safeJson(res.text)?.proof;
|
|
53
|
+
const proven = typeof proof === 'string' &&
|
|
54
|
+
proof.length === expected.length &&
|
|
55
|
+
crypto.timingSafeEqual(Buffer.from(proof), Buffer.from(expected));
|
|
56
|
+
return { running: res.statusCode === 200 && proven, state };
|
|
41
57
|
}
|
|
42
58
|
catch {
|
|
43
59
|
return { running: false, state };
|
|
@@ -65,3 +81,11 @@ export class WebControl {
|
|
|
65
81
|
this.store.clear();
|
|
66
82
|
}
|
|
67
83
|
}
|
|
84
|
+
function safeJson(text) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/host/web/rpc.d.ts
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import type { WebSocket } from 'ws';
|
|
2
|
-
import { type RpcHost } from '../../app/kernel/index.js';
|
|
2
|
+
import { type Caller, type RpcHost } from '../../app/kernel/index.js';
|
|
3
3
|
import { LiveHub } from './live.js';
|
|
4
|
+
/** One call as this socket saw it (the audit log of remote actions). */
|
|
5
|
+
export interface RpcCallRecord {
|
|
6
|
+
readonly caller: Caller;
|
|
7
|
+
readonly op: string;
|
|
8
|
+
readonly kind: 'operation' | 'query';
|
|
9
|
+
readonly outcome: 'ok' | 'refused' | 'failed';
|
|
10
|
+
}
|
|
4
11
|
export declare class RpcSession {
|
|
5
12
|
private readonly ws;
|
|
6
13
|
private readonly host;
|
|
7
14
|
/** Shared by every socket of the server (one poll per query); a socket of its own by default. */
|
|
8
15
|
private readonly live;
|
|
16
|
+
/** Who uses this socket, as it is now (null: their access ended); checked before every call and subscription. */
|
|
17
|
+
private readonly caller;
|
|
18
|
+
private readonly onCall;
|
|
9
19
|
private readonly calls;
|
|
10
20
|
private readonly subscriptions;
|
|
11
21
|
private closed;
|
|
12
22
|
constructor(ws: WebSocket, host: RpcHost,
|
|
13
23
|
/** Shared by every socket of the server (one poll per query); a socket of its own by default. */
|
|
14
|
-
live?: LiveHub
|
|
24
|
+
live?: LiveHub,
|
|
25
|
+
/** Who uses this socket, as it is now (null: their access ended); checked before every call and subscription. */
|
|
26
|
+
caller?: () => Caller | null, onCall?: (record: RpcCallRecord) => void);
|
|
27
|
+
/** The caller may run `id` with `input` now; else the refusal is sent (and the socket closed when access ended). */
|
|
28
|
+
private permitted;
|
|
15
29
|
start(): void;
|
|
16
30
|
private send;
|
|
17
31
|
/** A browser that sends something that is not one of our messages is closed. */
|
package/dist/host/web/rpc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANSWER_SCHEMAS, ClientMessageSchema, MAX_OPEN_PER_SOCKET, ioFromAsker, toWireError, } from '../../app/kernel/index.js';
|
|
1
|
+
import { allows, ANSWER_SCHEMAS, OWNER, refusalText, ClientMessageSchema, MAX_OPEN_PER_SOCKET, ioFromAsker, toWireError, } from '../../app/kernel/index.js';
|
|
2
2
|
import { LiveHub } from './live.js';
|
|
3
3
|
/** What a question gets when the browser goes away before answering: no. */
|
|
4
4
|
function declined(q) {
|
|
@@ -8,15 +8,36 @@ export class RpcSession {
|
|
|
8
8
|
ws;
|
|
9
9
|
host;
|
|
10
10
|
live;
|
|
11
|
+
caller;
|
|
12
|
+
onCall;
|
|
11
13
|
calls = new Map();
|
|
12
14
|
subscriptions = new Map();
|
|
13
15
|
closed = false;
|
|
14
16
|
constructor(ws, host,
|
|
15
17
|
/** Shared by every socket of the server (one poll per query); a socket of its own by default. */
|
|
16
|
-
live = new LiveHub(host)
|
|
18
|
+
live = new LiveHub(host),
|
|
19
|
+
/** Who uses this socket, as it is now (null: their access ended); checked before every call and subscription. */
|
|
20
|
+
caller = () => OWNER, onCall = () => undefined) {
|
|
17
21
|
this.ws = ws;
|
|
18
22
|
this.host = host;
|
|
19
23
|
this.live = live;
|
|
24
|
+
this.caller = caller;
|
|
25
|
+
this.onCall = onCall;
|
|
26
|
+
}
|
|
27
|
+
/** The caller may run `id` with `input` now; else the refusal is sent (and the socket closed when access ended). */
|
|
28
|
+
permitted(rpcId, id, input, kind) {
|
|
29
|
+
const caller = this.caller();
|
|
30
|
+
if (!caller) {
|
|
31
|
+
this.ws.close(4001, 'signed out');
|
|
32
|
+
this.close();
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const need = this.host.need(id, input) ?? 'owner';
|
|
36
|
+
if (allows(caller, need))
|
|
37
|
+
return caller;
|
|
38
|
+
this.send({ t: 'error', id: rpcId, error: { kind: 'user', message: refusalText(caller, need) } });
|
|
39
|
+
this.onCall({ caller, op: id, kind, outcome: 'refused' });
|
|
40
|
+
return null;
|
|
20
41
|
}
|
|
21
42
|
start() {
|
|
22
43
|
this.ws.on('message', (data, binary) => this.receive(data, binary));
|
|
@@ -78,6 +99,9 @@ export class RpcSession {
|
|
|
78
99
|
this.send({ t: 'error', id, error: { kind: 'invalid', message: 'There is no ' + op + ' here.' } });
|
|
79
100
|
return;
|
|
80
101
|
}
|
|
102
|
+
const caller = this.permitted(id, op, input, this.host.kind(op) ?? 'operation');
|
|
103
|
+
if (!caller)
|
|
104
|
+
return;
|
|
81
105
|
this.calls.set(id, null);
|
|
82
106
|
const io = ioFromAsker((question) => new Promise((resolve) => {
|
|
83
107
|
if (this.closed)
|
|
@@ -88,9 +112,16 @@ export class RpcSession {
|
|
|
88
112
|
progress: (text) => this.send({ t: 'progress', id, text }),
|
|
89
113
|
log: (text, tone) => this.send({ t: 'log', id, text, ...(tone ? { tone } : {}) }),
|
|
90
114
|
});
|
|
115
|
+
const kind = this.host.kind(op) ?? 'operation';
|
|
91
116
|
this.host
|
|
92
|
-
.run(op, input, io)
|
|
93
|
-
.then((output) =>
|
|
117
|
+
.run(op, input, io, caller)
|
|
118
|
+
.then((output) => {
|
|
119
|
+
this.send({ t: 'result', id, output });
|
|
120
|
+
this.onCall({ caller, op, kind, outcome: 'ok' });
|
|
121
|
+
}, (e) => {
|
|
122
|
+
this.send({ t: 'error', id, error: toWireError(e) });
|
|
123
|
+
this.onCall({ caller, op, kind, outcome: 'failed' });
|
|
124
|
+
})
|
|
94
125
|
.finally(() => {
|
|
95
126
|
this.calls.delete(id);
|
|
96
127
|
// An operation may have changed what the live queries show: they read again now.
|
|
@@ -113,6 +144,8 @@ export class RpcSession {
|
|
|
113
144
|
this.send({ t: 'error', id, error: { kind: 'invalid', message: 'There is no ' + query + ' here.' } });
|
|
114
145
|
return;
|
|
115
146
|
}
|
|
147
|
+
if (!this.permitted(id, query, input, 'query'))
|
|
148
|
+
return;
|
|
116
149
|
const stop = this.live.watch(query, input, (event) => {
|
|
117
150
|
if (this.subscriptions.get(id) !== stop)
|
|
118
151
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RpcHost } from '../../app/kernel/index.js';
|
|
2
|
+
import type { RemoteGate } from '../remote/index.js';
|
|
2
3
|
import type { WebBackend } from './backend.js';
|
|
3
4
|
export interface WebRunOptions {
|
|
4
5
|
readonly storageRoot: string;
|
|
@@ -10,11 +11,14 @@ export interface WebRunOptions {
|
|
|
10
11
|
readonly version: string;
|
|
11
12
|
/** End after this long with no browser (sooner with no sandbox): WebServerOptions.idleExitMs. */
|
|
12
13
|
readonly idleExitMs?: number;
|
|
14
|
+
/** Other Windows users and devices (docs/design/remote-access.md); none: the owner only. */
|
|
15
|
+
readonly gate?: RemoteGate;
|
|
13
16
|
}
|
|
14
17
|
export declare class WebRunner {
|
|
15
18
|
private readonly options;
|
|
16
19
|
private readonly store;
|
|
17
20
|
constructor(options: WebRunOptions);
|
|
21
|
+
private lanOf;
|
|
18
22
|
/** Runs until stopped (from the browser, the CLI, or a signal). */
|
|
19
23
|
run(): Promise<void>;
|
|
20
24
|
}
|