@volter/supercode-orchestrator 0.5.50 → 0.5.52
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 +26 -1
- package/gates.mjs +5 -3
- package/index.d.ts +2 -0
- package/installed-apps.mjs +19 -0
- package/loop.mjs +2 -2
- package/mcp/mount.mjs +28 -2
- 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 } 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,31 @@ const ACCESS_OPS = {
|
|
|
460
460
|
* `apps remove NAME`. */
|
|
461
461
|
async function appsMain(argv) {
|
|
462
462
|
const [verb, ...rest] = argv;
|
|
463
|
+
// `apps exec rh2-agent --profile <agent> [--root R] -- <command…>`: the command run as the installed agent, its
|
|
464
|
+
// credential in the command's environment (rh2AgentEnv) and nowhere else: no config file carries the token. It is
|
|
465
|
+
// how an agent's session mounts `rh2 mcp` (mcp/mount.mjs).
|
|
466
|
+
if (verb === 'exec') {
|
|
467
|
+
const cut = rest.indexOf('--');
|
|
468
|
+
const own = cut < 0 ? rest : rest.slice(0, cut);
|
|
469
|
+
const command = cut < 0 ? [] : rest.slice(cut + 1);
|
|
470
|
+
const opts = {};
|
|
471
|
+
for (let i = 0; i < own.length; i += 1) if (own[i].startsWith('--')) { opts[own[i].slice(2)] = own[i + 1]; i += 1; }
|
|
472
|
+
const app = own.find((word, i) => !word.startsWith('--') && !own[i - 1]?.startsWith('--'));
|
|
473
|
+
if (app !== 'rh2-agent' || !opts.profile || !command.length) {
|
|
474
|
+
process.stderr.write('apps exec rh2-agent --profile <agent> [--root R] -- <command…>\n');
|
|
475
|
+
process.exit(2);
|
|
476
|
+
}
|
|
477
|
+
const record = installedAgentRecord(resolve(opts.root ?? defaultRoot()), opts.profile);
|
|
478
|
+
if (!record) {
|
|
479
|
+
process.stderr.write(`no rh2-agent is installed for ${opts.profile}: install it with apps install rh2-agent --profile ${opts.profile}\n`);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
const child = spawn(command[0], command.slice(1), { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record) } });
|
|
483
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
484
|
+
child.on('error', (error) => { process.stderr.write(`apps exec: ${error.message}\n`); process.exit(127); });
|
|
485
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
463
488
|
const flags = {}, positional = [];
|
|
464
489
|
for (let i = 0; i < rest.length; i += 1) {
|
|
465
490
|
if (rest[i].startsWith('--')) { flags[rest[i].slice(2)] = rest[i + 1]; i += 1; } else positional.push(rest[i]);
|
package/gates.mjs
CHANGED
|
@@ -81,7 +81,7 @@ function scriptEnv() {
|
|
|
81
81
|
* else with python3; the cwd is the job's workdir, else the scripts folder.
|
|
82
82
|
* Returns `{ok, output}`; a failure's output is the reason, in Hermes's words.
|
|
83
83
|
*/
|
|
84
|
-
export async function runJobScript(profile, scriptPath, workdir = null, { env = process.env, signal = null, secrets = [], automation = null } = {}) {
|
|
84
|
+
export async function runJobScript(profile, scriptPath, workdir = null, { env = process.env, signal = null, secrets = [], automation = null, fire = null } = {}) {
|
|
85
85
|
const raw = String(scriptPath ?? '');
|
|
86
86
|
if (raw.includes('\0')) return { ok: false, output: `Blocked: script path contains a NUL byte: ${JSON.stringify(raw)}` };
|
|
87
87
|
const scriptsDir = join(profile.dir, 'scripts');
|
|
@@ -103,8 +103,10 @@ export async function runJobScript(profile, scriptPath, workdir = null, { env =
|
|
|
103
103
|
const out = { stdout: [], stderr: [] };
|
|
104
104
|
const size = { stdout: 0, stderr: 0 };
|
|
105
105
|
let finished = false;
|
|
106
|
-
// A scheduled script is an automation: what it files names the automation, never an agent or the OS user (C4)
|
|
107
|
-
|
|
106
|
+
// A scheduled script is an automation: what it files names the automation, never an agent or the OS user (C4).
|
|
107
|
+
// SUPERCODE_FIRE_ID names the fire it runs for, so what it files can be keyed to that fire (a retried run of the
|
|
108
|
+
// same fire files nothing new), not to the wall clock an interval schedule drifts against.
|
|
109
|
+
const child = spawn(argv[0], argv[1], { cwd, env: { ...scriptEnv(), HERMES_HOME: profile.dir, ...(automation ? { SUPERCODE_AUTOMATION: automation } : {}), ...(fire ? { SUPERCODE_FIRE_ID: String(fire) } : {}) }, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
108
110
|
const kill = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already gone */ } };
|
|
109
111
|
const end = (result) => {
|
|
110
112
|
if (finished) return;
|
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
|
@@ -44,6 +44,25 @@ export async function installAgent({ root, origin, organization, principal, toke
|
|
|
44
44
|
return { ...record, name: `rh2-agent.${profile}`, token: undefined };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** The rh2-agent record installed for `agent` (an agent name, as `apps install rh2-agent --profile <agent>` names it),
|
|
48
|
+
* or null. */
|
|
49
|
+
export function installedAgentRecord(root, agent) {
|
|
50
|
+
if (typeof agent !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent)) return null;
|
|
51
|
+
const path = recordPath(root, 'rh2-agent', agent);
|
|
52
|
+
if (!existsSync(path)) return null;
|
|
53
|
+
try {
|
|
54
|
+
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
55
|
+
return record?.app === 'rh2-agent' && record.token ? record : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The environment the `rh2` CLI reads to act as an installed agent: its RH2 origin, its organization and its token. */
|
|
62
|
+
export function rh2AgentEnv(record) {
|
|
63
|
+
return { RH2_BASE_URL: record.origin, RH2_ORGANIZATION: record.organizationId, RH2_SESSION_TOKEN: record.token };
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/** Exchange an RH2 install code for the installation's token and keep the record. */
|
|
48
67
|
export async function installApp({ root, name, origin, clientId, clientSecret, code, profile = 'default', fetchImpl = globalThis.fetch }) {
|
|
49
68
|
const app = INSTALLABLE_APPS[name];
|
package/loop.mjs
CHANGED
|
@@ -385,7 +385,7 @@ export class Orchestrator {
|
|
|
385
385
|
}
|
|
386
386
|
// Hermes runs a no_agent script in the job's workdir, and an agent job's
|
|
387
387
|
// pre-run script from the scripts folder
|
|
388
|
-
const script = gate.script ? await runJobScript(profile, gate.script, gate.no_agent ? workdir : null, { signal, secrets, automation: `cron:${profile.name}/${job.id}
|
|
388
|
+
const script = gate.script ? await runJobScript(profile, gate.script, gate.no_agent ? workdir : null, { signal, secrets, automation: `cron:${profile.name}/${job.id}`, fire: fire_id }) : null;
|
|
389
389
|
return { monitor, script };
|
|
390
390
|
})()
|
|
391
391
|
.catch((error) => ({ script: { ok: false, output: `Script execution failed: ${error.message}` } }))
|
|
@@ -452,7 +452,7 @@ export class Orchestrator {
|
|
|
452
452
|
// this conversation, mounted through whatever door this harness publishes.
|
|
453
453
|
const mount = mountOrchestratorMcp({
|
|
454
454
|
root: this.state.root, profile, harness: worker.harness, surfaceKey: keyString, launch,
|
|
455
|
-
socketPath: this.bridge?.path ?? socketPathFor(this.state.root), vault: this.vault,
|
|
455
|
+
socketPath: this.bridge?.path ?? socketPathFor(this.state.root), vault: this.vault, agent: agent?.identity?.name ?? null,
|
|
456
456
|
});
|
|
457
457
|
this.log('info', `mcp mount (${worker.harness}, ${mount.door}): ${mount.ran}`);
|
|
458
458
|
if (resume_runtime_id) resume_runtime_id = await this.continueFromHermes(profile, keyString, worker, resume_runtime_id, cwd,agentHome);
|
package/mcp/mount.mjs
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// worker's own environment, which `activationEnv` already set per session.
|
|
14
14
|
|
|
15
15
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
16
|
-
import { dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
17
18
|
import { fileURLToPath } from 'node:url';
|
|
18
19
|
import {
|
|
19
20
|
acpEntry, doorFor, opencodeEntry, readOpencodeConfig, removeTomlBlock, serverToml, tomlString, upsertTomlBlock, withMcpConfigFlag, writeMcpConfigFile,
|
|
@@ -23,11 +24,31 @@ import { SERVER_NAME } from './tools.mjs';
|
|
|
23
24
|
import { SOCKET_ENV, socketPathFor } from './server.mjs';
|
|
24
25
|
import { mcpServers } from '../hermes-home.mjs';
|
|
25
26
|
import { KANBAN_ENV } from '../hermes-cli.mjs';
|
|
27
|
+
import { installedAgentRecord } from '../installed-apps.mjs';
|
|
26
28
|
|
|
27
29
|
export { MCP_DOORS, doorFor } from '@volter/supercode-harness-sdk/mcp-doors';
|
|
28
30
|
|
|
29
31
|
/** The stdio server this module mounts. */
|
|
30
32
|
export const SERVER_PATH = resolve(dirname(fileURLToPath(import.meta.url)), 'server.mjs');
|
|
33
|
+
/** The orchestrator's CLI, whose `apps exec` runs a command as an installed agent. */
|
|
34
|
+
const ORCHESTRATOR_BIN = resolve(dirname(fileURLToPath(import.meta.url)), '../bin/orchestrator.mjs');
|
|
35
|
+
/** The MCP server an agent with an installed rh2-agent gets: `rh2 mcp` as that agent (its own name, so the user's own
|
|
36
|
+
* `rh2` server, whatever it holds, is neither replaced nor relied on). */
|
|
37
|
+
export const RH2_AGENT_SERVER = 'rh2-agent';
|
|
38
|
+
|
|
39
|
+
/** `rh2 mcp` run as `agent`'s installed RH2 principal: `apps exec` puts the record's origin, organization and token
|
|
40
|
+
* in its environment, so no config file carries the token. Null when no rh2-agent is installed for it. */
|
|
41
|
+
function rh2AgentServer(root, agent, program) {
|
|
42
|
+
if (!installedAgentRecord(root, agent)) return null;
|
|
43
|
+
return { command: program, args: [ORCHESTRATOR_BIN, 'apps', 'exec', 'rh2-agent', '--root', root, '--profile', agent, '--', rh2Program(), 'mcp'] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The `rh2` CLI: on this process's PATH, else where RH2's installer puts it (`~/.local/bin`), else the bare name. A
|
|
47
|
+
* session's own PATH may lack the folder, so the server names the file. */
|
|
48
|
+
function rh2Program() {
|
|
49
|
+
const folders = [...String(process.env.PATH ?? '').split(delimiter), join(homedir(), '.local', 'bin')].filter(Boolean);
|
|
50
|
+
return folders.map((folder) => join(folder, 'rh2')).find((file) => existsSync(file)) ?? 'rh2';
|
|
51
|
+
}
|
|
31
52
|
|
|
32
53
|
const slug = (surfaceKey) => (surfaceKey ? String(surfaceKey).replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'session' : 'session');
|
|
33
54
|
|
|
@@ -43,13 +64,18 @@ const slug = (surfaceKey) => (surfaceKey ? String(surfaceKey).replace(/[^A-Za-z0
|
|
|
43
64
|
* @param {string} [o.socketPath] defaults to `<root>/orchestrator.sock`
|
|
44
65
|
* @param {string} [o.serverPath] defaults to this package's `mcp/server.mjs`
|
|
45
66
|
* @param {string} [o.program] the interpreter, defaults to this node
|
|
67
|
+
* @param {string|null} [o.agent] the agent the session is (its name), whose installed rh2-agent is mounted beside
|
|
46
68
|
* @returns {{door: string, ran: string, launch: object|undefined, mcp_servers: object[]|null, path: string|null}}
|
|
47
69
|
*/
|
|
48
|
-
export function mountOrchestratorMcp({ root, profile, harness, surfaceKey = null, launch, socketPath, serverPath = SERVER_PATH, program = process.execPath, vault = new Map(), task = null }) {
|
|
70
|
+
export function mountOrchestratorMcp({ root, profile, harness, surfaceKey = null, launch, socketPath, serverPath = SERVER_PATH, program = process.execPath, vault = new Map(), task = null, agent = null }) {
|
|
49
71
|
const door = doorFor(harness);
|
|
50
72
|
// the profile's own servers (`.mcp.json`, the workers' form; docs/plans/hermes-compat.md row 4), mounted
|
|
51
73
|
// beside this one through the same door, `${VAR}` resolved from the profile's vault
|
|
52
74
|
const own = resolveServers(mcpServers(profile.dir), vault);
|
|
75
|
+
// an agent the RH2 organization declares acts there as its own principal, through its installed record (ADR 0017),
|
|
76
|
+
// not through whatever `rh2` the machine's user configured; a profile's own `rh2-agent` entry wins
|
|
77
|
+
const rh2 = own[RH2_AGENT_SERVER] ? null : rh2AgentServer(root, agent, program);
|
|
78
|
+
if (rh2) own[RH2_AGENT_SERVER] = rh2;
|
|
53
79
|
const sock = socketPath ?? socketPathFor(root);
|
|
54
80
|
const home = join(profile.dir, harness);
|
|
55
81
|
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.52",
|
|
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": {
|