@optimuslabs/harness-map-staging 1.5.40 → 1.5.41

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,105 @@
1
+ /**
2
+ * Runs the blocked-agent window process as a one-off launchd job in the user's GUI session
3
+ * (docs/agent-exception-requests/DESIGN.md), instead of as a child of the hook.
4
+ *
5
+ * A child of the hook lives in the agent's process tree and context: an agent that stops what its
6
+ * hook started once the hook returns, or runs hooks where no window can open (seen with old Cursor
7
+ * versions), left the user with no pop-up at all. launchd starts the job itself, in the session the
8
+ * Dock and Finder run in, so neither can reach it. The job runs once (no KeepAlive), from a plist in
9
+ * a folder of its own (mode 600); the window process removes the job when it is done, and jobs a
10
+ * crashed process left behind are removed the next time one starts. Nothing here needs root.
11
+ */
12
+ import { spawnSync } from 'node:child_process';
13
+ import { randomBytes } from 'node:crypto';
14
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
15
+ import { tmpdir } from 'node:os';
16
+ import { basename, dirname, join } from 'node:path';
17
+ const LABEL_PREFIX = 'io.optimuslabs.agent-window.';
18
+ /** A label this module made, and nothing else: the prefix and 12 hex characters. */
19
+ const OUR_LABEL = /^io\.optimuslabs\.agent-window\.[0-9a-f]{12}$/;
20
+ /** The user's GUI launchd domain, and nothing else. */
21
+ const GUI_DOMAIN = /^gui\/\d+$/;
22
+ /** The env names the window process needs (story id, endpoint, home, temp folder): nothing else. */
23
+ const PASSED_ENV = /^(OPTIMUS_[A-Z0-9_]+|HOME|USER|PATH|TMPDIR|LANG)$/;
24
+ /** Set on the job, so the window process knows its label and plist folder. */
25
+ export const JOB_LABEL_ENV = 'OPTIMUS_WINDOW_JOB';
26
+ const JOB_DIR_ENV = 'OPTIMUS_WINDOW_JOB_DIR';
27
+ function xml(value) {
28
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
29
+ }
30
+ /** The job's plist: run `program` once, now, with `env`. */
31
+ export function jobPlist(label, program, env) {
32
+ const args = program.map((a) => `<string>${xml(a)}</string>`).join('');
33
+ const vars = Object.entries(env)
34
+ .map(([k, v]) => `<key>${xml(k)}</key><string>${xml(v)}</string>`)
35
+ .join('');
36
+ return ('<?xml version="1.0" encoding="UTF-8"?>\n' +
37
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' +
38
+ `<plist version="1.0"><dict><key>Label</key><string>${xml(label)}</string>` +
39
+ `<key>ProgramArguments</key><array>${args}</array>` +
40
+ `<key>EnvironmentVariables</key><dict>${vars}</dict>` +
41
+ '<key>RunAtLoad</key><true/><key>KeepAlive</key><false/></dict></plist>\n');
42
+ }
43
+ /** The env the job gets: what the window process needs from this one, and its own job details. */
44
+ function jobEnv(label, dir) {
45
+ const env = {};
46
+ for (const [k, v] of Object.entries(process.env))
47
+ if (PASSED_ENV.test(k) && v !== undefined)
48
+ env[k] = v;
49
+ env.TMPDIR = tmpdir();
50
+ env[JOB_LABEL_ENV] = label;
51
+ env[JOB_DIR_ENV] = dir;
52
+ return env;
53
+ }
54
+ function guiDomain() {
55
+ return typeof process.getuid === 'function' ? `gui/${process.getuid()}` : null;
56
+ }
57
+ /** Remove one of our jobs from the domain; anything that is not exactly ours is left alone. */
58
+ function bootout(domain, label) {
59
+ if (!GUI_DOMAIN.test(domain) || !OUR_LABEL.test(label))
60
+ return;
61
+ const target = `${domain}/${label}`;
62
+ // A fixed command, no shell, and a target checked above to be our own job in the user's domain.
63
+ spawnSync('launchctl', ['bootout', target]); // nosafe: SAFE801
64
+ }
65
+ /** Remove the window jobs no process runs any more (one a crashed window process left behind). */
66
+ function sweepFinishedJobs(domain) {
67
+ const out = spawnSync('launchctl', ['list'], { encoding: 'utf8' });
68
+ for (const line of (out.stdout || '').split('\n')) {
69
+ const [pid, , label] = line.trim().split(/\s+/);
70
+ if (pid === '-' && label)
71
+ bootout(domain, label);
72
+ }
73
+ }
74
+ /**
75
+ * Start `program` as a one-off launchd job in the user's GUI session; the job's label, or null when
76
+ * launchd would not take it (the caller then starts the process itself).
77
+ */
78
+ export function startLaunchdJob(program) {
79
+ const domain = guiDomain();
80
+ if (!domain)
81
+ return null;
82
+ const dir = mkdtempSync(join(tmpdir(), 'optimus-window-job-'));
83
+ const label = `${LABEL_PREFIX}${randomBytes(6).toString('hex')}`;
84
+ const plist = join(dir, `${label}.plist`);
85
+ writeFileSync(plist, jobPlist(label, program, jobEnv(label, dir)), { mode: 0o600 });
86
+ sweepFinishedJobs(domain);
87
+ // A fixed command, no shell; the plist is one this process just wrote in its own folder.
88
+ const out = spawnSync('launchctl', ['bootstrap', domain, plist], { encoding: 'utf8' }); // nosafe: SAFE801
89
+ if (out.status === 0)
90
+ return label;
91
+ rmSync(dir, { recursive: true, force: true });
92
+ throw new Error(`launchctl bootstrap failed (exit ${out.status}): ${(out.stderr || '').trim()}`);
93
+ }
94
+ /** In the window process: remove its own job and plist folder, if launchd started it. */
95
+ export function endLaunchdJob() {
96
+ const label = process.env[JOB_LABEL_ENV] || '';
97
+ const dir = process.env[JOB_DIR_ENV] || '';
98
+ const domain = guiDomain();
99
+ if (dir && basename(dir).startsWith('optimus-window-job-') && dirname(dir) === tmpdir()) {
100
+ rmSync(dir, { recursive: true, force: true });
101
+ }
102
+ // Last: removing the job ends this process.
103
+ if (domain)
104
+ bootout(domain, label);
105
+ }
@@ -2,24 +2,30 @@
2
2
  * Shows the blocked-agent window (docs/agent-exception-requests/DESIGN.md).
