windows-development-sandbox 1.11.7 → 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.
Files changed (246) hide show
  1. package/README.md +13 -6
  2. package/config.example.json +15 -4
  3. package/dist/app/features/access/access.def.d.ts +190 -0
  4. package/dist/app/features/access/access.def.js +273 -0
  5. package/dist/app/features/access/access.run.d.ts +79 -0
  6. package/dist/app/features/access/access.run.js +129 -0
  7. package/dist/app/features/access/index.d.ts +2 -0
  8. package/dist/app/features/access/index.js +3 -0
  9. package/dist/app/features/agents/agent-entry.js +5 -0
  10. package/dist/app/features/agents/agents.def.js +1 -0
  11. package/dist/app/features/apps/apps-rows.d.ts +4 -1
  12. package/dist/app/features/apps/apps-rows.js +7 -2
  13. package/dist/app/features/apps/apps.def.d.ts +19 -0
  14. package/dist/app/features/apps/apps.def.js +50 -12
  15. package/dist/app/features/apps/apps.run.d.ts +2 -0
  16. package/dist/app/features/apps/apps.run.js +1 -1
  17. package/dist/app/features/audit/audit.def.d.ts +350 -0
  18. package/dist/app/features/audit/audit.def.js +457 -0
  19. package/dist/app/features/audit/audit.run.d.ts +110 -0
  20. package/dist/app/features/audit/audit.run.js +133 -0
  21. package/dist/app/features/audit/index.d.ts +2 -0
  22. package/dist/app/features/audit/index.js +3 -0
  23. package/dist/app/features/defs.d.ts +349 -4
  24. package/dist/app/features/defs.js +16 -1
  25. package/dist/app/features/files/files.def.js +1 -0
  26. package/dist/app/features/health/health.def.js +1 -0
  27. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  28. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  29. package/dist/app/features/sessions/sessions.def.js +77 -9
  30. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  31. package/dist/app/features/sessions/sessions.run.js +71 -3
  32. package/dist/app/features/settings/settings.def.d.ts +8 -0
  33. package/dist/app/features/settings/settings.def.js +17 -0
  34. package/dist/app/features/settings/settings.run.d.ts +3 -0
  35. package/dist/app/features/settings/settings.run.js +16 -2
  36. package/dist/app/features/web/web.def.d.ts +9 -0
  37. package/dist/app/features/web/web.def.js +26 -1
  38. package/dist/app/features/web/web.run.d.ts +3 -0
  39. package/dist/app/features/web/web.run.js +11 -3
  40. package/dist/app/kernel/access.d.ts +33 -0
  41. package/dist/app/kernel/access.js +44 -0
  42. package/dist/app/kernel/define.d.ts +26 -2
  43. package/dist/app/kernel/index.d.ts +1 -0
  44. package/dist/app/kernel/index.js +1 -0
  45. package/dist/app/kernel/rpc.d.ts +5 -2
  46. package/dist/app/registry.d.ts +4 -1
  47. package/dist/app/registry.js +26 -4
  48. package/dist/cli/args.d.ts +0 -3
  49. package/dist/cli/args.js +3 -4
  50. package/dist/cli/commands/index.d.ts +1 -1
  51. package/dist/cli/commands/index.js +1 -1
  52. package/dist/cli/commands/launch/prepare.js +1 -1
  53. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  54. package/dist/cli/commands/maintenance/index.js +0 -1
  55. package/dist/cli/commands/registry.js +3 -2
  56. package/dist/cli/commands/remote.d.ts +2 -0
  57. package/dist/cli/commands/remote.js +49 -0
  58. package/dist/cli/commands/sessions/reopen.js +3 -0
  59. package/dist/cli/commands/web.js +66 -0
  60. package/dist/cli/manifest.d.ts +63 -45
  61. package/dist/cli/manifest.js +33 -18
  62. package/dist/cli/menu/io-menu.d.ts +5 -3
  63. package/dist/cli/menu/io-menu.js +12 -5
  64. package/dist/cli/menu/pages/apps-page.js +2 -15
  65. package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
  66. package/dist/cli/menu/pages/maintenance-page.js +51 -8
  67. package/dist/cli/menu/remote-menu.d.ts +19 -0
  68. package/dist/cli/menu/remote-menu.js +108 -0
  69. package/dist/cli/menu/views.js +4 -0
  70. package/dist/cli/operations/handler.js +15 -1
  71. package/dist/cli/services/remote-client.d.ts +16 -0
  72. package/dist/cli/services/remote-client.js +141 -0
  73. package/dist/cli/services/sessions-service.d.ts +5 -0
  74. package/dist/cli/services/sessions-service.js +17 -1
  75. package/dist/core/config/defaults.js +12 -2
  76. package/dist/core/local-config/file.js +16 -2
  77. package/dist/core/local-config/overrides.js +7 -0
  78. package/dist/core/run/versions.js +9 -8
  79. package/dist/core/schema/apps.d.ts +9 -0
  80. package/dist/core/schema/apps.js +9 -0
  81. package/dist/core/schema/config.d.ts +23 -3
  82. package/dist/core/schema/config.js +42 -5
  83. package/dist/core/schema/sandbox.d.ts +1 -0
  84. package/dist/core/schema/sandbox.js +11 -3
  85. package/dist/core/schema/web.d.ts +51 -0
  86. package/dist/core/schema/web.js +46 -1
  87. package/dist/core/util/fs.d.ts +5 -0
  88. package/dist/core/util/fs.js +23 -0
  89. package/dist/core/util/index.d.ts +1 -1
  90. package/dist/core/util/index.js +1 -1
  91. package/dist/guest/apps/app-installer.js +50 -4
  92. package/dist/guest/apps/index.d.ts +1 -1
  93. package/dist/guest/apps/index.js +1 -1
  94. package/dist/guest/apps/sources.js +1 -1
  95. package/dist/guest/audit/parse.d.ts +32 -2
  96. package/dist/guest/audit/parse.js +43 -5
  97. package/dist/guest/audit/process-audit.d.ts +3 -1
  98. package/dist/guest/audit/process-audit.js +9 -6
  99. package/dist/guest/boot/boot.js +12 -3
  100. package/dist/guest/boot/guide.d.ts +1 -1
  101. package/dist/guest/boot/guide.js +27 -3
  102. package/dist/guest/daemon/daemon.js +2 -0
  103. package/dist/guest/daemon/idle-shutdown.js +2 -1
  104. package/dist/guest/daemon/sessions.d.ts +23 -2
  105. package/dist/guest/daemon/sessions.js +151 -20
  106. package/dist/guest/entries/daemon.js +2 -1
  107. package/dist/guest/git/decision.js +7 -0
  108. package/dist/guest/sessions/cmdline.d.ts +14 -0
  109. package/dist/guest/sessions/cmdline.js +10 -0
  110. package/dist/guest/setup/addon-mcp.d.ts +2 -0
  111. package/dist/guest/setup/addon-mcp.js +49 -0
  112. package/dist/guest/setup/agent-addons.d.ts +9 -0
  113. package/dist/guest/setup/agent-addons.js +22 -0
  114. package/dist/guest/setup/headroom.d.ts +7 -0
  115. package/dist/guest/setup/headroom.js +60 -0
  116. package/dist/guest/setup/managed-policy.d.ts +2 -1
  117. package/dist/guest/setup/managed-policy.js +15 -6
  118. package/dist/guest/toolchains/plan.js +8 -3
  119. package/dist/guest/toolchains/setup.js +3 -0
  120. package/dist/host/audit/attribute.d.ts +46 -0
  121. package/dist/host/audit/attribute.js +76 -0
  122. package/dist/host/audit/collect.d.ts +29 -3
  123. package/dist/host/audit/collect.js +256 -61
  124. package/dist/host/audit/git-commits.d.ts +29 -0
  125. package/dist/host/audit/git-commits.js +157 -0
  126. package/dist/host/audit/index.d.ts +5 -0
  127. package/dist/host/audit/index.js +6 -0
  128. package/dist/host/audit/log.d.ts +45 -7
  129. package/dist/host/audit/log.js +148 -45
  130. package/dist/host/audit/redact.d.ts +2 -0
  131. package/dist/host/audit/redact.js +67 -0
  132. package/dist/host/channel/channel.d.ts +5 -1
  133. package/dist/host/channel/channel.js +12 -0
  134. package/dist/host/guard/agent-files.js +7 -1
  135. package/dist/host/guard/commands.js +13 -1
  136. package/dist/host/guard/git-config.js +27 -7
  137. package/dist/host/guard/git-dirs.js +78 -3
  138. package/dist/host/guard/mark-of-the-web.js +3 -1
  139. package/dist/host/guard/nested-repos.js +7 -2
  140. package/dist/host/guard/store.js +1 -0
  141. package/dist/host/guard/types.d.ts +2 -0
  142. package/dist/host/launch/mappings.js +11 -1
  143. package/dist/host/launch/project-scan.js +2 -21
  144. package/dist/host/launch/stage.js +4 -0
  145. package/dist/host/life/session-ledger.js +2 -1
  146. package/dist/host/remote/access-store.d.ts +156 -0
  147. package/dist/host/remote/access-store.js +308 -0
  148. package/dist/host/remote/cert.d.ts +20 -0
  149. package/dist/host/remote/cert.js +103 -0
  150. package/dist/host/remote/door-client.d.ts +13 -0
  151. package/dist/host/remote/door-client.js +54 -0
  152. package/dist/host/remote/firewall.d.ts +44 -0
  153. package/dist/host/remote/firewall.js +118 -0
  154. package/dist/host/remote/gate.d.ts +125 -0
  155. package/dist/host/remote/gate.js +391 -0
  156. package/dist/host/remote/index.d.ts +8 -0
  157. package/dist/host/remote/index.js +10 -0
  158. package/dist/host/remote/lan.d.ts +24 -0
  159. package/dist/host/remote/lan.js +88 -0
  160. package/dist/host/remote/tls-store.d.ts +22 -0
  161. package/dist/host/remote/tls-store.js +63 -0
  162. package/dist/host/remote/windows-helper.d.ts +67 -0
  163. package/dist/host/remote/windows-helper.js +129 -0
  164. package/dist/host/web/bridge.d.ts +5 -1
  165. package/dist/host/web/bridge.js +6 -2
  166. package/dist/host/web/browser.js +1 -1
  167. package/dist/host/web/control.d.ts +4 -1
  168. package/dist/host/web/control.js +28 -4
  169. package/dist/host/web/rpc.d.ts +16 -2
  170. package/dist/host/web/rpc.js +37 -4
  171. package/dist/host/web/runner.d.ts +4 -0
  172. package/dist/host/web/runner.js +12 -0
  173. package/dist/host/web/server.d.ts +18 -2
  174. package/dist/host/web/server.js +97 -24
  175. package/dist/host/web/state.d.ts +5 -0
  176. package/dist/host/web/state.js +8 -0
  177. package/dist/runtime/agent.js +184 -18
  178. package/dist/runtime/daemon.js +772 -338
  179. package/dist/runtime/db.js +184 -18
  180. package/dist/runtime/gitguard.js +6 -0
  181. package/dist/runtime/main.js +966 -553
  182. package/dist/runtime/netproxy.js +184 -18
  183. package/dist/runtime/view.js +186 -18
  184. package/dist/selftest/app.test.js +66 -7
  185. package/dist/selftest/audit.test.js +354 -47
  186. package/dist/selftest/channel.test.js +16 -0
  187. package/dist/selftest/cmdline.test.js +16 -0
  188. package/dist/selftest/git-access.test.js +8 -0
  189. package/dist/selftest/hcs.test.js +35 -0
  190. package/dist/selftest/hostile-input.test.js +12 -1
  191. package/dist/selftest/index.js +2 -0
  192. package/dist/selftest/live-daemon.test.js +25 -0
  193. package/dist/selftest/local-config.test.js +30 -0
  194. package/dist/selftest/menu-screens.js +1 -1
  195. package/dist/selftest/menu.test.js +3 -3
  196. package/dist/selftest/remote.test.d.ts +2 -0
  197. package/dist/selftest/remote.test.js +536 -0
  198. package/dist/selftest/repo-guard.test.js +56 -2
  199. package/dist/selftest/resume.test.js +2 -1
  200. package/dist/selftest/sharing.test.js +9 -2
  201. package/dist/selftest/support/engine-golden.js +9 -0
  202. package/dist/selftest/support/fixtures.d.ts +14 -0
  203. package/dist/selftest/support/web-preview.js +157 -0
  204. package/dist/selftest/toolchains.test.js +147 -2
  205. package/dist/selftest/web.test.js +2 -0
  206. package/dist/shared/apps/agent-addons.d.ts +64 -0
  207. package/dist/shared/apps/agent-addons.js +95 -0
  208. package/dist/shared/apps/catalogue.d.ts +33 -2
  209. package/dist/shared/apps/catalogue.js +102 -11
  210. package/dist/shared/apps/index.d.ts +3 -2
  211. package/dist/shared/apps/index.js +3 -2
  212. package/dist/shared/apps/selection.d.ts +2 -0
  213. package/dist/shared/apps/selection.js +7 -0
  214. package/dist/shared/channel/schema.d.ts +23 -0
  215. package/dist/shared/channel/schema.js +8 -0
  216. package/dist/shared/pty/protocol.d.ts +2 -0
  217. package/dist/shared/pty/protocol.js +2 -0
  218. package/dist/sync/watcher.js +9 -1
  219. package/dist/web/index.html +2 -2
  220. package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
  221. package/dist/web/main-Z2WYAE2Z.js +221 -0
  222. package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
  223. package/native/csharp/HcsHost/Hcs.cs +9 -0
  224. package/native/csharp/HcsHost/Program.cs +26 -1
  225. package/native/csharp/HcsHost/Share.cs +173 -0
  226. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  227. package/native/csharp/HostTools/Program.cs +17 -1
  228. package/native/csharp/HostTools/WebHelper.cs +499 -0
  229. package/native/scripts/get-network-profile.ps1 +19 -0
  230. package/native/scripts/set-folder-access.ps1 +11 -0
  231. package/native/scripts/set-web-firewall.ps1 +103 -0
  232. package/package.json +1 -1
  233. package/sandbox/addons/ccusage/package-lock.json +111 -0
  234. package/sandbox/addons/ccusage/package.json +7 -0
  235. package/sandbox/addons/context7/package-lock.json +1313 -0
  236. package/sandbox/addons/context7/package.json +7 -0
  237. package/sandbox/addons/headroom/requirements.in +1 -0
  238. package/sandbox/addons/headroom/requirements.txt +2922 -0
  239. package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
  240. package/sandbox/addons/playwright-mcp/package.json +7 -0
  241. package/sandbox/csharp/PtyHost.cs +20 -0
  242. package/schemas/config.schema.json +78 -4
  243. package/schemas/sandbox-config.schema.json +2 -20
  244. package/dist/cli/commands/maintenance/audit.d.ts +0 -3
  245. package/dist/cli/commands/maintenance/audit.js +0 -58
  246. package/dist/web/main-K2JMCAZI.js +0 -219
