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
package/dist/cli/menu/views.js
CHANGED
|
@@ -18,6 +18,7 @@ import { updatesView } from '../../app/features/updates/index.js';
|
|
|
18
18
|
import { webView } from '../../app/features/web/index.js';
|
|
19
19
|
import { vpnView } from '../../app/features/vpn/index.js';
|
|
20
20
|
import { githubView } from '../../app/features/github/index.js';
|
|
21
|
+
import { accessView } from '../../app/features/access/index.js';
|
|
21
22
|
export const MENU_VIEWS = [
|
|
22
23
|
{ view: agentsView, section: 'Agents' },
|
|
23
24
|
{ view: sessionsView, section: 'Sessions' },
|
|
@@ -34,6 +35,7 @@ export const MENU_VIEWS = [
|
|
|
34
35
|
{ view: updatesView, section: 'Maintenance' },
|
|
35
36
|
{ view: engineView, section: 'Maintenance' },
|
|
36
37
|
{ view: webView, section: 'Maintenance' },
|
|
38
|
+
{ view: accessView, section: 'Maintenance' },
|
|
37
39
|
// The Settings page draws the VPN row itself (its login is staged with the settings); the view lists its operations.
|
|
38
40
|
{ view: vpnView, section: 'Settings' },
|
|
39
41
|
// The same for the GitHub token row.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Runs a definition as a command: its input from the command line, its questions on the terminal, and its output as
|
|
2
2
|
// --json, as the definition's one-line text, or as the page of the view that shows it. A declined question prints
|
|
3
|
-
// "Nothing changed." and exits 1, as the commands always have.
|
|
3
|
+
// "Nothing changed." and exits 1, as the commands always have. An operation that opens a session's terminal (a new
|
|
4
|
+
// shell) attaches this terminal to it afterwards.
|
|
4
5
|
import { VIEWS } from '../../app/features/defs.js';
|
|
5
6
|
import { runDefinition } from '../../app/registry.js';
|
|
6
7
|
import { Cancelled } from '../../app/kernel/index.js';
|
|
@@ -33,6 +34,16 @@ function printOutput(def, output, input) {
|
|
|
33
34
|
else
|
|
34
35
|
console.log(JSON.stringify(output, null, 2));
|
|
35
36
|
}
|
|
37
|
+
/** Attaches this terminal to the session an operation opened (in a script: says how to). */
|
|
38
|
+
async function attachOpened(ctx, session) {
|
|
39
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
40
|
+
nextStep('sandbox attach ' + session);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
// Loaded when needed: attach reaches the launch commands, which reach this handler (no import cycle).
|
|
44
|
+
const { attach } = await import('../commands/sessions/attach.js');
|
|
45
|
+
return attach({ ...ctx, options: { ...ctx.options, positionals: [session] } });
|
|
46
|
+
}
|
|
36
47
|
/** The command for a definition. */
|
|
37
48
|
export function definitionHandler(def) {
|
|
38
49
|
return async (ctx) => {
|
|
@@ -47,6 +58,9 @@ export function definitionHandler(def) {
|
|
|
47
58
|
return code;
|
|
48
59
|
}
|
|
49
60
|
printOutput(def, output, def.input.parse(input));
|
|
61
|
+
const opened = def.kind === 'operation' ? def.opens?.(output) : undefined;
|
|
62
|
+
if (opened?.kind === 'terminal')
|
|
63
|
+
return attachOpened(ctx, opened.session);
|
|
50
64
|
return code;
|
|
51
65
|
}
|
|
52
66
|
catch (e) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type OperationIO } from '../../app/kernel/index.js';
|
|
2
|
+
export declare class RemoteClient {
|
|
3
|
+
private readonly socket;
|
|
4
|
+
private readonly pending;
|
|
5
|
+
private next;
|
|
6
|
+
private constructor();
|
|
7
|
+
/** Trades the one-time token for a sign-in cookie, then opens the RPC socket. */
|
|
8
|
+
static connect(port: number, token: string): Promise<RemoteClient>;
|
|
9
|
+
private receive;
|
|
10
|
+
private send;
|
|
11
|
+
/** Runs a definition on the owner's side (a query or an operation) and returns its output. */
|
|
12
|
+
run(id: string, input: unknown, io: OperationIO): Promise<unknown>;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
/** Connects, or fails with the reason and what to do. */
|
|
16
|
+
export declare function remoteClient(port: number, token: string): Promise<RemoteClient>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// `sandbox remote`'s line to the owner's web UI (docs/design/remote-access.md): signs in with the one-time token of
|
|
2
|
+
// Windows sign-in, then runs the shared core's queries and operations over the RPC socket, as the browser does. What
|
|
3
|
+
// the owner's server sends is checked against the kernel's schemas (anything else is dropped) and every text is shown
|
|
4
|
+
// through safeText by the menu. The owner's server checks the role of every call.
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { WebSocket } from 'ws';
|
|
7
|
+
import { fromWireError, ServerMessageSchema, } from '../../app/kernel/index.js';
|
|
8
|
+
import { fail } from '../../core/util/index.js';
|
|
9
|
+
import { CSRF_HEADER, CSRF_VALUE } from '../../shared/web/index.js';
|
|
10
|
+
const CONNECT_TIMEOUT_MS = 10_000;
|
|
11
|
+
/** Asks an OperationIO a question that came over the socket. */
|
|
12
|
+
function askIO(io, q) {
|
|
13
|
+
switch (q.kind) {
|
|
14
|
+
case 'confirm':
|
|
15
|
+
return io.confirm(q);
|
|
16
|
+
case 'choose':
|
|
17
|
+
return io.choose(q);
|
|
18
|
+
case 'checklist':
|
|
19
|
+
return io.checklist(q);
|
|
20
|
+
case 'text':
|
|
21
|
+
return io.text(q);
|
|
22
|
+
case 'secret':
|
|
23
|
+
return io.secret(q);
|
|
24
|
+
case 'path':
|
|
25
|
+
return io.path(q);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class RemoteClient {
|
|
29
|
+
socket;
|
|
30
|
+
pending = new Map();
|
|
31
|
+
next = 1;
|
|
32
|
+
constructor(socket) {
|
|
33
|
+
this.socket = socket;
|
|
34
|
+
socket.on('message', (data, binary) => {
|
|
35
|
+
if (binary)
|
|
36
|
+
return;
|
|
37
|
+
let value;
|
|
38
|
+
try {
|
|
39
|
+
value = JSON.parse(data.toString());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const parsed = ServerMessageSchema.safeParse(value);
|
|
45
|
+
if (parsed.success)
|
|
46
|
+
this.receive(parsed.data);
|
|
47
|
+
});
|
|
48
|
+
socket.on('close', () => {
|
|
49
|
+
for (const p of this.pending.values())
|
|
50
|
+
p.reject(new Error('The connection to the web UI closed.'));
|
|
51
|
+
this.pending.clear();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Trades the one-time token for a sign-in cookie, then opens the RPC socket. */
|
|
55
|
+
static async connect(port, token) {
|
|
56
|
+
const origin = 'http://127.0.0.1:' + port;
|
|
57
|
+
const cookie = await new Promise((resolve, reject) => {
|
|
58
|
+
const body = JSON.stringify({ token });
|
|
59
|
+
const req = http.request({
|
|
60
|
+
host: '127.0.0.1',
|
|
61
|
+
port,
|
|
62
|
+
method: 'POST',
|
|
63
|
+
path: '/api/login/once',
|
|
64
|
+
timeout: CONNECT_TIMEOUT_MS,
|
|
65
|
+
headers: {
|
|
66
|
+
Origin: origin,
|
|
67
|
+
[CSRF_HEADER]: CSRF_VALUE,
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
'Content-Length': Buffer.byteLength(body),
|
|
70
|
+
},
|
|
71
|
+
}, (res) => {
|
|
72
|
+
res.resume();
|
|
73
|
+
const value = (res.headers['set-cookie'] ?? [])[0]?.split(';')[0];
|
|
74
|
+
if (res.statusCode === 200 && value)
|
|
75
|
+
resolve(value);
|
|
76
|
+
else
|
|
77
|
+
reject(new Error('The web UI did not sign you in (' + res.statusCode + ').'));
|
|
78
|
+
});
|
|
79
|
+
req.on('timeout', () => req.destroy(new Error('The web UI did not answer.')));
|
|
80
|
+
req.on('error', reject);
|
|
81
|
+
req.end(body);
|
|
82
|
+
});
|
|
83
|
+
const socket = new WebSocket('ws://127.0.0.1:' + port + '/api/rpc', {
|
|
84
|
+
headers: { Cookie: cookie, Origin: origin },
|
|
85
|
+
handshakeTimeout: CONNECT_TIMEOUT_MS,
|
|
86
|
+
});
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
socket.once('open', () => resolve());
|
|
89
|
+
socket.once('error', reject);
|
|
90
|
+
socket.once('unexpected-response', (_req, res) => reject(new Error('Refused (' + res.statusCode + ').')));
|
|
91
|
+
});
|
|
92
|
+
return new RemoteClient(socket);
|
|
93
|
+
}
|
|
94
|
+
receive(m) {
|
|
95
|
+
const p = this.pending.get(m.id);
|
|
96
|
+
if (!p)
|
|
97
|
+
return;
|
|
98
|
+
switch (m.t) {
|
|
99
|
+
case 'progress':
|
|
100
|
+
return p.io.progress(m.text);
|
|
101
|
+
case 'log':
|
|
102
|
+
return p.io.log(m.text, m.tone);
|
|
103
|
+
case 'ask':
|
|
104
|
+
void askIO(p.io, m.question).then((value) => this.send({ t: 'answer', id: m.id, value }), () => this.send({ t: 'answer', id: m.id, value: m.question.kind === 'confirm' ? false : undefined }));
|
|
105
|
+
return;
|
|
106
|
+
case 'result':
|
|
107
|
+
case 'data':
|
|
108
|
+
this.pending.delete(m.id);
|
|
109
|
+
return p.resolve(m.output);
|
|
110
|
+
case 'error':
|
|
111
|
+
this.pending.delete(m.id);
|
|
112
|
+
return p.reject(fromWireError(m.error));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
send(message) {
|
|
116
|
+
if (this.socket.readyState === WebSocket.OPEN)
|
|
117
|
+
this.socket.send(JSON.stringify(message));
|
|
118
|
+
}
|
|
119
|
+
/** Runs a definition on the owner's side (a query or an operation) and returns its output. */
|
|
120
|
+
run(id, input, io) {
|
|
121
|
+
if (this.socket.readyState !== WebSocket.OPEN)
|
|
122
|
+
return Promise.reject(new Error('Not connected to the web UI.'));
|
|
123
|
+
const callId = 'c' + this.next++;
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
this.pending.set(callId, { io, resolve, reject });
|
|
126
|
+
this.send({ t: 'call', id: callId, op: id, input: input ?? {} });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
close() {
|
|
130
|
+
this.socket.close(1000);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Connects, or fails with the reason and what to do. */
|
|
134
|
+
export async function remoteClient(port, token) {
|
|
135
|
+
try {
|
|
136
|
+
return await RemoteClient.connect(port, token);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
return fail('Could not reach the web UI on port ' + port + ': ' + (e instanceof Error ? e.message : String(e)), 'Ask its owner to start it (sandbox web start), then try again.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SessionStatus } from '../../core/types.js';
|
|
2
|
+
import type { ShellChoice } from '../../shared/channel/index.js';
|
|
2
3
|
import type { RegistryEntry } from '../../host/projects/index.js';
|
|
3
4
|
import type { Viewers } from '../../shared/pty/index.js';
|
|
4
5
|
import type { Context } from '../commands/context.js';
|
|
@@ -38,5 +39,9 @@ export declare class SessionsService {
|
|
|
38
39
|
restartScope(all: boolean): RestartScope;
|
|
39
40
|
/** Restarts the agents (one kind, or all) of a project or of every session; returns the sandbox's message. */
|
|
40
41
|
restart(harness: string | null, scope?: string): Promise<string>;
|
|
42
|
+
/** Starts a plain shell in a folder of the sandbox and waits until it runs. */
|
|
43
|
+
openShell(shell: ShellChoice, guestDir: string): Promise<SessionStatus>;
|
|
44
|
+
/** Ends a running session and everything it started; returns the sandbox's message (throws when it refused). */
|
|
45
|
+
close(sessionId: string): Promise<string>;
|
|
41
46
|
private projects;
|
|
42
47
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Sessions of the running sandbox (agents and plain shells): list, pick one to attach to, restart the agents, open a
|
|
2
|
+
// shell, close one. Returns data, never prints.
|
|
2
3
|
import _ from 'lodash';
|
|
4
|
+
import { fail } from '../../core/util/index.js';
|
|
3
5
|
import { SandboxChannel, sessionsFirstIn, sessionViewers } from '../../host/channel/index.js';
|
|
4
6
|
import { isSandboxRunning } from '../../host/sandbox/index.js';
|
|
5
7
|
import { RunDir } from '../../shared/channel/index.js';
|
|
6
8
|
import { isWithin, projectForGuestDir } from '../../shared/paths/index.js';
|
|
7
9
|
import { hereDir, locateHere, registryOf } from '../commands/projects/here.js';
|
|
10
|
+
/** A shell needs no install: the longest it may take is its project's first setup. */
|
|
11
|
+
const SHELL_START_MS = 15 * 60 * 1000;
|
|
8
12
|
export function viewerText(v) {
|
|
9
13
|
return v === undefined
|
|
10
14
|
? '-'
|
|
@@ -75,6 +79,18 @@ export class SessionsService {
|
|
|
75
79
|
async restart(harness, scope) {
|
|
76
80
|
return (await this.channel.restart(harness, scope)).message;
|
|
77
81
|
}
|
|
82
|
+
/** Starts a plain shell in a folder of the sandbox and waits until it runs. */
|
|
83
|
+
openShell(shell, guestDir) {
|
|
84
|
+
const channel = this.channel;
|
|
85
|
+
return channel.waitForSession(channel.requestShell(shell, guestDir), SHELL_START_MS, () => undefined);
|
|
86
|
+
}
|
|
87
|
+
/** Ends a running session and everything it started; returns the sandbox's message (throws when it refused). */
|
|
88
|
+
async close(sessionId) {
|
|
89
|
+
const answer = await this.channel.closeSession(sessionId);
|
|
90
|
+
if (!answer.ok)
|
|
91
|
+
fail(answer.message);
|
|
92
|
+
return answer.message;
|
|
93
|
+
}
|
|
78
94
|
projects() {
|
|
79
95
|
return registryOf(this.ctx).read()?.projects ?? [];
|
|
80
96
|
}
|
|
@@ -95,7 +95,17 @@ export const DEFAULTS = {
|
|
|
95
95
|
sqlserver: { enabled: 'auto' },
|
|
96
96
|
},
|
|
97
97
|
attach: { enabled: true, reuseSession: true, auto: false, autoHide: false, confirmCtrlC: true },
|
|
98
|
-
web: {
|
|
98
|
+
web: {
|
|
99
|
+
enabled: false,
|
|
100
|
+
port: 47800,
|
|
101
|
+
openOnLaunch: false,
|
|
102
|
+
remote: {
|
|
103
|
+
windowsSignIn: false,
|
|
104
|
+
users: [],
|
|
105
|
+
rememberDays: 30,
|
|
106
|
+
lan: { enabled: false, port: 47801, address: '' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
99
109
|
agentCommands: { enabled: false, agents: 'auto' },
|
|
100
110
|
secrets: { strip: true, allow: [], patterns: [], warnFiles: true },
|
|
101
111
|
personalization: { enabled: true },
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import { fileExists, parseJsonc } from '../util/index.js';
|
|
4
|
+
import { fail, fileExists, parseJsonc } from '../util/index.js';
|
|
5
5
|
export const LOCAL_CONFIG_NAME = '.sandbox-config.json';
|
|
6
|
+
const MAX_LOCAL_CONFIG_BYTES = 1024 * 1024;
|
|
6
7
|
export function readLocalConfig(dir) {
|
|
7
8
|
const file = path.join(dir, LOCAL_CONFIG_NAME);
|
|
8
9
|
if (!fileExists(file))
|
|
9
10
|
return null;
|
|
10
|
-
|
|
11
|
+
// One handle for the size check and the read (the hash covers exactly the bytes parsed); a repo's file over 1 MB is
|
|
12
|
+
// refused, not loaded.
|
|
13
|
+
const fd = fs.openSync(file, 'r');
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
const size = fs.fstatSync(fd).size;
|
|
17
|
+
if (size > MAX_LOCAL_CONFIG_BYTES)
|
|
18
|
+
fail(file + ' is over 1 MB, so it is not read.', 'Make it smaller, or start with --no-local-config.');
|
|
19
|
+
raw = Buffer.alloc(size);
|
|
20
|
+
fs.readSync(fd, raw, 0, size, 0);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
fs.closeSync(fd);
|
|
24
|
+
}
|
|
11
25
|
return {
|
|
12
26
|
path: file,
|
|
13
27
|
hash: crypto.createHash('sha256').update(raw).digest('hex'),
|
|
@@ -14,6 +14,11 @@ export function normalizeOverrides(data, dir, source) {
|
|
|
14
14
|
const missing = scripts?.filter((s) => !fileExists(s)) ?? [];
|
|
15
15
|
if (missing.length)
|
|
16
16
|
fail(source + ' is not valid:\n - scripts: file not found: ' + missing.join(', '));
|
|
17
|
+
// A repo ships this file: a script path outside the project (%USERPROFILE%\.ssh\id_ed25519, ..\..) would copy that
|
|
18
|
+
// file of this PC into the launch folder, which the sandbox reads.
|
|
19
|
+
const outside = scripts?.filter((s) => !insideProject(dir, s)) ?? [];
|
|
20
|
+
if (outside.length)
|
|
21
|
+
fail(source + ' is not valid:\n - scripts: only files inside the project can be used: ' + outside.join(', '));
|
|
17
22
|
const mappings = overrides.mappings?.map((m) => ({ ...m, host: expandPath(m.host, dir) }));
|
|
18
23
|
return { ...overrides, scripts, mappings };
|
|
19
24
|
}
|
|
@@ -62,6 +67,8 @@ export const OFF_IS_SAFER = [
|
|
|
62
67
|
'claudeProfile.enabled',
|
|
63
68
|
'claudeProfile.syncHistory',
|
|
64
69
|
'claudeProfile.syncClaudeMem',
|
|
70
|
+
// Maps this PC's Node, Git, nvm and agent folders into the sandbox.
|
|
71
|
+
'provisioning.copyFromHost',
|
|
65
72
|
];
|
|
66
73
|
export const GIT_STRICTNESS = { full: 0, readonly: 1, none: 2 };
|
|
67
74
|
export const NEVER_LOOSER = [...ON_IS_SAFER, ...OFF_IS_SAFER, 'provisioning.git.access'];
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { z } from 'zod';
|
|
4
|
-
import { fileExists } from '../util/index.js';
|
|
3
|
+
import { fileExists, readSmallFile } from '../util/index.js';
|
|
4
|
+
/** A version spec as version files hold it (20, lts/iron, 3.12.1, v18.*): anything else is ignored, never printed. */
|
|
5
|
+
const VERSION_SPEC = /^[\w./*~^-]{1,64}$/;
|
|
5
6
|
const firstLine = (file) => {
|
|
6
|
-
|
|
7
|
+
const text = fileExists(file) ? readSmallFile(file, 64 * 1024) : null;
|
|
8
|
+
if (text === null)
|
|
7
9
|
return null;
|
|
8
|
-
const line =
|
|
9
|
-
.readFileSync(file, 'utf8')
|
|
10
|
+
const line = text
|
|
10
11
|
.replace(/^\uFEFF/, '')
|
|
11
12
|
.split(/\r?\n/)
|
|
12
13
|
.map((l) => l.replace(/#.*$/, '').trim())
|
|
13
14
|
.find((l) => l !== '');
|
|
14
|
-
return line
|
|
15
|
+
return line !== undefined && VERSION_SPEC.test(line) ? line : null;
|
|
15
16
|
};
|
|
16
17
|
const VOLTA_PIN = 'package.json volta.node';
|
|
17
18
|
const PackageJsonSchema = z.object({
|
|
@@ -32,8 +33,8 @@ export function voltaNode(workspace) {
|
|
|
32
33
|
if (!fileExists(file))
|
|
33
34
|
return null;
|
|
34
35
|
try {
|
|
35
|
-
return (PackageJsonSchema.safeParse(JSON.parse(
|
|
36
|
-
null);
|
|
36
|
+
return (PackageJsonSchema.safeParse(JSON.parse((readSmallFile(file, 4 * 1024 * 1024) ?? '').replace(/^\uFEFF/, ''))).data
|
|
37
|
+
?.volta?.node ?? null);
|
|
37
38
|
}
|
|
38
39
|
catch {
|
|
39
40
|
return null;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* An environment variable a repo's .sandbox-config.json may not set: the git guard's and the sandbox's own (setting
|
|
4
|
+
* AGENT_GIT_ACCESS or GIT_CONFIG_COUNT would undo provisioning.git.access), interpreter hooks (NODE_OPTIONS reaches the
|
|
5
|
+
* git guard's node), proxies and API endpoints (ANTHROPIC_BASE_URL would send your API key to the repo's server).
|
|
6
|
+
*/
|
|
7
|
+
export declare function reservedEnvName(name: string): boolean;
|
|
2
8
|
export declare const ConfigSchema: z.ZodObject<{
|
|
3
9
|
$schema: z.ZodOptional<z.ZodString>;
|
|
4
10
|
defaultHarness: z.ZodString;
|
|
@@ -253,6 +259,22 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
253
259
|
enabled: z.ZodBoolean;
|
|
254
260
|
port: z.ZodNumber;
|
|
255
261
|
openOnLaunch: z.ZodBoolean;
|
|
262
|
+
remote: z.ZodObject<{
|
|
263
|
+
windowsSignIn: z.ZodBoolean;
|
|
264
|
+
users: z.ZodArray<z.ZodObject<{
|
|
265
|
+
name: z.ZodString;
|
|
266
|
+
role: z.ZodEnum<{
|
|
267
|
+
view: "view";
|
|
268
|
+
control: "control";
|
|
269
|
+
}>;
|
|
270
|
+
}, z.core.$strict>>;
|
|
271
|
+
rememberDays: z.ZodNumber;
|
|
272
|
+
lan: z.ZodObject<{
|
|
273
|
+
enabled: z.ZodBoolean;
|
|
274
|
+
port: z.ZodNumber;
|
|
275
|
+
address: z.ZodString;
|
|
276
|
+
}, z.core.$strict>;
|
|
277
|
+
}, z.core.$strict>;
|
|
256
278
|
}, z.core.$strict>;
|
|
257
279
|
agentCommands: z.ZodObject<{
|
|
258
280
|
enabled: z.ZodBoolean;
|
|
@@ -367,9 +389,6 @@ export declare const LocalOverridesSchema: z.ZodObject<{
|
|
|
367
389
|
scripts: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
368
390
|
sandbox: z.ZodOptional<z.ZodObject<{
|
|
369
391
|
memoryMb: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"auto">, z.ZodNumber]>>;
|
|
370
|
-
maxMemoryMb: z.ZodOptional<z.ZodNumber>;
|
|
371
|
-
maxMemoryPercent: z.ZodOptional<z.ZodNumber>;
|
|
372
|
-
reserveHostMb: z.ZodOptional<z.ZodNumber>;
|
|
373
392
|
vGpu: z.ZodOptional<z.ZodBoolean>;
|
|
374
393
|
networking: z.ZodOptional<z.ZodBoolean>;
|
|
375
394
|
clipboard: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -14,6 +14,39 @@ import { ResumeSchema } from './resume.js';
|
|
|
14
14
|
import { WebSchema } from './web.js';
|
|
15
15
|
import { VpnSchema } from './vpn.js';
|
|
16
16
|
import { GithubSchema } from './github.js';
|
|
17
|
+
const RESERVED_ENV_PREFIX = /^(AGENT_|GIT_|GH_|GITHUB_|NODE_|NPM_CONFIG_(USERCONFIG|GLOBALCONFIG|PREFIX|SCRIPT_SHELL)|SSL_|CLAUDE|ANTHROPIC_|OPENAI_|CODEX_|GEMINI_|GOOGLE_|COPILOT_|AWS_|AZURE_)/i;
|
|
18
|
+
const RESERVED_ENV_SUFFIX = /(_PROXY|_BASE_URL|_ENDPOINT|_CA_BUNDLE|_CA_CERTS)$/i;
|
|
19
|
+
const RESERVED_ENV_NAMES = new Set([
|
|
20
|
+
'PATH',
|
|
21
|
+
'PATHEXT',
|
|
22
|
+
'BASH_ENV',
|
|
23
|
+
'ENV',
|
|
24
|
+
'COMSPEC',
|
|
25
|
+
'PSMODULEPATH',
|
|
26
|
+
'PYTHONPATH',
|
|
27
|
+
'PYTHONSTARTUP',
|
|
28
|
+
'PERL5OPT',
|
|
29
|
+
'RUBYOPT',
|
|
30
|
+
'JAVA_TOOL_OPTIONS',
|
|
31
|
+
'LD_PRELOAD',
|
|
32
|
+
'SYSTEMROOT',
|
|
33
|
+
'WINDIR',
|
|
34
|
+
'HOME',
|
|
35
|
+
'USERPROFILE',
|
|
36
|
+
'APPDATA',
|
|
37
|
+
'LOCALAPPDATA',
|
|
38
|
+
'PROGRAMDATA',
|
|
39
|
+
'TEMP',
|
|
40
|
+
'TMP',
|
|
41
|
+
].map((n) => n.toUpperCase()));
|
|
42
|
+
/**
|
|
43
|
+
* An environment variable a repo's .sandbox-config.json may not set: the git guard's and the sandbox's own (setting
|
|
44
|
+
* AGENT_GIT_ACCESS or GIT_CONFIG_COUNT would undo provisioning.git.access), interpreter hooks (NODE_OPTIONS reaches the
|
|
45
|
+
* git guard's node), proxies and API endpoints (ANTHROPIC_BASE_URL would send your API key to the repo's server).
|
|
46
|
+
*/
|
|
47
|
+
export function reservedEnvName(name) {
|
|
48
|
+
return RESERVED_ENV_PREFIX.test(name) || RESERVED_ENV_SUFFIX.test(name) || RESERVED_ENV_NAMES.has(name.toUpperCase());
|
|
49
|
+
}
|
|
17
50
|
export const ConfigSchema = z
|
|
18
51
|
.strictObject({
|
|
19
52
|
$schema: SchemaRefSchema.optional(),
|
|
@@ -88,10 +121,14 @@ export const LocalOverridesSchema = z
|
|
|
88
121
|
.array(MappingSchema)
|
|
89
122
|
.describe('Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once).'),
|
|
90
123
|
scripts: StringsSchema.describe('Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent.'),
|
|
91
|
-
// This PC's resources are yours to share: CPU
|
|
124
|
+
// This PC's resources are yours to share: CPU, paging file (this PC's disk) and the RAM kept for this PC are not a
|
|
125
|
+
// project's to set.
|
|
92
126
|
sandbox: SandboxSchema.omit({
|
|
93
127
|
cpuLimit: true,
|
|
94
128
|
pageFileMb: true,
|
|
129
|
+
reserveHostMb: true,
|
|
130
|
+
maxMemoryPercent: true,
|
|
131
|
+
maxMemoryMb: true,
|
|
95
132
|
engine: true,
|
|
96
133
|
agentMemoryHeadroomMb: true,
|
|
97
134
|
pressureResponse: true,
|
|
@@ -108,7 +145,7 @@ export const LocalOverridesSchema = z
|
|
|
108
145
|
toolchains: z
|
|
109
146
|
.strictObject({
|
|
110
147
|
..._.mapValues(_.omit(ToolchainsSchema.shape, 'environment'), (shape) => shape.partial()),
|
|
111
|
-
environment: ToolchainsSchema.shape.environment,
|
|
148
|
+
environment: ToolchainsSchema.shape.environment.refine((env) => !Object.keys(env).some(reservedEnvName), 'names the sandbox sets or that carry logins (AGENT_*, GIT_*, PATH, BASH_ENV, NODE_*, proxies, ANTHROPIC_*, CLAUDE_* ...) cannot be set by a project'),
|
|
112
149
|
})
|
|
113
150
|
.partial(),
|
|
114
151
|
databases: z.strictObject(_.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
|
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const WEB_DEFAULT_PORT = 47800;
|
|
3
|
+
export declare const WEB_LAN_DEFAULT_PORT = 47801;
|
|
4
|
+
export declare const REMOTE_ROLES: readonly ["view", "control"];
|
|
5
|
+
export type RemoteRole = (typeof REMOTE_ROLES)[number];
|
|
6
|
+
/** A Windows account name: "bob", "PC\bob" or "DOMAIN\bob" (no paths, quotes or wildcards). */
|
|
7
|
+
export declare const WindowsUserNameSchema: z.ZodString;
|
|
8
|
+
export declare const RemoteUserSchema: z.ZodObject<{
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
role: z.ZodEnum<{
|
|
11
|
+
view: "view";
|
|
12
|
+
control: "control";
|
|
13
|
+
}>;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
export type RemoteUser = z.output<typeof RemoteUserSchema>;
|
|
16
|
+
export declare const WebLanSchema: z.ZodObject<{
|
|
17
|
+
enabled: z.ZodBoolean;
|
|
18
|
+
port: z.ZodNumber;
|
|
19
|
+
address: z.ZodString;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export declare const WebRemoteSchema: z.ZodObject<{
|
|
22
|
+
windowsSignIn: z.ZodBoolean;
|
|
23
|
+
users: z.ZodArray<z.ZodObject<{
|
|
24
|
+
name: z.ZodString;
|
|
25
|
+
role: z.ZodEnum<{
|
|
26
|
+
view: "view";
|
|
27
|
+
control: "control";
|
|
28
|
+
}>;
|
|
29
|
+
}, z.core.$strict>>;
|
|
30
|
+
rememberDays: z.ZodNumber;
|
|
31
|
+
lan: z.ZodObject<{
|
|
32
|
+
enabled: z.ZodBoolean;
|
|
33
|
+
port: z.ZodNumber;
|
|
34
|
+
address: z.ZodString;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
}, z.core.$strict>;
|
|
37
|
+
export type WebRemote = z.output<typeof WebRemoteSchema>;
|
|
3
38
|
export declare const WebSchema: z.ZodObject<{
|
|
4
39
|
enabled: z.ZodBoolean;
|
|
5
40
|
port: z.ZodNumber;
|
|
6
41
|
openOnLaunch: z.ZodBoolean;
|
|
42
|
+
remote: z.ZodObject<{
|
|
43
|
+
windowsSignIn: z.ZodBoolean;
|
|
44
|
+
users: z.ZodArray<z.ZodObject<{
|
|
45
|
+
name: z.ZodString;
|
|
46
|
+
role: z.ZodEnum<{
|
|
47
|
+
view: "view";
|
|
48
|
+
control: "control";
|
|
49
|
+
}>;
|
|
50
|
+
}, z.core.$strict>>;
|
|
51
|
+
rememberDays: z.ZodNumber;
|
|
52
|
+
lan: z.ZodObject<{
|
|
53
|
+
enabled: z.ZodBoolean;
|
|
54
|
+
port: z.ZodNumber;
|
|
55
|
+
address: z.ZodString;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
}, z.core.$strict>;
|
|
7
58
|
}, z.core.$strict>;
|
package/dist/core/schema/web.js
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
|
-
// The web UI settings (sandbox web): not part of LocalOverrides, a repo never turns a way into your agents on.
|
|
1
|
+
// The web UI settings (sandbox web): not part of LocalOverrides, a repo never turns a way into your agents on. The
|
|
2
|
+
// remote group (other Windows users of this PC, devices on your network) is off by default: design/remote-access.md.
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
export const WEB_DEFAULT_PORT = 47800;
|
|
5
|
+
export const WEB_LAN_DEFAULT_PORT = 47801;
|
|
6
|
+
export const REMOTE_ROLES = ['view', 'control'];
|
|
7
|
+
/** A Windows account name: "bob", "PC\bob" or "DOMAIN\bob" (no paths, quotes or wildcards). */
|
|
8
|
+
export const WindowsUserNameSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.trim()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(120)
|
|
13
|
+
.regex(/^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/, 'a Windows user name (name or DOMAIN\\name)');
|
|
14
|
+
export const RemoteUserSchema = z.strictObject({
|
|
15
|
+
name: WindowsUserNameSchema.describe('The Windows user (name, or DOMAIN\\name)'),
|
|
16
|
+
role: z
|
|
17
|
+
.enum(REMOTE_ROLES)
|
|
18
|
+
.describe('view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.'),
|
|
19
|
+
});
|
|
20
|
+
export const WebLanSchema = z.strictObject({
|
|
21
|
+
enabled: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.describe('(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out.'),
|
|
24
|
+
port: z.number().int().min(1024).max(65535).describe('HTTPS port for paired devices on your network.'),
|
|
25
|
+
address: z
|
|
26
|
+
.string()
|
|
27
|
+
.trim()
|
|
28
|
+
.max(15)
|
|
29
|
+
.regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, 'an IPv4 address or empty')
|
|
30
|
+
.describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one)."),
|
|
31
|
+
});
|
|
32
|
+
export const WebRemoteSchema = z.strictObject({
|
|
33
|
+
windowsSignIn: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.describe('(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link.'),
|
|
36
|
+
users: z
|
|
37
|
+
.array(RemoteUserSchema)
|
|
38
|
+
.max(32)
|
|
39
|
+
.describe('Who else may sign in with Windows, and their role (view or control).'),
|
|
40
|
+
rememberDays: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(90)
|
|
45
|
+
.describe('How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed).'),
|
|
46
|
+
lan: WebLanSchema,
|
|
47
|
+
});
|
|
4
48
|
export const WebSchema = z.strictObject({
|
|
5
49
|
enabled: z
|
|
6
50
|
.boolean()
|
|
@@ -14,4 +58,5 @@ export const WebSchema = z.strictObject({
|
|
|
14
58
|
openOnLaunch: z
|
|
15
59
|
.boolean()
|
|
16
60
|
.describe('(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.'),
|
|
61
|
+
remote: WebRemoteSchema.describe('(Beta) Access for other Windows users of this PC and for paired devices.'),
|
|
17
62
|
});
|
package/dist/core/util/fs.d.ts
CHANGED
|
@@ -3,6 +3,11 @@ export declare function dirExists(p: string): boolean;
|
|
|
3
3
|
export declare function fileExists(p: string): boolean;
|
|
4
4
|
/** fileExists without blocking the program (the menu looks for programs while it stays responsive). */
|
|
5
5
|
export declare function fileExistsAsync(p: string): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* A file the sandbox or a repo may have written, read through one handle and only if that handle is a regular file of
|
|
8
|
+
* at most `maxBytes` (null otherwise): a multi-gigabyte .nvmrc must not stop a launch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readSmallFile(file: string, maxBytes?: number): string | null;
|
|
6
11
|
export declare function readJson<T>(p: string): T;
|
|
7
12
|
export declare function tryReadJson<T>(p: string): T | null;
|
|
8
13
|
/** Atomic (a crash or a second command never leaves half a file); refused outside temp during the self-test. */
|
package/dist/core/util/fs.js
CHANGED
|
@@ -28,6 +28,29 @@ export async function fileExistsAsync(p) {
|
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A file the sandbox or a repo may have written, read through one handle and only if that handle is a regular file of
|
|
33
|
+
* at most `maxBytes` (null otherwise): a multi-gigabyte .nvmrc must not stop a launch.
|
|
34
|
+
*/
|
|
35
|
+
export function readSmallFile(file, maxBytes = 256 * 1024) {
|
|
36
|
+
let fd = null;
|
|
37
|
+
try {
|
|
38
|
+
fd = fs.openSync(file, 'r');
|
|
39
|
+
const stat = fs.fstatSync(fd);
|
|
40
|
+
if (!stat.isFile() || stat.size > maxBytes)
|
|
41
|
+
return null;
|
|
42
|
+
const buffer = Buffer.alloc(stat.size);
|
|
43
|
+
fs.readSync(fd, buffer, 0, stat.size, 0);
|
|
44
|
+
return buffer.toString('utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (fd !== null)
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
31
54
|
export function readJson(p) {
|
|
32
55
|
return JSON.parse(fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, ''));
|
|
33
56
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { isWin, sleep, captureOutput } from './process.js';
|
|
2
2
|
export { UserError, fail, errMsg } from './errors.js';
|
|
3
3
|
export { parseJsonc, readJsonc } from './jsonc.js';
|
|
4
|
-
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, tryReadJson, writeJson, realPath } from './fs.js';
|
|
4
|
+
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, readSmallFile, tryReadJson, writeJson, realPath, } from './fs.js';
|
|
5
5
|
export { xmlEscape, safeText, plainText } from './text.js';
|
|
6
6
|
export { homeDir, expandPath, expandVariables, isRootPath, isUserProfile, isInsideOrEqual, findOnPath, } from './paths.js';
|
|
7
7
|
export { isLink, removeLink, assertNoLinks } from './links.js';
|
package/dist/core/util/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { isWin, sleep, captureOutput } from './process.js';
|
|
2
2
|
export { UserError, fail, errMsg } from './errors.js';
|
|
3
3
|
export { parseJsonc, readJsonc } from './jsonc.js';
|
|
4
|
-
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, tryReadJson, writeJson, realPath } from './fs.js';
|
|
4
|
+
export { ensureDir, dirExists, fileExists, fileExistsAsync, readJson, readSmallFile, tryReadJson, writeJson, realPath, } from './fs.js';
|
|
5
5
|
export { xmlEscape, safeText, plainText } from './text.js';
|
|
6
6
|
export { homeDir, expandPath, expandVariables, isRootPath, isUserProfile, isInsideOrEqual, findOnPath, } from './paths.js';
|
|
7
7
|
export { isLink, removeLink, assertNoLinks } from './links.js';
|