@zgeoff/atc 3.3.1 → 3.4.1
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 +5 -3
- package/src/agents/agent-adapter.ts +10 -0
- package/src/agents/build-agent-adapters.ts +21 -3
- package/src/agents/build-cli-argv.ts +2 -4
- package/src/agents/claude-adapter.ts +13 -3
- package/src/agents/codex-adapter.ts +31 -2
- package/src/agents/gateway-adapter.ts +20 -8
- package/src/agents/grok-adapter.ts +16 -13
- package/src/agents/load-claude-config-bundle.ts +6 -6
- package/src/agents/print-codex-hook-file.ts +7 -3
- package/src/agents/print-grok-hook-file.ts +8 -3
- package/src/agents/render-sdk-message.ts +57 -0
- package/src/agents/resolve-agent-home.ts +7 -3
- package/src/agents/resolve-claude-global-config-path.ts +4 -4
- package/src/agents/start-claude-headless-run.ts +2 -57
- package/src/agents/update-claude-project-trust.ts +41 -30
- package/src/agents/with-claude-config-lock.ts +37 -12
- package/src/agents/write-atc-bridge.ts +10 -7
- package/src/agents/write-hook-settings.ts +13 -7
- package/src/client/boot-daemon.ts +122 -39
- package/src/client/dirs.ts +4 -2
- package/src/client/index.ts +17 -1
- package/src/client/log-client-event.ts +20 -0
- package/src/client/resolve-workspace-root.ts +10 -5
- package/src/client/spawn-picker.ts +200 -121
- package/src/client/ui.ts +7 -7
- package/src/clients.ts +41 -11
- package/src/daemon/daemon.ts +143 -33
- package/src/daemon/event-signal.ts +19 -3
- package/src/daemon/make-hook-runner.ts +82 -22
- package/src/daemon/permission-registry.ts +11 -6
- package/src/daemon/restore-fleet.ts +62 -17
- package/src/daemon/run-eject-handoff.ts +20 -2
- package/src/daemon/session-runtime.ts +0 -4
- package/src/daemon/sessions.ts +6 -0
- package/src/daemon/start-tcp-listener.ts +12 -10
- package/src/federation/daemon-caller.ts +25 -4
- package/src/federation/daemon-pool.ts +3 -1
- package/src/federation/open-gateway-caller.ts +6 -1
- package/src/gateway.ts +2 -179
- package/src/grants.ts +36 -11
- package/src/hook-report.ts +17 -3
- package/src/mcp/read-report-texts.ts +11 -7
- package/src/mcp/start-mcp-http-server.ts +5 -1
- package/src/mcp-http-server.ts +88 -32
- package/src/report.ts +21 -3
- package/src/run-bridge-tap.ts +36 -7
- package/src/run-config-migrate.ts +35 -11
- package/src/run-gateway-cli.ts +327 -0
- package/src/run-gateway.ts +51 -18
- package/src/shared/build-atc-command.ts +7 -3
- package/src/shared/bun-sqlite-driver.ts +10 -0
- package/src/shared/claim-daemon-lock.ts +10 -4
- package/src/shared/collect-dir-roots.ts +6 -6
- package/src/shared/collect-hooks.ts +7 -6
- package/src/shared/config.ts +19 -9
- package/src/shared/system-clock.ts +24 -0
- package/src/sources/github/collect-github-repos.ts +1 -1
- package/src/sources/github/read-git-protocol.ts +1 -1
- package/src/sources/github/run-gh.ts +24 -9
- package/src/store/state-store.ts +57 -7
- package/src/tap.ts +44 -13
- package/src/test-utils/build-mock-agent-adapter.ts +25 -0
- package/src/test-utils/build-mock-agent-entry.ts +32 -0
- package/src/test-utils/build-mock-auth-binding.ts +28 -0
- package/src/test-utils/build-mock-auth-profile.ts +62 -0
- package/src/test-utils/build-mock-daemon-record.ts +26 -0
- package/src/test-utils/build-mock-execution-target.ts +25 -0
- package/src/test-utils/build-mock-fleet-entry.ts +33 -0
- package/src/test-utils/build-mock-gateway-config.ts +32 -0
- package/src/test-utils/build-mock-imp-identity.ts +18 -0
- package/src/test-utils/build-mock-imp-session-request.ts +53 -0
- package/src/test-utils/build-mock-key-binding-claim.ts +34 -0
- package/src/test-utils/build-mock-message-record.ts +26 -0
- package/src/test-utils/build-mock-mirror-session.ts +42 -0
- package/src/test-utils/build-mock-registry-daemon.ts +28 -0
- package/src/test-utils/build-mock-session-descriptor.ts +66 -0
- package/src/test-utils/build-mock-session.ts +43 -0
- package/src/test-utils/build-mock-sortable-session-view.ts +20 -0
- package/src/test-utils/build-mock-stored-event.ts +20 -0
- package/src/test-utils/build-mock-stored-row.ts +23 -0
- package/src/test-utils/build-stub-attention-adapter.ts +22 -0
- package/src/test-utils/build-stub-bridge-context.ts +20 -0
- package/src/test-utils/build-stub-brokered-agent-adapter.ts +96 -0
- package/src/test-utils/build-stub-brokered-gateway-adapter.ts +50 -0
- package/src/test-utils/build-stub-channel-opener.ts +45 -0
- package/src/test-utils/build-stub-claude-headless-run.ts +13 -0
- package/src/test-utils/build-stub-client-screens.ts +33 -0
- package/src/test-utils/build-stub-clock.ts +67 -0
- package/src/test-utils/build-stub-daemon-context.ts +81 -0
- package/src/test-utils/build-stub-daemon-requests.ts +103 -0
- package/src/test-utils/{fixture-dir-provider.ts → build-stub-dir-provider.ts} +20 -12
- package/src/test-utils/build-stub-execution-provider.ts +112 -0
- package/src/test-utils/build-stub-fleet-caller.ts +60 -0
- package/src/test-utils/build-stub-forking-gh.ts +10 -0
- package/src/test-utils/build-stub-gh.ts +50 -0
- package/src/test-utils/build-stub-handoff-daemon.ts +11 -0
- package/src/test-utils/build-stub-harness-relay.ts +50 -0
- package/src/test-utils/build-stub-headless-runner.ts +60 -0
- package/src/test-utils/build-stub-held-gh.ts +19 -0
- package/src/test-utils/build-stub-held-inbox-source.ts +36 -0
- package/src/test-utils/build-stub-held-provider.ts +58 -0
- package/src/test-utils/build-stub-held-zoxide.ts +13 -0
- package/src/test-utils/build-stub-host-hold.ts +31 -0
- package/src/test-utils/build-stub-host-provider.ts +26 -0
- package/src/test-utils/build-stub-log.ts +15 -0
- package/src/test-utils/build-stub-mcp-claude.ts +26 -0
- package/src/test-utils/build-stub-mcp-grok.ts +19 -0
- package/src/test-utils/build-stub-mcp-stdio-server.ts +20 -0
- package/src/test-utils/build-stub-peer-socket.ts +81 -0
- package/src/test-utils/build-stub-picker-host.ts +36 -0
- package/src/test-utils/build-stub-proxied-agent-adapter.ts +29 -0
- package/src/test-utils/build-stub-pty-provider.ts +53 -0
- package/src/test-utils/build-stub-recording-atc.ts +10 -0
- package/src/test-utils/build-stub-recording-filter.ts +9 -0
- package/src/test-utils/build-stub-recording-git.ts +8 -0
- package/src/test-utils/build-stub-signed-in-gh.ts +18 -0
- package/src/test-utils/build-stub-signed-out-gh.ts +8 -0
- package/src/test-utils/build-stub-soft-kill-provider.ts +29 -0
- package/src/test-utils/build-stub-targets.ts +58 -0
- package/src/test-utils/build-stub-terminal.ts +19 -0
- package/src/test-utils/build-stub-timeout-scheduler.ts +48 -0
- package/src/test-utils/build-stub-tui-claude.ts +47 -0
- package/src/test-utils/build-stub-tui-grok.ts +53 -0
- package/src/test-utils/build-stub-wait-clock.ts +23 -0
- package/src/test-utils/build-target-options-from-config.ts +37 -0
- package/src/test-utils/collect-process-tree.ts +75 -0
- package/src/test-utils/create-git-fixture.ts +117 -0
- package/src/test-utils/create-migrated-state-db.ts +12 -10
- package/src/test-utils/create-stub-bin.ts +19 -0
- package/src/test-utils/create-stub-claude.ts +75 -0
- package/src/test-utils/create-stub-codex.ts +31 -0
- package/src/test-utils/create-stub-composer.ts +65 -0
- package/src/test-utils/create-stub-echo-claude.ts +21 -0
- package/src/test-utils/create-stub-failing-agent-adapter.ts +111 -0
- package/src/test-utils/create-stub-grok.ts +44 -0
- package/src/test-utils/create-stub-guest-clis.ts +55 -0
- package/src/test-utils/create-stub-harness-guest.ts +35 -0
- package/src/test-utils/{fixture-imp-port.ts → create-stub-imp-port.ts} +144 -63
- package/src/test-utils/create-stub-recording-claude.ts +20 -0
- package/src/test-utils/create-stub-recording-codex.ts +20 -0
- package/src/test-utils/create-stub-shared-server-codex.ts +56 -0
- package/src/test-utils/{setup-fake-systemd.ts → create-stub-systemd.ts} +15 -9
- package/src/test-utils/get-gateway-config.ts +3 -3
- package/src/test-utils/get-only-effect-ref.ts +21 -0
- package/src/test-utils/get-only-imp-name.ts +19 -0
- package/src/test-utils/get-records.ts +20 -0
- package/src/test-utils/get-string.ts +14 -0
- package/src/test-utils/keys.ts +26 -0
- package/src/test-utils/merge-deep.ts +36 -0
- package/src/test-utils/mock-overrides.ts +13 -0
- package/src/test-utils/open-repo-step.ts +24 -0
- package/src/test-utils/read-query-plan.ts +22 -0
- package/src/test-utils/register-test-cleanup.ts +63 -0
- package/src/test-utils/resolve-atc-command.ts +13 -0
- package/src/test-utils/resolve-gateway-command.ts +13 -0
- package/src/test-utils/run-atc.ts +56 -0
- package/src/test-utils/run-legacy-daemon.ts +27 -5
- package/src/test-utils/run-listener-daemon.ts +5 -10
- package/src/test-utils/run-mcp-authorization.ts +7 -4
- package/src/test-utils/run-source-daemon.ts +2 -2
- package/src/test-utils/seed-faker.ts +7 -0
- package/src/test-utils/send-lines-before-handshake.ts +35 -0
- package/src/test-utils/send-mcp-request.ts +25 -0
- package/src/test-utils/setup-mcp-home.ts +110 -0
- package/src/test-utils/setup-mcp-http.ts +26 -30
- package/src/test-utils/setup-temp-dir.ts +7 -6
- package/src/test-utils/spawn-claude-session.ts +34 -0
- package/src/test-utils/spawn-grok-session.ts +39 -0
- package/src/test-utils/start-cut-proxy.ts +29 -8
- package/src/test-utils/start-daemon-process.ts +193 -0
- package/src/test-utils/start-git-http-server.ts +28 -4
- package/src/test-utils/start-mcp-stdio.ts +190 -0
- package/src/test-utils/start-output-capture.ts +20 -0
- package/src/test-utils/start-stub-answering-listener.ts +65 -0
- package/src/test-utils/start-stub-closing-listener.ts +31 -0
- package/src/test-utils/start-stub-dropping-daemon.ts +105 -0
- package/src/test-utils/start-stub-impd-info.ts +38 -0
- package/src/test-utils/start-stub-impd.ts +143 -0
- package/src/test-utils/start-stub-legacy-daemon.ts +171 -0
- package/src/test-utils/start-stub-mcp-server.ts +55 -0
- package/src/test-utils/start-stub-recording-listener.ts +47 -0
- package/src/test-utils/start-stub-refusing-listener.ts +43 -0
- package/src/test-utils/start-stub-reporter-socket.ts +79 -0
- package/src/test-utils/start-stub-restore-daemon.ts +59 -0
- package/src/test-utils/start-stub-session-bridge.ts +100 -0
- package/src/test-utils/start-stub-silent-server.ts +38 -0
- package/src/test-utils/start-stub-source-daemon.ts +41 -0
- package/src/test-utils/start-stub-stalled-client.ts +61 -0
- package/src/test-utils/start-stub-stalled-listener.ts +44 -0
- package/src/test-utils/start-stub-tap.ts +38 -0
- package/src/test-utils/start-stub-tcp-daemon.ts +75 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +32 -0
- package/src/test-utils/start-stub-unanswering-listener.ts +41 -0
- package/src/test-utils/start-stub-unanswering-unix-listener.ts +38 -0
- package/src/test-utils/start-test-daemon.ts +229 -0
- package/src/test-utils/start-tui-harness.ts +291 -0
- package/src/test-utils/subscribe-to-socket-lines.ts +81 -14
- package/src/test-utils/try-send-request.ts +29 -0
- package/src/test-utils/wait-for-event.ts +41 -0
- package/src/test-utils/wait-for.ts +15 -3
- package/src/verify-restored-fleet.ts +30 -17
- package/src/workspace/check-repository-access.ts +4 -0
- package/src/workspace/collect-remote-refs.ts +4 -2
- package/src/workspace/run-git.ts +6 -0
- package/src/test-utils/start-legacy-daemon.ts +0 -111
package/src/run-bridge-tap.ts
CHANGED
|
@@ -29,6 +29,24 @@ const SEEN_LIMIT = 10_000;
|
|
|
29
29
|
// opened the tap.
|
|
30
30
|
type TapEnd = 'closed' | 'refused' | 'lost' | 'unreachable';
|
|
31
31
|
|
|
32
|
+
// Where the tap prints and how it exits: stdout, stderr, and the process by
|
|
33
|
+
// default.
|
|
34
|
+
interface BridgeTapIO {
|
|
35
|
+
readonly writeStdout: (text: string) => Promise<unknown>;
|
|
36
|
+
readonly printError: (line: string) => void;
|
|
37
|
+
readonly exit: (code: number) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PROCESS_IO: BridgeTapIO = {
|
|
41
|
+
writeStdout: (text) => Bun.write(Bun.stdout, text),
|
|
42
|
+
printError: (line) => {
|
|
43
|
+
console.error(line);
|
|
44
|
+
},
|
|
45
|
+
exit: (code) => {
|
|
46
|
+
process.exit(code);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
32
50
|
/**
|
|
33
51
|
* Streams the session's inbox to stdout from inside a remote host, through
|
|
34
52
|
* the session bridge at the given socket. It prints each message once as
|
|
@@ -40,21 +58,29 @@ type TapEnd = 'closed' | 'refused' | 'lost' | 'unreachable';
|
|
|
40
58
|
* outbox still holds. Exits 0 once the inbox closes and 1 when the bridge
|
|
41
59
|
* refuses the tap.
|
|
42
60
|
*/
|
|
43
|
-
export async function runBridgeTap(
|
|
61
|
+
export async function runBridgeTap(
|
|
62
|
+
socketPath: string,
|
|
63
|
+
outbox: string,
|
|
64
|
+
io: BridgeTapIO = PROCESS_IO,
|
|
65
|
+
): Promise<void> {
|
|
44
66
|
const seen = new Set<string>();
|
|
45
67
|
|
|
46
68
|
let wait = FIRST_RETRY_MS;
|
|
47
69
|
|
|
48
70
|
for (;;) {
|
|
49
|
-
const end = await runTapConnection(socketPath, outbox, seen);
|
|
71
|
+
const end = await runTapConnection(socketPath, outbox, seen, io);
|
|
50
72
|
|
|
51
73
|
if (end === 'closed') {
|
|
52
|
-
|
|
74
|
+
io.exit(0);
|
|
75
|
+
|
|
76
|
+
return;
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
if (end === 'refused') {
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
io.printError('atc tap: the session bridge refused the tap');
|
|
81
|
+
io.exit(1);
|
|
82
|
+
|
|
83
|
+
return;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
if (end === 'lost') {
|
|
@@ -73,6 +99,7 @@ async function runTapConnection(
|
|
|
73
99
|
|
|
74
100
|
// oxlint-disable-next-line prefer-readonly-parameter-types -- the ids the tap printed, which every connection adds to
|
|
75
101
|
seen: Set<string>,
|
|
102
|
+
io: BridgeTapIO,
|
|
76
103
|
): Promise<TapEnd> {
|
|
77
104
|
const ended = Promise.withResolvers<TapEnd>();
|
|
78
105
|
let opened = false;
|
|
@@ -139,9 +166,11 @@ async function runTapConnection(
|
|
|
139
166
|
const printed = `${JSON.stringify({ id: message.message, from: message.from, text: message.text, sentAt: message.sentAt })}\n`;
|
|
140
167
|
|
|
141
168
|
try {
|
|
142
|
-
await
|
|
169
|
+
await io.writeStdout(printed);
|
|
143
170
|
} catch {
|
|
144
|
-
|
|
171
|
+
io.exit(1);
|
|
172
|
+
|
|
173
|
+
return;
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
updateSeenIDs(seen, message.message);
|
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import { copyFileSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { buildMigratedConfig } from './build-migrated-config';
|
|
3
3
|
|
|
4
|
+
// Where the command's output goes, the console by default: text printed as
|
|
5
|
+
// it is, and lines printed to stdout or stderr.
|
|
6
|
+
interface ConfigMigrateIO {
|
|
7
|
+
readonly printText: (text: string) => void;
|
|
8
|
+
readonly print: (line: string) => void;
|
|
9
|
+
readonly printError: (line: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CONSOLE_IO: ConfigMigrateIO = {
|
|
13
|
+
printText: (text) => {
|
|
14
|
+
process.stdout.write(text);
|
|
15
|
+
},
|
|
16
|
+
print: (line) => {
|
|
17
|
+
console.log(line);
|
|
18
|
+
},
|
|
19
|
+
printError: (line) => {
|
|
20
|
+
console.error(line);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
4
24
|
/**
|
|
5
25
|
* Runs `atc config migrate`: prints the config.json rewritten around
|
|
6
26
|
* `agents`, or with `write` copies the file to a timestamped backup beside it
|
|
7
27
|
* and rewrites it in place. Returns the exit code. Notes and problems go to
|
|
8
28
|
* stderr and name keys and paths, never values.
|
|
9
29
|
*/
|
|
10
|
-
export function runConfigMigrate(
|
|
30
|
+
export function runConfigMigrate(
|
|
31
|
+
file: string,
|
|
32
|
+
write: boolean,
|
|
33
|
+
io: ConfigMigrateIO = CONSOLE_IO,
|
|
34
|
+
): number {
|
|
11
35
|
let text: string;
|
|
12
36
|
|
|
13
37
|
try {
|
|
14
38
|
text = readFileSync(file, 'utf8');
|
|
15
39
|
} catch {
|
|
16
|
-
|
|
40
|
+
io.printError(`atc config migrate: ${file} cannot be read`);
|
|
17
41
|
|
|
18
42
|
return 1;
|
|
19
43
|
}
|
|
@@ -23,7 +47,7 @@ export function runConfigMigrate(file: string, write: boolean): number {
|
|
|
23
47
|
try {
|
|
24
48
|
raw = JSON.parse(text);
|
|
25
49
|
} catch {
|
|
26
|
-
|
|
50
|
+
io.printError(`atc config migrate: ${file} is not valid JSON`);
|
|
27
51
|
|
|
28
52
|
return 1;
|
|
29
53
|
}
|
|
@@ -31,23 +55,23 @@ export function runConfigMigrate(file: string, write: boolean): number {
|
|
|
31
55
|
const result = buildMigratedConfig(raw);
|
|
32
56
|
|
|
33
57
|
if (result.kind === 'unusable') {
|
|
34
|
-
|
|
58
|
+
io.printError(`atc config migrate: ${file}: ${result.detail}`);
|
|
35
59
|
|
|
36
60
|
return 1;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
if (result.kind === 'current') {
|
|
40
|
-
|
|
64
|
+
io.print('config.json already uses agents; nothing to migrate');
|
|
41
65
|
|
|
42
66
|
return 0;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
for (const note of result.notes) {
|
|
46
|
-
|
|
70
|
+
io.printError(note);
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
if (!write) {
|
|
50
|
-
|
|
74
|
+
io.printText(result.text);
|
|
51
75
|
|
|
52
76
|
return 0;
|
|
53
77
|
}
|
|
@@ -57,7 +81,7 @@ export function runConfigMigrate(file: string, write: boolean): number {
|
|
|
57
81
|
try {
|
|
58
82
|
copyFileSync(file, backup);
|
|
59
83
|
} catch {
|
|
60
|
-
|
|
84
|
+
io.printError(`atc config migrate: ${backup} cannot be written; ${file} is unchanged`);
|
|
61
85
|
|
|
62
86
|
return 1;
|
|
63
87
|
}
|
|
@@ -65,13 +89,13 @@ export function runConfigMigrate(file: string, write: boolean): number {
|
|
|
65
89
|
try {
|
|
66
90
|
writeFileSync(file, result.text);
|
|
67
91
|
} catch {
|
|
68
|
-
|
|
92
|
+
io.printError(`atc config migrate: ${file} cannot be written; the backup is at ${backup}`);
|
|
69
93
|
|
|
70
94
|
return 1;
|
|
71
95
|
}
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
io.print(`backup: ${backup}`);
|
|
98
|
+
io.print(`wrote: ${file}`);
|
|
75
99
|
|
|
76
100
|
return 0;
|
|
77
101
|
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { defineCommand, renderUsage, runCommand } from 'citty';
|
|
3
|
+
import pkg from '../package.json';
|
|
4
|
+
import type { runClients } from './clients';
|
|
5
|
+
import { collectRedirectURIs } from './collect-redirect-uris';
|
|
6
|
+
import { parseGatewayStateDir } from './parse-gateway-state-dir';
|
|
7
|
+
import { parsePort } from './parse-port';
|
|
8
|
+
import type { runGateway } from './run-gateway';
|
|
9
|
+
|
|
10
|
+
// What the command line reaches outside itself, the process's own by
|
|
11
|
+
// default: the subcommands it hands a parsed command to, the console, and
|
|
12
|
+
// the exit.
|
|
13
|
+
interface GatewayCLIIO {
|
|
14
|
+
readonly runGateway: (...args: Readonly<Parameters<typeof runGateway>>) => Promise<void>;
|
|
15
|
+
readonly runClients: (...args: Readonly<Parameters<typeof runClients>>) => Promise<void>;
|
|
16
|
+
readonly print: (line: string) => void;
|
|
17
|
+
readonly printError: (line: string) => void;
|
|
18
|
+
readonly exit: (code: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PROCESS_IO: GatewayCLIIO = {
|
|
22
|
+
runGateway: async (...args) => {
|
|
23
|
+
const gateway = await import('./run-gateway');
|
|
24
|
+
|
|
25
|
+
await gateway.runGateway(...args);
|
|
26
|
+
},
|
|
27
|
+
runClients: async (...args) => {
|
|
28
|
+
const clients = await import('./clients');
|
|
29
|
+
|
|
30
|
+
await clients.runClients(...args);
|
|
31
|
+
},
|
|
32
|
+
print: (line) => {
|
|
33
|
+
console.log(line);
|
|
34
|
+
},
|
|
35
|
+
printError: (line) => {
|
|
36
|
+
console.error(line);
|
|
37
|
+
},
|
|
38
|
+
exit: (code) => {
|
|
39
|
+
process.exit(code);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Every flag a subcommand declares, so a flag in any position, or one no
|
|
44
|
+
// subcommand declares, is caught before a subcommand runs.
|
|
45
|
+
const GATEWAY_FLAGS = {
|
|
46
|
+
values: new Set(['host', 'port', 'public-url', 'registry', 'redirect-uri', 'state-dir']),
|
|
47
|
+
switches: new Set(['help', 'h', 'version']),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Runs the `atc-gateway` command line: reads the state directory from the
|
|
52
|
+
* whole of it, since the argument parser hands a subcommand only the
|
|
53
|
+
* arguments after its name, then parses the rest and runs the subcommand
|
|
54
|
+
* it names. A state directory that cannot be read prints the reason and
|
|
55
|
+
* exits 1 before any subcommand runs. `--help` anywhere prints the usage of
|
|
56
|
+
* the command it follows and exits 0, `--version` alone prints the version,
|
|
57
|
+
* and a command line the parser refuses prints that command's usage and
|
|
58
|
+
* the reason, then exits 1.
|
|
59
|
+
*/
|
|
60
|
+
export async function runGatewayCLI(
|
|
61
|
+
argv: readonly string[],
|
|
62
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
63
|
+
io: GatewayCLIIO = PROCESS_IO,
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const parsed = parseGatewayStateDir(argv, env, GATEWAY_FLAGS);
|
|
66
|
+
|
|
67
|
+
if (!parsed.ok) {
|
|
68
|
+
io.printError(`atc-gateway: ${parsed.message}`);
|
|
69
|
+
io.exit(1);
|
|
70
|
+
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const gateway = buildGatewayCommand(parsed.stateDir, io);
|
|
75
|
+
const renderNamedUsage = pickUsage(gateway.usages, argv);
|
|
76
|
+
|
|
77
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
78
|
+
const usage = await renderNamedUsage();
|
|
79
|
+
|
|
80
|
+
io.print(`${usage}\n`);
|
|
81
|
+
io.exit(0);
|
|
82
|
+
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (argv.length === 1 && argv[0] === '--version') {
|
|
87
|
+
io.print(pkg.version);
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await runCommand(gateway.command, { rawArgs: [...argv] });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (isUsageError(error)) {
|
|
96
|
+
const usage = await renderNamedUsage();
|
|
97
|
+
|
|
98
|
+
io.print(`${usage}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
|
|
103
|
+
io.printError(message);
|
|
104
|
+
io.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The usage renderer of the deepest command the command line names: each
|
|
110
|
+
* word past the flags and their values, joined to the ones before it, while
|
|
111
|
+
* the joined name has a usage.
|
|
112
|
+
*/
|
|
113
|
+
function pickUsage(
|
|
114
|
+
usages: Readonly<Record<string, () => Promise<string>>>,
|
|
115
|
+
argv: readonly string[],
|
|
116
|
+
): () => Promise<string> {
|
|
117
|
+
let name = '';
|
|
118
|
+
let index = 0;
|
|
119
|
+
|
|
120
|
+
while (index < argv.length) {
|
|
121
|
+
const token = argv[index] ?? '';
|
|
122
|
+
|
|
123
|
+
index += 1;
|
|
124
|
+
|
|
125
|
+
if (token.startsWith('-')) {
|
|
126
|
+
// A value flag without `=` takes the next token as its value.
|
|
127
|
+
if (!token.includes('=') && GATEWAY_FLAGS.values.has(token.replace(/^-+/u, ''))) {
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const longer = name === '' ? token : `${name} ${token}`;
|
|
135
|
+
|
|
136
|
+
if (usages[longer] === undefined) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
name = longer;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return usages[name] ?? usages[''] ?? (() => Promise.resolve(''));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The codes citty gives the errors it throws for a command line it refuses;
|
|
147
|
+
// citty exports no error class to test against.
|
|
148
|
+
const USAGE_ERROR_CODES: ReadonlySet<string> = new Set([
|
|
149
|
+
'EARG',
|
|
150
|
+
'E_UNKNOWN_COMMAND',
|
|
151
|
+
'E_NO_COMMAND',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
function isUsageError(error: unknown): boolean {
|
|
155
|
+
return (
|
|
156
|
+
error instanceof Error &&
|
|
157
|
+
'code' in error &&
|
|
158
|
+
typeof error.code === 'string' &&
|
|
159
|
+
USAGE_ERROR_CODES.has(error.code)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The flag every subcommand takes for the directory holding `gateway.db` and
|
|
164
|
+
// `mcp-auth.db`.
|
|
165
|
+
const STATE_DIR_ARG = {
|
|
166
|
+
'state-dir': {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'Directory for gateway.db and mcp-auth.db (default $ATC_GATEWAY_STATE_DIR)',
|
|
169
|
+
},
|
|
170
|
+
} as const;
|
|
171
|
+
|
|
172
|
+
const NO_STATE_DIR = 'atc-gateway: give --state-dir or set ATC_GATEWAY_STATE_DIR';
|
|
173
|
+
|
|
174
|
+
const ROOT_META = {
|
|
175
|
+
name: 'atc-gateway',
|
|
176
|
+
version: pkg.version,
|
|
177
|
+
description: 'Serve the atc MCP tools over HTTP for the daemons a registry lists',
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const CLIENTS_META = {
|
|
181
|
+
name: 'clients',
|
|
182
|
+
description: 'List, add, or remove the clients that may connect to the gateway',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The `atc-gateway` command tree, which serves the MCP tools over HTTP for
|
|
187
|
+
* the daemons a registry lists and manages the clients that may connect to
|
|
188
|
+
* it, and the usage of each command in it, keyed by the subcommand names
|
|
189
|
+
* that lead to it. Every subcommand takes the state directory read from
|
|
190
|
+
* the whole command line, or exits 1 without one.
|
|
191
|
+
*/
|
|
192
|
+
function buildGatewayCommand(stateDir: string | null, io: GatewayCLIIO) {
|
|
193
|
+
const serve = defineCommand({
|
|
194
|
+
meta: { name: 'serve', description: 'Serve MCP over HTTP behind OAuth' },
|
|
195
|
+
args: {
|
|
196
|
+
host: { type: 'string', description: 'Address to bind (default 127.0.0.1)' },
|
|
197
|
+
port: { type: 'string', description: 'Port to listen on (default 8414)' },
|
|
198
|
+
'public-url': {
|
|
199
|
+
type: 'string',
|
|
200
|
+
required: true,
|
|
201
|
+
description: 'Origin clients reach the gateway at, such as https://mcp.example.com',
|
|
202
|
+
},
|
|
203
|
+
registry: { type: 'string', required: true, description: 'The registry JSON file' },
|
|
204
|
+
...STATE_DIR_ARG,
|
|
205
|
+
},
|
|
206
|
+
async run(ctx) {
|
|
207
|
+
const port = ctx.args.port === undefined ? null : parsePort(ctx.args.port);
|
|
208
|
+
|
|
209
|
+
if (port !== null && !port.ok) {
|
|
210
|
+
io.printError(`atc-gateway: ${port.message}`);
|
|
211
|
+
io.exit(1);
|
|
212
|
+
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (stateDir === null) {
|
|
217
|
+
io.printError(NO_STATE_DIR);
|
|
218
|
+
io.exit(1);
|
|
219
|
+
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await io.runGateway(`atc-gateway/${pkg.version}`, {
|
|
224
|
+
host: ctx.args.host ?? '127.0.0.1',
|
|
225
|
+
port: port === null ? 8414 : port.port,
|
|
226
|
+
publicURL: ctx.args['public-url'],
|
|
227
|
+
registryPath: ctx.args.registry,
|
|
228
|
+
stateDir,
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const list = defineCommand({
|
|
234
|
+
meta: { name: 'list', description: 'List the clients', hidden: true },
|
|
235
|
+
args: STATE_DIR_ARG,
|
|
236
|
+
async run() {
|
|
237
|
+
if (stateDir === null) {
|
|
238
|
+
io.printError(NO_STATE_DIR);
|
|
239
|
+
io.exit(1);
|
|
240
|
+
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await io.runClients(
|
|
245
|
+
{ kind: 'list' },
|
|
246
|
+
{ dbPath: join(stateDir, 'mcp-auth.db'), command: 'atc-gateway clients' },
|
|
247
|
+
);
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const add = defineCommand({
|
|
252
|
+
meta: { name: 'add', description: 'Add a client and print its client ID' },
|
|
253
|
+
args: {
|
|
254
|
+
name: { type: 'positional', required: true, description: 'The client name' },
|
|
255
|
+
'redirect-uri': {
|
|
256
|
+
type: 'string',
|
|
257
|
+
required: true,
|
|
258
|
+
description: 'A redirect URI the client returns to; repeat for more',
|
|
259
|
+
},
|
|
260
|
+
...STATE_DIR_ARG,
|
|
261
|
+
},
|
|
262
|
+
async run(ctx) {
|
|
263
|
+
if (stateDir === null) {
|
|
264
|
+
io.printError(NO_STATE_DIR);
|
|
265
|
+
io.exit(1);
|
|
266
|
+
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await io.runClients(
|
|
271
|
+
{ kind: 'add', name: ctx.args.name, redirectURIs: collectRedirectURIs(ctx.rawArgs) },
|
|
272
|
+
{ dbPath: join(stateDir, 'mcp-auth.db'), command: 'atc-gateway clients' },
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const remove = defineCommand({
|
|
278
|
+
meta: { name: 'remove', description: 'Remove a client and revoke every grant it holds' },
|
|
279
|
+
args: {
|
|
280
|
+
id: { type: 'positional', required: true, description: 'The client ID' },
|
|
281
|
+
...STATE_DIR_ARG,
|
|
282
|
+
},
|
|
283
|
+
async run(ctx) {
|
|
284
|
+
if (stateDir === null) {
|
|
285
|
+
io.printError(NO_STATE_DIR);
|
|
286
|
+
io.exit(1);
|
|
287
|
+
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await io.runClients(
|
|
292
|
+
{ kind: 'remove', clientID: ctx.args.id },
|
|
293
|
+
{ dbPath: join(stateDir, 'mcp-auth.db'), command: 'atc-gateway clients' },
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const clients = defineCommand({
|
|
299
|
+
meta: CLIENTS_META,
|
|
300
|
+
args: STATE_DIR_ARG,
|
|
301
|
+
default: 'list',
|
|
302
|
+
subCommands: { list, add, remove },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const command = defineCommand({
|
|
306
|
+
meta: ROOT_META,
|
|
307
|
+
|
|
308
|
+
// Declared at every level, so the argument parser reads `--state-dir <dir>`
|
|
309
|
+
// before a subcommand as a flag and its value, not as a subcommand name.
|
|
310
|
+
args: STATE_DIR_ARG,
|
|
311
|
+
subCommands: { serve, clients },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Usage names a command after its parent, so each renders under a parent
|
|
315
|
+
// holding only that parent's name.
|
|
316
|
+
return {
|
|
317
|
+
command,
|
|
318
|
+
usages: {
|
|
319
|
+
'': () => renderUsage(command),
|
|
320
|
+
serve: () => renderUsage(serve, { meta: ROOT_META }),
|
|
321
|
+
clients: () => renderUsage(clients, { meta: ROOT_META }),
|
|
322
|
+
'clients list': () => renderUsage(list, { meta: CLIENTS_META }),
|
|
323
|
+
'clients add': () => renderUsage(add, { meta: CLIENTS_META }),
|
|
324
|
+
'clients remove': () => renderUsage(remove, { meta: CLIENTS_META }),
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
package/src/run-gateway.ts
CHANGED
|
@@ -14,6 +14,35 @@ interface GatewayFlags {
|
|
|
14
14
|
readonly stateDir: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// What the gateway reaches outside itself, the process's own by default:
|
|
18
|
+
// the environment its daemon tokens come from, the console, the exit, and
|
|
19
|
+
// the signal listeners that stop it.
|
|
20
|
+
interface GatewayIO {
|
|
21
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
22
|
+
readonly print: (line: string) => void;
|
|
23
|
+
readonly printError: (line: string) => void;
|
|
24
|
+
readonly exit: (code: number) => void;
|
|
25
|
+
readonly registerSignal: (signal: 'SIGINT' | 'SIGTERM', listener: () => Promise<void>) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PROCESS_IO: GatewayIO = {
|
|
29
|
+
env: process.env,
|
|
30
|
+
print: (line) => {
|
|
31
|
+
console.log(line);
|
|
32
|
+
},
|
|
33
|
+
printError: (line) => {
|
|
34
|
+
console.error(line);
|
|
35
|
+
},
|
|
36
|
+
exit: (code) => {
|
|
37
|
+
process.exit(code);
|
|
38
|
+
},
|
|
39
|
+
registerSignal: (signal, listener) => {
|
|
40
|
+
process.on(signal, () => {
|
|
41
|
+
void listener();
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
17
46
|
/**
|
|
18
47
|
* Runs `atc-gateway` in the foreground: serves the MCP tools over HTTP for
|
|
19
48
|
* the daemons the registry lists, routing every call to one of them over
|
|
@@ -23,15 +52,21 @@ interface GatewayFlags {
|
|
|
23
52
|
* registry that fails to load, or a server that fails to start, prints the
|
|
24
53
|
* reason and exits 1.
|
|
25
54
|
*/
|
|
26
|
-
export async function runGateway(
|
|
27
|
-
|
|
55
|
+
export async function runGateway(
|
|
56
|
+
build: string,
|
|
57
|
+
flags: GatewayFlags,
|
|
58
|
+
io: GatewayIO = PROCESS_IO,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const loaded = loadGatewayRegistry(flags.registryPath, io.env);
|
|
28
61
|
|
|
29
62
|
if (!loaded.ok) {
|
|
30
63
|
for (const error of loaded.errors) {
|
|
31
|
-
|
|
64
|
+
io.printError(`atc-gateway: ${error}`);
|
|
32
65
|
}
|
|
33
66
|
|
|
34
|
-
|
|
67
|
+
io.exit(1);
|
|
68
|
+
|
|
69
|
+
return;
|
|
35
70
|
}
|
|
36
71
|
|
|
37
72
|
mkdirSync(flags.stateDir, { recursive: true });
|
|
@@ -59,35 +94,33 @@ export async function runGateway(build: string, flags: GatewayFlags): Promise<vo
|
|
|
59
94
|
// The line carries a client's name, so control and format characters
|
|
60
95
|
// are dropped before it reaches the log.
|
|
61
96
|
printApproval: (line) => {
|
|
62
|
-
|
|
97
|
+
io.print(line.replaceAll(/[\p{Cc}\p{Cf}]/gu, ''));
|
|
63
98
|
},
|
|
64
99
|
|
|
65
100
|
// Request lines go to stderr, so stdout keeps the approval lines alone.
|
|
66
101
|
printRequest: (line) => {
|
|
67
|
-
|
|
102
|
+
io.printError(line);
|
|
68
103
|
},
|
|
69
104
|
});
|
|
70
105
|
} catch (error) {
|
|
71
106
|
await gateway.stop();
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
108
|
+
io.printError(`atc-gateway: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
io.exit(1);
|
|
76
110
|
|
|
77
|
-
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
78
113
|
|
|
114
|
+
// The handlers go in before the serving line, so a signal sent once the
|
|
115
|
+
// line appears always finds them.
|
|
79
116
|
const stopServing = async () => {
|
|
80
117
|
await server.stop();
|
|
81
118
|
await gateway.stop();
|
|
82
119
|
|
|
83
|
-
|
|
120
|
+
io.exit(0);
|
|
84
121
|
};
|
|
85
122
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
process.on('SIGTERM', () => {
|
|
91
|
-
void stopServing();
|
|
92
|
-
});
|
|
123
|
+
io.registerSignal('SIGINT', stopServing);
|
|
124
|
+
io.registerSignal('SIGTERM', stopServing);
|
|
125
|
+
io.print(`atc-gateway: serving ${server.origin}/mcp, listening on ${server.listening}`);
|
|
93
126
|
}
|
|
@@ -4,10 +4,14 @@ import { isCompiledBinary } from './is-compiled-binary';
|
|
|
4
4
|
/**
|
|
5
5
|
* The argv that runs this build of atc with `args`: the compiled binary is
|
|
6
6
|
* its own entry, and a source run is bun with the CLI entrypoint ahead of
|
|
7
|
-
* the arguments.
|
|
7
|
+
* the arguments. `compiled` tells the two apart, and defaults to how this
|
|
8
|
+
* process runs.
|
|
8
9
|
*/
|
|
9
|
-
export function buildATCCommand(
|
|
10
|
-
|
|
10
|
+
export function buildATCCommand(
|
|
11
|
+
args: readonly string[],
|
|
12
|
+
compiled: boolean = isCompiledBinary(),
|
|
13
|
+
): string[] {
|
|
14
|
+
return compiled
|
|
11
15
|
? [process.execPath, ...args]
|
|
12
16
|
: [process.execPath, join(import.meta.dir, '..', 'cli.ts'), ...args];
|
|
13
17
|
}
|
|
@@ -16,6 +16,12 @@ export class BunSqliteDriver implements Driver {
|
|
|
16
16
|
|
|
17
17
|
private releaseTurn: () => void = () => {};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* How many acquires are waiting for an earlier caller to release the
|
|
21
|
+
* connection.
|
|
22
|
+
*/
|
|
23
|
+
waiting = 0;
|
|
24
|
+
|
|
19
25
|
constructor(sqlite: Database) {
|
|
20
26
|
this.connection = buildConnection(sqlite);
|
|
21
27
|
}
|
|
@@ -32,8 +38,12 @@ export class BunSqliteDriver implements Driver {
|
|
|
32
38
|
release = resolve;
|
|
33
39
|
});
|
|
34
40
|
|
|
41
|
+
this.waiting += 1;
|
|
42
|
+
|
|
35
43
|
await previous;
|
|
36
44
|
|
|
45
|
+
this.waiting -= 1;
|
|
46
|
+
|
|
37
47
|
if (release !== undefined) {
|
|
38
48
|
this.releaseTurn = release;
|
|
39
49
|
}
|