@vellumai/assistant 0.5.10 → 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 +8 -0
- package/ARCHITECTURE.md +43 -43
- package/Dockerfile +2 -0
- package/docs/architecture/integrations.md +3 -10
- package/docs/architecture/memory.md +7 -12
- 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__/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-018-rekey-compound-credential-keys.test.ts +181 -0
- 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/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 +26 -8
- 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 +26 -2
- 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/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 +90 -0
- 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/docs/architecture/keychain-broker.md +0 -68
- 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
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mocks — must be declared before any imports that depend on them
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const BASE_CONFIG = {
|
|
8
|
+
contextWindow: { maxInputTokens: 100000 },
|
|
9
|
+
services: { inference: { model: "test-model", provider: "test-provider" } },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let mockConfig: Record<string, unknown> = {
|
|
13
|
+
secretDetection: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
blockIngress: true,
|
|
16
|
+
},
|
|
17
|
+
...BASE_CONFIG,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
|
|
21
|
+
|
|
22
|
+
mock.module("../config/loader.js", () => ({
|
|
23
|
+
getConfig: () => mockConfig,
|
|
24
|
+
loadConfig: () => mockConfig,
|
|
25
|
+
invalidateConfigCache: () => {},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module("../util/logger.js", () => ({
|
|
29
|
+
getLogger: () =>
|
|
30
|
+
new Proxy({} as Record<string, unknown>, {
|
|
31
|
+
get: () => () => {},
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module("../util/platform.js", () => ({
|
|
36
|
+
getRootDir: () => "/tmp/vellum-test-secret-ingress-http",
|
|
37
|
+
getWorkspaceDir: () => "/tmp/vellum-test-secret-ingress-http/workspace",
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
mock.module("../memory/conversation-key-store.js", () => ({
|
|
41
|
+
getOrCreateConversation: () => ({ conversationId: "conv-test" }),
|
|
42
|
+
getConversationByKey: () => null,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module("../memory/attachments-store.js", () => ({
|
|
46
|
+
getAttachmentsByIds: () => [],
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
mock.module("../memory/canonical-guardian-store.js", () => ({
|
|
50
|
+
createCanonicalGuardianRequest: () => ({
|
|
51
|
+
id: "canonical-id",
|
|
52
|
+
requestCode: "ABC123",
|
|
53
|
+
}),
|
|
54
|
+
generateCanonicalRequestCode: () => "ABC123",
|
|
55
|
+
listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
|
|
56
|
+
listCanonicalGuardianRequests: () => [],
|
|
57
|
+
listPendingRequestsByConversationScope: () => [],
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
|
|
61
|
+
bridgeConfirmationRequestToGuardian: async () => undefined,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const addMessageMock = mock(
|
|
65
|
+
async (
|
|
66
|
+
_conversationId: string,
|
|
67
|
+
_role: string,
|
|
68
|
+
_content?: string,
|
|
69
|
+
_metadata?: Record<string, unknown>,
|
|
70
|
+
) => ({
|
|
71
|
+
id: "persisted-msg-id",
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
mock.module("../memory/conversation-crud.js", () => ({
|
|
76
|
+
addMessage: (
|
|
77
|
+
conversationId: string,
|
|
78
|
+
role: string,
|
|
79
|
+
content: string,
|
|
80
|
+
metadata?: Record<string, unknown>,
|
|
81
|
+
) => addMessageMock(conversationId, role, content, metadata),
|
|
82
|
+
getMessages: () => [],
|
|
83
|
+
provenanceFromTrustContext: () => undefined,
|
|
84
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
85
|
+
setConversationOriginInterfaceIfUnset: () => {},
|
|
86
|
+
getConversationType: () => undefined,
|
|
87
|
+
getConversationMemoryScopeId: () => undefined,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
mock.module("../runtime/local-actor-identity.js", () => ({
|
|
91
|
+
resolveLocalTrustContext: () => ({
|
|
92
|
+
trustClass: "guardian",
|
|
93
|
+
sourceChannel: "vellum",
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
mock.module("../runtime/trust-context-resolver.js", () => ({
|
|
98
|
+
resolveTrustContext: () => ({
|
|
99
|
+
trustClass: "guardian",
|
|
100
|
+
sourceChannel: "vellum",
|
|
101
|
+
}),
|
|
102
|
+
withSourceChannel: (sourceChannel: unknown, ctx: unknown) => ({
|
|
103
|
+
...(ctx as Record<string, unknown>),
|
|
104
|
+
sourceChannel,
|
|
105
|
+
}),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
mock.module("../runtime/guardian-reply-router.js", () => ({
|
|
109
|
+
routeGuardianReply: async () => ({
|
|
110
|
+
consumed: false,
|
|
111
|
+
decisionApplied: false,
|
|
112
|
+
type: "not_consumed" as const,
|
|
113
|
+
}),
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Imports (after mocks)
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
121
|
+
import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
|
|
122
|
+
|
|
123
|
+
const testAuthContext: AuthContext = {
|
|
124
|
+
subject: "actor:self:test-user",
|
|
125
|
+
principalType: "actor",
|
|
126
|
+
assistantId: "self",
|
|
127
|
+
actorPrincipalId: "test-user",
|
|
128
|
+
scopeProfile: "actor_client_v1",
|
|
129
|
+
scopes: new Set([
|
|
130
|
+
"chat.read",
|
|
131
|
+
"chat.write",
|
|
132
|
+
"approval.read",
|
|
133
|
+
"approval.write",
|
|
134
|
+
"settings.read",
|
|
135
|
+
"settings.write",
|
|
136
|
+
"attachments.read",
|
|
137
|
+
"attachments.write",
|
|
138
|
+
"calls.read",
|
|
139
|
+
"calls.write",
|
|
140
|
+
"feature_flags.read",
|
|
141
|
+
"feature_flags.write",
|
|
142
|
+
]),
|
|
143
|
+
policyEpoch: 1,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function makeRequest(body: Record<string, unknown>): Request {
|
|
147
|
+
return new Request("http://localhost/v1/messages", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
conversationKey: "test-conversation",
|
|
152
|
+
sourceChannel: "vellum",
|
|
153
|
+
interface: "macos",
|
|
154
|
+
...body,
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const persistUserMessageMock = mock(async () => "persisted-id");
|
|
160
|
+
const runAgentLoopMock = mock(async () => undefined);
|
|
161
|
+
|
|
162
|
+
function makeSendMessageDeps() {
|
|
163
|
+
const session = {
|
|
164
|
+
setTrustContext: () => {},
|
|
165
|
+
updateClient: () => {},
|
|
166
|
+
emitConfirmationStateChanged: () => {},
|
|
167
|
+
emitActivityState: () => {},
|
|
168
|
+
setTurnChannelContext: () => {},
|
|
169
|
+
setTurnInterfaceContext: () => {},
|
|
170
|
+
ensureActorScopedHistory: async () => {},
|
|
171
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
172
|
+
isProcessing: () => false,
|
|
173
|
+
hasAnyPendingConfirmation: () => false,
|
|
174
|
+
denyAllPendingConfirmations: () => {},
|
|
175
|
+
enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
|
|
176
|
+
persistUserMessage: persistUserMessageMock,
|
|
177
|
+
runAgentLoop: runAgentLoopMock,
|
|
178
|
+
getMessages: () => [] as unknown[],
|
|
179
|
+
assistantId: "self",
|
|
180
|
+
trustContext: undefined,
|
|
181
|
+
hasPendingConfirmation: () => false,
|
|
182
|
+
setHostBashProxy: () => {},
|
|
183
|
+
setHostFileProxy: () => {},
|
|
184
|
+
setHostCuProxy: () => {},
|
|
185
|
+
addPreactivatedSkillId: () => {},
|
|
186
|
+
} as unknown as import("../daemon/conversation.js").Conversation;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
sendMessageDeps: {
|
|
190
|
+
getOrCreateConversation: async () => session,
|
|
191
|
+
assistantEventHub: { publish: async () => {} } as any,
|
|
192
|
+
resolveAttachments: () => [],
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Tests
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
describe("secret ingress — HTTP route", () => {
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
mockConfig = {
|
|
204
|
+
secretDetection: {
|
|
205
|
+
enabled: true,
|
|
206
|
+
blockIngress: true,
|
|
207
|
+
},
|
|
208
|
+
...BASE_CONFIG,
|
|
209
|
+
};
|
|
210
|
+
persistUserMessageMock.mockClear();
|
|
211
|
+
runAgentLoopMock.mockClear();
|
|
212
|
+
addMessageMock.mockClear();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("POST /v1/messages with GitHub token returns 422 secret_blocked", async () => {
|
|
216
|
+
const req = makeRequest({
|
|
217
|
+
content: "Here is my token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const res = await handleSendMessage(
|
|
221
|
+
req,
|
|
222
|
+
makeSendMessageDeps(),
|
|
223
|
+
testAuthContext,
|
|
224
|
+
);
|
|
225
|
+
expect(res.status).toBe(422);
|
|
226
|
+
|
|
227
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
228
|
+
expect(body.error).toBe("secret_blocked");
|
|
229
|
+
expect(body.accepted).toBe(false);
|
|
230
|
+
expect(body.detectedTypes).toContain("GitHub Token");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("POST /v1/messages with normal text returns 202 accepted", async () => {
|
|
234
|
+
const req = makeRequest({
|
|
235
|
+
content: "Hello, can you help me with my project?",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const res = await handleSendMessage(
|
|
239
|
+
req,
|
|
240
|
+
makeSendMessageDeps(),
|
|
241
|
+
testAuthContext,
|
|
242
|
+
);
|
|
243
|
+
expect(res.status).toBe(202);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("POST /v1/messages with bypassSecretCheck: true and secret returns 202", async () => {
|
|
247
|
+
const req = makeRequest({
|
|
248
|
+
content: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
|
|
249
|
+
bypassSecretCheck: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const res = await handleSendMessage(
|
|
253
|
+
req,
|
|
254
|
+
makeSendMessageDeps(),
|
|
255
|
+
testAuthContext,
|
|
256
|
+
);
|
|
257
|
+
expect(res.status).toBe(202);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("POST /v1/messages with JWT eyJ... returns 202 (not in curated patterns)", async () => {
|
|
261
|
+
const req = makeRequest({
|
|
262
|
+
content:
|
|
263
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const res = await handleSendMessage(
|
|
267
|
+
req,
|
|
268
|
+
makeSendMessageDeps(),
|
|
269
|
+
testAuthContext,
|
|
270
|
+
);
|
|
271
|
+
expect(res.status).toBe(202);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("POST /v1/messages with blockIngress: false config and secret returns 202", async () => {
|
|
275
|
+
mockConfig = {
|
|
276
|
+
secretDetection: {
|
|
277
|
+
enabled: true,
|
|
278
|
+
blockIngress: false,
|
|
279
|
+
},
|
|
280
|
+
...BASE_CONFIG,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const req = makeRequest({
|
|
284
|
+
content: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij1234",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const res = await handleSendMessage(
|
|
288
|
+
req,
|
|
289
|
+
makeSendMessageDeps(),
|
|
290
|
+
testAuthContext,
|
|
291
|
+
);
|
|
292
|
+
expect(res.status).toBe(202);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("message is NOT persisted when blocked", async () => {
|
|
296
|
+
const req = makeRequest({
|
|
297
|
+
content: "AKIAIOSFODNN7EXAMPLE",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const res = await handleSendMessage(
|
|
301
|
+
req,
|
|
302
|
+
makeSendMessageDeps(),
|
|
303
|
+
testAuthContext,
|
|
304
|
+
);
|
|
305
|
+
expect(res.status).toBe(422);
|
|
306
|
+
|
|
307
|
+
// persistUserMessage should not have been called
|
|
308
|
+
expect(persistUserMessageMock).not.toHaveBeenCalled();
|
|
309
|
+
// addMessage should not have been called
|
|
310
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mocks — must be declared before any imports that depend on them
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
let mockConfig = {
|
|
8
|
+
secretDetection: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
blockIngress: true,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
mock.module("../config/loader.js", () => ({
|
|
15
|
+
getConfig: () => mockConfig,
|
|
16
|
+
loadConfig: () => mockConfig,
|
|
17
|
+
invalidateConfigCache: () => {},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module("../util/logger.js", () => ({
|
|
21
|
+
getLogger: () => ({
|
|
22
|
+
info: () => {},
|
|
23
|
+
warn: () => {},
|
|
24
|
+
error: () => {},
|
|
25
|
+
debug: () => {},
|
|
26
|
+
trace: () => {},
|
|
27
|
+
fatal: () => {},
|
|
28
|
+
silent: () => {},
|
|
29
|
+
child: function () {
|
|
30
|
+
return this;
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module("../util/platform.js", () => ({
|
|
36
|
+
getRootDir: () => "/tmp/vellum-test-ingress",
|
|
37
|
+
getWorkspaceDir: () => "/tmp/vellum-test-ingress/workspace",
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { resetAllowlist } from "../security/secret-allowlist.js";
|
|
41
|
+
import { checkIngressForSecrets } from "../security/secret-ingress.js";
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Tests
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
describe("checkIngressForSecrets", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
mockConfig = {
|
|
50
|
+
secretDetection: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
blockIngress: true,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
resetAllowlist();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
resetAllowlist();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── Blocked patterns ───────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
test("blocks Google OAuth secret (GOCSPX-*)", () => {
|
|
65
|
+
const result = checkIngressForSecrets(
|
|
66
|
+
"My client secret is GOCSPX-abcdefghijklmnopqrstuvwxyz12",
|
|
67
|
+
);
|
|
68
|
+
expect(result.blocked).toBe(true);
|
|
69
|
+
expect(result.detectedTypes).toContain("Google OAuth Client Secret");
|
|
70
|
+
expect(result.userNotice).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("blocks GitHub PAT (ghp_*)", () => {
|
|
74
|
+
const result = checkIngressForSecrets(
|
|
75
|
+
"Here is my token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
|
|
76
|
+
);
|
|
77
|
+
expect(result.blocked).toBe(true);
|
|
78
|
+
expect(result.detectedTypes).toContain("GitHub Token");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("blocks Slack bot token (xoxb-*)", () => {
|
|
82
|
+
const result = checkIngressForSecrets(
|
|
83
|
+
"Use this: xoxb-1234567890-9876543210-AbCdEfGhIjKlMnOpQrStUvWx",
|
|
84
|
+
);
|
|
85
|
+
expect(result.blocked).toBe(true);
|
|
86
|
+
expect(result.detectedTypes).toContain("Slack Bot Token");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("blocks Anthropic API key (sk-ant-*)", () => {
|
|
90
|
+
const key =
|
|
91
|
+
"sk-ant-api03-abcDefGhiJklMnoPqrStuVwxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIj";
|
|
92
|
+
const result = checkIngressForSecrets(`Key: ${key}`);
|
|
93
|
+
expect(result.blocked).toBe(true);
|
|
94
|
+
expect(result.detectedTypes).toContain("Anthropic API Key");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("blocks private key header", () => {
|
|
98
|
+
const result = checkIngressForSecrets(
|
|
99
|
+
"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAK...",
|
|
100
|
+
);
|
|
101
|
+
expect(result.blocked).toBe(true);
|
|
102
|
+
expect(result.detectedTypes).toContain("Private Key");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("blocks AWS access key (AKIA*)", () => {
|
|
106
|
+
const result = checkIngressForSecrets("AWS key: AKIAIOSFODNN7EXAMPLE");
|
|
107
|
+
expect(result.blocked).toBe(true);
|
|
108
|
+
expect(result.detectedTypes).toContain("AWS Access Key");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("blocks Stripe secret key (sk_live_*)", () => {
|
|
112
|
+
const result = checkIngressForSecrets("sk_live_abcdefghijklmnopqrstuvwx");
|
|
113
|
+
expect(result.blocked).toBe(true);
|
|
114
|
+
expect(result.detectedTypes).toContain("Stripe Secret Key");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("blocks SendGrid API key", () => {
|
|
118
|
+
const result = checkIngressForSecrets(
|
|
119
|
+
"SG.abcdefghijklmnopqrstuv.ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst",
|
|
120
|
+
);
|
|
121
|
+
expect(result.blocked).toBe(true);
|
|
122
|
+
expect(result.detectedTypes).toContain("SendGrid API Key");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("blocks npm token", () => {
|
|
126
|
+
const result = checkIngressForSecrets(
|
|
127
|
+
"npm_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
|
|
128
|
+
);
|
|
129
|
+
expect(result.blocked).toBe(true);
|
|
130
|
+
expect(result.detectedTypes).toContain("npm Token");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("blocks OpenAI project key (sk-proj-*)", () => {
|
|
134
|
+
const key = "sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCd";
|
|
135
|
+
const result = checkIngressForSecrets(`My key: ${key}`);
|
|
136
|
+
expect(result.blocked).toBe(true);
|
|
137
|
+
expect(result.detectedTypes).toContain("OpenAI Project Key");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("blocks Google API key (AIza*)", () => {
|
|
141
|
+
const result = checkIngressForSecrets(
|
|
142
|
+
"AIzaSyA0123456789abcdefghijklmnopqrstuvw",
|
|
143
|
+
);
|
|
144
|
+
expect(result.blocked).toBe(true);
|
|
145
|
+
expect(result.detectedTypes).toContain("Google API Key");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("blocks GitLab PAT (glpat-*)", () => {
|
|
149
|
+
const result = checkIngressForSecrets("glpat-abcdefghijklmnopqrst");
|
|
150
|
+
expect(result.blocked).toBe(true);
|
|
151
|
+
expect(result.detectedTypes).toContain("GitLab Token");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("blocks Telegram Bot Token", () => {
|
|
155
|
+
const result = checkIngressForSecrets(
|
|
156
|
+
"Bot token: 123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi",
|
|
157
|
+
);
|
|
158
|
+
expect(result.blocked).toBe(true);
|
|
159
|
+
expect(result.detectedTypes).toContain("Telegram Bot Token");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("blocks Twilio API Key (SK*)", () => {
|
|
163
|
+
const result = checkIngressForSecrets(
|
|
164
|
+
"Twilio key: SK0123456789abcdef0123456789abcdef",
|
|
165
|
+
);
|
|
166
|
+
expect(result.blocked).toBe(true);
|
|
167
|
+
expect(result.detectedTypes).toContain("Twilio API Key");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── Not blocked (excluded patterns) ────────────────────────────────
|
|
171
|
+
|
|
172
|
+
test("does not block normal text", () => {
|
|
173
|
+
const result = checkIngressForSecrets(
|
|
174
|
+
"Hello, can you help me set up my project?",
|
|
175
|
+
);
|
|
176
|
+
expect(result.blocked).toBe(false);
|
|
177
|
+
expect(result.detectedTypes).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("does not block high-entropy hex (40-char git SHA)", () => {
|
|
181
|
+
const result = checkIngressForSecrets(
|
|
182
|
+
"Commit: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
|
|
183
|
+
);
|
|
184
|
+
expect(result.blocked).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("does not block UUID", () => {
|
|
188
|
+
const result = checkIngressForSecrets(
|
|
189
|
+
"ID: 550e8400-e29b-41d4-a716-446655440000",
|
|
190
|
+
);
|
|
191
|
+
expect(result.blocked).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("does not block JWT (eyJ...)", () => {
|
|
195
|
+
const result = checkIngressForSecrets(
|
|
196
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
|
197
|
+
);
|
|
198
|
+
expect(result.blocked).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("does not block password=mysecretvalue (generic assignment)", () => {
|
|
202
|
+
const result = checkIngressForSecrets(
|
|
203
|
+
'password=mysecretvalue\nsecret="hello world"',
|
|
204
|
+
);
|
|
205
|
+
expect(result.blocked).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("does not block postgres connection string", () => {
|
|
209
|
+
const result = checkIngressForSecrets(
|
|
210
|
+
"postgres://user:pass@host:5432/mydb",
|
|
211
|
+
);
|
|
212
|
+
expect(result.blocked).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── Config flags ───────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
test("does not block when secretDetection.enabled is false", () => {
|
|
218
|
+
mockConfig = {
|
|
219
|
+
secretDetection: {
|
|
220
|
+
enabled: false,
|
|
221
|
+
blockIngress: true,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
const result = checkIngressForSecrets(
|
|
225
|
+
"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
|
|
226
|
+
);
|
|
227
|
+
expect(result.blocked).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("does not block when blockIngress is false", () => {
|
|
231
|
+
mockConfig = {
|
|
232
|
+
secretDetection: {
|
|
233
|
+
enabled: true,
|
|
234
|
+
blockIngress: false,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const result = checkIngressForSecrets(
|
|
238
|
+
"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
|
|
239
|
+
);
|
|
240
|
+
expect(result.blocked).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ── Placeholder / test values ──────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
test("does not block placeholder test keys (sk-test-*)", () => {
|
|
246
|
+
const result = checkIngressForSecrets("sk-test-abc123");
|
|
247
|
+
expect(result.blocked).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("does not block fake_ prefixed values", () => {
|
|
251
|
+
// A GitHub-like token with fake_ prefix
|
|
252
|
+
const result = checkIngressForSecrets(
|
|
253
|
+
"fake_ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
|
|
254
|
+
);
|
|
255
|
+
expect(result.blocked).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("does not block repeated character patterns", () => {
|
|
259
|
+
// AKIA followed by 16 repeated X characters
|
|
260
|
+
const result = checkIngressForSecrets("AKIAXXXXXXXXXXXXXXXX");
|
|
261
|
+
expect(result.blocked).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── Multiple secrets ───────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
test("reports multiple detected types", () => {
|
|
267
|
+
const result = checkIngressForSecrets(
|
|
268
|
+
"AWS: AKIAIOSFODNN7EXAMPLE\nGitHub: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
|
|
269
|
+
);
|
|
270
|
+
expect(result.blocked).toBe(true);
|
|
271
|
+
expect(result.detectedTypes).toContain("AWS Access Key");
|
|
272
|
+
expect(result.detectedTypes).toContain("GitHub Token");
|
|
273
|
+
expect(result.detectedTypes.length).toBe(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("user notice does not echo secret values", () => {
|
|
277
|
+
const secret = "AKIAIOSFODNN7EXAMPLE";
|
|
278
|
+
const result = checkIngressForSecrets(`Key: ${secret}`);
|
|
279
|
+
expect(result.blocked).toBe(true);
|
|
280
|
+
expect(result.userNotice).toBeDefined();
|
|
281
|
+
expect(result.userNotice!).not.toContain(secret);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -47,7 +47,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
47
47
|
}),
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
-
// Track
|
|
50
|
+
// Track credential store writes
|
|
51
51
|
const storedKeys = new Map<string, string>();
|
|
52
52
|
mock.module("../security/secure-keys.js", () => {
|
|
53
53
|
const syncSet = (key: string, value: string) => {
|
|
@@ -112,7 +112,7 @@ describe("one-time send override", () => {
|
|
|
112
112
|
);
|
|
113
113
|
expect(result.isError).toBe(true);
|
|
114
114
|
expect(result.content).toContain("not enabled");
|
|
115
|
-
// Value must NOT be stored in
|
|
115
|
+
// Value must NOT be stored in credential store
|
|
116
116
|
expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
|
|
117
117
|
});
|
|
118
118
|
|
|
@@ -134,11 +134,11 @@ describe("one-time send override", () => {
|
|
|
134
134
|
);
|
|
135
135
|
expect(result.isError).toBe(false);
|
|
136
136
|
expect(result.content).toContain("NOT saved");
|
|
137
|
-
// Value must NOT be stored in
|
|
137
|
+
// Value must NOT be stored in credential store
|
|
138
138
|
expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
test("store delivery always persists to
|
|
141
|
+
test("store delivery always persists to credential store regardless of allowOneTimeSend", async () => {
|
|
142
142
|
mockConfig.secretDetection.allowOneTimeSend = true;
|
|
143
143
|
const context = {
|
|
144
144
|
workingDir: "/tmp",
|
|
@@ -137,7 +137,7 @@ describe("frontmatter feature-flag integration", () => {
|
|
|
137
137
|
expect(skill!.featureFlag).toBe("contacts");
|
|
138
138
|
|
|
139
139
|
const key = skillFlagKey(skill!);
|
|
140
|
-
expect(key).toBe("
|
|
140
|
+
expect(key).toBe("contacts");
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
test("skillFlagKey returns undefined for skill without feature-flag", () => {
|
|
@@ -162,7 +162,7 @@ describe("frontmatter feature-flag integration", () => {
|
|
|
162
162
|
|
|
163
163
|
test("resolveSkillStates includes skill with featureFlag when flag is ON", () => {
|
|
164
164
|
_setOverridesForTesting({
|
|
165
|
-
|
|
165
|
+
contacts: true,
|
|
166
166
|
});
|
|
167
167
|
const skill = buildSkillSummary("contacts", SKILL_MD_WITH_FLAG)!;
|
|
168
168
|
const config = makeConfig();
|
|
@@ -176,7 +176,7 @@ describe("frontmatter feature-flag integration", () => {
|
|
|
176
176
|
const skill = buildSkillSummary("plain-skill", SKILL_MD_WITHOUT_FLAG)!;
|
|
177
177
|
// Even with an explicit false override for this skill ID, it should pass through
|
|
178
178
|
_setOverridesForTesting({
|
|
179
|
-
"
|
|
179
|
+
"plain-skill": false,
|
|
180
180
|
});
|
|
181
181
|
const config = makeConfig();
|
|
182
182
|
|
|
@@ -200,7 +200,7 @@ describe("frontmatter feature-flag integration", () => {
|
|
|
200
200
|
|
|
201
201
|
// Step 3: Derive the flag key
|
|
202
202
|
const key = skillFlagKey(skill);
|
|
203
|
-
expect(key).toBe("
|
|
203
|
+
expect(key).toBe("contacts");
|
|
204
204
|
|
|
205
205
|
// Step 4: Check flag state — "contacts" has defaultEnabled: true in registry
|
|
206
206
|
const configDefault = makeConfig();
|