@vellumai/assistant 0.3.18 → 0.3.20
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/ARCHITECTURE.md +155 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +605 -104
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/checker.test.ts +60 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +779 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +185 -1
- package/src/__tests__/guardian-grant-minting.test.ts +532 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +58 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trust-store.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +276 -212
- package/src/calls/call-domain.ts +56 -6
- package/src/calls/guardian-dispatch.ts +56 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +59 -4
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +26 -0
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +260 -422
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +13 -1
- package/src/memory/embedding-local.ts +22 -8
- package/src/memory/guardian-action-store.ts +133 -2
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +35 -1
- package/src/memory/scoped-approval-grants.ts +518 -0
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/permissions/checker.ts +27 -0
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +154 -0
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -74
- package/src/runtime/routes/inbound-message-handler.ts +568 -409
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -731,6 +731,27 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
731
731
|
type: 'voice_config_update',
|
|
732
732
|
activationKey: 'fn',
|
|
733
733
|
},
|
|
734
|
+
generate_avatar: {
|
|
735
|
+
type: 'generate_avatar',
|
|
736
|
+
description: 'a friendly purple cat with green eyes wearing a tiny hat',
|
|
737
|
+
},
|
|
738
|
+
guardian_actions_pending_request: {
|
|
739
|
+
type: 'guardian_actions_pending_request',
|
|
740
|
+
conversationId: 'conv-guardian-001',
|
|
741
|
+
},
|
|
742
|
+
guardian_action_decision: {
|
|
743
|
+
type: 'guardian_action_decision',
|
|
744
|
+
requestId: 'req-guardian-001',
|
|
745
|
+
action: 'approve_once',
|
|
746
|
+
conversationId: 'conv-guardian-001',
|
|
747
|
+
},
|
|
748
|
+
reorder_threads: {
|
|
749
|
+
type: 'reorder_threads',
|
|
750
|
+
updates: [
|
|
751
|
+
{ sessionId: 'sess-001', displayOrder: 0, isPinned: false },
|
|
752
|
+
{ sessionId: 'sess-002', displayOrder: 1, isPinned: true },
|
|
753
|
+
],
|
|
754
|
+
},
|
|
734
755
|
};
|
|
735
756
|
|
|
736
757
|
// ---------------------------------------------------------------------------
|
|
@@ -1081,6 +1102,7 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1081
1102
|
degraded: false,
|
|
1082
1103
|
updateAvailable: false,
|
|
1083
1104
|
userInvocable: true,
|
|
1105
|
+
provenance: { kind: 'first-party', provider: 'Vellum' },
|
|
1084
1106
|
},
|
|
1085
1107
|
],
|
|
1086
1108
|
},
|
|
@@ -2019,6 +2041,42 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
2019
2041
|
emoji: '',
|
|
2020
2042
|
home: '',
|
|
2021
2043
|
},
|
|
2044
|
+
avatar_updated: {
|
|
2045
|
+
type: 'avatar_updated',
|
|
2046
|
+
avatarPath: '/Users/test/.vellum/workspace/data/avatar/custom-avatar.png',
|
|
2047
|
+
},
|
|
2048
|
+
generate_avatar_response: {
|
|
2049
|
+
type: 'generate_avatar_response',
|
|
2050
|
+
success: true,
|
|
2051
|
+
error: undefined,
|
|
2052
|
+
},
|
|
2053
|
+
guardian_actions_pending_response: {
|
|
2054
|
+
type: 'guardian_actions_pending_response',
|
|
2055
|
+
conversationId: 'conv-guardian-001',
|
|
2056
|
+
prompts: [
|
|
2057
|
+
{
|
|
2058
|
+
requestId: 'req-guardian-001',
|
|
2059
|
+
requestCode: 'REQ-GU',
|
|
2060
|
+
state: 'pending',
|
|
2061
|
+
questionText: 'Approve tool: bash',
|
|
2062
|
+
toolName: 'bash',
|
|
2063
|
+
actions: [
|
|
2064
|
+
{ action: 'approve_once', label: 'Approve once' },
|
|
2065
|
+
{ action: 'reject', label: 'Reject' },
|
|
2066
|
+
],
|
|
2067
|
+
expiresAt: 1700100000000,
|
|
2068
|
+
conversationId: 'conv-guardian-001',
|
|
2069
|
+
callSessionId: null,
|
|
2070
|
+
},
|
|
2071
|
+
],
|
|
2072
|
+
},
|
|
2073
|
+
guardian_action_decision_response: {
|
|
2074
|
+
type: 'guardian_action_decision_response',
|
|
2075
|
+
applied: true,
|
|
2076
|
+
reason: undefined,
|
|
2077
|
+
requestId: 'req-guardian-001',
|
|
2078
|
+
userText: undefined,
|
|
2079
|
+
},
|
|
2022
2080
|
};
|
|
2023
2081
|
|
|
2024
2082
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for notification decision fallback copy.
|
|
3
|
+
*
|
|
4
|
+
* Ensures fallback decisions still produce human-friendly copy when the
|
|
5
|
+
* decision-model call is unavailable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
mock.module('../channels/config.js', () => ({
|
|
11
|
+
getDeliverableChannels: () => ['vellum', 'telegram', 'sms'],
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module('../config/loader.js', () => ({
|
|
15
|
+
getConfig: () => ({
|
|
16
|
+
notifications: {
|
|
17
|
+
decisionModelIntent: 'latency-optimized',
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module('../notifications/decisions-store.js', () => ({
|
|
23
|
+
createDecision: () => {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module('../notifications/preference-summary.js', () => ({
|
|
27
|
+
getPreferenceSummary: () => undefined,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
mock.module('../notifications/thread-candidates.js', () => ({
|
|
31
|
+
buildThreadCandidates: () => undefined,
|
|
32
|
+
serializeCandidatesForPrompt: () => undefined,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module('../providers/provider-send-message.js', () => ({
|
|
36
|
+
getConfiguredProvider: () => null,
|
|
37
|
+
createTimeout: () => ({
|
|
38
|
+
signal: new AbortController().signal,
|
|
39
|
+
cleanup: () => {},
|
|
40
|
+
}),
|
|
41
|
+
extractToolUse: () => null,
|
|
42
|
+
userMessage: (text: string) => ({ role: 'user', content: text }),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module('../util/logger.js', () => ({
|
|
46
|
+
getLogger: () =>
|
|
47
|
+
new Proxy({} as Record<string, unknown>, {
|
|
48
|
+
get: () => () => {},
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
import { evaluateSignal } from '../notifications/decision-engine.js';
|
|
53
|
+
import type { NotificationSignal } from '../notifications/signal.js';
|
|
54
|
+
import type { NotificationChannel } from '../notifications/types.js';
|
|
55
|
+
|
|
56
|
+
function makeSignal(overrides?: Partial<NotificationSignal>): NotificationSignal {
|
|
57
|
+
return {
|
|
58
|
+
signalId: 'sig-fallback-guardian-1',
|
|
59
|
+
assistantId: 'self',
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
sourceChannel: 'voice',
|
|
62
|
+
sourceSessionId: 'call-session-1',
|
|
63
|
+
sourceEventName: 'guardian.question',
|
|
64
|
+
contextPayload: {
|
|
65
|
+
questionText: 'What is the gate code?',
|
|
66
|
+
},
|
|
67
|
+
attentionHints: {
|
|
68
|
+
requiresAction: true,
|
|
69
|
+
urgency: 'high',
|
|
70
|
+
isAsyncBackground: false,
|
|
71
|
+
visibleInSourceNow: false,
|
|
72
|
+
},
|
|
73
|
+
...overrides,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('notification decision fallback copy', () => {
|
|
78
|
+
test('uses human-friendly template copy for guardian.question', async () => {
|
|
79
|
+
const signal = makeSignal();
|
|
80
|
+
const decision = await evaluateSignal(signal, ['vellum'] as NotificationChannel[]);
|
|
81
|
+
|
|
82
|
+
expect(decision.fallbackUsed).toBe(true);
|
|
83
|
+
expect(decision.renderedCopy.vellum?.title).toBe('Guardian Question');
|
|
84
|
+
expect(decision.renderedCopy.vellum?.body).toBe('What is the gate code?');
|
|
85
|
+
expect(decision.renderedCopy.vellum?.title).not.toBe('guardian.question');
|
|
86
|
+
expect(decision.renderedCopy.vellum?.body).not.toContain('Action required: guardian.question');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
evaluateRemoteSkillInstall,
|
|
5
|
+
filterInstallableRemoteSkills,
|
|
6
|
+
type RemoteSkillPolicy,
|
|
7
|
+
} from '../skills/remote-skill-policy.js';
|
|
8
|
+
|
|
9
|
+
describe('remote skill policy — clawhub', () => {
|
|
10
|
+
const policy: RemoteSkillPolicy = {
|
|
11
|
+
blockSuspicious: true,
|
|
12
|
+
blockMalware: true,
|
|
13
|
+
maxSkillsShRisk: 'medium',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
test('suspicious skills are excluded from installable list', () => {
|
|
17
|
+
const candidates = [
|
|
18
|
+
{
|
|
19
|
+
provider: 'clawhub' as const,
|
|
20
|
+
slug: 'safe-skill',
|
|
21
|
+
moderation: { isSuspicious: false, isMalwareBlocked: false },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
provider: 'clawhub' as const,
|
|
25
|
+
slug: 'suspicious-skill',
|
|
26
|
+
moderation: { isSuspicious: true, isMalwareBlocked: false },
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const installable = filterInstallableRemoteSkills(candidates, policy);
|
|
31
|
+
expect(installable.map((skill) => skill.slug)).toEqual(['safe-skill']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('suspicious skills are not installable when installation is attempted', () => {
|
|
35
|
+
const decision = evaluateRemoteSkillInstall(
|
|
36
|
+
{
|
|
37
|
+
provider: 'clawhub',
|
|
38
|
+
slug: 'suspicious-skill',
|
|
39
|
+
moderation: { isSuspicious: true, isMalwareBlocked: false },
|
|
40
|
+
},
|
|
41
|
+
policy,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(decision).toEqual({ ok: false, reason: 'clawhub_suspicious' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('malware-blocked skills are excluded from installable list and blocked on install', () => {
|
|
48
|
+
const candidates = [
|
|
49
|
+
{
|
|
50
|
+
provider: 'clawhub' as const,
|
|
51
|
+
slug: 'malware-skill',
|
|
52
|
+
moderation: { isSuspicious: false, isMalwareBlocked: true },
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
expect(filterInstallableRemoteSkills(candidates, policy)).toEqual([]);
|
|
57
|
+
|
|
58
|
+
const decision = evaluateRemoteSkillInstall(candidates[0], policy);
|
|
59
|
+
expect(decision).toEqual({ ok: false, reason: 'clawhub_malware_blocked' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('clawhub skill with undefined moderation is blocked (fail-closed)', () => {
|
|
63
|
+
const decision = evaluateRemoteSkillInstall(
|
|
64
|
+
{
|
|
65
|
+
provider: 'clawhub',
|
|
66
|
+
slug: 'no-moderation-skill',
|
|
67
|
+
moderation: undefined,
|
|
68
|
+
},
|
|
69
|
+
policy,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(decision).toEqual({ ok: false, reason: 'clawhub_moderation_missing' });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('clawhub skill with null moderation is blocked (fail-closed)', () => {
|
|
76
|
+
const decision = evaluateRemoteSkillInstall(
|
|
77
|
+
{
|
|
78
|
+
provider: 'clawhub',
|
|
79
|
+
slug: 'null-moderation-skill',
|
|
80
|
+
moderation: null,
|
|
81
|
+
},
|
|
82
|
+
policy,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(decision).toEqual({ ok: false, reason: 'clawhub_moderation_missing' });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('clawhub skill without moderation property is blocked (fail-closed)', () => {
|
|
89
|
+
const decision = evaluateRemoteSkillInstall(
|
|
90
|
+
{
|
|
91
|
+
provider: 'clawhub',
|
|
92
|
+
slug: 'missing-moderation-skill',
|
|
93
|
+
},
|
|
94
|
+
policy,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(decision).toEqual({ ok: false, reason: 'clawhub_moderation_missing' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('clawhub skills with missing moderation are excluded from installable list', () => {
|
|
101
|
+
const candidates = [
|
|
102
|
+
{
|
|
103
|
+
provider: 'clawhub' as const,
|
|
104
|
+
slug: 'safe-skill',
|
|
105
|
+
moderation: { isSuspicious: false, isMalwareBlocked: false },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
provider: 'clawhub' as const,
|
|
109
|
+
slug: 'no-moderation-skill',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const installable = filterInstallableRemoteSkills(candidates, policy);
|
|
114
|
+
expect(installable.map((skill) => skill.slug)).toEqual(['safe-skill']);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('remote skill policy — skills.sh', () => {
|
|
119
|
+
const policy: RemoteSkillPolicy = {
|
|
120
|
+
blockSuspicious: true,
|
|
121
|
+
blockMalware: true,
|
|
122
|
+
maxSkillsShRisk: 'medium',
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
test('high-risk skills are excluded from installable list', () => {
|
|
126
|
+
const candidates = [
|
|
127
|
+
{
|
|
128
|
+
provider: 'skillssh' as const,
|
|
129
|
+
slug: 'safe-skill',
|
|
130
|
+
audit: { risk: 'low' as const },
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
provider: 'skillssh' as const,
|
|
134
|
+
slug: 'suspicious-skill',
|
|
135
|
+
audit: { risk: 'high' as const },
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const installable = filterInstallableRemoteSkills(candidates, policy);
|
|
140
|
+
expect(installable.map((skill) => skill.slug)).toEqual(['safe-skill']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('high-risk skills are not installable when installation is attempted', () => {
|
|
144
|
+
const decision = evaluateRemoteSkillInstall(
|
|
145
|
+
{
|
|
146
|
+
provider: 'skillssh',
|
|
147
|
+
slug: 'suspicious-skill',
|
|
148
|
+
audit: { risk: 'high' },
|
|
149
|
+
},
|
|
150
|
+
policy,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(decision).toEqual({ ok: false, reason: 'skillssh_risk_exceeds_threshold' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('unknown risk is treated as suspicious and blocked by default', () => {
|
|
157
|
+
const decision = evaluateRemoteSkillInstall(
|
|
158
|
+
{
|
|
159
|
+
provider: 'skillssh',
|
|
160
|
+
slug: 'unknown-risk-skill',
|
|
161
|
+
audit: { risk: 'unknown' },
|
|
162
|
+
},
|
|
163
|
+
policy,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(decision).toEqual({ ok: false, reason: 'skillssh_risk_exceeds_threshold' });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('risk threshold is enforced even when blockSuspicious is false', () => {
|
|
170
|
+
const permissivePolicy: RemoteSkillPolicy = {
|
|
171
|
+
blockSuspicious: false,
|
|
172
|
+
blockMalware: false,
|
|
173
|
+
maxSkillsShRisk: 'medium',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const decision = evaluateRemoteSkillInstall(
|
|
177
|
+
{
|
|
178
|
+
provider: 'skillssh',
|
|
179
|
+
slug: 'high-risk-skill',
|
|
180
|
+
audit: { risk: 'high' },
|
|
181
|
+
},
|
|
182
|
+
permissivePolicy,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(decision).toEqual({ ok: false, reason: 'skillssh_risk_exceeds_threshold' });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('prototype property risk label is treated as unknown and blocked', () => {
|
|
189
|
+
const decision = evaluateRemoteSkillInstall(
|
|
190
|
+
{
|
|
191
|
+
provider: 'skillssh',
|
|
192
|
+
slug: 'proto-risk-skill',
|
|
193
|
+
// "toString" exists on Object.prototype — must not be treated as a known risk label
|
|
194
|
+
audit: { risk: 'toString' as never },
|
|
195
|
+
},
|
|
196
|
+
policy,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(decision).toEqual({ ok: false, reason: 'skillssh_risk_exceeds_threshold' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('unrecognized risk string is coerced to unknown and blocked', () => {
|
|
203
|
+
const decision = evaluateRemoteSkillInstall(
|
|
204
|
+
{
|
|
205
|
+
provider: 'skillssh',
|
|
206
|
+
slug: 'bogus-risk-skill',
|
|
207
|
+
// Cast to bypass type checking — simulates a provider returning a novel risk label
|
|
208
|
+
audit: { risk: 'super-duper-risky' as never },
|
|
209
|
+
},
|
|
210
|
+
policy,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(decision).toEqual({ ok: false, reason: 'skillssh_risk_exceeds_threshold' });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -3,14 +3,10 @@ import * as realChildProcess from 'node:child_process';
|
|
|
3
3
|
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
4
4
|
|
|
5
5
|
const execSyncMock = mock((_command: string, _opts?: unknown): unknown => undefined);
|
|
6
|
-
const execFileSyncMock = mock(
|
|
7
|
-
(_file: string, _args?: readonly string[], _opts?: unknown): unknown => undefined,
|
|
8
|
-
);
|
|
9
6
|
|
|
10
7
|
mock.module('node:child_process', () => ({
|
|
11
8
|
...realChildProcess,
|
|
12
9
|
execSync: execSyncMock,
|
|
13
|
-
execFileSync: execFileSyncMock,
|
|
14
10
|
}));
|
|
15
11
|
|
|
16
12
|
// Mock platform detection — default to macOS
|
|
@@ -36,18 +32,8 @@ mock.module('../util/platform.js', () => ({
|
|
|
36
32
|
// Mock config loader — return a config with sandbox settings
|
|
37
33
|
let mockSandboxConfig: {
|
|
38
34
|
enabled: boolean;
|
|
39
|
-
backend: 'native' | 'docker';
|
|
40
|
-
docker: { image: string; cpus: number; memoryMb: number; pidsLimit: number; network: 'none' | 'bridge' };
|
|
41
35
|
} = {
|
|
42
36
|
enabled: true,
|
|
43
|
-
backend: 'native',
|
|
44
|
-
docker: {
|
|
45
|
-
image: 'vellum-sandbox:latest',
|
|
46
|
-
cpus: 1,
|
|
47
|
-
memoryMb: 512,
|
|
48
|
-
pidsLimit: 256,
|
|
49
|
-
network: 'none',
|
|
50
|
-
},
|
|
51
37
|
};
|
|
52
38
|
|
|
53
39
|
mock.module('../config/loader.js', () => ({
|
|
@@ -72,24 +58,13 @@ const { runSandboxDiagnostics } = await import(
|
|
|
72
58
|
|
|
73
59
|
beforeEach(() => {
|
|
74
60
|
execSyncMock.mockReset();
|
|
75
|
-
execFileSyncMock.mockReset();
|
|
76
61
|
mockIsMacOS = true;
|
|
77
62
|
mockIsLinux = false;
|
|
78
63
|
mockSandboxConfig = {
|
|
79
64
|
enabled: true,
|
|
80
|
-
backend: 'native',
|
|
81
|
-
docker: {
|
|
82
|
-
image: 'vellum-sandbox:latest',
|
|
83
|
-
cpus: 1,
|
|
84
|
-
memoryMb: 512,
|
|
85
|
-
pidsLimit: 256,
|
|
86
|
-
network: 'none',
|
|
87
|
-
},
|
|
88
65
|
};
|
|
89
|
-
// Default: all commands succeed.
|
|
90
|
-
|
|
91
|
-
execSyncMock.mockImplementation(() => 'Docker version 24.0.7, build afdd53b');
|
|
92
|
-
execFileSyncMock.mockImplementation(() => 'ok\n');
|
|
66
|
+
// Default: all commands succeed.
|
|
67
|
+
execSyncMock.mockImplementation(() => undefined);
|
|
93
68
|
});
|
|
94
69
|
|
|
95
70
|
describe('runSandboxDiagnostics — config reporting', () => {
|
|
@@ -103,22 +78,6 @@ describe('runSandboxDiagnostics — config reporting', () => {
|
|
|
103
78
|
const result = runSandboxDiagnostics();
|
|
104
79
|
expect(result.config.enabled).toBe(false);
|
|
105
80
|
});
|
|
106
|
-
|
|
107
|
-
test('reports configured backend', () => {
|
|
108
|
-
const result = runSandboxDiagnostics();
|
|
109
|
-
expect(result.config.backend).toBe('native');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('reports docker backend when configured', () => {
|
|
113
|
-
mockSandboxConfig.backend = 'docker';
|
|
114
|
-
const result = runSandboxDiagnostics();
|
|
115
|
-
expect(result.config.backend).toBe('docker');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('reports docker image', () => {
|
|
119
|
-
const result = runSandboxDiagnostics();
|
|
120
|
-
expect(result.config.dockerImage).toBe('vellum-sandbox:latest');
|
|
121
|
-
});
|
|
122
81
|
});
|
|
123
82
|
|
|
124
83
|
describe('runSandboxDiagnostics — active backend reason', () => {
|
|
@@ -127,12 +86,6 @@ describe('runSandboxDiagnostics — active backend reason', () => {
|
|
|
127
86
|
expect(result.activeBackendReason).toContain('Native backend');
|
|
128
87
|
});
|
|
129
88
|
|
|
130
|
-
test('explains docker backend selection', () => {
|
|
131
|
-
mockSandboxConfig.backend = 'docker';
|
|
132
|
-
const result = runSandboxDiagnostics();
|
|
133
|
-
expect(result.activeBackendReason).toContain('Docker backend');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
89
|
test('explains when sandbox is disabled', () => {
|
|
137
90
|
mockSandboxConfig.enabled = false;
|
|
138
91
|
const result = runSandboxDiagnostics();
|
|
@@ -203,207 +156,11 @@ describe('runSandboxDiagnostics — native backend check (unsupported OS)', () =
|
|
|
203
156
|
});
|
|
204
157
|
});
|
|
205
158
|
|
|
206
|
-
describe('runSandboxDiagnostics —
|
|
207
|
-
test('
|
|
208
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
209
|
-
if (typeof cmd === 'string' && cmd === 'docker --version') {
|
|
210
|
-
return 'Docker version 24.0.7, build afdd53b';
|
|
211
|
-
}
|
|
212
|
-
return undefined;
|
|
213
|
-
});
|
|
214
|
-
const result = runSandboxDiagnostics();
|
|
215
|
-
const cliCheck = result.checks.find((c) => c.label === 'Docker CLI installed');
|
|
216
|
-
expect(cliCheck).toBeDefined();
|
|
217
|
-
expect(cliCheck!.ok).toBe(true);
|
|
218
|
-
expect(cliCheck!.detail).toContain('Docker version');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('fails when docker CLI is not found', () => {
|
|
222
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
223
|
-
if (typeof cmd === 'string' && cmd === 'docker --version') {
|
|
224
|
-
throw new Error('command not found: docker');
|
|
225
|
-
}
|
|
226
|
-
return undefined;
|
|
227
|
-
});
|
|
228
|
-
const result = runSandboxDiagnostics();
|
|
229
|
-
const cliCheck = result.checks.find((c) => c.label === 'Docker CLI installed');
|
|
230
|
-
expect(cliCheck).toBeDefined();
|
|
231
|
-
expect(cliCheck!.ok).toBe(false);
|
|
232
|
-
expect(cliCheck!.detail).toContain('not found');
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe('runSandboxDiagnostics — Docker daemon check', () => {
|
|
237
|
-
test('passes when daemon is reachable', () => {
|
|
238
|
-
const result = runSandboxDiagnostics();
|
|
239
|
-
const daemonCheck = result.checks.find((c) => c.label === 'Docker daemon running');
|
|
240
|
-
expect(daemonCheck).toBeDefined();
|
|
241
|
-
expect(daemonCheck!.ok).toBe(true);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test('fails when daemon is not running', () => {
|
|
245
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
246
|
-
if (typeof cmd === 'string' && cmd === 'docker info') {
|
|
247
|
-
throw new Error('Cannot connect to the Docker daemon');
|
|
248
|
-
}
|
|
249
|
-
return 'Docker version 24.0.7';
|
|
250
|
-
});
|
|
251
|
-
const result = runSandboxDiagnostics();
|
|
252
|
-
const daemonCheck = result.checks.find((c) => c.label === 'Docker daemon running');
|
|
253
|
-
expect(daemonCheck).toBeDefined();
|
|
254
|
-
expect(daemonCheck!.ok).toBe(false);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test('skipped when CLI is not available', () => {
|
|
258
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
259
|
-
if (typeof cmd === 'string' && cmd.includes('docker')) {
|
|
260
|
-
throw new Error('command not found');
|
|
261
|
-
}
|
|
262
|
-
return undefined;
|
|
263
|
-
});
|
|
264
|
-
const result = runSandboxDiagnostics();
|
|
265
|
-
const daemonCheck = result.checks.find((c) => c.label === 'Docker daemon running');
|
|
266
|
-
expect(daemonCheck).toBeUndefined();
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('runSandboxDiagnostics — Docker image check', () => {
|
|
271
|
-
test('passes when image is available locally', () => {
|
|
272
|
-
const result = runSandboxDiagnostics();
|
|
273
|
-
const imageCheck = result.checks.find((c) => c.label.includes('Docker image available'));
|
|
274
|
-
expect(imageCheck).toBeDefined();
|
|
275
|
-
expect(imageCheck!.ok).toBe(true);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test('fails when image is not available', () => {
|
|
279
|
-
execFileSyncMock.mockImplementation(
|
|
280
|
-
(file: string, args?: readonly string[]) => {
|
|
281
|
-
if (file === 'docker' && Array.isArray(args) && args.includes('inspect')) {
|
|
282
|
-
throw new Error('No such image');
|
|
283
|
-
}
|
|
284
|
-
return 'ok\n';
|
|
285
|
-
},
|
|
286
|
-
);
|
|
287
|
-
const result = runSandboxDiagnostics();
|
|
288
|
-
const imageCheck = result.checks.find((c) => c.label.includes('Docker image available'));
|
|
289
|
-
expect(imageCheck).toBeDefined();
|
|
290
|
-
expect(imageCheck!.ok).toBe(false);
|
|
291
|
-
expect(imageCheck!.detail).toContain('docker build');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('includes configured image name in label', () => {
|
|
295
|
-
mockSandboxConfig.docker.image = 'alpine:3.19';
|
|
296
|
-
const result = runSandboxDiagnostics();
|
|
297
|
-
const imageCheck = result.checks.find((c) => c.label.includes('Docker image available'));
|
|
298
|
-
expect(imageCheck).toBeDefined();
|
|
299
|
-
expect(imageCheck!.label).toContain('alpine:3.19');
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test('skipped when daemon is not running', () => {
|
|
303
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
304
|
-
if (typeof cmd === 'string' && cmd === 'docker info') {
|
|
305
|
-
throw new Error('Cannot connect');
|
|
306
|
-
}
|
|
307
|
-
return 'Docker version 24.0.7';
|
|
308
|
-
});
|
|
309
|
-
const result = runSandboxDiagnostics();
|
|
310
|
-
const imageCheck = result.checks.find((c) => c.label.includes('Docker image available'));
|
|
311
|
-
expect(imageCheck).toBeUndefined();
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
describe('runSandboxDiagnostics — Docker mount writable check', () => {
|
|
316
|
-
test('passes when mount probe succeeds', () => {
|
|
317
|
-
const result = runSandboxDiagnostics();
|
|
318
|
-
const mountCheck = result.checks.find((c) => c.label === 'Docker mount writable');
|
|
319
|
-
expect(mountCheck).toBeDefined();
|
|
320
|
-
expect(mountCheck!.ok).toBe(true);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test('uses configured image and sandbox working dir for mount probe', () => {
|
|
324
|
-
mockSandboxConfig.docker.image = 'alpine:3.19';
|
|
325
|
-
runSandboxDiagnostics();
|
|
326
|
-
const runCall = execFileSyncMock.mock.calls.find(
|
|
327
|
-
(call: unknown[]) => call[0] === 'docker' && Array.isArray(call[1]) && call[1].includes('run'),
|
|
328
|
-
);
|
|
329
|
-
expect(runCall).toBeDefined();
|
|
330
|
-
const args = runCall![1] as string[];
|
|
331
|
-
expect(args).toContain('alpine:3.19');
|
|
332
|
-
// Mount source should be the sandbox working dir (getSandboxWorkingDir)
|
|
333
|
-
const mountArg = args.find((a: string) => a.startsWith('type=bind'));
|
|
334
|
-
expect(mountArg).toContain('/tmp/vellum-test/workspace');
|
|
335
|
-
// Probe command should be 'test -w /workspace' matching runtime preflight
|
|
336
|
-
expect(args).toContain('test');
|
|
337
|
-
expect(args).toContain('-w');
|
|
338
|
-
expect(args).toContain('/workspace');
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
test('fails when mount probe errors', () => {
|
|
342
|
-
execFileSyncMock.mockImplementation(
|
|
343
|
-
(file: string, args?: readonly string[]) => {
|
|
344
|
-
if (file === 'docker' && Array.isArray(args) && args.includes('run')) {
|
|
345
|
-
throw new Error('mount failed');
|
|
346
|
-
}
|
|
347
|
-
return undefined;
|
|
348
|
-
},
|
|
349
|
-
);
|
|
350
|
-
const result = runSandboxDiagnostics();
|
|
351
|
-
const mountCheck = result.checks.find((c) => c.label === 'Docker mount writable');
|
|
352
|
-
expect(mountCheck).toBeDefined();
|
|
353
|
-
expect(mountCheck!.ok).toBe(false);
|
|
354
|
-
expect(mountCheck!.detail).toContain('File Sharing');
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test('skipped when daemon is not running', () => {
|
|
358
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
359
|
-
if (typeof cmd === 'string' && cmd === 'docker info') {
|
|
360
|
-
throw new Error('Cannot connect');
|
|
361
|
-
}
|
|
362
|
-
return 'Docker version 24.0.7';
|
|
363
|
-
});
|
|
364
|
-
const result = runSandboxDiagnostics();
|
|
365
|
-
const mountCheck = result.checks.find((c) => c.label === 'Docker mount writable');
|
|
366
|
-
expect(mountCheck).toBeUndefined();
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
describe('runSandboxDiagnostics — check cascade', () => {
|
|
371
|
-
test('Docker daemon, image, and run checks are skipped when CLI is missing', () => {
|
|
372
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
373
|
-
if (typeof cmd === 'string' && cmd.includes('docker')) {
|
|
374
|
-
throw new Error('not found');
|
|
375
|
-
}
|
|
376
|
-
return undefined;
|
|
377
|
-
});
|
|
378
|
-
const result = runSandboxDiagnostics();
|
|
379
|
-
const labels = result.checks.map((c) => c.label);
|
|
380
|
-
expect(labels).toContain('Docker CLI installed');
|
|
381
|
-
expect(labels).not.toContain('Docker daemon running');
|
|
382
|
-
expect(labels.find((l) => l.includes('Docker image'))).toBeUndefined();
|
|
383
|
-
expect(labels).not.toContain('Docker mount writable');
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
test('image and run checks are skipped when daemon is down', () => {
|
|
387
|
-
execSyncMock.mockImplementation((cmd: string) => {
|
|
388
|
-
if (typeof cmd === 'string' && cmd === 'docker info') {
|
|
389
|
-
throw new Error('Cannot connect');
|
|
390
|
-
}
|
|
391
|
-
return 'Docker version 24.0.7';
|
|
392
|
-
});
|
|
393
|
-
const result = runSandboxDiagnostics();
|
|
394
|
-
const labels = result.checks.map((c) => c.label);
|
|
395
|
-
expect(labels).toContain('Docker CLI installed');
|
|
396
|
-
expect(labels).toContain('Docker daemon running');
|
|
397
|
-
expect(labels.find((l) => l.includes('Docker image'))).toBeUndefined();
|
|
398
|
-
expect(labels).not.toContain('Docker mount writable');
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('all Docker checks run when everything works', () => {
|
|
159
|
+
describe('runSandboxDiagnostics — only native checks', () => {
|
|
160
|
+
test('only includes native backend check', () => {
|
|
402
161
|
const result = runSandboxDiagnostics();
|
|
403
162
|
const labels = result.checks.map((c) => c.label);
|
|
404
|
-
expect(labels).
|
|
405
|
-
expect(labels).toContain('
|
|
406
|
-
expect(labels.find((l) => l.includes('Docker image'))).toBeDefined();
|
|
407
|
-
expect(labels).toContain('Docker mount writable');
|
|
163
|
+
expect(labels).toHaveLength(1);
|
|
164
|
+
expect(labels[0]).toContain('Native sandbox');
|
|
408
165
|
});
|
|
409
166
|
});
|