windows-development-sandbox 1.1.0 → 1.4.1

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 (222) hide show
  1. package/.sandbox-config.example.json +2 -1
  2. package/README.md +3 -2
  3. package/config.example.json +39 -5
  4. package/dist/cli/args.d.ts +3 -0
  5. package/dist/cli/args.js +3 -1
  6. package/dist/cli/boot-progress.d.ts +22 -0
  7. package/dist/cli/boot-progress.js +59 -0
  8. package/dist/cli/commands/accounts.js +2 -2
  9. package/dist/cli/commands/agent-commands.js +1 -1
  10. package/dist/cli/commands/context.js +2 -1
  11. package/dist/cli/commands/data/clean.js +6 -1
  12. package/dist/cli/commands/data/collect.js +3 -3
  13. package/dist/cli/commands/index.d.ts +1 -1
  14. package/dist/cli/commands/index.js +1 -1
  15. package/dist/cli/commands/launch.d.ts +5 -2
  16. package/dist/cli/commands/launch.js +113 -25
  17. package/dist/cli/commands/maintenance.d.ts +2 -0
  18. package/dist/cli/commands/maintenance.js +70 -4
  19. package/dist/cli/commands/project.d.ts +10 -6
  20. package/dist/cli/commands/project.js +42 -3
  21. package/dist/cli/commands/restart.js +17 -7
  22. package/dist/cli/commands/sessions.js +5 -4
  23. package/dist/cli/commands/user-config.js +1 -1
  24. package/dist/cli/index.js +6 -4
  25. package/dist/cli/manifest.js +6 -1
  26. package/dist/cli/menu/apps.d.ts +21 -0
  27. package/dist/cli/menu/apps.js +122 -0
  28. package/dist/cli/menu/command-form.d.ts +28 -0
  29. package/dist/cli/menu/command-form.js +219 -0
  30. package/dist/cli/menu/index.d.ts +26 -0
  31. package/dist/cli/menu/index.js +180 -0
  32. package/dist/cli/menu/settings/categories.d.ts +9 -0
  33. package/dist/cli/menu/settings/categories.js +28 -0
  34. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  35. package/dist/cli/menu/settings/config-file.js +71 -0
  36. package/dist/cli/menu/settings/editors.d.ts +30 -0
  37. package/dist/cli/menu/settings/editors.js +305 -0
  38. package/dist/cli/menu/settings/index.d.ts +32 -0
  39. package/dist/cli/menu/settings/index.js +136 -0
  40. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  41. package/dist/cli/menu/settings/schema-node.js +104 -0
  42. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  43. package/dist/cli/menu/several.js +102 -0
  44. package/dist/cli/progress.d.ts +15 -0
  45. package/dist/cli/progress.js +52 -0
  46. package/dist/cli/tui/app.d.ts +39 -0
  47. package/dist/cli/tui/app.js +52 -0
  48. package/dist/cli/tui/dialogs.d.ts +57 -0
  49. package/dist/cli/tui/dialogs.js +162 -0
  50. package/dist/cli/tui/frame.d.ts +18 -0
  51. package/dist/cli/tui/frame.js +27 -0
  52. package/dist/cli/tui/index.d.ts +7 -0
  53. package/dist/cli/tui/index.js +8 -0
  54. package/dist/cli/tui/list-screen.d.ts +101 -0
  55. package/dist/cli/tui/list-screen.js +299 -0
  56. package/dist/cli/tui/terminal.d.ts +37 -0
  57. package/dist/cli/tui/terminal.js +89 -0
  58. package/dist/cli/tui/text.d.ts +20 -0
  59. package/dist/cli/tui/text.js +105 -0
  60. package/dist/cli/tui/theme.d.ts +45 -0
  61. package/dist/cli/tui/theme.js +30 -0
  62. package/dist/core/config.js +10 -7
  63. package/dist/core/local-config.d.ts +31 -1
  64. package/dist/core/local-config.js +72 -7
  65. package/dist/core/project-policy.d.ts +50 -3
  66. package/dist/core/project-policy.js +90 -4
  67. package/dist/core/schema/apps.d.ts +40 -0
  68. package/dist/core/schema/apps.js +27 -0
  69. package/dist/core/schema/config.d.ts +69 -3
  70. package/dist/core/schema/config.js +18 -6
  71. package/dist/core/schema/profile.d.ts +5 -0
  72. package/dist/core/schema/profile.js +4 -2
  73. package/dist/core/schema/projects.d.ts +1 -0
  74. package/dist/core/schema/projects.js +1 -0
  75. package/dist/core/schema/sandbox.d.ts +29 -1
  76. package/dist/core/schema/sandbox.js +29 -1
  77. package/dist/core/schema/toolchains.d.ts +3 -0
  78. package/dist/core/schema/toolchains.js +6 -1
  79. package/dist/core/types.d.ts +24 -1
  80. package/dist/core/util.d.ts +2 -0
  81. package/dist/core/util.js +2 -0
  82. package/dist/core/workspace.d.ts +21 -4
  83. package/dist/core/workspace.js +99 -21
  84. package/dist/guest/apps/index.d.ts +10 -0
  85. package/dist/guest/apps/index.js +206 -0
  86. package/dist/guest/apps/resolvers.d.ts +8 -0
  87. package/dist/guest/apps/resolvers.js +90 -0
  88. package/dist/guest/audit-parse.d.ts +7 -0
  89. package/dist/guest/audit-parse.js +24 -0
  90. package/dist/guest/audit.d.ts +4 -0
  91. package/dist/guest/audit.js +47 -0
  92. package/dist/guest/daemon/relays.d.ts +1 -0
  93. package/dist/guest/daemon/relays.js +18 -0
  94. package/dist/guest/daemon.js +4 -0
  95. package/dist/guest/env.d.ts +7 -2
  96. package/dist/guest/env.js +17 -3
  97. package/dist/guest/helpers.d.ts +1 -1
  98. package/dist/guest/main.js +20 -3
  99. package/dist/guest/netproxy.d.ts +1 -0
  100. package/dist/guest/netproxy.js +100 -0
  101. package/dist/guest/ptyhost.js +14 -4
  102. package/dist/guest/setup/claude.d.ts +0 -4
  103. package/dist/guest/setup/claude.js +48 -2
  104. package/dist/guest/setup/network.d.ts +9 -0
  105. package/dist/guest/setup/network.js +97 -4
  106. package/dist/guest/setup/user.js +4 -0
  107. package/dist/guest/toolchains/index.js +29 -3
  108. package/dist/guest/toolchains/native/mesa.js +13 -3
  109. package/dist/guest/toolchains/plan.d.ts +3 -1
  110. package/dist/guest/toolchains/plan.js +53 -9
  111. package/dist/guest/toolchains/python.js +26 -2
  112. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  113. package/dist/guest/toolchains/vscode-profile.js +69 -0
  114. package/dist/guest/toolchains/vscode.d.ts +17 -0
  115. package/dist/guest/toolchains/vscode.js +87 -1
  116. package/dist/host/attach.js +2 -1
  117. package/dist/host/audit/collect.d.ts +34 -0
  118. package/dist/host/audit/collect.js +126 -0
  119. package/dist/host/audit/log.d.ts +46 -0
  120. package/dist/host/audit/log.js +143 -0
  121. package/dist/host/channel.d.ts +2 -2
  122. package/dist/host/channel.js +3 -2
  123. package/dist/host/generate.d.ts +7 -1
  124. package/dist/host/generate.js +23 -6
  125. package/dist/host/join.d.ts +7 -0
  126. package/dist/host/join.js +23 -9
  127. package/dist/host/launch/mappings.d.ts +7 -2
  128. package/dist/host/launch/mappings.js +68 -18
  129. package/dist/host/launch/projects.d.ts +2 -0
  130. package/dist/host/launch/projects.js +11 -5
  131. package/dist/host/launch/stage.js +1 -1
  132. package/dist/host/motw.d.ts +17 -0
  133. package/dist/host/motw.js +74 -0
  134. package/dist/host/platform.js +3 -0
  135. package/dist/host/port-forward.d.ts +8 -0
  136. package/dist/host/port-forward.js +37 -0
  137. package/dist/host/projects.d.ts +25 -2
  138. package/dist/host/projects.js +54 -5
  139. package/dist/host/repo-guard.d.ts +10 -3
  140. package/dist/host/repo-guard.js +142 -14
  141. package/dist/host/stops.d.ts +67 -0
  142. package/dist/host/stops.js +126 -0
  143. package/dist/host/terminal.d.ts +8 -0
  144. package/dist/host/terminal.js +45 -1
  145. package/dist/host/workspace-view/index.d.ts +20 -0
  146. package/dist/host/workspace-view/index.js +53 -0
  147. package/dist/host/workspace-view/rules.d.ts +26 -0
  148. package/dist/host/workspace-view/rules.js +93 -0
  149. package/dist/host/workspace-view/view.d.ts +50 -0
  150. package/dist/host/workspace-view/view.js +281 -0
  151. package/dist/runtime/daemon.js +1387 -558
  152. package/dist/runtime/db.js +199 -14
  153. package/dist/runtime/main.js +3082 -631
  154. package/dist/runtime/netproxy.js +25948 -0
  155. package/dist/runtime/view.js +531 -148
  156. package/dist/selftest/audit.test.d.ts +2 -0
  157. package/dist/selftest/audit.test.js +57 -0
  158. package/dist/selftest/channel.test.js +45 -2
  159. package/dist/selftest/cli.test.js +47 -17
  160. package/dist/selftest/fixtures.d.ts +47 -1
  161. package/dist/selftest/fixtures.js +3 -1
  162. package/dist/selftest/generate.test.js +7 -2
  163. package/dist/selftest/index.js +6 -2
  164. package/dist/selftest/live-network.test.d.ts +2 -0
  165. package/dist/selftest/live-network.test.js +63 -0
  166. package/dist/selftest/live-terminal.test.d.ts +2 -0
  167. package/dist/selftest/live-terminal.test.js +169 -0
  168. package/dist/selftest/local-config.test.js +42 -4
  169. package/dist/selftest/mirror.test.js +36 -7
  170. package/dist/selftest/network.test.js +12 -0
  171. package/dist/selftest/projects.test.js +77 -8
  172. package/dist/selftest/repo-guard.test.js +59 -1
  173. package/dist/selftest/sync.test.js +50 -10
  174. package/dist/selftest/toolchains.test.js +30 -0
  175. package/dist/selftest/workspace-view.test.d.ts +2 -0
  176. package/dist/selftest/workspace-view.test.js +89 -0
  177. package/dist/shared/apps.d.ts +49 -0
  178. package/dist/shared/apps.js +114 -0
  179. package/dist/shared/console-input.d.ts +11 -0
  180. package/dist/shared/console-input.js +61 -0
  181. package/dist/shared/download.d.ts +3 -0
  182. package/dist/shared/download.js +25 -2
  183. package/dist/shared/guest-paths.d.ts +27 -0
  184. package/dist/shared/guest-paths.js +68 -0
  185. package/dist/shared/netpolicy.d.ts +27 -0
  186. package/dist/shared/netpolicy.js +51 -0
  187. package/dist/shared/projects.d.ts +27 -4
  188. package/dist/shared/projects.js +72 -13
  189. package/dist/shared/pty-client.d.ts +26 -4
  190. package/dist/shared/pty-client.js +204 -25
  191. package/dist/shared/rundir.d.ts +2 -0
  192. package/dist/shared/rundir.js +2 -0
  193. package/dist/shared/schema.d.ts +34 -0
  194. package/dist/shared/schema.js +14 -0
  195. package/dist/shared/system.d.ts +1 -0
  196. package/dist/shared/system.js +1 -0
  197. package/dist/sync/claude-mem.js +3 -2
  198. package/dist/sync/history.d.ts +6 -0
  199. package/dist/sync/history.js +26 -11
  200. package/dist/sync/watcher.js +78 -6
  201. package/package.json +2 -1
  202. package/sandbox/cargoshim.cs +56 -0
  203. package/sandbox/cpucap.cs +33 -0
  204. package/sandbox/cpucap.ps1 +4 -35
  205. package/sandbox/logon.cmd +6 -0
  206. package/sandbox/memlog.ps1 +34 -0
  207. package/sandbox/pagefile.cs +31 -0
  208. package/sandbox/pagefile.ps1 +21 -0
  209. package/sandbox/ptyhost.cs +162 -7
  210. package/schemas/config.schema.json +318 -3
  211. package/schemas/sandbox-config.schema.json +129 -14
  212. package/dist/cli/menu-multi.js +0 -82
  213. package/dist/cli/menu.d.ts +0 -14
  214. package/dist/cli/menu.js +0 -290
  215. package/dist/cli/settings/edit.d.ts +0 -13
  216. package/dist/cli/settings/edit.js +0 -19
  217. package/dist/cli/settings/index.d.ts +0 -8
  218. package/dist/cli/settings/index.js +0 -131
  219. package/dist/cli/settings/prompts.d.ts +0 -7
  220. package/dist/cli/settings/prompts.js +0 -48
  221. package/dist/cli/settings/schema.d.ts +0 -33
  222. package/dist/cli/settings/schema.js +0 -29
