agent-relay-server 0.32.2 → 0.32.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +2 -2
  2. package/public/assets/display-JI19Vc7L.js.map +1 -1
  3. package/src/branch-landed.ts +38 -2
  4. package/src/cli.ts +3 -3
  5. package/src/maintenance.ts +21 -21
  6. package/src/mcp.ts +2 -2
  7. package/src/ratchet-files.ts +37 -0
  8. package/src/routes/_shared.ts +376 -0
  9. package/src/routes/activity.ts +61 -0
  10. package/src/routes/agent-profiles.ts +47 -0
  11. package/src/routes/agent-sessions.ts +488 -0
  12. package/src/routes/agents-spawn.ts +274 -0
  13. package/src/routes/agents.ts +251 -0
  14. package/src/routes/artifacts.ts +226 -0
  15. package/src/routes/automations.ts +83 -0
  16. package/src/routes/commands.ts +317 -0
  17. package/src/routes/config.ts +66 -0
  18. package/src/routes/connectors.ts +108 -0
  19. package/src/routes/inbox.ts +142 -0
  20. package/src/routes/index.ts +293 -0
  21. package/src/routes/insights.ts +81 -0
  22. package/src/routes/integrations.ts +592 -0
  23. package/src/routes/memory.ts +337 -0
  24. package/src/routes/messages.ts +529 -0
  25. package/src/routes/orchestrator-bootstrap.ts +100 -0
  26. package/src/routes/orchestrator-proxy.ts +160 -0
  27. package/src/routes/orchestrator.ts +490 -0
  28. package/src/routes/pairs.ts +197 -0
  29. package/src/routes/provider-config.ts +112 -0
  30. package/src/routes/recipes.ts +113 -0
  31. package/src/routes/spawn-policy.ts +231 -0
  32. package/src/routes/spec.ts +54 -0
  33. package/src/routes/sse.ts +9 -0
  34. package/src/routes/stats.ts +32 -0
  35. package/src/routes/steward.ts +45 -0
  36. package/src/routes/tasks.ts +174 -0
  37. package/src/routes/tokens.ts +311 -0
  38. package/src/routes/workspaces.ts +364 -0
  39. package/src/routes.ts +3 -6822
  40. package/src/validation.ts +134 -0
  41. package/src/workspace-actions.ts +7 -1
  42. package/src/workspace-merge.ts +12 -1
