@stage-labs/metro 0.1.0-beta.306 → 0.1.0-beta.307

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.306",
3
+ "version": "0.1.0-beta.307",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,6 +25,7 @@ import { stopMcpServer, watchMcpServer } from '../mcp-server/link.js';
25
25
  import { refreshConnectorTools, watchConnectorTools } from '../mcp-server/connector-tools.js';
26
26
  import { setAnnouncedFile } from '../mcp-server/announced.js';
27
27
  import { holdServerApproval } from '../mcp/server-approvals.js';
28
+ import { removeConnectorLeftovers } from '../connectors/leftovers.js';
28
29
  import { removeMetroPlugin } from '../claude/plugin-cleanup.js';
29
30
  import { runningAsRoot } from '../metro-user/privilege.js';
30
31
  import { provisionAgentUser } from '../agent-user/provision.js';
@@ -139,6 +140,7 @@ function sessionApis(setLiveEvents: (on: boolean) => void, switchModel: (model:
139
140
 
140
141
 
141
142
  async function main(): Promise<void> {
143
+ removeConnectorLeftovers();
142
144
  applyLocalOwner();
143
145
  installBearerSessions(agentsDir(), localOwner);
144
146
  watchPolicySnapshot();
@@ -21,19 +21,17 @@ export interface Enrollment {
21
21
  export interface AgentKey {
22
22
  keyId: string;
23
23
  signingKey: string;
24
- sealingKey: string;
25
24
  signing: KeyObject;
26
- sealing: KeyObject;
27
25
  enrollment: Enrollment | null;
28
26
  }
29
27
 
30
28
  const pathOf = (dir: string): string => join(dir, FILE);
31
29
 
32
- function privateKeyOf(raw: unknown, type: 'ed25519' | 'x25519'): KeyObject | null {
30
+ function privateSigningKey(raw: unknown): KeyObject | null {
33
31
  if (typeof raw !== 'string' || raw === '') return null;
34
32
  try {
35
33
  const key = createPrivateKey({ key: Buffer.from(raw, 'base64'), format: 'der', type: 'pkcs8' });
36
- return key.asymmetricKeyType === type ? key : null;
34
+ return key.asymmetricKeyType === 'ed25519' ? key : null;
37
35
  } catch {
38
36
  return null;
39
37
  }
@@ -45,9 +43,9 @@ function enrollmentOf(raw: unknown): Enrollment | null {
45
43
  return server !== null && isOrganizationId(raw.organization) ? { server, organization: raw.organization, at: raw.at } : null;
46
44
  }
47
45
 
48
- function agentKeyOf(signing: KeyObject, sealing: KeyObject, enrollment: Enrollment | null): AgentKey {
46
+ function agentKeyOf(signing: KeyObject, enrollment: Enrollment | null): AgentKey {
49
47
  const signingKey = rawPublicKey(createPublicKey(signing));
50
- return { keyId: agentKeyId(signingKey), signingKey, sealingKey: rawPublicKey(createPublicKey(sealing)), signing, sealing, enrollment };
48
+ return { keyId: agentKeyId(signingKey), signingKey, signing, enrollment };
51
49
  }
52
50
 
53
51
  const unreadable = (): null => {
@@ -67,10 +65,9 @@ function storedOf(path: string): Record<string, unknown> | null {
67
65
 
68
66
  function keyOf(stored: Record<string, unknown> | null): AgentKey | null {
69
67
  if (stored === null) return null;
70
- const signing = privateKeyOf(stored.signing, 'ed25519');
71
- const sealing = privateKeyOf(stored.sealing, 'x25519');
72
- if (stored.version !== 1 || signing === null || sealing === null) return unreadable();
73
- return agentKeyOf(signing, sealing, enrollmentOf(stored.enrollment));
68
+ const signing = privateSigningKey(stored.signing);
69
+ if (stored.version !== 1 || signing === null) return unreadable();
70
+ return agentKeyOf(signing, enrollmentOf(stored.enrollment));
74
71
  }
75
72
 
76
73
  export function readAgentKey(dir = agentsDir()): AgentKey | null {
@@ -132,12 +129,12 @@ export function clearPendingAgentKey(dir: string): void {
132
129
  }
133
130
 
134
131
  export function newAgentKey(): AgentKey {
135
- return agentKeyOf(generateKeyPairSync('ed25519').privateKey, generateKeyPairSync('x25519').privateKey, null);
132
+ return agentKeyOf(generateKeyPairSync('ed25519').privateKey, null);
136
133
  }
137
134
 
138
135
  const exported = (key: KeyObject): string => key.export({ format: 'der', type: 'pkcs8' }).toString('base64');
139
136
 
140
- const privateKeys = (key: AgentKey): Record<string, unknown> => ({ version: 1, signing: exported(key.signing), sealing: exported(key.sealing) });
137
+ const privateKeys = (key: AgentKey): Record<string, unknown> => ({ version: 1, signing: exported(key.signing) });
141
138
 
142
139
  export function savePendingAgentKey(key: AgentKey, owner: string, ticketHash: string, dir: string, expectedServer?: string): void {
143
140
  writeKey(pendingPath(dir), { ...privateKeys(key), owner, ticketHash, ...(expectedServer === undefined ? {} : { expectedServer }) });
@@ -66,7 +66,7 @@ async function exchange(ticket: string, owner: string, dir: string, deps: Enroll
66
66
  if (finished !== null) return finished;
67
67
  const key = pending?.ticketHash === ticketHash ? pending : newAgentKey();
68
68
  if (key !== pending) savePendingAgentKey(key, owner, ticketHash, dir);
69
- const enrolled = enrolledOf(await agentCall(api, key, '/api/agents/enroll', { ticket, organization: owner, signingKey: key.signingKey, sealingKey: key.sealingKey }), key, owner);
69
+ const enrolled = enrolledOf(await agentCall(api, key, '/api/agents/enroll', { ticket, organization: owner, signingKey: key.signingKey }), key, owner);
70
70
  sameOwner(deps, owner);
71
71
  savePendingAgentKey(key, owner, ticketHash, dir, enrolled.server);
72
72
  const confirmed = enrolledOf(await agentCall(api, key, '/api/agents/session'), key, owner);
@@ -0,0 +1,59 @@
1
+ import { lstatSync, readdirSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { errMsg, log } from '@metro-labs/core/log';
4
+ import { agentsDir } from '../agents/files.js';
5
+
6
+ const WRITTEN = new Set([
7
+ 'connectors.json',
8
+ 'connector-migrations.json',
9
+ 'connector-migrations.initialized',
10
+ 'connector-server-keys.json',
11
+ 'connector-server-keys.initialized',
12
+ 'org-connector-sync.json',
13
+ ]);
14
+
15
+ const OPERATIONS = 'connector-migration-operations-[A-Za-z0-9][A-Za-z0-9_-]{10}\\.json';
16
+
17
+ const PATTERNS = [
18
+ new RegExp(`^${OPERATIONS}(?:\\.initialized)?$`),
19
+ new RegExp(`^(?:connectors\\.json|connector-server-keys\\.json|connector-server-keys\\.initialized|org-connector-sync\\.json|${OPERATIONS})\\.tmp-[0-9]+$`),
20
+ /^connector-migrations\.json\.[0-9a-f]{24}\.tmp$/,
21
+ ];
22
+
23
+ const isConnectorLeftover =(name: string): boolean => WRITTEN.has(name) || PATTERNS.some((pattern) => pattern.test(name));
24
+
25
+ const codeOf = (err: unknown): unknown => (err instanceof Error && 'code' in err ? err.code : undefined);
26
+
27
+ function namesIn(dir: string): string[] {
28
+ try {
29
+ return readdirSync(dir).filter(isConnectorLeftover);
30
+ } catch (err) {
31
+ if (codeOf(err) !== 'ENOENT') log.warn({ err: errMsg(err), dir }, 'connectors: could not list the old connector files');
32
+ return [];
33
+ }
34
+ }
35
+
36
+ function removeOne(path: string): boolean {
37
+ try {
38
+ if (!lstatSync(path).isFile()) {
39
+ log.warn({ path }, 'connectors: left an old connector file alone, it is not a regular file');
40
+ return false;
41
+ }
42
+ unlinkSync(path);
43
+ return true;
44
+ } catch (err) {
45
+ if (codeOf(err) !== 'ENOENT') log.warn({ err: errMsg(err), path }, 'connectors: could not remove an old connector file');
46
+ return false;
47
+ }
48
+ }
49
+
50
+ export function removeConnectorLeftovers(dir = agentsDir()): number {
51
+ try {
52
+ const removed = namesIn(dir).filter((name) => removeOne(join(dir, name))).length;
53
+ log.info({ removed }, 'connectors: removed the files of the old connector path');
54
+ return removed;
55
+ } catch (err) {
56
+ log.warn({ err: errMsg(err) }, 'connectors: could not remove the files of the old connector path');
57
+ return 0;
58
+ }
59
+ }
@@ -1,4 +1,4 @@
1
- import { createHash, createPublicKey, diffieHellman, generateKeyPairSync, randomBytes, sign, verify, type KeyObject } from 'node:crypto';
1
+ import { createHash, createPublicKey, randomBytes, sign, verify, type KeyObject } from 'node:crypto';
2
2
  import { strongEd25519 } from './ed25519.js';
3
3
 
4
4
  export const AGENT_SCHEME = 'MetroAgent';
@@ -25,40 +25,25 @@ export interface AgentProof {
25
25
  signature: string;
26
26
  }
27
27
 
28
- type Curve = 'Ed25519' | 'X25519';
29
-
30
28
  const digest = (bytes: Uint8Array): string => createHash('sha256').update(bytes).digest('base64url');
31
29
 
32
30
  export const agentKeyId = (signingKey: string): string => digest(Buffer.from(signingKey, 'base64url'));
33
31
 
34
- function publicKeyOf(raw: string, curve: Curve): KeyObject | null {
32
+ function publicKeyOf(raw: string): KeyObject | null {
35
33
  if (!RAW_KEY_RE.test(raw) || Buffer.from(raw, 'base64url').toString('base64url') !== raw) return null;
36
34
  try {
37
- const key = createPublicKey({ key: { kty: 'OKP', crv: curve, x: raw }, format: 'jwk' });
38
- return key.asymmetricKeyType === curve.toLowerCase() ? key : null;
35
+ const key = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: raw }, format: 'jwk' });
36
+ return key.asymmetricKeyType === 'ed25519' ? key : null;
39
37
  } catch {
40
38
  return null;
41
39
  }
42
40
  }
43
41
 
44
42
  export function signingKeyOf(raw: string): KeyObject | null {
45
- const key = publicKeyOf(raw, 'Ed25519');
43
+ const key = publicKeyOf(raw);
46
44
  return key !== null && strongEd25519(Buffer.from(raw, 'base64url')) ? key : null;
47
45
  }
48
46
 
49
- function contributes(key: KeyObject): boolean {
50
- try {
51
- return diffieHellman({ privateKey: generateKeyPairSync('x25519').privateKey, publicKey: key }).some((byte) => byte !== 0);
52
- } catch {
53
- return false;
54
- }
55
- }
56
-
57
- export function sealingKeyOf(raw: string): KeyObject | null {
58
- const key = publicKeyOf(raw, 'X25519');
59
- return key !== null && contributes(key) ? key : null;
60
- }
61
-
62
47
  export function rawPublicKey(key: KeyObject): string {
63
48
  const { x } = key.export({ format: 'jwk' });
64
49
  if (typeof x !== 'string') throw new Error('not an OKP public key');
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.306"
2
+ "version": "0.1.0-beta.307"
3
3
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0-beta.306",
2
+ "version": "0.1.0-beta.307",
3
3
  "dependencies": {
4
4
  "@anthropic-ai/claude-agent-sdk": "0.3.287",
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",