@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.
@@ -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
- // Symlinks report isSymbolicLink(), not isFile(), so this also keeps the walk inside the skill dir.
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.isDirectory())) {
106
+ for (const d of readdirSync(skillsDir, { withFileTypes: true }).filter((d) => isSkillDirEntry(d))) {
104
107
  const skillRoot = join(skillsDir, d.name);
105
- if (!existsSync(join(skillRoot, skillFilename)))
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: 0, bytes: 0 };
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
- return { violations: [] };
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 && collectManifestEntryViolations(entry).length > 0)
569
- return true;
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(reportAutofixApplied(v.uuid, 'verified'));
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(reportAutofixApplied(inst.uuid, 'success'));
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(reportAutofixApplied(entry.uuid, 'verified', { config_snapshot_after: rawContent }).then(() => {
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/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_optimus_npx_base; node \"\$NPX_BASE/optimuslabs/log-llm-config/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";
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
- const TRUSTED_CLAUDE_RESTART_COMMAND = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
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
- return cmd.trim() === TRUSTED_CLAUDE_RESTART_COMMAND;
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
- { re: /AC[a-z0-9]{32}/i, label: 'Twilio account SID' },
42
- { re: /SK[a-z0-9]{32}/i, label: 'Twilio API key' },
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: /key-[a-f0-9]{32}/i, label: 'Mailgun API key' },
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
- const GENERIC_TOKEN = /\b[a-zA-Z0-9]{32,}\b/g;
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) || looksLikeUrlContext(valueStr)) {
110
+ if (!GENERIC_KEY_HINT.test(keyName)) {
93
111
  return null;
94
112
  }
95
- const genericMatches = valueStr.match(GENERIC_TOKEN) ?? [];
96
- if (genericMatches.some((token) => token.length >= 40)) {
97
- return 'Potential token/secret';
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
- file_type: c.file_type,
84
- file_path: canonicalCursorUserStateVscdbPath(c.file_path),
85
- raw_content: c.raw_content,
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 optimus-tofu dist
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, "../../optimus-tofu/dist/index.js");
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.16",
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/log-llm-config"
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/log-llm-config#readme",
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"