@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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +2 -2
  3. package/runtime/marketplace/plugin/METRO.md +2 -0
  4. package/runtime/marketplace/plugin/bin/guard.mjs +2 -1
  5. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +20 -2
  6. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +4 -1
  7. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -11
  8. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +10 -3
  9. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +16 -8
  10. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +6 -3
  11. package/runtime/node_modules/@metro-labs/daemon/src/channels/notice.ts +22 -0
  12. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +3 -1
  13. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +1 -0
  14. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -11
  15. package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-adopt.ts +57 -0
  16. package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime.ts +39 -95
  17. package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-control.ts +27 -45
  18. package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync-watch.ts +2 -17
  19. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +12 -37
  20. package/runtime/node_modules/@metro-labs/daemon/src/connectors/publication.ts +8 -29
  21. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-session.ts +0 -1
  22. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +33 -138
  23. package/runtime/node_modules/@metro-labs/daemon/src/connectors/server-keys.ts +22 -9
  24. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -0
  25. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +27 -8
  26. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +6 -4
  27. package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +109 -0
  28. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/config.ts +29 -0
  29. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/link.ts +186 -0
  30. package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/token.ts +50 -0
  31. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +4 -58
  32. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +0 -12
  33. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +0 -7
  34. package/runtime/node_modules/@metro-labs/http/package.json +2 -1
  35. package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +2 -1
  36. package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +81 -0
  37. package/runtime/runtime.json +1 -1
  38. package/runtime/sdk-runner/runner.json +1 -1
  39. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +0 -316
  40. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +0 -105
  41. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +0 -18
  42. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-api.ts +0 -60
  43. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-control.ts +0 -366
  44. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-guard.ts +0 -95
  45. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-operations.ts +0 -226
  46. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-preview.ts +0 -235
  47. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-source.ts +0 -42
  48. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-state.ts +0 -142
  49. package/runtime/node_modules/@metro-labs/daemon/src/connectors/migration-url.ts +0 -9
  50. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +0 -178
  51. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +0 -171
  52. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-passive.ts +0 -96
  53. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +0 -393
  54. package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-api.ts +0 -116
  55. package/runtime/node_modules/@metro-labs/daemon/src/connectors/transfer-crypto.ts +0 -44
  56. 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 type { MigrationRuntimeMapping } from './migration-guard.js';
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: RuntimeRow): string => JSON.stringify([
51
- row.relayId, row.relayName, row.name, row.url, row.auth, row.policy.read ?? 'allow', row.policy.write ?? 'allow',
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 enabled(): boolean {
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
- if (!drain) session.controller.abort();
77
+ session.controller.abort();
95
78
  }
96
79
 
97
- private publish(rows: RuntimeRow[]): void {
80
+ private publish(rows: DeliveredConnector[]): void {
98
81
  const metadata: GateRow[] = rows.map((row) => ({
99
- id: row.relayId, name: row.relayName,
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: RuntimeRow[], epoch: number, available: boolean): void {
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
- const error = valid ? rowError(row) ?? orgUnavailable() : orgUnavailable();
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, row: RuntimeRow | undefined, error: ApiError): void {
125
- const drain = !isOrgRelayId(entry.relayId) && row?.relayId !== entry.relayId;
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.abortEntry(entry, error, drain);
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 mappedRows(rows: DeliveredConnector[], mapping: MigrationRuntimeMapping): RuntimeRow[] {
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 mapping = this.mapping();
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, wanted, epoch)) return null;
155
- const rows = this.mappedRows(snapshot?.connectors ?? [], mapping);
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: RuntimeRow[], epoch: number): void {
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, relayId: row.relayId, epoch, stamp: stampOf(row), controller: new AbortController(), groups: {}, scan: null, nextScan: 0, error: null });
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, wanted: boolean, epoch: number): boolean {
175
- if (snapshot !== null || !wanted || !this.sync.lapsed()) return false;
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 operation = this.migrationOperation(entry);
227
- try {
228
- const send = this.deps.fetch ?? fetch;
229
- const groups = await discoverOrgGroups(row.url, {
230
- fresh: (revision) => this.fresh(entry, revision === undefined ? undefined : { connectorId: entry.id, revision }),
231
- current: () => this.currentEntry(entry), signal,
232
- fetch: (input, init) => operation?.dispatch(() => send(input, init), { effectful: migrationRequestEffectful(init?.method, init?.body) }) ?? send(input, init),
233
- });
234
- this.currentEntry(entry);
235
- if (JSON.stringify(groups) !== JSON.stringify(entry.groups)) {
236
- for (const session of this.sessions) if (session.entry === entry && session.revision !== null) this.abort(session, orgUnavailable());
237
- }
238
- entry.groups = groups;
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 mapped = rows.find((row) => row.relayId === id);
295
- const error = rowError(mapped);
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 = mapped === undefined ? undefined : this.entries.get(mapped.id);
242
+ const entry = found === undefined ? undefined : this.entries.get(found.id);
298
243
  if (entry === undefined) throw orgUnavailable();
299
- const session: Session = { entry, operation: this.migrationOperation(entry), controller: new AbortController(), revision: null, error: null };
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()); session.operation?.close(); this.sessions.delete(session); },
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 type { DeliverySnapshot } from '@metro-labs/http/connector-delivery';
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 readonly draining = new Set<OrgConnectorRuntime>();
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.stop();
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) this.clear();
77
- else if (start && this.sync === null) {
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
- const mapping = () => migrationRuntimeMapping(this.deps.owner(), this.deps.key(), this.dir);
88
- if (liveEnabled() || mapping().aliases.length > 0) this.runtime = new OrgConnectorRuntime(this.sync, { enabled: liveEnabled, mapping, migrationDir: this.dir });
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: 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)) return;
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> | null {
17
- return isOrgRelayId(id) || migrationSource(id)?.phase === 'active' ? control().open(id) : null;
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 { isOrgRelayId } from './relay-id.js';
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
- export function serverKeysOf(rows: PluginRow[], saved?: ReadonlyMap<string, string>, published: PluginRow[] = rows): Map<string, string> {
67
- const keys = new Map(saved ?? legacyOwners(rows, published));
68
- const reserved = new Set(keys.values());
69
- const local = rows.filter((row) => !isOrgRelayId(row.id));
70
- const organization = rows.filter((row) => isOrgRelayId(row.id)).sort((a, b) => a.id.localeCompare(b.id));
71
- for (const row of [...local, ...organization]) {
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 slug = serverKey(row.name);
74
- const wanted = isOrgRelayId(row.id)
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
- let rows: GateRow[];
23
+ if (organization === null) return;
34
24
  let keys: Map<string, string>;
35
25
  try {
36
- const records = migrationRecords().filter((record) => record.phase !== 'rolled_back');
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(rows, keys);
50
- syncPluginServers(rows, { keys });
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 {
@@ -7,5 +7,4 @@ export interface RelaySession {
7
7
  blocker: () => BlockedReason;
8
8
  signal: AbortSignal;
9
9
  close: () => void;
10
- dispatch?: (send: () => Promise<Response>, effectful?: boolean) => Promise<Response>;
11
10
  }