@optimuslabs/harness-map-staging 1.5.38 → 1.5.40

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.
@@ -22,7 +22,7 @@ import { parseJsonWithJsoncFallback } from './log_config_files/readers/file_read
22
22
  import { PORTABLE_CURSOR_USER_SETTINGS, resolveRemediationConfigPath, resolveRemediationUploadFileType, } from './log_config_files/runtime/remediation_config_path.js';
23
23
  import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid.js';
24
24
  import { cursorAutofixDefersInlineVerify, syncRemediations, } from './log_config_files/runtime/remediation_sync.js';
25
- import { blockedCommandMessage, blockedSkillOfCommand, claudeProcess, recordSessionProcess, stoppedSessionMessage, skillBlockWindow, stoppingSkills, } from './log_config_files/runtime/skill_block_sessions.js';
25
+ import { blockedCommandMessage, blockedSkillOfCommand, claudeProcess, recordSessionProcess, stoppedSessionMessage, openSkillBlockWindow, stoppingSkills, } from './log_config_files/runtime/skill_block_sessions.js';
26
26
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
27
27
  import { agentAccessDecision, approvalId } from './agent_access/decision.js';
28
28
  import { launchAgentAccessWindow } from './agent_access/window_runner.js';
@@ -103,7 +103,9 @@ function titled(violationMessage) {
103
103
  function blockPayload(ide, violationMessage, windowShown = false) {
104
104
  const text = titled(violationMessage);
105
105
  if (ide === 'claude' || ide === 'copilot' || ide === 'opencode') {
106
- return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
106
+ // Claude's hook skips its own alert too, and takes the field out before Claude Code reads it.
107
+ const flag = windowShown && ide === 'claude' ? { __optimus_window: true } : {};
108
+ return JSON.stringify({ decision: 'block', reason: text, systemMessage: text, ...flag });
107
109
  }
108
110
  return JSON.stringify({ continue: false, user_message: text, ...(windowShown ? { __optimus_window: true } : {}) });
109
111
  }
@@ -348,12 +350,6 @@ async function syncApprovals() {
348
350
  // Network or auth failure — fall back to local file state.
349
351
  }
350
352
  }
351
- /** Open the skill block window beside the block, in the background, when the sync sent its words. */
352
- function openSkillBlockWindow(skill) {
353
- const words = skillBlockWindow(skill);
354
- if (words)
355
- launchAgentAccessWindow('claude', words);
356
- }
357
353
  /**
358
354
  * Skill block killswitch, Claude Code only for now: refuse the prompt of a session that loaded a
359
355
  * skill which is now blocked, or whose `claude` process did in an earlier session (a `/clear` keeps
@@ -372,16 +368,16 @@ function blockedBySkillBlock(agent, ide, sessionId) {
372
368
  const skills = stoppingSkills(sessionId, proc);
373
369
  if (skills.length > 0) {
374
370
  logRemediationApplyFailure('skill_block_session_stopped', { ide, agent, skills: skills.join(',') });
375
- openSkillBlockWindow(skills[0]);
376
- console.log(blockPayload(ide, stoppedSessionMessage(skills)));
371
+ const windowShown = openSkillBlockWindow(skills[0]);
372
+ console.log(blockPayload(ide, stoppedSessionMessage(skills), windowShown));
377
373
  return true;
378
374
  }
379
375
  const typed = blockedSkillOfCommand(process.env.OPTIMUS_PROMPT_FIRST_LINE || '', () => proc?.startedAt ?? null);
380
376
  if (!typed)
381
377
  return false;
382
378
  logRemediationApplyFailure('skill_block_command_refused', { ide, agent, skills: typed });
383
- openSkillBlockWindow(typed);
384
- console.log(blockPayload(ide, blockedCommandMessage(typed)));
379
+ const windowShown = openSkillBlockWindow(typed);
380
+ console.log(blockPayload(ide, blockedCommandMessage(typed), windowShown));
385
381
  return true;
386
382
  }
387
383
  /** Agents whose remediations are JSON files written to disk at once (no deferred SQLite apply). */
@@ -2,7 +2,8 @@
2
2
  * Paths and JSON persistence for ~/.optimuslabs/management:
3
3
  * - remediation_instructions.json (remediations[])
4
4
  */
