wotann 0.5.97 → 0.5.98

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 (214) hide show
  1. package/README.md +18 -38
  2. package/dist/api/anthropic-gateway.d.ts +4 -5
  3. package/dist/api/anthropic-gateway.js +4 -5
  4. package/dist/api/server.js +4 -2
  5. package/dist/api/ws-hardening.d.ts +2 -2
  6. package/dist/api/ws-hardening.js +2 -2
  7. package/dist/auth/login.d.ts +1 -2
  8. package/dist/auth/login.js +3 -44
  9. package/dist/browser/agentic-browser.d.ts +14 -0
  10. package/dist/browser/agentic-browser.js +150 -3
  11. package/dist/browser/browser-tools.d.ts +6 -0
  12. package/dist/browser/browser-tools.js +57 -6
  13. package/dist/channels/gateway.d.ts +16 -1
  14. package/dist/channels/gateway.js +46 -0
  15. package/dist/channels/unified-dispatch.d.ts +5 -1
  16. package/dist/channels/unified-dispatch.js +21 -9
  17. package/dist/cli/cli-detection.js +0 -8
  18. package/dist/cli/commands/browse.d.ts +4 -4
  19. package/dist/cli/commands/browse.js +17 -5
  20. package/dist/cli/commands/computer-use.d.ts +1 -1
  21. package/dist/cli/commands/computer-use.js +7 -42
  22. package/dist/cli/commands.js +8 -38
  23. package/dist/cli/first-run-runner-factory.d.ts +2 -3
  24. package/dist/cli/first-run-runner-factory.js +6 -3
  25. package/dist/cli/onboarding-screens/done-screen.js +1 -1
  26. package/dist/cli/onboarding-screens.d.ts +4 -7
  27. package/dist/cli/onboarding-screens.js +29 -35
  28. package/dist/cli/orphan-wires/arena-cmd.d.ts +14 -3
  29. package/dist/cli/orphan-wires/arena-cmd.js +12 -10
  30. package/dist/cli/orphan-wires/harness-introspect-cmd.d.ts +1 -1
  31. package/dist/cli/orphan-wires/harness-introspect-cmd.js +2 -2
  32. package/dist/cli/orphan-wires/redteam-scan-cmd.d.ts +13 -5
  33. package/dist/cli/orphan-wires/redteam-scan-cmd.js +9 -12
  34. package/dist/cli/run-onboarding-wizard.js +9 -16
  35. package/dist/cli/runtime-query.d.ts +1 -1
  36. package/dist/cli/runtime-query.js +5 -1
  37. package/dist/cli/test-provider.js +1 -4
  38. package/dist/cli/voice-cmds.d.ts +6 -8
  39. package/dist/cli/voice-cmds.js +10 -24
  40. package/dist/computer-use/computer-agent.d.ts +14 -0
  41. package/dist/computer-use/computer-agent.js +59 -2
  42. package/dist/computer-use/perception-engine.d.ts +4 -1
  43. package/dist/computer-use/perception-engine.js +11 -3
  44. package/dist/context/limits.d.ts +2 -4
  45. package/dist/context/limits.js +17 -29
  46. package/dist/context/maximizer.d.ts +0 -1
  47. package/dist/context/maximizer.js +11 -22
  48. package/dist/context/window-intelligence.js +1 -1
  49. package/dist/core/agent-bridge.d.ts +7 -6
  50. package/dist/core/agent-bridge.js +31 -10
  51. package/dist/core/agent-tool-context.d.ts +4 -0
  52. package/dist/core/agent-tool-context.js +12 -1
  53. package/dist/core/config-discovery.d.ts +4 -4
  54. package/dist/core/config-discovery.js +11 -12
  55. package/dist/core/config.d.ts +1 -5
  56. package/dist/core/config.js +4 -16
  57. package/dist/core/default-provider.js +6 -3
  58. package/dist/core/effort-mode.d.ts +4 -5
  59. package/dist/core/effort-mode.js +31 -14
  60. package/dist/core/hardware-detect.d.ts +3 -11
  61. package/dist/core/hardware-detect.js +11 -22
  62. package/dist/core/prompt-override.js +17 -4
  63. package/dist/core/runtime.d.ts +31 -5
  64. package/dist/core/runtime.js +262 -37
  65. package/dist/core/schema-migration.js +9 -3
  66. package/dist/core/types.d.ts +14 -3
  67. package/dist/core/workspace.js +7 -26
  68. package/dist/daemon/auto-update.d.ts +6 -11
  69. package/dist/daemon/auto-update.js +12 -148
  70. package/dist/daemon/kairos-rpc.d.ts +4 -1
  71. package/dist/daemon/kairos-rpc.js +436 -268
  72. package/dist/daemon/kairos-tools.d.ts +3 -3
  73. package/dist/daemon/kairos-tools.js +3 -5
  74. package/dist/daemon/kairos.js +35 -3
  75. package/dist/daemon/rpc-handlers/sandbox-rpc.d.ts +12 -3
  76. package/dist/daemon/rpc-handlers/sandbox-rpc.js +64 -38
  77. package/dist/daemon/start.js +0 -9
  78. package/dist/desktop/companion-server.d.ts +1 -0
  79. package/dist/desktop/companion-server.js +136 -113
  80. package/dist/index.js +70 -144
  81. package/dist/intelligence/harness-introspect.d.ts +2 -2
  82. package/dist/intelligence/harness-introspect.js +5 -12
  83. package/dist/intelligence/provider-arbitrage.js +16 -26
  84. package/dist/intelligence/task-semantic-router.js +12 -11
  85. package/dist/lib.d.ts +0 -7
  86. package/dist/lib.js +0 -13
  87. package/dist/memory/cross-encoder.d.ts +3 -3
  88. package/dist/memory/cross-encoder.js +3 -3
  89. package/dist/memory/local-embedding.d.ts +2 -2
  90. package/dist/memory/local-embedding.js +4 -105
  91. package/dist/memory/onnx-cross-encoder.d.ts +17 -23
  92. package/dist/memory/onnx-cross-encoder.js +19 -49
  93. package/dist/memory/store.d.ts +3 -3
  94. package/dist/memory/store.js +9 -23
  95. package/dist/middleware/layers.d.ts +2 -2
  96. package/dist/middleware/layers.js +5 -4
  97. package/dist/mobile/ios-app.d.ts +3 -2
  98. package/dist/mobile/ios-app.js +11 -6
  99. package/dist/orchestration/agent-registry.d.ts +2 -2
  100. package/dist/orchestration/agent-registry.js +16 -14
  101. package/dist/orchestration/architect-editor.js +6 -10
  102. package/dist/orchestration/council.js +10 -1
  103. package/dist/orchestration/proof-bundles.d.ts +4 -3
  104. package/dist/plugins/service-manifest.d.ts +1 -1
  105. package/dist/providers/adapter-registry.js +10 -11
  106. package/dist/providers/capability-equalizer.js +6 -32
  107. package/dist/providers/cli-registry.js +0 -21
  108. package/dist/providers/discovery.d.ts +0 -70
  109. package/dist/providers/discovery.js +3 -211
  110. package/dist/providers/dynamic-discovery.d.ts +0 -2
  111. package/dist/providers/dynamic-discovery.js +0 -45
  112. package/dist/providers/fallback-chain.d.ts +6 -11
  113. package/dist/providers/fallback-chain.js +30 -27
  114. package/dist/providers/model-defaults.js +4 -1
  115. package/dist/providers/model-discovery.d.ts +2 -1
  116. package/dist/providers/model-discovery.js +10 -29
  117. package/dist/providers/model-router.d.ts +1 -10
  118. package/dist/providers/model-router.js +19 -63
  119. package/dist/providers/model-switcher.js +18 -1
  120. package/dist/providers/onboarding-auth-choices.js +3 -0
  121. package/dist/providers/openai-compat-adapter.js +1 -1
  122. package/dist/providers/preset-library.d.ts +1 -1
  123. package/dist/providers/preset-library.js +12 -5
  124. package/dist/providers/provider-brain.js +5 -2
  125. package/dist/providers/provider-ladder.d.ts +10 -16
  126. package/dist/providers/provider-ladder.js +19 -16
  127. package/dist/providers/provider-service.js +0 -41
  128. package/dist/providers/public-tier/fallback-chain.js +4 -7
  129. package/dist/providers/rate-limiter.d.ts +5 -5
  130. package/dist/providers/rate-limiter.js +17 -8
  131. package/dist/providers/registry.d.ts +1 -1
  132. package/dist/providers/registry.js +7 -50
  133. package/dist/providers/smart-routing.js +6 -2
  134. package/dist/providers/state-aware-failover.js +6 -1
  135. package/dist/providers/usage-intelligence.d.ts +3 -3
  136. package/dist/providers/usage-intelligence.js +4 -20
  137. package/dist/security/approval-queue-decision-broker.d.ts +29 -0
  138. package/dist/security/approval-queue-decision-broker.js +76 -0
  139. package/dist/security/file-freeze.js +3 -0
  140. package/dist/security/guardrails-off.d.ts +5 -11
  141. package/dist/security/guardrails-off.js +35 -116
  142. package/dist/security/hash-audit-chain.d.ts +5 -4
  143. package/dist/security/hash-audit-chain.js +8 -6
  144. package/dist/security/pii-scrubber.d.ts +3 -2
  145. package/dist/security/pii-scrubber.js +5 -13
  146. package/dist/security/privacy-router.d.ts +2 -2
  147. package/dist/security/privacy-router.js +14 -10
  148. package/dist/security/private-mode.d.ts +5 -6
  149. package/dist/security/private-mode.js +11 -13
  150. package/dist/security/provenance-label.d.ts +11 -0
  151. package/dist/security/provenance-label.js +29 -0
  152. package/dist/security/provenance-tracker.d.ts +42 -0
  153. package/dist/security/provenance-tracker.js +187 -0
  154. package/dist/security/taint-approval-coordinator.d.ts +49 -0
  155. package/dist/security/taint-approval-coordinator.js +148 -0
  156. package/dist/security/taint-receipt.d.ts +37 -0
  157. package/dist/security/taint-receipt.js +77 -0
  158. package/dist/security/taint-sink-gate.d.ts +22 -0
  159. package/dist/security/taint-sink-gate.js +189 -0
  160. package/dist/security/visual-taint.d.ts +17 -0
  161. package/dist/security/visual-taint.js +64 -0
  162. package/dist/session/approval-queue.d.ts +23 -2
  163. package/dist/session/approval-queue.js +84 -6
  164. package/dist/telemetry/cost-oracle.js +17 -10
  165. package/dist/tools/agent-tools.d.ts +5 -0
  166. package/dist/tools/agent-tools.js +275 -170
  167. package/dist/training/pipeline.d.ts +2 -2
  168. package/dist/training/pipeline.js +5 -13
  169. package/dist/ui/components/CommandPaletteCommands.js +3 -3
  170. package/dist/ui/components/ModelPicker.d.ts +1 -1
  171. package/dist/ui/components/ModelPicker.js +5 -2
  172. package/dist/ui/components/OnboardingWizard.d.ts +3 -3
  173. package/dist/ui/components/OnboardingWizard.js +4 -10
  174. package/dist/ui/components/ProviderSetupOverlay.js +3 -4
  175. package/dist/ui/components/StartupScreen.js +1 -1
  176. package/dist/ui/components/v3/AppV3.js +3 -0
  177. package/dist/ui/components/v3/OnboardingTour.d.ts +4 -3
  178. package/dist/ui/components/v3/OnboardingTour.js +10 -5
  179. package/dist/utils/atomic-io.d.ts +1 -0
  180. package/dist/utils/atomic-io.js +23 -0
  181. package/dist/utils/atomic-write.d.ts +2 -0
  182. package/dist/utils/atomic-write.js +17 -0
  183. package/dist/utils/bounded-base64.d.ts +2 -0
  184. package/dist/utils/bounded-base64.js +17 -0
  185. package/dist/utils/path-realpath.d.ts +12 -0
  186. package/dist/utils/path-realpath.js +38 -2
  187. package/dist/utils/sidecar-downloader.d.ts +2 -2
  188. package/dist/utils/sidecar-downloader.js +2 -20
  189. package/dist/utils/sqlite-kv-store.d.ts +1 -1
  190. package/dist/utils/sqlite-kv-store.js +4 -3
  191. package/dist/verification/reproduction/autonomous-gate.d.ts +5 -4
  192. package/dist/verification/reproduction/autonomous-gate.js +5 -4
  193. package/dist/verification/reproduction/checkout-prep.d.ts +6 -7
  194. package/dist/verification/reproduction/enforcement.d.ts +2 -0
  195. package/dist/verification/reproduction/enforcement.js +5 -1
  196. package/dist/verification/reproduction/index.d.ts +1 -1
  197. package/dist/verification/reproduction/index.js +1 -1
  198. package/dist/verification/reproduction/replay-runner.d.ts +4 -4
  199. package/dist/verification/reproduction/reproduce.d.ts +1 -1
  200. package/dist/verification/reproduction/verdict.d.ts +4 -2
  201. package/dist/verification/verifier-engine.js +1 -1
  202. package/dist/verification/verifier-model-selector.js +8 -16
  203. package/dist/voice/edge-tts-backend.d.ts +3 -9
  204. package/dist/voice/edge-tts-backend.js +8 -9
  205. package/dist/voice/stt-detector.d.ts +6 -13
  206. package/dist/voice/stt-detector.js +17 -128
  207. package/dist/voice/tts-engine.d.ts +9 -19
  208. package/dist/voice/tts-engine.js +36 -113
  209. package/dist/voice/vibevoice-backend.js +17 -1
  210. package/dist/voice/voice-mode.d.ts +3 -8
  211. package/dist/voice/voice-mode.js +14 -152
  212. package/dist/voice/voice-pipeline.d.ts +10 -12
  213. package/dist/voice/voice-pipeline.js +47 -245
  214. package/package.json +4 -4