@@ -0,0 +1,30 @@
1
+ /** Where the sandbox's tools are (absent = not installed). */
2
+ export interface ToolPaths {
3
+ javaHome?: string;
4
+ python?: string;
5
+ goRoot?: string;
6
+ go?: string;
7
+ gcc?: string;
8
+ cl?: string;
9
+ cmake?: string;
10
+ dotnet?: string;
11
+ node?: string;
12
+ pwsh?: string;
13
+ git?: string;
14
+ bash?: string;
15
+ php?: string;
16
+ ruby?: string;
17
+ deno?: string;
18
+ }
19
+ /** User settings for the sandbox's VS Code. Only keys with a value are set; the caller removes the ones it set before. */
20
+ export declare const vscodeSettings: (p: ToolPaths) => Record<string, unknown>;
21
+ export declare const EXTENSION_ID: RegExp;
22
+ /** The extensions to have: always EditorConfig, then per toolchain and database in use, the project's, and yours. */
23
+ export declare const vscodeExtensions: (o: {
24
+ toolchains: string[];
25
+ databases: string[];
26
+ recommended: string[];
27
+ extra: string[];
28
+ }) => string[];
29
+ /** Extensions not installed yet, from the names of the folders in VS Code's extensions folder (publisher.name-1.2.3). */
30
+ export declare const missingExtensions: (wanted: string[], folders: string[]) => string[];
@@ -0,0 +1,69 @@
1
+ // What VS Code is told about the sandbox: the settings that point it at the sandbox's own tools, and the extensions
2
+ // for the languages in use. Pure (tested); vscode.ts finds the paths and writes them.
3
+ /** The JDK's major version from its folder name (jdk-21.0.5+11 -> 21). */
4
+ const javaMajor = (home) => /(?:jdk|jre)?-?(\d{1,2})(?:[.+_-]|$)/i.exec(home.split(/[\\/]/).pop() ?? '')?.[1];
5
+ /** User settings for the sandbox's VS Code. Only keys with a value are set; the caller removes the ones it set before. */
6
+ export const vscodeSettings = (p) => {
7
+ const profiles = {};
8
+ if (p.pwsh)
9
+ profiles['PowerShell 7'] = { path: p.pwsh, args: ['-NoLogo'] };
10
+ if (p.bash)
11
+ profiles['Git Bash'] = { path: p.bash, args: ['--login', '-i'] };
12
+ const settings = {
13
+ 'telemetry.telemetryLevel': 'off',
14
+ 'update.mode': 'none',
15
+ 'workbench.startupEditor': 'none',
16
+ 'git.path': p.git,
17
+ 'terminal.integrated.profiles.windows': Object.keys(profiles).length ? profiles : undefined,
18
+ 'terminal.integrated.defaultProfile.windows': p.pwsh ? 'PowerShell 7' : undefined,
19
+ 'python.defaultInterpreterPath': p.python,
20
+ 'java.jdt.ls.java.home': p.javaHome,
21
+ 'java.configuration.runtimes': p.javaHome ? [{ name: 'JavaSE-' + (javaMajor(p.javaHome) ?? '21'), path: p.javaHome, default: true }] : undefined,
22
+ 'maven.terminal.useJavaHome': p.javaHome ? true : undefined,
23
+ 'go.goroot': p.goRoot,
24
+ 'go.alternateTools': p.go ? { go: p.go } : undefined,
25
+ 'C_Cpp.default.compilerPath': p.cl ?? p.gcc,
26
+ 'cmake.cmakePath': p.cmake,
27
+ 'dotnet.dotnetPath': p.dotnet ? p.dotnet.replace(/[\\/]dotnet\.exe$/i, '') : undefined,
28
+ 'dotnetAcquisitionExtension.existingDotnetPath': p.dotnet ? [{ extensionId: 'ms-dotnettools.csharp', path: p.dotnet }] : undefined,
29
+ 'php.validate.executablePath': p.php,
30
+ 'php.debug.executablePath': p.php,
31
+ 'rubyLsp.rubyVersionManager': p.ruby ? { identifier: 'none' } : undefined,
32
+ 'deno.path': p.deno,
33
+ 'powershell.powerShellAdditionalExePaths': p.pwsh ? { 'PowerShell 7 (sandbox)': p.pwsh } : undefined,
34
+ 'npm.packageManager': 'auto',
35
+ };
36
+ return Object.fromEntries(Object.entries(settings).filter(([, v]) => v !== undefined));
37
+ };
38
+ /** Extensions per toolchain (Marketplace ids). */
39
+ const BY_TOOLCHAIN = {
40
+ node: ['dbaeumer.vscode-eslint', 'esbenp.prettier-vscode'],
41
+ python: ['ms-python.python', 'ms-python.debugpy'],
42
+ java: ['redhat.java', 'vscjava.vscode-java-debug', 'vscjava.vscode-java-test', 'vscjava.vscode-maven', 'vscjava.vscode-gradle'],
43
+ go: ['golang.go'],
44
+ rust: ['rust-lang.rust-analyzer', 'vadimcn.vscode-lldb'],
45
+ cpp: ['ms-vscode.cpptools', 'ms-vscode.cmake-tools'],
46
+ msvc: ['ms-vscode.cpptools', 'ms-vscode.cmake-tools'],
47
+ dotnet: ['ms-dotnettools.csharp'],
48
+ ruby: ['shopify.ruby-lsp'],
49
+ php: ['bmewburn.vscode-intelephense-client', 'xdebug.php-debug'],
50
+ deno: ['denoland.vscode-deno'],
51
+ pwsh: ['ms-vscode.powershell'],
52
+ };
53
+ const BY_DATABASE = {
54
+ postgres: ['mtxr.sqltools', 'mtxr.sqltools-driver-pg'],
55
+ mysql: ['mtxr.sqltools', 'mtxr.sqltools-driver-mysql'],
56
+ mongodb: ['mongodb.mongodb-vscode'],
57
+ redis: ['redis.redis-for-vscode'],
58
+ };
59
+ export const EXTENSION_ID = /^[\w-]+\.[\w-]+$/;
60
+ /** The extensions to have: always EditorConfig, then per toolchain and database in use, the project's, and yours. */
61
+ export const vscodeExtensions = (o) => {
62
+ const all = ['editorconfig.editorconfig', ...o.toolchains.flatMap((t) => BY_TOOLCHAIN[t] ?? []), ...o.databases.flatMap((d) => BY_DATABASE[d] ?? []), ...o.recommended, ...o.extra];
63
+ return [...new Set(all.map((e) => e.toLowerCase()))].filter((e) => EXTENSION_ID.test(e));
64
+ };
65
+ /** Extensions not installed yet, from the names of the folders in VS Code's extensions folder (publisher.name-1.2.3). */
66
+ export const missingExtensions = (wanted, folders) => {
67
+ const have = folders.map((f) => f.toLowerCase());
68
+ return wanted.filter((id) => !have.some((f) => f.startsWith(id + '-')));
69
+ };
@@ -1,2 +1,19 @@
1
+ import type { Toolchains } from '../../core/types.js';
1
2
  import { type ToolEnv } from './common.js';
