@vellumai/assistant 0.3.19 → 0.3.21

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 (199) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -3,60 +3,149 @@ import { RiskLevel } from '../../permissions/types.js';
3
3
  import type { ToolDefinition } from '../../providers/types.js';
4
4
  import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
5
5
 
6
- const TOOL_NAME = 'voice_config_update';
6
+ /**
7
+ * Valid voice config settings and their UserDefaults key mappings.
8
+ */
9
+ const VOICE_SETTINGS = {
10
+ activation_key: { userDefaultsKey: 'pttActivationKey', type: 'string' as const },
11
+ wake_word_enabled: { userDefaultsKey: 'wakeWordEnabled', type: 'boolean' as const },
12
+ wake_word_keyword: { userDefaultsKey: 'wakeWordKeyword', type: 'string' as const },
13
+ wake_word_timeout: { userDefaultsKey: 'wakeWordTimeoutSeconds', type: 'number' as const },
14
+ } as const;
7
15
 
8
- export const voiceConfigUpdateTool: Tool = {
9
- name: TOOL_NAME,
10
- description:
11
- 'Change the push-to-talk activation key. Valid keys: fn (Fn/Globe key), ctrl (Control key), fn_shift (Fn+Shift), none (disable PTT).',
12
- category: 'system',
13
- defaultRiskLevel: RiskLevel.Low,
16
+ type VoiceSettingName = keyof typeof VOICE_SETTINGS;
17
+
18
+ const VALID_SETTINGS = Object.keys(VOICE_SETTINGS) as VoiceSettingName[];
19
+
20
+ const VALID_TIMEOUTS = [5, 10, 15, 30, 60];
21
+
22
+ function validateSetting(setting: string, value: unknown): { ok: true; coerced: string | boolean | number } | { ok: false; error: string } {
23
+ if (!VALID_SETTINGS.includes(setting as VoiceSettingName)) {
24
+ return { ok: false, error: `Unknown setting "${setting}". Valid settings: ${VALID_SETTINGS.join(', ')}` };
25
+ }
26
+
27
+ switch (setting) {
28
+ case 'activation_key': {
29
+ if (typeof value !== 'string' || value.length === 0) {
30
+ return { ok: false, error: 'activation_key must be a non-empty string' };
31
+ }
32
+ // Use the canonical normalizer from config-voice handler
33
+ const result = normalizeActivationKey(value);
34
+ if (!result.ok) {
35
+ return { ok: false, error: result.reason };
36
+ }
37
+ return { ok: true, coerced: result.value };
38
+ }
39
+ case 'wake_word_enabled': {
40
+ if (typeof value === 'boolean') return { ok: true, coerced: value };
41
+ if (value === 'true') return { ok: true, coerced: true };
42
+ if (value === 'false') return { ok: true, coerced: false };
43
+ return { ok: false, error: 'wake_word_enabled must be a boolean (or "true"/"false" string)' };
44
+ }
45
+ case 'wake_word_keyword': {
46
+ if (typeof value !== 'string' || value.trim().length === 0) {
47
+ return { ok: false, error: 'wake_word_keyword must be a non-empty string' };
48
+ }
49
+ return { ok: true, coerced: value.trim() };
50
+ }
51
+ case 'wake_word_timeout': {
52
+ const num = typeof value === 'number' ? value : Number(value);
53
+ if (Number.isNaN(num) || !VALID_TIMEOUTS.includes(num)) {
54
+ return { ok: false, error: `wake_word_timeout must be one of: ${VALID_TIMEOUTS.join(', ')}` };
55
+ }
56
+ return { ok: true, coerced: num };
57
+ }
58
+ default:
59
+ return { ok: false, error: `Unknown setting "${setting}"` };
60
+ }
61
+ }
62
+
63
+ const FRIENDLY_NAMES: Record<VoiceSettingName, string> = {
64
+ activation_key: 'PTT activation key',
65
+ wake_word_enabled: 'Wake word',
66
+ wake_word_keyword: 'Wake word keyword',
67
+ wake_word_timeout: 'Wake word timeout',
68
+ };
69
+
70
+ export class VoiceConfigUpdateTool implements Tool {
71
+ name = 'voice_config_update';
72
+ description =
73
+ 'Update a voice configuration setting (PTT activation key, wake word enabled/keyword/timeout). ' +
74
+ 'Changes take effect immediately via IPC broadcast to the desktop client.';
75
+ category = 'system';
76
+ defaultRiskLevel = RiskLevel.Low;
14
77
 
15
78
  getDefinition(): ToolDefinition {
16
79
  return {
17
- name: TOOL_NAME,
80
+ name: this.name,
18
81
  description: this.description,
19
82
  input_schema: {
20
83
  type: 'object',
21
84
  properties: {
85
+ setting: {
86
+ type: 'string',
87
+ enum: [...VALID_SETTINGS],
88
+ description: 'The voice setting to change',
89
+ },
90
+ value: {
91
+ description: 'The new value for the setting (type depends on setting)',
92
+ },
93
+ // Backward compat: legacy schema used activation_key directly
22
94
  activation_key: {
23
95
  type: 'string',
24
- description:
25
- 'The activation key to set. Accepts enum values (fn, ctrl, fn_shift, none) or natural language (e.g. "Control", "Fn+Shift", "Off").',
96
+ description: 'Deprecated — use setting: "activation_key" with value instead',
26
97
  },
27
98
  },
28
- required: ['activation_key'],
29
99
  },
30
100
  };
31
- },
32
-
33
- async execute(
34
- input: Record<string, unknown>,
35
- _context: ToolContext,
36
- ): Promise<ToolExecutionResult> {
37
- const rawKey = input.activation_key;
38
- if (typeof rawKey !== 'string' || rawKey.trim() === '') {
101
+ }
102
+
103
+ async execute(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
104
+ // Backward compat: if activation_key is provided without setting/value, treat as setting: "activation_key"
105
+ let setting = input.setting as string | undefined;
106
+ let value = input.value;
107
+
108
+ if (!setting && typeof input.activation_key === 'string') {
109
+ setting = 'activation_key';
110
+ value = input.activation_key;
111
+ }
112
+
113
+ if (!setting) {
114
+ return {
115
+ content: `Error: "setting" is required. Valid settings: ${VALID_SETTINGS.join(', ')}`,
116
+ isError: true,
117
+ };
118
+ }
119
+
120
+ if (value === undefined) {
39
121
  return {
40
- content: 'Error: activation_key is required and must be a non-empty string.',
122
+ content: `Error: "value" is required for setting "${setting}".`,
41
123
  isError: true,
42
124
  };
43
125
  }
44
126
 
45
- const result = normalizeActivationKey(rawKey);
46
- if (!result.ok) {
47
- return { content: result.reason, isError: true };
127
+ const validation = validateSetting(setting, value);
128
+ if (!validation.ok) {
129
+ return { content: `Error: ${validation.error}`, isError: true };
48
130
  }
49
131
 
50
- const labels: Record<string, string> = {
51
- fn: 'Fn/Globe key',
52
- ctrl: 'Control key',
53
- fn_shift: 'Fn+Shift',
54
- none: 'disabled',
55
- };
132
+ const meta = VOICE_SETTINGS[setting as VoiceSettingName];
133
+ const friendlyName = FRIENDLY_NAMES[setting as VoiceSettingName];
134
+
135
+ // Send client_settings_update IPC to write to UserDefaults
136
+ if (context.sendToClient) {
137
+ context.sendToClient({
138
+ type: 'client_settings_update',
139
+ key: meta.userDefaultsKey,
140
+ value: validation.coerced,
141
+ });
142
+ }
56
143
 
57
144
  return {
58
- content: `Push-to-talk activation key updated to ${labels[result.value]} (${result.value}).`,
145
+ content: `${friendlyName} updated to ${JSON.stringify(validation.coerced)}. The change has been broadcast to the desktop client.`,
59
146
  isError: false,
60
147
  };
61
- },
62
- };
148
+ }
149
+ }
150
+
151
+ export const voiceConfigUpdateTool = new VoiceConfigUpdateTool();
@@ -6,24 +6,31 @@ import { join } from 'node:path';
6
6
  import { ToolError } from '../../../util/errors.js';
7
7
  import { getLogger } from '../../../util/logger.js';
8
8
  import { isLinux,isMacOS } from '../../../util/platform.js';
9
- import type { SandboxBackend, SandboxResult } from './types.js';
9
+ import type { SandboxBackend, SandboxResult, WrapOptions } from './types.js';
10
10
 
11
11
  const log = getLogger('sandbox');
12
12
 
13
13
  const HASH_DISPLAY_LENGTH = 12;
14
14
 
15
15
  /**
16
- * macOS sandbox-exec profile that restricts shell commands:
16
+ * Build a macOS sandbox-exec SBPL profile.
17
+ *
18
+ * The profile restricts shell commands:
17
19
  * - Denies all by default
18
20
  * - Allows read access to most of the filesystem (needed for toolchains)
19
21
  * - Allows write access only to the working directory and temp dirs
20
- * - Blocks outbound network access
22
+ * - Blocks outbound network access (unless proxied)
21
23
  * - Blocks process debugging (ptrace)
22
24
  *
23
- * The WORKING_DIR placeholder is replaced at runtime with the actual
24
- * working directory path.
25
+ * When `allowNetwork` is true the `(deny network*)` rule is replaced with
26
+ * `(allow network*)` so the process can reach the local credential proxy.
25
27
  */
26
- const SANDBOX_PROFILE = `
28
+ function buildSandboxProfile(allowNetwork: boolean): string {
29
+ const networkRule = allowNetwork
30
+ ? ';; Allow network access (proxied mode — needed to reach the credential proxy)\n(allow network*)'
31
+ : ';; Block network access\n(deny network*)';
32
+
33
+ return `
27
34
  (version 1)
28
35
  (deny default)
29
36
 
@@ -54,12 +61,12 @@ const SANDBOX_PROFILE = `
54
61
  ;; Allow IOKit (needed for some system calls)
55
62
  (allow iokit-open)
56
63
 
57
- ;; Block network access
58
- (deny network*)
64
+ ${networkRule}
59
65
 
60
66
  ;; Block process debugging
61
67
  (deny process-info-pidinfo (target others))
62
68
  `.trim();
69
+ }
63
70
 
