@stage-labs/metro 0.1.0-beta.169 → 0.1.0-beta.170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +0 -16
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +6 -8
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +1 -12
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +5 -110
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -7
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +2 -3
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +0 -7
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.170",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.170",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { webhookPort } from '../net/tunnel.js';
|
|
2
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
2
|
import { parseId } from '@metro-labs/core/ids';
|
|
4
3
|
import { log } from '@metro-labs/core/log';
|
|
@@ -63,20 +62,6 @@ export function target(path: string): Target {
|
|
|
63
62
|
return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
const localMcpEndpoint = (): string => `http://127.0.0.1:${String(webhookPort())}/mcp`;
|
|
67
|
-
|
|
68
|
-
interface KeyPayload {
|
|
69
|
-
key: string | null;
|
|
70
|
-
endpoint: string | null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const credentials = (key: string): KeyPayload => ({ key, endpoint: `${localMcpEndpoint()}?token=${key}` });
|
|
74
|
-
|
|
75
|
-
function keyPayload(agent: AgentSummary): KeyPayload {
|
|
76
|
-
const value = agent.owned ? agent.key : null;
|
|
77
|
-
return value === null ? { key: null, endpoint: null } : credentials(value);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
65
|
function livenessPayload(
|
|
81
66
|
agent: AgentSummary,
|
|
82
67
|
live: Map<string, { connected: boolean; lastSeenAt: number }>,
|
|
@@ -100,7 +85,6 @@ function agentPayload(
|
|
|
100
85
|
owned: agent.owned,
|
|
101
86
|
connector_ids: connectors.get(agent.id) ?? [],
|
|
102
87
|
...livenessPayload(agent, live),
|
|
103
|
-
...keyPayload(agent),
|
|
104
88
|
};
|
|
105
89
|
}
|
|
106
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
|
-
import { apiFailure, apiSession, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
|
|
3
|
+
import { apiFailure, apiSession, cors, readJsonBody, requireAdmin, sendJson } from '@metro-labs/http/api-http';
|
|
4
4
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
5
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
6
6
|
import { parseId } from '@metro-labs/core/ids';
|
|
@@ -14,14 +14,14 @@ const STATION_NAMES = new Set<string>(STATIONS);
|
|
|
14
14
|
|
|
15
15
|
export interface AgentBundle {
|
|
16
16
|
version: 1;
|
|
17
|
-
agent: { id: string; name: string;
|
|
17
|
+
agent: { id: string; name: string; stations: LoadedAccount[] };
|
|
18
18
|
connectors: LoadedConnector[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const loadedAgentOf = (bundle: AgentBundle): LoadedAgent => ({
|
|
22
22
|
id: bundle.agent.id,
|
|
23
23
|
name: bundle.agent.name,
|
|
24
|
-
key:
|
|
24
|
+
key: null,
|
|
25
25
|
accounts: bundle.agent.stations,
|
|
26
26
|
connectors: [],
|
|
27
27
|
});
|
|
@@ -67,11 +67,10 @@ function connectorOf(raw: unknown): LoadedConnector {
|
|
|
67
67
|
|
|
68
68
|
function agentOf(raw: unknown): AgentBundle['agent'] {
|
|
69
69
|
if (!isRecord(raw)) throw bad('not a v1 agent bundle');
|
|
70
|
-
const { id, name,
|
|
70
|
+
const { id, name, stations } = raw;
|
|
71
71
|
if (typeof id !== 'string' || parseId(id) === null) throw bad('agent id is not an id');
|
|
72
|
-
if (typeof key !== 'string' || key === '') throw bad('agent has no key');
|
|
73
72
|
if (!Array.isArray(stations)) throw bad('agent has no station list');
|
|
74
|
-
return { id, name: typeof name === 'string' ? name : '',
|
|
73
|
+
return { id, name: typeof name === 'string' ? name : '', stations: stations.map(stationOf) };
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
export function parseBundle(raw: unknown): AgentBundle {
|
|
@@ -106,6 +105,7 @@ export function handleBundleRequest(req: IncomingMessage, res: ServerResponse, d
|
|
|
106
105
|
apiSession(req)
|
|
107
106
|
.then((session) => {
|
|
108
107
|
if (!session) throw new ApiError('unauthorized', 401);
|
|
108
|
+
requireAdmin(session);
|
|
109
109
|
return answer(req, deps, session.subject, path);
|
|
110
110
|
})
|
|
111
111
|
.then((body) => {
|
|
@@ -174,15 +174,13 @@ export function assertLocalOwner(subject: string, dir = agentsDir()): void {
|
|
|
174
174
|
if (!isOwner(subject, dir)) throw new AgentAdminError('no such project', 404);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function assertImportable(agent: LoadedAgent):
|
|
177
|
+
function assertImportable(agent: LoadedAgent): void {
|
|
178
178
|
const immovable = agent.accounts.find((a) => !MOVABLE_STATIONS.has(a.station));
|
|
179
179
|
if (immovable !== undefined)
|
|
180
180
|
throw new AgentAdminError(
|
|
181
181
|
`a ${immovable.station} endpoint needs a public url and cannot live on a local daemon`,
|
|
182
182
|
400,
|
|
183
183
|
);
|
|
184
|
-
if (agent.key === null)
|
|
185
|
-
throw new AgentAdminError('that agent has no key; reset it on metro.box first', 400);
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
interface ImportTarget {
|
|
@@ -190,7 +188,7 @@ interface ImportTarget {
|
|
|
190
188
|
previous: AgentFile | undefined;
|
|
191
189
|
}
|
|
192
190
|
|
|
193
|
-
function importTarget(dir: string, agent: LoadedAgent
|
|
191
|
+
function importTarget(dir: string, agent: LoadedAgent): ImportTarget {
|
|
194
192
|
const existing = storedAgents(dir);
|
|
195
193
|
const same = existing.find((s) => s.file.id === agent.id);
|
|
196
194
|
if (same === undefined && existing.length > 0)
|
|
@@ -210,15 +208,15 @@ export async function localImportAgent(
|
|
|
210
208
|
assertLocalOwner(subject, dir);
|
|
211
209
|
assertImportable(agent);
|
|
212
210
|
const { path, previous } = importTarget(dir, agent);
|
|
213
|
-
const
|
|
211
|
+
const key = previous?.key ?? agent.key ?? newApiKey();
|
|
212
|
+
const file = fileFor({ ...agent, key }, localOwner(dir), path, previous, mode);
|
|
214
213
|
ensureSecureDir(join(path, '..'));
|
|
215
214
|
save({ path, file });
|
|
216
|
-
|
|
217
|
-
registerKey(agent.key, agent.id);
|
|
215
|
+
registerKey(key, agent.id);
|
|
218
216
|
return Promise.resolve({
|
|
219
217
|
id: agent.id,
|
|
220
218
|
name: agent.name,
|
|
221
|
-
key
|
|
219
|
+
key,
|
|
222
220
|
stations: file.stations.length,
|
|
223
221
|
});
|
|
224
222
|
}
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
startWebhookServer,
|
|
19
19
|
trainEventToMetroEvent,
|
|
20
20
|
} from '../routes/http.js';
|
|
21
|
-
import { localAgentKey } from '../stations/materialize.js';
|
|
22
21
|
import { agentsDir, fileSource } from '../agents/files.js';
|
|
23
22
|
import { syncPluginServers } from '../connectors/plugin-sync.js';
|
|
24
23
|
import { readLocalConnectors } from '../connectors/store.js';
|
|
@@ -36,7 +35,6 @@ import {
|
|
|
36
35
|
agentLiveness,
|
|
37
36
|
createMetroMcp,
|
|
38
37
|
} from '../mcp/index.js';
|
|
39
|
-
import { metroCall } from '../mcp/ctx.js';
|
|
40
38
|
import { gatherAccountsForAgents } from '../mcp/accounts.js';
|
|
41
39
|
import {
|
|
42
40
|
accountStationCapabilities,
|
|
@@ -80,15 +78,6 @@ function announceLocalEndpoint(): void {
|
|
|
80
78
|
process.stderr.write(
|
|
81
79
|
`\n${tunnel === null ? localConnectHint(webhookPort(), localOwner()) : tunnelPendingHint()}`,
|
|
82
80
|
);
|
|
83
|
-
const key = localAgentKey();
|
|
84
|
-
if (key === null) {
|
|
85
|
-
log.info('no agent on this machine yet');
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const url = `http://127.0.0.1:${String(webhookPort())}/mcp?token=${key}`;
|
|
89
|
-
process.stderr.write(
|
|
90
|
-
`\nConnect an agent on this machine:\n\n claude mcp add --transport http metro "${url}"\n\n`,
|
|
91
|
-
);
|
|
92
81
|
}
|
|
93
82
|
|
|
94
83
|
async function syncStations(station: StationName): Promise<void> {
|
|
@@ -135,7 +124,7 @@ installBearerSessions(agentsDir(), localOwner);
|
|
|
135
124
|
emit,
|
|
136
125
|
sessionApis(),
|
|
137
126
|
metroMcp.httpHandler,
|
|
138
|
-
|
|
127
|
+
true,
|
|
139
128
|
);
|
|
140
129
|
metroMcp.startInbound();
|
|
141
130
|
syncPluginServers(readLocalConnectors());
|
|
@@ -143,7 +143,7 @@ async function created(req: IncomingMessage, path: string, dir: string): Promise
|
|
|
143
143
|
return createClaudeSkill(body.name, typeof body.text === 'string' ? body.text : undefined, dir);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const ADMIN_ONLY = /^\/(login|session|version|setup)(\/|$)/;
|
|
146
|
+
const ADMIN_ONLY = /^\/api\/claude\/(login|session|version|setup)(\/|$)/;
|
|
147
147
|
const LOGIN = 'login';
|
|
148
148
|
const SESSION = 'session';
|
|
149
149
|
const SETUP = 'setup';
|
|
@@ -1,30 +1,21 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { stationByName } from '../stations/registry.js';
|
|
3
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
3
|
import { subscribeEvents, type MetroEvent } from '@metro-labs/core/events';
|
|
5
|
-
import {
|
|
4
|
+
import { eventInScope } from '../agents/scope.js';
|
|
6
5
|
import { hasAnyKey } from '../agents/keys.js';
|
|
7
6
|
import {
|
|
8
7
|
allowedAgents,
|
|
9
8
|
authenticate,
|
|
10
9
|
} from '../mcp/request-identity.js';
|
|
11
|
-
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
12
|
-
|
|
13
|
-
export type MonitorCall = (
|
|
14
|
-
train: string,
|
|
15
|
-
action: string,
|
|
16
|
-
args: Record<string, unknown>,
|
|
17
|
-
) => Promise<{ result: unknown }>;
|
|
18
10
|
|
|
19
11
|
const KEEPALIVE_MS = 25_000;
|
|
20
|
-
const CALL_BODY_MAX = 256 * 1024;
|
|
21
12
|
|
|
22
13
|
const monitorEnabled = (): boolean => hasAnyKey();
|
|
23
14
|
|
|
24
15
|
function cors(req: IncomingMessage): Record<string, string> {
|
|
25
16
|
return {
|
|
26
17
|
'access-control-allow-origin': req.headers.origin ?? '*',
|
|
27
|
-
'access-control-allow-methods': 'GET,
|
|
18
|
+
'access-control-allow-methods': 'GET, OPTIONS',
|
|
28
19
|
'access-control-allow-headers': 'Authorization, Content-Type',
|
|
29
20
|
'access-control-max-age': '86400',
|
|
30
21
|
vary: 'Origin',
|
|
@@ -41,33 +32,6 @@ function sendJson(
|
|
|
41
32
|
res.end(JSON.stringify(body));
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
function parseCallArgs(raw: string): Record<string, unknown> {
|
|
45
|
-
if (!raw) return {};
|
|
46
|
-
const parsed = JSON.parse(raw) as { args?: unknown };
|
|
47
|
-
const value =
|
|
48
|
-
parsed && typeof parsed === 'object' && 'args' in parsed
|
|
49
|
-
? parsed.args
|
|
50
|
-
: parsed;
|
|
51
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
52
|
-
throw new Error('call body must be a JSON object');
|
|
53
|
-
return value as Record<string, unknown>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function readCallArgs(
|
|
57
|
-
req: IncomingMessage,
|
|
58
|
-
): Promise<Record<string, unknown>> {
|
|
59
|
-
const chunks: Buffer[] = [];
|
|
60
|
-
let total = 0;
|
|
61
|
-
for await (const chunk of req) {
|
|
62
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string);
|
|
63
|
-
total += buf.length;
|
|
64
|
-
if (total > CALL_BODY_MAX)
|
|
65
|
-
throw new Error(`request body exceeds ${CALL_BODY_MAX} bytes`);
|
|
66
|
-
chunks.push(buf);
|
|
67
|
-
}
|
|
68
|
-
return parseCallArgs(Buffer.concat(chunks).toString('utf8').trim());
|
|
69
|
-
}
|
|
70
|
-
|
|
71
35
|
function startTailStream(
|
|
72
36
|
req: IncomingMessage,
|
|
73
37
|
res: ServerResponse,
|
|
@@ -122,67 +86,12 @@ function startTailStream(
|
|
|
122
86
|
req.on('error', cleanup);
|
|
123
87
|
}
|
|
124
88
|
|
|
125
|
-
async function handleCall(
|
|
126
|
-
req: IncomingMessage,
|
|
127
|
-
res: ServerResponse,
|
|
128
|
-
train: string,
|
|
129
|
-
action: string,
|
|
130
|
-
call: MonitorCall,
|
|
131
|
-
allowed: Set<string>,
|
|
132
|
-
): Promise<void> {
|
|
133
|
-
let args: Record<string, unknown>;
|
|
134
|
-
try {
|
|
135
|
-
args = await readCallArgs(req);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
sendJson(res, req, 400, { error: `bad JSON body: ${errMsg(err)}` });
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (callTargetDenied(allowed, train, args)) {
|
|
141
|
-
sendJson(res, req, 403, {
|
|
142
|
-
error: 'metro: this account is outside your authorized scope',
|
|
143
|
-
});
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (stationByName(train)?.hasTrain === false) {
|
|
147
|
-
sendJson(res, req, 400, {
|
|
148
|
-
error: `metro: station '${train}' runs in-core and takes no calls`,
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const { result } = await call(train, action, args);
|
|
154
|
-
sendJson(res, req, 200, { result });
|
|
155
|
-
} catch (err) {
|
|
156
|
-
sendJson(res, req, 502, { error: errMsg(err) });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
89
|
function routeApi(
|
|
161
90
|
req: IncomingMessage,
|
|
162
91
|
res: ServerResponse,
|
|
163
92
|
path: string,
|
|
164
|
-
call: MonitorCall,
|
|
165
93
|
allowed: Set<string>,
|
|
166
94
|
): void {
|
|
167
|
-
const callMatch = /^\/api\/call\/([^/]+)\/([^/]+)$/.exec(path);
|
|
168
|
-
if (callMatch) {
|
|
169
|
-
if (req.method !== 'POST') {
|
|
170
|
-
sendJson(res, req, 405, { error: 'method not allowed' });
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
handleCall(
|
|
174
|
-
req,
|
|
175
|
-
res,
|
|
176
|
-
callMatch[1] ?? '',
|
|
177
|
-
callMatch[2] ?? '',
|
|
178
|
-
call,
|
|
179
|
-
allowed,
|
|
180
|
-
).catch((err: unknown) => {
|
|
181
|
-
log.warn({ err: errMsg(err) }, 'monitor: call handler error');
|
|
182
|
-
if (!res.headersSent) sendJson(res, req, 500, { error: errMsg(err) });
|
|
183
|
-
});
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
95
|
if (path === '/api/tail') {
|
|
187
96
|
if (req.method !== 'GET') {
|
|
188
97
|
sendJson(res, req, 405, { error: 'method not allowed' });
|
|
@@ -194,11 +103,7 @@ function routeApi(
|
|
|
194
103
|
sendJson(res, req, 404, { error: 'not found' });
|
|
195
104
|
}
|
|
196
105
|
|
|
197
|
-
function preflight(
|
|
198
|
-
req: IncomingMessage,
|
|
199
|
-
res: ServerResponse,
|
|
200
|
-
path: string,
|
|
201
|
-
): boolean {
|
|
106
|
+
function preflight(req: IncomingMessage, res: ServerResponse): boolean {
|
|
202
107
|
if (!monitorEnabled()) {
|
|
203
108
|
sendJson(res, req, 404, { error: 'not found' });
|
|
204
109
|
return true;
|
|
@@ -207,31 +112,21 @@ function preflight(
|
|
|
207
112
|
res.writeHead(204, cors(req)).end();
|
|
208
113
|
return true;
|
|
209
114
|
}
|
|
210
|
-
if (req.method === 'GET' && path === '/api/health') {
|
|
211
|
-
sendJson(res, req, 200, {
|
|
212
|
-
ok: true,
|
|
213
|
-
service: 'metro',
|
|
214
|
-
version: METRO_VERSION,
|
|
215
|
-
uptime_s: Math.round(process.uptime()),
|
|
216
|
-
});
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
115
|
return false;
|
|
220
116
|
}
|
|
221
117
|
|
|
222
118
|
export function handleMonitorRequest(
|
|
223
119
|
req: IncomingMessage,
|
|
224
120
|
res: ServerResponse,
|
|
225
|
-
call: MonitorCall,
|
|
226
121
|
): boolean {
|
|
227
122
|
const path = (req.url ?? '').split('?', 2)[0] ?? '';
|
|
228
123
|
if (!path.startsWith('/api/')) return false;
|
|
229
|
-
if (preflight(req, res
|
|
124
|
+
if (preflight(req, res)) return true;
|
|
230
125
|
const identity = authenticate(req);
|
|
231
126
|
if (!identity) {
|
|
232
127
|
sendJson(res, req, 401, { error: 'unauthorized' });
|
|
233
128
|
return true;
|
|
234
129
|
}
|
|
235
|
-
routeApi(req, res, path,
|
|
130
|
+
routeApi(req, res, path, allowedAgents(identity));
|
|
236
131
|
return true;
|
|
237
132
|
}
|
|
@@ -38,7 +38,6 @@ import { handleModeRequest } from '@metro-labs/http/mode-api';
|
|
|
38
38
|
import { handleUploadRequest } from '../files/upload-api.js';
|
|
39
39
|
import {
|
|
40
40
|
handleMonitorRequest,
|
|
41
|
-
type MonitorCall,
|
|
42
41
|
} from '../monitor/api.js';
|
|
43
42
|
import { applyMcpCors, handleMcpPreflight } from '@metro-labs/http/cors';
|
|
44
43
|
import { METRO_VERSION } from '@metro-labs/core/version';
|
|
@@ -181,11 +180,11 @@ export async function startWebhookServer(
|
|
|
181
180
|
emit: Emit,
|
|
182
181
|
apis: SessionApis = {},
|
|
183
182
|
mcp?: McpHandler,
|
|
184
|
-
|
|
183
|
+
monitor = false,
|
|
185
184
|
): Promise<Server> {
|
|
186
185
|
const port = webhookPort();
|
|
187
186
|
const server = createServer((req, res) => {
|
|
188
|
-
handleRequest(req, res, emit, apis, mcp,
|
|
187
|
+
handleRequest(req, res, emit, apis, mcp, monitor).catch(
|
|
189
188
|
(err: unknown) => {
|
|
190
189
|
log.warn({ err: errMsg(err) }, 'webhook handler error');
|
|
191
190
|
if (!res.headersSent) res.writeHead(500).end();
|
|
@@ -340,7 +339,7 @@ async function handlePreMcpRoutes(
|
|
|
340
339
|
res: ServerResponse,
|
|
341
340
|
emit: Emit,
|
|
342
341
|
apis: SessionApis,
|
|
343
|
-
|
|
342
|
+
monitor = false,
|
|
344
343
|
): Promise<boolean> {
|
|
345
344
|
if (handleEarlyRoutes(req, res, apis)) return true;
|
|
346
345
|
if (handleUploadRequest(req, res)) return true;
|
|
@@ -348,7 +347,7 @@ async function handlePreMcpRoutes(
|
|
|
348
347
|
if (apis.relayApi && handleRelayRequest(req, res, apis.relayApi)) return true;
|
|
349
348
|
if (await handleWebhookRoute(req, res, emit)) return true;
|
|
350
349
|
if (await handleThreemaCallback(req, res)) return true;
|
|
351
|
-
return
|
|
350
|
+
return monitor && handleMonitorRequest(req, res);
|
|
352
351
|
}
|
|
353
352
|
|
|
354
353
|
async function handleRequest(
|
|
@@ -357,9 +356,9 @@ async function handleRequest(
|
|
|
357
356
|
emit: Emit,
|
|
358
357
|
apis: SessionApis = {},
|
|
359
358
|
mcp?: McpHandler,
|
|
360
|
-
|
|
359
|
+
monitor = false,
|
|
361
360
|
): Promise<void> {
|
|
362
|
-
if (await handlePreMcpRoutes(req, res, emit, apis,
|
|
361
|
+
if (await handlePreMcpRoutes(req, res, emit, apis, monitor)) return;
|
|
363
362
|
if (mcp && isMcpPath(req)) {
|
|
364
363
|
applyMcpCors(req, res);
|
|
365
364
|
if (handleMcpPreflight(req, res)) return;
|
|
@@ -143,14 +143,13 @@ const relayApi: RelayApiDeps = {
|
|
|
143
143
|
function bundleApi(deps: LocalModeDeps): BundleApiDeps {
|
|
144
144
|
return {
|
|
145
145
|
bundle: async (subject, agentId) => {
|
|
146
|
-
|
|
146
|
+
await localOwnedAgentOrThrow(subject, agentId);
|
|
147
147
|
const file = readLocalAgentFile(agentId);
|
|
148
148
|
const bundle: AgentBundle = {
|
|
149
149
|
version: 1,
|
|
150
|
-
agent: { id: file.id, name: file.name ?? '',
|
|
150
|
+
agent: { id: file.id, name: file.name ?? '', stations: file.stations },
|
|
151
151
|
connectors: readLocalConnectors().map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
|
|
152
152
|
};
|
|
153
|
-
if (bundle.agent.key === '') throw new ApiError(`agent ${agent.id} has no key to bundle`, 400);
|
|
154
153
|
return bundle;
|
|
155
154
|
},
|
|
156
155
|
restore: async (subject, bundle, mode) => {
|
|
@@ -200,7 +200,6 @@ const stationLabels = (m: Map<StationName, number>): string[] =>
|
|
|
200
200
|
[...m].map(([station, n]) => `${station}(${n})`);
|
|
201
201
|
|
|
202
202
|
function applyKeyMap(list: LoadedAgent[]): void {
|
|
203
|
-
lastKey = list.find((a) => a.key !== null)?.key ?? null;
|
|
204
203
|
setKeyMap(
|
|
205
204
|
list.flatMap((agent) =>
|
|
206
205
|
agent.key === null ? [] : [{ key: agent.key, agentId: agent.id }],
|
|
@@ -208,12 +207,6 @@ function applyKeyMap(list: LoadedAgent[]): void {
|
|
|
208
207
|
);
|
|
209
208
|
}
|
|
210
209
|
|
|
211
|
-
let lastKey: string | null = null;
|
|
212
|
-
|
|
213
|
-
export function localAgentKey(): string | null {
|
|
214
|
-
return lastKey;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
210
|
async function loadAndWrite(
|
|
218
211
|
source: StationSource,
|
|
219
212
|
): Promise<WrittenStations & { agents: number }> {
|
package/runtime/runtime.json
CHANGED