@optimuslabs/harness-map-staging 1.5.35 → 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 +196 -105
- package/dist/compliance_prompt_gate.js +89 -26
- 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
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* The backend knows which sessions loaded a skill: the tool calls the skill usage view attributes to
|
|
7
7
|
* it. The remediation sync sends this machine's sessions that loaded a blocked skill
|
|
8
8
|
* (`skill_block_sessions` in remediation_instructions.json), and the prompt gate refuses their
|
|
9
|
-
* prompts.
|
|
9
|
+
* prompts. `/clear` gives a new session id but the `claude` process keeps the skill, so the gate also
|
|
10
|
+
* records which session ids each process has run (skill_block_processes.json) and stops every session
|
|
11
|
+
* of a process that ran a listed one. An unblock takes the sessions off the list.
|
|
10
12
|
*/
|
|
11
13
|
import { execFileSync } from 'node:child_process';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { basename } from 'node:path';
|
|
16
|
+
import { atomicWriteJson, getSkillBlockProcessesPath, readRemediationInstructionsFile, } from './management_storage.js';
|
|
17
|
+
import { quarantinedAt } from './skill_block.js';
|
|
15
18
|
/** A process's parent, start time and command name, from `ps`; null when it cannot be read. */
|
|
16
19
|
function processRow(pid) {
|
|
17
20
|
try {
|
|
@@ -25,41 +28,84 @@ function processRow(pid) {
|
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* The `claude` process running this hook, or null when it cannot be found. It lists its skills when
|
|
32
|
+
* it starts and keeps them across `/clear`, which only changes the session id, so the process (not
|
|
33
|
+
* the session) is what holds a loaded skill.
|
|
31
34
|
*/
|
|
32
|
-
export function
|
|
35
|
+
export function claudeProcess() {
|
|
33
36
|
let pid = process.ppid;
|
|
34
37
|
for (let depth = 0; depth < 12 && pid > 1; depth += 1) {
|
|
35
38
|
const row = processRow(pid);
|
|
36
39
|
if (!row)
|
|
37
40
|
return null;
|
|
38
41
|
if (row.name === 'claude')
|
|
39
|
-
return row.startedAt;
|
|
42
|
+
return Number.isNaN(row.startedAt) ? null : { pid, startedAt: row.startedAt };
|
|
40
43
|
pid = row.parent;
|
|
41
44
|
}
|
|
42
45
|
return null;
|
|
43
46
|
}
|
|
44
|
-
/**
|
|
45
|
-
export function
|
|
47
|
+
/** When the `claude` process running this hook started (ms), or null when it cannot be found. */
|
|
48
|
+
export function claudeProcessStartedAt() {
|
|
49
|
+
return claudeProcess()?.startedAt ?? null;
|
|
50
|
+
}
|
|
51
|
+
/** The table's key: a pid with its start time, since the system reuses pids. */
|
|
52
|
+
function processKey(proc) {
|
|
53
|
+
return `${proc.pid}:${proc.startedAt}`;
|
|
54
|
+
}
|
|
55
|
+
function readProcessTable() {
|
|
56
|
+
const path = getSkillBlockProcessesPath();
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return {};
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
61
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Record that `proc` runs `sessionId`, and drop processes not seen for 30 days (the server's window).
|
|
69
|
+
* Two prompts at once can each write the table and lose one session; the next prompt records it again.
|
|
70
|
+
*/
|
|
71
|
+
export function recordSessionProcess(sessionId, proc, now = Date.now()) {
|
|
72
|
+
if (!sessionId || !proc)
|
|
73
|
+
return;
|
|
74
|
+
const table = readProcessTable();
|
|
75
|
+
const cutoff = now - 30 * 24 * 60 * 60 * 1000;
|
|
76
|
+
for (const [key, entry] of Object.entries(table)) {
|
|
77
|
+
if (!(Date.parse(entry?.last_seen ?? '') >= cutoff))
|
|
78
|
+
delete table[key];
|
|
79
|
+
}
|
|
80
|
+
const key = processKey(proc);
|
|
81
|
+
const sessions = new Set(table[key]?.sessions ?? []);
|
|
82
|
+
sessions.add(sessionId);
|
|
83
|
+
table[key] = { sessions: [...sessions], last_seen: new Date(now).toISOString() };
|
|
84
|
+
atomicWriteJson(getSkillBlockProcessesPath(), table);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The names of the blocked skills this session loaded, or its process loaded in an earlier session
|
|
88
|
+
* (before a `/clear`); empty when it may go on. Without the process, only the session id counts.
|
|
89
|
+
*/
|
|
90
|
+
export function stoppingSkills(sessionId, proc = null) {
|
|
46
91
|
if (!sessionId)
|
|
47
92
|
return [];
|
|
93
|
+
const own = new Set([sessionId, ...(proc ? (readProcessTable()[processKey(proc)]?.sessions ?? []) : [])]);
|
|
48
94
|
const sessions = readRemediationInstructionsFile().skill_block_sessions ?? [];
|
|
49
|
-
return sessions.filter((s) => s.session_id
|
|
95
|
+
return sessions.filter((s) => own.has(s.session_id)).map((s) => s.skill_name || 'a skill');
|
|
50
96
|
}
|
|
51
97
|
/** The notice for a stopped session. */
|
|
52
98
|
export function stoppedSessionMessage(skills) {
|
|
53
99
|
const names = [...new Set(skills)].join(', ');
|
|
54
|
-
return (`Your organization blocked ${names}, which this
|
|
55
|
-
'this conversation, then quit Claude Code and start it again to
|
|
56
|
-
'session keeps it stopped.');
|
|
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.');
|
|
57
103
|
}
|
|
58
104
|
/**
|
|
59
105
|
* The blocked skill a typed slash command runs, when this process may still have it, or null:
|
|
60
|
-
* `/airtable:airtable-cli …` or `/airtable-cli …`. A process started before the folder was
|
|
61
|
-
* listed the skill and keeps it, even after `/clear`, so this refuses it. A process
|
|
62
|
-
*
|
|
106
|
+
* `/airtable:airtable-cli …` or `/airtable-cli …`. A process started before the folder was
|
|
107
|
+
* quarantined listed the skill and keeps it, even after `/clear`, so this refuses it. A process
|
|
108
|
+
* started after it never listed it: this answers null, and Claude Code says the skill does not exist.
|
|
63
109
|
* Matches the skill's name or its folder's name, which can differ (`aikido-issues` lives in
|
|
64
110
|
* `skills/issues`). An unknown start time counts as too old, so a block is never skipped.
|
|
65
111
|
*/
|
|
@@ -73,9 +119,8 @@ export function blockedSkillOfCommand(promptFirstLine, processStartedAt = claude
|
|
|
73
119
|
if (hits.length === 0)
|
|
74
120
|
return null;
|
|
75
121
|
const startedAt = processStartedAt();
|
|
76
|
-
const locks = readSkillBlockState();
|
|
77
122
|
const mayStillHaveIt = hits.some((b) => {
|
|
78
|
-
const lockedAt = Date.parse(
|
|
123
|
+
const lockedAt = Date.parse(quarantinedAt(b.skill_dir) ?? '');
|
|
79
124
|
return startedAt === null || Number.isNaN(lockedAt) || startedAt <= lockedAt;
|
|
80
125
|
});
|
|
81
126
|
return mayStillHaveIt ? hits[0].skill_name || typed : null;
|
|
@@ -84,3 +129,14 @@ export function blockedSkillOfCommand(promptFirstLine, processStartedAt = claude
|
|
|
84
129
|
export function blockedCommandMessage(skill) {
|
|
85
130
|
return `Your organization blocked ${skill}. Quit Claude Code and start it again to continue.`;
|
|
86
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The window to open beside the block of the skill named `skill` (a name `stoppingSkills` or
|
|
134
|
+
* `blockedSkillOfCommand` gave), as the sync sent it for that skill's resource; undefined when the
|
|
135
|
+
* server sent none, so only the one-line block shows.
|
|
136
|
+
*/
|
|
137
|
+
export function skillBlockWindow(skill) {
|
|
138
|
+
const file = readRemediationInstructionsFile();
|
|
139
|
+
const name = skill.toLowerCase();
|
|
140
|
+
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;
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optimuslabs/harness-map-staging",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.38",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dialog_prefs-staging": "dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "tsc -p tsconfig.json && node ../../../scripts/ensure-bin-executable.mjs",
|
|
18
|
+
"build": "tsc -p tsconfig.json && node ../../../scripts/ensure-bin-executable.mjs && node -e \"const fs=require('fs');fs.mkdirSync('dist/agent_block_window',{recursive:true});fs.copyFileSync('src/agent_block_window/agent_block_window.jxa','dist/agent_block_window/agent_block_window.jxa')\"",
|
|
19
19
|
"dev": "ts-node src/cli.ts",
|
|
20
20
|
"lint": "eslint \"src/**/*.ts\"",
|
|
21
21
|
"prepare": "npm run build",
|