windows-development-sandbox 1.11.7 → 1.12.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 +12 -6
- package/config.example.json +4 -3
- package/dist/app/features/apps/apps-rows.d.ts +4 -1
- package/dist/app/features/apps/apps-rows.js +7 -2
- package/dist/app/features/apps/apps.def.d.ts +19 -0
- package/dist/app/features/apps/apps.def.js +49 -12
- package/dist/app/features/apps/apps.run.d.ts +2 -0
- package/dist/app/features/apps/apps.run.js +1 -1
- package/dist/app/features/audit/audit.def.d.ts +341 -0
- package/dist/app/features/audit/audit.def.js +454 -0
- package/dist/app/features/audit/audit.run.d.ts +107 -0
- package/dist/app/features/audit/audit.run.js +133 -0
- package/dist/app/features/audit/index.d.ts +2 -0
- package/dist/app/features/audit/index.js +3 -0
- package/dist/app/features/defs.d.ts +194 -1
- package/dist/app/features/defs.js +4 -0
- package/dist/app/features/settings/settings.run.d.ts +3 -0
- package/dist/app/features/settings/settings.run.js +16 -2
- package/dist/app/kernel/define.d.ts +12 -1
- package/dist/app/registry.js +2 -0
- package/dist/cli/args.d.ts +0 -3
- package/dist/cli/args.js +3 -4
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch/prepare.js +1 -1
- package/dist/cli/commands/maintenance/index.d.ts +0 -1
- package/dist/cli/commands/maintenance/index.js +0 -1
- package/dist/cli/commands/registry.js +1 -2
- package/dist/cli/manifest.d.ts +0 -41
- package/dist/cli/manifest.js +0 -14
- package/dist/cli/menu/pages/apps-page.js +2 -15
- package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
- package/dist/cli/menu/pages/maintenance-page.js +51 -8
- package/dist/cli/menu/views.js +2 -0
- package/dist/core/config/defaults.js +1 -1
- package/dist/core/schema/apps.d.ts +9 -0
- package/dist/core/schema/apps.js +9 -0
- package/dist/core/schema/config.d.ts +1 -0
- package/dist/core/schema/config.js +3 -3
- package/dist/core/schema/sandbox.d.ts +1 -0
- package/dist/core/schema/sandbox.js +11 -3
- package/dist/guest/apps/app-installer.js +50 -4
- package/dist/guest/apps/index.d.ts +1 -1
- package/dist/guest/apps/index.js +1 -1
- package/dist/guest/apps/sources.js +1 -1
- package/dist/guest/audit/parse.d.ts +32 -2
- package/dist/guest/audit/parse.js +43 -5
- package/dist/guest/audit/process-audit.d.ts +3 -1
- package/dist/guest/audit/process-audit.js +9 -6
- package/dist/guest/boot/boot.js +12 -3
- package/dist/guest/boot/guide.d.ts +1 -1
- package/dist/guest/boot/guide.js +27 -3
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +27 -2
- package/dist/guest/entries/daemon.js +2 -1
- package/dist/guest/setup/addon-mcp.d.ts +2 -0
- package/dist/guest/setup/addon-mcp.js +49 -0
- package/dist/guest/setup/agent-addons.d.ts +9 -0
- package/dist/guest/setup/agent-addons.js +22 -0
- package/dist/guest/setup/headroom.d.ts +7 -0
- package/dist/guest/setup/headroom.js +60 -0
- package/dist/guest/setup/managed-policy.d.ts +2 -1
- package/dist/guest/setup/managed-policy.js +15 -6
- package/dist/guest/toolchains/plan.js +8 -3
- package/dist/host/audit/attribute.d.ts +46 -0
- package/dist/host/audit/attribute.js +76 -0
- package/dist/host/audit/collect.d.ts +29 -3
- package/dist/host/audit/collect.js +256 -61
- package/dist/host/audit/git-commits.d.ts +29 -0
- package/dist/host/audit/git-commits.js +157 -0
- package/dist/host/audit/index.d.ts +5 -0
- package/dist/host/audit/index.js +6 -0
- package/dist/host/audit/log.d.ts +44 -7
- package/dist/host/audit/log.js +148 -45
- package/dist/host/audit/redact.d.ts +2 -0
- package/dist/host/audit/redact.js +67 -0
- package/dist/runtime/agent.js +110 -15
- package/dist/runtime/daemon.js +569 -316
- package/dist/runtime/db.js +110 -15
- package/dist/runtime/main.js +1011 -672
- package/dist/runtime/netproxy.js +110 -15
- package/dist/runtime/view.js +110 -15
- package/dist/selftest/app.test.js +25 -1
- package/dist/selftest/audit.test.js +354 -47
- package/dist/selftest/hcs.test.js +35 -0
- package/dist/selftest/menu-screens.js +1 -1
- package/dist/selftest/menu.test.js +3 -3
- package/dist/selftest/support/engine-golden.js +9 -0
- package/dist/selftest/support/fixtures.d.ts +1 -0
- package/dist/selftest/support/web-preview.js +94 -0
- package/dist/selftest/toolchains.test.js +147 -2
- package/dist/shared/apps/agent-addons.d.ts +64 -0
- package/dist/shared/apps/agent-addons.js +95 -0
- package/dist/shared/apps/catalogue.d.ts +33 -2
- package/dist/shared/apps/catalogue.js +102 -11
- package/dist/shared/apps/index.d.ts +3 -2
- package/dist/shared/apps/index.js +3 -2
- package/dist/shared/apps/selection.d.ts +2 -0
- package/dist/shared/apps/selection.js +7 -0
- package/dist/sync/watcher.js +9 -1
- package/dist/web/index.html +2 -2
- package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
- package/dist/web/{main-K2JMCAZI.js → main-UD4QCXZR.js} +66 -66
- package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
- package/native/csharp/HcsHost/Hcs.cs +9 -0
- package/native/csharp/HcsHost/Program.cs +26 -1
- package/native/csharp/HcsHost/Share.cs +173 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/addons/ccusage/package-lock.json +111 -0
- package/sandbox/addons/ccusage/package.json +7 -0
- package/sandbox/addons/context7/package-lock.json +1313 -0
- package/sandbox/addons/context7/package.json +7 -0
- package/sandbox/addons/headroom/requirements.in +1 -0
- package/sandbox/addons/headroom/requirements.txt +2922 -0
- package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
- package/sandbox/addons/playwright-mcp/package.json +7 -0
- package/schemas/config.schema.json +13 -4
- package/schemas/sandbox-config.schema.json +2 -2
- package/dist/cli/commands/maintenance/audit.d.ts +0 -3
- package/dist/cli/commands/maintenance/audit.js +0 -58
package/dist/host/audit/log.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
2
|
+
export declare const AUDIT_KINDS: readonly ["proc", "file", "commit", "session", "guard", "note"];
|
|
3
|
+
export type AuditKind = (typeof AUDIT_KINDS)[number];
|
|
4
|
+
/** A session id as the sandbox's run folder names them. */
|
|
5
|
+
export declare const AuditSessionIdSchema: z.ZodString;
|
|
6
|
+
/**
|
|
7
|
+
* One event. k: proc (a program started), file (changed in a project), commit (a git commit in a project), session,
|
|
8
|
+
* guard (repo guard), note.
|
|
9
|
+
*/
|
|
3
10
|
export declare const AuditEventSchema: z.ZodObject<{
|
|
4
11
|
t: z.ZodString;
|
|
5
12
|
k: z.ZodEnum<{
|
|
6
13
|
file: "file";
|
|
7
14
|
session: "session";
|
|
8
15
|
note: "note";
|
|
16
|
+
commit: "commit";
|
|
9
17
|
proc: "proc";
|
|
10
18
|
guard: "guard";
|
|
11
19
|
}>;
|
|
@@ -15,8 +23,23 @@ export declare const AuditEventSchema: z.ZodObject<{
|
|
|
15
23
|
a: z.ZodOptional<z.ZodString>;
|
|
16
24
|
w: z.ZodOptional<z.ZodString>;
|
|
17
25
|
n: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
s: z.ZodOptional<z.ZodString>;
|
|
27
|
+
ag: z.ZodOptional<z.ZodString>;
|
|
28
|
+
pj: z.ZodOptional<z.ZodString>;
|
|
29
|
+
sv: z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
p: "p";
|
|
31
|
+
t: "t";
|
|
32
|
+
}>>;
|
|
33
|
+
h: z.ZodOptional<z.ZodString>;
|
|
34
|
+
au: z.ZodOptional<z.ZodString>;
|
|
18
35
|
}, z.core.$strip>;
|
|
19
36
|
export type AuditEvent = z.output<typeof AuditEventSchema>;
|
|
37
|
+
/** An event with every text field cut to its limit. */
|
|
38
|
+
export declare function clipEvent(e: AuditEvent): AuditEvent;
|
|
39
|
+
/** The audit log keeps at most this many days, whatever audit.days says (older settings allowed up to 365). */
|
|
40
|
+
export declare const MAX_AUDIT_DAYS = 30;
|
|
41
|
+
/** The first day (YYYY-MM-DD, UTC like the day files) of a window of `days` days ending today. */
|
|
42
|
+
export declare function firstDay(days: number, now?: number): string;
|
|
20
43
|
export declare class AuditLog {
|
|
21
44
|
readonly dir: string;
|
|
22
45
|
private readonly limits;
|
|
@@ -27,20 +50,34 @@ export declare class AuditLog {
|
|
|
27
50
|
/** <storage>\audit\<data set name>. */
|
|
28
51
|
static dirFor(storageRoot: string, dataPath: string): string;
|
|
29
52
|
private dayFile;
|
|
30
|
-
|
|
53
|
+
private dayFiles;
|
|
54
|
+
/**
|
|
55
|
+
* Writes events (each cut to its fields' limits and checked, so every line written can be read back). Past
|
|
56
|
+
* audit.maxMb nothing more is written but one note saying so, until old days are deleted.
|
|
57
|
+
*/
|
|
58
|
+
append(events: readonly AuditEvent[]): void;
|
|
59
|
+
private fullNoted;
|
|
60
|
+
/** One line saying the log is full (once until it has room again). */
|
|
61
|
+
private noteFull;
|
|
31
62
|
/** Compresses finished days, then deletes days past the limits. Cheap when there is nothing to do. */
|
|
32
63
|
maintain(today?: string): void;
|
|
33
|
-
/**
|
|
64
|
+
/** The text of one day's file, or '' when it cannot be read within the limits. */
|
|
65
|
+
private dayText;
|
|
66
|
+
/**
|
|
67
|
+
* Events of the last `days` days (never older than audit.days), oldest first, optionally of some kinds and
|
|
68
|
+
* containing a text. At most MAX_READ_EVENTS (the newest).
|
|
69
|
+
*/
|
|
34
70
|
read(o?: {
|
|
35
71
|
days?: number;
|
|
36
|
-
kinds?:
|
|
72
|
+
kinds?: readonly AuditKind[];
|
|
37
73
|
grep?: string;
|
|
38
74
|
}): AuditEvent[];
|
|
39
75
|
/** Total size on disk. */
|
|
40
76
|
bytes(): number;
|
|
41
77
|
}
|
|
42
78
|
/**
|
|
43
|
-
* Folds repeats: the same event (kind, program/path, command line, action, project) within one minute becomes
|
|
44
|
-
* with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
|
|
79
|
+
* Folds repeats: the same event (kind, program/path, command line, action, project, session) within one minute becomes
|
|
80
|
+
* one line with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
|
|
81
|
+
* Commits are never folded or dropped.
|
|
45
82
|
*/
|
|
46
|
-
export declare function fold(events: AuditEvent[], cap?: number): AuditEvent[];
|
|
83
|
+
export declare function fold(events: readonly AuditEvent[], cap?: number): AuditEvent[];
|
package/dist/host/audit/log.js
CHANGED
|
@@ -1,29 +1,77 @@
|
|
|
1
1
|
// The audit log (audit.enabled): one file per day of JSON lines in host-only storage (<storage>\audit\<data set>),
|
|
2
2
|
// which the sandbox cannot reach. Days before today are gzipped; days past audit.days and, past audit.maxMb, the
|
|
3
|
-
// oldest days are deleted. Lines are short (field names are one or two letters) and capped in length
|
|
3
|
+
// oldest days are deleted. Lines are short (field names are one or two letters) and capped in length; reading back is
|
|
4
|
+
// bounded too (a day's file, the events read).
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import zlib from 'node:zlib';
|
|
7
8
|
import { z } from 'zod';
|
|
8
|
-
|
|
9
|
+
export const AUDIT_KINDS = ['proc', 'file', 'commit', 'session', 'guard', 'note'];
|
|
10
|
+
/** A session id as the sandbox's run folder names them. */
|
|
11
|
+
export const AuditSessionIdSchema = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
|
|
12
|
+
/**
|
|
13
|
+
* One event. k: proc (a program started), file (changed in a project), commit (a git commit in a project), session,
|
|
14
|
+
* guard (repo guard), note.
|
|
15
|
+
*/
|
|
9
16
|
export const AuditEventSchema = z.object({
|
|
10
17
|
/** ISO time. */
|
|
11
18
|
t: z.string().max(40),
|
|
12
|
-
k: z.enum(
|
|
13
|
-
/** proc: program; file: path in the project; session: agent; guard/note: what
|
|
19
|
+
k: z.enum(AUDIT_KINDS),
|
|
20
|
+
/** proc: program; file: path in the project; commit: its message (first line); session: agent; guard/note: what. */
|
|
14
21
|
p: z.string().max(1024).optional(),
|
|
15
|
-
/** proc: its command line (capped). */
|
|
22
|
+
/** proc: its command line (capped, secrets hidden). */
|
|
16
23
|
c: z.string().max(2048).optional(),
|
|
17
24
|
/** proc: the program that started it. */
|
|
18
25
|
pp: z.string().max(1024).optional(),
|
|
19
|
-
/** file: change | rename; session: start | end. */
|
|
26
|
+
/** file: change | rename; session: start | end; commit: commit | amend | merge | cherry-pick | revert | rebase. */
|
|
20
27
|
a: z.string().max(32).optional(),
|
|
21
|
-
/** project folder (file, session). */
|
|
28
|
+
/** project folder on this PC (file, commit, session). */
|
|
22
29
|
w: z.string().max(1024).optional(),
|
|
23
30
|
/** how many times it happened in this line's minute (repeats are counted, not repeated). */
|
|
24
31
|
n: z.number().int().min(1).max(1e9).optional(),
|
|
32
|
+
/** The agent session it belongs to, when known. */
|
|
33
|
+
s: AuditSessionIdSchema.optional(),
|
|
34
|
+
/** That session's agent and project (kept here: the session's own record goes with the sandbox). */
|
|
35
|
+
ag: z.string().max(64).optional(),
|
|
36
|
+
pj: z.string().max(128).optional(),
|
|
37
|
+
/**
|
|
38
|
+
* How it was linked to the session: p = by the programs the session started (as the sandbox reported them); t = it
|
|
39
|
+
* was the only session running in that project at the time (likely, not proven).
|
|
40
|
+
*/
|
|
41
|
+
sv: z.enum(['p', 't']).optional(),
|
|
42
|
+
/** commit: its id. */
|
|
43
|
+
h: z
|
|
44
|
+
.string()
|
|
45
|
+
.regex(/^[0-9a-f]{7,64}$/)
|
|
46
|
+
.optional(),
|
|
47
|
+
/** commit: who git says made it ("Name <email>"). */
|
|
48
|
+
au: z.string().max(256).optional(),
|
|
25
49
|
});
|
|
50
|
+
/** Each text field's limit (the schema's): text is cut to it before writing, so a line is never refused on reading. */
|
|
51
|
+
const FIELD_MAX = { p: 1024, c: 2048, pp: 1024, a: 32, w: 1024, ag: 64, pj: 128, au: 256 };
|
|
52
|
+
/** An event with every text field cut to its limit. */
|
|
53
|
+
export function clipEvent(e) {
|
|
54
|
+
const out = { ...e };
|
|
55
|
+
for (const [key, max] of Object.entries(FIELD_MAX)) {
|
|
56
|
+
const v = out[key];
|
|
57
|
+
if (typeof v === 'string' && v.length > max)
|
|
58
|
+
out[key] = v.slice(0, max - 1) + '…';
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
26
62
|
const DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl(\.gz)?$/;
|
|
63
|
+
const DAY_MS = 86400000;
|
|
64
|
+
/** A day's file is never unpacked past this (a damaged or planted file cannot take all memory). */
|
|
65
|
+
const MAX_DAY_BYTES = 128 * 1024 * 1024;
|
|
66
|
+
/** Most events one read returns (the newest ones). */
|
|
67
|
+
const MAX_READ_EVENTS = 400000;
|
|
68
|
+
/** The audit log keeps at most this many days, whatever audit.days says (older settings allowed up to 365). */
|
|
69
|
+
export const MAX_AUDIT_DAYS = 30;
|
|
70
|
+
/** The first day (YYYY-MM-DD, UTC like the day files) of a window of `days` days ending today. */
|
|
71
|
+
export function firstDay(days, now = Date.now()) {
|
|
72
|
+
const kept = Math.min(MAX_AUDIT_DAYS, Math.max(1, days));
|
|
73
|
+
return new Date(now - (kept - 1) * DAY_MS).toISOString().slice(0, 10);
|
|
74
|
+
}
|
|
27
75
|
export class AuditLog {
|
|
28
76
|
dir;
|
|
29
77
|
limits;
|
|
@@ -38,19 +86,55 @@ export class AuditLog {
|
|
|
38
86
|
dayFile(day, gz = false) {
|
|
39
87
|
return path.join(this.dir, day + '.jsonl' + (gz ? '.gz' : ''));
|
|
40
88
|
}
|
|
89
|
+
dayFiles() {
|
|
90
|
+
if (!fs.existsSync(this.dir))
|
|
91
|
+
return [];
|
|
92
|
+
return fs
|
|
93
|
+
.readdirSync(this.dir)
|
|
94
|
+
.filter((n) => DAY_FILE.test(n))
|
|
95
|
+
.sort();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Writes events (each cut to its fields' limits and checked, so every line written can be read back). Past
|
|
99
|
+
* audit.maxMb nothing more is written but one note saying so, until old days are deleted.
|
|
100
|
+
*/
|
|
41
101
|
append(events) {
|
|
42
102
|
if (!events.length)
|
|
43
103
|
return;
|
|
44
104
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
105
|
+
const limit = this.limits.maxMb * 1024 * 1024;
|
|
106
|
+
let size = this.bytes();
|
|
107
|
+
if (size >= limit) {
|
|
108
|
+
this.noteFull(events[events.length - 1].t);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
45
111
|
const byDay = new Map();
|
|
46
112
|
for (const e of events) {
|
|
47
113
|
const day = e.t.slice(0, 10);
|
|
48
|
-
|
|
114
|
+
const checked = AuditEventSchema.safeParse(clipEvent(e));
|
|
115
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !checked.success)
|
|
49
116
|
continue;
|
|
50
|
-
|
|
117
|
+
const line = JSON.stringify(checked.data);
|
|
118
|
+
size += line.length + 1;
|
|
119
|
+
if (size > limit) {
|
|
120
|
+
this.noteFull(e.t);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
byDay.set(day, [...(byDay.get(day) ?? []), line]);
|
|
51
124
|
}
|
|
52
125
|
for (const [day, lines] of byDay)
|
|
53
126
|
fs.appendFileSync(this.dayFile(day), lines.join('\n') + '\n');
|
|
127
|
+
if (size <= limit)
|
|
128
|
+
this.fullNoted = false;
|
|
129
|
+
}
|
|
130
|
+
fullNoted = false;
|
|
131
|
+
/** One line saying the log is full (once until it has room again). */
|
|
132
|
+
noteFull(t) {
|
|
133
|
+
if (this.fullNoted)
|
|
134
|
+
return;
|
|
135
|
+
this.fullNoted = true;
|
|
136
|
+
const note = { t, k: 'note', p: 'the audit log reached audit.maxMb: nothing more is recorded for now' };
|
|
137
|
+
fs.appendFileSync(this.dayFile(t.slice(0, 10)), JSON.stringify(note) + '\n');
|
|
54
138
|
}
|
|
55
139
|
/** Compresses finished days, then deletes days past the limits. Cheap when there is nothing to do. */
|
|
56
140
|
maintain(today = new Date().toISOString().slice(0, 10)) {
|
|
@@ -61,41 +145,58 @@ export class AuditLog {
|
|
|
61
145
|
if (!m || m[2] || m[1] >= today)
|
|
62
146
|
continue;
|
|
63
147
|
const file = path.join(this.dir, name);
|
|
64
|
-
|
|
148
|
+
// Lines that arrived for a day already compressed (late reports) are added as another gzip member: the day's
|
|
149
|
+
// compressed file is never written over.
|
|
150
|
+
fs.appendFileSync(file + '.gz', zlib.gzipSync(fs.readFileSync(file)));
|
|
65
151
|
fs.rmSync(file);
|
|
66
152
|
}
|
|
67
|
-
const oldest =
|
|
68
|
-
const
|
|
69
|
-
.readdirSync(this.dir)
|
|
70
|
-
.filter((n) => DAY_FILE.test(n))
|
|
71
|
-
.sort();
|
|
72
|
-
for (const name of files.filter((n) => DAY_FILE.exec(n)[1] < oldest))
|
|
153
|
+
const oldest = firstDay(this.limits.days, Date.parse(today));
|
|
154
|
+
for (const name of this.dayFiles().filter((n) => DAY_FILE.exec(n)[1] < oldest))
|
|
73
155
|
fs.rmSync(path.join(this.dir, name), { force: true });
|
|
74
|
-
let left =
|
|
75
|
-
.readdirSync(this.dir)
|
|
76
|
-
.filter((n) => DAY_FILE.test(n))
|
|
77
|
-
.sort();
|
|
156
|
+
let left = this.dayFiles();
|
|
78
157
|
const size = () => left.reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
|
|
79
158
|
while (left.length > 1 && size() > this.limits.maxMb * 1024 * 1024) {
|
|
80
159
|
fs.rmSync(path.join(this.dir, left[0]), { force: true });
|
|
81
160
|
left = left.slice(1);
|
|
82
161
|
}
|
|
83
162
|
}
|
|
84
|
-
/**
|
|
163
|
+
/** The text of one day's file, or '' when it cannot be read within the limits. */
|
|
164
|
+
dayText(name) {
|
|
165
|
+
try {
|
|
166
|
+
const file = path.join(this.dir, name);
|
|
167
|
+
const stat = fs.lstatSync(file);
|
|
168
|
+
if (!stat.isFile() || stat.size > MAX_DAY_BYTES)
|
|
169
|
+
return '';
|
|
170
|
+
const raw = fs.readFileSync(file);
|
|
171
|
+
return name.endsWith('.gz')
|
|
172
|
+
? zlib.gunzipSync(raw, { maxOutputLength: MAX_DAY_BYTES }).toString('utf8')
|
|
173
|
+
: raw.toString('utf8');
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Damaged or too big once unpacked: that day is left out rather than failing the whole read.
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Events of the last `days` days (never older than audit.days), oldest first, optionally of some kinds and
|
|
182
|
+
* containing a text. At most MAX_READ_EVENTS (the newest).
|
|
183
|
+
*/
|
|
85
184
|
read(o = {}) {
|
|
86
|
-
|
|
87
|
-
return [];
|
|
88
|
-
const since = new Date(Date.now() - ((o.days ?? 1) - 1) * 86400000).toISOString().slice(0, 10);
|
|
185
|
+
const since = firstDay(Math.min(o.days ?? 1, this.limits.days));
|
|
89
186
|
const needle = o.grep?.toLowerCase();
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
187
|
+
const days = [];
|
|
188
|
+
let count = 0;
|
|
189
|
+
// Newest day first, so the cap keeps the newest events.
|
|
190
|
+
for (const name of this.dayFiles()
|
|
191
|
+
.filter((n) => DAY_FILE.exec(n)[1] >= since)
|
|
192
|
+
.reverse()) {
|
|
193
|
+
if (count >= MAX_READ_EVENTS)
|
|
194
|
+
break;
|
|
195
|
+
const events = [];
|
|
196
|
+
for (const line of this.dayText(name).split('\n')) {
|
|
197
|
+
if (!line.trim() || line.length > 16384)
|
|
198
|
+
continue;
|
|
199
|
+
if (needle && !line.toLowerCase().includes(needle))
|
|
99
200
|
continue;
|
|
100
201
|
let parsed;
|
|
101
202
|
try {
|
|
@@ -109,26 +210,24 @@ export class AuditLog {
|
|
|
109
210
|
continue;
|
|
110
211
|
if (o.kinds?.length && !o.kinds.includes(e.data.k))
|
|
111
212
|
continue;
|
|
112
|
-
if (needle && !line.toLowerCase().includes(needle))
|
|
113
|
-
continue;
|
|
114
213
|
events.push(e.data);
|
|
115
214
|
}
|
|
215
|
+
const room = MAX_READ_EVENTS - count;
|
|
216
|
+
const kept = events.length > room ? events.slice(events.length - room) : events;
|
|
217
|
+
count += kept.length;
|
|
218
|
+
days.push(kept);
|
|
116
219
|
}
|
|
117
|
-
return
|
|
220
|
+
return days.reverse().flat();
|
|
118
221
|
}
|
|
119
222
|
/** Total size on disk. */
|
|
120
223
|
bytes() {
|
|
121
|
-
|
|
122
|
-
return 0;
|
|
123
|
-
return fs
|
|
124
|
-
.readdirSync(this.dir)
|
|
125
|
-
.filter((n) => DAY_FILE.test(n))
|
|
126
|
-
.reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
|
|
224
|
+
return this.dayFiles().reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
|
|
127
225
|
}
|
|
128
226
|
}
|
|
129
227
|
/**
|
|
130
|
-
* Folds repeats: the same event (kind, program/path, command line, action, project) within one minute becomes
|
|
131
|
-
* with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
|
|
228
|
+
* Folds repeats: the same event (kind, program/path, command line, action, project, session) within one minute becomes
|
|
229
|
+
* one line with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
|
|
230
|
+
* Commits are never folded or dropped.
|
|
132
231
|
*/
|
|
133
232
|
export function fold(events, cap = 300) {
|
|
134
233
|
const out = [];
|
|
@@ -136,8 +235,12 @@ export function fold(events, cap = 300) {
|
|
|
136
235
|
const perMinute = new Map();
|
|
137
236
|
const dropped = new Map();
|
|
138
237
|
for (const e of events) {
|
|
238
|
+
if (e.k === 'commit') {
|
|
239
|
+
out.push({ ...e });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
139
242
|
const minute = e.t.slice(0, 16);
|
|
140
|
-
const key = [minute, e.k, e.p, e.c, e.a, e.w].join('\0');
|
|
243
|
+
const key = [minute, e.k, e.p, e.c, e.a, e.w, e.s].join('\0');
|
|
141
244
|
const seen = index.get(key);
|
|
142
245
|
if (seen) {
|
|
143
246
|
seen.n = (seen.n ?? 1) + (e.n ?? 1);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Secrets never reach the audit log: a command line (or a commit message) is scrubbed before it is written. Values that
|
|
2
|
+
// are credentials whatever they are called (tokens, keys, passwords in URLs: secret-patterns.ts), and the value after
|
|
3
|
+
// a name that says it holds one (--password x, --token=x, API_KEY=x, -p<pw> for mysql, Authorization: ...), become ***.
|
|
4
|
+
// The text is already capped (2 KB), so these simple patterns stay cheap.
|
|
5
|
+
import { REDACTED } from '../../core/schema/index.js';
|
|
6
|
+
import { SECRET_VALUES, SECRET_WORDS } from '../profile/secret-patterns.js';
|
|
7
|
+
/** The value patterns, each finding every match; a match runs on to the end of its word (the rest of the secret). */
|
|
8
|
+
const VALUES = SECRET_VALUES.map((r) => new RegExp('(?:' + r.source + ')[^\\s"\'`]*', r.flags.includes('g') ? r.flags : r.flags + 'g'));
|
|
9
|
+
/**
|
|
10
|
+
* `--name value`, `--name=value`, `/name:value`, `NAME=value`, `$env:NAME = "value"`: the flag's dashes (a value after
|
|
11
|
+
* a space only counts for a flag), the name, the separator, and the value (looked at, not taken: the next word may be
|
|
12
|
+
* a name of its own).
|
|
13
|
+
*/
|
|
14
|
+
const NAMED = /(?<=^|[\s"'`;&|(])(?:\$env:)?(--?|\/)?([A-Za-z][\w.-]{1,63})(\s*[=:]\s*|\s+)(?=("[^"]*"|'[^']*'|[^\s"'`;&|)]+))/g;
|
|
15
|
+
/** Headers whose whole value is a credential. */
|
|
16
|
+
const HEADERS = /\b(authorization|proxy-authorization|x-api-key|api-key|x-auth-token|cookie|private-token)(\s*:\s*)[^"'\r\n]+/gi;
|
|
17
|
+
/** mysql -p<password> / -pPASSWORD (no space), and user:password for curl (-u x:y, -ux:y, --user=x:y). */
|
|
18
|
+
const SHORT = /(\s-p)(?!\s)([^\s"'`]+)|(\s(?:-u|--user)(?:\s+|=)?)([^\s:"'`]+):([^\s"'`]+)/g;
|
|
19
|
+
/** A token as a URL's user name (https://<token>@host), and PowerShell's ConvertTo-SecureString "<password>". */
|
|
20
|
+
const MORE = [
|
|
21
|
+
/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'`]{8,}@/gi,
|
|
22
|
+
/(ConvertTo-SecureString\s+(?:-String\s+)?)("[^"]*"|'[^']*'|[^\s"'`)]+)/gi,
|
|
23
|
+
// `-p <password>` only where it means one (docker/podman/npm login, sshpass): elsewhere -p is a port or a package.
|
|
24
|
+
/(\blogin\b[^&|;\r\n]{0,200}?\s-p\s+|\bsshpass\s+-p\s+)("[^"]*"|'[^']*'|[^\s"'`]+)/gi,
|
|
25
|
+
];
|
|
26
|
+
/** Words that make any name a secret's wherever they stand in it (PGPASSWORD, accessToken, mySecretKey). */
|
|
27
|
+
const ANYWHERE = /PASSW(OR)?D|TOKEN|SECRET|APIKEY|API_KEY|ACCESS_?KEY|PRIVATE_?KEY|CREDENTIAL/;
|
|
28
|
+
/** Does a name (a flag or a variable) say it holds a secret? camelCase counts as words. */
|
|
29
|
+
function secretName(name) {
|
|
30
|
+
const upper = name
|
|
31
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
32
|
+
.toUpperCase()
|
|
33
|
+
.replace(/[-.]/g, '_');
|
|
34
|
+
return SECRET_WORDS.test(upper) || ANYWHERE.test(upper);
|
|
35
|
+
}
|
|
36
|
+
/** The values after names that say they hold a secret, as *** (see NAMED). */
|
|
37
|
+
function redactNamed(text) {
|
|
38
|
+
const cuts = [];
|
|
39
|
+
for (const m of text.matchAll(NAMED)) {
|
|
40
|
+
const [whole, dashes, name, sep, value] = m;
|
|
41
|
+
if (!secretName(name) || !(dashes || sep.trim()) || value === REDACTED || value.startsWith('-'))
|
|
42
|
+
continue;
|
|
43
|
+
const start = (m.index ?? 0) + whole.length;
|
|
44
|
+
cuts.push([start, start + value.length]);
|
|
45
|
+
}
|
|
46
|
+
let out = '';
|
|
47
|
+
let from = 0;
|
|
48
|
+
for (const [start, end] of cuts) {
|
|
49
|
+
if (start < from)
|
|
50
|
+
continue;
|
|
51
|
+
out += text.slice(from, start) + REDACTED;
|
|
52
|
+
from = end;
|
|
53
|
+
}
|
|
54
|
+
return out + text.slice(from);
|
|
55
|
+
}
|
|
56
|
+
/** `text` with every credential in it replaced by ***. */
|
|
57
|
+
export function redactCommandLine(text) {
|
|
58
|
+
let out = text;
|
|
59
|
+
for (const r of VALUES)
|
|
60
|
+
out = out.replace(r, REDACTED);
|
|
61
|
+
out = out.replace(HEADERS, (_m, name, sep) => name + sep + REDACTED);
|
|
62
|
+
for (const r of MORE)
|
|
63
|
+
out = out.replace(r, (_m, lead) => lead + REDACTED + (r === MORE[0] ? '@' : ''));
|
|
64
|
+
out = redactNamed(out);
|
|
65
|
+
out = out.replace(SHORT, (m, p, _pw, u, user) => p ? p + REDACTED : u ? u + user + ':' + REDACTED : m);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
package/dist/runtime/agent.js
CHANGED
|
@@ -25541,14 +25541,19 @@ var NetworkSchema = external_exports.strictObject({
|
|
|
25541
25541
|
});
|
|
25542
25542
|
var AuditSchema = external_exports.strictObject({
|
|
25543
25543
|
enabled: external_exports.boolean().describe(
|
|
25544
|
-
"Keep an audit log on this PC of what
|
|
25544
|
+
"Keep an audit log on this PC of what your agents did: programs started (with their command lines, secrets hidden), files changed in the projects, git commits, sessions, and what the repo guard caught, each linked to the agent session that did it where that can be known. The sandbox cannot change it. Read it with sandbox audit or on the Activity page."
|
|
25545
|
+
),
|
|
25546
|
+
days: external_exports.number().int().min(1).max(365).describe(
|
|
25547
|
+
"Days the audit log is kept (1 to 30; a larger number keeps 30); older days are deleted when a sandbox starts and once a day."
|
|
25545
25548
|
),
|
|
25546
|
-
days: external_exports.number().int().min(1).max(365).describe("Days the audit log is kept."),
|
|
25547
25549
|
maxMb: external_exports.number().int().min(5).max(5e3).describe("Most space the audit log may take, in MB (older days go first). Days before today are compressed."),
|
|
25548
25550
|
processes: external_exports.boolean().describe(
|
|
25549
25551
|
"Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed)."
|
|
25550
25552
|
),
|
|
25551
|
-
files: external_exports.boolean().describe("Record the files changed in the
|
|
25553
|
+
files: external_exports.boolean().describe("Record the files changed in the projects while the sandbox runs."),
|
|
25554
|
+
commits: external_exports.boolean().describe(
|
|
25555
|
+
"Record the git commits made in the projects while the sandbox runs (read from each project's .git folder on this PC)."
|
|
25556
|
+
)
|
|
25552
25557
|
});
|
|
25553
25558
|
var WorkspaceFilterSchema = external_exports.strictObject({
|
|
25554
25559
|
enabled: external_exports.boolean().describe(
|
|
@@ -25751,7 +25756,100 @@ var ToolchainsSchema = external_exports.strictObject({
|
|
|
25751
25756
|
|
|
25752
25757
|
// src/shared/apps/catalogue.ts
|
|
25753
25758
|
var gh = (github, asset) => ({ github, asset });
|
|
25759
|
+
var RTK_VERSION = "1.0.1";
|
|
25760
|
+
var RTK_URL = "https://github.com/rtk-ai/rtk/releases/download/v" + RTK_VERSION + "/rtk-oss-x86_64-pc-windows-msvc.zip";
|
|
25761
|
+
var RTK_SHA256 = "0e53e2eca471e15fbd3636e64e6bc0b182cbc278ed0339c80450a2dd03f4cd6b";
|
|
25762
|
+
var HEADROOM_VERSION = "0.41.0";
|
|
25763
|
+
var CCUSAGE_VERSION = "20.0.36";
|
|
25764
|
+
var PLAYWRIGHT_MCP_VERSION = "0.0.83";
|
|
25765
|
+
var CONTEXT7_MCP_VERSION = "4.4.2";
|
|
25754
25766
|
var APPS = [
|
|
25767
|
+
// ---- agent add-ons
|
|
25768
|
+
{
|
|
25769
|
+
id: "rtk",
|
|
25770
|
+
name: "rtk (Rust Token Killer)",
|
|
25771
|
+
category: "agents",
|
|
25772
|
+
about: "Compacts the output of shell commands (git, tests, builds, ls ...) before the agent reads it, so it uses fewer tokens. Claude Code's Bash commands go through it on their own.",
|
|
25773
|
+
size: "6 MB",
|
|
25774
|
+
source: { url: RTK_URL, sha256: RTK_SHA256 },
|
|
25775
|
+
exe: "rtk.exe",
|
|
25776
|
+
cli: true,
|
|
25777
|
+
plugsInto: "Claude Code through a hook in the sandbox's managed settings; other agents are told to use it",
|
|
25778
|
+
userOnly: true
|
|
25779
|
+
},
|
|
25780
|
+
{
|
|
25781
|
+
id: "headroom",
|
|
25782
|
+
name: "Headroom",
|
|
25783
|
+
category: "agents",
|
|
25784
|
+
about: "Compresses what Claude Code sends to the model (tool output, logs, files) so it uses fewer tokens: a local proxy on 127.0.0.1:8787 that sits in Claude Code's API path, offline apart from forwarding to api.anthropic.com.",
|
|
25785
|
+
size: "150 MB",
|
|
25786
|
+
source: { locked: "uv", package: "headroom-ai", version: HEADROOM_VERSION },
|
|
25787
|
+
exe: "headroom.exe",
|
|
25788
|
+
service: true,
|
|
25789
|
+
plugsInto: "Claude Code: the sandbox starts the proxy and points Claude Code at it (managed settings)",
|
|
25790
|
+
userOnly: true
|
|
25791
|
+
},
|
|
25792
|
+
{
|
|
25793
|
+
id: "ccusage",
|
|
25794
|
+
name: "ccusage",
|
|
25795
|
+
category: "agents",
|
|
25796
|
+
about: "Reports the agents' token use and cost by day, month or session from their logs in the sandbox (`ccusage daily`; add --offline to skip fetching prices).",
|
|
25797
|
+
size: "5 MB",
|
|
25798
|
+
source: { locked: "npm", package: "ccusage", version: CCUSAGE_VERSION },
|
|
25799
|
+
exe: "ccusage.cmd",
|
|
25800
|
+
cli: true,
|
|
25801
|
+
plugsInto: "Reads Claude Code's session logs in the sandbox; changes nothing",
|
|
25802
|
+
userOnly: true
|
|
25803
|
+
},
|
|
25804
|
+
{
|
|
25805
|
+
id: "playwright-mcp",
|
|
25806
|
+
name: "Playwright MCP",
|
|
25807
|
+
category: "agents",
|
|
25808
|
+
about: "Lets Claude Code drive a browser (Microsoft Edge, already in the sandbox) to open pages, click, fill forms and take screenshots: the MCP server of microsoft/playwright-mcp.",
|
|
25809
|
+
size: "20 MB",
|
|
25810
|
+
source: { locked: "npm", package: "@playwright/mcp", version: PLAYWRIGHT_MCP_VERSION },
|
|
25811
|
+
exe: "playwright-mcp.cmd",
|
|
25812
|
+
service: true,
|
|
25813
|
+
mcp: { name: "sbx-playwright", script: "node_modules/@playwright/mcp/cli.js", args: ["--browser", "msedge"] },
|
|
25814
|
+
plugsInto: "Claude Code as the MCP server sbx-playwright (the sandbox's Claude settings)",
|
|
25815
|
+
userOnly: true
|
|
25816
|
+
},
|
|
25817
|
+
{
|
|
25818
|
+
id: "context7",
|
|
25819
|
+
name: "Context7 MCP",
|
|
25820
|
+
category: "agents",
|
|
25821
|
+
about: "Up-to-date documentation and code examples for libraries, looked up by Claude Code while it works (upstash/context7). The library names and questions go to Upstash's context7.com.",
|
|
25822
|
+
size: "55 MB",
|
|
25823
|
+
source: { locked: "npm", package: "@upstash/context7-mcp", version: CONTEXT7_MCP_VERSION },
|
|
25824
|
+
exe: "context7-mcp.cmd",
|
|
25825
|
+
service: true,
|
|
25826
|
+
mcp: { name: "sbx-context7", script: "node_modules/@upstash/context7-mcp/dist/index.js" },
|
|
25827
|
+
plugsInto: "Claude Code as the MCP server sbx-context7 (the sandbox's Claude settings)",
|
|
25828
|
+
userOnly: true
|
|
25829
|
+
},
|
|
25830
|
+
{
|
|
25831
|
+
id: "claude-mem",
|
|
25832
|
+
name: "claude-mem",
|
|
25833
|
+
category: "agents",
|
|
25834
|
+
about: "Persistent memory for Claude Code (a plugin you install on this PC): its database is brought into the sandbox.",
|
|
25835
|
+
size: "",
|
|
25836
|
+
source: { resolve: "setting" },
|
|
25837
|
+
exe: "",
|
|
25838
|
+
setting: "claudeProfile.syncClaudeMem",
|
|
25839
|
+
plugsInto: "Claude Code, with your Claude settings copied from this PC",
|
|
25840
|
+
userOnly: true
|
|
25841
|
+
},
|
|
25842
|
+
{
|
|
25843
|
+
id: "rea",
|
|
25844
|
+
name: "REA (Reverse Engineer Anything)",
|
|
25845
|
+
category: "agents",
|
|
25846
|
+
about: "Agent-driven reverse engineering (MCP server and CLI, by morluto). Run `rea setup` once to add it to Claude Code and Codex. Native binaries need Ghidra; needs Node 22.19+.",
|
|
25847
|
+
size: "8 MB",
|
|
25848
|
+
source: { npm: "rea-agents" },
|
|
25849
|
+
exe: "rea.cmd",
|
|
25850
|
+
cli: true,
|
|
25851
|
+
plugsInto: "Claude Code and Codex as an MCP server, after `rea setup`"
|
|
25852
|
+
},
|
|
25755
25853
|
// ---- editors
|
|
25756
25854
|
{
|
|
25757
25855
|
id: "vscode",
|
|
@@ -26213,16 +26311,6 @@ var APPS = [
|
|
|
26213
26311
|
cli: true
|
|
26214
26312
|
},
|
|
26215
26313
|
// ---- reverse engineering
|
|
26216
|
-
{
|
|
26217
|
-
id: "rea",
|
|
26218
|
-
name: "REA (Reverse Engineer Anything)",
|
|
26219
|
-
category: "reversing",
|
|
26220
|
-
about: "Agent-driven reverse engineering (MCP server and CLI, by morluto). Run `rea setup` once to add it to Claude Code and Codex. Native binaries need Ghidra; needs Node 22.19+.",
|
|
26221
|
-
size: "8 MB",
|
|
26222
|
-
source: { npm: "rea-agents" },
|
|
26223
|
-
exe: "rea.cmd",
|
|
26224
|
-
cli: true
|
|
26225
|
-
},
|
|
26226
26314
|
{
|
|
26227
26315
|
id: "ghidra",
|
|
26228
26316
|
name: "Ghidra",
|
|
@@ -26502,6 +26590,8 @@ var APPS = [
|
|
|
26502
26590
|
// src/core/schema/apps.ts
|
|
26503
26591
|
var CATALOGUE_IDS = APPS.filter((a) => !a.setting).map((a) => a.id);
|
|
26504
26592
|
var AppIdSchema = external_exports.enum(CATALOGUE_IDS);
|
|
26593
|
+
var PROJECT_IDS = APPS.filter((a) => !a.setting && !a.userOnly).map((a) => a.id);
|
|
26594
|
+
var ProjectAppIdSchema = external_exports.enum(PROJECT_IDS);
|
|
26505
26595
|
var CustomAppIdSchema = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,40}$/, "lower-case letters, digits and - only");
|
|
26506
26596
|
var compiles = (pattern) => {
|
|
26507
26597
|
try {
|
|
@@ -26525,6 +26615,11 @@ var CustomAppSchema = external_exports.strictObject({
|
|
|
26525
26615
|
}).refine((a) => a.github && a.asset ? !a.url : Boolean(a.url) && !a.github && !a.asset, {
|
|
26526
26616
|
message: "give github + asset, or url (not both)"
|
|
26527
26617
|
});
|
|
26618
|
+
var ProjectAppsSchema = external_exports.strictObject({
|
|
26619
|
+
install: external_exports.array(ProjectAppIdSchema).describe(
|
|
26620
|
+
"Apps from the catalogue this project adds to yours (agent add-ons such as rtk, Headroom and the MCP servers are set in your own settings only)."
|
|
26621
|
+
)
|
|
26622
|
+
});
|
|
26528
26623
|
var AppsSchema = external_exports.strictObject({
|
|
26529
26624
|
install: external_exports.array(AppIdSchema).describe(
|
|
26530
26625
|
"Apps from the catalogue to install in every sandbox (the Apps screen in the menu lists them by category). Each is downloaded once and kept."
|
|
@@ -26689,8 +26784,8 @@ var LocalOverridesSchema = external_exports.strictObject({
|
|
|
26689
26784
|
attach: AttachSchema.partial(),
|
|
26690
26785
|
personalization: toggle("Copy your desktop look into the sandbox.").partial(),
|
|
26691
26786
|
// Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add
|
|
26692
|
-
// them.
|
|
26693
|
-
apps:
|
|
26787
|
+
// them, nor agent add-ons that are yours only (rtk hooks every agent command, Headroom sits in its API path).
|
|
26788
|
+
apps: ProjectAppsSchema.partial(),
|
|
26694
26789
|
claudeProfile: ClaudeProfileSchema.pick({
|
|
26695
26790
|
enabled: true,
|
|
26696
26791
|
overwrite: true,
|