3
3
  *
4
4
  * Started detached by the prompt gate (`launchAgentAccessWindow`), so the hook returns its block at
5
- * once. Run as `node window_runner.js <agent> [<words.json>]`: an agent's own block reads the window's
5
+ * once. Run as `node window_runner.js <agent> <kind> [<words.json>]`: an agent's own block reads the window's
6
6
  * words from the synced approvals; a tenant or email-domain block passes them, built by the gate, in
7
7
  * a file of its own temporary folder. It draws the window with `osascript` (macOS only),
8
8
  * then acts on what the user chose: Open launches the approved app, Cancel only closes it. The prompt
9
- * was already blocked. Nothing here talks to the network; a failure is logged and never thrown.
9
+ * was already blocked. Whether the window was shown, or why not (the `osascript` error), is reported
10
+ * into the prompt's story as a `compliance/agent_access_window` functional run, so a machine where `osascript`
11
+ * cannot draw it is visible on the server; the gate had already told the hook the window was up, so
12
+ * the user saw no pop-up at all. Nothing is ever thrown.
10
13
  */
11
14
  import { spawn, spawnSync } from 'node:child_process';
12
15
  import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
13
- import { tmpdir } from 'node:os';
16
+ import { release, tmpdir } from 'node:os';
14
17
  import { basename, dirname, join } from 'node:path';
15
18
  import { fileURLToPath } from 'node:url';
19
+ import { reportFunctionalRun } from '../log_config_files/runtime/client_event_reporter.js';
16
20
  import { hookRunLog } from '../log_config_files/runtime/hook_logger.js';
17
21
  import { readRemediationInstructionsFile } from '../log_config_files/runtime/management_storage.js';
18
22
  import { approvalId } from './decision.js';
23
+ import { endLaunchdJob, JOB_LABEL_ENV, startLaunchdJob } from './launchd_job.js';
19
24
  const HERE = dirname(fileURLToPath(import.meta.url));
20
25
  const WINDOW_SCRIPT = join(HERE, '..', 'agent_block_window', 'agent_block_window.jxa');
21
26
  /** The agents the gate checks, by approval id: nothing else is ever passed to a process. */
22
27
  const AGENTS = ['claude', 'cursor', 'codex', 'copilot', 'opencode'];
