windows-development-sandbox 0.0.0-stage → 1.0.5

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 (231) hide show
  1. package/.sandbox-config.example.json +14 -0
  2. package/README.md +78 -2
  3. package/config.example.json +332 -0
  4. package/dist/api.d.ts +31 -0
  5. package/dist/api.js +30 -0
  6. package/dist/bin/agent-sandbox.d.ts +2 -0
  7. package/dist/bin/agent-sandbox.js +3 -0
  8. package/dist/bin/claude-host.d.ts +2 -0
  9. package/dist/bin/claude-host.js +5 -0
  10. package/dist/bin/claude-sbx.d.ts +2 -0
  11. package/dist/bin/claude-sbx.js +3 -0
  12. package/dist/bin/codex-sbx.d.ts +2 -0
  13. package/dist/bin/codex-sbx.js +3 -0
  14. package/dist/bin/gemini-sbx.d.ts +2 -0
  15. package/dist/bin/gemini-sbx.js +3 -0
  16. package/dist/bin/sandbox.d.ts +2 -0
  17. package/dist/bin/sandbox.js +4 -0
  18. package/dist/bin/sbx-hide.d.ts +2 -0
  19. package/dist/bin/sbx-hide.js +3 -0
  20. package/dist/bin/sbx-show.d.ts +2 -0
  21. package/dist/bin/sbx-show.js +3 -0
  22. package/dist/cli/args.d.ts +32 -0
  23. package/dist/cli/args.js +111 -0
  24. package/dist/cli/commands/accounts.d.ts +2 -0
  25. package/dist/cli/commands/accounts.js +52 -0
  26. package/dist/cli/commands/context.d.ts +21 -0
  27. package/dist/cli/commands/context.js +54 -0
  28. package/dist/cli/commands/data.d.ts +46 -0
  29. package/dist/cli/commands/data.js +249 -0
  30. package/dist/cli/commands/index.d.ts +8 -0
  31. package/dist/cli/commands/index.js +8 -0
  32. package/dist/cli/commands/launch.d.ts +5 -0
  33. package/dist/cli/commands/launch.js +152 -0
  34. package/dist/cli/commands/maintenance.d.ts +8 -0
  35. package/dist/cli/commands/maintenance.js +125 -0
  36. package/dist/cli/commands/repair.d.ts +59 -0
  37. package/dist/cli/commands/repair.js +332 -0
  38. package/dist/cli/commands/sessions.d.ts +11 -0
  39. package/dist/cli/commands/sessions.js +113 -0
  40. package/dist/cli/commands/setup.d.ts +45 -0
  41. package/dist/cli/commands/setup.js +152 -0
  42. package/dist/cli/index.d.ts +14 -0
  43. package/dist/cli/index.js +96 -0
  44. package/dist/cli/manifest.d.ts +95 -0
  45. package/dist/cli/manifest.js +87 -0
  46. package/dist/cli/menu.d.ts +22 -0
  47. package/dist/cli/menu.js +187 -0
  48. package/dist/cli/settings.d.ts +11 -0
  49. package/dist/cli/settings.js +156 -0
  50. package/dist/core/config.d.ts +17 -0
  51. package/dist/core/config.js +103 -0
  52. package/dist/core/detect-databases.d.ts +15 -0
  53. package/dist/core/detect-databases.js +147 -0
  54. package/dist/core/detect-pwsh.d.ts +18 -0
  55. package/dist/core/detect-pwsh.js +111 -0
  56. package/dist/core/detect-toolchains.d.ts +30 -0
  57. package/dist/core/detect-toolchains.js +58 -0
  58. package/dist/core/local-config.d.ts +12 -0
  59. package/dist/core/local-config.js +64 -0
  60. package/dist/core/schema.d.ts +604 -0
  61. package/dist/core/schema.js +264 -0
  62. package/dist/core/types.d.ts +162 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/core/util.d.ts +53 -0
  65. package/dist/core/util.js +145 -0
  66. package/dist/core/workspace.d.ts +23 -0
  67. package/dist/core/workspace.js +162 -0
  68. package/dist/guest/daemon.d.ts +1 -0
  69. package/dist/guest/daemon.js +186 -0
  70. package/dist/guest/db.d.ts +1 -0
  71. package/dist/guest/db.js +60 -0
  72. package/dist/guest/env.d.ts +49 -0
  73. package/dist/guest/env.js +99 -0
  74. package/dist/guest/gitguard.d.ts +7 -0
  75. package/dist/guest/gitguard.js +60 -0
  76. package/dist/guest/gitrules.d.ts +2 -0
  77. package/dist/guest/gitrules.js +11 -0
  78. package/dist/guest/guide.d.ts +12 -0
  79. package/dist/guest/guide.js +68 -0
  80. package/dist/guest/main.d.ts +1 -0
  81. package/dist/guest/main.js +120 -0
  82. package/dist/guest/ptyhost.d.ts +40 -0
  83. package/dist/guest/ptyhost.js +94 -0
  84. package/dist/guest/setup/agents.d.ts +8 -0
  85. package/dist/guest/setup/agents.js +73 -0
  86. package/dist/guest/setup/claude.d.ts +21 -0
  87. package/dist/guest/setup/claude.js +138 -0
  88. package/dist/guest/setup/environment.d.ts +6 -0
  89. package/dist/guest/setup/environment.js +65 -0
  90. package/dist/guest/setup/hooks.d.ts +1 -0
  91. package/dist/guest/setup/hooks.js +36 -0
  92. package/dist/guest/setup/network.d.ts +10 -0
  93. package/dist/guest/setup/network.js +99 -0
  94. package/dist/guest/setup/policy.d.ts +3 -0
  95. package/dist/guest/setup/policy.js +18 -0
  96. package/dist/guest/setup/user.d.ts +7 -0
  97. package/dist/guest/setup/user.js +47 -0
  98. package/dist/guest/toolchains/binstall.d.ts +7 -0
  99. package/dist/guest/toolchains/binstall.js +55 -0
  100. package/dist/guest/toolchains/catalogue.d.ts +51 -0
  101. package/dist/guest/toolchains/catalogue.js +31 -0
  102. package/dist/guest/toolchains/cli.d.ts +10 -0
  103. package/dist/guest/toolchains/cli.js +64 -0
  104. package/dist/guest/toolchains/common.d.ts +37 -0
  105. package/dist/guest/toolchains/common.js +124 -0
  106. package/dist/guest/toolchains/databases.d.ts +7 -0
  107. package/dist/guest/toolchains/databases.js +226 -0
  108. package/dist/guest/toolchains/git.d.ts +3 -0
  109. package/dist/guest/toolchains/git.js +112 -0
  110. package/dist/guest/toolchains/go.d.ts +3 -0
  111. package/dist/guest/toolchains/go.js +33 -0
  112. package/dist/guest/toolchains/index.d.ts +13 -0
  113. package/dist/guest/toolchains/index.js +122 -0
  114. package/dist/guest/toolchains/java.d.ts +8 -0
  115. package/dist/guest/toolchains/java.js +96 -0
  116. package/dist/guest/toolchains/managers.d.ts +16 -0
  117. package/dist/guest/toolchains/managers.js +82 -0
  118. package/dist/guest/toolchains/msvc.d.ts +10 -0
  119. package/dist/guest/toolchains/msvc.js +216 -0
  120. package/dist/guest/toolchains/native.d.ts +12 -0
  121. package/dist/guest/toolchains/native.js +220 -0
  122. package/dist/guest/toolchains/node.d.ts +3 -0
  123. package/dist/guest/toolchains/node.js +132 -0
  124. package/dist/guest/toolchains/pwsh.d.ts +4 -0
  125. package/dist/guest/toolchains/pwsh.js +14 -0
  126. package/dist/guest/toolchains/python.d.ts +3 -0
  127. package/dist/guest/toolchains/python.js +47 -0
  128. package/dist/guest/toolchains/runtimes.d.ts +17 -0
  129. package/dist/guest/toolchains/runtimes.js +40 -0
  130. package/dist/guest/toolchains/scripting.d.ts +10 -0
  131. package/dist/guest/toolchains/scripting.js +137 -0
  132. package/dist/guest/toolchains/versions.d.ts +47 -0
  133. package/dist/guest/toolchains/versions.js +77 -0
  134. package/dist/guest/toolchains/vscode.d.ts +2 -0
  135. package/dist/guest/toolchains/vscode.js +53 -0
  136. package/dist/guest/view.d.ts +1 -0
  137. package/dist/guest/view.js +22 -0
  138. package/dist/host/accounts.d.ts +32 -0
  139. package/dist/host/accounts.js +119 -0
  140. package/dist/host/channel.d.ts +37 -0
  141. package/dist/host/channel.js +180 -0
  142. package/dist/host/claude-profile.d.ts +15 -0
  143. package/dist/host/claude-profile.js +103 -0
  144. package/dist/host/cleanup.d.ts +23 -0
  145. package/dist/host/cleanup.js +66 -0
  146. package/dist/host/generate.d.ts +15 -0
  147. package/dist/host/generate.js +185 -0
  148. package/dist/host/personalize.d.ts +10 -0
  149. package/dist/host/personalize.js +97 -0
  150. package/dist/host/platform.d.ts +17 -0
  151. package/dist/host/platform.js +114 -0
  152. package/dist/host/project-scan.d.ts +23 -0
  153. package/dist/host/project-scan.js +173 -0
  154. package/dist/host/repo-guard.d.ts +23 -0
  155. package/dist/host/repo-guard.js +128 -0
  156. package/dist/host/secrets.d.ts +14 -0
  157. package/dist/host/secrets.js +133 -0
  158. package/dist/host/terminal.d.ts +32 -0
  159. package/dist/host/terminal.js +127 -0
  160. package/dist/host/window.d.ts +4 -0
  161. package/dist/host/window.js +68 -0
  162. package/dist/runtime/daemon.js +20291 -0
  163. package/dist/runtime/db.js +20423 -0
  164. package/dist/runtime/gitguard.js +84 -0
  165. package/dist/runtime/main.js +22398 -0
  166. package/dist/runtime/view.js +19966 -0
  167. package/dist/selftest/cli.test.d.ts +2 -0
  168. package/dist/selftest/cli.test.js +30 -0
  169. package/dist/selftest/config.test.d.ts +3 -0
  170. package/dist/selftest/config.test.js +74 -0
  171. package/dist/selftest/data.test.d.ts +2 -0
  172. package/dist/selftest/data.test.js +73 -0
  173. package/dist/selftest/detect.test.d.ts +2 -0
  174. package/dist/selftest/detect.test.js +42 -0
  175. package/dist/selftest/fixtures.d.ts +232 -0
  176. package/dist/selftest/fixtures.js +63 -0
  177. package/dist/selftest/guest.test.d.ts +3 -0
  178. package/dist/selftest/guest.test.js +61 -0
  179. package/dist/selftest/harness.d.ts +14 -0
  180. package/dist/selftest/harness.js +36 -0
  181. package/dist/selftest/helpers.test.d.ts +2 -0
  182. package/dist/selftest/helpers.test.js +125 -0
  183. package/dist/selftest/host.test.d.ts +2 -0
  184. package/dist/selftest/host.test.js +71 -0
  185. package/dist/selftest/index.d.ts +1 -0
  186. package/dist/selftest/index.js +56 -0
  187. package/dist/selftest/live.test.d.ts +7 -0
  188. package/dist/selftest/live.test.js +127 -0
  189. package/dist/selftest/mirror.test.d.ts +3 -0
  190. package/dist/selftest/mirror.test.js +74 -0
  191. package/dist/selftest/security.test.d.ts +3 -0
  192. package/dist/selftest/security.test.js +143 -0
  193. package/dist/selftest/setup.test.d.ts +2 -0
  194. package/dist/selftest/setup.test.js +23 -0
  195. package/dist/selftest/sync.test.d.ts +3 -0
  196. package/dist/selftest/sync.test.js +82 -0
  197. package/dist/shared/download.d.ts +58 -0
  198. package/dist/shared/download.js +200 -0
  199. package/dist/shared/guest-paths.d.ts +30 -0
  200. package/dist/shared/guest-paths.js +61 -0
  201. package/dist/shared/netrules.d.ts +18 -0
  202. package/dist/shared/netrules.js +98 -0
  203. package/dist/shared/paths.d.ts +9 -0
  204. package/dist/shared/paths.js +22 -0
  205. package/dist/shared/pty-client.d.ts +86 -0
  206. package/dist/shared/pty-client.js +227 -0
  207. package/dist/shared/rundir.d.ts +30 -0
  208. package/dist/shared/rundir.js +86 -0
  209. package/dist/shared/safe-remove.d.ts +10 -0
  210. package/dist/shared/safe-remove.js +55 -0
  211. package/dist/shared/schema.d.ts +84 -0
  212. package/dist/shared/schema.js +51 -0
  213. package/dist/shared/system.d.ts +15 -0
  214. package/dist/shared/system.js +19 -0
  215. package/dist/sync/claude-mem.d.ts +8 -0
  216. package/dist/sync/claude-mem.js +77 -0
  217. package/dist/sync/history.d.ts +30 -0
  218. package/dist/sync/history.js +129 -0
  219. package/dist/sync/watcher.d.ts +13 -0
  220. package/dist/sync/watcher.js +116 -0
  221. package/hooks/post-agent/README.txt +1 -0
  222. package/hooks/pre-provision/README.txt +1 -0
  223. package/package.json +91 -4
  224. package/sandbox/bootstrap.ps1 +258 -0
  225. package/sandbox/gitshim.cs +35 -0
  226. package/sandbox/logon.cmd +16 -0
  227. package/sandbox/ptyhost.cs +603 -0
  228. package/sandbox/trust.cmd +35 -0
  229. package/sandbox/vswhere.cs +83 -0
  230. package/schemas/config.schema.json +1062 -0
  231. package/schemas/sandbox-config.schema.json +952 -0
