@dotdrelle/wiki-manager 0.15.66 → 0.15.71

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 (72) hide show
  1. package/.env.example +10 -3
  2. package/README.md +57 -0
  3. package/agent-runtimes.example.json +68 -0
  4. package/agents.docker-compose.yml +39 -1
  5. package/docker-compose.yml +3 -3
  6. package/package.json +3 -2
  7. package/src/activity/activityAggregator.test.js +2 -2
  8. package/src/agent/graph.js +13 -11
  9. package/src/agent/skillRecursion.test.js +13 -12
  10. package/src/cli/wiki-manager.js +125 -37
  11. package/src/cli/wiki-manager.test.js +16 -16
  12. package/src/commands/slash.js +59 -5
  13. package/src/contracts/schemas.js +67 -0
  14. package/src/core/activity.js +5 -0
  15. package/src/core/agentEvents.js +139 -25
  16. package/src/core/agentEvents.test.js +26 -1
  17. package/src/core/buildInfo.json +2 -2
  18. package/src/core/commandFailure.test.js +2 -2
  19. package/src/core/currentArtifact.test.js +5 -5
  20. package/src/core/dockerCompose.test.js +8 -40
  21. package/src/core/env.js +14 -0
  22. package/src/core/env.test.js +19 -0
  23. package/src/core/googleGrants.test.js +1 -1
  24. package/src/core/mcp.js +1 -1
  25. package/src/core/mcp.test.js +1 -1
  26. package/src/core/otherWorkspacesRunning.test.js +6 -6
  27. package/src/core/runtimeEventAdapter.js +81 -0
  28. package/src/core/runtimeEventAdapter.test.js +61 -0
  29. package/src/core/runtimeLog.js +35 -1
  30. package/src/core/runtimeLog.test.js +27 -2
  31. package/src/core/skillChainView.test.js +2 -2
  32. package/src/core/skillCompiler.test.js +1 -1
  33. package/src/core/skillInvocation.js +13 -8
  34. package/src/core/skillInvocation.test.js +1 -1
  35. package/src/core/startupCheck.js +58 -0
  36. package/src/core/startupCheck.test.js +29 -1
  37. package/src/core/wikiSetup.js +25 -0
  38. package/src/core/wikiSetup.test.js +35 -0
  39. package/src/core/wikirc.test.js +6 -6
  40. package/src/core/workspaceInherit.test.js +14 -14
  41. package/src/orchestrator/agentRegistry.js +1 -22
  42. package/src/orchestrator/agentRegistry.test.js +6 -6
  43. package/src/orchestrator/assignmentManager.js +16 -4
  44. package/src/orchestrator/capabilityRegistry.js +8 -1
  45. package/src/orchestrator/dispatcher.js +405 -2
  46. package/src/orchestrator/dispatcher.test.js +158 -4
  47. package/src/orchestrator/objectiveResolver.js +10 -6
  48. package/src/orchestrator/objectiveResolver.test.js +26 -27
  49. package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
  50. package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
  51. package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
  52. package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
  53. package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
  54. package/src/orchestrator/providers/runtimeProvider.js +101 -0
  55. package/src/orchestrator/providers/runtimeProviders.js +378 -0
  56. package/src/orchestrator/providers/runtimeProviders.test.js +384 -0
  57. package/src/orchestrator/resultAggregator.js +35 -2
  58. package/src/orchestrator/resultAggregator.test.js +62 -0
  59. package/src/orchestrator/scheduler.test.js +4 -4
  60. package/src/runtime/delegation.test.js +11 -11
  61. package/src/runtime/recoveryManager.js +70 -5
  62. package/src/runtime/runner.test.js +1 -1
  63. package/src/runtime/server.test.js +2 -2
  64. package/src/runtime/skillChain.e2e.test.js +2 -2
  65. package/src/runtime/store.test.js +8 -5
  66. package/src/runtime/supervisor.js +5 -10
  67. package/src/runtime/workspaceIsolation.test.js +26 -26
  68. package/src/shell/RightPane.tsx +23 -3
  69. package/src/shell/StartupScreen.tsx +44 -7
  70. package/src/shell/repl.js +24 -2
  71. package/src/shell/repl.test.js +13 -0
  72. package/wiki-workspace +53 -3