64
71
  /**
65
72
  * Escape a path for safe embedding inside an SBPL quoted string.
@@ -83,15 +90,18 @@ function escapeSBPL(path: string): string {
83
90
  * a hash of the path) to avoid race conditions when concurrent commands
84
91
  * use different working directories.
85
92
  */
86
- function getProfilePath(workingDir: string): string {
93
+ function getProfilePath(workingDir: string, allowNetwork: boolean): string {
87
94
  const dir = join(process.env.HOME ?? '/tmp', '.vellum');
88
95
  if (!existsSync(dir)) {
89
96
  mkdirSync(dir, { recursive: true });
90
97
  }
91
- const hash = createHash('sha256').update(workingDir).digest('hex').slice(0, HASH_DISPLAY_LENGTH);
98
+ // Include the network flag in the hash so proxied and non-proxied profiles
99
+ // for the same directory don't collide.
100
+ const hashInput = allowNetwork ? `${workingDir}:proxied` : workingDir;
101
+ const hash = createHash('sha256').update(hashInput).digest('hex').slice(0, HASH_DISPLAY_LENGTH);
92
102
  const path = join(dir, `sandbox-profile-${hash}.sb`);
93
103
 
94
- const profile = SANDBOX_PROFILE.replace(/__WORKING_DIR__/g, () => escapeSBPL(workingDir));
104
+ const profile = buildSandboxProfile(allowNetwork).replace(/__WORKING_DIR__/g, () => escapeSBPL(workingDir));
95
105
  writeFileSync(path, profile + '\n');
96
106
  return path;
97
107
  }
@@ -137,20 +147,29 @@ function isBwrapAvailable(): boolean {
137
147
  * - Network access blocked (--unshare-net)
138
148
  * - PID namespace isolated (--unshare-pid)
139
149
  */
140
- function buildBwrapArgs(workingDir: string, command: string): string[] {
141
- return [
150
+ function buildBwrapArgs(workingDir: string, command: string, allowNetwork: boolean): string[] {
151
+ const args = [
142
152
  // Filesystem: read-only root, writable working dir and temp
143
153
  '--ro-bind', '/', '/',
144
154
  '--bind', workingDir, workingDir,
145
155
  '--bind', '/tmp', '/tmp',
146
156
  '--dev', '/dev',
147
157
  '--proc', '/proc',
148
- // Isolation
149
- '--unshare-net',
158
+ ];
159
+
160
+ // Only isolate the network namespace when network access is not needed.
161
+ // In proxied mode the process must be able to reach 127.0.0.1:<proxy-port>.
162
+ if (!allowNetwork) {
163
+ args.push('--unshare-net');
164
+ }
165
+
166
+ args.push(
150
167
  '--unshare-pid',
151
168
  // Run bash inside the sandbox
152
169
  'bash', '-c', '--', command,
153
- ];
170
+ );
171
+
172
+ return args;
154
173
  }
155
174
 
156
175
  /**
@@ -158,9 +177,11 @@ function buildBwrapArgs(workingDir: string, command: string): string[] {
158
177
  * macOS sandbox-exec (SBPL profiles) and Linux bwrap (bubblewrap).
159
178
  */
160
179
  export class NativeBackend implements SandboxBackend {
161
- wrap(command: string, workingDir: string, _options?: import('./types.js').WrapOptions): SandboxResult {
180
+ wrap(command: string, workingDir: string, options?: WrapOptions): SandboxResult {
181
+ const allowNetwork = options?.networkMode === 'proxied';
182
+
162
183
  if (isMacOS()) {
163
- const profile = getProfilePath(workingDir);
184
+ const profile = getProfilePath(workingDir, allowNetwork);
164
185
  return {
165
186
  command: 'sandbox-exec',
166
187
  args: ['-f', profile, 'bash', '-c', '--', command],
@@ -176,7 +197,7 @@ export class NativeBackend implements SandboxBackend {
176
197
  }
177
198
  return {
178
199
  command: 'bwrap',
179
- args: buildBwrapArgs(workingDir, command),
200
+ args: buildBwrapArgs(workingDir, command, allowNetwork),
180
201
  sandboxed: true,
181
202
  };
182
203
  }
@@ -10,15 +10,15 @@ export interface SandboxResult {
10
10
  export interface WrapOptions {
11
11
  /**
12
12
  * Network mode for this invocation.
13
- * - 'off': no container network (--network=none). This is the default.
14
- * - 'proxied': bridge network so the container can reach a host proxy (--network=bridge).
13
+ * - 'off': network access is blocked (sandbox-exec deny network / bwrap --unshare-net). This is the default.
14
+ * - 'proxied': network access is allowed so the process can reach the local credential proxy.
15
15
  */
16
16
  networkMode?: 'off' | 'proxied';
17
17
  }
18
18
 
19
19
  /**
20
20
  * A sandbox backend knows how to wrap a shell command so it runs
21
- * inside an OS-level sandbox (macOS sandbox-exec, Linux bwrap, Docker, etc.).
21
+ * inside an OS-level sandbox (macOS sandbox-exec, Linux bwrap, etc.).
22
22
  */
23
23
  export interface SandboxBackend {
24
24
  /** Wrap a command for sandboxed execution in the given working directory. */
@@ -109,7 +109,7 @@ const initGuard = new PromiseGuard<void>();
109
109
  * don't exist — fall back to:
110
110
  * 1. `../Resources/<file>` (macOS .app bundle layout)
111
111
  * 2. Next to the compiled binary (process.execPath)
112
- * This matches the pattern used by docker.ts for Dockerfile.sandbox.
112
+ * This matches the pattern used for compiled Bun binary asset resolution.
113
113
  */
114
114
  function findWasmPath(pkg: string, file: string): string {
115
115
  const dir = import.meta.dirname ?? __dirname;
@@ -1,9 +1,8 @@
1
- import { execFileSync,execSync } from 'node:child_process';
1
+ import { execSync } from 'node:child_process';
2
2
 
3
3
  import { getConfig } from '../../config/loader.js';
4
4
  import type { SandboxConfig } from '../../config/schema.js';
5
- import { getSandboxWorkingDir,isLinux, isMacOS } from '../../util/platform.js';
6
- import { DEFAULT_SANDBOX_IMAGE } from './backends/docker.js';
5
+ import { isLinux, isMacOS } from '../../util/platform.js';
7
6
 
8
7
  export interface SandboxCheckResult {
9
8
  label: string;
@@ -14,73 +13,12 @@ export interface SandboxCheckResult {
14
13
  export interface SandboxDiagnostics {
15
14
  config: {
16
15
  enabled: boolean;
17
- backend: string;
18
- dockerImage: string;
19
16
  };
20
17
  /** Why the active backend was selected (config vs platform default). */
21
18
  activeBackendReason: string;
22
19
  checks: SandboxCheckResult[];
23
20
  }
24
21
 
25
- function checkDockerCli(): SandboxCheckResult {
26
- try {
27
- const out = execSync('docker --version', { stdio: 'pipe', timeout: 5000, encoding: 'utf-8' }).trim();
28
- return { label: 'Docker CLI installed', ok: true, detail: out };
29
- } catch {
30
- return { label: 'Docker CLI installed', ok: false, detail: 'docker not found in PATH' };
31
- }
32
- }
33
-
34
- function checkDockerDaemon(): SandboxCheckResult {
35
- try {
36
- execSync('docker info', { stdio: 'pipe', timeout: 10000 });
37
- return { label: 'Docker daemon running', ok: true };
38
- } catch {
39
- return { label: 'Docker daemon running', ok: false, detail: 'daemon not reachable — start Docker Desktop or run "sudo systemctl start docker"' };
40
- }
41
- }
42
-
43
- function checkDockerImage(image: string): SandboxCheckResult {
44
- try {
45
- execFileSync('docker', ['image', 'inspect', image], { stdio: 'pipe', timeout: 10000 });
46
- return { label: `Docker image available (${image})`, ok: true };
47
- } catch {
48
- // The default sandbox image is built locally from Dockerfile.sandbox — docker pull won't work.
49
- const remediation = image === DEFAULT_SANDBOX_IMAGE
50
- ? 'build with: docker build --no-cache -t vellum-sandbox:latest -f assistant/Dockerfile.sandbox assistant'
51
- : `pull with: docker pull ${image}`;
52
- return { label: `Docker image available (${image})`, ok: false, detail: remediation };
53
- }
54
- }
55
-
56
- function checkDockerMountProbe(image: string): SandboxCheckResult {
57
- // Use the same sandbox path and writable-mount probe that the runtime
58
- // preflight uses (checkMountProbe in docker.ts) so doctor validates
59
- // exactly what runtime enforces.
60
- const sandboxRoot = getSandboxWorkingDir();
61
- try {
62
- execFileSync(
63
- 'docker',
64
- [
65
- 'run', '--rm',
66
- '--mount', `type=bind,src=${sandboxRoot},dst=/workspace`,
67
- image, 'test', '-w', '/workspace',
68
- ],
69
- { stdio: 'pipe', timeout: 15000 },
70
- );
71
- return { label: 'Docker mount writable', ok: true };
72
- } catch (err) {
73
- const msg = err instanceof Error ? err.message : 'unknown error';
74
- return {
75
- label: 'Docker mount writable',
76
- ok: false,
77
- detail: 'Cannot bind-mount sandbox root or /workspace is not writable. ' +
78
- 'If using Docker Desktop, enable file sharing for this path in Settings > Resources > File Sharing. ' +
79
- `(${msg})`,
80
- };
81
- }
82
- }
83
-
84
22
  function checkNativeBackend(): SandboxCheckResult {
85
23
  if (isMacOS()) {
86
24
  try {
@@ -105,15 +43,12 @@ function getActiveBackendReason(sandboxConfig: SandboxConfig): string {
105
43
  if (!sandboxConfig.enabled) {
106
44
  return 'Sandbox is disabled in configuration';
107
45
  }
108
- if (sandboxConfig.backend === 'docker') {
109
- return 'Docker backend selected in configuration (sandbox.backend = "docker")';
110
- }
111
- return 'Native backend selected in configuration (sandbox.backend = "native")';
46
+ return 'Native backend selected';
112
47
  }
113
48
 
114
49
  /**
115
- * Run sandbox backend diagnostics. Checks Docker availability,
116
- * native backend availability, and reports current configuration.
50
+ * Run sandbox backend diagnostics. Checks native backend availability
51
+ * and reports current configuration.
117
52
  */
118
53
  export function runSandboxDiagnostics(): SandboxDiagnostics {
119
54
  const config = getConfig();
@@ -121,28 +56,12 @@ export function runSandboxDiagnostics(): SandboxDiagnostics {
121
56
 
122
57
  const checks: SandboxCheckResult[] = [];
123
58
 
124
- // Always check native backend availability as a diagnostic signal
59
+ // Check native backend availability
125
60
  checks.push(checkNativeBackend());
126
61
 
127
- // Docker checks: CLI, daemon, image, container execution
128
- const cliResult = checkDockerCli();
129
- checks.push(cliResult);
130
-
131
- if (cliResult.ok) {
132
- const daemonResult = checkDockerDaemon();
133
- checks.push(daemonResult);
134
-
135
- if (daemonResult.ok) {
136
- checks.push(checkDockerImage(sandboxConfig.docker.image));
137
- checks.push(checkDockerMountProbe(sandboxConfig.docker.image));
138
- }
139
- }
140
-
141
62
  return {
142
63
  config: {
143
64
  enabled: sandboxConfig.enabled,
144
- backend: sandboxConfig.backend,
145
- dockerImage: sandboxConfig.docker.image,
146
65
  },
147
66
  activeBackendReason: getActiveBackendReason(sandboxConfig),
148
67
  checks,
@@ -1,6 +1,4 @@
1
1
  import type { SandboxConfig } from '../../config/schema.js';
2
- import { getSandboxWorkingDir } from '../../util/platform.js';
3
- import { DockerBackend } from './backends/docker.js';
4
2
  import { NativeBackend } from './backends/native.js';
5
3
  import type { SandboxResult, WrapOptions } from './backends/types.js';
6
4
 
@@ -12,7 +10,7 @@ const nativeBackend = new NativeBackend();
12
10
  * Wrap a shell command for sandboxed execution.
13
11
  *
14
12
  * When sandboxing is disabled, returns a plain bash invocation.
15
- * When enabled, delegates to the configured backend (native or docker).
13
+ * When enabled, delegates to the native backend.
16
14
  * Fails closed if the backend cannot be applied.
17
15
  *
18
16
  * @param options Per-invocation overrides (e.g. networkMode for proxied bash).
@@ -31,14 +29,5 @@ export function wrapCommand(
31
29
  };
32
30
  }
33
31
 
34
- if (config.backend === 'docker') {
35
- // Always mount the canonical sandbox fs root, not whatever workingDir
36
- // happens to be. workingDir may be a subdirectory; the mount source
37
- // must be the fixed root so the entire sandbox filesystem is available.
38
- const sandboxRoot = getSandboxWorkingDir();
39
- const backend = new DockerBackend(sandboxRoot, config.docker);
40
- return backend.wrap(command, workingDir, options);
41
- }
42
-
43
32
  return nativeBackend.wrap(command, workingDir, options);
44
33
  }
@@ -1,5 +1,4 @@
1
- import { execSync,spawn } from 'node:child_process';
2
- import { platform } from 'node:os';
1
+ import { spawn } from 'node:child_process';
3
2
 
4
3
  import { getConfig } from '../../config/loader.js';
5
4
  import { RiskLevel } from '../../permissions/types.js';
@@ -21,32 +20,6 @@ import { wrapCommand } from './sandbox.js';
21
20
 
22
21
  const log = getLogger('shell-tool');
23
22
 
24
- /**
25
- * Returns the host address to bind the proxy to when Docker sandbox is active.
26
- * On macOS, Docker Desktop routes host.docker.internal through the VM to
27
- * 127.0.0.1, so no bind change is needed. On Linux, we need to bind to the
28
- * Docker bridge gateway IP so containers can reach the proxy.
29
- */
30
- function getDockerProxyHost(): string {
31
- if (platform() !== 'linux') return '127.0.0.1';
32
-
33
- try {
34
- // Docker bridge gateway is the default route from inside docker0 network.
35
- // `ip -4 addr show docker0` outputs the gateway IP assigned to the bridge.
36
- const output = execSync('ip -4 addr show docker0 2>/dev/null', { encoding: 'utf-8' });
37
- const match = output.match(/inet\s+(\d+\.\d+\.\d+\.\d+)/);
38
- if (match) return match[1];
39
- } catch {
40
- // docker0 interface may not exist (e.g. rootless Docker, custom networks)
41
- }
42
- // Fallback: bind to localhost when docker0 is unavailable (e.g. rootless
43
- // Docker, custom networks). This avoids EADDRNOTAVAIL from 172.17.0.1 while
44
- // keeping the proxy off public interfaces — 0.0.0.0 would expose the
45
- // unauthenticated credential proxy to the network. Docker containers won't
46
- // be able to reach localhost on the host, but that's a safer failure mode.
47
- return '127.0.0.1';
48
- }
49
-
50
23
  class ShellTool implements Tool {
51
24
  name = 'bash';
52
25
  description = 'Execute a shell command on the local machine';
@@ -153,7 +126,6 @@ class ShellTool implements Tool {
153
126
  const sandboxConfig = context.sandboxOverride != null
154
127
  ? { ...config.sandbox, enabled: context.sandboxOverride }
155
128
  : config.sandbox;
156
- const isDockerSandbox = sandboxConfig.enabled && sandboxConfig.backend === 'docker';
157
129
 
158
130
  // Acquire proxy session if proxied mode is requested.
159
131
  // `getOrStartSession` serializes per-conversation so concurrent proxied
@@ -170,9 +142,9 @@ class ShellTool implements Tool {
170
142
  undefined,
171
143
  getDataDir(),
172
144
  context.proxyApprovalCallback,
173
- isDockerSandbox ? { listenHost: getDockerProxyHost() } : undefined,
145
+ undefined,
174
146
  );
175
- proxyEnv = getSessionEnv(session.id, { dockerMode: isDockerSandbox });
147
+ proxyEnv = getSessionEnv(session.id);
176
148
  } catch (err) {
177
149
  log.error({ err }, 'Failed to start proxy session');
178
150
  return {