@vellumai/assistant 0.4.48 → 0.4.49

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.
Files changed (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -35,18 +35,23 @@ import {
35
35
  } from "../events/tool-profiling-listener.js";
36
36
  import { registerToolTraceListener } from "../events/tool-trace-listener.js";
37
37
  import { getHookManager } from "../hooks/manager.js";
38
+ import { resolveCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
38
39
  import { PermissionPrompter } from "../permissions/prompter.js";
39
40
  import { SecretPrompter } from "../permissions/secret-prompter.js";
41
+ import { patternMatchesCandidate } from "../permissions/trust-store.js";
40
42
  import type { UserDecision } from "../permissions/types.js";
41
43
  import { buildSystemPrompt } from "../prompts/system-prompt.js";
42
44
  import type { Message } from "../providers/types.js";
43
45
  import type { Provider } from "../providers/types.js";
44
46
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
45
47
  import type { AuthContext } from "../runtime/auth/types.js";
48
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
46
49
  import * as approvalOverrides from "../runtime/session-approval-overrides.js";
47
50
  import { ToolExecutor } from "../tools/executor.js";
48
51
  import type { AssistantAttachmentDraft } from "./assistant-attachments.js";
49
52
  import { HostBashProxy } from "./host-bash-proxy.js";
53
+ import type { CuObservationResult } from "./host-cu-proxy.js";
54
+ import { HostCuProxy } from "./host-cu-proxy.js";
50
55
  import { HostFileProxy } from "./host-file-proxy.js";
51
56
  import type {
52
57
  ServerMessage,
@@ -161,6 +166,7 @@ export class Session {
161
166
  /** @internal */ taskRunId?: string;
162
167
  /** @internal */ callSessionId?: string;
163
168
  /** @internal */ hostBashProxy?: HostBashProxy;
169
+ /** @internal */ hostCuProxy?: HostCuProxy;
164
170
  /** @internal */ hostFileProxy?: HostFileProxy;
165
171
  /** @internal */ readonly queue = new MessageQueue();
166
172
  /** @internal */ currentActiveSurfaceId?: string;
@@ -199,10 +205,6 @@ export class Session {
199
205
  actions?: Array<{ id: string; label: string; style?: string }>;
200
206
  display?: string;
201
207
  }> = [];
202
- /** @internal */ onEscalateToComputerUse?: (
203
- task: string,
204
- sourceSessionId: string,
205
- ) => boolean;
206
208
  /** @internal */ workspaceTopLevelContext: string | null = null;
207
209
  /** @internal */ workspaceTopLevelDirty = true;
208
210
  public readonly traceEmitter: TraceEmitter;
@@ -346,7 +348,7 @@ export class Session {
346
348
  );
347
349
  this.contextWindowManager = new ContextWindowManager({
348
350
  provider,
349
- systemPrompt,
351
+ systemPrompt: () => resolveSystemPromptCallback([]).systemPrompt,
350
352
  config: config.contextWindow,
351
353
  });
352
354
 
@@ -388,6 +390,7 @@ export class Session {
388
390
  this.traceEmitter.updateSender(sendToClient);
389
391
  if (!opts?.skipProxySenderUpdate) {
390
392
  this.hostBashProxy?.updateSender(sendToClient, !hasNoClient);
393
+ this.hostCuProxy?.updateSender(sendToClient, !hasNoClient);
391
394
  this.hostFileProxy?.updateSender(sendToClient, !hasNoClient);
392
395
  }
393
396
  }
@@ -400,6 +403,7 @@ export class Session {
400
403
  /** Mark host proxies as unavailable so tool execution uses local fallback. */
401
404
  clearProxyAvailability(): void {
402
405
  this.hostBashProxy?.updateSender(this.sendToClient, false);
406
+ this.hostCuProxy?.updateSender(this.sendToClient, false);
403
407
  this.hostFileProxy?.updateSender(this.sendToClient, false);
404
408
  }
405
409
 
@@ -407,6 +411,7 @@ export class Session {
407
411
  restoreProxyAvailability(): void {
408
412
  if (!this.hasNoClient) {
409
413
  this.hostBashProxy?.updateSender(this.sendToClient, true);
414
+ this.hostCuProxy?.updateSender(this.sendToClient, true);
410
415
  this.hostFileProxy?.updateSender(this.sendToClient, true);
411
416
  }
412
417
  }
@@ -415,16 +420,6 @@ export class Session {
415
420
  this.sandboxOverride = enabled;
416
421
  }
417
422
 
418
- setEscalationHandler(
419
- handler: (task: string, sourceSessionId: string) => boolean,
420
- ): void {
421
- this.onEscalateToComputerUse = handler;
422
- }
423
-
424
- hasEscalationHandler(): boolean {
425
- return this.onEscalateToComputerUse !== undefined;
426
- }
427
-
428
423
  isProcessing(): boolean {
429
424
  return this.processing;
430
425
  }
@@ -447,6 +442,7 @@ export class Session {
447
442
  dispose(): void {
448
443
  approvalOverrides.clearMode(this.conversationId);
449
444
  this.hostBashProxy?.dispose();
445
+ this.hostCuProxy?.dispose();
450
446
  this.hostFileProxy?.dispose();
451
447
  disposeSession(this);
452
448
  }
@@ -589,6 +585,122 @@ export class Session {
589
585
  undefined,
590
586
  "Resuming after approval",
591
587
  );
588
+
589
+ // Cascade to other pending confirmations that match this decision
590
+ this.cascadePendingApprovals(requestId, decision, selectedPattern);
591
+ }
592
+
593
+ /**
594
+ * After resolving one confirmation, auto-resolve other pending
595
+ * confirmations in the same conversation that match the decision.
596
+ *
597
+ * - allow_10m / allow_thread → approve ALL pending in conversation
598
+ * - always_allow / always_allow_high_risk → approve pattern-matching pending
599
+ * - always_deny → deny pattern-matching pending
600
+ * - allow / deny (one-time) → no cascading
601
+ */
602
+ private cascadePendingApprovals(
603
+ primaryRequestId: string,
604
+ decision: UserDecision,
605
+ selectedPattern?: string,
606
+ ): void {
607
+ // Single-action decisions don't cascade
608
+ if (decision === "allow" || decision === "deny") return;
609
+
610
+ const pendingRequestIds = this.prompter.getPendingRequestIds();
611
+ if (pendingRequestIds.length === 0) return;
612
+
613
+ for (const candidateId of pendingRequestIds) {
614
+ if (candidateId === primaryRequestId) continue;
615
+
616
+ const interaction = pendingInteractions.get(candidateId);
617
+ if (!interaction) continue;
618
+ if (interaction.conversationId !== this.conversationId) continue;
619
+ if (interaction.kind !== "confirmation") continue;
620
+
621
+ const cascadeResult = this.shouldCascade(
622
+ decision,
623
+ selectedPattern,
624
+ interaction.confirmationDetails,
625
+ );
626
+ if (!cascadeResult) continue;
627
+
628
+ // Consume from pending-interactions tracker
629
+ pendingInteractions.resolve(candidateId);
630
+
631
+ // Resolve via handleConfirmationResponse which emits events.
632
+ // Use simple "allow"/"deny" so the permission-checker won't save
633
+ // duplicate rules or re-activate temporary modes. Recursion
634
+ // terminates because allow/deny exit cascadePendingApprovals early.
635
+ this.handleConfirmationResponse(
636
+ candidateId,
637
+ cascadeResult.allow ? "allow" : "deny",
638
+ undefined,
639
+ undefined,
640
+ undefined,
641
+ {
642
+ source: "system",
643
+ causedByRequestId: primaryRequestId,
644
+ },
645
+ );
646
+
647
+ // Sync the canonical guardian request status for the cascaded request.
648
+ // Best-effort: canonical request tracking should not break the cascade flow.
649
+ try {
650
+ const targetStatus = cascadeResult.allow ? "approved" : "denied";
651
+ resolveCanonicalGuardianRequest(candidateId, "pending", {
652
+ status: targetStatus,
653
+ });
654
+ } catch {
655
+ // Ignore — canonical request tracking is best-effort
656
+ }
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Determine whether a pending confirmation should be auto-resolved
662
+ * based on the cascading decision and pattern.
663
+ */
664
+ private shouldCascade(
665
+ decision: UserDecision,
666
+ selectedPattern: string | undefined,
667
+ details?: import("../runtime/pending-interactions.js").ConfirmationDetails,
668
+ ): { allow: boolean } | null {
669
+ // Temporary overrides apply to the entire conversation
670
+ if (decision === "allow_10m" || decision === "allow_thread") {
671
+ return { allow: true };
672
+ }
673
+
674
+ // Persistent allow: cascade if the pattern matches any allowlist candidate.
675
+ // "always_allow" must NOT cascade to high-risk pending confirmations —
676
+ // only "always_allow_high_risk" has consent for those.
677
+ if (
678
+ (decision === "always_allow" || decision === "always_allow_high_risk") &&
679
+ selectedPattern &&
680
+ details
681
+ ) {
682
+ if (decision === "always_allow" && details.riskLevel === "high") {
683
+ return null;
684
+ }
685
+ for (const option of details.allowlistOptions) {
686
+ if (patternMatchesCandidate(selectedPattern, option.pattern)) {
687
+ return { allow: true };
688
+ }
689
+ }
690
+ return null;
691
+ }
692
+
693
+ // Persistent deny: cascade denial if the pattern matches
694
+ if (decision === "always_deny" && selectedPattern && details) {
695
+ for (const option of details.allowlistOptions) {
696
+ if (patternMatchesCandidate(selectedPattern, option.pattern)) {
697
+ return { allow: false };
698
+ }
699
+ }
700
+ return null;
701
+ }
702
+
703
+ return null;
592
704
  }
593
705
 
594
706
  handleSecretResponse(
@@ -632,6 +744,17 @@ export class Session {
632
744
  this.hostFileProxy = proxy;
633
745
  }
634
746
 
747
+ resolveHostCu(requestId: string, observation: CuObservationResult): void {
748
+ this.hostCuProxy?.resolve(requestId, observation);
749
+ }
750
+
751
+ setHostCuProxy(proxy: HostCuProxy | undefined): void {
752
+ if (this.hostCuProxy && this.hostCuProxy !== proxy) {
753
+ this.hostCuProxy.dispose();
754
+ }
755
+ this.hostCuProxy = proxy;
756
+ }
757
+
635
758
  // ── Server-authoritative state signals ─────────────────────────────
636
759
 
637
760
  emitConfirmationStateChanged(
@@ -220,9 +220,7 @@ registerHook(
220
220
  const SETTING_TO_KEY: Record<string, string> = {
221
221
  activation_key: "pttActivationKey",
222
222
  tts_voice_id: "ttsVoiceId",
223
- wake_word_enabled: "wakeWordEnabled",
224
- wake_word_keyword: "wakeWordKeyword",
225
- wake_word_timeout: "wakeWordTimeoutSeconds",
223
+ conversation_timeout: "voiceConversationTimeoutSeconds",
226
224
  };
227
225
  const key = SETTING_TO_KEY[setting];
228
226
  if (!key) return;
@@ -231,12 +229,8 @@ registerHook(
231
229
  // the validation logic in the tool's execute method.
232
230
  const raw = input.value;
233
231
  let coerced: string | boolean | number = raw as string;
234
- if (setting === "wake_word_enabled") {
235
- coerced = raw === true || raw === "true";
236
- } else if (setting === "wake_word_timeout") {
232
+ if (setting === "conversation_timeout") {
237
233
  coerced = typeof raw === "number" ? raw : Number(raw);
238
- } else if (setting === "wake_word_keyword" && typeof raw === "string") {
239
- coerced = raw.trim();
240
234
  } else if (setting === "tts_voice_id" && typeof raw === "string") {
241
235
  coerced = raw.trim();
242
236
  }
@@ -79,8 +79,8 @@ export async function handleWatchObservation(
79
79
  "Observation added to session",
80
80
  );
81
81
 
82
- // 4. Every 3 observations: call the LLM for live commentary (chat-initiated watch only)
83
- if (!session.isRideShotgun && session.observations.length % 3 === 0) {
82
+ // 4. Every 3 observations: call the LLM for live commentary
83
+ if (session.observations.length % 3 === 0) {
84
84
  log.debug(
85
85
  { watchId: msg.watchId, observationCount: session.observations.length },
86
86
  "Triggering commentary generation (every 3rd observation)",
@@ -1,4 +1,4 @@
1
- import { getLogger, isDebug, truncateForLog } from "../util/logger.js";
1
+ import { getLogger, truncateForLog } from "../util/logger.js";
2
2
  import type { EventBus, Subscription } from "./bus.js";
3
3
  import type { AssistantDomainEvents } from "./domain-events.js";
4
4
 
@@ -23,7 +23,7 @@ export function registerToolMetricsLoggingListener(
23
23
  options?: MetricsListenerOptions,
24
24
  ): Subscription {
25
25
  const logger = options?.logger ?? defaultLogger;
26
- const debugEnabled = options?.debugEnabled ?? isDebug;
26
+ const debugEnabled = options?.debugEnabled ?? (() => false);
27
27
  const truncate = options?.truncate ?? truncateForLog;
28
28
 
29
29
  return eventBus.onAny((event) => {
@@ -2,7 +2,7 @@ import { type FSWatcher, watch } from "node:fs";
2
2
 
3
3
  import { Debouncer } from "../util/debounce.js";
4
4
  import { pathExists } from "../util/fs.js";
5
- import { getLogger, isDebug } from "../util/logger.js";
5
+ import { getLogger } from "../util/logger.js";
6
6
  import { getHooksDir } from "../util/platform.js";
7
7
  import { discoverHooks } from "./discovery.js";
8
8
  import { runHookScript } from "./runner.js";
@@ -73,9 +73,6 @@ export class HookManager {
73
73
  "Hook exited with non-zero code",
74
74
  );
75
75
  }
76
- if (result.stderr && isDebug()) {
77
- process.stderr.write(result.stderr);
78
- }
79
76
  } catch (err) {
80
77
  log.warn({ err, hook: hook.name, event }, "Hook execution failed");
81
78
  }
@@ -8,13 +8,13 @@
8
8
  * 1. **User Settings** (`config.ingress.publicBaseUrl`) — set via
9
9
  * the in-chat config flow, the Settings UI, or `config set ingress.publicBaseUrl`. This is the
10
10
  * primary source of truth. When the assistant spawns or restarts
11
- * the gateway, this value is forwarded as the `INGRESS_PUBLIC_BASE_URL`
12
- * environment variable so both processes agree on the same URL.
11
+ * the gateway, the workspace config file is read so both processes
12
+ * agree on the same URL.
13
13
  *
14
- * 2. **Environment variable** (`INGRESS_PUBLIC_BASE_URL`) — serves as a
15
- * fallback for operational use (e.g. direct gateway-only deployments
16
- * without the assistant, or CI overrides). When the assistant is
17
- * managing the gateway, the env var is set automatically from (1).
14
+ * 2. **Module-level state** (`getIngressPublicBaseUrl()`) — serves as a
15
+ * fallback for operational use (e.g. runtime tunnel updates). When
16
+ * tunnels start or stop, `setIngressPublicBaseUrl()` updates this
17
+ * value in-process.
18
18
  *
19
19
  * This chain ensures that:
20
20
  * - The assistant's outbound callback URLs (Twilio webhooks, OAuth
@@ -70,7 +70,7 @@ export function getPublicBaseUrl(config: IngressConfig): string {
70
70
  }
71
71
 
72
72
  throw new Error(
73
- "No public base URL configured. Set ingress.publicBaseUrl in config or INGRESS_PUBLIC_BASE_URL env var.",
73
+ "No public base URL configured. Set ingress.publicBaseUrl in config.",
74
74
  );
75
75
  }
76
76
 
package/src/logfire.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getLogfireToken, isMonitoringEnabled } from "./config/env.js";
1
+ import { getLogfireToken } from "./config/env.js";
2
2
  import type {
3
3
  Message,
4
4
  Provider,
@@ -13,7 +13,7 @@ const log = getLogger("logfire");
13
13
 
14
14
  type LogfireModule = typeof import("@pydantic/logfire-node");
15
15
 
16
- const LOGFIRE_ENABLED: boolean = !!getLogfireToken() && isMonitoringEnabled();
16
+ let logfireEnabled: boolean = !!getLogfireToken();
17
17
 
18
18
  let logfireInstance: LogfireModule | null = null;
19
19
 
@@ -23,7 +23,7 @@ let logfireInstance: LogfireModule | null = null;
23
23
  * Non-fatal on failure (logs warning and continues).
24
24
  */
25
25
  export async function initLogfire(): Promise<void> {
26
- if (!LOGFIRE_ENABLED) return;
26
+ if (!logfireEnabled) return;
27
27
 
28
28
  try {
29
29
  const logfire = await import("@pydantic/logfire-node");
@@ -42,12 +42,23 @@ export async function initLogfire(): Promise<void> {
42
42
  }
43
43
  }
44
44
 
45
+ /**
46
+ * Disable Logfire after early initialization. Called when the user has opted
47
+ * out via the feature flag. Nulls out the instance so future wrapWithLogfire
48
+ * calls become no-ops.
49
+ */
50
+ export function disableLogfire(): void {
51
+ logfireEnabled = false;
52
+ logfireInstance = null;
53
+ log.info("Logfire disabled by feature flag");
54
+ }
55
+
45
56
  /**
46
57
  * Wraps a provider with Logfire tracing spans.
47
- * When LOGFIRE_ENABLED is false, returns the provider as-is (no wrapper allocated).
58
+ * When logfireEnabled is false, returns the provider as-is (no wrapper allocated).
48
59
  */
49
60
  export function wrapWithLogfire(provider: Provider): Provider {
50
- if (!LOGFIRE_ENABLED) return provider;
61
+ if (!logfireEnabled) return provider;
51
62
  return new LogfireProvider(provider);
52
63
  }
53
64
 
@@ -117,6 +117,27 @@ export function getOrCreateConversation(conversationKey: string): {
117
117
  return { conversationId: existing.conversationId, created: false };
118
118
  }
119
119
 
120
+ // Check if the conversationKey itself is an existing conversation ID.
121
+ // This happens when the client loads a thread from the conversations list
122
+ // and uses the server's conversationId as its local sessionId / conversationKey.
123
+ const existingConversation = tx
124
+ .select({ id: conversations.id })
125
+ .from(conversations)
126
+ .where(eq(conversations.id, conversationKey))
127
+ .get();
128
+
129
+ if (existingConversation) {
130
+ tx.insert(conversationKeys)
131
+ .values({
132
+ id: uuid(),
133
+ conversationKey,
134
+ conversationId: existingConversation.id,
135
+ createdAt: Date.now(),
136
+ })
137
+ .run();
138
+ return { conversationId: existingConversation.id, created: false };
139
+ }
140
+
120
141
  const now = Date.now();
121
142
  const conversationId = uuid();
122
143
 
@@ -28,6 +28,7 @@ import {
28
28
  createMediaAssetsTables,
29
29
  createMessagesFts,
30
30
  createNotificationTables,
31
+ createOAuthTables,
31
32
  createScopedApprovalGrantsTable,
32
33
  createSequenceTables,
33
34
  createTasksAndWorkItemsTables,
@@ -340,6 +341,9 @@ export function initializeDb(): void {
340
341
  // 52. Drop the legacy reminders table after data migration
341
342
  migrateDropRemindersTable(database);
342
343
 
344
+ // 53. OAuth provider/app/connection tables
345
+ createOAuthTables(database);
346
+
343
347
  validateMigrationState(database);
344
348
 
345
349
  if (process.env.BUN_TEST === "1") {
@@ -0,0 +1,60 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+
3
+ /**
4
+ * OAuth provider, app, and connection tables.
5
+ * Creates tables in FK-dependency order: providers → apps → connections.
6
+ */
7
+ export function createOAuthTables(database: DrizzleDb): void {
8
+ database.run(/*sql*/ `
9
+ CREATE TABLE IF NOT EXISTS oauth_providers (
10
+ provider_key TEXT PRIMARY KEY,
11
+ auth_url TEXT NOT NULL,
12
+ token_url TEXT NOT NULL,
13
+ token_endpoint_auth_method TEXT,
14
+ userinfo_url TEXT,
15
+ base_url TEXT,
16
+ default_scopes TEXT NOT NULL DEFAULT '[]',
17
+ scope_policy TEXT NOT NULL DEFAULT '{}',
18
+ extra_params TEXT,
19
+ callback_transport TEXT,
20
+ loopback_port INTEGER,
21
+ created_at INTEGER NOT NULL,
22
+ updated_at INTEGER NOT NULL
23
+ )
24
+ `);
25
+
26
+ database.run(/*sql*/ `
27
+ CREATE TABLE IF NOT EXISTS oauth_apps (
28
+ id TEXT PRIMARY KEY,
29
+ provider_key TEXT NOT NULL REFERENCES oauth_providers(provider_key),
30
+ client_id TEXT NOT NULL,
31
+ created_at INTEGER NOT NULL,
32
+ updated_at INTEGER NOT NULL
33
+ )
34
+ `);
35
+
36
+ database.run(/*sql*/ `
37
+ CREATE TABLE IF NOT EXISTS oauth_connections (
38
+ id TEXT PRIMARY KEY,
39
+ oauth_app_id TEXT NOT NULL REFERENCES oauth_apps(id),
40
+ provider_key TEXT NOT NULL,
41
+ account_info TEXT,
42
+ granted_scopes TEXT NOT NULL DEFAULT '[]',
43
+ expires_at INTEGER,
44
+ has_refresh_token INTEGER NOT NULL DEFAULT 0,
45
+ status TEXT NOT NULL DEFAULT 'active',
46
+ label TEXT,
47
+ metadata TEXT,
48
+ created_at INTEGER NOT NULL,
49
+ updated_at INTEGER NOT NULL
50
+ )
51
+ `);
52
+
53
+ database.run(
54
+ /*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_apps_provider_client ON oauth_apps(provider_key, client_id)`,
55
+ );
56
+
57
+ database.run(
58
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_oauth_connections_provider_key ON oauth_connections(provider_key)`,
59
+ );
60
+ }
@@ -90,6 +90,7 @@ export { migrateDropAccountsTable } from "./145-drop-accounts-table.js";
90
90
  export { migrateScheduleOneShotRouting } from "./146-schedule-oneshot-routing.js";
91
91
  export { migrateRemindersToSchedules } from "./147-migrate-reminders-to-schedules.js";
92
92
  export { migrateDropRemindersTable } from "./148-drop-reminders-table.js";
93
+ export { createOAuthTables } from "./149-oauth-tables.js";
93
94
  export {
94
95
  MIGRATION_REGISTRY,
95
96
  type MigrationRegistryEntry,
@@ -5,4 +5,5 @@ export * from "./guardian.js";
5
5
  export * from "./infrastructure.js";
6
6
  export * from "./memory-core.js";
7
7
  export * from "./notifications.js";
8
+ export * from "./oauth.js";
8
9
  export * from "./tasks.js";
@@ -0,0 +1,65 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ sqliteTable,
5
+ text,
6
+ uniqueIndex,
7
+ } from "drizzle-orm/sqlite-core";
8
+
9
+ export const oauthProviders = sqliteTable("oauth_providers", {
10
+ providerKey: text("provider_key").primaryKey(),
11
+ authUrl: text("auth_url").notNull(),
12
+ tokenUrl: text("token_url").notNull(),
13
+ tokenEndpointAuthMethod: text("token_endpoint_auth_method"),
14
+ userinfoUrl: text("userinfo_url"),
15
+ baseUrl: text("base_url"),
16
+ defaultScopes: text("default_scopes").notNull().default("[]"),
17
+ scopePolicy: text("scope_policy").notNull().default("{}"),
18
+ extraParams: text("extra_params"),
19
+ callbackTransport: text("callback_transport"),
20
+ loopbackPort: integer("loopback_port"),
21
+ createdAt: integer("created_at").notNull(),
22
+ updatedAt: integer("updated_at").notNull(),
23
+ });
24
+
25
+ export const oauthApps = sqliteTable(
26
+ "oauth_apps",
27
+ {
28
+ id: text("id").primaryKey(),
29
+ providerKey: text("provider_key")
30
+ .notNull()
31
+ .references(() => oauthProviders.providerKey),
32
+ clientId: text("client_id").notNull(),
33
+ createdAt: integer("created_at").notNull(),
34
+ updatedAt: integer("updated_at").notNull(),
35
+ },
36
+ (table) => [
37
+ uniqueIndex("idx_oauth_apps_provider_client").on(
38
+ table.providerKey,
39
+ table.clientId,
40
+ ),
41
+ ],
42
+ );
43
+
44
+ export const oauthConnections = sqliteTable(
45
+ "oauth_connections",
46
+ {
47
+ id: text("id").primaryKey(),
48
+ oauthAppId: text("oauth_app_id")
49
+ .notNull()
50
+ .references(() => oauthApps.id),
51
+ providerKey: text("provider_key").notNull(),
52
+ accountInfo: text("account_info"),
53
+ grantedScopes: text("granted_scopes").notNull().default("[]"),
54
+ expiresAt: integer("expires_at"),
55
+ hasRefreshToken: integer("has_refresh_token").notNull().default(0),
56
+ status: text("status").notNull().default("active"),
57
+ label: text("label"),
58
+ metadata: text("metadata"),
59
+ createdAt: integer("created_at").notNull(),
60
+ updatedAt: integer("updated_at").notNull(),
61
+ },
62
+ (table) => [
63
+ index("idx_oauth_connections_provider_key").on(table.providerKey),
64
+ ],
65
+ );
@@ -82,10 +82,10 @@ export interface MessagingProvider {
82
82
 
83
83
  /**
84
84
  * Override the default credential check used by getConnectedProviders().
85
- * When present, the registry calls this instead of looking for
86
- * credential/{credentialService}/access_token. Useful for providers
87
- * that don't use OAuth (e.g. Telegram bot tokens stored under a
88
- * non-standard key).
85
+ * When present, the registry calls this instead of checking for an
86
+ * active oauth-store connection via isProviderConnected(). Useful
87
+ * for providers that don't use OAuth (e.g. Telegram bot tokens stored
88
+ * under a non-standard key).
89
89
  */
90
90
  isConnected?(): boolean;
91
91