agent-relay-server 0.62.3 → 0.63.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 (181) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +4 -2
  3. package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
  4. package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
  7. package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
  8. package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
  10. package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
  11. package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
  12. package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
  13. package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
  14. package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
  15. package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
  16. package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
  18. package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
  19. package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
  20. package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
  21. package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
  22. package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
  23. package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
  24. package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
  25. package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
  26. package/public/assets/display-B06I5sqa.js.map +1 -0
  27. package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
  28. package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
  29. package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
  30. package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
  31. package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
  32. package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
  33. package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
  34. package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
  35. package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
  36. package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
  37. package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
  38. package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
  39. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
  40. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
  41. package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
  42. package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
  43. package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
  44. package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
  45. package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
  46. package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
  47. package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
  48. package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
  49. package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
  50. package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
  51. package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
  52. package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
  53. package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
  54. package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
  55. package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
  56. package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
  57. package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
  58. package/public/assets/store-C1E8jmNE.js.map +1 -0
  59. package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
  60. package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
  61. package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
  62. package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
  63. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
  64. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
  65. package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
  66. package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
  67. package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
  68. package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
  69. package/public/index.html +3 -3
  70. package/runner/src/adapter.ts +2 -1
  71. package/runner/src/config.ts +143 -5
  72. package/src/agent-ref.ts +8 -6
  73. package/src/artifact-storage.ts +3 -5
  74. package/src/automations/constants.ts +12 -0
  75. package/src/automations/cron.ts +129 -0
  76. package/src/automations/crud.ts +84 -0
  77. package/src/automations/dispatch.ts +275 -0
  78. package/src/automations/index.ts +5 -0
  79. package/src/automations/reconcile.ts +245 -0
  80. package/src/automations/storage.ts +181 -0
  81. package/src/automations/types.ts +16 -0
  82. package/src/automations/validation.ts +140 -0
  83. package/src/automations.ts +1 -1045
  84. package/src/bus.ts +3 -4
  85. package/src/cli/_shared.ts +5 -4
  86. package/src/cli/agent-detect.ts +6 -4
  87. package/src/cli/introspect.ts +3 -2
  88. package/src/cli/token.ts +2 -1
  89. package/src/cli/workspace.ts +2 -1
  90. package/src/config.ts +176 -1
  91. package/src/connectors.ts +3 -3
  92. package/src/db/activity.ts +1 -1
  93. package/src/db/agent-predicates.ts +9 -0
  94. package/src/db/agent-search.ts +2 -2
  95. package/src/db/agents.ts +13 -41
  96. package/src/db/artifacts.ts +8 -8
  97. package/src/db/channel-identity.ts +38 -0
  98. package/src/db/channels.ts +37 -61
  99. package/src/db/connection.ts +2 -1
  100. package/src/db/continuation-archives.ts +7 -7
  101. package/src/db/delivery.ts +7 -12
  102. package/src/db/inbox.ts +1 -1
  103. package/src/db/integrations.ts +4 -4
  104. package/src/db/issue-lifecycle.ts +6 -6
  105. package/src/db/mappers.ts +1 -2
  106. package/src/db/merge-lease.ts +3 -3
  107. package/src/db/message-claims.ts +16 -0
  108. package/src/db/message-queries.ts +8 -0
  109. package/src/db/message-reads.ts +26 -9
  110. package/src/db/messages.ts +17 -41
  111. package/src/db/notification-subscriptions.ts +2 -2
  112. package/src/db/orchestrators.ts +4 -4
  113. package/src/db/pair-maintenance.ts +9 -0
  114. package/src/db/pairs.ts +10 -18
  115. package/src/db/plans.ts +1 -1
  116. package/src/db/policy-runtime.ts +14 -0
  117. package/src/db/project-entries.ts +1 -1
  118. package/src/db/projects.ts +2 -2
  119. package/src/db/shared.ts +7 -0
  120. package/src/db/stats.ts +2 -2
  121. package/src/db/task-core.ts +18 -0
  122. package/src/db/task-maintenance.ts +24 -0
  123. package/src/db/tasks.ts +5 -17
  124. package/src/db/teams.ts +3 -3
  125. package/src/db/workspaces.ts +6 -6
  126. package/src/index.ts +6 -5
  127. package/src/issue-tracker/github.ts +3 -3
  128. package/src/maintenance/constants.ts +52 -0
  129. package/src/maintenance/events.ts +32 -0
  130. package/src/maintenance/index.ts +7 -0
  131. package/src/maintenance/jobs.ts +340 -0
  132. package/src/maintenance/orphaned-sessions.ts +86 -0
  133. package/src/maintenance/scheduler.ts +231 -0
  134. package/src/maintenance/teams.ts +65 -0
  135. package/src/maintenance/types.ts +31 -0
  136. package/src/maintenance/workspaces.ts +251 -0
  137. package/src/maintenance.ts +1 -1099
  138. package/src/mcp/identity.ts +131 -0
  139. package/src/mcp/index.ts +208 -0
  140. package/src/mcp/tools-messaging.ts +459 -0
  141. package/src/mcp/tools-spawn.ts +145 -0
  142. package/src/mcp/tools-workspace.ts +227 -0
  143. package/src/mcp/types.ts +24 -0
  144. package/src/mcp/validation.ts +104 -0
  145. package/src/mcp.ts +1 -1319
  146. package/src/notification-router.ts +2 -2
  147. package/src/notification-rules.ts +2 -2
  148. package/src/project-ingest.ts +3 -3
  149. package/src/project-instructions.ts +1 -1
  150. package/src/provider-config-store.ts +5 -4
  151. package/src/provider-state.ts +2 -4
  152. package/src/reviewer-pipeline.ts +1 -1
  153. package/src/routes/agent-sessions.ts +5 -4
  154. package/src/routes/orchestrator-proxy.ts +5 -4
  155. package/src/routes/workspaces.ts +5 -4
  156. package/src/security.ts +13 -7
  157. package/src/self-resume.ts +3 -7
  158. package/src/services/agent-action.ts +2 -2
  159. package/src/services/issue-activity.ts +0 -4
  160. package/src/services/issue-associations.ts +2 -2
  161. package/src/services/issue-lifecycle.ts +8 -11
  162. package/src/services/managed-running.ts +1 -1
  163. package/src/services/parity-harness.ts +3 -3
  164. package/src/services/plan-graph.ts +4 -7
  165. package/src/services/send-message.ts +16 -12
  166. package/src/services/shutdown-agent.ts +2 -2
  167. package/src/services/team-templates.ts +2 -2
  168. package/src/services/team.ts +2 -2
  169. package/src/settings/modules.ts +1 -1
  170. package/src/settings/registry.ts +3 -3
  171. package/src/spawn-command.ts +1 -1
  172. package/src/sse.ts +0 -4
  173. package/src/upgrade.ts +7 -9
  174. package/src/workspace-actions.ts +1 -1
  175. package/src/workspace-auto-merge.ts +4 -5
  176. package/src/workspace-claim.ts +1 -2
  177. package/src/workspace-orphans.ts +2 -7
  178. package/src/workspace-probe.ts +5 -8
  179. package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
  180. package/public/assets/display-DzNvOu1j.js.map +0 -1
  181. package/public/assets/store-iTiheuTD.js.map +0 -1
