@volter/supercode-orchestrator 0.5.52 → 0.5.54

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 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, installedAgentRecord, installedApps, removeApp, rh2AgentEnv } 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,18 +460,17 @@ 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);
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') {
470
467
  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');
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');
475
474
  process.exit(2);
476
475
  }
477
476
  const record = installedAgentRecord(resolve(opts.root ?? defaultRoot()), opts.profile);
@@ -479,9 +478,14 @@ async function appsMain(argv) {
479
478
  process.stderr.write(`no rh2-agent is installed for ${opts.profile}: install it with apps install rh2-agent --profile ${opts.profile}\n`);
480
479
  process.exit(1);
481
480
  }
482
- const child = spawn(command[0], command.slice(1), { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record) } });
481
+ const program = rh2Program();
482
+ if (!program) {
483
+ process.stderr.write('apps mcp: no rh2 CLI in an absolute PATH folder or ~/.local/bin\n');
484
+ process.exit(127);
485
+ }
486
+ const child = spawn(program, ['mcp'], { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record) } });
483
487
  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); });
488
+ child.on('error', (error) => { process.stderr.write(`apps mcp: ${error.message}\n`); process.exit(127); });
485
489
  child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
486
490
  return;
487
491
  }
@@ -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, isAbsolute, 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 (!/^https:\/\/|^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(String(origin))) throw new Error('the RH2 origin is an https URL');
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}`);
@@ -52,12 +53,27 @@ export function installedAgentRecord(root, agent) {
52
53
  if (!existsSync(path)) return null;
53
54
  try {
54
55
  const record = JSON.parse(readFileSync(path, 'utf8'));
55
- return record?.app === 'rh2-agent' && record.token ? record : null;
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;
56
58
  } catch {
57
59
  return null;
58
60
  }
59
61
  }
60
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, by absolute path: in an absolute folder on this process's PATH, else where RH2's installer puts it
69
+ * (`~/.local/bin`); null when neither has it. A relative PATH entry is never searched (a session's working directory
70
+ * may be a checkout that carries its own `rh2`, which would then run with the agent's token), and neither is a bare
71
+ * name, which the OS would look up the same way. */
72
+ export function rh2Program() {
73
+ const folders = [...String(process.env.PATH ?? '').split(delimiter), join(homedir(), '.local', 'bin')].filter((folder) => folder && isAbsolute(folder));
74
+ return folders.map((folder) => join(folder, 'rh2')).find((file) => existsSync(file)) ?? null;
75
+ }
76
+
61
77
  /** The environment the `rh2` CLI reads to act as an installed agent: its RH2 origin, its organization and its token. */
62
78
  export function rh2AgentEnv(record) {
63
79
  return { RH2_BASE_URL: record.origin, RH2_ORGANIZATION: record.organizationId, RH2_SESSION_TOKEN: record.token };
@@ -67,7 +83,7 @@ export function rh2AgentEnv(record) {
67
83
  export async function installApp({ root, name, origin, clientId, clientSecret, code, profile = 'default', fetchImpl = globalThis.fetch }) {
68
84
  const app = INSTALLABLE_APPS[name];
69
85
  if (!app) throw new Error(`no installable app named ${name} (${Object.keys(INSTALLABLE_APPS).join(', ')})`);
70
- if (!/^https:\/\/|^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(String(origin))) throw new Error('the RH2 origin is an https URL');
86
+ if (!rh2Origin(origin)) throw new Error('the RH2 origin is an https URL');
71
87
  const base = new URL(origin).origin;
72
88
  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 }) });
73
89
  const payload = await response.json().catch(() => ({}));
package/loop.mjs CHANGED
@@ -452,7 +452,9 @@ 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, agent: agent?.identity?.name ?? null,
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
@@ -13,8 +13,7 @@
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 { delimiter, dirname, join, resolve } from 'node:path';
17
- import { homedir } from 'node:os';
16
+ import { dirname, join, resolve } from 'node:path';
18
17
  import { fileURLToPath } from 'node:url';
19
18
  import {
20
19
  acpEntry, doorFor, opencodeEntry, readOpencodeConfig, removeTomlBlock, serverToml, tomlString, upsertTomlBlock, withMcpConfigFlag, writeMcpConfigFile,
@@ -30,24 +29,17 @@ export { MCP_DOORS, doorFor } from '@volter/supercode-harness-sdk/mcp-doors';
30
29
 
31
30
  /** The stdio server this module mounts. */
32
31
  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. */
32
+ /** The orchestrator's CLI, whose `apps mcp` runs `rh2 mcp` as an installed agent. */
34
33
  const ORCHESTRATOR_BIN = resolve(dirname(fileURLToPath(import.meta.url)), '../bin/orchestrator.mjs');
35
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
36
35
  * `rh2` server, whatever it holds, is neither replaced nor relied on). */
37
36
  export const RH2_AGENT_SERVER = 'rh2-agent';
38
37
 
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. */
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. */
41
40
  function rh2AgentServer(root, agent, program) {
42
41
  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';
42
+ return { command: program, args: [ORCHESTRATOR_BIN, 'apps', 'mcp', 'rh2-agent', '--root', root, '--profile', agent] };
51
43
  }
52
44
 
53
45
  const slug = (surfaceKey) => (surfaceKey ? String(surfaceKey).replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'session' : 'session');
@@ -64,7 +56,8 @@ const slug = (surfaceKey) => (surfaceKey ? String(surfaceKey).replace(/[^A-Za-z0
64
56
  * @param {string} [o.socketPath] defaults to `<root>/orchestrator.sock`
65
57
  * @param {string} [o.serverPath] defaults to this package's `mcp/server.mjs`
66
58
  * @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
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
68
61
  * @returns {{door: string, ran: string, launch: object|undefined, mcp_servers: object[]|null, path: string|null}}
69
62
  */
70
63
  export function mountOrchestratorMcp({ root, profile, harness, surfaceKey = null, launch, socketPath, serverPath = SERVER_PATH, program = process.execPath, vault = new Map(), task = null, agent = null }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.52",
3
+ "version": "0.5.54",
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": {