@zgeoff/atc 2.17.0 → 2.20.0
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 +1 -1
- package/src/agents/agent-adapter.ts +5 -0
- package/src/agents/gateway-adapter.ts +25 -2
- package/src/cli.ts +6 -1
- package/src/client/build-workspace-destination.ts +38 -0
- package/src/client/collect-agent-picks.ts +5 -2
- package/src/client/pick-refusal-step.ts +29 -0
- package/src/client/resolve-workspace-root.ts +49 -0
- package/src/client/spawn-picker.ts +1655 -182
- package/src/daemon/build-agent-list.ts +8 -6
- package/src/daemon/build-auth-binding.ts +58 -0
- package/src/daemon/build-imp-name.ts +11 -0
- package/src/daemon/build-imp-provider.ts +25 -2
- package/src/daemon/daemon-connection.ts +7 -0
- package/src/daemon/imp-port.ts +4 -2
- package/src/daemon/imp-provider.ts +24 -19
- package/src/daemon/restore-fleet.ts +2 -1
- package/src/daemon/sessions.ts +6 -0
- package/src/protocol/protocol.ts +1 -0
- package/src/shared/collect-auth-profiles.ts +122 -0
- package/src/shared/collect-gateways.ts +223 -6
- package/src/shared/collect-workspaces-config.ts +25 -10
- package/src/shared/config.ts +49 -6
- package/src/shared/resolve-auth-profiles.ts +186 -0
- package/src/store/run-migrations.ts +77 -0
- package/src/store/runtime-auth-binding.ts +110 -0
- package/src/store/state-store.ts +186 -0
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
2
|
+
import type { DaemonError } from '../protocol/daemon-error';
|
|
2
3
|
import type { HookEvent } from '../protocol/hook-event';
|
|
3
4
|
import type { AgentID } from '../shared/agent-id';
|
|
4
5
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
@@ -205,6 +206,10 @@ export interface AgentAdapter {
|
|
|
205
206
|
// Absent: the agent runs there as a local spawn plans it, with no files.
|
|
206
207
|
readonly planGuestSpawn?: (opts: SpawnOptions, guest: GuestPaths) => GuestSpawnPlan | null;
|
|
207
208
|
|
|
209
|
+
// The refusal every start of this agent's harness gets, on any target,
|
|
210
|
+
// or null when it may start. Absent: no start is refused.
|
|
211
|
+
readonly findSpawnRefusal?: () => DaemonError | null;
|
|
212
|
+
|
|
208
213
|
// The command that exits 0 inside a remote host when the agent there can
|
|
209
214
|
// sign in without a person. Absent: atc runs no check.
|
|
210
215
|
readonly planAuthCheck?: () => readonly string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
2
|
+
import { DaemonError } from '../protocol/daemon-error';
|
|
2
3
|
import type { HookEvent } from '../protocol/hook-event';
|
|
3
4
|
import type { AgentID } from '../shared/agent-id';
|
|
4
5
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
@@ -39,7 +40,8 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
39
40
|
readonly id: AgentID;
|
|
40
41
|
|
|
41
42
|
// A headless turn carries the same settings file the terminal spawn does,
|
|
42
|
-
// so it reaches this backend rather than the default one.
|
|
43
|
+
// so it reaches this backend rather than the default one. A gateway whose
|
|
44
|
+
// credential comes through impd's broker runs no headless turn.
|
|
43
45
|
readonly headlessRunner: HeadlessRunner | null;
|
|
44
46
|
|
|
45
47
|
// The CLI's hooks are authoritative; no screen heuristics needed.
|
|
@@ -90,7 +92,7 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
90
92
|
this.claude = new ClaudeAdapter(config);
|
|
91
93
|
|
|
92
94
|
this.headlessRunner =
|
|
93
|
-
headlessRun === null
|
|
95
|
+
headlessRun === null || gateway.auth !== undefined
|
|
94
96
|
? null
|
|
95
97
|
: makeClaudeHeadlessRunner(headlessRun, {
|
|
96
98
|
claudeBin: gateway.bin,
|
|
@@ -100,6 +102,21 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
100
102
|
});
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
// A gateway whose credential comes through impd's broker never starts
|
|
106
|
+
// until the broker path exists: started without it, the CLI would send
|
|
107
|
+
// whatever credential it holds to the gateway's host.
|
|
108
|
+
findSpawnRefusal(): DaemonError | null {
|
|
109
|
+
if (this.gateway.auth === undefined) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return new DaemonError(
|
|
114
|
+
'auth_target_unsupported',
|
|
115
|
+
`gateway '${this.id}' takes its credential from impd's broker, and brokered credentials are not wired yet`,
|
|
116
|
+
{ agent: this.id },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
104
121
|
return {
|
|
105
122
|
bin: this.gateway.bin,
|
|
@@ -142,7 +159,13 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
142
159
|
// explicit flag, so that mode overrides the one the CLI would restore, and
|
|
143
160
|
// the generated settings file, because
|
|
144
161
|
// without it the CLI would resume the session against the default backend.
|
|
162
|
+
// A gateway whose credential comes through impd's broker has none, since
|
|
163
|
+
// outside atc the broker never reaches it.
|
|
145
164
|
buildResumeCommand(cwd: string, agentSessionID: AgentSessionID | undefined): string | null {
|
|
165
|
+
if (this.gateway.auth !== undefined) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
const args = [
|
|
147
170
|
...this.gateway.args,
|
|
148
171
|
...buildRestoreModeArgs(this.gateway.args, this.gateway.settings),
|
package/src/cli.ts
CHANGED
|
@@ -182,7 +182,12 @@ const main = defineCommand({
|
|
|
182
182
|
console.error(`atc daemon: config: ${line}`);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
for (const problem of [
|
|
185
|
+
for (const problem of [
|
|
186
|
+
...cfg.principalErrors,
|
|
187
|
+
...cfg.workspaceErrors,
|
|
188
|
+
...cfg.authProfileErrors,
|
|
189
|
+
...cfg.gatewayErrors,
|
|
190
|
+
]) {
|
|
186
191
|
console.error(`atc daemon: config: ${problem}`);
|
|
187
192
|
}
|
|
188
193
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
interface DestinationParts {
|
|
2
|
+
// The root the checkout lands under, absolute on its target.
|
|
3
|
+
readonly root: string;
|
|
4
|
+
|
|
5
|
+
// The repository URL the checkout comes from, as the daemon resolved it.
|
|
6
|
+
readonly url: string;
|
|
7
|
+
|
|
8
|
+
// The branch or tag the commit was chosen by, or null for a bare commit.
|
|
9
|
+
readonly ref: string | null;
|
|
10
|
+
readonly sha: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The default directory a git workspace lands in: `<repo>-<ref>-<short
|
|
15
|
+
* sha>` under the root, with the ref left out for a bare commit. The repo
|
|
16
|
+
* is the URL's last path segment without `.git`, and each part keeps only
|
|
17
|
+
* `[A-Za-z0-9._-]`, with `/` turned into `-`, so the name is one path
|
|
18
|
+
* segment on any target.
|
|
19
|
+
*/
|
|
20
|
+
export function buildWorkspaceDestination(parts: DestinationParts): string {
|
|
21
|
+
const repo =
|
|
22
|
+
parts.url
|
|
23
|
+
.replace(/\/+$/u, '')
|
|
24
|
+
.split(/[/:]/u)
|
|
25
|
+
.at(-1)
|
|
26
|
+
?.replace(/\.git$/u, '') ?? '';
|
|
27
|
+
|
|
28
|
+
const name = [repo, ...(parts.ref === null ? [] : [parts.ref]), parts.sha.slice(0, 7)]
|
|
29
|
+
.map((part) => toSafeSegment(part))
|
|
30
|
+
.filter((part) => part !== '')
|
|
31
|
+
.join('-');
|
|
32
|
+
|
|
33
|
+
return `${parts.root.replace(/\/+$/u, '')}/${name}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toSafeSegment(part: string): string {
|
|
37
|
+
return part.replaceAll('/', '-').replaceAll(/[^\w.-]/gu, '');
|
|
38
|
+
}
|
|
@@ -9,7 +9,8 @@ export interface AgentPick {
|
|
|
9
9
|
/**
|
|
10
10
|
* The agent choices the spawn and adopt flows offer, in menu order. An agent
|
|
11
11
|
* whose configured binary does not resolve is left out, so every row in the
|
|
12
|
-
* menu is a session that can start.
|
|
12
|
+
* menu is a session that can start. A gateway with auth is left out too, since
|
|
13
|
+
* every start of one is refused. Resolution follows the rule a spawn
|
|
13
14
|
* follows: a bare name comes off PATH, a name carrying a separator is taken
|
|
14
15
|
* as a path, and either way it has to be executable.
|
|
15
16
|
*/
|
|
@@ -22,7 +23,9 @@ export function collectAgentPicks(config: Config): AgentPick[] {
|
|
|
22
23
|
{ agent: 'claude', label: 'Claude', bin: config.claudeBin },
|
|
23
24
|
{ agent: 'grok', label: 'Grok', bin: config.grokBin },
|
|
24
25
|
{ agent: 'codex', label: 'Codex', bin: config.codexBin },
|
|
25
|
-
...config.gateways
|
|
26
|
+
...config.gateways
|
|
27
|
+
.filter((g) => g.auth === undefined)
|
|
28
|
+
.map((g) => ({ agent: g.id, label: g.label, bin: g.bin })),
|
|
26
29
|
];
|
|
27
30
|
|
|
28
31
|
return candidates
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The steps of the git spawn flow a refused spawn can return to.
|
|
2
|
+
type RefusalStep = 'confirm' | 'destination' | 'ref' | 'source';
|
|
3
|
+
|
|
4
|
+
const REFUSAL_STEPS: ReadonlyMap<string, RefusalStep> = new Map<string, RefusalStep>([
|
|
5
|
+
['workspace_exists', 'destination'],
|
|
6
|
+
['ref_not_found', 'ref'],
|
|
7
|
+
['invalid_git_url', 'source'],
|
|
8
|
+
['credential_in_url', 'source'],
|
|
9
|
+
['credential_missing', 'source'],
|
|
10
|
+
['clone_failed', 'source'],
|
|
11
|
+
['has_submodules', 'confirm'],
|
|
12
|
+
['lfs_unsupported', 'confirm'],
|
|
13
|
+
['unreadable_tree', 'confirm'],
|
|
14
|
+
['sanitize_failed', 'confirm'],
|
|
15
|
+
['tar_failed', 'confirm'],
|
|
16
|
+
['transfer_failed', 'confirm'],
|
|
17
|
+
['workspace_mismatch', 'confirm'],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The step of the git spawn flow that can fix a workspace spawn's
|
|
22
|
+
* refusal: the destination for one that already exists, the ref for one
|
|
23
|
+
* the upstream lacks, the source step for a URL or clone failure, and the
|
|
24
|
+
* confirm screen for a refusal of the commit or the target's copy of it.
|
|
25
|
+
* Null for any other refusal, which stays on the step that sent the spawn.
|
|
26
|
+
*/
|
|
27
|
+
export function pickRefusalStep(code: string): RefusalStep | null {
|
|
28
|
+
return REFUSAL_STEPS.get(code) ?? null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { WorkspacesConfig } from '../shared/collect-workspaces-config';
|
|
2
|
+
import { resolveHomeDir } from '../shared/resolve-home-dir';
|
|
3
|
+
|
|
4
|
+
type WorkspaceRoot =
|
|
5
|
+
| { readonly ok: true; readonly root: string }
|
|
6
|
+
| { readonly ok: false; readonly message: string };
|
|
7
|
+
|
|
8
|
+
interface RootTarget {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
|
|
11
|
+
// Whether the target is the daemon's own machine, where `~` is this
|
|
12
|
+
// user's home.
|
|
13
|
+
readonly inPlace: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Where checkouts land on the daemon's own machine without a configured root.
|
|
17
|
+
const DEFAULT_ROOT = '~/.local/share/atc/workspaces';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The directory git workspaces land under on a target: the target's own
|
|
21
|
+
* root, else the global root, else the default. On the daemon's own
|
|
22
|
+
* machine a leading `~` expands to the home directory. On any other target
|
|
23
|
+
* the root must be absolute, since `~` there is a home this client cannot
|
|
24
|
+
* see, so a root that is not is refused with the config key to set.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveWorkspaceRoot(config: WorkspacesConfig, target: RootTarget): WorkspaceRoot {
|
|
27
|
+
const configured = config.targetRoots.get(target.id) ?? config.root;
|
|
28
|
+
|
|
29
|
+
if (target.inPlace) {
|
|
30
|
+
const root = expandHome(configured ?? DEFAULT_ROOT);
|
|
31
|
+
|
|
32
|
+
return root.startsWith('/')
|
|
33
|
+
? { ok: true, root }
|
|
34
|
+
: { ok: false, message: `workspaces root '${root}' must be an absolute path` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (configured === null || !configured.startsWith('/')) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
message: `set workspaces.targets.${target.id} in config.json to an absolute path on that target`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ok: true, root: configured };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function expandHome(dir: string): string {
|
|
48
|
+
return dir === '~' || dir.startsWith('~/') ? `${resolveHomeDir()}${dir.slice(1)}` : dir;
|
|
49
|
+
}
|