3
+ /**
4
+ * After every toolchain is installed: points VS Code at the sandbox's own JDK, Python, Go, compilers, .NET, Git and
5
+ * terminals (user settings; only the keys this tool manages are changed, your own stay), and installs the extensions
6
+ * for the languages in use in the background (they persist, so only new ones are downloaded).
7
+ */
8
+ export declare const configureVscode: (tools: {
9
+ vars: Record<string, string>;
10
+ prepend: string[];
11
+ append: string[];
12
+ summary: string[];
13
+ }, o: {
14
+ toolchains: string[];
15
+ databases: string[];
16
+ config: Toolchains["vscode"];
17
+ workspace: string;
18
+ }) => void;
2
19
  export declare const setupVscode: (env: ToolEnv, refresh: boolean) => Promise<void>;
@@ -3,9 +3,13 @@
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser';
6
8
  import { fetchJson, installArchive } from '../../shared/download.js';
7
- import { dryRun, exists, isWin, log, mkdirp, powershell } from '../env.js';
9
+ import { SYSTEM } from '../../shared/system.js';
10
+ import { dryRun, exists, isWin, log, logsDir, mkdirp, powershell } from '../env.js';
8
11
  import { arch, tool } from './common.js';
12
+ import { EXTENSION_ID, missingExtensions, vscodeExtensions, vscodeSettings } from './vscode-profile.js';
9
13
  /** Newer builds keep their files under a commit-hash folder, so the installed version is recorded here. */
