@karma42/agent 0.0.0-stage → 0.1.0-beta.10

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.
@@ -0,0 +1,109 @@
1
+ import { randomBytes, randomUUID, createHash } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { canonicalPath, ensureHome, privateDir, readPrivate, writePrivate, lstatExists, digest } from './state.mjs';
4
+ import { sessionHome } from './session.mjs';
5
+ import { withBrowserLock } from './browser-pairing.mjs';
6
+
7
+ const hash = value => createHash('sha256').update(value).digest('hex');
8
+ const TOKEN = /^[\w-]{43}$/;
9
+ const ID = /^[a-f0-9]{64}$/;
10
+ const object = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
11
+ const ownerRef = { type: 'string', description: 'Opaque owner_ref returned for your browser-approved human account. Never use an agent connection_ref.' };
12
+ const project = { type: 'string', pattern: '^[a-f0-9]{64}$' };
13
+ const revision = { type: 'integer', minimum: 0 };
14
+ const requestId = { type: 'string', pattern: '^[\\w-]{1,64}$', description: 'Keep this request ID and the original input when retrying a lost response.' };
15
+ export const OWNER_TOOLS = [
16
+ { name: 'connect_owner_account', description: 'Connect your human Karma42 account for owner management through browser sign-in and explicit approval. Separate from agent identity; no project or model starts. Returns owner_ref and a browser approval link, or reuses an unexpired approved account.', inputSchema: object({}) },
17
+ { name: 'finish_owner_account_connection', description: 'Check this human account approval once. Reuse the same owner_ref after lost replies; do not create another approval request.', inputSchema: object({ owner_ref: ownerRef }, ['owner_ref']) },
18
+ { name: 'list_my_projects', description: 'List your human account’s own proposals and owned projects with states and next steps. Does not use an agent’s identity.', annotations: { readOnlyHint: true }, inputSchema: object({ owner_ref: ownerRef, cursor: { type: 'string', maxLength: 500 } }, ['owner_ref']) },
19
+ { name: 'list_my_proposals', description: 'Check your own project proposal statuses and reviewer feedback.', annotations: { readOnlyHint: true }, inputSchema: object({ owner_ref: ownerRef, cursor: { type: 'string', maxLength: 500 } }, ['owner_ref']) },
20
+ { name: 'get_my_proposal', description: 'Read your own proposal status, review feedback and next step.', annotations: { readOnlyHint: true }, inputSchema: object({ owner_ref: ownerRef, project_id: project }, ['owner_ref', 'project_id']) },
21
+ { name: 'get_owned_project_status', description: 'Read the owned project’s state, readiness, contributors and blockers before starting it.', annotations: { readOnlyHint: true }, inputSchema: object({ owner_ref: ownerRef, project_id: project }, ['owner_ref', 'project_id']) },
22
+ { name: 'start_owned_project', description: 'Start this project only after your human explicitly instructs it and reviews current readiness. Uses optimistic revision and idempotent request ID. Starting the project does not launch participants’ models.', inputSchema: object({ owner_ref: ownerRef, project_id: project, expected_revision: revision, client_request_id: requestId, start_without_minimum: { type: 'boolean', description: 'True only if the human explicitly approves starting below the contributor minimum.' }, approved: { type: 'boolean', const: true } }, ['owner_ref', 'project_id', 'expected_revision', 'client_request_id', 'approved']) },
23
+ { name: 'list_owned_project_members', description: 'List project members and current project roles so the owner can select an exact person and membership revision.', annotations: { readOnlyHint: true }, inputSchema: object({ owner_ref: ownerRef, project_id: project, cursor: { type: 'string', maxLength: 500 } }, ['owner_ref', 'project_id']) },
24
+ { name: 'set_owned_project_role', description: 'After explicit owner instruction, assign an existing participant’s project member/moderator/admin role. These are project roles only; this never grants platform reviewer or co-founder permissions. Requires the current membership revision and an idempotent request ID.', inputSchema: object({ owner_ref: ownerRef, project_id: project, email: { type: 'string', maxLength: 254 }, role: { type: 'string', enum: ['member', 'moderator', 'admin'] }, expected_revision: revision, client_request_id: requestId, approved: { type: 'boolean', const: true } }, ['owner_ref', 'project_id', 'email', 'role', 'expected_revision', 'client_request_id', 'approved']) },
25
+ { name: 'list_my_project_agents', description: 'List only your own registered agent identities in a project you own. Returns authoritative IDs/names for selecting a team; not worker or online status.', annotations: { readOnlyHint: true }, inputSchema: object({ owner_ref: ownerRef, project_id: project }, ['owner_ref','project_id']) },
26
+ { name: 'register_my_project_agents', description: 'After the human approves the exact team names, atomically register 1–10 identities under that human account in its owned project. Retry the same names and client_request_id after lost replies. Creates no connection, worker, model, folder or GitHub changes. New owner approval must include register_own_agents. Use returned IDs for separate batch connection approval.', inputSchema: object({ owner_ref: ownerRef, project_id: project, names: { type:'array', minItems:1, maxItems:10, uniqueItems:true, items:{type:'string',pattern:'^[\\w-]{1,64}$'} }, client_request_id:requestId, approved:{type:'boolean',const:true} }, ['owner_ref','project_id','names','client_request_id','approved']) },
27
+ { name: 'disconnect_owner_account', description: 'Revoke this saved human management connection. Agent bindings and running agents are separate and remain unchanged.', inputSchema: object({ owner_ref: ownerRef }, ['owner_ref']) },
28
+ ];
29
+ export function ownerOrigin(site) {
30
+ const u = new URL(site ?? process.env.KARMA42_SITE_URL ?? 'https://karma42.ai');
31
+ if (u.username || u.password || u.search || u.hash || u.pathname !== '/' || !(u.protocol === 'https:' && ['karma42.ai', 'karma42-beta.web.app'].includes(u.hostname) && !u.port || u.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(u.hostname))) throw new Error('Owner management requires the Karma42 website or explicit loopback development origin.');
32
+ return u.origin;
33
+ }
34
+ export function createOwnerClient({ home = sessionHome(), site, fetchImpl = fetch, now = Date.now } = {}) {
35
+ const origin = ownerOrigin(site); home = canonicalPath(home);
36
+ function directory() { ensureHome(home); const root = join(home, 'owners'); privateDir(root); const dir = join(root, digest(origin)); privateDir(dir); return dir; }
37
+ function receipt() {
38
+ const p = join(directory(), 'owner.json'); if (!lstatExists(p)) return null;
39
+ const r = readPrivate(p, true);
40
+ if (r.schema !== 'karma42-owner-1' || r.origin !== origin || !/^[a-f0-9-]{36}$/.test(r.requestId ?? '') || !TOKEN.test(r.pollingSecret ?? '') || !TOKEN.test(r.token ?? '') || !['pending','approved','expired','cancelled','revoked'].includes(r.status)) throw new Error('Owner connection state is invalid. No credentials were changed.');
41
+ return r;
42
+ }
43
+ const save = r => writePrivate(join(directory(), 'owner.json'), r);
44
+ async function request(path, { method = 'GET', body, token, poll } = {}) {
45
+ let response;
46
+ try { response = await fetchImpl(new URL('/api/owner' + path, origin), { method, redirect: 'error', credentials: 'omit', signal: AbortSignal.timeout(15000), headers: { Accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(poll ? { Authorization: `Bearer ${poll}` } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) }); }
47
+ catch { throw new Error('Owner service unavailable. Retry the same request; no permissions were changed.'); }
48
+ let size = 0, chunks = [];
49
+ for await (const c of response.body) { size += c.byteLength; if (size > 256 * 1024) { await response.body.cancel?.().catch(() => {}); throw new Error('Owner service returned an oversized response.'); } chunks.push(c); }
50
+ let data; try { data = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { throw new Error('Owner service returned an invalid response.'); }
51
+ if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('Owner service returned an invalid response.');
52
+ if (!response.ok) {
53
+ const detail = response.status < 500 && typeof data.message === 'string' && data.message.length <= 500 && !/[\x00-\x1f\x7f-\x9f]/.test(data.message) ? data.message : null;
54
+ throw new Error(response.status === 401 ? 'Owner account authorization expired or was revoked. Sign in again.' : detail ? `Owner operation refused: ${detail}` : 'Owner operation refused. Read current status and review the original request before retrying.');
55
+ }
56
+ return data;
57
+ }
58
+ function publicState(r) {
59
+ const url = new URL('/viewer/', origin); url.searchParams.set('view','owner-connect'); url.searchParams.set('request',r.requestId);
60
+ return { status: r.status, owner_id: digest(origin), account_name: r.personName ?? null, expires_at: r.connectionExpiresAt ?? r.expiresAt ?? null, ...(r.status === 'pending' ? { approval_url: url.href, request_check: r.verificationCode, next_step: 'Sign in and approve human project management on Karma42. Then finish this same connection.' } : {}), scope: r.scopes ?? ['read_projects','start_projects','manage_project_roles'], agent_work_started: false };
61
+ }
62
+ async function connect(clientName = 'Karma42 CLI') {
63
+ return withBrowserLock(directory(), async () => {
64
+ let r = receipt();
65
+ if (r?.status === 'approved' && r.connectionExpiresAt > now()) return publicState(r);
66
+ if (!r || ['expired','cancelled','revoked','approved'].includes(r.status)) { r = { schema:'karma42-owner-1', origin, requestId:randomUUID(), pollingSecret:randomBytes(32).toString('base64url'), token:randomBytes(32).toString('base64url'), clientName, registerOwnAgents:true, status:'pending' }; save(r); }
67
+ const response = await request('/connect', { method:'POST', body:{ requestId:r.requestId, pollingSecret:r.pollingSecret, candidateTokenHash:hash(r.token), clientName:r.clientName, registerOwnAgents:r.registerOwnAgents===true } });
68
+ if (response.requestId !== r.requestId || !['pending','approved','expired','cancelled'].includes(response.status)) throw new Error('Owner service returned an invalid connection response. Retry this saved request.');
69
+ r = { ...r, status:response.status === 'approved' ? 'pending' : response.status, expiresAt:response.expiresAt, verificationCode:response.verificationCode, ...(Array.isArray(response.scopes) ? {scopes:response.scopes} : {}) }; save(r);
70
+ return publicState(r);
71
+ });
72
+ }
73
+ async function finish() {
74
+ return withBrowserLock(directory(), async () => {
75
+ const r = receipt(); if (!r) throw new Error('Owner connection has not been requested. Connect your human account first.');
76
+ if (r.status === 'approved' && r.connectionExpiresAt > now()) return publicState(r);
77
+ const response = await request(`/connect/${r.requestId}`, { poll:r.pollingSecret });
78
+ if (!['pending','approved','expired','cancelled'].includes(response.status) || response.status === 'approved' && (!Number.isFinite(response.connectionExpiresAt) || response.connectionExpiresAt <= now() || typeof response.personName !== 'string')) throw new Error('Owner service returned an invalid approval response.');
79
+ const saved = { ...r, ...response }; save(saved); return publicState(saved);
80
+ });
81
+ }
82
+ async function operation(name, args) {
83
+ const r = receipt(); if (!r || r.status !== 'approved' || r.connectionExpiresAt <= now()) throw new Error('Owner account requires browser sign-in and approval first.');
84
+ const specs = { list_my_project_agents:[`/projects/${args.project_id}/agents`], register_my_project_agents:[`/projects/${args.project_id}/agents`,'POST'], list_my_projects:['/projects'], list_my_proposals:['/proposals'], get_my_proposal:[`/proposals/${args.project_id}`], get_owned_project_status:[`/projects/${args.project_id}/status`], list_owned_project_members:[`/projects/${args.project_id}/members`], start_owned_project:[`/projects/${args.project_id}/start`,'POST'], set_owned_project_role:[`/projects/${args.project_id}/members`,'POST'], disconnect_owner_account:['/connections/revoke','POST'] };
85
+ if (!specs[name]) throw new Error('Owner operation is unknown.');
86
+ if ((name.includes('owned_project') || name === 'get_my_proposal' || name === 'list_my_project_agents' || name === 'register_my_project_agents') && !ID.test(args.project_id ?? '')) throw new Error('Owner operation requires an exact project ID from your project list.');
87
+ const schema = OWNER_TOOLS.find(t=>t.name===name).inputSchema;
88
+ if (Object.keys(args).some(k=>!Object.hasOwn(schema.properties,k))) throw new Error('Owner operation accepts only documented fields.');
89
+ if (args.cursor !== undefined && (typeof args.cursor !== 'string' || args.cursor.length > 500)) throw new Error('Owner cursor is invalid.');
90
+ let [path,method = 'GET'] = specs[name], body;
91
+ if (name === 'start_owned_project' || name === 'set_owned_project_role') {
92
+ if (args.approved !== true || !Number.isSafeInteger(args.expected_revision) || args.expected_revision < 0 || !/^[\w-]{1,64}$/.test(args.client_request_id ?? '')) throw new Error('Owner mutation requires explicit human approval, current revision and the original request ID.');
93
+ body = { expected_revision:args.expected_revision, client_request_id:args.client_request_id };
94
+ if (name === 'start_owned_project') { if (args.start_without_minimum !== undefined && typeof args.start_without_minimum !== 'boolean') throw new Error('Owner minimum override must be explicitly selected.'); if (args.start_without_minimum !== undefined) body.start_without_minimum=args.start_without_minimum; }
95
+ else { if (!['member','moderator','admin'].includes(args.role) || typeof args.email !== 'string' || args.email.length>254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(args.email)) throw new Error('Owner role assignment requires an existing participant email and project member, moderator or admin role.'); Object.assign(body,{ email:args.email,role:args.role,remove:false }); }
96
+ }
97
+ if (name === 'register_my_project_agents') {
98
+ if (args.approved !== true || !/^[\w-]{1,64}$/.test(args.client_request_id ?? '') || !Array.isArray(args.names) || args.names.length < 1 || args.names.length > 10 || new Set(args.names).size !== args.names.length || args.names.some(n => typeof n !== 'string' || !/^[\w-]{1,64}$/.test(n))) throw new Error('Owner team registration requires approved distinct names and the original request ID.');
99
+ if (!r.scopes?.includes('register_own_agents')) throw new Error('Owner connection did not approve agent registration. Disconnect it and approve a new owner connection including that permission.');
100
+ body = { names:args.names,client_request_id:args.client_request_id,approved:true };
101
+ }
102
+ if (name === 'disconnect_owner_account') body = {};
103
+ if (args.cursor !== undefined) path += '?cursor=' + encodeURIComponent(args.cursor);
104
+ const response = await request(path,{method,body,token:r.token});
105
+ if (name === 'disconnect_owner_account') save({...r,status:'revoked'});
106
+ return response;
107
+ }
108
+ return { connect,finish,operation };
109
+ }
@@ -0,0 +1,15 @@
1
+ import { isAbsolute } from 'node:path';
2
+ function clean(value, label) {
3
+ if (typeof value !== 'string' || !value || value.length > 2048 || /[\x00-\x1f\x7f-\x9f]/.test(value)) throw new Error(`Provide a valid ${label}.`);
4
+ return value;
5
+ }
6
+ export function projectIdentity({ projectUrl, repo = 'https://github.com/placeholder/project', folder }) {
7
+ let url;
8
+ try { url = new URL(clean(projectUrl, 'project URL')); } catch { throw new Error('Use a Karma42 project service URL.'); }
9
+ if (!['https:', 'http:'].includes(url.protocol) || (url.protocol === 'http:' && !['localhost', '127.0.0.1'].includes(url.hostname)) || url.username || url.password || url.search || url.hash || !/^\/p\/[a-f0-9]{64}\/?$/.test(url.pathname)) throw new Error('Use an HTTPS project service URL ending in /p/PROJECT_ID, without credentials or query parameters.');
10
+ clean(repo, 'GitHub repository URL');
11
+ if (!/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/?$/.test(repo) || /\/(?:\.|\.\.)(?:\/|$)/.test(repo)) throw new Error('Use a GitHub repository URL without credentials, query parameters or fragments.');
12
+ clean(folder, 'absolute project folder');
13
+ if (!isAbsolute(folder)) throw new Error('Choose an absolute project folder.');
14
+ return { projectUrl: url.href.replace(/\/$/, ''), repo: repo.replace(/\/$/, ''), folder };
15
+ }
@@ -0,0 +1,444 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import { PassThrough } from 'node:stream';
4
+ import { EventEmitter } from 'node:events';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { join, isAbsolute } from 'node:path';
7
+ import { verifyBundle } from './system.mjs';
8
+ import { assertStateOutsideWorkspace, canonicalPath, connectionDir, digest, load, lstatExists, readPrivate } from './state.mjs';
9
+ import { sessionHome, sessionId, validateSession } from './session.mjs';
10
+ import { projectIdentity } from './project-identity.mjs';
11
+ import { VERSION } from './version.mjs';
12
+ import { createBrowserOperations } from './browser-pairing.mjs';
13
+ import { createBatchBrowserOperations } from './browser-batch.mjs';
14
+ import { serveSession } from './session-stdio.mjs';
15
+ import { createProjectWorker } from './worker-runtime.mjs';
16
+ import { workerRecoveryStatus, cancelWorkerRecovery } from './worker-recovery.mjs';
17
+ import { createOwnerClient, OWNER_TOOLS } from './owner-client.mjs';
18
+ import { createDiscovery, DISCOVERY_TOOLS } from './discovery.mjs';
19
+
20
+ const objectSchema = (properties, required) => ({ type: 'object', properties, required, additionalProperties: false });
21
+ const approvalWaitSchema = { type: 'integer', minimum: 0, maximum: 60, default: 0, description: 'Wait without a model for browser approval, up to this many seconds. Reuse the same reference if still pending; no typed done is required.' };
22
+ const reference = { type: 'string', description: 'Use the opaque connection_ref returned for this exact project agent. There is no default connection.' };
23
+ const GUIDE = readFileSync(new URL('../skills/karma42/SKILL.md', import.meta.url), 'utf8');
24
+ const BOOTSTRAP = [
25
+ { name:'get_karma42_guide', description:'Read the Karma42 owner/team orchestration skill before managing projects or assembling agents. No sign-in or connection needed.', annotations:{readOnlyHint:true}, inputSchema:objectSchema({},[]) },
26
+ { name: 'connect_project_agent', description: 'Connect one human-selected project agent. Reuse its approved local connection or return browser approval details. Returns an explicit connection_ref required by every project tool. No client settings, model or permissions are changed. Use the human-selected project/folder and exact agent IDs/names provided by the human or returned by the owner team tools.', inputSchema: objectSchema({ project_url: { type: 'string' }, agent_id: { type: 'string' }, agent_name: { type: 'string' }, folder: { type: 'string' }, reconnect: { type: 'boolean', description: 'Explicit human-approved replacement only; rotates credentials after browser approval.' } }, ['project_url', 'agent_id', 'agent_name', 'folder']) },
27
+ { name: 'finish_project_agent_connection', description: 'Check this binding’s browser approval and bind when approved. Optional wait_seconds waits within a bounded budget without blocking other agents. Retry the same connection_ref after a lost reply. Never edit client settings or create a new pairing merely to retry.', inputSchema: objectSchema({ connection_ref: reference, wait_seconds: approvalWaitSchema, connection_id: { type: 'string', description: 'Optional crosscheck with the connection ID returned during onboarding.' } }, ['connection_ref']) },
28
+ { name: 'disconnect_project_agent', description: 'Stop and release only this project-agent binding. Other bindings in a shared MCP process remain available. The connection_ref becomes invalid; private approved credentials are retained for explicit future reuse.', inputSchema: objectSchema({ connection_ref: reference }, ['connection_ref']) },
29
+ ];
30
+ const BATCH_TOOLS = [
31
+ { name: 'connect_project_agents', description: 'Connect an exact batch of one to ten of your own project agents. One browser approval covers only these agents and folders in this project. Never approve another participant’s agents. Each credential and connection_ref remains independent; this does not start workers or grant work authorization.', inputSchema: objectSchema({ project_url: { type: 'string' }, agents: { type: 'array', minItems: 1, maxItems: 10, items: objectSchema({ agent_id: { type:'string' }, agent_name: { type:'string' }, folder: { type:'string' } }, ['agent_id','agent_name','folder']) }, reconnect: { type:'boolean', description:'Explicit human-approved replacement only.' } }, ['project_url','agents']) },
32
+ { name: 'finish_project_agents_connection', description: 'Check the exact batch_ref returned during onboarding and bind each approved agent separately. Optional wait_seconds waits within a bounded budget without blocking other agents. Retry this same batch after a lost reply. No worker is started.', inputSchema: objectSchema({ batch_ref: { type:'string' }, wait_seconds: approvalWaitSchema }, ['batch_ref']) },
33
+ ];
34
+ const WORKER_TOOLS = [
35
+ { name: 'start_project_worker', description: 'After your human explicitly approves this work, start one supervised worker for this verified project agent. Uses the existing coordination connection and guarded native permissions. Return promptly; the supervisor can steer, pause or stop through worker controls. Requires a separate approved Git checkout/worktree matching the project repository, distinct from the connection/main-chat folder. Agents may self-organize within the shared approved mission; fixed roles are optional.', inputSchema: objectSchema({ connection_ref: reference, recovery_ref: { type: 'string', format: 'uuid', description: 'Only after explicit approval to recover the exact saved mission from get_project_worker. Repeat its saved client/model/scope/folder and omit minutes. Preserves the original deadline; never takes over a surviving supervisor or native process.' }, client: { type: 'string', enum: ['claude', 'codex'] }, model: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,149}$', description: 'Optional human-approved model ID or client alias. Omit for the client default; verify the actual model through worker status.' }, minutes: { type: 'integer', minimum: 15, maximum: 240, multipleOf: 15, default: 60 }, scope: { type: 'string', minLength: 1, maxLength: 4000 }, folder: { type: 'string' }, approved: { type: 'boolean', const: true, description: 'Set true only after the human explicitly approves this worker, duration, workspace and work scope. Connection approval alone is insufficient.' } }, ['connection_ref', 'client', 'scope', 'folder', 'approved']) },
36
+ { name: 'get_project_worker', description: 'Read this binding’s actual supervised worker status or saved mission recovery details after its supervisor ended. Reports model, last-turn usage, permission observations, original deadline, requested/applied steering and safe failure stage/code. Saved records are not live process observations. This does not restart work or extend the deadline.', inputSchema: objectSchema({ connection_ref: reference }, ['connection_ref']) },
37
+ { name: 'control_project_worker', description: 'Steer, pause, resume or stop only this binding’s supervised worker. Steering waits for the next natural model turn without interrupting active work; check requested/applied sequence rather than assuming delivery. Explicit pause/stop still interrupts execution. Resuming preserves the original deadline. Instruction is required only for steer.', inputSchema: objectSchema({ connection_ref: reference, action: { type: 'string', enum: ['pause', 'resume', 'stop', 'steer'] }, instruction: { type: 'string', minLength: 1, maxLength: 2000 } }, ['connection_ref', 'action']) },
38
+ ];
39
+ const READ_TOOLS = new Set(['get_project', 'find_work', 'find_agents', 'get_updates', 'read_thread', 'get_issue_coordination', 'list_work_intents', 'get_decision', 'list_decisions']);
40
+ const result = data => ({ content: [{ type: 'text', text: JSON.stringify(data) }] });
41
+
42
+ // MCP hosts may share a server across conversations. Every tool carries an explicit binding
43
+ // reference; neither a process nor a shared state file ever has a default active project.
44
+ export async function serveReusable({ home = sessionHome(), input = process.stdin, output = process.stdout, signals = process, browserFactory = createBrowserOperations, batchBrowserFactory = createBatchBrowserOperations, boundServer = serveSession, workerFactory, discoveryOptions, ownerFactory = createOwnerClient, ownerOptions, approvalWait = ms => new Promise(resolve => setTimeout(resolve, ms)), ...boundOptions }) {
45
+ const bundled = verifyBundle();
46
+ const projectTools = JSON.parse(bundled.files['project-tools.json'].toString('utf8')).tools.map(tool => ({
47
+ ...tool, description: `${tool.description ?? ''} Always supply this project agent’s connection_ref.`,
48
+ inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, connection_ref: reference }, required: [...new Set([...(tool.inputSchema.required ?? []), 'connection_ref'])] },
49
+ }));
50
+ const localList = { name: 'list_project_agents', description: 'List only project-agent bindings in this MCP process and their observed local worker status. Does not enumerate your account or claim online presence. No model is started. Use the returned connection_ref to steer or stop a specific worker.', annotations: { readOnlyHint: true, destructiveHint: false }, inputSchema: objectSchema({ cursor: { type: 'string', description: 'Use the next cursor returned by this local list.' } }, []) };
51
+ const discovery = createDiscovery(discoveryOptions);
52
+ const owner = ownerFactory({ home, ...ownerOptions }), ownerReference = randomUUID();
53
+ let ownerConnected = false;
54
+ const catalog = [...OWNER_TOOLS, ...DISCOVERY_TOOLS, localList, ...BOOTSTRAP, ...BATCH_TOOLS, ...WORKER_TOOLS, ...projectTools], allowed = new Set(projectTools.map(tool => tool.name));
55
+ home = canonicalPath(home);
56
+ const browser = browserFactory({ home, io: { say() {} } });
57
+ const batchBrowser = batchBrowserFactory({ home, io: { say() {} } });
58
+ const bindings = new Map(), identities = new Map(), batches = new Map();
59
+ const approvalTasks = new Map();
60
+ let clientName = 'MCP client', stopped = false;
61
+ const lines = createInterface({ input, crlfDelay: Infinity });
62
+ const stop = () => { stopped = true; lines.close(); for (const context of bindings.values()) void closeContext(context).catch(() => {}); };
63
+ input.once('end', stop);
64
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) signals.on(signal, stop);
65
+ const send = rpc => { if (!stopped) output.write(`${JSON.stringify(rpc)}\n`); };
66
+ const fail = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
67
+ const state = context => ({ state: context.stopped || context.stopping || context.closing ? 'stopped' : context.verified ? 'connected' : context.record ? 'bound' : context.observedState ?? 'pending', verified: !!context.verified && !context.stopped && !context.stopping && !context.closing, connection_ref: context.ref, connection_id: context.selected.id, agent_id: context.selected.agentId, agent_name: context.selected.agentName, project_url: context.selected.service, folder: context.selected.folder });
68
+ function validateRecord(record, context) {
69
+ validateSession(record, context.selected.id);
70
+ if (record.service !== context.selected.service || record.agentId !== context.selected.agentId || record.agentName !== context.selected.agentName || record.folder !== context.selected.folder) throw new Error('Saved approval does not match this agent and working folder.');
71
+ assertStateOutsideWorkspace(home, record.folder);
72
+ }
73
+ function assertLive(context) {
74
+ if (stopped || context.stopped || context.stopping || context.closing || bindings.get(context.ref) !== context) throw new Error('This binding stopped or disconnected. Reconnect its explicitly selected agent before continuing.');
75
+ }
76
+ function bind(record, context) {
77
+ assertLive(context);
78
+ validateRecord(record, context);
79
+ if (context.record) return state(context);
80
+ const dir = connectionDir(home, record.id), token = readPrivate(join(dir, 'token')).trim();
81
+ if (!/^[\w-]{43}$/.test(token)) throw new Error('Invalid private credential. Reconnect in My agents.');
82
+ const journal = join(dir, `sends-${digest(token)}.json`);
83
+ if (lstatExists(journal)) readPrivate(journal);
84
+ context.record = record; context.input = new PassThrough(); context.output = new PassThrough(); context.signals = new EventEmitter(); context.waiting = new Map();
85
+ context.lines = createInterface({ input: context.output });
86
+ context.lines.on('line', line => {
87
+ let reply;
88
+ try { reply = JSON.parse(line); } catch { void closeContext(context).catch(() => {}); return; }
89
+ const pending = context.waiting.get(reply.id);
90
+ if (pending) { context.waiting.delete(reply.id); pending.resolve(reply); }
91
+ });
92
+ // Each explicit binding retains the canonical bridge’s lazy run ownership, identity
93
+ // verification and journal. This launches no model or separate coding conversation.
94
+ context.task = boundServer({ ...boundOptions, connection: record.id, home, input: context.input, output: context.output, signals: context.signals,
95
+ onReady: async handles => { context.ready = handles; await boundOptions.onReady?.(handles); },
96
+ canRetry: () => {
97
+ if (boundOptions.canRetry && !boundOptions.canRetry()) return false;
98
+ if (!context.worker) return true;
99
+ const status = context.worker.status();
100
+ return status.active === true && ['working', 'running', 'waiting'].includes(status.state);
101
+ },
102
+ onStopping: async () => { context.stopping = true; await context.worker?.stop(); if (context.startTask) { await context.startTask.catch(() => {}); await context.worker?.stop(); } await boundOptions.onStopping?.(); },
103
+ });
104
+ context.task.catch(() => {}).finally(() => {
105
+ context.stopped = true;
106
+ for (const pending of context.waiting.values()) pending.reject(new Error('Bound connection stopped.'));
107
+ context.waiting.clear();
108
+ });
109
+ return { ...state(context), next_step: 'Call get_project with this connection_ref and verify your identity, then read the goals, rules, repository and state. Ask your human before starting work.' };
110
+ }
111
+ async function connect(args) {
112
+ if (Object.keys(args).some(key => !['project_url', 'agent_id', 'agent_name', 'folder', 'reconnect'].includes(key)) || !/^[\w-]{1,200}$/.test(args.agent_id ?? '') || !/^[\w-]{1,64}$/.test(args.agent_name ?? '') || (args.reconnect !== undefined && typeof args.reconnect !== 'boolean')) throw new Error('Use the project-specific agent ID and name supplied by your human.');
113
+ if (typeof args.project_url !== 'string' || typeof args.folder !== 'string' || !isAbsolute(args.folder)) throw new Error('Invalid project or folder.');
114
+ const folder = canonicalPath(args.folder);
115
+ assertStateOutsideWorkspace(home, folder);
116
+ for (const other of bindings.values()) {
117
+ if (workerOwnsMutations(other) && other.workerFolder) {
118
+ try { assertStateOutsideWorkspace(other.workerFolder, folder); } catch { throw new Error('Use a separate chat folder: this folder overlaps an active worker in this MCP process.'); }
119
+ }
120
+ }
121
+ const identity = projectIdentity({ projectUrl: args.project_url, folder });
122
+ const selected = { id: sessionId(identity.projectUrl, args.agent_id), service: identity.projectUrl, agentId: args.agent_id, agentName: args.agent_name, folder };
123
+ let context = bindings.get(identities.get(selected.id));
124
+ if (context) {
125
+ assertLive(context);
126
+ if (context.selected.agentName !== selected.agentName || context.selected.folder !== selected.folder) throw new Error('This project agent already has a binding with a different name or folder. Disconnect its exact connection_ref before changing it.');
127
+ if (context.record) {
128
+ if (args.reconnect) throw new Error('Cannot replace credentials while this binding is active. Disconnect its connection_ref before explicitly reconnecting.');
129
+ return state(context);
130
+ }
131
+ } else {
132
+ context = { ref: randomUUID(), selected, verified: false }; bindings.set(context.ref, context); identities.set(selected.id, context.ref);
133
+ }
134
+ const dir = connectionDir(home, selected.id);
135
+ if (lstatExists(join(dir, 'connection.json'))) validateSession(load(dir), selected.id);
136
+ if (!args.reconnect && !lstatExists(join(dir, 'pending-browser.json')) && lstatExists(join(dir, 'connection.json'))) return bind(load(dir), context);
137
+ const response = await browser.connect({ clientName, folder, 'project-url': selected.service, 'agent-id': selected.agentId, reconnect: args.reconnect });
138
+ assertLive(context);
139
+ if (response.schema === 'mcp-connection-1') return bind(response, context);
140
+ context.observedState = response.status;
141
+ return { ...state(context), state: response.status, approval_url: response.approvalUrl, request_check: response.requestCheck, next_step: 'Open the approval link, check the project, agent, folder and request check, and approve. Wait for browser approval with finish_project_agent_connection, this connection_ref and wait_seconds:60; no typed done is required.' };
142
+ }
143
+ async function connectBatch(args) {
144
+ if (Object.keys(args).some(key => !['project_url','agents','reconnect'].includes(key)) || !Array.isArray(args.agents) || args.agents.length < 1 || args.agents.length > 10 || (args.reconnect !== undefined && typeof args.reconnect !== 'boolean')) throw new Error('Use one to ten explicitly selected agents for one project.');
145
+ const selected = args.agents.map(agent => {
146
+ 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.');
147
+ const folder = canonicalPath(agent.folder); assertStateOutsideWorkspace(home, folder);
148
+ const identity = projectIdentity({ projectUrl: args.project_url, folder });
149
+ for (const other of bindings.values()) {
150
+ if (workerOwnsMutations(other) && other.workerFolder) {
151
+ try { assertStateOutsideWorkspace(other.workerFolder, folder); } catch { throw new Error('Use a separate chat folder: this folder overlaps an active worker in this MCP process.'); }
152
+ }
153
+ }
154
+ return { id: sessionId(identity.projectUrl,agent.agent_id), service:identity.projectUrl, agentId:agent.agent_id, agentName:agent.agent_name, folder };
155
+ }).sort((a,b) => a.agentId.localeCompare(b.agentId));
156
+ if (new Set(selected.map(agent => agent.id)).size !== selected.length) throw new Error('Use distinct agent IDs in a batch.');
157
+ const contexts = selected.map(agent => bindings.get(identities.get(agent.id)));
158
+ for (let index=0; index<contexts.length; index++) {
159
+ const context = contexts[index]; if (!context) continue;
160
+ assertLive(context);
161
+ if (context.selected.agentName !== selected[index].agentName || context.selected.folder !== selected[index].folder) throw new Error('This project agent already has a different name or folder. Disconnect its exact connection_ref first.');
162
+ if (context.record && args.reconnect) throw new Error('Cannot replace credentials while a batch binding is active. Disconnect its exact connection_ref first.');
163
+ }
164
+ if (contexts.every(context => context?.record)) return { state:'bound', agents:contexts.map(state), next_step:'Verify each agent with get_project using its separate connection_ref. Work requires separate approval.' };
165
+ if (contexts.some(context => context?.record)) throw new Error('Cannot replace credentials for active agents in a batch. Select only unbound agents or disconnect their exact connection_ref first.');
166
+ const response = await batchBrowser.connect({ ...args, clientName });
167
+ if (stopped) throw new Error('The MCP session closed before the batch could be bound.');
168
+ for (const context of contexts) if (context) assertLive(context);
169
+ const key = response.id;
170
+ let batch = [...batches.values()].find(candidate => candidate.id === key);
171
+ if (!batch) { batch = { ref:randomUUID(), id:key, contexts:selected.map((agent,index) => {
172
+ const context = contexts[index] ?? { ref:randomUUID(), selected:agent, verified:false };
173
+ bindings.set(context.ref,context); identities.set(agent.id,context.ref); return context;
174
+ }) }; batches.set(batch.ref,batch); }
175
+ return batchResponse(response,batch);
176
+ }
177
+ function batchResponse(response,batch) {
178
+ for (const context of batch.contexts) assertLive(context);
179
+ if (response.status === 'saved') {
180
+ // Validate every record before binding any, so a mismatch cannot select another agent.
181
+ if (response.records.length !== batch.contexts.length) throw new Error('Approved batch identity differs from the selected agents.');
182
+ for (const context of batch.contexts) validateRecord(response.records.find(record => record.id === context.selected.id),context);
183
+ for (const context of batch.contexts) if (!context.record) bind(response.records.find(record => record.id === context.selected.id),context);
184
+ return { state:'bound', batch_ref:batch.ref, agents:batch.contexts.map(state), next_step:'Verify each agent with get_project using its separate connection_ref. Work requires separate approval.' };
185
+ }
186
+ for (const context of batch.contexts) context.observedState = response.status;
187
+ return { state:response.status, batch_ref:batch.ref, approval_url:response.approvalUrl, request_check:response.requestCheck, agents:batch.contexts.map(state), next_step:'Approve this exact batch of your own agents in Karma42, wait for the result with finish_project_agents_connection, this batch_ref and wait_seconds:60; no typed done is required. Connection approval does not start work.' };
188
+ }
189
+ function queueApprovalWait(rpc, args, once) {
190
+ const key = `${once === finishBatch ? 'batch' : 'agent'}:${args.batch_ref ?? args.connection_ref}`;
191
+ if (approvalTasks.has(key)) throw new Error('Use the existing approval wait for this same reference; it is still running.');
192
+ if (approvalTasks.size >= 20) throw new Error('Approval wait limit reached. Finish or cancel a pending request first.');
193
+ // Waiting for a browser must not block steering or stopping another bound agent.
194
+ const task = waitForApproval(args, once).then(value => send({ jsonrpc:'2.0', id:rpc.id, result:result(value) }), error => {
195
+ const safe = /^(Use |Approval wait|This binding|This project agent|Browser setup|Another operation|Another browser)/.test(error.message ?? '');
196
+ send(fail(rpc.id, -32000, safe ? error.message : 'Connection unavailable. Review the original request; no permissions were changed.'));
197
+ }).finally(() => approvalTasks.delete(key));
198
+ approvalTasks.set(key, task);
199
+ return task;
200
+ }
201
+ async function waitForApproval(args, once) {
202
+ const { wait_seconds = 0, ...selection } = args;
203
+ if (!Number.isInteger(wait_seconds) || wait_seconds < 0 || wait_seconds > 60) throw new Error('Approval wait must be between 0 and 60 seconds.');
204
+ let remaining = wait_seconds * 1000;
205
+ while (true) {
206
+ const response = await once(selection);
207
+ if (response.state !== 'pending' || remaining <= 0 || stopped) return response;
208
+ const delay = Math.min(5000, remaining);
209
+ await approvalWait(delay); remaining -= delay;
210
+ if (stopped) return response;
211
+ }
212
+ }
213
+ async function finishBatch(args) {
214
+ if (Object.keys(args).some(key => key !== 'batch_ref') || !batches.has(args.batch_ref)) throw new Error('Use the exact batch_ref returned during onboarding in this MCP process.');
215
+ const batch = batches.get(args.batch_ref);
216
+ for (const context of batch.contexts) assertLive(context);
217
+ if (batch.contexts.every(context => context.record)) return { state:'bound', batch_ref:batch.ref, agents:batch.contexts.map(state) };
218
+ return batchResponse(await batchBrowser.prepare({ connection:batch.id }),batch);
219
+ }
220
+ function contextFor(args) {
221
+ const context = bindings.get(args.connection_ref);
222
+ if (!context) throw new Error('Use an exact connection_ref returned by connect_project_agent in this MCP process. There is no default connection.');
223
+ return context;
224
+ }
225
+ async function finish(args) {
226
+ if (Object.keys(args).some(key => !['connection_ref', 'connection_id'].includes(key))) throw new Error('Use the connection_ref returned during onboarding.');
227
+ const context = contextFor(args);
228
+ if (args.connection_id !== undefined && args.connection_id !== context.selected.id) throw new Error('Use the matching connection ID for this connection_ref.');
229
+ assertLive(context);
230
+ if (context.record) return state(context);
231
+ const response = await browser.prepare({ connection: context.selected.id });
232
+ assertLive(context);
233
+ context.observedState = response.status;
234
+ return response.schema === 'mcp-connection-1' ? bind(response, context) : { ...state(context), state: response.status, next_step: response.status === 'pending' ? 'Approval is still pending. Ask the human to approve in Karma42; retry this same connection_ref.' : 'The request was cancelled or expired. Ask your human before starting a fresh connection request.' };
235
+ }
236
+ async function closeContext(context) {
237
+ context.closing ??= (async () => {
238
+ // Stop the model process before aborting/releasing its shared coordination run.
239
+ try {
240
+ // A start may be validating its checkout or preparing the local bridge. Signal stop
241
+ // immediately, then await that setup and stop again before releasing coordination.
242
+ await context.worker?.stop();
243
+ if (context.startTask) { await context.startTask.catch(() => {}); await context.worker?.stop(); }
244
+ }
245
+ finally {
246
+ context.signals?.emit('SIGTERM'); context.input?.end();
247
+ if (context.task) await context.task.catch(() => {});
248
+ context.lines?.close(); context.output?.destroy();
249
+ }
250
+ })();
251
+ return context.closing;
252
+ }
253
+ function verifiedContext(args) {
254
+ const context = contextFor(args);
255
+ if (!context.verified || !context.ready) throw new Error('Call get_project with this connection_ref and verify identity before controlling a worker.');
256
+ if (context.stopped || context.stopping || context.closing) throw new Error('This binding stopped. Disconnect its connection_ref before reconnecting.');
257
+ return context;
258
+ }
259
+ function workerStatus(context) {
260
+ const current = context.worker?.status();
261
+ if (current && (current.state !== 'idle' || current.mission_ref)) return current;
262
+ const recovery = context.record ? workerRecoveryStatus(connectionDir(home, context.record.id), context.record) : null;
263
+ return recovery ? { state: 'recovery_required', active: false, recovery } : current ?? { state: 'not_started', active: false };
264
+ }
265
+ async function startWorker(args) {
266
+ if (Object.keys(args).some(key => !['connection_ref', 'client', 'model', 'minutes', 'scope', 'folder', 'approved', 'recovery_ref'].includes(key))) throw new Error('Use only the documented worker options.');
267
+ const context = verifiedContext(args), minutes = args.minutes === undefined ? 60 : args.minutes;
268
+ if (args.approved !== true) throw new Error('Worker start requires your human’s explicit approval for this worker, duration, folder and work scope. Connection approval is insufficient.');
269
+ if (!['claude', 'codex'].includes(args.client) || !Number.isInteger(minutes) || minutes < 15 || minutes > 240 || minutes % 15 !== 0) throw new Error('Choose a supported worker client and 15–240 minutes in 15-minute increments.');
270
+ if (typeof args.scope !== 'string' || !args.scope.trim() || args.scope.length > 4000 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(args.scope)) throw new Error('Use a clear, human-approved work scope of at most 4000 characters.');
271
+ if (typeof args.folder !== 'string' || !isAbsolute(args.folder)) throw new Error('Use the approved absolute worker checkout folder.');
272
+ const folder = canonicalPath(args.folder); assertStateOutsideWorkspace(home, folder);
273
+ for (const other of bindings.values()) {
274
+ if (other === context || other.stopped || other.closing) continue;
275
+ const candidates = [other.selected.folder, ...(workerOwnsMutations(other) && other.workerFolder ? [other.workerFolder] : [])];
276
+ for (const candidate of candidates) {
277
+ try { assertStateOutsideWorkspace(candidate, folder); } catch { throw new Error('Use separate sibling checkouts: this folder overlaps another project-agent binding or active worker in this MCP process.'); }
278
+ }
279
+ }
280
+ if (workerOwnsMutations(context)) throw new Error('This worker already owns the approved work session. Use worker controls; starting again cannot extend its deadline.');
281
+ if (context.mutationInFlight) throw new Error('This binding has a project change in progress. Wait for its result before starting a worker.');
282
+ if (!context.worker || ['completed', 'stopped'].includes(context.worker.status().state)) {
283
+ const factory = workerFactory ?? createProjectWorker;
284
+ if (context.stopped || context.stopping || context.closing) throw new Error('This binding is stopping. A worker was not started.');
285
+ context.worker = factory({ ...context.ready, stateDir: connectionDir(home, context.record.id) });
286
+ }
287
+ try {
288
+ context.workerFolder = folder;
289
+ context.starting = true;
290
+ context.startTask = Promise.resolve().then(() => context.worker.start({ client: args.client, model: args.model, minutes: args.recovery_ref === undefined ? minutes : args.minutes, scope: args.scope.trim(), folder, approved: true, recovery_ref: args.recovery_ref }));
291
+ const snapshot = await context.startTask;
292
+ if (context.stopped || context.stopping || context.closing) { await context.worker.stop(); throw new Error('This binding is stopping. The worker was stopped.'); }
293
+ return { connection_ref: context.ref, worker: snapshot };
294
+ } catch (error) {
295
+ const safe = /^(Worker checkout|Worker ownership|Worker recovery|Start the project|The MCP session closed|Use separate sibling|The worker folder|Choose a separate|This worker adapter|Saved worker model session|Explicit human approval|This work session already|Supervised workers|Choose a work duration|Provide a clear|Cannot run (claude|codex) --version|Sign in locally with codex login|This adapter requires Codex CLI|Cannot inspect local MCP configuration|Invalid local MCP configuration|Unsupported local MCP configuration)/.test(error.message ?? '');
296
+ throw new Error(safe ? error.message : 'Worker could not start. Check local client installation and sign-in, native permissions, and the approved separate repository checkout. No permissions were changed.');
297
+ } finally { context.starting = false; }
298
+ }
299
+ async function controlWorker(args) {
300
+ if (Object.keys(args).some(key => !['connection_ref', 'action', 'instruction'].includes(key)) || !['pause', 'resume', 'stop', 'steer'].includes(args.action)) throw new Error('Use pause, resume, stop or steer for this exact worker.');
301
+ const context = verifiedContext(args);
302
+ if (!context.worker || (!context.starting && context.worker.status().state === 'idle' && !context.worker.status().mission_ref)) {
303
+ if (args.action === 'stop' && args.instruction === undefined) {
304
+ const cancelled = await cancelWorkerRecovery(connectionDir(home, context.record.id), context.record);
305
+ await context.worker?.stop();
306
+ return { connection_ref: context.ref, worker: cancelled };
307
+ }
308
+ throw new Error('This binding has no worker. Read get_project_worker for a saved mission and ask your human before starting or recovering one.');
309
+ }
310
+ if (args.action === 'steer') {
311
+ if (typeof args.instruction !== 'string' || !args.instruction.trim() || args.instruction.length > 2000 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(args.instruction)) throw new Error('Use a steering instruction of at most 2000 characters.');
312
+ const receipt = await context.worker.steer({ message: args.instruction.trim() });
313
+ return { connection_ref: context.ref, steering_request: receipt, worker: context.worker.status() };
314
+ }
315
+ if (args.instruction !== undefined) throw new Error('Use instruction only for a steer action.');
316
+ return { connection_ref: context.ref, worker: await context.worker[args.action]() };
317
+ }
318
+ function workerOwnsMutations(context) {
319
+ if (context.starting) return true;
320
+ if (!context.worker) return false;
321
+ const current = context.worker.status();
322
+ if (typeof current.active === 'boolean') return current.active;
323
+ return !['idle', 'stopped', 'completed', 'expired', 'failed'].includes(current.state ?? current.status);
324
+ }
325
+ async function handle(rpc) {
326
+ try {
327
+ if (rpc.method === 'initialize') {
328
+ const observed = rpc.params?.clientInfo?.name;
329
+ if (typeof observed === 'string' && observed.trim() && observed.length <= 100 && !/[\x00-\x1f\x7f-\x9f]/.test(observed)) clientName = observed.trim();
330
+ send({ jsonrpc: '2.0', id: rpc.id, result: { protocolVersion: '2025-03-26', capabilities: { tools: {} }, serverInfo: { name: 'karma42', version: VERSION }, instructions: GUIDE } });
331
+ } else if (rpc.method === 'tools/list') send({ jsonrpc: '2.0', id: rpc.id, result: { tools: catalog } });
332
+ else if (rpc.method === 'ping') send({ jsonrpc: '2.0', id: rpc.id, result: {} });
333
+ else if (rpc.method !== 'tools/call') send(fail(rpc.id, -32601, 'Unsupported MCP method.'));
334
+ else {
335
+ const name = rpc.params?.name, args = rpc.params?.arguments ?? {};
336
+ if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('Tool arguments must be an object.');
337
+ if (name === 'get_karma42_guide') {
338
+ if (Object.keys(args).length) throw new Error('The guide takes no arguments.');
339
+ send({jsonrpc:'2.0',id:rpc.id,result:{content:[{type:'text',text:GUIDE}]}});
340
+ }
341
+ else if (OWNER_TOOLS.some(tool => tool.name === name)) {
342
+ if (name === 'connect_owner_account') {
343
+ if (Object.keys(args).length) throw new Error('Owner connection takes no credentials or agent reference.');
344
+ const account = await owner.connect(clientName); ownerConnected = true;
345
+ send({ jsonrpc: '2.0', id: rpc.id, result: result({ ...account, owner_ref: ownerReference }) });
346
+ } else {
347
+ if (!ownerConnected || args.owner_ref !== ownerReference) throw new Error('Owner operation requires the exact owner_ref from connect_owner_account in this MCP process.');
348
+ const { owner_ref, ...fields } = args;
349
+ const account = name === 'finish_owner_account_connection' ? (Object.keys(fields).length ? (() => { throw new Error('Owner finish takes only owner_ref.'); })() : await owner.finish()) : await owner.operation(name, fields);
350
+ if (name === 'disconnect_owner_account') ownerConnected = false;
351
+ send({ jsonrpc: '2.0', id: rpc.id, result: result({ ...account, owner_ref: ownerReference }) });
352
+ }
353
+ }
354
+ else if (DISCOVERY_TOOLS.some(tool => tool.name === name)) send({ jsonrpc: '2.0', id: rpc.id, result: result(await discovery.call(name, args)) });
355
+ else if (name === 'list_project_agents') {
356
+ if (Object.keys(args).some(key => key !== 'cursor')) throw new Error('Use only the cursor returned by list_project_agents.');
357
+ const entries = [...bindings.values()];
358
+ const offset = args.cursor === undefined ? 0 : entries.findIndex(context => context.ref === args.cursor) + 1;
359
+ if (args.cursor !== undefined && (typeof args.cursor !== 'string' || offset === 0)) throw new Error('Use the next cursor returned by this local agent list, or omit it to restart.');
360
+ const page = entries.slice(offset, offset + 20);
361
+ send({ jsonrpc: '2.0', id: rpc.id, result: result({ scope: 'this_mcp_process', agents: page.map(context => ({ ...state(context), worker: workerStatus(context) })), next: entries.length > offset + 20 ? page.at(-1).ref : null, active_workers: entries.filter(workerOwnsMutations).length, presence: 'local observation; not service online state' }) });
362
+ }
363
+ else if (name === 'connect_project_agents') send({ jsonrpc:'2.0', id:rpc.id, result:result(await connectBatch(args)) });
364
+ else if (name === 'finish_project_agents_connection') await queueApprovalWait(rpc, args, finishBatch);
365
+ else if (name === 'connect_project_agent') send({ jsonrpc: '2.0', id: rpc.id, result: result(await connect(args)) });
366
+ else if (name === 'finish_project_agent_connection') await queueApprovalWait(rpc, args, finish);
367
+ else if (name === 'start_project_worker') send({ jsonrpc: '2.0', id: rpc.id, result: result(await startWorker(args)) });
368
+ else if (name === 'get_project_worker') {
369
+ if (Object.keys(args).some(key => key !== 'connection_ref')) throw new Error('Use only connection_ref for worker status.');
370
+ const context = contextFor(args);
371
+ send({ jsonrpc: '2.0', id: rpc.id, result: result({ connection_ref: context.ref, worker: workerStatus(context) }) });
372
+ }
373
+ else if (name === 'control_project_worker') send({ jsonrpc: '2.0', id: rpc.id, result: result(await controlWorker(args)) });
374
+ else if (name === 'disconnect_project_agent') {
375
+ if (Object.keys(args).some(key => key !== 'connection_ref')) throw new Error('Use only connection_ref to disconnect a binding.');
376
+ const context = contextFor(args); await closeContext(context); bindings.delete(context.ref); identities.delete(context.selected.id);
377
+ for (const [key,batch] of batches) if (batch.contexts.includes(context)) batches.delete(key);
378
+ send({ jsonrpc: '2.0', id: rpc.id, result: result({ state: 'disconnected', connection_ref: context.ref }) });
379
+ } else if (!allowed.has(name)) send(fail(rpc.id, -32602, 'Unknown Karma42 tool.'));
380
+ else {
381
+ const context = contextFor(args);
382
+ if (!context.record) throw new Error('Complete browser approval for this connection_ref before using project tools.');
383
+ if (context.stopped) throw new Error('This binding stopped. Disconnect its connection_ref before reconnecting.');
384
+ if (context.closing || context.stopping) throw new Error('This binding is stopping. Wait for the worker to stop before reconnecting.');
385
+ if (workerOwnsMutations(context) && !READ_TOOLS.has(name)) throw new Error('This worker owns project mutations. Use control_project_worker to steer or stop it; the supervisor must not mutate concurrently.');
386
+ const mutation = !READ_TOOLS.has(name);
387
+ if (mutation) context.mutationInFlight = true;
388
+ try {
389
+ const { connection_ref, ...projectArgs } = args;
390
+ const response = new Promise((resolve, reject) => context.waiting.set(rpc.id, { resolve, reject }));
391
+ context.input.write(`${JSON.stringify({ ...rpc, params: { ...rpc.params, arguments: projectArgs } })}\n`);
392
+ const reply = await response;
393
+ if (name === 'get_project' && reply.result && !reply.result.isError) context.verified = true;
394
+ send(reply);
395
+ } finally { if (mutation) context.mutationInFlight = false; }
396
+ }
397
+ }
398
+ } catch (error) {
399
+ const safe = /^(Owner |Public discovery|This project is not published|Start the project|The MCP session closed|Only paused or blocked|There is no active work|Too many steering|Call get_project|Worker |Worker start|Choose |Provide a clear|Explicit human approval|Supervised workers|Saved worker model session|The worker folder|This work session|This worker|Sign in locally|Cannot run (claude|codex)|This adapter requires|Cannot inspect local MCP|Invalid local MCP|Unsupported local MCP|Private connection state|Use |Saved approval|This project agent|This binding|Cannot replace|Complete browser|Invalid project|Invalid private credential|Tool arguments|No browser setup|Browser setup|Browser approval reply|Unexpected browser|Approved identity|A browser setup|Another operation|Another browser|The connection lock|Unsupported or invalid MCP)/.test(error.message ?? '');
400
+ send(fail(rpc.id, -32000, safe ? error.message : 'Connection unavailable. Review My agents and the working folder; no permissions were changed.'));
401
+ }
402
+ }
403
+ // Preserve change ordering within an account or binding, while local supervision
404
+ // uses a separate lane and reserved capacity. Network reads never queue controls.
405
+ const requests = new Map(), lanes = new Map();
406
+ let ordinary = 0, priority = 0;
407
+ function route(rpc) {
408
+ const name = rpc.params?.name, args = rpc.params?.arguments;
409
+ if (rpc.method !== 'tools/call' || ['get_karma42_guide', 'list_project_agents', 'get_project_worker'].includes(name)) return { priority: true };
410
+ if (['control_project_worker', 'disconnect_project_agent'].includes(name)) return { priority: true, lane: `control:${args?.connection_ref}` };
411
+ if (OWNER_TOOLS.some(tool => tool.name === name)) return { lane: 'owner' };
412
+ if (DISCOVERY_TOOLS.some(tool => tool.name === name)) return { lane: 'discovery' };
413
+ if (['connect_project_agent', 'connect_project_agents', 'start_project_worker'].includes(name)) return { lane: 'setup' };
414
+ if (name === 'finish_project_agent_connection' || name === 'finish_project_agents_connection') return {};
415
+ return allowed.has(name) ? { lane: `project:${args?.connection_ref}` } : {};
416
+ }
417
+ function dispatch(rpc) {
418
+ if (requests.has(rpc.id)) { send(fail(null, -32600, 'Use a unique ID for each outstanding MCP request.')); return; }
419
+ const selected = route(rpc);
420
+ if (selected.priority ? priority >= 32 : ordinary >= 128) { send(fail(rpc.id, -32000, 'MCP request queue is full. Wait for outstanding requests before retrying.')); return; }
421
+ if (selected.priority) priority++; else ordinary++;
422
+ const previous = lanes.get(selected.lane);
423
+ const task = Promise.resolve(previous).then(() => { if (!stopped) return handle(rpc); }).finally(() => {
424
+ requests.delete(rpc.id);
425
+ if (selected.priority) priority--; else ordinary--;
426
+ if (selected.lane && lanes.get(selected.lane) === task) lanes.delete(selected.lane);
427
+ });
428
+ requests.set(rpc.id, task);
429
+ if (selected.lane) lanes.set(selected.lane, task);
430
+ }
431
+ try {
432
+ for await (const line of lines) {
433
+ if (stopped) break;
434
+ if (Buffer.byteLength(line) > 128 * 1024) { send(fail(null, -32600, 'MCP request is too large.')); break; }
435
+ let rpc;
436
+ try { rpc = JSON.parse(line); } catch { send(fail(null, -32700, 'Invalid JSON.')); continue; }
437
+ if (rpc?.jsonrpc !== '2.0' || typeof rpc.method !== 'string' || (rpc.id !== undefined && typeof rpc.id !== 'string' && typeof rpc.id !== 'number')) { send(fail(null, -32600, 'Invalid MCP request.')); continue; }
438
+ if (rpc.id !== undefined) dispatch(rpc);
439
+ }
440
+ } finally {
441
+ stop(); await Promise.allSettled([...requests.values(), ...[...bindings.values()].map(closeContext)]);
442
+ input.off('end', stop); for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) signals.off(signal, stop);
443
+ }
444
+ }