@volter/supercode-orchestrator 0.5.51 → 0.5.53
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/bin/runtime.mjs +25 -1
- package/index.d.ts +2 -0
- package/installed-apps.mjs +35 -3
- package/loop.mjs +2 -0
- package/mcp/mount.mjs +20 -1
- package/package.json +1 -1
package/bin/runtime.mjs
CHANGED
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
TICK_MS, accessPolicy, applyOperator, performRemoveProfileDir, OPERATOR_OPS,
|
|
71
71
|
} from '../index.mjs';
|
|
72
72
|
import * as events from '../events.mjs';
|
|
73
|
-
import { appView, installAgent, installApp, installedApps, removeApp } from '../installed-apps.mjs';
|
|
73
|
+
import { appView, installAgent, installApp, installedAgentRecord, installedApps, removeApp, rh2AgentEnv, rh2Program } from '../installed-apps.mjs';
|
|
74
74
|
import { httpJobExecutor } from '../hosted.mjs';
|
|
75
75
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
76
76
|
|
|
@@ -460,6 +460,30 @@ const ACCESS_OPS = {
|
|
|
460
460
|
* `apps remove NAME`. */
|
|
461
461
|
async function appsMain(argv) {
|
|
462
462
|
const [verb, ...rest] = argv;
|
|
463
|
+
// `apps mcp rh2-agent --profile <agent> [--root R]`: `rh2 mcp` as the installed agent, its credential in that one
|
|
464
|
+
// process's environment (rh2AgentEnv) and nowhere else: no config file carries the token, and the command is fixed,
|
|
465
|
+
// so nothing can be run in its place to print the token. It is how an agent's session mounts RH2 (mcp/mount.mjs).
|
|
466
|
+
if (verb === 'mcp') {
|
|
467
|
+
const opts = {};
|
|
468
|
+
const words = [];
|
|
469
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
470
|
+
if (rest[i].startsWith('--')) { opts[rest[i].slice(2)] = rest[i + 1]; i += 1; } else words.push(rest[i]);
|
|
471
|
+
}
|
|
472
|
+
if (words[0] !== 'rh2-agent' || words.length !== 1 || !opts.profile) {
|
|
473
|
+
process.stderr.write('apps mcp rh2-agent --profile <agent> [--root R]\n');
|
|
474
|
+
process.exit(2);
|
|
475
|
+
}
|
|
476
|
+
const record = installedAgentRecord(resolve(opts.root ?? defaultRoot()), opts.profile);
|
|
477
|
+
if (!record) {
|
|
478
|
+
process.stderr.write(`no rh2-agent is installed for ${opts.profile}: install it with apps install rh2-agent --profile ${opts.profile}\n`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
const child = spawn(rh2Program(), ['mcp'], { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record) } });
|
|
482
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
483
|
+
child.on('error', (error) => { process.stderr.write(`apps mcp: ${error.message}\n`); process.exit(127); });
|
|
484
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
463
487
|
const flags = {}, positional = [];
|
|
464
488
|
for (let i = 0; i < rest.length; i += 1) {
|
|
465
489
|
if (rest[i].startsWith('--')) { flags[rest[i].slice(2)] = rest[i + 1]; i += 1; } else positional.push(rest[i]);
|
package/index.d.ts
CHANGED
|
@@ -433,6 +433,8 @@ export function mountOrchestratorMcp(o: {
|
|
|
433
433
|
root: Path; profile: Profile; harness: string; surfaceKey?: string | null;
|
|
434
434
|
launch?: { program: string; arguments?: string[]; env?: Record<string, string> };
|
|
435
435
|
socketPath?: Path; serverPath?: Path; program?: string;
|
|
436
|
+
/** The agent the session is; its installed rh2-agent is mounted beside the orchestrator server as `rh2-agent`. */
|
|
437
|
+
agent?: string | null;
|
|
436
438
|
}): { door: McpDoor; ran: string; launch: unknown; mcp_servers: McpServerLaunch[] | null; path: Path | null };
|
|
437
439
|
|
|
438
440
|
export function socketPathFor(root: Path): Path;
|
package/installed-apps.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// `apps/` folder (owner-only); no `platforms:` block attaches an integration. Uninstalling on the other side revokes the
|
|
5
5
|
// token, and the channel stops; `remove` forgets the record here.
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
7
|
+
import { delimiter, join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
8
9
|
|
|
9
10
|
export const APPS_DIR = 'apps';
|
|
10
11
|
/** The apps this orchestrator can host, by name: what each installs and which adapter it attaches. */
|
|
@@ -31,7 +32,7 @@ export function installedApps(root) {
|
|
|
31
32
|
export async function installAgent({ root, origin, organization, principal, token, profile, fetchImpl = globalThis.fetch }) {
|
|
32
33
|
if (!profile) throw new Error('an RH2 agent is installed for a profile (--profile <agent>)');
|
|
33
34
|
if (!/^prn_/.test(String(principal))) throw new Error('--principal is the RH2 agent\'s principal id (prn_…)');
|
|
34
|
-
if (
|
|
35
|
+
if (!rh2Origin(origin)) throw new Error('the RH2 origin is an https URL');
|
|
35
36
|
const base = new URL(origin).origin;
|
|
36
37
|
const response = await fetchImpl(`${base}/api/v3/rooms`, { headers: { authorization: `Bearer ${token}`, 'x-rh2-organization': organization, origin: base, accept: 'application/json' } });
|
|
37
38
|
if (!response.ok) throw new Error(`RH2 refused the agent's token in ${organization}: HTTP ${response.status}`);
|
|
@@ -44,11 +45,42 @@ export async function installAgent({ root, origin, organization, principal, toke
|
|
|
44
45
|
return { ...record, name: `rh2-agent.${profile}`, token: undefined };
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/** The rh2-agent record installed for `agent` (an agent name, as `apps install rh2-agent --profile <agent>` names it),
|
|
49
|
+
* or null. */
|
|
50
|
+
export function installedAgentRecord(root, agent) {
|
|
51
|
+
if (typeof agent !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent)) return null;
|
|
52
|
+
const path = recordPath(root, 'rh2-agent', agent);
|
|
53
|
+
if (!existsSync(path)) return null;
|
|
54
|
+
try {
|
|
55
|
+
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
56
|
+
// the origin install accepted (https, or a loopback for a local RH2), held again wherever the record is used
|
|
57
|
+
return record?.app === 'rh2-agent' && record.token && rh2Origin(record.origin) ? record : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether `origin` is one an RH2 app may be installed for: https, or a loopback for a local RH2. */
|
|
64
|
+
function rh2Origin(origin) {
|
|
65
|
+
return /^https:\/\/|^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(String(origin ?? ''));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The `rh2` CLI: on this process's PATH, else where RH2's installer puts it (`~/.local/bin`), else the bare name. */
|
|
69
|
+
export function rh2Program() {
|
|
70
|
+
const folders = [...String(process.env.PATH ?? '').split(delimiter), join(homedir(), '.local', 'bin')].filter(Boolean);
|
|
71
|
+
return folders.map((folder) => join(folder, 'rh2')).find((file) => existsSync(file)) ?? 'rh2';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The environment the `rh2` CLI reads to act as an installed agent: its RH2 origin, its organization and its token. */
|
|
75
|
+
export function rh2AgentEnv(record) {
|
|
76
|
+
return { RH2_BASE_URL: record.origin, RH2_ORGANIZATION: record.organizationId, RH2_SESSION_TOKEN: record.token };
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
/** Exchange an RH2 install code for the installation's token and keep the record. */
|
|
48
80
|
export async function installApp({ root, name, origin, clientId, clientSecret, code, profile = 'default', fetchImpl = globalThis.fetch }) {
|
|
49
81
|
const app = INSTALLABLE_APPS[name];
|
|
50
82
|
if (!app) throw new Error(`no installable app named ${name} (${Object.keys(INSTALLABLE_APPS).join(', ')})`);
|
|
51
|
-
if (
|
|
83
|
+
if (!rh2Origin(origin)) throw new Error('the RH2 origin is an https URL');
|
|
52
84
|
const base = new URL(origin).origin;
|
|
53
85
|
const response = await fetchImpl(`${base}/api/v3/automation/apps/oauth/access`, { method: 'POST', redirect: 'manual', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify({ clientId, clientSecret, code }) });
|
|
54
86
|
const payload = await response.json().catch(() => ({}));
|
package/loop.mjs
CHANGED
|
@@ -453,6 +453,8 @@ export class Orchestrator {
|
|
|
453
453
|
const mount = mountOrchestratorMcp({
|
|
454
454
|
root: this.state.root, profile, harness: worker.harness, surfaceKey: keyString, launch,
|
|
455
455
|
socketPath: this.bridge?.path ?? socketPathFor(this.state.root), vault: this.vault,
|
|
456
|
+
// the agent's own sessions speak in RH2 as it; a worker launched to act for it (agent_as) does not
|
|
457
|
+
agent: agent_as ? null : agent?.identity?.name ?? null,
|
|
456
458
|
});
|
|
457
459
|
this.log('info', `mcp mount (${worker.harness}, ${mount.door}): ${mount.ran}`);
|
|
458
460
|
if (resume_runtime_id) resume_runtime_id = await this.continueFromHermes(profile, keyString, worker, resume_runtime_id, cwd,agentHome);
|
package/mcp/mount.mjs
CHANGED
|
@@ -23,11 +23,24 @@ import { SERVER_NAME } from './tools.mjs';
|
|
|
23
23
|
import { SOCKET_ENV, socketPathFor } from './server.mjs';
|
|
24
24
|
import { mcpServers } from '../hermes-home.mjs';
|
|
25
25
|
import { KANBAN_ENV } from '../hermes-cli.mjs';
|
|
26
|
+
import { installedAgentRecord } from '../installed-apps.mjs';
|
|
26
27
|
|
|
27
28
|
export { MCP_DOORS, doorFor } from '@volter/supercode-harness-sdk/mcp-doors';
|
|
28
29
|
|
|
29
30
|
/** The stdio server this module mounts. */
|
|
30
31
|
export const SERVER_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'server.mjs');
|
|
32
|
+
/** The orchestrator's CLI, whose `apps mcp` runs `rh2 mcp` as an installed agent. */
|
|
33
|
+
const ORCHESTRATOR_BIN = resolve(dirname(fileURLToPath(import.meta.url)), '../bin/orchestrator.mjs');
|
|
34
|
+
/** The MCP server an agent with an installed rh2-agent gets: `rh2 mcp` as that agent (its own name, so the user's own
|
|
35
|
+
* `rh2` server, whatever it holds, is neither replaced nor relied on). */
|
|
36
|
+
export const RH2_AGENT_SERVER = 'rh2-agent';
|
|
37
|
+
|
|
38
|
+
/** `rh2 mcp` run as `agent`'s installed RH2 principal: `apps mcp` puts the record's origin, organization and token in
|
|
39
|
+
* its environment, so no config file carries the token. Null when no rh2-agent is installed for it. */
|
|
40
|
+
function rh2AgentServer(root, agent, program) {
|
|
41
|
+
if (!installedAgentRecord(root, agent)) return null;
|
|
42
|
+
return { command: program, args: [ORCHESTRATOR_BIN, 'apps', 'mcp', 'rh2-agent', '--root', root, '--profile', agent] };
|
|
43
|
+
}
|
|
31
44
|
|
|
32
45
|
const slug = (surfaceKey) => (surfaceKey ? String(surfaceKey).replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'session' : 'session');
|
|
33
46
|
|
|
@@ -43,13 +56,19 @@ const slug = (surfaceKey) => (surfaceKey ? String(surfaceKey).replace(/[^A-Za-z0
|
|
|
43
56
|
* @param {string} [o.socketPath] defaults to `<root>/orchestrator.sock`
|
|
44
57
|
* @param {string} [o.serverPath] defaults to this package's `mcp/server.mjs`
|
|
45
58
|
* @param {string} [o.program] the interpreter, defaults to this node
|
|
59
|
+
* @param {string|null} [o.agent] the agent whose own session this is (its name), whose installed rh2-agent is mounted
|
|
60
|
+
* beside; null for a worker acting for an agent
|
|
46
61
|
* @returns {{door: string, ran: string, launch: object|undefined, mcp_servers: object[]|null, path: string|null}}
|
|
47
62
|
*/
|
|
48
|
-
export function mountOrchestratorMcp({ root, profile, harness, surfaceKey = null, launch, socketPath, serverPath = SERVER_PATH, program = process.execPath, vault = new Map(), task = null }) {
|
|
63
|
+
export function mountOrchestratorMcp({ root, profile, harness, surfaceKey = null, launch, socketPath, serverPath = SERVER_PATH, program = process.execPath, vault = new Map(), task = null, agent = null }) {
|
|
49
64
|
const door = doorFor(harness);
|
|
50
65
|
// the profile's own servers (`.mcp.json`, the workers' form; docs/plans/hermes-compat.md row 4), mounted
|
|
51
66
|
// beside this one through the same door, `${VAR}` resolved from the profile's vault
|
|
52
67
|
const own = resolveServers(mcpServers(profile.dir), vault);
|
|
68
|
+
// an agent the RH2 organization declares acts there as its own principal, through its installed record (ADR 0017),
|
|
69
|
+
// not through whatever `rh2` the machine's user configured; a profile's own `rh2-agent` entry wins
|
|
70
|
+
const rh2 = own[RH2_AGENT_SERVER] ? null : rh2AgentServer(root, agent, program);
|
|
71
|
+
if (rh2) own[RH2_AGENT_SERVER] = rh2;
|
|
53
72
|
const sock = socketPath ?? socketPathFor(root);
|
|
54
73
|
const home = join(profile.dir, harness);
|
|
55
74
|
const env = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
|
|
6
6
|
"exports": {
|