@@ -0,0 +1,46 @@
1
+ /** A session as the audit remembers it (its run record goes with the sandbox). */
2
+ export interface KnownSession {
3
+ readonly id: string;
4
+ readonly agent: string;
5
+ /** The project it runs in (hub name). */
6
+ readonly project: string;
7
+ /** When it started and ended (ms). */
8
+ readonly from: number;
9
+ readonly to?: number;
10
+ }
11
+ /** A git command a session ran that can make a commit (from the programs the sandbox reported). */
12
+ export interface GitRun {
13
+ readonly t: number;
14
+ readonly session: string;
15
+ }
16
+ /** A link to a session: its id and how it was made (p: process tree / git command; t: only session running). */
17
+ export interface Link {
18
+ readonly s: string;
19
+ readonly sv: 'p' | 't';
20
+ }
21
+ /** git commit, merge, cherry-pick, revert, rebase, am, pull: the commands that can add a commit. */
22
+ export declare function makesCommits(program: string, commandLine: string): boolean;
23
+ /** The sessions of this sandbox seen so far, with when each ran. */
24
+ export declare class SessionBook {
25
+ private readonly sessions;
26
+ /** Takes the sessions as they are now: new ones are added, ended ones get their end time. */
27
+ update(list: readonly {
28
+ readonly id: string;
29
+ readonly harness: string;
30
+ readonly project?: string;
31
+ readonly phase: string;
32
+ readonly startedAt: string;
33
+ readonly updatedAt: string;
34
+ }[], fallbackProject: string, now?: number): void;
35
+ get(id: string | undefined): KnownSession | undefined;
36
+ /** The one session running in this project at time t, or undefined when none or several were. */
37
+ onlyRunning(project: string, t: number): KnownSession | undefined;
38
+ /** A file change in a project at time t: linked only when one session ran there then. */
39
+ linkFile(project: string, t: number): Link | undefined;
40
+ /**
41
+ * A commit in a project at time t: the session whose git command made it when that project's sessions ran git
42
+ * commands that make commits around then and all were the same session's; else, when `likely` is allowed (programs
43
+ * are not recorded, so no git command could be seen), the one session running there.
44
+ */
45
+ linkCommit(project: string, t: number, gitRuns: readonly GitRun[], likely?: boolean): Link | undefined;
46
+ }
@@ -0,0 +1,76 @@
1
+ // Which agent session an event belongs to, on the PC side. Programs come linked from the sandbox (by the process tree,
2
+ // guest/audit/parse.ts); the host only keeps a link to a session it knows. Files and commits are seen from this PC,
3
+ // where the writer cannot be told: they are linked only when exactly one session was running in that project at the
4
+ // time ("likely", marked as such), or, for a commit, when that project's sessions ran exactly one git command that
5
+ // makes commits around then. Anything else stays "no session": nothing is guessed. Pure, so the self-test checks it.
6
+ import path from 'node:path';
7
+ /** How long before and after a commit a git command counts as the one that made it. */
8
+ const GIT_BEFORE_MS = 3 * 60 * 1000;
9
+ const GIT_AFTER_MS = 30 * 1000;
10
+ /** Sessions remembered at most. */
11
+ const MAX_SESSIONS = 512;
12
+ /** git commit, merge, cherry-pick, revert, rebase, am, pull: the commands that can add a commit. */
13
+ export function makesCommits(program, commandLine) {
14
+ return (/^git(\.exe)?$/i.test(path.win32.basename(program)) &&
15
+ /\s(commit|merge|cherry-pick|revert|rebase|am|pull)(\s|$)/i.test(commandLine));
16
+ }
17
+ /** The sessions of this sandbox seen so far, with when each ran. */
18
+ export class SessionBook {
19
+ sessions = new Map();
20
+ /** Takes the sessions as they are now: new ones are added, ended ones get their end time. */
21
+ update(list, fallbackProject, now = Date.now()) {
22
+ for (const s of list) {
23
+ const known = this.sessions.get(s.id);
24
+ const ended = s.phase === 'exited' || s.phase === 'failed';
25
+ if (known) {
26
+ if (ended && known.to === undefined)
27
+ this.sessions.set(s.id, { ...known, to: Math.min(now, Date.parse(s.updatedAt) || now) });
28
+ continue;
29
+ }
30
+ if (this.sessions.size >= MAX_SESSIONS)
31
+ continue;
32
+ const from = Date.parse(s.startedAt) || now;
33
+ this.sessions.set(s.id, {
34
+ id: s.id,
35
+ agent: s.harness,
36
+ project: s.project ?? fallbackProject,
37
+ from,
38
+ ...(ended ? { to: Math.max(from, Date.parse(s.updatedAt) || now) } : {}),
39
+ });
40
+ }
41
+ // A session whose record is gone is not running any more: it ended by now at the latest.
42
+ const listed = new Set(list.map((s) => s.id));
43
+ for (const [id, s] of this.sessions)
44
+ if (!listed.has(id) && s.to === undefined)
45
+ this.sessions.set(id, { ...s, to: now });
46
+ }
47
+ get(id) {
48
+ return id === undefined ? undefined : this.sessions.get(id);
49
+ }
50
+ /** The one session running in this project at time t, or undefined when none or several were. */
51
+ onlyRunning(project, t) {
52
+ const running = [...this.sessions.values()].filter((s) => s.project.toLowerCase() === project.toLowerCase() && s.from <= t && (s.to === undefined || t <= s.to));
53
+ return running.length === 1 ? running[0] : undefined;
54
+ }
55
+ /** A file change in a project at time t: linked only when one session ran there then. */
56
+ linkFile(project, t) {
57
+ const only = this.onlyRunning(project, t);
58
+ return only ? { s: only.id, sv: 't' } : undefined;
59
+ }
60
+ /**
61
+ * A commit in a project at time t: the session whose git command made it when that project's sessions ran git
62
+ * commands that make commits around then and all were the same session's; else, when `likely` is allowed (programs
63
+ * are not recorded, so no git command could be seen), the one session running there.
64
+ */
65
+ linkCommit(project, t, gitRuns, likely = true) {
66
+ const near = new Set(gitRuns
67
+ .filter((g) => g.t >= t - GIT_BEFORE_MS && g.t <= t + GIT_AFTER_MS)
68
+ .map((g) => g.session)
69
+ .filter((id) => this.get(id)?.project.toLowerCase() === project.toLowerCase()));
70
+ if (near.size === 1)
71
+ return { s: [...near][0], sv: 'p' };
72
+ if (near.size > 1)
73
+ return undefined;
74
+ return likely ? this.linkFile(project, t) : undefined;
75
+ }
76
+ }
@@ -5,29 +5,55 @@ declare const AuditSettingsSchema: z.ZodObject<{
5
5
  maxMb: z.ZodNumber;
6
6
  processes: z.ZodBoolean;
7
7
  files: z.ZodBoolean;
8
+ commits: z.ZodDefault<z.ZodBoolean>;
8
9
  projects: z.ZodArray<z.ZodString>;
9
10
  }, z.core.$strip>;
