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.
- package/package.json +2 -2
- package/public/assets/display-JI19Vc7L.js.map +1 -1
- package/src/branch-landed.ts +38 -2
- package/src/cli.ts +3 -3
- package/src/maintenance.ts +21 -21
- package/src/mcp.ts +2 -2
- package/src/ratchet-files.ts +37 -0
- package/src/routes/_shared.ts +376 -0
- package/src/routes/activity.ts +61 -0
- package/src/routes/agent-profiles.ts +47 -0
- package/src/routes/agent-sessions.ts +488 -0
- package/src/routes/agents-spawn.ts +274 -0
- package/src/routes/agents.ts +251 -0
- package/src/routes/artifacts.ts +226 -0
- package/src/routes/automations.ts +83 -0
- package/src/routes/commands.ts +317 -0
- package/src/routes/config.ts +66 -0
- package/src/routes/connectors.ts +108 -0
- package/src/routes/inbox.ts +142 -0
- package/src/routes/index.ts +293 -0
- package/src/routes/insights.ts +81 -0
- package/src/routes/integrations.ts +592 -0
- package/src/routes/memory.ts +337 -0
- package/src/routes/messages.ts +529 -0
- package/src/routes/orchestrator-bootstrap.ts +100 -0
- package/src/routes/orchestrator-proxy.ts +160 -0
- package/src/routes/orchestrator.ts +490 -0
- package/src/routes/pairs.ts +197 -0
- package/src/routes/provider-config.ts +112 -0
- package/src/routes/recipes.ts +113 -0
- package/src/routes/spawn-policy.ts +231 -0
- package/src/routes/spec.ts +54 -0
- package/src/routes/sse.ts +9 -0
- package/src/routes/stats.ts +32 -0
- package/src/routes/steward.ts +45 -0
- package/src/routes/tasks.ts +174 -0
- package/src/routes/tokens.ts +311 -0
- package/src/routes/workspaces.ts +364 -0
- package/src/routes.ts +3 -6822
- package/src/validation.ts +134 -0
- package/src/workspace-actions.ts +7 -1
- 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
|
+
}
|
package/src/workspace-actions.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/workspace-merge.ts
CHANGED
|
@@ -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 ??
|
|
117
|
+
strategy: opts.strategy ?? DEFAULT_MERGE_STRATEGY,
|
|
107
118
|
deleteBranch,
|
|
108
119
|
push: opts.push !== false,
|
|
109
120
|
prTitle: opts.prTitle,
|