@vellumai/assistant 0.5.9 → 0.5.11
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/AGENTS.md +9 -1
- package/ARCHITECTURE.md +48 -48
- package/Dockerfile +2 -0
- package/README.md +1 -1
- package/docs/architecture/integrations.md +6 -13
- package/docs/architecture/memory.md +7 -12
- package/docs/architecture/security.md +5 -5
- package/docs/credential-execution-service.md +9 -9
- package/docs/skills.md +1 -1
- package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
- package/openapi.yaml +7130 -0
- package/package.json +2 -1
- package/scripts/generate-openapi.ts +562 -0
- package/src/__tests__/acp-session.test.ts +239 -44
- package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
- package/src/__tests__/browser-skill-endstate.test.ts +1 -1
- package/src/__tests__/btw-routes.test.ts +8 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
- package/src/__tests__/channel-approvals.test.ts +7 -7
- package/src/__tests__/channel-readiness-service.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +10 -2
- package/src/__tests__/context-memory-e2e.test.ts +2 -6
- package/src/__tests__/conversation-skill-tools.test.ts +1 -3
- package/src/__tests__/conversation-title-service.test.ts +2 -15
- package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
- package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
- package/src/__tests__/credential-security-e2e.test.ts +4 -4
- package/src/__tests__/credential-security-invariants.test.ts +3 -3
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/heartbeat-service.test.ts +35 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
- package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
- package/src/__tests__/log-export-workspace.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-recall-log-store.test.ts +182 -0
- package/src/__tests__/memory-recall-quality.test.ts +6 -8
- package/src/__tests__/memory-regressions.test.ts +53 -42
- package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
- package/src/__tests__/messaging-skill-split.test.ts +2 -17
- package/src/__tests__/oauth-cli.test.ts +98 -551
- package/src/__tests__/platform-callback-registration.test.ts +119 -0
- package/src/__tests__/secret-ingress-channel.test.ts +261 -0
- package/src/__tests__/secret-ingress-cli.test.ts +201 -0
- package/src/__tests__/secret-ingress-http.test.ts +312 -0
- package/src/__tests__/secret-ingress.test.ts +283 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
- package/src/__tests__/skill-feature-flags.test.ts +11 -19
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-inline-command.test.ts +3 -3
- package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
- package/src/__tests__/skill-memory.test.ts +2 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
- package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
- package/src/__tests__/skills.test.ts +16 -2
- package/src/__tests__/slack-channel-config.test.ts +1 -1
- package/src/__tests__/slack-skill.test.ts +5 -69
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
- package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
- package/src/acp/client-handler.ts +113 -31
- package/src/acp/session-manager.ts +29 -27
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/cli/AGENTS.md +73 -0
- package/src/cli/commands/autonomy.ts +3 -5
- package/src/cli/commands/credential-execution.ts +1 -2
- package/src/cli/commands/credentials.ts +4 -4
- package/src/cli/commands/memory.ts +2 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
- package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
- package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
- package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
- package/src/cli/commands/oauth/apps.ts +29 -11
- package/src/cli/commands/oauth/connect.ts +373 -0
- package/src/cli/commands/oauth/connections.ts +14 -493
- package/src/cli/commands/oauth/disconnect.ts +333 -0
- package/src/cli/commands/oauth/index.ts +62 -10
- package/src/cli/commands/oauth/mode.ts +263 -0
- package/src/cli/commands/oauth/ping.ts +222 -0
- package/src/cli/commands/oauth/providers.ts +30 -3
- package/src/cli/commands/oauth/request.ts +576 -0
- package/src/cli/commands/oauth/shared.ts +132 -0
- package/src/cli/commands/oauth/status.ts +202 -0
- package/src/cli/commands/oauth/token.ts +159 -0
- package/src/cli/commands/platform.ts +20 -14
- package/src/cli.ts +82 -17
- package/src/config/assistant-feature-flags.ts +74 -11
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +13 -36
- package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +2 -2
- package/src/config/bundled-skills/settings/SKILL.md +5 -3
- package/src/config/bundled-skills/settings/TOOLS.json +17 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
- package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
- package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
- package/src/config/bundled-skills/slack/SKILL.md +58 -44
- package/src/config/bundled-tool-registry.ts +2 -19
- package/src/config/env.ts +5 -1
- package/src/config/feature-flag-registry.json +57 -41
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/security.ts +9 -1
- package/src/config/schemas/services.ts +1 -1
- package/src/config/skill-state.ts +1 -3
- package/src/config/skills.ts +2 -4
- package/src/credential-execution/feature-gates.ts +9 -16
- package/src/credential-execution/process-manager.ts +12 -0
- package/src/daemon/config-watcher.ts +4 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
- package/src/daemon/conversation-agent-loop.ts +49 -2
- package/src/daemon/conversation-memory.ts +0 -1
- package/src/daemon/handlers/config-slack-channel.ts +43 -1
- package/src/daemon/handlers/conversations.ts +41 -33
- package/src/daemon/lifecycle.ts +28 -5
- package/src/daemon/message-types/acp.ts +0 -15
- package/src/daemon/message-types/memory.ts +0 -1
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +9 -0
- package/src/daemon/server.ts +19 -7
- package/src/email/feature-gate.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +48 -0
- package/src/inbound/platform-callback-registration.ts +61 -7
- package/src/mcp/mcp-oauth-provider.ts +3 -3
- package/src/memory/app-store.ts +3 -3
- package/src/memory/conversation-crud.ts +124 -0
- package/src/memory/conversation-title-service.ts +7 -17
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-local.ts +47 -2
- package/src/memory/indexer.ts +13 -10
- package/src/memory/items-extractor.ts +12 -4
- package/src/memory/job-utils.ts +5 -0
- package/src/memory/jobs-store.ts +10 -2
- package/src/memory/journal-memory.ts +6 -2
- package/src/memory/llm-request-log-store.ts +88 -21
- package/src/memory/memory-recall-log-store.ts +128 -0
- package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
- package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/validate-migration-state.ts +14 -1
- package/src/memory/retriever.test.ts +4 -5
- package/src/memory/schema/infrastructure.ts +31 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +54 -0
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +26 -5
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -2
- package/src/permissions/trust-client.ts +2 -2
- package/src/platform/client.ts +2 -2
- package/src/prompts/journal-context.ts +6 -1
- package/src/providers/anthropic/client.ts +143 -1
- package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
- package/src/runtime/auth/route-policy.ts +0 -1
- package/src/runtime/btw-sidechain.ts +7 -1
- package/src/runtime/channel-approvals.ts +2 -2
- package/src/runtime/channel-readiness-service.ts +30 -7
- package/src/runtime/http-router.ts +31 -0
- package/src/runtime/http-server.ts +21 -4
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/pending-interactions.ts +21 -3
- package/src/runtime/routes/acp-routes.ts +46 -28
- package/src/runtime/routes/app-management-routes.ts +123 -0
- package/src/runtime/routes/app-routes.ts +31 -0
- package/src/runtime/routes/approval-routes.ts +108 -3
- package/src/runtime/routes/attachment-routes.ts +45 -0
- package/src/runtime/routes/avatar-routes.ts +16 -0
- package/src/runtime/routes/brain-graph-routes.ts +18 -0
- package/src/runtime/routes/btw-routes.ts +20 -0
- package/src/runtime/routes/call-routes.ts +81 -0
- package/src/runtime/routes/channel-readiness-routes.ts +48 -7
- package/src/runtime/routes/channel-routes.ts +18 -0
- package/src/runtime/routes/channel-verification-routes.ts +49 -1
- package/src/runtime/routes/contact-routes.ts +77 -0
- package/src/runtime/routes/conversation-attention-routes.ts +37 -0
- package/src/runtime/routes/conversation-management-routes.ts +94 -0
- package/src/runtime/routes/conversation-query-routes.ts +78 -0
- package/src/runtime/routes/conversation-routes.ts +115 -38
- package/src/runtime/routes/conversation-starter-routes.ts +29 -0
- package/src/runtime/routes/debug-routes.ts +23 -0
- package/src/runtime/routes/diagnostics-routes.ts +30 -0
- package/src/runtime/routes/documents-routes.ts +42 -0
- package/src/runtime/routes/events-routes.ts +10 -0
- package/src/runtime/routes/global-search-routes.ts +35 -0
- package/src/runtime/routes/guardian-action-routes.ts +47 -2
- package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
- package/src/runtime/routes/heartbeat-routes.ts +278 -0
- package/src/runtime/routes/host-bash-routes.ts +16 -1
- package/src/runtime/routes/host-cu-routes.ts +23 -1
- package/src/runtime/routes/host-file-routes.ts +18 -1
- package/src/runtime/routes/identity-routes.ts +35 -0
- package/src/runtime/routes/inbound-message-handler.ts +46 -25
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
- package/src/runtime/routes/integrations/twilio.ts +32 -22
- package/src/runtime/routes/invite-routes.ts +83 -0
- package/src/runtime/routes/log-export-routes.ts +14 -0
- package/src/runtime/routes/memory-item-routes.ts +99 -1
- package/src/runtime/routes/migration-rollback-routes.ts +25 -0
- package/src/runtime/routes/migration-routes.ts +40 -0
- package/src/runtime/routes/notification-routes.ts +20 -0
- package/src/runtime/routes/oauth-apps.ts +11 -3
- package/src/runtime/routes/pairing-routes.ts +15 -0
- package/src/runtime/routes/recording-routes.ts +72 -0
- package/src/runtime/routes/schedule-routes.ts +77 -5
- package/src/runtime/routes/secret-routes.ts +63 -1
- package/src/runtime/routes/settings-routes.ts +91 -1
- package/src/runtime/routes/skills-routes.ts +98 -16
- package/src/runtime/routes/subagents-routes.ts +38 -3
- package/src/runtime/routes/surface-action-routes.ts +66 -24
- package/src/runtime/routes/surface-content-routes.ts +20 -0
- package/src/runtime/routes/telemetry-routes.ts +12 -0
- package/src/runtime/routes/trace-event-routes.ts +25 -0
- package/src/runtime/routes/trust-rules-routes.ts +46 -0
- package/src/runtime/routes/tts-routes.ts +15 -4
- package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
- package/src/runtime/routes/usage-routes.ts +59 -0
- package/src/runtime/routes/watch-routes.ts +28 -0
- package/src/runtime/routes/work-items-routes.ts +59 -0
- package/src/runtime/routes/workspace-commit-routes.ts +12 -0
- package/src/runtime/routes/workspace-routes.ts +102 -0
- package/src/schedule/scheduler.ts +7 -1
- package/src/security/AGENTS.md +7 -0
- package/src/security/credential-backend.ts +1 -1
- package/src/security/encrypted-store.ts +3 -3
- package/src/security/oauth2.ts +55 -0
- package/src/security/secret-ingress.ts +174 -0
- package/src/security/secret-patterns.ts +133 -0
- package/src/security/secret-scanner.ts +28 -117
- package/src/signals/confirm.ts +12 -8
- package/src/signals/user-message.ts +18 -3
- package/src/skills/skill-memory.ts +1 -2
- package/src/tasks/task-runner.ts +7 -1
- package/src/tools/credentials/broker.ts +1 -1
- package/src/tools/credentials/metadata-store.ts +1 -1
- package/src/tools/credentials/vault.ts +2 -3
- package/src/tools/memory/definitions.ts +1 -1
- package/src/tools/memory/handlers.test.ts +2 -4
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/tool-manifest.ts +1 -1
- package/src/util/log-redact.ts +9 -34
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
- package/src/workspace/migrations/AGENTS.md +11 -0
- package/src/workspace/migrations/runner.ts +16 -6
- package/src/workspace/migrations/types.ts +7 -0
- package/docs/architecture/keychain-broker.md +0 -69
- package/src/__tests__/keychain-broker-client.test.ts +0 -800
- package/src/cli/commands/oauth/platform.ts +0 -525
- package/src/config/bundled-skills/slack/TOOLS.json +0 -272
- package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
- package/src/security/keychain-broker-client.ts +0 -446
|
@@ -1,220 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Mock state
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
const isAvailableFn = mock((): boolean => true);
|
|
8
|
-
const brokerGetFn = mock(
|
|
9
|
-
async (
|
|
10
|
-
_account: string,
|
|
11
|
-
): Promise<{ found: boolean; value?: string } | null> => ({
|
|
12
|
-
found: true,
|
|
13
|
-
value: "secret",
|
|
14
|
-
}),
|
|
15
|
-
);
|
|
16
|
-
const brokerDelFn = mock(async (_account: string): Promise<boolean> => true);
|
|
17
|
-
const brokerListFn = mock(async (): Promise<string[]> => []);
|
|
18
|
-
const createBrokerClientFn = mock(() => ({
|
|
19
|
-
isAvailable: isAvailableFn,
|
|
20
|
-
get: brokerGetFn,
|
|
21
|
-
del: brokerDelFn,
|
|
22
|
-
list: brokerListFn,
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
const setKeyFn = mock(
|
|
26
|
-
(_account: string, _value: string): boolean => true,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Mock modules — before importing module under test
|
|
31
|
-
//
|
|
32
|
-
// The logger is mocked with a silent Proxy to suppress pino output in tests.
|
|
33
|
-
// The broker client and encrypted store are mocked to control migration
|
|
34
|
-
// behavior without touching real keychain or filesystem state.
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
mock.module("../util/logger.js", () => ({
|
|
38
|
-
getLogger: () =>
|
|
39
|
-
new Proxy({} as Record<string, unknown>, {
|
|
40
|
-
get: () => () => {},
|
|
41
|
-
}),
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
mock.module("../security/keychain-broker-client.js", () => ({
|
|
45
|
-
createBrokerClient: createBrokerClientFn,
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
mock.module("../security/encrypted-store.js", () => ({
|
|
49
|
-
setKey: setKeyFn,
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
// Import after mocking
|
|
53
3
|
import { migrateCredentialsFromKeychainMigration } from "../workspace/migrations/016-migrate-credentials-from-keychain.js";
|
|
54
4
|
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// Helpers
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
const WORKSPACE_DIR = "/mock-home/.vellum/workspace";
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Tests
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
5
|
describe("016-migrate-credentials-from-keychain migration", () => {
|
|
66
|
-
beforeEach(() => {
|
|
67
|
-
isAvailableFn.mockClear();
|
|
68
|
-
brokerGetFn.mockClear();
|
|
69
|
-
brokerDelFn.mockClear();
|
|
70
|
-
brokerListFn.mockClear();
|
|
71
|
-
createBrokerClientFn.mockClear();
|
|
72
|
-
setKeyFn.mockClear();
|
|
73
|
-
|
|
74
|
-
// Defaults: mac production build
|
|
75
|
-
process.env.VELLUM_DESKTOP_APP = "1";
|
|
76
|
-
delete process.env.VELLUM_DEV;
|
|
77
|
-
|
|
78
|
-
isAvailableFn.mockReturnValue(true);
|
|
79
|
-
brokerGetFn.mockResolvedValue({ found: true, value: "secret" });
|
|
80
|
-
brokerDelFn.mockResolvedValue(true);
|
|
81
|
-
brokerListFn.mockResolvedValue([]);
|
|
82
|
-
setKeyFn.mockReturnValue(true);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
6
|
test("has correct migration id", () => {
|
|
86
7
|
expect(migrateCredentialsFromKeychainMigration.id).toBe(
|
|
87
8
|
"016-migrate-credentials-from-keychain",
|
|
88
9
|
);
|
|
89
10
|
});
|
|
90
11
|
|
|
91
|
-
test("
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
95
|
-
|
|
96
|
-
expect(createBrokerClientFn).not.toHaveBeenCalled();
|
|
97
|
-
expect(brokerListFn).not.toHaveBeenCalled();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("skips when VELLUM_DESKTOP_APP is not '1'", async () => {
|
|
101
|
-
process.env.VELLUM_DESKTOP_APP = "0";
|
|
102
|
-
|
|
103
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
104
|
-
|
|
105
|
-
expect(createBrokerClientFn).not.toHaveBeenCalled();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("skips when VELLUM_DEV=1", async () => {
|
|
109
|
-
process.env.VELLUM_DEV = "1";
|
|
110
|
-
|
|
111
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
112
|
-
|
|
113
|
-
expect(createBrokerClientFn).not.toHaveBeenCalled();
|
|
114
|
-
expect(brokerListFn).not.toHaveBeenCalled();
|
|
12
|
+
test("run is a no-op", async () => {
|
|
13
|
+
await migrateCredentialsFromKeychainMigration.run("/fake");
|
|
115
14
|
});
|
|
116
15
|
|
|
117
|
-
test(
|
|
118
|
-
|
|
119
|
-
async () => {
|
|
120
|
-
isAvailableFn.mockReturnValue(false);
|
|
121
|
-
|
|
122
|
-
// Throwing skips the checkpoint so the migration retries on next startup
|
|
123
|
-
await expect(
|
|
124
|
-
migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR),
|
|
125
|
-
).rejects.toThrow("Keychain broker not available after waiting");
|
|
126
|
-
|
|
127
|
-
// Should not proceed to list or migrate keys
|
|
128
|
-
expect(brokerListFn).not.toHaveBeenCalled();
|
|
129
|
-
expect(setKeyFn).not.toHaveBeenCalled();
|
|
130
|
-
},
|
|
131
|
-
{ timeout: 10_000 },
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
test("no-ops when keychain has no accounts", async () => {
|
|
135
|
-
brokerListFn.mockResolvedValue([]);
|
|
136
|
-
|
|
137
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
138
|
-
|
|
139
|
-
expect(setKeyFn).not.toHaveBeenCalled();
|
|
140
|
-
expect(brokerDelFn).not.toHaveBeenCalled();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("copies credentials from keychain to encrypted store and deletes from keychain", async () => {
|
|
144
|
-
brokerListFn.mockResolvedValue(["account-a", "account-b"]);
|
|
145
|
-
brokerGetFn.mockImplementation(async (account: string) => {
|
|
146
|
-
if (account === "account-a") return { found: true, value: "secret-a" };
|
|
147
|
-
if (account === "account-b") return { found: true, value: "secret-b" };
|
|
148
|
-
return null;
|
|
149
|
-
});
|
|
150
|
-
setKeyFn.mockReturnValue(true);
|
|
151
|
-
|
|
152
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
153
|
-
|
|
154
|
-
// Should have written each key to encrypted store
|
|
155
|
-
expect(setKeyFn).toHaveBeenCalledTimes(2);
|
|
156
|
-
expect(setKeyFn).toHaveBeenCalledWith("account-a", "secret-a");
|
|
157
|
-
expect(setKeyFn).toHaveBeenCalledWith("account-b", "secret-b");
|
|
158
|
-
|
|
159
|
-
// Should have deleted each key from keychain after successful migration
|
|
160
|
-
expect(brokerDelFn).toHaveBeenCalledTimes(2);
|
|
161
|
-
expect(brokerDelFn).toHaveBeenCalledWith("account-a");
|
|
162
|
-
expect(brokerDelFn).toHaveBeenCalledWith("account-b");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("skips key when broker.get returns null", async () => {
|
|
166
|
-
brokerListFn.mockResolvedValue(["ghost-key", "real-key"]);
|
|
167
|
-
brokerGetFn.mockImplementation(async (account: string) => {
|
|
168
|
-
if (account === "ghost-key") return null;
|
|
169
|
-
if (account === "real-key") return { found: true, value: "real-secret" };
|
|
170
|
-
return null;
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
174
|
-
|
|
175
|
-
// ghost-key should not be written or deleted
|
|
176
|
-
expect(setKeyFn).not.toHaveBeenCalledWith(
|
|
177
|
-
"ghost-key",
|
|
178
|
-
expect.anything(),
|
|
179
|
-
);
|
|
180
|
-
expect(brokerDelFn).not.toHaveBeenCalledWith("ghost-key");
|
|
181
|
-
|
|
182
|
-
// real-key should be migrated
|
|
183
|
-
expect(setKeyFn).toHaveBeenCalledWith("real-key", "real-secret");
|
|
184
|
-
expect(brokerDelFn).toHaveBeenCalledWith("real-key");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("skips key when broker.get returns not found", async () => {
|
|
188
|
-
brokerListFn.mockResolvedValue(["missing-key"]);
|
|
189
|
-
brokerGetFn.mockResolvedValue({ found: false });
|
|
190
|
-
|
|
191
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
192
|
-
|
|
193
|
-
expect(setKeyFn).not.toHaveBeenCalled();
|
|
194
|
-
expect(brokerDelFn).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("skips key when setKey fails and does not delete from keychain", async () => {
|
|
198
|
-
brokerListFn.mockResolvedValue(["fail-key", "ok-key"]);
|
|
199
|
-
brokerGetFn.mockImplementation(async (account: string) => {
|
|
200
|
-
if (account === "fail-key")
|
|
201
|
-
return { found: true, value: "fail-secret" };
|
|
202
|
-
if (account === "ok-key") return { found: true, value: "ok-secret" };
|
|
203
|
-
return null;
|
|
204
|
-
});
|
|
205
|
-
setKeyFn.mockImplementation((account: string) => {
|
|
206
|
-
if (account === "fail-key") return false;
|
|
207
|
-
return true;
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
|
|
211
|
-
|
|
212
|
-
// fail-key should NOT have been deleted from keychain (setKey failed)
|
|
213
|
-
expect(brokerDelFn).not.toHaveBeenCalledWith("fail-key");
|
|
214
|
-
|
|
215
|
-
// ok-key should have been migrated and deleted
|
|
216
|
-
expect(setKeyFn).toHaveBeenCalledWith("ok-key", "ok-secret");
|
|
217
|
-
expect(brokerDelFn).toHaveBeenCalledWith("ok-key");
|
|
218
|
-
expect(brokerDelFn).toHaveBeenCalledTimes(1);
|
|
16
|
+
test("down is a no-op", async () => {
|
|
17
|
+
await migrateCredentialsFromKeychainMigration.down("/fake");
|
|
219
18
|
});
|
|
220
19
|
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mocks — must precede migration import
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
// In-memory credential store. Using `let` so tests can reset between runs.
|
|
8
|
+
let store = new Map<string, string>();
|
|
9
|
+
let storeUnreachable = false;
|
|
10
|
+
|
|
11
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
12
|
+
listSecureKeysAsync: async () => ({
|
|
13
|
+
accounts: [...store.keys()],
|
|
14
|
+
unreachable: storeUnreachable,
|
|
15
|
+
}),
|
|
16
|
+
getSecureKeyAsync: async (key: string) => store.get(key),
|
|
17
|
+
setSecureKeyAsync: async (key: string, value: string) => {
|
|
18
|
+
store.set(key, value);
|
|
19
|
+
return true;
|
|
20
|
+
},
|
|
21
|
+
deleteSecureKeyAsync: async (key: string) => {
|
|
22
|
+
store.delete(key);
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { rekeyCompoundCredentialKeysMigration } from "../workspace/migrations/018-rekey-compound-credential-keys.js";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function resetStore(entries: Record<string, string> = {}): void {
|
|
33
|
+
store = new Map(Object.entries(entries));
|
|
34
|
+
storeUnreachable = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function storeEntries(): Record<string, string> {
|
|
38
|
+
return Object.fromEntries(store);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Tests
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
describe("018-rekey-compound-credential-keys migration", () => {
|
|
46
|
+
test("has correct migration id", () => {
|
|
47
|
+
expect(rekeyCompoundCredentialKeysMigration.id).toBe(
|
|
48
|
+
"018-rekey-compound-credential-keys",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("run() re-keys compound credential from indexOf to lastIndexOf format", async () => {
|
|
53
|
+
resetStore({
|
|
54
|
+
"credential/integration/google:access_token": "my-token",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
58
|
+
|
|
59
|
+
expect(storeEntries()).toEqual({
|
|
60
|
+
"credential/integration:google/access_token": "my-token",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("run() leaves simple single-colon keys unchanged", async () => {
|
|
65
|
+
resetStore({
|
|
66
|
+
"credential/github/token": "gh-token",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
70
|
+
|
|
71
|
+
expect(storeEntries()).toEqual({
|
|
72
|
+
"credential/github/token": "gh-token",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("run() ignores non-credential keys", async () => {
|
|
77
|
+
resetStore({
|
|
78
|
+
"other/integration/google:access_token": "my-token",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
82
|
+
|
|
83
|
+
expect(storeEntries()).toEqual({
|
|
84
|
+
"other/integration/google:access_token": "my-token",
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("run() is idempotent — second run is a no-op", async () => {
|
|
89
|
+
resetStore({
|
|
90
|
+
"credential/integration/google:access_token": "my-token",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
94
|
+
const afterFirst = storeEntries();
|
|
95
|
+
|
|
96
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
97
|
+
|
|
98
|
+
expect(storeEntries()).toEqual(afterFirst);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("run() deletes orphaned old key when new key already exists", async () => {
|
|
102
|
+
resetStore({
|
|
103
|
+
"credential/integration/google:access_token": "old-token",
|
|
104
|
+
"credential/integration:google/access_token": "new-token",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
108
|
+
|
|
109
|
+
// Old key removed; new key (already present) wins
|
|
110
|
+
expect(storeEntries()).toEqual({
|
|
111
|
+
"credential/integration:google/access_token": "new-token",
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("run() throws when credential store is unreachable", async () => {
|
|
116
|
+
resetStore();
|
|
117
|
+
storeUnreachable = true;
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
rekeyCompoundCredentialKeysMigration.run("/fake"),
|
|
121
|
+
).rejects.toThrow("Credential store unreachable");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("down() reverses run() — re-keys from lastIndexOf back to indexOf format", async () => {
|
|
125
|
+
resetStore({
|
|
126
|
+
"credential/integration:google/access_token": "my-token",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await rekeyCompoundCredentialKeysMigration.down("/fake");
|
|
130
|
+
|
|
131
|
+
expect(storeEntries()).toEqual({
|
|
132
|
+
"credential/integration/google:access_token": "my-token",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("down() leaves simple keys unchanged", async () => {
|
|
137
|
+
resetStore({
|
|
138
|
+
"credential/github/token": "gh-token",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await rekeyCompoundCredentialKeysMigration.down("/fake");
|
|
142
|
+
|
|
143
|
+
expect(storeEntries()).toEqual({
|
|
144
|
+
"credential/github/token": "gh-token",
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("down() is idempotent — second down() is a no-op", async () => {
|
|
149
|
+
resetStore({
|
|
150
|
+
"credential/integration:google/access_token": "my-token",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await rekeyCompoundCredentialKeysMigration.down("/fake");
|
|
154
|
+
const afterFirst = storeEntries();
|
|
155
|
+
|
|
156
|
+
await rekeyCompoundCredentialKeysMigration.down("/fake");
|
|
157
|
+
|
|
158
|
+
expect(storeEntries()).toEqual(afterFirst);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("run() then down() restores original state", async () => {
|
|
162
|
+
const original = {
|
|
163
|
+
"credential/integration/google:access_token": "my-token",
|
|
164
|
+
};
|
|
165
|
+
resetStore(original);
|
|
166
|
+
|
|
167
|
+
await rekeyCompoundCredentialKeysMigration.run("/fake");
|
|
168
|
+
await rekeyCompoundCredentialKeysMigration.down("/fake");
|
|
169
|
+
|
|
170
|
+
expect(storeEntries()).toEqual(original);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("down() throws when credential store is unreachable", async () => {
|
|
174
|
+
resetStore();
|
|
175
|
+
storeUnreachable = true;
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
rekeyCompoundCredentialKeysMigration.down("/fake"),
|
|
179
|
+
).rejects.toThrow("Credential store unreachable");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -130,16 +130,16 @@ describe("runWorkspaceMigrations", () => {
|
|
|
130
130
|
throw new Error("migration 002 failed");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
).rejects.toThrow("migration 002 failed");
|
|
133
|
+
// Runner no longer throws — it marks failed migrations and continues
|
|
134
|
+
await runWorkspaceMigrations(WORKSPACE_DIR, [m1, m2]);
|
|
136
135
|
|
|
137
|
-
// m1 ran successfully
|
|
136
|
+
// m1 ran successfully, m2 was attempted
|
|
138
137
|
expect(m1.run).toHaveBeenCalledTimes(1);
|
|
138
|
+
expect(m2.run).toHaveBeenCalledTimes(1);
|
|
139
139
|
|
|
140
|
-
// Checkpoints saved: started m1, completed m1, started m2 =
|
|
141
|
-
expect(writeFileSyncFn).toHaveBeenCalledTimes(
|
|
142
|
-
expect(renameSyncFn).toHaveBeenCalledTimes(
|
|
140
|
+
// Checkpoints saved: started m1, completed m1, started m2, failed m2 = 4 writes
|
|
141
|
+
expect(writeFileSyncFn).toHaveBeenCalledTimes(4);
|
|
142
|
+
expect(renameSyncFn).toHaveBeenCalledTimes(4);
|
|
143
143
|
|
|
144
144
|
// Verify the completed checkpoint contains m1
|
|
145
145
|
// The second write is the "completed" marker for m1
|
|
@@ -149,6 +149,14 @@ describe("runWorkspaceMigrations", () => {
|
|
|
149
149
|
const parsed = JSON.parse(completedWrite);
|
|
150
150
|
expect(parsed.applied["001"]).toBeDefined();
|
|
151
151
|
expect(parsed.applied["001"].status).toBe("completed");
|
|
152
|
+
|
|
153
|
+
// Verify m2 is marked as failed
|
|
154
|
+
const failedWrite = (
|
|
155
|
+
writeFileSyncFn.mock.calls[3] as unknown[]
|
|
156
|
+
)[1] as string;
|
|
157
|
+
const failedParsed = JSON.parse(failedWrite);
|
|
158
|
+
expect(failedParsed.applied["002"]).toBeDefined();
|
|
159
|
+
expect(failedParsed.applied["002"].status).toBe("failed");
|
|
152
160
|
});
|
|
153
161
|
|
|
154
162
|
test("idempotent on re-run", async () => {
|
|
@@ -31,6 +31,8 @@ import type {
|
|
|
31
31
|
} from "@agentclientprotocol/sdk";
|
|
32
32
|
|
|
33
33
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
34
|
+
import type { UserDecision } from "../permissions/types.js";
|
|
35
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
34
36
|
import { getLogger } from "../util/logger.js";
|
|
35
37
|
|
|
36
38
|
const log = getLogger("acp:client-handler");
|
|
@@ -51,6 +53,8 @@ interface TerminalState {
|
|
|
51
53
|
export class VellumAcpClientHandler implements Client {
|
|
52
54
|
private terminals = new Map<string, TerminalState>();
|
|
53
55
|
private accumulatedText = "";
|
|
56
|
+
/** Tracks pending ACP permission requestIds for cleanup on session close. */
|
|
57
|
+
readonly pendingRequestIds = new Set<string>();
|
|
54
58
|
|
|
55
59
|
/** Returns the full agent response text accumulated from agent_message_chunk events. */
|
|
56
60
|
get responseText(): string {
|
|
@@ -60,10 +64,7 @@ export class VellumAcpClientHandler implements Client {
|
|
|
60
64
|
constructor(
|
|
61
65
|
private readonly acpSessionId: string,
|
|
62
66
|
private readonly sendToVellum: (msg: ServerMessage) => void,
|
|
63
|
-
private readonly
|
|
64
|
-
string,
|
|
65
|
-
{ resolve: (optionId: string) => void }
|
|
66
|
-
>,
|
|
67
|
+
private readonly parentConversationId: string,
|
|
67
68
|
) {}
|
|
68
69
|
|
|
69
70
|
async sessionUpdate(params: SessionNotification): Promise<void> {
|
|
@@ -152,33 +153,87 @@ export class VellumAcpClientHandler implements Client {
|
|
|
152
153
|
params: RequestPermissionRequest,
|
|
153
154
|
): Promise<RequestPermissionResponse> {
|
|
154
155
|
const requestId = randomUUID();
|
|
156
|
+
const toolTitle = params.toolCall.title ?? "Unknown tool";
|
|
157
|
+
const toolKind = params.toolCall.kind ?? "other";
|
|
158
|
+
const options = params.options;
|
|
159
|
+
|
|
155
160
|
log.info(
|
|
156
161
|
{
|
|
157
162
|
acpSessionId: this.acpSessionId,
|
|
158
163
|
requestId,
|
|
159
|
-
toolTitle
|
|
160
|
-
toolKind
|
|
161
|
-
optionCount:
|
|
164
|
+
toolTitle,
|
|
165
|
+
toolKind,
|
|
166
|
+
optionCount: options.length,
|
|
162
167
|
},
|
|
163
168
|
"ACP permission requested",
|
|
164
169
|
);
|
|
165
170
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
// Normalize rawInput into a Record for the confirmation_request shape
|
|
172
|
+
const rawInput = params.toolCall.rawInput;
|
|
173
|
+
const input: Record<string, unknown> =
|
|
174
|
+
rawInput != null &&
|
|
175
|
+
typeof rawInput === "object" &&
|
|
176
|
+
!Array.isArray(rawInput)
|
|
177
|
+
? (rawInput as Record<string, unknown>)
|
|
178
|
+
: { command: rawInput };
|
|
179
|
+
|
|
180
|
+
const toolName = `ACP Agent: ${toolTitle}`;
|
|
181
|
+
const acpOptions = options.map((opt) => ({
|
|
182
|
+
optionId: opt.optionId,
|
|
183
|
+
name: opt.name,
|
|
184
|
+
kind: opt.kind,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
// Send the confirmation_request first — this triggers makeEventSender
|
|
188
|
+
// which registers a normal "confirmation" entry in pendingInteractions.
|
|
170
189
|
this.sendToVellum({
|
|
171
|
-
type: "
|
|
172
|
-
acpSessionId: this.acpSessionId,
|
|
190
|
+
type: "confirmation_request",
|
|
173
191
|
requestId,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
toolName,
|
|
193
|
+
input,
|
|
194
|
+
riskLevel: "medium",
|
|
195
|
+
allowlistOptions: [],
|
|
196
|
+
scopeOptions: [],
|
|
197
|
+
persistentDecisionsAllowed: false,
|
|
198
|
+
acpToolKind: toolKind,
|
|
199
|
+
acpOptions,
|
|
200
|
+
conversationId: this.parentConversationId,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Now overwrite with our ACP registration that has directResolve.
|
|
204
|
+
// This must come AFTER sendToVellum so it wins over makeEventSender's
|
|
205
|
+
// registration.
|
|
206
|
+
const optionIdPromise = new Promise<string>((resolve) => {
|
|
207
|
+
const timeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
208
|
+
const timer = setTimeout(() => {
|
|
209
|
+
const pending = pendingInteractions.resolve(requestId);
|
|
210
|
+
if (pending?.directResolve) {
|
|
211
|
+
pending.directResolve("deny");
|
|
212
|
+
}
|
|
213
|
+
}, timeoutMs);
|
|
214
|
+
|
|
215
|
+
this.pendingRequestIds.add(requestId);
|
|
216
|
+
pendingInteractions.register(requestId, {
|
|
217
|
+
conversation: null,
|
|
218
|
+
conversationId: this.parentConversationId,
|
|
219
|
+
kind: "acp_confirmation",
|
|
220
|
+
confirmationDetails: {
|
|
221
|
+
toolName,
|
|
222
|
+
input,
|
|
223
|
+
riskLevel: "medium",
|
|
224
|
+
allowlistOptions: [],
|
|
225
|
+
scopeOptions: [],
|
|
226
|
+
persistentDecisionsAllowed: false,
|
|
227
|
+
acpToolKind: toolKind,
|
|
228
|
+
acpOptions,
|
|
229
|
+
},
|
|
230
|
+
directResolve: (decision: UserDecision) => {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
this.pendingRequestIds.delete(requestId);
|
|
233
|
+
const optionId = mapDecisionToOptionId(decision, options);
|
|
234
|
+
resolve(optionId);
|
|
235
|
+
},
|
|
236
|
+
});
|
|
182
237
|
});
|
|
183
238
|
|
|
184
239
|
const optionId = await optionIdPromise;
|
|
@@ -336,18 +391,45 @@ export class VellumAcpClientHandler implements Client {
|
|
|
336
391
|
}
|
|
337
392
|
|
|
338
393
|
/**
|
|
339
|
-
*
|
|
394
|
+
* Maps a UserDecision to the best-matching ACP option ID.
|
|
340
395
|
*/
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
396
|
+
function mapDecisionToOptionId(
|
|
397
|
+
decision: UserDecision,
|
|
398
|
+
options: Array<{ optionId: string; kind: string }>,
|
|
399
|
+
): string {
|
|
400
|
+
const isAllow =
|
|
401
|
+
decision === "allow" ||
|
|
402
|
+
decision === "allow_10m" ||
|
|
403
|
+
decision === "allow_conversation" ||
|
|
404
|
+
decision === "always_allow" ||
|
|
405
|
+
decision === "always_allow_high_risk" ||
|
|
406
|
+
decision === "temporary_override" ||
|
|
407
|
+
decision === "dangerously_skip_permissions";
|
|
408
|
+
|
|
409
|
+
if (isAllow) {
|
|
410
|
+
// Prefer allow_always for persistent decisions, fallback to allow_once
|
|
411
|
+
if (decision === "always_allow" || decision === "always_allow_high_risk") {
|
|
412
|
+
const alwaysOpt = options.find((o) => o.kind === "allow_always");
|
|
413
|
+
if (alwaysOpt) return alwaysOpt.optionId;
|
|
414
|
+
}
|
|
415
|
+
const allowOpt =
|
|
416
|
+
options.find((o) => o.kind === "allow_once") ??
|
|
417
|
+
options.find((o) => o.kind === "allow_always");
|
|
418
|
+
if (allowOpt) return allowOpt.optionId;
|
|
350
419
|
}
|
|
420
|
+
|
|
421
|
+
// Deny: prefer reject_always for persistent deny, fallback to reject_once
|
|
422
|
+
if (decision === "always_deny") {
|
|
423
|
+
const alwaysDeny = options.find((o) => o.kind === "reject_always");
|
|
424
|
+
if (alwaysDeny) return alwaysDeny.optionId;
|
|
425
|
+
}
|
|
426
|
+
const denyOpt =
|
|
427
|
+
options.find((o) => o.kind === "reject_once") ??
|
|
428
|
+
options.find((o) => o.kind === "reject_always");
|
|
429
|
+
if (denyOpt) return denyOpt.optionId;
|
|
430
|
+
|
|
431
|
+
// Fallback: return first option
|
|
432
|
+
return options[0]?.optionId ?? "deny";
|
|
351
433
|
}
|
|
352
434
|
|
|
353
435
|
/**
|