@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { getLogger } from "../../util/logger.js";
|
|
2
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const log = getLogger("workspace-migrations");
|
|
5
|
+
|
|
6
|
+
const BROKER_WAIT_INTERVAL_MS = 500;
|
|
7
|
+
const BROKER_WAIT_MAX_ATTEMPTS = 10; // 5 seconds total
|
|
8
|
+
|
|
9
|
+
export const migrateCredentialsFromKeychainMigration: WorkspaceMigration = {
|
|
10
|
+
id: "016-migrate-credentials-from-keychain",
|
|
11
|
+
description:
|
|
12
|
+
"Copy keychain credentials back to encrypted store for CES unification",
|
|
13
|
+
|
|
14
|
+
async down(_workspaceDir: string): Promise<void> {
|
|
15
|
+
// Reverse: copy credentials from encrypted store back to keychain.
|
|
16
|
+
// Mirrors the forward logic of 015-migrate-credentials-to-keychain.
|
|
17
|
+
if (
|
|
18
|
+
process.env.VELLUM_DESKTOP_APP !== "1" ||
|
|
19
|
+
process.env.VELLUM_DEV === "1"
|
|
20
|
+
) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { createBrokerClient } =
|
|
25
|
+
await import("../../security/keychain-broker-client.js");
|
|
26
|
+
const client = createBrokerClient();
|
|
27
|
+
|
|
28
|
+
let brokerAvailable = false;
|
|
29
|
+
for (let i = 0; i < BROKER_WAIT_MAX_ATTEMPTS; i++) {
|
|
30
|
+
if (client.isAvailable()) {
|
|
31
|
+
brokerAvailable = true;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
await new Promise((r) => setTimeout(r, BROKER_WAIT_INTERVAL_MS));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!brokerAvailable) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Keychain broker not available after waiting — credential rollback " +
|
|
40
|
+
"will be retried on next startup",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { listKeys, getKey, deleteKey } =
|
|
45
|
+
await import("../../security/encrypted-store.js");
|
|
46
|
+
|
|
47
|
+
const accounts = listKeys();
|
|
48
|
+
if (accounts.length === 0) return;
|
|
49
|
+
|
|
50
|
+
let rolledBackCount = 0;
|
|
51
|
+
let failedCount = 0;
|
|
52
|
+
|
|
53
|
+
for (const account of accounts) {
|
|
54
|
+
const value = getKey(account);
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
log.warn(
|
|
57
|
+
{ account },
|
|
58
|
+
"Failed to read key from encrypted store during rollback — skipping",
|
|
59
|
+
);
|
|
60
|
+
failedCount++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await client.set(account, value);
|
|
65
|
+
if (result.status === "ok") {
|
|
66
|
+
deleteKey(account);
|
|
67
|
+
rolledBackCount++;
|
|
68
|
+
} else {
|
|
69
|
+
log.warn(
|
|
70
|
+
{ account, status: result.status },
|
|
71
|
+
"Failed to write key to keychain during rollback — skipping",
|
|
72
|
+
);
|
|
73
|
+
failedCount++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
log.info(
|
|
78
|
+
{ rolledBackCount, failedCount },
|
|
79
|
+
"Credential rollback from encrypted store to keychain complete",
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async run(_workspaceDir: string): Promise<void> {
|
|
84
|
+
// Only run on mac production builds (desktop app, non-dev).
|
|
85
|
+
if (
|
|
86
|
+
process.env.VELLUM_DESKTOP_APP !== "1" ||
|
|
87
|
+
process.env.VELLUM_DEV === "1"
|
|
88
|
+
) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { createBrokerClient } =
|
|
93
|
+
await import("../../security/keychain-broker-client.js");
|
|
94
|
+
const client = createBrokerClient();
|
|
95
|
+
|
|
96
|
+
// Wait for the broker to become available (up to 5 seconds), matching
|
|
97
|
+
// the retry strategy in secure-keys.ts waitForBrokerAvailability().
|
|
98
|
+
let brokerAvailable = false;
|
|
99
|
+
for (let i = 0; i < BROKER_WAIT_MAX_ATTEMPTS; i++) {
|
|
100
|
+
if (client.isAvailable()) {
|
|
101
|
+
brokerAvailable = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
await new Promise((r) => setTimeout(r, BROKER_WAIT_INTERVAL_MS));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!brokerAvailable) {
|
|
108
|
+
// Unlike migration 015, we return silently here. If the broker is not
|
|
109
|
+
// available, credentials may already be in the encrypted store from
|
|
110
|
+
// before migration 015 ran, or from a non-desktop environment.
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { setKey } = await import("../../security/encrypted-store.js");
|
|
115
|
+
|
|
116
|
+
const accounts = await client.list();
|
|
117
|
+
if (accounts.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let migratedCount = 0;
|
|
122
|
+
let failedCount = 0;
|
|
123
|
+
|
|
124
|
+
for (const account of accounts) {
|
|
125
|
+
const result = await client.get(account);
|
|
126
|
+
if (!result || !result.found || result.value === undefined) {
|
|
127
|
+
log.warn({ account }, "Failed to read key from keychain — skipping");
|
|
128
|
+
failedCount++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const written = setKey(account, result.value);
|
|
133
|
+
if (written) {
|
|
134
|
+
await client.del(account);
|
|
135
|
+
migratedCount++;
|
|
136
|
+
} else {
|
|
137
|
+
log.warn(
|
|
138
|
+
{ account },
|
|
139
|
+
"Failed to write key to encrypted store — skipping",
|
|
140
|
+
);
|
|
141
|
+
failedCount++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
log.info(
|
|
146
|
+
{ migratedCount, failedCount },
|
|
147
|
+
"Credential migration from keychain complete",
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmdirSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { eq } from "drizzle-orm";
|
|
12
|
+
|
|
13
|
+
import { generateUserFileSlug } from "../../contacts/contact-store.js";
|
|
14
|
+
import { getDb } from "../../memory/db.js";
|
|
15
|
+
import { contacts } from "../../memory/schema/contacts.js";
|
|
16
|
+
import {
|
|
17
|
+
isTemplateContent,
|
|
18
|
+
stripCommentLines,
|
|
19
|
+
} from "../../prompts/system-prompt.js";
|
|
20
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export const seedPersonaDirsMigration: WorkspaceMigration = {
|
|
23
|
+
id: "017-seed-persona-dirs",
|
|
24
|
+
description:
|
|
25
|
+
"Create users/ and channels/ persona directories and migrate customized USER.md",
|
|
26
|
+
|
|
27
|
+
down(workspaceDir: string): void {
|
|
28
|
+
// Remove the seeded persona directories only if they are empty.
|
|
29
|
+
// We don't delete user-created content — only clean up the empty
|
|
30
|
+
// directories that the forward migration created.
|
|
31
|
+
const usersDir = join(workspaceDir, "users");
|
|
32
|
+
const channelsDir = join(workspaceDir, "channels");
|
|
33
|
+
|
|
34
|
+
for (const dir of [usersDir, channelsDir]) {
|
|
35
|
+
if (!existsSync(dir)) continue;
|
|
36
|
+
try {
|
|
37
|
+
const entries = readdirSync(dir);
|
|
38
|
+
if (entries.length === 0) {
|
|
39
|
+
rmdirSync(dir);
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Best-effort: skip if we can't read or remove
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
run(workspaceDir: string): void {
|
|
48
|
+
// Create persona directories
|
|
49
|
+
mkdirSync(join(workspaceDir, "users"), { recursive: true });
|
|
50
|
+
mkdirSync(join(workspaceDir, "channels"), { recursive: true });
|
|
51
|
+
|
|
52
|
+
// Check if USER.md exists and has been customized
|
|
53
|
+
const userMdPath = join(workspaceDir, "USER.md");
|
|
54
|
+
if (!existsSync(userMdPath)) return;
|
|
55
|
+
|
|
56
|
+
const rawContent = readFileSync(userMdPath, "utf-8");
|
|
57
|
+
const content = stripCommentLines(rawContent);
|
|
58
|
+
if (!content) return;
|
|
59
|
+
|
|
60
|
+
// Skip if the content is the unmodified template
|
|
61
|
+
if (isTemplateContent(content, "USER.md")) return;
|
|
62
|
+
|
|
63
|
+
// Determine destination filename based on guardian contact
|
|
64
|
+
let destFilename = "guardian.md";
|
|
65
|
+
try {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
const guardian = db
|
|
68
|
+
.select()
|
|
69
|
+
.from(contacts)
|
|
70
|
+
.where(eq(contacts.role, "guardian"))
|
|
71
|
+
.limit(1)
|
|
72
|
+
.get();
|
|
73
|
+
|
|
74
|
+
if (guardian) {
|
|
75
|
+
if (guardian.userFile) {
|
|
76
|
+
destFilename = guardian.userFile;
|
|
77
|
+
} else {
|
|
78
|
+
const slug = generateUserFileSlug(guardian.displayName);
|
|
79
|
+
db.update(contacts)
|
|
80
|
+
.set({ userFile: slug })
|
|
81
|
+
.where(eq(contacts.id, guardian.id))
|
|
82
|
+
.run();
|
|
83
|
+
destFilename = slug;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// DB might not be initialized yet — fall back to guardian.md
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const destPath = join(workspaceDir, "users", destFilename);
|
|
91
|
+
if (!existsSync(destPath)) {
|
|
92
|
+
copyFileSync(userMdPath, destPath);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
* - Skips if the old workspace directory doesn't exist or is empty.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
cpSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
18
24
|
import { join } from "node:path";
|
|
19
25
|
|
|
20
26
|
import {
|
|
@@ -30,6 +36,22 @@ export const migrateToWorkspaceVolumeMigration: WorkspaceMigration = {
|
|
|
30
36
|
description:
|
|
31
37
|
"Copy workspace data from old /data/.vellum/workspace to new WORKSPACE_DIR volume on first boot",
|
|
32
38
|
|
|
39
|
+
down(workspaceDir: string): void {
|
|
40
|
+
// This migration copies data between volumes. Actually reversing the copy
|
|
41
|
+
// (deleting data from the workspace volume) is dangerous and could cause
|
|
42
|
+
// data loss. Instead, we just remove the sentinel file so the migration
|
|
43
|
+
// will re-run and re-evaluate on next startup.
|
|
44
|
+
const sentinelPath = join(workspaceDir, SENTINEL_FILENAME);
|
|
45
|
+
if (existsSync(sentinelPath)) {
|
|
46
|
+
try {
|
|
47
|
+
unlinkSync(sentinelPath);
|
|
48
|
+
} catch {
|
|
49
|
+
// Best-effort — the migration runner's checkpoint removal will
|
|
50
|
+
// also ensure the migration re-runs.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
33
55
|
run(workspaceDir: string): void {
|
|
34
56
|
const workspaceDirOverride = getWorkspaceDirOverride();
|
|
35
57
|
|
|
@@ -10,6 +10,10 @@ import { appDirRenameMigration } from "./010-app-dir-rename.js";
|
|
|
10
10
|
import { backfillInstallationIdMigration } from "./011-backfill-installation-id.js";
|
|
11
11
|
import { renameConversationDiskViewDirsMigration } from "./012-rename-conversation-disk-view-dirs.js";
|
|
12
12
|
import { repairConversationDiskViewMigration } from "./013-repair-conversation-disk-view.js";
|
|
13
|
+
import { migrateCredentialsToKeychainMigration } from "./015-migrate-credentials-to-keychain.js";
|
|
14
|
+
import { extractFeatureFlagsToProtectedMigration } from "./016-extract-feature-flags-to-protected.js";
|
|
15
|
+
import { migrateCredentialsFromKeychainMigration } from "./016-migrate-credentials-from-keychain.js";
|
|
16
|
+
import { seedPersonaDirsMigration } from "./017-seed-persona-dirs.js";
|
|
13
17
|
import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
|
|
14
18
|
import type { WorkspaceMigration } from "./types.js";
|
|
15
19
|
|
|
@@ -31,4 +35,8 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
|
|
|
31
35
|
renameConversationDiskViewDirsMigration,
|
|
32
36
|
repairConversationDiskViewMigration,
|
|
33
37
|
migrateToWorkspaceVolumeMigration,
|
|
38
|
+
migrateCredentialsToKeychainMigration,
|
|
39
|
+
migrateCredentialsFromKeychainMigration,
|
|
40
|
+
seedPersonaDirsMigration,
|
|
41
|
+
extractFeatureFlagsToProtectedMigration,
|
|
34
42
|
];
|
|
@@ -7,10 +7,16 @@ import type { WorkspaceMigration } from "./types.js";
|
|
|
7
7
|
|
|
8
8
|
const log = getLogger("workspace-migrations");
|
|
9
9
|
|
|
10
|
+
export function getLastWorkspaceMigrationId(
|
|
11
|
+
migrations: WorkspaceMigration[],
|
|
12
|
+
): string | null {
|
|
13
|
+
return migrations.length > 0 ? migrations[migrations.length - 1].id : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export type CheckpointFile = {
|
|
11
17
|
applied: Record<
|
|
12
18
|
string,
|
|
13
|
-
{ appliedAt: string; status?: "started" | "completed" }
|
|
19
|
+
{ appliedAt: string; status?: "started" | "completed" | "rolling_back" }
|
|
14
20
|
>;
|
|
15
21
|
};
|
|
16
22
|
|
|
@@ -73,7 +79,7 @@ export async function runWorkspaceMigrations(
|
|
|
73
79
|
const checkpoints = loadCheckpoints(workspaceDir);
|
|
74
80
|
|
|
75
81
|
for (const [id, entry] of Object.entries(checkpoints.applied)) {
|
|
76
|
-
if (entry.status === "started") {
|
|
82
|
+
if (entry.status === "started" || entry.status === "rolling_back") {
|
|
77
83
|
log.warn(
|
|
78
84
|
`Workspace migration "${id}" was interrupted during a previous run; will re-run`,
|
|
79
85
|
);
|
|
@@ -115,3 +121,101 @@ export async function runWorkspaceMigrations(
|
|
|
115
121
|
saveCheckpoints(workspaceDir, checkpoints);
|
|
116
122
|
}
|
|
117
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Roll back workspace (filesystem) migrations in reverse order, stopping before
|
|
127
|
+
* the target migration.
|
|
128
|
+
*
|
|
129
|
+
* Migrations after `targetMigrationId` in the registry array are reversed in
|
|
130
|
+
* reverse order; the target migration itself is kept applied.
|
|
131
|
+
*
|
|
132
|
+
* **Usage**: Pass the full migrations array (typically `WORKSPACE_MIGRATIONS`
|
|
133
|
+
* from `registry.ts`) and the ID of the migration you want to roll back *to*.
|
|
134
|
+
* For example, `rollbackWorkspaceMigrations(dir, migrations, "010-app-dir-rename")`
|
|
135
|
+
* rolls back all applied migrations that appear after `010-app-dir-rename` in
|
|
136
|
+
* the registry.
|
|
137
|
+
*
|
|
138
|
+
* **Checkpoint state**: Each rolled-back migration's entry is deleted from the
|
|
139
|
+
* `.workspace-migrations.json` checkpoint file. If the process crashes
|
|
140
|
+
* mid-rollback, the `"rolling_back"` marker is detected and cleared by
|
|
141
|
+
* `runWorkspaceMigrations` on the next startup (it re-runs interrupted
|
|
142
|
+
* migrations).
|
|
143
|
+
*
|
|
144
|
+
* **Warning — data loss**: Every workspace migration must define a `down()`
|
|
145
|
+
* method (enforced at the type level), but some rollbacks are lossy (e.g.,
|
|
146
|
+
* file deletions or format conversions that discard the original cannot fully
|
|
147
|
+
* restore prior state). Review each migration's `down()` implementation
|
|
148
|
+
* before calling this function.
|
|
149
|
+
*
|
|
150
|
+
* **Important**: Stop the assistant before running rollbacks. Rolling back
|
|
151
|
+
* workspace migrations while the assistant is running may cause file conflicts,
|
|
152
|
+
* stale caches, or data corruption.
|
|
153
|
+
*
|
|
154
|
+
* @param workspaceDir The workspace directory path (e.g., `~/.vellum/workspace`).
|
|
155
|
+
* @param migrations The full ordered array of workspace migrations (from `WORKSPACE_MIGRATIONS`).
|
|
156
|
+
* @param targetMigrationId The migration ID to roll back to (exclusive — all
|
|
157
|
+
* migrations after this one are reversed).
|
|
158
|
+
*/
|
|
159
|
+
export async function rollbackWorkspaceMigrations(
|
|
160
|
+
workspaceDir: string,
|
|
161
|
+
migrations: WorkspaceMigration[],
|
|
162
|
+
targetMigrationId: string,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
// Find the index of the target migration
|
|
165
|
+
const targetIndex = migrations.findIndex((m) => m.id === targetMigrationId);
|
|
166
|
+
if (targetIndex === -1) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Target migration "${targetMigrationId}" not found in the migrations array`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Collect migrations that come after the target, in reverse order
|
|
173
|
+
const migrationsToRollback = migrations.slice(targetIndex + 1).reverse();
|
|
174
|
+
if (migrationsToRollback.length === 0) {
|
|
175
|
+
log.info("No migrations to roll back");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const checkpoints = loadCheckpoints(workspaceDir);
|
|
180
|
+
|
|
181
|
+
for (const migration of migrationsToRollback) {
|
|
182
|
+
// Only roll back migrations that have been fully applied.
|
|
183
|
+
// Legacy checkpoints may not have a status field (just appliedAt) — treat
|
|
184
|
+
// missing/undefined status as completed, matching runWorkspaceMigrations behavior.
|
|
185
|
+
const entry = checkpoints.applied[migration.id];
|
|
186
|
+
if (
|
|
187
|
+
!entry ||
|
|
188
|
+
entry.status === "started" ||
|
|
189
|
+
entry.status === "rolling_back"
|
|
190
|
+
) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
log.info(
|
|
195
|
+
`Rolling back workspace migration: ${migration.id} — ${migration.description}`,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Mark as rolling_back before execution (for crash recovery)
|
|
199
|
+
checkpoints.applied[migration.id] = {
|
|
200
|
+
appliedAt: checkpoints.applied[migration.id]!.appliedAt,
|
|
201
|
+
status: "rolling_back",
|
|
202
|
+
};
|
|
203
|
+
saveCheckpoints(workspaceDir, checkpoints);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await migration.down(workspaceDir);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
log.error(
|
|
209
|
+
{ migrationId: migration.id, error },
|
|
210
|
+
`Workspace migration rollback failed: ${migration.id}`,
|
|
211
|
+
);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Remove the migration entry from checkpoints
|
|
216
|
+
delete checkpoints.applied[migration.id];
|
|
217
|
+
saveCheckpoints(workspaceDir, checkpoints);
|
|
218
|
+
|
|
219
|
+
log.info(`Rolled back workspace migration: ${migration.id}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -8,4 +8,8 @@ export interface WorkspaceMigration {
|
|
|
8
8
|
* Must be idempotent — safe to re-run if it was interrupted.
|
|
9
9
|
* Both synchronous and asynchronous migrations are supported. */
|
|
10
10
|
run(workspaceDir: string): void | Promise<void>;
|
|
11
|
+
/** Reverse the migration. Receives the workspace directory path.
|
|
12
|
+
* Must be idempotent — safe to re-run if it was interrupted.
|
|
13
|
+
* Both synchronous and asynchronous rollbacks are supported. */
|
|
14
|
+
down(workspaceDir: string): void | Promise<void>;
|
|
11
15
|
}
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { describe, expect, mock, spyOn, test } from "bun:test";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Mocks — must be set up before importing modules that use them
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
mock.module("@anthropic-ai/claude-agent-sdk", () => ({
|
|
10
|
-
query: () => ({
|
|
11
|
-
async *[Symbol.asyncIterator]() {
|
|
12
|
-
yield {
|
|
13
|
-
type: "result" as const,
|
|
14
|
-
session_id: "s",
|
|
15
|
-
subtype: "success" as const,
|
|
16
|
-
result: "ok",
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
}),
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
mock.module("../util/logger.js", () => ({
|
|
23
|
-
getLogger: () =>
|
|
24
|
-
new Proxy({} as Record<string, unknown>, {
|
|
25
|
-
get: () => () => {},
|
|
26
|
-
}),
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
mock.module("../config/loader.js", () => ({
|
|
30
|
-
getConfig: () => ({
|
|
31
|
-
ui: {},
|
|
32
|
-
services: {
|
|
33
|
-
inference: {
|
|
34
|
-
mode: "your-own",
|
|
35
|
-
provider: "anthropic",
|
|
36
|
-
model: "claude-opus-4-6",
|
|
37
|
-
},
|
|
38
|
-
"image-generation": {
|
|
39
|
-
mode: "your-own",
|
|
40
|
-
provider: "gemini",
|
|
41
|
-
model: "gemini-3.1-flash-image-preview",
|
|
42
|
-
},
|
|
43
|
-
"web-search": { mode: "your-own", provider: "inference-provider-native" },
|
|
44
|
-
},
|
|
45
|
-
}),
|
|
46
|
-
loadConfig: () => ({}),
|
|
47
|
-
loadRawConfig: () => ({}),
|
|
48
|
-
saveConfig: () => {},
|
|
49
|
-
saveRawConfig: () => {},
|
|
50
|
-
invalidateConfigCache: () => {},
|
|
51
|
-
getNestedValue: () => undefined,
|
|
52
|
-
setNestedValue: () => {},
|
|
53
|
-
syncConfigToLockfile: () => {},
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
mock.module("../security/secure-keys.js", () => ({
|
|
57
|
-
getSecureKeyAsync: async (name: string) =>
|
|
58
|
-
name === "anthropic" ? "fake-anthropic-key" : null,
|
|
59
|
-
getProviderKeyAsync: async (provider: string) =>
|
|
60
|
-
provider === "anthropic" ? "fake-anthropic-key" : undefined,
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Imports (after mocks)
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
import { claudeCodeTool } from "../tools/claude-code/claude-code.js";
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Locate the bundled skill directory relative to the test file
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
const SKILL_DIR = path.resolve(
|
|
74
|
-
import.meta.dirname ?? __dirname,
|
|
75
|
-
"../config/bundled-skills/claude-code",
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
const SHARED_DIR = path.resolve(
|
|
79
|
-
import.meta.dirname ?? __dirname,
|
|
80
|
-
"../config/bundled-skills/_shared",
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Tests
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
describe("Claude Code skill migration regression", () => {
|
|
88
|
-
test("skill script wrapper exports a `run` function", async () => {
|
|
89
|
-
const wrapperPath = path.join(SKILL_DIR, "tools/claude-code.ts");
|
|
90
|
-
// The wrapper module must exist and export `run`
|
|
91
|
-
const mod = await import(wrapperPath);
|
|
92
|
-
expect(typeof mod.run).toBe("function");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("TOOLS.json manifest lists claude_code as the tool name", () => {
|
|
96
|
-
const manifestPath = path.join(SKILL_DIR, "TOOLS.json");
|
|
97
|
-
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
98
|
-
const manifest = JSON.parse(raw);
|
|
99
|
-
|
|
100
|
-
expect(manifest.version).toBe(1);
|
|
101
|
-
expect(Array.isArray(manifest.tools)).toBe(true);
|
|
102
|
-
|
|
103
|
-
const toolNames = manifest.tools.map((t: { name: string }) => t.name);
|
|
104
|
-
expect(toolNames).toContain("claude_code");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("TOOLS.json input_schema matches claudeCodeTool.getDefinition()", () => {
|
|
108
|
-
const manifestPath = path.join(SKILL_DIR, "TOOLS.json");
|
|
109
|
-
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
110
|
-
const manifest = JSON.parse(raw);
|
|
111
|
-
|
|
112
|
-
const manifestTool = manifest.tools.find(
|
|
113
|
-
(t: { name: string }) => t.name === "claude_code",
|
|
114
|
-
);
|
|
115
|
-
expect(manifestTool).toBeDefined();
|
|
116
|
-
|
|
117
|
-
const runtimeDef = claudeCodeTool.getDefinition();
|
|
118
|
-
|
|
119
|
-
// The input_schema declared in the static manifest must match the
|
|
120
|
-
// runtime definition. Drift here would mean the model sees a different
|
|
121
|
-
// schema than the executor actually supports.
|
|
122
|
-
expect(manifestTool.input_schema).toEqual(runtimeDef.input_schema);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("TOOLS.json description matches claudeCodeTool.getDefinition()", () => {
|
|
126
|
-
const manifestPath = path.join(SKILL_DIR, "TOOLS.json");
|
|
127
|
-
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
128
|
-
const manifest = JSON.parse(raw);
|
|
129
|
-
|
|
130
|
-
const manifestTool = manifest.tools.find(
|
|
131
|
-
(t: { name: string }) => t.name === "claude_code",
|
|
132
|
-
);
|
|
133
|
-
expect(manifestTool).toBeDefined();
|
|
134
|
-
|
|
135
|
-
const runtimeDef = claudeCodeTool.getDefinition();
|
|
136
|
-
|
|
137
|
-
// Description parity guards against a manifest edit that diverges from
|
|
138
|
-
// the canonical tool description.
|
|
139
|
-
expect(manifestTool.description).toBe(runtimeDef.description);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("wrapper run() delegates to claudeCodeTool.execute()", async () => {
|
|
143
|
-
// Verifies the wrapper is not a stale stub but actually calls through
|
|
144
|
-
// to the canonical execute method with the exact input and context.
|
|
145
|
-
const spy = spyOn(claudeCodeTool, "execute");
|
|
146
|
-
|
|
147
|
-
const wrapperPath = path.join(SKILL_DIR, "tools/claude-code.ts");
|
|
148
|
-
const mod = await import(wrapperPath);
|
|
149
|
-
|
|
150
|
-
const input = { prompt: "hello" };
|
|
151
|
-
const ctx = {
|
|
152
|
-
conversationId: "test",
|
|
153
|
-
workingDir: "/tmp",
|
|
154
|
-
onOutput: () => {},
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const result = await mod.run(input, ctx);
|
|
158
|
-
expect(result.isError).toBeFalsy();
|
|
159
|
-
|
|
160
|
-
// The wrapper must delegate to the canonical execute method
|
|
161
|
-
expect(spy).toHaveBeenCalledTimes(1);
|
|
162
|
-
expect(spy).toHaveBeenCalledWith(input, ctx);
|
|
163
|
-
|
|
164
|
-
spy.mockRestore();
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
// Bundled skill shared guidance — CES tools instead of token-reveal
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
describe("CLI_RETRIEVAL_PATTERN.md CES guidance", () => {
|
|
173
|
-
const patternPath = path.join(SHARED_DIR, "CLI_RETRIEVAL_PATTERN.md");
|
|
174
|
-
const content = fs.readFileSync(patternPath, "utf-8");
|
|
175
|
-
|
|
176
|
-
test("teaches handle discovery via assistant credentials list", () => {
|
|
177
|
-
expect(content).toContain("assistant credentials list");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("teaches handle discovery via assistant oauth connections list", () => {
|
|
181
|
-
expect(content).toContain("assistant oauth connections list");
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("teaches make_authenticated_request CES tool", () => {
|
|
185
|
-
expect(content).toContain("make_authenticated_request");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("teaches run_authenticated_command CES tool", () => {
|
|
189
|
-
expect(content).toContain("run_authenticated_command");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("warns that host_bash is outside CES secrecy boundary", () => {
|
|
193
|
-
expect(content).toContain("outside the CES secrecy boundary");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// -- Deprecated patterns must NOT appear --
|
|
197
|
-
|
|
198
|
-
test("does not teach proxied bash with credential_ids", () => {
|
|
199
|
-
expect(content).not.toContain("credential_ids");
|
|
200
|
-
expect(content).not.toContain("network_mode: proxied");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("does not teach oauth connections token for raw token extraction", () => {
|
|
204
|
-
expect(content).not.toContain("oauth connections token");
|
|
205
|
-
});
|
|
206
|
-
});
|