@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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
CHANNEL_IDS,
|
|
13
13
|
INTERFACE_IDS,
|
|
14
|
+
isInteractiveInterface,
|
|
14
15
|
parseChannelId,
|
|
15
16
|
parseInterfaceId,
|
|
16
17
|
} from "../../channels/types.js";
|
|
@@ -656,14 +657,7 @@ export async function handleSendMessage(
|
|
|
656
657
|
}
|
|
657
658
|
|
|
658
659
|
const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
|
|
659
|
-
|
|
660
|
-
// permission prompts. Channel interfaces (telegram, slack, etc.) route
|
|
661
|
-
// approvals through the guardian system and have no interactive prompter UI.
|
|
662
|
-
const isInteractiveInterface =
|
|
663
|
-
sourceInterface === "macos" ||
|
|
664
|
-
sourceInterface === "ios" ||
|
|
665
|
-
sourceInterface === "cli" ||
|
|
666
|
-
sourceInterface === "vellum";
|
|
660
|
+
const isInteractive = isInteractiveInterface(sourceInterface);
|
|
667
661
|
// Only create the host bash proxy for desktop client interfaces that can
|
|
668
662
|
// execute commands on the user's machine. Non-desktop sessions (CLI,
|
|
669
663
|
// channels, headless) fall back to local execution.
|
|
@@ -709,7 +703,7 @@ export async function handleSendMessage(
|
|
|
709
703
|
session.isProcessing() &&
|
|
710
704
|
sourceInterface !== "macos" &&
|
|
711
705
|
sourceInterface !== "ios";
|
|
712
|
-
session.updateClient(onEvent, !
|
|
706
|
+
session.updateClient(onEvent, !isInteractive, {
|
|
713
707
|
skipProxySenderUpdate: preservingProxies,
|
|
714
708
|
});
|
|
715
709
|
|
|
@@ -781,7 +775,7 @@ export async function handleSendMessage(
|
|
|
781
775
|
userMessageInterface: sourceInterface,
|
|
782
776
|
assistantMessageInterface: sourceInterface,
|
|
783
777
|
},
|
|
784
|
-
{ isInteractive
|
|
778
|
+
{ isInteractive },
|
|
785
779
|
);
|
|
786
780
|
if (enqueueResult.rejected) {
|
|
787
781
|
return Response.json(
|
|
@@ -821,6 +815,31 @@ export async function handleSendMessage(
|
|
|
821
815
|
);
|
|
822
816
|
}
|
|
823
817
|
|
|
818
|
+
// Auto-deny pending confirmations for idle sessions. The legacy
|
|
819
|
+
// handleUserMessage called autoDenyPendingConfirmations unconditionally
|
|
820
|
+
// before dispatching, so an idle session with lingering confirmations
|
|
821
|
+
// (e.g. the user never responded to a tool-approval prompt) must deny
|
|
822
|
+
// them before starting the new turn.
|
|
823
|
+
if (session.hasAnyPendingConfirmation()) {
|
|
824
|
+
for (const interaction of pendingInteractions.getByConversation(
|
|
825
|
+
mapping.conversationId,
|
|
826
|
+
)) {
|
|
827
|
+
if (
|
|
828
|
+
interaction.session === session &&
|
|
829
|
+
interaction.kind === "confirmation"
|
|
830
|
+
) {
|
|
831
|
+
session.emitConfirmationStateChanged({
|
|
832
|
+
sessionId: mapping.conversationId,
|
|
833
|
+
requestId: interaction.requestId,
|
|
834
|
+
state: "denied" as const,
|
|
835
|
+
source: "auto_deny" as const,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
session.denyAllPendingConfirmations();
|
|
840
|
+
pendingInteractions.removeBySession(session);
|
|
841
|
+
}
|
|
842
|
+
|
|
824
843
|
// Session is idle — persist and fire agent loop immediately
|
|
825
844
|
session.setTurnChannelContext({
|
|
826
845
|
userMessageChannel: sourceChannel,
|
|
@@ -845,7 +864,7 @@ export async function handleSendMessage(
|
|
|
845
864
|
provider: config.provider,
|
|
846
865
|
estimatedCost: session.usageStats.estimatedCost,
|
|
847
866
|
};
|
|
848
|
-
const slashResult = resolveSlash(rawContent, slashContext);
|
|
867
|
+
const slashResult = await resolveSlash(rawContent, slashContext);
|
|
849
868
|
|
|
850
869
|
if (slashResult.kind === "unknown") {
|
|
851
870
|
session.processing = true;
|
|
@@ -890,7 +909,7 @@ export async function handleSendMessage(
|
|
|
890
909
|
// a config change from a concurrent request.
|
|
891
910
|
const modelInfoEvent =
|
|
892
911
|
isModelSlashCommand(rawContent) || isProviderShortcut(rawContent)
|
|
893
|
-
? buildModelInfoEvent()
|
|
912
|
+
? await buildModelInfoEvent()
|
|
894
913
|
: null;
|
|
895
914
|
|
|
896
915
|
const response = Response.json(
|
|
@@ -960,7 +979,7 @@ export async function handleSendMessage(
|
|
|
960
979
|
// Fire-and-forget the agent loop; events flow to the hub via onEvent.
|
|
961
980
|
session
|
|
962
981
|
.runAgentLoop(resolvedContent, messageId, onEvent, {
|
|
963
|
-
isInteractive
|
|
982
|
+
isInteractive,
|
|
964
983
|
isUserMessage: true,
|
|
965
984
|
})
|
|
966
985
|
.catch((err) => {
|
|
@@ -250,7 +250,7 @@ export async function handleChannelInbound(
|
|
|
250
250
|
canonicalAssistantId,
|
|
251
251
|
assistantId,
|
|
252
252
|
content,
|
|
253
|
-
|
|
253
|
+
channelId: resolvedMember?.channel.id,
|
|
254
254
|
});
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -306,7 +306,7 @@ export async function handleChannelInbound(
|
|
|
306
306
|
// retries). This was previously in ACL enforcement which runs before dedup,
|
|
307
307
|
// causing retries to inflate interaction counts.
|
|
308
308
|
if (!result.duplicate && resolvedMember) {
|
|
309
|
-
touchContactInteraction(resolvedMember.
|
|
309
|
+
touchContactInteraction(resolvedMember.channel.id);
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
// external_conversation_bindings is assistant-agnostic. Restrict writes to
|
|
@@ -390,6 +390,13 @@ export async function handleChannelInbound(
|
|
|
390
390
|
? (rawCommandIntent as Record<string, unknown>)
|
|
391
391
|
: undefined;
|
|
392
392
|
|
|
393
|
+
// Extract chat type (e.g. "private", "group", "supergroup") for group chat gating
|
|
394
|
+
const sourceChatType =
|
|
395
|
+
typeof sourceMetadata?.chatType === "string" &&
|
|
396
|
+
sourceMetadata.chatType.trim().length > 0
|
|
397
|
+
? sourceMetadata.chatType.trim()
|
|
398
|
+
: undefined;
|
|
399
|
+
|
|
393
400
|
// Preserve locale from sourceMetadata so the model can greet in the user's language
|
|
394
401
|
const sourceLanguageCode =
|
|
395
402
|
typeof sourceMetadata?.languageCode === "string" &&
|
|
@@ -620,6 +627,7 @@ export async function handleChannelInbound(
|
|
|
620
627
|
assistantId: canonicalAssistantId,
|
|
621
628
|
approvalCopyGenerator,
|
|
622
629
|
externalMessageId: sourceMessageId ?? externalMessageId,
|
|
630
|
+
chatType: sourceChatType,
|
|
623
631
|
});
|
|
624
632
|
}
|
|
625
633
|
|
|
@@ -77,6 +77,8 @@ export interface BackgroundProcessingParams {
|
|
|
77
77
|
sourceLanguageCode?: string;
|
|
78
78
|
/** External message ID (e.g. Slack message ts) used for reaction indicators. */
|
|
79
79
|
externalMessageId?: string;
|
|
80
|
+
/** Chat type from the gateway (e.g. "private", "group", "supergroup"). */
|
|
81
|
+
chatType?: string;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/**
|
|
@@ -105,6 +107,7 @@ export function processChannelMessageInBackground(
|
|
|
105
107
|
commandIntent,
|
|
106
108
|
sourceLanguageCode,
|
|
107
109
|
externalMessageId,
|
|
110
|
+
chatType,
|
|
108
111
|
} = params;
|
|
109
112
|
|
|
110
113
|
(async () => {
|
|
@@ -193,6 +196,7 @@ export function processChannelMessageInBackground(
|
|
|
193
196
|
channelId: sourceChannel,
|
|
194
197
|
hints: metadataHints.length > 0 ? metadataHints : undefined,
|
|
195
198
|
uxBrief: metadataUxBrief,
|
|
199
|
+
chatType,
|
|
196
200
|
},
|
|
197
201
|
assistantId,
|
|
198
202
|
trustContext: trustCtx,
|
|
@@ -30,8 +30,8 @@ export interface EditInterceptParams {
|
|
|
30
30
|
canonicalAssistantId: string;
|
|
31
31
|
assistantId: string;
|
|
32
32
|
content: string | undefined;
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/** Channel ID for channel-level interaction tracking. */
|
|
34
|
+
channelId?: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -52,7 +52,7 @@ export async function handleEditIntercept(
|
|
|
52
52
|
canonicalAssistantId,
|
|
53
53
|
assistantId,
|
|
54
54
|
content,
|
|
55
|
-
|
|
55
|
+
channelId,
|
|
56
56
|
} = params;
|
|
57
57
|
|
|
58
58
|
// Dedup the edit event itself (retried edited_message webhooks)
|
|
@@ -73,8 +73,8 @@ export async function handleEditIntercept(
|
|
|
73
73
|
|
|
74
74
|
// Track contact interaction only for genuinely new edit events (not webhook
|
|
75
75
|
// retries), matching the pattern used for the normal message path.
|
|
76
|
-
if (
|
|
77
|
-
touchContactInteraction(
|
|
76
|
+
if (channelId) {
|
|
77
|
+
touchContactInteraction(channelId);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Retry lookup a few times -- the original message may still be processing
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "../../../../messaging/providers/slack/client.js";
|
|
14
14
|
import type { SlackConversation } from "../../../../messaging/providers/slack/types.js";
|
|
15
15
|
import { getConnectionByProvider } from "../../../../oauth/oauth-store.js";
|
|
16
|
-
import {
|
|
16
|
+
import { getSecureKeyAsync } from "../../../../security/secure-keys.js";
|
|
17
17
|
import { getLogger } from "../../../../util/logger.js";
|
|
18
18
|
import { httpError } from "../../../http-errors.js";
|
|
19
19
|
import type { RouteDefinition } from "../../../http-router.js";
|
|
@@ -27,10 +27,10 @@ const log = getLogger("slack-share");
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolve the Slack bot token from the OAuth connection store.
|
|
29
29
|
*/
|
|
30
|
-
function resolveSlackToken(): string | undefined {
|
|
30
|
+
async function resolveSlackToken(): Promise<string | undefined> {
|
|
31
31
|
const conn = getConnectionByProvider("integration:slack");
|
|
32
32
|
return conn
|
|
33
|
-
?
|
|
33
|
+
? await getSecureKeyAsync(`oauth_connection/${conn.id}/access_token`)
|
|
34
34
|
: undefined;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -61,7 +61,7 @@ const TYPE_SORT_ORDER: Record<string, number> = {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
export async function handleListSlackChannels(): Promise<Response> {
|
|
64
|
-
const token = resolveSlackToken();
|
|
64
|
+
const token = await resolveSlackToken();
|
|
65
65
|
if (!token) {
|
|
66
66
|
return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
|
|
67
67
|
}
|
|
@@ -137,7 +137,7 @@ export async function handleListSlackChannels(): Promise<Response> {
|
|
|
137
137
|
export async function handleShareToSlackChannel(
|
|
138
138
|
req: Request,
|
|
139
139
|
): Promise<Response> {
|
|
140
|
-
const token = resolveSlackToken();
|
|
140
|
+
const token = await resolveSlackToken();
|
|
141
141
|
if (!token) {
|
|
142
142
|
return httpError("SERVICE_UNAVAILABLE", "No Slack token configured", 503);
|
|
143
143
|
}
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
* of requiring direct filesystem access.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
lstatSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
statSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
11
18
|
|
|
12
19
|
import { desc } from "drizzle-orm";
|
|
13
20
|
|
|
@@ -18,6 +25,7 @@ import {
|
|
|
18
25
|
getDataDir,
|
|
19
26
|
getRootDir,
|
|
20
27
|
getWorkspaceConfigPath,
|
|
28
|
+
getWorkspaceDir,
|
|
21
29
|
} from "../../util/platform.js";
|
|
22
30
|
import { httpError } from "../http-errors.js";
|
|
23
31
|
import type { RouteDefinition } from "../http-router.js";
|
|
@@ -36,6 +44,7 @@ interface ExportResponse {
|
|
|
36
44
|
auditRows: Array<Record<string, unknown>>;
|
|
37
45
|
logFiles: Record<string, string>;
|
|
38
46
|
configSnapshot?: Record<string, unknown>;
|
|
47
|
+
workspaceFiles: Record<string, string>;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/**
|
|
@@ -91,12 +100,16 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
|
|
|
91
100
|
// --- Sanitized config snapshot ---
|
|
92
101
|
const configSnapshot = readSanitizedConfig();
|
|
93
102
|
|
|
103
|
+
// --- Workspace files ---
|
|
104
|
+
const workspaceFiles = collectWorkspaceFiles();
|
|
105
|
+
|
|
94
106
|
log.info(
|
|
95
107
|
{
|
|
96
108
|
auditCount: auditRows.length,
|
|
97
109
|
logFileCount: Object.keys(logFiles).length,
|
|
98
110
|
totalBytes,
|
|
99
111
|
hasConfig: configSnapshot !== undefined,
|
|
112
|
+
workspaceFileCount: Object.keys(workspaceFiles).length,
|
|
100
113
|
},
|
|
101
114
|
"Export completed",
|
|
102
115
|
);
|
|
@@ -106,6 +119,7 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
|
|
|
106
119
|
auditRows,
|
|
107
120
|
logFiles,
|
|
108
121
|
configSnapshot,
|
|
122
|
+
workspaceFiles,
|
|
109
123
|
};
|
|
110
124
|
return Response.json(payload);
|
|
111
125
|
} catch (err) {
|
|
@@ -115,6 +129,112 @@ async function handleExport(body: ExportRequestBody): Promise<Response> {
|
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
|
|
132
|
+
/** Directory prefixes to skip when collecting workspace files. */
|
|
133
|
+
const WORKSPACE_SKIP_DIRS = new Set(["embedding-models", "data/qdrant"]);
|
|
134
|
+
|
|
135
|
+
/** Files at the workspace root to skip (already covered by sanitized fields). */
|
|
136
|
+
const WORKSPACE_SKIP_ROOT_FILES = new Set(["config.json"]);
|
|
137
|
+
|
|
138
|
+
/** Maximum cumulative size for workspace file contents (10 MB). */
|
|
139
|
+
const MAX_WORKSPACE_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Recursively collects files from the workspace directory into a
|
|
143
|
+
* `Record<string, string>` map of relative path to content.
|
|
144
|
+
*
|
|
145
|
+
* - Skips `config.json` at the workspace root (already exported as a
|
|
146
|
+
* sanitized `configSnapshot`; the raw file contains secrets).
|
|
147
|
+
* - Skips symlinks to prevent reading files outside the workspace.
|
|
148
|
+
* - Skips directories in `WORKSPACE_SKIP_DIRS`.
|
|
149
|
+
* - For `.db` files, shells out to `sqlite3 <path> .dump` and stores the
|
|
150
|
+
* SQL text output with a `.sql` suffix appended to the key.
|
|
151
|
+
* - Skips binary files (detected via null-byte heuristic).
|
|
152
|
+
* - Stops collecting once `MAX_WORKSPACE_PAYLOAD_BYTES` is reached.
|
|
153
|
+
*/
|
|
154
|
+
function collectWorkspaceFiles(): Record<string, string> {
|
|
155
|
+
const wsDir = getWorkspaceDir();
|
|
156
|
+
if (!existsSync(wsDir)) return {};
|
|
157
|
+
|
|
158
|
+
const result: Record<string, string> = {};
|
|
159
|
+
let totalBytes = 0;
|
|
160
|
+
|
|
161
|
+
function walk(dir: string): void {
|
|
162
|
+
let entries: string[];
|
|
163
|
+
try {
|
|
164
|
+
entries = readdirSync(dir);
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const fullPath = join(dir, entry);
|
|
171
|
+
const relPath = relative(wsDir, fullPath);
|
|
172
|
+
|
|
173
|
+
// Check if this path falls under a skipped directory prefix
|
|
174
|
+
if (
|
|
175
|
+
[...WORKSPACE_SKIP_DIRS].some(
|
|
176
|
+
(prefix) => relPath === prefix || relPath.startsWith(prefix + "/"),
|
|
177
|
+
)
|
|
178
|
+
) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Skip root-level files that are already exported separately
|
|
183
|
+
if (dir === wsDir && WORKSPACE_SKIP_ROOT_FILES.has(entry)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Use lstatSync to avoid following symlinks
|
|
189
|
+
const stat = lstatSync(fullPath);
|
|
190
|
+
|
|
191
|
+
// Skip symlinks — they could point outside the workspace
|
|
192
|
+
if (stat.isSymbolicLink()) continue;
|
|
193
|
+
|
|
194
|
+
if (stat.isDirectory()) {
|
|
195
|
+
walk(fullPath);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!stat.isFile()) continue;
|
|
199
|
+
|
|
200
|
+
// Enforce cumulative size cap
|
|
201
|
+
if (totalBytes + stat.size > MAX_WORKSPACE_PAYLOAD_BYTES) continue;
|
|
202
|
+
|
|
203
|
+
// SQLite DB handling: dump as SQL text
|
|
204
|
+
if (entry.endsWith(".db")) {
|
|
205
|
+
try {
|
|
206
|
+
const proc = spawnSync("sqlite3", [fullPath, ".dump"], {
|
|
207
|
+
timeout: 10_000,
|
|
208
|
+
});
|
|
209
|
+
if (proc.status === 0 && proc.stdout) {
|
|
210
|
+
const output =
|
|
211
|
+
proc.stdout instanceof Buffer
|
|
212
|
+
? proc.stdout.toString("utf-8")
|
|
213
|
+
: String(proc.stdout);
|
|
214
|
+
result[relPath + ".sql"] = output;
|
|
215
|
+
totalBytes += Buffer.byteLength(output, "utf-8");
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Skip if dump fails
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Read as UTF-8 and skip binary files (null-byte heuristic)
|
|
224
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
225
|
+
if (content.includes("\0")) continue;
|
|
226
|
+
result[relPath] = content;
|
|
227
|
+
totalBytes += stat.size;
|
|
228
|
+
} catch {
|
|
229
|
+
// Skip unreadable files
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
walk(wsDir);
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
118
238
|
/**
|
|
119
239
|
* Replaces a string value with a presence flag: "(set)" if truthy, "(empty)" otherwise.
|
|
120
240
|
*/
|
|
@@ -134,14 +254,6 @@ function readSanitizedConfig(): Record<string, unknown> | undefined {
|
|
|
134
254
|
const raw = readFileSync(configPath, "utf-8");
|
|
135
255
|
const config = JSON.parse(raw) as Record<string, unknown>;
|
|
136
256
|
|
|
137
|
-
// Strip API key values — preserve which providers have keys configured
|
|
138
|
-
if (config.apiKeys && typeof config.apiKeys === "object") {
|
|
139
|
-
const keys = config.apiKeys as Record<string, unknown>;
|
|
140
|
-
config.apiKeys = Object.fromEntries(
|
|
141
|
-
Object.keys(keys).map((k) => [k, redactStringValue(keys[k])]),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
257
|
// Strip ingress webhook secret
|
|
146
258
|
if (config.ingress && typeof config.ingress === "object") {
|
|
147
259
|
const ingress = config.ingress as Record<string, unknown>;
|
|
@@ -52,8 +52,8 @@ export function sessionQueryRouteDefinitions(
|
|
|
52
52
|
endpoint: "model",
|
|
53
53
|
method: "GET",
|
|
54
54
|
policyKey: "model",
|
|
55
|
-
handler: () => {
|
|
56
|
-
const info = getModelInfo();
|
|
55
|
+
handler: async () => {
|
|
56
|
+
const info = await getModelInfo();
|
|
57
57
|
return Response.json(info);
|
|
58
58
|
},
|
|
59
59
|
},
|
|
@@ -74,7 +74,7 @@ export function sessionQueryRouteDefinitions(
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
try {
|
|
77
|
-
const info = setModel(body.modelId, deps.getModelSetContext());
|
|
77
|
+
const info = await setModel(body.modelId, deps.getModelSetContext());
|
|
78
78
|
return Response.json(info);
|
|
79
79
|
} catch (err) {
|
|
80
80
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
|
+
import { setIngressPublicBaseUrl } from "../../config/env.js";
|
|
14
|
+
import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
|
|
13
15
|
import { loadSkillCatalog } from "../../config/skills.js";
|
|
16
|
+
import {
|
|
17
|
+
computeGatewayTarget,
|
|
18
|
+
getIngressConfigResult,
|
|
19
|
+
} from "../../daemon/handlers/config-ingress.js";
|
|
14
20
|
import { normalizeActivationKey } from "../../daemon/handlers/config-voice.js";
|
|
15
21
|
import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
|
|
16
22
|
import {
|
|
@@ -694,5 +700,52 @@ export function settingsRouteDefinitions(): RouteDefinition[] {
|
|
|
694
700
|
policyKey: "diagnostics/env-vars",
|
|
695
701
|
handler: () => handleEnvVars(),
|
|
696
702
|
},
|
|
703
|
+
|
|
704
|
+
// Ingress config (GET / PUT)
|
|
705
|
+
{
|
|
706
|
+
endpoint: "integrations/ingress/config",
|
|
707
|
+
method: "GET",
|
|
708
|
+
policyKey: "integrations/ingress/config:GET",
|
|
709
|
+
handler: () => Response.json(getIngressConfigResult()),
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
endpoint: "integrations/ingress/config",
|
|
713
|
+
method: "PUT",
|
|
714
|
+
policyKey: "integrations/ingress/config",
|
|
715
|
+
handler: async ({ req }) => {
|
|
716
|
+
try {
|
|
717
|
+
const body = (await req.json()) as {
|
|
718
|
+
publicBaseUrl?: string;
|
|
719
|
+
enabled?: boolean;
|
|
720
|
+
};
|
|
721
|
+
const value = (body.publicBaseUrl ?? "").trim().replace(/\/+$/, "");
|
|
722
|
+
const raw = loadRawConfig();
|
|
723
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
724
|
+
ingress.publicBaseUrl = value || undefined;
|
|
725
|
+
if (body.enabled !== undefined) {
|
|
726
|
+
ingress.enabled = body.enabled;
|
|
727
|
+
}
|
|
728
|
+
saveRawConfig({ ...raw, ingress });
|
|
729
|
+
|
|
730
|
+
const isEnabled = (ingress.enabled as boolean | undefined) ?? false;
|
|
731
|
+
if (value && isEnabled) {
|
|
732
|
+
setIngressPublicBaseUrl(value);
|
|
733
|
+
} else {
|
|
734
|
+
setIngressPublicBaseUrl(undefined);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return Response.json({
|
|
738
|
+
enabled: isEnabled,
|
|
739
|
+
publicBaseUrl: value,
|
|
740
|
+
localGatewayTarget: computeGatewayTarget(),
|
|
741
|
+
success: true,
|
|
742
|
+
});
|
|
743
|
+
} catch (err) {
|
|
744
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
745
|
+
log.error({ err }, "Failed to update ingress config via HTTP");
|
|
746
|
+
return httpError("INTERNAL_ERROR", message, 500);
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
},
|
|
697
750
|
];
|
|
698
751
|
}
|
|
@@ -165,7 +165,7 @@ const voiceTemplates: Record<
|
|
|
165
165
|
"That code was incorrect. Please try again.",
|
|
166
166
|
|
|
167
167
|
[GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_SUCCESS]: (_vars) =>
|
|
168
|
-
"Verification successful.
|
|
168
|
+
"Verification successful.",
|
|
169
169
|
|
|
170
170
|
[GUARDIAN_VERIFY_TEMPLATE_KEYS.VOICE_FAILURE]: (_vars) =>
|
|
171
171
|
"Too many incorrect attempts. Goodbye.",
|
package/src/security/oauth2.ts
CHANGED
|
@@ -359,9 +359,9 @@ function startLoopbackServerAndWaitForCode(
|
|
|
359
359
|
server.close();
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
server.listen(loopbackPort ?? 0, "
|
|
362
|
+
server.listen(loopbackPort ?? 0, "localhost", () => {
|
|
363
363
|
const addr = server.address() as { port: number };
|
|
364
|
-
boundRedirectUri = `http://
|
|
364
|
+
boundRedirectUri = `http://localhost:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
|
|
365
365
|
|
|
366
366
|
const authParams = new URLSearchParams({
|
|
367
367
|
...config.extraParams,
|
|
@@ -617,9 +617,9 @@ function startLoopbackServerForPreparedFlow(
|
|
|
617
617
|
server.close();
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
server.listen(loopbackPort ?? 0, "
|
|
620
|
+
server.listen(loopbackPort ?? 0, "localhost", () => {
|
|
621
621
|
const addr = server.address() as { port: number };
|
|
622
|
-
const redirectUri = `http://
|
|
622
|
+
const redirectUri = `http://localhost:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
|
|
623
623
|
listening = true;
|
|
624
624
|
resolveSetup({ redirectUri, codePromise });
|
|
625
625
|
});
|
|
@@ -37,8 +37,8 @@ function getBroker(): KeychainBrokerClient {
|
|
|
37
37
|
*
|
|
38
38
|
* @deprecated Use `getSecureKeyAsync` instead. This sync variant only reads
|
|
39
39
|
* from the encrypted file store, bypassing the keychain broker. Retained only
|
|
40
|
-
* for
|
|
41
|
-
* that cannot do async I/O.
|
|
40
|
+
* for sync code paths in `providers/registry.ts`, `providers/managed-proxy/context.ts`,
|
|
41
|
+
* and `memory/embedding-backend.ts` that cannot do async I/O.
|
|
42
42
|
*/
|
|
43
43
|
export function getSecureKey(account: string): string | undefined {
|
|
44
44
|
return encryptedStore.getKey(account);
|
|
@@ -50,8 +50,8 @@ export function getSecureKey(account: string): string | undefined {
|
|
|
50
50
|
*
|
|
51
51
|
* @deprecated Use `setSecureKeyAsync` instead. This sync variant only writes
|
|
52
52
|
* to the encrypted file store, bypassing the keychain broker. Retained only
|
|
53
|
-
* for
|
|
54
|
-
* that cannot do async I/O.
|
|
53
|
+
* for sync code paths in `providers/registry.ts`, `providers/managed-proxy/context.ts`,
|
|
54
|
+
* and `memory/embedding-backend.ts` that cannot do async I/O.
|
|
55
55
|
*/
|
|
56
56
|
export function setSecureKey(account: string, value: string): boolean {
|
|
57
57
|
return encryptedStore.setKey(account, value);
|