@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/gateway.ts
CHANGED
|
@@ -1,183 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defineCommand, runMain } from 'citty';
|
|
3
|
-
import pkg from '../package.json';
|
|
4
|
-
import { collectRedirectURIs } from './collect-redirect-uris';
|
|
5
|
-
import { parseGatewayStateDir } from './parse-gateway-state-dir';
|
|
6
|
-
import { parsePort } from './parse-port';
|
|
7
|
-
|
|
8
|
-
// The flag every subcommand takes for the directory holding `gateway.db` and
|
|
9
|
-
// `mcp-auth.db`.
|
|
10
|
-
const STATE_DIR_ARG = {
|
|
11
|
-
'state-dir': {
|
|
12
|
-
type: 'string',
|
|
13
|
-
description: 'Directory for gateway.db and mcp-auth.db (default $ATC_GATEWAY_STATE_DIR)',
|
|
14
|
-
},
|
|
15
|
-
} as const;
|
|
16
|
-
|
|
17
|
-
// Every flag a subcommand declares, so a flag in any position, or one no
|
|
18
|
-
// subcommand declares, is caught before a subcommand runs.
|
|
19
|
-
const GATEWAY_FLAGS = {
|
|
20
|
-
values: new Set(['host', 'port', 'public-url', 'registry', 'redirect-uri', 'state-dir']),
|
|
21
|
-
switches: new Set(['help', 'h', 'version']),
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// The state directory comes from the whole command line, since the argument
|
|
25
|
-
// parser hands a subcommand only the arguments after its name.
|
|
26
|
-
const parsedStateDir = parseGatewayStateDir(process.argv.slice(2), process.env, GATEWAY_FLAGS);
|
|
27
|
-
|
|
28
|
-
if (!parsedStateDir.ok) {
|
|
29
|
-
console.error(`atc-gateway: ${parsedStateDir.message}`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const stateDir = parsedStateDir.stateDir;
|
|
34
|
-
const NO_STATE_DIR = 'atc-gateway: give --state-dir or set ATC_GATEWAY_STATE_DIR';
|
|
1
|
+
import { runGatewayCLI } from './run-gateway-cli';
|
|
35
2
|
|
|
36
3
|
// atc-gateway entry: serves the MCP tools over HTTP for the daemons a
|
|
37
4
|
// registry lists, and manages the clients that may connect to it. It loads
|
|
38
5
|
// nothing that starts a daemon or a session on this machine.
|
|
39
|
-
|
|
40
|
-
meta: {
|
|
41
|
-
name: 'atc-gateway',
|
|
42
|
-
version: pkg.version,
|
|
43
|
-
description: 'Serve the atc MCP tools over HTTP for the daemons a registry lists',
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
// Declared at every level, so the argument parser reads `--state-dir <dir>`
|
|
47
|
-
// before a subcommand as a flag and its value, not as a subcommand name.
|
|
48
|
-
args: STATE_DIR_ARG,
|
|
49
|
-
subCommands: {
|
|
50
|
-
serve: () =>
|
|
51
|
-
defineCommand({
|
|
52
|
-
meta: { name: 'serve', description: 'Serve MCP over HTTP behind OAuth' },
|
|
53
|
-
args: {
|
|
54
|
-
host: { type: 'string', description: 'Address to bind (default 127.0.0.1)' },
|
|
55
|
-
port: { type: 'string', description: 'Port to listen on (default 8414)' },
|
|
56
|
-
'public-url': {
|
|
57
|
-
type: 'string',
|
|
58
|
-
required: true,
|
|
59
|
-
description: 'Origin clients reach the gateway at, such as https://mcp.example.com',
|
|
60
|
-
},
|
|
61
|
-
registry: { type: 'string', required: true, description: 'The registry JSON file' },
|
|
62
|
-
...STATE_DIR_ARG,
|
|
63
|
-
},
|
|
64
|
-
async run(ctx) {
|
|
65
|
-
const port = ctx.args.port === undefined ? null : parsePort(ctx.args.port);
|
|
66
|
-
|
|
67
|
-
if (port !== null && !port.ok) {
|
|
68
|
-
console.error(`atc-gateway: ${port.message}`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (stateDir === null) {
|
|
73
|
-
console.error(NO_STATE_DIR);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const gateway = await import('./run-gateway');
|
|
78
|
-
|
|
79
|
-
await gateway.runGateway(`atc-gateway/${pkg.version}`, {
|
|
80
|
-
host: ctx.args.host ?? '127.0.0.1',
|
|
81
|
-
port: port === null ? 8414 : port.port,
|
|
82
|
-
publicURL: ctx.args['public-url'],
|
|
83
|
-
registryPath: ctx.args.registry,
|
|
84
|
-
stateDir,
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
}),
|
|
88
|
-
clients: () =>
|
|
89
|
-
defineCommand({
|
|
90
|
-
meta: {
|
|
91
|
-
name: 'clients',
|
|
92
|
-
description: 'List, add, or remove the clients that may connect to the gateway',
|
|
93
|
-
},
|
|
94
|
-
args: STATE_DIR_ARG,
|
|
95
|
-
default: 'list',
|
|
96
|
-
subCommands: {
|
|
97
|
-
list: () =>
|
|
98
|
-
defineCommand({
|
|
99
|
-
meta: { name: 'list', description: 'List the clients', hidden: true },
|
|
100
|
-
args: STATE_DIR_ARG,
|
|
101
|
-
async run() {
|
|
102
|
-
if (stateDir === null) {
|
|
103
|
-
console.error(NO_STATE_DIR);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const clients = await import('./clients');
|
|
108
|
-
|
|
109
|
-
await clients.runClients(
|
|
110
|
-
{ kind: 'list' },
|
|
111
|
-
{
|
|
112
|
-
dbPath: join(stateDir, 'mcp-auth.db'),
|
|
113
|
-
command: 'atc-gateway clients',
|
|
114
|
-
},
|
|
115
|
-
);
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
add: () =>
|
|
119
|
-
defineCommand({
|
|
120
|
-
meta: { name: 'add', description: 'Add a client and print its client ID' },
|
|
121
|
-
args: {
|
|
122
|
-
name: { type: 'positional', required: true, description: 'The client name' },
|
|
123
|
-
'redirect-uri': {
|
|
124
|
-
type: 'string',
|
|
125
|
-
required: true,
|
|
126
|
-
description: 'A redirect URI the client returns to; repeat for more',
|
|
127
|
-
},
|
|
128
|
-
...STATE_DIR_ARG,
|
|
129
|
-
},
|
|
130
|
-
async run(ctx) {
|
|
131
|
-
if (stateDir === null) {
|
|
132
|
-
console.error(NO_STATE_DIR);
|
|
133
|
-
process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const clients = await import('./clients');
|
|
137
|
-
|
|
138
|
-
await clients.runClients(
|
|
139
|
-
{
|
|
140
|
-
kind: 'add',
|
|
141
|
-
name: ctx.args.name,
|
|
142
|
-
redirectURIs: collectRedirectURIs(ctx.rawArgs),
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
dbPath: join(stateDir, 'mcp-auth.db'),
|
|
146
|
-
command: 'atc-gateway clients',
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
},
|
|
150
|
-
}),
|
|
151
|
-
remove: () =>
|
|
152
|
-
defineCommand({
|
|
153
|
-
meta: {
|
|
154
|
-
name: 'remove',
|
|
155
|
-
description: 'Remove a client and revoke every grant it holds',
|
|
156
|
-
},
|
|
157
|
-
args: {
|
|
158
|
-
id: { type: 'positional', required: true, description: 'The client ID' },
|
|
159
|
-
...STATE_DIR_ARG,
|
|
160
|
-
},
|
|
161
|
-
async run(ctx) {
|
|
162
|
-
if (stateDir === null) {
|
|
163
|
-
console.error(NO_STATE_DIR);
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const clients = await import('./clients');
|
|
168
|
-
|
|
169
|
-
await clients.runClients(
|
|
170
|
-
{ kind: 'remove', clientID: ctx.args.id },
|
|
171
|
-
{
|
|
172
|
-
dbPath: join(stateDir, 'mcp-auth.db'),
|
|
173
|
-
command: 'atc-gateway clients',
|
|
174
|
-
},
|
|
175
|
-
);
|
|
176
|
-
},
|
|
177
|
-
}),
|
|
178
|
-
},
|
|
179
|
-
}),
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
await runMain(main);
|
|
6
|
+
await runGatewayCLI(process.argv.slice(2), process.env);
|
package/src/grants.ts
CHANGED
|
@@ -1,33 +1,58 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import { collectGrants } from './mcp/collect-grants';
|
|
3
4
|
import { openMCPAuth } from './mcp/open-mcp-auth';
|
|
4
5
|
import { revokeGrant } from './mcp/revoke-grant';
|
|
5
|
-
import { mcpAuthDBFile
|
|
6
|
+
import { mcpAuthDBFile } from './shared/config';
|
|
7
|
+
|
|
8
|
+
// Where the command's lines and its exit code go: the console and the
|
|
9
|
+
// process by default.
|
|
10
|
+
interface GrantsIO {
|
|
11
|
+
readonly print: (line: string) => void;
|
|
12
|
+
readonly printError: (line: string) => void;
|
|
13
|
+
readonly setExitCode: (code: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PROCESS_IO: GrantsIO = {
|
|
17
|
+
print: (line) => {
|
|
18
|
+
console.log(line);
|
|
19
|
+
},
|
|
20
|
+
printError: (line) => {
|
|
21
|
+
console.error(line);
|
|
22
|
+
},
|
|
23
|
+
setExitCode: (code) => {
|
|
24
|
+
process.exitCode = code;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* Runs `atc grants`: lists the grants clients of `atc mcp --http` hold, or
|
|
9
30
|
* revokes one. A revoked grant's tokens stop working at once, and its client
|
|
10
31
|
* has to go through approval again. It opens the authorization server's
|
|
11
|
-
* database directly, so it works whether or not the server is
|
|
32
|
+
* database at `dbPath` directly, so it works whether or not the server is
|
|
33
|
+
* running. Revoking a grant that does not exist sets exit code 1.
|
|
12
34
|
*/
|
|
13
|
-
export async function runGrants(
|
|
14
|
-
|
|
35
|
+
export async function runGrants(
|
|
36
|
+
revoke: string | null,
|
|
37
|
+
dbPath: string = mcpAuthDBFile,
|
|
38
|
+
io: GrantsIO = PROCESS_IO,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
15
41
|
|
|
16
|
-
const store = await openMCPAuth({ dbPath
|
|
42
|
+
const store = await openMCPAuth({ dbPath, origin: null });
|
|
17
43
|
|
|
18
44
|
try {
|
|
19
45
|
if (revoke !== null) {
|
|
20
46
|
const revoked = await revokeGrant(store.db, revoke);
|
|
21
47
|
|
|
22
48
|
if (!revoked) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
process.exitCode = 1;
|
|
49
|
+
io.printError(`atc grants: no grant has the ID '${revoke}'`);
|
|
50
|
+
io.setExitCode(1);
|
|
26
51
|
|
|
27
52
|
return;
|
|
28
53
|
}
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
io.print(`Revoked grant ${revoke}`);
|
|
31
56
|
|
|
32
57
|
return;
|
|
33
58
|
}
|
|
@@ -35,13 +60,13 @@ export async function runGrants(revoke: string | null): Promise<void> {
|
|
|
35
60
|
const grants = await collectGrants(store.db);
|
|
36
61
|
|
|
37
62
|
if (grants.length === 0) {
|
|
38
|
-
|
|
63
|
+
io.print('No grants.');
|
|
39
64
|
|
|
40
65
|
return;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
for (const grant of grants) {
|
|
44
|
-
|
|
69
|
+
io.print(
|
|
45
70
|
`${grant.grantID} ${grant.clientName} ${grant.scopes.join(',')} last used ${grant.lastUsedAt ?? 'never'}`,
|
|
46
71
|
);
|
|
47
72
|
}
|
package/src/hook-report.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { normalizeHookEventName } from './agents/normalize-hook-event';
|
|
2
2
|
import { isRecord, sendReport } from './shared/report';
|
|
3
3
|
|
|
4
|
+
// Where the reporter reads its event and how it exits: stdin and the
|
|
5
|
+
// process by default.
|
|
6
|
+
interface HookReportIO {
|
|
7
|
+
readonly readStdin: () => Promise<string>;
|
|
8
|
+
readonly exit: (code: number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PROCESS_IO: HookReportIO = {
|
|
12
|
+
readStdin: () => new Response(Bun.stdin.stream()).text(),
|
|
13
|
+
exit: (code) => {
|
|
14
|
+
process.exit(code);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
4
18
|
/**
|
|
5
19
|
* Runs as a hook inside wrangled sessions. Reads the hook event from stdin
|
|
6
20
|
* (Claude snake_case keys or Grok camelCase keys) and forwards a PascalCase
|
|
@@ -9,12 +23,12 @@ import { isRecord, sendReport } from './shared/report';
|
|
|
9
23
|
* session's own. Always exits 0 so it never blocks the session it reports
|
|
10
24
|
* on.
|
|
11
25
|
*/
|
|
12
|
-
export async function runHookReport(agent: string): Promise<void> {
|
|
26
|
+
export async function runHookReport(agent: string, io: HookReportIO = PROCESS_IO): Promise<void> {
|
|
13
27
|
const sock = process.env['ATC_SOCKET'];
|
|
14
28
|
const atcId = process.env['ATC_SESSION_ID'];
|
|
15
29
|
|
|
16
30
|
if (sock !== undefined && sock !== '' && atcId !== undefined && atcId !== '') {
|
|
17
|
-
const raw = await
|
|
31
|
+
const raw = await io.readStdin();
|
|
18
32
|
|
|
19
33
|
let payload: Record<string, unknown> = {};
|
|
20
34
|
|
|
@@ -33,5 +47,5 @@ export async function runHookReport(agent: string): Promise<void> {
|
|
|
33
47
|
await sendReport(sock, line, 2000);
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
io.exit(0);
|
|
37
51
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isRecord } from '../shared/report';
|
|
2
|
+
import { systemClock } from '../shared/system-clock';
|
|
3
|
+
import type { Clock } from '../shared/system-clock';
|
|
2
4
|
import type { FleetCaller } from './types';
|
|
3
5
|
|
|
4
6
|
// The most report text one events page carries, in UTF-8 bytes: as much as
|
|
@@ -26,33 +28,34 @@ type ReportRead = Readonly<Record<string, unknown>>;
|
|
|
26
28
|
* deadline: it then holds the cursor of the last event it keeps and `more`
|
|
27
29
|
* true, so the next read starts at that report. Every other field of the
|
|
28
30
|
* page, such as a gateway's `unavailable` and `truncated`, passes through
|
|
29
|
-
* unchanged.
|
|
31
|
+
* unchanged. The deadline runs on `clock`, the wall clock by default.
|
|
30
32
|
*/
|
|
31
33
|
export async function readReportTexts(
|
|
32
34
|
caller: FleetCaller,
|
|
33
35
|
page: Readonly<Record<string, unknown>>,
|
|
34
36
|
deadlineMs: number = REPORT_READ_DEADLINE_MS,
|
|
37
|
+
clock: Clock = systemClock,
|
|
35
38
|
): Promise<Readonly<Record<string, unknown>>> {
|
|
36
39
|
const raw: unknown = page['events'];
|
|
37
40
|
const events = Array.isArray(raw) ? raw.filter((event) => isRecord(event)) : [];
|
|
38
|
-
const endsAt =
|
|
41
|
+
const endsAt = clock.now() + deadlineMs;
|
|
39
42
|
const timeout = Promise.withResolvers<ReportRead>();
|
|
40
43
|
|
|
41
|
-
const
|
|
44
|
+
const cancel = clock.schedule(() => {
|
|
42
45
|
timeout.resolve({
|
|
43
46
|
textError: `timeout: the report text did not arrive within ${deadlineMs} ms`,
|
|
44
47
|
});
|
|
45
48
|
}, deadlineMs);
|
|
46
49
|
|
|
47
|
-
const read = await readPageReports(caller, page, events, endsAt, timeout.promise);
|
|
50
|
+
const read = await readPageReports(caller, page, events, endsAt, timeout.promise, clock);
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
cancel();
|
|
50
53
|
|
|
51
54
|
return read;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
// The page with its reports read, one at a time, until the budget or the
|
|
55
|
-
// deadline at `endsAt` stops it. A read still out when `timeout` settles
|
|
58
|
+
// deadline at `endsAt` on `clock` stops it. A read still out when `timeout` settles
|
|
56
59
|
// gives way to the timeout it settles with.
|
|
57
60
|
async function readPageReports(
|
|
58
61
|
caller: FleetCaller,
|
|
@@ -60,6 +63,7 @@ async function readPageReports(
|
|
|
60
63
|
events: readonly Readonly<Record<string, unknown>>[],
|
|
61
64
|
endsAt: number,
|
|
62
65
|
timeout: Readonly<Promise<ReportRead>>,
|
|
66
|
+
clock: Clock,
|
|
63
67
|
): Promise<Readonly<Record<string, unknown>>> {
|
|
64
68
|
const kept: Readonly<Record<string, unknown>>[] = [];
|
|
65
69
|
let used = 0;
|
|
@@ -72,7 +76,7 @@ async function readPageReports(
|
|
|
72
76
|
|
|
73
77
|
const last = kept.at(-1);
|
|
74
78
|
|
|
75
|
-
if (last !== undefined &&
|
|
79
|
+
if (last !== undefined && clock.now() >= endsAt) {
|
|
76
80
|
return { ...page, events: kept, cursor: last['cursor'], more: true };
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -38,6 +38,10 @@ interface MCPHTTPServerOptions {
|
|
|
38
38
|
|
|
39
39
|
// Serves `/healthz` and `/readyz` for an orchestrator's probes.
|
|
40
40
|
readonly probes?: boolean;
|
|
41
|
+
|
|
42
|
+
// The time pending approvals and the limit on approval starts run on, in
|
|
43
|
+
// epoch milliseconds; the wall clock when unset.
|
|
44
|
+
readonly now?: () => number;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
@@ -171,7 +175,7 @@ export async function startMCPHTTPServer(options: MCPHTTPServerOptions): Promise
|
|
|
171
175
|
origin,
|
|
172
176
|
resource: `${origin}/mcp`,
|
|
173
177
|
store,
|
|
174
|
-
approvals: new ApprovalState(600_000, () => Date.now()),
|
|
178
|
+
approvals: new ApprovalState(600_000, options.now ?? (() => Date.now())),
|
|
175
179
|
printApproval: options.printApproval,
|
|
176
180
|
},
|
|
177
181
|
};
|
package/src/mcp-http-server.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import { bootDaemonClient } from './client/boot-daemon';
|
|
4
|
+
import type { DaemonBoot, DaemonPaths } from './client/boot-daemon';
|
|
3
5
|
import { DaemonClient } from './client/daemon-client';
|
|
4
6
|
import { collectClients } from './mcp/collect-clients';
|
|
5
7
|
import { openMCPAuth } from './mcp/open-mcp-auth';
|
|
6
8
|
import { ReconnectingCaller } from './mcp/reconnecting-caller';
|
|
7
9
|
import { startMCPHTTPServer } from './mcp/start-mcp-http-server';
|
|
8
|
-
import {
|
|
10
|
+
import type { MCPHTTPConfig } from './shared/collect-mcp-http-config';
|
|
11
|
+
import { mcpAuthDBFile } from './shared/config';
|
|
9
12
|
import { loadMCPHTTPConfig } from './shared/load-mcp-http-config';
|
|
10
13
|
|
|
11
14
|
interface MCPHTTPFlags {
|
|
@@ -15,6 +18,41 @@ interface MCPHTTPFlags {
|
|
|
15
18
|
readonly waitForDaemon: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
// What the server reaches outside itself, the process's own by default: its
|
|
22
|
+
// config, the authorization database, the files that locate the daemon it
|
|
23
|
+
// boots or waits for (null for the process's own, the only ones at which it
|
|
24
|
+
// starts a daemon), the console, the exit, and the signal listeners that
|
|
25
|
+
// stop it.
|
|
26
|
+
interface MCPHTTPServerIO {
|
|
27
|
+
readonly loadConfig: () => MCPHTTPConfig;
|
|
28
|
+
readonly dbPath: string;
|
|
29
|
+
readonly daemonPaths: DaemonPaths | null;
|
|
30
|
+
readonly print: (line: string) => void;
|
|
31
|
+
readonly printError: (line: string) => void;
|
|
32
|
+
readonly exit: (code: number) => void;
|
|
33
|
+
readonly registerSignal: (signal: 'SIGINT' | 'SIGTERM', listener: () => Promise<void>) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PROCESS_IO: MCPHTTPServerIO = {
|
|
37
|
+
loadConfig: loadMCPHTTPConfig,
|
|
38
|
+
dbPath: mcpAuthDBFile,
|
|
39
|
+
daemonPaths: null,
|
|
40
|
+
print: (line) => {
|
|
41
|
+
console.log(line);
|
|
42
|
+
},
|
|
43
|
+
printError: (line) => {
|
|
44
|
+
console.error(line);
|
|
45
|
+
},
|
|
46
|
+
exit: (code) => {
|
|
47
|
+
process.exit(code);
|
|
48
|
+
},
|
|
49
|
+
registerSignal: (signal, listener) => {
|
|
50
|
+
process.on(signal, () => {
|
|
51
|
+
void listener();
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
18
56
|
// How long `--wait-for-daemon` waits before it exits. A service manager's
|
|
19
57
|
// restart covers a daemon that takes longer.
|
|
20
58
|
const DAEMON_WAIT_MS = 30_000;
|
|
@@ -25,19 +63,41 @@ const DAEMON_WAIT_MS = 30_000;
|
|
|
25
63
|
* serves MCP over HTTP until Ctrl-C. Approval codes print here, so the
|
|
26
64
|
* terminal running it is where the operator approves a client.
|
|
27
65
|
*/
|
|
28
|
-
export async function runMCPHTTPServer(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
export async function runMCPHTTPServer(
|
|
67
|
+
build: string,
|
|
68
|
+
flags: MCPHTTPFlags,
|
|
69
|
+
io: MCPHTTPServerIO = PROCESS_IO,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const config = io.loadConfig();
|
|
72
|
+
|
|
73
|
+
const waitOptions = flags.waitForDaemon
|
|
74
|
+
? {
|
|
75
|
+
waitForDaemonMs: DAEMON_WAIT_MS,
|
|
76
|
+
onWaitForDaemon: () => {
|
|
77
|
+
io.printError(
|
|
78
|
+
`atc mcp --http: no daemon answers yet; waiting up to ${DAEMON_WAIT_MS / 1000}s for one, without starting it`,
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
: {};
|
|
83
|
+
|
|
84
|
+
const bootOptions =
|
|
85
|
+
io.daemonPaths === null ? waitOptions : { ...waitOptions, paths: io.daemonPaths };
|
|
86
|
+
|
|
87
|
+
let boot: DaemonBoot;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
boot = await bootDaemonClient(bootOptions);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
io.printError(`atc mcp --http: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
io.exit(1);
|
|
94
|
+
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
37
97
|
|
|
38
98
|
boot.client.stop();
|
|
39
99
|
|
|
40
|
-
mkdirSync(
|
|
100
|
+
mkdirSync(dirname(io.dbPath), { recursive: true });
|
|
41
101
|
|
|
42
102
|
const caller = new ReconnectingCaller(boot.socketPath, build, (path) => DaemonClient.open(path));
|
|
43
103
|
|
|
@@ -48,45 +108,41 @@ export async function runMCPHTTPServer(build: string, flags: MCPHTTPFlags): Prom
|
|
|
48
108
|
port: flags.port ?? config.port,
|
|
49
109
|
publicURL: flags.publicURL ?? config.publicURL,
|
|
50
110
|
allowedHosts: config.allowedHosts,
|
|
51
|
-
dbPath:
|
|
111
|
+
dbPath: io.dbPath,
|
|
52
112
|
|
|
53
113
|
// The line carries a client's name, so control and format characters
|
|
54
114
|
// are dropped before it reaches the operator's terminal.
|
|
55
115
|
printApproval: (line) => {
|
|
56
|
-
|
|
116
|
+
io.print(line.replaceAll(/[\p{Cc}\p{Cf}]/gu, ''));
|
|
57
117
|
},
|
|
58
118
|
|
|
59
119
|
// Request lines go to stderr, so stdout keeps the approval lines alone.
|
|
60
120
|
printRequest: (line) => {
|
|
61
|
-
|
|
121
|
+
io.printError(line);
|
|
62
122
|
},
|
|
63
123
|
});
|
|
64
124
|
|
|
65
|
-
|
|
125
|
+
// The handlers go in before the serving line, so a signal sent once the
|
|
126
|
+
// line appears always finds them.
|
|
127
|
+
const stopServing = async () => {
|
|
128
|
+
await server.stop();
|
|
129
|
+
await caller.stop();
|
|
130
|
+
|
|
131
|
+
io.exit(0);
|
|
132
|
+
};
|
|
66
133
|
|
|
67
|
-
|
|
134
|
+
io.registerSignal('SIGINT', stopServing);
|
|
135
|
+
io.registerSignal('SIGTERM', stopServing);
|
|
136
|
+
io.print(`atc mcp --http: serving ${server.origin}/mcp, listening on ${server.listening}`);
|
|
137
|
+
|
|
138
|
+
const admin = await openMCPAuth({ dbPath: io.dbPath, origin: null });
|
|
68
139
|
const clients = await collectClients(admin.db);
|
|
69
140
|
|
|
70
141
|
await admin.close();
|
|
71
142
|
|
|
72
143
|
if (clients.length === 0) {
|
|
73
|
-
|
|
144
|
+
io.print(
|
|
74
145
|
'No clients can connect yet. Add one with: atc clients add <name> --redirect-uri <uri>',
|
|
75
146
|
);
|
|
76
147
|
}
|
|
77
|
-
|
|
78
|
-
const stopServing = async () => {
|
|
79
|
-
await server.stop();
|
|
80
|
-
await caller.stop();
|
|
81
|
-
|
|
82
|
-
process.exit(0);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
process.on('SIGINT', () => {
|
|
86
|
-
void stopServing();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
process.on('SIGTERM', () => {
|
|
90
|
-
void stopServing();
|
|
91
|
-
});
|
|
92
148
|
}
|
package/src/report.ts
CHANGED
|
@@ -16,6 +16,20 @@ interface ReportOptions {
|
|
|
16
16
|
readonly turn: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Where the reporter reads its text and how it exits: stdin and the process
|
|
20
|
+
// by default.
|
|
21
|
+
interface ReportIO {
|
|
22
|
+
readonly readStdin: () => Promise<string>;
|
|
23
|
+
readonly exit: (code: number) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PROCESS_IO: ReportIO = {
|
|
27
|
+
readStdin: () => new Response(Bun.stdin.stream()).text(),
|
|
28
|
+
exit: (code) => {
|
|
29
|
+
process.exit(code);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* Runs inside wrangled sessions: reads stdin verbatim and forwards it to the
|
|
21
35
|
* atc socket as a Report envelope of the given kind. An `answered` report
|
|
@@ -26,7 +40,11 @@ interface ReportOptions {
|
|
|
26
40
|
* given. Inside a remote host it goes to the session bridge instead. Always
|
|
27
41
|
* exits 0 so it never blocks the session it reports on.
|
|
28
42
|
*/
|
|
29
|
-
export async function runReport(
|
|
43
|
+
export async function runReport(
|
|
44
|
+
kind: string,
|
|
45
|
+
options: ReportOptions,
|
|
46
|
+
io: ReportIO = PROCESS_IO,
|
|
47
|
+
): Promise<void> {
|
|
30
48
|
try {
|
|
31
49
|
const sock = process.env['ATC_SOCKET'];
|
|
32
50
|
const atcId = process.env['ATC_SESSION_ID'];
|
|
@@ -38,7 +56,7 @@ export async function runReport(kind: string, options: ReportOptions): Promise<v
|
|
|
38
56
|
atcId !== '' &&
|
|
39
57
|
REPORT_KINDS.some((known) => known === kind)
|
|
40
58
|
) {
|
|
41
|
-
const stdin = await
|
|
59
|
+
const stdin = await io.readStdin();
|
|
42
60
|
|
|
43
61
|
const payload = buildReportPayload(kind, options, stdin);
|
|
44
62
|
|
|
@@ -52,7 +70,7 @@ export async function runReport(kind: string, options: ReportOptions): Promise<v
|
|
|
52
70
|
}
|
|
53
71
|
} catch {}
|
|
54
72
|
|
|
55
|
-
|
|
73
|
+
io.exit(0);
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
// Inside a remote host, a report goes to the session bridge under an id of
|