@stage-labs/metro 0.1.0-beta.297 → 0.1.0-beta.299
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/connectors/migration-guard.ts +2 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime-discovery.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-runtime.ts +20 -8
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/org-sync.ts +14 -3
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.299",
|
|
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",
|
|
@@ -8,6 +8,7 @@ import { assertMigrationOutcomeKnown, beginMigrationOperation, drainMigrationLis
|
|
|
8
8
|
const locked = (phase: string): boolean => phase !== 'rolled_back';
|
|
9
9
|
const retired = (record: NonNullable<ReturnType<typeof migrationRecord>>): boolean => record.move === true || record.phase === 'cutover' || record.phase === 'active' || record.phase === 'rollback';
|
|
10
10
|
const refused = (): ApiError => new ApiError('This local connector is locked while it moves to the organization.', 409);
|
|
11
|
+
const editRefused = (): ApiError => new ApiError('This local connector is locked while it moves to the organization. To change or remove it, open Move to organization and roll back its migration first.', 409);
|
|
11
12
|
|
|
12
13
|
export function migrationSource(id: string, dir = agentsDir()): ReturnType<typeof migrationRecords>[number] | null {
|
|
13
14
|
const record = migrationRecord(id, dir);
|
|
@@ -27,7 +28,7 @@ export function assertLocalMigrationUse(id: string, dir = agentsDir()): void {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function assertLocalMigrationEdit(id: string, dir = agentsDir()): void {
|
|
30
|
-
if (migrationRecords(dir).some((record) => locked(record.phase) && record.sourceId === id)) throw
|
|
31
|
+
if (migrationRecords(dir).some((record) => locked(record.phase) && record.sourceId === id)) throw editRefused();
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function migrationExportableRows<T extends { id: string; name: string }>(rows: readonly T[], dir = agentsDir()): T[] {
|
|
@@ -98,8 +98,9 @@ export async function discoverOrgGroups(url: string, guard: DiscoveryGuard): Pro
|
|
|
98
98
|
try {
|
|
99
99
|
return await guardedRequest(input, init, guard, budget);
|
|
100
100
|
} catch (error) {
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const safe = orgRuntimeError(error);
|
|
102
|
+
if (init?.method !== 'DELETE' || safe.status !== 502) failure.error = safe;
|
|
103
|
+
throw safe;
|
|
103
104
|
}
|
|
104
105
|
};
|
|
105
106
|
const tools = await listRemoteTools(new URL(url), { kind: 'none' }, send);
|
|
@@ -3,7 +3,7 @@ import { agentsDir } from '../agents/files.js';
|
|
|
3
3
|
import { assertMigrationOutcomeKnown, beginMigrationOperation, migrationRequestEffectful, type MigrationOperation } from './migration-operations.js';
|
|
4
4
|
import type { FetchLike } from '@metro-labs/core/stations/oauth';
|
|
5
5
|
import type { ToolGroup } from '@metro-labs/core/stations/types';
|
|
6
|
-
import { effectiveConnectorPolicy, type DeliveredConnector } from '@metro-labs/http/connector-delivery';
|
|
6
|
+
import { effectiveConnectorPolicy, type DeliveredConnector, type DeliverySnapshot } from '@metro-labs/http/connector-delivery';
|
|
7
7
|
import type { ApiError } from '@metro-labs/http/api-error';
|
|
8
8
|
import { requestConnectorBlocker, type GateRow } from './gates.js';
|
|
9
9
|
import { publishOrgConnectors } from './publication.js';
|
|
@@ -92,7 +92,7 @@ export class OrgConnectorRuntime {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
private publish(rows: RuntimeRow[]): void {
|
|
95
|
-
const metadata: GateRow[] = rows.
|
|
95
|
+
const metadata: GateRow[] = rows.map((row) => ({
|
|
96
96
|
id: row.relayId, name: row.relayName,
|
|
97
97
|
config: { policy: effectiveConnectorPolicy(row, this.entries.get(row.id)?.groups ?? {}), toolGroups: { ...this.entries.get(row.id)?.groups } },
|
|
98
98
|
}));
|
|
@@ -145,14 +145,13 @@ export class OrgConnectorRuntime {
|
|
|
145
145
|
private update(): RuntimeRow[] | null {
|
|
146
146
|
try {
|
|
147
147
|
const mapping = this.mapping();
|
|
148
|
-
const
|
|
149
|
-
const
|
|
148
|
+
const wanted = this.enabled() || mapping.aliases.length > 0;
|
|
149
|
+
const snapshot = wanted ? this.sync.read() : null;
|
|
150
150
|
const epoch = this.sync.epoch();
|
|
151
|
+
if (this.held(snapshot, wanted, epoch)) return null;
|
|
152
|
+
const rows = this.mappedRows(snapshot?.connectors ?? [], mapping);
|
|
151
153
|
this.invalidateEntries(rows, epoch, snapshot !== null);
|
|
152
|
-
|
|
153
|
-
if (row.state !== 'ready' || this.entries.has(row.id)) continue;
|
|
154
|
-
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 });
|
|
155
|
-
}
|
|
154
|
+
this.track(rows, epoch);
|
|
156
155
|
this.publish(rows);
|
|
157
156
|
return snapshot === null ? null : rows;
|
|
158
157
|
} catch {
|
|
@@ -162,6 +161,19 @@ export class OrgConnectorRuntime {
|
|
|
162
161
|
}
|
|
163
162
|
}
|
|
164
163
|
|
|
164
|
+
private track(rows: RuntimeRow[], epoch: number): void {
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (row.state !== 'ready' || this.entries.has(row.id)) continue;
|
|
167
|
+
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 });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private held(snapshot: DeliverySnapshot | null, wanted: boolean, epoch: number): boolean {
|
|
172
|
+
if (snapshot !== null || !wanted || !this.sync.lapsed()) return false;
|
|
173
|
+
this.invalidateEntries([], epoch, false);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
165
177
|
refresh(): void {
|
|
166
178
|
this.update();
|
|
167
179
|
}
|
|
@@ -5,7 +5,7 @@ import { ApiError } from '@metro-labs/http/api-error';
|
|
|
5
5
|
import { AGENT_CLOCK_SKEW_MS } from '@metro-labs/http/agent-signature';
|
|
6
6
|
import { CONNECTOR_DELIVERY_LEASE_MS, CONNECTOR_DELIVERY_MAX_BYTES, openConnectorDelivery, type DeliveryContext, type DeliverySnapshot } from '@metro-labs/http/connector-delivery';
|
|
7
7
|
import type { AgentKey } from './agent-key.js';
|
|
8
|
-
import { agentCall, type MetroApi } from './metro-api.js';
|
|
8
|
+
import { agentCall, MetroApiRefusal, type MetroApi } from './metro-api.js';
|
|
9
9
|
|
|
10
10
|
interface Identity {
|
|
11
11
|
key: AgentKey;
|
|
@@ -93,6 +93,12 @@ export class OrgConnectorSync {
|
|
|
93
93
|
this.expiry = null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
private refused = false;
|
|
97
|
+
|
|
98
|
+
lapsed(): boolean {
|
|
99
|
+
return !this.refused && (this.receipt.state === 'expired' || this.receipt.state === 'unavailable');
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
invalidate(reason: 'waiting' | 'expired' | 'unavailable' = 'waiting'): void {
|
|
97
103
|
this.generation += 1;
|
|
98
104
|
this.controller?.abort();
|
|
@@ -147,8 +153,13 @@ export class OrgConnectorSync {
|
|
|
147
153
|
this.token = identity.token;
|
|
148
154
|
const generation = this.generation;
|
|
149
155
|
this.controller = new AbortController();
|
|
150
|
-
this.running = this.fetch(identity, refresh, generation, this.controller.signal).
|
|
151
|
-
|
|
156
|
+
this.running = this.fetch(identity, refresh, generation, this.controller.signal).then(() => {
|
|
157
|
+
this.refused = false;
|
|
158
|
+
}, (err: unknown) => {
|
|
159
|
+
if (generation === this.generation) {
|
|
160
|
+
this.refused = err instanceof MetroApiRefusal && err.upstreamStatus < 500;
|
|
161
|
+
this.invalidate('unavailable');
|
|
162
|
+
}
|
|
152
163
|
throw err;
|
|
153
164
|
}).finally(() => { this.running = null; });
|
|
154
165
|
return this.running;
|
package/runtime/runtime.json
CHANGED