5
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
6
7
  import { dirname, join } from 'node:path';
7
8
  import { homedir } from 'node:os';
8
9
  import { OPTIMUSLABS_MANAGEMENT_REL, OPTIMUSLABS_LOGS_REL } from '../../bootstrap_constants.js';
@@ -123,13 +124,23 @@ export function writeFileCollectionVscdbContract(contract) {
123
124
  return;
124
125
  atomicWriteJson(getFileCollectionVscdbContractPath(), sanitized);
125
126
  }
127
+ /**
128
+ * Write `data` as JSON to `filePath` in one step: to a temporary file of this write's own, then a
129
+ * rename. Two hooks writing at once (two Claude Code windows, the gate and the compliance runner)
130
+ * each use their own temporary file, so neither can rename the other's half-written one into place.
131
+ */
126
132
  export function atomicWriteJson(filePath, data) {
127
133
  const dir = dirname(filePath);
128
134
  if (!existsSync(dir))
129
135
  mkdirSync(dir, { recursive: true, mode: 0o700 });
130
- const tmp = `${filePath}.tmp`;
131
- writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
132
- renameSync(tmp, filePath);
136
+ const tmp = `${filePath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
137
+ try {
138
+ writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
139
+ renameSync(tmp, filePath);
140
+ }
141
+ finally {
142
+ rmSync(tmp, { force: true });
143
+ }
133
144
  }
134
145
  function parseInstructionsRaw(raw) {
135
146
  try {
@@ -6,6 +6,7 @@ import { parseJsonWithJsoncFallback, parseTomlText, stringifyToml } from '../rea
6
6
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
7
7
  import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeApprovedTenants, writeUnapprovedEmailDomains, writeAllowedEmailDomains, writeTeamEmailDomains, writeApprovedAgents, writeAgentAccessWindows, writeRemediationInstructionsFile, writeSkillBlockSessions, writeSkillBlockWindows, writeSkillBlocks, } from './management_storage.js';
8
8
  import { applySkillBlocks } from './skill_block.js';
9
+ import { openQuarantineWindows } from './skill_block_sessions.js';
9
10
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
10
11
  import { ensureAuthentication } from '../auth/auth_flow.js';
11
12
  import { createSignature } from '../sender/signing.js';
@@ -1711,7 +1712,9 @@ function enforceSkillBlocks(syncResult) {
1711
1712
  storeSkillBlockSessions(syncResult.skill_block_sessions);
1712
1713
  storeSkillBlockWindows(syncResult.skill_block_windows);
1713
1714
  try {
1714
- applySkillBlocks(entries);
1715
+ const outcome = applySkillBlocks(entries);
1716
+ // Tell the user when a skill is quarantined, whether or not a session of theirs loaded it.
1717
+ openQuarantineWindows(outcome.quarantined, entries);
1715
1718
  }
1716
1719
  catch (err) {
1717
1720
  const msg = `skill_block_error: ${errorText(err)}`;
@@ -89,7 +89,7 @@ function makeStub(dir) {
89
89
  chmodSync(dir, 0o000);
90
90
  }
91
91
  /** Every file of `dir` with its hash, and every symlink with its target, sorted by path. */
92
- function hashFolder(dir, prefix = '') {
92
+ function readFolderHashes(dir, prefix = '') {
93
93
  const files = [];
94
94
  for (const name of readdirSync(dir).sort()) {
95
95
  const full = join(dir, name);
@@ -98,7 +98,7 @@ function hashFolder(dir, prefix = '') {
98
98
  if (stat.isSymbolicLink())
99
99
  files.push({ path: rel, link: readlinkSync(full) });
100
100
  else if (stat.isDirectory())
101
- files.push(...hashFolder(full, rel));
101
+ files.push(...readFolderHashes(full, rel));
102
102
  else if (stat.isFile()) {
103
103
  const sha256 = createHash('sha256').update(readFileSync(full)).digest('hex');
104
104
  files.push({ path: rel, sha256, size: stat.size });
@@ -153,7 +153,7 @@ function moveIn(dir, id, entry, outcome) {
153
153
  quarantined_at: new Date().toISOString(),
154
154
  mode: permissionBits(dir),
155
155
  status: 'moving',
156
- files: hashFolder(dir),
156
+ files: readFolderHashes(dir),
157
157
  };
158
158
  writeReceipt(id, receipt);
159
159
  renameSync(dir, vaultPath(id));
@@ -191,7 +191,7 @@ function keepQuarantined(dir, id, receipt, home, outcome) {
191
191
  outcome.refused.push({ skill_dir: dir, reason });
192
192
  return;
193
193
  }
194
- if (sameFiles(hashFolder(dir), receipt.files)) {
194
+ if (sameFiles(readFolderHashes(dir), receipt.files)) {
195
195
  rmSync(dir, { recursive: true, force: true });
196
196
  }
197
197
  else {
@@ -260,7 +260,7 @@ function restore(id, receipt, outcome) {
260
260
  return;
261
261
  }
262
262
  chmodSync(vault, receipt.mode);
263
- if (!sameFiles(hashFolder(vault), receipt.files))
263
+ if (!sameFiles(readFolderHashes(vault), receipt.files))
264
264
  log(`skill_block: ${dir} changed in the vault; restored as it is`);
265
265
  mkdirSync(dirname(dir), { recursive: true });
266
266
  renameSync(vault, dir);
@@ -14,12 +14,18 @@ import { execFileSync } from 'node:child_process';
14
14
  import { existsSync, readFileSync } from 'node:fs';
15
15
  import { basename } from 'node:path';
16
16
  import { atomicWriteJson, getSkillBlockProcessesPath, readRemediationInstructionsFile, } from './management_storage.js';
17
+ import { launchAgentAccessWindow } from '../../agent_access/window_runner.js';
17
18
  import { quarantinedAt } from './skill_block.js';
18
- /** A process's parent, start time and command name, from `ps`; null when it cannot be read. */
19
- function processRow(pid) {
19
+ /**
20
+ * A process's parent, start time and command name, from `ps`; null when it cannot be read. `ps`
21
+ * writes the start time in the user's language ("Do. 5 Nov." in German), which the pattern below
22
+ * cannot read, so it runs in the C locale.
23
+ */
24
+ export function processRow(pid) {
20
25
  try {
26
+ const options = { encoding: 'utf8', env: { ...process.env, LC_ALL: 'C' } };
21
27
  // A fixed command, no shell, and a process id that is always a number.
22
- const out = execFileSync('ps', ['-o', 'ppid=,lstart=,comm=', '-p', String(pid)], { encoding: 'utf8' }).trim(); // nosafe: SAFE801
28
+ const out = execFileSync('ps', ['-o', 'ppid=,lstart=,comm=', '-p', String(pid)], options).trim(); // nosafe: SAFE801
23
29
  const row = out.match(/^(\d+)\s+(\S+\s+\S+\s+\d+\s+[\d:]+\s+\d+)\s+(.*)$/);
24
30
  return row ? { parent: Number(row[1]), startedAt: Date.parse(row[2]), name: basename(row[3]).toLowerCase() } : null;
25
31
  }
@@ -97,9 +103,7 @@ export function stoppingSkills(sessionId, proc = null) {
97
103
  /** The notice for a stopped session. */
98
104
  export function stoppedSessionMessage(skills) {
99
105
  const names = [...new Set(skills)].join(', ');
100
- return (`Your organization blocked ${names}, which this Claude Code loaded (a /clear keeps it). Copy ` +
101
- 'anything you need from this conversation, then quit Claude Code and start it again to ' +
102
- 'continue. Resuming this session keeps it stopped.');
106
+ return `Your organization blocked ${names}. Quit Claude Code and start it again to clear this session's memory and continue.`;
103
107
  }