10
11
  export type AuditSettings = z.output<typeof AuditSettingsSchema>;
11
12
  /** Host-only: the audit settings of the running sandbox (written at launch, read by the watcher). */
12
13
  export declare function saveAuditSettings(storageRoot: string, dataPath: string, settings: AuditSettings): void;
13
14
  export declare function readAuditSettings(storageRoot: string, dataPath: string): AuditSettings | null;
15
+ /** A project of the running sandbox: its name (as sessions name it) and its folder on this PC. */
16
+ export interface AuditProject {
17
+ readonly name: string;
18
+ readonly host: string;
19
+ }
14
20
  export declare class AuditCollector {
15
21
  private readonly dataPath;
16
22
  private readonly settings;
23
+ private readonly projects;
17
24
  private readonly log;
18
25
  private pending;
19
26
  private sessions;
27
+ private readonly book;
28
+ private gitRuns;
29
+ private held;
30
+ private readonly commitWatcher;
31
+ private readonly watched;
20
32
  private watchers;
21
- constructor(dataPath: string, storageRoot: string, settings: AuditSettings);
33
+ private maintainedOn;
34
+ constructor(dataPath: string, storageRoot: string, settings: AuditSettings, projects?: () => readonly AuditProject[]);
22
35
  private add;
23
- /** Starts watching the projects' files (when audit.files). */
36
+ /** Deletes old days, then starts watching the projects' files and commits. */
24
37
  start(): void;
38
+ /** The projects not watched yet (one that joined the sandbox since): their files and their reflogs from now on. */
39
+ private watchProjects;
40
+ private watchFiles;
41
+ private linked;
25
42
  /** Programs the sandbox reported: taken in (checked) and deleted. */
26
43
  private takeGuestReports;
44
+ /** One line the sandbox wrote: only a program, with only the fields it may set. */
45
+ private takeLine;
46
+ private primaryProject;
27
47
  private trackSessions;
48
+ /** New commits are held a moment; held ones whose time has come are written, linked to a session if one is known. */
49
+ private takeCommits;
50
+ /** File changes not linked yet: to the one session running in their project then, if there was exactly one. */
51
+ private linkFiles;
28
52
  guard(detail: string): void;
53
+ /** Old days go when the watcher starts and again whenever the day changes while it runs. */
54
+ private maintainDaily;
29
55
  /** Writes what came in since the last call (repeats folded); call every few seconds. */
30
- flush(): void;
56
+ flush(final?: boolean): void;
31
57
  stop(): void;
32
58
  maintain(): void;
33
59
  }