@@ -57,7 +57,7 @@ export interface NotificationScope {
57
57
  paths?: string[];
58
58
  }
59
59
 
60
- export interface NotificationMessage {
60
+ interface NotificationMessage {
61
61
  subject?: string;
62
62
  body: string;
63
63
  payload?: Record<string, unknown>;
@@ -67,7 +67,7 @@ export interface NotificationMessage {
67
67
  replyExpected?: boolean;
68
68
  }
69
69
 
70
- export interface RouteNotificationInput {
70
+ interface RouteNotificationInput {
71
71
  type: NotificationType;
72
72
  scope: NotificationScope;
73
73
  /**
@@ -33,7 +33,7 @@ export interface NotificationRule {
33
33
  evaluate(agent: AgentCard, ctx: NotificationRuleContext): RuleDecision;
34
34
  }
35
35
 
36
- export interface AudienceMember {
36
+ interface AudienceMember {
37
37
  agentId: string;
38
38
  /** The rule whose decision was decisive (last non-abstain wins) — carried for explainability (#446). */
39
39
  matchedRule: string;
@@ -161,7 +161,7 @@ export function setSubscriptionRules(rules: NotificationRule[]): void {
161
161
  }
162
162
 
163
163
  /** The effective ordered rule list for a type: its registered defaults, then subscription overrides. */
164
- export function audienceRulesFor(type: NotificationType): NotificationRule[] {
164
+ function audienceRulesFor(type: NotificationType): NotificationRule[] {
165
165
  const defaults = audienceRegistry.get(type);
166
166
  if (!defaults) return [];
167
167
  return subscriptionRules.length === 0 ? defaults : [...defaults, ...subscriptionRules];
@@ -16,7 +16,7 @@ import type { ProjectEntry } from "./types";
16
16
 
17
17
  const INGEST_FILENAME = "CLAUDE.md";
18
18
 
19
- export interface ParsedSection {
19
+ interface ParsedSection {
20
20
  /** Heading text (the entry title). For pre-heading preamble content: "(preamble)". */
21
21
  title: string;
22
22
  /** Section body — lines under the heading up to the next heading, trimmed. May be "". */
@@ -25,14 +25,14 @@ export interface ParsedSection {
25
25
  anchor: string;
26
26
  }
27
27
 
28
- export interface IngestRootResult {
28
+ interface IngestRootResult {
29
29
  rootPath: string;
30
30
  filePath: string;
31
31
  found: boolean;
32
32
  sections: number;
33
33
  }
34
34
 
35
- export interface IngestResult {
35
+ interface IngestResult {
36
36
  projectId: string;
37
37
  created: number;
38
38
  updated: number;
@@ -31,7 +31,7 @@ export interface ProjectInstructionPolicy {
31
31
  policy?: ProjectInstructionPolicyName;
32
32
  }
33
33
 
34
- export interface ComposeProjectInstructionsOptions {
34
+ interface ComposeProjectInstructionsOptions {
35
35
  /** Named policy selecting the included source set. Default `all`. */
36
36
  policy?: ProjectInstructionPolicyName;
37
37
  /** Explicit source set; overrides `policy` when given (the low-level lever). */
@@ -3,6 +3,7 @@ import { hostname } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { ValidationError, listOrchestrators } from "./db";
5
5
  import { getConfig, listConfig, setConfig } from "./config-store";
6
+ import { providerConfigHomeFromEnv } from "./config";
6
7
  import { cleanString, cleanStringArray, optionalEnum } from "./validation";
7
8
  import { emitConfigChanged } from "./sse";
8
9
  import {
@@ -24,15 +25,15 @@ export const VALID_PROVIDER_CONFIGS = SPAWN_PROVIDERS;
24
25
  type ProviderConfigProvider = string;
25
26
  type ProviderConfigEntry = ConfigEntry<ProviderConfig>;
26
27
 
27
- export function defaultProviderConfigHost(): string {
28
+ function defaultProviderConfigHost(): string {
28
29
  return hostname();
29
30
  }
30
31
 
31
- export function providerConfigKey(host: string, provider: string): string {
32
+ function providerConfigKey(host: string, provider: string): string {
32
33
  return `${normalizeProviderConfigHost(host)}/${normalizeProviderConfigProvider(provider)}`;
33
34
  }
34
35
 
35
- export function parseProviderConfigKey(key: string): { host: string; provider: ProviderConfigProvider } | null {
36
+ function parseProviderConfigKey(key: string): { host: string; provider: ProviderConfigProvider } | null {
36
37
  const slash = key.lastIndexOf("/");
37
38
  if (slash <= 0 || slash === key.length - 1) return null;
38
39
  const host = cleanString(key.slice(0, slash), "host", { required: true, max: 120 })!;
@@ -234,5 +235,5 @@ function maskEnv(env: Record<string, string>): Record<string, string> {
234
235
  }
235
236
 
236
237
  function agentRelayHome(): string {
237
- return process.env.AGENT_RELAY_HOME || join(process.env.HOME || ".", ".agent-relay");
238
+ return providerConfigHomeFromEnv();
238
239
  }
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from "agent-relay-sdk";
2
+ import { rateLimitFallbackMs } from "./config";
2
3
 
3
4
  // One server-side home for reading an agent's provider runtime state (the
4
5
  // `meta.providerState` that both Claude and Codex ride for blocked/approval
@@ -6,7 +7,7 @@ import { isRecord } from "agent-relay-sdk";
6
7
  // the transient-agent reaper (don't reap a held agent), and the rate-limit
7
8
  // resume sweep. The dashboard has its own client-side copy in display.ts.
8
9
 
9
- export interface ProviderStateView {
10
+ interface ProviderStateView {
10
11
  state: string;
11
12
  reason?: string;
12
13
  label?: string;
@@ -48,9 +49,6 @@ export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
48
49
  // rather than waiting the full (unknown) limit window. Re-entering the hold on a
49
50
  // still-limited retry is cheap, and a subscription limit almost always carries a
50
51
  // reset time. Read at call-time so operators/tests can tune it.
51
- const rateLimitFallbackMs = (): number =>
52
- Number(process.env.AGENT_RELAY_RATE_LIMIT_FALLBACK_MS) || 60 * 60 * 1000;
53
-
54
52
  /**
55
53
  * When a rate-limit hold should be lifted. The provider's `resetAt` is
56
54
  * authoritative; otherwise fall back to `enteredAt + fallback window`. Null only
@@ -9,7 +9,7 @@ import type { Command, WorkspaceMergePreview, WorkspaceRecord } from "./types";
9
9
 
10
10
  type ReviewDecision = "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED";
11
11
 
12
- export function parsePrNumber(prUrl: string | undefined): number | undefined {
12
+ function parsePrNumber(prUrl: string | undefined): number | undefined {
13
13
  if (!prUrl) return undefined;
14
14
  const match = prUrl.match(/\/pull\/(\d+)(?:\D|$)/);
15
15
  if (!match) return undefined;
@@ -14,6 +14,7 @@ import { authContextFromRequest } from "../services/auth-context";
14
14
  import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../services/shutdown-agent";
15
15
  import { type AgentCard, type SpawnApprovalMode } from "../types";
16
16
  import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
17
+ import { relayToken } from "../config";
17
18
 
18
19
  const CLAUDE_RESUME_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
20
 
@@ -486,8 +487,8 @@ async function proxyOrchestratorDelete(req: Request, orchestratorId: string, pat
486
487
  const incoming = new URL(req.url);
487
488
  const proxyUrl = `${orch.apiUrl}${path}${incoming.search}`;
488
489
  const headers: Record<string, string> = {};
489
- const relayToken = process.env.AGENT_RELAY_TOKEN;
490
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
490
+ const token = relayToken();
491
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
491
492
  try {
492
493
  const res = await fetch(proxyUrl, {
493
494
  method: "DELETE",
@@ -507,8 +508,8 @@ async function proxyOrchestratorJson(orchestratorId: string, path: string, metho
507
508
  if (!orch.apiUrl) return error("orchestrator does not expose an API", 422);
508
509
  if (orch.status !== "online") return error("orchestrator is offline", 422);
509
510
  const headers: Record<string, string> = { "Content-Type": "application/json" };
510
- const relayToken = process.env.AGENT_RELAY_TOKEN;
511
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
511
+ const token = relayToken();
512
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
512
513
  try {
513
514
  const res = await fetch(`${orch.apiUrl}${path}`, {
514
515
  method,
@@ -5,6 +5,7 @@ import { authorizeRoute, cleanJsonArray, error, json, type Handler } from "./_sh
5
5
  import { cleanStringArray } from "../validation";
6
6
  import { emitOrchestratorStatus } from "../sse";
7
7
  import { type OrchestratorRuntimeInput, type SpawnProvider } from "../types";
8
+ import { relayToken } from "../config";
8
9
 
9
10
  export const getOrchestratorDirectories: Handler = async (req, params) => {
10
11
  const orch = getOrchestrator(params.id!);
@@ -63,8 +64,8 @@ async function proxyOrchestratorGet(req: Request, orchestratorId: string, path:
63
64
  const incoming = new URL(req.url);
64
65
  const proxyUrl = `${orch.apiUrl}${path}${incoming.search}`;
65
66
  const headers: Record<string, string> = {};
66
- const relayToken = process.env.AGENT_RELAY_TOKEN;
67
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
67
+ const token = relayToken();
68
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
68
69
  try {
69
70
  const res = await fetch(proxyUrl, { headers, signal: AbortSignal.timeout(10_000) });
70
71
  const contentType = res.headers.get("content-type") ?? "";
@@ -88,8 +89,8 @@ async function proxyOrchestratorPost(req: Request, orchestratorId: string, path:
88
89
  const headers: Record<string, string> = {
89
90
  "Content-Type": req.headers.get("content-type") ?? "application/json",
90
91
  };
91
- const relayToken = process.env.AGENT_RELAY_TOKEN;
92
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
92
+ const token = relayToken();
93
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
93
94
  try {
94
95
  const body = await req.text();
95
96
  const res = await fetch(proxyUrl, {
@@ -13,6 +13,7 @@ import { resolve } from "node:path";
13
13
  import { type WorkspaceAutoMergePolicy, type WorkspaceDiagnostics, type WorkspaceGitState, type WorkspaceMergeStrategy, type WorkspaceRecord, type WorkspaceStatus } from "../types";
14
14
  import { AUTO_MERGE_POLICIES } from "../workspace-merge";
15
15
  import { workspaceActiveClaim } from "../workspace-claim";
16
+ import { relayToken } from "../config";
16
17
 
17
18
  export const getWorkspaces: Handler = (req) => {
18
19
  try {
@@ -50,8 +51,8 @@ async function proxyWorkspaceHostGet(workspaceId: string, hostPath: string, extr
50
51
  if (workspace.baseSha) query.set("baseSha", workspace.baseSha);
51
52
  for (const [key, value] of Object.entries(extraQuery ?? {})) query.set(key, value);
52
53
  const headers: Record<string, string> = {};
53
- const relayToken = process.env.AGENT_RELAY_TOKEN;
54
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
54
+ const token = relayToken();
55
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
55
56
  try {
56
57
  const res = await fetch(`${orch.apiUrl}${hostPath}?${query.toString()}`, {
57
58
  headers,
@@ -145,8 +146,8 @@ async function fetchWorkspaceGitState(workspace: WorkspaceRecord): Promise<{ sta
145
146
  if (workspace.baseRef) query.set("baseRef", workspace.baseRef);
146
147
  if (workspace.baseSha) query.set("baseSha", workspace.baseSha);
147
148
  const headers: Record<string, string> = {};
148
- const relayToken = process.env.AGENT_RELAY_TOKEN;
149
- if (relayToken) headers[RELAY_TOKEN_HEADER] = relayToken;
149
+ const token = relayToken();
150
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
150
151
  try {
151
152
  const res = await fetch(`${orch.apiUrl}/api/workspace/state?${query.toString()}`, { headers, signal: AbortSignal.timeout(10_000) });
152
153
  if (!res.ok) return { unavailable: `host returned ${res.status}` };
package/src/security.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { AUTH_TOKEN, CORS_ORIGINS, getIntegrationTokens, type IntegrationTokenConfig } from "./config";
1
+ import {
2
+ CORS_ORIGINS,
3
+ allowUnauthenticated,
4
+ corsOriginsFromEnv,
5
+ getIntegrationTokens,
6
+ relayToken,
7
+ tokenSecretFromEnv,
8
+ type IntegrationTokenConfig,
9
+ } from "./config";
2
10
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
3
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
12
  import { dirname, join, resolve } from "node:path";
@@ -14,7 +22,7 @@ export function isLoopbackHost(hostname: string): boolean {
14
22
  }
15
23
 
16
24
  export function assertSafeNetworkConfig(host: string): void {
17
- if (authToken() || process.env.AGENT_RELAY_ALLOW_UNAUTH === "1") return;
25
+ if (authToken() || allowUnauthenticated()) return;
18
26
  if (isLoopbackHost(host)) return;
19
27
  throw new Error(
20
28
  `Refusing to bind unauthenticated relay on ${host}. Set AGENT_RELAY_TOKEN or AGENT_RELAY_ALLOW_UNAUTH=1.`,
@@ -407,7 +415,7 @@ export function unauthorized(req: Request): Response {
407
415
  }
408
416
 
409
417
  function authToken(): string {
410
- return process.env.AGENT_RELAY_TOKEN ?? AUTH_TOKEN;
418
+ return relayToken();
411
419
  }
412
420
 
413
421
  function extractToken(req: Request): string | null {
@@ -417,7 +425,7 @@ function extractToken(req: Request): string | null {
417
425
  }
418
426
 
419
427
  function tokenSecret(): string {
420
- const env = process.env.AGENT_RELAY_TOKEN_SECRET;
428
+ const env = tokenSecretFromEnv();
421
429
  if (env) return env;
422
430
  const path = join(process.env.HOME || ".", ".agent-relay", "token-secret");
423
431
  if (existsSync(path)) return readFileSync(path, "utf8").trim();
@@ -542,7 +550,5 @@ function isTokenRevoked(jti: string): boolean {
542
550
  }
543
551
 
544
552
  function corsOrigins(): string[] {
545
- const raw = process.env.AGENT_RELAY_CORS_ORIGINS;
546
- if (raw === undefined) return CORS_ORIGINS;
547
- return raw.split(",").map((origin) => origin.trim()).filter(Boolean);
553
+ return corsOriginsFromEnv() ?? CORS_ORIGINS;
548
554
  }
@@ -14,7 +14,7 @@ import { emitRelayEvent } from "./events";
14
14
  import { routeNotification } from "./notification-router";
15
15
  import type { AgentCard, Command, ContinuationEnvelope, ContinuationLedgerEntry, SelfResumeMode } from "./types";
16
16
 
17
- export interface CompactAndResumeInput {
17
+ interface CompactAndResumeInput {
18
18
  agentId: string;
19
19
  workingState: string;
20
20
  ruledOut?: unknown;
@@ -22,7 +22,7 @@ export interface CompactAndResumeInput {
22
22
  objective?: string;
23
23
  }
24
24
 
25
- export interface CompactAndResumeResult {
25
+ interface CompactAndResumeResult {
26
26
  ok: boolean;
27
27
  mode: SelfResumeMode;
28
28
  envelope: ContinuationEnvelope;
@@ -110,7 +110,7 @@ export function enqueueContinuationInjectionAfterClear(command: Command): Comman
110
110
  return inject;
111
111
  }
112
112
 
113
- export function renderContinuation(envelope: ContinuationEnvelope): string {
113
+ function renderContinuation(envelope: ContinuationEnvelope): string {
114
114
  const ledger = envelope.ledger.length
115
115
  ? envelope.ledger.map((entry) => `- gen ${entry.gen} @ ${new Date(entry.at).toISOString()}: ${entry.ruledOut}${entry.why ? `; why: ${entry.why}` : ""}`).join("\n")
116
116
  : "- none";
@@ -231,7 +231,3 @@ function ledgerEntry(ruledOut: string, why: string, generation: number, now: num
231
231
  if (!cleaned) throw new ValidationError("ruledOut entries cannot be blank");
232
232
  return { gen: generation, ruledOut: cleaned, why, at: now };
233
233
  }
234
-
235
- export function continuationForAgent(agentId: string): ContinuationEnvelope | null {
236
- return getContinuationEnvelope(agentId);
237
- }
@@ -8,7 +8,7 @@ import type { AgentCard, Command } from "../types";
8
8
 
9
9
  export type AgentAction = "compact" | "clearContext" | "inject";
10
10
 
11
- export interface AgentActionInput {
11
+ interface AgentActionInput {
12
12
  agentId: string;
13
13
  action: AgentAction;
14
14
  message?: string;
@@ -16,7 +16,7 @@ export interface AgentActionInput {
16
16
  requestedBy?: string;
17
17
  }
18
18
 
19
- export interface AgentActionResult {
19
+ interface AgentActionResult {
20
20
  action: AgentAction;
21
21
  command: Command;
22
22
  }
@@ -24,7 +24,3 @@ export function recordAndEmitIssueActivity(input: RecordIssueActivityInput) {
24
24
  emitActivityEvent(event);
25
25
  return event;
26
26
  }
27
-
28
- export function resolveIssueActivityMapping(input: { issueRefId?: string; issueRef?: string; defaultRepo?: string; provider?: IssueProvider }) {
29
- return resolveIssueActivityRef(input);
30
- }
@@ -8,7 +8,7 @@ import { getIssueRef, resolveIssueRefByExternalKey, resolveIssueRefFromString }
8
8
  import type { IssueAssociationEntityType, IssueAssociationIssue, IssueProvider } from "../types";
9
9
  import { ValidationError } from "../db/connection";
10
10
 
11
- export interface ResolveAssociationIssueInput {
11
+ interface ResolveAssociationIssueInput {
12
12
  issueRefId?: string;
13
13
  ref?: string;
14
14
  defaultRepo?: string;
@@ -19,7 +19,7 @@ export interface ResolveAssociationIssueInput {
19
19
  title?: string;
20
20
  }
21
21
 
22
- export interface AssociateIssueInput extends ResolveAssociationIssueInput {
22
+ interface AssociateIssueInput extends ResolveAssociationIssueInput {
23
23
  entityType: IssueAssociationEntityType;
24
24
  entityId: string;
25
25
  }
@@ -9,6 +9,7 @@ import {
9
9
  import { getIssueRef, resolveIssueRefByExternalKey, resolveIssueRefFromString, updateIssueRef } from "../db/issue-refs";
10
10
  import { associateIssue } from "../db/issue-associations";
11
11
  import { resolveIssueTrackerAdapter } from "../issue-tracker";
12
+ import { defaultIssueRepo } from "../config";
12
13
  import type { IssueTrackerAdapter } from "../issue-tracker";
13
14
  import { recordAndEmitIssueActivity } from "./issue-activity";
14
15
  import type { IssueAssociationEntityType, IssueProvider, IssueRef, WorkspaceRecord } from "../types";
@@ -17,7 +18,7 @@ type LifecycleWorkspace = Pick<WorkspaceRecord, "id" | "repoRoot" | "branch" | "
17
18
  type AdapterResolver = (ref: IssueRef) => IssueTrackerAdapter;
18
19
  type LogFn = (message: string) => void;
19
20
 
20
- export interface LandedIssueLifecycleInput {
21
+ interface LandedIssueLifecycleInput {
21
22
  workspace: LifecycleWorkspace;
22
23
  mergedSha?: string;
23
24
  subject?: string;
@@ -31,7 +32,7 @@ export interface LandedIssueLifecycleInput {
31
32
  prLand?: boolean;
32
33
  }
33
34
 
34
- export interface ReopenIssueLifecycleInput {
35
+ interface ReopenIssueLifecycleInput {
35
36
  issueRefId?: string;
36
37
  ref?: string;
37
38
  defaultRepo?: string;
@@ -46,19 +47,19 @@ export interface ReopenIssueLifecycleInput {
46
47
  entityId?: string;
47
48
  }
48
49
 
49
- export interface IssueLifecycleOptions {
50
+ interface IssueLifecycleOptions {
50
51
  adapterResolver?: AdapterResolver;
51
52
  log?: LogFn;
52
53
  }
53
54
 
54
- export type LandIssueRefSource = "association" | "commit" | "backlink";
55
+ type LandIssueRefSource = "association" | "commit" | "backlink";
55
56
 
56
- export type CloseIssueLifecycleResult =
57
+ type CloseIssueLifecycleResult =
57
58
  | { issueRef: IssueRef; action: "closed"; source: LandIssueRefSource; landKey: string }
58
59
  | { issueRef: IssueRef; action: "skipped"; reason: "already-closed" | "already-processed"; landKey: string }
59
60
  | { issueRef: IssueRef; action: "failed"; error: string; landKey: string };
60
61
 
61
- export type ReopenIssueLifecycleResult =
62
+ type ReopenIssueLifecycleResult =
62
63
  | { issueRef: IssueRef; action: "reopened"; landKey: string }
63
64
  | { issueRef: IssueRef; action: "skipped"; reason: "already-open"; landKey: string }
64
65
  | { issueRef: IssueRef; action: "failed"; error: string; landKey: string };
@@ -78,10 +79,6 @@ function logFailure(opts: IssueLifecycleOptions, message: string): void {
78
79
  else console.warn(message);
79
80
  }
80
81
 
81
- function defaultIssueRepo(): string | undefined {
82
- return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
83
- }
84
-
85
82
  /**
86
83
  * `owner/repo` out of any git remote URL form: `git@github.com:owner/repo.git`,
87
84
  * `https://github.com/owner/repo.git`, `ssh://git@host/owner/repo`, trailing slash/`.git`
@@ -210,7 +207,7 @@ export function parseBacklinkIssueRefs(text: string | undefined, fallbackDefault
210
207
  return issueRefMap(refs);
211
208
  }
212
209
 
213
- export function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): { source: LandIssueRefSource; refs: IssueRef[] } {
210
+ function resolveIssueRefsForLand(input: LandedIssueLifecycleInput): { source: LandIssueRefSource; refs: IssueRef[] } {
214
211
  const associated = associatedIssueRefs(input.workspace);
215
212
  if (associated.length > 0) return { source: "association", refs: associated };
216
213
  const fallbackDefaultRepo = landDefaultRepo(input.workspace);
@@ -19,7 +19,7 @@ import { emitMessageAvailable, emitMessageDeliveryUpdated, emitNewMessage } from
19
19
  import type { AuthContext } from "./auth-context";
20
20
  import type { ManagedAgentState } from "../types";
21
21
 
22
- export interface ManagedRunningInput {
22
+ interface ManagedRunningInput {
23
23
  policyName?: string;
24
24
  spawnRequestId?: string;
25
25
  agentId: string;
@@ -15,7 +15,7 @@ import { subscribeRelayEvents } from "../events";
15
15
  /** An emitted relay event, normalized to the fields parity compares — volatile
16
16
  * `seq`/`timestamp` are deliberately dropped so only behavior, not ordering noise,
17
17
  * is asserted. */
18
- export interface CapturedEvent {
18
+ interface CapturedEvent {
19
19
  type: string;
20
20
  source: string;
21
21
  subject?: string;
@@ -36,7 +36,7 @@ export interface Capture<S> {
36
36
  * settled so the harness can snapshot. Keep the abstraction swappable: a later slice
37
37
  * can replace a direct-call transport with an end-to-end HTTP one without reshaping
38
38
  * the harness. */
39
- export interface ParityTransport<C> {
39
+ interface ParityTransport<C> {
40
40
  id: string;
41
41
  invoke(ctx: C): Promise<void>;
42
42
  }
@@ -57,7 +57,7 @@ export interface ParityOp<C, S> {
57
57
  }
58
58
 
59
59
  /** Drive one transport in isolation and capture everything it produced. */
60
- export async function captureTransport<C, S>(
60
+ async function captureTransport<C, S>(
61
61
  op: ParityOp<C, S>,
62
62
  transportId: string,
63
63
  ctx: C,
@@ -4,12 +4,13 @@ import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/
4
4
  import { resolveIssueRefFromString } from "../db/issue-refs";
5
5
  import { emitRelayEvent } from "../events";
6
6
  import { cleanString, cleanStringArray, optionalEnum } from "../validation";
7
+ import { defaultIssueRepo } from "../config";
7
8
  import type { AuthContext } from "./auth-context";
8
9
  import type { AgentCard, Plan, PlanEdge, PlanNode, PlanOwnerRef, PlanStatus } from "../types";
9
10
 
10
11
  export const PLAN_STATUSES = ["pending", "in-progress", "landed", "blocked"] as const;
11
12
 
12
- export interface PlanNodeInput {
13
+ interface PlanNodeInput {
13
14
  id?: unknown;
14
15
  label?: unknown;
15
16
  scopeBlurb?: unknown;
@@ -21,7 +22,7 @@ export interface PlanNodeInput {
21
22
  blackboardRefs?: unknown;
22
23
  }
23
24
 
24
- export interface PlanNodePatch {
25
+ interface PlanNodePatch {
25
26
  label?: unknown;
26
27
  scopeBlurb?: unknown;
27
28
  ownerRef?: unknown;
@@ -32,7 +33,7 @@ export interface PlanNodePatch {
32
33
  blackboardRefs?: unknown;
33
34
  }
34
35
 
35
- export interface PlanCreateInput {
36
+ interface PlanCreateInput {
36
37
  threadId: unknown;
37
38
  teamId?: unknown;
38
39
  channel?: unknown;
@@ -86,10 +87,6 @@ function cleanRefs(value: unknown): PlanNode["refs"] | undefined {
86
87
  return Object.keys(out).length ? out : undefined;
87
88
  }
88
89
 
89
- function defaultIssueRepo(): string | undefined {
90
- return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
91
- }
92
-
93
90
  function syncPlanNodeIssueAssociation(planId: string, node: PlanNode): void {
94
91
  if (!node.refs?.issueId) return;
95
92
  associateIssue(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
@@ -22,7 +22,14 @@
22
22
  // OUT OF SCOPE (transport-edge, excluded from the parity facet): automatic memory injection
23
23
  // (needs request-derived context) and per-transport audit (HTTP domain row vs MCP tool-call row).
24
24
  import { isMechanicalMessageKind, isRecord, isReservedAgentId } from "agent-relay-sdk";
25
- import { ValidationError, getMessage, listAgents, sendMessageWithResult, channelObligationAnsweredBy } from "../db";
25
+ import {
26
+ ValidationError,
27
+ getMessage,
28
+ listAgents,
29
+ sendMessageWithResult,
30
+ channelObligationAnsweredBy,
31
+ spawnObligationAnsweredBy,
32
+ } from "../db";
26
33
  import { planSend, type DeliveryReceipt } from "../agent-ref";
27
34
  import { emitMessageQueued, emitNewMessage } from "../sse";
28
35
  import { getLifecycleManager } from "../lifecycle-manager";
@@ -32,14 +39,14 @@ import type { AuthContext } from "./auth-context";
32
39
  import type { IntegrationTokenConfig } from "../config";
33
40
  import type { Message, SendMessageInput } from "../types";
34
41
 
35
- export interface SendMessageResult {
42
+ interface SendMessageResult {
36
43
  message: Message;
37
44
  created: boolean;
38
45
  /** Synchronous delivery receipt (live recipients / queued / store-ahead). */
39
46
  receipt: DeliveryReceipt;
40
47
  }
41
48
 
42
- export interface SendMessageOptions {
49
+ interface SendMessageOptions {
43
50
  /** The integration token when the MCP caller authenticated with one. AuthContext stays lean
44
51
  * (foundation contract) — the integration-specific target policy rides here, not on the ctx. */
45
52
  integration?: IntegrationTokenConfig | null;
@@ -159,16 +166,13 @@ export function sendMessageService(
159
166
  // Resolve BEFORE authorize so both run against the canonical id (kills the HTTP-authorizes-raw
160
167
  // vs MCP-authorizes-resolved drift).
161
168
  const receipt = resolveSendTarget(input, input.from);
162
- // Channel-answer linking: an agent answering the human through a channel bridge posts a body-less
163
- // message to the channel agent (e.g. `agent-relay /message telegram '{"action":"reply",...}'`) that
164
- // carries no reply/conversation linkage, so its reply obligation never clears and the Stop hook nags
165
- // it into a redundant `/reply` that leaks back to the human as a second message. When the agent has
166
- // exactly ONE open obligation on that channel this send is unambiguously the answer — link it here,
167
- // at send time, so the obligation clears with no extra message. (`channelObligationAnsweredBy` is a
168
- // no-op unless `to` resolved to a channel agent with a single open obligation; deciding at send time
169
- // — not query time — keeps a later sibling conversation from retroactively mis-attributing it.)
169
+ // Reply-obligation linking: some real answers are ordinary sends without replyTo/thread linkage.
170
+ // Channel bridge replies and spawn report-backs both have a single unambiguous open obligation case;
171
+ // link those at send time so the obligation clears with one message and the Stop hook does not nag
172
+ // the agent into a redundant `/reply`.
170
173
  if (!input.replyTo && !isMechanicalMessageKind(input.kind)) {
171
- const answered = channelObligationAnsweredBy(input.from, input.to);
174
+ const answered = channelObligationAnsweredBy(input.from, input.to)
175
+ ?? spawnObligationAnsweredBy(input.from, input.to);
172
176
  if (answered !== null) input.replyTo = answered;
173
177
  }
174
178
  authorizeSend(input, ctx, opts, reservedSink);
@@ -50,7 +50,7 @@ export class ShutdownTargetError extends Error {}
50
50
 
51
51
  type OnlineOrchestrator = NonNullable<ReturnType<typeof getOrchestrator>>;
52
52
 
53
- export interface ShutdownAgentInput {
53
+ interface ShutdownAgentInput {
54
54
  agentId?: string;
55
55
  policyName?: string;
56
56
  spawnRequestId?: string;
@@ -69,7 +69,7 @@ export interface ShutdownAgentInput {
69
69
  auditMetadata?: Record<string, unknown>;
70
70
  }
71
71
 
72
- export interface ShutdownAgentResult {
72
+ interface ShutdownAgentResult {
73
73
  command: Command;
74
74
  orchestratorId: string;
75
75
  agent: AgentCard | null;
@@ -8,7 +8,7 @@ import { setCallerTeamBrief } from "./team";
8
8
  import { spawnAgent, type SpawnAgentInput, type SpawnAgentResult } from "./spawn-agent";
9
9
  import type { AgentLifecycle, SpawnApprovalMode, SpawnProvider, TeamTemplate, TeamTemplateCompositionEntry, WorkspaceMode } from "../types";
10
10
 
11
- export interface TeamTemplateInstantiateInput {
11
+ interface TeamTemplateInstantiateInput {
12
12
  slug: string;
13
13
  orchestratorId?: unknown;
14
14
  cwd?: unknown;
@@ -19,7 +19,7 @@ export interface TeamTemplateInstantiateInput {
19
19
  waitForRegistrationMs?: unknown;
20
20
  }
21
21
 
22
- export interface TeamTemplateInstantiateResult {
22
+ interface TeamTemplateInstantiateResult {
23
23
  template: TeamTemplate;
24
24
  team: ReturnType<typeof setCallerTeamBrief>["team"];
25
25
  spawned: SpawnAgentResult[];
@@ -7,7 +7,7 @@ import type { Team } from "../types";
7
7
  export class TeamAuthError extends Error {}
8
8
  export class TeamNotFoundError extends Error {}
9
9
 
10
- export interface TeamPayload {
10
+ interface TeamPayload {
11
11
  team: Team;
12
12
  roster: ReturnType<typeof getTeamRoster>;
13
13
  brief: string | null;
@@ -16,7 +16,7 @@ export interface TeamPayload {
16
16
  issues: ReturnType<typeof listIssueRefsForEntity>;
17
17
  }
18
18
 
19
- export interface TeamBriefInput {
19
+ interface TeamBriefInput {
20
20
  title?: string;
21
21
  brief?: string;
22
22
  definitionOfDone?: string;
@@ -73,7 +73,7 @@ export function registerChannelTypeSetting(input: {
73
73
  });
74
74
  }
75
75
 
76
- export interface ProviderSettingsDeclaration {
76
+ interface ProviderSettingsDeclaration {
77
77
  provider: string;
78
78
  defaults: ProviderConfig;
79
79
  display?: Partial<SettingDisplay>;