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.
Files changed (121) hide show
  1. package/README.md +12 -6
  2. package/config.example.json +4 -3
  3. package/dist/app/features/apps/apps-rows.d.ts +4 -1
  4. package/dist/app/features/apps/apps-rows.js +7 -2
  5. package/dist/app/features/apps/apps.def.d.ts +19 -0
  6. package/dist/app/features/apps/apps.def.js +49 -12
  7. package/dist/app/features/apps/apps.run.d.ts +2 -0
  8. package/dist/app/features/apps/apps.run.js +1 -1
  9. package/dist/app/features/audit/audit.def.d.ts +341 -0
  10. package/dist/app/features/audit/audit.def.js +454 -0
  11. package/dist/app/features/audit/audit.run.d.ts +107 -0
  12. package/dist/app/features/audit/audit.run.js +133 -0
  13. package/dist/app/features/audit/index.d.ts +2 -0
  14. package/dist/app/features/audit/index.js +3 -0
  15. package/dist/app/features/defs.d.ts +194 -1
  16. package/dist/app/features/defs.js +4 -0
  17. package/dist/app/features/settings/settings.run.d.ts +3 -0
  18. package/dist/app/features/settings/settings.run.js +16 -2
  19. package/dist/app/kernel/define.d.ts +12 -1
  20. package/dist/app/registry.js +2 -0
  21. package/dist/cli/args.d.ts +0 -3
  22. package/dist/cli/args.js +3 -4
  23. package/dist/cli/commands/index.d.ts +1 -1
  24. package/dist/cli/commands/index.js +1 -1
  25. package/dist/cli/commands/launch/prepare.js +1 -1
  26. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  27. package/dist/cli/commands/maintenance/index.js +0 -1
  28. package/dist/cli/commands/registry.js +1 -2
  29. package/dist/cli/manifest.d.ts +0 -41
  30. package/dist/cli/manifest.js +0 -14
  31. package/dist/cli/menu/pages/apps-page.js +2 -15
  32. package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
  33. package/dist/cli/menu/pages/maintenance-page.js +51 -8
  34. package/dist/cli/menu/views.js +2 -0
  35. package/dist/core/config/defaults.js +1 -1
  36. package/dist/core/schema/apps.d.ts +9 -0
  37. package/dist/core/schema/apps.js +9 -0
  38. package/dist/core/schema/config.d.ts +1 -0
  39. package/dist/core/schema/config.js +3 -3
  40. package/dist/core/schema/sandbox.d.ts +1 -0
  41. package/dist/core/schema/sandbox.js +11 -3
  42. package/dist/guest/apps/app-installer.js +50 -4
  43. package/dist/guest/apps/index.d.ts +1 -1
  44. package/dist/guest/apps/index.js +1 -1
  45. package/dist/guest/apps/sources.js +1 -1
  46. package/dist/guest/audit/parse.d.ts +32 -2
  47. package/dist/guest/audit/parse.js +43 -5
  48. package/dist/guest/audit/process-audit.d.ts +3 -1
  49. package/dist/guest/audit/process-audit.js +9 -6
  50. package/dist/guest/boot/boot.js +12 -3
  51. package/dist/guest/boot/guide.d.ts +1 -1
  52. package/dist/guest/boot/guide.js +27 -3
  53. package/dist/guest/daemon/sessions.d.ts +7 -0
  54. package/dist/guest/daemon/sessions.js +27 -2
  55. package/dist/guest/entries/daemon.js +2 -1
  56. package/dist/guest/setup/addon-mcp.d.ts +2 -0
  57. package/dist/guest/setup/addon-mcp.js +49 -0
  58. package/dist/guest/setup/agent-addons.d.ts +9 -0
  59. package/dist/guest/setup/agent-addons.js +22 -0
  60. package/dist/guest/setup/headroom.d.ts +7 -0
  61. package/dist/guest/setup/headroom.js +60 -0
  62. package/dist/guest/setup/managed-policy.d.ts +2 -1
  63. package/dist/guest/setup/managed-policy.js +15 -6
  64. package/dist/guest/toolchains/plan.js +8 -3
  65. package/dist/host/audit/attribute.d.ts +46 -0
  66. package/dist/host/audit/attribute.js +76 -0
  67. package/dist/host/audit/collect.d.ts +29 -3
  68. package/dist/host/audit/collect.js +256 -61
  69. package/dist/host/audit/git-commits.d.ts +29 -0
  70. package/dist/host/audit/git-commits.js +157 -0
  71. package/dist/host/audit/index.d.ts +5 -0
  72. package/dist/host/audit/index.js +6 -0
  73. package/dist/host/audit/log.d.ts +44 -7
  74. package/dist/host/audit/log.js +148 -45
  75. package/dist/host/audit/redact.d.ts +2 -0
  76. package/dist/host/audit/redact.js +67 -0
  77. package/dist/runtime/agent.js +110 -15
  78. package/dist/runtime/daemon.js +569 -316
  79. package/dist/runtime/db.js +110 -15
  80. package/dist/runtime/main.js +1011 -672
  81. package/dist/runtime/netproxy.js +110 -15
  82. package/dist/runtime/view.js +110 -15
  83. package/dist/selftest/app.test.js +25 -1
  84. package/dist/selftest/audit.test.js +354 -47
  85. package/dist/selftest/hcs.test.js +35 -0
  86. package/dist/selftest/menu-screens.js +1 -1
  87. package/dist/selftest/menu.test.js +3 -3
  88. package/dist/selftest/support/engine-golden.js +9 -0
  89. package/dist/selftest/support/fixtures.d.ts +1 -0
  90. package/dist/selftest/support/web-preview.js +94 -0
  91. package/dist/selftest/toolchains.test.js +147 -2
  92. package/dist/shared/apps/agent-addons.d.ts +64 -0
  93. package/dist/shared/apps/agent-addons.js +95 -0
  94. package/dist/shared/apps/catalogue.d.ts +33 -2
  95. package/dist/shared/apps/catalogue.js +102 -11
  96. package/dist/shared/apps/index.d.ts +3 -2
  97. package/dist/shared/apps/index.js +3 -2
  98. package/dist/shared/apps/selection.d.ts +2 -0
  99. package/dist/shared/apps/selection.js +7 -0
  100. package/dist/sync/watcher.js +9 -1
  101. package/dist/web/index.html +2 -2
  102. package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
  103. package/dist/web/{main-K2JMCAZI.js → main-UD4QCXZR.js} +66 -66
  104. package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
  105. package/native/csharp/HcsHost/Hcs.cs +9 -0
  106. package/native/csharp/HcsHost/Program.cs +26 -1
  107. package/native/csharp/HcsHost/Share.cs +173 -0
  108. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  109. package/package.json +1 -1
  110. package/sandbox/addons/ccusage/package-lock.json +111 -0
  111. package/sandbox/addons/ccusage/package.json +7 -0
  112. package/sandbox/addons/context7/package-lock.json +1313 -0
  113. package/sandbox/addons/context7/package.json +7 -0
  114. package/sandbox/addons/headroom/requirements.in +1 -0
  115. package/sandbox/addons/headroom/requirements.txt +2922 -0
  116. package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
  117. package/sandbox/addons/playwright-mcp/package.json +7 -0
  118. package/schemas/config.schema.json +13 -4
  119. package/schemas/sandbox-config.schema.json +2 -2
  120. package/dist/cli/commands/maintenance/audit.d.ts +0 -3
  121. package/dist/cli/commands/maintenance/audit.js +0 -58