28
+ const KINDS = ['agent', 'login', 'skill'];
23
29
  /** A macOS bundle identifier, and nothing that could be read as an option or a path. */
24
30
  const BUNDLE_ID = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
25
31
  /** Where the gate leaves a login block's words for the runner: a folder of its own under tmpdir. */
@@ -56,18 +62,23 @@ function syncedWindow(agent) {
56
62
  return readRemediationInstructionsFile().agent_access_windows?.[approvalId(agent)];
57
63
  }
58
64
  /**
59
- * Start the runner in the background, detached from the hook, on macOS only; true when it started.
60
- * `words` are a tenant or email-domain block's, built by the gate; without them the runner reads the
61
- * agent's own block window from the synced approvals. False when there is nothing to show (an
62
- * approvals file synced before the window existed), so Cursor's hook keeps its own alert.
65
+ * Start the runner in the background, on macOS only; true when it started. `words` are a tenant or
66
+ * email-domain block's, built by the gate; without them the runner reads the agent's own block
67
+ * window from the synced approvals. False when there is nothing to show (an approvals file synced
68
+ * before the window existed), so Cursor's hook keeps its own alert.
69
+ *
70
+ * launchd starts it in the user's GUI session (`launchd_job.ts`), out of reach of what the agent
71
+ * does to its hook's processes; when launchd will not take the job, it is started here, detached.
63
72
  */