package/src/validation.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ValidationError } from "./db";
2
+ import { type TokenConstraints, type WorkspaceMetadata, type WorkspaceMode, type WorkspaceProbe, type WorkspaceStatus } from "./types";
3
+ import { VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
2
4
 
3
5
  /**
4
6
  * Trim + validate an optional string input. Throws `ValidationError` when the
@@ -78,3 +80,135 @@ export function cleanStringArray(
78
80
  }
79
81
  return [...new Set(cleaned)];
80
82
  }
83
+
84
+ // ---- generic validators relocated from routes.ts (#299) ----
85
+ export const VALID_WORKSPACE_STATUSES = ["active", "ready", "conflict", "review_requested", "merge_planned", "merged", "abandoned", "cleanup_requested", "cleaned"] as const;
86
+
87
+ export function cleanNullableString(value: unknown, field: string, max: number): string | null | undefined {
88
+ if (value === undefined) return undefined;
89
+ if (value === null) return null;
90
+ return cleanString(value, field, { max }) ?? null;
91
+ }
92
+
93
+ function cleanConstraintStringArray(value: unknown, field: string): string[] | undefined {
94
+ if (value === undefined || value === null) return undefined;
95
+ if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array of strings`);
96
+ const cleaned = value.map((item) => cleanString(item, `${field} item`, { max: 500 })).filter(Boolean) as string[];
97
+ if (cleaned.length > 100) throw new ValidationError(`${field} can contain at most 100 values`);
98
+ return [...new Set(cleaned)];
99
+ }
100
+
101
+ export function cleanTokenConstraints(value: unknown): TokenConstraints | undefined {
102
+ if (value === undefined || value === null) return undefined;
103
+ if (!isRecord(value)) throw new ValidationError("constraints must be an object");
104
+ const constraints: TokenConstraints = {};
105
+ const arrayKeys = [
106
+ "agents",
107
+ "policies",
108
+ "parentAgents",
109
+ "targets",
110
+ "channels",
111
+ "orchestrators",
112
+ "hosts",
113
+ "cwdPrefixes",
114
+ "taskIds",
115
+ "memoryScopes",
116
+ "integrationNames",
117
+ "spawnRequestIds",
118
+ ] as const;
119
+ for (const key of arrayKeys) {
120
+ const cleaned = cleanConstraintStringArray(value[key], `constraints.${key}`);
121
+ if (cleaned?.length) constraints[key] = cleaned;
122
+ }
123
+ const cwd = cleanString(value.cwd, "constraints.cwd", { max: 500 });
124
+ if (cwd) constraints.cwd = cwd;
125
+ for (const key of ["terminalAttach", "logsRead", "canDelegate"] as const) {
126
+ if (value[key] !== undefined) {
127
+ if (typeof value[key] !== "boolean") throw new ValidationError(`constraints.${key} must be a boolean`);
128
+ constraints[key] = value[key];
129
+ }
130
+ }
131
+ return constraints;
132
+ }
133
+
134
+ export function cleanMeta(value: unknown): Record<string, unknown> | undefined {
135
+ if (value === undefined || value === null) return undefined;
136
+ if (!isRecord(value)) throw new ValidationError("meta must be an object");
137
+ if (JSON.stringify(value).length > 8192) throw new ValidationError("meta is too large");
138
+ return value;
139
+ }
140
+
141
+ export function cleanParams(value: unknown, field = "params"): Record<string, unknown> | undefined {
142
+ if (value === undefined || value === null) return undefined;
143
+ if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
144
+ if (JSON.stringify(value).length > 65_536) throw new ValidationError(`${field} is too large`);
145
+ return value;
146
+ }
147
+
148
+ export function cleanWorkspaceMetadata(value: unknown, field: string): WorkspaceMetadata | undefined {
149
+ if (value === undefined || value === null) return undefined;
150
+ if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
151
+ return {
152
+ id: cleanString(value.id, `${field}.id`, { max: 160 }),
153
+ mode: optionalEnum(value.mode, `${field}.mode`, VALID_WORKSPACE_MODES, "shared") as WorkspaceMode,
154
+ requestedMode: optionalEnum(value.requestedMode, `${field}.requestedMode`, VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
155
+ repoRoot: cleanString(value.repoRoot, `${field}.repoRoot`, { max: 1000 }),
156
+ sourceCwd: cleanString(value.sourceCwd, `${field}.sourceCwd`, { max: 1000 }),
157
+ worktreePath: cleanString(value.worktreePath, `${field}.worktreePath`, { max: 1000 }),
158
+ branch: cleanString(value.branch, `${field}.branch`, { max: 240 }),
159
+ baseRef: cleanString(value.baseRef, `${field}.baseRef`, { max: 240 }),
160
+ baseSha: cleanString(value.baseSha, `${field}.baseSha`, { max: 80 }),
161
+ status: optionalEnum(value.status, `${field}.status`, VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined,
162
+ stewardAgentId: cleanString(value.stewardAgentId, `${field}.stewardAgentId`, { max: 240 }),
163
+ probe: isRecord(value.probe) ? value.probe as unknown as WorkspaceProbe : undefined,
164
+ };
165
+ }
166
+
167
+ export function cleanPositiveId(value: unknown, field: string): number | undefined {
168
+ if (value === undefined || value === null) return undefined;
169
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
170
+ throw new ValidationError(`${field} must be a positive integer`);
171
+ }
172
+ return value;
173
+ }
174
+
175
+ export function cleanNullablePositiveId(value: unknown, field: string): number | null | undefined {
176
+ if (value === undefined) return undefined;
177
+ if (value === null) return null;
178
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
179
+ throw new ValidationError(`${field} must be a positive integer or null`);
180
+ }
181
+ return value;
182
+ }
183
+
184
+ export function cleanEpoch(value: unknown, field: string): number | undefined {
185
+ if (value === undefined || value === null) return undefined;
186
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
187
+ throw new ValidationError(`${field} must be a non-negative integer`);
188
+ }
189
+ return value;
190
+ }
191
+
192
+ export function cleanTtlMs(value: unknown): number | undefined {
193
+ if (value === undefined || value === null) return undefined;
194
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
195
+ throw new ValidationError("ttlMs must be a positive integer");
196
+ }
197
+ return value;
198
+ }
199
+
200
+ export function cleanOperatorId(value: unknown): string {
201
+ return cleanString(value, "operatorId", { max: 200 }) || "user";
202
+ }
203
+
204
+ export function cleanPositiveIdArray(value: unknown, field: string, max = 500): number[] {
205
+ if (!Array.isArray(value)) throw new ValidationError(`${field} must be an array`);
206
+ if (value.length === 0) throw new ValidationError(`${field} required`);
207
+ if (value.length > max) throw new ValidationError(`${field} max ${max}`);
208
+ return value.map((item, index) => {
209
+ if (typeof item !== "number" || !Number.isSafeInteger(item) || item <= 0) {
210
+ throw new ValidationError(`${field}[${index}] must be a positive integer`);
211
+ }
212
+ return item;
213
+ });
214
+ }
@@ -20,6 +20,9 @@ import {
20
20
  } from "./db";
21
21
  import { emitActivityEvent } from "./sse";
22
22
  import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
23
+ // Re-export the land-strategy contract so the MCP land tool validates against the same
24
+ // tuple as the HTTP route without a second import hop (one source: workspace-merge, #304).
25
+ export { LAND_STRATEGIES, DEFAULT_MERGE_STRATEGY } from "./workspace-merge";
23
26
  import { claimMetadataPatch, workspaceActiveClaim } from "./workspace-claim";
24
27
  import { TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
25
28
  import type { Command, WorkspaceMergeStrategy, WorkspaceRecord, WorkspaceStatus } from "./types";
@@ -161,7 +164,10 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
161
164
  if (action === "merge") {
162
165
  const result = requestWorkspaceMerge(workspace, {
163
166
  requestedBy: agentId ?? "dashboard",
164
- strategy: input.strategy ?? "auto",
167
+ // Pass the strategy through verbatim (undefined when unset) — the single
168
+ // default lives in requestWorkspaceMerge, so both land surfaces resolve it
169
+ // identically instead of each applying its own fallback (#304).
170
+ strategy: input.strategy,
165
171
  deleteBranch: input.deleteBranch !== false,
166
172
  prTitle: input.prTitle,
167
173
  prBody: input.prBody,
@@ -10,6 +10,17 @@ import {
10
10
  import type { Command, WorkspaceMergeStrategy, WorkspaceRecord } from "./types";
11
11
  import { isPathWithinBase } from "./utils";
12
12
 
13
+ // One home for the land-strategy contract, shared by BOTH land surfaces — the HTTP
14
+ // route (`POST /api/workspaces/:id/actions`, driven by `agent-relay workspace land`)
15
+ // and the `relay_workspace_land` MCP tool. They validate against the same tuple and
16
+ // fall back to the same default, so the effective strategy can't drift by surface
17
+ // (#304: the CLI used to omit the strategy and let the server default while MCP
18
+ // passed its own — two encodings of one rule). The default is applied ONCE, in
19
+ // `requestWorkspaceMerge` below; callers pass `strategy` through verbatim (undefined
20
+ // when unset) so there is a single resolution point, not one per entrypoint.
21
+ export const LAND_STRATEGIES = ["pr", "rebase-ff", "auto"] as const;
22
+ export const DEFAULT_MERGE_STRATEGY: WorkspaceMergeStrategy = "auto";
23
+
13
24
  interface RequestWorkspaceMergeOptions {
14
25
  /** Who asked for the merge (lease holder + audit). e.g. an agent id, "dashboard", "auto-merge". */
15
26
  requestedBy: string;
@@ -103,7 +114,7 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
103
114
  branch: workspace.branch,
104
115
  baseRef: workspace.baseRef,
105
116
  baseSha: workspace.baseSha,
106
- strategy: opts.strategy ?? "auto",
117
+ strategy: opts.strategy ?? DEFAULT_MERGE_STRATEGY,
107
118
  deleteBranch,
108
119
  push: opts.push !== false,
109
120
  prTitle: opts.prTitle,