@@ -386,20 +386,6 @@ const COMMAND_LIST = [
386
386
  json,
387
387
  },
388
388
  { name: 'sync', summary: 'copy conversation history back to this PC now', group: 'Data', args: [], options: [] },
389
- {
390
- name: 'audit',
391
- summary: 'what happened in your sandboxes: programs started, files changed, sessions, repo guard (turn it on: sandbox config set audit.enabled true)',
392
- group: 'Health',
393
- json,
394
- args: [],
395
- options: [
396
- { flag: '--days', type: 'number', value: 'n', description: 'the last n days (default: today)', default: 1 },
397
- text('--kind', 'kind', 'only proc, file, session or guard events', {
398
- choices: ['proc', 'file', 'session', 'guard'],
399
- }),
400
- text('--grep', 'text', 'only events containing this text'),
401
- ],
402
- },
403
389
  {
404
390
  name: 'logs',
405
391
  summary: 'print the newest sandbox log',
@@ -4,7 +4,7 @@
4
4
  // (apps.toggle), so the staging stays here: "r" runs the feature's request operation, "c" stages your own programs.
5
5
  import _ from 'lodash';
6
6
  import { errMsg } from '../../../core/util/index.js';
7
- import { APP_GROUPS, appIsOn, appRows, customRows, CUSTOM_KEY, entriesOf, INSTALL, ISSUES_URL, requestApp, REQUEST_APP, SAVED, } from '../../../app/features/apps/index.js';
7
+ import { APP_GROUPS, appDetails, appIsOn, appRows, customRows, CUSTOM_KEY, entriesOf, INSTALL, ISSUES_URL, requestApp, REQUEST_APP, SAVED, } from '../../../app/features/apps/index.js';
8
8
  import { action } from '../../../app/kernel/index.js';
9
9
  import { Theme } from '../../tui/index.js';
10
10
  import { ValueEditor } from '../settings/value-editor.js';
@@ -37,20 +37,7 @@ export function appsCategories(file) {
37
37
  ];
38
38
  }