64
- export function launchAgentAccessWindow(agent, words) {
65
- if (process.platform !== 'darwin' || !AGENTS.includes(agent))
73
+ export function launchAgentAccessWindow(agent, kind, words) {
74
+ if (process.platform !== 'darwin' || !AGENTS.includes(agent) || !KINDS.includes(kind))
66
75
  return false;
67
76
  if (!words && !syncedWindow(agent))
68
77
  return false;
69
78
  try {
70
- const args = [fileURLToPath(import.meta.url), agent, ...(words ? [writeWords(words)] : [])];
79
+ const args = [fileURLToPath(import.meta.url), agent, kind, ...(words ? [writeWords(words)] : [])];
80
+ if (startByLaunchd([process.execPath, ...args]))
81
+ return true;
71
82
  const options = { detached: true, stdio: 'ignore' };
72
83
  // An argument list, no shell: an agent from the fixed list above, and the path of a file it wrote.
73
84
  const child = spawn(process.execPath, args, options); // nosafe: SAFE801
@@ -79,24 +90,48 @@ export function launchAgentAccessWindow(agent, words) {
79
90
  return false;
80
91
  }
81
92
  }
93
+ /** Start the runner as a launchd job; false (logged) when launchd would not take it. */
94
+ function startByLaunchd(program) {
95
+ try {
96
+ return startLaunchdJob(program) !== null;
97
+ }
98
+ catch (err) {
99
+ hookRunLog(`agent_access_window: launchd: ${err instanceof Error ? err.message : String(err)}`);
100
+ return false;
101
+ }
102
+ }
103
+ /** The installed Cursor's version ('' for another agent or when it cannot be read). */
104
+ function agentVersion(agent) {
105
+ if (agent !== 'cursor')
106
+ return '';
107
+ const plist = '/Applications/Cursor.app/Contents/Info.plist';
108
+ const out = spawnSync('plutil', ['-extract', 'CFBundleShortVersionString', 'raw', plist], { encoding: 'utf8' });
109
+ return out.status === 0 ? (out.stdout || '').trim().slice(0, 32) : '';
110
+ }
82
111
  /** Draw the window and return what the user did; null when it could not open. */
83
112
  export function showWindow(words) {
113
+ return drawWindow(words).result;
114
+ }
115
+ /** Draw the window: what the user did, or why it could not be shown (`error`, '' when it was). */
116
+ export function drawWindow(words) {
84
117
  if (!words)
85
- return null;
118
+ return { result: null, error: 'no words to show' };
86
119
  const dir = mkdtempSync(join(tmpdir(), 'optimus-window-'));
87
120
  const payloadPath = join(dir, 'payload.json');
88
121
  try {
89
122
  writeFileSync(payloadPath, JSON.stringify({ words }));
90
123
  const out = spawnSync('osascript', ['-l', 'JavaScript', WINDOW_SCRIPT, payloadPath], { encoding: 'utf8' });
91
124
  if (out.status !== 0) {
92
- hookRunLog(`agent_access_window: osascript failed: ${(out.stderr || '').trim()}`);
93
- return null;
125
+ const error = `osascript failed (exit ${out.status ?? out.signal}): ${(out.stderr || out.error?.message || '').trim()}`;
126
+ hookRunLog(`agent_access_window: ${error}`);
127
+ return { result: null, error };
94
128
  }
95
- return JSON.parse(out.stdout.trim());
129
+ return { result: JSON.parse(out.stdout.trim()), error: '' };
96
130
  }
97
131
  catch (err) {
98
- hookRunLog(`agent_access_window: ${err instanceof Error ? err.message : String(err)}`);
99
- return null;
132
+ const error = err instanceof Error ? err.message : String(err);
133
+ hookRunLog(`agent_access_window: ${error}`);
134
+ return { result: null, error };
100
135
  }
101
136
  finally {
102
137
  rmSync(dir, { recursive: true, force: true });
@@ -115,15 +150,33 @@ export function actOn(result) {
115
150
  if (result.action === 'open')
116
151
  openApp(result.bundle_id ?? '');
117
152
  }
118
- export function runAgentAccessWindow(agent, wordsPath = '') {
153
+ /**
154
+ * Show the window, act on the choice, and report into the prompt's story whether it was shown
155
+ * (the runner inherits the gate's story id). Awaited, so the report is sent before the runner exits.
156
+ */
157
+ export async function runAgentAccessWindow(agent, kind, wordsPath = '') {
158
+ const launched = process.env[JOB_LABEL_ENV] ? 'launchd' : 'spawn';
159
+ const metadata = { darwin: release(), kind, launched, agent_version: agentVersion(agent) };
160
+ // The server knows a fixed set of units; the window is part of the prompt gate's compliance.
161
+ // "started" first: a window process the agent stopped never reports more, and that shows.
162
+ const report = { unit: 'compliance', command: 'agent_access_window', metadata };
163
+ await reportFunctionalRun({ ...report, outcome: 'started', message: `${agent}: window process started (${launched})` });
119
164
  const words = wordsPath ? readWords(wordsPath) : syncedWindow(agent);
120
- const result = showWindow(words ?? undefined);
165
+ const { result, error } = drawWindow(words ?? undefined);
121
166
  if (result)
122
167
  actOn(result);
168
+ await reportFunctionalRun({
169
+ ...report,
170
+ outcome: result ? 'ok' : 'failed',
171
+ message: (result ? `${agent}: shown, closed with ${result.action}` : `${agent}: not shown: ${error}`).slice(0, 500),
172
+ detail: error,
173
+ });
174
+ endLaunchdJob();
123
175
  }
124
176
  const isEntryPoint = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
125
177
  if (isEntryPoint) {
126
- const [agent, wordsPath] = process.argv.slice(2);
127
- if (agent && AGENTS.includes(agent))
128
- runAgentAccessWindow(agent, wordsPath);
178
+ const [agent, kind, wordsPath] = process.argv.slice(2);
179
+ if (agent && AGENTS.includes(agent) && KINDS.includes(kind)) {
180
+ void runAgentAccessWindow(agent, kind, wordsPath);
181
+ }
129
182
  }
@@ -334,7 +334,8 @@ function blockedByLogin(agent, ide) {
334
334
  // The blocked-agent window opens beside every block, in the background, so the hook answers at
335
335
  // once: an agent's own from the synced approvals, a login's from the words built here. Where it
336
336
  // could not start, Cursor's hook keeps its alert.
337
- const windowShown = launchAgentAccessWindow(approvalId(agent), block.window);
337
+ // A login block (tenant, email domain) brings its own words; the agent's own comes from the sync.
338
+ const windowShown = launchAgentAccessWindow(approvalId(agent), block.window ? 'login' : 'agent', block.window);
338
339
  console.log(agent === 'codex' ? codexBlockPayload(block.reason) : blockPayload(ide, block.reason, windowShown));
339
340
  return true;
340
341
  }
@@ -175,7 +175,7 @@ function openWindowOf(uuid) {
175
175
  if (!words)
176
176
  return false;
177
177
  windowsShown.add(uuid);
178
- return launchAgentAccessWindow('claude', words);
178
+ return launchAgentAccessWindow('claude', 'skill', words);
179
179
  }
180
180
  /** Open the window beside the block of the skill named `skill` (a stopped session, a typed command). */
181
181
  export function openSkillBlockWindow(skill) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimuslabs/harness-map-staging",
3
- "version": "1.5.40",
3
+ "version": "1.5.41",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {