@rubytech/taskmaster 1.14.2 → 1.16.1

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 (47) hide show
  1. package/dist/agents/skills/frontmatter.js +1 -0
  2. package/dist/agents/skills/workspace.js +64 -22
  3. package/dist/agents/system-prompt.js +1 -1
  4. package/dist/agents/taskmaster-tools.js +6 -4
  5. package/dist/agents/tool-policy.js +2 -1
  6. package/dist/agents/tools/contact-create-tool.js +4 -3
  7. package/dist/agents/tools/contact-delete-tool.js +3 -2
  8. package/dist/agents/tools/contact-lookup-tool.js +5 -4
  9. package/dist/agents/tools/contact-update-tool.js +6 -3
  10. package/dist/agents/tools/memory-tool.js +3 -1
  11. package/dist/agents/tools/qr-generate-tool.js +45 -0
  12. package/dist/agents/workspace-migrations.js +351 -0
  13. package/dist/build-info.json +3 -3
  14. package/dist/config/agent-tools-reconcile.js +79 -0
  15. package/dist/control-ui/assets/{index-B3nkSwMP.js → index-Bd75cI7J.js} +547 -573
  16. package/dist/control-ui/assets/index-Bd75cI7J.js.map +1 -0
  17. package/dist/control-ui/assets/index-BkymP95Y.css +1 -0
  18. package/dist/control-ui/index.html +2 -2
  19. package/dist/gateway/server-http.js +5 -0
  20. package/dist/gateway/server-methods/web.js +13 -0
  21. package/dist/gateway/server.impl.js +29 -1
  22. package/dist/hooks/bundled/ride-dispatch/HOOK.md +57 -0
  23. package/dist/hooks/bundled/ride-dispatch/handler.js +450 -0
  24. package/dist/hooks/bundled/ride-dispatch/stripe-webhook.js +191 -0
  25. package/dist/memory/internal.js +24 -1
  26. package/dist/memory/manager.js +3 -3
  27. package/dist/records/records-manager.js +7 -2
  28. package/package.json +1 -1
  29. package/skills/business-assistant/SKILL.md +1 -1
  30. package/skills/qr-code/SKILL.md +63 -0
  31. package/skills/sales-closer/SKILL.md +1 -1
  32. package/templates/beagle-zanzibar/agents/admin/AGENTS.md +67 -1
  33. package/templates/beagle-zanzibar/agents/public/AGENTS.md +102 -22
  34. package/templates/beagle-zanzibar/skills/beagle-zanzibar/SKILL.md +7 -8
  35. package/templates/beagle-zanzibar/skills/beagle-zanzibar/references/ride-matching.md +46 -55
  36. package/templates/customer/agents/admin/BOOTSTRAP.md +5 -1
  37. package/templates/customer/agents/public/AGENTS.md +1 -2
  38. package/templates/real-agent/skills/buyer-feedback/SKILL.md +111 -0
  39. package/templates/real-agent/skills/property-enquiry/SKILL.md +126 -0
  40. package/templates/real-agent/skills/valuation-booking/SKILL.md +182 -0
  41. package/templates/real-agent/skills/vendor-updates/SKILL.md +153 -0
  42. package/templates/real-agent/skills/viewing-management/SKILL.md +111 -0
  43. package/templates/taskmaster/agents/public/AGENTS.md +1 -1
  44. package/templates/taskmaster/agents/public/IDENTITY.md +1 -1
  45. package/templates/taskmaster/agents/public/SOUL.md +2 -2
  46. package/dist/control-ui/assets/index-B3nkSwMP.js.map +0 -1
  47. package/dist/control-ui/assets/index-l54GcTyj.css +0 -1
@@ -285,6 +285,352 @@ async function patchBeagleSkillRename(agentsPath, content) {
285
285
  return null;
286
286
  return content.replaceAll("zanzi-taxi", "beagle-zanzibar");
287
287
  }