39
39
  function appItem(row) {
40
- const notes = [
41
- Theme.muted('Download ') +
42
- '~' +
43
- row.size +
44
- ' once, kept in C:\\AgentData\\tools\\' +
45
- (row.setting ? '' : 'apps\\') +
46
- row.id,
47
- Theme.muted('Starts ') + (row.cli ? 'from a terminal (on PATH)' : 'from the desktop or Start > Sandbox apps'),
48
- ...(row.mesa ? [Theme.muted('Graphics ') + 'OpenGL on the CPU (Mesa) is added for it'] : []),
49
- ...(row.fullGitOnly
50
- ? [Theme.warn('Needs full git access (Settings > Projects & setup > Git): it pushes with its own git.')]
51
- : []),
52
- ...(row.setting ? [Theme.muted('Setting ') + row.setting] : []),
53
- ];
40
+ const notes = appDetails(row).map((d) => d.warn ? Theme.warn(d.text) : (d.label ? Theme.muted(d.label.padEnd(10)) : '') + d.text);
54
41
  return {
55
42
  value: row.id,
56
43
  label: row.name,
@@ -3,6 +3,7 @@ import { type Engines, type Leftovers } from '../../../app/features/engine/index
3
3
  import { type HealthLast } from '../../../app/features/health/index.js';
4
4
  import { type ContainedStatus, type StorageShow } from '../../../app/features/storage/index.js';
5
5
  import { type LogFiles } from '../../../app/features/logs/index.js';
6
+ import { type AuditList } from '../../../app/features/audit/index.js';
6
7
  import { type UpdatesStatus } from '../../../app/features/updates/updates.def.js';
7
8
  import { type WebStatus } from '../../../app/features/web/index.js';
8
9
  import type { Context } from '../../commands/context.js';
@@ -31,6 +32,8 @@ export interface MaintenanceModel {
31
32
  readonly engine: Engines | null;
32
33
  /** HCS systems left running, read with the engines. */
33
34
  readonly leftovers?: Leftovers | null;
35
+ /** The audit log today (the audit view's query; null: not read yet or unreadable). */
36
+ readonly audit?: AuditList | null;
34
37
  /** The web UI's server (the web feature's status): running or not, and its address. */
35
38
  readonly web: WebStatus;
36
39
  /** The commands listed under "More commands". */
@@ -52,6 +55,7 @@ export declare class MaintenancePage extends ListPage<string> {
52
55
  private engine;
53
56
  private leftovers;
54
57
  private web;
58
+ private audit;
55
59
  private readonly operations;
56
60
  /** The newer version the last check found (null: none, or not looked yet). */
57
61
  private latest;
@@ -1,6 +1,6 @@
1
1
  // Section 7: keeping things in shape. Updates (the program and your saved settings), Data & storage (the data folder,
2
- // what is stored, clean-up), Health (doctor, repair and this PC's setup together), the logs, and the less common
3
- // commands. Each is a category on the left and loads when you open it.
2
+ // what is stored, clean-up), Health (doctor, repair and this PC's setup together), Activity (the audit log), the logs,
3
+ // and the less common commands. Each is a category on the left and loads when you open it.
4
4
  import { dataPage, showData } from '../../../app/features/data/index.js';
5
5
  import { readQuery } from '../../../app/registry.js';
6
6
  import { errMsg } from '../../../core/util/index.js';
@@ -8,6 +8,7 @@ import { enginePage, engineLeftovers, engineStatus, } from '../../../app/feature
8
8
  import { healthPage, lastHealth } from '../../../app/features/health/index.js';
9
9
  import { containedStatus, showStorage, storagePage, } from '../../../app/features/storage/index.js';
10
10
  import { CRASH, listLogs, logsPage } from '../../../app/features/logs/index.js';
11
+ import { auditArgv, auditPage, listAudit } from '../../../app/features/audit/index.js';
11
12
  import { updatesPage, updatesStatus } from '../../../app/features/updates/updates.def.js';
12
13
  import { webPage, webStatus } from '../../../app/features/web/index.js';
13
14
  import { commandSpec } from '../../manifest.js';
@@ -32,6 +33,11 @@ const CATEGORIES = [
32
33
  label: 'Health',
33
34
  description: 'Checks this PC and your settings, repairs problems, and sets up Windows Sandbox.',
34
35
  },
36
+ {
37
+ id: 'activity',
38
+ label: 'Activity',
39
+ description: 'What your agents ran, changed and committed in the last 24 hours (the audit log). Enter on a session shows all of it.',
40
+ },
35
41
  { id: 'logs', label: 'Logs', description: "The sandbox's own logs." },
36
42
  {
37
43
  id: 'web',
@@ -45,14 +51,13 @@ const CATEGORIES = [
45
51
  },
46
52
  { id: 'more', label: 'More commands', description: 'Less common commands.' },
47
53
  ];
48
- const MORE_COMMANDS = ['sync', 'audit', 'config', 'generate', 'exec', 'agent-commands'];
54
+ const MORE_COMMANDS = ['sync', 'config', 'generate', 'exec', 'agent-commands'];
49
55
  const NO_LOGS = { folder: '', files: [], crash: false };
50
56
  /** Until the status is read: the server counts as not running. */
51
57
  const NO_WEB = { running: false, port: null, address: null };
52
58
  /** Plain names for the commands under More commands. */
53
59
  const MORE_LABELS = {
54
60
  sync: 'Copy history back now',
55
- audit: 'What happened in the sandbox',
56
61
  config: 'Your settings file',
57
62
  generate: 'Preview a start',
58
63
  exec: 'Run a command in the sandbox',
@@ -107,6 +112,10 @@ const NO_UPDATES = {
107
112
  function logsMenu(files) {
108
113
  return menuView({ ...logsPage(files), empty: undefined }, { category: 'logs', refresh: false });
109
114
  }
115
+ /** The audit view (src/app/features/audit) as the Activity category's rows and keys (none until it was read). */
116
+ function auditMenu(m) {
117
+ return m.audit ? menuView(auditPage(m.audit), { category: 'activity', prefix: 'audit:', refresh: false }) : undefined;
118
+ }
110
119
  /** The Web UI view (src/app/features/web) as the menu's rows, keys and Enter labels, for the Web UI category. */
111
120
  function webMenu(m) {
112
121
  return menuView(webPage(m.web), { category: 'web', prefix: 'web:', refresh: false });
@@ -120,6 +129,7 @@ export function maintenanceOptions(m) {
120
129
  const updates = updatesMenu(m);
121
130
  const engine = engineMenu(m);
122
131
  const web = webMenu(m);
132
+ const audit = auditMenu(m);
123
133
  const more = m.more.map((c) => ({
124
134
  value: 'more:' + c.name,
125
135
  label: MORE_LABELS[c.name] ?? Text.humanize(c.name),
@@ -129,7 +139,16 @@ export function maintenanceOptions(m) {
129
139
  return {
130
140
  path: [],
131
141
  categories: CATEGORIES,
132
- items: [...updates.items, ...dataItems(m), ...health.items, ...logs, ...web.items, ...engine.items, ...more],
142
+ items: [
143
+ ...updates.items,
144
+ ...dataItems(m),
145
+ ...health.items,
146
+ ...(audit?.items ?? []),
147
+ ...logs,
148
+ ...web.items,
149
+ ...engine.items,
150
+ ...more,
151
+ ],
133
152
  empty: 'Nothing here yet.',
134
153
  actions: maintenanceActions(m),
135
154
  enterLabel: (category, id) => category === 'health'
@@ -140,9 +159,11 @@ export function maintenanceOptions(m) {
140
159
  ? engine.enterLabel(category, id)
141
160
  : category === 'web'
142
161
  ? web.enterLabel(category, id)
143
- : (category === 'data'
144
- ? storageMenu(m.storage)?.enterLabel(category, id) || dataMenuView(m.data)?.enterLabel(category, id)
145
- : undefined) || enterLabel(id ?? ''),
162
+ : category === 'activity'
163
+ ? (audit?.enterLabel(category, id) ?? '')
164
+ : (category === 'data'
165
+ ? storageMenu(m.storage)?.enterLabel(category, id) || dataMenuView(m.data)?.enterLabel(category, id)
166
+ : undefined) || enterLabel(id ?? ''),
146
167
  };
147
168
  }
148
169
  /**
@@ -201,6 +222,8 @@ function maintenanceActions(m) {
201
222
  return [{ key: 'l', label: m.engine ? 'check again' : 'check' }, ...engineMenu(m).actions];
202
223
  case 'web':
203
224
  return webMenu(m).actions;
225
+ case 'activity':
226
+ return auditMenu(m)?.actions ?? [];
204
227
  default:
205
228
  return [];
206
229
  }
@@ -217,6 +240,7 @@ export class MaintenancePage extends ListPage {
217
240
  engine = null;
218
241
  leftovers = null;
219
242
  web = NO_WEB;
243
+ audit = null;
220
244
  operations;
221
245
  /** The newer version the last check found (null: none, or not looked yet). */
222
246
  latest = null;
@@ -235,6 +259,7 @@ export class MaintenancePage extends ListPage {
235
259
  engine: this.engine,
236
260
  leftovers: this.leftovers,
237
261
  web: this.web,
262
+ audit: this.audit,
238
263
  more: MORE_COMMANDS.flatMap((name) => {
239
264
  const spec = commandSpec(name);
240
265
  return spec ? [{ name, summary: spec.summary }] : [];
@@ -259,6 +284,13 @@ export class MaintenancePage extends ListPage {
259
284
  }
260
285
  this.last = (await readQuery(lastHealth, this.ctx)).last;
261
286
  this.updates = await readQuery(updatesStatus, this.ctx);
287
+ try {
288
+ this.audit = await readQuery(listAudit, this.ctx, { limit: 200 });
289
+ }
290
+ catch {
291
+ // An unreadable log: the category says there is nothing yet.
292
+ this.audit = null;
293
+ }
262
294
  try {
263
295
  this.web = await readQuery(webStatus, this.ctx);
264
296
  }
@@ -306,6 +338,17 @@ export class MaintenancePage extends ListPage {
306
338
  return await this.operations.perform(chosen.action, [this.label], (text) => this.busy(text));
307
339
  return {};
308
340
  }
341
+ // Activity: a session's row shows its events on the normal screen (sandbox audit --session ...); turning the
342
+ // log on runs the settings operation.
343
+ if (this.category === 'activity') {
344
+ const chosen = auditMenu(this.model())?.resolve(result);
345
+ if (chosen?.kind !== 'action')
346
+ return {};
347
+ const target = chosen.action.kind === 'link' ? chosen.action.target : undefined;
348
+ if (target?.kind === 'view')
349
+ return { exit: { kind: 'run', argv: auditArgv(target.params) } };
350
+ return await this.operations.perform(chosen.action, [this.label], (text) => this.busy(text));
351
+ }
309
352
  if (key === 'm' || id === 'data:measure')
310
353
  return await this.measure();
311
354
  // Only the open category's rows and keys: `f` and Enter mean something else in Health.
@@ -7,6 +7,7 @@ import { accountsView } from '../../app/features/accounts/index.js';
7
7
  import { appsView } from '../../app/features/apps/index.js';
8
8
  import { healthView } from '../../app/features/health/index.js';
9
9
  import { logsView } from '../../app/features/logs/index.js';
10
+ import { auditView } from '../../app/features/audit/index.js';
10
11
  import { projectsView } from '../../app/features/projects/index.js';
11
12
  import { sandboxView } from '../../app/features/sandbox/index.js';
12
13
  import { sessionsView } from '../../app/features/sessions/index.js';
@@ -29,6 +30,7 @@ export const MENU_VIEWS = [
29
30
  { view: dataView, section: 'Maintenance' },
30
31
  { view: storageView, section: 'Maintenance' },
31
32
  { view: logsView, section: 'Maintenance' },
33
+ { view: auditView, section: 'Maintenance' },
32
34
  { view: updatesView, section: 'Maintenance' },
33
35
  { view: engineView, section: 'Maintenance' },
34
36
  { view: webView, section: 'Maintenance' },
@@ -3,7 +3,7 @@ export const DEFAULTS = {
3
3
  guestWorkspace: 'auto',
4
4
  readOnly: false,
5
5
  workspaceFilter: { enabled: false, exclude: [], include: [] },
6
- audit: { enabled: false, days: 14, maxMb: 100, processes: true, files: true },
6
+ audit: { enabled: true, days: 7, maxMb: 100, processes: true, files: true, commits: true },
7
7
  mappings: [],
8
8
  sandbox: {
9
9
  engine: 'windows-sandbox',
@@ -2,6 +2,9 @@ import { z } from 'zod';
2
2
  export declare const AppIdSchema: z.ZodEnum<{
3
3
  [x: string]: string;
4
4
  }>;
5
+ export declare const ProjectAppIdSchema: z.ZodEnum<{
6
+ [x: string]: string;
7
+ }>;
5
8
  export declare const CustomAppIdSchema: z.ZodString;
6
9
  export declare const CustomAppSchema: z.ZodObject<{
7
10
  name: z.ZodString;
@@ -18,6 +21,12 @@ export declare const CustomAppSchema: z.ZodObject<{
18
21
  exe: z.ZodString;
19
22
  cli: z.ZodOptional<z.ZodBoolean>;
20
23
  }, z.core.$strict>;
24
+ /** What a project file may say about apps: catalogue apps it adds to yours (no add-ons that are yours only). */
25
+ export declare const ProjectAppsSchema: z.ZodObject<{
26
+ install: z.ZodArray<z.ZodEnum<{
27
+ [x: string]: string;
28
+ }>>;
29
+ }, z.core.$strict>;
21
30
  export declare const AppsSchema: z.ZodObject<{
22
31
  install: z.ZodArray<z.ZodEnum<{
23
32
  [x: string]: string;
@@ -3,6 +3,9 @@ import { z } from 'zod';
3
3
  import { APPS } from '../../shared/apps/index.js';
4
4
  const CATALOGUE_IDS = APPS.filter((a) => !a.setting).map((a) => a.id);
5
5
  export const AppIdSchema = z.enum(CATALOGUE_IDS);
6
+ /** The apps a project's .sandbox-config.json may add: not the add-ons that are yours only (they change the agents). */
7
+ const PROJECT_IDS = APPS.filter((a) => !a.setting && !a.userOnly).map((a) => a.id);
8
+ export const ProjectAppIdSchema = z.enum(PROJECT_IDS);
6
9
  export const CustomAppIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,40}$/, 'lower-case letters, digits and - only');
7
10
  const compiles = (pattern) => {
8
11
  try {
@@ -51,6 +54,12 @@ export const CustomAppSchema = z
51
54
  .refine((a) => (a.github && a.asset ? !a.url : Boolean(a.url) && !a.github && !a.asset), {
52
55
  message: 'give github + asset, or url (not both)',
53
56
  });
57
+ /** What a project file may say about apps: catalogue apps it adds to yours (no add-ons that are yours only). */
58
+ export const ProjectAppsSchema = z.strictObject({
59
+ install: z
60
+ .array(ProjectAppIdSchema)
61
+ .describe('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).'),
62
+ });
54
63
  export const AppsSchema = z.strictObject({
55
64
  install: z
56
65
  .array(AppIdSchema)
@@ -15,6 +15,7 @@ export declare const ConfigSchema: z.ZodObject<{
15
15
  maxMb: z.ZodNumber;
16
16
  processes: z.ZodBoolean;
17
17
  files: z.ZodBoolean;
18
+ commits: z.ZodBoolean;
18
19
  }, z.core.$strict>;
19
20
  mappings: z.ZodArray<z.ZodObject<{
20
21
  host: z.ZodString;
@@ -8,7 +8,7 @@ import { HarnessSchema } from './harness.js';
8
8
  import { AgentCommandsSchema, AttachSchema, ClaudeProfileSchema, SecretsSchema } from './profile.js';
9
9
  import { AuditSchema, DownloadsSchema, SharingSchema, GitSchema, GuestWorkspaceSchema, MappingSchema, NetworkSchema, ProvisioningSchema, SandboxSchema, WorkspaceFilterSchema, } from './sandbox.js';
10
10
  import { ToolchainsSchema } from './toolchains.js';
11
- import { AppsSchema } from './apps.js';
11
+ import { AppsSchema, ProjectAppsSchema } from './apps.js';
12
12
  import { ProjectsSchema } from './projects.js';
13
13
  import { ResumeSchema } from './resume.js';
14
14
  import { WebSchema } from './web.js';
@@ -115,8 +115,8 @@ export const LocalOverridesSchema = z
115
115
  attach: AttachSchema.partial(),
116
116
  personalization: toggle('Copy your desktop look into the sandbox.').partial(),
117
117
  // Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add
118
- // them.
119
- apps: AppsSchema.pick({ install: true }).partial(),
118
+ // them, nor agent add-ons that are yours only (rtk hooks every agent command, Headroom sits in its API path).
119
+ apps: ProjectAppsSchema.partial(),
120
120
  claudeProfile: ClaudeProfileSchema.pick({
121
121
  enabled: true,
122
122
  overwrite: true,
@@ -49,6 +49,7 @@ export declare const AuditSchema: z.ZodObject<{
49
49
  maxMb: z.ZodNumber;
50
50
  processes: z.ZodBoolean;
51
51
  files: z.ZodBoolean;
52
+ commits: z.ZodBoolean;
52
53
  }, z.core.$strict>;
53
54
  export declare const WorkspaceFilterSchema: z.ZodObject<{
54
55
  enabled: z.ZodBoolean;
@@ -108,8 +108,13 @@ export const NetworkSchema = z.strictObject({
108
108
  export const AuditSchema = z.strictObject({
109
109
  enabled: z
110
110
  .boolean()
111
- .describe('Keep an audit log on this PC of what happened in your sandboxes: programs started (with their command lines), files changed in the project, sessions, and what the repo guard caught. The sandbox cannot change it. Read it with sandbox audit.'),
112
- days: z.number().int().min(1).max(365).describe('Days the audit log is kept.'),
111
+ .describe('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.'),
112
+ days: z
113
+ .number()
114
+ .int()
115
+ .min(1)
116
+ .max(365)
117
+ .describe('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.'),
113
118
  maxMb: z
114
119
  .number()
115
120
  .int()
@@ -119,7 +124,10 @@ export const AuditSchema = z.strictObject({
119
124
  processes: z
120
125
  .boolean()
121
126
  .describe('Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed).'),
122
- files: z.boolean().describe('Record the files changed in the project while the sandbox runs.'),
127
+ files: z.boolean().describe('Record the files changed in the projects while the sandbox runs.'),
128
+ commits: z
129
+ .boolean()
130
+ .describe("Record the git commits made in the projects while the sandbox runs (read from each project's .git folder on this PC)."),
123
131
  });
124
132
  export const WorkspaceFilterSchema = z.strictObject({
125
133
  enabled: z
@@ -5,12 +5,13 @@ import path from 'node:path';
5
5
  import { appsToInstall } from '../../shared/apps/index.js';
6
6
  import { downloader, moveIntoPlace, stagingFolder } from '../../shared/download/index.js';
7
7
  import { safeRemove } from '../../shared/io/safe-remove.js';
8
- import { dryRun } from '../runtime/paths.js';
8
+ import { dryRun, launchDir } from '../runtime/paths.js';
9
9
  import { execAsync } from '../runtime/process.js';
10
10
  import { exists, mkdirp } from '../runtime/fs.js';
11
11
  import { log, warn } from '../runtime/log.js';
12
12
  import { errMsg } from '../../core/util/errors.js';
13
13
  import { tool } from '../toolchains/tool-env.js';
14
+ import { uvVars } from '../toolchains/runtimes.js';
14
15
  import { afterInstall } from './after-install.js';
15
16
  import { findExe } from './find-exe.js';
16
17
  import { shortcuts } from './shortcuts.js';
@@ -58,9 +59,10 @@ export class AppInstaller {
58
59
  if (!exe)
59
60
  throw new Error(entry.exe + ' not found in the download');
60
61
  await afterInstall(entry, exe);
61
- if (entry.cli || entry.id === 'sysinternals')
62
+ // A proxy or MCP server (agent add-ons) is started by the sandbox or the agent: no PATH entry, no shortcut.
63
+ if (!entry.service && (entry.cli || entry.id === 'sysinternals'))
62
64
  this.env.prepend.push(path.dirname(exe));
63
- if (!entry.cli)
65
+ if (!entry.cli && !entry.service)
64
66
  this.gui.push({ name: entry.name, exe });
65
67
  this.installed.push(entry.name);
66
68
  }
@@ -74,7 +76,11 @@ export class AppInstaller {
74
76
  const work = stagingFolder(dest);
75
77
  const out = path.join(work, 'out');
76
78
  try {
77
- if ('npm' in entry.source) {
79
+ if ('locked' in entry.source) {
80
+ log('Installing ' + entry.name + ' ' + entry.source.version + ' (pinned, every file checked) ...');
81
+ await installLocked(entry.id, entry.source.locked, mkdirp(out));
82
+ }
83
+ else if ('npm' in entry.source) {
78
84
  const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
79
85
  log('Installing ' + entry.name + ' (npm) ...');
80
86
  await execAsync(process.execPath, [
@@ -103,6 +109,46 @@ export class AppInstaller {
103
109
  }
104
110
  }
105
111
  }
112
+ /** The Python the uv lock files were made for (sandbox\addons\<id>\requirements.txt). */
113
+ const LOCKED_PYTHON = '3.13';
114
+ /**
115
+ * Installs a pinned package from its lock file in the launch folder (sandbox\addons\<id>) into `out`. npm: `npm ci`
116
+ * checks every tarball against the lock file's integrity, and runs no install scripts. uv: a relocatable venv (the
117
+ * folder moves into place afterwards) and `--require-hashes`, so every wheel must match its SHA-256 in the file; no
118
+ * source builds.
119
+ */
120
+ async function installLocked(id, kind, out) {
121
+ const lock = path.join(launchDir, 'sandbox', 'addons', id);
122
+ if (kind === 'npm') {
123
+ for (const f of ['package.json', 'package-lock.json'])
124
+ fs.copyFileSync(path.join(lock, f), path.join(out, f));
125
+ const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
126
+ await execAsync(process.execPath, [npmCli, 'ci', '--ignore-scripts', '--no-audit', '--no-fund', '--loglevel=error'], { cwd: out });
127
+ return;
128
+ }
129
+ const uv = findExe(tool('uv'), 'uv.exe');
130
+ if (!uv)
131
+ throw new Error('uv is not installed (the uv step failed)');
132
+ const env = { ...process.env, ...uvVars() };
133
+ const venv = path.join(out, 'venv');
134
+ await execAsync(uv, ['venv', '--relocatable', '--python', LOCKED_PYTHON, '--python-preference', 'only-managed', venv], {
135
+ env,
136
+ });
137
+ await execAsync(uv, [
138
+ 'pip',
139
+ 'install',
140
+ '--python',
141
+ path.join(venv, 'Scripts', 'python.exe'),
142
+ '--require-hashes',
143
+ '--no-deps',
144
+ '--only-binary',
145
+ ':all:',
146
+ '--link-mode',
147
+ 'copy',
148
+ '-r',
149
+ path.join(lock, 'requirements.txt'),
150
+ ], { env });
151
+ }
106
152
  /** Installs apps.install and apps.custom (skipped ones are listed in the summary). */
107
153
  export async function setupApps(env, apps, o) {
108
154
  const { entries, skipped } = appsToInstall(apps, o.gitAccess);
@@ -1,3 +1,3 @@
1
- export { appsNeedMesa } from '../../shared/apps/index.js';
1
+ export { appsNeedMesa, appsNeedUv } from '../../shared/apps/index.js';
2
2
  export { setupApps, type AppsOptions } from './app-installer.js';
3
3
  export { findExe } from './find-exe.js';
@@ -1,6 +1,6 @@
1
1
  // Installs the apps you chose (apps.install from the catalogue, apps.custom of your own) into C:\AgentData\tools\apps.
2
2
  // Installers are never run: archives are unpacked, NSIS installers opened with 7-Zip and MSIs extracted (msiexec /a),
3
3
  // so nothing is registered and nothing is lost when the sandbox closes.
4
- export { appsNeedMesa } from '../../shared/apps/index.js';
4
+ export { appsNeedMesa, appsNeedUv } from '../../shared/apps/index.js';
5
5
  export { setupApps } from './app-installer.js';
6
6
  export { findExe } from './find-exe.js';
@@ -9,7 +9,7 @@ export async function resolve(entry) {
9
9
  }
10
10
  if ('url' in s) {
11
11
  const name = decodeURIComponent(new URL(s.url).pathname.split('/').pop() || entry.id);
12
- return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + '.zip' };
12
+ return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + '.zip', sha256: s.sha256 };
13
13
  }
14
14
  if ('resolve' in s && RESOLVERS[s.resolve])
15
15
  return RESOLVERS[s.resolve]();
@@ -1,7 +1,37 @@
1
1
  /** Background programs of Windows itself: never what an agent ran. */
2
2
  export declare const NOISE: Set<string>;
3
- /** The events in wevtutil's XML output as audit lines, with the newest record id seen. */
4
- export declare function parseProcessEvents(xml: string): {
3
+ /** A session's ptyhost: everything it starts (and what those start) is that session's. */
4
+ export interface SessionRoot {
5
+ readonly session: string;
6
+ readonly pid: number;
7
+ /** When the ptyhost started and ended (ms); a reused process id outside this window is not the session's. */
8
+ readonly from: number;
9
+ readonly to?: number;
10
+ }
11
+ /**
12
+ * Follows the process tree through the events themselves: a program belongs to a session when its parent is that
13
+ * session's ptyhost, or a program that belongs to it. Every new process id is recorded (as no session's when its
14
+ * parent is not one's), so an id Windows gives to another process later never inherits a session. A program whose
15
+ * parent was never seen is no session's: nothing is guessed.
16
+ */
17
+ export declare class Attributor {
18
+ private readonly owner;
19
+ /** The session of a new process (pid, started by ppid at time t), or undefined. */
20
+ attribute(e: {
21
+ readonly pid: number;
22
+ readonly ppid: number;
23
+ readonly t: number;
24
+ }, roots: readonly SessionRoot[]): string | undefined;
25
+ }
26
+ /**
27
+ * The events in wevtutil's XML output as audit lines, with the newest record id seen. With an attributor and the
28
+ * sessions' roots, each line says which session started it (s), when that is known. Every event goes through the
29
+ * attributor, Windows' own background programs too (they are left out of the lines only afterwards).
30
+ */
31
+ export declare function parseProcessEvents(xml: string, attribution?: {
32
+ readonly attributor: Attributor;
33
+ readonly roots: readonly SessionRoot[];
34
+ }): {
5
35
  lines: string[];
6
36
  last: number;
7
37
  };
@@ -1,5 +1,5 @@
1
- // Reading Windows process-creation events (Security log, 4688, as wevtutil prints them) into audit lines. Pure, so
2
- // the self-test can check it on the PC side.
1
+ // Reading Windows process-creation events (Security log, 4688, as wevtutil prints them) into audit lines, and which
2
+ // agent session each program belongs to. Pure, so the self-test can check it on the PC side.
3
3
  import path from 'node:path';
4
4
  /** Background programs of Windows itself: never what an agent ran. */
5
5
  export const NOISE = new Set([
@@ -28,6 +28,29 @@ export const NOISE = new Set([
28
28
  'usoclient.exe',
29
29
  'musnotification.exe',
30
30
  ]);
31
+ /** Windows' clocks and ours differ by a moment: a root counts this long around its window. */
32
+ const ROOT_SLACK_MS = 5000;
33
+ /** Most process ids remembered (the oldest are forgotten first). */
34
+ const MAX_KNOWN = 50000;
35
+ /**
36
+ * Follows the process tree through the events themselves: a program belongs to a session when its parent is that
37
+ * session's ptyhost, or a program that belongs to it. Every new process id is recorded (as no session's when its
38
+ * parent is not one's), so an id Windows gives to another process later never inherits a session. A program whose
39
+ * parent was never seen is no session's: nothing is guessed.
40
+ */
41
+ export class Attributor {
42
+ owner = new Map();
43
+ /** The session of a new process (pid, started by ppid at time t), or undefined. */
44
+ attribute(e, roots) {
45
+ const root = roots.find((r) => r.pid === e.ppid && e.t >= r.from - ROOT_SLACK_MS && (r.to === undefined || e.t <= r.to + ROOT_SLACK_MS));
46
+ const session = root?.session ?? this.owner.get(e.ppid) ?? undefined;
47
+ this.owner.delete(e.pid);
48
+ this.owner.set(e.pid, session ?? null);
49
+ if (this.owner.size > MAX_KNOWN)
50
+ this.owner.delete(this.owner.keys().next().value);
51
+ return session;
52
+ }
53
+ }
31
54
  const decode = (s) => s
32
55
  .replace(/&lt;/g, '<')
33
56
  .replace(/&gt;/g, '>')
@@ -35,8 +58,17 @@ const decode = (s) => s
35
58
  .replace(/&apos;/g, "'")
36
59
  .replace(/&amp;/g, '&');
37
60
  const field = (event, name) => decode(new RegExp("<Data Name='" + name + "'>([^<]*)</Data>").exec(event)?.[1] ?? '');
38
- /** The events in wevtutil's XML output as audit lines, with the newest record id seen. */
39
- export function parseProcessEvents(xml) {
61
+ /** A process id as the event writes it (0x1a2c). */
62
+ const pidOf = (text) => {
63
+ const n = /^0x[0-9a-f]{1,8}$/i.test(text) ? parseInt(text, 16) : /^\d{1,10}$/.test(text) ? Number(text) : 0;
64
+ return Number.isSafeInteger(n) ? n : 0;
65
+ };
66
+ /**
67
+ * The events in wevtutil's XML output as audit lines, with the newest record id seen. With an attributor and the
68
+ * sessions' roots, each line says which session started it (s), when that is known. Every event goes through the
69
+ * attributor, Windows' own background programs too (they are left out of the lines only afterwards).
70
+ */
71
+ export function parseProcessEvents(xml, attribution) {
40
72
  const lines = [];
41
73
  let last = 0;
42
74
  for (const event of xml.split('</Event>')) {
@@ -45,15 +77,21 @@ export function parseProcessEvents(xml) {
45
77
  continue;
46
78
  last = Math.max(last, id);
47
79
  const program = field(event, 'NewProcessName');
80
+ const t = /SystemTime='([^']+)'/.exec(event)?.[1] ?? new Date().toISOString();
81
+ const pid = pidOf(field(event, 'NewProcessId'));
82
+ const ppid = pidOf(field(event, 'ProcessId'));
83
+ const session = attribution && pid && ppid
84
+ ? attribution.attributor.attribute({ pid, ppid, t: Date.parse(t) || Date.now() }, attribution.roots)
85
+ : undefined;
48
86
  if (!program || NOISE.has(path.win32.basename(program).toLowerCase()))
49
87
  continue;
50
- const t = /SystemTime='([^']+)'/.exec(event)?.[1] ?? new Date().toISOString();
51
88
  lines.push(JSON.stringify({
52
89
  t,
53
90
  k: 'proc',
54
91
  p: program.slice(0, 1024),
55
92
  c: field(event, 'CommandLine').slice(0, 2000),
56
93
  pp: field(event, 'ParentProcessName').slice(0, 1024),
94
+ ...(session ? { s: session } : {}),
57
95
  }));
58
96
  }
59
97
  return { lines, last };
@@ -1,6 +1,8 @@
1
+ import { type SessionRoot } from './parse.js';
1
2
  /** Turns on process-creation auditing with command lines (the registry is new every boot). */
2
3
  export declare function enableProcessAudit(): void;
3
4
  /**
4
5
  * Reads new process events every 15 s for the daemon's life (from the moment it started: older ones are boot noise).
6
+ * `roots` gives the sessions' ptyhosts, so each program is linked to the session that started it (parse.ts).
5
7
  */
6
- export declare function startProcessCollector(): void;
8
+ export declare function startProcessCollector(roots?: () => readonly SessionRoot[]): void;