@stage-labs/metro 0.1.0-beta.302 → 0.1.0-beta.304
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 +2 -2
- package/runtime/marketplace/plugin/METRO.md +2 -0
- package/runtime/marketplace/plugin/bin/guard.mjs +2 -1
- package/runtime/marketplace/plugin/bin/metro-plugin.mjs +20 -2
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +4 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -11
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +10 -3
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +16 -8
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +6 -3
- package/runtime/node_modules/@metro-labs/daemon/src/channels/notice.ts +22 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +3 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -11
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-adopt.ts +57 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime.ts +39 -95
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-control.ts +27 -45
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-watch.ts +2 -17
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +12 -37
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +8 -29
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-session.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +33 -138
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/server-keys.ts +22 -9
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +27 -8
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +6 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +109 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/config.ts +29 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/link.ts +186 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/token.ts +50 -0
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +4 -58
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +0 -12
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +0 -7
- package/runtime/node_modules/@metro-labs/http/package.json +2 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +2 -1
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +81 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +0 -316
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +0 -105
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +0 -18
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-api.ts +0 -60
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-control.ts +0 -366
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-guard.ts +0 -95
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-operations.ts +0 -226
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-preview.ts +0 -235
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-source.ts +0 -42
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-state.ts +0 -142
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-url.ts +0 -9
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +0 -178
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +0 -171
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-passive.ts +0 -96
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +0 -393
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-api.ts +0 -116
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-crypto.ts +0 -44
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-store.ts +0 -46
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { log } from '@metro-labs/core/log';
|
|
2
|
-
import { agentsDir } from '../agents/files.js';
|
|
3
|
-
import { assertMigrationOutcomeKnown, beginMigrationOperation, migrationRequestEffectful, type MigrationOperation } from './migration-operations.js';
|
|
4
2
|
import type { FetchLike } from '@metro-labs/core/stations/oauth';
|
|
5
3
|
import type { ToolGroup } from '@metro-labs/core/stations/types';
|
|
6
4
|
import { effectiveConnectorPolicy, type DeliveredConnector, type DeliverySnapshot } from '@metro-labs/http/connector-delivery';
|
|
7
5
|
import type { ApiError } from '@metro-labs/http/api-error';
|
|
8
6
|
import { requestConnectorBlocker, type GateRow } from './gates.js';
|
|
9
7
|
import { publishOrgConnectors } from './publication.js';
|
|
10
|
-
import
|
|
11
|
-
import { isOrgRelayId, orgRelayId } from './relay-id.js';
|
|
8
|
+
import { orgRelayId } from './relay-id.js';
|
|
12
9
|
import type { RelaySession } from './relay-session.js';
|
|
13
10
|
import type { RelayTarget } from './relay-target.js';
|
|
14
11
|
import type { OrgConnectorSync } from './org-sync.js';
|
|
15
12
|
import { discoverOrgGroups, orgMissing, orgRuntimeError, orgSignin, orgUnavailable } from './org-runtime-discovery.js';
|
|
16
13
|
|
|
17
|
-
interface RuntimeDeps {
|
|
18
|
-
enabled?: () => boolean;
|
|
19
|
-
mapping?: () => MigrationRuntimeMapping;
|
|
20
|
-
migrationDir?: string;
|
|
14
|
+
export interface RuntimeDeps {
|
|
21
15
|
publish?: (rows: readonly GateRow[]) => void;
|
|
22
16
|
fetch?: FetchLike;
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
interface RuntimeRow extends DeliveredConnector {
|
|
26
|
-
relayId: string;
|
|
27
|
-
relayName: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
19
|
interface Entry {
|
|
31
20
|
id: string;
|
|
32
|
-
relayId: string;
|
|
33
21
|
epoch: number;
|
|
34
22
|
stamp: string;
|
|
35
23
|
controller: AbortController;
|
|
@@ -41,14 +29,13 @@ interface Entry {
|
|
|
41
29
|
|
|
42
30
|
interface Session {
|
|
43
31
|
entry: Entry;
|
|
44
|
-
operation: MigrationOperation | null;
|
|
45
32
|
controller: AbortController;
|
|
46
33
|
revision: string | null;
|
|
47
34
|
error: ApiError | null;
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
const stampOf = (row:
|
|
51
|
-
row.
|
|
37
|
+
const stampOf = (row: DeliveredConnector): string => JSON.stringify([
|
|
38
|
+
row.name, row.url, row.auth, row.policy.read ?? 'allow', row.policy.write ?? 'allow',
|
|
52
39
|
Object.entries(row.policy.tools ?? {}).sort(([a], [b]) => a.localeCompare(b)), row.policyLayers,
|
|
53
40
|
]);
|
|
54
41
|
|
|
@@ -85,18 +72,14 @@ export class OrgConnectorRuntime {
|
|
|
85
72
|
this.update();
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
private
|
|
89
|
-
return !this.stopped && (this.deps.enabled?.() ?? process.env.METRO_CONNECTORS_LIVE_ENABLED === 'true');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private abort(session: Session, error: ApiError, drain = false): void {
|
|
75
|
+
private abort(session: Session, error: ApiError): void {
|
|
93
76
|
session.error ??= error;
|
|
94
|
-
|
|
77
|
+
session.controller.abort();
|
|
95
78
|
}
|
|
96
79
|
|
|
97
|
-
private publish(rows:
|
|
80
|
+
private publish(rows: DeliveredConnector[]): void {
|
|
98
81
|
const metadata: GateRow[] = rows.map((row) => ({
|
|
99
|
-
id: row.
|
|
82
|
+
id: orgRelayId(row.id), name: row.name,
|
|
100
83
|
config: { policy: effectiveConnectorPolicy(row, this.entries.get(row.id)?.groups ?? {}), toolGroups: { ...this.entries.get(row.id)?.groups } },
|
|
101
84
|
}));
|
|
102
85
|
const text = JSON.stringify(metadata);
|
|
@@ -111,48 +94,27 @@ export class OrgConnectorRuntime {
|
|
|
111
94
|
}
|
|
112
95
|
}
|
|
113
96
|
|
|
114
|
-
private invalidateEntries(rows:
|
|
97
|
+
private invalidateEntries(rows: DeliveredConnector[], epoch: number, available: boolean): void {
|
|
115
98
|
for (const [id, entry] of this.entries) {
|
|
116
99
|
const row = rows.find((value) => value.id === id);
|
|
117
100
|
const valid = available && entry.epoch === epoch;
|
|
118
101
|
if (valid && row?.state === 'ready' && entry.stamp === stampOf(row)) continue;
|
|
119
|
-
|
|
120
|
-
this.invalidateEntry(entry, row, error);
|
|
102
|
+
this.invalidateEntry(entry, valid ? rowError(row) ?? orgUnavailable() : orgUnavailable());
|
|
121
103
|
}
|
|
122
104
|
}
|
|
123
105
|
|
|
124
|
-
private invalidateEntry(entry: Entry,
|
|
125
|
-
|
|
126
|
-
if (!drain) entry.controller.abort();
|
|
106
|
+
private invalidateEntry(entry: Entry, error: ApiError): void {
|
|
107
|
+
entry.controller.abort();
|
|
127
108
|
this.entries.delete(entry.id);
|
|
128
|
-
this.
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
private abortEntry(entry: Entry, error: ApiError, drain = false): void {
|
|
132
|
-
for (const session of this.sessions) if (session.entry === entry) this.abort(session, error, drain);
|
|
109
|
+
for (const session of this.sessions) if (session.entry === entry) this.abort(session, error);
|
|
133
110
|
}
|
|
134
111
|
|
|
135
|
-
private
|
|
136
|
-
return rows.flatMap((row) => {
|
|
137
|
-
const alias = mapping.aliases.find((value) => value.destinationId === row.id);
|
|
138
|
-
if (alias !== undefined) return [{ ...row, relayId: alias.sourceId, relayName: alias.name }];
|
|
139
|
-
return this.enabled() && !mapping.reserved.includes(row.id) ? [{ ...row, relayId: orgRelayId(row.id), relayName: row.name }] : [];
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private mapping(): MigrationRuntimeMapping {
|
|
144
|
-
if (this.stopped) return { aliases: [], reserved: [] };
|
|
145
|
-
return this.deps.mapping?.() ?? { aliases: [], reserved: [] };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private update(): RuntimeRow[] | null {
|
|
112
|
+
private update(): DeliveredConnector[] | null {
|
|
149
113
|
try {
|
|
150
|
-
const
|
|
151
|
-
const wanted = this.enabled() || mapping.aliases.length > 0;
|
|
152
|
-
const snapshot = wanted ? this.sync.read() : null;
|
|
114
|
+
const snapshot = this.stopped ? null : this.sync.read();
|
|
153
115
|
const epoch = this.sync.epoch();
|
|
154
|
-
if (this.held(snapshot,
|
|
155
|
-
const rows =
|
|
116
|
+
if (this.held(snapshot, epoch)) return null;
|
|
117
|
+
const rows = snapshot?.connectors ?? [];
|
|
156
118
|
this.invalidateEntries(rows, epoch, snapshot !== null);
|
|
157
119
|
this.track(rows, epoch);
|
|
158
120
|
this.publish(rows);
|
|
@@ -164,27 +126,19 @@ export class OrgConnectorRuntime {
|
|
|
164
126
|
}
|
|
165
127
|
}
|
|
166
128
|
|
|
167
|
-
private track(rows:
|
|
129
|
+
private track(rows: DeliveredConnector[], epoch: number): void {
|
|
168
130
|
for (const row of rows) {
|
|
169
131
|
if (row.state !== 'ready' || this.entries.has(row.id)) continue;
|
|
170
|
-
this.entries.set(row.id, { id: row.id,
|
|
132
|
+
this.entries.set(row.id, { id: row.id, epoch, stamp: stampOf(row), controller: new AbortController(), groups: {}, scan: null, nextScan: 0, error: null });
|
|
171
133
|
}
|
|
172
134
|
}
|
|
173
135
|
|
|
174
|
-
private held(snapshot: DeliverySnapshot | null,
|
|
175
|
-
if (snapshot !== null ||
|
|
136
|
+
private held(snapshot: DeliverySnapshot | null, epoch: number): boolean {
|
|
137
|
+
if (snapshot !== null || this.stopped || !this.sync.lapsed()) return false;
|
|
176
138
|
this.invalidateEntries([], epoch, false);
|
|
177
139
|
return true;
|
|
178
140
|
}
|
|
179
141
|
|
|
180
|
-
refresh(): void {
|
|
181
|
-
this.update();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
idle(id?: string): boolean {
|
|
185
|
-
return ![...this.sessions].some((session) => id === undefined || session.entry.relayId === id);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
142
|
private currentEntry(entry: Entry): DeliveredConnector {
|
|
189
143
|
const rows = this.update();
|
|
190
144
|
if (rows === null || entry.epoch !== this.sync.epoch()) throw orgUnavailable();
|
|
@@ -193,7 +147,6 @@ export class OrgConnectorRuntime {
|
|
|
193
147
|
const error = rowError(row);
|
|
194
148
|
if (error !== null) throw error;
|
|
195
149
|
if (entry.controller.signal.aborted || this.entries.get(entry.id) !== entry) throw orgUnavailable();
|
|
196
|
-
if (!isOrgRelayId(entry.relayId)) assertMigrationOutcomeKnown(entry.relayId, this.deps.migrationDir);
|
|
197
150
|
return row;
|
|
198
151
|
}
|
|
199
152
|
|
|
@@ -216,30 +169,22 @@ export class OrgConnectorRuntime {
|
|
|
216
169
|
return this.currentEntry(entry);
|
|
217
170
|
}
|
|
218
171
|
|
|
219
|
-
private migrationOperation(entry: Entry): MigrationOperation | null {
|
|
220
|
-
return isOrgRelayId(entry.relayId) ? null : beginMigrationOperation(entry.relayId, this.deps.migrationDir ?? agentsDir(), true);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
172
|
private async scan(entry: Entry): Promise<void> {
|
|
224
173
|
const row = this.currentEntry(entry);
|
|
225
174
|
const signal = AbortSignal.any([entry.controller.signal, AbortSignal.timeout(45_000)]);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
entry.error = null;
|
|
240
|
-
entry.nextScan = performance.now() + 60_000;
|
|
241
|
-
this.update();
|
|
242
|
-
} finally { operation?.close(); }
|
|
175
|
+
const groups = await discoverOrgGroups(row.url, {
|
|
176
|
+
fresh: (revision) => this.fresh(entry, revision === undefined ? undefined : { connectorId: entry.id, revision }),
|
|
177
|
+
current: () => this.currentEntry(entry), signal,
|
|
178
|
+
fetch: this.deps.fetch ?? fetch,
|
|
179
|
+
});
|
|
180
|
+
this.currentEntry(entry);
|
|
181
|
+
if (JSON.stringify(groups) !== JSON.stringify(entry.groups)) {
|
|
182
|
+
for (const session of this.sessions) if (session.entry === entry && session.revision !== null) this.abort(session, orgUnavailable());
|
|
183
|
+
}
|
|
184
|
+
entry.groups = groups;
|
|
185
|
+
entry.error = null;
|
|
186
|
+
entry.nextScan = performance.now() + 60_000;
|
|
187
|
+
this.update();
|
|
243
188
|
}
|
|
244
189
|
|
|
245
190
|
private discover(entry: Entry): Promise<void> | null {
|
|
@@ -291,16 +236,15 @@ export class OrgConnectorRuntime {
|
|
|
291
236
|
open(id: string): RelaySession {
|
|
292
237
|
const rows = this.update();
|
|
293
238
|
if (rows === null) throw orgUnavailable();
|
|
294
|
-
const
|
|
295
|
-
const error = rowError(
|
|
239
|
+
const found = rows.find((row) => orgRelayId(row.id) === id);
|
|
240
|
+
const error = rowError(found);
|
|
296
241
|
if (error !== null) throw error;
|
|
297
|
-
const entry =
|
|
242
|
+
const entry = found === undefined ? undefined : this.entries.get(found.id);
|
|
298
243
|
if (entry === undefined) throw orgUnavailable();
|
|
299
|
-
const session: Session = { entry,
|
|
244
|
+
const session: Session = { entry, controller: new AbortController(), revision: null, error: null };
|
|
300
245
|
this.sessions.add(session);
|
|
301
246
|
const blocker = requestConnectorBlocker();
|
|
302
247
|
return {
|
|
303
|
-
dispatch: (send, effectful) => { this.currentSession(session); return session.operation?.dispatch(send, { effectful }) ?? send(); },
|
|
304
248
|
target: (force) => this.target(session, force),
|
|
305
249
|
current: () => {
|
|
306
250
|
const row = this.currentSession(session);
|
|
@@ -312,7 +256,7 @@ export class OrgConnectorRuntime {
|
|
|
312
256
|
return blocker();
|
|
313
257
|
},
|
|
314
258
|
signal: session.controller.signal,
|
|
315
|
-
close: () => { this.abort(session, orgUnavailable());
|
|
259
|
+
close: () => { this.abort(session, orgUnavailable()); this.sessions.delete(session); },
|
|
316
260
|
};
|
|
317
261
|
}
|
|
318
262
|
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import type { ConnectorSyncStatus } from '@metro-labs/core/connector-sync';
|
|
3
|
-
import { log } from '@metro-labs/core/log';
|
|
3
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
4
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
5
5
|
import { agentsDir } from '../agents/files.js';
|
|
6
6
|
import { METRO_API, type MetroApi } from './metro-api.js';
|
|
7
7
|
import { OrgConnectorSync, type OrgSyncDeps } from './org-sync.js';
|
|
8
|
-
import { OrgConnectorRuntime } from './org-runtime.js';
|
|
8
|
+
import { OrgConnectorRuntime, type RuntimeDeps } from './org-runtime.js';
|
|
9
9
|
import { connectorSyncScope, disableSavedSync, saveSyncSetting, syncOptedIn, syncSetting } from './org-sync-setting.js';
|
|
10
10
|
import type { RelaySession } from './relay-session.js';
|
|
11
|
-
import
|
|
12
|
-
import { localMigrationIdle, migrationRuntimeMapping } from './migration-guard.js';
|
|
13
|
-
import { publishConnectors } from './publication.js';
|
|
11
|
+
import { publishOrgConnectors } from './publication.js';
|
|
14
12
|
import { orgRuntimeError, orgUnavailable } from './org-runtime-discovery.js';
|
|
15
13
|
|
|
16
|
-
type ControlDeps = Pick<OrgSyncDeps, 'owner' | 'key' | 'monotonic'> & { api?: MetroApi; dir?: string };
|
|
14
|
+
type ControlDeps = Pick<OrgSyncDeps, 'owner' | 'key' | 'monotonic'> & RuntimeDeps & { api?: MetroApi; dir?: string };
|
|
17
15
|
type Context = Pick<ConnectorSyncStatus, 'owner' | 'agentId' | 'scope' | 'enrolled'>;
|
|
18
16
|
const POLL_MS = 60_000;
|
|
19
|
-
const liveEnabled = (): boolean => process.env.METRO_CONNECTORS_LIVE_ENABLED === 'true';
|
|
20
17
|
const emptyReceipt = { lastReceivedAt: null, leaseExpiresAt: null, counts: null };
|
|
21
18
|
|
|
22
19
|
export class OrgConnectorControl {
|
|
@@ -24,7 +21,7 @@ export class OrgConnectorControl {
|
|
|
24
21
|
private readonly api: MetroApi;
|
|
25
22
|
private sync: OrgConnectorSync | null = null;
|
|
26
23
|
private runtime: OrgConnectorRuntime | null = null;
|
|
27
|
-
private
|
|
24
|
+
private unpublished = false;
|
|
28
25
|
private scope: string | null = null;
|
|
29
26
|
private operation = randomBytes(16).toString('hex');
|
|
30
27
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -49,20 +46,23 @@ export class OrgConnectorControl {
|
|
|
49
46
|
this.sync?.invalidate();
|
|
50
47
|
this.sync = null;
|
|
51
48
|
this.runtime = null;
|
|
52
|
-
this.retire(runtime);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private retire(runtime: OrgConnectorRuntime | null): void {
|
|
56
|
-
for (const previous of this.draining) if (previous.idle()) this.draining.delete(previous);
|
|
57
|
-
if (runtime === null) return;
|
|
58
|
-
if (!runtime.idle()) this.draining.add(runtime);
|
|
59
49
|
try {
|
|
60
|
-
runtime
|
|
50
|
+
runtime?.stop();
|
|
61
51
|
} catch {
|
|
62
52
|
log.warn('organization connectors: stopped runtime publication could not be cleared');
|
|
63
53
|
}
|
|
64
54
|
}
|
|
65
55
|
|
|
56
|
+
private unpublish(): void {
|
|
57
|
+
if (this.runtime !== null || this.unpublished) return;
|
|
58
|
+
try {
|
|
59
|
+
(this.deps.publish ?? publishOrgConnectors)([]);
|
|
60
|
+
this.unpublished = true;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.warn({ err: errMsg(err) }, 'organization connectors: the connector list could not be cleared');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
66
|
private reconcile(start: boolean): { context: Context; setting: ReturnType<typeof syncSetting> } {
|
|
67
67
|
const context = this.context();
|
|
68
68
|
if (this.scope !== null && this.scope !== context.scope) {
|
|
@@ -73,8 +73,10 @@ export class OrgConnectorControl {
|
|
|
73
73
|
this.scope = context.scope;
|
|
74
74
|
const setting = syncSetting(this.dir, context.scope);
|
|
75
75
|
if (setting.source === 'scope_changed') disableSavedSync(this.dir);
|
|
76
|
-
if (!setting.enabled || !context.enrolled)
|
|
77
|
-
|
|
76
|
+
if (!setting.enabled || !context.enrolled) {
|
|
77
|
+
this.clear();
|
|
78
|
+
this.unpublish();
|
|
79
|
+
} else if (start && this.sync === null) {
|
|
78
80
|
const scope = context.scope;
|
|
79
81
|
this.sync = new OrgConnectorSync({ ...this.deps, api: this.api, enabled: () => this.context().scope === scope && syncSetting(this.dir, scope).enabled });
|
|
80
82
|
}
|
|
@@ -84,8 +86,8 @@ export class OrgConnectorControl {
|
|
|
84
86
|
|
|
85
87
|
private startRuntime(): void {
|
|
86
88
|
if (this.sync === null || this.runtime !== null) return;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
this.unpublished = false;
|
|
90
|
+
this.runtime = new OrgConnectorRuntime(this.sync, this.deps);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
status(): ConnectorSyncStatus {
|
|
@@ -94,7 +96,7 @@ export class OrgConnectorControl {
|
|
|
94
96
|
const receipt = this.sync?.status() ?? { state: 'waiting' as const, ...emptyReceipt };
|
|
95
97
|
const state = !setting.enabled ? 'off' : !context.enrolled ? 'not_enrolled' : receipt.state;
|
|
96
98
|
const scope = createHash('sha256').update(`${context.scope}:${this.operation}`).digest('hex');
|
|
97
|
-
return { ...context, ...setting, ...receipt, scope, state, liveEnabled:
|
|
99
|
+
return { ...context, ...setting, ...receipt, scope, state, liveEnabled: false };
|
|
98
100
|
} catch {
|
|
99
101
|
this.clear();
|
|
100
102
|
throw new ApiError('Metro could not read the connector sync status.', 503);
|
|
@@ -105,7 +107,6 @@ export class OrgConnectorControl {
|
|
|
105
107
|
const current = this.status();
|
|
106
108
|
if (scope !== current.scope) throw new ApiError('The organization or enrollment changed. Refresh the sync status before trying again.', 409);
|
|
107
109
|
if (enabled && !current.enrolled) throw new ApiError('Enroll this agent with its organization before enabling sync.', 409);
|
|
108
|
-
if (enabled && liveEnabled()) throw new ApiError('Live organization tools are enabled separately on this server. Disable that setting before enabling memory-only sync here.', 409);
|
|
109
110
|
saveSyncSetting(this.dir, enabled, this.context().scope);
|
|
110
111
|
if (!enabled) this.clear();
|
|
111
112
|
else this.tick();
|
|
@@ -113,7 +114,10 @@ export class OrgConnectorControl {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
async poll(): Promise<void> {
|
|
116
|
-
if (this.sync === null && !syncOptedIn(this.dir))
|
|
117
|
+
if (this.sync === null && !syncOptedIn(this.dir)) {
|
|
118
|
+
this.unpublish();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
117
121
|
try {
|
|
118
122
|
this.reconcile(true);
|
|
119
123
|
} catch (err) {
|
|
@@ -123,28 +127,6 @@ export class OrgConnectorControl {
|
|
|
123
127
|
await this.sync?.sync();
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
async fresh(): Promise<DeliverySnapshot> {
|
|
127
|
-
this.reconcile(true);
|
|
128
|
-
const sync = this.sync;
|
|
129
|
-
if (sync === null) throw new ApiError('Enable organization connector sync before moving this connector.', 409);
|
|
130
|
-
await sync.syncFresh();
|
|
131
|
-
this.reconcile(false);
|
|
132
|
-
const snapshot = this.sync === sync ? sync.read() : null;
|
|
133
|
-
if (snapshot === null) throw new ApiError('A fresh organization connector snapshot is unavailable.', 503);
|
|
134
|
-
return snapshot;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
refreshRuntime(): void {
|
|
138
|
-
this.reconcile(true);
|
|
139
|
-
this.runtime?.refresh();
|
|
140
|
-
publishConnectors();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
migrationIdle(id: string): boolean {
|
|
144
|
-
for (const runtime of this.draining) if (runtime.idle()) this.draining.delete(runtime);
|
|
145
|
-
return localMigrationIdle(id, this.dir) && (this.runtime?.idle(id) ?? true) && [...this.draining].every((runtime) => runtime.idle(id));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
130
|
private tick(): void {
|
|
149
131
|
this.poll().catch(() => {
|
|
150
132
|
log.warn('organization connectors: memory sync unavailable; cached credentials were cleared');
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { ApiError } from '@metro-labs/http/api-error';
|
|
2
2
|
import type { ConnectorSyncStatus } from '@metro-labs/core/connector-sync';
|
|
3
3
|
import { OrgConnectorControl } from './org-sync-control.js';
|
|
4
|
-
import { isOrgRelayId } from './relay-id.js';
|
|
5
4
|
import type { RelaySession } from './relay-session.js';
|
|
6
|
-
import type { DeliverySnapshot } from '@metro-labs/http/connector-delivery';
|
|
7
|
-
import { migrationSource } from './migration-guard.js';
|
|
8
5
|
|
|
9
6
|
let active: OrgConnectorControl | null = null;
|
|
10
7
|
|
|
@@ -13,20 +10,8 @@ function control(): OrgConnectorControl {
|
|
|
13
10
|
return active;
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
export function orgConnectorRelaySession(id: string): Promise<RelaySession>
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function freshOrgConnectors(): Promise<DeliverySnapshot> {
|
|
21
|
-
return control().fresh();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function refreshOrgConnectorRuntime(): void {
|
|
25
|
-
control().refreshRuntime();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function orgConnectorMigrationIdle(id: string): boolean {
|
|
29
|
-
return control().migrationIdle(id);
|
|
13
|
+
export function orgConnectorRelaySession(id: string): Promise<RelaySession> {
|
|
14
|
+
return control().open(id);
|
|
30
15
|
}
|
|
31
16
|
|
|
32
17
|
export function orgConnectorSyncStatus(): ConnectorSyncStatus {
|
|
@@ -5,11 +5,12 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
6
|
import { claudeDir } from '../claude/files.js';
|
|
7
7
|
import { loopbackBase } from '../files/attach-serve.js';
|
|
8
|
-
import {
|
|
8
|
+
import { MCP_SERVER_KEY, mcpServerUrl } from '../mcp-server/config.js';
|
|
9
9
|
|
|
10
10
|
const MARKER = join('bin', 'metro-plugin.mjs');
|
|
11
11
|
const FILE = '.mcp.json';
|
|
12
12
|
const HELPER = 'node "${CLAUDE_PLUGIN_ROOT}/bin/metro-plugin.mjs" headers';
|
|
13
|
+
const MCP_HELPER = 'node "${CLAUDE_PLUGIN_ROOT}/bin/metro-plugin.mjs" mcp-headers';
|
|
13
14
|
const SLUG_MAX = 40;
|
|
14
15
|
|
|
15
16
|
export interface PluginRow {
|
|
@@ -33,29 +34,6 @@ export function serverKey(name: string): string {
|
|
|
33
34
|
return slug === '' ? 'connector' : slug;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function legacyOwners(rows: PluginRow[], published: PluginRow[]): Map<string, string> {
|
|
37
|
-
const visible = new Set(published.map((row) => row.id));
|
|
38
|
-
const taken = new Map<string, number>();
|
|
39
|
-
const owners = new Map<string, string>();
|
|
40
|
-
const reserved = new Set<string>();
|
|
41
|
-
for (const row of rows.filter((entry) => !isOrgRelayId(entry.id))) {
|
|
42
|
-
const slug = serverKey(row.name);
|
|
43
|
-
const seen = taken.get(slug) ?? 0;
|
|
44
|
-
taken.set(slug, seen + 1);
|
|
45
|
-
const key = seen === 0 ? slug : `${slug}-${row.id.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 4)}`;
|
|
46
|
-
reserved.add(key);
|
|
47
|
-
if (visible.has(row.id)) owners.set(key, row.id);
|
|
48
|
-
}
|
|
49
|
-
for (const row of rows.filter((entry) => isOrgRelayId(entry.id) && visible.has(entry.id)).sort((a, b) => a.id.localeCompare(b.id))) {
|
|
50
|
-
const key = availableKey(orgKey(row), reserved);
|
|
51
|
-
reserved.add(key);
|
|
52
|
-
owners.set(key, row.id);
|
|
53
|
-
}
|
|
54
|
-
return new Map([...owners].map(([key, id]) => [id, key]));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const orgKey = (row: PluginRow): string => `org-${serverKey(row.name).slice(0, 35)}-${Buffer.from(row.id.slice(4)).toString('hex')}`;
|
|
58
|
-
|
|
59
37
|
function availableKey(wanted: string, reserved: ReadonlySet<string>): string {
|
|
60
38
|
let key = wanted;
|
|
61
39
|
let suffix = 1;
|
|
@@ -63,28 +41,25 @@ function availableKey(wanted: string, reserved: ReadonlySet<string>): string {
|
|
|
63
41
|
return key;
|
|
64
42
|
}
|
|
65
43
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
for (const row of [...
|
|
44
|
+
const reservedKeys = (): ReadonlySet<string> => new Set(mcpServerUrl() === null ? [] : [MCP_SERVER_KEY]);
|
|
45
|
+
|
|
46
|
+
export function serverKeysOf(rows: PluginRow[], saved?: ReadonlyMap<string, string>, reserved: ReadonlySet<string> = reservedKeys()): Map<string, string> {
|
|
47
|
+
const keys = new Map([...(saved ?? [])].filter(([, key]) => !reserved.has(key)));
|
|
48
|
+
const used = new Set([...keys.values(), ...reserved]);
|
|
49
|
+
for (const row of [...rows].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
72
50
|
if (keys.has(row.id)) continue;
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
? orgKey(row)
|
|
76
|
-
: reserved.has(slug) ? `${slug}-${row.id.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 4)}` : slug;
|
|
77
|
-
const key = availableKey(wanted, reserved);
|
|
78
|
-
reserved.add(key);
|
|
51
|
+
const key = availableKey(serverKey(row.name), used);
|
|
52
|
+
used.add(key);
|
|
79
53
|
keys.set(row.id, key);
|
|
80
54
|
}
|
|
81
55
|
return keys;
|
|
82
56
|
}
|
|
83
57
|
|
|
84
|
-
export function pluginServers(rows: PluginRow[], base: string, keys: ReadonlyMap<string, string> = serverKeysOf(rows)): Record<string, PluginServer> {
|
|
58
|
+
export function pluginServers(rows: PluginRow[], base: string, keys: ReadonlyMap<string, string> = serverKeysOf(rows), mcpUrl = mcpServerUrl()): Record<string, PluginServer> {
|
|
85
59
|
const out: Record<string, PluginServer> = {};
|
|
86
60
|
const visible = new Set(rows.map((row) => row.id));
|
|
87
61
|
for (const [id, key] of keys) if (visible.has(id)) out[key] = { type: 'http', url: `${base}/relay/${id}`, headersHelper: HELPER };
|
|
62
|
+
if (mcpUrl !== null) out[MCP_SERVER_KEY] = { type: 'http', url: mcpUrl, headersHelper: MCP_HELPER };
|
|
88
63
|
return out;
|
|
89
64
|
}
|
|
90
65
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { registerConnectors, type GateRow } from './gates.js';
|
|
2
2
|
import { syncPluginServers } from './plugin-sync.js';
|
|
3
3
|
import { persistentServerKeys } from './server-keys.js';
|
|
4
|
-
import { isOrgRelayId } from './relay-id.js';
|
|
5
|
-
import { migrationRecords } from './migration-state.js';
|
|
6
4
|
|
|
7
|
-
let local: GateRow[] = [];
|
|
8
5
|
let organization: GateRow[] | null = null;
|
|
6
|
+
const BOOT = Date.now();
|
|
7
|
+
const READY_WAIT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
export const connectorsReady = (now = Date.now()): boolean => organization !== null || now - BOOT >= READY_WAIT_MS;
|
|
9
10
|
|
|
10
11
|
function metadata(rows: readonly GateRow[]): GateRow[] {
|
|
11
12
|
return rows.map((row) => structuredClone({
|
|
@@ -18,41 +19,19 @@ function metadata(rows: readonly GateRow[]): GateRow[] {
|
|
|
18
19
|
}));
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function publishedRows(records: ReturnType<typeof migrationRecords>, organization: GateRow[]): GateRow[] {
|
|
22
|
-
const retired = new Set(records.filter((record) => !['preparing', 'prepared'].includes(record.phase)).map((record) => record.sourceId));
|
|
23
|
-
const aliases = new Map(organization.filter((row) => records.some((record) => record.phase === 'active' && record.sourceId === row.id && record.name === row.name)).map((row) => [row.id, row]));
|
|
24
|
-
const rows = local.flatMap((row) => {
|
|
25
|
-
const alias = aliases.get(row.id);
|
|
26
|
-
return alias !== undefined ? [alias] : retired.has(row.id) ? [] : [row];
|
|
27
|
-
});
|
|
28
|
-
rows.push(...organization.filter((row) => isOrgRelayId(row.id) && !records.some((record) => row.id === `org-${record.destinationId ?? ''}`)));
|
|
29
|
-
return rows;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
22
|
export function publishConnectors(): void {
|
|
33
|
-
|
|
23
|
+
if (organization === null) return;
|
|
34
24
|
let keys: Map<string, string>;
|
|
35
25
|
try {
|
|
36
|
-
|
|
37
|
-
if (organization === null && records.some((record) => record.phase === 'active')) return;
|
|
38
|
-
const org = organization ?? [];
|
|
39
|
-
rows = publishedRows(records, org);
|
|
40
|
-
const active = new Set(records.filter((record) => record.phase === 'active').map((record) => record.sourceId));
|
|
41
|
-
const reserved = [...rows, ...local.filter((row) => active.has(row.id))];
|
|
42
|
-
keys = persistentServerKeys([...local, ...org.filter((row) => isOrgRelayId(row.id))], undefined, reserved);
|
|
26
|
+
keys = persistentServerKeys(organization);
|
|
43
27
|
}
|
|
44
28
|
catch (error) {
|
|
45
29
|
registerConnectors([]);
|
|
46
30
|
syncPluginServers([]);
|
|
47
31
|
throw error;
|
|
48
32
|
}
|
|
49
|
-
registerConnectors(
|
|
50
|
-
syncPluginServers(
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function publishLocalConnectors(rows: readonly GateRow[]): void {
|
|
54
|
-
local = metadata(rows.filter((row) => !isOrgRelayId(row.id)));
|
|
55
|
-
publishConnectors();
|
|
33
|
+
registerConnectors(organization, keys);
|
|
34
|
+
syncPluginServers(organization, { keys });
|
|
56
35
|
}
|
|
57
36
|
|
|
58
37
|
export function publishOrgConnectors(rows: readonly GateRow[]): void {
|