fix(fleet): align migration SSH parity docs
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
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:
@@ -22,10 +22,11 @@ path value. These request-shape failures are reported before any input file is r
|
|||||||
observation JSON is validated fail-closed: unknown fields, malformed values, and records for non-local
|
observation JSON is validated fail-closed: unknown fields, malformed values, and records for non-local
|
||||||
agents are rejected. Decisions must supply a positive v2 `generation`, a reviewed `fleetHost` whenever
|
agents are rejected. Decisions must supply a positive v2 `generation`, a reviewed `fleetHost` whenever
|
||||||
v1 agents include `host` or `ssh`, explicit `defaultRuntime`, and per-local-agent provider, model,
|
v1 agents include `host` or `ssh`, explicit `defaultRuntime`, and per-local-agent provider, model,
|
||||||
reasoning, enabled state, and launch policy. A supported socket field declared by v1 remains authoritative,
|
reasoning, enabled state, and launch policy. The v1 source remains authoritative for socket semantics:
|
||||||
including an explicit empty value representing the default tmux server; a matching `socketName` decision is
|
a supported declared socket field, including an explicit empty value for the default tmux server, is
|
||||||
accepted, an incompatible decision blocks, and a decision supplies the socket only when both supported v1
|
preserved; if both supported root aliases are absent, the production v1 default is the literal empty socket.
|
||||||
socket fields are genuinely absent. If v1 omitted `tool_policy`, decisions must supply an
|
A matching `socketName` decision is accepted and an incompatible decision blocks, but a decision never
|
||||||
|
supplies or repairs a missing source socket. If v1 omitted `tool_policy`, decisions must supply an
|
||||||
explicit replacement; it is never derived from `class`. `model_hint` is never split or treated as
|
explicit replacement; it is never derived from `class`. `model_hint` is never split or treated as
|
||||||
authority.
|
authority.
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ be marked disabled. Observed-stopped agents always remain stopped.
|
|||||||
| `provider`, `model_hint`, `reasoning_level` | Explicit provider/model/reasoning decisions; no model-hint inference |
|
| `provider`, `model_hint`, `reasoning_level` | Explicit provider/model/reasoning decisions; no model-hint inference |
|
||||||
| `class`, `tool_policy` | Only approved aliases canonicalize automatically; other classes require explicit preserve/replace disposition and shared-resolver validation |
|
| `class`, `tool_policy` | Only approved aliases canonicalize automatically; other classes require explicit preserve/replace disposition and shared-resolver validation |
|
||||||
| `kickstart_template` | No v2 field; explicit inventory-only disposition required |
|
| `kickstart_template` | No v2 field; explicit inventory-only disposition required |
|
||||||
| agent `host`, `ssh` | `host != fleetHost` is demonstrably remote and inventory-only; `host == fleetHost` stays local; ssh-only, missing fleet-host evidence, or contradictory targets block |
|
| agent `host`, `ssh` | `host != fleetHost` is demonstrably remote and inventory-only; `host == fleetHost` stays local; SSH targets with or without an explicit user must agree with `host`; ssh-only, missing fleet-host evidence, or contradictory targets block |
|
||||||
| agent `socket` | Same-host candidate only when it matches the canonical fleet socket; conflicts block for explicit future disposition |
|
| agent `socket` | Same-host candidate only when it matches the canonical fleet socket; conflicts block for explicit future disposition |
|
||||||
| root `connector` | Inventory-only; never contacted or reconciled |
|
| root `connector` | Inventory-only; never contacted or reconciled |
|
||||||
| unknown fields or snake/camel synonym collisions | Inventoried and block readiness |
|
| unknown fields or snake/camel synonym collisions | Inventoried and block readiness |
|
||||||
|
|||||||
@@ -352,6 +352,41 @@ describe('v1-to-v2 migration preview', (): void => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('matches production v1 locality for a remote agent with a bare-host SSH target', async () => {
|
||||||
|
const source = v1Source.replace('ssh: operator@build.example.test', 'ssh: build.example.test');
|
||||||
|
const rosterPath = join(
|
||||||
|
await mkdtemp(join(tmpdir(), 'v1-bare-host-ssh-parity-')),
|
||||||
|
'roster.yaml',
|
||||||
|
);
|
||||||
|
temporaryDirectories.push(dirname(rosterPath));
|
||||||
|
await writeFile(rosterPath, source);
|
||||||
|
|
||||||
|
await expect(loadFleetRoster(rosterPath)).resolves.toMatchObject({
|
||||||
|
agents: [
|
||||||
|
expect.any(Object),
|
||||||
|
expect.objectContaining({ host: 'build.example.test', ssh: 'build.example.test' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const preview = await previewV1ToV2Migration({
|
||||||
|
source,
|
||||||
|
decisions: decisions(),
|
||||||
|
observations: observations(),
|
||||||
|
personaDirs: await semanticDirs(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preview.status).toBe('ready');
|
||||||
|
expect(preview.blockers).toEqual([]);
|
||||||
|
expect(preview.inventoryOnlyAgents).toEqual([
|
||||||
|
{
|
||||||
|
name: 'remote0',
|
||||||
|
fields: ['host', 'socket', 'ssh'],
|
||||||
|
disposition: 'inventory-only',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(preview.canonicalRoster?.agents.map(({ name }) => name)).toEqual(['coder0']);
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['socket_name', " socket_name: ''\n holder_session: _holder"],
|
['socket_name', " socket_name: ''\n holder_session: _holder"],
|
||||||
['socketName', " socketName: ''\n holder_session: _holder"],
|
['socketName', " socketName: ''\n holder_session: _holder"],
|
||||||
|
|||||||
@@ -778,8 +778,8 @@ function classifyAgentLocality(
|
|||||||
if (hasSsh) {
|
if (hasSsh) {
|
||||||
if (!ssh) return 'ambiguous';
|
if (!ssh) return 'ambiguous';
|
||||||
const separator = ssh.lastIndexOf('@');
|
const separator = ssh.lastIndexOf('@');
|
||||||
const sshHost = separator >= 0 ? ssh.slice(separator + 1) : undefined;
|
const sshHost = separator >= 0 ? ssh.slice(separator + 1) : ssh;
|
||||||
if (!sshHost || sshHost !== host) return 'ambiguous';
|
if (sshHost !== host) return 'ambiguous';
|
||||||
}
|
}
|
||||||
return host === fleetHost ? 'local' : 'remote';
|
return host === fleetHost ? 'local' : 'remote';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user