agent-relay-server 0.123.2 → 0.124.0

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 (68) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-NCggGKTA.js → MessageBubble-SQK8-Mzp.js} +2 -2
  4. package/public/assets/{MessageBubble-NCggGKTA.js.map → MessageBubble-SQK8-Mzp.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-CSdszmGk.js → PlanGraphPanel-CS-aow4S.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CSdszmGk.js.map → PlanGraphPanel-CS-aow4S.js.map} +1 -1
  7. package/public/assets/{activity-Cyp2nolX.js → activity-W31XvhC2.js} +2 -2
  8. package/public/assets/{activity-Cyp2nolX.js.map → activity-W31XvhC2.js.map} +1 -1
  9. package/public/assets/{agent-profiles-D5Izg0Vn.js → agent-profiles-DXysA9To.js} +2 -2
  10. package/public/assets/{agent-profiles-D5Izg0Vn.js.map → agent-profiles-DXysA9To.js.map} +1 -1
  11. package/public/assets/{agents-DIOxuJ6M.js → agents-B5GRNRnN.js} +2 -2
  12. package/public/assets/{agents-DIOxuJ6M.js.map → agents-B5GRNRnN.js.map} +1 -1
  13. package/public/assets/{automation-Bn6mOLAR.js → automation-DYlgHYcb.js} +2 -2
  14. package/public/assets/{automation-Bn6mOLAR.js.map → automation-DYlgHYcb.js.map} +1 -1
  15. package/public/assets/{chat-eUaPz399.js → chat-Clz6qOtn.js} +2 -2
  16. package/public/assets/{chat-eUaPz399.js.map → chat-Clz6qOtn.js.map} +1 -1
  17. package/public/assets/{formatted-body-CvrQ2D1J.js → formatted-body-BYQ-SPtj.js} +3 -3
  18. package/public/assets/{formatted-body-CvrQ2D1J.js.map → formatted-body-BYQ-SPtj.js.map} +1 -1
  19. package/public/assets/{formatted-body-impl-BJ7jdbpR.js → formatted-body-impl-L1EMnNW6.js} +2 -2
  20. package/public/assets/{formatted-body-impl-BJ7jdbpR.js.map → formatted-body-impl-L1EMnNW6.js.map} +1 -1
  21. package/public/assets/{index-DnUo0Zqg.js → index-0uIGXT14.js} +6 -6
  22. package/public/assets/{index-DnUo0Zqg.js.map → index-0uIGXT14.js.map} +1 -1
  23. package/public/assets/index-DOJ7KidA.css +2 -0
  24. package/public/assets/{insights-CQGH5LhD.js → insights-DwZxq1Dn.js} +2 -2
  25. package/public/assets/{insights-CQGH5LhD.js.map → insights-DwZxq1Dn.js.map} +1 -1
  26. package/public/assets/{maintenance-7c7UvpAt.js → maintenance-COcmUDnf.js} +2 -2
  27. package/public/assets/{maintenance-7c7UvpAt.js.map → maintenance-COcmUDnf.js.map} +1 -1
  28. package/public/assets/{managed-agents-yh4HPO5a.js → managed-agents-D9-dgMeX.js} +2 -2
  29. package/public/assets/{managed-agents-yh4HPO5a.js.map → managed-agents-D9-dgMeX.js.map} +1 -1
  30. package/public/assets/{markdown-preview-impl-BuOn5-Uc.js → markdown-preview-impl-DH372bL-.js} +2 -2
  31. package/public/assets/{markdown-preview-impl-BuOn5-Uc.js.map → markdown-preview-impl-DH372bL-.js.map} +1 -1
  32. package/public/assets/{memory-Qv0JN8CE.js → memory-B9hRK8sk.js} +2 -2
  33. package/public/assets/{memory-Qv0JN8CE.js.map → memory-B9hRK8sk.js.map} +1 -1
  34. package/public/assets/{messages-CgjHJmc2.js → messages-BHkHlXzF.js} +2 -2
  35. package/public/assets/{messages-CgjHJmc2.js.map → messages-BHkHlXzF.js.map} +1 -1
  36. package/public/assets/{orchestrators-CgL4CFzb.js → orchestrators-CXy3SDSn.js} +2 -2
  37. package/public/assets/{orchestrators-CgL4CFzb.js.map → orchestrators-CXy3SDSn.js.map} +1 -1
  38. package/public/assets/{overview-Bri04eYE.js → overview-D2yRyCQq.js} +2 -2
  39. package/public/assets/{overview-Bri04eYE.js.map → overview-D2yRyCQq.js.map} +1 -1
  40. package/public/assets/{pairs-C78LwUH2.js → pairs-5tXdHylI.js} +2 -2
  41. package/public/assets/{pairs-C78LwUH2.js.map → pairs-5tXdHylI.js.map} +1 -1
  42. package/public/assets/{prompt-settings-DDEkMqPZ.js → prompt-settings-Bn5mmi9w.js} +2 -2
  43. package/public/assets/{prompt-settings-DDEkMqPZ.js.map → prompt-settings-Bn5mmi9w.js.map} +1 -1
  44. package/public/assets/registry-C3sB7qv5.js +4 -0
  45. package/public/assets/registry-C3sB7qv5.js.map +1 -0
  46. package/public/assets/{security-wvnRKFvm.js → security-CmD-YE-_.js} +2 -2
  47. package/public/assets/{security-wvnRKFvm.js.map → security-CmD-YE-_.js.map} +1 -1
  48. package/public/assets/{select-BIfbVmLf.js → select-C0UlhK2k.js} +2 -2
  49. package/public/assets/{select-BIfbVmLf.js.map → select-C0UlhK2k.js.map} +1 -1
  50. package/public/assets/{settings-B6Ntahx7.js → settings-DrqMG5nG.js} +2 -2
  51. package/public/assets/{settings-B6Ntahx7.js.map → settings-DrqMG5nG.js.map} +1 -1
  52. package/public/assets/{tasks-CUglafo5.js → tasks-ChQjtXHu.js} +2 -2
  53. package/public/assets/{tasks-CUglafo5.js.map → tasks-ChQjtXHu.js.map} +1 -1
  54. package/public/assets/{teams-EE8RHhHM.js → teams-itjNlDLp.js} +2 -2
  55. package/public/assets/{teams-EE8RHhHM.js.map → teams-itjNlDLp.js.map} +1 -1
  56. package/public/assets/{terminal-viewer-impl-DBRkdyag.js → terminal-viewer-impl-B9M6s2vB.js} +2 -2
  57. package/public/assets/{terminal-viewer-impl-DBRkdyag.js.map → terminal-viewer-impl-B9M6s2vB.js.map} +1 -1
  58. package/public/assets/{work-queue-BFQVVL4T.js → work-queue-Kw0uMkEF.js} +2 -2
  59. package/public/assets/{work-queue-BFQVVL4T.js.map → work-queue-Kw0uMkEF.js.map} +1 -1
  60. package/public/index.html +3 -3
  61. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  62. package/src/db/provisioning-registry.ts +174 -1
  63. package/src/routes/provisioning-registry.ts +3 -0
  64. package/src/routing-policy-store.ts +55 -0
  65. package/src/spawn-targets.ts +130 -28
  66. package/public/assets/index-DcmG5gLR.css +0 -2
  67. package/public/assets/registry-5KhP5FE5.js +0 -2
  68. package/public/assets/registry-5KhP5FE5.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{a as e,gt as t,jn as n,rr as r,yn as i}from"./lucide-react-CoU_Xts2.js";import{p as a,t as o}from"./store-zFLzd0Jd.js";import{K as s,ct as c,d as l}from"./display-GFbGUG0O.js";import{t as u}from"./badge-XEru6wto.js";import{t as d}from"./button-C3CQSr4p.js";import{At as f,G as p,wt as m}from"./index-DnUo0Zqg.js";import{r as h,t as g}from"./card-C5XTIPvq.js";var _=r(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=m(),S=f(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`T${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(p,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(i,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-BFQVVL4T.js.map
1
+ import{a as e,gt as t,jn as n,rr as r,yn as i}from"./lucide-react-CoU_Xts2.js";import{p as a,t as o}from"./store-zFLzd0Jd.js";import{K as s,ct as c,d as l}from"./display-GFbGUG0O.js";import{t as u}from"./badge-XEru6wto.js";import{t as d}from"./button-C3CQSr4p.js";import{At as f,G as p,wt as m}from"./index-0uIGXT14.js";import{r as h,t as g}from"./card-C5XTIPvq.js";var _=r(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),n=o(e=>e.agentsById),r=o(e=>e.compose),i=o(e=>e.set),s=o(e=>e.doClaimTask),c=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=m(),S=f(),C=S.filter(e=>e.claimable).length;function w(e){if(!r.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&c(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`T${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(t,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:r.from,onChange:e=>i({compose:{...r,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:l(e)},e.id))]})]})]}),(0,_.jsx)(p,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(t=>(0,_.jsx)(b,{item:t,now:e,agentName:e=>n[e]?l(n[e]):e.slice(-10),onClaim:()=>w(t),onCancel:()=>T(t),onEvents:t.task?()=>E(t):void 0},t.id))]})})]})}function b({item:t,now:r,agentName:a,onClaim:o,onCancel:l,onEvents:f}){let p=c[t.severity]||c.info,m=v[t.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:t.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:t.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:t.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:t.status}),t.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),t.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:t.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[t.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(i,{className:`w-3 h-3 inline mr-1`}),a(t.owner)]}),t.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:t.source}),(0,_.jsxs)(`span`,{title:String(t.updatedAt),children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),s(r,t.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[t.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(i,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:l,children:[(0,_.jsx)(e,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-Kw0uMkEF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-BFQVVL4T.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `T${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"work-queue-Kw0uMkEF.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `T${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
package/public/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
13
13
  <link rel="manifest" href="manifest.webmanifest">
14
14
  <link rel="apple-touch-icon" href="icons/agent-relay-192.png">
15
- <script type="module" crossorigin src="./assets/index-DnUo0Zqg.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-0uIGXT14.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-C_JMBXql.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-CoU_Xts2.js">
@@ -36,12 +36,12 @@
36
36
  <link rel="modulepreload" crossorigin href="./assets/agent-type-icon-BxUopALO.js">
37
37
  <link rel="modulepreload" crossorigin href="./assets/button-C3CQSr4p.js">
38
38
  <link rel="modulepreload" crossorigin href="./assets/copy-button-Bil2CjzT.js">
39
- <link rel="modulepreload" crossorigin href="./assets/formatted-body-CvrQ2D1J.js">
39
+ <link rel="modulepreload" crossorigin href="./assets/formatted-body-BYQ-SPtj.js">
40
40
  <link rel="modulepreload" crossorigin href="./assets/task-chip-BHM-e3I8.js">
41
41
  <link rel="modulepreload" crossorigin href="./assets/badge-XEru6wto.js">
42
42
  <link rel="modulepreload" crossorigin href="./assets/input-CXMbJ848.js">
43
43
  <link rel="modulepreload" crossorigin href="./assets/themes-DjSwmAZN.js">
44
- <link rel="stylesheet" crossorigin href="./assets/index-DcmG5gLR.css">
44
+ <link rel="stylesheet" crossorigin href="./assets/index-DOJ7KidA.css">
45
45
  </head>
46
46
  <body class="bg-background text-foreground antialiased">
47
47
  <div id="root"></div>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.123.2",
4
+ "version": "0.124.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -10,7 +10,12 @@ import { errMessage, isRecord, SPAWN_PROVIDERS } from "agent-relay-sdk";
10
10
  import type {
11
11
  AgentProfileProvider,
12
12
  HookAssetDefinition,
13
+ McpApplicability,
13
14
  McpAssetDefinition,
15
+ McpEnvValue,
16
+ McpHealthProbe,
17
+ McpRunConfig,
18
+ McpSetupRecipe,
14
19
  PluginAssetDefinition,
15
20
  ProvisioningCapability,
16
21
  ProvisioningAsset,
@@ -41,6 +46,158 @@ const BUNDLED_SOURCE_TYPE = "bundled";
41
46
  // (src/db/provisioning-callmux.ts) when splitting literals from placeholders.
42
47
  export const SHARED_CALLMUX_CONFIG_ENV_ALLOWLIST: Readonly<Record<string, readonly string[]>> = { vent: ["VENT_REPO"] };
43
48
 
49
+ // #1030 slice 1 (#1110) — MCP setup recipe / run config validation.
50
+ const MCP_SETUP_KINDS = ["npm-global", "download", "bundled"] as const;
51
+ const MCP_TRANSPORTS = ["stdio", "ws", "http"] as const;
52
+ const MCP_HEALTH_PROBE_KINDS = ["tcp", "http", "none"] as const;
53
+ const MCP_LISTENER_MODES = ["shared", "per-agent"] as const;
54
+ // Exact pin only — no ranges ("^1.2.3"), tags ("latest", "next"), or partial versions
55
+ // ("1.2"). The same recipe must reproduce byte-for-byte on every host.
56
+ const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
57
+ const SHA256_HEX = /^[a-f0-9]{64}$/i;
58
+ // callmux's own secret-ref grammar (`env:NAME`, `file:/path`), plus `secret:<name>` for
59
+ // the relay-owned secret store a follow-on issue (#1111) resolves. A ref is stored and
60
+ // validated for shape only in this slice — no resolution (env lookup / file read /
61
+ // secret-store fetch) happens here.
62
+ const MCP_ENV_REF_SCHEMES = ["secret", "env", "file"] as const;
63
+ // Content-shaped credential detection for literal runConfig.env values (#1030 review
64
+ // steer): a value that looks like a real token/key must be rejected in favor of a
65
+ // `secret:<name>` reference — the registry never carries a resolved secret.
66
+ const INLINE_SECRET_CONTENT_PATTERNS: RegExp[] = [
67
+ /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i,
68
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
69
+ /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/,
70
+ /\bsk-[A-Za-z0-9_-]{20,}\b/,
71
+ /\bAKIA[0-9A-Z]{16}\b/,
72
+ /^Bearer\s+\S{16,}$/i,
73
+ ];
74
+
75
+ function looksLikeInlineSecret(value: string): boolean {
76
+ if (INLINE_SECRET_CONTENT_PATTERNS.some((pattern) => pattern.test(value))) return true;
77
+ // Crude high-entropy heuristic: a long token-shaped string mixing upper/lower/digit.
78
+ return value.length >= 24 && /^[A-Za-z0-9_.-]+$/.test(value) && /[0-9]/.test(value) && /[A-Z]/.test(value) && /[a-z]/.test(value);
79
+ }
80
+
81
+ function assertMcpSetupRecipe(value: unknown): McpSetupRecipe {
82
+ if (!isRecord(value)) throw new ValidationError("mcp setup recipe must be an object");
83
+ const kind = value.kind;
84
+ if (kind === "npm-global") {
85
+ const pkg = value.pkg;
86
+ const version = value.version;
87
+ if (typeof pkg !== "string" || !pkg.trim()) throw new ValidationError("mcp setup npm-global requires pkg");
88
+ if (typeof version !== "string" || !EXACT_SEMVER.test(version)) {
89
+ throw new ValidationError("mcp setup npm-global requires an exact pinned version (e.g. 1.2.3), not a range or tag");
90
+ }
91
+ return { kind, pkg, version };
92
+ }
93
+ if (kind === "download") {
94
+ const url = value.url;
95
+ const sha256 = value.sha256;
96
+ const bin = value.bin;
97
+ if (typeof url !== "string" || !/^https:\/\//.test(url)) throw new ValidationError("mcp setup download requires an https url");
98
+ if (typeof sha256 !== "string" || !SHA256_HEX.test(sha256)) throw new ValidationError("mcp setup download requires a sha256 checksum (64 hex chars)");
99
+ if (typeof bin !== "string" || !bin.trim()) throw new ValidationError("mcp setup download requires bin");
100
+ return { kind, url, sha256: sha256.toLowerCase(), bin };
101
+ }
102
+ if (kind === "bundled") return { kind: "bundled" };
103
+ throw new ValidationError(`mcp setup recipe kind must be one of: ${MCP_SETUP_KINDS.join(", ")}`);
104
+ }
105
+
106
+ // Splits a leading `<scheme>:<ref>` off an env value when scheme is one of the
107
+ // recognized MCP_ENV_REF_SCHEMES. Returns null for a bare literal (no colon, or a
108
+ // colon-containing literal like a Windows path whose prefix isn't a known scheme).
109
+ function parseMcpEnvRef(value: string): { scheme: (typeof MCP_ENV_REF_SCHEMES)[number]; ref: string } | null {
110
+ const colon = value.indexOf(":");
111
+ if (colon <= 0) return null;
112
+ const scheme = value.slice(0, colon);
113
+ if (!(MCP_ENV_REF_SCHEMES as readonly string[]).includes(scheme)) return null;
114
+ return { scheme: scheme as (typeof MCP_ENV_REF_SCHEMES)[number], ref: value.slice(colon + 1) };
115
+ }
116
+
117
+ function assertMcpRunConfigEnvValue(key: string, value: unknown): McpEnvValue {
118
+ if (!ENV_NAME.test(key)) throw new ValidationError(`mcp runConfig env key ${key} is not a valid env name`);
119
+ if (typeof value !== "string") throw new ValidationError(`mcp runConfig env ${key} must be a string (a literal, or a "secret:"/"env:"/"file:" reference)`);
120
+ const parsed = parseMcpEnvRef(value);
121
+ if (parsed) {
122
+ if (!parsed.ref.trim()) throw new ValidationError(`mcp runConfig env ${key} ${parsed.scheme}: reference must include a name/path`);
123
+ return value;
124
+ }
125
+ if (looksLikeInlineSecret(value) || (isSecretLikeEnvName(key) && !isEnvPlaceholder(value))) {
126
+ throw new ValidationError(`mcp runConfig env ${key} looks like an inline secret; use "secret:<name>" instead of a literal value`);
127
+ }
128
+ return value;
129
+ }
130
+
131
+ function assertMcpHealthProbe(value: unknown): McpHealthProbe {
132
+ if (!isRecord(value)) throw new ValidationError("mcp runConfig healthProbe must be an object");
133
+ const kind = value.kind;
134
+ if (kind !== "tcp" && kind !== "http" && kind !== "none") {
135
+ throw new ValidationError(`mcp runConfig healthProbe.kind must be one of: ${MCP_HEALTH_PROBE_KINDS.join(", ")}`);
136
+ }
137
+ const probe: McpHealthProbe = { kind };
138
+ if (value.path !== undefined) {
139
+ if (typeof value.path !== "string" || !value.path) throw new ValidationError("mcp runConfig healthProbe.path must be a non-empty string");
140
+ probe.path = value.path;
141
+ }
142
+ if (value.intervalMs !== undefined) {
143
+ if (typeof value.intervalMs !== "number" || !(value.intervalMs > 0)) throw new ValidationError("mcp runConfig healthProbe.intervalMs must be a positive number");
144
+ probe.intervalMs = value.intervalMs;
145
+ }
146
+ if (value.timeoutMs !== undefined) {
147
+ if (typeof value.timeoutMs !== "number" || !(value.timeoutMs > 0)) throw new ValidationError("mcp runConfig healthProbe.timeoutMs must be a positive number");
148
+ probe.timeoutMs = value.timeoutMs;
149
+ }
150
+ return probe;
151
+ }
152
+
153
+ function assertMcpRunConfig(value: unknown): McpRunConfig {
154
+ if (!isRecord(value)) throw new ValidationError("mcp runConfig must be an object");
155
+ const command = value.command;
156
+ if (typeof command !== "string" || !command.trim()) throw new ValidationError("mcp runConfig requires command");
157
+ const runConfig: McpRunConfig = { command, transport: "stdio", listenerMode: "shared" };
158
+ if (value.args !== undefined) {
159
+ if (!Array.isArray(value.args) || !value.args.every((arg) => typeof arg === "string")) throw new ValidationError("mcp runConfig args must be strings");
160
+ runConfig.args = value.args;
161
+ }
162
+ if (value.env !== undefined) {
163
+ if (!isRecord(value.env)) throw new ValidationError("mcp runConfig env must be an object");
164
+ const env: Record<string, McpEnvValue> = {};
165
+ for (const [key, raw] of Object.entries(value.env)) env[key] = assertMcpRunConfigEnvValue(key, raw);
166
+ runConfig.env = env;
167
+ }
168
+ const transport = value.transport;
169
+ if (transport !== "stdio" && transport !== "ws" && transport !== "http") {
170
+ throw new ValidationError(`mcp runConfig transport must be one of: ${MCP_TRANSPORTS.join(", ")}`);
171
+ }
172
+ runConfig.transport = transport;
173
+ if (value.healthProbe !== undefined) runConfig.healthProbe = assertMcpHealthProbe(value.healthProbe);
174
+ const listenerMode = value.listenerMode;
175
+ if (listenerMode !== "shared" && listenerMode !== "per-agent") {
176
+ throw new ValidationError(`mcp runConfig listenerMode must be one of: ${MCP_LISTENER_MODES.join(", ")}`);
177
+ }
178
+ runConfig.listenerMode = listenerMode;
179
+ return runConfig;
180
+ }
181
+
182
+ function assertMcpApplicability(value: unknown): McpApplicability {
183
+ if (!isRecord(value)) throw new ValidationError("mcp applicability must be an object");
184
+ const applicability: McpApplicability = {};
185
+ if (value.providers !== undefined) {
186
+ if (!Array.isArray(value.providers) || !value.providers.every((p) => typeof p === "string")) {
187
+ throw new ValidationError("mcp applicability.providers must be an array of strings");
188
+ }
189
+ for (const provider of value.providers) assertProvider(provider as AgentProfileProvider);
190
+ applicability.providers = value.providers as AgentProfileProvider[];
191
+ }
192
+ if (value.profiles !== undefined) {
193
+ if (!Array.isArray(value.profiles) || !value.profiles.every((p) => typeof p === "string" && p.trim())) {
194
+ throw new ValidationError("mcp applicability.profiles must be an array of non-empty strings");
195
+ }
196
+ applicability.profiles = value.profiles as string[];
197
+ }
198
+ return applicability;
199
+ }
200
+
44
201
  interface ProvisioningAssetRow {
45
202
  id: string;
46
203
  capability_id?: string | null;
@@ -179,6 +336,9 @@ export interface PatchProvisioningAssetVariantInput {
179
336
 
180
337
  export interface CreateMcpProvisioningVariantInput {
181
338
  server: unknown;
339
+ setup?: unknown;
340
+ runConfig?: unknown;
341
+ applicability?: unknown;
182
342
  enabled?: boolean;
183
343
  approvalStatus?: "approved" | "pending";
184
344
  versionConstraint?: string;
@@ -213,6 +373,9 @@ function assertDefinition(kind: ProvisioningAssetKind, definition: ResolvedAsset
213
373
  assertMcpSecretPlaceholders(def.server, def.name);
214
374
  const metadata = isRecord(def.metadata) ? def.metadata : {};
215
375
  if (isRecord(metadata.callmuxServer)) assertCallmuxServerPlaceholders(metadata.callmuxServer as ProvisioningMcpServer, def.name);
376
+ if (def.setup !== undefined) assertMcpSetupRecipe(def.setup);
377
+ if (def.runConfig !== undefined) assertMcpRunConfig(def.runConfig);
378
+ if (def.applicability !== undefined) assertMcpApplicability(def.applicability);
216
379
  }
217
380
  if (kind === "hook") {
218
381
  const def = definition as HookAssetDefinition;
@@ -873,11 +1036,21 @@ export function createMcpProvisioningVariant(
873
1036
  const capability = getProvisioningCapabilityRowById(capabilityId);
874
1037
  if (capability.kind !== "mcp") throw new ValidationError("MCP variants require an mcp capability");
875
1038
  const server = coerceMcpServer(input.server);
1039
+ const setup = input.setup !== undefined ? assertMcpSetupRecipe(input.setup) : undefined;
1040
+ const runConfig = input.runConfig !== undefined ? assertMcpRunConfig(input.runConfig) : undefined;
1041
+ const applicability = input.applicability !== undefined ? assertMcpApplicability(input.applicability) : undefined;
876
1042
  const asset = upsertProvisioningAsset({
877
1043
  kind: "mcp",
878
1044
  name: capability.slug,
879
1045
  provider: "any",
880
- definition: { kind: "mcp", name: capability.slug, server },
1046
+ definition: {
1047
+ kind: "mcp",
1048
+ name: capability.slug,
1049
+ server,
1050
+ ...(setup ? { setup } : {}),
1051
+ ...(runConfig ? { runConfig } : {}),
1052
+ ...(applicability ? { applicability } : {}),
1053
+ },
881
1054
  enabled: input.enabled,
882
1055
  });
883
1056
  const approvalStatus = input.approvalStatus ?? "approved";
@@ -134,6 +134,9 @@ export const postProvisioningVariantMcpRoute: Handler = async (req, params) => {
134
134
  const body = expectObject(parsed.body);
135
135
  const variant = createMcpProvisioningVariant(params.id!, {
136
136
  server: body.server ?? body,
137
+ setup: body.setup,
138
+ runConfig: body.runConfig,
139
+ applicability: body.applicability,
137
140
  enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
138
141
  approvalStatus: optionalEnum(body.approvalStatus, "approvalStatus", APPROVAL_STATUSES, "approved"),
139
142
  versionConstraint: cleanString(body.versionConstraint, "versionConstraint", { max: 120 }),
@@ -25,6 +25,8 @@ import type {
25
25
  DispatchRule,
26
26
  ProviderQuotaRoutingConfig,
27
27
  QuotaBandThresholds,
28
+ QuotaPaceBandThresholds,
29
+ QuotaPacingConfig,
28
30
  RoutingPolicy,
29
31
  RoutingRule,
30
32
  } from "agent-relay-sdk";
@@ -107,6 +109,7 @@ export function normalizeRoutingPolicy(value: unknown): RoutingPolicy {
107
109
  rules: normalizeRules(input.rules),
108
110
  ...(input.quotaBands !== undefined ? { quotaBands: normalizeQuotaBands(input.quotaBands) } : {}),
109
111
  ...(input.providerQuota !== undefined ? { providerQuota: normalizeProviderQuota(input.providerQuota) } : {}),
112
+ ...(input.quotaPacing !== undefined ? { quotaPacing: normalizeQuotaPacing(input.quotaPacing) } : {}),
110
113
  ...(input.dispatch !== undefined ? { dispatch: normalizeDispatchPolicy(input.dispatch) } : {}),
111
114
  };
112
115
  }
@@ -264,6 +267,58 @@ function normalizeQuotaBands(raw: unknown): Partial<QuotaBandThresholds> {
264
267
  return result;
265
268
  }
266
269
 
270
+ // #1098/#1105 — the budget-pacing controller's tunables. Same shape as normalizeQuotaBands: every
271
+ // field optional, so a partial PUT only overrides what it names and everything else keeps falling
272
+ // back to the provider-quota.ts defaults.
273
+ function normalizeQuotaPacing(raw: unknown): QuotaPacingConfig {
274
+ if (!isRecord(raw)) throw new ValidationError("quotaPacing must be an object");
275
+ const result: QuotaPacingConfig = {};
276
+ if (raw.delta !== undefined) {
277
+ if (typeof raw.delta !== "number" || !Number.isFinite(raw.delta) || raw.delta <= 0) {
278
+ throw new ValidationError("quotaPacing.delta must be a positive finite number");
279
+ }
280
+ result.delta = raw.delta;
281
+ }
282
+ if (raw.thresholds !== undefined) result.thresholds = normalizeQuotaPaceThresholds(raw.thresholds);
283
+ if (raw.windowLengthMs !== undefined) result.windowLengthMs = normalizeQuotaPaceWindowLengths(raw.windowLengthMs);
284
+ return result;
285
+ }
286
+
287
+ function normalizeQuotaPaceThresholds(raw: unknown): Partial<QuotaPaceBandThresholds> {
288
+ if (!isRecord(raw)) throw new ValidationError("quotaPacing.thresholds must be an object");
289
+ const result: Partial<QuotaPaceBandThresholds> = {};
290
+ if (raw.preferAt !== undefined) result.preferAt = cleanFraction(raw.preferAt, "quotaPacing.thresholds.preferAt");
291
+ if (raw.normalAt !== undefined) result.normalAt = cleanFraction(raw.normalAt, "quotaPacing.thresholds.normalAt");
292
+ if (raw.cautionAt !== undefined) result.cautionAt = cleanFraction(raw.cautionAt, "quotaPacing.thresholds.cautionAt");
293
+ // Descending: preferAt > normalAt > cautionAt (weight 1 = best/promote, 0 = worst/avoid).
294
+ const orderedPairs: Array<[string, number]> = [];
295
+ if (result.preferAt !== undefined) orderedPairs.push(["preferAt", result.preferAt]);
296
+ if (result.normalAt !== undefined) orderedPairs.push(["normalAt", result.normalAt]);
297
+ if (result.cautionAt !== undefined) orderedPairs.push(["cautionAt", result.cautionAt]);
298
+ for (let i = 0; i < orderedPairs.length - 1; i++) {
299
+ const cur = orderedPairs[i]!;
300
+ const next = orderedPairs[i + 1]!;
301
+ if (cur[1] <= next[1]) {
302
+ throw new ValidationError(`quotaPacing.thresholds.${cur[0]} must be greater than quotaPacing.thresholds.${next[0]}`);
303
+ }
304
+ }
305
+ return result;
306
+ }
307
+
308
+ function normalizeQuotaPaceWindowLengths(raw: unknown): Partial<Record<"5h" | "7d", number>> {
309
+ if (!isRecord(raw)) throw new ValidationError("quotaPacing.windowLengthMs must be an object");
310
+ const result: Partial<Record<"5h" | "7d", number>> = {};
311
+ for (const role of ["5h", "7d"] as const) {
312
+ const value = raw[role];
313
+ if (value === undefined) continue;
314
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
315
+ throw new ValidationError(`quotaPacing.windowLengthMs.${role} must be a positive finite number`);
316
+ }
317
+ result[role] = value;
318
+ }
319
+ return result;
320
+ }
321
+
267
322
  function normalizeProviderQuota(raw: unknown): Partial<Record<string, ProviderQuotaRoutingConfig>> {
268
323
  if (!isRecord(raw)) throw new ValidationError("providerQuota must be an object");
269
324
  const result: Partial<Record<string, ProviderQuotaRoutingConfig>> = {};