agent-relay-server 0.88.3 → 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/src/branch-landed.ts +2 -2
- package/src/config.ts +7 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +36 -0
- package/src/db/scheduled-timers.ts +297 -0
- package/src/db/schema.ts +31 -0
- package/src/index.ts +2 -0
- 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/services/scheduler.ts +303 -0
- package/src/services/send-message.ts +19 -18
- package/src/token-db.ts +5 -5
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { errMessage } from "agent-relay-sdk";
|
|
3
|
+
import { hasScope } from "agent-relay-sdk/types";
|
|
4
|
+
import {
|
|
5
|
+
MAX_BODY_BYTES,
|
|
6
|
+
SCHEDULER_CLAIM_LEASE_MS,
|
|
7
|
+
SCHEDULER_MAX_ACTIVE_TIMERS,
|
|
8
|
+
SCHEDULER_MAX_FAILURES,
|
|
9
|
+
SCHEDULER_MAX_PAYLOAD_BYTES,
|
|
10
|
+
SCHEDULER_MIN_RECURRING_INTERVAL_MS,
|
|
11
|
+
SCHEDULER_RETRY_MS,
|
|
12
|
+
SCHEDULER_TICK_MS,
|
|
13
|
+
} from "../config";
|
|
14
|
+
import {
|
|
15
|
+
ValidationError,
|
|
16
|
+
cancelScheduledTimer,
|
|
17
|
+
claimScheduledTimer,
|
|
18
|
+
completeScheduledTimerRun,
|
|
19
|
+
countActiveScheduledTimers,
|
|
20
|
+
disableScheduledTimer,
|
|
21
|
+
getScheduledTimer,
|
|
22
|
+
insertScheduledTimer,
|
|
23
|
+
listDueScheduledTimerIds,
|
|
24
|
+
listScheduledTimers,
|
|
25
|
+
recordScheduledTimerFailure,
|
|
26
|
+
type ScheduledTimer,
|
|
27
|
+
type TimerScheduleDef,
|
|
28
|
+
type TimerScheduleKind,
|
|
29
|
+
} from "../db";
|
|
30
|
+
import type { ComponentToken, SendMessageInput } from "../types";
|
|
31
|
+
import { ServiceAuthError } from "./errors";
|
|
32
|
+
import type { AuthContext } from "./auth-context";
|
|
33
|
+
import { sendMessageService, validateSendMessageAuthorization } from "./send-message";
|
|
34
|
+
|
|
35
|
+
export interface ScheduleTimerInput {
|
|
36
|
+
target?: string;
|
|
37
|
+
afterMs?: number;
|
|
38
|
+
at?: string;
|
|
39
|
+
everyMs?: number;
|
|
40
|
+
rrule?: string;
|
|
41
|
+
timezone?: string;
|
|
42
|
+
subject?: string;
|
|
43
|
+
body: string;
|
|
44
|
+
payload?: Record<string, unknown>;
|
|
45
|
+
maxRuns?: number;
|
|
46
|
+
expiresAt?: string | number;
|
|
47
|
+
idempotencyKey?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RunDueTimersResult {
|
|
51
|
+
claimed: number;
|
|
52
|
+
fired: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
disabled: number;
|
|
55
|
+
messageIds: number[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let schedulerTimer: Timer | undefined;
|
|
59
|
+
|
|
60
|
+
export function startRelayScheduler(): void {
|
|
61
|
+
if (schedulerTimer) return;
|
|
62
|
+
void runDueScheduledTimers().catch((error) => console.warn(`relay scheduler tick failed: ${errMessage(error)}`));
|
|
63
|
+
schedulerTimer = setInterval(() => {
|
|
64
|
+
void runDueScheduledTimers().catch((error) => console.warn(`relay scheduler tick failed: ${errMessage(error)}`));
|
|
65
|
+
}, SCHEDULER_TICK_MS);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stopRelaySchedulerForTests(): void {
|
|
69
|
+
if (schedulerTimer) clearInterval(schedulerTimer);
|
|
70
|
+
schedulerTimer = undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function scheduleTimer(input: ScheduleTimerInput, ctx: AuthContext): ScheduledTimer {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const schedule = normalizeSchedule(input, now);
|
|
76
|
+
const target = normalizeTarget(input.target, ctx);
|
|
77
|
+
const createdBy = ctx.callerAgentId ?? ctx.actor.id;
|
|
78
|
+
const deliveryFrom = ctx.callerAgentId ?? "system";
|
|
79
|
+
const body = cleanBody(input.body);
|
|
80
|
+
const payload = cleanPayload(input.payload);
|
|
81
|
+
const maxRuns = cleanPositiveInt(input.maxRuns, "maxRuns");
|
|
82
|
+
const expiresAt = cleanExpiresAt(input.expiresAt, now);
|
|
83
|
+
const idempotencyKey = cleanOptionalString(input.idempotencyKey, "idempotencyKey", 240);
|
|
84
|
+
|
|
85
|
+
if (schedule.kind === "interval") {
|
|
86
|
+
if ((schedule.def.everyMs ?? 0) < SCHEDULER_MIN_RECURRING_INTERVAL_MS) {
|
|
87
|
+
throw new ValidationError(`everyMs must be at least ${SCHEDULER_MIN_RECURRING_INTERVAL_MS}`);
|
|
88
|
+
}
|
|
89
|
+
if (!maxRuns && !expiresAt && !canCreatePersistentTimer(ctx)) {
|
|
90
|
+
throw new ServiceAuthError("recurring timers require maxRuns, expiresAt, or schedule:persistent");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (expiresAt !== undefined && expiresAt <= schedule.nextRunAt) {
|
|
94
|
+
throw new ValidationError("expiresAt must be after the first run");
|
|
95
|
+
}
|
|
96
|
+
if (countActiveScheduledTimers(createdBy) >= SCHEDULER_MAX_ACTIVE_TIMERS) {
|
|
97
|
+
throw new ServiceAuthError(`active timer quota exceeded (${SCHEDULER_MAX_ACTIVE_TIMERS})`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
validateSendMessageAuthorization({
|
|
101
|
+
from: deliveryFrom,
|
|
102
|
+
to: target,
|
|
103
|
+
body,
|
|
104
|
+
subject: cleanOptionalString(input.subject, "subject", 200),
|
|
105
|
+
payload,
|
|
106
|
+
idempotencyKey: idempotencyKey ? `${idempotencyKey}:probe` : undefined,
|
|
107
|
+
}, ctx);
|
|
108
|
+
|
|
109
|
+
return insertScheduledTimer({
|
|
110
|
+
target,
|
|
111
|
+
createdBy,
|
|
112
|
+
createdByKind: ctx.actor.kind,
|
|
113
|
+
createdByScopes: ctx.scopes,
|
|
114
|
+
createdByConstraints: ctx.constraints,
|
|
115
|
+
deliveryFrom,
|
|
116
|
+
nextRunAt: schedule.nextRunAt,
|
|
117
|
+
scheduleKind: schedule.kind,
|
|
118
|
+
scheduleDef: schedule.def,
|
|
119
|
+
subject: cleanOptionalString(input.subject, "subject", 200),
|
|
120
|
+
body,
|
|
121
|
+
payload,
|
|
122
|
+
maxRuns,
|
|
123
|
+
expiresAt,
|
|
124
|
+
idempotencyKey,
|
|
125
|
+
}, now);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function listTimers(ctx: AuthContext, input: { includeDisabled?: boolean; limit?: number; createdBy?: string } = {}): ScheduledTimer[] {
|
|
129
|
+
const admin = canAdministerTimers(ctx);
|
|
130
|
+
const createdBy = admin ? input.createdBy : (ctx.callerAgentId ?? ctx.actor.id);
|
|
131
|
+
return listScheduledTimers({
|
|
132
|
+
createdBy,
|
|
133
|
+
includeDisabled: input.includeDisabled,
|
|
134
|
+
limit: input.limit,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function cancelTimer(id: string, ctx: AuthContext): ScheduledTimer {
|
|
139
|
+
const timer = getScheduledTimer(id);
|
|
140
|
+
if (!timer) throw new ValidationError(`timer ${id} not found`);
|
|
141
|
+
if (!canAdministerTimers(ctx) && timer.createdBy !== (ctx.callerAgentId ?? ctx.actor.id)) {
|
|
142
|
+
throw new ServiceAuthError("cannot cancel a timer created by another caller");
|
|
143
|
+
}
|
|
144
|
+
return cancelScheduledTimer(id) ?? timer;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function runDueScheduledTimers(input: { now?: number; limit?: number; owner?: string } = {}): Promise<RunDueTimersResult> {
|
|
148
|
+
const now = input.now ?? Date.now();
|
|
149
|
+
const owner = input.owner ?? `${process.pid}-${randomUUID()}`;
|
|
150
|
+
const ids = listDueScheduledTimerIds(now, input.limit ?? 25);
|
|
151
|
+
const result: RunDueTimersResult = { claimed: 0, fired: 0, failed: 0, disabled: 0, messageIds: [] };
|
|
152
|
+
for (const id of ids) {
|
|
153
|
+
const timer = claimScheduledTimer(id, owner, now, SCHEDULER_CLAIM_LEASE_MS);
|
|
154
|
+
if (!timer) continue;
|
|
155
|
+
result.claimed += 1;
|
|
156
|
+
const fired = fireClaimedTimer(timer, now);
|
|
157
|
+
if (fired.status === "fired") {
|
|
158
|
+
result.fired += 1;
|
|
159
|
+
result.messageIds.push(fired.messageId);
|
|
160
|
+
} else if (fired.status === "disabled") {
|
|
161
|
+
result.disabled += 1;
|
|
162
|
+
} else {
|
|
163
|
+
result.failed += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function fireClaimedTimer(timer: ScheduledTimer, now: number): { status: "fired"; messageId: number } | { status: "failed" | "disabled" } {
|
|
170
|
+
if (timer.expiresAt !== undefined && timer.expiresAt <= now) {
|
|
171
|
+
disableScheduledTimer(timer.id, "timer expired before delivery");
|
|
172
|
+
return { status: "disabled" };
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const runNumber = timer.runCount + 1;
|
|
176
|
+
const result = sendMessageService({
|
|
177
|
+
from: timer.deliveryFrom,
|
|
178
|
+
to: timer.target,
|
|
179
|
+
subject: timer.subject,
|
|
180
|
+
body: timer.body,
|
|
181
|
+
payload: {
|
|
182
|
+
...timer.payload,
|
|
183
|
+
scheduler: {
|
|
184
|
+
timerId: timer.id,
|
|
185
|
+
correlationId: timer.correlationId,
|
|
186
|
+
run: runNumber,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
meta: {
|
|
190
|
+
scheduler: {
|
|
191
|
+
timerId: timer.id,
|
|
192
|
+
correlationId: timer.correlationId,
|
|
193
|
+
run: runNumber,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
idempotencyKey: timerRunIdempotencyKey(timer, runNumber),
|
|
197
|
+
} satisfies SendMessageInput, persistedAuthContext(timer));
|
|
198
|
+
completeScheduledTimerRun({ timer, messageId: result.message.id, now });
|
|
199
|
+
return { status: "fired", messageId: result.message.id };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const updated = recordScheduledTimerFailure({
|
|
202
|
+
id: timer.id,
|
|
203
|
+
error: errMessage(error),
|
|
204
|
+
now,
|
|
205
|
+
retryMs: SCHEDULER_RETRY_MS,
|
|
206
|
+
maxFailures: SCHEDULER_MAX_FAILURES,
|
|
207
|
+
});
|
|
208
|
+
return updated?.enabled ? { status: "failed" } : { status: "disabled" };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function timerRunIdempotencyKey(timer: ScheduledTimer, runNumber: number): string {
|
|
213
|
+
const base = timer.idempotencyKey ?? `scheduler:${timer.id}`;
|
|
214
|
+
return `${base}:run:${runNumber}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function persistedAuthContext(timer: ScheduledTimer): AuthContext {
|
|
218
|
+
const component: ComponentToken = {
|
|
219
|
+
sub: timer.createdBy,
|
|
220
|
+
role: timer.createdByKind,
|
|
221
|
+
scope: timer.createdByScopes,
|
|
222
|
+
constraints: timer.createdByConstraints,
|
|
223
|
+
iat: Math.floor(timer.createdAt / 1000),
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
actor: { id: timer.createdBy, kind: "component" },
|
|
227
|
+
scopes: timer.createdByScopes,
|
|
228
|
+
constraints: timer.createdByConstraints,
|
|
229
|
+
callerAgentId: timer.deliveryFrom === "system" ? undefined : timer.deliveryFrom,
|
|
230
|
+
component,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeSchedule(input: ScheduleTimerInput, now: number): { kind: TimerScheduleKind; def: TimerScheduleDef; nextRunAt: number } {
|
|
235
|
+
if (input.rrule || input.timezone) throw new ValidationError("rrule/timezone timers are not supported yet");
|
|
236
|
+
const modes = [input.afterMs !== undefined, input.at !== undefined, input.everyMs !== undefined].filter(Boolean).length;
|
|
237
|
+
if (modes !== 1) throw new ValidationError("provide exactly one of afterMs, at, or everyMs");
|
|
238
|
+
if (input.afterMs !== undefined) {
|
|
239
|
+
const afterMs = cleanPositiveInt(input.afterMs, "afterMs")!;
|
|
240
|
+
return { kind: "once", def: { afterMs }, nextRunAt: now + afterMs };
|
|
241
|
+
}
|
|
242
|
+
if (input.at !== undefined) {
|
|
243
|
+
const atMs = Date.parse(input.at);
|
|
244
|
+
if (!Number.isFinite(atMs) || atMs <= now) throw new ValidationError("at must be a future ISO timestamp");
|
|
245
|
+
return { kind: "once", def: { at: new Date(atMs).toISOString() }, nextRunAt: atMs };
|
|
246
|
+
}
|
|
247
|
+
const everyMs = cleanPositiveInt(input.everyMs, "everyMs")!;
|
|
248
|
+
return { kind: "interval", def: { everyMs }, nextRunAt: now + everyMs };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeTarget(target: string | undefined, ctx: AuthContext): string {
|
|
252
|
+
const raw = cleanOptionalString(target, "target", 200) ?? "me";
|
|
253
|
+
if (raw !== "me") return raw;
|
|
254
|
+
if (!ctx.callerAgentId) throw new ValidationError("target defaults to me, but caller has no single agent identity");
|
|
255
|
+
return ctx.callerAgentId;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cleanBody(value: unknown): string {
|
|
259
|
+
if (typeof value !== "string" || !value.trim()) throw new ValidationError("body required");
|
|
260
|
+
const body = value.trim();
|
|
261
|
+
if (new TextEncoder().encode(body).byteLength > MAX_BODY_BYTES) throw new ValidationError(`body exceeds ${MAX_BODY_BYTES} bytes`);
|
|
262
|
+
return body;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function cleanPayload(value: unknown): Record<string, unknown> | undefined {
|
|
266
|
+
if (value === undefined || value === null) return undefined;
|
|
267
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new ValidationError("payload must be an object");
|
|
268
|
+
const payload = value as Record<string, unknown>;
|
|
269
|
+
if (new TextEncoder().encode(JSON.stringify(payload)).byteLength > SCHEDULER_MAX_PAYLOAD_BYTES) {
|
|
270
|
+
throw new ValidationError(`payload exceeds ${SCHEDULER_MAX_PAYLOAD_BYTES} bytes`);
|
|
271
|
+
}
|
|
272
|
+
return payload;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function cleanOptionalString(value: unknown, field: string, max: number): string | undefined {
|
|
276
|
+
if (value === undefined || value === null) return undefined;
|
|
277
|
+
if (typeof value !== "string") throw new ValidationError(`${field} must be a string`);
|
|
278
|
+
const trimmed = value.trim();
|
|
279
|
+
if (!trimmed) return undefined;
|
|
280
|
+
if (trimmed.length > max) throw new ValidationError(`${field} must be ${max} characters or fewer`);
|
|
281
|
+
return trimmed;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function cleanPositiveInt(value: unknown, field: string): number | undefined {
|
|
285
|
+
if (value === undefined || value === null) return undefined;
|
|
286
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new ValidationError(`${field} must be a positive integer`);
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cleanExpiresAt(value: unknown, now: number): number | undefined {
|
|
291
|
+
if (value === undefined || value === null) return undefined;
|
|
292
|
+
const ms = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
293
|
+
if (!Number.isFinite(ms) || ms <= now) throw new ValidationError("expiresAt must be a future ISO timestamp or unix timestamp in milliseconds");
|
|
294
|
+
return ms;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function canAdministerTimers(ctx: AuthContext): boolean {
|
|
298
|
+
return ctx.actor.kind === "system" || hasScope(ctx.scopes, "system:admin");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function canCreatePersistentTimer(ctx: AuthContext): boolean {
|
|
302
|
+
return canAdministerTimers(ctx) || hasScope(ctx.scopes, "schedule:persistent");
|
|
303
|
+
}
|
|
@@ -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") {
|
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:*"];
|