agent-relay-server 0.60.0 → 0.61.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 (172) hide show
  1. package/docs/openapi.json +516 -1
  2. package/package.json +3 -2
  3. package/public/assets/MessageBubble-CX63ZV4X.js +2 -0
  4. package/public/assets/MessageBubble-CX63ZV4X.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-CXix1C-H.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CXix1C-H.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
  7. package/public/assets/{activity-CRafQ1Je.js → activity-2xPOScu_.js} +2 -2
  8. package/public/assets/{activity-CRafQ1Je.js.map → activity-2xPOScu_.js.map} +1 -1
  9. package/public/assets/{agent-profiles-DW4o5-ND.js → agent-profiles-BfGTTbeY.js} +2 -2
  10. package/public/assets/{agent-profiles-DW4o5-ND.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
  11. package/public/assets/agents-Bg3WOBRV.js +2 -0
  12. package/public/assets/{agents-Cbegv0Hm.js.map → agents-Bg3WOBRV.js.map} +1 -1
  13. package/public/assets/{analytics-DX6T7JX3.js → analytics-De0cEZGx.js} +3 -3
  14. package/public/assets/{analytics-DX6T7JX3.js.map → analytics-De0cEZGx.js.map} +1 -1
  15. package/public/assets/{automation-CzSBv4gg.js → automation-CHxhzq2t.js} +2 -2
  16. package/public/assets/{automation-CzSBv4gg.js.map → automation-CHxhzq2t.js.map} +1 -1
  17. package/public/assets/{badge-Cy6qO_Jz.js → badge-DhLyCPag.js} +2 -2
  18. package/public/assets/{badge-Cy6qO_Jz.js.map → badge-DhLyCPag.js.map} +1 -1
  19. package/public/assets/branch-state-badge-DSyWKluD.js +2 -0
  20. package/public/assets/{branch-state-badge-CRA50r81.js.map → branch-state-badge-DSyWKluD.js.map} +1 -1
  21. package/public/assets/{button-73wpRw65.js → button-K20wAZG1.js} +2 -2
  22. package/public/assets/{button-73wpRw65.js.map → button-K20wAZG1.js.map} +1 -1
  23. package/public/assets/card-CcZ9hLI7.js +2 -0
  24. package/public/assets/{card-BU6PvVHv.js.map → card-CcZ9hLI7.js.map} +1 -1
  25. package/public/assets/{channels-B1MQCPCT.js → channels-BG0Nib7y.js} +2 -2
  26. package/public/assets/{channels-B1MQCPCT.js.map → channels-BG0Nib7y.js.map} +1 -1
  27. package/public/assets/chat-CjP_crSl.js +2 -0
  28. package/public/assets/{chat-BqIaFYGJ.js.map → chat-CjP_crSl.js.map} +1 -1
  29. package/public/assets/{connectors-C9aFJIrh.js → connectors-DWh7MwV_.js} +2 -2
  30. package/public/assets/{connectors-C9aFJIrh.js.map → connectors-DWh7MwV_.js.map} +1 -1
  31. package/public/assets/{copy-button-HRopUwui.js → copy-button-eF3cKp4Q.js} +2 -2
  32. package/public/assets/{copy-button-HRopUwui.js.map → copy-button-eF3cKp4Q.js.map} +1 -1
  33. package/public/assets/display-DzNvOu1j.js +3 -0
  34. package/public/assets/{display-CketawEF.js.map → display-DzNvOu1j.js.map} +1 -1
  35. package/public/assets/{dist-Kkpnjx_M.js → dist-1dhfdhm9.js} +2 -2
  36. package/public/assets/{dist-Kkpnjx_M.js.map → dist-1dhfdhm9.js.map} +1 -1
  37. package/public/assets/dist-BLBF7qqr.js +2 -0
  38. package/public/assets/{dist-De2oguBm.js.map → dist-BLBF7qqr.js.map} +1 -1
  39. package/public/assets/{dist-T7u7IZGJ.js → dist-BrQsqBbc.js} +2 -2
  40. package/public/assets/{dist-T7u7IZGJ.js.map → dist-BrQsqBbc.js.map} +1 -1
  41. package/public/assets/{dist-Cko2KPW8.js → dist-COQuunEv.js} +2 -2
  42. package/public/assets/{dist-Cko2KPW8.js.map → dist-COQuunEv.js.map} +1 -1
  43. package/public/assets/{dist-CLHMeXYi.js → dist-Ci5Y37nj.js} +2 -2
  44. package/public/assets/{dist-CLHMeXYi.js.map → dist-Ci5Y37nj.js.map} +1 -1
  45. package/public/assets/{dist-Ci1MM51P.js → dist-CoROafNg.js} +2 -2
  46. package/public/assets/{dist-Ci1MM51P.js.map → dist-CoROafNg.js.map} +1 -1
  47. package/public/assets/{dist-CC5f7oYw.js → dist-I-MFUv51.js} +2 -2
  48. package/public/assets/{dist-CC5f7oYw.js.map → dist-I-MFUv51.js.map} +1 -1
  49. package/public/assets/{dist-DFLciw0h.js → dist-UQGKwqZH.js} +2 -2
  50. package/public/assets/{dist-DFLciw0h.js.map → dist-UQGKwqZH.js.map} +1 -1
  51. package/public/assets/dist-YpHfnnIA.js +2 -0
  52. package/public/assets/{dist-DUoZnlB2.js.map → dist-YpHfnnIA.js.map} +1 -1
  53. package/public/assets/dist-qd198ib9.js +2 -0
  54. package/public/assets/{dist-D41tzJYg.js.map → dist-qd198ib9.js.map} +1 -1
  55. package/public/assets/{dist-DmN94rc8.js → dist-yGOq6sB3.js} +2 -2
  56. package/public/assets/{dist-DmN94rc8.js.map → dist-yGOq6sB3.js.map} +1 -1
  57. package/public/assets/{es2015-Df08Opjx.js → es2015-CRxz9lOj.js} +2 -2
  58. package/public/assets/{es2015-Df08Opjx.js.map → es2015-CRxz9lOj.js.map} +1 -1
  59. package/public/assets/{formatted-body-impl-DOXH7oDF.js → formatted-body-impl-u0W_hWYA.js} +2 -2
  60. package/public/assets/{formatted-body-impl-DOXH7oDF.js.map → formatted-body-impl-u0W_hWYA.js.map} +1 -1
  61. package/public/assets/index-BwfVOzUG.js +21 -0
  62. package/public/assets/{index-br_Y6Cvt.js.map → index-BwfVOzUG.js.map} +1 -1
  63. package/public/assets/index-Dn_GDXbi.css +2 -0
  64. package/public/assets/{input-B2EDz-BE.js → input-BHYNpXAG.js} +2 -2
  65. package/public/assets/{input-B2EDz-BE.js.map → input-BHYNpXAG.js.map} +1 -1
  66. package/public/assets/{insights-B1edq5lk.js → insights-CAF9Hi5D.js} +2 -2
  67. package/public/assets/{insights-B1edq5lk.js.map → insights-CAF9Hi5D.js.map} +1 -1
  68. package/public/assets/{integrations-Cm4fjp7_.js → integrations-CG9qyHUf.js} +2 -2
  69. package/public/assets/{integrations-Cm4fjp7_.js.map → integrations-CG9qyHUf.js.map} +1 -1
  70. package/public/assets/{lib-BZrjIp-o.js → lib-Df4aph5u.js} +2 -2
  71. package/public/assets/{lib-BZrjIp-o.js.map → lib-Df4aph5u.js.map} +1 -1
  72. package/public/assets/lucide-react-L2UxFDic.js +9 -0
  73. package/public/assets/lucide-react-L2UxFDic.js.map +1 -0
  74. package/public/assets/{maintenance-D6zvw0fU.js → maintenance-DgHA35ve.js} +2 -2
  75. package/public/assets/{maintenance-D6zvw0fU.js.map → maintenance-DgHA35ve.js.map} +1 -1
  76. package/public/assets/{managed-agents-CzfEznM1.js → managed-agents-Rk6ghKR9.js} +2 -2
  77. package/public/assets/{managed-agents-CzfEznM1.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
  78. package/public/assets/{markdown-preview-impl-DZlCaTHQ.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
  79. package/public/assets/{markdown-preview-impl-DZlCaTHQ.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
  80. package/public/assets/memory-DamwWa9A.js +2 -0
  81. package/public/assets/{memory-CUUAzJDc.js.map → memory-DamwWa9A.js.map} +1 -1
  82. package/public/assets/{messages-6QZH46LG.js → messages--NU-YIL6.js} +2 -2
  83. package/public/assets/{messages-6QZH46LG.js.map → messages--NU-YIL6.js.map} +1 -1
  84. package/public/assets/{orchestrators-CrcqkPuc.js → orchestrators-DQnIn6qx.js} +2 -2
  85. package/public/assets/{orchestrators-CrcqkPuc.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
  86. package/public/assets/{overview-418xXZO_.js → overview-DCmNgXDf.js} +2 -2
  87. package/public/assets/{overview-418xXZO_.js.map → overview-DCmNgXDf.js.map} +1 -1
  88. package/public/assets/{pairs-CnedATgT.js → pairs-DNELeC9P.js} +2 -2
  89. package/public/assets/{pairs-CnedATgT.js.map → pairs-DNELeC9P.js.map} +1 -1
  90. package/public/assets/{react-dom-WI6TjVe7.js → react-dom-CrP_AtcL.js} +2 -2
  91. package/public/assets/{react-dom-WI6TjVe7.js.map → react-dom-CrP_AtcL.js.map} +1 -1
  92. package/public/assets/{security-DS13pbag.js → security-Dd3Kh0oC.js} +2 -2
  93. package/public/assets/{security-DS13pbag.js.map → security-Dd3Kh0oC.js.map} +1 -1
  94. package/public/assets/{select-BOprGZDS.js → select-DQbeJZX0.js} +2 -2
  95. package/public/assets/{select-BOprGZDS.js.map → select-DQbeJZX0.js.map} +1 -1
  96. package/public/assets/settings-BMAJrBIH.js +4 -0
  97. package/public/assets/settings-BMAJrBIH.js.map +1 -0
  98. package/public/assets/store-B8xP6z6T.js +9 -0
  99. package/public/assets/store-B8xP6z6T.js.map +1 -0
  100. package/public/assets/tasks-BYR4L9GP.js +2 -0
  101. package/public/assets/{tasks-B-4oCPuv.js.map → tasks-BYR4L9GP.js.map} +1 -1
  102. package/public/assets/teams-CoTifrq_.js +2 -0
  103. package/public/assets/{teams-sCQAs_QA.js.map → teams-CoTifrq_.js.map} +1 -1
  104. package/public/assets/{terminal-viewer-impl-BhqS2XpS.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
  105. package/public/assets/{terminal-viewer-impl-BhqS2XpS.js.map → terminal-viewer-impl-B1YzfpE6.js.map} +1 -1
  106. package/public/assets/{use-keyboard-viewport-fOL_3RHB.js → use-keyboard-viewport-DrRH_QzG.js} +2 -2
  107. package/public/assets/{use-keyboard-viewport-fOL_3RHB.js.map → use-keyboard-viewport-DrRH_QzG.js.map} +1 -1
  108. package/public/assets/{work-queue-BDhjlwUH.js → work-queue-x57vlgyZ.js} +2 -2
  109. package/public/assets/{work-queue-BDhjlwUH.js.map → work-queue-x57vlgyZ.js.map} +1 -1
  110. package/public/assets/{workspaces-6I-C69nf.js → workspaces-CYm5bLg9.js} +3 -3
  111. package/public/assets/{workspaces-6I-C69nf.js.map → workspaces-CYm5bLg9.js.map} +1 -1
  112. package/public/index.html +22 -22
  113. package/runner/src/config.ts +66 -2
  114. package/src/automations.ts +3 -4
  115. package/src/cli/index.ts +11 -0
  116. package/src/cli/merges.ts +89 -0
  117. package/src/config-store.ts +5 -6
  118. package/src/config.ts +4 -0
  119. package/src/connectors.ts +5 -0
  120. package/src/db/merge-lease.ts +95 -1
  121. package/src/db/migrations.ts +21 -0
  122. package/src/db/schema.ts +1 -1
  123. package/src/index.ts +5 -2
  124. package/src/maintenance.ts +8 -8
  125. package/src/mcp-merge-tools.ts +66 -0
  126. package/src/mcp-provider-catalog.ts +9 -0
  127. package/src/mcp.ts +7 -8
  128. package/src/merge-pause-maintenance.ts +13 -0
  129. package/src/provider-catalog-store.ts +33 -4
  130. package/src/provider-config-store.ts +216 -0
  131. package/src/recipe-runner.ts +2 -1
  132. package/src/recipe-validator.ts +4 -3
  133. package/src/routes/agent-sessions.ts +4 -2
  134. package/src/routes/agents-spawn.ts +2 -2
  135. package/src/routes/commands.ts +20 -0
  136. package/src/routes/index.ts +19 -2
  137. package/src/routes/insights.ts +4 -3
  138. package/src/routes/merges.ts +95 -0
  139. package/src/routes/provider-config.ts +89 -41
  140. package/src/routes/settings.ts +127 -0
  141. package/src/routes/steward.ts +5 -5
  142. package/src/routes/tokens.ts +32 -1
  143. package/src/security.ts +36 -37
  144. package/src/services/merge-pause.ts +37 -0
  145. package/src/services/spawn-agent.ts +10 -8
  146. package/src/settings/modules.ts +149 -0
  147. package/src/settings/registry.ts +397 -0
  148. package/src/spawn-command.ts +5 -1
  149. package/src/team-template-config.ts +2 -2
  150. package/src/token-db.ts +20 -0
  151. package/src/workspace-pr-completion.ts +70 -0
  152. package/public/assets/MessageBubble-BrEKymoV.js +0 -2
  153. package/public/assets/MessageBubble-BrEKymoV.js.map +0 -1
  154. package/public/assets/agents-Cbegv0Hm.js +0 -2
  155. package/public/assets/branch-state-badge-CRA50r81.js +0 -2
  156. package/public/assets/card-BU6PvVHv.js +0 -2
  157. package/public/assets/chat-BqIaFYGJ.js +0 -2
  158. package/public/assets/display-CketawEF.js +0 -3
  159. package/public/assets/dist-D41tzJYg.js +0 -2
  160. package/public/assets/dist-DUoZnlB2.js +0 -2
  161. package/public/assets/dist-De2oguBm.js +0 -2
  162. package/public/assets/index-DaNOQJci.css +0 -2
  163. package/public/assets/index-br_Y6Cvt.js +0 -21
  164. package/public/assets/lucide-react-H_-8G79A.js +0 -9
  165. package/public/assets/lucide-react-H_-8G79A.js.map +0 -1
  166. package/public/assets/memory-CUUAzJDc.js +0 -2
  167. package/public/assets/settings-BgInn_f3.js +0 -10
  168. package/public/assets/settings-BgInn_f3.js.map +0 -1
  169. package/public/assets/store-DHO_a8BD.js +0 -9
  170. package/public/assets/store-DHO_a8BD.js.map +0 -1
  171. package/public/assets/tasks-B-4oCPuv.js +0 -2
  172. package/public/assets/teams-sCQAs_QA.js +0 -2
package/src/security.ts CHANGED
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path";
5
5
  import { getDb } from "./db";
6
6
  import { isPathWithinBase } from "./utils";
7
7
  import type { ComponentToken, TokenConstraints } from "./types";
8
+ import { hasScope, normalizeScope } from "agent-relay-sdk/types";
8
9
 
9
10
  const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
10
11
 
@@ -99,30 +100,7 @@ type AuthorizationCheck = {
99
100
  resource?: AuthorizationResource;
100
101
  };
101
102
 
102
- const SCOPE_ALIASES: Record<string, string> = {
103
- "admin:*": "system:admin",
104
- "system:write": "system:admin",
105
- "agents:read": "agent:read",
106
- "agents:write": "agent:write",
107
- "messages:read": "message:read",
108
- "messages:write": "message:send",
109
- "tasks:read": "task:read",
110
- "tasks:write": "task:write",
111
- "commands:read": "command:read",
112
- "commands:write": "command:write",
113
- "channels:read": "channel:read",
114
- "channels:write": "channel:write",
115
- "integrations:read": "integration:read",
116
- "integrations:write": "integration:write",
117
- "connectors:read": "integration:read",
118
- "connectors:write": "integration:write",
119
- "events:create": "integration:write",
120
- "tasks:create": "task:write",
121
- };
122
-
123
- export function normalizeScope(scope: string): string {
124
- return SCOPE_ALIASES[scope] ?? scope;
125
- }
103
+ export { normalizeScope };
126
104
 
127
105
  export function getIntegrationAuth(req: Request): IntegrationAuth | null {
128
106
  const token = extractToken(req);
@@ -170,6 +148,8 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
170
148
  if (pathname === "/api/orchestrators/bootstrap") return "token:write";
171
149
  if (pathname.match(/^\/api\/orchestrators\/[^/]+\/logs\//)) return "logs:read";
172
150
  if (pathname.match(/^\/api\/orchestrators\/[^/]+\/terminal\//)) return "terminal:attach";
151
+ const settingScope = requiredSettingsScopeFor(method, pathname);
152
+ if (settingScope !== undefined) return settingScope;
173
153
  if (pathname.startsWith("/api/artifacts")) {
174
154
  if (method === "GET" || method === "HEAD") return "artifact:read";
175
155
  if (method === "DELETE") return "artifact:admin";
@@ -195,11 +175,13 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
195
175
  if (pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
196
176
  if (pathname.startsWith("/api/messages")) return method === "GET" ? "message:read" : "message:send";
197
177
  if (pathname.startsWith("/api/commands")) return method === "GET" ? "command:read" : "command:write";
178
+ if (pathname.startsWith("/api/merges")) return method === "GET" ? "command:read" : "command:write";
198
179
  if (pathname.startsWith("/api/workspaces")) return method === "GET" ? "agent:read" : "agent:write";
199
180
  if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
200
181
  if (pathname.startsWith("/api/recipes")) return method === "GET" ? "recipes:read" : "recipes:write";
201
182
  if (pathname === "/api/runtime-tokens/renew") return "agent:write";
202
183
  if (pathname.startsWith("/api/runtime-tokens")) return "token:write";
184
+ if (pathname === "/api/tokens/me") return null;
203
185
  if (pathname.startsWith("/api/tokens") || pathname.startsWith("/api/token-profiles")) return method === "GET" ? "token:read" : "token:write";
204
186
  if (pathname.startsWith("/api/maintenance")) return "system:admin";
205
187
  if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
@@ -250,11 +232,15 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
250
232
  if (pathname === "/api/mcp") return "mcp:use";
251
233
  if (pathname.match(/^\/api\/orchestrators\/[^/]+\/logs\//)) return "logs:read";
252
234
  if (pathname.match(/^\/api\/orchestrators\/[^/]+\/terminal\//)) return "terminal:attach";
235
+ const settingScope = requiredSettingsScopeFor(method, pathname);
236
+ if (settingScope !== undefined) return settingScope ?? "system:admin";
253
237
  if (pathname.startsWith("/api/commands")) {
254
238
  if (method === "GET") return "command:read";
255
239
  return "command:write";
256
240
  }
241
+ if (pathname.startsWith("/api/merges")) return method === "GET" ? "command:read" : "command:write";
257
242
  if (pathname.startsWith("/api/workspaces")) return method === "GET" ? "agent:read" : "agent:write";
243
+ if (pathname === "/api/tokens/me") return null;
258
244
  if (pathname.startsWith("/api/tokens") || pathname.startsWith("/api/token-profiles")) return method === "GET" ? "token:read" : "token:write";
259
245
  if (pathname.startsWith("/api/maintenance")) return "system:admin";
260
246
  if (pathname === "/api/recipes/start") return "recipe:start";
@@ -296,6 +282,32 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
296
282
  return "system:admin";
297
283
  }
298
284
 
285
+ const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notifications", "workspace", "landing", "provider"]);
286
+
287
+ function requiredSettingsScopeFor(method: string, pathname: string): string | null | undefined {
288
+ if (pathname === "/api/settings") return method === "GET" ? "settings:read" : null;
289
+ const settingsMatch = pathname.match(/^\/api\/settings\/([^/]+)(?:\/[^/]+)?$/);
290
+ if (settingsMatch) return settingsScope(method, decodeURIComponent(settingsMatch[1]!));
291
+ if (pathname === "/api/steward-config") return settingsScope(method, "steward");
292
+ if (pathname === "/api/workspace-config") return settingsScope(method, "workspace");
293
+ if (pathname === "/api/insights/config") return settingsScope(method, "insights");
294
+ const configMatch = pathname.match(/^\/api\/config\/([^/]+)(?:\/[^/]+)?(?:\/history)?$/);
295
+ if (configMatch) {
296
+ const namespace = decodeURIComponent(configMatch[1]!);
297
+ if (CONFIG_SETTINGS_NAMESPACES.has(namespace)) return settingsScope(method, namespace);
298
+ }
299
+ if (pathname.match(/^\/api\/providers\/[^/]+\/models\/[^/]+$/) && ["PUT", "PATCH", "DELETE"].includes(method)) {
300
+ return "settings:write:provider";
301
+ }
302
+ return undefined;
303
+ }
304
+
305
+ function settingsScope(method: string, namespace: string): string | null {
306
+ if (method === "GET" || method === "HEAD") return `settings:read:${namespace}`;
307
+ if (["PUT", "PATCH", "POST", "DELETE"].includes(method)) return `settings:write:${namespace}`;
308
+ return null;
309
+ }
310
+
299
311
  export function signComponentToken(payload: Omit<ComponentToken, "iat"> & { iat?: number }): string {
300
312
  const header = { alg: "HS256", typ: "JWT" };
301
313
  const body: ComponentToken = {
@@ -402,19 +414,6 @@ function isComponentToken(value: unknown): value is ComponentToken {
402
414
  (record.jti === undefined || typeof record.jti === "string");
403
415
  }
404
416
 
405
- function hasScope(scopes: string[], requiredScope: string): boolean {
406
- const required = normalizeScope(requiredScope);
407
- if (scopes.includes("*")) return true;
408
- for (const rawScope of scopes) {
409
- const scope = normalizeScope(rawScope);
410
- if (scope === "system:admin") return true;
411
- if (scope === required) return true;
412
- const [category] = required.split(":");
413
- if (category && scope === `${category}:*`) return true;
414
- }
415
- return false;
416
- }
417
-
418
417
  /** Authoritative parent lineage from a registering token's signed constraints — agent-initiated
419
418
  * spawn (`spawnedBy`) or the delegating-component path (`parentAgents`). One home, used by both the
420
419
  * HTTP `postAgent` route and the bus register path so `spawned_by` is set however a child registers
@@ -0,0 +1,37 @@
1
+ import {
2
+ acquireMergePause as acquireMergePauseRow,
3
+ getMergePause,
4
+ listMergePauses,
5
+ releaseMergePause as releaseMergePauseRow,
6
+ renewMergePause as renewMergePauseRow,
7
+ type MergePauseRecord,
8
+ } from "../db";
9
+
10
+ export type { MergePauseRecord };
11
+
12
+ export function listAutoMergePauses(): MergePauseRecord[] {
13
+ return listMergePauses();
14
+ }
15
+
16
+ export function getAutoMergePause(id: string): MergePauseRecord | null {
17
+ return getMergePause(id);
18
+ }
19
+
20
+ export function pauseAutoMerge(input: {
21
+ repoRoot: string;
22
+ baseRef?: string;
23
+ holder: string;
24
+ reason?: string;
25
+ ttlMs?: number;
26
+ now?: number;
27
+ }): { ok: true; pause: MergePauseRecord } | { ok: false; pause: MergePauseRecord } {
28
+ return acquireMergePauseRow(input);
29
+ }
30
+
31
+ export function renewAutoMergePause(id: string, ttlMs?: number): MergePauseRecord | null {
32
+ return renewMergePauseRow(id, ttlMs);
33
+ }
34
+
35
+ export function resumeAutoMerge(input: { id?: string; repoRoot?: string; baseRef?: string }): boolean {
36
+ return releaseMergePauseRow(input);
37
+ }
@@ -208,19 +208,21 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
208
208
  // resolves it. Absent (graceful) when the cwd is inside no registered root.
209
209
  const callerProject = resolveProjectForCwd(resolvedCwd);
210
210
 
211
- // #409 instruction projection (default OFF). Only when the caller opts in AND the
212
- // spawn cwd resolves to a project do we compose a knowledge preamble and PREPEND it to
213
- // systemPromptAppend. Absent flag / no project / empty block ⇒ systemPromptAppend is
214
- // left exactly as the caller passed it, so the spawn command stays byte-identical.
215
- let systemPromptAppend = input.systemPromptAppend;
211
+ // #409 + #463 one composition home for the prompt material handed to providers.
212
+ // Precedence is: project preamble, profile system, profile append entries, then
213
+ // transient caller/fork context. Spawns without profile/project material stay
214
+ // byte-identical to the caller-provided systemPromptAppend.
215
+ const appendBlocks: string[] = [];
216
216
  if (input.projectInstructions && callerProject) {
217
217
  const block = composeProjectInstructions(callerProject, {
218
218
  ...(input.projectInstructions.policy ? { policy: input.projectInstructions.policy } : {}),
219
219
  });
220
- if (block) {
221
- systemPromptAppend = systemPromptAppend ? `${block}\n\n${systemPromptAppend}` : block;
222
- }
220
+ if (block.trim()) appendBlocks.push(block);
223
221
  }
222
+ if (agentProfile?.instructions.system?.trim()) appendBlocks.push(agentProfile.instructions.system);
223
+ appendBlocks.push(...(agentProfile?.instructions.append.filter((entry) => entry.trim()) ?? []));
224
+ if (input.systemPromptAppend?.trim()) appendBlocks.push(input.systemPromptAppend);
225
+ const systemPromptAppend = appendBlocks.length ? appendBlocks.join("\n\n") : input.systemPromptAppend;
224
226
 
225
227
  // Child runner token. `agentInitiated` (set whenever an agent is the caller) forces a
226
228
  // non-spawn-capable child (no grandchildren); `spawnedBy` stamps authoritative lineage the
@@ -0,0 +1,149 @@
1
+ import {
2
+ SPAWN_PROVIDERS,
3
+ errMessage,
4
+ isRecord,
5
+ type ConnectorManifest,
6
+ type JSONSchema,
7
+ type SettingDescriptor,
8
+ type SettingDisplay,
9
+ type SettingScope,
10
+ type SettingTier,
11
+ } from "agent-relay-sdk";
12
+ import { ValidationError } from "../db";
13
+ import { cleanString } from "../validation";
14
+ import type { ProviderConfig } from "../../runner/src/adapter";
15
+ import { defaultProviderConfig } from "../../runner/src/config";
16
+ import { SETTING_ADMIN_SCOPE, getSettingDescriptor, registerProviderConfigSetting, registerSetting, validateSettingValue } from "./registry";
17
+
18
+ export const CONNECTOR_CONFIG_NAMESPACE = "connector-config";
19
+ export const CHANNEL_TYPE_CONFIG_NAMESPACE = "channel-type";
20
+
21
+ export function registerConnectorConfigSetting(
22
+ manifest: Pick<ConnectorManifest, "id" | "displayName" | "description" | "configSchema">,
23
+ opts: { defaults?: unknown } = {},
24
+ ): SettingDescriptor | undefined {
25
+ if (!manifest.configSchema) return undefined;
26
+ const schema = schemaFromRecord(manifest.configSchema, `connector ${manifest.id} configSchema`);
27
+ return registerModuleSetting({
28
+ namespace: CONNECTOR_CONFIG_NAMESPACE,
29
+ key: manifest.id,
30
+ schema,
31
+ defaults: opts.defaults ?? schemaDefault(schema) ?? {},
32
+ scope: { read: "integration:read", write: SETTING_ADMIN_SCOPE },
33
+ tier: "server-runtime",
34
+ display: {
35
+ group: "Connectors",
36
+ label: manifest.displayName || manifest.id,
37
+ description: manifest.description ?? `Configuration for connector ${manifest.id}.`,
38
+ order: 100,
39
+ },
40
+ });
41
+ }
42
+
43
+ export function validateConnectorConfig(manifest: ConnectorManifest, config: unknown): void {
44
+ const descriptor = getSettingDescriptor(CONNECTOR_CONFIG_NAMESPACE, manifest.id) ?? registerConnectorConfigSetting(manifest);
45
+ if (!descriptor) return;
46
+ const validation = validateSettingValue(descriptor.namespace, descriptor.key, config);
47
+ if (!validation.valid) throw new ValidationError(validation.errors.join("; "));
48
+ }
49
+
50
+ export function registerChannelTypeSetting(input: {
51
+ type: string;
52
+ schema: JSONSchema;
53
+ defaults: unknown;
54
+ scope?: Partial<SettingScope>;
55
+ tier?: SettingTier;
56
+ display?: Partial<SettingDisplay>;
57
+ }): SettingDescriptor {
58
+ const type = cleanString(input.type, "channel type", { required: true, max: 80 })!;
59
+ return registerModuleSetting({
60
+ namespace: CHANNEL_TYPE_CONFIG_NAMESPACE,
61
+ key: type,
62
+ schema: input.schema,
63
+ defaults: input.defaults,
64
+ scope: { read: "integration:read", write: SETTING_ADMIN_SCOPE, ...input.scope },
65
+ tier: input.tier ?? "server-runtime",
66
+ display: {
67
+ group: "Channels",
68
+ label: `${type} channel`,
69
+ description: `Configuration for ${type} channel integrations.`,
70
+ order: 120,
71
+ ...input.display,
72
+ },
73
+ });
74
+ }
75
+
76
+ export interface ProviderSettingsDeclaration {
77
+ provider: string;
78
+ defaults: ProviderConfig;
79
+ display?: Partial<SettingDisplay>;
80
+ }
81
+
82
+ const providerSettingsDeclarations = new Map<string, ProviderSettingsDeclaration>();
83
+ let builtInProvidersSeeded = false;
84
+
85
+ export function registerProviderSettingsDeclaration(input: {
86
+ provider: string;
87
+ defaults?: ProviderConfig;
88
+ display?: Partial<SettingDisplay>;
89
+ }): ProviderSettingsDeclaration {
90
+ const provider = cleanString(input.provider, "provider", { required: true, max: 80 })!;
91
+ const declaration: ProviderSettingsDeclaration = {
92
+ provider,
93
+ defaults: input.defaults ?? defaultProviderConfig(provider),
94
+ display: input.display,
95
+ };
96
+ providerSettingsDeclarations.set(provider, declaration);
97
+ return declaration;
98
+ }
99
+
100
+ export function listProviderSettingsDeclarations(): ProviderSettingsDeclaration[] {
101
+ ensureBuiltInProviderSettingsDeclarations();
102
+ return [...providerSettingsDeclarations.values()];
103
+ }
104
+
105
+ export function getProviderSettingsDeclaration(provider: string): ProviderSettingsDeclaration | undefined {
106
+ ensureBuiltInProviderSettingsDeclarations();
107
+ return providerSettingsDeclarations.get(provider);
108
+ }
109
+
110
+ export function providerConfigDefaults(provider: string): ProviderConfig {
111
+ return getProviderSettingsDeclaration(provider)?.defaults ?? defaultProviderConfig(provider);
112
+ }
113
+
114
+ export function registerDeclaredProviderConfigSetting(input: { host: string; provider: string }): SettingDescriptor {
115
+ const declaration = getProviderSettingsDeclaration(input.provider);
116
+ if (!declaration) throw new ValidationError(`provider must be one of: ${listProviderSettingsDeclarations().map((d) => d.provider).join(", ")}`);
117
+ return registerProviderConfigSetting({
118
+ host: input.host,
119
+ provider: declaration.provider,
120
+ defaults: declaration.defaults,
121
+ display: declaration.display,
122
+ });
123
+ }
124
+
125
+ function ensureBuiltInProviderSettingsDeclarations(): void {
126
+ if (builtInProvidersSeeded) return;
127
+ builtInProvidersSeeded = true;
128
+ for (const provider of SPAWN_PROVIDERS) {
129
+ registerProviderSettingsDeclaration({ provider, defaults: defaultProviderConfig(provider) });
130
+ }
131
+ }
132
+
133
+ function registerModuleSetting(input: Parameters<typeof registerSetting>[0]): SettingDescriptor {
134
+ try {
135
+ return registerSetting(input);
136
+ } catch (error) {
137
+ if (error instanceof ValidationError) throw error;
138
+ throw new ValidationError(errMessage(error));
139
+ }
140
+ }
141
+
142
+ function schemaFromRecord(value: Record<string, unknown>, field: string): JSONSchema {
143
+ if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
144
+ return value as JSONSchema;
145
+ }
146
+
147
+ function schemaDefault(schema: JSONSchema): unknown {
148
+ return Object.prototype.hasOwnProperty.call(schema, "default") ? schema.default : undefined;
149
+ }