@vellumai/assistant 0.3.22 → 0.3.24
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.
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -84
- package/src/__tests__/approval-primitive.test.ts +72 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -4
- package/src/__tests__/host-shell-tool.test.ts +25 -0
- package/src/__tests__/ipc-snapshot.test.ts +0 -42
- package/src/__tests__/mcp-cli.test.ts +120 -3
- package/src/__tests__/skill-feature-flags-integration.test.ts +0 -4
- package/src/__tests__/terminal-tools.test.ts +19 -1
- package/src/__tests__/tool-approval-handler.test.ts +94 -5
- package/src/__tests__/tool-executor.test.ts +1 -1
- package/src/cli/mcp.ts +25 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +13 -8
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/reminder/SKILL.md +7 -6
- package/src/config/bundled-skills/time-based-actions/SKILL.md +7 -6
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +10 -10
- package/src/config/system-prompt.ts +0 -72
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +7 -7
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +14 -6
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -10
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/daemon/handlers/config.ts +0 -4
- package/src/daemon/handlers/navigate-settings.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +0 -10
- package/src/daemon/ipc-contract.ts +0 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/shutdown-handlers.ts +1 -1
- package/src/instrument.ts +15 -1
- package/src/mcp/client.ts +3 -3
- package/src/memory/conversation-crud.ts +26 -4
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +46 -18
- package/src/permissions/checker.ts +4 -4
- package/src/runtime/routes/inbound-message-handler.ts +2 -2
- package/src/runtime/routes/ingress-routes.ts +7 -2
- package/src/tools/executor.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +4 -29
- package/src/tools/swarm/delegate.ts +3 -0
- package/src/tools/system/navigate-settings.ts +0 -1
- package/src/tools/terminal/safe-env.ts +9 -0
- package/src/tools/tool-approval-handler.ts +2 -33
- package/src/tools/tool-manifest.ts +33 -88
- package/src/daemon/handlers/config-parental.ts +0 -164
- package/src/daemon/ipc-contract/parental-control.ts +0 -109
- package/src/security/parental-control-store.ts +0 -184
|
@@ -25,7 +25,6 @@ export * from './ipc-contract/memory.js';
|
|
|
25
25
|
export * from './ipc-contract/messages.js';
|
|
26
26
|
export * from './ipc-contract/notifications.js';
|
|
27
27
|
export * from './ipc-contract/pairing.js';
|
|
28
|
-
export * from './ipc-contract/parental-control.js';
|
|
29
28
|
export * from './ipc-contract/schedules.js';
|
|
30
29
|
export * from './ipc-contract/sessions.js';
|
|
31
30
|
export * from './ipc-contract/settings.js';
|
|
@@ -50,7 +49,6 @@ import type { _MemoryServerMessages } from './ipc-contract/memory.js';
|
|
|
50
49
|
import type { _MessagesClientMessages, _MessagesServerMessages } from './ipc-contract/messages.js';
|
|
51
50
|
import type { _NotificationsClientMessages, _NotificationsServerMessages } from './ipc-contract/notifications.js';
|
|
52
51
|
import type { _PairingClientMessages, _PairingServerMessages } from './ipc-contract/pairing.js';
|
|
53
|
-
import type { _ParentalControlClientMessages, _ParentalControlServerMessages } from './ipc-contract/parental-control.js';
|
|
54
52
|
import type { _SchedulesClientMessages, _SchedulesServerMessages } from './ipc-contract/schedules.js';
|
|
55
53
|
import type { _SessionsClientMessages, _SessionsServerMessages } from './ipc-contract/sessions.js';
|
|
56
54
|
import type { _SettingsClientMessages, _SettingsServerMessages } from './ipc-contract/settings.js';
|
|
@@ -89,7 +87,6 @@ export type ClientMessage =
|
|
|
89
87
|
| _WorkspaceClientMessages
|
|
90
88
|
| _SchedulesClientMessages
|
|
91
89
|
| _DiagnosticsClientMessages
|
|
92
|
-
| _ParentalControlClientMessages
|
|
93
90
|
| _InboxClientMessages
|
|
94
91
|
| _PairingClientMessages
|
|
95
92
|
| _NotificationsClientMessages
|
|
@@ -116,7 +113,6 @@ export type ServerMessage =
|
|
|
116
113
|
| _SchedulesServerMessages
|
|
117
114
|
| _SettingsServerMessages
|
|
118
115
|
| _DiagnosticsServerMessages
|
|
119
|
-
| _ParentalControlServerMessages
|
|
120
116
|
| _InboxServerMessages
|
|
121
117
|
| _PairingServerMessages
|
|
122
118
|
| _NotificationsServerMessages
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { reconcileCallsOnStartup } from '../calls/call-recovery.js';
|
|
|
8
8
|
import { setRelayBroadcast } from '../calls/relay-server.js';
|
|
9
9
|
import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js';
|
|
10
10
|
import { setVoiceBridgeDeps } from '../calls/voice-session-bridge.js';
|
|
11
|
+
import { isAssistantFeatureFlagEnabled } from '../config/assistant-feature-flags.js';
|
|
11
12
|
import {
|
|
12
13
|
getQdrantUrlEnv,
|
|
13
14
|
getRuntimeHttpHost,
|
|
@@ -21,8 +22,9 @@ import { syncUpdateBulletinOnStartup } from '../config/update-bulletin.js';
|
|
|
21
22
|
import { HeartbeatService } from '../heartbeat/heartbeat-service.js';
|
|
22
23
|
import { getHookManager } from '../hooks/manager.js';
|
|
23
24
|
import { installTemplates } from '../hooks/templates.js';
|
|
24
|
-
import { initSentry } from '../instrument.js';
|
|
25
|
+
import { closeSentry, initSentry } from '../instrument.js';
|
|
25
26
|
import { initLogfire } from '../logfire.js';
|
|
27
|
+
import { getMcpServerManager } from '../mcp/manager.js';
|
|
26
28
|
import * as attachmentsStore from '../memory/attachments-store.js';
|
|
27
29
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
28
30
|
import { initializeDb } from '../memory/db.js';
|
|
@@ -54,7 +56,6 @@ import { createGuardianActionCopyGenerator, createGuardianFollowUpConversationGe
|
|
|
54
56
|
import { initPairingHandlers } from './handlers/pairing.js';
|
|
55
57
|
import { installCliLaunchers } from './install-cli-launchers.js';
|
|
56
58
|
import type { ServerMessage } from './ipc-protocol.js';
|
|
57
|
-
import { getMcpServerManager } from '../mcp/manager.js';
|
|
58
59
|
import { initializeProvidersAndTools, registerMessagingProviders,registerWatcherProviders } from './providers-setup.js';
|
|
59
60
|
import { seedInterfaceFiles } from './seed-files.js';
|
|
60
61
|
import { DaemonServer } from './server.js';
|
|
@@ -96,7 +97,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
96
97
|
let socketCreated = false;
|
|
97
98
|
|
|
98
99
|
try {
|
|
100
|
+
// Initialize crash reporting eagerly so early startup failures are
|
|
101
|
+
// captured. After config loads we check the opt-out flag and call
|
|
102
|
+
// closeSentry() if the user has disabled it.
|
|
99
103
|
initSentry();
|
|
104
|
+
|
|
100
105
|
await initLogfire();
|
|
101
106
|
|
|
102
107
|
// Migration order matters: first move legacy flat files into the data dir
|
|
@@ -173,6 +178,13 @@ export async function runDaemon(): Promise<void> {
|
|
|
173
178
|
initLogger({ dir: config.logFile.dir, retentionDays: config.logFile.retentionDays });
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
// If the user has opted out of crash reporting, stop Sentry from capturing
|
|
182
|
+
// future events. Early-startup crashes before this point are still captured.
|
|
183
|
+
const collectUsageData = isAssistantFeatureFlagEnabled('feature_flags.collect-usage-data.enabled', config);
|
|
184
|
+
if (!collectUsageData) {
|
|
185
|
+
await closeSentry();
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
await initializeProvidersAndTools(config);
|
|
177
189
|
|
|
178
190
|
// Start the IPC socket BEFORE Qdrant so that clients can connect
|
|
@@ -576,9 +576,9 @@ export async function processMessage(
|
|
|
576
576
|
|
|
577
577
|
let stateApplied = true;
|
|
578
578
|
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
579
|
-
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !==
|
|
579
|
+
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !== null;
|
|
580
580
|
} else if (turnResult.disposition === 'decline') {
|
|
581
|
-
stateApplied = finalizeFollowup(request.id, 'declined') !==
|
|
581
|
+
stateApplied = finalizeFollowup(request.id, 'declined') !== null;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
if (!stateApplied) {
|
|
@@ -2,8 +2,8 @@ import * as Sentry from '@sentry/node';
|
|
|
2
2
|
|
|
3
3
|
import type { HeartbeatService } from '../heartbeat/heartbeat-service.js';
|
|
4
4
|
import type { HookManager } from '../hooks/manager.js';
|
|
5
|
-
import { getSqlite, resetDb } from '../memory/db.js';
|
|
6
5
|
import type { McpServerManager } from '../mcp/manager.js';
|
|
6
|
+
import { getSqlite, resetDb } from '../memory/db.js';
|
|
7
7
|
import type { QdrantManager } from '../memory/qdrant-manager.js';
|
|
8
8
|
import type { RuntimeHttpServer } from '../runtime/http-server.js';
|
|
9
9
|
import { browserManager } from '../tools/browser/browser-manager.js';
|
package/src/instrument.ts
CHANGED
|
@@ -32,7 +32,12 @@ function redactObject(obj: unknown): unknown {
|
|
|
32
32
|
return obj;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Call after dotenv has loaded so SENTRY_DSN is available.
|
|
37
|
+
* Always initializes Sentry to capture early startup crashes. If the user
|
|
38
|
+
* later opts out via the "collect-usage-data" feature flag, call closeSentry()
|
|
39
|
+
* after config is loaded to stop future event capturing.
|
|
40
|
+
*/
|
|
36
41
|
export function initSentry(): void {
|
|
37
42
|
Sentry.init({
|
|
38
43
|
dsn: getSentryDsn(),
|
|
@@ -60,3 +65,12 @@ export function initSentry(): void {
|
|
|
60
65
|
},
|
|
61
66
|
});
|
|
62
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Stop capturing future Sentry events. Called after config loads when the
|
|
71
|
+
* user has opted out of crash reporting so that early-startup crashes are
|
|
72
|
+
* still captured but subsequent events are suppressed.
|
|
73
|
+
*/
|
|
74
|
+
export async function closeSentry(): Promise<void> {
|
|
75
|
+
await Sentry.close();
|
|
76
|
+
}
|
package/src/mcp/client.ts
CHANGED
|
@@ -50,7 +50,7 @@ export class McpClient {
|
|
|
50
50
|
} catch (err) {
|
|
51
51
|
// Clean up the transport on failure (e.g., kill spawned stdio process)
|
|
52
52
|
try { await this.client.close(); } catch { /* ignore cleanup errors */ }
|
|
53
|
-
this.transport =
|
|
53
|
+
this.transport = null;
|
|
54
54
|
throw err;
|
|
55
55
|
}
|
|
56
56
|
this.connected = true;
|
|
@@ -85,7 +85,7 @@ export class McpClient {
|
|
|
85
85
|
const isError = result.isError === true;
|
|
86
86
|
|
|
87
87
|
// Handle structuredContent if present
|
|
88
|
-
if (result.structuredContent !== undefined
|
|
88
|
+
if (result.structuredContent !== undefined) {
|
|
89
89
|
return {
|
|
90
90
|
content: JSON.stringify(result.structuredContent),
|
|
91
91
|
isError,
|
|
@@ -96,7 +96,7 @@ export class McpClient {
|
|
|
96
96
|
const textParts: string[] = [];
|
|
97
97
|
if (Array.isArray(result.content)) {
|
|
98
98
|
for (const block of result.content) {
|
|
99
|
-
if (typeof block === 'object' && block !==
|
|
99
|
+
if (typeof block === 'object' && block !== undefined && 'type' in block) {
|
|
100
100
|
if (block.type === 'text' && 'text' in block) {
|
|
101
101
|
textParts.push(String(block.text));
|
|
102
102
|
} else if (block.type === 'resource' && 'resource' in block) {
|
|
@@ -156,7 +156,27 @@ export function createConversation(titleOrOpts?: string | { title?: string; thre
|
|
|
156
156
|
source,
|
|
157
157
|
memoryScopeId,
|
|
158
158
|
};
|
|
159
|
-
|
|
159
|
+
|
|
160
|
+
// Retry on SQLITE_BUSY and SQLITE_IOERR — transient disk I/O errors or WAL
|
|
161
|
+
// contention can cause the first attempt to fail even under normal load.
|
|
162
|
+
const MAX_RETRIES = 3;
|
|
163
|
+
for (let attempt = 0; ; attempt++) {
|
|
164
|
+
try {
|
|
165
|
+
db.insert(conversations).values(conversation).run();
|
|
166
|
+
break;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const code = (err as { code?: string }).code ?? '';
|
|
169
|
+
if (attempt < MAX_RETRIES && (code.startsWith('SQLITE_BUSY') || code.startsWith('SQLITE_IOERR'))) {
|
|
170
|
+
log.warn({ attempt, conversationId: id, code }, 'createConversation: transient SQLite error, retrying');
|
|
171
|
+
// Synchronous sleep — createConversation is synchronous and the
|
|
172
|
+
// retry window is short (50-150ms), so Bun.sleepSync is appropriate.
|
|
173
|
+
Bun.sleepSync(50 * (attempt + 1));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
160
180
|
return conversation;
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -213,7 +233,8 @@ export async function addMessage(conversationId: string, role: string, content:
|
|
|
213
233
|
? metadata.userMessageChannel
|
|
214
234
|
: null;
|
|
215
235
|
// Wrap insert + updatedAt bump in a transaction so they're atomic.
|
|
216
|
-
// Retry on SQLITE_BUSY
|
|
236
|
+
// Retry on SQLITE_BUSY* and SQLITE_IOERR* — covers WAL contention variants
|
|
237
|
+
// (SQLITE_BUSY_SNAPSHOT, SQLITE_BUSY_RECOVERY) and transient disk I/O errors.
|
|
217
238
|
// Timestamp is recomputed each attempt so a late retry doesn't persist a stale updatedAt.
|
|
218
239
|
const MAX_RETRIES = 3;
|
|
219
240
|
let now!: number;
|
|
@@ -243,8 +264,9 @@ export async function addMessage(conversationId: string, role: string, content:
|
|
|
243
264
|
});
|
|
244
265
|
break;
|
|
245
266
|
} catch (err) {
|
|
246
|
-
|
|
247
|
-
|
|
267
|
+
const errCode = (err as { code?: string }).code ?? '';
|
|
268
|
+
if (attempt < MAX_RETRIES && (errCode.startsWith('SQLITE_BUSY') || errCode.startsWith('SQLITE_IOERR'))) {
|
|
269
|
+
log.warn({ attempt, conversationId, code: errCode }, 'addMessage: transient SQLite error, retrying');
|
|
248
270
|
await Bun.sleep(50 * (attempt + 1));
|
|
249
271
|
continue;
|
|
250
272
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
import { getSqliteFrom } from '../db-connection.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Add indexes, a column, and a unique constraint for schema improvements:
|
|
@@ -15,23 +16,50 @@ export function migrateSchemaIndexesAndColumns(database: DrizzleDb): void {
|
|
|
15
16
|
database.run(/*sql*/ `ALTER TABLE memory_jobs ADD COLUMN started_at INTEGER`);
|
|
16
17
|
} catch { /* already exists */ }
|
|
17
18
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
ORDER BY updated_at DESC
|
|
29
|
-
) AS rn
|
|
30
|
-
FROM notification_deliveries
|
|
31
|
-
)
|
|
32
|
-
WHERE rn = 1
|
|
33
|
-
)
|
|
34
|
-
`);
|
|
19
|
+
// Ensure notification_decision_id column exists on notification_deliveries.
|
|
20
|
+
// Migration 114 (createNotificationTables) should have created this column,
|
|
21
|
+
// but on databases where 114 crashed mid-run the column may be absent. Rather
|
|
22
|
+
// than silently skipping the dedup+index step (leaving the schema incompatible
|
|
23
|
+
// with runtime code that writes notificationDecisionId), we add the column
|
|
24
|
+
// here if it is missing, then proceed unconditionally.
|
|
25
|
+
const raw = getSqliteFrom(database);
|
|
26
|
+
const notifDdl = raw.query(
|
|
27
|
+
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'notification_deliveries'`,
|
|
28
|
+
).get() as { sql: string } | null;
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
if (notifDdl && !notifDdl.sql.includes('notification_decision_id')) {
|
|
31
|
+
// ADD COLUMN cannot carry NOT NULL without a default in SQLite, so we add
|
|
32
|
+
// it as nullable TEXT. Existing rows get NULL, which is valid until the
|
|
33
|
+
// runtime backfills or replaces them. The unique index below is created
|
|
34
|
+
// with WHERE NOT NULL to tolerate the transition period.
|
|
35
|
+
try {
|
|
36
|
+
database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN notification_decision_id TEXT`);
|
|
37
|
+
} catch { /* column was added concurrently — safe to continue */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (notifDdl) {
|
|
41
|
+
// Deduplicate before creating the unique index — the prior schema allowed
|
|
42
|
+
// multiple rows per (notification_decision_id, channel) via the wider
|
|
43
|
+
// (decision_id, channel, destination, attempt) unique index. Keep the
|
|
44
|
+
// row with the latest updated_at for each group.
|
|
45
|
+
try {
|
|
46
|
+
database.run(/*sql*/ `
|
|
47
|
+
DELETE FROM notification_deliveries
|
|
48
|
+
WHERE id NOT IN (
|
|
49
|
+
SELECT id FROM (
|
|
50
|
+
SELECT id, ROW_NUMBER() OVER (
|
|
51
|
+
PARTITION BY notification_decision_id, channel
|
|
52
|
+
ORDER BY updated_at DESC
|
|
53
|
+
) AS rn
|
|
54
|
+
FROM notification_deliveries
|
|
55
|
+
)
|
|
56
|
+
WHERE rn = 1
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
} catch { /* deduplication failed — unique index creation below may fail too, which is non-fatal */ }
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_deliveries_decision_channel ON notification_deliveries(notification_decision_id, channel) WHERE notification_decision_id IS NOT NULL`);
|
|
63
|
+
} catch { /* index already exists or constraint violation — safe to continue */ }
|
|
64
|
+
}
|
|
37
65
|
}
|
|
@@ -398,10 +398,10 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
|
|
|
398
398
|
if (HIGH_RISK_PROGRAMS.has(prog)) return RiskLevel.High;
|
|
399
399
|
|
|
400
400
|
if (prog === 'rm') {
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
if (isRmOfKnownSafeFile(seg.args)) {
|
|
401
|
+
// Only downgrade rm of known safe workspace files for sandboxed bash.
|
|
402
|
+
// host_bash has a global allow rule that would auto-approve Medium-risk
|
|
403
|
+
// commands, so rm on the host must always require explicit approval.
|
|
404
|
+
if (toolName === 'bash' && isRmOfKnownSafeFile(seg.args)) {
|
|
405
405
|
maxRisk = RiskLevel.Medium;
|
|
406
406
|
continue;
|
|
407
407
|
}
|
|
@@ -1087,9 +1087,9 @@ export async function handleChannelInbound(
|
|
|
1087
1087
|
|
|
1088
1088
|
let stateApplied = true;
|
|
1089
1089
|
if (turnResult.disposition === 'call_back' || turnResult.disposition === 'message_back') {
|
|
1090
|
-
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !==
|
|
1090
|
+
stateApplied = progressFollowupState(request.id, 'dispatching', turnResult.disposition) !== null;
|
|
1091
1091
|
} else if (turnResult.disposition === 'decline') {
|
|
1092
|
-
stateApplied = finalizeFollowup(request.id, 'declined') !==
|
|
1092
|
+
stateApplied = finalizeFollowup(request.id, 'declined') !== null;
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
1095
|
if (!stateApplied) {
|
|
@@ -93,8 +93,13 @@ export async function handleRevokeMember(req: Request, memberId: string): Promis
|
|
|
93
93
|
* POST /v1/ingress/members/:id/block
|
|
94
94
|
*/
|
|
95
95
|
export async function handleBlockMember(req: Request, memberId: string): Promise<Response> {
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
let reason: string | undefined;
|
|
97
|
+
try {
|
|
98
|
+
const body = (await req.json()) as Record<string, unknown>;
|
|
99
|
+
reason = body.reason as string | undefined;
|
|
100
|
+
} catch {
|
|
101
|
+
// Body is optional — callers may omit it entirely
|
|
102
|
+
}
|
|
98
103
|
|
|
99
104
|
const result = blockIngressMember(memberId, reason);
|
|
100
105
|
|
package/src/tools/executor.ts
CHANGED
|
@@ -56,8 +56,8 @@ export class ToolExecutor {
|
|
|
56
56
|
startedAtMs: startTime,
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
// Run pre-execution approval gates (abort,
|
|
60
|
-
//
|
|
59
|
+
// Run pre-execution approval gates (abort, guardian policy,
|
|
60
|
+
// allowed-tool-set, task-run preflight, tool registry lookup).
|
|
61
61
|
const gateResult = await this.approvalHandler.checkPreExecutionGates(
|
|
62
62
|
name, input, context, executionTarget, riskLevel, startTime,
|
|
63
63
|
(event) => emitLifecycleEvent(context, event),
|
|
@@ -9,38 +9,13 @@ import type { ToolDefinition } from '../../providers/types.js';
|
|
|
9
9
|
import { redactSecrets } from '../../security/secret-scanner.js';
|
|
10
10
|
import { getLogger } from '../../util/logger.js';
|
|
11
11
|
import { formatShellOutput } from '../shared/shell-output.js';
|
|
12
|
+
import { buildSanitizedEnv } from '../terminal/safe-env.js';
|
|
12
13
|
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
13
14
|
|
|
14
15
|
const log = getLogger('host-shell-tool');
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'HOME',
|
|
19
|
-
'TERM',
|
|
20
|
-
'LANG',
|
|
21
|
-
'EDITOR',
|
|
22
|
-
'SHELL',
|
|
23
|
-
'USER',
|
|
24
|
-
'TMPDIR',
|
|
25
|
-
'LC_ALL',
|
|
26
|
-
'LC_CTYPE',
|
|
27
|
-
'XDG_RUNTIME_DIR',
|
|
28
|
-
'DISPLAY',
|
|
29
|
-
'COLORTERM',
|
|
30
|
-
'TERM_PROGRAM',
|
|
31
|
-
'SSH_AUTH_SOCK',
|
|
32
|
-
'SSH_AGENT_PID',
|
|
33
|
-
'GPG_TTY',
|
|
34
|
-
'GNUPGHOME',
|
|
35
|
-
] as const;
|
|
36
|
-
|
|
37
|
-
function buildSanitizedEnv(): Record<string, string> {
|
|
38
|
-
const env: Record<string, string> = {};
|
|
39
|
-
for (const key of SAFE_ENV_VARS) {
|
|
40
|
-
if (process.env[key] != null) {
|
|
41
|
-
env[key] = process.env[key]!;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
17
|
+
function buildHostShellEnv(): Record<string, string> {
|
|
18
|
+
const env = buildSanitizedEnv();
|
|
44
19
|
// Ensure ~/.local/bin and ~/.bun/bin are in PATH so `vellum` and `bun` are
|
|
45
20
|
// always reachable, even when the daemon is launched from a macOS app
|
|
46
21
|
// bundle that inherits a minimal PATH.
|
|
@@ -125,7 +100,7 @@ class HostShellTool implements Tool {
|
|
|
125
100
|
|
|
126
101
|
const child = spawn('bash', ['-c', '--', command], {
|
|
127
102
|
cwd: workingDir,
|
|
128
|
-
env:
|
|
103
|
+
env: buildHostShellEnv(),
|
|
129
104
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
105
|
});
|
|
131
106
|
|
|
@@ -8,6 +8,7 @@ import { executeSwarm } from '../../swarm/orchestrator.js';
|
|
|
8
8
|
import { generatePlan } from '../../swarm/router-planner.js';
|
|
9
9
|
import { getLogger } from '../../util/logger.js';
|
|
10
10
|
import { truncate } from '../../util/truncate.js';
|
|
11
|
+
import { registerTool } from '../registry.js';
|
|
11
12
|
import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
|
|
12
13
|
|
|
13
14
|
const log = getLogger('swarm-delegate');
|
|
@@ -178,6 +179,8 @@ export const swarmDelegateTool: Tool = {
|
|
|
178
179
|
},
|
|
179
180
|
};
|
|
180
181
|
|
|
182
|
+
registerTool(swarmDelegateTool);
|
|
183
|
+
|
|
181
184
|
/** Clear all active sessions — only for testing. */
|
|
182
185
|
export function _resetSwarmActive(): void {
|
|
183
186
|
activeSessions.clear();
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Shared by the sandbox bash tool and skill sandbox runner.
|
|
7
7
|
*/
|
|
8
|
+
import { getGatewayInternalBaseUrl, getIngressPublicBaseUrl } from '../../config/env.js';
|
|
9
|
+
|
|
8
10
|
const SAFE_ENV_VARS = [
|
|
9
11
|
'PATH',
|
|
10
12
|
'HOME',
|
|
@@ -33,5 +35,12 @@ export function buildSanitizedEnv(): Record<string, string> {
|
|
|
33
35
|
env[key] = process.env[key]!;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
38
|
+
// Always inject an internal gateway base for local control-plane/API calls.
|
|
39
|
+
const internalGatewayBase = getGatewayInternalBaseUrl();
|
|
40
|
+
env.INTERNAL_GATEWAY_BASE_URL = internalGatewayBase;
|
|
41
|
+
// Inject a public gateway base when ingress is configured; otherwise fall
|
|
42
|
+
// back to the internal base so commands remain functional in local-only mode.
|
|
43
|
+
const publicGatewayBase = getIngressPublicBaseUrl()?.replace(/\/+$/, '');
|
|
44
|
+
env.GATEWAY_BASE_URL = publicGatewayBase || internalGatewayBase;
|
|
36
45
|
return env;
|
|
37
46
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { consumeGrantForInvocation } from '../approvals/approval-primitive.js';
|
|
2
|
-
import { isToolBlocked } from '../security/parental-control-store.js';
|
|
3
2
|
import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
|
|
4
3
|
import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
|
|
5
4
|
import { getLogger } from '../util/logger.js';
|
|
@@ -40,8 +39,8 @@ export type PreExecutionGateResult =
|
|
|
40
39
|
| { allowed: false; result: ToolExecutionResult };
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
|
-
* Handles pre-execution approval gates: abort checks,
|
|
44
|
-
*
|
|
42
|
+
* Handles pre-execution approval gates: abort checks, guardian policy,
|
|
43
|
+
* allowed-tool-set gating, and task-run preflight checks.
|
|
45
44
|
* These run before the interactive permission prompt flow.
|
|
46
45
|
*/
|
|
47
46
|
export class ToolApprovalHandler {
|
|
@@ -81,36 +80,6 @@ export class ToolApprovalHandler {
|
|
|
81
80
|
return { allowed: false, result: { content: 'Cancelled', isError: true } };
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
// Reject tools blocked by parental control settings before any permission check.
|
|
85
|
-
if (isToolBlocked(name)) {
|
|
86
|
-
log.warn(
|
|
87
|
-
{
|
|
88
|
-
toolName: name,
|
|
89
|
-
sessionId: context.sessionId,
|
|
90
|
-
conversationId: context.conversationId,
|
|
91
|
-
principal: context.principal,
|
|
92
|
-
reason: 'blocked_by_parental_controls',
|
|
93
|
-
},
|
|
94
|
-
'Parental control blocked tool invocation',
|
|
95
|
-
);
|
|
96
|
-
const durationMs = Date.now() - startTime;
|
|
97
|
-
emitLifecycleEvent({
|
|
98
|
-
type: 'permission_denied',
|
|
99
|
-
toolName: name,
|
|
100
|
-
executionTarget,
|
|
101
|
-
input,
|
|
102
|
-
workingDir: context.workingDir,
|
|
103
|
-
sessionId: context.sessionId,
|
|
104
|
-
conversationId: context.conversationId,
|
|
105
|
-
requestId: context.requestId,
|
|
106
|
-
riskLevel,
|
|
107
|
-
decision: 'deny',
|
|
108
|
-
reason: 'Blocked by parental control settings',
|
|
109
|
-
durationMs,
|
|
110
|
-
});
|
|
111
|
-
return { allowed: false, result: { content: 'This tool is blocked by parental control settings.', isError: true } };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
83
|
// Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
|
|
115
84
|
const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.guardianActorRole);
|
|
116
85
|
if (guardianCheck.denied) {
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* so adding/removing tools only requires editing this manifest.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { RiskLevel } from '../permissions/types.js';
|
|
10
9
|
import { accountManageTool } from './credentials/account-registry.js';
|
|
11
10
|
import { credentialStoreTool } from './credentials/vault.js';
|
|
12
11
|
import { memorySaveTool, memorySearchTool, memoryUpdateTool } from './memory/register.js';
|
|
@@ -20,22 +19,34 @@ import type { Tool } from './types.js';
|
|
|
20
19
|
import { screenWatchTool } from './watch/screen-watch.js';
|
|
21
20
|
|
|
22
21
|
// ── Eager side-effect modules ───────────────────────────────────────
|
|
23
|
-
//
|
|
22
|
+
// These static imports trigger top-level `registerTool()` side effects.
|
|
23
|
+
//
|
|
24
|
+
// IMPORTANT: These MUST be static imports (not dynamic `await import()`).
|
|
25
|
+
// When the daemon is compiled with `bun --compile`, dynamic imports with
|
|
26
|
+
// relative string literals resolve against the virtual `/$bunfs/root/`
|
|
27
|
+
// filesystem root rather than the module's own directory, causing
|
|
28
|
+
// "Cannot find module './filesystem/read.js'" crashes in production builds.
|
|
29
|
+
// Static imports are resolved at bundle time and are always safe.
|
|
30
|
+
import './assets/materialize.js';
|
|
31
|
+
import './assets/search.js';
|
|
32
|
+
import './filesystem/edit.js';
|
|
33
|
+
import './filesystem/read.js';
|
|
34
|
+
import './filesystem/view-image.js';
|
|
35
|
+
import './filesystem/write.js';
|
|
36
|
+
import './network/web-fetch.js';
|
|
37
|
+
import './network/web-search.js';
|
|
38
|
+
import './skills/delete-managed.js';
|
|
39
|
+
import './skills/load.js';
|
|
40
|
+
import './skills/scaffold-managed.js';
|
|
41
|
+
import './swarm/delegate.js';
|
|
42
|
+
import './system/request-permission.js';
|
|
43
|
+
import './system/version.js';
|
|
44
|
+
import './terminal/shell.js';
|
|
24
45
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await import('./network/web-search.js');
|
|
30
|
-
await import('./network/web-fetch.js');
|
|
31
|
-
await import('./skills/load.js');
|
|
32
|
-
await import('./skills/scaffold-managed.js');
|
|
33
|
-
await import('./skills/delete-managed.js');
|
|
34
|
-
await import('./system/request-permission.js');
|
|
35
|
-
await import('./assets/search.js');
|
|
36
|
-
await import('./assets/materialize.js');
|
|
37
|
-
await import('./filesystem/view-image.js');
|
|
38
|
-
await import('./system/version.js');
|
|
46
|
+
// loadEagerModules is a no-op now that all eager registrations happen via
|
|
47
|
+
// static imports above. Kept for API compatibility with registry.ts callers.
|
|
48
|
+
export function loadEagerModules(): Promise<void> {
|
|
49
|
+
return Promise.resolve();
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
// Tool names registered by the eager modules above. Listed explicitly so
|
|
@@ -43,6 +54,7 @@ export async function loadEagerModules(): Promise<void> {
|
|
|
43
54
|
// already in the registry before init ran (e.g. when a test file imports
|
|
44
55
|
// an eager module at the top level).
|
|
45
56
|
export const eagerModuleToolNames: string[] = [
|
|
57
|
+
'bash',
|
|
46
58
|
'file_read',
|
|
47
59
|
'file_write',
|
|
48
60
|
'file_edit',
|
|
@@ -54,6 +66,7 @@ export const eagerModuleToolNames: string[] = [
|
|
|
54
66
|
'request_system_permission',
|
|
55
67
|
'asset_search',
|
|
56
68
|
'asset_materialize',
|
|
69
|
+
'swarm_delegate',
|
|
57
70
|
'view_image',
|
|
58
71
|
'version',
|
|
59
72
|
];
|
|
@@ -78,76 +91,8 @@ export const explicitTools: Tool[] = [
|
|
|
78
91
|
|
|
79
92
|
// ── Lazy tool descriptors ───────────────────────────────────────────
|
|
80
93
|
// Tools that defer module loading until first invocation.
|
|
94
|
+
// bash and swarm_delegate were previously lazy but are now eagerly registered
|
|
95
|
+
// via side-effect imports above, preserving their full definitions (including
|
|
96
|
+
// the `reason` field on bash) and fixing bun --compile module-not-found crashes.
|
|
81
97
|
|
|
82
|
-
export const lazyTools: LazyToolDescriptor[] = [
|
|
83
|
-
{
|
|
84
|
-
name: 'bash',
|
|
85
|
-
description: 'Execute a shell command on the local machine',
|
|
86
|
-
category: 'terminal',
|
|
87
|
-
defaultRiskLevel: RiskLevel.Medium,
|
|
88
|
-
definition: {
|
|
89
|
-
name: 'bash',
|
|
90
|
-
description: 'Execute a shell command on the local machine',
|
|
91
|
-
input_schema: {
|
|
92
|
-
type: 'object',
|
|
93
|
-
properties: {
|
|
94
|
-
command: {
|
|
95
|
-
type: 'string',
|
|
96
|
-
description: 'The shell command to execute',
|
|
97
|
-
},
|
|
98
|
-
timeout_seconds: {
|
|
99
|
-
type: 'number',
|
|
100
|
-
description: 'Optional timeout in seconds. Defaults to the configured default (120s). Cannot exceed the configured maximum.',
|
|
101
|
-
},
|
|
102
|
-
network_mode: {
|
|
103
|
-
type: 'string',
|
|
104
|
-
enum: ['off', 'proxied'],
|
|
105
|
-
description: 'Network access mode for the command. "off" (default) blocks network access; "proxied" routes traffic through the credential proxy.',
|
|
106
|
-
},
|
|
107
|
-
credential_ids: {
|
|
108
|
-
type: 'array',
|
|
109
|
-
items: { type: 'string' },
|
|
110
|
-
description: 'Optional list of credential IDs to inject via the proxy when network_mode is "proxied".',
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
required: ['command'],
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
loader: async () => {
|
|
117
|
-
const mod = await import('./terminal/shell.js');
|
|
118
|
-
return mod.shellTool;
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: 'swarm_delegate',
|
|
123
|
-
description: 'Decompose a complex task into parallel specialist subtasks and execute them concurrently.',
|
|
124
|
-
category: 'orchestration',
|
|
125
|
-
defaultRiskLevel: RiskLevel.Medium,
|
|
126
|
-
definition: {
|
|
127
|
-
name: 'swarm_delegate',
|
|
128
|
-
description: 'Decompose a complex task into parallel specialist subtasks and execute them concurrently. Use this for multi-part tasks that benefit from parallel research, coding, and review.',
|
|
129
|
-
input_schema: {
|
|
130
|
-
type: 'object',
|
|
131
|
-
properties: {
|
|
132
|
-
objective: {
|
|
133
|
-
type: 'string',
|
|
134
|
-
description: 'The complex task to decompose and execute in parallel',
|
|
135
|
-
},
|
|
136
|
-
context: {
|
|
137
|
-
type: 'string',
|
|
138
|
-
description: 'Optional additional context about the task or codebase',
|
|
139
|
-
},
|
|
140
|
-
max_workers: {
|
|
141
|
-
type: 'number',
|
|
142
|
-
description: 'Maximum concurrent workers (1-6, default from config)',
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
required: ['objective'],
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
loader: async () => {
|
|
149
|
-
const mod = await import('./swarm/delegate.js');
|
|
150
|
-
return mod.swarmDelegateTool;
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
];
|
|
98
|
+
export const lazyTools: LazyToolDescriptor[] = [];
|