104
108
  /**
105
109
  * The blocked skill a typed slash command runs, when this process may still have it, or null:
@@ -114,7 +118,8 @@ export function blockedSkillOfCommand(promptFirstLine, processStartedAt = claude
114
118
  if (!command.startsWith('/'))
115
119
  return null;
116
120
  const typed = command.slice(command.lastIndexOf(':') + 1).replace(/^\//, '').toLowerCase();
117
- const blocks = readRemediationInstructionsFile().skill_blocks ?? [];
121
+ // A plugin skill only: a running Claude Code drops a custom skill by itself once it is quarantined.
122
+ const blocks = (readRemediationInstructionsFile().skill_blocks ?? []).filter((b) => isPluginSkillDir(b.skill_dir));
118
123
  const hits = blocks.filter((b) => b.skill_name.toLowerCase() === typed || basename(b.skill_dir).toLowerCase() === typed);
119
124
  if (hits.length === 0)
120
125
  return null;
@@ -125,9 +130,18 @@ export function blockedSkillOfCommand(promptFirstLine, processStartedAt = claude
125
130
  });
126
131
  return mayStillHaveIt ? hits[0].skill_name || typed : null;
127
132
  }
133
+ /**
134
+ * Whether `dir` is a Claude Code plugin's skill (`~/.claude/plugins/...`). Claude Code reads plugin
135
+ * skills once, when it starts, and keeps them until it quits, so a running one still offers a
136
+ * quarantined plugin skill. A custom skill (`~/.claude/skills`, a project's `.claude/skills`) is read
137
+ * again as its folder changes: Claude Code drops it at quarantine (docs/skill-block/DESIGN.md).
138
+ */
139
+ export function isPluginSkillDir(dir) {
140
+ return dir.includes('/.claude/plugins/');
141
+ }
128
142
  /** The notice for a typed command of a blocked skill in a process that may still have it. */