@@ -1,26 +1,68 @@
1
1
  // What goes into the audit log, from the background watcher while the sandbox runs:
2
2
  // - programs started in the sandbox: the .jsonl files it leaves in run\audit\ (untrusted: links refused, each file
3
- // and line size-capped, every line checked and forced to kind "proc"), taken in and deleted;
3
+ // and line size-capped, every line checked and forced to kind "proc", times kept to now, command lines scrubbed of
4
+ // secrets, a session it names kept only when this sandbox has that session), taken in and deleted;
4
5
  // - files changed in the projects: watched from this PC (the sandbox cannot hide or fake these);
6
+ // - git commits in the projects: read from each project's reflog on this PC (git-commits.ts);
5
7
  // - sessions starting and ending, and what the repo guard caught.
8
+ // Each event is linked to the agent session that did it where that can be known (attribute.ts), else to none.
6
9
  import fs from 'node:fs';
7
10
  import path from 'node:path';
8
11
  import { z } from 'zod';
9
12
  import { assertNoLinks } from '../../core/util/index.js';
10
13
  import { RunDir } from '../../shared/channel/index.js';
11
14
  import { JsonFile } from '../../shared/io/index.js';
15
+ import { hubName } from '../../shared/paths/index.js';
16
+ // The file itself, not the barrel: the barrel leads back to the watcher, which imports this file.
17
+ import { ProjectRegistry } from '../projects/registry.js';
12
18
  import { hostStateFile } from '../storage/index.js';
