@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,
|
|
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
|
-
|
|
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
|
|
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(...
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|