@optimuslabs/harness-map-staging 1.5.16 → 1.5.19
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/log_config_files/collection/directory_collector.js +10 -0
- package/dist/log_config_files/collection/plugin_collector.js +12 -4
- package/dist/log_config_files/collection/skill_symlink.js +191 -0
- package/dist/log_config_files/runtime/compliance_check.js +83 -12
- package/dist/log_config_files/runtime/prompt-collection-policy.js +57 -0
- package/dist/log_config_files/runtime/remediation_sync.js +19 -4
- package/dist/log_config_files/runtime/secret_regex_scan.js +28 -8
- package/dist/log_config_files/sender/batch_sender.js +14 -5
- package/dist/log_uuid/startup_sender.js +2 -0
- package/dist/tofu.js +2 -2
- package/package.json +3 -3
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
4
|
+
import { collectSkillMdRecord, isSkillDirEntry } from './skill_symlink.js';
|
|
4
5
|
function collectSubdirMdFiles(dirPath, fileType, mdFilename, source) {
|
|
5
6
|
const results = [];
|
|
7
|
+
const isSkillMd = mdFilename.toLowerCase() === 'skill.md';
|
|
6
8
|
try {
|
|
7
9
|
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
10
|
+
if (isSkillMd) {
|
|
11
|
+
if (!isSkillDirEntry(entry))
|
|
12
|
+
continue;
|
|
13
|
+
const record = collectSkillMdRecord(join(dirPath, entry.name), fileType, source, mdFilename);
|
|
14
|
+
if (record)
|
|
15
|
+
results.push(record);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
8
18
|
if (!entry.isDirectory())
|
|
9
19
|
continue;
|
|
10
20
|
const mdPath = join(dirPath, entry.name, mdFilename);
|
|
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
6
6
|
import { getInstalledPluginsPath, getPluginHooksPath, getPluginMcpPaths, getPluginSkillFilename, getPluginSkillsDir } from '../paths/path_constants_helpers.js';
|
|
7
7
|
import { versionFromPluginCachePath, parseInstalledPluginKey } from './plugin_version_helpers.js';
|
|
8
|
+
import { collectSkillMdRecord, isSkillDirEntry, symlinkInfoAllowsExtraFiles } from './skill_symlink.js';
|
|
8
9
|
/** Directories never worth shipping: git history is huge and already-compressed, deps are not the skill. */
|
|
9
10
|
const SKILL_SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
10
11
|
/** Fonts are the single largest thing in skill folders (canvas-design ships 54) and carry no signal. */
|
|
@@ -53,7 +54,9 @@ function collectSkillDirFiles(dir, results, constants, skillFilename, budget, de
|
|
|
53
54
|
collectSkillDirFiles(full, results, constants, skillFilename, budget, depth + 1);
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
if (ent.name === skillFilename)
|
|
58
|
+
continue;
|
|
59
|
+
// Extra skill files: skip nested symlinks so the walk stays inside this skill dir.
|
|
57
60
|
if (!ent.isFile())
|
|
58
61
|
continue;
|
|
59
62
|
const ext = extname(ent.name).toLowerCase();
|
|
@@ -100,11 +103,16 @@ function collectSkillDirFiles(dir, results, constants, skillFilename, budget, de
|
|
|
100
103
|
function collectSkillFiles(skillsDir, results, constants) {
|
|
101
104
|
const skillFilename = getPluginSkillFilename(constants);
|
|
102
105
|
try {
|
|
103
|
-
for (const d of readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d
|
|
106
|
+
for (const d of readdirSync(skillsDir, { withFileTypes: true }).filter((d) => isSkillDirEntry(d))) {
|
|
104
107
|
const skillRoot = join(skillsDir, d.name);
|
|
105
|
-
|
|
108
|
+
const ver = versionFromPluginCachePath(join(skillRoot, skillFilename), constants);
|
|
109
|
+
const record = collectSkillMdRecord(skillRoot, 'claude_skill', 'file', skillFilename, ver ? { version: ver } : {});
|
|
110
|
+
if (!record)
|
|
111
|
+
continue;
|
|
112
|
+
results.push(record);
|
|
113
|
+
if (!symlinkInfoAllowsExtraFiles(record.symlink_info))
|
|
106
114
|
continue;
|
|
107
|
-
const budget = { files:
|
|
115
|
+
const budget = { files: 1, bytes: 0 };
|
|
108
116
|
collectSkillDirFiles(skillRoot, results, constants, skillFilename, budget, 0);
|
|
109
117
|
if (budget.files >= SKILL_MAX_FILES || budget.bytes >= SKILL_MAX_TOTAL_BYTES) {
|
|
110
118
|
console.warn(`Skill ${skillRoot} hit collection cap (${budget.files} files, ${budget.bytes} bytes)`);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { accessSync, constants, lstatSync, readlinkSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { readMarkdownFile } from '../readers/file_readers.js';
|
|
4
|
+
function isMissingPathError(err) {
|
|
5
|
+
const code = err?.code;
|
|
6
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve readlink's raw target the way the OS does when parent dirs include
|
|
10
|
+
* symlinks. A lexical join against the logical parent is wrong when that parent
|
|
11
|
+
* is itself a link (e.g. ~/.config → a dotfiles repo).
|
|
12
|
+
*/
|
|
13
|
+
export function resolveRawSymlinkTarget(linkPath, target) {
|
|
14
|
+
if (isAbsolute(target))
|
|
15
|
+
return resolve(target);
|
|
16
|
+
const physicalParent = realpathSync(dirname(linkPath));
|
|
17
|
+
return resolve(physicalParent, target);
|
|
18
|
+
}
|
|
19
|
+
function probeAccess(path) {
|
|
20
|
+
try {
|
|
21
|
+
accessSync(path, constants.F_OK);
|
|
22
|
+
return 'valid';
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return isMissingPathError(err) ? 'broken' : 'inaccessible';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readResolvedSymlinkTarget(linkPath) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = readlinkSync(linkPath);
|
|
31
|
+
const target = resolveRawSymlinkTarget(linkPath, raw);
|
|
32
|
+
return { target, status: probeAccess(target) };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err?.code === 'EINVAL') {
|
|
36
|
+
return { target: null, status: 'valid' };
|
|
37
|
+
}
|
|
38
|
+
return { target: null, status: isMissingPathError(err) ? 'broken' : 'inaccessible' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function asOverall(status) {
|
|
42
|
+
return status === 'missing' ? 'broken' : status;
|
|
43
|
+
}
|
|
44
|
+
function worseStatus(current, next) {
|
|
45
|
+
const rank = { valid: 0, broken: 1, inaccessible: 2 };
|
|
46
|
+
return rank[next] > rank[current] ? next : current;
|
|
47
|
+
}
|
|
48
|
+
function emptyPath(path, status = 'valid') {
|
|
49
|
+
return { path, is_symlink: false, points_to: null, status };
|
|
50
|
+
}
|
|
51
|
+
/** True for a skills-dir child that is a real folder or a symlink slot. */
|
|
52
|
+
export function isSkillDirEntry(entry) {
|
|
53
|
+
return entry.isDirectory() || Boolean(entry.isSymbolicLink?.());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Inspect one agent skill slot: the folder (local vs symlink) and SKILL.md inside it.
|
|
57
|
+
*/
|
|
58
|
+
export function inspectSkillLink(skillDir, skillFilename = 'SKILL.md') {
|
|
59
|
+
const skillMdPath = join(skillDir, skillFilename);
|
|
60
|
+
const skill_folder = emptyPath(skillDir);
|
|
61
|
+
const skill_md = emptyPath(skillMdPath);
|
|
62
|
+
try {
|
|
63
|
+
const dirStat = lstatSync(skillDir);
|
|
64
|
+
if (dirStat.isSymbolicLink()) {
|
|
65
|
+
skill_folder.is_symlink = true;
|
|
66
|
+
const resolved = readResolvedSymlinkTarget(skillDir);
|
|
67
|
+
skill_folder.points_to = resolved.target;
|
|
68
|
+
skill_folder.status = resolved.status;
|
|
69
|
+
}
|
|
70
|
+
else if (!dirStat.isDirectory()) {
|
|
71
|
+
skill_folder.status = 'broken';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
skill_folder.status = isMissingPathError(err) ? 'broken' : 'inaccessible';
|
|
76
|
+
skill_md.status = 'missing';
|
|
77
|
+
return {
|
|
78
|
+
status: asOverall(skill_folder.status),
|
|
79
|
+
skill_folder,
|
|
80
|
+
skill_md,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const mdStat = lstatSync(skillMdPath);
|
|
85
|
+
if (mdStat.isSymbolicLink()) {
|
|
86
|
+
skill_md.is_symlink = true;
|
|
87
|
+
const resolved = readResolvedSymlinkTarget(skillMdPath);
|
|
88
|
+
skill_md.points_to = resolved.target;
|
|
89
|
+
skill_md.status = resolved.status;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
skill_md.status = isMissingPathError(err) ? 'missing' : 'inaccessible';
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
status: worseStatus(asOverall(skill_folder.status), asOverall(skill_md.status)),
|
|
97
|
+
skill_folder,
|
|
98
|
+
skill_md,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function symlinkEntry(info) {
|
|
102
|
+
return {
|
|
103
|
+
path: info.path,
|
|
104
|
+
points_to: info.points_to,
|
|
105
|
+
status: info.status === 'missing' ? 'broken' : info.status,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Only include the path that is actually a symlink. No symlink → `{ symlink_enabled: false }`. */
|
|
109
|
+
export function skillLinkPayload(info) {
|
|
110
|
+
const symlink_info = { symlink_enabled: false };
|
|
111
|
+
if (info.skill_folder.is_symlink) {
|
|
112
|
+
symlink_info.symlink_enabled = true;
|
|
113
|
+
symlink_info.skill_folder = symlinkEntry(info.skill_folder);
|
|
114
|
+
}
|
|
115
|
+
if (info.skill_md.is_symlink) {
|
|
116
|
+
symlink_info.symlink_enabled = true;
|
|
117
|
+
symlink_info.skill_md = symlinkEntry(info.skill_md);
|
|
118
|
+
}
|
|
119
|
+
return symlink_info;
|
|
120
|
+
}
|
|
121
|
+
export function symlinkInfoAllowsExtraFiles(info) {
|
|
122
|
+
if (!info || typeof info !== 'object')
|
|
123
|
+
return true;
|
|
124
|
+
const payload = info;
|
|
125
|
+
if (!payload.symlink_enabled)
|
|
126
|
+
return true;
|
|
127
|
+
for (const entry of [payload.skill_folder, payload.skill_md]) {
|
|
128
|
+
if (entry?.status && entry.status !== 'valid')
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function shouldEmitSkillSlot(info, skillFilename) {
|
|
134
|
+
if (info.skill_folder.is_symlink)
|
|
135
|
+
return true;
|
|
136
|
+
if (info.status === 'inaccessible')
|
|
137
|
+
return true;
|
|
138
|
+
if (info.skill_md.is_symlink)
|
|
139
|
+
return true;
|
|
140
|
+
try {
|
|
141
|
+
accessSync(join(info.skill_folder.path, skillFilename), constants.F_OK);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function unknownInspectFailure(skillDir, skillFilename, err) {
|
|
149
|
+
const status = isMissingPathError(err) ? 'broken' : 'inaccessible';
|
|
150
|
+
return {
|
|
151
|
+
status,
|
|
152
|
+
skill_folder: emptyPath(skillDir, status),
|
|
153
|
+
skill_md: emptyPath(join(skillDir, skillFilename), 'missing'),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Collect one SKILL.md inventory row, including symlink metadata even when the
|
|
158
|
+
* file is missing, dangling, or unreadable.
|
|
159
|
+
*/
|
|
160
|
+
export function collectSkillMdRecord(skillDir, fileType, source, skillFilename = 'SKILL.md', extraRaw = {}) {
|
|
161
|
+
let info;
|
|
162
|
+
try {
|
|
163
|
+
info = inspectSkillLink(skillDir, skillFilename);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
info = unknownInspectFailure(skillDir, skillFilename, err);
|
|
167
|
+
}
|
|
168
|
+
if (!shouldEmitSkillSlot(info, skillFilename))
|
|
169
|
+
return null;
|
|
170
|
+
const mdPath = join(skillDir, skillFilename);
|
|
171
|
+
const raw = {
|
|
172
|
+
source,
|
|
173
|
+
...extraRaw,
|
|
174
|
+
};
|
|
175
|
+
const symlink_info = skillLinkPayload(info);
|
|
176
|
+
if (info.status === 'valid') {
|
|
177
|
+
const content = readMarkdownFile(mdPath);
|
|
178
|
+
if (content !== null) {
|
|
179
|
+
raw.content = content;
|
|
180
|
+
}
|
|
181
|
+
else if (symlink_info.symlink_enabled) {
|
|
182
|
+
const skill_md = symlink_info.skill_md;
|
|
183
|
+
const skill_folder = symlink_info.skill_folder;
|
|
184
|
+
if (skill_md)
|
|
185
|
+
skill_md.status = 'inaccessible';
|
|
186
|
+
else if (skill_folder)
|
|
187
|
+
skill_folder.status = 'inaccessible';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { file_type: fileType, file_path: mdPath, raw_content: raw, symlink_info };
|
|
191
|
+
}
|
|
@@ -89,6 +89,12 @@ function targetsCurrentAgent(entry, agent) {
|
|
|
89
89
|
complianceRunnerDiag(`Ignoring remediation with unknown target_agent: ${t}`);
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
|
+
// Claude Desktop cannot run hooks, so nothing ever executes with --agent=claude_desktop and
|
|
93
|
+
// these rows would otherwise never be applied by anyone. The Claude Code hook owns them: it
|
|
94
|
+
// writes Desktop's own config file and the spec's restart_command relaunches the app
|
|
95
|
+
// (trusted_restarts already maps claude_desktop -> claude for exactly this reason).
|
|
96
|
+
if (normalized === 'claude_desktop' && agent === 'claude')
|
|
97
|
+
return true;
|
|
92
98
|
return normalized === agent;
|
|
93
99
|
}
|
|
94
100
|
// ---------------------------------------------------------------------------
|
|
@@ -361,6 +367,28 @@ function violationFromCheck(entry, compliance, check, expected) {
|
|
|
361
367
|
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Apply remediation ops for ${check.setting_path} in ${entry.config_file_path}`,
|
|
362
368
|
};
|
|
363
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* True when a secret-scan hit sits at (or under) a path this remediation has ops for.
|
|
372
|
+
*
|
|
373
|
+
* The scan reads the whole file, but the entry only carries ops for its own checks. Blocking on
|
|
374
|
+
* any other hit pins the row non-compliant forever -- the gate re-prompts on every turn with a fix
|
|
375
|
+
* that cannot resolve it (e.g. Claude Code's own 64-hex `machineID` matching a provider pattern).
|
|
376
|
+
*/
|
|
377
|
+
function secretFindingIsOwnedByEntry(findingPath, entry, checks) {
|
|
378
|
+
return checks.some((check) => {
|
|
379
|
+
const target = canonicalComplianceSettingPath(entry.config_file_path, check);
|
|
380
|
+
if (!target)
|
|
381
|
+
return false;
|
|
382
|
+
if (target.includes('*')) {
|
|
383
|
+
const pattern = target
|
|
384
|
+
.split('.')
|
|
385
|
+
.map((seg) => (seg === '*' ? '[^.]+' : seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
|
386
|
+
.join('\\.');
|
|
387
|
+
return new RegExp(`^${pattern}(\\.|$)`).test(findingPath);
|
|
388
|
+
}
|
|
389
|
+
return findingPath === target || findingPath.startsWith(`${target}.`);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
364
392
|
/** Evaluate one manifest row against on-disk config (used by gate + post-restart verify). */
|
|
365
393
|
export function evaluateManifestEntryCompliance(entry) {
|
|
366
394
|
const compliance = entry.fix ?? entry.compliance;
|
|
@@ -370,11 +398,15 @@ export function evaluateManifestEntryCompliance(entry) {
|
|
|
370
398
|
if (checks.length === 0)
|
|
371
399
|
return { violations: [] };
|
|
372
400
|
const loaded = loadRemediationConfigJson(entry.config_file_path, checks.map((c) => c.setting_path));
|
|
373
|
-
if (!loaded.ok)
|
|
374
|
-
|
|
401
|
+
if (!loaded.ok) {
|
|
402
|
+
// A config we cannot read is not a config we can call compliant. file_not_found is genuinely
|
|
403
|
+
// compliant (the config is gone); any other reason means the file is there and we simply could
|
|
404
|
+
// not parse it, so callers must not treat the empty violation list as "verified".
|
|
405
|
+
return { violations: [], unevaluable: loaded.reason !== 'file_not_found' };
|
|
406
|
+
}
|
|
375
407
|
const configJson = loaded.json;
|
|
376
408
|
if (compliance.requires_secret_scan === true) {
|
|
377
|
-
const secretFindings = scanJsonForHardcodedSecrets(configJson);
|
|
409
|
+
const secretFindings = scanJsonForHardcodedSecrets(configJson).filter((f) => secretFindingIsOwnedByEntry(f.path, entry, checks));
|
|
378
410
|
return secretFindings.length === 0
|
|
379
411
|
? { violations: [] }
|
|
380
412
|
: {
|
|
@@ -565,8 +597,12 @@ export function reportPostRestartVerificationOutcomes(violations) {
|
|
|
565
597
|
const entriesByUuid = new Map(remediations.map((entry) => [entry.uuid, entry]));
|
|
566
598
|
const outcomes = processPendingPostRestartVerifications((uuid) => {
|
|
567
599
|
const entry = entriesByUuid.get(uuid);
|
|
568
|
-
if (entry
|
|
569
|
-
|
|
600
|
+
if (entry) {
|
|
601
|
+
// Unreadable config counts as still-violating: never verify what could not be checked.
|
|
602
|
+
const { violations: entryViolations, unevaluable } = evaluateManifestEntryCompliance(entry);
|
|
603
|
+
if (unevaluable || entryViolations.length > 0)
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
570
606
|
return violations.some((v) => v.uuid === uuid);
|
|
571
607
|
});
|
|
572
608
|
const reportPromises = outcomes.map((o) => {
|
|
@@ -597,6 +633,35 @@ export async function runPostApplyVerification(agent = 'cursor') {
|
|
|
597
633
|
}
|
|
598
634
|
return outcomes;
|
|
599
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Re-read the remediated config so the server can verify the apply instead of trusting the report.
|
|
638
|
+
*
|
|
639
|
+
* Without a snapshot the server's post-apply re-check has nothing to evaluate and clears the
|
|
640
|
+
* linked findings on the endpoint's word alone -- the same blind close that let a fix that never
|
|
641
|
+
* happened be recorded as remediated. vscdb-backed paths are not JSON files, so they stay absent.
|
|
642
|
+
*/
|
|
643
|
+
function configSnapshotForReport(configFilePath) {
|
|
644
|
+
const diskPath = resolveRemediationConfigPath(configFilePath);
|
|
645
|
+
if (diskPath.includes('#'))
|
|
646
|
+
return undefined;
|
|
647
|
+
try {
|
|
648
|
+
const parsed = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
|
|
649
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
650
|
+
return parsed;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
/* unreadable: report without a snapshot rather than failing the report */
|
|
655
|
+
}
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
/** Report an apply outcome, attaching the on-disk config only when it can actually be read. */
|
|
659
|
+
function reportAutofixWithSnapshot(uuid, result, configFilePath) {
|
|
660
|
+
const snapshot = configSnapshotForReport(configFilePath);
|
|
661
|
+
return snapshot
|
|
662
|
+
? reportAutofixApplied(uuid, result, { config_snapshot_after: snapshot })
|
|
663
|
+
: reportAutofixApplied(uuid, result);
|
|
664
|
+
}
|
|
600
665
|
/**
|
|
601
666
|
* Immediate autofix succeeded (inline recheck OK or Claude stale-recheck tolerance).
|
|
602
667
|
* Clear pending verification locally and report verified so the next prompt does not POST
|
|
@@ -605,7 +670,7 @@ export async function runPostApplyVerification(agent = 'cursor') {
|
|
|
605
670
|
export function confirmAppliedAutofixVerified(appliedViolations, reportPromises) {
|
|
606
671
|
for (const v of appliedViolations) {
|
|
607
672
|
markRemediationApplyVerified(v.uuid);
|
|
608
|
-
reportPromises.push(
|
|
673
|
+
reportPromises.push(reportAutofixWithSnapshot(v.uuid, 'verified', v.config_file_path));
|
|
609
674
|
}
|
|
610
675
|
}
|
|
611
676
|
export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
@@ -682,7 +747,7 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
|
682
747
|
fixed++;
|
|
683
748
|
appliedViolations.push(violation);
|
|
684
749
|
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
685
|
-
reportPromises.push(
|
|
750
|
+
reportPromises.push(reportAutofixWithSnapshot(inst.uuid, 'success', inst.config_file_path));
|
|
686
751
|
// Every successful autofix (Cursor + Claude, restart or immediate JSON) awaits verification on
|
|
687
752
|
// the next compliance check so we can quarantine stuck applies and stop restart/retry loops.
|
|
688
753
|
markRemediationApplyPendingVerification(inst.uuid);
|
|
@@ -904,8 +969,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
|
904
969
|
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
905
970
|
const promises = [];
|
|
906
971
|
for (const entry of entries) {
|
|
907
|
-
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
908
|
-
if (violations.length > 0)
|
|
972
|
+
const { violations, unevaluable } = evaluateManifestEntryCompliance(entry);
|
|
973
|
+
if (violations.length > 0 || unevaluable)
|
|
909
974
|
continue;
|
|
910
975
|
const inst = entry;
|
|
911
976
|
const uploadFileType = resolveRemediationUploadFileType(entry.config_file_path, inst.file_type ?? undefined);
|
|
@@ -969,8 +1034,8 @@ export function reportCompliantRemediationVerifiedStatus(agent = 'cursor') {
|
|
|
969
1034
|
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
970
1035
|
const promises = [];
|
|
971
1036
|
for (const entry of entries) {
|
|
972
|
-
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
973
|
-
if (violations.length > 0)
|
|
1037
|
+
const { violations, unevaluable } = evaluateManifestEntryCompliance(entry);
|
|
1038
|
+
if (violations.length > 0 || unevaluable)
|
|
974
1039
|
continue;
|
|
975
1040
|
const tracking = readRemediationApplyTrackingFile();
|
|
976
1041
|
const prev = tracking.entries[entry.uuid];
|
|
@@ -982,7 +1047,13 @@ export function reportCompliantRemediationVerifiedStatus(agent = 'cursor') {
|
|
|
982
1047
|
const rawContent = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
|
|
983
1048
|
if (rawContent === null)
|
|
984
1049
|
continue;
|
|
985
|
-
promises.push(
|
|
1050
|
+
promises.push(
|
|
1051
|
+
// Nothing was applied on this path, so before === after. Sending both makes a no-op
|
|
1052
|
+
// "verified" distinguishable from a real apply in EnforcementLog.
|
|
1053
|
+
reportAutofixApplied(entry.uuid, 'verified', {
|
|
1054
|
+
config_snapshot_before: rawContent,
|
|
1055
|
+
config_snapshot_after: rawContent,
|
|
1056
|
+
}).then(() => {
|
|
986
1057
|
markRemediationApplyVerified(entry.uuid);
|
|
987
1058
|
hookRunLog(`compliance_check: reported verified (already compliant) uuid=${entry.uuid}`);
|
|
988
1059
|
}));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Org-pushed endpoint config. Prompt collection and later org options share this file. */
|
|
5
|
+
const ORG_CONFIG_RELATIVE_PATH = path.join(".optimuslabs", "management", "org", "config.json");
|
|
6
|
+
export function orgConfigPath() {
|
|
7
|
+
return path.join(os.homedir(), ORG_CONFIG_RELATIVE_PATH);
|
|
8
|
+
}
|
|
9
|
+
function readOrgConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(fs.readFileSync(orgConfigPath(), "utf8"));
|
|
12
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Missing or invalid — treat as empty.
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
function collectPromptsFromConfig(cfg) {
|
|
22
|
+
if (typeof cfg.collect_prompts === "boolean")
|
|
23
|
+
return cfg.collect_prompts;
|
|
24
|
+
// Previous on-disk key was inverted (true = hash / don't collect).
|
|
25
|
+
if (typeof cfg.hash_tool_call_prompts === "boolean")
|
|
26
|
+
return cfg.hash_tool_call_prompts !== true;
|
|
27
|
+
// No org config yet: match server default (collection off).
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
export function readCollectPrompts() {
|
|
31
|
+
return collectPromptsFromConfig(readOrgConfig());
|
|
32
|
+
}
|
|
33
|
+
export function persistCollectPrompts(value) {
|
|
34
|
+
if (typeof value !== "boolean")
|
|
35
|
+
return;
|
|
36
|
+
const filePath = orgConfigPath();
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
const next = { ...readOrgConfig(), collect_prompts: value };
|
|
40
|
+
delete next.hash_tool_call_prompts;
|
|
41
|
+
fs.writeFileSync(filePath, `${JSON.stringify(next)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Best-effort: next heartbeat can retry.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function persistCollectPromptsFromResponse(body) {
|
|
48
|
+
if (!body || typeof body !== "object")
|
|
49
|
+
return;
|
|
50
|
+
if (typeof body.collect_prompts === "boolean") {
|
|
51
|
+
persistCollectPrompts(body.collect_prompts);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof body.hash_tool_call_prompts === "boolean") {
|
|
55
|
+
persistCollectPrompts(body.hash_tool_call_prompts !== true);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -551,15 +551,26 @@ function assertSafeSqliteIdentifiersForItemTable(table, keyColumn, valueColumn)
|
|
|
551
551
|
const TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT && ' +
|
|
552
552
|
'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
|
|
553
553
|
'OPTIMUS_DEFERRED_LOG="${HOME}/.optimuslabs/management/vscdb/deferred_vscdb_restart.log" && mkdir -p "$(dirname "$OPTIMUS_DEFERRED_LOG")" && export OPTIMUS_DEFERRED_LOG && ' +
|
|
554
|
-
"nohup bash -c 'exec >>\"\$OPTIMUS_DEFERRED_LOG\" 2>&1; echo deferred_restart:begin ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ) REPO_ROOT=\"\$REPO_ROOT\" CURSOR_PROJECT=\"\$CURSOR_PROJECT\"; sleep 2; STATIC_DEV_ENV=\"\$HOME/.optimuslabs/management/optimus_dev.env\"; ENV_LABEL=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then ENV_LABEL=\$(grep -E \"^(environment|ENVIRONMENT|OPTIMUS_ENVIRONMENT)=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; if [ -z \"\$ENV_LABEL\" ] && [ -n \"\${OPTIMUS_ENVIRONMENT:-}\" ]; then ENV_LABEL=\$(printf \"%s\" \"\$OPTIMUS_ENVIRONMENT\" | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; [ -z \"\$ENV_LABEL\" ] && ENV_LABEL=\"production\"; NPX_BASE=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then _nb=\$(grep -E \"^npx=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | xargs); [ -n \"\$_nb\" ] && [ -d \"\$_nb\" ] && NPX_BASE=\"\$_nb\"; fi; p=\"\${npm_config_prefix:-\${NPM_CONFIG_PREFIX:-}}\"; gp=\"\${npm_config_global_prefix:-}\"; gc=\"\${npm_config_globalconfig:-}\"; if [[ \"\$p\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gp\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gc\" == *\"/Applications/Cursor.app/\"* ]]; then unset npm_config_prefix NPM_CONFIG_PREFIX npm_config_global_prefix npm_config_globalconfig npm_node_execpath 2>/dev/null; fi; APPLY_EC=0; if [ -f \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_dev_npx_packages; node \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif [ \"\$ENV_LABEL\" = \"development\" ] && [ -n \"\$NPX_BASE\" ] && [ -f \"\$NPX_BASE/optimuslabs/
|
|
554
|
+
"nohup bash -c 'exec >>\"\$OPTIMUS_DEFERRED_LOG\" 2>&1; echo deferred_restart:begin ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ) REPO_ROOT=\"\$REPO_ROOT\" CURSOR_PROJECT=\"\$CURSOR_PROJECT\"; sleep 2; STATIC_DEV_ENV=\"\$HOME/.optimuslabs/management/optimus_dev.env\"; ENV_LABEL=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then ENV_LABEL=\$(grep -E \"^(environment|ENVIRONMENT|OPTIMUS_ENVIRONMENT)=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; if [ -z \"\$ENV_LABEL\" ] && [ -n \"\${OPTIMUS_ENVIRONMENT:-}\" ]; then ENV_LABEL=\$(printf \"%s\" \"\$OPTIMUS_ENVIRONMENT\" | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; [ -z \"\$ENV_LABEL\" ] && ENV_LABEL=\"production\"; NPX_BASE=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then _nb=\$(grep -E \"^npx=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | xargs); [ -n \"\$_nb\" ] && [ -d \"\$_nb\" ] && NPX_BASE=\"\$_nb\"; fi; p=\"\${npm_config_prefix:-\${NPM_CONFIG_PREFIX:-}}\"; gp=\"\${npm_config_global_prefix:-}\"; gc=\"\${npm_config_globalconfig:-}\"; if [[ \"\$p\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gp\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gc\" == *\"/Applications/Cursor.app/\"* ]]; then unset npm_config_prefix NPM_CONFIG_PREFIX npm_config_global_prefix npm_config_globalconfig npm_node_execpath 2>/dev/null; fi; APPLY_EC=0; if [ -f \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_dev_npx_packages; node \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif [ \"\$ENV_LABEL\" = \"development\" ] && [ -n \"\$NPX_BASE\" ] && [ -f \"\$NPX_BASE/optimuslabs/harness-map/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_optimus_npx_base; node \"\$NPX_BASE/optimuslabs/harness-map/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif command -v npx >/dev/null 2>&1; then echo deferred_restart:apply_via_npx env=\"\$ENV_LABEL\"; cd \"\$REPO_ROOT\" || true; if [ \"\$ENV_LABEL\" = \"staging\" ]; then npx --yes --package=@optimuslabs/harness-map-staging@latest apply-deferred-vscdb-staging; APPLY_EC=\$?; else npx --yes --package=@optimuslabs/harness-map@latest apply-deferred-vscdb; APPLY_EC=\$?; fi; else echo deferred_restart:no_npx; APPLY_EC=127; fi; echo deferred_restart:apply_exit=\$APPLY_EC; if [ \$APPLY_EC -ne 0 ]; then echo deferred_restart:APPLY_FAILED_see_messages_above; fi; echo deferred_restart:open_cursor; env -u npm_config_package -u npm_lifecycle_event -u npm_lifecycle_script -u npm_config_local_prefix open -a Cursor \"\$CURSOR_PROJECT\"; echo deferred_restart:open_exit=\$?; echo deferred_restart:end ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ)' >/dev/null 2>&1 & killall -9 Cursor";
|
|
555
555
|
/** Legacy manifests; hooks may run with cwd under `.cursor` where `pwd` is wrong. */
|
|
556
556
|
const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY = 'CURSOR_PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export CURSOR_PROJECT && nohup bash -c \'sleep 2 && open -a Cursor "$CURSOR_PROJECT"\' >/dev/null 2>&1 & killall -9 Cursor';
|
|
557
557
|
const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT && ' +
|
|
558
558
|
'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
|
|
559
559
|
"nohup bash -c 'sleep 2 && open -a Cursor \"$CURSOR_PROJECT\"' >/dev/null 2>&1 & killall -9 Cursor";
|
|
560
|
-
|
|
560
|
+
/** Pre-wait-loop command; still accepted so an older manifest keeps restarting on a new client. */
|
|
561
|
+
const TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
|
|
562
|
+
/**
|
|
563
|
+
* Relaunch Claude Desktop once it has actually exited.
|
|
564
|
+
*
|
|
565
|
+
* The legacy command killed the app and relaunched after a flat 2s. A quit that takes longer (state
|
|
566
|
+
* save) meant `open -a Claude` landed while the app was still terminating, the launch was coalesced
|
|
567
|
+
* into the dying instance, and Desktop never came back. Poll for exit instead, bounded at ~20s.
|
|
568
|
+
* executeTrustedRestartCommands already spawns this detached, so it runs inline (no nohup/&).
|
|
569
|
+
*/
|
|
570
|
+
const TRUSTED_CLAUDE_RESTART_COMMAND = "pkill -x 'Claude'; for i in $(seq 1 80); do pgrep -x 'Claude' >/dev/null || break; sleep 0.25; done; open -a Claude";
|
|
561
571
|
export function isClaudeRestartCommand(cmd) {
|
|
562
|
-
|
|
572
|
+
const t = cmd.trim();
|
|
573
|
+
return t === TRUSTED_CLAUDE_RESTART_COMMAND || t === TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY;
|
|
563
574
|
}
|
|
564
575
|
export function isCursorRestartCommand(cmd) {
|
|
565
576
|
const t = cmd.trim();
|
|
@@ -579,7 +590,8 @@ export function isTrustedRestartCommandForAutofix(cmd) {
|
|
|
579
590
|
return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
|
|
580
591
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
|
|
581
592
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY ||
|
|
582
|
-
t === TRUSTED_CLAUDE_RESTART_COMMAND
|
|
593
|
+
t === TRUSTED_CLAUDE_RESTART_COMMAND ||
|
|
594
|
+
t === TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY);
|
|
583
595
|
}
|
|
584
596
|
/** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
|
|
585
597
|
function cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp) {
|
|
@@ -1537,6 +1549,9 @@ export function reportAutofixApplied(remediationUuid, result, details) {
|
|
|
1537
1549
|
if (details?.config_snapshot_after && typeof details.config_snapshot_after === 'object') {
|
|
1538
1550
|
bodyPayload.config_snapshot_after = details.config_snapshot_after;
|
|
1539
1551
|
}
|
|
1552
|
+
if (details?.config_snapshot_before && typeof details.config_snapshot_before === 'object') {
|
|
1553
|
+
bodyPayload.config_snapshot_before = details.config_snapshot_before;
|
|
1554
|
+
}
|
|
1540
1555
|
const signature = createSignature(payload, authKey.key);
|
|
1541
1556
|
const body = JSON.stringify({ ...bodyPayload, signature });
|
|
1542
1557
|
return executeBody(url, 'POST', body, 8000)
|
|
@@ -38,10 +38,12 @@ const KNOWN_SECRET_PATTERNS = [
|
|
|
38
38
|
{ re: /ASIA[0-9A-Z]{16}/i, label: 'AWS temporary access key ID' },
|
|
39
39
|
{ re: /AIza[0-9A-Za-z_-]{35}/i, label: 'Google API key' },
|
|
40
40
|
{ re: /ya29\.[0-9A-Za-z_-]+/i, label: 'Google OAuth access token' },
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Anchored + case-sensitive: Twilio SIDs are uppercase AC/SK + exactly 32 hex. Unanchored
|
|
42
|
+
// /AC[a-z0-9]{32}/i matched any long hex string containing "ac" (e.g. a 64-hex machine id).
|
|
43
|
+
{ re: /\bAC[a-f0-9]{32}\b/, label: 'Twilio account SID' },
|
|
44
|
+
{ re: /\bSK[a-f0-9]{32}\b/, label: 'Twilio API key' },
|
|
43
45
|
{ re: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/i, label: 'SendGrid API key' },
|
|
44
|
-
{ re:
|
|
46
|
+
{ re: /\bkey-[a-f0-9]{32}\b/i, label: 'Mailgun API key' },
|
|
45
47
|
{ re: /npm_[a-zA-Z0-9]{36}/i, label: 'npm access token' },
|
|
46
48
|
{ re: /pypi-[a-zA-Z0-9_-]{50,}/i, label: 'PyPI API token' },
|
|
47
49
|
{ re: /discord(?:app)?\.com\/api\/webhooks\/\d+\/[a-zA-Z0-9_-]+/i, label: 'Discord webhook URL' },
|
|
@@ -57,7 +59,9 @@ const KNOWN_SECRET_PATTERNS = [
|
|
|
57
59
|
{ re: /Bearer\s+[a-zA-Z0-9\-_.]{20,}/i, label: 'Bearer token' },
|
|
58
60
|
];
|
|
59
61
|
const BASIC_AUTH_IN_URL = /[a-zA-Z0-9._%+-]+:[a-zA-Z0-9._%+-]+@/gi;
|
|
60
|
-
|
|
62
|
+
// Hyphens/underscores count as part of one token so hyphenated keys (ctx7sk-..., sk-ant-...)
|
|
63
|
+
// are measured as a single candidate instead of segment-by-segment (matches HardcodedSecretRule).
|
|
64
|
+
const GENERIC_TOKEN = /\b[a-zA-Z0-9][a-zA-Z0-9_-]{31,}\b/g;
|
|
61
65
|
const GENERIC_KEY_HINT = /(?:api[_-]?key|app[_-]?key|secret|token|password|passwd|pass\b|(?<!o)auth|credential|private[_-]?key|access[_-]?key|master[_-]?key)/i;
|
|
62
66
|
function basicAuthMatchIsCredential(value, matchIndex) {
|
|
63
67
|
const before = value.slice(0, matchIndex);
|
|
@@ -70,6 +74,20 @@ function basicAuthMatchIsCredential(value, matchIndex) {
|
|
|
70
74
|
function looksLikeUrlContext(value) {
|
|
71
75
|
return value.includes(':') && (value.toLowerCase().includes('http') || value.includes('://'));
|
|
72
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Values to run GENERIC_TOKEN against. A URL's host/path is skipped (high false-positive rate),
|
|
79
|
+
* but query-string values are scanned — MCP tokens commonly live there. Mirrors
|
|
80
|
+
* HardcodedSecretRule._generic_token_scan_targets.
|
|
81
|
+
*/
|
|
82
|
+
function genericTokenScanTargets(value) {
|
|
83
|
+
if (!looksLikeUrlContext(value))
|
|
84
|
+
return [value];
|
|
85
|
+
const queryStart = value.indexOf('?');
|
|
86
|
+
if (queryStart === -1)
|
|
87
|
+
return [];
|
|
88
|
+
const query = value.slice(queryStart + 1).split('#')[0] ?? '';
|
|
89
|
+
return [...new URLSearchParams(query).values()];
|
|
90
|
+
}
|
|
73
91
|
/**
|
|
74
92
|
* Scan one scalar config value. Returns secret type label or null if clean / env-backed.
|
|
75
93
|
*/
|
|
@@ -89,12 +107,14 @@ export function scanScalarForHardcodedSecret(value, keyName) {
|
|
|
89
107
|
return 'Basic authentication credentials';
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
|
-
if (!GENERIC_KEY_HINT.test(keyName)
|
|
110
|
+
if (!GENERIC_KEY_HINT.test(keyName)) {
|
|
93
111
|
return null;
|
|
94
112
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
for (const target of genericTokenScanTargets(valueStr)) {
|
|
114
|
+
const genericMatches = target.match(GENERIC_TOKEN) ?? [];
|
|
115
|
+
if (genericMatches.some((token) => token.length >= 40)) {
|
|
116
|
+
return 'Potential token/secret';
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
return null;
|
|
100
120
|
}
|
|
@@ -4,6 +4,7 @@ import { loadEndpointBase } from './endpoint_config.js';
|
|
|
4
4
|
import { hookRunLog } from '../runtime/hook_logger.js';
|
|
5
5
|
import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
|
|
6
6
|
import { resolveWorkspaceRepoFromEnv } from '../runtime/workspace_repo.js';
|
|
7
|
+
import { persistCollectPromptsFromResponse } from '../runtime/prompt-collection-policy.js';
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
@@ -79,11 +80,16 @@ function buildBatchChunks(configFiles, basePayloadSize) {
|
|
|
79
80
|
return chunks;
|
|
80
81
|
}
|
|
81
82
|
function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
|
|
82
|
-
const config_files = chunk.map((c) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
const config_files = chunk.map((c) => {
|
|
84
|
+
const item = {
|
|
85
|
+
file_type: c.file_type,
|
|
86
|
+
file_path: canonicalCursorUserStateVscdbPath(c.file_path),
|
|
87
|
+
raw_content: c.raw_content,
|
|
88
|
+
};
|
|
89
|
+
if (c.symlink_info !== undefined)
|
|
90
|
+
item.symlink_info = c.symlink_info;
|
|
91
|
+
return item;
|
|
92
|
+
});
|
|
87
93
|
const payload = { hardware_uuid: hardwareUuid, metadata, config_files };
|
|
88
94
|
if (hookRequestId != null)
|
|
89
95
|
payload.hook_request_id = hookRequestId;
|
|
@@ -127,6 +133,7 @@ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequ
|
|
|
127
133
|
const timeoutMs = Math.min(20000 + Math.ceil(bodySize / (1024 * 1024)) * 15000, 90000);
|
|
128
134
|
try {
|
|
129
135
|
const response = (await postStartupPayload(apiUrl, body, timeoutMs));
|
|
136
|
+
persistCollectPromptsFromResponse(response);
|
|
130
137
|
if (response.status === 'accepted') {
|
|
131
138
|
totals.accepted += typeof response.accepted === 'number' ? response.accepted : chunk.length;
|
|
132
139
|
const failedList = Array.isArray(response.failed) ? response.failed : [];
|
|
@@ -221,6 +228,8 @@ async function sendConfigFile(configFile, hardwareUuid, authKey, repoIdentifier)
|
|
|
221
228
|
const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
|
|
222
229
|
const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
|
|
223
230
|
const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
|
|
231
|
+
if (configFile.symlink_info !== undefined)
|
|
232
|
+
payload.symlink_info = configFile.symlink_info;
|
|
224
233
|
const signature = createSignature(payload, authKey.key);
|
|
225
234
|
const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', organization_uuid: readOrganizationUuid(), repo_identifier: repoIdentifier ?? resolveWorkspaceRepoFromEnv() } };
|
|
226
235
|
try {
|
|
@@ -5,6 +5,7 @@ import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
|
5
5
|
import { writeAuthKey, readStoredAuthKey, loadEndpointBase, buildStartupEndpointUrl } from './auth_key_store.js';
|
|
6
6
|
import { resolveUserProfile } from './user_profile.js';
|
|
7
7
|
import { resolveWorkspaceRepo } from '../log_config_files/runtime/workspace_repo.js';
|
|
8
|
+
import { persistCollectPromptsFromResponse } from '../log_config_files/runtime/prompt-collection-policy.js';
|
|
8
9
|
import fs from 'node:fs';
|
|
9
10
|
import os from 'node:os';
|
|
10
11
|
import path from 'node:path';
|
|
@@ -83,6 +84,7 @@ const maybeSendToEndpoint = async (hardwareUuid, timestamp, options = {}) => {
|
|
|
83
84
|
const requestBody = buildRequestBody(hardwareUuid, timestamp);
|
|
84
85
|
try {
|
|
85
86
|
const response = await postStartupPayload(startupEndpointUrl, requestBody);
|
|
87
|
+
persistCollectPromptsFromResponse(response);
|
|
86
88
|
const result = classifyEndpointResponse(response);
|
|
87
89
|
console.log(result.message);
|
|
88
90
|
if (result.branch === 'key_issued' && result.key) {
|
package/dist/tofu.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Reads ~/.optimuslabs/management/optimus_dev.env at startup:
|
|
5
5
|
* - No file / unreadable → staging (ignores OPTIMUS_ENVIRONMENT).
|
|
6
6
|
* - File present → environment= line in file, then OPTIMUS_ENVIRONMENT, then staging.
|
|
7
|
-
* - environment=development + local dist present → loads sibling
|
|
7
|
+
* - environment=development + local dist present → loads sibling tofu dist
|
|
8
8
|
* - staging / production / npx installs → published @optimuslabs/tofu-staging npm package
|
|
9
9
|
*
|
|
10
10
|
* All other source files import from this module, never directly from
|
|
@@ -14,7 +14,7 @@ import path from "path";
|
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
15
|
import { shouldUseLocalTofuDist } from "./tofu_environment.js";
|
|
16
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const localTofuPath = path.join(__dirname, "../../
|
|
17
|
+
const localTofuPath = path.join(__dirname, "../../tofu/dist/index.js");
|
|
18
18
|
const useLocalTofu = shouldUseLocalTofuDist(localTofuPath);
|
|
19
19
|
const tofu = (useLocalTofu
|
|
20
20
|
? await import(localTofuPath)
|
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.19",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
36
36
|
"url": "git+https://github.com/optimuslabs-io/optimus-secure-fdn.git",
|
|
37
|
-
"directory": "npx_packages/staging/optimuslabs/
|
|
37
|
+
"directory": "npx_packages/staging/optimuslabs/harness-map"
|
|
38
38
|
},
|
|
39
39
|
"bugs": {
|
|
40
40
|
"url": "https://github.com/optimuslabs-io/optimus-secure-fdn/issues"
|
|
41
41
|
},
|
|
42
|
-
"homepage": "https://github.com/optimuslabs-io/optimus-secure-fdn/tree/main/npx_packages/staging/optimuslabs/
|
|
42
|
+
"homepage": "https://github.com/optimuslabs-io/optimus-secure-fdn/tree/main/npx_packages/staging/optimuslabs/harness-map#readme",
|
|
43
43
|
"files": [
|
|
44
44
|
"dist/**/*",
|
|
45
45
|
"README.md"
|