agent-relay-server 0.88.2 → 0.89.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.
@@ -53,6 +53,24 @@ interface SendMessageOptions {
53
53
  integration?: IntegrationTokenConfig | null;
54
54
  }
55
55
 
56
+ export function validateSendMessageAuthorization(
57
+ input: SendMessageInput,
58
+ ctx: AuthContext,
59
+ opts: SendMessageOptions = {},
60
+ ): DeliveryReceipt {
61
+ applyReplyRouting(input);
62
+ if (!input.to) throw new ValidationError("to is required (or provide replyTo to auto-route)");
63
+ // De-singletoned human addressing (#393): collapse a human address to its stored/canonical
64
+ // form so `user:default` is a fully WORKING alias of the legacy bare `'user'` sink.
65
+ if (isHumanAgentId(input.to)) input.to = canonicalHumanAgentId(input.to);
66
+ if (input.from && isHumanAgentId(input.from)) input.from = canonicalHumanAgentId(input.from);
67
+ const reservedSink = isMechanicalMessageKind(input.kind) && isReservedAgentId(input.to);
68
+ input.from = resolveFrom(input, ctx, reservedSink);
69
+ const receipt = resolveSendTarget(input, input.from);
70
+ authorizeSend(input, ctx, opts, reservedSink);
71
+ return receipt;
72
+ }
73
+
56
74
  /** `from` resolves from the token identity: `callerAgentId` (the resolved agent behind the
57
75
  * token) cannot be spoofed and WINS over any wire `from`. The wire value is a fallback only
58
76
  * for identity-less tokens (admin/server/integration/multi-agent).
@@ -157,22 +175,7 @@ export function sendMessageService(
157
175
  ctx: AuthContext,
158
176
  opts: SendMessageOptions = {},
159
177
  ): SendMessageResult {
160
- applyReplyRouting(input);
161
- if (!input.to) throw new ValidationError("to is required (or provide replyTo to auto-route)");
162
- // De-singletoned human addressing (#393): collapse a human address to its stored/canonical
163
- // form so `user:default` is a fully WORKING alias of the legacy bare `'user'` sink — it routes
164
- // to the same agent row + inbox + thread, with zero live-byte movement. Per-user `user:<id>`
165
- // addresses pass through unchanged for the later #388 waves; non-human ids are untouched.
166
- if (isHumanAgentId(input.to)) input.to = canonicalHumanAgentId(input.to);
167
- if (input.from && isHumanAgentId(input.from)) input.from = canonicalHumanAgentId(input.from);
168
- // The reserved-sink observability lane (#284) governs both `from` resolution (wire `from` is
169
- // authoritative — the runner reports an agent's turn) and authorization (drop the recipient
170
- // predicate). Compute once, after reply-routing has settled `to`.
171
- const reservedSink = isMechanicalMessageKind(input.kind) && isReservedAgentId(input.to);
172
- input.from = resolveFrom(input, ctx, reservedSink);
173
- // Resolve BEFORE authorize so both run against the canonical id (kills the HTTP-authorizes-raw
174
- // vs MCP-authorizes-resolved drift).
175
- const receipt = resolveSendTarget(input, input.from);
178
+ const receipt = validateSendMessageAuthorization(input, ctx, opts);
176
179
  // Reply-obligation linking: some real answers are ordinary sends without replyTo/thread linkage.
177
180
  // Channel bridge replies and spawn report-backs both have a single unambiguous open obligation case;
178
181
  // link those at send time so the obligation clears with one message and the Stop hook does not nag
@@ -182,8 +185,6 @@ export function sendMessageService(
182
185
  ?? spawnObligationAnsweredBy(input.from, input.to);
183
186
  if (answered !== null) input.replyTo = answered;
184
187
  }
185
- authorizeSend(input, ctx, opts, reservedSink);
186
-
187
188
  const result = sendMessageWithResult(input);
188
189
  if (result.created) {
189
190
  if (result.message.deliveryStatus === "queued") {
@@ -137,7 +137,7 @@ export function reapTransientAgents(): Record<string, unknown> {
137
137
  emitRelayEvent({ type: "agent.exited", source: "server", subject: agent.id, data: { agentId: agent.id, parent: agent.spawnedBy, reason: "transient-complete", workspaceId: land.workspaceId, workspaceStatus: land.status } });
138
138
  // #645 — persist a clean terminal marker so the #634 heartbeat-gap watchdog reads this benign
139
139
  // completion as a terminus (not a silent death) once the reaped worker stops heartbeating.
140
- recordTerminalEvent({ agentId: agent.id, reason: "transient-complete", kind: "agent.exited", createdAt: now });
140
+ recordTerminalEvent({ agentId: agent.id, reason: "transient-complete", kind: "agent.exited", epoch: agent.epoch, createdAt: now });
141
141
  createActivityEvent({
142
142
  clientId: `transient-agent-reaped-${agent.id}-${now}`,
143
143
  kind: "state",
@@ -20,6 +20,9 @@ export interface TeamMemberWatch {
20
20
  lastActiveAt: number;
21
21
  /** Whether the member's CURRENT silence has already pinged the coordinator (re-armed on recovery). */