@@ -143,6 +143,34 @@ async function pickBackend(dep) {
143
143
  function errEnv(error, detail) {
144
144
  return { ok: false, error, ...(detail !== undefined ? { detail } : {}) };
145
145
  }
146
+ function registerBrowserDomSource(dep, content) {
147
+ if (content.length === 0)
148
+ return;
149
+ dep.taint?.coordinator.registerSource({
150
+ kind: "browser-dom",
151
+ content,
152
+ integrity: "untrusted",
153
+ });
154
+ }
155
+ async function authorizeBrowserMutation(dep, tool, args) {
156
+ if (!dep.taint)
157
+ return errEnv("not_configured", "browser mutation taint policy is unavailable");
158
+ const sessionId = dep.taint.sessionId.trim();
159
+ if (!sessionId)
160
+ return errEnv("not_configured", "browser mutation taint session is unavailable");
161
+ const action = Object.freeze({
162
+ tool,
163
+ args: Object.freeze(structuredClone(args)),
164
+ cwd: dep.taint.cwd?.trim() || ".",
165
+ });
166
+ const authorization = await dep.taint.coordinator.authorize({ sessionId, action });
167
+ if (!authorization.authorized)
168
+ return errEnv("upstream_error", authorization.reason);
169
+ const verification = dep.taint.coordinator.verifyExecution({ authorization, action });
170
+ return verification.ok
171
+ ? null
172
+ : errEnv("upstream_error", `browser mutation execution verification failed: ${verification.reason}`);
173
+ }
146
174
  // ── Dispatcher ──────────────────────────────────────────────
147
175
  export async function dispatchBrowserTool(toolName, input, dep) {
148
176
  // Agentic trio — these don't need a page-bound backend; they
@@ -150,7 +178,7 @@ export async function dispatchBrowserTool(toolName, input, dep) {
150
178
  if (toolName === "browser.plan" ||
151
179
  toolName === "browser.spawn_tab" ||
152
180
  toolName === "browser.approve_action") {
153
- return dispatchAgenticTool(toolName, input, dep.agentic);
181
+ return dispatchAgenticTool(toolName, input, dep);
154
182
  }
155
183
  const backend = await pickBackend(dep);
156
184
  if (!backend) {
@@ -163,6 +191,9 @@ export async function dispatchBrowserTool(toolName, input, dep) {
163
191
  return errEnv("bad_input", "url required");
164
192
  if (!isSafeUrl(url))
165
193
  return errEnv("ssrf_blocked", url);
194
+ const taint = await authorizeBrowserMutation(dep, toolName, { url });
195
+ if (taint)
196
+ return taint;
166
197
  if (backend === "chrome" && dep.chrome) {
167
198
  const result = await dep.chrome.execute({ type: "navigate", url });
168
199
  return result.success
@@ -181,6 +212,9 @@ export async function dispatchBrowserTool(toolName, input, dep) {
181
212
  const selector = input["selector"];
182
213
  if (typeof selector !== "string" || selector.trim().length === 0)
183
214
  return errEnv("bad_input", "selector required");
215
+ const taint = await authorizeBrowserMutation(dep, toolName, { selector });
216
+ if (taint)
217
+ return taint;
184
218
  if (backend === "chrome" && dep.chrome) {
185
219
  const result = await dep.chrome.execute({ type: "click", selector });
186
220
  return result.success
@@ -200,6 +234,9 @@ export async function dispatchBrowserTool(toolName, input, dep) {
200
234
  const text = input["text"];
201
235
  if (typeof selector !== "string" || typeof text !== "string")
202
236
  return errEnv("bad_input", "selector + text required");
237
+ const taint = await authorizeBrowserMutation(dep, toolName, { selector, text });
238
+ if (taint)
239
+ return taint;
203
240
  if (backend === "chrome" && dep.chrome) {
204
241
  const result = await dep.chrome.execute({ type: "type", selector, value: text });
205
242
  return result.success
@@ -234,14 +271,20 @@ export async function dispatchBrowserTool(toolName, input, dep) {
234
271
  const result = await dep.chrome.execute({ type: "read_dom" });
235
272
  if (!result.success)
236
273
  return errEnv("upstream_error", result.error);
237
- const text = result.domTree ? dep.chrome.domToText(result.domTree) : (result.data ?? "");
274
+ const text = result.domTree
275
+ ? dep.chrome.domToText(result.domTree)
276
+ : typeof result.data === "string"
277
+ ? result.data
278
+ : "";
279
+ registerBrowserDomSource(dep, text);
238
280
  return { ok: true, data: { text, backend } };
239
281
  }
240
282
  if (backend === "camoufox" && dep.camoufox) {
241
283
  const result = await dep.camoufox.getText();
242
- return result.success
243
- ? { ok: true, data: { text: result.text, backend } }
244
- : errEnv("upstream_error", result.error);
284
+ if (!result.success)
285
+ return errEnv("upstream_error", result.error);
286
+ registerBrowserDomSource(dep, result.text);
287
+ return { ok: true, data: { text: result.text, backend } };
245
288
  }
246
289
  return errEnv("not_configured", "backend vanished");
247
290
  }
@@ -251,7 +294,8 @@ export async function dispatchBrowserTool(toolName, input, dep) {
251
294
  }
252
295
  }
253
296
  }
254
- async function dispatchAgenticTool(toolName, input, agentic) {
297
+ async function dispatchAgenticTool(toolName, input, dep) {
298
+ const agentic = dep.agentic;
255
299
  switch (toolName) {
256
300
  case "browser.plan": {
257
301
  const task = input["task"];
@@ -289,6 +333,13 @@ async function dispatchAgenticTool(toolName, input, agentic) {
289
333
  const taskId = typeof taskIdRaw === "string" && taskIdRaw.length > 0 ? taskIdRaw : "default";
290
334
  if (!agentic || !agentic.tabRegistry)
291
335
  return errEnv("not_configured", "tab-registry not wired");
336
+ const taint = await authorizeBrowserMutation(dep, toolName, {
337
+ ownership,
338
+ ...(url !== undefined ? { url } : {}),
339
+ taskId,
340
+ });
341
+ if (taint)
342
+ return taint;
292
343
  const tabId = agentic.spawnTabId?.(url) ?? `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
293
344
  const owner = ownership === "user" ? { kind: "user" } : { kind: "agent", taskId };
294
345
  const result = agentic.tabRegistry.register(url !== undefined ? { tabId, owner, url } : { tabId, owner });
@@ -65,7 +65,7 @@ export type AgentOutboundMessageResult = {
65
65
  readonly messageId: string;
66
66
  } | {
67
67
  readonly ok: false;
68
- readonly reason: "channel_not_allowed" | "adapter_not_connected" | "missing_recipient_for_pairing" | "pairing_required";
68
+ readonly reason: "channel_not_allowed" | "adapter_not_connected" | "missing_recipient_for_pairing" | "pairing_required" | "taint_denied";
69
69
  readonly pairingCode?: string;
70
70
  };
71
71
  export type AgentMessageToolSender = (request: {
@@ -102,10 +102,22 @@ export interface DeviceNode {
102
102
  }
103
103
  export interface GatewayConfig {
104
104
  readonly requirePairing: boolean;
105
+ readonly requireOutboundAuthorization: boolean;
105
106
  readonly pairingCodeTTL: number;
106
107
  readonly maxQueueSize: number;
107
108
  readonly allowedChannels: readonly ChannelType[];
108
109
  }
110
+ export interface ChannelOutboundPayload {
111
+ readonly channelType: ChannelType;
112
+ readonly channelId: string;
113
+ readonly content: string;
114
+ readonly replyTo?: string;
115
+ readonly purpose: "send" | "agent-message" | "broadcast" | "response";
116
+ }
117
+ export type ChannelOutboundAuthorizer = (payload: ChannelOutboundPayload) => Promise<{
118
+ readonly ok: boolean;
119
+ readonly reason?: string;
120
+ }>;
109
121
  export declare class ChannelGateway {
110
122
  private readonly config;
111
123
  private adapters;
@@ -114,6 +126,7 @@ export declare class ChannelGateway {
114
126
  private devices;
115
127
  private messageQueue;
116
128
  private messageHandler;
129
+ private outboundAuthorizer;
117
130
  private routePolicyEngine;
118
131
  private pushInversionRegistry;
119
132
  constructor(config?: Partial<GatewayConfig>);
@@ -163,6 +176,7 @@ export declare class ChannelGateway {
163
176
  * This is typically the agent's query function.
164
177
  */
165
178
  setMessageHandler(handler: (message: ChannelMessage) => Promise<string>): void;
179
+ setOutboundAuthorizer(authorizer: ChannelOutboundAuthorizer): void;
166
180
  /**
167
181
  * Connect all registered adapters.
168
182
  */
@@ -239,4 +253,5 @@ export declare class ChannelGateway {
239
253
  getVerifiedSenderCount(): number;
240
254
  private handleIncomingMessage;
241
255
  private queueMessage;
256
+ private authorizeOutbound;
242
257
  }
@@ -31,6 +31,7 @@ import { randomUUID } from "node:crypto";
31
31
  import { createPushInversionRegistry, } from "./push-inversion.js";
32
32
  const DEFAULT_CONFIG = {
33
33
  requirePairing: true,
34
+ requireOutboundAuthorization: false,
34
35
  pairingCodeTTL: 5 * 60 * 1000, // 5 minutes
35
36
  maxQueueSize: 100,
36
37
  allowedChannels: ["telegram", "slack", "discord", "webchat", "cli"],
@@ -43,6 +44,7 @@ export class ChannelGateway {
43
44
  devices = new Map();
44
45
  messageQueue = [];
45
46
  messageHandler = null;
47
+ outboundAuthorizer = null;
46
48
  // Route-policy engine (optional). When present, every inbound message
47
49
  // is gated by the per-channel policy: pairing rules, rate limits, and
48
50
  // response formatting. When absent the gateway falls back to its
@@ -129,6 +131,9 @@ export class ChannelGateway {
129
131
  setMessageHandler(handler) {
130
132
  this.messageHandler = handler;
131
133
  }
134
+ setOutboundAuthorizer(authorizer) {
135
+ this.outboundAuthorizer = authorizer;
136
+ }
132
137
  /**
133
138
  * Connect all registered adapters.
134
139
  */
@@ -237,6 +242,9 @@ export class ChannelGateway {
237
242
  const adapter = this.adapters.get(channelType);
238
243
  if (!adapter?.connected)
239
244
  return false;
245
+ if (!(await this.authorizeOutbound({ channelType, channelId, content, replyTo, purpose: "send" }))) {
246
+ return false;
247
+ }
240
248
  return adapter.send(channelId, content, replyTo);
241
249
  }
242
250
  /**
@@ -261,6 +269,15 @@ export class ChannelGateway {
261
269
  return { ok: false, reason: "pairing_required", pairingCode: pairing.code };
262
270
  }
263
271
  }
272
+ if (!(await this.authorizeOutbound({
273
+ channelType: request.channelType,
274
+ channelId: request.channelId,
275
+ content: request.content,
276
+ replyTo: request.replyTo,
277
+ purpose: "agent-message",
278
+ }))) {
279
+ return { ok: false, reason: "taint_denied" };
280
+ }
264
281
  const sent = await adapter.send(request.channelId, request.content, request.replyTo);
265
282
  if (!sent)
266
283
  return { ok: false, reason: "adapter_not_connected" };
@@ -292,6 +309,14 @@ export class ChannelGateway {
292
309
  const sent = [];
293
310
  for (const [type, adapter] of this.adapters) {
294
311
  if (adapter.connected) {
312
+ if (!(await this.authorizeOutbound({
313
+ channelType: type,
314
+ channelId: "broadcast",
315
+ content,
316
+ purpose: "broadcast",
317
+ }))) {
318
+ continue;
319
+ }
295
320
  const ok = await adapter.send("broadcast", content);
296
321
  if (ok)
297
322
  sent.push(type);
@@ -402,6 +427,17 @@ export class ChannelGateway {
402
427
  : rawResponse;
403
428
  const adapter = this.adapters.get(message.channelType);
404
429
  if (adapter?.connected) {
430
+ const authorized = await this.authorizeOutbound({
431
+ channelType: message.channelType,
432
+ channelId: message.channelId,
433
+ content: response,
434
+ replyTo: message.id,
435
+ purpose: "response",
436
+ });
437
+ if (!authorized) {
438
+ await adapter.send(message.channelId, "denied: outbound_authorization_required", message.id);
439
+ return;
440
+ }
405
441
  await adapter.send(message.channelId, response, message.id);
406
442
  }
407
443
  else {
@@ -421,6 +457,16 @@ export class ChannelGateway {
421
457
  }
422
458
  this.messageQueue.push(message);
423
459
  }
460
+ async authorizeOutbound(payload) {
461
+ if (!this.outboundAuthorizer)
462
+ return !this.config.requireOutboundAuthorization;
463
+ try {
464
+ return (await this.outboundAuthorizer(payload)).ok;
465
+ }
466
+ catch {
467
+ return false;
468
+ }
469
+ }
424
470
  }
425
471
  function normalizeChannelOverride(value) {
426
472
  return typeof value === "string" ? value : undefined;
@@ -19,7 +19,7 @@
19
19
  * A message received on Telegram can be responded to on Slack and emailed.
20
20
  * The dispatch plane abstracts away the transport layer completely.
21
21
  */
22
- import type { ChannelAdapter, ChannelMessage, ChannelType, DeviceNode } from "./gateway.js";
22
+ import type { ChannelAdapter, ChannelMessage, ChannelOutboundAuthorizer, ChannelType, DeviceNode } from "./gateway.js";
23
23
  import type { DispatchRoutePolicy } from "./dispatch.js";
24
24
  import { RoutePolicyEngine } from "./route-policies.js";
25
25
  import type { ComputerSessionStore, SessionEvent } from "../session/computer-session-store.js";
@@ -57,6 +57,7 @@ export interface ChannelHealth {
57
57
  }
58
58
  export interface UnifiedDispatchConfig {
59
59
  readonly requirePairing: boolean;
60
+ readonly requireOutboundAuthorization: boolean;
60
61
  readonly pairingCodeTTL: number;
61
62
  readonly maxQueueSize: number;
62
63
  readonly maxInboxSize: number;
@@ -84,6 +85,7 @@ export declare class UnifiedDispatchPlane {
84
85
  private readonly policies;
85
86
  private readonly routePolicyEngine;
86
87
  private messageHandler;
88
+ private outboundAuthorizer;
87
89
  private computerSessionStore;
88
90
  private computerSessionDispose;
89
91
  private readonly computerSessionListeners;
@@ -91,6 +93,7 @@ export declare class UnifiedDispatchPlane {
91
93
  constructor(config?: Partial<UnifiedDispatchConfig>);
92
94
  registerAdapter(adapter: ChannelAdapter): void;
93
95
  setMessageHandler(handler: (message: ChannelMessage) => Promise<string>): void;
96
+ setOutboundAuthorizer(authorizer: ChannelOutboundAuthorizer): void;
94
97
  /**
95
98
  * Attach a ComputerSessionStore so session events flow through this plane
96
99
  * to every registered listener. Calling again with a different store
@@ -184,6 +187,7 @@ export declare class UnifiedDispatchPlane {
184
187
  private handleIncomingMessage;
185
188
  private processTask;
186
189
  private updateHealth;
190
+ private authorizeOutbound;
187
191
  private trimInbox;
188
192
  }
189
193
  export { analyzeComplexity, classifyPriority };
@@ -24,6 +24,7 @@ import { RoutePolicyEngine, createDefaultPolicy } from "./route-policies.js";
24
24
  import { SurfaceRegistry, } from "./fan-out.js";
25
25
  const DEFAULT_CONFIG = {
26
26
  requirePairing: true,
27
+ requireOutboundAuthorization: false,
27
28
  pairingCodeTTL: 5 * 60 * 1000,
28
29
  maxQueueSize: 100,
29
30
  maxInboxSize: 500,
@@ -96,6 +97,7 @@ export class UnifiedDispatchPlane {
96
97
  policies = new Map();
97
98
  routePolicyEngine;
98
99
  messageHandler = null;
100
+ outboundAuthorizer = null;
99
101
  // Computer-session bridge (Phase 3 P1-F1). When wired, incoming SessionEvents
100
102
  // fan out to every listener registered via `onComputerSessionEvent`. The
101
103
  // ComputerSessionStore remains the single source of truth for session state
@@ -138,6 +140,9 @@ export class UnifiedDispatchPlane {
138
140
  setMessageHandler(handler) {
139
141
  this.messageHandler = handler;
140
142
  }
143
+ setOutboundAuthorizer(authorizer) {
144
+ this.outboundAuthorizer = authorizer;
145
+ }
141
146
  // ── Computer-Session Bridge (Phase 3 P1-F1) ────────────
142
147
  /**
143
148
  * Attach a ComputerSessionStore so session events flow through this plane
@@ -317,7 +322,7 @@ export class UnifiedDispatchPlane {
317
322
  for (const channelType of targetChannels) {
318
323
  const adapter = this.adapters.get(channelType);
319
324
  if (adapter?.connected) {
320
- const ok = await adapter.send("forward", content);
325
+ const ok = await this.sendToChannel(channelType, "forward", content);
321
326
  if (ok)
322
327
  forwarded.push(channelType);
323
328
  }
@@ -353,6 +358,9 @@ export class UnifiedDispatchPlane {
353
358
  const adapter = this.adapters.get(channelType);
354
359
  if (!adapter?.connected)
355
360
  return false;
361
+ if (!(await this.authorizeOutbound({ channelType, channelId, content, replyTo, purpose: "send" }))) {
362
+ return false;
363
+ }
356
364
  const ok = await adapter.send(channelId, content, replyTo);
357
365
  if (ok) {
358
366
  this.updateHealth(channelType, {
@@ -365,12 +373,9 @@ export class UnifiedDispatchPlane {
365
373
  const sent = [];
366
374
  for (const [type, adapter] of this.adapters) {
367
375
  if (adapter.connected) {
368
- const ok = await adapter.send("broadcast", content);
376
+ const ok = await this.sendToChannel(type, "broadcast", content);
369
377
  if (ok) {
370
378
  sent.push(type);
371
- this.updateHealth(type, {
372
- messagesSent: (this.channelHealth.get(type)?.messagesSent ?? 0) + 1,
373
- });
374
379
  }
375
380
  }
376
381
  }
@@ -503,10 +508,7 @@ export class UnifiedDispatchPlane {
503
508
  // Respond on the original channel
504
509
  const adapter = this.adapters.get(task.message.channelType);
505
510
  if (adapter?.connected) {
506
- await adapter.send(task.message.channelId, response, task.message.id);
507
- this.updateHealth(task.message.channelType, {
508
- messagesSent: (this.channelHealth.get(task.message.channelType)?.messagesSent ?? 0) + 1,
509
- });
511
+ await this.sendToChannel(task.message.channelType, task.message.channelId, response, task.message.id);
510
512
  }
511
513
  // Cross-channel routing: also send to configured response channels
512
514
  if (this.config.enableCrossChannelRouting) {
@@ -538,6 +540,16 @@ export class UnifiedDispatchPlane {
538
540
  };
539
541
  this.channelHealth.set(type, { ...current, ...updates });
540
542
  }
543
+ async authorizeOutbound(payload) {
544
+ if (!this.outboundAuthorizer)
545
+ return !this.config.requireOutboundAuthorization;
546
+ try {
547
+ return (await this.outboundAuthorizer(payload)).ok;
548
+ }
549
+ catch {
550
+ return false;
551
+ }
552
+ }
541
553
  trimInbox() {
542
554
  if (this.inbox.size > this.config.maxInboxSize) {
543
555
  const completed = [...this.inbox.entries()]
@@ -65,14 +65,6 @@ export const CLI_REGISTRY = Object.freeze([
65
65
  versionRegex: /(\d+\.\d+\.\d+)/,
66
66
  url: "https://github.com/google-gemini/gemini-cli",
67
67
  },
68
- {
69
- id: "ollama",
70
- displayName: "Ollama",
71
- binaryNames: ["ollama"],
72
- versionArgs: ["--version"],
73
- versionRegex: /version is (\d+\.\d+\.\d+)/,
74
- url: "https://ollama.ai",
75
- },
76
68
  {
77
69
  id: "aider",
78
70
  displayName: "Aider",
@@ -15,9 +15,9 @@
15
15
  * - A planner stub (cheap heuristic — production wires to a real LLM)
16
16
  * - A browser driver stub (production wires to chrome-bridge.ts)
17
17
  *
18
- * QB #6 (honest stubs): when running without --enable-driver the command
19
- * runs in dry-run mode against a synthetic page. Production callers wire
20
- * their own browser driver via `runAgenticBrowse`.
18
+ * QB #6 (honest stubs): the command runs in dry-run mode against a
19
+ * synthetic page. `--enable-driver` fails closed until this CLI owns a
20
+ * real driver wire. Production callers inject one via `runAgenticBrowse`.
21
21
  */
22
22
  import type { BrowsePlan, BrowseSessionStatus } from "../../browser/agentic-browser.js";
23
23
  export interface BrowseCommandOptions {
@@ -30,7 +30,7 @@ export interface BrowseCommandOptions {
30
30
  readonly startUrl?: string;
31
31
  /** When true, requires every browser action to pause for human approval. */
32
32
  readonly alwaysAsk?: boolean;
33
- /** When false (default), runs in dry-run mode (no real browser nav). */
33
+ /** Reserved for a future real driver wire. Currently fails closed when true. */
34
34
  readonly enableDriver?: boolean;
35
35
  }
36
36
  export interface BrowseCommandResult {
@@ -15,9 +15,9 @@
15
15
  * - A planner stub (cheap heuristic — production wires to a real LLM)
16
16
  * - A browser driver stub (production wires to chrome-bridge.ts)
17
17
  *
18
- * QB #6 (honest stubs): when running without --enable-driver the command
19
- * runs in dry-run mode against a synthetic page. Production callers wire
20
- * their own browser driver via `runAgenticBrowse`.
18
+ * QB #6 (honest stubs): the command runs in dry-run mode against a
19
+ * synthetic page. `--enable-driver` fails closed until this CLI owns a
20
+ * real driver wire. Production callers inject one via `runAgenticBrowse`.
21
21
  */
22
22
  import { runAgenticBrowse } from "../../browser/agentic-browser.js";
23
23
  import { inspectUrl } from "../../security/url-instruction-guard.js";
@@ -59,7 +59,7 @@ function deriveUrlFromTask(task) {
59
59
  const q = encodeURIComponent(task.slice(0, 256));
60
60
  return `https://duckduckgo.com/html/?q=${q}`;
61
61
  }
62
- // ── Driver stub (replaced when --enable-driver passes) ──────────
62
+ // ── Driver stub ─────────────────────────────────────────────────
63
63
  /**
64
64
  * Honest dry-run driver — never opens a browser. Returns a synthetic
65
65
  * page snapshot so the orchestrator's security pipeline can exercise
@@ -151,7 +151,7 @@ function buildTrifectaWrapper(alwaysAsk) {
151
151
  // ── Run command ────────────────────────────────────────────────
152
152
  export async function runBrowseCommand(opts) {
153
153
  const task = opts.task.trim();
154
- const dryRun = opts.enableDriver !== true;
154
+ const dryRun = true;
155
155
  const placeholderPlan = {
156
156
  id: "plan-empty",
157
157
  task: "",
@@ -171,6 +171,17 @@ export async function runBrowseCommand(opts) {
171
171
  };
172
172
  }
173
173
  const plan = buildHeuristicPlan(task, opts.startUrl);
174
+ if (opts.enableDriver === true) {
175
+ return {
176
+ ok: false,
177
+ task,
178
+ plan,
179
+ status: "failed",
180
+ stepsExecuted: 0,
181
+ dryRun,
182
+ error: "--enable-driver is not wired; no real browser action executed",
183
+ };
184
+ }
174
185
  // QB #6 honest stubs: per-session HMAC secret + cheap heuristic
175
186
  // classifier. Production callers (Claude Sub, OpenAI, etc.) wire a
176
187
  // real LLM-backed classifier here.
@@ -201,6 +212,7 @@ export async function runBrowseCommand(opts) {
201
212
  hiddenTextScan: buildHiddenTextScan(),
202
213
  trifectaGuard: buildTrifectaWrapper(opts.alwaysAsk === true),
203
214
  browserDriver: createDryRunDriver(),
215
+ executionMode: "dry-run",
204
216
  ...(opts.maxSteps !== undefined ? { maxStepsOverride: opts.maxSteps } : {}),
205
217
  ...(cursorEmit !== undefined ? { cursorEmit } : {}),
206
218
  };
@@ -42,7 +42,7 @@
42
42
  import { ComputerUseAgent, type ComputerAgentRepertoire } from "../../computer-use/computer-agent.js";
43
43
  import { type ReplayReport } from "../../computer-use/action-replay.js";
44
44
  import type { ComputerAction } from "../../computer-use/driver-contract.js";
45
- import { CuaDriverComputerDriver } from "../../computer-use/cua-driver-backend.js";
45
+ import type { CuaDriverComputerDriver } from "../../computer-use/cua-driver-backend.js";
46
46
  export type ComputerUseSubcommand = "click" | "type" | "key" | "screenshot" | "wait" | "scroll" | "repertoire" | "replay";
47
47
  /** Subcommands that the CLI exposes. Source of truth for the
48
48
  * `src/index.ts` dispatcher's `valid` allow-list. */
@@ -42,7 +42,6 @@
42
42
  import { promises as fs } from "node:fs";
43
43
  import { ComputerUseAgent, } from "../../computer-use/computer-agent.js";
44
44
  import { deserializeSession, replaySession, } from "../../computer-use/action-replay.js";
45
- import { CuaDriverComputerDriver, cuaDriverInstallHint, isCuaDriverAvailable, } from "../../computer-use/cua-driver-backend.js";
46
45
  /** Subcommands that the CLI exposes. Source of truth for the
47
46
  * `src/index.ts` dispatcher's `valid` allow-list. */
48
47
  export const COMPUTER_USE_SUBCOMMANDS = Object.freeze([
@@ -165,47 +164,13 @@ export async function runComputerUseCommand(opts) {
165
164
  dryRun: true,
166
165
  };
167
166
  }
168
- const available = opts.isComputerDriverAvailable ?? isCuaDriverAvailable;
169
- if (opts.makeComputerDriver === undefined && !available()) {
170
- return {
171
- ok: false,
172
- subcommand: opts.subcommand,
173
- action,
174
- dryRun: false,
175
- error: `cua-driver unavailable — ${cuaDriverInstallHint()}`,
176
- };
177
- }
178
- // --execute path: explicit opt-in only. It uses the provider-agnostic
179
- // CUA driver backend rather than Anthropic computer_use schemas; dry-run
180
- // remains the default safety boundary.
181
- const driver = opts.makeComputerDriver ? opts.makeComputerDriver() : new CuaDriverComputerDriver();
182
- try {
183
- const execution = await driver.execute(action);
184
- return {
185
- ok: execution.ok,
186
- subcommand: opts.subcommand,
187
- action,
188
- dryRun: false,
189
- ...(execution.ok ? {} : { error: execution.error ?? "computer-use execute failed" }),
190
- execution: {
191
- ok: execution.ok,
192
- ...(execution.output !== undefined ? { output: execution.output } : {}),
193
- ...(execution.error !== undefined ? { error: execution.error } : {}),
194
- },
195
- };
196
- }
197
- catch (err) {
198
- return {
199
- ok: false,
200
- subcommand: opts.subcommand,
201
- action,
202
- dryRun: false,
203
- error: `execute failed: ${err instanceof Error ? err.message : String(err)}`,
204
- };
205
- }
206
- finally {
207
- await driver.close();
208
- }
167
+ return {
168
+ ok: false,
169
+ subcommand: opts.subcommand,
170
+ action,
171
+ dryRun: false,
172
+ error: "computer-use --execute requires daemon — run `wotann engine start`, then retry",
173
+ };
209
174
  }
210
175
  // ── Phase-2 subcommand handlers ─────────────────────────────────
211
176
  /**
@@ -9,7 +9,7 @@ import { promisify } from "node:util";
9
9
  import { createWorkspace } from "../core/workspace.js";
10
10
  import { discoverProviders, formatFullStatus } from "../providers/discovery.js";
11
11
  import { PROVIDER_DEFAULTS } from "../providers/model-defaults.js";
12
- import { resolveOllamaHost } from "../providers/ollama-host.js";
12
+ import { isForbiddenTrustLayerProvider } from "../providers/fallback-chain.js";
13
13
  import { buildPRDescription, parseConflictBlocks, parseDiffStat, renderCommitMessage, suggestCommitMessage, suggestConflictResolution, } from "../git/magic-git.js";
14
14
  const execFileAsync = promisify(execFile);
15
15
  export async function runInit(targetDir, options) {
@@ -54,20 +54,12 @@ export async function runInit(targetDir, options) {
54
54
  console.log(chalk.dim(` ○ ${p.label}`) + chalk.dim(` — ${setupHint}`));
55
55
  }
56
56
  }
57
- // Free-tier guidance
57
+ // Legacy --free compatibility guidance. Trust-layer inference stays BYO.
58
58
  if (options.free) {
59
59
  console.log();
60
- console.log(chalk.bold(" Free-tier setup:"));
61
- console.log(chalk.dim(" Primary: Ollama local (free, private, offline)"));
62
- console.log(chalk.dim(" Overflow: Cerebras Groq Google AI Studio"));
63
- console.log(chalk.dim(" KV cache: q8_0 (doubles Ollama context window)"));
64
- const ollamaActive = active.some((p) => p.provider === "ollama");
65
- if (!ollamaActive) {
66
- console.log();
67
- console.log(chalk.yellow(" ⚠ Ollama not detected."));
68
- console.log(chalk.dim(" Install: https://ollama.ai"));
69
- console.log(chalk.dim(" Then: ollama pull qwen3-coder-next"));
70
- }
60
+ console.log(chalk.bold(" BYO provider required:"));
61
+ console.log(chalk.dim(" --free no longer configures local or anonymous model runtimes."));
62
+ console.log(chalk.dim(" Configure a remote subscription or API key with `wotann login`."));
71
63
  }
72
64
  // Extended context guidance
73
65
  if (options.extendedContext) {
@@ -91,8 +83,6 @@ function getProviderSetupHint(provider) {
91
83
  return 'npx @openai/codex --full-auto "hello"';
92
84
  case "copilot":
93
85
  return "export GH_TOKEN=ghp_... (GitHub PAT)";
94
- case "ollama":
95
- return "ollama serve (https://ollama.ai)";
96
86
  case "gemini":
97
87
  return "export GEMINI_API_KEY=... (free at ai.google.dev)";
98
88
  case "huggingface":
@@ -176,7 +166,7 @@ export async function runDoctor(targetDir, options = {}) {
176
166
  detail: `${nodeVersion} (requires ≥22.13.0)`,
177
167
  });
178
168
  // S4-9: Expanded doctor checks — daemon health, socket, DB integrity,
179
- // Ollama reachability, port conflicts, API key validity.
169
+ // port conflicts, and API key validity.
180
170
  // 4. Daemon socket reachability
181
171
  const { resolveDaemonSocketPath, isNamedPipe } = await import("../daemon/transport/socket-path.js");
182
172
  const socketPath = process.platform === "win32"
@@ -241,27 +231,7 @@ export async function runDoctor(targetDir, options = {}) {
241
231
  detail: "Not yet created (no-op)",
242
232
  });
243
233
  }
244
- // 6. Ollama reachability (if configured) — Round 9 single-source-of-truth.
245
- const ollamaUrl = resolveOllamaHost();
246
- try {
247
- const controller = new AbortController();
248
- const timer = setTimeout(() => controller.abort(), 1500);
249
- const res = await fetch(`${ollamaUrl}/api/version`, { signal: controller.signal });
250
- clearTimeout(timer);
251
- checks.push({
252
- name: "Ollama",
253
- ok: res.ok,
254
- detail: res.ok ? `Reachable at ${ollamaUrl}` : `HTTP ${res.status}`,
255
- });
256
- }
257
- catch {
258
- checks.push({
259
- name: "Ollama",
260
- ok: false,
261
- detail: `Not reachable at ${ollamaUrl} (install ollama or set OLLAMA_URL)`,
262
- });
263
- }
264
- // 7. Session token perms (if present) — must be 0o600
234
+ // 6. Session token perms (if present) — must be 0o600
265
235
  const tokenPath = resolveWotannHomeSubdir("session-token.json");
266
236
  if (existsSync(tokenPath)) {
267
237
  try {
@@ -489,7 +459,7 @@ export async function runMagicGit(options) {
489
459
  }
490
460
  export function buildModelCatalogRows(defaults, statuses) {
491
461
  const statusByProvider = new Map(statuses.map((status) => [status.provider, status]));
492
- return Object.entries(defaults).map(([provider, entry]) => {
462
+ return Object.entries(defaults).filter(([provider]) => !isForbiddenTrustLayerProvider(provider)).map(([provider, entry]) => {
493
463
  const status = statusByProvider.get(provider);
494
464
  const seen = new Set();
495
465
  const models = [];