@karma42/agent 0.1.0-beta.10 → 0.1.0-beta.12
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/README.md +15 -13
- package/package.json +1 -1
- package/runtime/control-v2.mjs +67 -9
- package/runtime/manifest.json +3 -3
- package/runtime/project-tools.json +1 -1
- package/runtime/turn-report-journal.mjs +81 -0
- package/skills/karma42/SKILL.md +22 -4
- package/src/cli.mjs +3 -15
- package/src/layout.mjs +20 -0
- package/src/operation-lock.mjs +46 -0
- package/src/owner-client.mjs +143 -19
- package/src/reusable-stdio.mjs +79 -106
- package/src/session-stdio.mjs +1 -1
- package/src/system.mjs +20 -20
- package/src/version.mjs +3 -1
- package/src/worker-runtime.mjs +10 -6
- package/src/browser-batch.mjs +0 -130
- package/src/browser-pairing.mjs +0 -140
package/src/browser-batch.mjs
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
|
2
|
-
import { join, isAbsolute } from 'node:path';
|
|
3
|
-
import { projectIdentity } from './project-identity.mjs';
|
|
4
|
-
import { assertStateOutsideWorkspace, canonicalPath, connectionDir, digest, load, lstatExists, readPrivate, save, writePrivate } from './state.mjs';
|
|
5
|
-
import { sessionHome, sessionId, validateSession } from './session.mjs';
|
|
6
|
-
import { browserApprovalUrl, withBrowserLock } from './browser-pairing.mjs';
|
|
7
|
-
import { system } from './system.mjs';
|
|
8
|
-
|
|
9
|
-
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
10
|
-
const TOKEN = /^[\w-]{43}$/;
|
|
11
|
-
const uuid = /^[a-f0-9-]{36}$/;
|
|
12
|
-
const validClient = value => typeof value === 'string' && value.trim() && value.length <= 100 && !/[\x00-\x1f\x7f-\x9f]/.test(value);
|
|
13
|
-
const batchPath = dir => join(dir, 'pending-batch.json');
|
|
14
|
-
const agentPath = dir => join(dir, 'pending-browser.json');
|
|
15
|
-
function select(options, home) {
|
|
16
|
-
if (!validClient(options.clientName) || !Array.isArray(options.agents) || options.agents.length < 1 || options.agents.length > 10) throw new Error('Use a valid client name and one to ten explicitly selected agents for one project.');
|
|
17
|
-
const agents = options.agents.map(agent => {
|
|
18
|
-
if (!agent || Object.keys(agent).some(key => !['agent_id', 'agent_name', 'folder'].includes(key)) || !/^[\w-]{1,200}$/.test(agent.agent_id ?? '') || !/^[\w-]{1,64}$/.test(agent.agent_name ?? '') || typeof agent.folder !== 'string' || !isAbsolute(agent.folder)) throw new Error('Use each agent’s exact ID, name and approved absolute working folder.');
|
|
19
|
-
const folder = canonicalPath(agent.folder); assertStateOutsideWorkspace(home, folder);
|
|
20
|
-
const plan = projectIdentity({ projectUrl: options.project_url, folder });
|
|
21
|
-
return { id: sessionId(plan.projectUrl, agent.agent_id), service: plan.projectUrl, agentId: agent.agent_id, agentName: agent.agent_name, folder };
|
|
22
|
-
}).sort((a,b) => a.agentId.localeCompare(b.agentId));
|
|
23
|
-
if (new Set(agents.map(agent => agent.agentId)).size !== agents.length) throw new Error('Use distinct agent IDs in a batch.');
|
|
24
|
-
return agents;
|
|
25
|
-
}
|
|
26
|
-
function validate(receipt, id, home) {
|
|
27
|
-
if (receipt?.schema !== 'mcp-browser-batch-1' || receipt.id !== id || !uuid.test(receipt.batchId ?? '') || !validClient(receipt.clientName) || !Array.isArray(receipt.agents)) throw new Error('Invalid private batch receipt. Existing connections preserved.');
|
|
28
|
-
const agents = select({ project_url: receipt.service, clientName: receipt.clientName, agents: receipt.agents.map(agent => ({ agent_id: agent.agentId, agent_name: agent.agentName, folder: agent.folder })) }, home);
|
|
29
|
-
const expected = sessionId(receipt.service, `batch-${digest([receipt.clientName, agents]).slice(0,32)}`);
|
|
30
|
-
if (expected !== id || agents.length !== receipt.agents.length) throw new Error('Invalid private batch identity. Existing connections preserved.');
|
|
31
|
-
for (let index = 0; index < agents.length; index++) {
|
|
32
|
-
const request = receipt.agents[index];
|
|
33
|
-
if (JSON.stringify(agents[index]) !== JSON.stringify({ id: request.id, service: request.service, agentId: request.agentId, agentName: request.agentName, folder: request.folder }) || !uuid.test(request.requestId ?? '') || !TOKEN.test(request.candidateToken ?? '') || !TOKEN.test(request.pollingSecret ?? '')) throw new Error('Invalid private batch request. Existing connections preserved.');
|
|
34
|
-
}
|
|
35
|
-
if (new Set(receipt.agents.map(agent => agent.requestId)).size !== agents.length) throw new Error('Invalid private batch request IDs.');
|
|
36
|
-
return receipt;
|
|
37
|
-
}
|
|
38
|
-
export function createBatchBrowserOperations({ home = sessionHome(), io, sys = system } = {}) {
|
|
39
|
-
home = canonicalPath(home);
|
|
40
|
-
const locks = (agents, action, index = 0) => index === agents.length ? action() : withBrowserLock(connectionDir(home, agents[index].id), () => locks(agents, action, index + 1));
|
|
41
|
-
const summary = receipt => ({ id: receipt.id, batchId: receipt.batchId, status: receipt.status, approvalUrl: browserApprovalUrl({ service: receipt.service, requestId: receipt.batchId }), requestCheck: receipt.verificationCode, agents: receipt.agents.map(({ id, agentId, agentName, folder }) => ({ id, agentId, agentName, folder })) });
|
|
42
|
-
function writeAgent(receipt, agent) {
|
|
43
|
-
writePrivate(agentPath(connectionDir(home, agent.id)), { schema: 'mcp-browser-pairing-1', ...agent, clientName: receipt.clientName, batchId: receipt.batchId, createdAt: receipt.createdAt, registered: true, expiresAt: receipt.expiresAt, status: agent.status === 'saved' ? 'saved' : receipt.status });
|
|
44
|
-
}
|
|
45
|
-
async function register(dir, receipt) {
|
|
46
|
-
let reply;
|
|
47
|
-
try { reply = await sys.request(receipt.service, 'pairing-batches/start', { batchId: receipt.batchId, requests: receipt.agents.map(agent => ({ requestId: agent.requestId, agentId: agent.agentId, clientName: receipt.clientName, folder: agent.folder, pollingSecret: agent.pollingSecret, candidateTokenHash: hash(agent.candidateToken) })) }); }
|
|
48
|
-
catch { throw new Error(`Browser batch reply unavailable. Your private batch is saved. Retry this exact batch with connection ${receipt.id}; do not create another request.`); }
|
|
49
|
-
if (reply?.batchId !== receipt.batchId || !['pending', 'approved', 'cancelled', 'expired'].includes(reply.status) || !/^[A-F0-9]{8}$/.test(reply.verificationCode ?? '')) throw new Error('Unexpected browser batch reply. Private requests preserved.');
|
|
50
|
-
receipt = { ...receipt, registered: true, status: reply.status, expiresAt: reply.expiresAt, verificationCode: reply.verificationCode };
|
|
51
|
-
writePrivate(batchPath(dir), receipt);
|
|
52
|
-
for (const agent of receipt.agents) writeAgent(receipt, agent);
|
|
53
|
-
return receipt;
|
|
54
|
-
}
|
|
55
|
-
function recordFor(agent) {
|
|
56
|
-
const dir = connectionDir(home, agent.id), record = validateSession(load(dir), agent.id);
|
|
57
|
-
if (record.service !== agent.service || record.agentId !== agent.agentId || record.agentName !== agent.agentName || record.folder !== agent.folder || readPrivate(join(dir,'token')).trim() !== agent.candidateToken) throw new Error('Browser batch was superseded by another connection. Use the current exact agent approval; credentials were preserved.');
|
|
58
|
-
return record;
|
|
59
|
-
}
|
|
60
|
-
function currentAgents(receipt) {
|
|
61
|
-
for (const agent of receipt.agents) {
|
|
62
|
-
const path = agentPath(connectionDir(home,agent.id));
|
|
63
|
-
if (!lstatExists(path)) throw new Error('Browser batch agent receipt is missing. Existing connections preserved.');
|
|
64
|
-
const current = readPrivate(path,true);
|
|
65
|
-
if (current.batchId !== receipt.batchId || current.requestId !== agent.requestId || current.candidateToken !== agent.candidateToken || current.pollingSecret !== agent.pollingSecret || current.folder !== agent.folder) throw new Error('Browser batch was superseded by another setup. Finish the current exact request; credentials were preserved.');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
async connect(options) {
|
|
70
|
-
const agents = select(options, home), service = agents[0].service;
|
|
71
|
-
if (options.reconnect !== undefined && typeof options.reconnect !== 'boolean') throw new Error('Use an explicit human-approved reconnect choice.');
|
|
72
|
-
sys.verifyBundle();
|
|
73
|
-
const id = sessionId(service, `batch-${digest([options.clientName, agents]).slice(0,32)}`), dir = connectionDir(home, id);
|
|
74
|
-
return withBrowserLock(dir, () => locks(agents, async () => {
|
|
75
|
-
let receipt = lstatExists(batchPath(dir)) ? validate(readPrivate(batchPath(dir), true), id, home) : null;
|
|
76
|
-
if (receipt?.status === 'saved' && !options.reconnect) return { id, batchId: receipt.batchId, status: 'saved', records: receipt.agents.map(recordFor) };
|
|
77
|
-
if (receipt && ['saved', 'expired', 'cancelled'].includes(receipt.status) && options.reconnect) receipt = null;
|
|
78
|
-
if (!receipt) {
|
|
79
|
-
for (const agent of agents) {
|
|
80
|
-
const path = agentPath(connectionDir(home, agent.id));
|
|
81
|
-
if (lstatExists(path)) {
|
|
82
|
-
const pending = readPrivate(path, true);
|
|
83
|
-
if (!['saved','cancelled','expired'].includes(pending.status)) throw new Error('Another browser setup is pending for an agent in this batch. Finish or cancel that exact request first.');
|
|
84
|
-
}
|
|
85
|
-
if (lstatExists(join(connectionDir(home, agent.id), 'connection.json'))) validateSession(load(connectionDir(home, agent.id)), agent.id);
|
|
86
|
-
}
|
|
87
|
-
receipt = { schema: 'mcp-browser-batch-1', id, service, batchId: randomUUID(), clientName: options.clientName, createdAt: sys.now(), status: 'pending', registered: false, agents: agents.map(agent => ({ ...agent, requestId: randomUUID(), candidateToken: randomBytes(32).toString('base64url'), pollingSecret: randomBytes(32).toString('base64url') })) };
|
|
88
|
-
// The full immutable manifest and every candidate are durable before any network call.
|
|
89
|
-
writePrivate(batchPath(dir), receipt);
|
|
90
|
-
for (const agent of receipt.agents) writeAgent(receipt, agent);
|
|
91
|
-
}
|
|
92
|
-
currentAgents(receipt);
|
|
93
|
-
if (!receipt.registered) receipt = await register(dir, receipt);
|
|
94
|
-
return summary(receipt);
|
|
95
|
-
}));
|
|
96
|
-
},
|
|
97
|
-
async prepare({ connection }) {
|
|
98
|
-
const dir = connectionDir(home, connection);
|
|
99
|
-
return withBrowserLock(dir, async () => {
|
|
100
|
-
if (!lstatExists(batchPath(dir))) throw new Error('No browser batch found. Retry the exact batch returned during onboarding.');
|
|
101
|
-
let receipt = validate(readPrivate(batchPath(dir), true), connection, home);
|
|
102
|
-
return locks(receipt.agents, async () => {
|
|
103
|
-
currentAgents(receipt);
|
|
104
|
-
if (!receipt.registered) receipt = await register(dir, receipt);
|
|
105
|
-
if (['cancelled','expired'].includes(receipt.status)) return summary(receipt);
|
|
106
|
-
const records = [];
|
|
107
|
-
for (const agent of receipt.agents) {
|
|
108
|
-
if (agent.status === 'saved') { records.push(recordFor(agent)); continue; }
|
|
109
|
-
let reply;
|
|
110
|
-
try { reply = await sys.request(receipt.service, `pairings/${agent.requestId}/poll`, {}, agent.pollingSecret); }
|
|
111
|
-
catch { throw new Error(`Browser batch approval reply unavailable. Retry connection ${connection}; saved credentials and remaining requests are preserved.`); }
|
|
112
|
-
if (!['pending','approved','cancelled','expired'].includes(reply?.status)) throw new Error('Unexpected browser batch approval reply. Existing connections preserved.');
|
|
113
|
-
if (reply.status !== 'approved') {
|
|
114
|
-
if (reply.status !== 'pending') { receipt.status = reply.status; writePrivate(batchPath(dir), receipt); for (const request of receipt.agents) writeAgent(receipt, request); }
|
|
115
|
-
return { ...summary(receipt), status: reply.status, savedAgents: records.map(record => record.agentId) };
|
|
116
|
-
}
|
|
117
|
-
if (reply.agentId !== agent.agentId || reply.agentName !== agent.agentName || !Number.isFinite(reply.approvedAt)) throw new Error('Approved batch identity differs from the requested agent. Existing connection preserved.');
|
|
118
|
-
const plan = projectIdentity({ projectUrl: receipt.service, repo: reply.repoUrl, folder: agent.folder });
|
|
119
|
-
const record = validateSession({ schema:'mcp-connection-1', id:agent.id, service:receipt.service, agentId:agent.agentId, agentName:agent.agentName, repo:plan.repo, folder:plan.folder, createdAt:sys.now(), approvedAt:reply.approvedAt }, agent.id);
|
|
120
|
-
const agentDir = connectionDir(home, agent.id);
|
|
121
|
-
writePrivate(join(agentDir, 'token'), agent.candidateToken); save(agentDir, record);
|
|
122
|
-
agent.status = 'saved'; writeAgent(receipt, agent); writePrivate(batchPath(dir), receipt); records.push(record);
|
|
123
|
-
}
|
|
124
|
-
receipt.status = 'saved'; writePrivate(batchPath(dir), receipt);
|
|
125
|
-
return { id: receipt.id, batchId: receipt.batchId, status:'saved', records };
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
package/src/browser-pairing.mjs
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
|
2
|
-
import { constants, openSync, closeSync, writeFileSync, lstatSync, unlinkSync } from 'node:fs';
|
|
3
|
-
import { join, isAbsolute } from 'node:path';
|
|
4
|
-
import { projectIdentity } from './project-identity.mjs';
|
|
5
|
-
import { assertStateOutsideWorkspace, canonicalPath, connectionDir, lstatExists, readPrivate, save, writePrivate } from './state.mjs';
|
|
6
|
-
import { sessionHome, sessionId, validateSession } from './session.mjs';
|
|
7
|
-
import { system } from './system.mjs';
|
|
8
|
-
|
|
9
|
-
const TOKEN = /^[\w-]{43}$/;
|
|
10
|
-
const hash = value => createHash('sha256').update(value).digest('hex');
|
|
11
|
-
// Only locks created by this browser flow can be recovered automatically. Legacy/live bridge locks remain untouched.
|
|
12
|
-
export function withBrowserLock(dir, action, processAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { if (error.code === 'ESRCH') return false; return true; } }) {
|
|
13
|
-
const path = join(dir, 'operation.lock'), guard = join(dir, 'browser-recovery.lock');
|
|
14
|
-
const create = () => {
|
|
15
|
-
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
16
|
-
try { writeFileSync(fd, JSON.stringify({ kind: 'karma42-browser-operation-1', pid: process.pid, nonce: randomUUID() })); }
|
|
17
|
-
finally { closeSync(fd); }
|
|
18
|
-
return lstatSync(path);
|
|
19
|
-
};
|
|
20
|
-
let owned;
|
|
21
|
-
try { owned = create(); }
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (error.code !== 'EEXIST') throw error;
|
|
24
|
-
let guardFd;
|
|
25
|
-
try { guardFd = openSync(guard, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); }
|
|
26
|
-
catch { throw new Error('Another browser setup operation is checking this connection. Retry shortly; no process was stopped.'); }
|
|
27
|
-
try {
|
|
28
|
-
writeFileSync(guardFd, `${process.pid}\n`);
|
|
29
|
-
const before = lstatSync(path), raw = readPrivate(path);
|
|
30
|
-
let holder;
|
|
31
|
-
try { holder = JSON.parse(raw); } catch {}
|
|
32
|
-
if (holder?.kind !== 'karma42-browser-operation-1' || !Number.isSafeInteger(holder.pid) || holder.pid <= 0 || typeof holder.nonce !== 'string' || processAlive(holder.pid)) {
|
|
33
|
-
throw new Error('Another operation holds this connection. Finish or cancel its setup, or disconnect its active MCP entry, before retrying. No process was stopped.');
|
|
34
|
-
}
|
|
35
|
-
// Guard serializes recovery attempts; reread inode/content before touching the confirmed-dead lock.
|
|
36
|
-
const current = lstatSync(path);
|
|
37
|
-
if (current.dev !== before.dev || current.ino !== before.ino || readPrivate(path) !== raw) throw new Error('The connection lock changed. Retry setup; no process was stopped.');
|
|
38
|
-
unlinkSync(path);
|
|
39
|
-
owned = create();
|
|
40
|
-
} finally { closeSync(guardFd); unlinkSync(guard); }
|
|
41
|
-
}
|
|
42
|
-
return Promise.resolve().then(action).finally(() => {
|
|
43
|
-
if (!lstatExists(path)) return;
|
|
44
|
-
const current = lstatSync(path);
|
|
45
|
-
if (current.dev === owned.dev && current.ino === owned.ino) unlinkSync(path);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
const receiptPath = dir => join(dir, 'pending-browser.json');
|
|
49
|
-
function validReceipt(receipt, id) {
|
|
50
|
-
if (receipt?.schema !== 'mcp-browser-pairing-1' || receipt.id !== id || id !== sessionId(receipt.service, receipt.agentId)
|
|
51
|
-
|| !TOKEN.test(receipt.pollingSecret ?? '') || !TOKEN.test(receipt.candidateToken ?? '')
|
|
52
|
-
|| !/^[\w-]{1,200}$/.test(receipt.agentId ?? '') || !/^[a-f0-9-]{36}$/.test(receipt.requestId ?? '')
|
|
53
|
-
|| typeof receipt.clientName !== 'string' || !receipt.clientName.trim() || receipt.clientName.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(receipt.clientName)) throw new Error('Invalid private browser setup receipt. Review My agents.');
|
|
54
|
-
const plan = projectIdentity({ projectUrl: receipt.service, folder: receipt.folder });
|
|
55
|
-
if (plan.projectUrl !== receipt.service || canonicalPath(receipt.folder) !== receipt.folder) throw new Error('Browser setup workspace changed. Review My agents.');
|
|
56
|
-
return receipt;
|
|
57
|
-
}
|
|
58
|
-
export function browserApprovalUrl(receipt) {
|
|
59
|
-
const service = new URL(receipt.service);
|
|
60
|
-
const page = new URL('/viewer/', service.origin);
|
|
61
|
-
page.searchParams.set('project', service.pathname.split('/')[2]);
|
|
62
|
-
page.searchParams.set('view', 'agent-connect');
|
|
63
|
-
page.searchParams.set('request', receipt.requestId);
|
|
64
|
-
return page.href;
|
|
65
|
-
}
|
|
66
|
-
export function createBrowserOperations({ home = sessionHome(), io, sys = system } = {}) {
|
|
67
|
-
home = canonicalPath(home);
|
|
68
|
-
const showPending = receipt => io?.say?.('Browser approval is required. Complete it on Karma42, then check this same connection.');
|
|
69
|
-
const startBody = receipt => ({ requestId: receipt.requestId, agentId: receipt.agentId, clientName: receipt.clientName, folder: receipt.folder, pollingSecret: receipt.pollingSecret, candidateTokenHash: hash(receipt.candidateToken) });
|
|
70
|
-
async function start(dir, receipt) {
|
|
71
|
-
let result;
|
|
72
|
-
try { result = await sys.request(receipt.service, 'pairings/start', startBody(receipt)); }
|
|
73
|
-
catch { throw new Error(`Browser setup reply unavailable. Your private request is saved. Retry finish_project_agent_connection with connection ${receipt.id}; do not create another request or replace credentials.`); }
|
|
74
|
-
if (result?.requestId !== receipt.requestId || !['pending', 'approved', 'cancelled', 'expired'].includes(result.status)) throw new Error('Unexpected browser setup reply. The private request was preserved.');
|
|
75
|
-
receipt = { ...receipt, registered: true, expiresAt: result.expiresAt };
|
|
76
|
-
writePrivate(receiptPath(dir), receipt);
|
|
77
|
-
return receipt;
|
|
78
|
-
}
|
|
79
|
-
function saved(record) { return validateSession(record, record.id); }
|
|
80
|
-
return {
|
|
81
|
-
async connect(options) {
|
|
82
|
-
if (!/^[\w-]{1,200}$/.test(options['agent-id'] ?? '')) throw new Error('Browser setup requires the project-specific agent ID from the project instructions.');
|
|
83
|
-
if (typeof options.clientName !== 'string' || !options.clientName.trim() || options.clientName.length > 100 || /[\x00-\x1f\x7f-\x9f]/.test(options.clientName)) throw new Error('Use a valid MCP client name.');
|
|
84
|
-
if (typeof options.folder !== 'string' || !isAbsolute(options.folder)) throw new Error('Use the actual absolute working folder approved by your human.');
|
|
85
|
-
const folder = canonicalPath(options.folder);
|
|
86
|
-
assertStateOutsideWorkspace(home, folder);
|
|
87
|
-
const plan = projectIdentity({ projectUrl: options['project-url'], folder });
|
|
88
|
-
sys.verifyBundle();
|
|
89
|
-
const id = sessionId(plan.projectUrl, options['agent-id']), dir = connectionDir(home, id);
|
|
90
|
-
if (lstatExists(join(dir, 'connection.json'))) validateSession(readPrivate(join(dir, 'connection.json'), true), id);
|
|
91
|
-
return withBrowserLock(dir, async () => {
|
|
92
|
-
let receipt;
|
|
93
|
-
if (lstatExists(receiptPath(dir))) {
|
|
94
|
-
receipt = validReceipt(readPrivate(receiptPath(dir), true), id);
|
|
95
|
-
if (['cancelled', 'expired'].includes(receipt.status) || (receipt.status === 'saved' && options.reconnect)) receipt = null;
|
|
96
|
-
else {
|
|
97
|
-
if (receipt.folder !== folder) throw new Error('A browser setup or saved approval belongs to another workspace. Use an explicit reconnect after the completed setup, or finish/cancel its pending request first.');
|
|
98
|
-
if (receipt.status === 'saved') return saved(validateSession(readPrivate(join(dir, 'connection.json'), true), id));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (!receipt) {
|
|
102
|
-
receipt = { schema: 'mcp-browser-pairing-1', id, service: plan.projectUrl, agentId: options['agent-id'], clientName: options.clientName, folder,
|
|
103
|
-
requestId: randomUUID(), candidateToken: randomBytes(32).toString('base64url'), pollingSecret: randomBytes(32).toString('base64url'), createdAt: sys.now(), status: 'pending', registered: false };
|
|
104
|
-
// Persist the candidate BEFORE contacting the server: lost start/approval replies never lose the credential.
|
|
105
|
-
writePrivate(receiptPath(dir), receipt);
|
|
106
|
-
}
|
|
107
|
-
if (!receipt.registered) receipt = await start(dir, receipt);
|
|
108
|
-
showPending(receipt); return { id, status: receipt.status, approvalUrl: browserApprovalUrl(receipt), requestCheck: hash(receipt.requestId + ':' + hash(receipt.candidateToken)).slice(0, 8).toUpperCase() };
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
async prepare(options) {
|
|
112
|
-
const id = options.connection, dir = connectionDir(home, id);
|
|
113
|
-
if (lstatExists(receiptPath(dir))) assertStateOutsideWorkspace(home, validReceipt(readPrivate(receiptPath(dir), true), id).folder);
|
|
114
|
-
return withBrowserLock(dir, async () => {
|
|
115
|
-
if (!lstatExists(receiptPath(dir))) throw new Error('No browser setup found. Use connect_project_agent from the project instructions.');
|
|
116
|
-
let receipt = validReceipt(readPrivate(receiptPath(dir), true), id);
|
|
117
|
-
if (receipt.status === 'saved') return saved(validateSession(readPrivate(join(dir, 'connection.json'), true), id));
|
|
118
|
-
if (!receipt.registered) receipt = await start(dir, receipt);
|
|
119
|
-
let result;
|
|
120
|
-
try { result = await sys.request(receipt.service, `pairings/${receipt.requestId}/poll`, {}, receipt.pollingSecret); }
|
|
121
|
-
catch { throw new Error(`Browser approval reply unavailable. Retry finish_project_agent_connection with connection ${id}; your private request and existing connection are preserved.`); }
|
|
122
|
-
if (!['pending', 'approved', 'cancelled', 'expired'].includes(result?.status)) throw new Error('Unexpected browser approval reply. Existing connection preserved.');
|
|
123
|
-
if (result.status === 'pending') { showPending(receipt); return { id, status: 'pending' }; }
|
|
124
|
-
if (result.status !== 'approved') {
|
|
125
|
-
writePrivate(receiptPath(dir), { ...receipt, status: result.status });
|
|
126
|
-
io.say(`Browser setup ${result.status}. Existing saved credentials were not changed. Ask your human before starting a fresh connection request.`);
|
|
127
|
-
return { id, status: result.status };
|
|
128
|
-
}
|
|
129
|
-
if (result.agentId !== receipt.agentId || !/^[\w-]{1,64}$/.test(result.agentName ?? '') || !Number.isFinite(result.approvedAt)) throw new Error('Approved identity differs from the requested agent. Existing connection preserved. Review My agents.');
|
|
130
|
-
assertStateOutsideWorkspace(home, receipt.folder);
|
|
131
|
-
const plan = projectIdentity({ projectUrl: receipt.service, repo: result.repoUrl, folder: receipt.folder });
|
|
132
|
-
const record = { schema: 'mcp-connection-1', id, service: receipt.service, agentId: receipt.agentId, agentName: result.agentName, repo: plan.repo, folder: plan.folder, createdAt: sys.now(), approvedAt: result.approvedAt };
|
|
133
|
-
writePrivate(join(dir, 'token'), receipt.candidateToken); save(dir, record);
|
|
134
|
-
// Keep the private receipt for restart/recovery; never exchange another credential on retry.
|
|
135
|
-
writePrivate(receiptPath(dir), { ...receipt, status: 'saved' });
|
|
136
|
-
return saved(record);
|
|
137
|
-
});
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|