@zgeoff/atc 2.12.0 → 2.13.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/README.md +10 -10
- package/package.json +2 -1
- package/src/agents/agent-adapter.ts +48 -11
- package/src/agents/build-atc-bridge-files.ts +13 -0
- package/src/agents/build-claude-query-options.ts +72 -0
- package/src/agents/build-cli-command.ts +7 -5
- package/src/{daemon → agents}/build-headless-env.ts +12 -6
- package/src/agents/build-hook-settings.ts +8 -2
- package/src/agents/build-restore-mode-args.ts +23 -0
- package/src/agents/claude-adapter.ts +64 -17
- package/src/agents/codex-adapter.ts +5 -0
- package/src/agents/find-claude-permission-mode.ts +24 -0
- package/src/agents/gateway-adapter.ts +38 -11
- package/src/agents/grok-adapter.ts +5 -0
- package/src/agents/make-claude-headless-runner.ts +46 -0
- package/src/agents/plan-pasted-line-input.ts +22 -0
- package/src/agents/plan-typed-line-input.ts +8 -0
- package/src/agents/resolve-claude-permission-mode.ts +13 -0
- package/src/{daemon/start-headless-run.ts → agents/start-claude-headless-run.ts} +6 -53
- package/src/agents/write-atc-bridge.ts +2 -6
- package/src/cli.ts +8 -12
- package/src/client/index.ts +50 -1
- package/src/client/ui.ts +8 -0
- package/src/daemon/build-execution-targets.ts +49 -10
- package/src/daemon/build-imp-provider.ts +68 -0
- package/src/daemon/build-report-trail-entry.ts +4 -2
- package/src/daemon/build-scoped-context.ts +4 -1
- package/src/daemon/build-session-lifecycle.ts +52 -0
- package/src/daemon/build-tar-archive.ts +85 -0
- package/src/daemon/daemon-connection.ts +97 -4
- package/src/daemon/daemon.ts +298 -41
- package/src/daemon/execution-provider.ts +109 -4
- package/src/daemon/hooks.ts +5 -17
- package/src/daemon/imp-client-port.ts +343 -0
- package/src/daemon/imp-harness.ts +618 -0
- package/src/daemon/imp-port-error.ts +18 -0
- package/src/daemon/imp-port.ts +246 -0
- package/src/daemon/imp-provider.ts +601 -0
- package/src/daemon/is-binding-current.ts +43 -0
- package/src/daemon/local-pty-provider.ts +62 -3
- package/src/daemon/materialize-workspace.ts +571 -0
- package/src/daemon/parse-hook-line.ts +31 -0
- package/src/daemon/pick-session-state.ts +15 -0
- package/src/daemon/restore-fleet.ts +41 -23
- package/src/daemon/screen-model.ts +7 -0
- package/src/daemon/sessions.ts +574 -61
- package/src/daemon/start-headless-turn.ts +1 -1
- package/src/daemon/start-session-bridge.ts +294 -0
- package/src/mcp/mcp-tools.ts +9 -2
- package/src/mcp/require-daemon-features.ts +3 -0
- package/src/mcp/run-tool.ts +27 -8
- package/src/protocol/daemon-features.ts +11 -0
- package/src/protocol/protocol.ts +23 -0
- package/src/protocol/request-param-schemas.ts +66 -0
- package/src/report.ts +37 -2
- package/src/run-bridge-tap.ts +241 -0
- package/src/shared/collect-clean-env.ts +9 -1
- package/src/shared/open-bridge-socket.ts +86 -0
- package/src/shared/send-bridge-request.ts +47 -0
- package/src/statusline.ts +34 -3
- package/src/store/fleet-entry.ts +19 -0
- package/src/store/run-migrations.ts +76 -0
- package/src/store/state-store.ts +182 -21
- package/src/store/trail-entry.ts +4 -0
- package/src/store/workspace-materialization.ts +70 -0
- package/src/tap.ts +11 -0
- package/src/workspace/check-url-credentials.ts +59 -0
- package/src/workspace/create-workspace-clone.ts +13 -1
- package/src/workspace/repository-env-vars.ts +22 -0
- package/src/workspace/run-git.ts +2 -18
- /package/src/{daemon → agents}/resolve-headless-executable.ts +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One file in an archive: its path relative to the directory it unpacks
|
|
3
|
+
* into, its bytes, and its permission bits.
|
|
4
|
+
*/
|
|
5
|
+
export interface ArchiveFile {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly content: string | Uint8Array;
|
|
8
|
+
readonly mode?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const BLOCK = 512;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Packs files into a POSIX ustar archive that `tar -x` unpacks, each with
|
|
15
|
+
* its parent directories created on the way. A path longer than ustar
|
|
16
|
+
* holds is refused.
|
|
17
|
+
*/
|
|
18
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- file bytes have no readonly form
|
|
19
|
+
export function buildTarArchive(files: readonly ArchiveFile[]): Uint8Array {
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
|
|
22
|
+
const blocks: Uint8Array[] = [];
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const content = typeof file.content === 'string' ? encoder.encode(file.content) : file.content;
|
|
26
|
+
|
|
27
|
+
blocks.push(
|
|
28
|
+
buildHeader(file.path, content.length, file.mode ?? 0o644),
|
|
29
|
+
content,
|
|
30
|
+
new Uint8Array((BLOCK - (content.length % BLOCK)) % BLOCK),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Two empty blocks end the archive.
|
|
35
|
+
blocks.push(new Uint8Array(BLOCK * 2));
|
|
36
|
+
|
|
37
|
+
const archive = new Uint8Array(blocks.reduce((total, block) => total + block.length, 0));
|
|
38
|
+
|
|
39
|
+
let at = 0;
|
|
40
|
+
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
archive.set(block, at);
|
|
43
|
+
|
|
44
|
+
at += block.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return archive;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildHeader(path: string, size: number, mode: number): Uint8Array {
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
|
|
53
|
+
const name = encoder.encode(path);
|
|
54
|
+
|
|
55
|
+
if (name.length > 100) {
|
|
56
|
+
throw new Error(`archive path is longer than 100 bytes: ${path}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const header = new Uint8Array(BLOCK);
|
|
60
|
+
|
|
61
|
+
header.set(name, 0);
|
|
62
|
+
header.set(encoder.encode(formatOctal(mode, 8)), 100);
|
|
63
|
+
header.set(encoder.encode(formatOctal(0, 8)), 108);
|
|
64
|
+
header.set(encoder.encode(formatOctal(0, 8)), 116);
|
|
65
|
+
header.set(encoder.encode(formatOctal(size, 12)), 124);
|
|
66
|
+
header.set(encoder.encode(formatOctal(0, 12)), 136);
|
|
67
|
+
|
|
68
|
+
// The checksum counts its own field as spaces.
|
|
69
|
+
header.set(encoder.encode(' '), 148);
|
|
70
|
+
|
|
71
|
+
header[156] = '0'.codePointAt(0) ?? 0;
|
|
72
|
+
|
|
73
|
+
header.set(encoder.encode('ustar\u000000'), 257);
|
|
74
|
+
|
|
75
|
+
const checksum = header.reduce((total, byte) => total + byte, 0);
|
|
76
|
+
|
|
77
|
+
header.set(encoder.encode(`${checksum.toString(8).padStart(6, '0')}\u0000 `), 148);
|
|
78
|
+
|
|
79
|
+
return header;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// An octal number padded to fill its field, ending in a NUL.
|
|
83
|
+
function formatOctal(value: number, width: number): string {
|
|
84
|
+
return `${value.toString(8).padStart(width - 1, '0')}\u0000`;
|
|
85
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
1
|
+
import { basename, isAbsolute } from 'node:path';
|
|
2
2
|
import type { AgentAdapter, AgentID, SpawnOptions, SpawnOverrides } from '../agents/agent-adapter';
|
|
3
3
|
import { DaemonError } from '../protocol/daemon-error';
|
|
4
4
|
import { DAEMON_FEATURES } from '../protocol/daemon-features';
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
encodeMessage,
|
|
16
16
|
} from '../protocol/protocol';
|
|
17
17
|
import type { ErrorCode, EventMsg, RequestMsg } from '../protocol/protocol';
|
|
18
|
+
import type { SpawnWorkspaceSource } from '../protocol/request-param-schemas';
|
|
18
19
|
import type { TargetConfigError } from '../shared/collect-targets';
|
|
19
20
|
import type { DaemonID } from '../shared/daemon-id';
|
|
20
21
|
import type { MessageID } from '../shared/message-id';
|
|
@@ -49,6 +50,10 @@ export interface SpawnParams {
|
|
|
49
50
|
readonly parent: SessionID | null;
|
|
50
51
|
readonly overrides: SpawnOverrides;
|
|
51
52
|
readonly target: string;
|
|
53
|
+
|
|
54
|
+
// Where the session's working directory comes from; null runs the
|
|
55
|
+
// session in cwd as it stands.
|
|
56
|
+
readonly workspace: SpawnWorkspaceSource | null;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
interface SessionRecord {
|
|
@@ -61,6 +66,15 @@ interface SessionRecord {
|
|
|
61
66
|
|
|
62
67
|
// A transcript page with the file it came from, so a cursor into a replaced
|
|
63
68
|
// transcript is distinguishable from one into a grown transcript.
|
|
69
|
+
/**
|
|
70
|
+
* What `session.forget` answers: the token a forget that destroys a host
|
|
71
|
+
* must carry, with the time it stops being taken; or the forgotten session,
|
|
72
|
+
* with whether its host was destroyed.
|
|
73
|
+
*/
|
|
74
|
+
type ForgetResult =
|
|
75
|
+
| { readonly confirmToken: string; readonly expiresAt: number }
|
|
76
|
+
| { readonly forgotten: true; readonly destroyed: boolean };
|
|
77
|
+
|
|
64
78
|
interface SessionTranscriptRead {
|
|
65
79
|
readonly path: string;
|
|
66
80
|
readonly page: TranscriptPage;
|
|
@@ -99,6 +113,10 @@ export interface DaemonContext {
|
|
|
99
113
|
// without a target when the config gives no default.
|
|
100
114
|
readonly resolveSpawnTarget: (requested: string | undefined) => string;
|
|
101
115
|
|
|
116
|
+
// Throws the refusal for a target that cannot materialize a workspace:
|
|
117
|
+
// one whose provider cannot both transfer an archive and run a command.
|
|
118
|
+
readonly requireWorkspaceTarget: (target: string) => void;
|
|
119
|
+
|
|
102
120
|
// Runs the plan, which throws the refusal for a spawn it refuses, then
|
|
103
121
|
// spawns. Answers with the `session.spawn` ok payload, which a keyed
|
|
104
122
|
// retry replays as the first spawn answered it while the access, when
|
|
@@ -109,6 +127,13 @@ export interface DaemonContext {
|
|
|
109
127
|
access: TargetAccess | null,
|
|
110
128
|
) => Promise<Readonly<Record<string, unknown>>>;
|
|
111
129
|
readonly killSession: (id: SessionID) => Promise<boolean>;
|
|
130
|
+
|
|
131
|
+
// Forgets a session, or answers with the token a forget that destroys a
|
|
132
|
+
// host must carry. Throws the refusal for a token it does not take.
|
|
133
|
+
readonly forgetSession: (
|
|
134
|
+
id: SessionID,
|
|
135
|
+
confirmToken: string | undefined,
|
|
136
|
+
) => Promise<ForgetResult | 'missing'>;
|
|
112
137
|
readonly updateSession: (id: SessionID, name?: string, pinned?: boolean) => boolean | 'child_pin';
|
|
113
138
|
readonly quitDaemon: () => void;
|
|
114
139
|
readonly ackSession: (id: SessionID) => boolean;
|
|
@@ -127,6 +152,13 @@ export interface DaemonContext {
|
|
|
127
152
|
sessionID: SessionID,
|
|
128
153
|
data: string,
|
|
129
154
|
) => 'busy' | 'ok' | 'missing' | 'dead';
|
|
155
|
+
|
|
156
|
+
// Types a line into the session and submits it the way the session's
|
|
157
|
+
// agent accepts a line, as opposed to the raw bytes the input write takes.
|
|
158
|
+
readonly writeSessionLine: (
|
|
159
|
+
sessionID: SessionID,
|
|
160
|
+
text: string,
|
|
161
|
+
) => 'busy' | 'ok' | 'missing' | 'dead';
|
|
130
162
|
readonly ejectSession: (
|
|
131
163
|
id: SessionID,
|
|
132
164
|
prompt: string,
|
|
@@ -135,7 +167,7 @@ export interface DaemonContext {
|
|
|
135
167
|
id: SessionID,
|
|
136
168
|
cols: number,
|
|
137
169
|
rows: number,
|
|
138
|
-
) => 'ok' | 'missing' | 'no_transcript'
|
|
170
|
+
) => Promise<'ok' | 'missing' | 'no_transcript'>;
|
|
139
171
|
readonly resizeSession: (client: OutputClient, sessionID: SessionID, dims: Dims) => boolean;
|
|
140
172
|
readonly resyncClient: (sessionID: SessionID, client: OutputClient) => Promise<void>;
|
|
141
173
|
readonly queueBytes?: number;
|
|
@@ -551,6 +583,27 @@ export class DaemonConnection {
|
|
|
551
583
|
|
|
552
584
|
return;
|
|
553
585
|
}
|
|
586
|
+
case 'session.forget': {
|
|
587
|
+
const parsed = parseRequestParams('session.forget', req.p);
|
|
588
|
+
|
|
589
|
+
if (!parsed.ok) {
|
|
590
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
591
|
+
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const id = parsed.data.session;
|
|
596
|
+
|
|
597
|
+
const forgotten = await this.ctx.forgetSession(id, parsed.data.confirmToken);
|
|
598
|
+
|
|
599
|
+
if (forgotten === 'missing') {
|
|
600
|
+
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
601
|
+
} else {
|
|
602
|
+
this.sendOk(req.id, { ...forgotten });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
554
607
|
case 'session.resumeCommand': {
|
|
555
608
|
const parsed = parseRequestParams('session.resumeCommand', req.p);
|
|
556
609
|
|
|
@@ -636,7 +689,8 @@ export class DaemonConnection {
|
|
|
636
689
|
}
|
|
637
690
|
|
|
638
691
|
const sessionID = parsed.data.session;
|
|
639
|
-
|
|
692
|
+
|
|
693
|
+
const adoptResult = await ctx.adoptSession(sessionID, parsed.data.cols, parsed.data.rows);
|
|
640
694
|
|
|
641
695
|
if (adoptResult === 'ok') {
|
|
642
696
|
this.sendOk(req.id, {});
|
|
@@ -680,6 +734,11 @@ export class DaemonConnection {
|
|
|
680
734
|
|
|
681
735
|
return;
|
|
682
736
|
}
|
|
737
|
+
case 'session.submit': {
|
|
738
|
+
this.applySubmit(req, ctx);
|
|
739
|
+
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
683
742
|
case 'session.resize': {
|
|
684
743
|
this.applyResize(req, ctx);
|
|
685
744
|
|
|
@@ -787,6 +846,18 @@ export class DaemonConnection {
|
|
|
787
846
|
throw new DaemonError(overrides.code, overrides.message);
|
|
788
847
|
}
|
|
789
848
|
|
|
849
|
+
// Checked before the spawn starts, so a target that cannot take a
|
|
850
|
+
// workspace refuses it before any git command runs.
|
|
851
|
+
if (data.workspace !== undefined) {
|
|
852
|
+
this.ctx.requireWorkspaceTarget(target);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// A workspace is materialized at cwd on the target, which only an
|
|
856
|
+
// absolute path names.
|
|
857
|
+
if (data.workspace !== undefined && !isAbsolute(data.cwd)) {
|
|
858
|
+
throw new DaemonError('bad_args', 'a spawn with a workspace requires an absolute cwd');
|
|
859
|
+
}
|
|
860
|
+
|
|
790
861
|
let parent: SessionID | null = null;
|
|
791
862
|
|
|
792
863
|
if (data.parent !== undefined) {
|
|
@@ -813,6 +884,7 @@ export class DaemonConnection {
|
|
|
813
884
|
parent,
|
|
814
885
|
overrides: overrides.overrides,
|
|
815
886
|
target,
|
|
887
|
+
workspace: data.workspace ?? null,
|
|
816
888
|
};
|
|
817
889
|
};
|
|
818
890
|
|
|
@@ -869,8 +941,29 @@ export class DaemonConnection {
|
|
|
869
941
|
}
|
|
870
942
|
|
|
871
943
|
const sessionID = parsed.data.session;
|
|
872
|
-
const result = ctx.writeSessionInput(sessionID, parsed.data.d);
|
|
873
944
|
|
|
945
|
+
this.sendInputResult(req, sessionID, ctx.writeSessionInput(sessionID, parsed.data.d));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private applySubmit(req: RequestMsg, ctx: DaemonContext): void {
|
|
949
|
+
const parsed = parseRequestParams('session.submit', req.p);
|
|
950
|
+
|
|
951
|
+
if (!parsed.ok) {
|
|
952
|
+
this.sendErr(req.id, 'bad_args', parsed.message);
|
|
953
|
+
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const sessionID = parsed.data.session;
|
|
958
|
+
|
|
959
|
+
this.sendInputResult(req, sessionID, ctx.writeSessionLine(sessionID, parsed.data.text));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private sendInputResult(
|
|
963
|
+
req: RequestMsg,
|
|
964
|
+
sessionID: SessionID,
|
|
965
|
+
result: 'busy' | 'ok' | 'missing' | 'dead',
|
|
966
|
+
): void {
|
|
874
967
|
if (result === 'missing') {
|
|
875
968
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
876
969
|
|