13
- import { AuditEventSchema, AuditLog, fold } from './log.js';
19
+ import { makesCommits, SessionBook } from './attribute.js';
20
+ import { CommitWatcher } from './git-commits.js';
21
+ import { AuditEventSchema, AuditLog, AuditSessionIdSchema, fold } from './log.js';
22
+ import { redactCommandLine } from './redact.js';
14
23
  const MAX_FILES_PER_PASS = 200;
15
24
  const MAX_FILE_BYTES = 8 * 1024 * 1024;
25
+ /** Most of the sandbox's reports taken in per pass, all files together. */
26
+ const MAX_BYTES_PER_PASS = 16 * 1024 * 1024;
27
+ /** Commits waiting to be written, and git commands remembered for linking them, at most. */
28
+ const MAX_HELD_COMMITS = 500;
29
+ const MAX_GIT_RUNS = 2000;
30
+ /** A plain file's text (null when it is not a plain file or is bigger than `max`), checked on the open handle. */
31
+ function readPlainFile(file, max) {
32
+ if (!fs.lstatSync(file).isFile())
33
+ return null;
34
+ const fd = fs.openSync(file, 'r');
35
+ try {
36
+ const stat = fs.fstatSync(fd);
37
+ if (!stat.isFile() || stat.size > max)
38
+ return null;
39
+ const buffer = Buffer.alloc(stat.size);
40
+ const read = fs.readSync(fd, buffer, 0, stat.size, 0);
41
+ return buffer.subarray(0, read).toString('utf8');
42
+ }
43
+ finally {
44
+ fs.closeSync(fd);
45
+ }
46
+ }
16
47
  /** Paths in a project not worth a line each (git's own bookkeeping, dependency folders). */
17
48
  const QUIET = /(^|[\\/])(\.git[\\/](objects|logs|refs|index\.lock|FETCH_HEAD|ORIG_HEAD)|node_modules[\\/]|target[\\/]|\.venv[\\/]|__pycache__[\\/])/i;
