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.
- package/docs/openapi.json +1 -1
- package/package.json +2 -2
- package/public/assets/display-CWMHll1J.js.map +1 -1
- package/runner/src/adapter.ts +4 -0
- package/src/agent-lifecycle-events.ts +74 -4
- package/src/branch-landed.ts +2 -2
- package/src/commands-db.ts +4 -1
- package/src/config.ts +7 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +52 -0
- package/src/db/scheduled-timers.ts +297 -0
- package/src/db/schema.ts +31 -0
- package/src/db/terminal-events.ts +17 -11
- package/src/index.ts +2 -0
- package/src/maintenance/teams.ts +7 -2
- package/src/mcp/index.ts +5 -0
- package/src/mcp/tools-messaging.ts +22 -2
- package/src/mcp/tools-scheduler.ts +135 -0
- package/src/notification-types.ts +2 -0
- package/src/quota-advisory.ts +27 -3
- package/src/services/scheduler.ts +303 -0
- package/src/services/send-message.ts +19 -18
- package/src/services/transient-agent-reaper.ts +1 -1
- package/src/team-idle-tracker.ts +6 -0
- package/src/token-db.ts +5 -5
|
@@ -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
|
-
|
|
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",
|
package/src/team-idle-tracker.ts
CHANGED
|
@@ -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:*"];
|