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
|
||||
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,
|
||||
reasoning, enabled state, and launch policy. A supported socket field declared by v1 remains authoritative,
|
||||
including an explicit empty value representing the default tmux server; a matching `socketName` decision is
|
||||
accepted, an incompatible decision blocks, and a decision supplies the socket only when both supported v1
|
||||
socket fields are genuinely absent. If v1 omitted `tool_policy`, decisions must supply an
|
||||
reasoning, enabled state, and launch policy. The v1 source remains authoritative for socket semantics:
|
||||
a supported declared socket field, including an explicit empty value for the default tmux server, is
|
||||
preserved; if both supported root aliases are absent, the production v1 default is the literal empty socket.
|
||||
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
|
||||
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 |
|
||||
| `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 |
|
||||
| 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 |
|
||||
| root `connector` | Inventory-only; never contacted or reconciled |
|
||||
| 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([
|
||||
['socket_name', " socket_name: ''\n holder_session: _holder"],
|
||||
['socketName', " socketName: ''\n holder_session: _holder"],
|
||||
|
||||
@@ -778,8 +778,8 @@ function classifyAgentLocality(
|
||||
if (hasSsh) {
|
||||
if (!ssh) return 'ambiguous';
|
||||
const separator = ssh.lastIndexOf('@');
|
||||
const sshHost = separator >= 0 ? ssh.slice(separator + 1) : undefined;
|
||||
if (!sshHost || sshHost !== host) return 'ambiguous';
|
||||
const sshHost = separator >= 0 ? ssh.slice(separator + 1) : ssh;
|
||||
if (sshHost !== host) return 'ambiguous';
|
||||
}
|
||||
return host === fleetHost ? 'local' : 'remote';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user