@vellumai/assistant 0.7.2 → 0.7.3
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 +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
21
23
|
|
|
22
24
|
import type {
|
|
23
25
|
SecureKeyBackend,
|
|
@@ -28,6 +30,7 @@ import { getIsContainerized } from "../config/env-registry.js";
|
|
|
28
30
|
import type { CesClient } from "../credential-execution/client.js";
|
|
29
31
|
import { getAnyProviderEnvVar } from "../providers/provider-env-vars.js";
|
|
30
32
|
import { getLogger } from "../util/logger.js";
|
|
33
|
+
import { getProtectedDir } from "../util/platform.js";
|
|
31
34
|
import { createCesCredentialBackend } from "./ces-credential-client.js";
|
|
32
35
|
import { CesRpcCredentialBackend } from "./ces-rpc-credential-backend.js";
|
|
33
36
|
import type {
|
|
@@ -584,6 +587,58 @@ export function getActiveBackendName(): string {
|
|
|
584
587
|
return _resolvedBackend?.name ?? "none";
|
|
585
588
|
}
|
|
586
589
|
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Backend introspection
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
export type BackendInfo =
|
|
595
|
+
| {
|
|
596
|
+
backend: "encrypted-store";
|
|
597
|
+
storePath: string;
|
|
598
|
+
storeKeyPath: string;
|
|
599
|
+
storeExists: boolean;
|
|
600
|
+
storeKeyExists: boolean;
|
|
601
|
+
}
|
|
602
|
+
| { backend: "ces-rpc"; ready: boolean }
|
|
603
|
+
| { backend: "ces-http"; url: string }
|
|
604
|
+
| { backend: "none" };
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Resolve the active credential backend (triggering resolution if not yet
|
|
608
|
+
* done) and return introspection details specific to that backend.
|
|
609
|
+
*
|
|
610
|
+
* Useful for `credentials status` — shows which store this process is talking
|
|
611
|
+
* to, so path/socket mismatches between the CLI and daemon are immediately
|
|
612
|
+
* visible.
|
|
613
|
+
*/
|
|
614
|
+
export function getActiveBackendInfoAsync(): Promise<BackendInfo> {
|
|
615
|
+
return withCredentialTimeout(async () => {
|
|
616
|
+
const backend = await resolveBackendAsync();
|
|
617
|
+
if (backend.name === "encrypted-store") {
|
|
618
|
+
const protectedDir = getProtectedDir();
|
|
619
|
+
const storePath = join(protectedDir, "keys.enc");
|
|
620
|
+
const storeKeyPath = join(protectedDir, "store.key");
|
|
621
|
+
return {
|
|
622
|
+
backend: "encrypted-store" as const,
|
|
623
|
+
storePath,
|
|
624
|
+
storeKeyPath,
|
|
625
|
+
storeExists: existsSync(storePath),
|
|
626
|
+
storeKeyExists: existsSync(storeKeyPath),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (backend.name === "ces-rpc") {
|
|
630
|
+
return { backend: "ces-rpc" as const, ready: backend.isAvailable() };
|
|
631
|
+
}
|
|
632
|
+
if (backend.name === "ces-http") {
|
|
633
|
+
return {
|
|
634
|
+
backend: "ces-http" as const,
|
|
635
|
+
url: process.env.CES_CREDENTIAL_URL ?? "",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return { backend: "none" as const };
|
|
639
|
+
}, { backend: "none" as const });
|
|
640
|
+
}
|
|
641
|
+
|
|
587
642
|
/** @internal Test-only: reset the cached backends so they're re-created. */
|
|
588
643
|
export function _resetBackend(): void {
|
|
589
644
|
_cesClient = undefined;
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
type RemoteSkillProvider = "clawhub" | "skillssh";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
| "safe"
|
|
5
|
-
| "low"
|
|
6
|
-
| "medium"
|
|
7
|
-
| "high"
|
|
8
|
-
| "critical"
|
|
9
|
-
| "unknown";
|
|
3
|
+
type SkillsShRisk = "safe" | "low" | "medium" | "high" | "critical" | "unknown";
|
|
10
4
|
export type SkillsShRiskThreshold = Exclude<SkillsShRisk, "unknown">;
|
|
11
5
|
|
|
12
6
|
export interface RemoteSkillPolicy {
|
|
@@ -50,17 +44,17 @@ interface SkillsShRemoteSkillCandidate extends RemoteSkillCandidateBase {
|
|
|
50
44
|
audit?: SkillsShAuditState | null;
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
|
|
47
|
+
type RemoteSkillCandidate =
|
|
54
48
|
| ClawhubRemoteSkillCandidate
|
|
55
49
|
| SkillsShRemoteSkillCandidate;
|
|
56
50
|
|
|
57
|
-
|
|
51
|
+
type RemoteSkillDenyReason =
|
|
58
52
|
| "clawhub_suspicious"
|
|
59
53
|
| "clawhub_malware_blocked"
|
|
60
54
|
| "clawhub_moderation_missing"
|
|
61
55
|
| "skillssh_risk_exceeds_threshold";
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
type RemoteSkillInstallDecision =
|
|
64
58
|
| { ok: true }
|
|
65
59
|
| { ok: false; reason: RemoteSkillDenyReason };
|
|
66
60
|
|
package/src/subagent/index.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
export { mergeSkillIds } from "./manager.js";
|
|
2
|
-
export type {
|
|
3
|
-
SubagentConfig,
|
|
4
|
-
SubagentRole,
|
|
5
|
-
SubagentRoleConfig,
|
|
6
|
-
SubagentState,
|
|
7
|
-
SubagentStatus,
|
|
8
|
-
} from "./types.js";
|
|
2
|
+
export type { SubagentRole } from "./types.js";
|
|
9
3
|
export { SUBAGENT_ROLE_REGISTRY, TERMINAL_STATUSES } from "./types.js";
|
|
10
4
|
|
|
11
5
|
import { SubagentManager } from "./manager.js";
|
package/src/subagent/manager.ts
CHANGED
|
@@ -11,10 +11,7 @@
|
|
|
11
11
|
import { v4 as uuid } from "uuid";
|
|
12
12
|
|
|
13
13
|
import { getConfig } from "../config/loader.js";
|
|
14
|
-
import {
|
|
15
|
-
Conversation,
|
|
16
|
-
type ConversationMemoryPolicy,
|
|
17
|
-
} from "../daemon/conversation.js";
|
|
14
|
+
import { Conversation } from "../daemon/conversation.js";
|
|
18
15
|
import { findConversation } from "../daemon/conversation-store.js";
|
|
19
16
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
20
17
|
import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
|
|
@@ -231,16 +228,6 @@ export class SubagentManager {
|
|
|
231
228
|
const maxTokens = appConfig.llm.default.maxTokens;
|
|
232
229
|
const workingDir = getSandboxWorkingDir();
|
|
233
230
|
|
|
234
|
-
const memoryPolicy: ConversationMemoryPolicy = isFork
|
|
235
|
-
? {
|
|
236
|
-
scopeId: "default",
|
|
237
|
-
includeDefaultFallback: false,
|
|
238
|
-
}
|
|
239
|
-
: {
|
|
240
|
-
scopeId: `subagent:${subagentId}`,
|
|
241
|
-
includeDefaultFallback: true,
|
|
242
|
-
};
|
|
243
|
-
|
|
244
231
|
// ── Initialise state ────────────────────────────────────────────
|
|
245
232
|
const now = Date.now();
|
|
246
233
|
// For forks, default sendResultToUser to false (silent) unless explicitly true.
|
|
@@ -289,7 +276,6 @@ export class SubagentManager {
|
|
|
289
276
|
maxTokens,
|
|
290
277
|
wrappedSendToClient,
|
|
291
278
|
workingDir,
|
|
292
|
-
memoryPolicy,
|
|
293
279
|
undefined, // sharedCesClient
|
|
294
280
|
undefined, // speedOverride
|
|
295
281
|
"5m", // cacheTtl — subagents run tight tool-use loops, 5m is always hot
|
package/src/tasks/task-runner.ts
CHANGED
package/src/tasks/task-store.ts
CHANGED
|
@@ -27,7 +27,6 @@ export interface TaskRun {
|
|
|
27
27
|
finishedAt: number | null;
|
|
28
28
|
error: string | null;
|
|
29
29
|
principalId: string | null;
|
|
30
|
-
memoryScopeId: string | null;
|
|
31
30
|
createdAt: number;
|
|
32
31
|
}
|
|
33
32
|
|
|
@@ -109,7 +108,6 @@ export function createTaskRun(taskId: string): TaskRun {
|
|
|
109
108
|
finishedAt: null,
|
|
110
109
|
error: null,
|
|
111
110
|
principalId: null,
|
|
112
|
-
memoryScopeId: null,
|
|
113
111
|
createdAt: now,
|
|
114
112
|
};
|
|
115
113
|
db.insert(taskRuns).values(run).run();
|
|
@@ -125,7 +123,6 @@ export function updateTaskRun(
|
|
|
125
123
|
| "conversationId"
|
|
126
124
|
| "error"
|
|
127
125
|
| "principalId"
|
|
128
|
-
| "memoryScopeId"
|
|
129
126
|
| "startedAt"
|
|
130
127
|
| "finishedAt"
|
|
131
128
|
>
|
|
@@ -19,7 +19,7 @@ export interface BackgroundTool {
|
|
|
19
19
|
command: string;
|
|
20
20
|
startedAt: number;
|
|
21
21
|
/** Kills the process (bash) or aborts the proxy (host_bash). */
|
|
22
|
-
cancel: () => void;
|
|
22
|
+
cancel: (reason?: string) => void;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Maximum number of concurrent background tools allowed. */
|
|
@@ -61,16 +61,30 @@ export function listBackgroundTools(conversationId?: string): BackgroundTool[] {
|
|
|
61
61
|
* Cancels a background tool by ID: calls `tool.cancel()`, removes the entry,
|
|
62
62
|
* and returns `true`. Returns `false` if the ID is not found.
|
|
63
63
|
*/
|
|
64
|
-
export function cancelBackgroundTool(id: string): boolean {
|
|
64
|
+
export function cancelBackgroundTool(id: string, reason?: string): boolean {
|
|
65
65
|
const tool = registry.get(id);
|
|
66
66
|
if (!tool) {
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
|
-
tool.cancel();
|
|
69
|
+
tool.cancel(reason);
|
|
70
70
|
registry.delete(id);
|
|
71
71
|
return true;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function cancelBackgroundTools(
|
|
75
|
+
shouldCancel: (tool: BackgroundTool) => boolean,
|
|
76
|
+
reason?: string,
|
|
77
|
+
): BackgroundTool[] {
|
|
78
|
+
const cancelled: BackgroundTool[] = [];
|
|
79
|
+
for (const tool of Array.from(registry.values())) {
|
|
80
|
+
if (!shouldCancel(tool)) continue;
|
|
81
|
+
tool.cancel(reason);
|
|
82
|
+
registry.delete(tool.id);
|
|
83
|
+
cancelled.push(tool);
|
|
84
|
+
}
|
|
85
|
+
return cancelled;
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
/**
|
|
75
89
|
* Generates a short prefixed ID for a background tool.
|
|
76
90
|
* Format: `bg-<8 hex chars>` (e.g. `bg-a1b2c3d4`).
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import type { ToolContext } from "../types.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Singleton mocks — must precede the tool import so bun's module mock applies.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
let mockProxyAvailable = false;
|
|
13
|
+
|
|
14
|
+
mock.module("../../daemon/host-file-proxy.js", () => ({
|
|
15
|
+
HostFileProxy: {
|
|
16
|
+
get instance() {
|
|
17
|
+
return {
|
|
18
|
+
isAvailable: () => mockProxyAvailable,
|
|
19
|
+
request: () => Promise.resolve({ content: "ok", isError: false }),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
26
|
+
assistantEventHub: {
|
|
27
|
+
listClientsByCapability: () => [],
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const { hostFileEditTool } = await import("./edit.js");
|
|
32
|
+
|
|
33
|
+
const testDirs: string[] = [];
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
mockProxyAvailable = false;
|
|
37
|
+
for (const dir of testDirs.splice(0)) {
|
|
38
|
+
rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function makeTempDir(): string {
|
|
43
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "host-edit-test-")));
|
|
44
|
+
testDirs.push(dir);
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeContext(
|
|
49
|
+
workingDir: string,
|
|
50
|
+
transportInterface: ToolContext["transportInterface"],
|
|
51
|
+
): ToolContext {
|
|
52
|
+
return {
|
|
53
|
+
workingDir,
|
|
54
|
+
conversationId: "test-conv",
|
|
55
|
+
trustClass: "guardian",
|
|
56
|
+
transportInterface,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("host_file_edit cross-client guards", () => {
|
|
61
|
+
test("returns 'no client' error on web transport when proxy unavailable and no targetClientId", async () => {
|
|
62
|
+
const workingDir = makeTempDir();
|
|
63
|
+
const result = await hostFileEditTool.execute(
|
|
64
|
+
{
|
|
65
|
+
path: "/some/host/path.txt",
|
|
66
|
+
old_string: "foo",
|
|
67
|
+
new_string: "bar",
|
|
68
|
+
},
|
|
69
|
+
makeContext(workingDir, "web"),
|
|
70
|
+
);
|
|
71
|
+
expect(result.isError).toBe(true);
|
|
72
|
+
expect(result.content).toContain(
|
|
73
|
+
"no client with host_file capability is connected",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
|
|
78
|
+
const workingDir = makeTempDir();
|
|
79
|
+
const result = await hostFileEditTool.execute(
|
|
80
|
+
{
|
|
81
|
+
path: "/some/host/path.txt",
|
|
82
|
+
old_string: "foo",
|
|
83
|
+
new_string: "bar",
|
|
84
|
+
target_client_id: "abc-123",
|
|
85
|
+
},
|
|
86
|
+
makeContext(workingDir, "web"),
|
|
87
|
+
);
|
|
88
|
+
expect(result.isError).toBe(true);
|
|
89
|
+
expect(result.content).toContain(
|
|
90
|
+
'target client "abc-123" is no longer connected',
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("falls through to local fs on macos transport when proxy unavailable", async () => {
|
|
95
|
+
const workingDir = makeTempDir();
|
|
96
|
+
const result = await hostFileEditTool.execute(
|
|
97
|
+
{
|
|
98
|
+
path: "/nonexistent/x.txt",
|
|
99
|
+
old_string: "foo",
|
|
100
|
+
new_string: "bar",
|
|
101
|
+
},
|
|
102
|
+
makeContext(workingDir, "macos"),
|
|
103
|
+
);
|
|
104
|
+
// Proves the guard did NOT fire on macOS — instead we got the
|
|
105
|
+
// local FileSystemOps error path (file not found / IO error).
|
|
106
|
+
expect(result.isError).toBe(true);
|
|
107
|
+
expect(result.content.toLowerCase()).toMatch(/not found|enoent|editing/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("does NOT reject on macos transport with a stale target_client_id when proxy unavailable (regression: P2 fix)", async () => {
|
|
111
|
+
const workingDir = makeTempDir();
|
|
112
|
+
const result = await hostFileEditTool.execute(
|
|
113
|
+
{
|
|
114
|
+
path: "/nonexistent/x.txt",
|
|
115
|
+
old_string: "foo",
|
|
116
|
+
new_string: "bar",
|
|
117
|
+
target_client_id: "stale-mac",
|
|
118
|
+
},
|
|
119
|
+
makeContext(workingDir, "macos"),
|
|
120
|
+
);
|
|
121
|
+
// The disconnected-target guard is scoped to non-host-proxy transports
|
|
122
|
+
// (!supportsHostProxy). On macos, a stale target_client_id auto-filled
|
|
123
|
+
// from a prior cross-client turn must be silently ignored and the call
|
|
124
|
+
// must fall through to local FileSystemOps, NOT reject with "target
|
|
125
|
+
// client ... is no longer connected".
|
|
126
|
+
expect(result.isError).toBe(true);
|
|
127
|
+
expect(result.content).not.toContain("is no longer connected");
|
|
128
|
+
expect(result.content.toLowerCase()).toMatch(/not found|enoent|editing/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("rejects when target_client_id is set but transport metadata is missing (legacy/backwards-compat path)", async () => {
|
|
132
|
+
const workingDir = makeTempDir();
|
|
133
|
+
const result = await hostFileEditTool.execute(
|
|
134
|
+
{
|
|
135
|
+
path: "/some/host/path.txt",
|
|
136
|
+
old_string: "foo",
|
|
137
|
+
new_string: "bar",
|
|
138
|
+
target_client_id: "abc-123",
|
|
139
|
+
},
|
|
140
|
+
// transportInterface intentionally undefined (legacy callers).
|
|
141
|
+
makeContext(workingDir, undefined),
|
|
142
|
+
);
|
|
143
|
+
// Without transport metadata, falling through to local fs would
|
|
144
|
+
// silently target the daemon container. The guard fires for undefined
|
|
145
|
+
// transport AND non-host-proxy transports — only macos turns skip it.
|
|
146
|
+
expect(result.isError).toBe(true);
|
|
147
|
+
expect(result.content).toContain(
|
|
148
|
+
'target client "abc-123" is no longer connected',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -92,7 +92,8 @@ class HostFileEditTool implements Tool {
|
|
|
92
92
|
const replaceAll = input.replace_all === true;
|
|
93
93
|
|
|
94
94
|
const targetClientId =
|
|
95
|
-
typeof input.target_client_id === "string" &&
|
|
95
|
+
typeof input.target_client_id === "string" &&
|
|
96
|
+
input.target_client_id !== ""
|
|
96
97
|
? input.target_client_id
|
|
97
98
|
: undefined;
|
|
98
99
|
|
|
@@ -109,6 +110,45 @@ class HostFileEditTool implements Tool {
|
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
// Guard: non-host-proxy interfaces with no capable clients connected.
|
|
114
|
+
// Without this guard, the request would fall through to local
|
|
115
|
+
// FileSystemOps below and read the daemon container's filesystem
|
|
116
|
+
// instead of the user's host machine.
|
|
117
|
+
if (
|
|
118
|
+
targetClientId == null &&
|
|
119
|
+
transportInterface != null &&
|
|
120
|
+
!supportsHostProxy(transportInterface) &&
|
|
121
|
+
!HostFileProxy.instance.isAvailable()
|
|
122
|
+
) {
|
|
123
|
+
return {
|
|
124
|
+
content:
|
|
125
|
+
"Error: no client with host_file capability is connected. Connect a macOS client to use host_file from a non-desktop interface.",
|
|
126
|
+
isError: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Guard: explicit targetClientId provided but proxy is unavailable.
|
|
131
|
+
// Fires on non-host-proxy transports (web, ios) AND on legacy callers
|
|
132
|
+
// without transport metadata, where falling through to local fs would
|
|
133
|
+
// silently target the daemon container's filesystem instead of the
|
|
134
|
+
// intended host client. Skips only when transport is explicitly
|
|
135
|
+
// host-proxy-capable (macos), where local-fs fallback IS the intended
|
|
136
|
+
// offline behavior — a stale target_client_id auto-filled from a prior
|
|
137
|
+
// cross-client turn is silently ignored on those turns.
|
|
138
|
+
// Note: this scoping deliberately differs from host_bash
|
|
139
|
+
// (host-shell.ts:239-247), which rejects unconditionally for any
|
|
140
|
+
// stale target_client_id regardless of transport.
|
|
141
|
+
if (
|
|
142
|
+
targetClientId != null &&
|
|
143
|
+
!HostFileProxy.instance.isAvailable() &&
|
|
144
|
+
(transportInterface == null || !supportsHostProxy(transportInterface))
|
|
145
|
+
) {
|
|
146
|
+
return {
|
|
147
|
+
content: `Error: target client "${targetClientId}" is no longer connected. The specified client may have disconnected since the tool was called. Run \`assistant clients list --capability host_file\` to see currently connected clients.`,
|
|
148
|
+
isError: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
112
152
|
// Proxy to connected client for execution on the user's machine
|
|
113
153
|
// when a capable client is available (managed/cloud-hosted mode).
|
|
114
154
|
if (HostFileProxy.instance.isAvailable()) {
|
|
@@ -123,6 +163,8 @@ class HostFileEditTool implements Tool {
|
|
|
123
163
|
},
|
|
124
164
|
context.conversationId,
|
|
125
165
|
context.signal,
|
|
166
|
+
targetClientId,
|
|
167
|
+
context.sourceActorPrincipalId,
|
|
126
168
|
);
|
|
127
169
|
}
|
|
128
170
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import type { ToolContext } from "../types.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Singleton mocks — must precede the tool import so bun's module mock applies.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
let mockProxyAvailable = false;
|
|
13
|
+
|
|
14
|
+
mock.module("../../daemon/host-file-proxy.js", () => ({
|
|
15
|
+
HostFileProxy: {
|
|
16
|
+
get instance() {
|
|
17
|
+
return {
|
|
18
|
+
isAvailable: () => mockProxyAvailable,
|
|
19
|
+
request: () => Promise.resolve({ content: "ok", isError: false }),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
26
|
+
assistantEventHub: {
|
|
27
|
+
listClientsByCapability: () => [],
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const { hostFileReadTool } = await import("./read.js");
|
|
32
|
+
|
|
33
|
+
const testDirs: string[] = [];
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
mockProxyAvailable = false;
|
|
37
|
+
for (const dir of testDirs.splice(0)) {
|
|
38
|
+
rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function makeTempDir(): string {
|
|
43
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "host-read-test-")));
|
|
44
|
+
testDirs.push(dir);
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeContext(
|
|
49
|
+
workingDir: string,
|
|
50
|
+
transportInterface: ToolContext["transportInterface"],
|
|
51
|
+
): ToolContext {
|
|
52
|
+
return {
|
|
53
|
+
workingDir,
|
|
54
|
+
conversationId: "test-conv",
|
|
55
|
+
trustClass: "guardian",
|
|
56
|
+
transportInterface,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("host_file_read cross-client guards", () => {
|
|
61
|
+
test("returns 'no client' error on web transport when proxy unavailable and no targetClientId", async () => {
|
|
62
|
+
const workingDir = makeTempDir();
|
|
63
|
+
const result = await hostFileReadTool.execute(
|
|
64
|
+
{ path: "/some/host/path.txt" },
|
|
65
|
+
makeContext(workingDir, "web"),
|
|
66
|
+
);
|
|
67
|
+
expect(result.isError).toBe(true);
|
|
68
|
+
expect(result.content).toContain(
|
|
69
|
+
"no client with host_file capability is connected",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("returns 'specified client disconnected' error when targetClientId set but proxy unavailable on web", async () => {
|
|
74
|
+
const workingDir = makeTempDir();
|
|
75
|
+
const result = await hostFileReadTool.execute(
|
|
76
|
+
{ path: "/some/host/path.txt", target_client_id: "abc-123" },
|
|
77
|
+
makeContext(workingDir, "web"),
|
|
78
|
+
);
|
|
79
|
+
expect(result.isError).toBe(true);
|
|
80
|
+
expect(result.content).toContain(
|
|
81
|
+
'target client "abc-123" is no longer connected',
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("falls through to local fs on macos transport when proxy unavailable and path is non-image", async () => {
|
|
86
|
+
const workingDir = makeTempDir();
|
|
87
|
+
const result = await hostFileReadTool.execute(
|
|
88
|
+
{ path: "/nonexistent/x.txt" },
|
|
89
|
+
makeContext(workingDir, "macos"),
|
|
90
|
+
);
|
|
91
|
+
// Proves the guard did NOT fire on macOS — instead we got the
|
|
92
|
+
// local FileSystemOps NOT_FOUND error.
|
|
93
|
+
expect(result.isError).toBe(true);
|
|
94
|
+
expect(result.content).toContain("File not found");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("does NOT reject on macos transport with a stale target_client_id when proxy unavailable (regression: P2 fix)", async () => {
|
|
98
|
+
const workingDir = makeTempDir();
|
|
99
|
+
const result = await hostFileReadTool.execute(
|
|
100
|
+
{ path: "/nonexistent/x.txt", target_client_id: "stale-mac" },
|
|
101
|
+
makeContext(workingDir, "macos"),
|
|
102
|
+
);
|
|
103
|
+
// The disconnected-target guard is scoped to non-host-proxy transports
|
|
104
|
+
// (!supportsHostProxy). On macos, a stale target_client_id auto-filled
|
|
105
|
+
// from a prior cross-client turn must be silently ignored and the call
|
|
106
|
+
// must fall through to local FileSystemOps (NOT_FOUND for a fake path),
|
|
107
|
+
// NOT reject with "target client ... is no longer connected".
|
|
108
|
+
expect(result.isError).toBe(true);
|
|
109
|
+
expect(result.content).toContain("File not found");
|
|
110
|
+
expect(result.content).not.toContain("is no longer connected");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("rejects when target_client_id is set but transport metadata is missing (legacy/backwards-compat path)", async () => {
|
|
114
|
+
const workingDir = makeTempDir();
|
|
115
|
+
const result = await hostFileReadTool.execute(
|
|
116
|
+
{ path: "/some/host/path.txt", target_client_id: "abc-123" },
|
|
117
|
+
// transportInterface intentionally undefined (legacy callers).
|
|
118
|
+
makeContext(workingDir, undefined),
|
|
119
|
+
);
|
|
120
|
+
// When transport metadata is missing we cannot rule out a non-host-proxy
|
|
121
|
+
// turn, so falling through to local fs would silently target the daemon
|
|
122
|
+
// container. The guard fires for both undefined transport AND
|
|
123
|
+
// non-host-proxy transports — only macos turns skip it.
|
|
124
|
+
expect(result.isError).toBe(true);
|
|
125
|
+
expect(result.content).toContain(
|
|
126
|
+
'target client "abc-123" is no longer connected',
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -63,7 +63,8 @@ class HostFileReadTool implements Tool {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const targetClientId =
|
|
66
|
-
typeof input.target_client_id === "string" &&
|
|
66
|
+
typeof input.target_client_id === "string" &&
|
|
67
|
+
input.target_client_id !== ""
|
|
67
68
|
? input.target_client_id
|
|
68
69
|
: undefined;
|
|
69
70
|
|
|
@@ -80,6 +81,45 @@ class HostFileReadTool implements Tool {
|
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
// Guard: non-host-proxy interfaces with no capable clients connected.
|
|
85
|
+
// Without this guard, the request would fall through to local
|
|
86
|
+
// FileSystemOps below and read the daemon container's filesystem
|
|
87
|
+
// instead of the user's host machine.
|
|
88
|
+
if (
|
|
89
|
+
targetClientId == null &&
|
|
90
|
+
transportInterface != null &&
|
|
91
|
+
!supportsHostProxy(transportInterface) &&
|
|
92
|
+
!HostFileProxy.instance.isAvailable()
|
|
93
|
+
) {
|
|
94
|
+
return {
|
|
95
|
+
content:
|
|
96
|
+
"Error: no client with host_file capability is connected. Connect a macOS client to use host_file from a non-desktop interface.",
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Guard: explicit targetClientId provided but proxy is unavailable.
|
|
102
|
+
// Fires on non-host-proxy transports (web, ios) AND on legacy callers
|
|
103
|
+
// without transport metadata, where falling through to local fs would
|
|
104
|
+
// silently target the daemon container's filesystem instead of the
|
|
105
|
+
// intended host client. Skips only when transport is explicitly
|
|
106
|
+
// host-proxy-capable (macos), where local-fs fallback IS the intended
|
|
107
|
+
// offline behavior — a stale target_client_id auto-filled from a prior
|
|
108
|
+
// cross-client turn is silently ignored on those turns.
|
|
109
|
+
// Note: this scoping deliberately differs from host_bash
|
|
110
|
+
// (host-shell.ts:239-247), which rejects unconditionally for any
|
|
111
|
+
// stale target_client_id regardless of transport.
|
|
112
|
+
if (
|
|
113
|
+
targetClientId != null &&
|
|
114
|
+
!HostFileProxy.instance.isAvailable() &&
|
|
115
|
+
(transportInterface == null || !supportsHostProxy(transportInterface))
|
|
116
|
+
) {
|
|
117
|
+
return {
|
|
118
|
+
content: `Error: target client "${targetClientId}" is no longer connected. The specified client may have disconnected since the tool was called. Run \`assistant clients list --capability host_file\` to see currently connected clients.`,
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
83
123
|
// Proxy to connected client for execution on the user's machine
|
|
84
124
|
// when a capable client is available (managed/cloud-hosted mode),
|
|
85
125
|
// including image reads that need the host filesystem view.
|
|
@@ -94,6 +134,8 @@ class HostFileReadTool implements Tool {
|
|
|
94
134
|
},
|
|
95
135
|
context.conversationId,
|
|
96
136
|
context.signal,
|
|
137
|
+
targetClientId,
|
|
138
|
+
context.sourceActorPrincipalId,
|
|
97
139
|
);
|
|
98
140
|
}
|
|
99
141
|
|