49
+ /** A commit waits this long before it is written, so the git command that made it (reported later) can claim it. */
50
+ const COMMIT_HOLD_MS = 45 * 1000;
51
+ /** Git commands are remembered this long for linking commits. */
52
+ const GIT_RUNS_KEPT_MS = 10 * 60 * 1000;
53
+ /** A time the sandbox reports is believed only this far from now (else it is now). */
54
+ const PAST_MS = 2 * 86400000;
55
+ const FUTURE_MS = 3600000;
56
+ /** Projects watched at most (the running sandbox's registry can grow while it runs). */
57
+ const MAX_PROJECTS = 64;
18
58
  const AuditSettingsSchema = z.object({
19
59
  enabled: z.boolean(),
20
60
  days: z.number().int().min(1).max(365),
21
61
  maxMb: z.number().int().min(5).max(5000),
22
62
  processes: z.boolean(),
23
63
  files: z.boolean(),
64
+ /** Missing in files written by versions before commits were recorded. */
65
+ commits: z.boolean().default(true),
24
66
  projects: z.array(z.string().max(1024)).max(64),
25
67
  });
26
68
  const settingsFile = (storageRoot, dataPath) => new JsonFile(AuditSettingsSchema, hostStateFile(storageRoot, dataPath, '-audit.json'));
