@vellumai/assistant 0.4.52 → 0.4.53
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/docs/architecture/keychain-broker.md +6 -20
- package/docs/architecture/memory.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +3 -1
- 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 +6 -1
- package/src/__tests__/browser-fill-credential.test.ts +3 -0
- 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 -0
- package/src/__tests__/call-routes-http.test.ts +1 -2
- package/src/__tests__/canonical-guardian-store.test.ts +33 -2
- package/src/__tests__/channel-readiness-service.test.ts +1 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
- package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
- package/src/__tests__/config-loader-backfill.test.ts +1 -2
- package/src/__tests__/config-schema.test.ts +6 -37
- package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
- package/src/__tests__/credential-broker-server-use.test.ts +16 -16
- package/src/__tests__/credential-security-invariants.test.ts +14 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
- package/src/__tests__/host-shell-tool.test.ts +0 -1
- package/src/__tests__/http-user-message-parity.test.ts +19 -0
- package/src/__tests__/list-messages-attachments.test.ts +0 -1
- package/src/__tests__/log-export-workspace.test.ts +233 -0
- package/src/__tests__/managed-proxy-context.test.ts +1 -1
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/media-generate-image.test.ts +7 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -1
- package/src/__tests__/memory-regressions.test.ts +0 -1
- 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-schedule-dedup.test.ts +237 -0
- package/src/__tests__/oauth-cli.test.ts +1 -10
- package/src/__tests__/oauth-store.test.ts +3 -5
- 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__/pricing.test.ts +0 -11
- package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
- package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
- package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
- package/src/__tests__/provider-registry-ollama.test.ts +8 -2
- package/src/__tests__/recording-handler.test.ts +0 -1
- 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__/secret-routes-managed-proxy.test.ts +0 -1
- package/src/__tests__/secret-scanner-executor.test.ts +0 -1
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/session-abort-tool-results.test.ts +3 -1
- 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 -1
- package/src/__tests__/session-error.test.ts +5 -4
- package/src/__tests__/session-history-web-search.test.ts +34 -9
- package/src/__tests__/session-pre-run-repair.test.ts +3 -1
- package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
- package/src/__tests__/session-queue.test.ts +3 -1
- package/src/__tests__/session-runtime-assembly.test.ts +118 -0
- package/src/__tests__/session-slash-known.test.ts +31 -13
- package/src/__tests__/session-slash-queue.test.ts +3 -1
- package/src/__tests__/session-slash-unknown.test.ts +3 -1
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
- package/src/__tests__/session-workspace-injection.test.ts +3 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
- package/src/__tests__/shell-tool-proxy-mode.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-share-routes.test.ts +1 -1
- package/src/__tests__/swarm-recursion.test.ts +5 -1
- package/src/__tests__/swarm-session-integration.test.ts +25 -14
- package/src/__tests__/swarm-tool.test.ts +5 -2
- package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
- 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 +0 -1
- package/src/__tests__/trust-store.test.ts +5 -1
- package/src/__tests__/twilio-routes.test.ts +2 -2
- 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/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 +5 -23
- package/src/cli/commands/oauth/connections.ts +4 -2
- package/src/cli/commands/oauth/providers.ts +1 -13
- package/src/cli/program.ts +2 -0
- package/src/cli/reference.ts +1 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -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/phone-calls/references/CONFIG.md +1 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
- package/src/config/feature-flag-registry.json +8 -0
- 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/contacts/contact-store.ts +21 -25
- 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/config-watcher.ts +24 -6
- 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 +21 -15
- package/src/daemon/handlers/config-telegram.ts +18 -6
- package/src/daemon/handlers/dictation.ts +0 -429
- package/src/daemon/handlers/skills.ts +1 -200
- package/src/daemon/lifecycle.ts +8 -5
- 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/server.ts +23 -2
- 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 +32 -20
- package/src/daemon/session.ts +1 -0
- package/src/events/domain-events.ts +1 -0
- package/src/media/app-icon-generator.ts +2 -1
- package/src/media/avatar-router.ts +3 -2
- package/src/memory/canonical-guardian-store.ts +25 -3
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.ts +25 -16
- 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/retriever.test.ts +19 -12
- package/src/memory/schema/contacts.ts +2 -2
- package/src/memory/schema/oauth.ts +0 -1
- 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 +10 -0
- package/src/oauth/seed-providers.ts +13 -5
- package/src/permissions/checker.ts +20 -1
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
- package/src/prompts/system-prompt.ts +2 -11
- package/src/prompts/templates/BOOTSTRAP.md +1 -3
- package/src/providers/anthropic/client.ts +16 -8
- package/src/providers/managed-proxy/constants.ts +1 -1
- package/src/providers/registry.ts +21 -15
- package/src/providers/types.ts +1 -1
- 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-types.ts +1 -0
- package/src/runtime/middleware/error-handler.ts +1 -2
- package/src/runtime/routes/app-management-routes.ts +1 -0
- package/src/runtime/routes/btw-routes.ts +20 -1
- package/src/runtime/routes/conversation-routes.ts +32 -13
- 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/session-query-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +53 -0
- package/src/runtime/routes/workspace-routes.ts +3 -0
- package/src/runtime/verification-templates.ts +1 -1
- package/src/security/oauth2.ts +4 -4
- package/src/security/secure-keys.ts +4 -4
- package/src/signals/bash.ts +157 -0
- 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/tools/claude-code/claude-code.ts +4 -4
- package/src/tools/credentials/broker.ts +7 -5
- package/src/tools/credentials/vault.ts +3 -2
- package/src/tools/network/__tests__/web-search.test.ts +18 -86
- package/src/tools/network/web-search.ts +9 -15
- package/src/util/platform.ts +7 -1
- package/src/util/pricing.ts +0 -1
- package/src/workspace/provider-commit-message-generator.ts +10 -6
package/ARCHITECTURE.md
CHANGED
|
@@ -641,7 +641,7 @@ The assistant feature-flag resolver (`src/config/assistant-feature-flags.ts`) is
|
|
|
641
641
|
| **3. `skill_load` tool** | `executeSkillLoad()` in `tools/skills/load.ts` | If the model attempts to load a flagged-off skill by name, the tool returns an error: `"skill is currently unavailable (disabled by feature flag)"`. |
|
|
642
642
|
| **4. Runtime tool projection** | `projectSkillTools()` in `daemon/session-skill-tools.ts` | Even if a skill was previously active in a session (has `<loaded_skill>` markers in history), the per-turn projection drops it when the flag is OFF. Already-registered tools are unregistered. |
|
|
643
643
|
| **5. Included child skills** | `executeSkillLoad()` in `tools/skills/load.ts` | When a parent skill includes children via the `includes` directive, each child is independently checked against its feature flag. Flagged-off children are silently excluded from the loaded skill content. |
|
|
644
|
-
| **6. Skill install gate** | `
|
|
644
|
+
| **6. Skill install gate** | `installSkill()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the function checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
|
|
645
645
|
|
|
646
646
|
All six enforcement points derive the flag key via `skillFlagKey(skill)` — which returns `undefined` for ungated skills, short-circuiting the check — and then call `isAssistantFeatureFlagEnabled(flagKey, config)` for consistency.
|
|
647
647
|
|
|
@@ -657,7 +657,7 @@ All six enforcement points derive the flag key via `skillFlagKey(skill)` — whi
|
|
|
657
657
|
| `src/tools/skills/load.ts` | `executeSkillLoad()` — enforcement points 3 and 5 |
|
|
658
658
|
| `src/daemon/session-skill-tools.ts` | `projectSkillTools()` — enforcement point 4 |
|
|
659
659
|
| `src/config/schema.ts` | `assistantFeatureFlagValues` field definition in `AssistantConfig` (Zod schema) |
|
|
660
|
-
| `src/daemon/handlers/skills.ts` | `
|
|
660
|
+
| `src/daemon/handlers/skills.ts` | `listSkills()` — uses `resolveSkillStates()` for client responses; `installSkill()` — enforcement point 6 |
|
|
661
661
|
| `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
|
|
662
662
|
| `src/config/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
|
|
663
663
|
|
|
@@ -179,30 +179,16 @@ The gateway reads credentials via async `readCredential()` which tries the broke
|
|
|
179
179
|
|
|
180
180
|
### Known sync exceptions
|
|
181
181
|
|
|
182
|
-
The migration from sync to async secure-key functions is
|
|
182
|
+
The migration from sync to async secure-key functions is complete for all call sites except provider initialization paths that require synchronous access. The following call sites still use the deprecated sync variants.
|
|
183
183
|
|
|
184
|
-
####
|
|
184
|
+
#### Provider initialization (must remain sync)
|
|
185
185
|
|
|
186
186
|
These call sites run in synchronous initialization contexts where async I/O is not feasible:
|
|
187
187
|
|
|
188
|
-
| File | Sync functions used
|
|
189
|
-
| -------------------------------------------------- |
|
|
190
|
-
| `assistant/src/
|
|
191
|
-
| `assistant/src/providers/
|
|
192
|
-
|
|
193
|
-
#### Tracked for future conversion
|
|
194
|
-
|
|
195
|
-
These call sites use sync `getSecureKey` in contexts that could potentially support async, but conversion has been deferred to avoid scope creep during the initial broker rollout:
|
|
196
|
-
|
|
197
|
-
| File | Sync functions used | Reason deferred |
|
|
198
|
-
| ------------------------------------------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
-
| `assistant/src/tools/credentials/broker.ts` | `getSecureKey` | Credential broker read paths are called synchronously from multiple consumers; converting requires async propagation. |
|
|
200
|
-
| `assistant/src/security/token-manager.ts` | `getSecureKey` | Token resolution is called in sync context during connection token lookup; async would require refactoring callers. |
|
|
201
|
-
| `assistant/src/runtime/routes/integrations/slack/share.ts` | `getSecureKey` | Slack share route reads OAuth token synchronously; converting requires making the route handler async-aware. |
|
|
202
|
-
| `assistant/src/runtime/channel-invite-transports/telegram.ts` | `getSecureKey` | Telegram transport reads bot token synchronously; converting requires async transport initialization. |
|
|
203
|
-
| `assistant/src/tools/network/web-search.ts` | `getSecureKey` | Web search tool reads API keys synchronously; converting requires async tool execution path. |
|
|
204
|
-
| `assistant/src/cli/commands/doctor.ts` | `getSecureKey` | Doctor diagnostic command reads provider keys synchronously; converting requires async CLI command execution. |
|
|
205
|
-
| `assistant/src/cli/commands/oauth/connections.ts` | `getSecureKey` | OAuth connections CLI reads client secrets synchronously during display; converting requires async command handler. |
|
|
188
|
+
| File | Sync functions used | Reason |
|
|
189
|
+
| -------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
+
| `assistant/src/providers/managed-proxy/context.ts` | `getSecureKey` | Provider context initialization is synchronous. The managed proxy context must resolve credentials before the first request can be processed, and the initialization path does not support awaiting. |
|
|
191
|
+
| `assistant/src/providers/registry.ts` | `getSecureKey` | Provider registry initialization is synchronous. API keys must be resolved at provider construction time, and the call chain from config watcher through to provider init is fully synchronous. |
|
|
206
192
|
|
|
207
193
|
Any new sync usage requires explicit justification and should be documented here.
|
|
208
194
|
|
|
@@ -293,8 +293,8 @@ The embedding backend is selected based on `memory.embeddings.provider` config:
|
|
|
293
293
|
|
|
294
294
|
- `auto` (default): Tries local → OpenAI → Gemini → Ollama, using the first available.
|
|
295
295
|
- `local`: ONNX-based local model (bge-small-en-v1.5). Lazy-loaded to avoid crashing in compiled binaries where onnxruntime-node is unavailable.
|
|
296
|
-
- `openai`: OpenAI text-embedding-3-small. Requires
|
|
297
|
-
- `gemini`: Gemini gemini-embedding-001. Requires
|
|
296
|
+
- `openai`: OpenAI text-embedding-3-small. Requires an OpenAI API key in secure storage.
|
|
297
|
+
- `gemini`: Gemini gemini-embedding-001. Requires a Gemini API key in secure storage. Only backend supporting multimodal embeddings (images, audio, video).
|
|
298
298
|
- `ollama`: Ollama nomic-embed-text. Requires Ollama to be configured.
|
|
299
299
|
|
|
300
300
|
An in-memory LRU vector cache (32 MB cap, keyed by `sha256(provider + model + content)`) avoids redundant embedding calls for identical content. Sparse embeddings are generated in-process (no external calls).
|
|
@@ -570,7 +570,7 @@ graph TB
|
|
|
570
570
|
|
|
571
571
|
**Commit message LLM fallback chain**: The generator runs a sequence of pre-flight checks before calling the LLM. Each check that fails produces a machine-readable `llmFallbackReason` in the structured log output and immediately returns a deterministic message. The checks, in order:
|
|
572
572
|
1. `disabled` — `commitMessageLLM.enabled` is `false` or `useConfiguredProvider` is `false`
|
|
573
|
-
2. `missing_provider_api_key` — the configured provider's API key is not
|
|
573
|
+
2. `missing_provider_api_key` — the configured provider's API key is not found in secure storage (skipped for keyless providers like Ollama that run without an API key)
|
|
574
574
|
3. `breaker_open` — the generator's internal circuit breaker is open after consecutive LLM failures (exponential backoff)
|
|
575
575
|
4. `insufficient_budget` — the remaining turn budget (`deadlineMs - Date.now()`) is below `minRemainingTurnBudgetMs`
|
|
576
576
|
5. `missing_fast_model` — no fast model could be resolved for the configured provider (see below); the provider is **not** called
|
package/package.json
CHANGED
|
@@ -79,7 +79,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
79
79
|
},
|
|
80
80
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
81
81
|
timeouts: { permissionTimeoutSec: 300 },
|
|
82
|
-
apiKeys: {},
|
|
83
82
|
skills: { entries: {}, allowBundled: true },
|
|
84
83
|
permissions: { mode: "workspace" },
|
|
85
84
|
}),
|
|
@@ -203,6 +202,9 @@ mock.module("../memory/llm-usage-store.js", () => ({
|
|
|
203
202
|
mock.module("../agent/loop.js", () => ({
|
|
204
203
|
AgentLoop: class {
|
|
205
204
|
constructor() {}
|
|
205
|
+
getToolTokenBudget() {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
206
208
|
async run(
|
|
207
209
|
_messages: Message[],
|
|
208
210
|
_onEvent: (event: AgentEvent) => void,
|
|
@@ -27,11 +27,16 @@ const geminiGenerateContentFn = mock(async () => geminiGenerateContentResult);
|
|
|
27
27
|
|
|
28
28
|
mock.module("../config/loader.js", () => ({
|
|
29
29
|
getConfig: () => ({
|
|
30
|
-
apiKeys: { gemini: mockGeminiKey },
|
|
31
30
|
imageGenModel: "gemini-2.5-flash-image",
|
|
32
31
|
}),
|
|
33
32
|
}));
|
|
34
33
|
|
|
34
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
35
|
+
getSecureKeyAsync: async (name: string) =>
|
|
36
|
+
name === "gemini" ? mockGeminiKey : null,
|
|
37
|
+
getSecureKey: () => null,
|
|
38
|
+
}));
|
|
39
|
+
|
|
35
40
|
mock.module("../util/platform.js", () => ({
|
|
36
41
|
getWorkspaceDir: () => mockWorkspaceDir,
|
|
37
42
|
}));
|
|
@@ -59,8 +59,11 @@ let mockGetCredentialMetadata: ReturnType<typeof mock>;
|
|
|
59
59
|
|
|
60
60
|
mock.module("../security/secure-keys.js", () => ({
|
|
61
61
|
getSecureKey: (...args: unknown[]) => mockGetSecureKey(...args),
|
|
62
|
+
getSecureKeyAsync: async (...args: unknown[]) => mockGetSecureKey(...args),
|
|
62
63
|
setSecureKey: () => true,
|
|
64
|
+
setSecureKeyAsync: async () => true,
|
|
63
65
|
deleteSecureKey: () => "deleted",
|
|
66
|
+
deleteSecureKeyAsync: async () => "deleted",
|
|
64
67
|
listSecureKeys: () => [],
|
|
65
68
|
getBackendType: () => "encrypted",
|
|
66
69
|
_resetBackend: () => {},
|
|
@@ -54,6 +54,17 @@ mock.module("../daemon/session-tool-setup.js", () => ({
|
|
|
54
54
|
buildToolDefinitions: () => MOCK_TOOLS,
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
|
+
const mockCheckIngressForSecrets = mock((content: string) => ({
|
|
58
|
+
blocked: false,
|
|
59
|
+
userNotice: "",
|
|
60
|
+
detectedTypes: [] as string[],
|
|
61
|
+
normalizedContent: content,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
mock.module("../security/secret-ingress.js", () => ({
|
|
65
|
+
checkIngressForSecrets: mockCheckIngressForSecrets,
|
|
66
|
+
}));
|
|
67
|
+
|
|
57
68
|
// ---------------------------------------------------------------------------
|
|
58
69
|
// Imports (after mocks)
|
|
59
70
|
// ---------------------------------------------------------------------------
|
|
@@ -218,6 +229,34 @@ describe("POST /v1/btw", () => {
|
|
|
218
229
|
expect(body.error.message).toContain("content");
|
|
219
230
|
});
|
|
220
231
|
|
|
232
|
+
test("returns 422 when content includes a blocked secret", async () => {
|
|
233
|
+
mockCheckIngressForSecrets.mockReturnValueOnce({
|
|
234
|
+
blocked: true,
|
|
235
|
+
userNotice: "Secret detected",
|
|
236
|
+
detectedTypes: ["api_key"],
|
|
237
|
+
normalizedContent: "sk-test-123",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const provider = makeMockProvider();
|
|
241
|
+
const session = makeMockSession(provider);
|
|
242
|
+
const res = await callHandler(
|
|
243
|
+
{ conversationKey: "key", content: "sk-test-123" },
|
|
244
|
+
{ sendMessageDeps: makeSendMessageDeps(session) },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(res.status).toBe(422);
|
|
248
|
+
const body = (await res.json()) as {
|
|
249
|
+
accepted: boolean;
|
|
250
|
+
error: string;
|
|
251
|
+
message: string;
|
|
252
|
+
detectedTypes: string[];
|
|
253
|
+
};
|
|
254
|
+
expect(body.accepted).toBe(false);
|
|
255
|
+
expect(body.error).toBe("secret_blocked");
|
|
256
|
+
expect(body.detectedTypes).toEqual(["api_key"]);
|
|
257
|
+
expect(provider.sendMessage).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
|
|
221
260
|
// -- Service unavailability (503) --
|
|
222
261
|
|
|
223
262
|
test("returns 503 when sendMessageDeps is unavailable", async () => {
|
|
@@ -53,7 +53,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
53
53
|
|
|
54
54
|
model: "test",
|
|
55
55
|
provider: "test",
|
|
56
|
-
apiKeys: {},
|
|
57
56
|
memory: { enabled: false },
|
|
58
57
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
59
58
|
secretDetection: { enabled: false },
|
|
@@ -62,7 +61,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
62
61
|
loadConfig: () => ({
|
|
63
62
|
model: "test",
|
|
64
63
|
provider: "test",
|
|
65
|
-
apiKeys: {},
|
|
66
64
|
memory: { enabled: false },
|
|
67
65
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
68
66
|
secretDetection: { enabled: false },
|
|
@@ -106,6 +104,7 @@ mock.module("../calls/twilio-config.js", () => ({
|
|
|
106
104
|
// Mock secure keys
|
|
107
105
|
mock.module("../security/secure-keys.js", () => ({
|
|
108
106
|
getSecureKey: () => null,
|
|
107
|
+
getSecureKeyAsync: async () => null,
|
|
109
108
|
}));
|
|
110
109
|
|
|
111
110
|
mock.module("../calls/voice-ingress-preflight.js", () => ({
|
|
@@ -761,7 +761,7 @@ describe("canonical-guardian-store", () => {
|
|
|
761
761
|
|
|
762
762
|
// ── expireAllPendingCanonicalRequests ───────────────────────────────
|
|
763
763
|
|
|
764
|
-
test("expireAllPendingCanonicalRequests transitions
|
|
764
|
+
test("expireAllPendingCanonicalRequests transitions interaction-bound pending to expired", () => {
|
|
765
765
|
const req1 = createCanonicalGuardianRequest({
|
|
766
766
|
kind: "tool_approval",
|
|
767
767
|
sourceType: "desktop",
|
|
@@ -770,7 +770,7 @@ describe("canonical-guardian-store", () => {
|
|
|
770
770
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
771
771
|
});
|
|
772
772
|
const req2 = createCanonicalGuardianRequest({
|
|
773
|
-
kind: "
|
|
773
|
+
kind: "pending_question",
|
|
774
774
|
sourceType: "channel",
|
|
775
775
|
conversationId: "conv-bulk-2",
|
|
776
776
|
guardianPrincipalId: TEST_PRINCIPAL,
|
|
@@ -784,6 +784,37 @@ describe("canonical-guardian-store", () => {
|
|
|
784
784
|
expect(getCanonicalGuardianRequest(req2.id)!.status).toBe("expired");
|
|
785
785
|
});
|
|
786
786
|
|
|
787
|
+
test("expireAllPendingCanonicalRequests does not expire persistent kinds (access_request, tool_grant_request)", () => {
|
|
788
|
+
const accessReq = createCanonicalGuardianRequest({
|
|
789
|
+
kind: "access_request",
|
|
790
|
+
sourceType: "channel",
|
|
791
|
+
conversationId: "conv-bulk-persist-1",
|
|
792
|
+
guardianPrincipalId: TEST_PRINCIPAL,
|
|
793
|
+
});
|
|
794
|
+
const grantReq = createCanonicalGuardianRequest({
|
|
795
|
+
kind: "tool_grant_request",
|
|
796
|
+
sourceType: "channel",
|
|
797
|
+
conversationId: "conv-bulk-persist-2",
|
|
798
|
+
guardianPrincipalId: TEST_PRINCIPAL,
|
|
799
|
+
});
|
|
800
|
+
// Also create an interaction-bound request to verify selective expiry
|
|
801
|
+
const toolApproval = createCanonicalGuardianRequest({
|
|
802
|
+
kind: "tool_approval",
|
|
803
|
+
sourceType: "desktop",
|
|
804
|
+
conversationId: "conv-bulk-persist-3",
|
|
805
|
+
guardianPrincipalId: TEST_PRINCIPAL,
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const count = expireAllPendingCanonicalRequests();
|
|
809
|
+
expect(count).toBe(1); // Only tool_approval expired
|
|
810
|
+
|
|
811
|
+
expect(getCanonicalGuardianRequest(accessReq.id)!.status).toBe("pending");
|
|
812
|
+
expect(getCanonicalGuardianRequest(grantReq.id)!.status).toBe("pending");
|
|
813
|
+
expect(getCanonicalGuardianRequest(toolApproval.id)!.status).toBe(
|
|
814
|
+
"expired",
|
|
815
|
+
);
|
|
816
|
+
});
|
|
817
|
+
|
|
787
818
|
test("expireAllPendingCanonicalRequests does not affect already-resolved requests", () => {
|
|
788
819
|
const approved = createCanonicalGuardianRequest({
|
|
789
820
|
kind: "tool_approval",
|
|
@@ -49,6 +49,7 @@ mock.module("../email/service.js", () => ({
|
|
|
49
49
|
|
|
50
50
|
mock.module("../security/secure-keys.js", () => ({
|
|
51
51
|
getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
|
|
52
|
+
getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? null,
|
|
52
53
|
}));
|
|
53
54
|
|
|
54
55
|
mock.module("../runtime/channel-invite-transports/whatsapp.js", () => ({
|
|
@@ -29,11 +29,15 @@ mock.module("../util/logger.js", () => ({
|
|
|
29
29
|
mock.module("../config/loader.js", () => ({
|
|
30
30
|
getConfig: () => ({
|
|
31
31
|
ui: {},
|
|
32
|
-
|
|
33
|
-
apiKeys: { anthropic: "test-key" },
|
|
34
32
|
}),
|
|
35
33
|
}));
|
|
36
34
|
|
|
35
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
36
|
+
getSecureKeyAsync: async (name: string) =>
|
|
37
|
+
name === "anthropic" ? "fake-anthropic-key" : null,
|
|
38
|
+
getSecureKey: () => null,
|
|
39
|
+
}));
|
|
40
|
+
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
38
42
|
// Imports (after mocks)
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
@@ -33,11 +33,16 @@ mock.module("../util/logger.js", () => ({
|
|
|
33
33
|
mock.module("../config/loader.js", () => ({
|
|
34
34
|
getConfig: () => ({
|
|
35
35
|
ui: {},
|
|
36
|
-
|
|
37
|
-
apiKeys: { anthropic: "test-key" },
|
|
38
36
|
}),
|
|
39
37
|
}));
|
|
40
38
|
|
|
39
|
+
// Mock secure-keys — provide a fake Anthropic API key
|
|
40
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
41
|
+
getSecureKeyAsync: async (name: string) =>
|
|
42
|
+
name === "anthropic" ? "fake-anthropic-key" : null,
|
|
43
|
+
getSecureKey: () => null,
|
|
44
|
+
}));
|
|
45
|
+
|
|
41
46
|
import { claudeCodeTool } from "../tools/claude-code/claude-code.js";
|
|
42
47
|
import type { ToolContext } from "../tools/types.js";
|
|
43
48
|
|
|
@@ -278,13 +278,12 @@ describe("config loader backfill", () => {
|
|
|
278
278
|
expect(contentAfter).toBe(contentBefore);
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
-
test("does not write
|
|
281
|
+
test("does not write dataDir during backfill", () => {
|
|
282
282
|
writeConfig({ provider: "anthropic" });
|
|
283
283
|
|
|
284
284
|
loadConfig();
|
|
285
285
|
|
|
286
286
|
const raw = readConfig();
|
|
287
|
-
expect(raw.apiKeys).toBeUndefined();
|
|
288
287
|
expect(raw.dataDir).toBeUndefined();
|
|
289
288
|
});
|
|
290
289
|
|
|
@@ -67,7 +67,10 @@ import {
|
|
|
67
67
|
resolveVoiceQualityProfile,
|
|
68
68
|
} from "../calls/voice-quality.js";
|
|
69
69
|
import { invalidateConfigCache, loadConfig } from "../config/loader.js";
|
|
70
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
AssistantConfigSchema,
|
|
72
|
+
DEFAULT_ELEVENLABS_VOICE_ID,
|
|
73
|
+
} from "../config/schema.js";
|
|
71
74
|
import { _setStorePath } from "../security/encrypted-store.js";
|
|
72
75
|
import { _setBackend } from "../security/secure-keys.js";
|
|
73
76
|
|
|
@@ -89,7 +92,6 @@ describe("AssistantConfigSchema", () => {
|
|
|
89
92
|
expect(result.provider).toBe("anthropic");
|
|
90
93
|
expect(result.model).toBe("claude-opus-4-6");
|
|
91
94
|
expect(result.maxTokens).toBe(16000);
|
|
92
|
-
expect(result.apiKeys).toEqual({});
|
|
93
95
|
expect(result.thinking).toEqual({
|
|
94
96
|
enabled: false,
|
|
95
97
|
streamThinking: false,
|
|
@@ -137,7 +139,6 @@ describe("AssistantConfigSchema", () => {
|
|
|
137
139
|
provider: "openai",
|
|
138
140
|
model: "gpt-4",
|
|
139
141
|
maxTokens: 4096,
|
|
140
|
-
apiKeys: { openai: "sk-test" },
|
|
141
142
|
thinking: { enabled: true },
|
|
142
143
|
timeouts: {
|
|
143
144
|
shellDefaultTimeoutSec: 30,
|
|
@@ -358,13 +359,6 @@ describe("AssistantConfigSchema", () => {
|
|
|
358
359
|
expect(result.success).toBe(false);
|
|
359
360
|
});
|
|
360
361
|
|
|
361
|
-
test("rejects non-string apiKeys values", () => {
|
|
362
|
-
const result = AssistantConfigSchema.safeParse({
|
|
363
|
-
apiKeys: { anthropic: 123 },
|
|
364
|
-
});
|
|
365
|
-
expect(result.success).toBe(false);
|
|
366
|
-
});
|
|
367
|
-
|
|
368
362
|
test("accepts partial nested objects with defaults", () => {
|
|
369
363
|
const result = AssistantConfigSchema.parse({
|
|
370
364
|
timeouts: { shellDefaultTimeoutSec: 30 },
|
|
@@ -906,10 +900,10 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
906
900
|
expect(profile.voice).toBe("test-voice-id");
|
|
907
901
|
});
|
|
908
902
|
|
|
909
|
-
test("defaults to
|
|
903
|
+
test("defaults to Amelia voice ID when elevenlabs.voiceId is not set", () => {
|
|
910
904
|
const config = AssistantConfigSchema.parse({});
|
|
911
905
|
const profile = resolveVoiceQualityProfile(config);
|
|
912
|
-
expect(profile.voice).toBe(
|
|
906
|
+
expect(profile.voice).toBe(DEFAULT_ELEVENLABS_VOICE_ID);
|
|
913
907
|
});
|
|
914
908
|
|
|
915
909
|
test("applies voice tuning params from elevenlabs config", () => {
|
|
@@ -1165,31 +1159,6 @@ describe("loadConfig with schema validation", () => {
|
|
|
1165
1159
|
expect(config.permissions.mode).toBe("workspace");
|
|
1166
1160
|
});
|
|
1167
1161
|
|
|
1168
|
-
test("does not mutate default apiKeys when fallback config is overridden by env keys", () => {
|
|
1169
|
-
const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
1170
|
-
try {
|
|
1171
|
-
const testKey = ["test", "in", "memory", "default", "leak"].join("-");
|
|
1172
|
-
process.env.ANTHROPIC_API_KEY = testKey;
|
|
1173
|
-
writeConfig("this is not a config object");
|
|
1174
|
-
|
|
1175
|
-
const configWithEnv = loadConfig();
|
|
1176
|
-
expect(configWithEnv.apiKeys.anthropic).toBe(testKey);
|
|
1177
|
-
|
|
1178
|
-
invalidateConfigCache();
|
|
1179
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
1180
|
-
writeConfig("still not a config object");
|
|
1181
|
-
|
|
1182
|
-
const configWithoutEnv = loadConfig();
|
|
1183
|
-
expect(configWithoutEnv.apiKeys.anthropic).toBeUndefined();
|
|
1184
|
-
} finally {
|
|
1185
|
-
if (originalAnthropicApiKey !== undefined) {
|
|
1186
|
-
process.env.ANTHROPIC_API_KEY = originalAnthropicApiKey;
|
|
1187
|
-
} else {
|
|
1188
|
-
delete process.env.ANTHROPIC_API_KEY;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
1162
|
// ── Calls config (loader integration) ──────────────────────────────
|
|
1194
1163
|
|
|
1195
1164
|
test("loads calls config from file", () => {
|
|
@@ -30,7 +30,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
30
30
|
ui: {},
|
|
31
31
|
model: "claude-opus-4-6",
|
|
32
32
|
provider: "anthropic",
|
|
33
|
-
apiKeys: { anthropic: "test-key" },
|
|
34
33
|
memory: { enabled: false },
|
|
35
34
|
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
|
|
36
35
|
secretDetection: { enabled: false },
|
|
@@ -371,7 +371,7 @@ describe("CredentialBroker.serverUse", () => {
|
|
|
371
371
|
expect(r2.result).toBe("triggered");
|
|
372
372
|
});
|
|
373
373
|
|
|
374
|
-
test("different services with same field name are independent (serverUseById)", () => {
|
|
374
|
+
test("different services with same field name are independent (serverUseById)", async () => {
|
|
375
375
|
const meta1 = upsertCredentialMetadata("github", "api_token", {
|
|
376
376
|
allowedTools: ["github_api"],
|
|
377
377
|
});
|
|
@@ -382,7 +382,7 @@ describe("CredentialBroker.serverUse", () => {
|
|
|
382
382
|
setSecureKey(credentialKey("gitlab", "api_token"), "gl_tok");
|
|
383
383
|
|
|
384
384
|
// github credential should not serve gitlab tool
|
|
385
|
-
const r1 = broker.serverUseById({
|
|
385
|
+
const r1 = await broker.serverUseById({
|
|
386
386
|
credentialId: meta1.credentialId,
|
|
387
387
|
requestingTool: "gitlab_api",
|
|
388
388
|
});
|
|
@@ -447,7 +447,7 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
447
447
|
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
448
448
|
});
|
|
449
449
|
|
|
450
|
-
test("returns metadata and injection templates for valid credential", () => {
|
|
450
|
+
test("returns metadata and injection templates for valid credential", async () => {
|
|
451
451
|
const meta = upsertCredentialMetadata("fal", "api_key", {
|
|
452
452
|
allowedTools: ["media_proxy"],
|
|
453
453
|
injectionTemplates: [
|
|
@@ -461,7 +461,7 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
461
461
|
});
|
|
462
462
|
setSecureKey(credentialKey("fal", "api_key"), "fal-secret-key");
|
|
463
463
|
|
|
464
|
-
const result = broker.serverUseById({
|
|
464
|
+
const result = await broker.serverUseById({
|
|
465
465
|
credentialId: meta.credentialId,
|
|
466
466
|
requestingTool: "media_proxy",
|
|
467
467
|
});
|
|
@@ -480,13 +480,13 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
480
480
|
expect(serialized).not.toContain("fal-secret-key");
|
|
481
481
|
});
|
|
482
482
|
|
|
483
|
-
test("denies when requesting tool is not in allowed list", () => {
|
|
483
|
+
test("denies when requesting tool is not in allowed list", async () => {
|
|
484
484
|
const meta = upsertCredentialMetadata("fal", "api_key", {
|
|
485
485
|
allowedTools: ["media_proxy"],
|
|
486
486
|
});
|
|
487
487
|
setSecureKey(credentialKey("fal", "api_key"), "fal-secret-key");
|
|
488
488
|
|
|
489
|
-
const result = broker.serverUseById({
|
|
489
|
+
const result = await broker.serverUseById({
|
|
490
490
|
credentialId: meta.credentialId,
|
|
491
491
|
requestingTool: "unauthorized_tool",
|
|
492
492
|
});
|
|
@@ -498,8 +498,8 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
498
498
|
expect(result.reason).toContain("media_proxy");
|
|
499
499
|
});
|
|
500
500
|
|
|
501
|
-
test("returns not found for unknown credential ID", () => {
|
|
502
|
-
const result = broker.serverUseById({
|
|
501
|
+
test("returns not found for unknown credential ID", async () => {
|
|
502
|
+
const result = await broker.serverUseById({
|
|
503
503
|
credentialId: "nonexistent-id",
|
|
504
504
|
requestingTool: "media_proxy",
|
|
505
505
|
});
|
|
@@ -510,14 +510,14 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
510
510
|
expect(result.reason).toContain("nonexistent-id");
|
|
511
511
|
});
|
|
512
512
|
|
|
513
|
-
test("denies when credential has domain restrictions", () => {
|
|
513
|
+
test("denies when credential has domain restrictions", async () => {
|
|
514
514
|
const meta = upsertCredentialMetadata("github", "oauth_token", {
|
|
515
515
|
allowedTools: ["media_proxy"],
|
|
516
516
|
allowedDomains: ["github.com"],
|
|
517
517
|
});
|
|
518
518
|
setSecureKey(credentialKey("github", "oauth_token"), "gho_test");
|
|
519
519
|
|
|
520
|
-
const result = broker.serverUseById({
|
|
520
|
+
const result = await broker.serverUseById({
|
|
521
521
|
credentialId: meta.credentialId,
|
|
522
522
|
requestingTool: "media_proxy",
|
|
523
523
|
});
|
|
@@ -528,13 +528,13 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
528
528
|
expect(result.reason).toContain("cannot be used server-side");
|
|
529
529
|
});
|
|
530
530
|
|
|
531
|
-
test("returns empty injection templates when credential has none", () => {
|
|
531
|
+
test("returns empty injection templates when credential has none", async () => {
|
|
532
532
|
const meta = upsertCredentialMetadata("vercel", "api_token", {
|
|
533
533
|
allowedTools: ["media_proxy"],
|
|
534
534
|
});
|
|
535
535
|
setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
|
|
536
536
|
|
|
537
|
-
const result = broker.serverUseById({
|
|
537
|
+
const result = await broker.serverUseById({
|
|
538
538
|
credentialId: meta.credentialId,
|
|
539
539
|
requestingTool: "media_proxy",
|
|
540
540
|
});
|
|
@@ -544,13 +544,13 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
544
544
|
expect(result.injectionTemplates).toEqual([]);
|
|
545
545
|
});
|
|
546
546
|
|
|
547
|
-
test("denies with empty allowedTools and suggests updating credential", () => {
|
|
547
|
+
test("denies with empty allowedTools and suggests updating credential", async () => {
|
|
548
548
|
const meta = upsertCredentialMetadata("stripe", "secret_key", {
|
|
549
549
|
allowedTools: [],
|
|
550
550
|
});
|
|
551
551
|
setSecureKey(credentialKey("stripe", "secret_key"), "sk_test_xyz");
|
|
552
552
|
|
|
553
|
-
const result = broker.serverUseById({
|
|
553
|
+
const result = await broker.serverUseById({
|
|
554
554
|
credentialId: meta.credentialId,
|
|
555
555
|
requestingTool: "media_proxy",
|
|
556
556
|
});
|
|
@@ -561,7 +561,7 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
561
561
|
expect(result.reason).toContain("credential_store");
|
|
562
562
|
});
|
|
563
563
|
|
|
564
|
-
test("denies when metadata exists but no stored secret value", () => {
|
|
564
|
+
test("denies when metadata exists but no stored secret value", async () => {
|
|
565
565
|
const meta = upsertCredentialMetadata("fal", "api_key", {
|
|
566
566
|
allowedTools: ["media_proxy"],
|
|
567
567
|
injectionTemplates: [
|
|
@@ -575,7 +575,7 @@ describe("CredentialBroker.serverUseById", () => {
|
|
|
575
575
|
});
|
|
576
576
|
// No setSecureKey — metadata exists but value doesn't
|
|
577
577
|
|
|
578
|
-
const result = broker.serverUseById({
|
|
578
|
+
const result = await broker.serverUseById({
|
|
579
579
|
credentialId: meta.credentialId,
|
|
580
580
|
requestingTool: "media_proxy",
|
|
581
581
|
});
|