@vellumai/assistant 0.3.15 → 0.3.18
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 +211 -12
- package/Dockerfile +1 -1
- package/README.md +11 -5
- package/docs/architecture/http-token-refresh.md +274 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +328 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +19 -15
- package/src/__tests__/checker.test.ts +103 -48
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +356 -0
- package/src/__tests__/conversation-pairing.test.ts +127 -27
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-dispatch.test.ts +120 -0
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +23 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +281 -0
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +138 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +38 -22
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +323 -0
- package/src/__tests__/update-template-contract.test.ts +24 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +241 -39
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/guardian-dispatch.ts +8 -0
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +8 -6
- package/src/cli/core-commands.ts +43 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +15 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +54 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +97 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +4 -2
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +219 -8
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/identity.ts +45 -25
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +6 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +314 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +31 -9
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +546 -59
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +60 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +35 -567
- package/src/memory/channel-guardian-store.ts +63 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +44 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +33 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +136 -0
- package/src/memory/guardian-action-store.ts +418 -5
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +521 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +10 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +4 -0
- package/src/memory/schema.ts +31 -8
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +159 -18
- package/src/notifications/broadcaster.ts +69 -33
- package/src/notifications/conversation-pairing.ts +99 -21
- package/src/notifications/decision-engine.ts +176 -8
- package/src/notifications/deliveries-store.ts +39 -8
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +269 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/checker.ts +1 -16
- package/src/permissions/defaults.ts +25 -5
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +78 -16
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +1 -1
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +271 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/version.ts +29 -2
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -881,7 +881,7 @@ describe('SMS channel approval decisions', () => {
|
|
|
881
881
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
882
882
|
|
|
883
883
|
describe('SMS guardian verify intercept', () => {
|
|
884
|
-
test('
|
|
884
|
+
test('verification code reply works with sourceChannel sms', async () => {
|
|
885
885
|
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
886
886
|
const { secret } = createVerificationChallenge('self', 'sms');
|
|
887
887
|
|
|
@@ -898,7 +898,7 @@ describe('SMS guardian verify intercept', () => {
|
|
|
898
898
|
interface: 'sms',
|
|
899
899
|
externalChatId: 'sms-chat-verify',
|
|
900
900
|
externalMessageId: `msg-${Date.now()}-${Math.random()}`,
|
|
901
|
-
content:
|
|
901
|
+
content: secret,
|
|
902
902
|
senderExternalUserId: 'sms-user-42',
|
|
903
903
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
904
904
|
}),
|
|
@@ -921,7 +921,11 @@ describe('SMS guardian verify intercept', () => {
|
|
|
921
921
|
deliverSpy.mockRestore();
|
|
922
922
|
});
|
|
923
923
|
|
|
924
|
-
test('
|
|
924
|
+
test('invalid verification code returns failed via SMS', async () => {
|
|
925
|
+
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
926
|
+
// Ensure there is a pending challenge so bare-code verification is intercepted.
|
|
927
|
+
createVerificationChallenge('self', 'sms');
|
|
928
|
+
|
|
925
929
|
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
926
930
|
|
|
927
931
|
const req = new Request('http://localhost/channels/inbound', {
|
|
@@ -935,7 +939,7 @@ describe('SMS guardian verify intercept', () => {
|
|
|
935
939
|
interface: 'sms',
|
|
936
940
|
externalChatId: 'sms-chat-verify-fail',
|
|
937
941
|
externalMessageId: `msg-${Date.now()}-${Math.random()}`,
|
|
938
|
-
content: '
|
|
942
|
+
content: '000000',
|
|
939
943
|
senderExternalUserId: 'sms-user-43',
|
|
940
944
|
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
941
945
|
}),
|
|
@@ -956,6 +960,51 @@ describe('SMS guardian verify intercept', () => {
|
|
|
956
960
|
|
|
957
961
|
deliverSpy.mockRestore();
|
|
958
962
|
});
|
|
963
|
+
|
|
964
|
+
test('64-char hex verification codes are intercepted when a pending challenge exists', async () => {
|
|
965
|
+
const { createHash, randomBytes } = await import('node:crypto');
|
|
966
|
+
const { createChallenge } = await import('../memory/channel-guardian-store.js');
|
|
967
|
+
|
|
968
|
+
const secret = randomBytes(32).toString('hex');
|
|
969
|
+
const challengeHash = createHash('sha256').update(secret).digest('hex');
|
|
970
|
+
createChallenge({
|
|
971
|
+
id: `challenge-hex-${Date.now()}`,
|
|
972
|
+
assistantId: 'self',
|
|
973
|
+
channel: 'sms',
|
|
974
|
+
challengeHash,
|
|
975
|
+
expiresAt: Date.now() + 600_000,
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
let processMessageCalled = false;
|
|
979
|
+
const processMessage = async () => {
|
|
980
|
+
processMessageCalled = true;
|
|
981
|
+
return { messageId: 'msg-hex-not-verify' };
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
const req = new Request('http://localhost/channels/inbound', {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: {
|
|
987
|
+
'Content-Type': 'application/json',
|
|
988
|
+
'X-Gateway-Origin': TEST_BEARER_TOKEN,
|
|
989
|
+
},
|
|
990
|
+
body: JSON.stringify({
|
|
991
|
+
sourceChannel: 'sms',
|
|
992
|
+
interface: 'sms',
|
|
993
|
+
externalChatId: 'sms-chat-hex-message',
|
|
994
|
+
externalMessageId: `msg-${Date.now()}-${Math.random()}`,
|
|
995
|
+
content: secret,
|
|
996
|
+
senderExternalUserId: 'sms-user-hex',
|
|
997
|
+
replyCallbackUrl: 'https://gateway.test/deliver',
|
|
998
|
+
}),
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
const res = await handleChannelInbound(req, processMessage, TEST_BEARER_TOKEN);
|
|
1002
|
+
const body = await res.json() as Record<string, unknown>;
|
|
1003
|
+
|
|
1004
|
+
expect(body.accepted).toBe(true);
|
|
1005
|
+
expect(body.guardianVerification).toBe('verified');
|
|
1006
|
+
expect(processMessageCalled).toBe(false);
|
|
1007
|
+
});
|
|
959
1008
|
});
|
|
960
1009
|
|
|
961
1010
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1242,14 +1291,14 @@ describe('deliver-once idempotency guard', () => {
|
|
|
1242
1291
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1243
1292
|
|
|
1244
1293
|
describe('assistant-scoped guardian verification via handleChannelInbound', () => {
|
|
1245
|
-
test('
|
|
1294
|
+
test('verification code uses the threaded assistantId (default: self)', async () => {
|
|
1246
1295
|
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
1247
1296
|
const { secret } = createVerificationChallenge('self', 'telegram');
|
|
1248
1297
|
|
|
1249
1298
|
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
1250
1299
|
|
|
1251
1300
|
const req = makeInboundRequest({
|
|
1252
|
-
content:
|
|
1301
|
+
content: secret,
|
|
1253
1302
|
senderExternalUserId: 'user-default-asst',
|
|
1254
1303
|
});
|
|
1255
1304
|
|
|
@@ -1262,7 +1311,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1262
1311
|
deliverSpy.mockRestore();
|
|
1263
1312
|
});
|
|
1264
1313
|
|
|
1265
|
-
test('
|
|
1314
|
+
test('verification code with explicit assistantId resolves against that assistant', async () => {
|
|
1266
1315
|
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
1267
1316
|
const { getGuardianBinding } = await import('../runtime/channel-guardian-service.js');
|
|
1268
1317
|
|
|
@@ -1271,7 +1320,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1271
1320
|
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
1272
1321
|
|
|
1273
1322
|
const req = makeInboundRequest({
|
|
1274
|
-
content:
|
|
1323
|
+
content: secret,
|
|
1275
1324
|
senderExternalUserId: 'user-for-asst-x',
|
|
1276
1325
|
});
|
|
1277
1326
|
|
|
@@ -1288,7 +1337,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1288
1337
|
deliverSpy.mockRestore();
|
|
1289
1338
|
});
|
|
1290
1339
|
|
|
1291
|
-
test('cross-assistant challenge
|
|
1340
|
+
test('cross-assistant challenge code does not verify against a different assistant scope', async () => {
|
|
1292
1341
|
const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
|
|
1293
1342
|
|
|
1294
1343
|
const { secret } = createVerificationChallenge('asst-A-cross', 'telegram');
|
|
@@ -1296,7 +1345,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1296
1345
|
const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
|
|
1297
1346
|
|
|
1298
1347
|
const req = makeInboundRequest({
|
|
1299
|
-
content:
|
|
1348
|
+
content: secret,
|
|
1300
1349
|
senderExternalUserId: 'user-cross-test',
|
|
1301
1350
|
});
|
|
1302
1351
|
|
|
@@ -1304,7 +1353,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
|
|
|
1304
1353
|
const body = await res.json() as Record<string, unknown>;
|
|
1305
1354
|
|
|
1306
1355
|
expect(body.accepted).toBe(true);
|
|
1307
|
-
expect(body.guardianVerification).
|
|
1356
|
+
expect(body.guardianVerification).toBeUndefined();
|
|
1308
1357
|
|
|
1309
1358
|
deliverSpy.mockRestore();
|
|
1310
1359
|
});
|
|
@@ -2506,7 +2555,15 @@ describe('non-decision status reply for different channels', () => {
|
|
|
2506
2555
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2507
2556
|
|
|
2508
2557
|
describe('background channel processing approval prompts', () => {
|
|
2509
|
-
test('marks channel turns interactive and delivers approval prompt when confirmation is pending', async () => {
|
|
2558
|
+
test('marks guardian channel turns interactive and delivers approval prompt when confirmation is pending', async () => {
|
|
2559
|
+
// Set up a guardian binding so the sender is recognized as a guardian
|
|
2560
|
+
createBinding({
|
|
2561
|
+
assistantId: 'self',
|
|
2562
|
+
channel: 'telegram',
|
|
2563
|
+
guardianExternalUserId: 'telegram-user-default',
|
|
2564
|
+
guardianDeliveryChatId: 'chat-123',
|
|
2565
|
+
});
|
|
2566
|
+
|
|
2510
2567
|
const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
|
|
2511
2568
|
const processCalls: Array<{ options?: Record<string, unknown> }> = [];
|
|
2512
2569
|
|
|
@@ -2549,4 +2606,43 @@ describe('background channel processing approval prompts', () => {
|
|
|
2549
2606
|
|
|
2550
2607
|
deliverPromptSpy.mockRestore();
|
|
2551
2608
|
});
|
|
2609
|
+
|
|
2610
|
+
test('non-guardian channel turns are not interactive to prevent self-approval', async () => {
|
|
2611
|
+
// Set up a guardian binding for a DIFFERENT user so the sender is non-guardian
|
|
2612
|
+
createBinding({
|
|
2613
|
+
assistantId: 'self',
|
|
2614
|
+
channel: 'telegram',
|
|
2615
|
+
guardianExternalUserId: 'guardian-user-other',
|
|
2616
|
+
guardianDeliveryChatId: 'guardian-chat-other',
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
const processCalls: Array<{ options?: Record<string, unknown> }> = [];
|
|
2620
|
+
|
|
2621
|
+
const processMessage = mock(async (
|
|
2622
|
+
_conversationId: string,
|
|
2623
|
+
_content: string,
|
|
2624
|
+
_attachmentIds?: string[],
|
|
2625
|
+
options?: Record<string, unknown>,
|
|
2626
|
+
) => {
|
|
2627
|
+
processCalls.push({ options });
|
|
2628
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2629
|
+
return { messageId: 'msg-ng-1' };
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
const req = makeInboundRequest({
|
|
2633
|
+
content: 'run something',
|
|
2634
|
+
sourceChannel: 'telegram',
|
|
2635
|
+
replyCallbackUrl: 'https://gateway.test/deliver/telegram',
|
|
2636
|
+
externalMessageId: 'msg-ng-1',
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage, 'token');
|
|
2640
|
+
const body = await res.json() as Record<string, unknown>;
|
|
2641
|
+
expect(body.accepted).toBe(true);
|
|
2642
|
+
|
|
2643
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
2644
|
+
|
|
2645
|
+
expect(processCalls.length).toBeGreaterThan(0);
|
|
2646
|
+
expect(processCalls[0].options?.isInteractive).toBe(false);
|
|
2647
|
+
});
|
|
2552
2648
|
});
|
|
@@ -426,7 +426,7 @@ describe('guardian service challenge validation', () => {
|
|
|
426
426
|
);
|
|
427
427
|
|
|
428
428
|
expect(result.success).toBe(true);
|
|
429
|
-
if (result.success) {
|
|
429
|
+
if (result.success && result.verificationType === 'guardian') {
|
|
430
430
|
expect(result.bindingId).toBeDefined();
|
|
431
431
|
}
|
|
432
432
|
});
|
|
@@ -528,7 +528,7 @@ describe('guardian service challenge validation', () => {
|
|
|
528
528
|
);
|
|
529
529
|
|
|
530
530
|
expect(result.success).toBe(true);
|
|
531
|
-
if (result.success) {
|
|
531
|
+
if (result.success && result.verificationType === 'guardian') {
|
|
532
532
|
expect(result.bindingId).toBeDefined();
|
|
533
533
|
}
|
|
534
534
|
|
|
@@ -1616,7 +1616,7 @@ describe('voice guardian challenge validation', () => {
|
|
|
1616
1616
|
);
|
|
1617
1617
|
|
|
1618
1618
|
expect(result.success).toBe(true);
|
|
1619
|
-
if (result.success) {
|
|
1619
|
+
if (result.success && result.verificationType === 'guardian') {
|
|
1620
1620
|
expect(result.bindingId).toBeDefined();
|
|
1621
1621
|
}
|
|
1622
1622
|
});
|
|
@@ -2261,7 +2261,7 @@ describe('outbound verification sessions', () => {
|
|
|
2261
2261
|
);
|
|
2262
2262
|
|
|
2263
2263
|
expect(result.success).toBe(true);
|
|
2264
|
-
if (result.success) {
|
|
2264
|
+
if (result.success && result.verificationType === 'guardian') {
|
|
2265
2265
|
expect(result.bindingId).toBeDefined();
|
|
2266
2266
|
}
|
|
2267
2267
|
});
|
|
@@ -2740,7 +2740,12 @@ describe('outbound SMS verification', () => {
|
|
|
2740
2740
|
|
|
2741
2741
|
expect(result.success).toBe(true);
|
|
2742
2742
|
if (result.success) {
|
|
2743
|
-
|
|
2743
|
+
// Guardian outbound sessions (no verificationPurpose override) create
|
|
2744
|
+
// guardian bindings on success
|
|
2745
|
+
expect(result.verificationType).toBe('guardian');
|
|
2746
|
+
if (result.verificationType === 'guardian') {
|
|
2747
|
+
expect(result.bindingId).toBeDefined();
|
|
2748
|
+
}
|
|
2744
2749
|
}
|
|
2745
2750
|
});
|
|
2746
2751
|
|
|
@@ -3166,7 +3171,7 @@ describe('outbound Telegram verification', () => {
|
|
|
3166
3171
|
expect(result.success).toBe(false);
|
|
3167
3172
|
});
|
|
3168
3173
|
|
|
3169
|
-
test('
|
|
3174
|
+
test('inbound-only Telegram verification flow still works with bare code', () => {
|
|
3170
3175
|
// Create an inbound-only challenge (no outbound session, no expected identity)
|
|
3171
3176
|
const challengeResult = createVerificationChallenge('self', 'telegram');
|
|
3172
3177
|
|
|
@@ -3274,23 +3279,22 @@ describe('outbound Telegram verification', () => {
|
|
|
3274
3279
|
expect(revoked).toBeNull();
|
|
3275
3280
|
});
|
|
3276
3281
|
|
|
3277
|
-
test('telegram template
|
|
3282
|
+
test('telegram template does not include verification code in message', () => {
|
|
3278
3283
|
const msg = composeVerificationTelegram(
|
|
3279
3284
|
GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_CHALLENGE_REQUEST,
|
|
3280
3285
|
{ code: 'abc123', expiresInMinutes: 10 },
|
|
3281
3286
|
);
|
|
3282
|
-
|
|
3283
|
-
expect(msg).toContain('
|
|
3284
|
-
expect(msg).toContain('/guardian_verify abc123');
|
|
3287
|
+
expect(msg).not.toContain('abc123');
|
|
3288
|
+
expect(msg).not.toContain('guardian_verify');
|
|
3285
3289
|
});
|
|
3286
3290
|
|
|
3287
|
-
test('telegram resend template
|
|
3291
|
+
test('telegram resend template does not include code and includes (resent) suffix', () => {
|
|
3288
3292
|
const msg = composeVerificationTelegram(
|
|
3289
3293
|
GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_RESEND,
|
|
3290
3294
|
{ code: 'xyz789', expiresInMinutes: 5 },
|
|
3291
3295
|
);
|
|
3292
|
-
expect(msg).toContain('xyz789');
|
|
3293
|
-
expect(msg).toContain('
|
|
3296
|
+
expect(msg).not.toContain('xyz789');
|
|
3297
|
+
expect(msg).not.toContain('guardian_verify');
|
|
3294
3298
|
expect(msg).toContain('(resent)');
|
|
3295
3299
|
});
|
|
3296
3300
|
|
|
@@ -3300,7 +3304,7 @@ describe('outbound Telegram verification', () => {
|
|
|
3300
3304
|
{ code: '999999', expiresInMinutes: 10, assistantName: 'MyBot' },
|
|
3301
3305
|
);
|
|
3302
3306
|
expect(msg).toContain('Vellum assistant');
|
|
3303
|
-
expect(msg).toContain('999999');
|
|
3307
|
+
expect(msg).not.toContain('999999');
|
|
3304
3308
|
});
|
|
3305
3309
|
|
|
3306
3310
|
test('start_outbound for telegram with missing destination fails', () => {
|
|
@@ -3703,13 +3707,13 @@ describe('M1–M4 hardening coverage', () => {
|
|
|
3703
3707
|
// ── M2: bootstrap sessions use high-entropy hex secrets ──
|
|
3704
3708
|
|
|
3705
3709
|
test('bootstrap (pending_bootstrap) sessions use high-entropy hex secrets, identity-bound use 6-digit numeric', () => {
|
|
3706
|
-
// Pending bootstrap: high-entropy hex (32 bytes = 64 hex chars)
|
|
3707
3710
|
const bootstrapResult = createOutboundSession({
|
|
3708
3711
|
assistantId: 'asst-entropy',
|
|
3709
3712
|
channel: 'telegram',
|
|
3710
3713
|
identityBindingStatus: 'pending_bootstrap',
|
|
3711
3714
|
destinationAddress: '@testuser',
|
|
3712
3715
|
});
|
|
3716
|
+
// Pending bootstrap: high-entropy hex (32 bytes = 64 hex chars)
|
|
3713
3717
|
expect(bootstrapResult.secret.length).toBe(64);
|
|
3714
3718
|
expect(bootstrapResult.secret).toMatch(/^[a-f0-9]{64}$/);
|
|
3715
3719
|
|
|
@@ -272,8 +272,8 @@ describe('Permission Checker', () => {
|
|
|
272
272
|
expect(await classifyRisk('bash', { command: 'some_custom_tool' })).toBe(RiskLevel.Medium);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
test('rm (without -r) is
|
|
276
|
-
expect(await classifyRisk('bash', { command: 'rm file.txt' })).toBe(RiskLevel.
|
|
275
|
+
test('rm (without -r) is high risk', async () => {
|
|
276
|
+
expect(await classifyRisk('bash', { command: 'rm file.txt' })).toBe(RiskLevel.High);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
279
|
test('chmod is medium risk', async () => {
|
|
@@ -374,7 +374,7 @@ describe('Permission Checker', () => {
|
|
|
374
374
|
expect(high.matchedRule?.id).toBe('default:allow-bash-global');
|
|
375
375
|
|
|
376
376
|
// Medium risk
|
|
377
|
-
const med = await check('bash', { command: '
|
|
377
|
+
const med = await check('bash', { command: 'curl https://example.com' }, '/tmp');
|
|
378
378
|
expect(med.decision).toBe('allow');
|
|
379
379
|
expect(med.matchedRule?.id).toBe('default:allow-bash-global');
|
|
380
380
|
|
|
@@ -391,7 +391,7 @@ describe('Permission Checker', () => {
|
|
|
391
391
|
const high = await check('bash', { command: 'sudo rm -rf /' }, '/tmp');
|
|
392
392
|
expect(high.decision).toBe('prompt');
|
|
393
393
|
|
|
394
|
-
const med = await check('bash', { command: '
|
|
394
|
+
const med = await check('bash', { command: 'curl https://example.com' }, '/tmp');
|
|
395
395
|
expect(med.decision).toBe('prompt');
|
|
396
396
|
|
|
397
397
|
// Low risk still auto-allows via the normal risk-based fallback
|
|
@@ -409,17 +409,31 @@ describe('Permission Checker', () => {
|
|
|
409
409
|
expect(result.decision).toBe('prompt');
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
-
test('host_bash
|
|
412
|
+
test('host_bash rm is always high risk → prompt', async () => {
|
|
413
413
|
const result = await check('host_bash', { command: 'rm file.txt' }, '/tmp');
|
|
414
414
|
expect(result.decision).toBe('prompt');
|
|
415
|
+
expect(result.reason).toContain('High risk');
|
|
415
416
|
});
|
|
416
417
|
|
|
417
|
-
test('
|
|
418
|
+
test('plain rm (without -rf) is high risk and prompts despite default allow rule', async () => {
|
|
419
|
+
// Validates that ALL rm commands are escalated to High risk, not just rm -rf.
|
|
420
|
+
// The default allow rule for host_bash auto-approves Low/Medium risk but
|
|
421
|
+
// High risk always prompts.
|
|
422
|
+
const result = await check('host_bash', { command: 'rm single-file.txt' }, '/tmp');
|
|
423
|
+
expect(result.decision).toBe('prompt');
|
|
424
|
+
expect(result.reason).toContain('High risk');
|
|
425
|
+
|
|
426
|
+
// Also verify rm -rf still prompts
|
|
427
|
+
const rfResult = await check('host_bash', { command: 'rm -rf /tmp/dir' }, '/tmp');
|
|
428
|
+
expect(rfResult.decision).toBe('prompt');
|
|
429
|
+
expect(rfResult.reason).toContain('High risk');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('rm is high risk even with matching trust rule → prompt', async () => {
|
|
418
433
|
addRule('bash', 'rm *', '/tmp');
|
|
419
434
|
const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
|
|
420
|
-
expect(result.decision).toBe('
|
|
421
|
-
expect(result.reason).toContain('
|
|
422
|
-
expect(result.matchedRule).toBeDefined();
|
|
435
|
+
expect(result.decision).toBe('prompt');
|
|
436
|
+
expect(result.reason).toContain('High risk');
|
|
423
437
|
});
|
|
424
438
|
|
|
425
439
|
test('file_read → auto-allow', async () => {
|
|
@@ -489,11 +503,11 @@ describe('Permission Checker', () => {
|
|
|
489
503
|
expect(result.matchedRule?.id).toBe('default:ask-host_file_edit-global');
|
|
490
504
|
});
|
|
491
505
|
|
|
492
|
-
test('host_bash
|
|
506
|
+
test('host_bash auto-allows low risk via default allow rule', async () => {
|
|
493
507
|
const result = await check('host_bash', { command: 'ls' }, '/tmp');
|
|
494
|
-
expect(result.decision).toBe('
|
|
495
|
-
expect(result.reason).toContain('
|
|
496
|
-
expect(result.matchedRule?.id).toBe('default:
|
|
508
|
+
expect(result.decision).toBe('allow');
|
|
509
|
+
expect(result.reason).toContain('Matched trust rule');
|
|
510
|
+
expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
|
|
497
511
|
});
|
|
498
512
|
|
|
499
513
|
test('scaffold_managed_skill prompts by default via managed skill ask rule', async () => {
|
|
@@ -597,7 +611,7 @@ describe('Permission Checker', () => {
|
|
|
597
611
|
});
|
|
598
612
|
|
|
599
613
|
// Deny rule tests
|
|
600
|
-
test('deny rule blocks
|
|
614
|
+
test('deny rule blocks high-risk command', async () => {
|
|
601
615
|
addRule('bash', 'rm *', '/tmp', 'deny');
|
|
602
616
|
const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
|
|
603
617
|
expect(result.decision).toBe('deny');
|
|
@@ -764,16 +778,16 @@ describe('Permission Checker', () => {
|
|
|
764
778
|
|
|
765
779
|
// Priority-based rule resolution
|
|
766
780
|
test('higher-priority allow rule overrides lower-priority deny rule', async () => {
|
|
767
|
-
addRule('bash', '
|
|
768
|
-
addRule('bash', '
|
|
769
|
-
const result = await check('bash', { command: '
|
|
781
|
+
addRule('bash', 'chmod *', '/tmp', 'deny', 0);
|
|
782
|
+
addRule('bash', 'chmod *', '/tmp', 'allow', 100);
|
|
783
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
770
784
|
expect(result.decision).toBe('allow');
|
|
771
785
|
});
|
|
772
786
|
|
|
773
787
|
test('higher-priority deny rule overrides lower-priority allow rule', async () => {
|
|
774
|
-
addRule('bash', '
|
|
775
|
-
addRule('bash', '
|
|
776
|
-
const result = await check('bash', { command: '
|
|
788
|
+
addRule('bash', 'chmod *', '/tmp', 'allow', 0);
|
|
789
|
+
addRule('bash', 'chmod *', '/tmp', 'deny', 100);
|
|
790
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
777
791
|
expect(result.decision).toBe('deny');
|
|
778
792
|
});
|
|
779
793
|
|
|
@@ -927,6 +941,30 @@ describe('Permission Checker', () => {
|
|
|
927
941
|
expect(result.matchedRule!.id).toBe('default:allow-file_write-bootstrap');
|
|
928
942
|
});
|
|
929
943
|
|
|
944
|
+
test('file_read of workspace UPDATES.md is auto-allowed', async () => {
|
|
945
|
+
const updatesPath = join(checkerTestDir, 'workspace', 'UPDATES.md');
|
|
946
|
+
const result = await check('file_read', { path: updatesPath }, '/tmp');
|
|
947
|
+
expect(result.decision).toBe('allow');
|
|
948
|
+
expect(result.matchedRule).toBeDefined();
|
|
949
|
+
expect(result.matchedRule!.id).toBe('default:allow-file_read-updates');
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test('file_write of workspace UPDATES.md is auto-allowed', async () => {
|
|
953
|
+
const updatesPath = join(checkerTestDir, 'workspace', 'UPDATES.md');
|
|
954
|
+
const result = await check('file_write', { path: updatesPath }, '/tmp');
|
|
955
|
+
expect(result.decision).toBe('allow');
|
|
956
|
+
expect(result.matchedRule).toBeDefined();
|
|
957
|
+
expect(result.matchedRule!.id).toBe('default:allow-file_write-updates');
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test('file_edit of workspace UPDATES.md is auto-allowed', async () => {
|
|
961
|
+
const updatesPath = join(checkerTestDir, 'workspace', 'UPDATES.md');
|
|
962
|
+
const result = await check('file_edit', { path: updatesPath }, '/tmp');
|
|
963
|
+
expect(result.decision).toBe('allow');
|
|
964
|
+
expect(result.matchedRule).toBeDefined();
|
|
965
|
+
expect(result.matchedRule!.id).toBe('default:allow-file_edit-updates');
|
|
966
|
+
});
|
|
967
|
+
|
|
930
968
|
test('file_write of non-workspace file is not auto-allowed', async () => {
|
|
931
969
|
const otherPath = join(checkerTestDir, 'workspace', 'OTHER.md');
|
|
932
970
|
const result = await check('file_write', { path: otherPath }, '/tmp');
|
|
@@ -1441,13 +1479,14 @@ describe('Permission Checker', () => {
|
|
|
1441
1479
|
expect(result.matchedRule?.id).toBe('default:allow-bash-global');
|
|
1442
1480
|
});
|
|
1443
1481
|
|
|
1444
|
-
test('host_bash
|
|
1482
|
+
test('host_bash auto-allows low risk in strict mode (default allow rule is a matching rule)', async () => {
|
|
1445
1483
|
testConfig.permissions.mode = 'strict';
|
|
1446
1484
|
const result = await check('host_bash', { command: 'ls' }, '/tmp');
|
|
1447
|
-
expect(result.decision).toBe('
|
|
1485
|
+
expect(result.decision).toBe('allow');
|
|
1486
|
+
expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
|
|
1448
1487
|
});
|
|
1449
1488
|
|
|
1450
|
-
test('
|
|
1489
|
+
test('high-risk host_bash (rm) with no matching rule returns prompt in strict mode', async () => {
|
|
1451
1490
|
testConfig.permissions.mode = 'strict';
|
|
1452
1491
|
const result = await check('host_bash', { command: 'rm file.txt' }, '/tmp');
|
|
1453
1492
|
expect(result.decision).toBe('prompt');
|
|
@@ -1544,8 +1583,8 @@ describe('Permission Checker', () => {
|
|
|
1544
1583
|
});
|
|
1545
1584
|
|
|
1546
1585
|
test('medium-risk tool with allow rule is NOT affected by allowHighRisk', async () => {
|
|
1547
|
-
addRule('bash', '
|
|
1548
|
-
const result = await check('bash', { command: '
|
|
1586
|
+
addRule('bash', 'chmod *', '/tmp', 'allow', 100);
|
|
1587
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
1549
1588
|
expect(result.decision).toBe('allow');
|
|
1550
1589
|
expect(result.reason).toContain('Matched trust rule');
|
|
1551
1590
|
// No mention of high-risk in the reason
|
|
@@ -1615,8 +1654,8 @@ describe('Permission Checker', () => {
|
|
|
1615
1654
|
|
|
1616
1655
|
test('strict mode: medium-risk with matching allow rule auto-allows', async () => {
|
|
1617
1656
|
testConfig.permissions.mode = 'strict';
|
|
1618
|
-
addRule('bash', '
|
|
1619
|
-
const result = await check('bash', { command: '
|
|
1657
|
+
addRule('bash', 'chmod *', '/tmp', 'allow');
|
|
1658
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
1620
1659
|
expect(result.decision).toBe('allow');
|
|
1621
1660
|
expect(result.reason).toContain('Matched trust rule');
|
|
1622
1661
|
});
|
|
@@ -2392,10 +2431,11 @@ describe('Permission Checker', () => {
|
|
|
2392
2431
|
expect(result.matchedRule?.id).toBe('default:allow-bash-global');
|
|
2393
2432
|
});
|
|
2394
2433
|
|
|
2395
|
-
test('low-risk host_bash
|
|
2434
|
+
test('low-risk host_bash auto-allows in strict mode (default allow rule is a matching rule)', async () => {
|
|
2396
2435
|
testConfig.permissions.mode = 'strict';
|
|
2397
2436
|
const result = await check('host_bash', { command: 'echo hello' }, '/tmp');
|
|
2398
|
-
expect(result.decision).toBe('
|
|
2437
|
+
expect(result.decision).toBe('allow');
|
|
2438
|
+
expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
|
|
2399
2439
|
});
|
|
2400
2440
|
|
|
2401
2441
|
test('low-risk file_read with no rule prompts in strict mode', async () => {
|
|
@@ -2457,10 +2497,10 @@ describe('Permission Checker', () => {
|
|
|
2457
2497
|
// target-scoped. ───────────────────────────────────────────────
|
|
2458
2498
|
|
|
2459
2499
|
describe('Invariant 4: host execution approvals are explicit and target-scoped', () => {
|
|
2460
|
-
test('host_bash
|
|
2500
|
+
test('host_bash auto-allows low risk via default allow rule', async () => {
|
|
2461
2501
|
const result = await check('host_bash', { command: 'ls' }, '/tmp');
|
|
2462
|
-
expect(result.decision).toBe('
|
|
2463
|
-
expect(result.matchedRule?.id).toBe('default:
|
|
2502
|
+
expect(result.decision).toBe('allow');
|
|
2503
|
+
expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
|
|
2464
2504
|
});
|
|
2465
2505
|
|
|
2466
2506
|
test('host_file_read prompts by default (no implicit allow)', async () => {
|
|
@@ -2507,11 +2547,11 @@ describe('Permission Checker', () => {
|
|
|
2507
2547
|
expect(matchResult.matchedRule?.id).toBe('inv4-target-scoped');
|
|
2508
2548
|
|
|
2509
2549
|
// Different target — the target-scoped rule should NOT match;
|
|
2510
|
-
// falls back to the default host_bash
|
|
2550
|
+
// falls back to the default host_bash allow rule (auto-allows medium risk)
|
|
2511
2551
|
const noMatchResult = await check('host_bash', { command: 'run script.js' }, '/tmp', {
|
|
2512
2552
|
executionTarget: '/usr/local/bin/bun',
|
|
2513
2553
|
});
|
|
2514
|
-
expect(noMatchResult.decision).toBe('
|
|
2554
|
+
expect(noMatchResult.decision).toBe('allow');
|
|
2515
2555
|
expect(noMatchResult.matchedRule?.id).not.toBe('inv4-target-scoped');
|
|
2516
2556
|
});
|
|
2517
2557
|
});
|
|
@@ -2581,7 +2621,7 @@ describe('Permission Checker', () => {
|
|
|
2581
2621
|
test('wildcard allow rule matches any command in legacy mode', async () => {
|
|
2582
2622
|
testConfig.permissions.mode = 'legacy';
|
|
2583
2623
|
addRule('bash', '*', 'everywhere');
|
|
2584
|
-
const result = await check('bash', { command: '
|
|
2624
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
2585
2625
|
expect(result.decision).toBe('allow');
|
|
2586
2626
|
expect(result.matchedRule).toBeDefined();
|
|
2587
2627
|
});
|
|
@@ -2589,7 +2629,7 @@ describe('Permission Checker', () => {
|
|
|
2589
2629
|
test('wildcard allow rule matches any command in strict mode', async () => {
|
|
2590
2630
|
testConfig.permissions.mode = 'strict';
|
|
2591
2631
|
addRule('bash', '*', 'everywhere');
|
|
2592
|
-
const result = await check('bash', { command: '
|
|
2632
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
2593
2633
|
expect(result.decision).toBe('allow');
|
|
2594
2634
|
expect(result.matchedRule).toBeDefined();
|
|
2595
2635
|
});
|
|
@@ -2700,12 +2740,27 @@ describe('Permission Checker', () => {
|
|
|
2700
2740
|
);
|
|
2701
2741
|
|
|
2702
2742
|
test('getDefaultRuleTemplates has no extra rules when extraDirs is empty', () => {
|
|
2703
|
-
// Default testConfig has no skills property → getConfig returns default
|
|
2704
|
-
// with extraDirs: []
|
|
2705
2743
|
const templates = getDefaultRuleTemplates();
|
|
2706
2744
|
const extraRules = templates.filter((t) => t.id.includes('extra-'));
|
|
2707
2745
|
expect(extraRules.length).toBe(0);
|
|
2708
2746
|
});
|
|
2747
|
+
|
|
2748
|
+
test('getDefaultRuleTemplates tolerates partial config mocks', () => {
|
|
2749
|
+
const originalSkills = testConfig.skills;
|
|
2750
|
+
const originalSandbox = testConfig.sandbox;
|
|
2751
|
+
try {
|
|
2752
|
+
testConfig.skills = {} as any;
|
|
2753
|
+
testConfig.sandbox = {} as any;
|
|
2754
|
+
|
|
2755
|
+
const templates = getDefaultRuleTemplates();
|
|
2756
|
+
expect(Array.isArray(templates)).toBe(true);
|
|
2757
|
+
expect(templates.some((t) => t.id.includes('extra-'))).toBe(false);
|
|
2758
|
+
expect(templates.some((t) => t.id === 'default:allow-bash-global')).toBe(true);
|
|
2759
|
+
} finally {
|
|
2760
|
+
testConfig.skills = originalSkills;
|
|
2761
|
+
testConfig.sandbox = originalSandbox;
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2709
2764
|
});
|
|
2710
2765
|
|
|
2711
2766
|
// ── backslash normalization gated to Windows (PR 3558 follow-up) ──
|
|
@@ -2928,8 +2983,8 @@ describe('bash network_mode=proxied force prompt', () => {
|
|
|
2928
2983
|
});
|
|
2929
2984
|
|
|
2930
2985
|
test('non-proxied bash with trust rule follows normal flow', async () => {
|
|
2931
|
-
addRule('bash', '
|
|
2932
|
-
const result = await check('bash', { command: '
|
|
2986
|
+
addRule('bash', 'chmod *', '/tmp');
|
|
2987
|
+
const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
2933
2988
|
expect(result.decision).toBe('allow');
|
|
2934
2989
|
expect(result.reason).not.toContain('Proxied network mode');
|
|
2935
2990
|
});
|
|
@@ -3221,10 +3276,10 @@ describe('workspace mode — auto-allow workspace-scoped operations', () => {
|
|
|
3221
3276
|
expect(result.reason).toContain('ask rule');
|
|
3222
3277
|
});
|
|
3223
3278
|
|
|
3224
|
-
test('host_bash →
|
|
3279
|
+
test('host_bash → allow (default allow rule matches)', async () => {
|
|
3225
3280
|
const result = await check('host_bash', { command: 'ls' }, workspaceDir);
|
|
3226
|
-
expect(result.decision).toBe('
|
|
3227
|
-
expect(result.reason).toContain('
|
|
3281
|
+
expect(result.decision).toBe('allow');
|
|
3282
|
+
expect(result.reason).toContain('Matched trust rule');
|
|
3228
3283
|
});
|
|
3229
3284
|
|
|
3230
3285
|
// ── explicit rules still take precedence in workspace mode ──
|
|
@@ -3404,20 +3459,20 @@ describe('integration regressions (PR 11)', () => {
|
|
|
3404
3459
|
});
|
|
3405
3460
|
|
|
3406
3461
|
test('raw legacy rule still works alongside new action key system', async () => {
|
|
3407
|
-
// Use medium-risk commands (
|
|
3462
|
+
// Use medium-risk commands (chmod) so they aren't auto-allowed by low-risk classification.
|
|
3408
3463
|
// Disable sandbox so the catch-all "**" rule doesn't interfere.
|
|
3409
3464
|
testConfig.sandbox.enabled = false;
|
|
3410
3465
|
try { rmSync(join(checkerTestDir, 'protected', 'trust.json')); } catch { /* may not exist */ }
|
|
3411
3466
|
clearCache();
|
|
3412
3467
|
try {
|
|
3413
|
-
addRule('bash', '
|
|
3468
|
+
addRule('bash', 'chmod 644 file.txt', 'everywhere');
|
|
3414
3469
|
|
|
3415
3470
|
// Exact match still works
|
|
3416
|
-
const r1 = await check('bash', { command: '
|
|
3471
|
+
const r1 = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
|
|
3417
3472
|
expect(r1.decision).toBe('allow');
|
|
3418
3473
|
|
|
3419
|
-
// Different
|
|
3420
|
-
const r2 = await check('bash', { command: '
|
|
3474
|
+
// Different chmod argument should not match this exact raw rule
|
|
3475
|
+
const r2 = await check('bash', { command: 'chmod 755 other.txt' }, '/tmp');
|
|
3421
3476
|
expect(r2.decision).not.toBe('allow');
|
|
3422
3477
|
} finally {
|
|
3423
3478
|
testConfig.sandbox.enabled = true;
|
|
@@ -77,8 +77,8 @@ describe('computer-use skill manifest regression', () => {
|
|
|
77
77
|
await initializeTools();
|
|
78
78
|
|
|
79
79
|
// The 12 computer_use_* action tools must NOT be in the global registry
|
|
80
|
-
// after initializeTools(). If they were, registerSkillTools() would
|
|
81
|
-
//
|
|
80
|
+
// after initializeTools(). If they were, registerSkillTools() would skip
|
|
81
|
+
// them as core tool collisions when the computer-use skill is activated.
|
|
82
82
|
for (const name of COMPUTER_USE_TOOL_NAMES) {
|
|
83
83
|
expect(getTool(name)).toBeUndefined();
|
|
84
84
|
}
|