fix(fleet): validate remote migration targets
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-07-16 05:36:53 -05:00
parent 01d8b16474
commit 66ac59af49
2 changed files with 307 additions and 39 deletions

View File

@@ -74,6 +74,14 @@ const AGENT_DECISION_FIELDS = new Set([
const CLASS_DISPOSITION_FIELDS = new Set(['action', 'className']);
const OBSERVATION_FIELDS = new Set(['systemd', 'tmux']);
const AGENT_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
const V1_TMUX_SOCKET_PATTERN = /^[A-Za-z0-9_.-]+$/;
const V1_REMOTE_TARGET_PATTERNS: Readonly<
Record<(typeof REMOTE_AGENT_INVENTORY_FIELDS)[number], RegExp>
> = Object.freeze({
host: /^[A-Za-z0-9_.:[\]-]+$/,
ssh: /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9_.:[\]-]+$/,
socket: V1_TMUX_SOCKET_PATTERN,
});
const AGENT_FIELDS = new Set([
'name',
'alias',
@@ -347,16 +355,23 @@ export async function previewV1ToV2Migration(
const hasDeclaredSnakeSocket = Object.hasOwn(tmux, 'socket_name');
const hasDeclaredCamelSocket = Object.hasOwn(tmux, 'socketName');
const hasDeclaredSocket = hasDeclaredSnakeSocket || hasDeclaredCamelSocket;
const declaredSocketName = hasDeclaredSnakeSocket
? trimmedString(tmux.socket_name)
: hasDeclaredCamelSocket
? trimmedString(tmux.socketName)
: undefined;
const socketName = hasDeclaredSocket ? declaredSocketName : decisions.socketName;
const declaredSocketValue = hasDeclaredSnakeSocket ? tmux.socket_name : tmux.socketName;
const sourceSocketName = typeof declaredSocketValue === 'string' ? declaredSocketValue : '';
const declaredSocketPath = hasDeclaredSnakeSocket ? 'tmux.socket_name' : 'tmux.socketName';
if (sourceSocketName && !V1_TMUX_SOCKET_PATTERN.test(sourceSocketName)) {
blockers.push(
blocker(
'invalid-v1-targeting-characters',
declaredSocketPath,
'Fleet tmux socket contains unsupported targeting characters.',
),
);
}
const socketName = sourceSocketName;
if (
hasDeclaredSocket &&
decisions.socketName !== undefined &&
decisions.socketName !== declaredSocketName
decisions.socketName !== sourceSocketName
) {
blockers.push(
blocker(
@@ -377,6 +392,17 @@ export async function previewV1ToV2Migration(
blockers.push(blocker('invalid-agent-name', `${path}.name`, 'Agent name is required.'));
continue;
}
const agentSocket = typeof rawAgent.socket === 'string' ? rawAgent.socket : undefined;
if (agentSocket !== undefined && agentSocket !== socketName) {
blockers.push(
blocker(
'agent-socket-disposition-required',
`${path}.socket`,
'Agent socket must match the canonical v1 fleet socket; independent per-agent sockets are not supported.',
name,
),
);
}
const locality = classifyAgentLocality(rawAgent, decisions.fleetHost);
const localityFields = REMOTE_AGENT_INVENTORY_FIELDS.filter(
(field) => rawAgent[field] !== undefined,
@@ -399,17 +425,6 @@ export async function previewV1ToV2Migration(
),
);
}
const agentSocket = stringValue(rawAgent.socket);
if (agentSocket && (!socketName || agentSocket !== socketName)) {
blockers.push(
blocker(
'agent-socket-disposition-required',
`${path}.socket`,
'Local agent socket must match the canonical v2 fleet socket or receive an explicit future disposition.',
name,
),
);
}
const decision = decisions.agents[name];
if (!decision) {
@@ -560,15 +575,6 @@ export async function previewV1ToV2Migration(
),
);
}
if (socketName === undefined) {
blockers.push(
blocker(
'named-socket-disposition-required',
'tmux.socket_name',
'A socket decision is required only when both supported v1 socket fields are absent.',
),
);
}
if (candidateAgents.length === 0) {
blockers.push(
blocker('no-local-candidates', 'agents', 'No local agents are eligible for v2 output.'),
@@ -579,7 +585,7 @@ export async function previewV1ToV2Migration(
let canonicalYaml: string | undefined;
const environment: EnvironmentMigrationPreview[] = [];
if (blockers.length === 0 && socketName !== undefined && decisions.defaultRuntime) {
if (blockers.length === 0 && decisions.defaultRuntime) {
const defaultRuntime = decisions.defaultRuntime;
try {
canonicalRoster = parseRosterV2(
@@ -966,12 +972,22 @@ function validateV1FieldCatalog(raw: Record<string, unknown>, blockers: Migratio
);
}
for (const key of REMOTE_AGENT_INVENTORY_FIELDS) {
if (agent[key] !== undefined && stringValue(agent[key]) === undefined) {
const target = agent[key];
if (target !== undefined && typeof target !== 'string') {
blockers.push(
blocker(
'invalid-v1-field-type',
`${path}.${key}`,
'Remote inventory field must be a non-empty string.',
'Remote inventory field must be a string.',
),
);
} else if (typeof target === 'string' && !V1_REMOTE_TARGET_PATTERNS[key].test(target)) {
blockers.push(
blocker(
'invalid-v1-targeting-characters',
`${path}.${key}`,
'Remote inventory field contains unsupported targeting characters.',
name,
),
);
}