@volter/supercode-orchestrator 0.5.215 → 0.5.216

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,275 @@
1
+ // Incarnation-bound local lifecycle door. A lease PID is never stop authority.
2
+ import { randomBytes, createHash } from "node:crypto";
3
+ import { chmodSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
4
+ import { createConnection, createServer } from "node:net";
5
+ import { join } from "node:path";
6
+ import { thisHost, LOCK_FILE } from "./lease.mjs";
7
+
8
+ const LIMIT = 16 * 1024;
9
+ const identityFields = [
10
+ "pid",
11
+ "started_at",
12
+ "root",
13
+ "host",
14
+ "boot",
15
+ "incarnation",
16
+ "launch_request",
17
+ ];
18
+ export const newDaemonIncarnation = () => randomBytes(32).toString("hex");
19
+ export function sameDaemon(a, b) {
20
+ return (
21
+ !!a &&
22
+ !!b &&
23
+ identityFields.every((key) => (a[key] ?? null) === (b[key] ?? null))
24
+ );
25
+ }
26
+ function selected(root, lease) {
27
+ const home = realpathSync(root);
28
+ const here = thisHost();
29
+ if (
30
+ !lease ||
31
+ !Number.isSafeInteger(lease.pid) ||
32
+ lease.pid <= 0 ||
33
+ typeof lease.started_at !== "string" ||
34
+ lease.root !== home ||
35
+ lease.host !== here.host ||
36
+ (lease.boot ?? null) !== here.boot ||
37
+ typeof lease.incarnation !== "string" ||
38
+ !/^[a-f0-9]{64}$/.test(lease.incarnation)
39
+ ) {
40
+ throw new Error(
41
+ "orchestrator stop refused: lease has no bound local daemon incarnation for this home",
42
+ );
43
+ }
44
+ return home;
45
+ }
46
+ function endpoint(root, lease) {
47
+ const key = createHash("sha256")
48
+ .update(root)
49
+ .update("\0")
50
+ .update(lease.incarnation)
51
+ .digest("hex")
52
+ .slice(0, 24);
53
+ if (process.platform === "win32")
54
+ return `\\\\.\\pipe\\supercode-orchestrator-${key}`;
55
+ const path = join(root, `oc-${key}.sock`);
56
+ if (Buffer.byteLength(path) > (process.platform === "darwin" ? 103 : 107))
57
+ throw new Error(
58
+ "orchestrator control socket path is too long; select a shorter home path",
59
+ );
60
+ return path;
61
+ }
62
+ function current(root) {
63
+ return JSON.parse(readFileSync(join(root, LOCK_FILE), "utf8"));
64
+ }
65
+
66
+ /** The daemon alone clears its exact issued record while it still holds the home lock. */
67
+ export function clearDaemonLease(root, lease) {
68
+ if (!sameDaemon(current(root), lease))
69
+ throw new Error("orchestrator lease changed; replacement record preserved");
70
+ unlinkSync(join(root, LOCK_FILE));
71
+ }
72
+
73
+ /** No fallback signal or lease write, including on cancellation or an unknown result. */
74
+ async function requestDaemonControl(
75
+ root,
76
+ lease,
77
+ op,
78
+ { signal, timeoutMs = 5000 } = {},
79
+ ) {
80
+ const home = selected(root, lease);
81
+ if (!sameDaemon(current(home), lease))
82
+ throw new Error("orchestrator stop refused: selected lease changed");
83
+ signal?.throwIfAborted();
84
+ return new Promise((resolve, reject) => {
85
+ const socket = createConnection(endpoint(home, lease));
86
+ let bytes = Buffer.alloc(0),
87
+ sent = false,
88
+ finished = false;
89
+ const finish = (error, receipt) => {
90
+ if (finished) return;
91
+ finished = true;
92
+ clearTimeout(timer);
93
+ signal?.removeEventListener("abort", abort);
94
+ socket.destroy();
95
+ if (error)
96
+ reject(
97
+ new Error(
98
+ `orchestrator ${op} ${sent ? "outcome unknown" : "refused before request"} (${lease.incarnation}): ${error.message}`,
99
+ { cause: error },
100
+ ),
101
+ );
102
+ else resolve(receipt);
103
+ };
104
+ const abort = () => finish(signal.reason ?? new Error("cancelled"));
105
+ const timer = setTimeout(
106
+ () =>
107
+ finish(
108
+ new Error("daemon control deadline expired; no automatic retry"),
109
+ ),
110
+ timeoutMs,
111
+ );
112
+ signal?.addEventListener("abort", abort, { once: true });
113
+ socket.on("error", (error) => finish(error));
114
+ socket.on("close", () =>
115
+ finish(
116
+ new Error(
117
+ "daemon control closed without a matching completion receipt",
118
+ ),
119
+ ),
120
+ );
121
+ socket.on("connect", () => {
122
+ try {
123
+ signal?.throwIfAborted();
124
+ if (!sameDaemon(current(home), lease))
125
+ throw new Error("selected lease changed before request");
126
+ sent = true;
127
+ socket.write(`${JSON.stringify({ version: 1, op, lease })}\n`);
128
+ } catch (error) {
129
+ finish(error);
130
+ }
131
+ });
132
+ socket.on("data", (chunk) => {
133
+ bytes = Buffer.concat([bytes, chunk]);
134
+ if (bytes.length > LIMIT)
135
+ return finish(new Error("oversized daemon control receipt"));
136
+ const end = bytes.indexOf(10);
137
+ if (end < 0) return;
138
+ try {
139
+ const receipt = JSON.parse(bytes.subarray(0, end).toString("utf8"));
140
+ if (
141
+ receipt.version !== 1 ||
142
+ !sameDaemon(receipt.daemon, lease) ||
143
+ !(
144
+ op === "describe"
145
+ ? ["running", "refused"]
146
+ : ["stopped", "failed", "refused"]
147
+ ).includes(receipt.status) ||
148
+ !Array.isArray(receipt.errors) ||
149
+ receipt.errors.some((error) => typeof error !== "string")
150
+ )
151
+ throw new Error("invalid daemon control receipt");
152
+ finish(null, receipt);
153
+ } catch (error) {
154
+ finish(error);
155
+ }
156
+ });
157
+ });
158
+ }
159
+ export const requestDaemonStop = (root, lease, options) =>
160
+ requestDaemonControl(root, lease, "stop", options);
161
+ export const describeDaemon = (root, lease, options) =>
162
+ requestDaemonControl(root, lease, "describe", options);
163
+
164
+ /** Separate from the model bridge: its response survives closing that bridge. */
165
+ export async function startDaemonControl({ root, lease, stop, complete }) {
166
+ const home = selected(root, lease),
167
+ path = endpoint(home, lease);
168
+ const sockets = new Map(),
169
+ tasks = new Set();
170
+ let closing = null,
171
+ stopping = false;
172
+ const server = createServer((socket) => {
173
+ sockets.set(socket, false);
174
+ socket.setTimeout(5000, () => socket.destroy());
175
+ socket.on("error", () => {});
176
+ socket.on("close", () => sockets.delete(socket));
177
+ let bytes = Buffer.alloc(0),
178
+ handled = false;
179
+ socket.on("data", (chunk) => {
180
+ if (handled) return;
181
+ bytes = Buffer.concat([bytes, chunk]);
182
+ if (bytes.length > LIMIT) {
183
+ handled = true;
184
+ socket.destroy();
185
+ return;
186
+ }
187
+ const end = bytes.indexOf(10);
188
+ if (end < 0) return;
189
+ handled = true;
190
+ let request;
191
+ try {
192
+ request = JSON.parse(bytes.subarray(0, end).toString("utf8"));
193
+ // Final admission is synchronous, against this process's immutable issuance
194
+ // and the home's current record; another PID's liveness never participates.
195
+ if (
196
+ closing ||
197
+ (stopping && request.op === "describe") ||
198
+ request.version !== 1 ||
199
+ !["stop", "describe"].includes(request.op) ||
200
+ !sameDaemon(request.lease, lease) ||
201
+ !sameDaemon(current(home), lease)
202
+ )
203
+ throw new Error(
204
+ "selected daemon incarnation is not the current home owner",
205
+ );
206
+ } catch (error) {
207
+ socket.end(
208
+ `${JSON.stringify({ version: 1, daemon: lease, status: "refused", errors: [error.message] })}\n`,
209
+ );
210
+ return;
211
+ }
212
+ if (request.op === "describe") {
213
+ socket.end(
214
+ `${JSON.stringify({ version: 1, daemon: lease, status: "running", errors: [] })}\n`,
215
+ );
216
+ return;
217
+ }
218
+ sockets.set(socket, true);
219
+ socket.setTimeout(0);
220
+ stopping = true;
221
+ const task = (async () => {
222
+ const receipt = await stop("control");
223
+ await new Promise((done) => {
224
+ if (socket.destroyed) return done();
225
+ socket.once("close", done);
226
+ socket.end(`${JSON.stringify(receipt)}\n`, () => {
227
+ socket.destroy();
228
+ done();
229
+ });
230
+ });
231
+ })();
232
+ tasks.add(task);
233
+ // Completion owns process exit, after every admitted response has flushed.
234
+ void task
235
+ .finally(() => {
236
+ tasks.delete(task);
237
+ setImmediate(complete);
238
+ })
239
+ .catch(() => {});
240
+ });
241
+ });
242
+ await new Promise((resolve, reject) => {
243
+ server.once("error", reject);
244
+ server.listen(path, () => {
245
+ server.removeListener("error", reject);
246
+ resolve();
247
+ });
248
+ });
249
+ try {
250
+ if (process.platform !== "win32") chmodSync(path, 0o600);
251
+ } catch (error) {
252
+ server.close();
253
+ throw error;
254
+ }
255
+ return {
256
+ close() {
257
+ // biome-ignore lint/suspicious/noAssignInExpressions: retain and return the single owned close barrier.
258
+ return (closing ??= (async () => {
259
+ const closed = new Promise((resolve, reject) =>
260
+ server.close((error) => (error ? reject(error) : resolve())),
261
+ );
262
+ for (const [socket, admitted] of sockets)
263
+ if (!admitted) socket.destroy();
264
+ const results = await Promise.allSettled([...tasks]);
265
+ await closed;
266
+ const errors = results
267
+ .filter((row) => row.status === "rejected")
268
+ .map((row) => row.reason);
269
+ if (errors.length)
270
+ throw new AggregateError(errors, "daemon control completion failed");
271
+ // net.Server removes its own Unix socket; no unlink of another incarnation.
272
+ })());
273
+ },
274
+ };
275
+ }
package/hermes-locks.mjs CHANGED
@@ -113,10 +113,13 @@ function writeRecord(file, record, flag) {
113
113
  try { writeSync(fd, JSON.stringify(record)); } finally { closeSync(fd); }
114
114
  }
115
115
 
116
- /** Release a token's scoped lock this process holds (Hermes's `release_scoped_lock`). */
116
+ /** Release this process's token claim; absence is settled, other failures remain failures. */
117
117
  export function releaseTokenLock({ scope, identity, env = process.env }) {
118
118
  const file = join(tokenLockDir(env), `${scope}-${scopeHash(identity)}.lock`);
119
- try { if (Number(JSON.parse(readFileSync(file, 'utf8')).pid) === process.pid) unlinkSync(file); } catch { /* gone */ }
119
+ try {
120
+ if (Number(JSON.parse(readFileSync(file, 'utf8')).pid) !== process.pid) throw new Error('token lock changed owner; retained');
121
+ unlinkSync(file);
122
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
120
123
  }
121
124
 
122
125
  // The flock itself: Node has none, so a small Python child takes it, writes Hermes's record naming
@@ -150,13 +153,20 @@ function holdFileLock(path, home, { python, what, tail }) {
150
153
  child = spawn(python, ['-c', HOLDER, path, JSON.stringify(pidRecord(home))], { stdio: ['pipe', 'pipe', 'inherit'] });
151
154
  } catch (error) { fail(new Error(`cannot hold ${path}: ${error.message}`)); return; }
152
155
  let out = '';
156
+ const ended = new Promise(resolve => child.once('close', (code, signal) => resolve({ code, signal })));
157
+ child.stdin.on('error', () => {}); // release joins the actual holder exit below
158
+ let releasing = null;
153
159
  child.on('error', (error) => fail(new Error(`cannot hold ${path} (${python}: ${error.message}); a home is served by one runtime, and this one could not claim it`)));
154
160
  child.stdout.on('data', (chunk) => {
155
161
  out += chunk;
156
162
  if (!out.includes('\n')) return;
157
163
  const line = out.split('\n')[0];
158
164
  if (line === 'ok') {
159
- done({ release: () => { try { child.stdin.end(); } catch { /* gone */ } } });
165
+ done({ release: () => releasing ??= (async () => {
166
+ child.stdin.end();
167
+ const result = await ended;
168
+ if (result.code !== 0) throw new Error(`lock holder for ${path} exited ${result.code ?? result.signal}`);
169
+ })() });
160
170
  } else {
161
171
  let holder = null;
162
172
  try { holder = JSON.parse(line.slice('held '.length)); } catch { /* unreadable record */ }
package/index.d.ts CHANGED
@@ -145,8 +145,8 @@ export type ChatType = 'dm' | 'group' | 'channel' | 'thread';
145
145
 
146
146
  export interface RuntimeState {
147
147
  leases: Record<string, { runtime_id: string; connection: string | null; since: Instant }>;
148
- queues: Record<string, { text: string; at: Instant; context_from?: string[] | null; skills?: string[]; job_id?: string }[]>;
149
- turns: Record<string, { started_at: Instant; buffer: string }>;
148
+ queues: Record<string, { text: string; at: Instant; context_from?: string[] | null; skills?: string[]; job_id?: string; webhook_input_id?: string }[]>;
149
+ turns: Record<string, { started_at: Instant; buffer: string; webhook_input_id?: string }>;
150
150
  starting: Record<string, unknown>;
151
151
  /** The last worker event per lease (§4.3's inactivity limit). */
152
152
  activity: Record<string, Instant>;
@@ -166,6 +166,7 @@ export type WorkerEvent =
166
166
  | { kind: 'text'; delta: string }
167
167
  /** Any other runtime event (a tool call, reasoning, a turn start): the worker is alive. */
168
168
  | { kind: 'activity' }
169
+ | { kind: 'steer_refused'; text: string; webhook_input_id?: string | null }
169
170
  | { kind: 'final'; text: string | null; outcome?: 'completed' | 'error'; message?: string | null }
170
171
  | { kind: 'request'; id: string; subject: string; options: RequestOption[] }
171
172
  | { kind: 'ended'; reason: string }
@@ -187,7 +188,8 @@ export type OrchestratorEvent =
187
188
  | { type: 'worker'; profile: ProfileName; keyString: string; runtime_id: string | null; ev: WorkerEvent; at: Instant }
188
189
  | { type: 'adapter'; platform: Platform; state: 'up' | 'down'; error: string | null; at: Instant }
189
190
  | { type: 'operator'; verb: string; profile: ProfileName; args: Record<string, unknown>; at: Instant }
190
- | { type: 'webhook'; name: string; payload: JsonValue; verified: boolean; at: Instant; event: string | null; delivery_id: string | null }
191
+ | { type: 'webhook'; name: string; payload: JsonValue; verified: boolean; at: Instant; event: string | null; delivery_id: string | null; admission_id?: string }
192
+ | { type: 'webhook_refused'; name: string; delivery_id: string | null; admission_id: string; profile?: ProfileName; keyString?: string; handed: boolean; stopping: boolean; at: Instant }
191
193
  | { type: 'restart'; at: Instant }
192
194
  | { type: 'stopping'; at: Instant };
193
195
 
@@ -197,7 +199,8 @@ export type Effect =
197
199
  | { type: 'execute_job'; profile: ProfileName; job_id: string; fire_id: string }
198
200
  | { type: 'runtime_start'; profile: ProfileName; keyString: string; worker: WorkerSpec; prompt: string | null }
199
201
  | { type: 'runtime_close'; profile: ProfileName; keyString: string; lease: RuntimeState['leases'][string] }
200
- | { type: 'send_input'; profile: ProfileName; keyString: string; text: string; context_from?: string[] | null; skills?: string[]; job_id?: string }
202
+ | { type: 'send_input'; profile: ProfileName; keyString: string; text: string; context_from?: string[] | null; skills?: string[]; job_id?: string; webhook_input_id?: string }
203
+ | { type: 'webhook_input'; admission_id: string; profile: ProfileName; keyString: string; refusal?: string }
201
204
  | { type: 'respond'; profile: ProfileName; keyString: string; request_id: string; response: JsonValue }
202
205
  | { type: 'deliver'; profile: ProfileName; obligation_id: string }
203
206
  | { type: 'write_local'; profile: ProfileName; path: RelPath; text: string }
@@ -288,6 +291,7 @@ export class ChatSdkAdapter implements ChannelAdapter {
288
291
  /** The one door from the SDK into the loop: `(thread, message)` -> an `inbound`. */
289
292
  handle(thread: unknown, message: unknown): unknown;
290
293
  send(target: SurfaceKey, content: { text: string; at?: Instant; reply_to?: string }): Promise<{ ok: true; message_id?: string }>;
294
+ /** Joins SDK platform/state disconnect and gateway work once; rejects with retained cleanup causes. A failed owner retains its token claim. */
291
295
  close(): Promise<void>;
292
296
  }
293
297
 
@@ -369,6 +373,7 @@ export class Orchestrator {
369
373
  readonly effectLog: Effect[];
370
374
  readonly webhookPort: number | null;
371
375
  start(): Promise<void>;
376
+ /** Seals webhook ingress before waits; joins admitted processing and retains cleanup failures. */
372
377
  stop(): Promise<void>;
373
378
  tick(at?: Instant): Promise<Effect[]>;
374
379
  drainHosted(): Promise<void>;
@@ -9,11 +9,12 @@ import { delimiter, isAbsolute, join } from 'node:path';
9
9
  export const APPS_DIR = 'apps';
10
10
  /** The apps this orchestrator can host, by name: what each installs and which adapter it attaches. */
11
11
  export const INSTALLABLE_APPS = Object.freeze({
12
- 'arcs-rooms': { side: 'rh2', module: true, describe: 'arcs-rooms installed module' },
13
12
  'orchestration-rh2': { side: 'rh2', module: true, describe: 'orchestration-rh2 installed module' },
14
13
  'room-chat': { side: 'rh2', adapter: 'rh2', describe: 'Room chat → orchestrator: a person\'s line in an RH2 Room reaches the agent seated there' },
15
14
  // An agent the RH2 organization declares is one participant there: its chat and its call are its own agent principal,
16
- // never an automation beside it. One record per agent, bound to its Teams id (`rh2-agent.<name>`, `agentId`).
15
+ // never an automation beside it. One record per agent per Workplace, bound to its Teams id (`agentId`) and that
16
+ // Workplace's origin: one installed by hand (`rh2-agent.<profile>`), or one the Agents → Room agents integration
17
+ // provisioned (`rh2-agent.<agent id>@<host>`, sdk/teams/workplace/agent-credentials.mjs).
17
18
  'rh2-agent': { side: 'rh2', adapter: 'rh2', perProfile: true, describe: 'An agent the RH2 organization declares, in its Rooms as its own principal' },
18
19
  });
19
20
 
@@ -48,19 +49,38 @@ export async function installAgent({ root, origin, organization, principal, toke
48
49
  return { ...record, name: `rh2-agent.${profile}`, token: undefined };
49
50
  }
50
51
 
51
- /** The rh2-agent record that is the identity of `agentId` (an agent's Teams id, as `apps install rh2-agent --agent`
52
- * named it), or null. It is found by that id alone, never by a name or profile another agent may share (a later
53
- * generation of a card's agent, another home's root agent named after the same folder), and only when exactly one
54
- * record names it. Its file name (`--profile`) is a label. */
55
- export function installedAgentRecord(root, agentId) {
56
- if (typeof agentId !== 'string' || !/^agent_[A-Za-z0-9_-]+$/.test(agentId)) return null;
52
+ /** Every rh2-agent record that is an identity of `agentId` (an agent's Teams id), one per Workplace: found by that id
53
+ * alone, never by a name or profile another agent may share (a later generation of a card's agent, another home's root
54
+ * agent named after the same folder). A file name is a label. */
55
+ export function installedAgentRecords(root, agentId) {
56
+ if (typeof agentId !== 'string' || !/^agent_[A-Za-z0-9_-]+$/.test(agentId)) return [];
57
57
  let records;
58
- try { records = installedApps(root); } catch { return null; }
58
+ try { records = installedApps(root); } catch { return []; }
59
59
  // the origin install accepted (https, or a loopback for a local RH2), held again wherever the record is used
60
- const own = records.filter((record) => record.app === 'rh2-agent' && record.agentId === agentId && record.token && rh2Origin(record.origin));
61
- return own.length === 1 ? own[0] : null;
60
+ return records.filter((record) => record.app === 'rh2-agent' && record.agentId === agentId && record.token && rh2Origin(record.origin))
61
+ .sort((a, b) => (a.origin < b.origin ? -1 : a.origin > b.origin ? 1 : 0));
62
62
  }
63
63
 
64
+ /** The rh2-agent record that is the identity of `agentId` on the Workplace at `origin`, or null; with no origin, its one
65
+ * record, else the one installed by hand (`apps install rh2-agent`), the identity its channel and its sessions' first
66
+ * Workplace have always been. Only one record may answer: two for one Workplace is none. */
67
+ export function installedAgentRecord(root, agentId, origin = null) {
68
+ const own = installedAgentRecords(root, agentId);
69
+ if (origin !== null) {
70
+ const at = own.filter((record) => record.origin === originOf(origin));
71
+ return at.length === 1 ? at[0] : null;
72
+ }
73
+ if (own.length === 1) return own[0];
74
+ const byHand = own.filter((record) => !record.provisionedBy);
75
+ return byHand.length === 1 ? byHand[0] : null;
76
+ }
77
+
78
+ const originOf = (value) => { try { return new URL(value).origin; } catch { return null; } };
79
+
80
+ // A record the Agents → Room agents integration provisioned is written by that service (sdk/teams/workplace/
81
+ // agent-credentials.mjs) into this same folder, in this same shape (`provisionedBy` names it), and read here like any.
82
+
83
+
64
84
  /** Whether `origin` is one an RH2 app may be installed for: https, or a loopback for a local RH2. */
65
85
  function rh2Origin(origin) {
66
86
  return /^https:\/\/|^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(String(origin ?? ''));
@@ -75,9 +95,10 @@ export function rh2Program() {
75
95
  return folders.map((folder) => join(folder, 'rh2')).find((file) => existsSync(file)) ?? null;
76
96
  }
77
97
 
78
- /** The environment the `rh2` CLI reads to act as an installed agent: its RH2 origin, its organization and its token. */
79
- export function rh2AgentEnv(record) {
80
- return { RH2_BASE_URL: record.origin, RH2_ORGANIZATION: record.organizationId, RH2_SESSION_TOKEN: record.token };
98
+ /** The environment the `rh2` CLI reads to act as an installed agent: its RH2 origin, its organization and its token,
99
+ * and the home whose records hold its credentials on its other Workplaces (`RH2_AGENT_HOMES`, read per call). */
100
+ export function rh2AgentEnv(record, root = null) {
101
+ return { RH2_BASE_URL: record.origin, RH2_ORGANIZATION: record.organizationId, RH2_SESSION_TOKEN: record.token, ...(root ? { RH2_AGENT_HOMES: root } : {}) };
81
102
  }
82
103
 
83
104
  /** Exchange an RH2 install code for the installation's token and keep the record. */
package/kanban-worker.mjs CHANGED
@@ -12,7 +12,7 @@ import { existsSync, statSync } from 'node:fs';
12
12
  import { basename, dirname, join, resolve } from 'node:path';
13
13
  import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
14
14
  import { load } from './orchestration.mjs';
15
- import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, declareWorkerHome, sandboxArguments, approvalPolicy, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
15
+ import { INSTRUCTION_ARGUMENTS, activationCwd, activationEnv, declareWorkerHome, modeArguments, approvalPolicy, approvalsReviewer, limitArguments, materializePersona, materializeSkills, modelArguments, turnCounter, userHomeArguments, userHomePersona } from './activation.mjs';
16
16
  import { workerRoute } from './worker-route.mjs';
17
17
  import { KANBAN_ENV, untrustedServerVerdict, hermesApproval, hermesInstall, memorySnapshot } from './hermes-cli.mjs';
18
18
  import { pinnedSkills, taskProtocol } from './board/worker-prompt.mjs';
@@ -95,13 +95,13 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
95
95
  const reasoning = args.reasoning && harness === 'codex' ? ['-c', `model_reasoning_effort=${JSON.stringify(args.reasoning)}`] : [];
96
96
  const launch = {
97
97
  program: base.program,
98
- arguments: boardGuardArguments(harness, [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...userHomeArguments(profile), ...reasoning, ...sandboxArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))], { enabled: ['claude-code','codex'].includes(harness), cwd }),
98
+ arguments: boardGuardArguments(harness, [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...userHomeArguments(profile), ...reasoning, ...modeArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))], { enabled: ['claude-code','codex'].includes(harness), cwd }),
99
99
  env: { ...(base.env ?? {}), ...runEnv },
100
100
  };
101
101
  declareWorkerHome({ harness, env: launch.env, cwd, log: (_level, line) => say(`hermes-worker: ${line}`) });
102
102
  const mount = mountOrchestratorMcp({ root, profile, harness, surfaceKey, launch, vault, task });
103
- const policy = approvalPolicy(profile, harness);
104
- const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}) });
103
+ const policy = approvalPolicy(profile, harness), reviewer = approvalsReviewer(profile, harness);
104
+ const runtime = await client.startManagedRuntime({ harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}), ...(reviewer ? { approvals_reviewer: reviewer } : {}) });
105
105
  try { await recordSession(root, task, harness, runtime.handle.runtime_id); } catch (error) { say(`hermes-worker: recording the session on its run: ${error.message}`); }
106
106
  let text = '', last = '';
107
107
  // `agent.max_turns` for a worker that takes no cap at launch, held as the daemon holds it (loop.mjs `holdTurnLimit`)
package/lease.mjs CHANGED
@@ -27,7 +27,7 @@ export function pidIsLive(pid) {
27
27
  }
28
28
  }
29
29
 
30
- /** Whether the lease is this machine's, since this boot. Only such a lease's pid may be signalled. */
30
+ /** Machine/boot observation only. Neither this nor PID liveness grants stop authority. */
31
31
  export function leaseIsHere(lease) {
32
32
  const here = thisHost();
33
33
  if (lease?.host && lease.host !== here.host) return false;