@@ -31,51 +73,109 @@ export function saveAuditSettings(storageRoot, dataPath, settings) {
31
73
  export function readAuditSettings(storageRoot, dataPath) {
32
74
  return settingsFile(storageRoot, dataPath).read();
33
75
  }
76
+ /** The sandbox's projects: the registry's (projects that joined while it runs too) and the ones it started with. */
77
+ function registryProjects(storageRoot, started) {
78
+ const out = [];
79
+ const add = (name, host) => {
80
+ if (!out.some((p) => p.host.toLowerCase() === host.toLowerCase()))
81
+ out.push({ name, host });
82
+ };
83
+ try {
84
+ for (const p of new ProjectRegistry(storageRoot).read()?.projects ?? [])
85
+ add(p.name, p.host);
86
+ }
87
+ catch {
88
+ // No registry (or an unreadable one): the projects it started with.
89
+ }
90
+ for (const host of started)
91
+ add(hubName(host, []), host);
92
+ return out.slice(0, MAX_PROJECTS);
93
+ }
94
+ /** A time from the sandbox as an ISO time near now: one too far off (a wrong clock, a made-up line) becomes now. */
95
+ function believable(t, now) {
96
+ const ms = Date.parse(t);
97
+ return Number.isNaN(ms) || ms < now - PAST_MS || ms > now + FUTURE_MS
98
+ ? new Date(now).toISOString()
99
+ : new Date(ms).toISOString();
100
+ }
34
101
  export class AuditCollector {
35
102
  dataPath;
36
103
  settings;
104
+ projects;
37
105
  log;
38
106
  pending = [];
39
107
  sessions = new Map();
108
+ book = new SessionBook();
109
+ gitRuns = [];
110
+ held = [];
111
+ commitWatcher = new CommitWatcher();
112
+ watched = new Set();
40
113
  watchers = [];
41
- constructor(dataPath, storageRoot, settings) {
114
+ maintainedOn = '';
115
+ constructor(dataPath, storageRoot, settings, projects = () => registryProjects(storageRoot, settings.projects)) {
42
116
  this.dataPath = dataPath;
43
117
  this.settings = settings;
118
+ this.projects = projects;
44
119
  this.log = new AuditLog(AuditLog.dirFor(storageRoot, dataPath), settings);
45
120
  }
46
121
  add(e) {
47
122
  this.pending.push({ t: new Date().toISOString(), ...e });
48
123
  }
49
- /** Starts watching the projects' files (when audit.files). */
124
+ /** Deletes old days, then starts watching the projects' files and commits. */
50
125
  start() {
126
+ this.maintainDaily();
51
127
  this.add({
52
128
  k: 'note',
53
129
  p: 'sandbox started; audit: ' +
54
- [this.settings.processes && 'programs', this.settings.files && 'files', 'sessions'].filter(Boolean).join(', '),
130
+ [
131
+ this.settings.processes && 'programs',
132
+ this.settings.files && 'files',
133
+ this.settings.commits && 'commits',
134
+ 'sessions',
135
+ ]
136
+ .filter(Boolean)
137
+ .join(', '),
55
138
  });
56
- if (!this.settings.files)
57
- return;
58
- for (const project of this.settings.projects) {
59
- try {
60
- this.watchers.push(fs.watch(project, { recursive: true }, (event, file) => {
61
- const rel = file ? String(file) : '';
62
- if (!rel || QUIET.test(rel))
63
- return;
64
- this.add({
65
- k: 'file',
66
- a: event === 'rename' ? 'rename' : 'change',
67
- p: rel.replace(/\\/g, '/').slice(0, 1024),
68
- w: project,
69
- });
70
- }));
71
- }
72
- catch {
73
- /* a project that is gone */
74
- }
139
+ this.watchProjects();
140
+ }
141
+ /** The projects not watched yet (one that joined the sandbox since): their files and their reflogs from now on. */
142
+ watchProjects() {
143
+ for (const project of this.projects()) {
144
+ const key = project.host.toLowerCase();
145
+ if (this.watched.has(key))
146
+ continue;
147
+ this.watched.add(key);
148
+ if (this.settings.commits)
149
+ this.commitWatcher.prime([project.host]);
150
+ if (this.settings.files)
151
+ this.watchFiles(project);
152
+ }
153
+ }
154
+ watchFiles(project) {
155
+ try {
156
+ this.watchers.push(fs.watch(project.host, { recursive: true }, (event, file) => {
157
+ const rel = file ? String(file) : '';
158
+ if (!rel || QUIET.test(rel))
159
+ return;
160
+ this.add({
161
+ k: 'file',
162
+ a: event === 'rename' ? 'rename' : 'change',
163
+ p: rel.replace(/\\/g, '/').slice(0, 1024),
164
+ w: project.host,
165
+ pj: project.name,
166
+ });
167
+ }));
75
168
  }
169
+ catch {
170
+ /* a project that is gone */
171
+ }
172
+ }
173
+ linked(link) {
174
+ const s = this.book.get(link?.s);
175
+ return link && s ? { s: s.id, ag: s.agent, pj: s.project, sv: link.sv } : {};
76
176
  }
77
177
  /** Programs the sandbox reported: taken in (checked) and deleted. */
78
- takeGuestReports() {
178
+ takeGuestReports(now) {
79
179
  if (!this.settings.processes)
80
180
  return;
81
181
  const dir = new RunDir(this.dataPath).auditDir;
@@ -91,38 +191,80 @@ export class AuditCollector {
91
191
  catch {
92
192
  return;
93
193
  }
194
+ // A budget for the whole pass: what is over it waits (the next pass takes the next files).
195
+ let budget = MAX_BYTES_PER_PASS;
94
196
  for (const name of names) {
95
197
  const file = path.join(dir, name);
96
198
  try {
97
- const stat = fs.lstatSync(file);
98
- if (stat.isFile() && stat.size <= MAX_FILE_BYTES) {
99
- for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
100
- if (!line.trim() || line.length > 8192)
101
- continue;
102
- let raw;
103
- try {
104
- raw = JSON.parse(line);
105
- }
106
- catch {
107
- continue;
108
- }
109
- const e = AuditEventSchema.safeParse(raw);
110
- if (e.success)
111
- this.pending.push({ ...e.data, k: 'proc', w: undefined, a: undefined });
112
- }
199
+ const size = fs.lstatSync(file).size;
200
+ if (size <= MAX_FILE_BYTES && size > budget)
201
+ break;
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ try {
207
+ const text = readPlainFile(file, MAX_FILE_BYTES);
208
+ if (text !== null) {
209
+ budget -= text.length;
210
+ for (const line of text.split('\n'))
211
+ this.takeLine(line, now);
113
212
  }
114
213
  }
214
+ catch {
215
+ /* unreadable: deleted below like any other */
216
+ }
115
217
  finally {
116
- fs.rmSync(file, { force: true });
218
+ try {
219
+ fs.rmSync(file, { force: true, recursive: true });
220
+ }
221
+ catch {
222
+ // Cannot be removed (held open): the next pass tries again.
223
+ }
117
224
  }
118
225
  }
119
226
  }
120
- trackSessions() {
121
- const now = new Map(new RunDir(this.dataPath).listSessions().map((s) => [s.id, s.phase]));
122
- for (const s of new RunDir(this.dataPath).listSessions()) {
227
+ /** One line the sandbox wrote: only a program, with only the fields it may set. */
228
+ takeLine(line, now) {
229
+ if (!line.trim() || line.length > 8192)
230
+ return;
231
+ let raw;
232
+ try {
233
+ raw = JSON.parse(line);
234
+ }
235
+ catch {
236
+ return;
237
+ }
238
+ const e = AuditEventSchema.safeParse(raw);
239
+ if (!e.success)
240
+ return;
241
+ const { t, p, c, pp } = e.data;
242
+ const claimed = AuditSessionIdSchema.safeParse(e.data.s);
243
+ const session = claimed.success ? this.book.get(claimed.data) : undefined;
244
+ const event = {
245
+ t: believable(t, now),
246
+ k: 'proc',
247
+ ...(p !== undefined ? { p } : {}),
248
+ ...(c !== undefined ? { c: redactCommandLine(c) } : {}),
249
+ ...(pp !== undefined ? { pp } : {}),
250
+ ...(session ? this.linked({ s: session.id, sv: 'p' }) : {}),
251
+ };
252
+ if (session && makesCommits(p ?? '', c ?? '') && this.gitRuns.length < MAX_GIT_RUNS)
253
+ this.gitRuns.push({ t: Date.parse(event.t), session: session.id });
254
+ this.pending.push(event);
255
+ }
256
+ primaryProject() {
257
+ return this.projects()[0]?.name ?? 'project';
258
+ }
259
+ trackSessions(now) {
260
+ const list = new RunDir(this.dataPath).listSessions();
261
+ this.book.update(list, this.primaryProject(), now);
262
+ const phases = new Map(list.map((s) => [s.id, s.phase]));
263
+ for (const s of list) {
123
264
  const before = this.sessions.get(s.id);
265
+ const who = this.linked({ s: s.id, sv: 'p' });
124
266
  if (!before && s.phase === 'running')
125
- this.add({ k: 'session', a: 'start', p: s.harness, w: s.cwd, c: s.id });
267
+ this.add({ k: 'session', a: 'start', p: s.harness, w: s.cwd, c: s.id, ...who, sv: undefined });
126
268
  if (before === 'running' && (s.phase === 'exited' || s.phase === 'failed'))
127
269
  this.add({
128
270
  k: 'session',
@@ -130,35 +272,88 @@ export class AuditCollector {
130
272
  p: s.harness,
131
273
  w: s.cwd,
132
274
  c: s.id + (s.exitCode !== undefined ? ' exit ' + s.exitCode : ''),
275
+ ...who,
276
+ sv: undefined,
133
277
  });
134
278
  }
135
- this.sessions = now;
279
+ this.sessions = phases;
280
+ }
281
+ /** New commits are held a moment; held ones whose time has come are written, linked to a session if one is known. */
282
+ takeCommits(now, final) {
283
+ if (!this.settings.commits)
284
+ return;
285
+ const projects = this.projects();
286
+ // Commits are never folded, so their number is capped here (a reflog full of made-up lines cannot flood the log).
287
+ const fresh = this.commitWatcher.commits(projects.map((p) => p.host));
288
+ const room = Math.max(0, MAX_HELD_COMMITS - this.held.length);
289
+ for (const commit of fresh.slice(0, room))
290
+ this.held.push({ commit, seenAt: now });
291
+ if (fresh.length > room)
292
+ this.add({ k: 'note', p: fresh.length - room + ' more commits were seen at once and not listed (audit cap)' });
293
+ const due = this.held.filter((h) => final || now - h.seenAt >= COMMIT_HOLD_MS);
294
+ this.held = this.held.filter((h) => !due.includes(h));
295
+ for (const { commit, seenAt } of due) {
296
+ const name = projects.find((p) => p.host.toLowerCase() === commit.project.toLowerCase())?.name ??
297
+ hubName(commit.project, []);
298
+ // Git's own time, unless the sandbox's clock (or the line) is far off: then when it was seen.
299
+ const t = believable(commit.t, seenAt);
300
+ this.pending.push({
301
+ t,
302
+ k: 'commit',
303
+ a: commit.action,
304
+ p: redactCommandLine(commit.message),
305
+ h: commit.sha,
306
+ au: commit.author,
307
+ w: commit.project,
308
+ pj: name,
309
+ // With programs recorded, a commit no session's git command made is not put on a session as "likely" (it
310
+ // may well be yours, made on this PC).
311
+ ...this.linked(this.book.linkCommit(name, Date.parse(t), this.gitRuns, !this.settings.processes)),
312
+ });
313
+ }
314
+ this.gitRuns = this.gitRuns.filter((g) => g.t >= now - GIT_RUNS_KEPT_MS);
315
+ }
316
+ /** File changes not linked yet: to the one session running in their project then, if there was exactly one. */
317
+ linkFiles() {
318
+ this.pending = this.pending.map((e) => e.k === 'file' && !e.s && e.pj ? { ...e, ...this.linked(this.book.linkFile(e.pj, Date.parse(e.t))) } : e);
136
319
  }
137
320
  guard(detail) {
138
321
  this.add({ k: 'guard', p: detail.slice(0, 1024) });
139
322
  }
323
+ /** Old days go when the watcher starts and again whenever the day changes while it runs. */
324
+ maintainDaily() {
325
+ const today = new Date().toISOString().slice(0, 10);
326
+ if (today === this.maintainedOn)
327
+ return;
328
+ this.maintainedOn = today;
329
+ this.log.maintain(today);
330
+ }
140
331
  /** Writes what came in since the last call (repeats folded); call every few seconds. */
141
- flush() {
142
- try {
143
- this.takeGuestReports();
144
- }
145
- catch {
146
- /* next pass */
147
- }
148
- try {
149
- this.trackSessions();
150
- }
151
- catch {
152
- /* next pass */
153
- }
332
+ flush(final = false) {
333
+ const now = Date.now();
334
+ const step = (work) => {
335
+ try {
336
+ work();
337
+ }
338
+ catch {
339
+ /* next pass */
340
+ }
341
+ };
342
+ step(() => this.watchProjects());
343
+ // Sessions first: a program or commit can only be linked to a session the book knows.
344
+ step(() => this.trackSessions(now));
345
+ step(() => this.takeGuestReports(now));
346
+ step(() => this.takeCommits(now, final));
347
+ step(() => this.linkFiles());
154
348
  const events = this.pending.sort((a, b) => a.t.localeCompare(b.t));
155
349
  this.pending = [];
156
350
  this.log.append(fold(events));
351
+ step(() => this.maintainDaily());
157
352
  }
158
353
  stop() {
159
354
  this.watchers.forEach((w) => w.close());
160
355
  this.add({ k: 'note', p: 'sandbox closed' });
161
- this.flush();
356
+ this.flush(true);
162
357
  this.log.maintain();
163
358
  }
164
359
  maintain() {
@@ -0,0 +1,29 @@
1
+ /** One commit as the reflog records it. */
2
+ export interface CommitSeen {
3
+ /** ISO time git wrote. */
4
+ readonly t: string;
5
+ readonly sha: string;
6
+ /** Who made it, as git names them ("Name <email>"). */
7
+ readonly author: string;
8
+ /** commit | amend | merge | cherry-pick | revert | rebase | initial. */
9
+ readonly action: string;
10
+ /** The first line of its message. */
11
+ readonly message: string;
12
+ /** The project folder on this PC. */
13
+ readonly project: string;
14
+ }
15
+ /** A reflog line as a commit, or null when it is not one (a checkout, a reset, a pull without a merge commit ...). */
16
+ export declare function parseReflogLine(line: string, project: string): CommitSeen | null;
17
+ /** Follows the projects' reflogs: commits() returns the commits added since the last call. */
18
+ export declare class CommitWatcher {
19
+ /** Reflog file -> bytes already read. */
20
+ private readonly offsets;
21
+ /** The reflogs of a project: the main one and each worktree's. */
22
+ private reflogs;
23
+ /** Starts from what is there now: only commits made from here on are reported. */
24
+ prime(projects: readonly string[]): void;
25
+ private sizeOf;
26
+ /** The commits added to the projects' reflogs since the last call. A reflog seen for the first time is primed. */
27
+ commits(projects: readonly string[]): CommitSeen[];
28
+ private readRange;
29
+ }