@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 +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/agent-key.ts +9 -12
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/enroll-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/leftovers.ts +59 -0
- package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +5 -20
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/runner.json +1 -1
package/package.json
CHANGED
|
@@ -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
|
|
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 ===
|
|
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,
|
|
46
|
+
function agentKeyOf(signing: KeyObject, enrollment: Enrollment | null): AgentKey {
|
|
49
47
|
const signingKey = rawPublicKey(createPublicKey(signing));
|
|
50
|
-
return { keyId: agentKeyId(signingKey), signingKey,
|
|
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 =
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
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)
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
38
|
-
return key.asymmetricKeyType ===
|
|
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
|
|
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');
|
package/runtime/runtime.json
CHANGED