@@ -0,0 +1,133 @@
1
+ // Keeps credentials on this PC. Everything copied into the sandbox from your Claude settings (settings.json "env",
2
+ // MCP servers' env / headers / args in .claude.json) goes through here first: cloud and production credentials are
3
+ // removed unless you let them through with secrets.allow. Pure functions, so they are easy to test.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ /** Variable names that are credentials by definition: cloud providers, hosting, CI, registries, payment, AI APIs, ... */
7
+ const SECRET_NAMES = [
8
+ /^AWS_/, /^AMAZON_/, /^AZURE_/, /^ARM_(CLIENT|TENANT|SUBSCRIPTION|ACCESS)_/, /^MSAL_/, /^CLOUDFLARE_/, /^CF_(API|ACCOUNT|ZONE|ACCESS)/,
9
+ /^GOOGLE_(APPLICATION_CREDENTIALS|API_KEY|CLOUD_|CLIENT_SECRET)/, /^GCP_/, /^GCLOUD_/, /^GOOGLE_GHA_/, /^FIREBASE_/,
10
+ /^DIGITALOCEAN_/, /^DO_(API|AUTH)_/, /^LINODE_/, /^HCLOUD_/, /^OCI_/, /^ALIBABA_CLOUD_/, /^ALICLOUD_/, /^IBMCLOUD_/,
11
+ /^HEROKU_/, /^VERCEL_/, /^NETLIFY_/, /^RENDER_API/, /^FLY_(API|ACCESS)_/, /^RAILWAY_/, /^SUPABASE_/, /^NEON_/, /^PLANETSCALE_/,
12
+ /^GITHUB_TOKEN$/, /^GH_TOKEN$/, /^GH_ENTERPRISE_TOKEN$/, /^GITHUB_PAT/, /^GITLAB_/, /^BITBUCKET_/, /^CIRCLE_TOKEN$/, /^TRAVIS_/, /^BUILDKITE_/,
13
+ /^NPM_TOKEN$/, /^NODE_AUTH_TOKEN$/, /^YARN_NPM_AUTH/, /^NUGET_(API_KEY|AUTH)/, /^PYPI_/, /^TWINE_/, /^CARGO_REGISTRY_TOKEN$/, /^GEM_HOST_API_KEY$/,
14
+ /^DOCKER_(PASSWORD|TOKEN|AUTH)/, /^REGISTRY_(PASSWORD|TOKEN)/, /^KUBECONFIG$/, /^KUBE_TOKEN$/, /^HELM_/, /^VAULT_/, /^CONSUL_HTTP_TOKEN$/, /^NOMAD_TOKEN$/,
15
+ /^TF_VAR_/, /^TF_TOKEN_/, /^TFE_TOKEN$/, /^PULUMI_ACCESS_TOKEN$/, /^SPACELIFT_/, /^ANSIBLE_VAULT/,
16
+ /^STRIPE_/, /^PAYPAL_/, /^BRAINTREE_/, /^SQUARE_/, /^ADYEN_/, /^TWILIO_/, /^SENDGRID_/, /^MAILGUN_/, /^POSTMARK_/, /^MAILCHIMP_/, /^RESEND_/,
17
+ /^SLACK_/, /^DISCORD_(TOKEN|BOT|WEBHOOK)/, /^TELEGRAM_BOT/, /^SENTRY_(AUTH|DSN)/, /^DATADOG_/, /^DD_(API|APP)_KEY$/, /^NEW_RELIC_/, /^HONEYCOMB_/,
18
+ /^OPENAI_/, /^ANTHROPIC_(API_KEY|AUTH_TOKEN)$/, /^CLAUDE_CODE_OAUTH_TOKEN$/, /^GEMINI_API_KEY$/, /^MISTRAL_API_KEY$/, /^COHERE_API_KEY$/,
19
+ /^GROQ_API_KEY$/, /^HF_TOKEN$/, /^HUGGING_?FACE_/, /^REPLICATE_API/, /^PINECONE_/, /^ELEVENLABS_/,
20
+ /^DATABASE_URL$/, /^.*_DATABASE_URL$/, /^(PG|POSTGRES|MYSQL|MARIADB|MSSQL|MONGO(DB)?|REDIS|ELASTIC(SEARCH)?|RABBITMQ|SNOWFLAKE|DATABRICKS)_/,
21
+ /^SSH_AUTH_SOCK$/, /^GPG_/, /^OP_(SERVICE_ACCOUNT|CONNECT)_/, /^BWS_ACCESS_TOKEN$/, /^DOPPLER_/, /^INFISICAL_/,
22
+ ];
23
+ /** Any name that says what it holds: ..._TOKEN, ..._SECRET, PASSWORD, API_KEY, ..._PRIVATE_KEY, connection strings, ... */
24
+ const SECRET_WORDS = /(^|_)(PROD|PRODUCTION|LIVE|SECRETS?|TOKENS?|PASSWORDS?|PASSWD|PWD|PASS|API_?KEYS?|ACCESS_?KEYS?|PRIVATE_?KEYS?|SIGNING_?KEYS?|CREDENTIALS?|CREDS|AUTH|AUTHORIZATION|BEARER|COOKIES?|SESSION_?KEYS?|CONN(ECTION)?_?STR(ING)?|DSN|WEBHOOK(_URL)?|CLIENT_?SECRET|SAS|SALT|PAT)(_|$)/;
25
+ /** Values that are credentials whatever they are called. */
26
+ const SECRET_VALUES = [
27
+ /\b(AKIA|ASIA|AGPA|AIDA|AROA)[0-9A-Z]{16}\b/, // AWS access key ids
28
+ /\bgh[pousr]_[A-Za-z0-9]{30,}/, /\bgithub_pat_[A-Za-z0-9_]{40,}/, /\bglpat-[A-Za-z0-9_-]{20,}/,
29
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/, /\bsk-(ant-|proj-|live_|test_)?[A-Za-z0-9_-]{20,}/, /\b[rs]k_live_[A-Za-z0-9]{20,}/,
30
+ /\bAIza[0-9A-Za-z_-]{35}\b/, /\bya29\.[0-9A-Za-z_-]{20,}/, /\bnpm_[A-Za-z0-9]{36}\b/, /\bhf_[A-Za-z0-9]{30,}/,
31
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /AccountKey=[A-Za-z0-9+/=]{20,}/i, /SharedAccessSignature=|[?&]sig=[A-Za-z0-9%+/=]{20,}/i,
32
+ /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s@/]+@/i, // user:password@host in any URL (database URLs, registries)
33
+ /\bBearer\s+\S{16,}/i, /\bBasic\s+[A-Za-z0-9+/=]{12,}/i,
34
+ /[?&](api[_-]?key|apikey|access[_-]?token|token|key|secret|password|sig|signature|auth)=[^&\s]{8,}/i, // credentials in a URL
35
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, // JWTs
36
+ ];
37
+ /** "AWS_REGION", "MY_*", "*_URL": * matches anything. Case-insensitive, like Windows variable names. */
38
+ const globMatch = (pattern, name) => new RegExp('^' + pattern.split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$', 'i').test(name);
39
+ export const isSecretName = (name, secrets) => {
40
+ // X-API-Key, x-auth-token, api.key: compared as X_API_KEY, X_AUTH_TOKEN, API_KEY.
41
+ const upper = name.toUpperCase().replace(/[-\s.]/g, '_');
42
+ return SECRET_NAMES.some((r) => r.test(upper)) || SECRET_WORDS.test(upper) || secrets.patterns.some((p) => globMatch(p, name));
43
+ };
44
+ export const isSecretValue = (value) => SECRET_VALUES.some((r) => r.test(value));
45
+ /** Whether a variable (name and value) may go into the sandbox. secrets.allow always wins. */
46
+ export const mayPass = (name, value, secrets) => {
47
+ if (!secrets.strip || secrets.allow.some((p) => globMatch(p, name)))
48
+ return true;
49
+ return !isSecretName(name, secrets) && !(typeof value === 'string' && isSecretValue(value));
50
+ };
51
+ /** A copy of an env/headers map without the secrets; the names removed are added to `removed` (with `where`). */
52
+ export const stripVariables = (vars, secrets, where, removed) => {
53
+ if (typeof vars !== 'object' || vars === null || Array.isArray(vars))
54
+ return vars;
55
+ return Object.fromEntries(Object.entries(vars).filter(([name, value]) => {
56
+ const ok = mayPass(name, value, secrets);
57
+ if (!ok)
58
+ removed.push(where + name);
59
+ return ok;
60
+ }));
61
+ };
62
+ /** Claude settings keys that run commands to fetch credentials on this PC (they cannot work in the sandbox anyway). */
63
+ const CREDENTIAL_HELPERS = ['apiKeyHelper', 'awsAuthRefresh', 'awsCredentialExport', 'otelHeadersHelper'];
64
+ /** settings.json for the sandbox: secrets out of "env", credential helper commands removed. */
65
+ export const stripSettings = (settings, secrets, removed) => {
66
+ if (!secrets.strip)
67
+ return settings;
68
+ const out = { ...settings };
69
+ for (const key of CREDENTIAL_HELPERS.filter((k) => k in out)) {
70
+ delete out[key];
71
+ removed.push('settings.json ' + key);
72
+ }
73
+ if ('env' in out)
74
+ out.env = stripVariables(out.env, secrets, 'settings.json env.', removed);
75
+ return out;
76
+ };
77
+ /** One MCP server: secret env vars and headers removed, and secret-looking command-line arguments blanked. */
78
+ const stripServer = (server, secrets, where, removed) => {
79
+ if (typeof server !== 'object' || server === null)
80
+ return server;
81
+ const out = { ...server };
82
+ if ('env' in out)
83
+ out.env = stripVariables(out.env, secrets, where + ' env.', removed);
84
+ if ('headers' in out)
85
+ out.headers = stripVariables(out.headers, secrets, where + ' header ', removed);
86
+ if (typeof out.url === 'string' && secrets.strip && isSecretValue(out.url)) {
87
+ removed.push(where + ' url (it carried a credential)');
88
+ out.url = out.url.replace(/([?&])[^=&#]+=[^&#]*/g, (m, sep) => (isSecretValue('?' + m.slice(1)) ? sep + m.slice(1).split('=')[0] + '=REMOVED' : m));
89
+ }
90
+ if (Array.isArray(out.args) && secrets.strip) {
91
+ out.args = out.args.map((arg, i) => {
92
+ const previous = i > 0 ? String(out.args[i - 1]) : '';
93
+ const [flag, inline] = typeof arg === 'string' && /^--?[\w-]+=/.test(arg) ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [null, null];
94
+ const secretFlag = (f) => /^--?[\w-]+$/.test(f) && isSecretName(f.replace(/^-+/, ''), secrets);
95
+ const flagged = typeof arg === 'string' && (isSecretValue(arg) || (flag !== null && (secretFlag(flag) || isSecretValue(inline))) || secretFlag(previous));
96
+ if (flagged)
97
+ removed.push(where + ' argument ' + (i + 1));
98
+ return flagged ? '[removed by agent-sandbox]' : arg;
99
+ });
100
+ }
101
+ return out;
102
+ };
103
+ /** Every MCP server in a .claude.json-shaped object (top level and per project). */
104
+ export const stripMcpServers = (state, secrets, removed) => {
105
+ if (!secrets.strip)
106
+ return state;
107
+ const servers = (owner, where) => {
108
+ const list = owner.mcpServers;
109
+ if (typeof list !== 'object' || list === null)
110
+ return owner;
111
+ return { ...owner, mcpServers: Object.fromEntries(Object.entries(list).map(([name, s]) => [name, stripServer(s, secrets, where + 'MCP ' + name, removed)])) };
112
+ };
113
+ const top = servers(state, '');
114
+ const projects = top.projects;
115
+ return projects ? { ...top, projects: Object.fromEntries(Object.entries(projects).map(([key, p]) => [key, servers(p, '')])) } : top;
116
+ };
117
+ /** Files in the workspace that usually hold secrets. The agent CAN read them (the whole folder is mapped); we only warn. */
118
+ const SECRET_FILES = /^(\.env(\..+)?|\.envrc|.*\.(pem|key|p12|pfx|jks|keystore|kdbx|tfvars|ovpn)|credentials(\.json)?|secrets?\.(json|ya?ml|toml)|service[-_]?account.*\.json|id_(rsa|ed25519|ecdsa)|\.npmrc|\.pypirc|\.netrc|\.git-credentials)$/i;
119
+ const TEMPLATE = /\.(example|sample|template|dist|defaults?)$/i;
120
+ /** Secret-looking files in the workspace root and one folder down (skipping dependency folders). */
121
+ export const findSecretFiles = (workspace) => {
122
+ const list = (dir) => { try {
123
+ return fs.readdirSync(dir, { withFileTypes: true });
124
+ }
125
+ catch {
126
+ return [];
127
+ } };
128
+ const hit = (name) => SECRET_FILES.test(name) && !TEMPLATE.test(name);
129
+ const top = list(workspace);
130
+ const below = top.filter((d) => d.isDirectory() && !/^(node_modules|\.git|vendor|target|dist|build|\.venv|venv)$/i.test(d.name))
131
+ .flatMap((d) => list(path.join(workspace, d.name)).filter((f) => f.isFile() && hit(f.name)).map((f) => d.name + '\\' + f.name));
132
+ return [...top.filter((d) => d.isFile() && hit(d.name)).map((d) => d.name), ...below];
133
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ export declare const terminalDir: (dataPath: string) => string;
3
+ export declare const terminalExe: (dataPath: string) => string;
4
+ /** Our own profile id, so the profile is found again (and user edits to it are kept) on later launches. */
5
+ export declare const PWSH_PROFILE_GUID = "{5f3a6c1e-7d2b-4e8a-9c41-0b6d2a8f7e93}";
6
+ declare const TerminalSettings: z.ZodObject<{
7
+ profiles: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodObject<{
8
+ guid: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$loose>>, z.ZodObject<{
10
+ list: z.ZodOptional<z.ZodArray<z.ZodObject<{
11
+ guid: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$loose>>>;
13
+ }, z.core.$loose>]>>;
14
+ }, z.core.$loose>;
15
+ /**
16
+ * settings.json with the "PowerShell 7" profile shown when `visible`, else hidden (never deleted: a user's tweaks to it
17
+ * survive turning pwsh off and on). Pure: returns null when nothing changes.
18
+ */
19
+ export declare const withPwshProfile: (settings: z.output<typeof TerminalSettings>, visible: boolean) => z.output<typeof TerminalSettings> | null;
20
+ /**
21
+ * Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
22
+ * not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
23
+ * shape-checked, only rewritten when the profile changes; a file that does not parse is left alone.
24
+ */
25
+ export declare const syncPwshProfile: (dataPath: string, allowed: boolean) => void;
26
+ export declare const ensureTerminal: (dataPath: string, refresh: boolean, options?: {
27
+ pwsh?: boolean;
28
+ }) => Promise<{
29
+ exe: string | null;
30
+ message: string;
31
+ }>;
32
+ export {};
@@ -0,0 +1,127 @@
1
+ // Windows Terminal for the sandbox window. The host downloads the portable build once into persistent storage, so
2
+ // logon.cmd can open it the moment the sandbox logs in (no PowerShell window first).
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { githubAsset, installArchive } from '../shared/download.js';
6
+ import { safeRemove } from '../shared/safe-remove.js';
7
+ import { z } from 'zod';
8
+ import { assertNoLinks, errMsg, fileExists, parseJsonc, writeJson } from '../core/util.js';
9
+ export const terminalDir = (dataPath) => path.join(dataPath, 'tools', 'terminal');
10
+ export const terminalExe = (dataPath) => path.join(terminalDir(dataPath), 'WindowsTerminal.exe');
11
+ /**
12
+ * Sandbox-friendly defaults; only written when there is no settings.json yet (first download), so edits made inside
13
+ * the sandbox stick, also across a refresh (which keeps the settings folder).
14
+ */
15
+ const SETTINGS = {
16
+ $schema: 'https://aka.ms/terminal-profiles-schema',
17
+ theme: 'dark',
18
+ firstWindowPreference: 'defaultProfile',
19
+ confirmCloseAllTabs: false,
20
+ // New tabs pick up the PATH and variables the runner writes machine-wide after the window opened.
21
+ compatibility: { reloadEnvironmentVariables: true },
22
+ warning: { confirmCloseAllTabs: false, largePaste: false, multiLinePaste: false },
23
+ profiles: { defaults: { font: { face: 'Cascadia Mono' }, startingDirectory: '%AGENT_WORKSPACE%', closeOnExit: 'graceful' } },
24
+ };
25
+ /** Our own profile id, so the profile is found again (and user edits to it are kept) on later launches. */
26
+ export const PWSH_PROFILE_GUID = '{5f3a6c1e-7d2b-4e8a-9c41-0b6d2a8f7e93}';
27
+ /** pwsh.exe as the sandbox sees it (toolchains/pwsh.ts installs it to tools\pwsh). */
28
+ const GUEST_PWSH = 'C:\\AgentData\\tools\\pwsh\\pwsh.exe';
29
+ const MAX_SETTINGS_BYTES = 1024 * 1024;
30
+ const TerminalSettings = z.looseObject({
31
+ profiles: z.union([z.array(z.looseObject({ guid: z.string().optional() })), z.looseObject({ list: z.array(z.looseObject({ guid: z.string().optional() })).optional() })]).optional(),
32
+ });
33
+ /**
34
+ * settings.json with the "PowerShell 7" profile shown when `visible`, else hidden (never deleted: a user's tweaks to it
35
+ * survive turning pwsh off and on). Pure: returns null when nothing changes.
36
+ */
37
+ export const withPwshProfile = (settings, visible) => {
38
+ const profiles = Array.isArray(settings.profiles) ? { list: settings.profiles } : { ...settings.profiles };
39
+ const list = [...(profiles.list ?? [])];
40
+ const at = list.findIndex((p) => p.guid?.toLowerCase() === PWSH_PROFILE_GUID);
41
+ if (at === -1 && !visible)
42
+ return null;
43
+ if (at === -1)
44
+ list.push({ guid: PWSH_PROFILE_GUID, name: 'PowerShell 7', commandline: '"' + GUEST_PWSH + '" -NoLogo', icon: GUEST_PWSH, hidden: false });
45
+ else if ((list[at].hidden === true) === !visible)
46
+ return null;
47
+ else
48
+ list[at] = { ...list[at], hidden: !visible };
49
+ return { ...settings, profiles: { ...profiles, list } };
50
+ };
51
+ /**
52
+ * Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
53
+ * not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
54
+ * shape-checked, only rewritten when the profile changes; a file that does not parse is left alone.
55
+ */
56
+ export const syncPwshProfile = (dataPath, allowed) => {
57
+ const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
58
+ assertNoLinks(dataPath, file);
59
+ if (!fileExists(file) || fs.statSync(file).size > MAX_SETTINGS_BYTES)
60
+ return;
61
+ const visible = allowed && fileExists(path.join(dataPath, 'tools', 'pwsh', 'pwsh.exe'));
62
+ let parsed;
63
+ try {
64
+ const result = TerminalSettings.safeParse(parseJsonc(fs.readFileSync(file, 'utf8'), file));
65
+ if (!result.success)
66
+ return;
67
+ parsed = result.data;
68
+ }
69
+ catch {
70
+ return;
71
+ }
72
+ const next = withPwshProfile(parsed, visible);
73
+ if (next)
74
+ writeJson(file, next);
75
+ };
76
+ export const ensureTerminal = async (dataPath, refresh, options = {}) => {
77
+ const result = await installTerminal(dataPath, refresh);
78
+ if (result.exe) {
79
+ // The profile is a nicety: a settings problem never costs the sandbox its terminal.
80
+ try {
81
+ syncPwshProfile(dataPath, options.pwsh ?? false);
82
+ }
83
+ catch (e) {
84
+ return { ...result, message: result.message + ' (PowerShell 7 profile not updated: ' + errMsg(e) + ')' };
85
+ }
86
+ }
87
+ return result;
88
+ };
89
+ const installTerminal = async (dataPath, refresh) => {
90
+ const exe = terminalExe(dataPath);
91
+ const dir = terminalDir(dataPath);
92
+ const settings = path.join(dir, 'settings');
93
+ // installArchive replaces tools\terminal: the portable settings folder waits here meanwhile (and goes back also when
94
+ // the download fails). One still parked from an interrupted earlier run is the one to keep.
95
+ const parked = path.join(dataPath, 'tools', 'terminal-settings.parked');
96
+ assertNoLinks(dataPath, settings);
97
+ assertNoLinks(dataPath, parked);
98
+ if (fileExists(exe) && !refresh)
99
+ return { exe, message: 'cached' };
100
+ try {
101
+ if (fs.existsSync(settings)) {
102
+ safeRemove(parked, path.dirname(parked));
103
+ fs.renameSync(settings, parked);
104
+ }
105
+ try {
106
+ const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
107
+ const asset = await githubAsset('microsoft/terminal', new RegExp('^Microsoft\\.WindowsTerminal_[\\d.]+_' + arch + '\\.zip$'));
108
+ await installArchive(asset.url, dir, { sha256: asset.sha256, locate: 'WindowsTerminal.exe', fileName: asset.name });
109
+ fs.writeFileSync(path.join(dir, '.portable'), '');
110
+ return { exe, message: 'downloaded ' + asset.name };
111
+ }
112
+ finally {
113
+ if (fs.existsSync(parked)) {
114
+ fs.mkdirSync(dir, { recursive: true });
115
+ safeRemove(settings, path.dirname(settings));
116
+ fs.renameSync(parked, settings);
117
+ }
118
+ const settingsFile = path.join(settings, 'settings.json');
119
+ assertNoLinks(dataPath, settingsFile);
120
+ if (fileExists(path.join(dir, '.portable')) && !fs.existsSync(settingsFile))
121
+ writeJson(settingsFile, SETTINGS);
122
+ }
123
+ }
124
+ catch (e) {
125
+ return { exe: null, message: 'download failed, the sandbox will use a plain PowerShell window (' + errMsg(e) + ')' };
126
+ }
127
+ };
@@ -0,0 +1,4 @@
1
+ /** Waits for the sandbox window and hides it; keeps watching briefly in case Windows re-shows it while connecting. */
2
+ export declare const hideSandboxWindow: (storageRoot: string, waitSeconds?: number) => Promise<boolean>;
3
+ /** Shows the sandbox window again (the ones we hid, or any hidden Windows Sandbox window). */
4
+ export declare const showSandboxWindow: (storageRoot: string) => boolean;
@@ -0,0 +1,68 @@
1
+ // Headless mode: Windows Sandbox always opens its client window, so we hide it (ShowWindow SW_HIDE) as soon as it
2
+ // appears. The VM and the logged-in session keep running; `sbx-show` brings the window back.
3
+ import path from 'node:path';
4
+ import { isWin, runPowerShell, runPowerShellAsync, tryReadJson, writeJson } from '../core/util.js';
5
+ const PRELUDE = `
6
+ $ErrorActionPreference = 'Stop'
7
+ Add-Type -Namespace AgentSandbox -Name Win -MemberDefinition @'
8
+ public delegate bool EnumProc(System.IntPtr hWnd, System.IntPtr lParam);
9
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, System.IntPtr lParam);
10
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint pid);
11
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool IsWindowVisible(System.IntPtr hWnd);
12
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool IsWindow(System.IntPtr hWnd);
13
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern System.IntPtr GetWindow(System.IntPtr hWnd, uint cmd);
14
+ [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)] public static extern int GetWindowText(System.IntPtr hWnd, System.Text.StringBuilder s, int n);
15
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool ShowWindow(System.IntPtr hWnd, int cmd);
16
+ [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool SetForegroundWindow(System.IntPtr hWnd);
17
+ '@
18
+ function Get-SandboxWindows([bool]$visibleOnly) {
19
+ $pids = @(Get-Process | Where-Object { $_.ProcessName -like 'WindowsSandbox*' } | ForEach-Object { [uint32]$_.Id })
20
+ $found = New-Object System.Collections.ArrayList
21
+ if ($pids.Count -eq 0) { return $found }
22
+ $cb = [AgentSandbox.Win+EnumProc]{ param($h, $l)
23
+ $p = [uint32]0; [void][AgentSandbox.Win]::GetWindowThreadProcessId($h, [ref]$p)
24
+ if ($pids -contains $p -and [AgentSandbox.Win]::GetWindow($h, 4) -eq [IntPtr]::Zero) {
25
+ $sb = New-Object System.Text.StringBuilder 256; [void][AgentSandbox.Win]::GetWindowText($h, $sb, 256)
26
+ $title = $sb.ToString()
27
+ if ($title -like '*Sandbox*' -and (-not $visibleOnly -or [AgentSandbox.Win]::IsWindowVisible($h))) { [void]$found.Add([int64]$h) }
28
+ }
29
+ return $true }
30
+ [void][AgentSandbox.Win]::EnumWindows($cb, [IntPtr]::Zero)
31
+ return $found
32
+ }
33
+ `;
34
+ const stateFile = (storageRoot) => path.join(storageRoot, 'run', 'hidden-windows.json');
35
+ /** Waits for the sandbox window and hides it; keeps watching briefly in case Windows re-shows it while connecting. */
36
+ export const hideSandboxWindow = async (storageRoot, waitSeconds = 180) => {
37
+ if (!isWin)
38
+ return false;
39
+ const script = PRELUDE + `
40
+ $hidden = New-Object System.Collections.ArrayList
41
+ $deadline = (Get-Date).AddSeconds(${waitSeconds}); $settle = $null
42
+ while ((Get-Date) -lt $deadline) {
43
+ foreach ($h in (Get-SandboxWindows $true)) { [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 0); if (-not $hidden.Contains($h)) { [void]$hidden.Add($h) } }
44
+ if ($hidden.Count -gt 0 -and $null -eq $settle) { $settle = (Get-Date).AddSeconds(15) }
45
+ if ($null -ne $settle -and (Get-Date) -gt $settle) { break }
46
+ Start-Sleep -Milliseconds 200
47
+ }
48
+ ConvertTo-Json -Compress @($hidden)`;
49
+ const stdout = await runPowerShellAsync(script);
50
+ const handles = JSON.parse(stdout.trim() || '[]').filter((h) => Number.isSafeInteger(h));
51
+ writeJson(stateFile(storageRoot), { handles });
52
+ return handles.length > 0;
53
+ };
54
+ /** Shows the sandbox window again (the ones we hid, or any hidden Windows Sandbox window). */
55
+ export const showSandboxWindow = (storageRoot) => {
56
+ if (!isWin)
57
+ return false;
58
+ // Only integers go into the script, whatever the file contains.
59
+ const recorded = (tryReadJson(stateFile(storageRoot))?.handles ?? []).filter((h) => Number.isSafeInteger(h));
60
+ const script = PRELUDE + `
61
+ $handles = @(${recorded.join(',')}) | Where-Object { [AgentSandbox.Win]::IsWindow([IntPtr]$_) }
62
+ if (-not $handles) { $handles = Get-SandboxWindows $false }
63
+ foreach ($h in $handles) { [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 5); [void][AgentSandbox.Win]::ShowWindow([IntPtr]$h, 9); [void][AgentSandbox.Win]::SetForegroundWindow([IntPtr]$h) }
64
+ @($handles).Count`;
65
+ const { stdout } = runPowerShell(script, true);
66
+ writeJson(stateFile(storageRoot), { handles: [] });
67
+ return Number(stdout.trim()) > 0;
68
+ };