@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
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { getGatewayInternalBaseUrl } from "../config/env.js";
|
|
2
|
-
import { getBaseDataDir, getIsContainerized } from "../config/env-registry.js";
|
|
3
|
-
import { readLockfile } from "../util/platform.js";
|
|
4
|
-
import { sleep } from "../util/retry.js";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_PROBE_TIMEOUT_MS = 2_000;
|
|
7
|
-
const DEFAULT_RECOVERY_POLL_TIMEOUT_MS = 30_000;
|
|
8
|
-
const DEFAULT_RECOVERY_POLL_INTERVAL_MS = 250;
|
|
9
|
-
const DEFAULT_WAKE_TIMEOUT_MS = 90_000;
|
|
10
|
-
|
|
11
|
-
interface LockfileAssistantEntry {
|
|
12
|
-
assistantId?: string;
|
|
13
|
-
cloud?: string;
|
|
14
|
-
hatchedAt?: string | number | Date;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface WakeCommandResult {
|
|
18
|
-
exitCode: number;
|
|
19
|
-
stdout: string;
|
|
20
|
-
stderr: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface LocalGatewayHealthResult {
|
|
24
|
-
target: string;
|
|
25
|
-
healthy: boolean;
|
|
26
|
-
localDeployment: boolean;
|
|
27
|
-
error?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface EnsureLocalGatewayReadyResult extends LocalGatewayHealthResult {
|
|
31
|
-
recovered: boolean;
|
|
32
|
-
recoveryAttempted: boolean;
|
|
33
|
-
recoverySkipped: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface ProbeLocalGatewayHealthOptions {
|
|
37
|
-
timeoutMs?: number;
|
|
38
|
-
fetchImpl?: typeof fetch;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface EnsureLocalGatewayReadyOptions extends ProbeLocalGatewayHealthOptions {
|
|
42
|
-
pollTimeoutMs?: number;
|
|
43
|
-
pollIntervalMs?: number;
|
|
44
|
-
wakeTimeoutMs?: number;
|
|
45
|
-
runWakeCommand?: () => Promise<WakeCommandResult>;
|
|
46
|
-
sleepImpl?: (ms: number) => Promise<void>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function getLatestAssistantEntry(): LockfileAssistantEntry | null {
|
|
50
|
-
try {
|
|
51
|
-
const lockData = readLockfile();
|
|
52
|
-
const assistants = lockData?.assistants;
|
|
53
|
-
if (!Array.isArray(assistants) || assistants.length === 0) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const sorted = [...assistants].sort((a, b) => {
|
|
58
|
-
const dateA = new Date(
|
|
59
|
-
(a as LockfileAssistantEntry).hatchedAt || 0,
|
|
60
|
-
).getTime();
|
|
61
|
-
const dateB = new Date(
|
|
62
|
-
(b as LockfileAssistantEntry).hatchedAt || 0,
|
|
63
|
-
).getTime();
|
|
64
|
-
return dateB - dateA;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return (sorted[0] as LockfileAssistantEntry) ?? null;
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function resolveLocalDeployment(): boolean {
|
|
74
|
-
if (getIsContainerized()) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const latestAssistant = getLatestAssistantEntry();
|
|
79
|
-
if (typeof latestAssistant?.cloud === "string") {
|
|
80
|
-
return latestAssistant.cloud === "local";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Derive instance name from BASE_DATA_DIR which follows the multi-instance
|
|
88
|
-
* path pattern (~/.local/share/vellum/assistants/<name>/).
|
|
89
|
-
*/
|
|
90
|
-
function resolveInstanceNameFromBaseDataDir(): string | undefined {
|
|
91
|
-
const base = getBaseDataDir();
|
|
92
|
-
if (!base || typeof base !== "string") return undefined;
|
|
93
|
-
|
|
94
|
-
const normalized = base.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
95
|
-
const match = normalized.match(/\/assistants\/([^/]+)$/);
|
|
96
|
-
if (match) return match[1];
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function resolveLocalAssistantName(): string | undefined {
|
|
101
|
-
const fromPath = resolveInstanceNameFromBaseDataDir();
|
|
102
|
-
if (fromPath) return fromPath;
|
|
103
|
-
|
|
104
|
-
const latestAssistant = getLatestAssistantEntry();
|
|
105
|
-
if (
|
|
106
|
-
latestAssistant &&
|
|
107
|
-
typeof latestAssistant.assistantId === "string" &&
|
|
108
|
-
latestAssistant.assistantId.trim().length > 0
|
|
109
|
-
) {
|
|
110
|
-
return latestAssistant.assistantId.trim();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function formatError(err: unknown): string {
|
|
117
|
-
return err instanceof Error ? err.message : String(err);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function runDefaultWakeCommand(
|
|
121
|
-
timeoutMs: number,
|
|
122
|
-
): Promise<WakeCommandResult> {
|
|
123
|
-
const assistantName = resolveLocalAssistantName();
|
|
124
|
-
const command = assistantName
|
|
125
|
-
? ["vellum", "wake", assistantName]
|
|
126
|
-
: ["vellum", "wake"];
|
|
127
|
-
|
|
128
|
-
// Only when the assistant name came from the instance path (e.g.
|
|
129
|
-
// ~/.local/share/vellum/assistants/<name>/), unset BASE_DATA_DIR so the
|
|
130
|
-
// spawned CLI reads the global lockfile. When the name came from the
|
|
131
|
-
// lockfile, keep BASE_DATA_DIR — vellum wake resolves names through the
|
|
132
|
-
// lockfile rooted at BASE_DATA_DIR, so clearing it would read the wrong
|
|
133
|
-
// lockfile (e.g. $HOME) and fail or wake the wrong assistant.
|
|
134
|
-
const fromInstancePath = resolveInstanceNameFromBaseDataDir();
|
|
135
|
-
const env =
|
|
136
|
-
fromInstancePath && getBaseDataDir()
|
|
137
|
-
? { ...process.env, BASE_DATA_DIR: undefined }
|
|
138
|
-
: process.env;
|
|
139
|
-
|
|
140
|
-
return new Promise((resolve, reject) => {
|
|
141
|
-
const proc = Bun.spawn(command, {
|
|
142
|
-
stdout: "pipe",
|
|
143
|
-
stderr: "pipe",
|
|
144
|
-
env: {
|
|
145
|
-
...env,
|
|
146
|
-
PATH: [env.PATH, "/opt/homebrew/bin", "/usr/local/bin"]
|
|
147
|
-
.filter(Boolean)
|
|
148
|
-
.join(":"),
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
const timer = setTimeout(() => {
|
|
152
|
-
proc.kill();
|
|
153
|
-
reject(
|
|
154
|
-
new Error(`Process timed out after ${timeoutMs}ms: ${command[0]}`),
|
|
155
|
-
);
|
|
156
|
-
}, timeoutMs);
|
|
157
|
-
proc.exited.then(async (exitCode) => {
|
|
158
|
-
clearTimeout(timer);
|
|
159
|
-
const stdout = await new Response(proc.stdout).text();
|
|
160
|
-
const stderr = await new Response(proc.stderr).text();
|
|
161
|
-
resolve({ exitCode, stdout, stderr });
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function probeLocalGatewayHealth(
|
|
167
|
-
options: ProbeLocalGatewayHealthOptions = {},
|
|
168
|
-
): Promise<LocalGatewayHealthResult> {
|
|
169
|
-
const target = getGatewayInternalBaseUrl();
|
|
170
|
-
const localDeployment = resolveLocalDeployment();
|
|
171
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
172
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const response = await fetchImpl(`${target}/healthz`, {
|
|
176
|
-
method: "GET",
|
|
177
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
178
|
-
});
|
|
179
|
-
if (!response.ok) {
|
|
180
|
-
return {
|
|
181
|
-
target,
|
|
182
|
-
healthy: false,
|
|
183
|
-
localDeployment,
|
|
184
|
-
error: `Gateway health check returned HTTP ${response.status}`,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
target,
|
|
190
|
-
healthy: true,
|
|
191
|
-
localDeployment,
|
|
192
|
-
};
|
|
193
|
-
} catch (err) {
|
|
194
|
-
return {
|
|
195
|
-
target,
|
|
196
|
-
healthy: false,
|
|
197
|
-
localDeployment,
|
|
198
|
-
error: formatError(err),
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export async function ensureLocalGatewayReady(
|
|
204
|
-
options: EnsureLocalGatewayReadyOptions = {},
|
|
205
|
-
): Promise<EnsureLocalGatewayReadyResult> {
|
|
206
|
-
const initialProbe = await probeLocalGatewayHealth(options);
|
|
207
|
-
if (initialProbe.healthy) {
|
|
208
|
-
return {
|
|
209
|
-
...initialProbe,
|
|
210
|
-
recovered: false,
|
|
211
|
-
recoveryAttempted: false,
|
|
212
|
-
recoverySkipped: false,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (!initialProbe.localDeployment) {
|
|
217
|
-
return {
|
|
218
|
-
...initialProbe,
|
|
219
|
-
recovered: false,
|
|
220
|
-
recoveryAttempted: false,
|
|
221
|
-
recoverySkipped: true,
|
|
222
|
-
error:
|
|
223
|
-
initialProbe.error ??
|
|
224
|
-
"Skipped gateway recovery because this assistant is not locally managed",
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const runWakeCommand =
|
|
229
|
-
options.runWakeCommand ??
|
|
230
|
-
(() =>
|
|
231
|
-
runDefaultWakeCommand(options.wakeTimeoutMs ?? DEFAULT_WAKE_TIMEOUT_MS));
|
|
232
|
-
const sleepImpl = options.sleepImpl ?? sleep;
|
|
233
|
-
const pollTimeoutMs =
|
|
234
|
-
options.pollTimeoutMs ?? DEFAULT_RECOVERY_POLL_TIMEOUT_MS;
|
|
235
|
-
const pollIntervalMs =
|
|
236
|
-
options.pollIntervalMs ?? DEFAULT_RECOVERY_POLL_INTERVAL_MS;
|
|
237
|
-
|
|
238
|
-
let wakeError: string | undefined;
|
|
239
|
-
try {
|
|
240
|
-
const wakeResult = await runWakeCommand();
|
|
241
|
-
if (wakeResult.exitCode !== 0) {
|
|
242
|
-
const detail = wakeResult.stderr.trim() || wakeResult.stdout.trim();
|
|
243
|
-
wakeError = detail
|
|
244
|
-
? `vellum wake exited with code ${wakeResult.exitCode}: ${detail}`
|
|
245
|
-
: `vellum wake exited with code ${wakeResult.exitCode}`;
|
|
246
|
-
}
|
|
247
|
-
} catch (err) {
|
|
248
|
-
wakeError = `Failed to run vellum wake: ${formatError(err)}`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const deadline = Date.now() + pollTimeoutMs;
|
|
252
|
-
let probe = await probeLocalGatewayHealth(options);
|
|
253
|
-
while (!probe.healthy && Date.now() < deadline) {
|
|
254
|
-
await sleepImpl(pollIntervalMs);
|
|
255
|
-
probe = await probeLocalGatewayHealth(options);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (probe.healthy) {
|
|
259
|
-
return {
|
|
260
|
-
...probe,
|
|
261
|
-
recovered: true,
|
|
262
|
-
recoveryAttempted: true,
|
|
263
|
-
recoverySkipped: false,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const combinedError = [wakeError, probe.error].filter(Boolean).join("; ");
|
|
268
|
-
return {
|
|
269
|
-
...probe,
|
|
270
|
-
recovered: false,
|
|
271
|
-
recoveryAttempted: true,
|
|
272
|
-
recoverySkipped: false,
|
|
273
|
-
error: combinedError || undefined,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { getConfig } from "../config/loader.js";
|
|
2
|
-
import { getLogger } from "../util/logger.js";
|
|
3
|
-
import { compileCustomPatterns, scanText } from "./secret-scanner.js";
|
|
4
|
-
|
|
5
|
-
const log = getLogger("secret-ingress");
|
|
6
|
-
|
|
7
|
-
export interface IngressCheckResult {
|
|
8
|
-
/** Whether the message should be blocked from entering the model context. */
|
|
9
|
-
blocked: boolean;
|
|
10
|
-
/** Secret types detected (empty if none). */
|
|
11
|
-
detectedTypes: string[];
|
|
12
|
-
/**
|
|
13
|
-
* User-facing notice explaining why the message was blocked.
|
|
14
|
-
* Does NOT echo the secret value — only describes what was found.
|
|
15
|
-
*/
|
|
16
|
-
userNotice?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Scan inbound user text for secrets before it enters model context.
|
|
21
|
-
*
|
|
22
|
-
* When `secretDetection.blockIngress` is `true` (default), any message
|
|
23
|
-
* containing a detected secret is rejected with a safe notice. This is
|
|
24
|
-
* independent of `secretDetection.action`, which only controls how
|
|
25
|
-
* secrets in tool *output* are handled.
|
|
26
|
-
*
|
|
27
|
-
* SECURITY: This function intentionally never logs the message content.
|
|
28
|
-
*/
|
|
29
|
-
export function checkIngressForSecrets(content: string): IngressCheckResult {
|
|
30
|
-
const config = getConfig();
|
|
31
|
-
if (!config.secretDetection.enabled) {
|
|
32
|
-
return { blocked: false, detectedTypes: [] };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!config.secretDetection.blockIngress) {
|
|
36
|
-
return { blocked: false, detectedTypes: [] };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const entropyConfig = {
|
|
40
|
-
enabled: true,
|
|
41
|
-
base64Threshold: config.secretDetection.entropyThreshold,
|
|
42
|
-
};
|
|
43
|
-
const compiledCustom = config.secretDetection.customPatterns?.length
|
|
44
|
-
? compileCustomPatterns(config.secretDetection.customPatterns)
|
|
45
|
-
: undefined;
|
|
46
|
-
const matches = scanText(content, entropyConfig, compiledCustom);
|
|
47
|
-
|
|
48
|
-
if (matches.length === 0) {
|
|
49
|
-
return { blocked: false, detectedTypes: [] };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const detectedTypes = [...new Set(matches.map((m) => m.type))];
|
|
53
|
-
log.warn(
|
|
54
|
-
{ detectedTypes, matchCount: matches.length },
|
|
55
|
-
"Blocked inbound message containing secrets",
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
blocked: true,
|
|
60
|
-
detectedTypes,
|
|
61
|
-
userNotice:
|
|
62
|
-
`Your message appears to contain sensitive information (${detectedTypes.join(
|
|
63
|
-
", ",
|
|
64
|
-
)}). ` +
|
|
65
|
-
`For security, it was not sent to the AI. ` +
|
|
66
|
-
`Please use the secure credential prompt instead — the assistant will ask for secrets when it needs them.`,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code worker backend for swarm execution.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from the swarm delegate tool so backend construction
|
|
5
|
-
* is testable and swappable independently of the tool adapter.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { resolveModelIntent } from "../providers/model-intents.js";
|
|
9
|
-
import type { ModelIntent } from "../providers/types.js";
|
|
10
|
-
import { getProviderKeyAsync } from "../security/secure-keys.js";
|
|
11
|
-
import { getLogger } from "../util/logger.js";
|
|
12
|
-
import type {
|
|
13
|
-
SwarmWorkerBackend,
|
|
14
|
-
SwarmWorkerBackendInput,
|
|
15
|
-
} from "./worker-backend.js";
|
|
16
|
-
import { getProfilePolicy } from "./worker-backend.js";
|
|
17
|
-
|
|
18
|
-
const log = getLogger("swarm-backend-claude-code");
|
|
19
|
-
|
|
20
|
-
const MAX_CLAUDE_CODE_DEPTH = 1;
|
|
21
|
-
const DEPTH_ENV_VAR = "VELLUM_CLAUDE_CODE_DEPTH";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Create a Claude Code worker backend that enforces profile-based tool policies.
|
|
25
|
-
* Uses the Claude Agent SDK to run autonomous worker tasks.
|
|
26
|
-
*/
|
|
27
|
-
export function createClaudeCodeBackend(): SwarmWorkerBackend {
|
|
28
|
-
return {
|
|
29
|
-
name: "claude_code",
|
|
30
|
-
|
|
31
|
-
async isAvailable(): Promise<boolean> {
|
|
32
|
-
const apiKey = await getProviderKeyAsync("anthropic");
|
|
33
|
-
return !!apiKey;
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
async runTask(input: SwarmWorkerBackendInput) {
|
|
37
|
-
const start = Date.now();
|
|
38
|
-
const stderrLines: string[] = [];
|
|
39
|
-
try {
|
|
40
|
-
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
41
|
-
const apiKey = await getProviderKeyAsync("anthropic");
|
|
42
|
-
if (!apiKey) {
|
|
43
|
-
return {
|
|
44
|
-
success: false,
|
|
45
|
-
output: "No API key",
|
|
46
|
-
failureReason: "backend_unavailable" as const,
|
|
47
|
-
durationMs: 0,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const profilePolicy = getProfilePolicy(input.profile);
|
|
52
|
-
|
|
53
|
-
// Enforce profile restrictions — swarm workers run autonomously so
|
|
54
|
-
// there is no user to prompt; denied tools are blocked, everything
|
|
55
|
-
// else is allowed.
|
|
56
|
-
const canUseTool: import("@anthropic-ai/claude-agent-sdk").CanUseTool =
|
|
57
|
-
async (toolName) => {
|
|
58
|
-
if (profilePolicy.deny.has(toolName)) {
|
|
59
|
-
log.debug(
|
|
60
|
-
{ toolName, profile: input.profile },
|
|
61
|
-
"Swarm worker tool denied by profile",
|
|
62
|
-
);
|
|
63
|
-
return {
|
|
64
|
-
behavior: "deny" as const,
|
|
65
|
-
message: `Tool "${toolName}" is denied by profile "${input.profile}"`,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return { behavior: "allow" as const };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Enforce nesting depth limit
|
|
72
|
-
const currentDepth = parseInt(process.env[DEPTH_ENV_VAR] ?? "0", 10);
|
|
73
|
-
if (currentDepth >= MAX_CLAUDE_CODE_DEPTH) {
|
|
74
|
-
log.warn(
|
|
75
|
-
{ currentDepth, max: MAX_CLAUDE_CODE_DEPTH },
|
|
76
|
-
"Swarm worker nesting depth exceeded",
|
|
77
|
-
);
|
|
78
|
-
return {
|
|
79
|
-
success: false,
|
|
80
|
-
output: `Nesting depth exceeded (depth ${currentDepth}, max ${MAX_CLAUDE_CODE_DEPTH})`,
|
|
81
|
-
failureReason: "backend_unavailable" as const,
|
|
82
|
-
durationMs: Date.now() - start,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Strip the SDK's nesting guard but set our own depth counter.
|
|
87
|
-
const subprocessEnv: Record<string, string | undefined> = {
|
|
88
|
-
...process.env,
|
|
89
|
-
ANTHROPIC_API_KEY: apiKey,
|
|
90
|
-
[DEPTH_ENV_VAR]: String(currentDepth + 1),
|
|
91
|
-
};
|
|
92
|
-
delete subprocessEnv.CLAUDECODE;
|
|
93
|
-
delete subprocessEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
94
|
-
|
|
95
|
-
const conversation = query({
|
|
96
|
-
prompt: input.prompt,
|
|
97
|
-
options: {
|
|
98
|
-
cwd: input.workingDir,
|
|
99
|
-
model: input.modelIntent
|
|
100
|
-
? resolveModelIntent(
|
|
101
|
-
"anthropic",
|
|
102
|
-
input.modelIntent as ModelIntent,
|
|
103
|
-
)
|
|
104
|
-
: "claude-sonnet-4-6",
|
|
105
|
-
canUseTool,
|
|
106
|
-
permissionMode: "default",
|
|
107
|
-
maxTurns: 30,
|
|
108
|
-
env: subprocessEnv,
|
|
109
|
-
stderr: (data: string) => {
|
|
110
|
-
const trimmed = data.trimEnd();
|
|
111
|
-
if (trimmed) {
|
|
112
|
-
stderrLines.push(trimmed);
|
|
113
|
-
log.debug(
|
|
114
|
-
{ stderr: trimmed },
|
|
115
|
-
"Swarm worker subprocess stderr",
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
let resultText = "";
|
|
123
|
-
let hasError = false;
|
|
124
|
-
for await (const message of conversation) {
|
|
125
|
-
if (input.signal?.aborted) break;
|
|
126
|
-
if (message.type === "assistant") {
|
|
127
|
-
if (message.error) {
|
|
128
|
-
log.error(
|
|
129
|
-
{ error: message.error, conversationId: message.session_id },
|
|
130
|
-
"Swarm worker assistant message error",
|
|
131
|
-
);
|
|
132
|
-
hasError = true;
|
|
133
|
-
resultText += `\n[Claude Code error: ${message.error}]`;
|
|
134
|
-
}
|
|
135
|
-
if (message.message?.content) {
|
|
136
|
-
for (const block of message.message.content) {
|
|
137
|
-
if (block.type === "text") resultText += block.text;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} else if (message.type === "result") {
|
|
141
|
-
if (message.subtype === "success") {
|
|
142
|
-
log.info(
|
|
143
|
-
{
|
|
144
|
-
numTurns: message.num_turns,
|
|
145
|
-
durationMs: message.duration_ms,
|
|
146
|
-
costUsd: message.total_cost_usd,
|
|
147
|
-
},
|
|
148
|
-
"Swarm worker completed",
|
|
149
|
-
);
|
|
150
|
-
if (message.result && !resultText) {
|
|
151
|
-
resultText = message.result;
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
hasError = true;
|
|
155
|
-
const errors = message.errors ?? [];
|
|
156
|
-
const denials = message.permission_denials ?? [];
|
|
157
|
-
log.error(
|
|
158
|
-
{
|
|
159
|
-
subtype: message.subtype,
|
|
160
|
-
errors,
|
|
161
|
-
permissionDenials: denials.length,
|
|
162
|
-
numTurns: message.num_turns,
|
|
163
|
-
durationMs: message.duration_ms,
|
|
164
|
-
},
|
|
165
|
-
"Swarm worker session failed",
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const parts: string[] = [
|
|
169
|
-
`[${message.subtype}] (${message.num_turns} turns, ${(
|
|
170
|
-
message.duration_ms / 1000
|
|
171
|
-
).toFixed(1)}s)`,
|
|
172
|
-
];
|
|
173
|
-
if (errors.length > 0) parts.push(`Errors: ${errors.join("; ")}`);
|
|
174
|
-
if (denials.length > 0)
|
|
175
|
-
parts.push(
|
|
176
|
-
`Permission denied: ${denials
|
|
177
|
-
.map((d: { tool_name: string }) => d.tool_name)
|
|
178
|
-
.join(", ")}`,
|
|
179
|
-
);
|
|
180
|
-
resultText += `\n${parts.join("\n")}`;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Treat abort as non-retryable cancellation, not a retryable timeout
|
|
186
|
-
if (input.signal?.aborted) {
|
|
187
|
-
return {
|
|
188
|
-
success: false,
|
|
189
|
-
output: "Cancelled (aborted)",
|
|
190
|
-
failureReason: "cancelled" as const,
|
|
191
|
-
durationMs: Date.now() - start,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
success: !hasError,
|
|
197
|
-
output: resultText || "Completed",
|
|
198
|
-
durationMs: Date.now() - start,
|
|
199
|
-
};
|
|
200
|
-
} catch (err) {
|
|
201
|
-
const errMessage = err instanceof Error ? err.message : String(err);
|
|
202
|
-
const recentStderr = stderrLines.slice(-20);
|
|
203
|
-
log.error(
|
|
204
|
-
{ err, stderrTail: recentStderr },
|
|
205
|
-
"Swarm worker execution failed",
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const parts = [errMessage];
|
|
209
|
-
if (recentStderr.length > 0) {
|
|
210
|
-
parts.push(
|
|
211
|
-
`\nSubprocess stderr (last ${
|
|
212
|
-
recentStderr.length
|
|
213
|
-
} lines):\n${recentStderr.join("\n")}`,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
success: false,
|
|
218
|
-
output: parts.join(""),
|
|
219
|
-
failureReason: "backend_unavailable" as const,
|
|
220
|
-
durationMs: Date.now() - start,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
}
|
package/src/swarm/checkpoint.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdirSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
renameSync,
|
|
6
|
-
unlinkSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
10
|
-
|
|
11
|
-
import { getLogger } from "../util/logger.js";
|
|
12
|
-
import { getRootDir } from "../util/platform.js";
|
|
13
|
-
import type { SwarmPlan, SwarmTaskResult } from "./types.js";
|
|
14
|
-
|
|
15
|
-
const log = getLogger("swarm-checkpoint");
|
|
16
|
-
|
|
17
|
-
/** Only allow safe token characters in runId (alphanumeric, hyphens, underscores, dots). */
|
|
18
|
-
const SAFE_RUN_ID = /^[a-zA-Z0-9._-]+$/;
|
|
19
|
-
|
|
20
|
-
function assertSafeRunId(runId: string): void {
|
|
21
|
-
if (!SAFE_RUN_ID.test(runId)) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
`Invalid runId: must match ${SAFE_RUN_ID} (got "${runId}")`,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SwarmCheckpoint {
|
|
29
|
-
runId: string;
|
|
30
|
-
objective: string;
|
|
31
|
-
/** Serialized plan for integrity verification on resume. */
|
|
32
|
-
planTaskIds: string[];
|
|
33
|
-
/** Stringified task dependency map for structural integrity on resume. */
|
|
34
|
-
planHash: string;
|
|
35
|
-
results: SwarmTaskResult[];
|
|
36
|
-
/** Set of task IDs whose dependents were blocked due to failure. */
|
|
37
|
-
blockedTaskIds: string[];
|
|
38
|
-
updatedAt: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getCheckpointDir(): string {
|
|
42
|
-
return join(getRootDir(), "swarm-checkpoints");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function getCheckpointPath(runId: string): string {
|
|
46
|
-
assertSafeRunId(runId);
|
|
47
|
-
return join(getCheckpointDir(), `${runId}.json`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Deterministic fingerprint of a plan's structure: objective, task IDs,
|
|
52
|
-
* roles, and dependency edges. Two plans with the same hash are structurally
|
|
53
|
-
* identical and safe to resume from.
|
|
54
|
-
*/
|
|
55
|
-
function computePlanHash(plan: SwarmPlan): string {
|
|
56
|
-
const parts = plan.tasks.map(
|
|
57
|
-
(t) => `${t.id}:${t.role}:${[...t.dependencies].sort().join(",")}`,
|
|
58
|
-
);
|
|
59
|
-
return `${plan.objective}|${parts.sort().join("|")}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Persist the current swarm progress to disk. */
|
|
63
|
-
export function writeCheckpoint(
|
|
64
|
-
runId: string,
|
|
65
|
-
plan: SwarmPlan,
|
|
66
|
-
results: Map<string, SwarmTaskResult>,
|
|
67
|
-
blockedTaskIds: Set<string>,
|
|
68
|
-
): void {
|
|
69
|
-
try {
|
|
70
|
-
const path = getCheckpointPath(runId);
|
|
71
|
-
const checkpoint: SwarmCheckpoint = {
|
|
72
|
-
runId,
|
|
73
|
-
objective: plan.objective,
|
|
74
|
-
planTaskIds: plan.tasks.map((t) => t.id),
|
|
75
|
-
planHash: computePlanHash(plan),
|
|
76
|
-
results: Array.from(results.values()),
|
|
77
|
-
blockedTaskIds: Array.from(blockedTaskIds),
|
|
78
|
-
updatedAt: new Date().toISOString(),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
-
// Atomic-ish write: write to temp then rename to avoid partial reads
|
|
83
|
-
const tmpPath = path + ".tmp";
|
|
84
|
-
writeFileSync(tmpPath, JSON.stringify(checkpoint, null, 2) + "\n");
|
|
85
|
-
renameSync(tmpPath, path);
|
|
86
|
-
} catch (err) {
|
|
87
|
-
// Checkpoint failures should not crash the orchestrator
|
|
88
|
-
log.warn(
|
|
89
|
-
{ runId, error: (err as Error).message },
|
|
90
|
-
"Failed to write checkpoint",
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Load a checkpoint from disk, or null if none exists. */
|
|
96
|
-
export function loadCheckpoint(runId: string): SwarmCheckpoint | null {
|
|
97
|
-
const path = getCheckpointPath(runId);
|
|
98
|
-
if (!existsSync(path)) return null;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const data = readFileSync(path, "utf-8");
|
|
102
|
-
return JSON.parse(data) as SwarmCheckpoint;
|
|
103
|
-
} catch (err) {
|
|
104
|
-
log.warn(
|
|
105
|
-
{ runId, error: (err as Error).message },
|
|
106
|
-
"Failed to read checkpoint, starting fresh",
|
|
107
|
-
);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Remove a checkpoint file after successful completion. */
|
|
113
|
-
export function removeCheckpoint(runId: string): void {
|
|
114
|
-
const path = getCheckpointPath(runId);
|
|
115
|
-
try {
|
|
116
|
-
if (existsSync(path)) unlinkSync(path);
|
|
117
|
-
} catch {
|
|
118
|
-
// Best-effort cleanup
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Validate that a checkpoint matches the current plan.
|
|
124
|
-
* Compares objective, task IDs, roles, and dependency structure via planHash.
|
|
125
|
-
* Falls back to subset check for checkpoints written before planHash existed.
|
|
126
|
-
*/
|
|
127
|
-
export function isCheckpointCompatible(
|
|
128
|
-
checkpoint: SwarmCheckpoint,
|
|
129
|
-
plan: SwarmPlan,
|
|
130
|
-
): boolean {
|
|
131
|
-
if (checkpoint.planHash) {
|
|
132
|
-
return checkpoint.planHash === computePlanHash(plan);
|
|
133
|
-
}
|
|
134
|
-
// Legacy checkpoint without planHash — fall back to subset check
|
|
135
|
-
const planTaskIds = new Set(plan.tasks.map((t) => t.id));
|
|
136
|
-
return checkpoint.planTaskIds.every((id) => planTaskIds.has(id));
|
|
137
|
-
}
|