@optimuslabs/harness-map-staging 1.5.38 → 1.5.39

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). */
@@ -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,6 +14,7 @@ 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
19
  /** A process's parent, start time and command name, from `ps`; null when it cannot be read. */
19
20
  function processRow(pid) {
@@ -97,9 +98,7 @@ export function stoppingSkills(sessionId, proc = null) {
97
98
  /** The notice for a stopped session. */
98
99
  export function stoppedSessionMessage(skills) {
99
100
  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.');
101
+ return `Your organization blocked ${names}. Quit Claude Code and start it again to clear this session's memory and continue.`;
103
102
  }
104
103
  /**
105
104
  * The blocked skill a typed slash command runs, when this process may still have it, or null:
@@ -114,7 +113,8 @@ export function blockedSkillOfCommand(promptFirstLine, processStartedAt = claude
114
113
  if (!command.startsWith('/'))
115
114
  return null;
116
115
  const typed = command.slice(command.lastIndexOf(':') + 1).replace(/^\//, '').toLowerCase();
117
- const blocks = readRemediationInstructionsFile().skill_blocks ?? [];
116
+ // A plugin skill only: a running Claude Code drops a custom skill by itself once it is quarantined.
117
+ const blocks = (readRemediationInstructionsFile().skill_blocks ?? []).filter((b) => isPluginSkillDir(b.skill_dir));
118
118
  const hits = blocks.filter((b) => b.skill_name.toLowerCase() === typed || basename(b.skill_dir).toLowerCase() === typed);
119
119
  if (hits.length === 0)
120
120
  return null;
@@ -125,9 +125,18 @@ export function blockedSkillOfCommand(promptFirstLine, processStartedAt = claude
125
125
  });
126
126
  return mayStillHaveIt ? hits[0].skill_name || typed : null;
127
127
  }
128
+ /**
129
+ * Whether `dir` is a Claude Code plugin's skill (`~/.claude/plugins/...`). Claude Code reads plugin
130
+ * skills once, when it starts, and keeps them until it quits, so a running one still offers a
131
+ * quarantined plugin skill. A custom skill (`~/.claude/skills`, a project's `.claude/skills`) is read
132
+ * again as its folder changes: Claude Code drops it at quarantine (docs/skill-block/DESIGN.md).
133
+ */
134
+ export function isPluginSkillDir(dir) {
135
+ return dir.includes('/.claude/plugins/');
136
+ }
128
137
  /** The notice for a typed command of a blocked skill in a process that may still have it. */
129
138
  export function blockedCommandMessage(skill) {
130
- return `Your organization blocked ${skill}. Quit Claude Code and start it again to continue.`;
139
+ return `Your organization blocked ${skill}. Quit Claude Code and start it again to clear this session's memory and continue.`;
131
140
  }
132
141
  /**
133
142
  * The window to open beside the block of the skill named `skill` (a name `stoppingSkills` or
@@ -135,8 +144,47 @@ export function blockedCommandMessage(skill) {
135
144
  * server sent none, so only the one-line block shows.
136
145
  */
137
146
  export function skillBlockWindow(skill) {
147
+ const uuid = resourceUuidOf(skill);
148
+ return uuid ? readRemediationInstructionsFile().skill_block_windows?.[uuid] : undefined;
149
+ }
150
+ /** The resource uuid of the blocked skill named `skill`, from the synced lists; '' when unknown. */
151
+ function resourceUuidOf(skill) {
138
152
  const file = readRemediationInstructionsFile();
139
153
  const name = skill.toLowerCase();
140
154
  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;
155
+ return entry?.resource_uuid ?? '';
156
+ }
157
+ /** The skills this run already opened the window for, so a prompt that quarantines a skill and is
158
+ * refused for it opens one window, not two. */
159
+ const windowsShown = new Set();
160
+ /**
161
+ * Open the window of one blocked skill, by resource uuid, once per run. True when it is open: just
162
+ * started, or already started this run; false when the server sent no window for it.
163
+ */
164
+ function openWindowOf(uuid) {
165
+ if (!uuid)
166
+ return false;
167
+ if (windowsShown.has(uuid))
168
+ return true;
169
+ const words = readRemediationInstructionsFile().skill_block_windows?.[uuid];
170
+ if (!words)
171
+ return false;
172
+ windowsShown.add(uuid);
173
+ return launchAgentAccessWindow('claude', words);
174
+ }
175
+ /** Open the window beside the block of the skill named `skill` (a stopped session, a typed command). */
176
+ export function openSkillBlockWindow(skill) {
177
+ return openWindowOf(resourceUuidOf(skill));
178
+ }
179
+ /**
180
+ * Open the window for each skill this sync moved into quarantine (`dirs`, the folders it moved), so
181
+ * the user hears of it when it happens, even when no session of theirs is known to have loaded it
182
+ * (a typed command whose turn made no tool call reaches the server as nothing). One window per
183
+ * skill however many of its folders moved.
184
+ */
185
+ export function openQuarantineWindows(dirs, entries) {
186
+ const moved = new Set(dirs);
187
+ const uuids = new Set(entries.filter((e) => moved.has(e.skill_dir)).map((e) => e.resource_uuid));
188
+ for (const uuid of uuids)
189
+ openWindowOf(uuid);
142
190
  }
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.39",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {