@vellumai/assistant 0.4.52 → 0.4.54
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/ARCHITECTURE.md +2 -2
- package/bun.lock +62 -349
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/keychain-broker.md +91 -40
- package/docs/architecture/memory.md +3 -3
- package/docs/architecture/security.md +2 -2
- package/knip.json +7 -29
- package/package.json +2 -9
- package/src/__tests__/agent-loop.test.ts +1 -1
- package/src/__tests__/app-git-history.test.ts +0 -2
- package/src/__tests__/app-git-service.test.ts +1 -6
- package/src/__tests__/approval-cascade.test.ts +3 -2
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/asset-materialize-tool.test.ts +0 -1
- package/src/__tests__/asset-search-tool.test.ts +0 -1
- package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
- package/src/__tests__/attachments-store.test.ts +0 -1
- package/src/__tests__/avatar-e2e.test.ts +5 -1
- package/src/__tests__/browser-fill-credential.test.ts +4 -6
- package/src/__tests__/btw-routes.test.ts +39 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +1 -1
- package/src/__tests__/call-routes-http.test.ts +1 -3
- package/src/__tests__/canonical-guardian-store.test.ts +33 -2
- package/src/__tests__/channel-guardian.test.ts +4 -4
- package/src/__tests__/channel-readiness-routes.test.ts +0 -1
- package/src/__tests__/channel-readiness-service.test.ts +1 -1
- package/src/__tests__/checker.test.ts +13 -11
- package/src/__tests__/claude-code-skill-regression.test.ts +5 -2
- package/src/__tests__/claude-code-tool-profiles.test.ts +7 -3
- package/src/__tests__/config-loader-backfill.test.ts +1 -5
- package/src/__tests__/config-schema.test.ts +9 -46
- package/src/__tests__/config-watcher.test.ts +11 -3
- package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
- package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
- package/src/__tests__/credential-broker-server-use.test.ts +76 -40
- package/src/__tests__/credential-security-e2e.test.ts +1 -6
- package/src/__tests__/credential-security-invariants.test.ts +27 -8
- package/src/__tests__/credential-vault-unit.test.ts +32 -16
- package/src/__tests__/credential-vault.test.ts +40 -28
- package/src/__tests__/credentials-cli.test.ts +1 -21
- package/src/__tests__/email-invite-adapter.test.ts +0 -1
- package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
- package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
- package/src/__tests__/gateway-only-enforcement.test.ts +1 -23
- package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
- package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
- package/src/__tests__/guardian-action-store.test.ts +0 -57
- package/src/__tests__/guardian-outbound-http.test.ts +1 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
- package/src/__tests__/hooks-blocking.test.ts +1 -1
- package/src/__tests__/hooks-config.test.ts +5 -29
- package/src/__tests__/hooks-discovery.test.ts +1 -1
- package/src/__tests__/hooks-integration.test.ts +1 -1
- package/src/__tests__/hooks-manager.test.ts +1 -1
- package/src/__tests__/hooks-runner.test.ts +1 -23
- package/src/__tests__/hooks-settings.test.ts +1 -1
- package/src/__tests__/hooks-templates.test.ts +1 -1
- package/src/__tests__/host-shell-tool.test.ts +0 -1
- package/src/__tests__/http-user-message-parity.test.ts +19 -0
- package/src/__tests__/integration-status.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +0 -3
- package/src/__tests__/list-messages-attachments.test.ts +0 -1
- package/src/__tests__/llm-usage-store.test.ts +50 -0
- package/src/__tests__/log-export-workspace.test.ts +233 -0
- package/src/__tests__/managed-proxy-context.test.ts +41 -41
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/media-generate-image.test.ts +9 -4
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -7
- package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
- package/src/__tests__/memory-regressions.test.ts +27 -28
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/notification-decision-fallback.test.ts +1 -1
- package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
- package/src/__tests__/oauth-cli.test.ts +2 -14
- package/src/__tests__/oauth-store.test.ts +3 -7
- package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
- package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
- package/src/__tests__/onboarding-template-contract.test.ts +1 -2
- package/src/__tests__/openai-provider.test.ts +7 -7
- package/src/__tests__/platform.test.ts +14 -4
- package/src/__tests__/pricing.test.ts +0 -234
- package/src/__tests__/provider-commit-message-generator.test.ts +19 -15
- package/src/__tests__/provider-fail-open-selection.test.ts +67 -62
- package/src/__tests__/provider-managed-proxy-integration.test.ts +88 -85
- package/src/__tests__/provider-registry-ollama.test.ts +10 -4
- package/src/__tests__/public-ingress-urls.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +0 -1
- package/src/__tests__/registry.test.ts +3 -103
- package/src/__tests__/relay-server.test.ts +0 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
- package/src/__tests__/runtime-events-sse.test.ts +0 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
- package/src/__tests__/secret-onetime-send.test.ts +1 -6
- package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -14
- package/src/__tests__/secret-scanner-executor.test.ts +0 -1
- package/src/__tests__/secure-keys.test.ts +241 -229
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/session-abort-tool-results.test.ts +3 -2
- package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
- package/src/__tests__/session-agent-loop.test.ts +2 -2
- package/src/__tests__/session-confirmation-signals.test.ts +3 -2
- package/src/__tests__/session-error.test.ts +5 -4
- package/src/__tests__/session-history-web-search.test.ts +34 -9
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
- package/src/__tests__/session-pre-run-repair.test.ts +3 -2
- package/src/__tests__/session-provider-retry-repair.test.ts +31 -27
- package/src/__tests__/session-queue.test.ts +5 -5
- package/src/__tests__/session-runtime-assembly.test.ts +118 -0
- package/src/__tests__/session-slash-known.test.ts +31 -14
- package/src/__tests__/session-slash-queue.test.ts +3 -2
- package/src/__tests__/session-slash-unknown.test.ts +3 -2
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
- package/src/__tests__/session-workspace-injection.test.ts +3 -2
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -2
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/skillssh-registry.test.ts +21 -0
- package/src/__tests__/slack-channel-config.test.ts +1 -7
- package/src/__tests__/slack-share-routes.test.ts +1 -1
- package/src/__tests__/swarm-recursion.test.ts +4 -1
- package/src/__tests__/swarm-session-integration.test.ts +24 -14
- package/src/__tests__/swarm-tool.test.ts +4 -2
- package/src/__tests__/task-compiler.test.ts +1 -1
- package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
- package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
- package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +1 -2
- package/src/__tests__/trust-store.test.ts +8 -83
- package/src/__tests__/twilio-config.test.ts +0 -1
- package/src/__tests__/twilio-provider.test.ts +0 -5
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-quality.test.ts +2 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/agent/loop.ts +17 -1
- package/src/bundler/app-bundler.ts +40 -24
- package/src/calls/call-controller.ts +16 -0
- package/src/calls/guardian-question-copy.ts +1 -1
- package/src/calls/relay-server.ts +29 -13
- package/src/calls/voice-control-protocol.ts +1 -0
- package/src/calls/voice-quality.ts +1 -1
- package/src/calls/voice-session-bridge.ts +9 -3
- package/src/channels/types.ts +16 -0
- package/src/cli/commands/bash.ts +173 -0
- package/src/cli/commands/doctor.ts +15 -57
- package/src/cli/commands/memory.ts +3 -5
- package/src/cli/commands/oauth/connections.ts +4 -2
- package/src/cli/commands/oauth/providers.ts +1 -13
- package/src/cli/commands/sessions.ts +1 -1
- package/src/cli/commands/usage.ts +359 -0
- package/src/cli/http-client.ts +22 -12
- package/src/cli/program.ts +4 -0
- package/src/cli/reference.ts +2 -0
- package/src/cli.ts +251 -181
- package/src/config/assistant-feature-flags.ts +0 -7
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +0 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +4 -3
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
- package/src/config/bundled-skills/messaging/SKILL.md +0 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
- package/src/config/bundled-skills/sequences/SKILL.md +0 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
- package/src/config/env.ts +13 -0
- package/src/config/feature-flag-registry.json +15 -39
- package/src/config/loader.ts +7 -135
- package/src/config/schema.ts +0 -6
- package/src/config/schemas/channels.ts +1 -0
- package/src/config/schemas/elevenlabs.ts +2 -2
- package/src/config/schemas/security.ts +1 -2
- package/src/config/skills.ts +1 -1
- package/src/contacts/contact-store.ts +21 -75
- package/src/contacts/contacts-write.ts +6 -6
- package/src/contacts/types.ts +2 -0
- package/src/context/token-estimator.ts +35 -2
- package/src/context/window-manager.ts +16 -2
- package/src/daemon/approved-devices-store.ts +0 -44
- package/src/daemon/classifier.ts +1 -1
- package/src/daemon/config-watcher.ts +35 -11
- package/src/daemon/context-overflow-reducer.ts +13 -2
- package/src/daemon/handlers/config-ingress.ts +25 -8
- package/src/daemon/handlers/config-model.ts +22 -16
- package/src/daemon/handlers/config-telegram.ts +18 -6
- package/src/daemon/handlers/dictation.ts +0 -429
- package/src/daemon/handlers/sessions.ts +4 -116
- package/src/daemon/handlers/skills.ts +2 -201
- package/src/daemon/lifecycle.ts +21 -20
- package/src/daemon/message-types/contacts.ts +2 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/sessions.ts +2 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +42 -5
- package/src/daemon/session-agent-loop-handlers.ts +1 -1
- package/src/daemon/session-agent-loop.ts +27 -79
- package/src/daemon/session-error.ts +5 -4
- package/src/daemon/session-process.ts +17 -10
- package/src/daemon/session-runtime-assembly.ts +50 -0
- package/src/daemon/session-slash.ts +34 -22
- package/src/daemon/session.ts +1 -0
- package/src/daemon/shutdown-handlers.ts +15 -0
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/guardrails.ts +1 -1
- package/src/email/service.ts +0 -5
- package/src/events/domain-events.ts +1 -0
- package/src/hooks/templates.ts +1 -1
- package/src/media/app-icon-generator.ts +4 -3
- package/src/media/avatar-router.ts +5 -4
- package/src/media/gemini-image-service.ts +5 -5
- package/src/memory/admin.ts +2 -2
- package/src/memory/app-git-service.ts +0 -7
- package/src/memory/canonical-guardian-store.ts +25 -3
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-title-service.ts +2 -2
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.ts +46 -33
- package/src/memory/external-conversation-store.ts +0 -30
- package/src/memory/guardian-action-store.ts +0 -31
- package/src/memory/guardian-approvals.ts +1 -56
- package/src/memory/indexer.ts +4 -3
- package/src/memory/items-extractor.ts +1 -1
- package/src/memory/job-handlers/backfill.ts +5 -2
- package/src/memory/job-handlers/index-maintenance.ts +2 -2
- package/src/memory/job-handlers/media-processing.ts +2 -2
- package/src/memory/job-handlers/summarization.ts +1 -1
- package/src/memory/job-utils.ts +1 -2
- package/src/memory/jobs-worker.ts +2 -2
- package/src/memory/llm-usage-store.ts +57 -11
- package/src/memory/media-store.ts +4 -535
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
- package/src/memory/migrations/110-channel-guardian.ts +0 -1
- package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
- package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
- package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/published-pages-store.ts +0 -83
- package/src/memory/qdrant-circuit-breaker.ts +0 -8
- package/src/memory/retriever.test.ts +19 -12
- package/src/memory/retriever.ts +1 -1
- package/src/memory/schema/contacts.ts +2 -2
- package/src/memory/schema/oauth.ts +0 -1
- package/src/memory/search/semantic.ts +1 -8
- package/src/memory/shared-app-links-store.ts +0 -15
- package/src/messaging/registry.ts +0 -5
- package/src/messaging/style-analyzer.ts +1 -1
- package/src/notifications/copy-composer.ts +5 -13
- package/src/notifications/decision-engine.ts +2 -2
- package/src/notifications/deliveries-store.ts +0 -39
- package/src/notifications/guardian-question-mode.ts +6 -10
- package/src/notifications/preference-extractor.ts +1 -1
- package/src/oauth/byo-connection.test.ts +29 -20
- package/src/oauth/connect-orchestrator.ts +5 -3
- package/src/oauth/connect-types.ts +9 -2
- package/src/oauth/manual-token-connection.ts +9 -7
- package/src/oauth/oauth-store.ts +2 -8
- package/src/oauth/provider-behaviors.ts +11 -1
- package/src/oauth/seed-providers.ts +13 -5
- package/src/permissions/checker.ts +21 -2
- package/src/permissions/shell-identity.ts +0 -5
- package/src/permissions/trust-store.ts +0 -37
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
- package/src/prompts/system-prompt.ts +5 -14
- package/src/prompts/templates/BOOTSTRAP.md +1 -3
- package/src/providers/anthropic/client.ts +16 -8
- package/src/providers/managed-proxy/constants.ts +9 -11
- package/src/providers/managed-proxy/context.ts +14 -9
- package/src/providers/provider-send-message.ts +4 -52
- package/src/providers/registry.ts +29 -57
- package/src/providers/types.ts +1 -1
- package/src/runtime/actor-token-store.ts +0 -23
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-invite-transports/telegram.ts +12 -6
- package/src/runtime/channel-retry-sweep.ts +6 -0
- package/src/runtime/http-router.ts +5 -1
- package/src/runtime/http-server.ts +101 -4
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/invite-instruction-generator.ts +25 -51
- package/src/runtime/invite-service.ts +0 -20
- package/src/runtime/middleware/error-handler.ts +1 -2
- package/src/runtime/routes/app-management-routes.ts +1 -0
- package/src/runtime/routes/attachment-routes.ts +1 -1
- package/src/runtime/routes/brain-graph-routes.ts +1 -1
- package/src/runtime/routes/btw-routes.ts +20 -1
- package/src/runtime/routes/call-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +64 -24
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +2 -2
- package/src/runtime/routes/documents-routes.ts +3 -3
- package/src/runtime/routes/global-search-routes.ts +1 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
- package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
- package/src/runtime/routes/inbound-message-handler.ts +10 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
- package/src/runtime/routes/integrations/slack/share.ts +5 -5
- package/src/runtime/routes/log-export-routes.ts +122 -10
- package/src/runtime/routes/secret-routes.ts +4 -4
- package/src/runtime/routes/session-query-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +53 -0
- package/src/runtime/routes/trust-rules-routes.ts +1 -1
- package/src/runtime/routes/workspace-routes.ts +3 -0
- package/src/runtime/verification-templates.ts +1 -1
- package/src/security/credential-backend.ts +148 -0
- package/src/security/oauth2.ts +5 -5
- package/src/security/secret-allowlist.ts +1 -1
- package/src/security/secure-keys.ts +98 -160
- package/src/security/token-manager.ts +0 -7
- package/src/sequence/guardrails.ts +0 -4
- package/src/sequence/store.ts +1 -20
- package/src/sequence/types.ts +1 -36
- package/src/signals/bash.ts +157 -0
- package/src/signals/cancel.ts +69 -0
- package/src/signals/conversation-undo.ts +127 -0
- package/src/signals/trust-rule.ts +174 -0
- package/src/skills/clawhub.ts +5 -5
- package/src/skills/managed-store.ts +4 -4
- package/src/skills/skillssh-registry.ts +6 -1
- package/src/swarm/backend-claude-code.ts +6 -6
- package/src/swarm/worker-backend.ts +1 -1
- package/src/swarm/worker-runner.ts +1 -1
- package/src/telegram/bot-username.ts +11 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
- package/src/telemetry/usage-telemetry-reporter.ts +181 -0
- package/src/tools/claude-code/claude-code.ts +6 -6
- package/src/tools/credentials/broker.ts +7 -5
- package/src/tools/credentials/vault.ts +11 -6
- package/src/tools/memory/handlers.test.ts +24 -26
- package/src/tools/memory/handlers.ts +1 -13
- package/src/tools/network/__tests__/web-search.test.ts +18 -86
- package/src/tools/network/web-search.ts +9 -15
- package/src/tools/registry.ts +5 -100
- package/src/tools/terminal/parser.ts +34 -4
- package/src/tools/tool-manifest.ts +0 -10
- package/src/usage/actors.ts +0 -12
- package/src/util/canonicalize-identity.ts +0 -9
- package/src/util/errors.ts +0 -3
- package/src/util/platform.ts +31 -8
- package/src/util/pricing.ts +0 -39
- package/src/watcher/constants.ts +0 -7
- package/src/watcher/providers/linear.ts +1 -1
- package/src/work-items/work-item-store.ts +4 -4
- package/src/workspace/commit-message-provider.ts +1 -1
- package/src/workspace/git-service.ts +44 -1
- package/src/workspace/provider-commit-message-generator.ts +11 -7
- package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
- package/src/browser-extension-relay/client.ts +0 -155
- package/src/contacts/index.ts +0 -18
- package/src/daemon/tls-certs.ts +0 -270
- package/src/errors.ts +0 -41
- package/src/events/index.ts +0 -18
- package/src/followups/index.ts +0 -10
- package/src/playbooks/index.ts +0 -10
- package/src/runtime/auth/index.ts +0 -44
- package/src/tasks/candidate-store.ts +0 -95
- package/src/tools/browser/api-map.ts +0 -313
- package/src/tools/browser/auto-navigate.ts +0 -469
- package/src/tools/browser/headless-browser.ts +0 -590
- package/src/tools/browser/recording-store.ts +0 -75
- package/src/tools/computer-use/registry.ts +0 -21
- package/src/tools/tasks/index.ts +0 -27
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for UsageTelemetryReporter.
|
|
3
|
+
*
|
|
4
|
+
* Covers both auth modes (authenticated / anonymous), watermark advancement,
|
|
5
|
+
* error handling, batch recursion, installation ID persistence, and payload shape.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Module mocks (must precede production imports)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const mockGetMemoryCheckpoint = mock<(key: string) => string | null>(
|
|
14
|
+
() => null,
|
|
15
|
+
);
|
|
16
|
+
const mockSetMemoryCheckpoint = mock<(key: string, value: string) => void>(
|
|
17
|
+
() => {},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
mock.module("../memory/checkpoints.js", () => ({
|
|
21
|
+
getMemoryCheckpoint: mockGetMemoryCheckpoint,
|
|
22
|
+
setMemoryCheckpoint: mockSetMemoryCheckpoint,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const mockQueryUnreportedUsageEvents = mock(
|
|
26
|
+
() =>
|
|
27
|
+
[] as ReturnType<
|
|
28
|
+
typeof import("../memory/llm-usage-store.js").queryUnreportedUsageEvents
|
|
29
|
+
>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
mock.module("../memory/llm-usage-store.js", () => ({
|
|
33
|
+
queryUnreportedUsageEvents: mockQueryUnreportedUsageEvents,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const mockResolveManagedProxyContext = mock(async () => ({
|
|
37
|
+
enabled: false,
|
|
38
|
+
platformBaseUrl: "",
|
|
39
|
+
assistantApiKey: "",
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
mock.module("../providers/managed-proxy/context.js", () => ({
|
|
43
|
+
resolveManagedProxyContext: mockResolveManagedProxyContext,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const mockGetTelemetryPlatformUrl = mock(() => "https://platform.vellum.ai");
|
|
47
|
+
const mockGetTelemetryAppToken = mock(() => "");
|
|
48
|
+
|
|
49
|
+
mock.module("../config/env.js", () => ({
|
|
50
|
+
getTelemetryPlatformUrl: mockGetTelemetryPlatformUrl,
|
|
51
|
+
getTelemetryAppToken: mockGetTelemetryAppToken,
|
|
52
|
+
// Re-export anything else the module might import transitively
|
|
53
|
+
str: () => undefined,
|
|
54
|
+
num: () => undefined,
|
|
55
|
+
bool: () => false,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
mock.module("../util/logger.js", () => ({
|
|
59
|
+
getLogger: () =>
|
|
60
|
+
new Proxy({} as Record<string, unknown>, {
|
|
61
|
+
get: () => () => {},
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Production import (after mocks)
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
import type { UsageEvent } from "../usage/types.js";
|
|
70
|
+
import { UsageTelemetryReporter } from "./usage-telemetry-reporter.js";
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
let eventIdCounter = 0;
|
|
77
|
+
|
|
78
|
+
function makeUsageEvent(overrides: Partial<UsageEvent> = {}): UsageEvent {
|
|
79
|
+
eventIdCounter += 1;
|
|
80
|
+
return {
|
|
81
|
+
id: `evt-${eventIdCounter}`,
|
|
82
|
+
createdAt: 1700000000000 + eventIdCounter * 1000,
|
|
83
|
+
provider: "anthropic",
|
|
84
|
+
model: "claude-sonnet-4-20250514",
|
|
85
|
+
inputTokens: 100,
|
|
86
|
+
outputTokens: 50,
|
|
87
|
+
cacheCreationInputTokens: 10,
|
|
88
|
+
cacheReadInputTokens: 5,
|
|
89
|
+
actor: "main_agent",
|
|
90
|
+
conversationId: "conv-1",
|
|
91
|
+
runId: null,
|
|
92
|
+
requestId: null,
|
|
93
|
+
estimatedCostUsd: 0.001,
|
|
94
|
+
pricingStatus: "priced",
|
|
95
|
+
...overrides,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const originalFetch = globalThis.fetch;
|
|
100
|
+
let mockFetch: ReturnType<typeof mock>;
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Setup / Teardown
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
eventIdCounter = 0;
|
|
108
|
+
mockGetMemoryCheckpoint.mockReset();
|
|
109
|
+
mockSetMemoryCheckpoint.mockReset();
|
|
110
|
+
mockQueryUnreportedUsageEvents.mockReset();
|
|
111
|
+
mockResolveManagedProxyContext.mockReset();
|
|
112
|
+
mockGetTelemetryPlatformUrl.mockReset();
|
|
113
|
+
mockGetTelemetryAppToken.mockReset();
|
|
114
|
+
|
|
115
|
+
// Defaults
|
|
116
|
+
mockGetMemoryCheckpoint.mockReturnValue(null);
|
|
117
|
+
mockResolveManagedProxyContext.mockResolvedValue({
|
|
118
|
+
enabled: false,
|
|
119
|
+
platformBaseUrl: "",
|
|
120
|
+
assistantApiKey: "",
|
|
121
|
+
});
|
|
122
|
+
mockGetTelemetryPlatformUrl.mockReturnValue("https://platform.vellum.ai");
|
|
123
|
+
mockGetTelemetryAppToken.mockReturnValue("default-test-token");
|
|
124
|
+
|
|
125
|
+
mockFetch = mock(() =>
|
|
126
|
+
Promise.resolve(new Response('{"accepted":0}', { status: 200 })),
|
|
127
|
+
);
|
|
128
|
+
globalThis.fetch = mockFetch as unknown as typeof fetch;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
globalThis.fetch = originalFetch;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Tests
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
describe("UsageTelemetryReporter", () => {
|
|
140
|
+
test("authenticated flush uses Api-Key header and proxy URL", async () => {
|
|
141
|
+
mockResolveManagedProxyContext.mockResolvedValue({
|
|
142
|
+
enabled: true,
|
|
143
|
+
platformBaseUrl: "https://test.vellum.ai",
|
|
144
|
+
assistantApiKey: "test-key",
|
|
145
|
+
});
|
|
146
|
+
const events = [makeUsageEvent(), makeUsageEvent()];
|
|
147
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
148
|
+
mockFetch.mockImplementation(() =>
|
|
149
|
+
Promise.resolve(
|
|
150
|
+
new Response(`{"accepted":${events.length}}`, { status: 200 }),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const reporter = new UsageTelemetryReporter();
|
|
155
|
+
await reporter.flush();
|
|
156
|
+
|
|
157
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
158
|
+
const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
159
|
+
expect(url).toBe(
|
|
160
|
+
"https://test.vellum.ai/v1/assistants/self-hosted-local/telemetry/usage/",
|
|
161
|
+
);
|
|
162
|
+
expect((opts.headers as Record<string, string>)["Authorization"]).toBe(
|
|
163
|
+
"Api-Key test-key",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("anonymous flush uses X-Telemetry-Token and default URL", async () => {
|
|
168
|
+
mockResolveManagedProxyContext.mockResolvedValue({
|
|
169
|
+
enabled: false,
|
|
170
|
+
platformBaseUrl: "",
|
|
171
|
+
assistantApiKey: "",
|
|
172
|
+
});
|
|
173
|
+
mockGetTelemetryPlatformUrl.mockReturnValue("https://platform.test.ai");
|
|
174
|
+
mockGetTelemetryAppToken.mockReturnValue("anon-token");
|
|
175
|
+
|
|
176
|
+
const events = [makeUsageEvent()];
|
|
177
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
178
|
+
mockFetch.mockImplementation(() =>
|
|
179
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const reporter = new UsageTelemetryReporter();
|
|
183
|
+
await reporter.flush();
|
|
184
|
+
|
|
185
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
186
|
+
const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
187
|
+
expect(url).toStartWith("https://platform.test.ai");
|
|
188
|
+
expect((opts.headers as Record<string, string>)["X-Telemetry-Token"]).toBe(
|
|
189
|
+
"anon-token",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("watermark advances on successful upload", async () => {
|
|
194
|
+
const events = [
|
|
195
|
+
makeUsageEvent({ id: "evt-w1", createdAt: 1700000001000 }),
|
|
196
|
+
makeUsageEvent({ id: "evt-w2", createdAt: 1700000002000 }),
|
|
197
|
+
];
|
|
198
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
199
|
+
mockFetch.mockImplementation(() =>
|
|
200
|
+
Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const reporter = new UsageTelemetryReporter();
|
|
204
|
+
await reporter.flush();
|
|
205
|
+
|
|
206
|
+
const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
207
|
+
(c) => c[0] === "telemetry:usage:last_reported_at",
|
|
208
|
+
);
|
|
209
|
+
expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
|
|
210
|
+
// The watermark should be set to the createdAt of the last event
|
|
211
|
+
expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
|
|
212
|
+
String(1700000002000),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// The compound cursor ID should also be set to the last event's id
|
|
216
|
+
const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
217
|
+
(c) => c[0] === "telemetry:usage:last_reported_id",
|
|
218
|
+
);
|
|
219
|
+
expect(idCalls.length).toBeGreaterThanOrEqual(1);
|
|
220
|
+
expect(idCalls[idCalls.length - 1][1]).toBe("evt-w2");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("watermark stays on failed upload", async () => {
|
|
224
|
+
const events = [makeUsageEvent()];
|
|
225
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
226
|
+
mockFetch.mockImplementation(() =>
|
|
227
|
+
Promise.resolve(new Response("error", { status: 500 })),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const reporter = new UsageTelemetryReporter();
|
|
231
|
+
await reporter.flush();
|
|
232
|
+
|
|
233
|
+
const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
|
|
234
|
+
(c) => c[0] === "telemetry:usage:last_reported_at",
|
|
235
|
+
);
|
|
236
|
+
expect(watermarkCalls.length).toBe(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("installation ID generated on first flush, reused thereafter", async () => {
|
|
240
|
+
let storedInstallId: string | null = null;
|
|
241
|
+
|
|
242
|
+
mockGetMemoryCheckpoint.mockImplementation((key: string) => {
|
|
243
|
+
if (key === "telemetry:installation_id") return storedInstallId;
|
|
244
|
+
return null;
|
|
245
|
+
});
|
|
246
|
+
mockSetMemoryCheckpoint.mockImplementation((key: string, value: string) => {
|
|
247
|
+
if (key === "telemetry:installation_id") storedInstallId = value;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const events = [makeUsageEvent()];
|
|
251
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
252
|
+
mockFetch.mockImplementation(() =>
|
|
253
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const reporter = new UsageTelemetryReporter();
|
|
257
|
+
|
|
258
|
+
// First flush — should generate and store a new installation ID
|
|
259
|
+
await reporter.flush();
|
|
260
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
261
|
+
const body1 = JSON.parse(
|
|
262
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
263
|
+
);
|
|
264
|
+
expect(body1.installation_id).toBeTruthy();
|
|
265
|
+
expect(typeof body1.installation_id).toBe("string");
|
|
266
|
+
|
|
267
|
+
// Second flush
|
|
268
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([makeUsageEvent()]);
|
|
269
|
+
await reporter.flush();
|
|
270
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
271
|
+
const body2 = JSON.parse(
|
|
272
|
+
(mockFetch.mock.calls[1] as [string, RequestInit])[1].body as string,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Both flushes should use the same installation ID
|
|
276
|
+
expect(body2.installation_id).toBe(body1.installation_id);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("empty batch makes no HTTP call", async () => {
|
|
280
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([]);
|
|
281
|
+
|
|
282
|
+
const reporter = new UsageTelemetryReporter();
|
|
283
|
+
await reporter.flush();
|
|
284
|
+
|
|
285
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("batch recursion when full, capped at 10", async () => {
|
|
289
|
+
// Always return exactly 500 events (BATCH_SIZE) to trigger recursion
|
|
290
|
+
const fullBatch = Array.from({ length: 500 }, (_, i) =>
|
|
291
|
+
makeUsageEvent({ id: `evt-batch-${i}`, createdAt: 1700000000000 + i }),
|
|
292
|
+
);
|
|
293
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(fullBatch);
|
|
294
|
+
mockFetch.mockImplementation(() =>
|
|
295
|
+
Promise.resolve(new Response('{"accepted":500}', { status: 200 })),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const reporter = new UsageTelemetryReporter();
|
|
299
|
+
await reporter.flush();
|
|
300
|
+
|
|
301
|
+
// MAX_CONSECUTIVE_BATCHES = 10
|
|
302
|
+
expect(mockFetch).toHaveBeenCalledTimes(10);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("stop() performs final flush", async () => {
|
|
306
|
+
const events = [makeUsageEvent()];
|
|
307
|
+
mockQueryUnreportedUsageEvents.mockReturnValue(events);
|
|
308
|
+
mockFetch.mockImplementation(() =>
|
|
309
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const reporter = new UsageTelemetryReporter();
|
|
313
|
+
reporter.start();
|
|
314
|
+
|
|
315
|
+
// Wait a tick so start()'s immediate flush settles.
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
317
|
+
const callsBeforeStop = mockFetch.mock.calls.length;
|
|
318
|
+
|
|
319
|
+
await reporter.stop();
|
|
320
|
+
|
|
321
|
+
// stop() must trigger at least one additional flush beyond what start() did.
|
|
322
|
+
expect(mockFetch.mock.calls.length).toBeGreaterThan(callsBeforeStop);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("payload shape is correct", async () => {
|
|
326
|
+
const event = makeUsageEvent({
|
|
327
|
+
id: "evt-shape-test",
|
|
328
|
+
provider: "anthropic",
|
|
329
|
+
model: "claude-sonnet-4-20250514",
|
|
330
|
+
inputTokens: 200,
|
|
331
|
+
outputTokens: 100,
|
|
332
|
+
cacheCreationInputTokens: 20,
|
|
333
|
+
cacheReadInputTokens: 15,
|
|
334
|
+
actor: "context_compactor",
|
|
335
|
+
createdAt: 1700000099000,
|
|
336
|
+
});
|
|
337
|
+
mockQueryUnreportedUsageEvents.mockReturnValue([event]);
|
|
338
|
+
mockFetch.mockImplementation(() =>
|
|
339
|
+
Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const reporter = new UsageTelemetryReporter();
|
|
343
|
+
await reporter.flush();
|
|
344
|
+
|
|
345
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
346
|
+
const body = JSON.parse(
|
|
347
|
+
(mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Top-level: installation_id and events array
|
|
351
|
+
expect(typeof body.installation_id).toBe("string");
|
|
352
|
+
expect(Array.isArray(body.events)).toBe(true);
|
|
353
|
+
expect(body.events.length).toBe(1);
|
|
354
|
+
|
|
355
|
+
const e = body.events[0];
|
|
356
|
+
expect(e.daemon_event_id).toBe("evt-shape-test");
|
|
357
|
+
expect(e.provider).toBe("anthropic");
|
|
358
|
+
expect(e.model).toBe("claude-sonnet-4-20250514");
|
|
359
|
+
expect(e.input_tokens).toBe(200);
|
|
360
|
+
expect(e.output_tokens).toBe(100);
|
|
361
|
+
expect(e.cache_creation_input_tokens).toBe(20);
|
|
362
|
+
expect(e.cache_read_input_tokens).toBe(15);
|
|
363
|
+
expect(e.actor).toBe("context_compactor");
|
|
364
|
+
expect(e.recorded_at).toBe(1700000099000);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage telemetry reporter.
|
|
3
|
+
*
|
|
4
|
+
* Periodically flushes LLM usage events from the local SQLite
|
|
5
|
+
* `llm_usage_events` table and POSTs them to the platform telemetry endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Two auth modes:
|
|
8
|
+
* - Authenticated: Api-Key header via managed proxy context
|
|
9
|
+
* - Anonymous: X-Telemetry-Token static token from env
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { v4 as uuid } from "uuid";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
getTelemetryAppToken,
|
|
16
|
+
getTelemetryPlatformUrl,
|
|
17
|
+
} from "../config/env.js";
|
|
18
|
+
import {
|
|
19
|
+
getMemoryCheckpoint,
|
|
20
|
+
setMemoryCheckpoint,
|
|
21
|
+
} from "../memory/checkpoints.js";
|
|
22
|
+
import { queryUnreportedUsageEvents } from "../memory/llm-usage-store.js";
|
|
23
|
+
import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
|
|
24
|
+
import { getLogger } from "../util/logger.js";
|
|
25
|
+
|
|
26
|
+
const log = getLogger("usage-telemetry");
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Constants
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const CHECKPOINT_KEY_WATERMARK = "telemetry:usage:last_reported_at";
|
|
33
|
+
const CHECKPOINT_KEY_WATERMARK_ID = "telemetry:usage:last_reported_id";
|
|
34
|
+
const CHECKPOINT_KEY_INSTALL_ID = "telemetry:installation_id";
|
|
35
|
+
const REPORT_INTERVAL_MS = 5 * 60 * 1000;
|
|
36
|
+
const BATCH_SIZE = 500;
|
|
37
|
+
const MAX_CONSECUTIVE_BATCHES = 10;
|
|
38
|
+
const TELEMETRY_PATH = "/v1/assistants/self-hosted-local/telemetry/usage/";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Installation ID
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function getOrCreateInstallationId(): string {
|
|
45
|
+
const existing = getMemoryCheckpoint(CHECKPOINT_KEY_INSTALL_ID);
|
|
46
|
+
if (existing) return existing;
|
|
47
|
+
|
|
48
|
+
const id = uuid();
|
|
49
|
+
setMemoryCheckpoint(CHECKPOINT_KEY_INSTALL_ID, id);
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Reporter
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export class UsageTelemetryReporter {
|
|
58
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
59
|
+
private activeFlush: Promise<void> | null = null;
|
|
60
|
+
|
|
61
|
+
start(): void {
|
|
62
|
+
this.flush().catch((err) => {
|
|
63
|
+
log.warn({ err }, "Initial usage telemetry flush failed");
|
|
64
|
+
});
|
|
65
|
+
this.timer = setInterval(() => {
|
|
66
|
+
this.flush().catch((err) => {
|
|
67
|
+
log.warn({ err }, "Scheduled usage telemetry flush failed");
|
|
68
|
+
});
|
|
69
|
+
}, REPORT_INTERVAL_MS);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async stop(): Promise<void> {
|
|
73
|
+
if (this.timer) {
|
|
74
|
+
clearInterval(this.timer);
|
|
75
|
+
this.timer = null;
|
|
76
|
+
}
|
|
77
|
+
if (this.activeFlush) {
|
|
78
|
+
await this.activeFlush;
|
|
79
|
+
}
|
|
80
|
+
await this.flush();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async flush(): Promise<void> {
|
|
84
|
+
if (this.activeFlush) return; // overlap guard
|
|
85
|
+
this.activeFlush = this._doFlush();
|
|
86
|
+
try {
|
|
87
|
+
await this.activeFlush;
|
|
88
|
+
} finally {
|
|
89
|
+
this.activeFlush = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async _doFlush(batchCount = 0): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
if (batchCount >= MAX_CONSECUTIVE_BATCHES) return;
|
|
96
|
+
|
|
97
|
+
// Read watermark (compound cursor: createdAt + id)
|
|
98
|
+
const watermark = Number(
|
|
99
|
+
getMemoryCheckpoint(CHECKPOINT_KEY_WATERMARK) ?? "0",
|
|
100
|
+
);
|
|
101
|
+
const watermarkId =
|
|
102
|
+
getMemoryCheckpoint(CHECKPOINT_KEY_WATERMARK_ID) ?? undefined;
|
|
103
|
+
|
|
104
|
+
// Query unreported events
|
|
105
|
+
const events = queryUnreportedUsageEvents(
|
|
106
|
+
watermark,
|
|
107
|
+
watermarkId,
|
|
108
|
+
BATCH_SIZE,
|
|
109
|
+
);
|
|
110
|
+
if (events.length === 0) return;
|
|
111
|
+
|
|
112
|
+
// Resolve auth context — skip flush when neither auth mode is viable
|
|
113
|
+
const proxyCtx = await resolveManagedProxyContext();
|
|
114
|
+
if (!proxyCtx.enabled && !getTelemetryAppToken()) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let url: string;
|
|
119
|
+
let authHeaders: Record<string, string>;
|
|
120
|
+
|
|
121
|
+
if (proxyCtx.enabled) {
|
|
122
|
+
url = `${proxyCtx.platformBaseUrl}${TELEMETRY_PATH}`;
|
|
123
|
+
authHeaders = { Authorization: `Api-Key ${proxyCtx.assistantApiKey}` };
|
|
124
|
+
} else {
|
|
125
|
+
url = `${getTelemetryPlatformUrl()}${TELEMETRY_PATH}`;
|
|
126
|
+
authHeaders = { "X-Telemetry-Token": getTelemetryAppToken() };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Build payload
|
|
130
|
+
const payload = {
|
|
131
|
+
installation_id: getOrCreateInstallationId(),
|
|
132
|
+
events: events.map((e) => ({
|
|
133
|
+
daemon_event_id: e.id,
|
|
134
|
+
provider: e.provider,
|
|
135
|
+
model: e.model,
|
|
136
|
+
input_tokens: e.inputTokens,
|
|
137
|
+
output_tokens: e.outputTokens,
|
|
138
|
+
cache_creation_input_tokens: e.cacheCreationInputTokens ?? null,
|
|
139
|
+
cache_read_input_tokens: e.cacheReadInputTokens ?? null,
|
|
140
|
+
actor: e.actor,
|
|
141
|
+
recorded_at: e.createdAt,
|
|
142
|
+
})),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Send
|
|
146
|
+
const resp = await fetch(url, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
...authHeaders,
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify(payload),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!resp.ok) {
|
|
156
|
+
await resp.text(); // consume body to release connection
|
|
157
|
+
log.warn(
|
|
158
|
+
{ status: resp.status, url },
|
|
159
|
+
"Usage telemetry POST failed — will retry next cycle",
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await resp.text(); // consume body to release connection
|
|
164
|
+
|
|
165
|
+
// Advance watermark (compound cursor)
|
|
166
|
+
const lastEvent = events[events.length - 1];
|
|
167
|
+
setMemoryCheckpoint(
|
|
168
|
+
CHECKPOINT_KEY_WATERMARK,
|
|
169
|
+
String(lastEvent.createdAt),
|
|
170
|
+
);
|
|
171
|
+
setMemoryCheckpoint(CHECKPOINT_KEY_WATERMARK_ID, lastEvent.id);
|
|
172
|
+
|
|
173
|
+
// If we got a full batch, there may be more events — recurse
|
|
174
|
+
if (events.length === BATCH_SIZE) {
|
|
175
|
+
await this._doFlush(batchCount + 1);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
log.warn({ err }, "Usage telemetry flush error — non-fatal, will retry");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
getCCCommand,
|
|
3
3
|
loadCCCommandTemplate,
|
|
4
4
|
} from "../../commands/cc-command-registry.js";
|
|
5
|
-
import { getConfig } from "../../config/loader.js";
|
|
6
5
|
import { RiskLevel } from "../../permissions/types.js";
|
|
7
6
|
import type { ToolDefinition } from "../../providers/types.js";
|
|
7
|
+
import { getSecureKeyAsync } from "../../security/secure-keys.js";
|
|
8
8
|
import type { WorkerProfile } from "../../swarm/worker-backend.js";
|
|
9
9
|
import { getProfilePolicy } from "../../swarm/worker-backend.js";
|
|
10
10
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -121,7 +121,7 @@ export const claudeCodeTool: Tool = {
|
|
|
121
121
|
type: "string",
|
|
122
122
|
enum: ["general", "researcher", "coder", "reviewer"],
|
|
123
123
|
description:
|
|
124
|
-
"Worker profile that scopes tool access. Defaults to general
|
|
124
|
+
"Worker profile that scopes tool access. Defaults to general.",
|
|
125
125
|
},
|
|
126
126
|
},
|
|
127
127
|
},
|
|
@@ -203,12 +203,12 @@ export const claudeCodeTool: Tool = {
|
|
|
203
203
|
const profilePolicy = getProfilePolicy(profileName);
|
|
204
204
|
|
|
205
205
|
// Validate API key
|
|
206
|
-
const
|
|
207
|
-
|
|
206
|
+
const apiKey =
|
|
207
|
+
(await getSecureKeyAsync("anthropic")) ?? process.env.ANTHROPIC_API_KEY;
|
|
208
208
|
if (!apiKey) {
|
|
209
209
|
return {
|
|
210
210
|
content:
|
|
211
|
-
"Error: No Anthropic API key configured. Set it via
|
|
211
|
+
"Error: No Anthropic API key configured. Set it via `keys set anthropic <key>` or configure it from the Settings page under API Keys.",
|
|
212
212
|
isError: true,
|
|
213
213
|
};
|
|
214
214
|
}
|
|
@@ -261,7 +261,7 @@ export const claudeCodeTool: Tool = {
|
|
|
261
261
|
return { behavior: "allow" as const };
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
// Auto-approve safe read-only tools (
|
|
264
|
+
// Auto-approve safe read-only tools (general profile default)
|
|
265
265
|
if (AUTO_APPROVE_TOOLS.has(toolName)) {
|
|
266
266
|
return { behavior: "allow" as const };
|
|
267
267
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
|
|
3
3
|
import { credentialKey } from "../../security/credential-key.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getSecureKeyAsync } from "../../security/secure-keys.js";
|
|
5
5
|
import { getLogger } from "../../util/logger.js";
|
|
6
6
|
import type {
|
|
7
7
|
AuthorizeRequest,
|
|
@@ -217,7 +217,7 @@ export class CredentialBroker {
|
|
|
217
217
|
// Deletion is deferred until after a successful fill so the value survives
|
|
218
218
|
// transient failures (e.g. stale element, page navigation, Playwright timeout).
|
|
219
219
|
const transient = this.transientValues.get(storageKey);
|
|
220
|
-
const value = transient?.value ??
|
|
220
|
+
const value = transient?.value ?? (await getSecureKeyAsync(storageKey));
|
|
221
221
|
if (!value) {
|
|
222
222
|
return {
|
|
223
223
|
success: false,
|
|
@@ -302,7 +302,7 @@ export class CredentialBroker {
|
|
|
302
302
|
|
|
303
303
|
const storageKey = credentialKey(request.service, request.field);
|
|
304
304
|
const transient = this.transientValues.get(storageKey);
|
|
305
|
-
const value = transient?.value ??
|
|
305
|
+
const value = transient?.value ?? (await getSecureKeyAsync(storageKey));
|
|
306
306
|
if (!value) {
|
|
307
307
|
return {
|
|
308
308
|
success: false,
|
|
@@ -344,7 +344,9 @@ export class CredentialBroker {
|
|
|
344
344
|
* never included in the result — the proxy reads it separately via
|
|
345
345
|
* the secure key backend at injection time.
|
|
346
346
|
*/
|
|
347
|
-
serverUseById(
|
|
347
|
+
async serverUseById(
|
|
348
|
+
request: ServerUseByIdRequest,
|
|
349
|
+
): Promise<ServerUseByIdResult> {
|
|
348
350
|
const resolved = resolveById(request.credentialId);
|
|
349
351
|
if (!resolved) {
|
|
350
352
|
return {
|
|
@@ -383,7 +385,7 @@ export class CredentialBroker {
|
|
|
383
385
|
|
|
384
386
|
// Fail-closed: verify the secret value actually exists in secure storage.
|
|
385
387
|
// Without this, downstream proxy code would attempt unauthenticated requests.
|
|
386
|
-
const value =
|
|
388
|
+
const value = await getSecureKeyAsync(resolved.storageKey);
|
|
387
389
|
if (!value) {
|
|
388
390
|
return {
|
|
389
391
|
success: false,
|
|
@@ -21,7 +21,7 @@ import { credentialKey } from "../../security/credential-key.js";
|
|
|
21
21
|
import {
|
|
22
22
|
deleteSecureKeyAsync,
|
|
23
23
|
getSecureKeyAsync,
|
|
24
|
-
|
|
24
|
+
listSecureKeysAsync,
|
|
25
25
|
setSecureKeyAsync,
|
|
26
26
|
} from "../../security/secure-keys.js";
|
|
27
27
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -367,10 +367,12 @@ class CredentialStoreTool implements Tool {
|
|
|
367
367
|
|
|
368
368
|
const allMetadata = listCredentialMetadata();
|
|
369
369
|
// Verify secrets still exist by reading all key names once (instead of
|
|
370
|
-
// per-entry
|
|
370
|
+
// per-entry getSecureKeyAsync calls that each re-read/re-derive the store).
|
|
371
|
+
// Uses the async variant to include keys from both the primary backend
|
|
372
|
+
// (e.g. keychain) and the encrypted store (legacy keys).
|
|
371
373
|
let secureKeySet: Set<string> | undefined;
|
|
372
374
|
try {
|
|
373
|
-
secureKeySet = new Set(
|
|
375
|
+
secureKeySet = new Set(await listSecureKeysAsync());
|
|
374
376
|
} catch (err) {
|
|
375
377
|
log.error(
|
|
376
378
|
{ err },
|
|
@@ -765,7 +767,9 @@ class CredentialStoreTool implements Tool {
|
|
|
765
767
|
if (dbApp) {
|
|
766
768
|
if (!clientId) clientId = dbApp.clientId;
|
|
767
769
|
if (!clientSecret) {
|
|
768
|
-
clientSecret = await getSecureKeyAsync(
|
|
770
|
+
clientSecret = await getSecureKeyAsync(
|
|
771
|
+
dbApp.clientSecretCredentialPath,
|
|
772
|
+
);
|
|
769
773
|
}
|
|
770
774
|
}
|
|
771
775
|
}
|
|
@@ -900,8 +904,9 @@ class CredentialStoreTool implements Tool {
|
|
|
900
904
|
| "loopback"
|
|
901
905
|
| "gateway"
|
|
902
906
|
| null) ?? "gateway";
|
|
903
|
-
|
|
904
|
-
|
|
907
|
+
const loopbackPort = descBehavior?.loopbackPort;
|
|
908
|
+
if (transport === "loopback" && loopbackPort) {
|
|
909
|
+
redirectUri = `http://localhost:${loopbackPort}/oauth/callback`;
|
|
905
910
|
} else if (transport === "loopback") {
|
|
906
911
|
redirectUri =
|
|
907
912
|
"(automatic — no redirect URI needed, uses random localhost port)";
|