@optimuslabs/harness-map-staging 1.5.36 → 1.5.38
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 +79 -18
- 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 +64 -1
- package/dist/log_config_files/runtime/skill_block.js +253 -72
- package/dist/log_config_files/runtime/skill_block_sessions.js +76 -20
- 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, skillBlockWindow, 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,16 @@ 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
|
return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
|
|
100
107
|
}
|
|
101
|
-
return JSON.stringify({ continue: false, user_message: text });
|
|
108
|
+
return JSON.stringify({ continue: false, user_message: text, ...(windowShown ? { __optimus_window: true } : {}) });
|
|
102
109
|
}
|
|
103
110
|
/**
|
|
104
111
|
* Codex's block: `reason` only. Codex shows `reason` as the block and `systemMessage` as a separate
|
|
@@ -269,32 +276,42 @@ function loginEmail(agent) {
|
|
|
269
276
|
return null;
|
|
270
277
|
}
|
|
271
278
|
/** Why this agent's signed-in tenant is blocked, or null when it is not (or cannot be read). */
|
|
272
|
-
function
|
|
279
|
+
function tenantBlock(agent) {
|
|
273
280
|
const { approved_tenants: approved, team_email_domains: teamDomains } = readRemediationInstructionsFile();
|
|
274
281
|
if (agent === 'claude')
|
|
275
|
-
return
|
|
282
|
+
return approvedTenantBlock('claude', approved, readClaudeSignedInTenant());
|
|
276
283
|
if (agent === 'claude_desktop') {
|
|
277
|
-
return
|
|
284
|
+
return approvedTenantBlock('claude_desktop', approved, readClaudeDesktopSignedInTenant());
|
|
278
285
|
}
|
|
279
286
|
if (agent === 'codex')
|
|
280
|
-
return
|
|
287
|
+
return codexTenantBlock(approved, readCodexSignedIn());
|
|
281
288
|
if (agent === 'cursor')
|
|
282
|
-
return
|
|
289
|
+
return cursorTeamBlock(approved, readCursorSignedInTeams(), teamDomains);
|
|
283
290
|
return null;
|
|
284
291
|
}
|
|
285
|
-
/**
|
|
286
|
-
|
|
292
|
+
/**
|
|
293
|
+
* The block for a login: which check refused it, why, and the blocked-agent window's words for a
|
|
294
|
+
* tenant or email-domain block (an agent's own block window comes synced); null when all pass.
|
|
295
|
+
*/
|
|
296
|
+
function loginBlock(agent, email, agentAllowed) {
|
|
287
297
|
const synced = readRemediationInstructionsFile();
|
|
288
|
-
const agentMsg = agentApprovalViolation(agent, synced.approved_agents);
|
|
298
|
+
const agentMsg = agentAllowed ? null : agentApprovalViolation(agent, synced.approved_agents);
|
|
289
299
|
if (agentMsg)
|
|
290
300
|
return { event: 'prompt_gate_block_unapproved_agent', reason: agentMsg };
|
|
291
301
|
if (email === null)
|
|
292
302
|
return null;
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
return {
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
const emailBlock = unapprovedEmailDomainBlock(email, synced.unapproved_email_domains);
|
|
304
|
+
if (emailBlock) {
|
|
305
|
+
return {
|
|
306
|
+
event: 'prompt_gate_block_unapproved_email_domain',
|
|
307
|
+
reason: unapprovedEmailDomainViolation(email, synced.unapproved_email_domains) ?? '',
|
|
308
|
+
window: emailDomainBlockWindow(agent, emailBlock, synced.team_email_domains),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const tenant = tenantBlock(agent);
|
|
312
|
+
if (!tenant)
|
|
313
|
+
return null;
|
|
314
|
+
return { event: 'prompt_gate_block_unapproved_tenant', reason: tenantBlockText(tenant), window: tenantBlockWindow(tenant) };
|
|
298
315
|
}
|
|
299
316
|
/**
|
|
300
317
|
* Block the prompt when the agent is not approved, or the login's email domain or its tenant was
|
|
@@ -306,11 +323,17 @@ function blockedByLogin(agent, ide) {
|
|
|
306
323
|
if (tenantGateSwitchedOff())
|
|
307
324
|
return false;
|
|
308
325
|
const email = loginEmail(agent);
|
|
309
|
-
const
|
|
326
|
+
const synced = readRemediationInstructionsFile();
|
|
327
|
+
const access = agentAccessDecision(agent, synced.approved_agents);
|
|
328
|
+
const block = loginBlock(agent, email, access.allowed);
|
|
310
329
|
if (!block)
|
|
311
330
|
return false;
|
|
312
331
|
logRemediationApplyFailure(block.event, { reason: block.reason, ide, agent, email: email ?? '' });
|
|
313
|
-
|
|
332
|
+
// The blocked-agent window opens beside every block, in the background, so the hook answers at
|
|
333
|
+
// once: an agent's own from the synced approvals, a login's from the words built here. Where it
|
|
334
|
+
// could not start, Cursor's hook keeps its alert.
|
|
335
|
+
const windowShown = launchAgentAccessWindow(approvalId(agent), block.window);
|
|
336
|
+
console.log(agent === 'codex' ? codexBlockPayload(block.reason) : blockPayload(ide, block.reason, windowShown));
|
|
314
337
|
return true;
|
|
315
338
|
}
|
|
316
339
|
/** Pull the organization's latest remediations and approved tenants, waiting at most 3 seconds. */
|
|
@@ -325,6 +348,42 @@ async function syncApprovals() {
|
|
|
325
348
|
// Network or auth failure — fall back to local file state.
|
|
326
349
|
}
|
|
327
350
|
}
|
|
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
|
+
/**
|
|
358
|
+
* Skill block killswitch, Claude Code only for now: refuse the prompt of a session that loaded a
|
|
359
|
+
* skill which is now blocked, or whose `claude` process did in an earlier session (a `/clear` keeps
|
|
360
|
+
* the skill), docs/skill-block/DESIGN.md. True when it refused.
|
|
361
|
+
*/
|
|
362
|
+
function blockedBySkillBlock(agent, ide, sessionId) {
|
|
363
|
+
if (ide !== 'claude')
|
|
364
|
+
return false;
|
|
365
|
+
const proc = claudeProcess();
|
|
366
|
+
try {
|
|
367
|
+
recordSessionProcess(sessionId, proc);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
hookRunLog(`skill_block_process_table_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
+
}
|
|
372
|
+
const skills = stoppingSkills(sessionId, proc);
|
|
373
|
+
if (skills.length > 0) {
|
|
374
|
+
logRemediationApplyFailure('skill_block_session_stopped', { ide, agent, skills: skills.join(',') });
|
|
375
|
+
openSkillBlockWindow(skills[0]);
|
|
376
|
+
console.log(blockPayload(ide, stoppedSessionMessage(skills)));
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
const typed = blockedSkillOfCommand(process.env.OPTIMUS_PROMPT_FIRST_LINE || '', () => proc?.startedAt ?? null);
|
|
380
|
+
if (!typed)
|
|
381
|
+
return false;
|
|
382
|
+
logRemediationApplyFailure('skill_block_command_refused', { ide, agent, skills: typed });
|
|
383
|
+
openSkillBlockWindow(typed);
|
|
384
|
+
console.log(blockPayload(ide, blockedCommandMessage(typed)));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
328
387
|
/** Agents whose remediations are JSON files written to disk at once (no deferred SQLite apply). */
|
|
329
388
|
function isImmediateJsonIde(ide) {
|
|
330
389
|
return ide === 'claude' || ide === 'copilot' || ide === 'opencode';
|
|
@@ -537,6 +596,8 @@ export async function runCompliancePromptGate() {
|
|
|
537
596
|
}
|
|
538
597
|
await reportPostRestartVerification(agent, complianceLogPath);
|
|
539
598
|
await syncApprovals();
|
|
599
|
+
if (blockedBySkillBlock(agent, ide, (process.env.OPTIMUS_SESSION_ID || '').trim()))
|
|
600
|
+
return;
|
|
540
601
|
const status = runLocalRemediationComplianceCheck(agent);
|
|
541
602
|
uploadSecondarySatisfied(status);
|
|
542
603
|
// 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,8 @@ 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';
|
|
8
9
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
9
10
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
10
11
|
import { createSignature } from '../sender/signing.js';
|
|
@@ -1656,6 +1657,67 @@ function storeApprovals(syncResult) {
|
|
|
1656
1657
|
const teamDomains = syncResult.team_email_domains;
|
|
1657
1658
|
if (Array.isArray(teamDomains))
|
|
1658
1659
|
writeTeamEmailDomains(lowercaseStrings(teamDomains));
|
|
1660
|
+
storeAgentAccess(syncResult);
|
|
1661
|
+
}
|
|
1662
|
+
/** The blocked-agent window's words for each agent that is not approved. */
|
|
1663
|
+
function storeAgentAccess(syncResult) {
|
|
1664
|
+
const windows = syncResult.agent_access_windows;
|
|
1665
|
+
if (windows && typeof windows === 'object' && !Array.isArray(windows))
|
|
1666
|
+
writeAgentAccessWindows(windows);
|
|
1667
|
+
}
|
|
1668
|
+
function isSkillBlockEntry(value) {
|
|
1669
|
+
if (!value || typeof value !== 'object')
|
|
1670
|
+
return false;
|
|
1671
|
+
const entry = value;
|
|
1672
|
+
return typeof entry.skill_dir === 'string' && entry.skill_dir !== '' && typeof entry.resource_uuid === 'string';
|
|
1673
|
+
}
|
|
1674
|
+
/** Keep the server's sessions to stop (for the prompt gate); an older server sends none. */
|
|
1675
|
+
function storeSkillBlockSessions(sessions) {
|
|
1676
|
+
if (!Array.isArray(sessions))
|
|
1677
|
+
return;
|
|
1678
|
+
writeSkillBlockSessions(sessions
|
|
1679
|
+
.filter((s) => typeof s?.session_id === 'string' && s.session_id !== '')
|
|
1680
|
+
.map((s) => ({
|
|
1681
|
+
session_id: s.session_id,
|
|
1682
|
+
resource_uuid: typeof s.resource_uuid === 'string' ? s.resource_uuid : '',
|
|
1683
|
+
skill_name: typeof s.skill_name === 'string' ? s.skill_name : '',
|
|
1684
|
+
})));
|
|
1685
|
+
}
|
|
1686
|
+
/** Keep the server's skill block windows (words only, drawn as sent); an older server sends none. */
|
|
1687
|
+
function storeSkillBlockWindows(windows) {
|
|
1688
|
+
if (!windows || typeof windows !== 'object' || Array.isArray(windows))
|
|
1689
|
+
return;
|
|
1690
|
+
const kept = {};
|
|
1691
|
+
for (const [uuid, words] of Object.entries(windows)) {
|
|
1692
|
+
if (words && typeof words === 'object' && !Array.isArray(words))
|
|
1693
|
+
kept[uuid] = words;
|
|
1694
|
+
}
|
|
1695
|
+
writeSkillBlockWindows(kept);
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Store the server's skill blocks and make the folders on disk match them. An older server sends
|
|
1699
|
+
* no `skill_blocks`: the folders are then left as they are, never unlocked by mistake.
|
|
1700
|
+
*/
|
|
1701
|
+
function enforceSkillBlocks(syncResult) {
|
|
1702
|
+
const blocks = syncResult.skill_blocks;
|
|
1703
|
+
if (!Array.isArray(blocks))
|
|
1704
|
+
return;
|
|
1705
|
+
const entries = blocks.filter(isSkillBlockEntry).map((entry) => ({
|
|
1706
|
+
resource_uuid: entry.resource_uuid,
|
|
1707
|
+
skill_name: typeof entry.skill_name === 'string' ? entry.skill_name : '',
|
|
1708
|
+
skill_dir: entry.skill_dir,
|
|
1709
|
+
}));
|
|
1710
|
+
writeSkillBlocks(entries);
|
|
1711
|
+
storeSkillBlockSessions(syncResult.skill_block_sessions);
|
|
1712
|
+
storeSkillBlockWindows(syncResult.skill_block_windows);
|
|
1713
|
+
try {
|
|
1714
|
+
applySkillBlocks(entries);
|
|
1715
|
+
}
|
|
1716
|
+
catch (err) {
|
|
1717
|
+
const msg = `skill_block_error: ${errorText(err)}`;
|
|
1718
|
+
hookRunLog(msg);
|
|
1719
|
+
complianceRunnerDiag(msg);
|
|
1720
|
+
}
|
|
1659
1721
|
}
|
|
1660
1722
|
async function reconcileInstructions(endpointBase, machineUuid, want) {
|
|
1661
1723
|
try {
|
|
@@ -1792,6 +1854,7 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
1792
1854
|
return;
|
|
1793
1855
|
applyClearApplyTrackingUuids(syncResult.clear_apply_tracking_uuids);
|
|
1794
1856
|
storeApprovals(syncResult);
|
|
1857
|
+
enforceSkillBlocks(syncResult);
|
|
1795
1858
|
if (syncResult.status === 'unchanged') {
|
|
1796
1859
|
await reconcileUnchanged(endpointBase, machineUuid, remediations, activeUuids);
|
|
1797
1860
|
return;
|