@zgeoff/atc 2.23.0 → 2.25.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 +2 -1
- package/src/agents/agent-adapter.ts +27 -2
- package/src/agents/gateway-adapter.ts +21 -3
- package/src/cli.ts +20 -8
- package/src/client/collect-target-picks.ts +5 -0
- package/src/client/spawn-picker.ts +15 -1
- package/src/clients.ts +19 -12
- package/src/daemon/broker-auth-host.ts +25 -0
- package/src/daemon/broker-authority-error.ts +2 -1
- package/src/daemon/build-agent-list.ts +16 -3
- package/src/daemon/build-scoped-context.ts +2 -0
- package/src/daemon/build-target-list.ts +5 -0
- package/src/daemon/daemon-connection.ts +52 -2
- package/src/daemon/daemon-context.ts +12 -0
- package/src/daemon/daemon.ts +58 -1
- package/src/daemon/execution-provider.ts +50 -2
- package/src/daemon/imp-client-port.ts +71 -23
- package/src/daemon/imp-harness.ts +299 -18
- package/src/daemon/imp-port.ts +23 -1
- package/src/daemon/imp-provider.ts +161 -40
- package/src/daemon/local-pty-provider.ts +11 -0
- package/src/daemon/restore-fleet.ts +15 -1
- package/src/daemon/runtime-auth-binder.ts +1086 -0
- package/src/daemon/sessions.ts +565 -25
- package/src/daemon/verify-broker-authority.ts +10 -5
- package/src/gateway.ts +183 -0
- package/src/mcp/mcp-tools.ts +2 -0
- package/src/mcp/require-daemon-features.ts +1 -0
- package/src/parse-gateway-state-dir.ts +94 -0
- package/src/protocol/daemon-features.ts +4 -0
- package/src/protocol/protocol.ts +16 -0
- package/src/protocol/request-param-schemas.ts +5 -0
- package/src/run-gateway.ts +93 -0
- package/src/shared/collect-gateways.ts +1 -22
- package/src/shared/is-broker-variable.ts +24 -0
- package/src/store/state-store.ts +11 -0
|
@@ -15,8 +15,9 @@ export interface BrokerActivation {
|
|
|
15
15
|
/**
|
|
16
16
|
* The gate before atc grants or relies on a brokered credential. It reads
|
|
17
17
|
* impd's features, then the token's identity, and writes nothing, so a
|
|
18
|
-
* refusal leaves impd as it was. It rejects unless impd has
|
|
19
|
-
* tokens
|
|
18
|
+
* refusal leaves impd as it was. It rejects unless impd has grantable
|
|
19
|
+
* tokens, secret rebinds and exec requirements, so a start can refuse to
|
|
20
|
+
* run without a ready broker, the token may manage each imp and reaches no
|
|
20
21
|
* imp outside the namespace whose imp names start with the prefix, and the
|
|
21
22
|
* token may grant every bound secret.
|
|
22
23
|
* Whether each secret's rules match the binding is a separate comparison.
|
|
@@ -28,11 +29,15 @@ export async function verifyBrokerAuthority(
|
|
|
28
29
|
): Promise<void> {
|
|
29
30
|
const features = await port.readFeatures();
|
|
30
31
|
|
|
31
|
-
if (!features.grantableTokens || !features.secretRebind) {
|
|
32
|
+
if (!features.grantableTokens || !features.secretRebind || !features.execRequire) {
|
|
32
33
|
throw new BrokerAuthorityError(
|
|
33
34
|
'auth_impd_too_old',
|
|
34
|
-
'impd lacks grantable tokens or
|
|
35
|
-
{
|
|
35
|
+
'impd lacks grantable tokens, secret rebinds or exec requirements; it must be 0.30.0 or later',
|
|
36
|
+
{
|
|
37
|
+
grantableTokens: features.grantableTokens,
|
|
38
|
+
secretRebind: features.secretRebind,
|
|
39
|
+
execRequire: features.execRequire,
|
|
40
|
+
},
|
|
36
41
|
);
|
|
37
42
|
}
|
|
38
43
|
|
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
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';
|
|
35
|
+
|
|
36
|
+
// atc-gateway entry: serves the MCP tools over HTTP for the daemons a
|
|
37
|
+
// registry lists, and manages the clients that may connect to it. It loads
|
|
38
|
+
// nothing that starts a daemon or a session on this machine.
|
|
39
|
+
const main = defineCommand({
|
|
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);
|
package/src/mcp/mcp-tools.ts
CHANGED
|
@@ -213,6 +213,7 @@ const AGENTS_OUTPUT: Readonly<Record<string, unknown>> = {
|
|
|
213
213
|
label: { type: 'string' },
|
|
214
214
|
kind: { type: 'string' },
|
|
215
215
|
installed: { type: 'boolean' },
|
|
216
|
+
brokerAuth: { type: 'boolean' },
|
|
216
217
|
capabilities: {
|
|
217
218
|
type: 'object',
|
|
218
219
|
properties: {
|
|
@@ -249,6 +250,7 @@ const AGENTS_OUTPUT: Readonly<Record<string, unknown>> = {
|
|
|
249
250
|
type: 'object',
|
|
250
251
|
additionalProperties: { type: 'boolean' },
|
|
251
252
|
},
|
|
253
|
+
brokerAuth: { type: 'boolean' },
|
|
252
254
|
},
|
|
253
255
|
required: ['id', 'provider', 'identity', 'available', 'default', 'capabilities'],
|
|
254
256
|
},
|
|
@@ -23,6 +23,7 @@ const FEATURE_USES: Readonly<Record<DaemonFeature, string>> = {
|
|
|
23
23
|
'git.probe': 'git.probe',
|
|
24
24
|
'transport.tcp': 'a TCP connection to the daemon',
|
|
25
25
|
'idempotency.replayOnly': 'a resend that only replays a held idempotency key',
|
|
26
|
+
'session.auth': 'session.auth.revoke and session.auth.rebind',
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flags `atc-gateway` accepts anywhere on its command line: those that
|
|
3
|
+
* take a value, and switches that take none. Names are written without
|
|
4
|
+
* their leading dashes.
|
|
5
|
+
*/
|
|
6
|
+
export interface GatewayFlags {
|
|
7
|
+
readonly values: ReadonlySet<string>;
|
|
8
|
+
readonly switches: ReadonlySet<string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ParsedStateDir =
|
|
12
|
+
| { readonly ok: true; readonly stateDir: string | null }
|
|
13
|
+
| { readonly ok: false; readonly message: string };
|
|
14
|
+
|
|
15
|
+
// A flag: one or two dashes, a name, and an optional `=<value>`. A lone `-`
|
|
16
|
+
// is a positional.
|
|
17
|
+
const FLAG_PATTERN = /^--?(?<name>[^=]+)(?:=(?<value>[\s\S]*))?$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Finds the state directory in the whole `atc-gateway` command line, before,
|
|
21
|
+
* between, or after its subcommands, so no position can drop the flag.
|
|
22
|
+
* `--state-dir <dir>` and `--state-dir=<dir>` count; a value after `--` or
|
|
23
|
+
* held by another flag never does. Every `--state-dir` must give the same
|
|
24
|
+
* directory. The flag wins over `ATC_GATEWAY_STATE_DIR`, and with neither
|
|
25
|
+
* the result is null. A flag the gateway does not know, a switch given a
|
|
26
|
+
* value, and a flag whose value is missing or starts with `-` are refused,
|
|
27
|
+
* since reading them either way could pick the wrong directory.
|
|
28
|
+
*/
|
|
29
|
+
export function parseGatewayStateDir(
|
|
30
|
+
argv: readonly string[],
|
|
31
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
32
|
+
flags: GatewayFlags,
|
|
33
|
+
): ParsedStateDir {
|
|
34
|
+
const given: string[] = [];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const token = argv[i] ?? '';
|
|
38
|
+
|
|
39
|
+
if (token === '--') {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const match = FLAG_PATTERN.exec(token);
|
|
44
|
+
|
|
45
|
+
if (match === null) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const name = match.groups?.['name'] ?? '';
|
|
50
|
+
const inline = match.groups?.['value'];
|
|
51
|
+
|
|
52
|
+
if (flags.switches.has(name)) {
|
|
53
|
+
if (inline !== undefined) {
|
|
54
|
+
return { ok: false, message: `${token} takes no value` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!flags.values.has(name)) {
|
|
61
|
+
return { ok: false, message: `unknown flag '${token}'` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const value = inline ?? argv[i + 1];
|
|
65
|
+
|
|
66
|
+
if (inline === undefined) {
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (value === undefined || value === '' || (inline === undefined && value.startsWith('-'))) {
|
|
71
|
+
return { ok: false, message: `--${name} needs a value; write --${name}=<value>` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (name === 'state-dir') {
|
|
75
|
+
given.push(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const distinct = [...new Set(given)];
|
|
80
|
+
|
|
81
|
+
if (distinct.length > 1) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: `--state-dir gives different directories: ${distinct.map((dir) => `'${dir}'`).join(', ')}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fromEnv = env['ATC_GATEWAY_STATE_DIR'];
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
stateDir: distinct[0] ?? (fromEnv === undefined || fromEnv === '' ? null : fromEnv),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -73,6 +73,10 @@ export const DAEMON_FEATURES = [
|
|
|
73
73
|
// replays a key the daemon holds and refuses one it does not hold with
|
|
74
74
|
// `idempotency_key_unknown`, running nothing.
|
|
75
75
|
'idempotency.replayOnly',
|
|
76
|
+
|
|
77
|
+
// `session.auth.revoke` and `session.auth.rebind` exist, open to the
|
|
78
|
+
// daemon's owner only.
|
|
79
|
+
'session.auth',
|
|
76
80
|
] as const;
|
|
77
81
|
|
|
78
82
|
export type DaemonFeature = (typeof DAEMON_FEATURES)[number];
|
package/src/protocol/protocol.ts
CHANGED
|
@@ -49,6 +49,22 @@ const ERROR_CODES = [
|
|
|
49
49
|
'host_unavailable',
|
|
50
50
|
'auth_not_configured',
|
|
51
51
|
'auth_target_unsupported',
|
|
52
|
+
'auth_impd_too_old',
|
|
53
|
+
'auth_token_scope',
|
|
54
|
+
'auth_token_too_broad',
|
|
55
|
+
'auth_imp_out_of_scope',
|
|
56
|
+
'auth_secret_not_grantable',
|
|
57
|
+
'auth_secret_mismatch',
|
|
58
|
+
'auth_runtime_mismatch',
|
|
59
|
+
'auth_runtime_exists',
|
|
60
|
+
'auth_grant_missing',
|
|
61
|
+
'auth_grants_mismatch',
|
|
62
|
+
'auth_rebind_required',
|
|
63
|
+
'auth_blocked',
|
|
64
|
+
'auth_binding_mismatch',
|
|
65
|
+
'auth_binding_invalid',
|
|
66
|
+
'auth_revocation_pending',
|
|
67
|
+
'broker_not_ready',
|
|
52
68
|
'host_leased',
|
|
53
69
|
'confirmation_required',
|
|
54
70
|
'confirm_token_invalid',
|
|
@@ -204,6 +204,11 @@ export const REQUEST_PARAM_SCHEMAS = {
|
|
|
204
204
|
.min(1, 'session.forget confirmToken must not be empty')
|
|
205
205
|
.optional(),
|
|
206
206
|
}),
|
|
207
|
+
|
|
208
|
+
// Owner-only: withdraw the grants of the runtime auth binding on the
|
|
209
|
+
// session's host, or bind that host to its agent's current selection.
|
|
210
|
+
'session.auth.revoke': SESSION_DEFAULTED,
|
|
211
|
+
'session.auth.rebind': SESSION_DEFAULTED,
|
|
207
212
|
'session.update': SESSION_DEFAULTED.extend({
|
|
208
213
|
name: buildOptionalString(),
|
|
209
214
|
pinned: buildOptionalBoolean(),
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DaemonClient } from './client/daemon-client';
|
|
4
|
+
import { loadGatewayRegistry } from './federation/load-gateway-registry';
|
|
5
|
+
import { openGatewayCaller } from './federation/open-gateway-caller';
|
|
6
|
+
import { startMCPHTTPServer } from './mcp/start-mcp-http-server';
|
|
7
|
+
import type { MCPHTTPServer } from './mcp/start-mcp-http-server';
|
|
8
|
+
|
|
9
|
+
interface GatewayFlags {
|
|
10
|
+
readonly host: string;
|
|
11
|
+
readonly port: number;
|
|
12
|
+
readonly publicURL: string;
|
|
13
|
+
readonly registryPath: string;
|
|
14
|
+
readonly stateDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs `atc-gateway` in the foreground: serves the MCP tools over HTTP for
|
|
19
|
+
* the daemons the registry lists, routing every call to one of them over
|
|
20
|
+
* TCP, until SIGINT or SIGTERM. It never starts or reaches a local daemon.
|
|
21
|
+
* The keyed-request bindings (`gateway.db`) and the authorization server
|
|
22
|
+
* (`mcp-auth.db`) live in the state directory, the only place it writes. A
|
|
23
|
+
* registry that fails to load, or a server that fails to start, prints the
|
|
24
|
+
* reason and exits 1.
|
|
25
|
+
*/
|
|
26
|
+
export async function runGateway(build: string, flags: GatewayFlags): Promise<void> {
|
|
27
|
+
const loaded = loadGatewayRegistry(flags.registryPath, process.env);
|
|
28
|
+
|
|
29
|
+
if (!loaded.ok) {
|
|
30
|
+
for (const error of loaded.errors) {
|
|
31
|
+
console.error(`atc-gateway: ${error}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
mkdirSync(flags.stateDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const gateway = openGatewayCaller({
|
|
40
|
+
registry: loaded.registry,
|
|
41
|
+
build,
|
|
42
|
+
openChannel: (address) => DaemonClient.open({ hostname: address.host, port: address.port }),
|
|
43
|
+
gatewayDBPath: join(flags.stateDir, 'gateway.db'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let server: MCPHTTPServer;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
server = await startMCPHTTPServer({
|
|
50
|
+
caller: gateway.caller,
|
|
51
|
+
build,
|
|
52
|
+
host: flags.host,
|
|
53
|
+
port: flags.port,
|
|
54
|
+
publicURL: flags.publicURL,
|
|
55
|
+
allowedHosts: [],
|
|
56
|
+
dbPath: join(flags.stateDir, 'mcp-auth.db'),
|
|
57
|
+
probes: true,
|
|
58
|
+
|
|
59
|
+
// The line carries a client's name, so control and format characters
|
|
60
|
+
// are dropped before it reaches the log.
|
|
61
|
+
printApproval: (line) => {
|
|
62
|
+
console.log(line.replaceAll(/[\p{Cc}\p{Cf}]/gu, ''));
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Request lines go to stderr, so stdout keeps the approval lines alone.
|
|
66
|
+
printRequest: (line) => {
|
|
67
|
+
console.error(line);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await gateway.stop();
|
|
72
|
+
|
|
73
|
+
console.error(`atc-gateway: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`atc-gateway: serving ${server.origin}/mcp, listening on ${server.listening}`);
|
|
78
|
+
|
|
79
|
+
const stopServing = async () => {
|
|
80
|
+
await server.stop();
|
|
81
|
+
await gateway.stop();
|
|
82
|
+
|
|
83
|
+
process.exit(0);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
process.on('SIGINT', () => {
|
|
87
|
+
void stopServing();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
process.on('SIGTERM', () => {
|
|
91
|
+
void stopServing();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { AgentID } from './agent-id';
|
|
3
3
|
import { buildOptionalStringArray } from './build-optional-string-array';
|
|
4
4
|
import type { AuthProfile } from './collect-auth-profiles';
|
|
5
|
+
import { isBrokerVariable } from './is-broker-variable';
|
|
5
6
|
import { isRecord } from './report';
|
|
6
7
|
import { resolveAuthProfiles } from './resolve-auth-profiles';
|
|
7
8
|
|
|
@@ -251,28 +252,6 @@ function collectEnvProblems(source: string, keys: readonly string[]): string[] {
|
|
|
251
252
|
.map((key) => `${source} must not set ${key}`);
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
// Variables impd sets in a brokered exec to route requests through the
|
|
255
|
-
// broker and trust its CA. impd lets a variable the caller sets win, so a
|
|
256
|
-
// configured one would route around the broker or mask a failed CA install.
|
|
257
|
-
const BROKER_VARIABLES: ReadonlySet<string> = new Set([
|
|
258
|
-
'HTTPS_PROXY',
|
|
259
|
-
'HTTP_PROXY',
|
|
260
|
-
'ALL_PROXY',
|
|
261
|
-
'NO_PROXY',
|
|
262
|
-
'NODE_USE_ENV_PROXY',
|
|
263
|
-
'SSL_CERT_FILE',
|
|
264
|
-
'SSL_CERT_DIR',
|
|
265
|
-
'NODE_EXTRA_CA_CERTS',
|
|
266
|
-
'GIT_SSL_CAINFO',
|
|
267
|
-
'REQUESTS_CA_BUNDLE',
|
|
268
|
-
'CURL_CA_BUNDLE',
|
|
269
|
-
]);
|
|
270
|
-
|
|
271
|
-
// Proxy and CA variables are read in either case by some clients.
|
|
272
|
-
function isBrokerVariable(key: string): boolean {
|
|
273
|
-
return BROKER_VARIABLES.has(key.toUpperCase());
|
|
274
|
-
}
|
|
275
|
-
|
|
276
255
|
function collectPlaceholderProblems(env: Readonly<Record<string, string>>): string[] {
|
|
277
256
|
return Object.keys(env)
|
|
278
257
|
.filter((key) => isBrokerVariable(key) || key === BASE_URL_VARIABLE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a variable is one impd sets in a brokered exec to route requests
|
|
3
|
+
* through the broker and trust its CA. impd lets a variable the caller
|
|
4
|
+
* sets win, so a caller's own value would route around the broker or mask
|
|
5
|
+
* a failed CA install. Some clients read proxy and CA variables in either
|
|
6
|
+
* case, so the check ignores case.
|
|
7
|
+
*/
|
|
8
|
+
export function isBrokerVariable(key: string): boolean {
|
|
9
|
+
return BROKER_VARIABLES.has(key.toUpperCase());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const BROKER_VARIABLES: ReadonlySet<string> = new Set([
|
|
13
|
+
'HTTPS_PROXY',
|
|
14
|
+
'HTTP_PROXY',
|
|
15
|
+
'ALL_PROXY',
|
|
16
|
+
'NO_PROXY',
|
|
17
|
+
'NODE_USE_ENV_PROXY',
|
|
18
|
+
'SSL_CERT_FILE',
|
|
19
|
+
'SSL_CERT_DIR',
|
|
20
|
+
'NODE_EXTRA_CA_CERTS',
|
|
21
|
+
'GIT_SSL_CAINFO',
|
|
22
|
+
'REQUESTS_CA_BUNDLE',
|
|
23
|
+
'CURL_CA_BUNDLE',
|
|
24
|
+
]);
|
package/src/store/state-store.ts
CHANGED
|
@@ -1054,6 +1054,17 @@ export class StateStore {
|
|
|
1054
1054
|
return row === undefined ? null : toRuntimeAuthBinding(row);
|
|
1055
1055
|
}
|
|
1056
1056
|
|
|
1057
|
+
// Every host's binding, by host key.
|
|
1058
|
+
async collectAuthBindings(): Promise<RuntimeAuthBinding[]> {
|
|
1059
|
+
const rows = await this.db
|
|
1060
|
+
.selectFrom('runtime_auth_binding')
|
|
1061
|
+
.selectAll()
|
|
1062
|
+
.orderBy('host_key')
|
|
1063
|
+
.execute();
|
|
1064
|
+
|
|
1065
|
+
return rows.map((row) => toRuntimeAuthBinding(row));
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1057
1068
|
// Writes one secret's grant row for a host, or moves the existing one to
|
|
1058
1069
|
// the given phase, revision and attempt.
|
|
1059
1070
|
async upsertAuthGrant(grant: Omit<RuntimeAuthGrant, 'updatedAt'>, at: number): Promise<void> {
|