288
+ // ---------------------------------------------------------------------------
289
+ // Migration: Beagle Zanzibar — prescribed ride request workflow (v1.14)
290
+ // ---------------------------------------------------------------------------
291
+ //
292
+ // The old "## Ride Request Flow" was a brief summary that left the agent
293
+ // guessing at tool calls. Replaced with a 12-step prescribed workflow with
294
+ // explicit tool invocations at each stage.
295
+ const OLD_RIDE_FLOW_HEADER = "## Ride Request Flow";
296
+ const NEW_RIDE_WORKFLOW_HEADER = "## Ride Request Workflow";
297
+ const BEAGLE_RIDE_WORKFLOW = `## Ride Request Workflow
298
+
299
+ This is a **prescribed workflow**. When a tourist requests a ride, execute every step in order. Do not stop, skip, or defer any step. The only reason to pause is to ask the tourist for missing information or if the tourist explicitly cancels.
300
+
301
+ ### Step 1 — Capture the request
302
+
303
+ Gather: pickup location, destination, date/time, number of passengers. If the tourist gave everything in one message, proceed immediately. If anything is missing, ask — then resume from Step 2 when they reply.
304
+
305
+ ### Step 2 — Check knowledge base
306
+
307
+ Call \`memory_search\` for the route. Note the fare range and journey time if found. If the route is not covered, note that — but proceed to Step 3 regardless. Never stop here.
308
+
309
+ ### Step 3 — Get the driver roster
310
+
311
+ Call \`contact_lookup\` with \`driver: true\`. This returns all registered drivers.
312
+
313
+ ### Step 4 — Check driver availability
314
+
315
+ For each driver returned in Step 3, call \`memory_get\` on \`memory/drivers/{name}.md\` to read their current \`status\`. Collect a list of drivers whose status is \`idle\`. Skip any driver whose status is \`awaiting_response\` or \`engaged\`.
316
+
317
+ ### Step 5 — Contact drivers
318
+
319
+ For up to 3 idle drivers from Step 4, call \`message\` to send each a negotiation request in Swahili. Include: the route, pickup time, number of passengers, and the job ID (\`[BGL-XXXX]\`). Before each send, call \`memory_write\` to set that driver's status to \`awaiting_response\`.
320
+
321
+ If no idle drivers are available, tell the tourist honestly and offer to try again later. This is the only permitted early exit other than tourist cancellation.
322
+
323
+ ### Step 6 — Notify the tourist
324
+
325
+ After sending all driver messages, respond to the tourist: confirm you've contacted drivers and are waiting for quotes. Do not send this message before completing Steps 3–5.
326
+
327
+ ### Step 7 — Present offers
328
+
329
+ When drivers respond with quotes, present up to 3 competing offers: fare, driver rating, vehicle type, estimated journey time. No driver personal details at this stage. See \`references/ride-matching.md\` for formatting.
330
+
331
+ ### Step 8 — Confirm booking
332
+
333
+ When the tourist chooses an offer, confirm the details and generate a Stripe payment link for the booking fee. See \`references/ride-matching.md\` Phase 4.
334
+
335
+ ### Step 9 — Post-payment
336
+
337
+ Once payment clears, send the tourist: driver name, phone number, vehicle details, plate number, and the pickup PIN. Send the driver: passenger details, pickup location, fare, and QR code. See \`references/pin-qr.md\`.
338
+
339
+ ### Step 10 — Record the booking
340
+
341
+ Call \`memory_write\` to create a structured record at \`memory/bookings/{job-id}.md\`. Update at each lifecycle event. See \`references/ride-matching.md\` Phase 7.
342
+
343
+ ### Step 11 — Driver reminders
344
+
345
+ For advance bookings, send tiered reminders via \`message\` (evening-before, 2-hour, 30-minute). Escalate immediately if a driver doesn't confirm. See \`references/ride-matching.md\` Phase 6.
346
+
347
+ ### Step 12 — Follow up
348
+
349
+ After estimated journey completion, prompt the tourist for feedback and collect ratings. See \`references/post-ride.md\`.`;
350
+ async function patchBeagleRideWorkflow(_agentsPath, content) {
351
+ if (!isBeaglePublicAgent(content))
352
+ return null;
353
+ // Already migrated?
354
+ if (content.includes(NEW_RIDE_WORKFLOW_HEADER))
355
+ return null;
356
+ // Old section not present?
357
+ if (!content.includes(OLD_RIDE_FLOW_HEADER))
358
+ return null;
359
+ // Find the old section boundaries: from "## Ride Request Flow" to the next "---"
360
+ const startIdx = content.indexOf(OLD_RIDE_FLOW_HEADER);
361
+ const separatorIdx = content.indexOf("\n---", startIdx + OLD_RIDE_FLOW_HEADER.length);
362
+ if (separatorIdx === -1)
363
+ return null;
364
+ return content.slice(0, startIdx) + BEAGLE_RIDE_WORKFLOW + content.slice(separatorIdx);
365
+ }
366
+ // ---------------------------------------------------------------------------
367
+ // Migration: Narrow escalation clause (v1.14)
368
+ // ---------------------------------------------------------------------------
369
+ //
370
+ // "Anything you're uncertain about" in escalation sections gives public agents
371
+ // a blanket excuse to defer any uncomfortable moment (including closing a sale).
372
+ // Replaced with a specific, bounded criterion.
373
+ const BLANKET_ESCALATION = "Anything you're uncertain about";
374
+ const NARROW_ESCALATION = "Technical questions you cannot answer from memory or product knowledge";
375
+ function hasBlanketEscalation(content) {
376
+ return content.toLowerCase().includes(BLANKET_ESCALATION.toLowerCase());
377
+ }
378
+ async function patchEscalationClause(_agentsPath, content) {
379
+ if (!isPublicAgent(content))
380
+ return null;
381
+ if (!hasBlanketEscalation(content))
382
+ return null;
383
+ // Replace all case-insensitive occurrences of the blanket clause
384
+ return content.replace(new RegExp(BLANKET_ESCALATION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), NARROW_ESCALATION);
385
+ }
386
+ // ---------------------------------------------------------------------------
387
+ // Migration: Beagle Zanzibar — Dispatch-based workflow (v1.15)
388
+ // ---------------------------------------------------------------------------
389
+ //
390
+ // Replaces the 12-step workflow that used `contact_lookup` and `message`
391
+ // directly with a 10-step dispatch-based workflow. The public agent now
392
+ // writes dispatch files; the admin agent handles privileged operations.
393
+ const DISPATCH_WORKFLOW_MARKER = "shared/dispatch/";
394
+ const BEAGLE_DISPATCH_WORKFLOW = `## Ride Request Workflow
395
+
396
+ This is a **prescribed workflow**. When a tourist requests a ride, execute every step in order. Do not stop, skip, or defer any step. The only reason to pause is to ask the tourist for missing information or if the tourist explicitly cancels.
397
+
398
+ ### Step 1 — Capture the request
399
+
400
+ Gather: pickup location, destination, date/time, number of passengers, luggage, special requests. If the tourist gave everything in one message, proceed immediately. If anything is missing, ask — then resume from Step 2 when they reply.
401
+
402
+ ### Step 2 — Check knowledge base
403
+
404
+ Call \`memory_search\` for the route. Note the fare range and journey time if found. If the route is not covered, note that — but proceed to Step 3 regardless. Never stop here.
405
+
406
+ ### Step 3 — Generate job ID
407
+
408
+ Create a booking job ID in the format \`BGL-XXXX\` (e.g. \`BGL-0042\`). Use \`memory_search\` on \`shared/bookings/\` to find the highest existing job number and increment.
409
+
410
+ ### Step 4 — Dispatch trip request
411
+
412
+ Write a dispatch file via \`memory_write\` to \`shared/dispatch/{job-id}-trip-request.md\` with the following format:
413
+
414
+ \\\`\\\`\\\`
415
+ # Dispatch: Trip Request
416
+ job_id: BGL-XXXX
417
+ phase: trip-request
418
+ tourist_phone: +XXXXXXXXXXX
419
+ tourist_name: [name if given]
420
+ pickup: [pickup location]
421
+ destination: [destination]
422
+ date: [date]
423
+ time: [time]
424
+ passengers: [count]
425
+ luggage: [description]
426
+ special_requests: [any requests or "none"]
427
+ fare_estimate: [range from knowledge base or "unknown"]
428
+ account_id: [your account ID]
429
+ \\\`\\\`\\\`
430
+
431
+ ### Step 5 — Notify the tourist
432
+
433
+ After writing the dispatch file, tell the tourist: "I'm reaching out to our drivers now. I'll have quotes for you shortly." Do not send this before writing the dispatch file.
434
+
435
+ ### Step 6 — Present offers
436
+
437
+ When driver offers appear in your conversation (injected by the operations agent), present up to 3 competing offers to the tourist: fare, driver rating, vehicle type, estimated journey time. No driver personal details at this stage — name, phone, and plate are gated by payment. See \`references/ride-matching.md\` for formatting.
438
+
439
+ ### Step 7 — Confirm booking
440
+
441
+ When the tourist chooses an offer, confirm the details and write a booking confirmation dispatch via \`memory_write\` to \`shared/dispatch/{job-id}-booking-confirm.md\`.
442
+
443
+ ### Step 8 — Payment
444
+
445
+ The operations agent will generate a Stripe payment link and send it directly to the tourist. Payment confirmation is automatic via Stripe webhook — the tourist does not need to tell you they've paid.
446
+
447
+ ### Step 9 — Post-payment
448
+
449
+ Once payment is confirmed (the operations agent will inject driver details and pickup PIN into the conversation), acknowledge the details to the tourist. Explain the PIN verification process.
450
+
451
+ ### Step 10 — Record and follow up
452
+
453
+ After estimated journey completion, prompt the tourist for feedback and collect ratings. See \`references/post-ride.md\`.`;
454
+ async function patchBeaglePublicDispatchWorkflow(_agentsPath, content) {
455
+ if (!isBeaglePublicAgent(content))
456
+ return null;
457
+ // Already migrated to dispatch model?
458
+ if (content.includes(DISPATCH_WORKFLOW_MARKER))
459
+ return null;
460
+ // Must have the old workflow to replace
461
+ if (!content.includes(NEW_RIDE_WORKFLOW_HEADER))
462
+ return null;
463
+ // Find the workflow section boundaries
464
+ const startIdx = content.indexOf(NEW_RIDE_WORKFLOW_HEADER);
465
+ if (startIdx === -1)
466
+ return null;
467
+ // Find the end: next "---" separator or "## " heading at same level
468
+ const afterHeader = startIdx + NEW_RIDE_WORKFLOW_HEADER.length;
469
+ const nextSeparator = content.indexOf("\n---", afterHeader);
470
+ const nextH2 = content.indexOf("\n## ", afterHeader);
471
+ let endIdx;
472
+ if (nextSeparator !== -1 && (nextH2 === -1 || nextSeparator < nextH2)) {
473
+ endIdx = nextSeparator;
474
+ }
475
+ else if (nextH2 !== -1) {
476
+ endIdx = nextH2;
477
+ }
478
+ else {
479
+ endIdx = content.length;
480
+ }
481
+ return content.slice(0, startIdx) + BEAGLE_DISPATCH_WORKFLOW + content.slice(endIdx);
482
+ }
483
+ // ---------------------------------------------------------------------------
484
+ // Migration: Beagle Zanzibar — Public agent tools dispatch (v1.15)
485
+ // ---------------------------------------------------------------------------
486
+ //
487
+ // Replaces the public agent's tools table to remove `contact_lookup` and
488
+ // `message`. These are now handled by the admin agent via dispatch.
489
+ const DISPATCH_TOOLS_MARKER = "dispatch requests";
490
+ const BEAGLE_DISPATCH_TOOLS_SECTION = `## Tools
491
+
492
+ | Tool | Use |
493
+ |------|-----|
494
+ | \`memory_search\` | Find bookings, knowledge base content |
495
+ | \`memory_get\` | Read specific files (bookings, knowledge base) |
496
+ | \`memory_write\` | Write dispatch requests, update booking state, store tourist preferences |
497
+ | \`memory_save_media\` | Save media files sent by tourists |
498
+ | \`web_search\` | Search the web for tourist queries |
499
+ | \`web_fetch\` | Fetch web content |
500
+ | \`current_time\` | Timestamps for booking records |
501
+
502
+ You do not have \`message\` or \`contact_lookup\` tools. Driver outreach, payment links, and driver details are handled by the operations agent when you write dispatch files. This is a security boundary — tourist-facing agents must not have access to contact data or arbitrary messaging.`;
503
+ async function patchBeaglePublicToolsDispatch(_agentsPath, content) {
504
+ if (!isBeaglePublicAgent(content))
505
+ return null;
506
+ // Already migrated?
507
+ if (content.includes(DISPATCH_TOOLS_MARKER))
508
+ return null;
509
+ // Must have a ## Tools section to replace
510
+ if (!content.includes("## Tools"))
511
+ return null;
512
+ // Find the tools section
513
+ const toolsStart = content.indexOf("## Tools");
514
+ if (toolsStart === -1)
515
+ return null;
516
+ // Find end of tools section: next "---" or next "## " heading
517
+ const afterTools = toolsStart + "## Tools".length;
518
+ const nextSeparator = content.indexOf("\n---", afterTools);
519
+ const nextH2 = content.indexOf("\n## ", afterTools);
520
+ let endIdx;
521
+ if (nextSeparator !== -1 && (nextH2 === -1 || nextSeparator < nextH2)) {
522
+ endIdx = nextSeparator;
523
+ }
524
+ else if (nextH2 !== -1) {
525
+ endIdx = nextH2;
526
+ }
527
+ else {
528
+ endIdx = content.length;
529
+ }
530
+ return content.slice(0, toolsStart) + BEAGLE_DISPATCH_TOOLS_SECTION + content.slice(endIdx);
531
+ }
532
+ // ---------------------------------------------------------------------------
533
+ // Migration: Beagle Zanzibar — Admin dispatch instructions (v1.15)
534
+ // ---------------------------------------------------------------------------
535
+ //
536
+ // Adds the "Ride Dispatch Processing" section to the admin agent and
537
+ // the `message` tool to its tools table.
538
+ const ADMIN_DISPATCH_MARKER = "## Ride Dispatch Processing";
539
+ const BEAGLE_ADMIN_DISPATCH_SECTION = `## Ride Dispatch Processing
540
+
541
+ Messages prefixed with \`[System: Ride Dispatch — ...]\` are automated dispatches from the ride-dispatch hook. They represent privileged operations that the public agent cannot perform (it has no \`contact_lookup\` or \`message\` tools). Process each dispatch type as instructed.
542
+
543
+ ### Trip Request
544
+
545
+ When you receive \`[System: Ride Dispatch — Trip Request]\`:
546
+
547
+ 1. Call \`contact_lookup\` to get the driver roster
548
+ 2. For each driver, call \`memory_get\` on \`drivers/{name}.md\` to check their status
549
+ 3. Select up to 3 idle drivers, preferring those with route history for the requested route
550
+ 4. For each selected driver:
551
+ - Update their status to \`awaiting_response\` via \`memory_write\`
552
+ - Write \`shared/active-negotiations/{driver-phone-digits}.md\` with \`job_id\`, \`driver_name\`, and \`contacted_at\`
553
+ 5. Message each driver in Swahili via the \`message\` tool with route details, pickup time, passengers, and job ID
554
+ 6. Message the tourist confirming drivers have been contacted and quotes are being gathered
555
+ 7. When driver replies arrive (dispatched as \`[System: Ride Dispatch — Driver Reply]\`), compile offers
556
+ 8. Message the tourist with up to 3 competing offers — fare, rating, vehicle type, journey time. Do NOT reveal driver name, phone, or plate (gated by payment)
557
+
558
+ ### Booking Confirmation
559
+
560
+ When you receive \`[System: Ride Dispatch — Booking Confirmation]\`:
561
+
562
+ 1. Load the \`stripe\` skill and generate a Checkout Session for the booking fee
563
+ - Set metadata: \`booking_id\`, \`tourist_phone\`, \`account_id\` (for webhook routing)
564
+ 2. Message the tourist with the payment link and booking terms
565
+ 3. Record booking details in \`shared/bookings/{job-id}.md\` via \`memory_write\`
566
+ 4. Clear \`shared/active-negotiations/{phone}.md\` for drivers NOT selected
567
+
568
+ ### Payment Confirmed
569
+
570
+ When you receive \`[System: Ride Dispatch — Payment Confirmed]\`:
571
+
572
+ 1. Read the booking record at \`shared/bookings/{job-id}.md\` for driver details
573
+ 2. Generate the pickup PIN and QR code (see \`references/pin-qr.md\`)
574
+ 3. Message the tourist with: driver name, phone, vehicle details, plate, and pickup PIN
575
+ 4. Message the driver with: passenger name, pickup time/location, fare, and QR code URL
576
+ 5. Update the booking record status to \`confirmed\`
577
+ 6. Clear the active negotiation file for the confirmed driver
578
+
579
+ ### Driver Reply
580
+
581
+ When you receive \`[System: Ride Dispatch — Driver Reply]\`:
582
+
583
+ Process the driver's message in the context of the ongoing negotiation. If it's a fare quote, note it. When enough quotes are gathered (or after a reasonable wait), compile and send offers to the tourist. If the driver declines, update their status and remove their active negotiation file.
584
+
585
+ ### Active Negotiation Index
586
+
587
+ When contacting drivers, write \`shared/active-negotiations/{phone-digits}.md\` for each:
588
+
589
+ \\\`\\\`\\\`
590
+ job_id: BGL-XXXX
591
+ driver_name: [name]
592
+ contacted_at: [timestamp]
593
+ \\\`\\\`\\\`
594
+
595
+ Clear these files when:
596
+ - A driver declines or is not selected
597
+ - The booking is confirmed (keep only the selected driver's file until pickup completes)
598
+ - A negotiation expires with no response
599
+
600
+ This index enables the hook to route driver WhatsApp replies to the correct ride session without requiring drivers to include job IDs in their messages.`;
601
+ const ADMIN_MESSAGE_TOOL_ROW = `| \`message\` | Send WhatsApp messages to drivers and tourists (ride dispatch, reminders, payment links) |`;
602
+ async function patchBeagleAdminDispatchInstructions(_agentsPath, content) {
603
+ if (!isBeagleTaxiAdmin(content))
604
+ return null;
605
+ // Already migrated?
606
+ if (content.includes(ADMIN_DISPATCH_MARKER))
607
+ return null;
608
+ let result = content;
609
+ // Add `message` tool if not present
610
+ if (!result.includes("`message`")) {
611
+ const sessionsListRow = "| `sessions_list`";
612
+ const idx = result.indexOf(sessionsListRow);
613
+ if (idx !== -1) {
614
+ result = result.slice(0, idx) + ADMIN_MESSAGE_TOOL_ROW + "\n" + result.slice(idx);
615
+ }
616
+ }
617
+ // Insert dispatch section before "## Operational Focus Areas"
618
+ const insertBefore = "## Operational Focus Areas";
619
+ const idx = result.indexOf(insertBefore);
620
+ if (idx !== -1) {
621
+ result =
622
+ result.slice(0, idx) + BEAGLE_ADMIN_DISPATCH_SECTION + "\n\n---\n\n" + result.slice(idx);
623
+ }
624
+ else {
625
+ result = result.trimEnd() + "\n\n---\n\n" + BEAGLE_ADMIN_DISPATCH_SECTION + "\n";
626
+ }
627
+ // Update boundaries: remove "Interact with tourists directly" if present
628
+ const oldBoundary = "- Interact with tourists directly\n";
629
+ if (result.includes(oldBoundary)) {
630
+ result = result.replace(oldBoundary, "- Message tourists outside of ride dispatch processing (dispatch messages are sent on behalf of the system, not as conversational interaction)\n");
631
+ }
632
+ return result;
633
+ }
288
634
  const MIGRATIONS = [
289
635
  { name: "skill-recommendations", apply: patchSkillRecommendations },
290
636
  { name: "owner-learning", apply: patchOwnerLearning },
@@ -294,6 +640,11 @@ const MIGRATIONS = [
294
640
  { name: "beagle-driver-substitution", apply: patchBeagleDriverSubstitution },
295
641
  { name: "beagle-public-tools", apply: patchBeaglePublicTools },
296
642
  { name: "beagle-skill-rename", apply: patchBeagleSkillRename },
643
+ { name: "beagle-ride-workflow", apply: patchBeagleRideWorkflow },
644
+ { name: "narrow-escalation", apply: patchEscalationClause },
645
+ { name: "beagle-public-dispatch-workflow", apply: patchBeaglePublicDispatchWorkflow },
646
+ { name: "beagle-public-tools-dispatch", apply: patchBeaglePublicToolsDispatch },
647
+ { name: "beagle-admin-dispatch-instructions", apply: patchBeagleAdminDispatchInstructions },
297
648
  ];
298
649
  /**
299
650
  * Run all workspace migrations for every configured agent.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.14.2",
3
- "commit": "f271b744c2d7ecc57dd62feadb9187cbc128b875",
4
- "builtAt": "2026-03-04T07:40:11.872Z"
2
+ "version": "1.16.1",
3
+ "commit": "cefc01b75d3ff0790f81d413cbc5273afa2af452",
4
+ "builtAt": "2026-03-04T17:39:48.836Z"
5
5
  }
@@ -19,6 +19,12 @@ function isAdminAgent(agent) {
19
19
  const id = agent.id?.trim() ?? "";
20
20
  return id === "admin" || id.endsWith("-admin");
21
21
  }
22
+ /** Tools that must be removed from Beagle public agents (privilege separation). */
23
+ const BEAGLE_PUBLIC_RESTRICTED_TOOLS = ["message", "contact_lookup", "group:contacts"];
24
+ function isBeaglePublicAgent(agent) {
25
+ const id = (agent.id ?? "").trim().toLowerCase();
26
+ return id.includes("beagle") && id.endsWith("-public");
27
+ }
22
28
  /**
23
29
  * Ensure admin agents use `group:contacts` instead of individual contact tools.
24
30
  *
@@ -118,3 +124,76 @@ export function reconcileStaleToolEntries(params) {
118
124
  }
119
125
  return { config, changes };
120
126
  }
127
+ /**
128
+ * Add `qr_generate` to any agent whose explicit allow list contains `document_to_pdf`.
129
+ *
130
+ * Both tools are in `group:documents`. Agents that reference the group by name already
131
+ * receive `qr_generate` automatically. Agents with explicit allow lists do not — this
132
+ * reconciliation patches them on gateway startup.
133
+ *
134
+ * Idempotent — skips agents that already have `qr_generate`.
135
+ */
136
+ export function reconcileQrGenerateTool(params) {
137
+ const config = structuredClone(params.config);
138
+ const changes = [];
139
+ const agents = config.agents?.list;
140
+ if (!Array.isArray(agents))
141
+ return { config, changes };
142
+ for (const agent of agents) {
143
+ if (!agent)
144
+ continue;
145
+ const allow = agent.tools?.allow;
146
+ if (!Array.isArray(allow))
147
+ continue;
148
+ // Only patch agents that explicitly have document_to_pdf and lack qr_generate
149
+ if (!allow.includes("document_to_pdf"))
150
+ continue;
151
+ if (allow.includes("qr_generate"))
152
+ continue;
153
+ const idx = allow.indexOf("document_to_pdf");
154
+ allow.splice(idx + 1, 0, "qr_generate");
155
+ changes.push(`Added qr_generate to agent "${agent.id ?? "<unnamed>"}" tools.allow.`);
156
+ }
157
+ return { config, changes };
158
+ }
159
+ /**
160
+ * Remove privileged tools (`message`, `contact_lookup`, `group:contacts`) from
161
+ * Beagle public agent allow lists.
162
+ *
163
+ * The public agent dispatches ride requests via memory files; the admin agent
164
+ * handles all privileged operations. Keeping `message` or contact tools on the
165
+ * public agent exposes driver PII to prompt injection.
166
+ *
167
+ * Runs unconditionally on gateway startup. Idempotent — skips agents that
168
+ * don't have any restricted tools.
169
+ */
170
+ export function reconcileBeaglePublicTools(params) {
171
+ const config = structuredClone(params.config);
172
+ const changes = [];
173
+ const agents = config.agents?.list;
174
+ if (!Array.isArray(agents))
175
+ return { config, changes };
176
+ for (const agent of agents) {
177
+ if (!agent || !isBeaglePublicAgent(agent))
178
+ continue;
179
+ const allow = agent.tools?.allow;
180
+ if (!Array.isArray(allow))
181
+ continue;
182
+ for (const tool of BEAGLE_PUBLIC_RESTRICTED_TOOLS) {
183
+ const idx = allow.indexOf(tool);
184
+ if (idx !== -1) {
185
+ allow.splice(idx, 1);
186
+ changes.push(`Removed ${tool} from agent "${agent.id}" tools.allow (privilege separation).`);
187
+ }
188
+ }
189
+ // Also remove individual contact tools that might have been added directly
190
+ for (const tool of INDIVIDUAL_CONTACT_TOOLS) {
191
+ const idx = allow.indexOf(tool);
192
+ if (idx !== -1) {
193
+ allow.splice(idx, 1);
194
+ changes.push(`Removed ${tool} from agent "${agent.id}" tools.allow (privilege separation).`);
195
+ }
196
+ }
197
+ }
198
+ return { config, changes };
199
+ }