@optimuslabs/harness-map-staging 1.5.36 → 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.
- package/dist/agent_access/decision.js +20 -0
- package/dist/agent_access/window_runner.js +129 -0
- package/dist/agent_block_window/agent_block_window.jxa +223 -0
- package/dist/approved_tenant_gate.js +149 -24
- package/dist/compliance_prompt_gate.js +76 -19
- package/dist/log_config_files/collection/skill_symlink.js +9 -0
- package/dist/log_config_files/runtime/management_storage.js +34 -0
- package/dist/log_config_files/runtime/remediation_sync.js +67 -1
- package/dist/log_config_files/runtime/skill_block.js +253 -72
- package/dist/log_config_files/runtime/skill_block_sessions.js +126 -22
- package/package.json +2 -2
|
@@ -22,9 +22,12 @@ 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, openSkillBlockWindow, stoppingSkills, } from './log_config_files/runtime/skill_block_sessions.js';
|
|
25
26
|
import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
|
|
27
|
+
import { agentAccessDecision, approvalId } from './agent_access/decision.js';
|
|
28
|
+
import { launchAgentAccessWindow } from './agent_access/window_runner.js';
|
|
26
29
|
import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
|
|
27
|
-
import { agentApprovalViolation,
|
|
30
|
+
import { agentApprovalViolation, approvedTenantBlock, codexTenantBlock, cursorTeamBlock, emailDomainBlockWindow, ORGANIZATION_LEAD, readClaudeDesktopSignedInTenant, readClaudeSignedInTenant, readCodexSignedIn, readCursorSignedInTeams, tenantBlockText, tenantBlockWindow, unapprovedEmailDomainBlock, unapprovedEmailDomainViolation, } from './approved_tenant_gate.js';
|
|
28
31
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
29
32
|
/** After verified (or failed verify), push enforcement + session log immediately — do not wait for background runner. */
|
|
30
33
|
async function flushRemediationOutcomeToServer(complianceLogPath) {
|
|
@@ -93,12 +96,18 @@ const BLOCK_TITLE = 'Optimus Labs\n\n';
|
|
|
93
96
|
function titled(violationMessage) {
|
|
94
97
|
return violationMessage.startsWith(ORGANIZATION_LEAD) ? violationMessage : BLOCK_TITLE + violationMessage;
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
/**
|
|
100
|
+
* `windowShown` tells Cursor's hook the blocked-agent window already says why, so it skips its own
|
|
101
|
+
* alert of the same message (`__optimus_window`); Cursor itself ignores the extra field.
|
|
102
|
+
*/
|
|
103
|
+
function blockPayload(ide, violationMessage, windowShown = false) {
|
|
97
104
|
const text = titled(violationMessage);
|
|
98
105
|
if (ide === 'claude' || ide === 'copilot' || ide === 'opencode') {
|
|
99
|
-
|
|
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 });
|
|
100
109
|
}
|
|
101
|
-
return JSON.stringify({ continue: false, user_message: text });
|
|
110
|
+
return JSON.stringify({ continue: false, user_message: text, ...(windowShown ? { __optimus_window: true } : {}) });
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
104
113
|
* Codex's block: `reason` only. Codex shows `reason` as the block and `systemMessage` as a separate
|
|
@@ -269,32 +278,42 @@ function loginEmail(agent) {
|
|
|
269
278
|
return null;
|
|
270
279
|
}
|
|
271
280
|
/** Why this agent's signed-in tenant is blocked, or null when it is not (or cannot be read). */
|
|
272
|
-
function
|
|
281
|
+
function tenantBlock(agent) {
|
|
273
282
|
const { approved_tenants: approved, team_email_domains: teamDomains } = readRemediationInstructionsFile();
|
|
274
283
|
if (agent === 'claude')
|
|
275
|
-
return
|
|
284
|
+
return approvedTenantBlock('claude', approved, readClaudeSignedInTenant());
|
|
276
285
|
if (agent === 'claude_desktop') {
|
|
277
|
-
return
|
|
286
|
+
return approvedTenantBlock('claude_desktop', approved, readClaudeDesktopSignedInTenant());
|
|
278
287
|
}
|
|
279
288
|
if (agent === 'codex')
|
|
280
|
-
return
|
|
289
|
+
return codexTenantBlock(approved, readCodexSignedIn());
|
|
281
290
|
if (agent === 'cursor')
|
|
282
|
-
return
|
|
291
|
+
return cursorTeamBlock(approved, readCursorSignedInTeams(), teamDomains);
|
|
283
292
|
return null;
|
|
284
293
|
}
|
|
285
|
-
/**
|
|
286
|
-
|
|
294
|
+
/**
|
|
295
|
+
* The block for a login: which check refused it, why, and the blocked-agent window's words for a
|
|
296
|
+
* tenant or email-domain block (an agent's own block window comes synced); null when all pass.
|
|
297
|
+
*/
|
|
298
|
+
function loginBlock(agent, email, agentAllowed) {
|
|
287
299
|
const synced = readRemediationInstructionsFile();
|
|
288
|
-
const agentMsg = agentApprovalViolation(agent, synced.approved_agents);
|
|
300
|
+
const agentMsg = agentAllowed ? null : agentApprovalViolation(agent, synced.approved_agents);
|
|
289
301
|
if (agentMsg)
|
|
290
302
|
return { event: 'prompt_gate_block_unapproved_agent', reason: agentMsg };
|
|
291
303
|
if (email === null)
|
|
292
304
|
return null;
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
return {
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
const emailBlock = unapprovedEmailDomainBlock(email, synced.unapproved_email_domains);
|
|
306
|
+
if (emailBlock) {
|
|
307
|
+
return {
|
|
308
|
+
event: 'prompt_gate_block_unapproved_email_domain',
|
|
309
|
+
reason: unapprovedEmailDomainViolation(email, synced.unapproved_email_domains) ?? '',
|
|
310
|
+
window: emailDomainBlockWindow(agent, emailBlock, synced.team_email_domains),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const tenant = tenantBlock(agent);
|
|
314
|
+
if (!tenant)
|
|
315
|
+
return null;
|
|
316
|
+
return { event: 'prompt_gate_block_unapproved_tenant', reason: tenantBlockText(tenant), window: tenantBlockWindow(tenant) };
|
|
298
317
|
}
|
|
299
318
|
/**
|
|
300
319
|
* Block the prompt when the agent is not approved, or the login's email domain or its tenant was
|
|
@@ -306,11 +325,17 @@ function blockedByLogin(agent, ide) {
|
|
|
306
325
|
if (tenantGateSwitchedOff())
|
|
307
326
|
return false;
|
|
308
327
|
const email = loginEmail(agent);
|
|
309
|
-
const
|
|
328
|
+
const synced = readRemediationInstructionsFile();
|
|
329
|
+
const access = agentAccessDecision(agent, synced.approved_agents);
|
|
330
|
+
const block = loginBlock(agent, email, access.allowed);
|
|
310
331
|
if (!block)
|
|
311
332
|
return false;
|
|
312
333
|
logRemediationApplyFailure(block.event, { reason: block.reason, ide, agent, email: email ?? '' });
|
|
313
|
-
|
|
334
|
+
// The blocked-agent window opens beside every block, in the background, so the hook answers at
|
|
335
|
+
// once: an agent's own from the synced approvals, a login's from the words built here. Where it
|
|
336
|
+
// could not start, Cursor's hook keeps its alert.
|
|
337
|
+
const windowShown = launchAgentAccessWindow(approvalId(agent), block.window);
|
|
338
|
+
console.log(agent === 'codex' ? codexBlockPayload(block.reason) : blockPayload(ide, block.reason, windowShown));
|
|
314
339
|
return true;
|
|
315
340
|
}
|
|
316
341
|
/** Pull the organization's latest remediations and approved tenants, waiting at most 3 seconds. */
|
|
@@ -325,6 +350,36 @@ async function syncApprovals() {
|
|
|
325
350
|
// Network or auth failure — fall back to local file state.
|
|
326
351
|
}
|
|
327
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Skill block killswitch, Claude Code only for now: refuse the prompt of a session that loaded a
|
|
355
|
+
* skill which is now blocked, or whose `claude` process did in an earlier session (a `/clear` keeps
|
|
356
|
+
* the skill), docs/skill-block/DESIGN.md. True when it refused.
|
|
357
|
+
*/
|
|
358
|
+
function blockedBySkillBlock(agent, ide, sessionId) {
|
|
359
|
+
if (ide !== 'claude')
|
|
360
|
+
return false;
|
|
361
|
+
const proc = claudeProcess();
|
|
362
|
+
try {
|
|
363
|
+
recordSessionProcess(sessionId, proc);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
hookRunLog(`skill_block_process_table_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
367
|
+
}
|
|
368
|
+
const skills = stoppingSkills(sessionId, proc);
|
|
369
|
+
if (skills.length > 0) {
|
|
370
|
+
logRemediationApplyFailure('skill_block_session_stopped', { ide, agent, skills: skills.join(',') });
|
|
371
|
+
const windowShown = openSkillBlockWindow(skills[0]);
|
|
372
|
+
console.log(blockPayload(ide, stoppedSessionMessage(skills), windowShown));
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
const typed = blockedSkillOfCommand(process.env.OPTIMUS_PROMPT_FIRST_LINE || '', () => proc?.startedAt ?? null);
|
|
376
|
+
if (!typed)
|
|
377
|
+
return false;
|
|
378
|
+
logRemediationApplyFailure('skill_block_command_refused', { ide, agent, skills: typed });
|
|
379
|
+
const windowShown = openSkillBlockWindow(typed);
|
|
380
|
+
console.log(blockPayload(ide, blockedCommandMessage(typed), windowShown));
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
328
383
|
/** Agents whose remediations are JSON files written to disk at once (no deferred SQLite apply). */
|
|
329
384
|
function isImmediateJsonIde(ide) {
|
|
330
385
|
return ide === 'claude' || ide === 'copilot' || ide === 'opencode';
|
|
@@ -537,6 +592,8 @@ export async function runCompliancePromptGate() {
|
|
|
537
592
|
}
|
|
538
593
|
await reportPostRestartVerification(agent, complianceLogPath);
|
|
539
594
|
await syncApprovals();
|
|
595
|
+
if (blockedBySkillBlock(agent, ide, (process.env.OPTIMUS_SESSION_ID || '').trim()))
|
|
596
|
+
return;
|
|
540
597
|
const status = runLocalRemediationComplianceCheck(agent);
|
|
541
598
|
uploadSecondarySatisfied(status);
|
|
542
599
|
// Approved tenants (OPT-687): checked against the list the sync above stored locally.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, lstatSync, readlinkSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { readMarkdownFile } from '../readers/file_readers.js';
|
|
4
|
+
import { quarantineReceiptFor } from '../runtime/skill_block.js';
|
|
4
5
|
function isMissingPathError(err) {
|
|
5
6
|
const code = err?.code;
|
|
6
7
|
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
@@ -173,6 +174,14 @@ export function collectSkillMdRecord(skillDir, fileType, source, skillFilename =
|
|
|
173
174
|
...extraRaw,
|
|
174
175
|
};
|
|
175
176
|
const symlink_info = skillLinkPayload(info);
|
|
177
|
+
const quarantine = quarantineReceiptFor(skillDir);
|
|
178
|
+
if (quarantine) {
|
|
179
|
+
// Quarantined for a skill block: moved into the vault, unchanged since (docs/skill-block). The
|
|
180
|
+
// receipt says when, and which files with which hashes; never their contents.
|
|
181
|
+
raw.locked_by_optimus = true;
|
|
182
|
+
raw.quarantine = { quarantined_at: quarantine.quarantined_at, files: quarantine.files };
|
|
183
|
+
return { file_type: fileType, file_path: mdPath, raw_content: raw, symlink_info };
|
|
184
|
+
}
|
|
176
185
|
if (info.status === 'valid') {
|
|
177
186
|
const content = readMarkdownFile(mdPath);
|
|
178
187
|
if (content !== null) {
|
|
@@ -77,6 +77,18 @@ export function getRemediationStateDir() {
|
|
|
77
77
|
export function getRemediationInstructionsPath() {
|
|
78
78
|
return join(getRemediationStateDir(), REMEDIATION_INSTRUCTIONS_BASENAME);
|
|
79
79
|
}
|
|
80
|
+
/** The mode each locked skill folder had before it was locked, so unblocking can restore it. */
|
|
81
|
+
export function getSkillBlockStatePath() {
|
|
82
|
+
return join(getRemediationStateDir(), 'skill_block_state.json');
|
|
83
|
+
}
|
|
84
|
+
/** The vault of quarantined skill folders and their receipts (docs/skill-block, "Quarantine"). */
|
|
85
|
+
export function getQuarantineDir() {
|
|
86
|
+
return join(getManagementDir(), 'quarantine');
|
|
87
|
+
}
|
|
88
|
+
/** The session ids each `claude` process has run, so a `/clear` cannot escape a skill block stop. */
|
|
89
|
+
export function getSkillBlockProcessesPath() {
|
|
90
|
+
return join(getRemediationStateDir(), 'skill_block_processes.json');
|
|
91
|
+
}
|
|
80
92
|
export function getDeferredVscdbApplyPath() {
|
|
81
93
|
return join(getVscdbStateDir(), DEFERRED_VSCDB_APPLY_BASENAME);
|
|
82
94
|
}
|
|
@@ -139,12 +151,18 @@ function writeInstructionsFile(file, remediationsWrittenAt) {
|
|
|
139
151
|
remediations_written_at: remediationsWrittenAt,
|
|
140
152
|
});
|
|
141
153
|
}
|
|
154
|
+
// The lists the sync replaces on every answer (the skill blocks with them); a writer that passes
|
|
155
|
+
// only `remediations` keeps them.
|
|
142
156
|
const APPROVAL_LISTS = [
|
|
143
157
|
'approved_tenants',
|
|
144
158
|
'unapproved_email_domains',
|
|
145
159
|
'allowed_email_domains',
|
|
146
160
|
'approved_agents',
|
|
147
161
|
'team_email_domains',
|
|
162
|
+
'skill_blocks',
|
|
163
|
+
'skill_block_sessions',
|
|
164
|
+
'skill_block_windows',
|
|
165
|
+
'agent_access_windows',
|
|
148
166
|
];
|
|
149
167
|
/** Each approval list from `file` when it names one, else the one on disk; absent ones left out. */
|
|
150
168
|
function keptApprovalLists(file, onDisk) {
|
|
@@ -172,6 +190,10 @@ function writeApprovalList(patch) {
|
|
|
172
190
|
export function writeUnapprovedEmailDomains(unapproved_email_domains) {
|
|
173
191
|
writeApprovalList({ unapproved_email_domains });
|
|
174
192
|
}
|
|
193
|
+
/** Replace only the blocked-agent windows, keeping everything else on disk. */
|
|
194
|
+
export function writeAgentAccessWindows(agent_access_windows) {
|
|
195
|
+
writeApprovalList({ agent_access_windows });
|
|
196
|
+
}
|
|
175
197
|
/** Replace only the approved agents, keeping everything else on disk. */
|
|
176
198
|
export function writeApprovedAgents(approved_agents) {
|
|
177
199
|
writeApprovalList({ approved_agents });
|
|
@@ -184,6 +206,18 @@ export function writeTeamEmailDomains(team_email_domains) {
|
|
|
184
206
|
export function writeAllowedEmailDomains(allowed_email_domains) {
|
|
185
207
|
writeApprovalList({ allowed_email_domains });
|
|
186
208
|
}
|
|
209
|
+
/** Replace only the skill blocks, keeping everything else on disk. */
|
|
210
|
+
export function writeSkillBlocks(skill_blocks) {
|
|
211
|
+
writeApprovalList({ skill_blocks });
|
|
212
|
+
}
|
|
213
|
+
/** Replace only the sessions to stop for skill blocks, keeping everything else on disk. */
|
|
214
|
+
export function writeSkillBlockSessions(skill_block_sessions) {
|
|
215
|
+
writeApprovalList({ skill_block_sessions });
|
|
216
|
+
}
|
|
217
|
+
/** Replace only the skill block windows, keeping everything else on disk. */
|
|
218
|
+
export function writeSkillBlockWindows(skill_block_windows) {
|
|
219
|
+
writeApprovalList({ skill_block_windows });
|
|
220
|
+
}
|
|
187
221
|
/** Replace only the approved-tenant list, keeping the remediations already on disk. */
|
|
188
222
|
export function writeApprovedTenants(approved_tenants) {
|
|
189
223
|
writeApprovalList({ approved_tenants });
|
|
@@ -4,7 +4,9 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
5
|
import { parseJsonWithJsoncFallback, parseTomlText, stringifyToml } from '../readers/file_readers.js';
|
|
6
6
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
7
|
-
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeApprovedTenants, writeUnapprovedEmailDomains, writeAllowedEmailDomains, writeTeamEmailDomains, writeApprovedAgents, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
7
|
+
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeApprovedTenants, writeUnapprovedEmailDomains, writeAllowedEmailDomains, writeTeamEmailDomains, writeApprovedAgents, writeAgentAccessWindows, writeRemediationInstructionsFile, writeSkillBlockSessions, writeSkillBlockWindows, writeSkillBlocks, } from './management_storage.js';
|
|
8
|
+
import { applySkillBlocks } from './skill_block.js';
|
|
9
|
+
import { openQuarantineWindows } from './skill_block_sessions.js';
|
|
8
10
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
9
11
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
10
12
|
import { createSignature } from '../sender/signing.js';
|
|
@@ -1656,6 +1658,69 @@ function storeApprovals(syncResult) {
|
|
|
1656
1658
|
const teamDomains = syncResult.team_email_domains;
|
|
1657
1659
|
if (Array.isArray(teamDomains))
|
|
1658
1660
|
writeTeamEmailDomains(lowercaseStrings(teamDomains));
|
|
1661
|
+
storeAgentAccess(syncResult);
|
|
1662
|
+
}
|
|
1663
|
+
/** The blocked-agent window's words for each agent that is not approved. */
|
|
1664
|
+
function storeAgentAccess(syncResult) {
|
|
1665
|
+
const windows = syncResult.agent_access_windows;
|
|
1666
|
+
if (windows && typeof windows === 'object' && !Array.isArray(windows))
|
|
1667
|
+
writeAgentAccessWindows(windows);
|
|
1668
|
+
}
|
|
1669
|
+
function isSkillBlockEntry(value) {
|
|
1670
|
+
if (!value || typeof value !== 'object')
|
|
1671
|
+
return false;
|
|
1672
|
+
const entry = value;
|
|
1673
|
+
return typeof entry.skill_dir === 'string' && entry.skill_dir !== '' && typeof entry.resource_uuid === 'string';
|
|
1674
|
+
}
|
|
1675
|
+
/** Keep the server's sessions to stop (for the prompt gate); an older server sends none. */
|
|
1676
|
+
function storeSkillBlockSessions(sessions) {
|
|
1677
|
+
if (!Array.isArray(sessions))
|
|
1678
|
+
return;
|
|
1679
|
+
writeSkillBlockSessions(sessions
|
|
1680
|
+
.filter((s) => typeof s?.session_id === 'string' && s.session_id !== '')
|
|
1681
|
+
.map((s) => ({
|
|
1682
|
+
session_id: s.session_id,
|
|
1683
|
+
resource_uuid: typeof s.resource_uuid === 'string' ? s.resource_uuid : '',
|
|
1684
|
+
skill_name: typeof s.skill_name === 'string' ? s.skill_name : '',
|
|
1685
|
+
})));
|
|
1686
|
+
}
|
|
1687
|
+
/** Keep the server's skill block windows (words only, drawn as sent); an older server sends none. */
|
|
1688
|
+
function storeSkillBlockWindows(windows) {
|
|
1689
|
+
if (!windows || typeof windows !== 'object' || Array.isArray(windows))
|
|
1690
|
+
return;
|
|
1691
|
+
const kept = {};
|
|
1692
|
+
for (const [uuid, words] of Object.entries(windows)) {
|
|
1693
|
+
if (words && typeof words === 'object' && !Array.isArray(words))
|
|
1694
|
+
kept[uuid] = words;
|
|
1695
|
+
}
|
|
1696
|
+
writeSkillBlockWindows(kept);
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Store the server's skill blocks and make the folders on disk match them. An older server sends
|
|
1700
|
+
* no `skill_blocks`: the folders are then left as they are, never unlocked by mistake.
|
|
1701
|
+
*/
|
|
1702
|
+
function enforceSkillBlocks(syncResult) {
|
|
1703
|
+
const blocks = syncResult.skill_blocks;
|
|
1704
|
+
if (!Array.isArray(blocks))
|
|
1705
|
+
return;
|
|
1706
|
+
const entries = blocks.filter(isSkillBlockEntry).map((entry) => ({
|
|
1707
|
+
resource_uuid: entry.resource_uuid,
|
|
1708
|
+
skill_name: typeof entry.skill_name === 'string' ? entry.skill_name : '',
|
|
1709
|
+
skill_dir: entry.skill_dir,
|
|
1710
|
+
}));
|
|
1711
|
+
writeSkillBlocks(entries);
|
|
1712
|
+
storeSkillBlockSessions(syncResult.skill_block_sessions);
|
|
1713
|
+
storeSkillBlockWindows(syncResult.skill_block_windows);
|
|
1714
|
+
try {
|
|
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);
|
|
1718
|
+
}
|
|
1719
|
+
catch (err) {
|
|
1720
|
+
const msg = `skill_block_error: ${errorText(err)}`;
|
|
1721
|
+
hookRunLog(msg);
|
|
1722
|
+
complianceRunnerDiag(msg);
|
|
1723
|
+
}
|
|
1659
1724
|
}
|
|
1660
1725
|
async function reconcileInstructions(endpointBase, machineUuid, want) {
|
|
1661
1726
|
try {
|
|
@@ -1792,6 +1857,7 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
1792
1857
|
return;
|
|
1793
1858
|
applyClearApplyTrackingUuids(syncResult.clear_apply_tracking_uuids);
|
|
1794
1859
|
storeApprovals(syncResult);
|
|
1860
|
+
enforceSkillBlocks(syncResult);
|
|
1795
1861
|
if (syncResult.status === 'unchanged') {
|
|
1796
1862
|
await reconcileUnchanged(endpointBase, machineUuid, remediations, activeUuids);
|
|
1797
1863
|
return;
|