@vellumai/assistant 0.11.1 → 0.11.2-staging.2
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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import type { AdmissionPolicy, TrustVerdict } from "@vellumai/gateway-client";
|
|
30
30
|
|
|
31
31
|
import type { TrustClass } from "../../runtime/actor-trust-resolver.js";
|
|
32
|
+
import type { SetupOutcome } from "../call-setup-router.js";
|
|
32
33
|
import type { CallSession } from "../types.js";
|
|
33
34
|
|
|
34
35
|
// `resolveActorTrust` must never be consulted by the router — the gateway
|
|
@@ -376,47 +377,75 @@ describe("routeSetup — verdict session stamp gates getPendingSession", () => {
|
|
|
376
377
|
});
|
|
377
378
|
|
|
378
379
|
// ---------------------------------------------------------------------------
|
|
379
|
-
// Floor table: trustClass × policy →
|
|
380
|
+
// Floor table: trustClass × policy → setup action
|
|
381
|
+
//
|
|
382
|
+
// A caller at or above the floor connects directly (`normal_call`). A caller
|
|
383
|
+
// BELOW a floor that a guardian approval could clear is routed into the
|
|
384
|
+
// guardian access-request flow instead of hung up on: `name_capture` for an
|
|
385
|
+
// unrecognized number, `unverified_caller` for a known contact who still owes
|
|
386
|
+
// verification. `guardian_only` / `no_one` sit above the trusted
|
|
387
|
+
// contact rank an approval grants, so they stay a hard `deny`.
|
|
380
388
|
// ---------------------------------------------------------------------------
|
|
381
389
|
|
|
382
390
|
describe("routeSetup — admission floor table", () => {
|
|
383
391
|
const cases: Array<{
|
|
384
392
|
policy: AdmissionPolicy;
|
|
385
|
-
|
|
386
|
-
denies: TrustClass[];
|
|
393
|
+
expected: Record<TrustClass, SetupOutcome["action"]>;
|
|
387
394
|
}> = [
|
|
388
395
|
{
|
|
389
396
|
policy: "strangers",
|
|
390
|
-
|
|
391
|
-
|
|
397
|
+
expected: {
|
|
398
|
+
unknown: "normal_call",
|
|
399
|
+
unverified_contact: "normal_call",
|
|
400
|
+
trusted_contact: "normal_call",
|
|
401
|
+
guardian: "normal_call",
|
|
402
|
+
},
|
|
392
403
|
},
|
|
393
404
|
{
|
|
394
405
|
policy: "any_contact",
|
|
395
|
-
|
|
396
|
-
|
|
406
|
+
expected: {
|
|
407
|
+
unknown: "name_capture",
|
|
408
|
+
unverified_contact: "normal_call",
|
|
409
|
+
trusted_contact: "normal_call",
|
|
410
|
+
guardian: "normal_call",
|
|
411
|
+
},
|
|
397
412
|
},
|
|
398
413
|
{
|
|
399
414
|
policy: "trusted_contacts",
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
expected: {
|
|
416
|
+
unknown: "name_capture",
|
|
417
|
+
unverified_contact: "unverified_caller",
|
|
418
|
+
trusted_contact: "normal_call",
|
|
419
|
+
guardian: "normal_call",
|
|
420
|
+
},
|
|
402
421
|
},
|
|
403
422
|
{
|
|
404
423
|
policy: "guardian_only",
|
|
405
|
-
|
|
406
|
-
|
|
424
|
+
expected: {
|
|
425
|
+
unknown: "deny",
|
|
426
|
+
unverified_contact: "deny",
|
|
427
|
+
trusted_contact: "deny",
|
|
428
|
+
guardian: "normal_call",
|
|
429
|
+
},
|
|
407
430
|
},
|
|
408
431
|
{
|
|
409
432
|
policy: "no_one",
|
|
410
|
-
|
|
411
|
-
|
|
433
|
+
expected: {
|
|
434
|
+
unknown: "deny",
|
|
435
|
+
unverified_contact: "deny",
|
|
436
|
+
trusted_contact: "deny",
|
|
437
|
+
guardian: "deny",
|
|
438
|
+
},
|
|
412
439
|
},
|
|
413
440
|
];
|
|
414
441
|
|
|
415
|
-
for (const { policy,
|
|
416
|
-
for (const trustClass of
|
|
417
|
-
|
|
442
|
+
for (const { policy, expected } of cases) {
|
|
443
|
+
for (const [trustClass, action] of Object.entries(expected) as Array<
|
|
444
|
+
[TrustClass, SetupOutcome["action"]]
|
|
445
|
+
>) {
|
|
446
|
+
test(`${policy} routes ${trustClass} to ${action}`, async () => {
|
|
418
447
|
const { outcome } = await route(policy, verdictFor(trustClass));
|
|
419
|
-
expect(outcome.action).toBe(
|
|
448
|
+
expect(outcome.action).toBe(action);
|
|
420
449
|
if (outcome.action === "deny") {
|
|
421
450
|
expect(outcome.logReason).toBe(
|
|
422
451
|
`Inbound voice admission floor: ${policy}`,
|
|
@@ -424,15 +453,116 @@ describe("routeSetup — admission floor table", () => {
|
|
|
424
453
|
}
|
|
425
454
|
});
|
|
426
455
|
}
|
|
427
|
-
for (const trustClass of admits) {
|
|
428
|
-
test(`${policy} admits ${trustClass}`, async () => {
|
|
429
|
-
const { outcome } = await route(policy, verdictFor(trustClass));
|
|
430
|
-
expect(outcome.action).not.toBe("deny");
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
456
|
}
|
|
434
457
|
});
|
|
435
458
|
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
// An unrecognized caller below a clearable floor reaches the guardian
|
|
461
|
+
// access-request flow instead of being hung up on. The default channel policy
|
|
462
|
+
// is `trusted_contacts`, so this is the out-of-the-box path for every inbound
|
|
463
|
+
// call from an unknown number.
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
describe("routeSetup: below-floor unknown caller reaches guardian approval", () => {
|
|
467
|
+
test("default trusted_contacts floor routes an unknown caller to name_capture", async () => {
|
|
468
|
+
const { outcome, resolved } = await route(
|
|
469
|
+
"trusted_contacts",
|
|
470
|
+
verdictFor("unknown"),
|
|
471
|
+
);
|
|
472
|
+
expect(outcome.action).toBe("name_capture");
|
|
473
|
+
if (outcome.action === "name_capture") {
|
|
474
|
+
expect(outcome.fromNumber).toBe("+12025550142");
|
|
475
|
+
}
|
|
476
|
+
expect(resolved.actorTrust.trustClass).toBe("unknown");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("an active voice invite still takes precedence over the approval flow", async () => {
|
|
480
|
+
activeVoiceInvite = {
|
|
481
|
+
inviteId: "inv_1",
|
|
482
|
+
inviteeName: "Alice Example",
|
|
483
|
+
guardianName: "Sam",
|
|
484
|
+
codeDigits: 6,
|
|
485
|
+
};
|
|
486
|
+
const { outcome } = await route("trusted_contacts", verdictFor("unknown"));
|
|
487
|
+
expect(outcome.action).toBe("invite_redemption");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("guardian_only keeps the hard deny (approval could not admit them)", async () => {
|
|
491
|
+
const { outcome } = await route("guardian_only", verdictFor("unknown"));
|
|
492
|
+
expect(outcome.action).toBe("deny");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("a blocked caller below a clearable floor is still denied", async () => {
|
|
496
|
+
const { outcome } = await route(
|
|
497
|
+
"trusted_contacts",
|
|
498
|
+
makeMemberVerdict("unknown", { status: "blocked" }),
|
|
499
|
+
);
|
|
500
|
+
expect(outcome.action).toBe("deny");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("a revoked caller below a clearable floor is still denied", async () => {
|
|
504
|
+
const { outcome } = await route(
|
|
505
|
+
"trusted_contacts",
|
|
506
|
+
makeMemberVerdict("unknown", { status: "revoked" }),
|
|
507
|
+
);
|
|
508
|
+
expect(outcome.action).toBe("deny");
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Status and policy are independent governance signals: the gateway derives
|
|
512
|
+
// trust class from status alone, so a channel the guardian set to
|
|
513
|
+
// `policy: "deny"` still resolves to `unverified_contact` while status is
|
|
514
|
+
// `unverified`/`pending`. The deny gate is trust-class independent and runs
|
|
515
|
+
// ahead of the floor, so it holds whether the floor admits or denies, and it
|
|
516
|
+
// outranks an active voice invite.
|
|
517
|
+
describe("member policy deny outranks every inbound flow", () => {
|
|
518
|
+
const denyVerdict = () =>
|
|
519
|
+
makeMemberVerdict("unverified_contact", {
|
|
520
|
+
status: "unverified",
|
|
521
|
+
policy: "deny",
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("denied below a clearable floor (not sent to verification guidance)", async () => {
|
|
525
|
+
const { outcome } = await route("trusted_contacts", denyVerdict());
|
|
526
|
+
expect(outcome.action).toBe("deny");
|
|
527
|
+
if (outcome.action === "deny") {
|
|
528
|
+
expect(outcome.logReason).toBe("Inbound voice ACL: member policy deny");
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("denied even when the floor would admit them", async () => {
|
|
533
|
+
const { outcome } = await route("strangers", denyVerdict());
|
|
534
|
+
expect(outcome.action).toBe("deny");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("denied with no floor configured", async () => {
|
|
538
|
+
const { outcome } = await route(null, denyVerdict());
|
|
539
|
+
expect(outcome.action).toBe("deny");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("denied even with an active voice invite", async () => {
|
|
543
|
+
activeVoiceInvite = {
|
|
544
|
+
inviteId: "inv_1",
|
|
545
|
+
inviteeName: "Alice Example",
|
|
546
|
+
guardianName: "Sam",
|
|
547
|
+
codeDigits: 6,
|
|
548
|
+
};
|
|
549
|
+
const { outcome } = await route("trusted_contacts", denyVerdict());
|
|
550
|
+
expect(outcome.action).toBe("deny");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("an active-status member with policy deny is denied", async () => {
|
|
554
|
+
const { outcome } = await route(
|
|
555
|
+
"trusted_contacts",
|
|
556
|
+
makeMemberVerdict("trusted_contact", {
|
|
557
|
+
status: "active",
|
|
558
|
+
policy: "deny",
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
expect(outcome.action).toBe("deny");
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
436
566
|
// ---------------------------------------------------------------------------
|
|
437
567
|
// Blocked / revoked always deny
|
|
438
568
|
// ---------------------------------------------------------------------------
|
|
@@ -542,14 +672,14 @@ describe("routeSetup — permissive floor admits to normal_call", () => {
|
|
|
542
672
|
).toBe("unverified_caller");
|
|
543
673
|
});
|
|
544
674
|
|
|
545
|
-
test("trusted_contacts (default)
|
|
675
|
+
test("trusted_contacts (default) keeps below-floor callers out of normal_call", async () => {
|
|
546
676
|
expect(
|
|
547
677
|
(await route("trusted_contacts", verdictFor("unknown"))).outcome.action,
|
|
548
|
-
).toBe("
|
|
678
|
+
).toBe("name_capture");
|
|
549
679
|
expect(
|
|
550
680
|
(await route("trusted_contacts", verdictFor("unverified_contact")))
|
|
551
681
|
.outcome.action,
|
|
552
|
-
).toBe("
|
|
682
|
+
).toBe("unverified_caller");
|
|
553
683
|
});
|
|
554
684
|
});
|
|
555
685
|
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
cutFrontDoorContentAtVerdict,
|
|
79
79
|
startVoiceTurn,
|
|
80
80
|
TOOL_RESULT_PREVIEW_MAX_CHARS,
|
|
81
|
+
type VoiceTurnOptions,
|
|
81
82
|
} from "../voice-session-bridge.js";
|
|
82
83
|
import {
|
|
83
84
|
escalatedContinuationRule,
|
|
@@ -118,7 +119,13 @@ interface FakeConversation {
|
|
|
118
119
|
requestId: string;
|
|
119
120
|
metadata?: Record<string, unknown>;
|
|
120
121
|
}) => Promise<{ id: string }>;
|
|
122
|
+
workingDir: string;
|
|
121
123
|
updateClient: (cb: unknown, reset?: boolean) => void;
|
|
124
|
+
handleConfirmationResponse: (
|
|
125
|
+
requestId: string,
|
|
126
|
+
decision: string,
|
|
127
|
+
opts?: { decisionContext?: string },
|
|
128
|
+
) => void;
|
|
122
129
|
runAgentLoop: (...args: unknown[]) => Promise<void>;
|
|
123
130
|
abort: (reason?: unknown) => void;
|
|
124
131
|
loadFromDb: () => Promise<void>;
|
|
@@ -134,14 +141,22 @@ function makeFakeConversation(opts: {
|
|
|
134
141
|
hasQueuedMessages?: () => boolean;
|
|
135
142
|
/** Runs before each persist resolves; throw to script a persist failure. */
|
|
136
143
|
onPersist?: (attempt: number) => void;
|
|
144
|
+
/** Workspace root; pass empty to model a missing boundary. */
|
|
145
|
+
workingDir?: string;
|
|
137
146
|
}) {
|
|
138
147
|
const waitForIdleCalls: WaitForIdleCall[] = [];
|
|
148
|
+
const confirmationDecisions: Array<{ requestId: string; decision: string }> =
|
|
149
|
+
[];
|
|
150
|
+
let clientCallback: ((msg: unknown) => Promise<void>) | undefined;
|
|
139
151
|
let persistCount = 0;
|
|
140
152
|
let lastPersistOpts:
|
|
141
153
|
| { content: string; requestId: string; metadata?: Record<string, unknown> }
|
|
142
154
|
| undefined;
|
|
143
155
|
const conversation: FakeConversation = {
|
|
144
156
|
conversationId: "conv-voice-bridge-test",
|
|
157
|
+
// The workspace boundary the reach check compares paths against. A real
|
|
158
|
+
// conversation always has one; without it the approval gate fails closed.
|
|
159
|
+
workingDir: opts.workingDir ?? "/tmp/workspace-voice-bridge-test",
|
|
145
160
|
callSessionId: undefined,
|
|
146
161
|
forcePromptSideEffects: false,
|
|
147
162
|
currentRequestId: undefined,
|
|
@@ -173,8 +188,14 @@ function makeFakeConversation(opts: {
|
|
|
173
188
|
},
|
|
174
189
|
// The install (reset falsy) / reset (reset true) pair marks a turn
|
|
175
190
|
// taking ownership of the conversation vs a turn's cleanup releasing it.
|
|
176
|
-
updateClient: (
|
|
191
|
+
updateClient: (cb, reset) => {
|
|
177
192
|
opts.events?.push(reset ? "client:reset" : "client:install");
|
|
193
|
+
if (reset !== true) {
|
|
194
|
+
clientCallback = cb as (msg: unknown) => Promise<void>;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
handleConfirmationResponse: (requestId, decision) => {
|
|
198
|
+
confirmationDecisions.push({ requestId, decision });
|
|
178
199
|
},
|
|
179
200
|
runAgentLoop: () => (opts.runAgentLoop ?? (async () => {}))(),
|
|
180
201
|
abort: () => {},
|
|
@@ -186,6 +207,11 @@ function makeFakeConversation(opts: {
|
|
|
186
207
|
return {
|
|
187
208
|
conversation,
|
|
188
209
|
waitForIdleCalls,
|
|
210
|
+
confirmationDecisions,
|
|
211
|
+
/** Deliver an event the way the conversation would, to the installed handler. */
|
|
212
|
+
emitToClient: async (msg: unknown) => {
|
|
213
|
+
await clientCallback?.(msg);
|
|
214
|
+
},
|
|
189
215
|
persistCount: () => persistCount,
|
|
190
216
|
lastPersistOpts: () => lastPersistOpts,
|
|
191
217
|
setProcessingFlag: (value: boolean) => {
|
|
@@ -338,7 +364,10 @@ describe("startVoiceTurn escalation-continuation persistence", () => {
|
|
|
338
364
|
expect(fake.lastPersistOpts()?.content).toBe(
|
|
339
365
|
ESCALATION_CONTINUATION_CONTENT,
|
|
340
366
|
);
|
|
341
|
-
expect(fake.lastPersistOpts()?.metadata).toEqual({
|
|
367
|
+
expect(fake.lastPersistOpts()?.metadata).toEqual({
|
|
368
|
+
voiceSessionTurn: true,
|
|
369
|
+
hidden: true,
|
|
370
|
+
});
|
|
342
371
|
});
|
|
343
372
|
|
|
344
373
|
test("the opener prompt is persisted un-hidden (unchanged)", async () => {
|
|
@@ -347,7 +376,9 @@ describe("startVoiceTurn escalation-continuation persistence", () => {
|
|
|
347
376
|
|
|
348
377
|
await startVoiceTurn(makeTurnOptions()); // content: CALL_OPENING_MARKER
|
|
349
378
|
|
|
350
|
-
expect(fake.lastPersistOpts()?.metadata).
|
|
379
|
+
expect(fake.lastPersistOpts()?.metadata).toEqual({
|
|
380
|
+
voiceSessionTurn: true,
|
|
381
|
+
});
|
|
351
382
|
});
|
|
352
383
|
});
|
|
353
384
|
|
|
@@ -390,7 +421,10 @@ describe("startVoiceTurn hiddenSyntheticPrompt", () => {
|
|
|
390
421
|
);
|
|
391
422
|
|
|
392
423
|
expect(fake.lastPersistOpts()?.content).toBe(SYNTHETIC_CONTENT);
|
|
393
|
-
expect(fake.lastPersistOpts()?.metadata).toEqual({
|
|
424
|
+
expect(fake.lastPersistOpts()?.metadata).toEqual({
|
|
425
|
+
voiceSessionTurn: true,
|
|
426
|
+
hidden: true,
|
|
427
|
+
});
|
|
394
428
|
expect(echoes).toHaveLength(0);
|
|
395
429
|
});
|
|
396
430
|
|
|
@@ -405,7 +439,9 @@ describe("startVoiceTurn hiddenSyntheticPrompt", () => {
|
|
|
405
439
|
}),
|
|
406
440
|
);
|
|
407
441
|
|
|
408
|
-
expect(fake.lastPersistOpts()?.metadata).
|
|
442
|
+
expect(fake.lastPersistOpts()?.metadata).toEqual({
|
|
443
|
+
voiceSessionTurn: true,
|
|
444
|
+
});
|
|
409
445
|
expect(echoes).toEqual([
|
|
410
446
|
expect.objectContaining({ text: SYNTHETIC_CONTENT }),
|
|
411
447
|
]);
|
|
@@ -475,9 +511,10 @@ describe("startVoiceTurn triage-and-escalate control prompt", () => {
|
|
|
475
511
|
});
|
|
476
512
|
|
|
477
513
|
describe("startVoiceTurn channel capabilities", () => {
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
//
|
|
514
|
+
// Whether a call can show a surface is a property of its channel, not of
|
|
515
|
+
// calls in general, so the bridge applies no voice-specific override: a
|
|
516
|
+
// phone call has no screen and resolves false on its own, while a live-voice
|
|
517
|
+
// call is a screen the user is holding and resolves true.
|
|
481
518
|
|
|
482
519
|
// The turn installs its capabilities, then cleanup resets them to null — so
|
|
483
520
|
// capture every applied value and read the installed (non-null) one.
|
|
@@ -497,7 +534,7 @@ describe("startVoiceTurn channel capabilities", () => {
|
|
|
497
534
|
);
|
|
498
535
|
}
|
|
499
536
|
|
|
500
|
-
test("a vellum/macos (live-voice) turn
|
|
537
|
+
test("a vellum/macos (live-voice) turn keeps its channel's dynamic UI", async () => {
|
|
501
538
|
const installed = captureInstalledCapabilities();
|
|
502
539
|
await startVoiceTurn({
|
|
503
540
|
...makeTurnOptions(),
|
|
@@ -505,14 +542,16 @@ describe("startVoiceTurn channel capabilities", () => {
|
|
|
505
542
|
userMessageInterface: "macos",
|
|
506
543
|
});
|
|
507
544
|
const caps = installed();
|
|
508
|
-
expect(caps?.supportsDynamicUi).toBe(
|
|
509
|
-
//
|
|
545
|
+
expect(caps?.supportsDynamicUi).toBe(true);
|
|
546
|
+
// Nothing else about the channel is rewritten either.
|
|
510
547
|
expect(caps?.dashboardCapable).toBe(true);
|
|
511
548
|
expect(caps?.supportsVoiceInput).toBe(true);
|
|
512
549
|
expect(caps?.clientOS).toBe("macos");
|
|
513
550
|
});
|
|
514
551
|
|
|
515
|
-
|
|
552
|
+
// The case the removed override existed for, still covered: a phone call has
|
|
553
|
+
// no screen to show a surface on, and says so through its channel.
|
|
554
|
+
test("phone defaults yield supportsDynamicUi false", async () => {
|
|
516
555
|
const installed = captureInstalledCapabilities();
|
|
517
556
|
await startVoiceTurn(makeTurnOptions());
|
|
518
557
|
const caps = installed();
|
|
@@ -521,6 +560,156 @@ describe("startVoiceTurn channel capabilities", () => {
|
|
|
521
560
|
});
|
|
522
561
|
});
|
|
523
562
|
|
|
563
|
+
describe("startVoiceTurn guardian approvals", () => {
|
|
564
|
+
function confirmationRequest(
|
|
565
|
+
toolName: string,
|
|
566
|
+
executionTarget?: "sandbox" | "host",
|
|
567
|
+
input: Record<string, unknown> = {},
|
|
568
|
+
) {
|
|
569
|
+
return {
|
|
570
|
+
type: "confirmation_request",
|
|
571
|
+
requestId: "req-1",
|
|
572
|
+
toolName,
|
|
573
|
+
input,
|
|
574
|
+
riskLevel: "medium",
|
|
575
|
+
allowlistOptions: [],
|
|
576
|
+
scopeOptions: [],
|
|
577
|
+
...(executionTarget !== undefined ? { executionTarget } : {}),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function runVoiceTurn(
|
|
582
|
+
overrides: Partial<VoiceTurnOptions>,
|
|
583
|
+
conversationOpts: { workingDir?: string } = {},
|
|
584
|
+
) {
|
|
585
|
+
const fake = makeFakeConversation({
|
|
586
|
+
processing: false,
|
|
587
|
+
...conversationOpts,
|
|
588
|
+
});
|
|
589
|
+
fakeConversation = fake.conversation;
|
|
590
|
+
const pendingAnnounced: string[] = [];
|
|
591
|
+
await startVoiceTurn({
|
|
592
|
+
...makeTurnOptions(),
|
|
593
|
+
trustContext: { trustClass: "guardian" },
|
|
594
|
+
onApprovalPending: (requestId: string) => {
|
|
595
|
+
pendingAnnounced.push(requestId);
|
|
596
|
+
},
|
|
597
|
+
...overrides,
|
|
598
|
+
} as VoiceTurnOptions);
|
|
599
|
+
return { ...fake, pendingAnnounced };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// A live-voice call has a screen, so a tool that reaches the workspace or
|
|
603
|
+
// the host is put to the user rather than decided for them. This is the hole
|
|
604
|
+
// it closes: a guardian call used to allow every confirmation outright.
|
|
605
|
+
test("leaves a sensitive tool pending for the user to answer", async () => {
|
|
606
|
+
const fake = await runVoiceTurn({
|
|
607
|
+
userMessageChannel: "vellum",
|
|
608
|
+
userMessageInterface: "macos",
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
await fake.emitToClient(confirmationRequest("bash", "host"));
|
|
612
|
+
|
|
613
|
+
expect(fake.confirmationDecisions).toEqual([]);
|
|
614
|
+
// The card renders in the app, and the call covers the app, so a pending
|
|
615
|
+
// decision has to be announced or the turn just goes quiet.
|
|
616
|
+
expect(fake.pendingAnnounced).toEqual(["req-1"]);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// The tools that read or render were never the reason approval exists, and
|
|
620
|
+
// gating them would interrupt the conversation constantly.
|
|
621
|
+
test("still allows a tool with no sensitive reach", async () => {
|
|
622
|
+
const fake = await runVoiceTurn({
|
|
623
|
+
userMessageChannel: "vellum",
|
|
624
|
+
userMessageInterface: "macos",
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
await fake.emitToClient(confirmationRequest("ui_show", "sandbox"));
|
|
628
|
+
|
|
629
|
+
expect(fake.confirmationDecisions).toEqual([
|
|
630
|
+
{ requestId: "req-1", decision: "allow" },
|
|
631
|
+
]);
|
|
632
|
+
// Nothing is waiting, so nothing interrupts the call.
|
|
633
|
+
expect(fake.pendingAnnounced).toEqual([]);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// There is no screen on a phone call, so a prompt there is a question nobody
|
|
637
|
+
// can answer.
|
|
638
|
+
test("a phone call keeps allowing sensitive tools outright", async () => {
|
|
639
|
+
const fake = await runVoiceTurn({ userMessageChannel: "phone" });
|
|
640
|
+
|
|
641
|
+
await fake.emitToClient(confirmationRequest("bash", "host"));
|
|
642
|
+
|
|
643
|
+
expect(fake.confirmationDecisions).toEqual([
|
|
644
|
+
{ requestId: "req-1", decision: "allow" },
|
|
645
|
+
]);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// The escape this gate exists to catch: a *sandbox* file tool pointed
|
|
649
|
+
// outside the workspace reaches the host filesystem on a non-containerized
|
|
650
|
+
// install. The reach check can only see that when it is given the workspace
|
|
651
|
+
// boundary; without it this read of a host file classifies as `none` and
|
|
652
|
+
// lands on the auto-allow.
|
|
653
|
+
test("an out-of-workspace path prompts even on a sandbox target", async () => {
|
|
654
|
+
const fake = await runVoiceTurn(
|
|
655
|
+
{ userMessageChannel: "vellum", userMessageInterface: "macos" },
|
|
656
|
+
{ workingDir: "/tmp/workspace-voice-bridge-test" },
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
await fake.emitToClient(
|
|
660
|
+
confirmationRequest("file_read", "sandbox", { path: "/etc/hosts" }),
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(fake.confirmationDecisions).toEqual([]);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// A path inside the workspace is the ordinary case, and gating it would
|
|
667
|
+
// interrupt the conversation for every file the assistant touches.
|
|
668
|
+
test("an in-workspace read is still allowed outright", async () => {
|
|
669
|
+
const fake = await runVoiceTurn(
|
|
670
|
+
{ userMessageChannel: "vellum", userMessageInterface: "macos" },
|
|
671
|
+
{ workingDir: "/tmp/workspace-voice-bridge-test" },
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
await fake.emitToClient(
|
|
675
|
+
confirmationRequest("file_read", "sandbox", {
|
|
676
|
+
path: "/tmp/workspace-voice-bridge-test/notes.md",
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
expect(fake.confirmationDecisions).toEqual([
|
|
681
|
+
{ requestId: "req-1", decision: "allow" },
|
|
682
|
+
]);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// With no boundary there is no way to tell an ordinary write from an escape,
|
|
686
|
+
// and the safe reading of "cannot tell" is "ask".
|
|
687
|
+
test("a missing workspace boundary fails closed", async () => {
|
|
688
|
+
const fake = await runVoiceTurn(
|
|
689
|
+
{ userMessageChannel: "vellum", userMessageInterface: "macos" },
|
|
690
|
+
{ workingDir: "" },
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
await fake.emitToClient(confirmationRequest("ui_show", "sandbox"));
|
|
694
|
+
|
|
695
|
+
expect(fake.confirmationDecisions).toEqual([]);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Requests from the proxy and network prompters carry no target. Unknown
|
|
699
|
+
// reads as the more consequential of the two: a prompt the user did not need
|
|
700
|
+
// costs less than an unreviewed action on their machine.
|
|
701
|
+
test("prompts when the execution target is unknown", async () => {
|
|
702
|
+
const fake = await runVoiceTurn({
|
|
703
|
+
userMessageChannel: "vellum",
|
|
704
|
+
userMessageInterface: "macos",
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await fake.emitToClient(confirmationRequest("bash"));
|
|
708
|
+
|
|
709
|
+
expect(fake.confirmationDecisions).toEqual([]);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
524
713
|
describe("startVoiceTurn conversation-lock wait", () => {
|
|
525
714
|
test("an idle conversation starts the turn without consulting waitForIdle", async () => {
|
|
526
715
|
const fake = makeFakeConversation({ processing: false });
|