129
143
  export function blockedCommandMessage(skill) {
130
- return `Your organization blocked ${skill}. Quit Claude Code and start it again to continue.`;
144
+ return `Your organization blocked ${skill}. Quit Claude Code and start it again to clear this session's memory and continue.`;
131
145
  }
132
146
  /**
133
147
  * The window to open beside the block of the skill named `skill` (a name `stoppingSkills` or
@@ -135,8 +149,47 @@ export function blockedCommandMessage(skill) {
135
149
  * server sent none, so only the one-line block shows.
136
150
  */
137
151
  export function skillBlockWindow(skill) {
152
+ const uuid = resourceUuidOf(skill);
153
+ return uuid ? readRemediationInstructionsFile().skill_block_windows?.[uuid] : undefined;
154
+ }
155
+ /** The resource uuid of the blocked skill named `skill`, from the synced lists; '' when unknown. */
156
+ function resourceUuidOf(skill) {
138
157
  const file = readRemediationInstructionsFile();
139
158
  const name = skill.toLowerCase();
140
159
  const entry = [...(file.skill_block_sessions ?? []), ...(file.skill_blocks ?? [])].find((e) => e.resource_uuid && e.skill_name.toLowerCase() === name);
141
- return entry ? file.skill_block_windows?.[entry.resource_uuid] : undefined;
160
+ return entry?.resource_uuid ?? '';
161
+ }
162
+ /** The skills this run already opened the window for, so a prompt that quarantines a skill and is
163
+ * refused for it opens one window, not two. */
164
+ const windowsShown = new Set();
165
+ /**
166
+ * Open the window of one blocked skill, by resource uuid, once per run. True when it is open: just
167
+ * started, or already started this run; false when the server sent no window for it.
168
+ */
169
+ function openWindowOf(uuid) {
170
+ if (!uuid)
171
+ return false;
172
+ if (windowsShown.has(uuid))
173
+ return true;
174
+ const words = readRemediationInstructionsFile().skill_block_windows?.[uuid];
175
+ if (!words)
176
+ return false;
177
+ windowsShown.add(uuid);
178
+ return launchAgentAccessWindow('claude', words);
179
+ }
180
+ /** Open the window beside the block of the skill named `skill` (a stopped session, a typed command). */
181
+ export function openSkillBlockWindow(skill) {
182
+ return openWindowOf(resourceUuidOf(skill));
183
+ }
184
+ /**
185
+ * Open the window for each skill this sync moved into quarantine (`dirs`, the folders it moved), so
186
+ * the user hears of it when it happens, even when no session of theirs is known to have loaded it
187
+ * (a typed command whose turn made no tool call reaches the server as nothing). One window per
188
+ * skill however many of its folders moved.
189
+ */
190
+ export function openQuarantineWindows(dirs, entries) {
191
+ const moved = new Set(dirs);
192
+ const uuids = new Set(entries.filter((e) => moved.has(e.skill_dir)).map((e) => e.resource_uuid));
193
+ for (const uuid of uuids)
194
+ openWindowOf(uuid);
142
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimuslabs/harness-map-staging",
3
- "version": "1.5.38",
3
+ "version": "1.5.40",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {