agent-relay-server 0.18.0 → 0.19.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.
- package/package.json +2 -2
- package/public/index.html +14 -10
- package/runner/src/config.ts +2 -1
- package/src/automations.ts +13 -28
- package/src/bus.ts +2 -2
- package/src/cli.ts +183 -8
- package/src/config-store.ts +9 -27
- package/src/daemon.ts +1 -4
- package/src/db.ts +9 -2
- package/src/dev.ts +1 -4
- package/src/http-body.ts +49 -0
- package/src/index.ts +2 -1
- package/src/maintenance.ts +3 -3
- package/src/mcp.ts +17 -68
- package/src/memory-broker-smoke.ts +2 -2
- package/src/memory-command-broker.ts +2 -2
- package/src/memory-http-broker.ts +2 -2
- package/src/orchestrator-lookup.ts +29 -0
- package/src/recipe-validator.ts +2 -2
- package/src/routes.ts +180 -179
- package/src/setup.ts +1 -4
- package/src/spawn-command.ts +2 -1
- package/src/steward.ts +2 -1
- package/src/upgrade.ts +88 -17
- package/src/validation.ts +54 -2
package/src/routes.ts
CHANGED
|
@@ -98,7 +98,9 @@ import {
|
|
|
98
98
|
setMessageReaction,
|
|
99
99
|
ValidationError,
|
|
100
100
|
} from "./db";
|
|
101
|
-
import { cleanString } from "./validation";
|
|
101
|
+
import { cleanString, cleanStringArray, optionalEnum } from "./validation";
|
|
102
|
+
import { listManagedOrchestratorsForAgent } from "./orchestrator-lookup";
|
|
103
|
+
import { bytesToStream, readBodyBytes } from "./http-body";
|
|
102
104
|
import { getArtifactStorage, maxArtifactBytes, normalizeDigest } from "./artifact-storage";
|
|
103
105
|
import {
|
|
104
106
|
deleteConfig,
|
|
@@ -155,7 +157,7 @@ import { listHostDirectories } from "./agent-spawn";
|
|
|
155
157
|
import { defaultProviderConfig, loadProviderConfig, providerConfigPublic, writeProviderConfig } from "../runner/src/config";
|
|
156
158
|
import type { ProviderConfig } from "../runner/src/adapter";
|
|
157
159
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
158
|
-
import { isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, VALID_EFFORTS, APPROVAL_MODES, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
160
|
+
import { errMessage, isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, VALID_EFFORTS, APPROVAL_MODES, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
159
161
|
import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
160
162
|
import { buildManagedSpawnParams, effectiveManagedPolicyWorkspaceMode } from "./managed-policy";
|
|
161
163
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams, type SpawnModelParams } from "./spawn-command";
|
|
@@ -263,44 +265,11 @@ type ParseBodyResult<T> =
|
|
|
263
265
|
|
|
264
266
|
async function parseBody<T>(req: Request): Promise<ParseBodyResult<T>> {
|
|
265
267
|
if (!req.body) return { ok: true, body: null };
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
let totalBytes = 0;
|
|
270
|
-
|
|
271
|
-
while (true) {
|
|
272
|
-
const { done, value } = await reader.read();
|
|
273
|
-
if (done) break;
|
|
274
|
-
if (!value) continue;
|
|
275
|
-
|
|
276
|
-
totalBytes += value.byteLength;
|
|
277
|
-
if (totalBytes > MAX_BODY_BYTES) {
|
|
278
|
-
try {
|
|
279
|
-
await reader.cancel();
|
|
280
|
-
} catch {
|
|
281
|
-
// Ignore cancellation errors — we already know this request is too large.
|
|
282
|
-
}
|
|
283
|
-
return {
|
|
284
|
-
ok: false,
|
|
285
|
-
status: 413,
|
|
286
|
-
error: `request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
chunks.push(value);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (totalBytes === 0) return { ok: true, body: null };
|
|
294
|
-
|
|
295
|
-
const merged = new Uint8Array(totalBytes);
|
|
296
|
-
let offset = 0;
|
|
297
|
-
for (const chunk of chunks) {
|
|
298
|
-
merged.set(chunk, offset);
|
|
299
|
-
offset += chunk.byteLength;
|
|
300
|
-
}
|
|
301
|
-
|
|
268
|
+
const read = await readBodyBytes(req.body, MAX_BODY_BYTES);
|
|
269
|
+
if (!read.ok) return { ok: false, status: read.status, error: read.error };
|
|
270
|
+
if (read.bytes.byteLength === 0) return { ok: true, body: null };
|
|
302
271
|
try {
|
|
303
|
-
const decoded = new TextDecoder().decode(
|
|
272
|
+
const decoded = new TextDecoder().decode(read.bytes);
|
|
304
273
|
return { ok: true, body: JSON.parse(decoded) as T };
|
|
305
274
|
} catch {
|
|
306
275
|
return { ok: false, status: 400, error: "invalid JSON body" };
|
|
@@ -359,14 +328,6 @@ function cleanNullableString(value: unknown, field: string, max: number): string
|
|
|
359
328
|
return cleanString(value, field, { max }) ?? null;
|
|
360
329
|
}
|
|
361
330
|
|
|
362
|
-
function cleanStringArray(value: unknown, field: string): string[] | undefined {
|
|
363
|
-
if (value === undefined || value === null) return undefined;
|
|
364
|
-
if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array of strings`);
|
|
365
|
-
const cleaned = value.map((item) => cleanString(item, `${field} item`, { max: 80 })).filter(Boolean) as string[];
|
|
366
|
-
if (cleaned.length > 50) throw new ValidationError(`${field} can contain at most 50 values`);
|
|
367
|
-
return [...new Set(cleaned)];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
331
|
function cleanConstraintStringArray(value: unknown, field: string): string[] | undefined {
|
|
371
332
|
if (value === undefined || value === null) return undefined;
|
|
372
333
|
if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array of strings`);
|
|
@@ -422,33 +383,20 @@ function cleanParams(value: unknown, field = "params"): Record<string, unknown>
|
|
|
422
383
|
return value;
|
|
423
384
|
}
|
|
424
385
|
|
|
425
|
-
function cleanEnum<T extends readonly string[]>(
|
|
426
|
-
value: unknown,
|
|
427
|
-
field: string,
|
|
428
|
-
valid: T,
|
|
429
|
-
fallback?: T[number],
|
|
430
|
-
): T[number] | undefined {
|
|
431
|
-
if (value === undefined || value === null) return fallback;
|
|
432
|
-
if (typeof value !== "string" || !valid.includes(value)) {
|
|
433
|
-
throw new ValidationError(`${field} must be one of: ${valid.join(", ")}`);
|
|
434
|
-
}
|
|
435
|
-
return value as T[number];
|
|
436
|
-
}
|
|
437
|
-
|
|
438
386
|
function cleanWorkspaceMetadata(value: unknown, field: string): WorkspaceMetadata | undefined {
|
|
439
387
|
if (value === undefined || value === null) return undefined;
|
|
440
388
|
if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
|
|
441
389
|
return {
|
|
442
390
|
id: cleanString(value.id, `${field}.id`, { max: 160 }),
|
|
443
|
-
mode:
|
|
444
|
-
requestedMode:
|
|
391
|
+
mode: optionalEnum(value.mode, `${field}.mode`, VALID_WORKSPACE_MODES, "shared") as WorkspaceMode,
|
|
392
|
+
requestedMode: optionalEnum(value.requestedMode, `${field}.requestedMode`, VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
|
|
445
393
|
repoRoot: cleanString(value.repoRoot, `${field}.repoRoot`, { max: 1000 }),
|
|
446
394
|
sourceCwd: cleanString(value.sourceCwd, `${field}.sourceCwd`, { max: 1000 }),
|
|
447
395
|
worktreePath: cleanString(value.worktreePath, `${field}.worktreePath`, { max: 1000 }),
|
|
448
396
|
branch: cleanString(value.branch, `${field}.branch`, { max: 240 }),
|
|
449
397
|
baseRef: cleanString(value.baseRef, `${field}.baseRef`, { max: 240 }),
|
|
450
398
|
baseSha: cleanString(value.baseSha, `${field}.baseSha`, { max: 80 }),
|
|
451
|
-
status:
|
|
399
|
+
status: optionalEnum(value.status, `${field}.status`, VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined,
|
|
452
400
|
stewardAgentId: cleanString(value.stewardAgentId, `${field}.stewardAgentId`, { max: 240 }),
|
|
453
401
|
probe: isRecord(value.probe) ? value.probe as unknown as WorkspaceProbe : undefined,
|
|
454
402
|
};
|
|
@@ -522,16 +470,16 @@ function normalizeAgentInput(body: unknown): RegisterAgentInput {
|
|
|
522
470
|
const input: RegisterAgentInput = {
|
|
523
471
|
id: cleanString(body.id, "id", { required: true, max: 200 })!,
|
|
524
472
|
name: cleanString(body.name, "name", { required: true, max: 200 })!,
|
|
525
|
-
kind:
|
|
473
|
+
kind: optionalEnum(body.kind, "kind", VALID_AGENT_KINDS) as AgentKind | undefined,
|
|
526
474
|
status: status as RegisterAgentInput["status"] | undefined,
|
|
527
475
|
ready: body.ready as boolean | undefined,
|
|
528
476
|
};
|
|
529
477
|
|
|
530
478
|
const label = cleanNullableString(body.label, "label", 120);
|
|
531
479
|
if (label !== undefined) input.label = label;
|
|
532
|
-
const tags = cleanStringArray(body.tags, "tags");
|
|
480
|
+
const tags = cleanStringArray(body.tags, "tags", { itemMax: 80, maxItems: 50 });
|
|
533
481
|
if (tags) input.tags = tags;
|
|
534
|
-
const capabilities = cleanStringArray(body.capabilities, "capabilities");
|
|
482
|
+
const capabilities = cleanStringArray(body.capabilities, "capabilities", { itemMax: 80, maxItems: 50 });
|
|
535
483
|
if (capabilities) input.capabilities = capabilities;
|
|
536
484
|
const machine = cleanString(body.machine, "machine", { max: 120 });
|
|
537
485
|
if (machine) input.machine = machine;
|
|
@@ -630,14 +578,14 @@ function normalizeChannelBindingInput(body: unknown): {
|
|
|
630
578
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
631
579
|
const channelId = cleanString(body.channelId, "channelId", { required: true, max: 200 })!;
|
|
632
580
|
const conversationId = cleanString(body.conversationId, "conversationId", { max: 300 });
|
|
633
|
-
const mode =
|
|
581
|
+
const mode = optionalEnum(body.mode, "mode", VALID_CHANNEL_BINDING_MODES, "exclusive") as ChannelBindingMode | undefined;
|
|
634
582
|
const priority = typeof body.priority === "number" && Number.isSafeInteger(body.priority) ? body.priority : undefined;
|
|
635
583
|
|
|
636
584
|
let target: ChannelRouteTarget | undefined;
|
|
637
585
|
if (typeof body.target === "string") {
|
|
638
586
|
target = routeTargetFromAddress(body.target);
|
|
639
587
|
} else if (isRecord(body.target)) {
|
|
640
|
-
const type =
|
|
588
|
+
const type = optionalEnum(body.target.type, "target.type", VALID_CHANNEL_BINDING_TARGET_TYPES)!;
|
|
641
589
|
const id = type === "broadcast" ? undefined : cleanString(body.target.id, "target.id", { required: true, max: 240 })!;
|
|
642
590
|
if (type === "orchestrator") throw new ValidationError("orchestrator channel targets are not supported yet");
|
|
643
591
|
target = type === "broadcast" ? { type: "broadcast" } : { type, id } as ChannelRouteTarget;
|
|
@@ -758,8 +706,8 @@ function validateChannelAttachmentItem(item: unknown, field: string): void {
|
|
|
758
706
|
if (!isRecord(item)) throw new ValidationError(`${field} must be an object`);
|
|
759
707
|
const artifactId = cleanString(item.artifactId, `${field}.artifactId`, { max: 120 });
|
|
760
708
|
if (artifactId) {
|
|
761
|
-
|
|
762
|
-
|
|
709
|
+
optionalEnum(item.kind, `${field}.kind`, VALID_ARTIFACT_KINDS);
|
|
710
|
+
optionalEnum(item.role, `${field}.role`, VALID_ARTIFACT_ROLES);
|
|
763
711
|
cleanString(item.title, `${field}.title`, { max: 240 });
|
|
764
712
|
if (item.ref !== undefined) validateChannelAttachmentSourceRef(item.ref, `${field}.ref`);
|
|
765
713
|
return;
|
|
@@ -794,8 +742,8 @@ function cleanAttachmentRefs(value: unknown, field = "attachments"): AttachmentR
|
|
|
794
742
|
const normalizedRef = isRecord(ref) ? { ref: { ...ref } as AttachmentRef["ref"] } : {};
|
|
795
743
|
return {
|
|
796
744
|
artifactId: cleanString(item.artifactId, `${field}[${index}].artifactId`, { required: true, max: 120 })!,
|
|
797
|
-
kind:
|
|
798
|
-
role:
|
|
745
|
+
kind: optionalEnum(item.kind, `${field}[${index}].kind`, VALID_ARTIFACT_KINDS) as ArtifactKind | undefined,
|
|
746
|
+
role: optionalEnum(item.role, `${field}[${index}].role`, VALID_ARTIFACT_ROLES) as "media" | "patch" | "report" | "log" | "output" | "input" | undefined,
|
|
799
747
|
title: cleanString(item.title, `${field}[${index}].title`, { max: 240 }),
|
|
800
748
|
...normalizedRef,
|
|
801
749
|
...(isRecord(item.metadata) ? { metadata: item.metadata } : {}),
|
|
@@ -803,14 +751,6 @@ function cleanAttachmentRefs(value: unknown, field = "attachments"): AttachmentR
|
|
|
803
751
|
});
|
|
804
752
|
}
|
|
805
753
|
|
|
806
|
-
function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
807
|
-
return new ReadableStream<Uint8Array>({
|
|
808
|
-
start(controller) {
|
|
809
|
-
controller.enqueue(bytes);
|
|
810
|
-
controller.close();
|
|
811
|
-
},
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
754
|
|
|
815
755
|
function normalizeChannelEventBody(body: unknown): {
|
|
816
756
|
body: string;
|
|
@@ -844,11 +784,11 @@ function requireChannelSession(req: Request, body: unknown, channel: ChannelSumm
|
|
|
844
784
|
|
|
845
785
|
function normalizeIntegrationEvent(body: unknown): IntegrationEventInput {
|
|
846
786
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
847
|
-
const status =
|
|
787
|
+
const status = optionalEnum(body.status, "status", [...VALID_TASK_STATUSES, "resolved"] as const);
|
|
848
788
|
return {
|
|
849
789
|
source: cleanString(body.source, "source", { max: 120 }),
|
|
850
790
|
type: cleanString(body.type, "type", { max: 80 }) ?? "event",
|
|
851
|
-
severity:
|
|
791
|
+
severity: optionalEnum(body.severity, "severity", VALID_TASK_SEVERITIES, "info"),
|
|
852
792
|
status,
|
|
853
793
|
title: cleanString(body.title, "title", { required: true, max: 240 })!,
|
|
854
794
|
body: cleanString(body.body, "body", { required: true, max: MAX_BODY_BYTES })!,
|
|
@@ -886,13 +826,13 @@ function normalizeIntegrationRegistryInput(name: string, body: unknown): {
|
|
|
886
826
|
displayName: cleanString(body.displayName, "displayName", { max: 120 }),
|
|
887
827
|
description: cleanString(body.description, "description", { max: 1000 }),
|
|
888
828
|
enabled: body.enabled as boolean | undefined,
|
|
889
|
-
scopes: cleanStringArray(body.scopes, "scopes"),
|
|
890
|
-
targets: cleanStringArray(body.targets, "targets"),
|
|
891
|
-
channels: cleanStringArray(body.channels, "channels"),
|
|
829
|
+
scopes: cleanStringArray(body.scopes, "scopes", { itemMax: 80, maxItems: 50 }),
|
|
830
|
+
targets: cleanStringArray(body.targets, "targets", { itemMax: 80, maxItems: 50 }),
|
|
831
|
+
channels: cleanStringArray(body.channels, "channels", { itemMax: 80, maxItems: 50 }),
|
|
892
832
|
type: cleanString(body.type, "type", { max: 80 }),
|
|
893
833
|
icon: cleanString(body.icon, "icon", { max: 80 }),
|
|
894
834
|
accentColor: cleanString(body.accentColor, "accentColor", { max: 32 }),
|
|
895
|
-
tags: cleanStringArray(body.tags, "tags"),
|
|
835
|
+
tags: cleanStringArray(body.tags, "tags", { itemMax: 80, maxItems: 50 }),
|
|
896
836
|
homepageUrl: cleanString(body.homepageUrl, "homepageUrl", { max: 500 }),
|
|
897
837
|
repositoryUrl: cleanString(body.repositoryUrl, "repositoryUrl", { max: 500 }),
|
|
898
838
|
docsUrl: cleanString(body.docsUrl, "docsUrl", { max: 500 }),
|
|
@@ -950,7 +890,7 @@ function resolveIntegrationEventTarget(input: IntegrationEventInput): Integratio
|
|
|
950
890
|
|
|
951
891
|
function normalizeTaskStatusInput(body: unknown): TaskStatusInput {
|
|
952
892
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
953
|
-
const status =
|
|
893
|
+
const status = optionalEnum(body.status, "status", VALID_TASK_STATUSES);
|
|
954
894
|
if (!status) throw new ValidationError("status required");
|
|
955
895
|
return {
|
|
956
896
|
status: status as TaskStatus,
|
|
@@ -1086,7 +1026,7 @@ function normalizeActivityInput(body: unknown): ActivityEventInput {
|
|
|
1086
1026
|
return {
|
|
1087
1027
|
operatorId: cleanOperatorId(body.operatorId),
|
|
1088
1028
|
clientId: cleanString(body.clientId, "clientId", { max: 240 }),
|
|
1089
|
-
kind:
|
|
1029
|
+
kind: optionalEnum(body.kind, "kind", VALID_ACTIVITY_KINDS, "operator") as ActivityKind,
|
|
1090
1030
|
title: cleanString(body.title, "title", { required: true, max: 200 })!,
|
|
1091
1031
|
body: cleanString(body.body, "body", { max: 1000 }),
|
|
1092
1032
|
meta: cleanString(body.meta, "meta", { max: 500 }),
|
|
@@ -1216,7 +1156,7 @@ async function postCallback(deliveryId: number, url: string, payload: unknown):
|
|
|
1216
1156
|
});
|
|
1217
1157
|
finishCallbackDelivery(deliveryId, response.ok, response.ok ? undefined : `${response.status} ${response.statusText}`);
|
|
1218
1158
|
} catch (e) {
|
|
1219
|
-
finishCallbackDelivery(deliveryId, false,
|
|
1159
|
+
finishCallbackDelivery(deliveryId, false, errMessage(e));
|
|
1220
1160
|
} finally {
|
|
1221
1161
|
clearTimeout(timeout);
|
|
1222
1162
|
}
|
|
@@ -1533,8 +1473,8 @@ async function artifactUploadInput(req: Request): Promise<{
|
|
|
1533
1473
|
const file = form.get("file");
|
|
1534
1474
|
if (!(file instanceof File)) throw new ValidationError("multipart upload requires file");
|
|
1535
1475
|
if (file.size > maxBytes) throw new ValidationError(`artifact exceeds ${maxBytes} bytes`);
|
|
1536
|
-
const sensitivity =
|
|
1537
|
-
const kind =
|
|
1476
|
+
const sensitivity = optionalEnum(form.get("sensitivity"), "sensitivity", VALID_ARTIFACT_SENSITIVITIES, "normal") as ArtifactSensitivity;
|
|
1477
|
+
const kind = optionalEnum(form.get("kind"), "kind", VALID_ARTIFACT_KINDS) as ArtifactKind | undefined;
|
|
1538
1478
|
const expiresAtRaw = form.get("expiresAt");
|
|
1539
1479
|
const expiresAt = typeof expiresAtRaw === "string" && expiresAtRaw.trim() ? Number(expiresAtRaw) : undefined;
|
|
1540
1480
|
if (expiresAt !== undefined && (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())) throw new ValidationError("expiresAt must be a future unix timestamp in milliseconds");
|
|
@@ -1562,8 +1502,8 @@ async function artifactUploadInput(req: Request): Promise<{
|
|
|
1562
1502
|
filename: cleanString(req.headers.get("X-Artifact-Filename"), "filename", { max: 240 }),
|
|
1563
1503
|
mediaType: contentType || undefined,
|
|
1564
1504
|
digest: cleanString(req.headers.get("X-Artifact-Digest"), "digest", { max: 80 }),
|
|
1565
|
-
sensitivity:
|
|
1566
|
-
kind:
|
|
1505
|
+
sensitivity: optionalEnum(req.headers.get("X-Artifact-Sensitivity"), "sensitivity", VALID_ARTIFACT_SENSITIVITIES, "normal") as ArtifactSensitivity,
|
|
1506
|
+
kind: optionalEnum(req.headers.get("X-Artifact-Kind"), "kind", VALID_ARTIFACT_KINDS) as ArtifactKind | undefined,
|
|
1567
1507
|
metadata: undefined,
|
|
1568
1508
|
expiresAt,
|
|
1569
1509
|
};
|
|
@@ -1576,10 +1516,10 @@ function cleanMemoryQueryFromParams(params: URLSearchParams): MemoryQuery {
|
|
|
1576
1516
|
const visibility = params.get("visibility");
|
|
1577
1517
|
const minRelevance = params.get("minRelevance");
|
|
1578
1518
|
const limit = params.get("limit");
|
|
1579
|
-
if (type) query.type =
|
|
1519
|
+
if (type) query.type = optionalEnum(type, "type", VALID_MEMORY_TYPES) as MemoryType;
|
|
1580
1520
|
if (scope) query.scope = cleanString(scope, "scope", { max: 240 });
|
|
1581
1521
|
const tags = [...params.getAll("tag"), ...(params.get("tags")?.split(",") ?? [])].map((tag) => tag.trim()).filter(Boolean);
|
|
1582
|
-
if (tags.length) query.tags = cleanStringArray(tags, "tags");
|
|
1522
|
+
if (tags.length) query.tags = cleanStringArray(tags, "tags", { itemMax: 80, maxItems: 50 });
|
|
1583
1523
|
if (minRelevance !== null) {
|
|
1584
1524
|
const parsed = Number(minRelevance);
|
|
1585
1525
|
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) throw new ValidationError("minRelevance must be between 0 and 1");
|
|
@@ -1590,7 +1530,7 @@ function cleanMemoryQueryFromParams(params: URLSearchParams): MemoryQuery {
|
|
|
1590
1530
|
if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > 100) throw new ValidationError("limit must be an integer between 1 and 100");
|
|
1591
1531
|
query.limit = parsed;
|
|
1592
1532
|
}
|
|
1593
|
-
if (visibility) query.visibility =
|
|
1533
|
+
if (visibility) query.visibility = optionalEnum(visibility, "visibility", VALID_MEMORY_VISIBILITIES) as MemoryVisibility;
|
|
1594
1534
|
query.includeExpired = params.get("includeExpired") === "true" ? true : undefined;
|
|
1595
1535
|
query.includeSensitive = params.get("includeSensitive") === "true" ? true : undefined;
|
|
1596
1536
|
return query;
|
|
@@ -1599,9 +1539,9 @@ function cleanMemoryQueryFromParams(params: URLSearchParams): MemoryQuery {
|
|
|
1599
1539
|
function cleanMemoryQueryFromBody(body: unknown): MemoryQuery {
|
|
1600
1540
|
if (!isRecord(body)) throw new ValidationError("memory query body must be an object");
|
|
1601
1541
|
const query: MemoryQuery = {};
|
|
1602
|
-
query.type =
|
|
1542
|
+
query.type = optionalEnum(body.type, "type", VALID_MEMORY_TYPES) as MemoryType | undefined;
|
|
1603
1543
|
query.scope = cleanString(body.scope, "scope", { max: 240 });
|
|
1604
|
-
query.tags = cleanStringArray(body.tags, "tags");
|
|
1544
|
+
query.tags = cleanStringArray(body.tags, "tags", { itemMax: 80, maxItems: 50 });
|
|
1605
1545
|
if (body.minRelevance !== undefined) {
|
|
1606
1546
|
if (typeof body.minRelevance !== "number" || body.minRelevance < 0 || body.minRelevance > 1) throw new ValidationError("minRelevance must be between 0 and 1");
|
|
1607
1547
|
query.minRelevance = body.minRelevance;
|
|
@@ -1611,7 +1551,7 @@ function cleanMemoryQueryFromBody(body: unknown): MemoryQuery {
|
|
|
1611
1551
|
query.limit = body.limit;
|
|
1612
1552
|
}
|
|
1613
1553
|
query.includeExpired = typeof body.includeExpired === "boolean" ? body.includeExpired : undefined;
|
|
1614
|
-
query.visibility =
|
|
1554
|
+
query.visibility = optionalEnum(body.visibility, "visibility", VALID_MEMORY_VISIBILITIES) as MemoryVisibility | undefined;
|
|
1615
1555
|
query.includeSensitive = typeof body.includeSensitive === "boolean" ? body.includeSensitive : undefined;
|
|
1616
1556
|
return query;
|
|
1617
1557
|
}
|
|
@@ -1633,15 +1573,15 @@ function cleanCreateMemoryInput(body: unknown, ctx: MemoryBrokerContext): Create
|
|
|
1633
1573
|
? undefined
|
|
1634
1574
|
: cleanMemoryPositiveInteger(body.ttlMs, "ttlMs");
|
|
1635
1575
|
return {
|
|
1636
|
-
type:
|
|
1576
|
+
type: optionalEnum(body.type, "type", VALID_MEMORY_TYPES) as MemoryType,
|
|
1637
1577
|
scope: cleanString(body.scope, "scope", { required: true, max: 240 })!,
|
|
1638
1578
|
title: cleanString(body.title, "title", { required: true, max: 240 })!,
|
|
1639
1579
|
content: cleanString(body.content, "content", { required: true, max: MAX_BODY_BYTES })!,
|
|
1640
|
-
tags: cleanStringArray(body.tags, "tags"),
|
|
1641
|
-
visibility:
|
|
1642
|
-
sensitivity:
|
|
1643
|
-
confidence:
|
|
1644
|
-
redactionState:
|
|
1580
|
+
tags: cleanStringArray(body.tags, "tags", { itemMax: 80, maxItems: 50 }),
|
|
1581
|
+
visibility: optionalEnum(body.visibility, "visibility", VALID_MEMORY_VISIBILITIES) as MemoryVisibility | undefined,
|
|
1582
|
+
sensitivity: optionalEnum(body.sensitivity, "sensitivity", VALID_MEMORY_SENSITIVITIES) as MemorySensitivity | undefined,
|
|
1583
|
+
confidence: optionalEnum(body.confidence, "confidence", VALID_MEMORY_CONFIDENCES) as MemoryConfidence | undefined,
|
|
1584
|
+
redactionState: optionalEnum(body.redactionState, "redactionState", VALID_MEMORY_REDACTION_STATES) as MemoryRedactionState | undefined,
|
|
1645
1585
|
relevanceScore: cleanMemoryScore(body.relevanceScore, "relevanceScore"),
|
|
1646
1586
|
sourceAgent: cleanString(body.sourceAgent, "sourceAgent", { max: 200 }),
|
|
1647
1587
|
sourceTask: body.sourceTask === undefined ? undefined : cleanMemoryPositiveInteger(body.sourceTask, "sourceTask"),
|
|
@@ -1656,11 +1596,11 @@ function cleanUpdateMemoryInput(body: unknown): UpdateMemoryInput {
|
|
|
1656
1596
|
const patch: UpdateMemoryInput = {};
|
|
1657
1597
|
const title = cleanString(body.title, "title", { max: 240 });
|
|
1658
1598
|
const content = cleanString(body.content, "content", { max: MAX_BODY_BYTES });
|
|
1659
|
-
const tags = cleanStringArray(body.tags, "tags");
|
|
1660
|
-
const visibility =
|
|
1661
|
-
const sensitivity =
|
|
1662
|
-
const confidence =
|
|
1663
|
-
const redactionState =
|
|
1599
|
+
const tags = cleanStringArray(body.tags, "tags", { itemMax: 80, maxItems: 50 });
|
|
1600
|
+
const visibility = optionalEnum(body.visibility, "visibility", VALID_MEMORY_VISIBILITIES) as MemoryVisibility | undefined;
|
|
1601
|
+
const sensitivity = optionalEnum(body.sensitivity, "sensitivity", VALID_MEMORY_SENSITIVITIES) as MemorySensitivity | undefined;
|
|
1602
|
+
const confidence = optionalEnum(body.confidence, "confidence", VALID_MEMORY_CONFIDENCES) as MemoryConfidence | undefined;
|
|
1603
|
+
const redactionState = optionalEnum(body.redactionState, "redactionState", VALID_MEMORY_REDACTION_STATES) as MemoryRedactionState | undefined;
|
|
1664
1604
|
const relevanceScore = cleanMemoryScore(body.relevanceScore, "relevanceScore");
|
|
1665
1605
|
const metadata = cleanParams(body.metadata, "metadata");
|
|
1666
1606
|
if (title !== undefined) patch.title = title;
|
|
@@ -1689,7 +1629,7 @@ function cleanMemoryInjectInput(body: unknown): {
|
|
|
1689
1629
|
} {
|
|
1690
1630
|
if (!isRecord(body)) throw new ValidationError("memory injection body must be an object");
|
|
1691
1631
|
const agentId = cleanString(body.agentId ?? body.target, "agentId", { required: true, max: 200 })!;
|
|
1692
|
-
const memoryIds = cleanStringArray(body.memoryIds, "memoryIds") ?? [];
|
|
1632
|
+
const memoryIds = cleanStringArray(body.memoryIds, "memoryIds", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
1693
1633
|
const query = body.query === undefined ? undefined : cleanMemoryQueryFromBody(body.query);
|
|
1694
1634
|
if (memoryIds.length === 0 && !query) throw new ValidationError("memoryIds or query required");
|
|
1695
1635
|
return {
|
|
@@ -1711,8 +1651,8 @@ function cleanTaskRoutingHints(value: unknown): TaskRoutingHints | undefined {
|
|
|
1711
1651
|
title: cleanString(value.title, "task.title", { max: 240 }),
|
|
1712
1652
|
text: cleanString(value.text, "task.text", { max: 4000 }),
|
|
1713
1653
|
scope: cleanString(value.scope, "task.scope", { max: 240 }),
|
|
1714
|
-
tags: cleanStringArray(value.tags, "task.tags"),
|
|
1715
|
-
capabilities: cleanStringArray(value.capabilities, "task.capabilities"),
|
|
1654
|
+
tags: cleanStringArray(value.tags, "task.tags", { itemMax: 80, maxItems: 50 }),
|
|
1655
|
+
capabilities: cleanStringArray(value.capabilities, "task.capabilities", { itemMax: 80, maxItems: 50 }),
|
|
1716
1656
|
target: cleanString(value.target, "task.target", { max: 200 }),
|
|
1717
1657
|
};
|
|
1718
1658
|
}
|
|
@@ -2075,7 +2015,7 @@ const patchAgentTags: Handler = async (req, params) => {
|
|
|
2075
2015
|
const parsed = await parseBody<unknown>(req);
|
|
2076
2016
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2077
2017
|
try {
|
|
2078
|
-
const tags = isRecord(parsed.body) ? cleanStringArray(parsed.body.tags, "tags") : undefined;
|
|
2018
|
+
const tags = isRecord(parsed.body) ? cleanStringArray(parsed.body.tags, "tags", { itemMax: 80, maxItems: 50 }) : undefined;
|
|
2079
2019
|
if (!tags) return error("tags field required");
|
|
2080
2020
|
const before = getAgent(params.id!);
|
|
2081
2021
|
const agent = setTags(params.id!, tags);
|
|
@@ -2212,18 +2152,15 @@ function agentRuntimeProvider(agent: AgentCard): string | undefined {
|
|
|
2212
2152
|
|
|
2213
2153
|
function managedControlOrchestrator(agent: AgentCard): NonNullable<ReturnType<typeof getOrchestrator>> | null {
|
|
2214
2154
|
if (agent.meta?.runnerManaged !== true) return null;
|
|
2215
|
-
const
|
|
2216
|
-
const metaTmuxSession = typeof agent.meta.tmuxSession === "string" ? agent.meta.tmuxSession : "";
|
|
2217
|
-
const metaPolicyName = typeof agent.meta.policyName === "string" ? agent.meta.policyName : "";
|
|
2218
|
-
const metaSpawnRequestId = typeof agent.meta.spawnRequestId === "string" ? agent.meta.spawnRequestId : "";
|
|
2155
|
+
const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
|
|
2219
2156
|
const candidates = [
|
|
2220
|
-
...
|
|
2221
|
-
|
|
2222
|
-
(
|
|
2223
|
-
(
|
|
2224
|
-
(
|
|
2225
|
-
(
|
|
2226
|
-
)
|
|
2157
|
+
...listManagedOrchestratorsForAgent({
|
|
2158
|
+
agentId: agent.id,
|
|
2159
|
+
sessionName: str(agent.meta.sessionName),
|
|
2160
|
+
tmuxSession: str(agent.meta.tmuxSession),
|
|
2161
|
+
policyName: str(agent.meta.policyName),
|
|
2162
|
+
spawnRequestId: str(agent.meta.spawnRequestId),
|
|
2163
|
+
}),
|
|
2227
2164
|
...(agent.machine ? [getOrchestrator(agent.machine)].filter((orch): orch is NonNullable<ReturnType<typeof getOrchestrator>> => Boolean(orch)) : []),
|
|
2228
2165
|
];
|
|
2229
2166
|
return candidates.find((orch) => orch.status === "online") ?? null;
|
|
@@ -2346,7 +2283,7 @@ const postAgentAction: Handler = async (req, params) => {
|
|
|
2346
2283
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2347
2284
|
try {
|
|
2348
2285
|
if (!isRecord(parsed.body)) return error("action required");
|
|
2349
|
-
const action =
|
|
2286
|
+
const action = optionalEnum(parsed.body.action, "action", VALID_AGENT_ACTIONS);
|
|
2350
2287
|
if (!action) return error("action required");
|
|
2351
2288
|
const agent = getAgent(params.id!);
|
|
2352
2289
|
if (!agent) return error("agent not found", 404);
|
|
@@ -2462,7 +2399,7 @@ const postAgentPermissionDecision: Handler = async (req, params) => {
|
|
|
2462
2399
|
try {
|
|
2463
2400
|
if (!isRecord(parsed.body)) return error("permission decision required");
|
|
2464
2401
|
const approvalId = cleanString(parsed.body.approvalId, "approvalId", { required: true, max: 240 })!;
|
|
2465
|
-
const decision =
|
|
2402
|
+
const decision = optionalEnum(parsed.body.decision, "decision", ["approve", "approve-session", "deny", "abort", "answer"] as const);
|
|
2466
2403
|
if (!decision) return error("decision required");
|
|
2467
2404
|
const reason = cleanString(parsed.body.reason, "reason", { max: 500 });
|
|
2468
2405
|
// AskUserQuestion answers: { "<question text>": "<chosen label(s)>" }
|
|
@@ -2591,13 +2528,13 @@ const postAgentSpawn: Handler = async (req) => {
|
|
|
2591
2528
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2592
2529
|
try {
|
|
2593
2530
|
if (!isRecord(parsed.body)) return error("provider required");
|
|
2594
|
-
const provider =
|
|
2531
|
+
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS);
|
|
2595
2532
|
if (!provider) return error("provider required");
|
|
2596
2533
|
const selection = cleanSpawnSelection(parsed.body, provider as SpawnProvider);
|
|
2597
|
-
const approvalMode =
|
|
2534
|
+
const approvalMode = optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES, "guarded") as SpawnApprovalMode;
|
|
2598
2535
|
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
2599
2536
|
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
2600
|
-
const workspaceMode =
|
|
2537
|
+
const workspaceMode = optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES, "inherit") as WorkspaceMode;
|
|
2601
2538
|
|
|
2602
2539
|
const orchestrators = listOrchestrators().filter(
|
|
2603
2540
|
(o) => o.status === "online" && o.providers.includes(provider as SpawnProvider),
|
|
@@ -2794,7 +2731,7 @@ function cleanTokenProfileInput(body: unknown, partial = false) {
|
|
|
2794
2731
|
const name = cleanString(body.name, "name", { required: !partial, max: 120 });
|
|
2795
2732
|
const description = cleanString(body.description, "description", { max: 500 });
|
|
2796
2733
|
const role = cleanString(body.role, "role", { required: !partial, max: 80 });
|
|
2797
|
-
const scope = cleanStringArray(body.scope ?? body.scopes, "scope");
|
|
2734
|
+
const scope = cleanStringArray(body.scope ?? body.scopes, "scope", { itemMax: 80, maxItems: 50 });
|
|
2798
2735
|
if (!partial && !scope?.length) throw new ValidationError("scope must contain at least one scope");
|
|
2799
2736
|
const constraints = body.constraints === null ? undefined : cleanTokenConstraints(body.constraints);
|
|
2800
2737
|
const ttlSeconds = cleanTtlSeconds(body.ttlSeconds);
|
|
@@ -2818,7 +2755,7 @@ const postToken: Handler = async (req) => {
|
|
|
2818
2755
|
const role = cleanString(parsed.body.role, "role", { max: 80 }) ?? profile?.role;
|
|
2819
2756
|
if (!role) return error("role required");
|
|
2820
2757
|
const sub = cleanString(parsed.body.sub, "sub", { max: 160 }) ?? role;
|
|
2821
|
-
const scope = cleanStringArray(parsed.body.scope ?? parsed.body.scopes, "scope");
|
|
2758
|
+
const scope = cleanStringArray(parsed.body.scope ?? parsed.body.scopes, "scope", { itemMax: 80, maxItems: 50 });
|
|
2822
2759
|
const constraints = cleanTokenConstraints(parsed.body.constraints);
|
|
2823
2760
|
const createdBy = cleanString(parsed.body.createdBy, "createdBy", { max: 120 });
|
|
2824
2761
|
const ttlSeconds = cleanTtlSeconds(parsed.body.ttlSeconds);
|
|
@@ -2906,7 +2843,7 @@ const postInteractiveRunnerRuntimeToken: Handler = async (req) => {
|
|
|
2906
2843
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2907
2844
|
try {
|
|
2908
2845
|
if (!isRecord(parsed.body)) return error("runtime token body required");
|
|
2909
|
-
const provider =
|
|
2846
|
+
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS)! as SpawnProvider;
|
|
2910
2847
|
const cwd = cleanString(parsed.body.cwd, "cwd", { required: true, max: 500 })!;
|
|
2911
2848
|
const runnerId = cleanString(parsed.body.runnerId, "runnerId", { required: true, max: 240 })!;
|
|
2912
2849
|
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
@@ -2953,9 +2890,9 @@ const postMcpRuntimeToken: Handler = async (req) => {
|
|
|
2953
2890
|
if (!isRecord(parsed.body)) return error("runtime token body required");
|
|
2954
2891
|
const sessionId = cleanString(parsed.body.sessionId, "sessionId", { required: true, max: 240 })!;
|
|
2955
2892
|
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
2956
|
-
const targets = cleanStringArray(parsed.body.targets, "targets");
|
|
2957
|
-
const channels = cleanStringArray(parsed.body.channels, "channels");
|
|
2958
|
-
const memoryScopes = cleanStringArray(parsed.body.memoryScopes, "memoryScopes");
|
|
2893
|
+
const targets = cleanStringArray(parsed.body.targets, "targets", { itemMax: 80, maxItems: 50 });
|
|
2894
|
+
const channels = cleanStringArray(parsed.body.channels, "channels", { itemMax: 80, maxItems: 50 });
|
|
2895
|
+
const memoryScopes = cleanStringArray(parsed.body.memoryScopes, "memoryScopes", { itemMax: 80, maxItems: 50 });
|
|
2959
2896
|
const runtimeToken = issueMcpRuntimeToken({
|
|
2960
2897
|
sessionId,
|
|
2961
2898
|
agentId,
|
|
@@ -2994,8 +2931,8 @@ const postIntegrationRuntimeToken: Handler = async (req) => {
|
|
|
2994
2931
|
try {
|
|
2995
2932
|
if (!isRecord(parsed.body)) return error("runtime token body required");
|
|
2996
2933
|
const name = cleanString(parsed.body.name, "name", { required: true, max: 120 })!;
|
|
2997
|
-
const targets = cleanStringArray(parsed.body.targets, "targets");
|
|
2998
|
-
const channels = cleanStringArray(parsed.body.channels, "channels");
|
|
2934
|
+
const targets = cleanStringArray(parsed.body.targets, "targets", { itemMax: 80, maxItems: 50 });
|
|
2935
|
+
const channels = cleanStringArray(parsed.body.channels, "channels", { itemMax: 80, maxItems: 50 });
|
|
2999
2936
|
const runtimeToken = issueIntegrationRuntimeToken({
|
|
3000
2937
|
name,
|
|
3001
2938
|
targets,
|
|
@@ -3097,7 +3034,7 @@ function cleanJsonArray(value: unknown, field: string): unknown[] | undefined {
|
|
|
3097
3034
|
|
|
3098
3035
|
function cleanSpawnSelection(body: Record<string, unknown>, provider: SpawnProvider): SpawnModelParams {
|
|
3099
3036
|
const model = cleanString(body.model, "model", { max: 120 });
|
|
3100
|
-
const effort =
|
|
3037
|
+
const effort = optionalEnum(body.effort, "effort", VALID_EFFORTS) as ProviderEffort | undefined;
|
|
3101
3038
|
return resolveSpawnModelParams(provider, model, effort);
|
|
3102
3039
|
}
|
|
3103
3040
|
|
|
@@ -3162,7 +3099,7 @@ const postOrchestrator: Handler = async (req) => {
|
|
|
3162
3099
|
const id = cleanString(parsed.body.id, "id", { required: true, max: 120 })!;
|
|
3163
3100
|
const hostname = cleanString(parsed.body.hostname, "hostname", { required: true, max: 120 })!;
|
|
3164
3101
|
const baseDir = cleanString(parsed.body.baseDir, "baseDir", { required: true, max: 500 })!;
|
|
3165
|
-
const providers = cleanStringArray(parsed.body.providers, "providers") as SpawnProvider[] | undefined;
|
|
3102
|
+
const providers = cleanStringArray(parsed.body.providers, "providers", { itemMax: 80, maxItems: 50 }) as SpawnProvider[] | undefined;
|
|
3166
3103
|
if (providers) {
|
|
3167
3104
|
for (const p of providers) {
|
|
3168
3105
|
if (!SPAWN_PROVIDERS.includes(p as any)) {
|
|
@@ -3170,7 +3107,7 @@ const postOrchestrator: Handler = async (req) => {
|
|
|
3170
3107
|
}
|
|
3171
3108
|
}
|
|
3172
3109
|
}
|
|
3173
|
-
const envKeys = cleanStringArray(parsed.body.envKeys, "envKeys");
|
|
3110
|
+
const envKeys = cleanStringArray(parsed.body.envKeys, "envKeys", { itemMax: 80, maxItems: 50 });
|
|
3174
3111
|
const apiUrl = cleanString(parsed.body.apiUrl, "apiUrl", { max: 500 });
|
|
3175
3112
|
const packageMetadata = cleanRuntimePackageMetadata(parsed.body.package);
|
|
3176
3113
|
const contracts = cleanRuntimeContracts(parsed.body.contracts);
|
|
@@ -3243,7 +3180,7 @@ const postOrchestratorHeartbeat: Handler = async (req, params) => {
|
|
|
3243
3180
|
version: cleanString(body.version, "version", { max: 80 }),
|
|
3244
3181
|
protocolVersion: cleanProtocolVersion(body.protocolVersion),
|
|
3245
3182
|
gitSha: cleanString(body.gitSha, "gitSha", { max: 80 }),
|
|
3246
|
-
providers: cleanStringArray(body.providers, "providers") as SpawnProvider[] | undefined,
|
|
3183
|
+
providers: cleanStringArray(body.providers, "providers", { itemMax: 80, maxItems: 50 }) as SpawnProvider[] | undefined,
|
|
3247
3184
|
providerStatus: cleanJsonArray(body.providerStatus, "providerStatus") as OrchestratorRuntimeInput["providerStatus"],
|
|
3248
3185
|
providerCatalog: cleanJsonArray(body.providerCatalog, "providerCatalog") as OrchestratorRuntimeInput["providerCatalog"],
|
|
3249
3186
|
};
|
|
@@ -3269,12 +3206,12 @@ const postOrchestratorBootstrap: Handler = async (req) => {
|
|
|
3269
3206
|
if (!isRecord(parsed.body)) return error("body required");
|
|
3270
3207
|
const id = cleanString(parsed.body.id, "id", { required: true, max: 120 })!;
|
|
3271
3208
|
const baseDir = cleanString(parsed.body.baseDir, "baseDir", { required: true, max: 500 })!;
|
|
3272
|
-
const providers = cleanStringArray(parsed.body.providers, "providers") as SpawnProvider[] | undefined;
|
|
3209
|
+
const providers = cleanStringArray(parsed.body.providers, "providers", { itemMax: 80, maxItems: 50 }) as SpawnProvider[] | undefined;
|
|
3273
3210
|
const apiPort = cleanSafeNumber(parsed.body.apiPort) ?? 4860;
|
|
3274
3211
|
if (!Number.isInteger(apiPort) || apiPort < 1 || apiPort > 65535) throw new ValidationError("apiPort must be an integer from 1 to 65535");
|
|
3275
3212
|
const relayUrl = cleanString(parsed.body.relayUrl, "relayUrl", { max: 500 }) ?? new URL(req.url).origin;
|
|
3276
3213
|
const version = cleanString(parsed.body.version, "version", { max: 80 }) ?? VERSION;
|
|
3277
|
-
const pathPrefix = cleanStringArray(parsed.body.pathPrefix, "pathPrefix");
|
|
3214
|
+
const pathPrefix = cleanStringArray(parsed.body.pathPrefix, "pathPrefix", { itemMax: 80, maxItems: 50 });
|
|
3278
3215
|
const providerList = providers?.length ? providers : ["claude", "codex"];
|
|
3279
3216
|
for (const p of providerList) {
|
|
3280
3217
|
if (!SPAWN_PROVIDERS.includes(p as any)) {
|
|
@@ -3369,19 +3306,19 @@ const patchOrchestratorAgents: Handler = async (req, params) => {
|
|
|
3369
3306
|
?? cleanString(a.tmuxSession, "tmuxSession", { required: true, max: 240 })!;
|
|
3370
3307
|
return {
|
|
3371
3308
|
agentId: cleanString(a.agentId, "agentId", { max: 240 }) || "",
|
|
3372
|
-
provider:
|
|
3309
|
+
provider: optionalEnum(a.provider, "provider", SPAWN_PROVIDERS)! as SpawnProvider,
|
|
3373
3310
|
sessionName,
|
|
3374
3311
|
tmuxSession: cleanString(a.tmuxSession, "tmuxSession", { max: 240 }) ?? sessionName,
|
|
3375
|
-
supervisor:
|
|
3312
|
+
supervisor: optionalEnum(a.supervisor, "supervisor", ["process", "systemd", "launchd", "unknown"] as const),
|
|
3376
3313
|
systemdUnit: cleanString(a.systemdUnit, "systemdUnit", { max: 240 }),
|
|
3377
3314
|
terminalSession: cleanString(a.terminalSession, "terminalSession", { max: 240 }),
|
|
3378
3315
|
terminalAvailable: typeof a.terminalAvailable === "boolean" ? a.terminalAvailable : undefined,
|
|
3379
3316
|
cwd: cleanString(a.cwd, "cwd", { required: true, max: 500 })!,
|
|
3380
3317
|
profile: cleanString(a.profile, "profile", { max: 120 }),
|
|
3381
|
-
workspaceMode:
|
|
3318
|
+
workspaceMode: optionalEnum(a.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES),
|
|
3382
3319
|
workspace: cleanWorkspaceMetadata(a.workspace, "workspace"),
|
|
3383
3320
|
label: cleanString(a.label, "label", { max: 120 }),
|
|
3384
|
-
approvalMode: (
|
|
3321
|
+
approvalMode: (optionalEnum(a.approvalMode, "approvalMode", APPROVAL_MODES, "guarded") ?? "guarded") as SpawnApprovalMode,
|
|
3385
3322
|
policyName: cleanString(a.policyName, "policyName", { max: 120 }),
|
|
3386
3323
|
spawnRequestId: cleanString(a.spawnRequestId, "spawnRequestId", { max: 160 }),
|
|
3387
3324
|
automationRunId: cleanString(a.automationRunId, "automationRunId", { max: 160 }),
|
|
@@ -3446,7 +3383,7 @@ const patchOrchestratorAgents: Handler = async (req, params) => {
|
|
|
3446
3383
|
|
|
3447
3384
|
function cleanManagedSessionExitDiagnostics(value: unknown, index: number): ManagedSessionExitDiagnostics {
|
|
3448
3385
|
if (!isRecord(value)) throw new ValidationError(`exitedAgents[${index}] must be an object`);
|
|
3449
|
-
const provider =
|
|
3386
|
+
const provider = optionalEnum(value.provider, `exitedAgents[${index}].provider`, SPAWN_PROVIDERS);
|
|
3450
3387
|
if (!provider) throw new ValidationError(`exitedAgents[${index}].provider required`);
|
|
3451
3388
|
const systemd = isRecord(value.systemd)
|
|
3452
3389
|
? {
|
|
@@ -3475,7 +3412,7 @@ function cleanManagedSessionExitDiagnostics(value: unknown, index: number): Mana
|
|
|
3475
3412
|
const diagnostics: ManagedSessionExitDiagnostics = {
|
|
3476
3413
|
agentId: cleanString(value.agentId, `exitedAgents[${index}].agentId`, { required: true, max: 240 })!,
|
|
3477
3414
|
provider: provider as SpawnProvider,
|
|
3478
|
-
workspaceMode:
|
|
3415
|
+
workspaceMode: optionalEnum(value.workspaceMode, `exitedAgents[${index}].workspaceMode`, VALID_WORKSPACE_MODES),
|
|
3479
3416
|
workspace: cleanWorkspaceMetadata(value.workspace, `exitedAgents[${index}].workspace`),
|
|
3480
3417
|
sessionName: cleanString(value.sessionName, `exitedAgents[${index}].sessionName`, { max: 240 }),
|
|
3481
3418
|
tmuxSession: cleanString(value.tmuxSession, `exitedAgents[${index}].tmuxSession`, { required: true, max: 240 })!,
|
|
@@ -3484,7 +3421,7 @@ function cleanManagedSessionExitDiagnostics(value: unknown, index: number): Mana
|
|
|
3484
3421
|
policyName: cleanString(value.policyName, `exitedAgents[${index}].policyName`, { max: 120 }),
|
|
3485
3422
|
spawnRequestId: cleanString(value.spawnRequestId, `exitedAgents[${index}].spawnRequestId`, { max: 160 }),
|
|
3486
3423
|
automationRunId: cleanString(value.automationRunId, `exitedAgents[${index}].automationRunId`, { max: 160 }),
|
|
3487
|
-
supervisor:
|
|
3424
|
+
supervisor: optionalEnum(value.supervisor, `exitedAgents[${index}].supervisor`, ["process", "systemd", "launchd", "unknown"] as const, "unknown")!,
|
|
3488
3425
|
systemdUnit: cleanString(value.systemdUnit, `exitedAgents[${index}].systemdUnit`, { max: 240 }),
|
|
3489
3426
|
terminalSession: cleanString(value.terminalSession, `exitedAgents[${index}].terminalSession`, { max: 240 }),
|
|
3490
3427
|
terminalAvailable: typeof value.terminalAvailable === "boolean" ? value.terminalAvailable : undefined,
|
|
@@ -3520,7 +3457,7 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
3520
3457
|
if (orch.status !== "online") return error("orchestrator is offline", 409);
|
|
3521
3458
|
|
|
3522
3459
|
if (!isRecord(parsed.body)) return error("body required");
|
|
3523
|
-
const provider =
|
|
3460
|
+
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS)! as SpawnProvider;
|
|
3524
3461
|
const selection = validateSpawnSelectionForOrchestrator(orch, provider, parsed.body);
|
|
3525
3462
|
if (selection instanceof Response) return selection;
|
|
3526
3463
|
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
@@ -3528,14 +3465,14 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
3528
3465
|
return error(`cwd must be within orchestrator base directory: ${orch.baseDir}`);
|
|
3529
3466
|
}
|
|
3530
3467
|
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
3531
|
-
const approvalMode =
|
|
3468
|
+
const approvalMode = optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES, "guarded") as SpawnApprovalMode;
|
|
3532
3469
|
const prompt = cleanString(parsed.body.prompt, "prompt", { max: 16000 });
|
|
3533
3470
|
const systemPromptAppend = cleanString(parsed.body.systemPromptAppend, "systemPromptAppend", { max: 64_000 });
|
|
3534
|
-
const tags = cleanStringArray(parsed.body.tags, "tags") ?? [];
|
|
3535
|
-
const capabilities = cleanStringArray(parsed.body.capabilities, "capabilities") ?? [];
|
|
3536
|
-
const providerArgs = cleanStringArray(parsed.body.providerArgs, "providerArgs") ?? [];
|
|
3471
|
+
const tags = cleanStringArray(parsed.body.tags, "tags", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
3472
|
+
const capabilities = cleanStringArray(parsed.body.capabilities, "capabilities", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
3473
|
+
const providerArgs = cleanStringArray(parsed.body.providerArgs, "providerArgs", { itemMax: 80, maxItems: 50 }) ?? [];
|
|
3537
3474
|
const profile = cleanString(parsed.body.profile, "profile", { max: 120 });
|
|
3538
|
-
const workspaceMode =
|
|
3475
|
+
const workspaceMode = optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES, "inherit") as WorkspaceMode;
|
|
3539
3476
|
const agentProfile = profile ? getAgentProfile(profile)?.value : undefined;
|
|
3540
3477
|
if (profile && !agentProfile) return error("agent profile not found", 404);
|
|
3541
3478
|
const policyName = cleanString(parsed.body.policyName, "policyName", { max: 120 });
|
|
@@ -3709,7 +3646,7 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
3709
3646
|
if (!orch) return error("orchestrator not found", 404);
|
|
3710
3647
|
|
|
3711
3648
|
if (!isRecord(parsed.body)) return error("body required");
|
|
3712
|
-
const action =
|
|
3649
|
+
const action = optionalEnum(parsed.body.action, "action", ["restart", "shutdown", "upgrade"] as const);
|
|
3713
3650
|
if (!action) return error("action required");
|
|
3714
3651
|
|
|
3715
3652
|
if (action === "upgrade") {
|
|
@@ -3718,7 +3655,7 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
3718
3655
|
const targetVersion = cleanString(parsed.body.targetVersion, "targetVersion", { max: 80 }) ?? VERSION;
|
|
3719
3656
|
if (!ORCH_UPGRADE_SEMVER_RE.test(targetVersion)) return error("targetVersion must be a semver like 0.10.20");
|
|
3720
3657
|
const force = parsed.body.force === true;
|
|
3721
|
-
const providers = (cleanStringArray(parsed.body.providers, "providers") ?? ["orchestrator"])
|
|
3658
|
+
const providers = (cleanStringArray(parsed.body.providers, "providers", { itemMax: 80, maxItems: 50 }) ?? ["orchestrator"])
|
|
3722
3659
|
.filter((p) => (VALID_ORCH_UPGRADE_PROVIDERS as readonly string[]).includes(p));
|
|
3723
3660
|
if (!providers.length) return error(`providers must be any of: ${VALID_ORCH_UPGRADE_PROVIDERS.join(", ")}`);
|
|
3724
3661
|
const selfUpgradeError = orchestratorSelfUpgradeError(orch);
|
|
@@ -3848,7 +3785,7 @@ const getWorkspaces: Handler = (req) => {
|
|
|
3848
3785
|
const url = new URL(req.url);
|
|
3849
3786
|
const repoRoot = cleanString(url.searchParams.get("repoRoot") ?? undefined, "repoRoot", { max: 1000 });
|
|
3850
3787
|
const ownerAgentId = cleanString(url.searchParams.get("agentId") ?? undefined, "agentId", { max: 240 });
|
|
3851
|
-
const status =
|
|
3788
|
+
const status = optionalEnum(url.searchParams.get("status") ?? undefined, "status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
|
|
3852
3789
|
return json(listWorkspaces({ repoRoot, ownerAgentId, status }));
|
|
3853
3790
|
} catch (e) {
|
|
3854
3791
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
@@ -4023,6 +3960,32 @@ function buildWorkspaceCleanupCommand(workspace: WorkspaceRecord, requestedBy: s
|
|
|
4023
3960
|
return { ok: true, command };
|
|
4024
3961
|
}
|
|
4025
3962
|
|
|
3963
|
+
// Build a `workspace.deps-refresh` command for a worktree's owning orchestrator
|
|
3964
|
+
// (issue #51). Re-provisions deps the shared symlinked node_modules has gone stale
|
|
3965
|
+
// on, or — checkOnly — just reports staleness. Same owner-resolution as cleanup.
|
|
3966
|
+
function buildWorkspaceDepsRefreshCommand(workspace: WorkspaceRecord, requestedBy: string, checkOnly: boolean): { ok: true; command: Command } | { ok: false; status: number; error: string } {
|
|
3967
|
+
const owners = listOrchestrators().filter((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
|
|
3968
|
+
const owner = owners.find((candidate) => candidate.status === "online") ?? owners[0];
|
|
3969
|
+
if (!owner) return { ok: false, status: 409, error: "no online orchestrator owns this workspace path" };
|
|
3970
|
+
const command = createCommand({
|
|
3971
|
+
type: "workspace.deps-refresh",
|
|
3972
|
+
source: "system",
|
|
3973
|
+
target: owner.agentId,
|
|
3974
|
+
correlationId: workspace.id,
|
|
3975
|
+
params: {
|
|
3976
|
+
action: "deps-refresh",
|
|
3977
|
+
workspaceId: workspace.id,
|
|
3978
|
+
repoRoot: workspace.repoRoot,
|
|
3979
|
+
worktreePath: workspace.worktreePath,
|
|
3980
|
+
checkOnly,
|
|
3981
|
+
requestedBy,
|
|
3982
|
+
requestedAt: Date.now(),
|
|
3983
|
+
queued: owner.status !== "online",
|
|
3984
|
+
},
|
|
3985
|
+
});
|
|
3986
|
+
return { ok: true, command };
|
|
3987
|
+
}
|
|
3988
|
+
|
|
4026
3989
|
// Fetch + parse a workspace's live git state from its owning host, or report why
|
|
4027
3990
|
// it's unavailable. Thin typed wrapper over the same host route the proxy uses.
|
|
4028
3991
|
async function fetchWorkspaceGitState(workspace: WorkspaceRecord): Promise<{ state: WorkspaceGitState } | { unavailable: string }> {
|
|
@@ -4161,7 +4124,7 @@ const postWorkspaceAction: Handler = async (req, params) => {
|
|
|
4161
4124
|
if (!isRecord(parsed.body)) return error("body required");
|
|
4162
4125
|
const workspace = getWorkspace(params.id!);
|
|
4163
4126
|
if (!workspace) return error("workspace not found", 404);
|
|
4164
|
-
const action =
|
|
4127
|
+
const action = optionalEnum(parsed.body.action, "action", ["status", "ready", "conflict-found", "request-review", "merge-plan", "merge", "abandon", "cleanup", "claim", "release-claim", "deps-refresh"] as const);
|
|
4165
4128
|
if (!action) return error("action required", 400);
|
|
4166
4129
|
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
4167
4130
|
const detail = cleanString(parsed.body.detail, "detail", { max: 4000 });
|
|
@@ -4198,7 +4161,7 @@ const postWorkspaceAction: Handler = async (req, params) => {
|
|
|
4198
4161
|
// Base merges go through the shared helper (lease + command + bind), the same
|
|
4199
4162
|
// path the auto-merge job uses, so both serialize per repo (issue #157).
|
|
4200
4163
|
if (action === "merge") {
|
|
4201
|
-
const strategy =
|
|
4164
|
+
const strategy = optionalEnum(parsed.body.strategy, "strategy", ["pr", "rebase-ff", "auto"] as const, "auto") as WorkspaceMergeStrategy;
|
|
4202
4165
|
const result = requestWorkspaceMerge(workspace, {
|
|
4203
4166
|
requestedBy: agentId ?? "dashboard",
|
|
4204
4167
|
strategy,
|
|
@@ -4222,6 +4185,31 @@ const postWorkspaceAction: Handler = async (req, params) => {
|
|
|
4222
4185
|
});
|
|
4223
4186
|
return json({ workspace: result.workspace, command: result.command }, 202);
|
|
4224
4187
|
}
|
|
4188
|
+
// Deps refresh (#51): self-service for the owning agent — re-provision deps the
|
|
4189
|
+
// shared symlinked node_modules has gone stale on. Emits a host command but is
|
|
4190
|
+
// authorized at agent:write (it only touches the agent's own worktree), so the
|
|
4191
|
+
// agent can run it themselves the moment typecheck reports a missing module.
|
|
4192
|
+
if (action === "deps-refresh") {
|
|
4193
|
+
if (workspace.mode !== "isolated" || !workspace.worktreePath) {
|
|
4194
|
+
return error(`workspace ${workspace.id} has no isolated worktree to refresh`, 422);
|
|
4195
|
+
}
|
|
4196
|
+
const checkOnly = parsed.body.checkOnly === true;
|
|
4197
|
+
const built = buildWorkspaceDepsRefreshCommand(workspace, agentId ?? "agent", checkOnly);
|
|
4198
|
+
if (!built.ok) return error(built.error, built.status);
|
|
4199
|
+
emitCommand(built.command);
|
|
4200
|
+
auditEvent({
|
|
4201
|
+
clientId: `workspace-deps-refresh-${workspace.id}-${Date.now()}`,
|
|
4202
|
+
kind: "state",
|
|
4203
|
+
title: checkOnly ? "Workspace deps check" : "Workspace deps refresh",
|
|
4204
|
+
body: detail ?? workspace.worktreePath,
|
|
4205
|
+
meta: workspace.branch ?? workspace.id,
|
|
4206
|
+
icon: "ti-package",
|
|
4207
|
+
view: "orchestrators",
|
|
4208
|
+
agentId,
|
|
4209
|
+
metadata: { action, workspaceId: workspace.id, repoRoot: workspace.repoRoot, worktreePath: workspace.worktreePath, checkOnly, commandId: built.command.id, ...authAuditMetadata(req) },
|
|
4210
|
+
});
|
|
4211
|
+
return json({ workspace, command: built.command }, 202);
|
|
4212
|
+
}
|
|
4225
4213
|
const statusByAction: Record<string, WorkspaceStatus | undefined> = {
|
|
4226
4214
|
status: undefined,
|
|
4227
4215
|
ready: "ready",
|
|
@@ -4392,7 +4380,7 @@ const getOrchestratorProviders: Handler = async (req, params) => {
|
|
|
4392
4380
|
const payload = await res.clone().json().catch(() => null) as unknown;
|
|
4393
4381
|
if (!isRecord(payload)) return res;
|
|
4394
4382
|
try {
|
|
4395
|
-
const providers = cleanStringArray(payload.providers, "providers") as SpawnProvider[] | undefined;
|
|
4383
|
+
const providers = cleanStringArray(payload.providers, "providers", { itemMax: 80, maxItems: 50 }) as SpawnProvider[] | undefined;
|
|
4396
4384
|
if (providers) {
|
|
4397
4385
|
for (const p of providers) {
|
|
4398
4386
|
if (!SPAWN_PROVIDERS.includes(p as any)) {
|
|
@@ -4536,7 +4524,7 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4536
4524
|
if (!current) return error("command not found", 404);
|
|
4537
4525
|
const denied = authorizeRoute(req, { scope: "command:write", resource: commandAuthorizationResource(current) });
|
|
4538
4526
|
if (denied) return denied;
|
|
4539
|
-
const status =
|
|
4527
|
+
const status = optionalEnum(parsed.body.status, "status", VALID_COMMAND_STATUSES);
|
|
4540
4528
|
const result = cleanParams(parsed.body.result, "result");
|
|
4541
4529
|
const err = cleanString(parsed.body.error, "error", { max: 4000 });
|
|
4542
4530
|
const command = updateCommand(params.id!, { status: status as CommandStatus | undefined, result, error: err });
|
|
@@ -4574,12 +4562,17 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4574
4562
|
}
|
|
4575
4563
|
if (command.status === "succeeded" && isRecord(command.result)) {
|
|
4576
4564
|
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
4577
|
-
const resultStatus =
|
|
4565
|
+
const resultStatus = optionalEnum(command.result.status, "result.status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
|
|
4578
4566
|
if (workspaceId && resultStatus) {
|
|
4579
4567
|
updateWorkspaceStatus(workspaceId, resultStatus, {
|
|
4580
4568
|
mergeResult: command.result,
|
|
4581
4569
|
mergeCommandId: command.id,
|
|
4582
4570
|
mergedAt: Date.now(),
|
|
4571
|
+
// The land consumes the steward's claim (#208 steward contract / vent
|
|
4572
|
+
// #62): a successful land must release it, or `steward inspect` keeps
|
|
4573
|
+
// showing a stale claim on the recycled workspace and blocks the next
|
|
4574
|
+
// automation. Clearing a null claim (auto-merge path) is a harmless no-op.
|
|
4575
|
+
...claimMetadataPatch(true),
|
|
4583
4576
|
});
|
|
4584
4577
|
// Land-and-continue (#206): the worktree was recycled onto a fresh branch.
|
|
4585
4578
|
// Repoint the row so the next merge targets the live branch, not the deleted one.
|
|
@@ -4602,7 +4595,7 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4602
4595
|
}
|
|
4603
4596
|
if (command.type === "workspace.reconcile" && command.status === "succeeded" && isRecord(command.result)) {
|
|
4604
4597
|
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
4605
|
-
const resultStatus =
|
|
4598
|
+
const resultStatus = optionalEnum(command.result.status, "result.status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
|
|
4606
4599
|
const removed = command.result.removed === true;
|
|
4607
4600
|
if (workspaceId && resultStatus) {
|
|
4608
4601
|
// Only act on workspaces the agent left in a live state; never overwrite
|
|
@@ -4635,6 +4628,14 @@ const patchCommand: Handler = async (req, params) => {
|
|
|
4635
4628
|
}
|
|
4636
4629
|
}
|
|
4637
4630
|
}
|
|
4631
|
+
if (command.type === "workspace.deps-refresh" && command.status === "succeeded" && isRecord(command.result)) {
|
|
4632
|
+
// Record the outcome on the row without touching status (#51) — observability
|
|
4633
|
+
// for the dashboard; the CLI reads the result straight off the command.
|
|
4634
|
+
const workspaceId = cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 });
|
|
4635
|
+
if (workspaceId) {
|
|
4636
|
+
patchWorkspaceMetadata(workspaceId, { lastDepsRefresh: command.result, lastDepsRefreshCommandId: command.id, lastDepsRefreshAt: Date.now() });
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4638
4639
|
settleFailedOrchestratorUpgrade(command);
|
|
4639
4640
|
emitCommand(command);
|
|
4640
4641
|
auditCommandOutcome(command);
|
|
@@ -5101,13 +5102,13 @@ const getProviderConfigsRoute: Handler = () => {
|
|
|
5101
5102
|
};
|
|
5102
5103
|
|
|
5103
5104
|
const getProviderConfigRoute: Handler = (_req, params) => {
|
|
5104
|
-
const provider =
|
|
5105
|
+
const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
|
|
5105
5106
|
if (!provider) return error("provider required");
|
|
5106
5107
|
return json(providerConfigPublic(loadProviderConfig(provider)));
|
|
5107
5108
|
};
|
|
5108
5109
|
|
|
5109
5110
|
const putProviderConfigRoute: Handler = async (req, params) => {
|
|
5110
|
-
const provider =
|
|
5111
|
+
const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
|
|
5111
5112
|
if (!provider) return error("provider required");
|
|
5112
5113
|
const parsed = await parseBody<unknown>(req);
|
|
5113
5114
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -5117,13 +5118,13 @@ const putProviderConfigRoute: Handler = async (req, params) => {
|
|
|
5117
5118
|
const headless = isRecord(parsed.body.headless) ? parsed.body.headless : {};
|
|
5118
5119
|
const config: ProviderConfig = {
|
|
5119
5120
|
command: cleanString(parsed.body.command, "command", { required: true, max: 500 })!,
|
|
5120
|
-
defaultArgs: cleanStringArray(parsed.body.defaultArgs, "defaultArgs") ?? defaults.defaultArgs,
|
|
5121
|
+
defaultArgs: cleanStringArray(parsed.body.defaultArgs, "defaultArgs", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultArgs,
|
|
5121
5122
|
env: cleanEnvRecord(parsed.body.env),
|
|
5122
|
-
pluginDirs: cleanStringArray(parsed.body.pluginDirs, "pluginDirs") ?? defaults.pluginDirs,
|
|
5123
|
-
defaultCapabilities: cleanStringArray(parsed.body.defaultCapabilities, "defaultCapabilities") ?? defaults.defaultCapabilities,
|
|
5123
|
+
pluginDirs: cleanStringArray(parsed.body.pluginDirs, "pluginDirs", { itemMax: 80, maxItems: 50 }) ?? defaults.pluginDirs,
|
|
5124
|
+
defaultCapabilities: cleanStringArray(parsed.body.defaultCapabilities, "defaultCapabilities", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultCapabilities,
|
|
5124
5125
|
defaultApprovalMode: cleanString(parsed.body.defaultApprovalMode, "defaultApprovalMode", { max: 80 }) ?? defaults.defaultApprovalMode,
|
|
5125
|
-
defaultTags: cleanStringArray(parsed.body.defaultTags, "defaultTags") ?? defaults.defaultTags,
|
|
5126
|
-
chatCaptureMode: (
|
|
5126
|
+
defaultTags: cleanStringArray(parsed.body.defaultTags, "defaultTags", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultTags,
|
|
5127
|
+
chatCaptureMode: (optionalEnum(parsed.body.chatCaptureMode, "chatCaptureMode", ["final", "full"]) ?? defaults.chatCaptureMode) as "final" | "full",
|
|
5127
5128
|
headless: {
|
|
5128
5129
|
tmuxPrefix: cleanString(headless.tmuxPrefix, "headless.tmuxPrefix", { max: 120 }) ?? defaults.headless.tmuxPrefix,
|
|
5129
5130
|
shutdownTimeoutMs: cleanPositiveInt(headless.shutdownTimeoutMs, "headless.shutdownTimeoutMs") ?? defaults.headless.shutdownTimeoutMs,
|
|
@@ -5137,7 +5138,7 @@ const putProviderConfigRoute: Handler = async (req, params) => {
|
|
|
5137
5138
|
};
|
|
5138
5139
|
|
|
5139
5140
|
const postProviderConfigTestRoute: Handler = async (_req, params) => {
|
|
5140
|
-
const provider =
|
|
5141
|
+
const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
|
|
5141
5142
|
if (!provider) return error("provider required");
|
|
5142
5143
|
const config = loadProviderConfig(provider);
|
|
5143
5144
|
const proc = Bun.spawn(["bash", "-lc", `command -v "$1"`, "bash", config.command], {
|
|
@@ -5212,7 +5213,7 @@ const postMessageDeliveryAttempt: Handler = async (req, params) => {
|
|
|
5212
5213
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
5213
5214
|
try {
|
|
5214
5215
|
if (!isRecord(parsed.body)) throw new ValidationError("body must be an object");
|
|
5215
|
-
const status =
|
|
5216
|
+
const status = optionalEnum(parsed.body.status, "status", VALID_DELIVERY_STATUSES);
|
|
5216
5217
|
if (!status) throw new ValidationError("status required");
|
|
5217
5218
|
const nextRetryAt = cleanPositiveId(parsed.body.nextRetryAt, "nextRetryAt");
|
|
5218
5219
|
const result = recordMessageDeliveryAttempt(id, {
|
|
@@ -5238,7 +5239,7 @@ const postMessageDeliveryAction: Handler = async (req, params) => {
|
|
|
5238
5239
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
5239
5240
|
try {
|
|
5240
5241
|
if (!isRecord(parsed.body)) throw new ValidationError("body must be an object");
|
|
5241
|
-
const action =
|
|
5242
|
+
const action = optionalEnum(parsed.body.action, "action", VALID_DELIVERY_ACTIONS);
|
|
5242
5243
|
if (!action) throw new ValidationError("action required");
|
|
5243
5244
|
const result = applyMessageDeliveryAction(id, {
|
|
5244
5245
|
action,
|
|
@@ -5287,7 +5288,7 @@ const postMessage: Handler = async (req) => {
|
|
|
5287
5288
|
const memoryInjection = await injectMemoryForMessageDelivery(result.message, autoMemoryTarget, memoryContext(req));
|
|
5288
5289
|
if (memoryInjection) emitCommand(memoryInjection.command);
|
|
5289
5290
|
} catch (e) {
|
|
5290
|
-
console.warn(`[memory] automatic message context assembly failed: ${
|
|
5291
|
+
console.warn(`[memory] automatic message context assembly failed: ${errMessage(e)}`);
|
|
5291
5292
|
}
|
|
5292
5293
|
}
|
|
5293
5294
|
if (result.message.deliveryStatus === "queued") {
|
|
@@ -5791,7 +5792,7 @@ const postConnectorAction: Handler = async (req, params) => {
|
|
|
5791
5792
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
5792
5793
|
try {
|
|
5793
5794
|
if (!isRecord(parsed.body)) throw new ValidationError("JSON object body required");
|
|
5794
|
-
const action =
|
|
5795
|
+
const action = optionalEnum(parsed.body.action, "action", VALID_CONNECTOR_ACTIONS)!;
|
|
5795
5796
|
const result = await runConnectorAction(params.id!, action);
|
|
5796
5797
|
return json(result, result.ok ? 200 : 502);
|
|
5797
5798
|
} catch (e) {
|
|
@@ -6271,7 +6272,7 @@ const postClaimTask: Handler = async (req, params) => {
|
|
|
6271
6272
|
const memoryInjection = await injectMemoryForTaskClaim(result.task!, agentId);
|
|
6272
6273
|
if (memoryInjection) emitCommand(memoryInjection.command);
|
|
6273
6274
|
} catch (e) {
|
|
6274
|
-
console.warn(`[memory] automatic task context assembly failed: ${
|
|
6275
|
+
console.warn(`[memory] automatic task context assembly failed: ${errMessage(e)}`);
|
|
6275
6276
|
}
|
|
6276
6277
|
void dispatchTaskCallbacks(id, "task.claimed");
|
|
6277
6278
|
return json(result.task);
|