22
22
  pinged: boolean;
23
+ /** Agent epoch at the time the member was last observed alive. Used to epoch-scope the terminal-event
24
+ * suppression check (#652): a stale event from an older epoch must not suppress a current-epoch alert. */
25
+ epoch: number;
23
26
  /** Member label for a readable ping. */
24
27
  label?: string;
25
28
  }
@@ -39,6 +42,9 @@ export interface TeamCoordinatorWatch {
39
42
  lastActiveAt: number;
40
43
  /** Whether the coordinator's CURRENT gone-spell already escalated (re-armed when it recovers). */
41
44
  escalated: boolean;
45
+ /** Agent epoch at the time the coordinator was last observed alive. Used to epoch-scope the terminal-event
46
+ * suppression check (#652): a stale event from an older epoch must not suppress a current-epoch alert. */
47
+ epoch: number;
42
48
  /** Coordinator label for a readable escalation. */
43
49
  label?: string;
44
50
  }
package/src/token-db.ts CHANGED
@@ -78,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
78
78
  name: "Provider Agent",
79
79
  description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
80
80
  role: "provider",
81
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
81
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "schedule:write", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
82
82
  ttlSeconds: 24 * 60 * 60,
83
83
  builtIn: true,
84
84
  createdBy: "system",
@@ -88,7 +88,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
88
88
  name: "Provider Child Agent",
89
89
  description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
90
90
  role: "provider",
91
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
91
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "schedule:write", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
92
92
  constraints: { canDelegate: false },
93
93
  ttlSeconds: 2 * 60 * 60,
94
94
  builtIn: true,
@@ -99,7 +99,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
99
99
  name: "Provider Interactive Agent",
100
100
  description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
101
101
  role: "provider",
102
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
102
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "schedule:write", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
103
103
  constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
104
104
  ttlSeconds: 30 * 24 * 60 * 60,
105
105
  builtIn: true,
@@ -120,7 +120,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
120
120
  name: "MCP Client",
121
121
  description: "Tool-client access for Relay MCP calls plus message read/send.",
122
122
  role: "mcp",
123
- scope: ["mcp:use", "message:read", "message:send"],
123
+ scope: ["mcp:use", "message:read", "message:send", "schedule:write"],
124
124
  ttlSeconds: 7 * 24 * 60 * 60,
125
125
  builtIn: true,
126
126
  createdBy: "system",
@@ -343,7 +343,7 @@ function ensureBuiltInTokenProfiles(): void {
343
343
  }
344
344
 
345
345
  function defaultScopes(role: string): string[] {
346
- if (role === "provider") return ["agent:read", "agent:write", "message:send", "message:read", "artifact:read"];
346
+ if (role === "provider") return ["agent:read", "agent:write", "message:send", "message:read", "schedule:write", "artifact:read"];
347
347
  if (role === "channel") return ["agent:read", "agent:write", "message:send", "message:read"];
348
348
  if (role === "orchestrator") return ["agent:read", "agent:write", "command:*", "task:read", "task:write"];
349
349
  if (role === "admin" || role === "dashboard") return ["admin:*"];