10
14
  const VERSION_MARKER = '.agent-sandbox-version';
11
15
  const desktopShortcut = (exe) => {
@@ -15,6 +19,88 @@ const desktopShortcut = (exe) => {
15
19
  const q = (s) => "'" + s.replace(/'/g, "''") + "'";
16
20
  powershell('$s = (New-Object -ComObject WScript.Shell).CreateShortcut(' + q(link) + '); $s.TargetPath = ' + q(exe) + '; $s.Arguments = ' + q('"' + (process.env.AGENT_WORKSPACE ?? '') + '"') + '; $s.Save()');
17
21
  };
22
+ /** The first of `names` in one of `dirs`. */
23
+ const findIn = (dirs, ...names) => {
24
+ for (const dir of dirs)
25
+ for (const name of names) {
26
+ const file = path.join(dir, name);
27
+ if (exists(file))
28
+ return file;
29
+ }
30
+ return undefined;
31
+ };
32
+ /** The project's .vscode/extensions.json recommendations (bounded; anything odd is ignored). */
33
+ const recommendedBy = (workspace) => {
34
+ const file = path.join(workspace, '.vscode', 'extensions.json');
35
+ try {
36
+ if (!exists(file) || fs.statSync(file).size > 64 * 1024)
37
+ return [];
38
+ const data = parseJsonc(fs.readFileSync(file, 'utf8'));
39
+ return Array.isArray(data?.recommendations) ? data.recommendations.filter((r) => typeof r === 'string' && EXTENSION_ID.test(r)).slice(0, 50) : [];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ };
45
+ /**
46
+ * After every toolchain is installed: points VS Code at the sandbox's own JDK, Python, Go, compilers, .NET, Git and
47
+ * terminals (user settings; only the keys this tool manages are changed, your own stay), and installs the extensions
48
+ * for the languages in use in the background (they persist, so only new ones are downloaded).
49
+ */
50
+ export const configureVscode = (tools, o) => {
51
+ const root = tool('vscode');
52
+ const code = path.join(root, 'bin', 'code.cmd');
53
+ if (dryRun) {
54
+ log('(dry run) would set up VS Code for: ' + o.toolchains.join(', '));
55
+ return;
56
+ }
57
+ if (!exists(path.join(root, 'Code.exe')))
58
+ return;
59
+ const dirs = [...tools.prepend, ...tools.append];
60
+ const git = findIn([tool('git', 'cmd'), ...dirs], 'git.exe');
61
+ const settings = vscodeSettings({
62
+ javaHome: tools.vars.JAVA_HOME, goRoot: tools.vars.GOROOT, go: findIn(dirs, 'go.exe'), python: findIn(dirs, 'python.exe'), gcc: findIn(dirs, 'gcc.exe'), cl: findIn(dirs, 'cl.exe'),
63
+ cmake: findIn(dirs, 'cmake.exe'), dotnet: findIn(dirs, 'dotnet.exe'), node: findIn(dirs, 'node.exe'), pwsh: findIn(dirs, 'pwsh.exe'), git,
64
+ bash: git ? findIn([path.join(path.dirname(path.dirname(git)), 'bin')], 'bash.exe') : undefined, php: findIn(dirs, 'php.exe'), ruby: findIn(dirs, 'ruby.exe'), deno: findIn(dirs, 'deno.exe'),
65
+ });
66
+ writeManagedSettings(path.join(root, 'data', 'user-data', 'User', 'settings.json'), path.join(root, 'data', '.agent-sandbox-managed.json'), settings);
67
+ const wanted = vscodeExtensions({ toolchains: o.toolchains, databases: o.databases, recommended: o.config.recommended ? recommendedBy(o.workspace) : [], extra: o.config.extensions });
68
+ let folders = [];
69
+ try {
70
+ folders = fs.readdirSync(path.join(root, 'data', 'extensions'));
71
+ }
72
+ catch { /* none yet */ }
73
+ const missing = missingExtensions(wanted, folders);
74
+ tools.summary.push('vscode: set up for ' + (o.toolchains.join(', ') || 'Node') + (missing.length ? '; installing ' + missing.length + ' extension(s) in the background' : '; extensions ready'));
75
+ if (!missing.length)
76
+ return;
77
+ // In the background: the Marketplace can take a minute, and nothing else waits for it.
78
+ const logFile = fs.openSync(path.join(logsDir, 'vscode-extensions.log'), 'a');
79
+ spawn(SYSTEM.cmd(), ['/d', '/c', code, ...missing.flatMap((id) => ['--install-extension', id])], { detached: true, stdio: ['ignore', logFile, logFile], windowsHide: true }).unref();
80
+ fs.closeSync(logFile);
81
+ };
82
+ /**
83
+ * Sets `values` in a settings file, keeping everything else (comments too), and removes keys this tool set last time
84
+ * but no longer has a value for (a toolchain turned off). The keys it manages are remembered in `managedFile`.
85
+ */
86
+ const writeManagedSettings = (file, managedFile, values) => {
87
+ let text = exists(file) ? fs.readFileSync(file, 'utf8') : '{}';
88
+ let before = [];
89
+ try {
90
+ before = JSON.parse(fs.readFileSync(managedFile, 'utf8'));
91
+ }
92
+ catch { /* first time */ }
93
+ const format = { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } };
94
+ for (const key of before.filter((k) => !(k in values))) {
95
+ if (parseJsonc(text)?.[key] !== undefined)
96
+ text = applyEdits(text, modify(text, [key], undefined, format));
97
+ }
98
+ for (const [key, value] of Object.entries(values))
99
+ text = applyEdits(text, modify(text, [key], value, format));
100
+ mkdirp(path.dirname(file));
101
+ fs.writeFileSync(file, text);
102
+ fs.writeFileSync(managedFile, JSON.stringify(Object.keys(values)));
103
+ };
18
104
  export const setupVscode = async (env, refresh) => {
19
105
  const root = tool('vscode');
20
106
  const exe = path.join(root, 'Code.exe');
@@ -14,7 +14,8 @@ const ipToInt = (ip) => ip.split('.').reduce((n, part) => n * 256 + Number(part)
14
14
  export const isSandboxAddress = (ip, interfaces = os.networkInterfaces()) => {
15
15
  if (!net.isIPv4(ip))
16
16
  return false;
17
- return Object.entries(interfaces).some(([name, list]) => /^vEthernet/i.test(name) && (list ?? []).some((i) => {
17
+ // WSL's adapter is a Hyper-V adapter too, but no sandbox lives there: keystrokes must never go to a WSL VM.
18
+ return Object.entries(interfaces).some(([name, list]) => /^vEthernet/i.test(name) && !/WSL/i.test(name) && (list ?? []).some((i) => {
18
19
  if (i.family !== 'IPv4' || i.internal)
19
20
  return false;
20
21
  const mask = ipToInt(i.netmask);
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ declare const AuditSettings: z.ZodObject<{
3
+ enabled: z.ZodBoolean;
4
+ days: z.ZodNumber;
5
+ maxMb: z.ZodNumber;
6
+ processes: z.ZodBoolean;
7
+ files: z.ZodBoolean;
8
+ projects: z.ZodArray<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type AuditSettings = z.output<typeof AuditSettings>;
11
+ /** Host-only: the audit settings of the running sandbox (written at launch, read by the watcher). */
12
+ export declare const saveAuditSettings: (storageRoot: string, dataPath: string, settings: AuditSettings) => void;
13
+ export declare const readAuditSettings: (storageRoot: string, dataPath: string) => AuditSettings | null;
14
+ export declare class AuditCollector {
15
+ private readonly dataPath;
16
+ private readonly settings;
17
+ private readonly log;
18
+ private pending;
19
+ private sessions;
20
+ private watchers;
21
+ constructor(dataPath: string, storageRoot: string, settings: AuditSettings);
22
+ private add;
23
+ /** Starts watching the projects' files (when audit.files). */
24
+ start(): void;
25
+ /** Programs the sandbox reported: taken in (checked) and deleted. */
26
+ private takeGuestReports;
27
+ private trackSessions;
28
+ guard(detail: string): void;
29
+ /** Writes what came in since the last call (repeats folded); call every few seconds. */
30
+ flush(): void;
31
+ stop(): void;
32
+ maintain(): void;
33
+ }
34
+ export {};
@@ -0,0 +1,126 @@
1
+ // What goes into the audit log, from the background watcher while the sandbox runs:
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;
4
+ // - files changed in the projects: watched from this PC (the sandbox cannot hide or fake these);
5
+ // - sessions starting and ending, and what the repo guard caught.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { z } from 'zod';
9
+ import { assertNoLinks } from '../../core/util.js';
10
+ import { listSessions, readValid, runPaths, writeJsonAtomic } from '../../shared/rundir.js';
11
+ import { AuditEvent, AuditLog, fold } from './log.js';
12
+ const MAX_FILES_PER_PASS = 200;
13
+ const MAX_FILE_BYTES = 8 * 1024 * 1024;
14
+ /** Paths in a project not worth a line each (git's own bookkeeping, dependency folders). */
15
+ const QUIET = /(^|[\\/])(\.git[\\/](objects|logs|refs|index\.lock|FETCH_HEAD|ORIG_HEAD)|node_modules[\\/]|target[\\/]|\.venv[\\/]|__pycache__[\\/])/i;
16
+ const AuditSettings = z.object({
17
+ enabled: z.boolean(), days: z.number().int().min(1).max(365), maxMb: z.number().int().min(5).max(5000), processes: z.boolean(), files: z.boolean(),
18
+ projects: z.array(z.string().max(1024)).max(64),
19
+ });
20
+ const settingsFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-audit.json');
21
+ /** Host-only: the audit settings of the running sandbox (written at launch, read by the watcher). */
22
+ export const saveAuditSettings = (storageRoot, dataPath, settings) => writeJsonAtomic(settingsFile(storageRoot, dataPath), settings);
23
+ export const readAuditSettings = (storageRoot, dataPath) => readValid(AuditSettings, settingsFile(storageRoot, dataPath));
24
+ export class AuditCollector {
25
+ dataPath;
26
+ settings;
27
+ log;
28
+ pending = [];
29
+ sessions = new Map();
30
+ watchers = [];
31
+ constructor(dataPath, storageRoot, settings) {
32
+ this.dataPath = dataPath;
33
+ this.settings = settings;
34
+ this.log = new AuditLog(AuditLog.dirFor(storageRoot, dataPath), settings);
35
+ }
36
+ add(e) { this.pending.push({ t: new Date().toISOString(), ...e }); }
37
+ /** Starts watching the projects' files (when audit.files). */
38
+ start() {
39
+ this.add({ k: 'note', p: 'sandbox started; audit: ' + [this.settings.processes && 'programs', this.settings.files && 'files', 'sessions'].filter(Boolean).join(', ') });
40
+ if (!this.settings.files)
41
+ return;
42
+ for (const project of this.settings.projects) {
43
+ try {
44
+ this.watchers.push(fs.watch(project, { recursive: true }, (event, file) => {
45
+ const rel = file ? String(file) : '';
46
+ if (!rel || QUIET.test(rel))
47
+ return;
48
+ this.add({ k: 'file', a: event === 'rename' ? 'rename' : 'change', p: rel.replace(/\\/g, '/').slice(0, 1024), w: project });
49
+ }));
50
+ }
51
+ catch { /* a project that is gone */ }
52
+ }
53
+ }
54
+ /** Programs the sandbox reported: taken in (checked) and deleted. */
55
+ takeGuestReports() {
56
+ if (!this.settings.processes)
57
+ return;
58
+ const dir = runPaths(this.dataPath).audit;
59
+ assertNoLinks(this.dataPath, dir);
60
+ let names = [];
61
+ try {
62
+ names = fs.readdirSync(dir).filter((n) => /^[\w-]+\.jsonl$/.test(n)).sort().slice(0, MAX_FILES_PER_PASS);
63
+ }
64
+ catch {
65
+ return;
66
+ }
67
+ for (const name of names) {
68
+ const file = path.join(dir, name);
69
+ try {
70
+ const stat = fs.lstatSync(file);
71
+ if (stat.isFile() && stat.size <= MAX_FILE_BYTES) {
72
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
73
+ if (!line.trim() || line.length > 8192)
74
+ continue;
75
+ let raw;
76
+ try {
77
+ raw = JSON.parse(line);
78
+ }
79
+ catch {
80
+ continue;
81
+ }
82
+ const e = AuditEvent.safeParse(raw);
83
+ if (e.success)
84
+ this.pending.push({ ...e.data, k: 'proc', w: undefined, a: undefined });
85
+ }
86
+ }
87
+ }
88
+ finally {
89
+ fs.rmSync(file, { force: true });
90
+ }
91
+ }
92
+ }
93
+ trackSessions() {
94
+ const now = new Map(listSessions(this.dataPath).map((s) => [s.id, s.phase]));
95
+ for (const s of listSessions(this.dataPath)) {
96
+ const before = this.sessions.get(s.id);
97
+ if (!before && s.phase === 'running')
98
+ this.add({ k: 'session', a: 'start', p: s.harness, w: s.cwd, c: s.id });
99
+ if (before === 'running' && (s.phase === 'exited' || s.phase === 'failed'))
100
+ this.add({ k: 'session', a: 'end', p: s.harness, w: s.cwd, c: s.id + (s.exitCode !== undefined ? ' exit ' + s.exitCode : '') });
101
+ }
102
+ this.sessions = now;
103
+ }
104
+ guard(detail) { this.add({ k: 'guard', p: detail.slice(0, 1024) }); }
105
+ /** Writes what came in since the last call (repeats folded); call every few seconds. */
106
+ flush() {
107
+ try {
108
+ this.takeGuestReports();
109
+ }
110
+ catch { /* next pass */ }
111
+ try {
112
+ this.trackSessions();
113
+ }
114
+ catch { /* next pass */ }
115
+ const events = this.pending.sort((a, b) => a.t.localeCompare(b.t));
116
+ this.pending = [];
117
+ this.log.append(fold(events));
118
+ }
119
+ stop() {
120
+ this.watchers.forEach((w) => w.close());
121
+ this.add({ k: 'note', p: 'sandbox closed' });
122
+ this.flush();
123
+ this.log.maintain();
124
+ }
125
+ maintain() { this.log.maintain(); }
126
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ /** One event. k: proc (a program started), file (changed in a project), session, guard (repo guard), note. */
3
+ export declare const AuditEvent: z.ZodObject<{
4
+ t: z.ZodString;
5
+ k: z.ZodEnum<{
6
+ file: "file";
7
+ session: "session";
8
+ proc: "proc";
9
+ guard: "guard";
10
+ note: "note";
11
+ }>;
12
+ p: z.ZodOptional<z.ZodString>;
13
+ c: z.ZodOptional<z.ZodString>;
14
+ pp: z.ZodOptional<z.ZodString>;
15
+ a: z.ZodOptional<z.ZodString>;
16
+ w: z.ZodOptional<z.ZodString>;
17
+ n: z.ZodOptional<z.ZodNumber>;
18
+ }, z.core.$strip>;
19
+ export type AuditEvent = z.output<typeof AuditEvent>;
20
+ export declare class AuditLog {
21
+ readonly dir: string;
22
+ private readonly limits;
23
+ constructor(dir: string, limits: {
24
+ days: number;
25
+ maxMb: number;
26
+ });
27
+ /** <storage>\audit\<data set name>. */
28
+ static dirFor(storageRoot: string, dataPath: string): string;
29
+ private dayFile;
30
+ append(events: AuditEvent[]): void;
31
+ /** Compresses finished days, then deletes days past the limits. Cheap when there is nothing to do. */
32
+ maintain(today?: string): void;
33
+ /** Events of the last `days` days (newest last), optionally of some kinds and containing a text. */
34
+ read(o?: {
35
+ days?: number;
36
+ kinds?: AuditEvent['k'][];
37
+ grep?: string;
38
+ }): AuditEvent[];
39
+ /** Total size on disk. */
40
+ bytes(): number;
41
+ }
42
+ /**
43
+ * Folds repeats: the same event (kind, program/path, command line, action, project) within one minute becomes one line
44
+ * with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
45
+ */
46
+ export declare const fold: (events: AuditEvent[], cap?: number) => AuditEvent[];
@@ -0,0 +1,143 @@
1
+ // The audit log (audit.enabled): one file per day of JSON lines in host-only storage (<storage>\audit\<data set>),
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.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import zlib from 'node:zlib';
7
+ import { z } from 'zod';
8
+ /** One event. k: proc (a program started), file (changed in a project), session, guard (repo guard), note. */
9
+ export const AuditEvent = z.object({
10
+ /** ISO time. */
11
+ t: z.string().max(40),
12
+ k: z.enum(['proc', 'file', 'session', 'guard', 'note']),
13
+ /** proc: program; file: path in the project; session: agent; guard/note: what happened. */
14
+ p: z.string().max(1024).optional(),
15
+ /** proc: its command line (capped). */
16
+ c: z.string().max(2048).optional(),
17
+ /** proc: the program that started it. */
18
+ pp: z.string().max(1024).optional(),
19
+ /** file: change | rename; session: start | end. */
20
+ a: z.string().max(32).optional(),
21
+ /** project folder (file, session). */
22
+ w: z.string().max(1024).optional(),
23
+ /** how many times it happened in this line's minute (repeats are counted, not repeated). */
24
+ n: z.number().int().min(1).max(1e9).optional(),
25
+ });
26
+ const DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl(\.gz)?$/;
27
+ export class AuditLog {
28
+ dir;
29
+ limits;
30
+ constructor(dir, limits) {
31
+ this.dir = dir;
32
+ this.limits = limits;
33
+ }
34
+ /** <storage>\audit\<data set name>. */
35
+ static dirFor(storageRoot, dataPath) { return path.join(storageRoot, 'audit', path.basename(dataPath)); }
36
+ dayFile(day, gz = false) { return path.join(this.dir, day + '.jsonl' + (gz ? '.gz' : '')); }
37
+ append(events) {
38
+ if (!events.length)
39
+ return;
40
+ fs.mkdirSync(this.dir, { recursive: true });
41
+ const byDay = new Map();
42
+ for (const e of events) {
43
+ const day = e.t.slice(0, 10);
44
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
45
+ continue;
46
+ byDay.set(day, [...(byDay.get(day) ?? []), JSON.stringify(e)]);
47
+ }
48
+ for (const [day, lines] of byDay)
49
+ fs.appendFileSync(this.dayFile(day), lines.join('\n') + '\n');
50
+ }
51
+ /** Compresses finished days, then deletes days past the limits. Cheap when there is nothing to do. */
52
+ maintain(today = new Date().toISOString().slice(0, 10)) {
53
+ if (!fs.existsSync(this.dir))
54
+ return;
55
+ for (const name of fs.readdirSync(this.dir)) {
56
+ const m = DAY_FILE.exec(name);
57
+ if (!m || m[2] || m[1] >= today)
58
+ continue;
59
+ const file = path.join(this.dir, name);
60
+ fs.writeFileSync(file + '.gz', zlib.gzipSync(fs.readFileSync(file)));
61
+ fs.rmSync(file);
62
+ }
63
+ const oldest = new Date(Date.parse(today) - (this.limits.days - 1) * 86400000).toISOString().slice(0, 10);
64
+ const files = fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n)).sort();
65
+ for (const name of files.filter((n) => DAY_FILE.exec(n)[1] < oldest))
66
+ fs.rmSync(path.join(this.dir, name), { force: true });
67
+ let left = fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n)).sort();
68
+ const size = () => left.reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
69
+ while (left.length > 1 && size() > this.limits.maxMb * 1024 * 1024) {
70
+ fs.rmSync(path.join(this.dir, left[0]), { force: true });
71
+ left = left.slice(1);
72
+ }
73
+ }
74
+ /** Events of the last `days` days (newest last), optionally of some kinds and containing a text. */
75
+ read(o = {}) {
76
+ if (!fs.existsSync(this.dir))
77
+ return [];
78
+ const since = new Date(Date.now() - ((o.days ?? 1) - 1) * 86400000).toISOString().slice(0, 10);
79
+ const needle = o.grep?.toLowerCase();
80
+ const events = [];
81
+ for (const name of fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n) && DAY_FILE.exec(n)[1] >= since).sort()) {
82
+ const raw = fs.readFileSync(path.join(this.dir, name));
83
+ const text = name.endsWith('.gz') ? zlib.gunzipSync(raw).toString('utf8') : raw.toString('utf8');
84
+ for (const line of text.split('\n')) {
85
+ if (!line.trim())
86
+ continue;
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(line);
90
+ }
91
+ catch {
92
+ continue;
93
+ }
94
+ const e = AuditEvent.safeParse(parsed);
95
+ if (!e.success)
96
+ continue;
97
+ if (o.kinds?.length && !o.kinds.includes(e.data.k))
98
+ continue;
99
+ if (needle && !line.toLowerCase().includes(needle))
100
+ continue;
101
+ events.push(e.data);
102
+ }
103
+ }
104
+ return events;
105
+ }
106
+ /** Total size on disk. */
107
+ bytes() {
108
+ if (!fs.existsSync(this.dir))
109
+ return 0;
110
+ return fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n)).reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
111
+ }
112
+ }
113
+ /**
114
+ * Folds repeats: the same event (kind, program/path, command line, action, project) within one minute becomes one line
115
+ * with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
116
+ */
117
+ export const fold = (events, cap = 300) => {
118
+ const out = [];
119
+ const index = new Map();
120
+ const perMinute = new Map();
121
+ const dropped = new Map();
122
+ for (const e of events) {
123
+ const minute = e.t.slice(0, 16);
124
+ const key = [minute, e.k, e.p, e.c, e.a, e.w].join('\0');
125
+ const seen = index.get(key);
126
+ if (seen) {
127
+ seen.n = (seen.n ?? 1) + (e.n ?? 1);
128
+ continue;
129
+ }
130
+ const count = (perMinute.get(minute) ?? 0) + 1;
131
+ perMinute.set(minute, count);
132
+ if (count > cap) {
133
+ dropped.set(minute, (dropped.get(minute) ?? 0) + (e.n ?? 1));
134
+ continue;
135
+ }
136
+ const copy = { ...e };
137
+ index.set(key, copy);
138
+ out.push(copy);
139
+ }
140
+ for (const [minute, n] of dropped)
141
+ out.push({ t: minute + ':59.999Z', k: 'note', p: n + ' more events in this minute were not listed (audit cap)' });
142
+ return out;
143
+ };
@@ -2,8 +2,8 @@ import type { GuestRequest, GuestResponse, Harness, SandboxStatus, SessionStatus
2
2
  import type { ProjectSpecT } from '../shared/schema.js';
3
3
  /** Where progress lines go: printed by default; a spinner passes its own. Text from the sandbox, made safe first. */
4
4
  export type Progress = (line: string) => void;
5
- /** Waits until the sandbox finished provisioning, reporting each boot step. */
6
- export declare const waitForReady: (dataPath: string, launchId: string | null, isAlive: () => boolean, progress?: Progress) => Promise<SandboxStatus>;
5
+ /** Waits until the sandbox finished provisioning, reporting each boot step (onStatus: every status read, validated). */
6
+ export declare const waitForReady: (dataPath: string, launchId: string | null, isAlive: () => boolean, progress?: Progress, onStatus?: (status: SandboxStatus) => void) => Promise<SandboxStatus>;
7
7
  export declare const sendRequest: (dataPath: string, request: GuestRequest) => void;
8
8
  export declare const waitForResponse: (dataPath: string, requestId: string, isAlive: () => boolean, timeoutMs: number) => Promise<GuestResponse>;
9
9
  /** project: a project registered after boot (provisioned first). harnessSpec: the agent's definition, for an agent the sandbox did not boot with. */
@@ -22,13 +22,14 @@ const until = async (timeoutMs, isAlive, poll, timeoutMessage) => {
22
22
  fail(timeoutMessage, 'Check what the sandbox is doing with sandbox logs -f (or sbx-show to see its window).');
23
23
  };
24
24
  const printLine = (line) => console.log(line);
25
- /** Waits until the sandbox finished provisioning, reporting each boot step. */
26
- export const waitForReady = (dataPath, launchId, isAlive, progress = (m) => printLine(' [sandbox] ' + m)) => {
25
+ /** Waits until the sandbox finished provisioning, reporting each boot step (onStatus: every status read, validated). */
26
+ export const waitForReady = (dataPath, launchId, isAlive, progress = (m) => printLine(' [sandbox] ' + m), onStatus) => {
27
27
  let last = '';
28
28
  return until(BOOT_TIMEOUT_MS, isAlive, () => {
29
29
  const status = readSandboxStatus(dataPath);
30
30
  if (!status || (launchId !== null && status.launchId !== launchId))
31
31
  return null;
32
+ onStatus?.(status);
32
33
  if (status.message !== last) {
33
34
  progress(safeText(status.message).slice(0, 300));
34
35
  last = status.message;
@@ -2,6 +2,12 @@ import type { GenerateOptions, Generated } from '../core/types.js';
2
2
  import { decideToolchains } from '../core/detect-toolchains.js';
3
3
  /** Which toolchains the project needs ("auto" settings follow the project's files and what this PC has). */
4
4
  export declare const decideProjectToolchains: (o: Pick<GenerateOptions, "run" | "pcLanguages">) => ReturnType<typeof decideToolchains>;
5
- export declare const generateSandbox: (o: GenerateOptions) => Generated & {
5
+ /**
6
+ * env.cmd, which logon.cmd calls before it opens Windows Terminal: Terminal expands "%AGENT_WORKSPACE%" (the starting
7
+ * folder of new tabs) from its own environment, and the runtime only sets the variable machine-wide after Terminal
8
+ * started. `%` is doubled for cmd; a Windows path holds no quotes.
9
+ */
10
+ export declare const logonEnv: (workspace: string) => string;
11
+ export declare const generateSandbox: (given: GenerateOptions) => Generated & {
6
12
  personalization: string;
7
13
  };