@@ -1,4 +1,4 @@
1
- import { createCapabilityRegistry } from './capabilityRegistry.js';
1
+ import { capabilityRegistryForSession } from './capabilityRegistry.js';
2
2
  import { CapabilityUnavailableError, resolve } from './capabilityResolver.js';
3
3
 
4
4
  export function createAssignmentManager({
@@ -27,9 +27,7 @@ export async function assign(task, {
27
27
  if (!capability) {
28
28
  throw new CapabilityUnavailableError(capability, 'task_missing_required_capability', { taskId: task?.id ?? task?.step });
29
29
  }
30
- const effectiveRegistry = registry ?? session?.capabilityRegistry ?? createCapabilityRegistry({
31
- agents: session?.agentRegistrySnapshot ?? session?.agents ?? [],
32
- });
30
+ const effectiveRegistry = registry ?? capabilityRegistryForSession(session);
33
31
  const effectiveWorkspaceConfig = workspaceConfig ?? session?.wikircConfig ?? session?.wikirc?.config ?? {};
34
32
  const retryAssignment = task?.retryAssignment;
35
33
  if (retryAssignment?.agentInstanceId) {
@@ -46,6 +44,14 @@ export async function assign(task, {
46
44
  capability,
47
45
  operation: task?.operation ?? null,
48
46
  serverName: agent?.serverName ?? provider?.serverName ?? null,
47
+ // Mirrors the primary resolution branch below: without this, retrying a
48
+ // task assigned to an external-runtime provider drops the routing
49
+ // discriminator and the dispatcher misroutes it to the MCP path, which
50
+ // throws "No MCP server found" since external-runtime agents carry no
51
+ // serverName.
52
+ providerKind: provider?.providerKind ?? 'mcp-agent',
53
+ runtimeProvider: provider?.runtimeProvider ?? null,
54
+ runtimeId: provider?.runtimeId ?? null,
49
55
  agent,
50
56
  retry: true,
51
57
  previousAgentInstanceId: retryAssignment.previousAgentInstanceId ?? null,
@@ -62,6 +68,12 @@ export async function assign(task, {
62
68
  capability,
63
69
  operation: task?.operation ?? null,
64
70
  serverName: agent?.serverName ?? provider?.serverName ?? null,
71
+ // External runtime providers (RFC § 8) resolve through the same registry
72
+ // as MCP agents; the assignment carries the routing discriminator so the
73
+ // dispatcher can hand the task to the runtime instead of agent_execute.
74
+ providerKind: provider?.providerKind ?? 'mcp-agent',
75
+ runtimeProvider: provider?.runtimeProvider ?? null,
76
+ runtimeId: provider?.runtimeId ?? null,
65
77
  agent,
66
78
  };
67
79
  }
@@ -19,6 +19,11 @@ export function createCapabilityRegistry({ agents = [], compatibleContractVersio
19
19
  capability,
20
20
  description: agent.description,
21
21
  lastSeenAt: agent.lastSeenAt ?? null,
22
+ // External runtime providers (RFC § 8) ride the same registry as MCP
23
+ // agents. These fields are null for every ordinary MCP agent.
24
+ providerKind: agent.providerKind ?? null,
25
+ runtimeId: agent.runtimeId ?? null,
26
+ runtimeProvider: agent.runtimeProvider ?? null,
22
27
  };
23
28
  const list = providers.get(key) ?? [];
24
29
  list.push(entry);
@@ -54,7 +59,9 @@ export function capabilityRegistryForSession(session) {
54
59
  ?? session?.agentRegistrySnapshot
55
60
  ?? session?.agents
56
61
  ?? [];
57
- if (agents.length > 0) return createCapabilityRegistry({ agents });
62
+ const runtimeAgents = session?.runtimeProviderAgents ?? [];
63
+ const merged = [...agents, ...runtimeAgents];
64
+ if (merged.length > 0) return createCapabilityRegistry({ agents: merged });
58
65
  if (session?.capabilityRegistry?.providersFor) return session.capabilityRegistry;
59
66
  return createCapabilityRegistry();
60
67
  }
@@ -1,7 +1,10 @@
1
1
  import { normalizeActivity, parseJsonText } from '../core/activity.js';
2
2
  import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
3
3
  import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
4
+ import { loadWorkspaceProfile } from '../core/profile.js';
5
+ import { mapRuntimeEvent } from '../core/runtimeEventAdapter.js';
4
6
  import { emitRuntimeLog, pollActivitiesOnce } from '../runtime/supervisor.js';
7
+ import { APPROVAL_DEFAULT_CLASS, approvalCovered } from './approvalPolicy.js';
5
8
  import { isSuccessful, isTerminal } from './taskStatuses.js';
6
9
 
7
10
 
@@ -32,6 +35,16 @@ export async function execute(task, assignment, {
32
35
  pollBusy = new Set(),
33
36
  pollIntervalMs = 2500,
34
37
  } = {}) {
38
+ if (isExternalRuntimeAssignment(assignment)) {
39
+ return executeExternalRuntime(task, assignment, {
40
+ session,
41
+ signal,
42
+ runId,
43
+ attempt,
44
+ timeoutMs,
45
+ pollIntervalMs,
46
+ });
47
+ }
35
48
  if (!session) throw new Error('dispatcher.execute requires session.');
36
49
  if (!assignment?.serverName) throw new Error(`No MCP server found for agent ${assignment?.agentInstanceId ?? '(unknown)'}.`);
37
50
  const serverName = assignment.serverName;
@@ -157,6 +170,243 @@ export async function execute(task, assignment, {
157
170
  }
158
171
  }
159
172
 
173
+ function isExternalRuntimeAssignment(assignment) {
174
+ return assignment?.providerKind === 'external-runtime'
175
+ && typeof assignment?.runtimeProvider?.execute === 'function';
176
+ }
177
+
178
+ // A proposal without mutations still waits for a human grant: the request is
179
+ // treated as the default approval class, covered by a run-scope grant like
180
+ // any other. Every announced class must be covered — the "global" approval is
181
+ // bounded by the proposal.
182
+ function pendingApprovalCovered(classes, approvals, context) {
183
+ const list = Array.isArray(classes) && classes.length > 0
184
+ ? classes
185
+ : [APPROVAL_DEFAULT_CLASS];
186
+ return list.every((approvalClass) => approvalCovered(
187
+ {
188
+ // Without an id, grantCoversTask's task/tool-scope match (which keys
189
+ // off task.id/localId/taskId) can never succeed, so a per-task
190
+ // `/approve item <id>` grant silently fails to cover this task and only
191
+ // a run-scope "approve all" can ever unblock it.
192
+ id: context?.taskId ?? null,
193
+ taskId: context?.taskId ?? null,
194
+ groupId: context?.groupId ?? null,
195
+ requiresApproval: true,
196
+ approvalClass: String(approvalClass),
197
+ },
198
+ approvals,
199
+ context,
200
+ ));
201
+ }
202
+
203
+ async function executeExternalRuntime(task, assignment, {
204
+ session,
205
+ signal = null,
206
+ runId = null,
207
+ attempt = null,
208
+ timeoutMs = null,
209
+ pollIntervalMs = 2500,
210
+ } = {}) {
211
+ if (!session) throw new Error('dispatcher.execute requires session.');
212
+ const runtimeProvider = assignment.runtimeProvider;
213
+ const taskId = String(task.id ?? task.step);
214
+ const taskTimeoutMs = resolvedTimeoutMs(assignment, timeoutMs);
215
+ let deadline = Date.now() + taskTimeoutMs;
216
+ let runtimeRunId = null;
217
+ let unsubscribe = null;
218
+ let pendingApproval = null;
219
+ try {
220
+ emitRuntimeLog(session, taskLogPayload('runtime.execute', task, assignment, {
221
+ runId,
222
+ attempt,
223
+ detail: 'external-runtime',
224
+ }));
225
+ const mcpPool = activeProfileMcp(session);
226
+ if (!mcpPool) {
227
+ // A degradation must announce itself. The runtime's whole value is its
228
+ // eyes: dispatched without the workspace wiki MCP, the Deep Agent
229
+ // improvises with its built-in backend (it ran `ls /` on the gateway
230
+ // container and answered "the workspace is empty" — a plausible,
231
+ // confident, entirely wrong answer). Say it in the journal, keep the
232
+ // run (the objective may still be answerable from the model alone),
233
+ // but never let blindness pass silently.
234
+ emitRuntimeLog(session, taskLogPayload('runtime.blind', task, assignment, {
235
+ runId,
236
+ attempt,
237
+ detail: 'no workspace wiki MCP pool — the endpoint is down or declares no read tools; the runtime will run without its eyes',
238
+ }));
239
+ }
240
+ const accepted = await runtimeProvider.execute({
241
+ objective: task.label ?? task.description ?? taskId,
242
+ operation: task.operation ?? null,
243
+ capability: task.requiredCapability ?? null,
244
+ arguments: task.arguments && typeof task.arguments === 'object' ? task.arguments : {},
245
+ workspace: workspaceRequest(session),
246
+ model: activeProfileModel(session),
247
+ language: session?.language ?? session?.wikircConfig?.language ?? null,
248
+ mcp: mcpPool,
249
+ systemPrompt: activeRuntimeSystemPrompt(session, task, assignment),
250
+ });
251
+ runtimeRunId = String(accepted?.runId ?? '');
252
+ if (!runtimeRunId) throw new Error('runtime.execute did not return runId.');
253
+ emitRuntimeLog(session, taskLogPayload('runtime.accepted', task, assignment, {
254
+ runId,
255
+ attempt,
256
+ jobId: runtimeRunId,
257
+ detail: String(accepted?.status ?? 'running'),
258
+ }));
259
+ dispatchAgentEvent(session, createAgentEvent('task.started', {
260
+ origin: 'dispatcher',
261
+ runId,
262
+ taskId,
263
+ payload: {
264
+ runId,
265
+ taskId,
266
+ attemptId: attempt?.attemptId ?? null,
267
+ agentInstanceId: assignment.agentInstanceId,
268
+ jobId: runtimeRunId,
269
+ startedAt: new Date().toISOString(),
270
+ },
271
+ }));
272
+ dispatchExternalRuntimeActivity(session, task, assignment, runtimeRunId, 'running', runId);
273
+ if (typeof runtimeProvider.subscribe === 'function') {
274
+ unsubscribe = runtimeProvider.subscribe(runtimeRunId, (event) => {
275
+ for (const mapped of mapRuntimeEvent(event)) {
276
+ dispatchAgentEvent(session, createAgentEvent(mapped.type, {
277
+ origin: 'runtime_provider',
278
+ runId,
279
+ taskId,
280
+ payload: mapped.payload,
281
+ }));
282
+ if (mapped.type === 'approval.requested') {
283
+ pendingApproval = {
284
+ approvalId: mapped.payload?.approvalId ?? null,
285
+ approvalClasses: Array.isArray(mapped.payload?.approvalClasses)
286
+ ? mapped.payload.approvalClasses
287
+ : [],
288
+ };
289
+ const summary = String(mapped.payload?.proposal?.summary ?? mapped.payload?.reason ?? '').trim();
290
+ dispatchAgentEvent(session, createAgentEvent('assistant_message', {
291
+ origin: 'runtime_provider',
292
+ runId,
293
+ payload: {
294
+ content: [
295
+ '⏸ Approval required before execution:',
296
+ ...(summary ? [` ${summary}`] : []),
297
+ 'Type /approve (or click "Approve") to proceed, "cancel" to abandon.',
298
+ ].filter(Boolean).join('\n'),
299
+ },
300
+ }));
301
+ }
302
+ }
303
+ });
304
+ }
305
+ let approvalWaitStartedAt = null;
306
+ while (true) {
307
+ throwIfAborted(signal);
308
+ // While a proposal waits for a human decision, the task timeout does
309
+ // not tick — the human decides, not the clock (the scheduler applies
310
+ // the same rule to its approval waits). The elapsed wait is credited
311
+ // back onto the deadline once the gate clears (below), rather than
312
+ // merely skipped from this check, so a late approval does not
313
+ // immediately expire the task on the very next iteration.
314
+ if (!pendingApproval && Date.now() > deadline) {
315
+ await runtimeProvider.cancel(runtimeRunId).catch(() => null);
316
+ throw new Error(`Task timed out after ${taskTimeoutMs}ms.`);
317
+ }
318
+ if (pendingApproval) {
319
+ approvalWaitStartedAt ??= Date.now();
320
+ const approvalBeingChecked = pendingApproval;
321
+ const approvals = session.agentProjection?.approvals ?? session.approvals ?? [];
322
+ const covered = pendingApprovalCovered(approvalBeingChecked.approvalClasses, approvals, {
323
+ runId,
324
+ taskId,
325
+ groupId: task.groupId ?? null,
326
+ workspaceId: session.workspace ?? null,
327
+ planRevision: session.planRevision ?? session.agentProjection?.planRevision ?? null,
328
+ });
329
+ if (covered && typeof runtimeProvider.approve === 'function') {
330
+ emitRuntimeLog(session, taskLogPayload('runtime.approval_granted', task, assignment, {
331
+ runId,
332
+ attempt,
333
+ jobId: runtimeRunId,
334
+ detail: 'unblocking runtime HITL',
335
+ }));
336
+ await runtimeProvider.approve(runtimeRunId, { approved: true, scope: approvalBeingChecked.approvalClasses });
337
+ // Only clear the approval just resolved: the runtime's event stream
338
+ // may have raised a second, unrelated one while the approve() round
339
+ // trip above was in flight.
340
+ if (pendingApproval === approvalBeingChecked) {
341
+ pendingApproval = null;
342
+ deadline += Date.now() - approvalWaitStartedAt;
343
+ approvalWaitStartedAt = null;
344
+ }
345
+ } else {
346
+ await delay(pollIntervalMs, signal);
347
+ continue;
348
+ }
349
+ }
350
+ const lastStatus = await runtimeProvider.status(runtimeRunId);
351
+ if (isTerminal(lastStatus?.status)) {
352
+ const refusedParams = Array.isArray(lastStatus?.result?.refusedParams)
353
+ ? lastStatus.result.refusedParams
354
+ : [];
355
+ if (refusedParams.length > 0) {
356
+ emitRuntimeLog(session, taskLogPayload('runtime.params_refused', task, assignment, {
357
+ runId,
358
+ attempt,
359
+ jobId: runtimeRunId,
360
+ detail: `model refused sampling parameters (${refusedParams.join(', ')}) — remove them from the workspace .wikirc (llm.<key>) so they are no longer sent`,
361
+ }));
362
+ }
363
+ emitRuntimeLog(session, taskLogPayload('runtime.result_returned', task, assignment, {
364
+ runId,
365
+ attempt,
366
+ jobId: runtimeRunId,
367
+ status: lastStatus.status,
368
+ detail: 'terminal status',
369
+ }));
370
+ dispatchExternalRuntimeActivity(session, task, assignment, runtimeRunId, lastStatus.status, runId);
371
+ return taskResultFromStatus(task, assignment, runtimeRunId, lastStatus, attempt);
372
+ }
373
+ await delay(pollIntervalMs, signal);
374
+ }
375
+ } catch (error) {
376
+ if (isAbortError(error) && runtimeRunId) {
377
+ await runtimeProvider.cancel(runtimeRunId).catch(() => null);
378
+ }
379
+ throw error;
380
+ } finally {
381
+ unsubscribe?.();
382
+ emitRuntimeLog(session, taskLogPayload('lock.released', task, assignment, {
383
+ runId,
384
+ attempt,
385
+ jobId: runtimeRunId,
386
+ detail: attempt?.locks?.join(',') || 'no locks',
387
+ }));
388
+ attempt?.release?.();
389
+ }
390
+ }
391
+
392
+ function dispatchExternalRuntimeActivity(session, task, assignment, runtimeRunId, status, runId) {
393
+ const activity = normalizeActivity({
394
+ id: runtimeRunId,
395
+ source: assignment.runtimeId ?? 'external-runtime',
396
+ kind: task.operation ?? task.requiredCapability ?? 'task',
397
+ label: task.label ?? task.description ?? String(task.id ?? task.step),
398
+ status,
399
+ progress: { percent: isTerminal(status) ? 100 : 0, stepId: String(task.id ?? task.step) },
400
+ outputRefs: [],
401
+ });
402
+ dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
403
+ origin: 'dispatcher',
404
+ runId,
405
+ taskId: String(task.id ?? task.step),
406
+ payload: { activity },
407
+ }));
408
+ }
409
+
160
410
  function executeRequest(task, session, runId) {
161
411
  return {
162
412
  taskId: String(task.id ?? task.step),
@@ -165,7 +415,20 @@ function executeRequest(task, session, runId) {
165
415
  idempotencyKey: task.idempotencyKey ?? undefined,
166
416
  operation: task.operation,
167
417
  workspace: workspaceRequest(session),
168
- arguments: task.arguments && typeof task.arguments === 'object' ? task.arguments : {},
418
+ arguments: {
419
+ ...(task.arguments && typeof task.arguments === 'object' ? task.arguments : {}),
420
+ // The production agent's confirmation guard
421
+ // (PRODUCTION_REQUIRE_CONFIRMATION=true) has one contract: "only an
422
+ // approved task may run a mutating operation — Donna passes
423
+ // confirm=true after the run-scope approval". The scheduler never
424
+ // dispatches a requiresApproval task before coverage
425
+ // (dependencyResolver.readyTasks), so reaching dispatch IS the
426
+ // approval. Without this, an operator who enables the guard sees every
427
+ // mutating production job fail with "requires confirm=true" — the
428
+ // first E2E ingest plan dispatched by the deep agent's
429
+ // planExpansionRequest failed exactly that way, 19 tasks in one batch.
430
+ ...(task.requiresApproval === true ? { confirm: true } : {}),
431
+ },
169
432
  constraints: {
170
433
  requireApprovalForMutations: task.requiresApproval === true,
171
434
  },
@@ -178,6 +441,137 @@ function workspaceRequest(session) {
178
441
  return { name: String(workspace ?? 'workspace') };
179
442
  }
180
443
 
444
+ // The model travels WITH the run: the runtime is workspace-agnostic and must
445
+ // follow the active profile — default or `/config use` — without a config
446
+ // sync. Numeric LLM parameters declared by the profile (temperature, …) ride
447
+ // along too: nothing is hardcoded here, the workspace config is the source.
448
+ function activeProfileModel(session) {
449
+ const llm = session?.wikircConfig?.llm ?? {};
450
+ const model = {
451
+ ...(llm.baseUrl ? { baseUrl: String(llm.baseUrl) } : {}),
452
+ ...(llm.model ? { model: String(llm.model) } : {}),
453
+ ...(llm.apiKey ? { apiKey: String(llm.apiKey) } : {}),
454
+ };
455
+ for (const key of ['temperature', 'maxTokens', 'topP', 'seed']) {
456
+ const value = Number(llm[key]);
457
+ if (Number.isFinite(value)) model[key] = value;
458
+ }
459
+ return Object.keys(model).length > 0 ? model : null;
460
+ }
461
+
462
+ // The read-only wiki MCP tools the runtime is allowed to see. An explicit
463
+ // allow-list, not a denylist: the runtime has eyes (read tools) and a mouth
464
+ // (gated side-effects through the DAG), never hands on the workspace. A denylist
465
+ // keyed on "write_page|add_source|…" already silently let build_context_write
466
+ // and template_write through, and would leak any future wiki_delete_page /
467
+ // wiki_move_page / wiki_rename the moment it is added upstream.
468
+ const READ_ONLY_WIKI_TOOLS = new Set([
469
+ 'help_list', 'help_read', 'help_search',
470
+ 'profile_read', 'template_read',
471
+ 'wiki_collect_context', 'wiki_list_ingested_sources', 'wiki_list_pages',
472
+ 'wiki_outline', 'wiki_read_deliverable', 'wiki_read_ingested_source',
473
+ 'wiki_read_page', 'wiki_read_pages', 'wiki_search_context',
474
+ 'wiki_workspace_status',
475
+ ]);
476
+
477
+ // An arbitrary external connector's tool names cannot be enumerated ahead of
478
+ // time the way READ_ONLY_WIKI_TOOLS can, so this stays a denylist — but a
479
+ // word-boundary one. The old test was a raw substring match
480
+ // (/write|send|create|update|add|remove/), which dropped safe reads whose name
481
+ // merely contained a mutating verb (`list_recent_updates`, `get_created_at`,
482
+ // `search_addresses`) and missed side-effecting tools that use another verb
483
+ // (`crawl_site`, `post_message`, `run_report`, `publish_draft`). The real
484
+ // guardrails against "hands on the workspace" are elsewhere —
485
+ // EXCLUDED_EXTERNAL_SERVERS covers every workspace-writing agent, and a
486
+ // per-tool `requireApproval` entry drops the whole server — this filter only
487
+ // keeps an obviously-mutating tool of an otherwise-safe connector out of the
488
+ // runtime's eyes.
489
+ const EXTERNAL_MUTATING_VERB = /(?:^|[_-])(?:write|send|post|create|delete|destroy|remove|drop|modify|edit|update|patch|put|upload|publish|submit|execute|run|crawl|trigger|cancel|approve|move|rename|set|add|insert|append|archive)(?:[_-]|$)/i;
490
+
491
+ function isReadOnlyExternalTool(toolName) {
492
+ const base = toolName.includes('__') ? toolName.slice(toolName.lastIndexOf('__') + 2) : toolName;
493
+ return !EXTERNAL_MUTATING_VERB.test(base);
494
+ }
495
+
496
+ // The runtime's EYES, per run: the active workspace's wiki MCP (read tools
497
+ // only) PLUS the declared external MCP endpoints that are safe to hand over
498
+ // (connected, no approval-gated tools, not a workspace-mutating server) —
499
+ // typically web search (exa). The allow-list here is the authority: nothing
500
+ // else reaches the runtime.
501
+ export function activeProfileMcp(session) {
502
+ const blocks = [];
503
+ const wiki = session?.mcp?.wiki;
504
+ if (wiki?.url && wiki.status === 'connected') {
505
+ const tools = (wiki.tools ?? [])
506
+ .map((tool) => String(tool.name ?? ''))
507
+ .filter((name) => {
508
+ if (!name) return false;
509
+ const base = name.includes('__') ? name.slice(name.lastIndexOf('__') + 2) : name;
510
+ return READ_ONLY_WIKI_TOOLS.has(base);
511
+ });
512
+ if (tools.length > 0) {
513
+ // Same credential contract as the manager's own MCP client (mcp.js
514
+ // `authorization: Bearer ${endpoint.token}`): the wiki detail carries
515
+ // `token`, not `headers` — without it the gateway's MCP connection is
516
+ // rejected by the workspace MCP server ("invalid or missing bearer token")
517
+ // and the Deep Agent runs blind.
518
+ const headers = {
519
+ ...(wiki.headers && typeof wiki.headers === 'object' ? wiki.headers : {}),
520
+ ...(wiki.token ? { Authorization: `Bearer ${wiki.token}` } : {}),
521
+ };
522
+ blocks.push({
523
+ name: 'wiki',
524
+ url: String(wiki.url),
525
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
526
+ tools,
527
+ });
528
+ }
529
+ }
530
+ // External connectors ride along ONLY when the operator declared them safe
531
+ // for the runtime's eyes. A connector added from the serve panel lands here
532
+ // too — without this, exa was offered in chat but the agentic path
533
+ // delegated to the gateway and the Deep Agent answered it had no web tools.
534
+ const EXCLUDED_EXTERNAL_SERVERS = new Set(['cme', 'documents', 'connectors', 'production']);
535
+ for (const [name, entry] of Object.entries(session?.mcp ?? {})) {
536
+ if (!entry?.external || entry.status !== 'connected') continue;
537
+ if (EXCLUDED_EXTERNAL_SERVERS.has(name)) continue;
538
+ if (Array.isArray(entry.requireApproval) && entry.requireApproval.length > 0) continue;
539
+ const tools = (entry.tools ?? [])
540
+ .map((tool) => String(tool.name ?? ''))
541
+ .filter((toolName) => toolName && isReadOnlyExternalTool(toolName));
542
+ if (tools.length === 0) continue;
543
+ blocks.push({
544
+ name,
545
+ url: String(entry.url),
546
+ ...(entry.headers && typeof entry.headers === 'object' ? { headers: entry.headers } : {}),
547
+ tools,
548
+ });
549
+ }
550
+ return blocks.length > 0 ? blocks : null;
551
+ }
552
+
553
+ // The Deep Agent's system prompt, built per run from the same ingredients
554
+ // Donna uses: role, the ONE capability being executed (with its declared
555
+ // description), the eyes/bouche/mains boundary, the workspace profile and the
556
+ // reply language. Without it, the runtime falls back to deepagents' generic
557
+ // assistant prompt — which is exactly the "upload your project" hallucination.
558
+ function activeRuntimeSystemPrompt(session, task, assignment) {
559
+ const capability = assignment?.capability ?? null;
560
+ const description = String(capability?.description ?? '').trim();
561
+ const language = session?.language ?? session?.wikircConfig?.language ?? null;
562
+ const profile = loadWorkspaceProfile(session?.workspacePath);
563
+ return [
564
+ 'You are the agentic analysis engine of a knowledge workspace (wikiLLM), executed behind the manager Donna.',
565
+ `Execute exactly ONE capability: ${task?.requiredCapability ?? 'unknown'}${description ? ` — ${description}` : ''}.`,
566
+ `Operation: ${task?.operation ?? 'run'}.`,
567
+ 'Boundary: you have READ tools only (the workspace wiki). You never modify the workspace — structural changes are proposals you return in your final answer (a planExpansionRequest), the manager integrates them under human approval. Side-effects on the outside world are gated by approval.',
568
+ 'Ground every claim in what the read tools return. Never invent pages, names, facts, jobs or results.',
569
+ 'Tool discipline: discover real page paths with the list/search tools BEFORE reading. Never guess a path — a read refused for "path not allowed" means the path was invented, so list/search first, then read exactly what exists.',
570
+ ...(language ? [`Reply in the workspace language: ${language}.`] : []),
571
+ ...(profile ? [`Workspace preferences — apply them to every reply:\n${profile}`] : []),
572
+ ].join('\n');
573
+ }
574
+
181
575
  function dispatchTaskActivity(session, task, assignment, jobId, statusTool, runId) {
182
576
  const activity = normalizeActivity({
183
577
  id: jobId,
@@ -215,7 +609,16 @@ function taskResultFromStatus(task, assignment, jobId, statusPayload, attempt =
215
609
  status: result.status ?? statusPayload?.status,
216
610
  outputRefs: Array.isArray(result.outputRefs) ? result.outputRefs : [],
217
611
  metrics: result.metrics ?? {},
218
- error: normalizeTaskError(result.error),
612
+ // The gateway reports its failure at the TOP level of the status payload
613
+ // ({ runId, status, error }), not inside `result`. Reading only
614
+ // `result.error` dropped the only actionable sentence ("no model…") and
615
+ // left Donna to invent a cause.
616
+ error: normalizeTaskError(result.error ?? statusPayload.error),
617
+ // Agent -> DAG (RFC § 30/31/41): a result may request the execution of a
618
+ // deterministic capability. Propagated here so resultAggregator.maybeExpandPlan
619
+ // picks it up and routes it through the manager's own resolution + approval
620
+ // — never a direct scheduler call by the runtime.
621
+ ...(result.planExpansionRequest ? { planExpansionRequest: result.planExpansionRequest } : {}),
219
622
  rawStatus: statusPayload,
220
623
  };
221
624
  }