@trylucy/agent-memory 0.0.0 → 0.1.3
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/LICENSE +202 -0
- package/README.md +205 -2
- package/contracts/agent-manifest.json +27 -0
- package/contracts/agent-memory-api.contract.json +200 -0
- package/contracts/hashes.json +23 -0
- package/package.json +83 -5
- package/schemas/canonical-recall/response.schema.json +56 -0
- package/schemas/v1/common.schema.json +191 -0
- package/schemas/v1/contract-document.schema.json +278 -0
- package/schemas/v1/create-proposal-request.schema.json +42 -0
- package/schemas/v1/error.schema.json +90 -0
- package/schemas/v1/memory-version-state.schema.json +39 -0
- package/schemas/v1/memory.schema.json +47 -0
- package/schemas/v1/proposal-status.schema.json +277 -0
- package/schemas/v1/public-surface.schema.json +26 -0
- package/schemas/v1/recall-request.schema.json +45 -0
- package/schemas/v1/recall-response.schema.json +60 -0
- package/schemas/v1/receipt.schema.json +67 -0
- package/schemas/v1/replace-proposal-request.schema.json +46 -0
- package/schemas/v1/replace-transition.schema.json +19 -0
- package/schemas/v1/scope.schema.json +10 -0
- package/scripts/lib/am01.mjs +159 -0
- package/src/client/canonical-validation.mjs +37 -0
- package/src/client/client.mjs +151 -0
- package/src/client/errors.mjs +101 -0
- package/src/client/http-transport.d.ts +12 -0
- package/src/client/http-transport.mjs +212 -0
- package/src/client/index.d.ts +243 -0
- package/src/client/index.mjs +38 -0
- package/src/client/serialization.mjs +53 -0
- package/src/client/validation.mjs +174 -0
- package/src/clients/claude-desktop.json +13 -0
- package/src/clients/claude.json +13 -0
- package/src/clients/codex.toml +8 -0
- package/src/clients/cursor.json +12 -0
- package/src/clients/generic.mcp.json +12 -0
- package/src/clients/openclaw.json +14 -0
- package/src/clients/vscode.json +13 -0
- package/src/integrations/hermes-advisory-lock.mjs +140 -0
- package/src/integrations/hermes-artifact.mjs +551 -0
- package/src/integrations/hermes-filesystem.mjs +112 -0
- package/src/integrations/hermes.d.ts +106 -0
- package/src/integrations/hermes.mjs +425 -0
- package/src/mcp/bin.mjs +4 -0
- package/src/mcp/config.mjs +60 -0
- package/src/mcp/index.d.ts +27 -0
- package/src/mcp/lifecycle.mjs +50 -0
- package/src/mcp/schemas.mjs +106 -0
- package/src/mcp/server.mjs +163 -0
- package/src/mcp/stdio.d.ts +28 -0
- package/src/mcp/stdio.mjs +59 -0
- package/src/onboarding/usage.mjs +44 -0
- package/src/projection/memory-md.mjs +319 -0
- package/src/provider/index.d.ts +9 -0
- package/src/provider/index.mjs +1 -0
- package/src/provider/memory-first.mjs +95 -0
- package/src/routing/destination.mjs +42 -0
- package/src/safety/secrets.mjs +31 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { lstatSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const EFFECTIVE_UID = typeof process.geteuid === 'function' ? process.geteuid() : null;
|
|
5
|
+
const SYSTEM_UID = lstatSync(sep).uid;
|
|
6
|
+
|
|
7
|
+
function isStickySystemDirectory(stat) {
|
|
8
|
+
return stat.uid === SYSTEM_UID && (stat.mode & 0o1000) !== 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertSafeDirectory(stat, path, label, { requireEffectiveOwner = false, requirePrivate = false } = {}) {
|
|
12
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not traverse a symbolic link.`);
|
|
13
|
+
if (!stat.isDirectory()) throw new Error(`${label} must be a directory.`);
|
|
14
|
+
if (EFFECTIVE_UID !== null) {
|
|
15
|
+
if (requireEffectiveOwner && stat.uid !== EFFECTIVE_UID) throw new Error(`${label} must be owned by the effective user.`);
|
|
16
|
+
if (!requireEffectiveOwner && stat.uid !== EFFECTIVE_UID && stat.uid !== SYSTEM_UID) {
|
|
17
|
+
throw new Error(`${label} has an untrusted owner at ${path}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if ((stat.mode & 0o022) !== 0 && !isStickySystemDirectory(stat)) {
|
|
21
|
+
throw new Error(`${label} has a group- or world-writable ancestor at ${path}.`);
|
|
22
|
+
}
|
|
23
|
+
if (requirePrivate && (stat.mode & 0o077) !== 0) throw new Error(`${label} must already be private.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertEffectiveOwner(stat, label) {
|
|
27
|
+
if (EFFECTIVE_UID !== null && stat.uid !== EFFECTIVE_UID) throw new Error(`${label} must be owned by the effective user.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function assertTrustedOwner(stat, label) {
|
|
31
|
+
if (EFFECTIVE_UID !== null && stat.uid !== EFFECTIVE_UID && stat.uid !== SYSTEM_UID) {
|
|
32
|
+
throw new Error(`${label} must be owned by the effective user or the trusted system owner.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate every existing directory from the filesystem root to `path`.
|
|
38
|
+
* System-owned sticky temporary roots are allowed; other writable or
|
|
39
|
+
* unexpected-owner ancestors are rejected.
|
|
40
|
+
*/
|
|
41
|
+
export function validateSafeDirectoryChain(path, label, { allowMissing = false, requireLeafOwner = false, requirePrivateLeaf = false } = {}) {
|
|
42
|
+
const absolute = assertAbsolute(path, label);
|
|
43
|
+
const parts = absolute.split(sep).filter(Boolean);
|
|
44
|
+
let current = sep;
|
|
45
|
+
const rootStat = lstatSync(current);
|
|
46
|
+
assertSafeDirectory(rootStat, current, label);
|
|
47
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
48
|
+
current = join(current, parts[index]);
|
|
49
|
+
let stat;
|
|
50
|
+
try { stat = lstatSync(current); } catch (error) {
|
|
51
|
+
if (allowMissing && error?.code === 'ENOENT') return false;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const leaf = index === parts.length - 1;
|
|
55
|
+
assertSafeDirectory(stat, current, label, {
|
|
56
|
+
requireEffectiveOwner: leaf && requireLeafOwner,
|
|
57
|
+
requirePrivate: leaf && requirePrivateLeaf,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assertAbsolute(path, label) {
|
|
64
|
+
if (typeof path !== 'string' || !isAbsolute(path)) throw new Error(`${label} must be an absolute path.`);
|
|
65
|
+
return resolve(path);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function assertInside(path, parent, label) {
|
|
69
|
+
const value = relative(parent, path);
|
|
70
|
+
if (value === '..' || value.startsWith(`..${sep}`) || isAbsolute(value)) throw new Error(`${label} must stay inside the isolated profile.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function directoryChain(path, label, { allowMissing = false } = {}) {
|
|
74
|
+
return validateSafeDirectoryChain(path, label, { allowMissing });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Verify every existing parent component without chmod or mkdir side effects. */
|
|
78
|
+
export function validateExistingParent(path, label, { allowMissing = false } = {}) {
|
|
79
|
+
const absolute = assertAbsolute(path, label);
|
|
80
|
+
return directoryChain(dirname(absolute), `${label} parent`, { allowMissing });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Verify an existing contained parent chain without chmod or mkdir side effects. */
|
|
84
|
+
export function validateContainedParent(path, profilePath, label) {
|
|
85
|
+
const profile = assertAbsolute(profilePath, 'profilePath');
|
|
86
|
+
const absolute = assertAbsolute(path, label);
|
|
87
|
+
assertInside(absolute, profile, label);
|
|
88
|
+
directoryChain(profile, 'Hermes profile');
|
|
89
|
+
const parent = dirname(absolute);
|
|
90
|
+
assertInside(parent, profile, `${label} parent`);
|
|
91
|
+
const tail = relative(profile, parent);
|
|
92
|
+
let current = profile;
|
|
93
|
+
for (const part of tail ? tail.split(sep) : []) {
|
|
94
|
+
current = join(current, part);
|
|
95
|
+
const stat = lstatSync(current);
|
|
96
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} parent must not traverse a symbolic link.`);
|
|
97
|
+
if (!stat.isDirectory()) throw new Error(`${label} parent must be a directory.`);
|
|
98
|
+
assertEffectiveOwner(stat, `${label} parent`);
|
|
99
|
+
if ((stat.mode & 0o022) !== 0) throw new Error(`${label} parent must not be group- or world-writable.`);
|
|
100
|
+
}
|
|
101
|
+
return absolute;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Verify an existing contained directory and all of its parents without writes. */
|
|
105
|
+
export function validateContainedDirectory(path, profilePath, label) {
|
|
106
|
+
const absolute = validateContainedParent(path, profilePath, label);
|
|
107
|
+
const stat = lstatSync(absolute);
|
|
108
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not be a symbolic link.`);
|
|
109
|
+
if (!stat.isDirectory()) throw new Error(`${label} must be a directory.`);
|
|
110
|
+
assertEffectiveOwner(stat, label);
|
|
111
|
+
return absolute;
|
|
112
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export interface HermesLucyArtifactOptions {
|
|
2
|
+
sourcePath: string;
|
|
3
|
+
nodePath: string;
|
|
4
|
+
/** Optional explicit absolute npm-cli.js selected for the offline lockfile build. */
|
|
5
|
+
npmPath?: string;
|
|
6
|
+
version: string;
|
|
7
|
+
/** Externally obtained full Git commit ID that this local checkout must equal. */
|
|
8
|
+
expectedSourceGitSha: string;
|
|
9
|
+
compatibility?: { hermes?: string };
|
|
10
|
+
/** Required alongside compatibility.hermes whenever a matching private artifact cache is reused. */
|
|
11
|
+
trustedHermesVersion?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Node runtime compatibility verified while the artifact was built. */
|
|
15
|
+
export interface HermesLucyArtifactNodeCompatibility {
|
|
16
|
+
required_range: string;
|
|
17
|
+
verified_version: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Optional Hermes host compatibility verified while the artifact was built. */
|
|
21
|
+
export interface HermesLucyArtifactHermesCompatibility {
|
|
22
|
+
required_range: string;
|
|
23
|
+
verified_host_version: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Compatibility data emitted in the build manifest and artifact receipt. */
|
|
27
|
+
export interface HermesLucyArtifactCompatibility {
|
|
28
|
+
node: HermesLucyArtifactNodeCompatibility;
|
|
29
|
+
hermes?: HermesLucyArtifactHermesCompatibility;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Dependency bytes provenance emitted in the build manifest and artifact receipt. */
|
|
33
|
+
export interface HermesLucyArtifactDependencyProvenance {
|
|
34
|
+
type: 'npm_ci_offline_lockfile';
|
|
35
|
+
lockfile: 'package-lock.json';
|
|
36
|
+
lockfile_sha256: string;
|
|
37
|
+
closure_digest: string;
|
|
38
|
+
npm_cli_version: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HermesLucyArtifactReceipt {
|
|
42
|
+
format: 2;
|
|
43
|
+
version: string;
|
|
44
|
+
path: string;
|
|
45
|
+
entrypoint: string;
|
|
46
|
+
node_path: string;
|
|
47
|
+
artifact_digest: string;
|
|
48
|
+
source_git_sha: string;
|
|
49
|
+
package_name: string;
|
|
50
|
+
package_version: string;
|
|
51
|
+
source_repository: string;
|
|
52
|
+
source_git_closure_digest: string;
|
|
53
|
+
dependency_provenance: HermesLucyArtifactDependencyProvenance;
|
|
54
|
+
compatibility: HermesLucyArtifactCompatibility;
|
|
55
|
+
created_at: string;
|
|
56
|
+
/** Absolute, isolated-profile path of the private receipt file. */
|
|
57
|
+
receiptPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface HermesLucyInstallOptions {
|
|
61
|
+
profilePath: string;
|
|
62
|
+
artifact: HermesLucyArtifactOptions;
|
|
63
|
+
baseUrl: string;
|
|
64
|
+
tokenEnv: string;
|
|
65
|
+
/** Raw tokens are deliberately rejected; use tokenEnv. */
|
|
66
|
+
token?: never;
|
|
67
|
+
/** Exactly memory:read and memory:propose, in that order. */
|
|
68
|
+
permissions?: ['memory:read', 'memory:propose'];
|
|
69
|
+
allowInsecureLoopback?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HermesLucyMcpConfig {
|
|
73
|
+
command: string;
|
|
74
|
+
args: [string];
|
|
75
|
+
env: Record<string, string>;
|
|
76
|
+
x_lucy_memory_artifact: {
|
|
77
|
+
version: string;
|
|
78
|
+
artifact_digest: string;
|
|
79
|
+
source_git_sha: string;
|
|
80
|
+
package_name: string;
|
|
81
|
+
package_version: string;
|
|
82
|
+
source_repository: string;
|
|
83
|
+
compatibility: HermesLucyArtifactCompatibility;
|
|
84
|
+
receipt_path: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type HermesLucyMcpConfigOptions = Omit<HermesLucyInstallOptions, 'artifact'> & { artifact: HermesLucyArtifactReceipt };
|
|
89
|
+
|
|
90
|
+
export function installHermesLucyArtifact(profilePath: string, options: HermesLucyArtifactOptions): HermesLucyArtifactReceipt;
|
|
91
|
+
export function lucyMcpConfig(options: HermesLucyMcpConfigOptions): HermesLucyMcpConfig;
|
|
92
|
+
export function installHermesLucyConfig(configPath: string, options: HermesLucyInstallOptions): { path: string; backup: string | null; artifact: HermesLucyArtifactReceipt };
|
|
93
|
+
export function updateHermesLucyConfig(configPath: string, options: HermesLucyInstallOptions): { path: string; backup: string | null; artifact: HermesLucyArtifactReceipt };
|
|
94
|
+
export function rollbackHermesLucyConfig(configPath: string, options: { profilePath: string; receiptPath: string; /** Trusted source file under the Git checkout used to bind rollback bytes. */ sourcePath: string; /** Full externally pinned Git SHA for sourcePath. */ expectedSourceGitSha: string; /** Explicit absolute Node executable selected from the independently trusted runtime distribution used for the rollback rebuild. */ trustedNodePath: string; /** Required when the receipt declares compatibility.hermes. */ trustedHermesVersion?: string }): { path: string; artifact: HermesLucyArtifactReceipt };
|
|
95
|
+
export function uninstallHermesLucyConfig(configPath: string, options: { profilePath: string }): { restored: false; removed?: boolean };
|
|
96
|
+
/** Removes only the private pre-install YAML backup; versioned artifact and receipt caches are retained. */
|
|
97
|
+
export function resetHermesLucyBackup(configPath: string, options: { profilePath: string }): void;
|
|
98
|
+
/** @deprecated Use resetHermesLucyBackup. This legacy alias removes only the private YAML backup; artifact and receipt caches are retained. */
|
|
99
|
+
export function resetHermesLucyArtifacts(configPath: string, options: { profilePath: string }): void;
|
|
100
|
+
export function hermesLucyStatus(configPath: string): {
|
|
101
|
+
installed: boolean;
|
|
102
|
+
hasToken?: boolean;
|
|
103
|
+
baseUrl?: string | null;
|
|
104
|
+
permissions?: string | null;
|
|
105
|
+
artifact?: string | null;
|
|
106
|
+
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
fstatSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
fchmodSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
linkSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
19
|
+
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
20
|
+
import { acquireHermesAdvisoryLock, assertHermesAdvisoryLockSupport } from './hermes-advisory-lock.mjs';
|
|
21
|
+
import {
|
|
22
|
+
assertAbsolute,
|
|
23
|
+
assertEffectiveOwner,
|
|
24
|
+
assertInside,
|
|
25
|
+
validateContainedParent,
|
|
26
|
+
validateExistingParent,
|
|
27
|
+
validateSafeDirectoryChain,
|
|
28
|
+
} from './hermes-filesystem.mjs';
|
|
29
|
+
import YAML from 'yaml';
|
|
30
|
+
import {
|
|
31
|
+
installHermesLucyArtifact as installClosedHermesLucyArtifact,
|
|
32
|
+
verifyHermesLucyArtifactAgainstSource,
|
|
33
|
+
} from './hermes-artifact.mjs';
|
|
34
|
+
|
|
35
|
+
const BACKUP_SUFFIX = '.lucy-memory.bak';
|
|
36
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
37
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
38
|
+
const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
39
|
+
const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
40
|
+
const VERSION = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
41
|
+
const REQUIRED_PERMISSIONS = ['memory:read', 'memory:propose'];
|
|
42
|
+
|
|
43
|
+
function assertSecureLocalWritesSupported() {
|
|
44
|
+
if (process.platform === 'win32') throw new Error('secure Hermes config writes are not supported on Windows');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertHermesInstallerMutationSupported() {
|
|
48
|
+
assertSecureLocalWritesSupported();
|
|
49
|
+
assertHermesAdvisoryLockSupport('Hermes installer');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function statRegularFile(path, label, { required = false } = {}) {
|
|
53
|
+
let stat;
|
|
54
|
+
try { stat = lstatSync(path); } catch (error) {
|
|
55
|
+
if (error?.code === 'ENOENT' && !required) return null;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not be a symbolic link.`);
|
|
59
|
+
if (!stat.isFile()) throw new Error(`${label} must be a regular file.`);
|
|
60
|
+
assertEffectiveOwner(stat, label);
|
|
61
|
+
if (stat.nlink !== 1) throw new Error(`${label} must not be hard linked.`);
|
|
62
|
+
return stat;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function secureDirectory(path, label) {
|
|
66
|
+
const absolute = assertAbsolute(path, label);
|
|
67
|
+
validateSafeDirectoryChain(dirname(absolute), `${label} parent`, { allowMissing: true });
|
|
68
|
+
const pieces = absolute.split(sep).filter(Boolean);
|
|
69
|
+
let current = sep;
|
|
70
|
+
for (const piece of pieces) {
|
|
71
|
+
current = join(current, piece);
|
|
72
|
+
try {
|
|
73
|
+
const stat = lstatSync(current);
|
|
74
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not traverse a symbolic link.`);
|
|
75
|
+
if (!stat.isDirectory()) throw new Error(`${label} must be a directory.`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
78
|
+
mkdirSync(current, { mode: PRIVATE_DIRECTORY_MODE });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
validateSafeDirectoryChain(absolute, label, { requireLeafOwner: true });
|
|
82
|
+
chmodSync(absolute, PRIVATE_DIRECTORY_MODE);
|
|
83
|
+
return absolute;
|
|
84
|
+
}
|
|
85
|
+
function isolatedProfile(path, { create = false } = {}) {
|
|
86
|
+
const absolute = assertAbsolute(path, 'profilePath');
|
|
87
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
88
|
+
if (!stat) {
|
|
89
|
+
if (!create) throw new Error('Hermes profile must exist.');
|
|
90
|
+
return secureDirectory(absolute, 'Hermes profile');
|
|
91
|
+
}
|
|
92
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('Hermes profile must be a directory without symbolic links.');
|
|
93
|
+
validateSafeDirectoryChain(absolute, 'Hermes profile', { requireLeafOwner: true });
|
|
94
|
+
assertEffectiveOwner(stat, 'Hermes profile');
|
|
95
|
+
if ((stat.mode & 0o077) !== 0) throw new Error('An existing Hermes profile must already be private; refusing to change its permissions.');
|
|
96
|
+
return absolute;
|
|
97
|
+
}
|
|
98
|
+
function assertConfigInside(path, profile) {
|
|
99
|
+
assertInside(path, profile, 'Hermes config');
|
|
100
|
+
const relativePath = relative(profile, path);
|
|
101
|
+
if (!relativePath || relativePath === '.lucy-memory' || relativePath.startsWith(`.lucy-memory${sep}`)) throw new Error('Hermes config must be a file inside the isolated profile and outside installer state.');
|
|
102
|
+
}
|
|
103
|
+
function existing(path) { return lstatSync(path, { throwIfNoEntry: false }); }
|
|
104
|
+
function acquireConfigLock(profile) {
|
|
105
|
+
const state = secureDirectory(join(profile, '.lucy-memory'), 'Hermes config state directory');
|
|
106
|
+
return acquireHermesAdvisoryLock(
|
|
107
|
+
state,
|
|
108
|
+
'.config.lock',
|
|
109
|
+
'Hermes config lock',
|
|
110
|
+
'Another Hermes config update is active for this isolated profile.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readRegularText(path, label) {
|
|
115
|
+
statRegularFile(path, label, { required: true });
|
|
116
|
+
const fd = openSync(path, constants.O_RDONLY | NOFOLLOW);
|
|
117
|
+
try {
|
|
118
|
+
if (!fstatSync(fd).isFile()) throw new Error(`${label} must be a regular file.`);
|
|
119
|
+
return readFileSync(fd, 'utf8');
|
|
120
|
+
} finally { closeSync(fd); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseYamlDocument(text, label) {
|
|
124
|
+
const document = YAML.parseDocument(text, { uniqueKeys: true, prettyErrors: true, strict: true, maxAliasCount: 0 });
|
|
125
|
+
if (document.errors.length || document.warnings.length) throw new Error(`Invalid ${label} YAML: ${[...document.errors, ...document.warnings].map((error) => error.message).join('; ')}`);
|
|
126
|
+
let forbidden = false;
|
|
127
|
+
YAML.visit(document, { Node: (_key, node) => { if (node?.anchor || YAML.isAlias(node) || node?.tag) forbidden = true; } });
|
|
128
|
+
if (forbidden) throw new Error(`${label} must not contain YAML anchors, aliases, or explicit tags.`);
|
|
129
|
+
const value = document.toJS();
|
|
130
|
+
if (value !== null && (typeof value !== 'object' || Array.isArray(value))) {
|
|
131
|
+
throw new Error(`${label} must contain a YAML mapping.`);
|
|
132
|
+
}
|
|
133
|
+
return document;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readYamlDocument(path, label) { return parseYamlDocument(readRegularText(path, label), label); }
|
|
137
|
+
|
|
138
|
+
function yamlObject(document, label) {
|
|
139
|
+
const value = document.toJS() ?? {};
|
|
140
|
+
if (typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must contain a YAML mapping.`);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertNoLegacyRawLucyToken(current) {
|
|
145
|
+
const lucy = current?.mcp_servers?.lucy;
|
|
146
|
+
if (!lucy || typeof lucy !== 'object' || Array.isArray(lucy)) return;
|
|
147
|
+
const environment = lucy.env;
|
|
148
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)
|
|
149
|
+
|| !Object.hasOwn(environment, 'LUCY_MEMORY_TOKEN')) return;
|
|
150
|
+
const token = environment.LUCY_MEMORY_TOKEN;
|
|
151
|
+
if (typeof token !== 'string' || !/^\$\{[A-Z_][A-Z0-9_]*\}$/.test(token)) {
|
|
152
|
+
throw new Error('Existing Lucy configuration contains a raw token; remove it and rotate the credential before installation.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function clampRegularFilePrivate(path, label) {
|
|
157
|
+
statRegularFile(path, label, { required: true });
|
|
158
|
+
const fd = openSync(path, constants.O_WRONLY | NOFOLLOW);
|
|
159
|
+
try {
|
|
160
|
+
if (!fstatSync(fd).isFile()) throw new Error(`${label} must be a regular file.`);
|
|
161
|
+
fchmodSync(fd, PRIVATE_FILE_MODE); fsyncSync(fd);
|
|
162
|
+
} finally { closeSync(fd); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function syncDirectory(path) {
|
|
166
|
+
let fd;
|
|
167
|
+
try { fd = openSync(path, constants.O_RDONLY); fsyncSync(fd); }
|
|
168
|
+
catch (error) { if (!['EINVAL', 'ENOTSUP', 'EISDIR', 'EPERM'].includes(error?.code)) throw error; }
|
|
169
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function atomicWritePrivate(path, contents, label) {
|
|
173
|
+
assertSecureLocalWritesSupported();
|
|
174
|
+
const parent = dirname(path);
|
|
175
|
+
if (!validateExistingParent(path, label, { allowMissing: false })) throw new Error(`${label} parent is missing.`);
|
|
176
|
+
statRegularFile(path, label);
|
|
177
|
+
const temporaryPath = join(parent, `.${basename(path)}.${randomBytes(16).toString('hex')}.tmp`);
|
|
178
|
+
let fd; let temporaryExists = false;
|
|
179
|
+
try {
|
|
180
|
+
fd = openSync(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, PRIVATE_FILE_MODE);
|
|
181
|
+
temporaryExists = true;
|
|
182
|
+
writeFileSync(fd, contents, 'utf8'); fchmodSync(fd, PRIVATE_FILE_MODE); fsyncSync(fd); closeSync(fd); fd = undefined;
|
|
183
|
+
statRegularFile(path, label); // detect replacement with symlink/special file before rename
|
|
184
|
+
renameSync(temporaryPath, path); temporaryExists = false; syncDirectory(parent);
|
|
185
|
+
} finally {
|
|
186
|
+
if (fd !== undefined) closeSync(fd);
|
|
187
|
+
if (temporaryExists) rmSync(temporaryPath, { force: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeRegularFile(path, label) {
|
|
192
|
+
if (!statRegularFile(path, label)) return false;
|
|
193
|
+
rmSync(path); return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function digest(text) { return createHash('sha256').update(text).digest('hex'); }
|
|
197
|
+
|
|
198
|
+
function envReference(tokenEnv, token) {
|
|
199
|
+
if (token !== undefined) throw new Error('Raw token values are forbidden; use tokenEnv with an environment variable name.');
|
|
200
|
+
if (typeof tokenEnv !== 'string' || !ENV_NAME.test(tokenEnv)) {
|
|
201
|
+
throw new Error('tokenEnv is required and must be an uppercase environment variable name.');
|
|
202
|
+
}
|
|
203
|
+
return `\${${tokenEnv}}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function managedPermissions(permissions) {
|
|
207
|
+
if (!Array.isArray(permissions) || permissions.length !== REQUIRED_PERMISSIONS.length
|
|
208
|
+
|| permissions.some((permission, index) => permission !== REQUIRED_PERMISSIONS[index])) {
|
|
209
|
+
throw new Error('Lucy requires exactly the memory:read and memory:propose permissions.');
|
|
210
|
+
}
|
|
211
|
+
return permissions;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function serializeYaml(document) { return document.toString({ lineWidth: 0 }); }
|
|
215
|
+
|
|
216
|
+
export function installHermesLucyArtifact(profilePath, options = {}) {
|
|
217
|
+
assertHermesInstallerMutationSupported();
|
|
218
|
+
return installClosedHermesLucyArtifact(profilePath, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateBaseUrl(baseUrl, allowInsecureLoopback) {
|
|
222
|
+
if (typeof baseUrl !== 'string' || !baseUrl) throw new Error('baseUrl is required');
|
|
223
|
+
let url; try { url = new URL(baseUrl); } catch { throw new Error('baseUrl must be an absolute HTTPS URL.'); }
|
|
224
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(url.hostname);
|
|
225
|
+
if (url.username || url.password || url.search || url.hash) throw new Error('baseUrl must not contain credentials, query parameters, or fragments.');
|
|
226
|
+
if (url.protocol !== 'https:' && !(allowInsecureLoopback && loopback && url.protocol === 'http:')) throw new Error('baseUrl must use HTTPS unless explicit insecure loopback is enabled.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function validateLucyMcpOptions({ baseUrl, tokenEnv, token, permissions = ['memory:read', 'memory:propose'], allowInsecureLoopback = false }) {
|
|
230
|
+
if (typeof allowInsecureLoopback !== 'boolean') throw new Error('allowInsecureLoopback must be a boolean');
|
|
231
|
+
validateBaseUrl(baseUrl, allowInsecureLoopback);
|
|
232
|
+
managedPermissions(permissions); envReference(tokenEnv, token);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function validateArtifactConfig(artifact) {
|
|
236
|
+
const required = ['path', 'node_path', 'entrypoint', 'version', 'artifact_digest', 'source_git_sha', 'package_name', 'package_version', 'source_repository', 'receiptPath'];
|
|
237
|
+
if (!artifact || required.some((key) => typeof artifact[key] !== 'string' || !artifact[key])) throw new Error('A complete versioned artifact is required.');
|
|
238
|
+
if (!artifact.compatibility || typeof artifact.compatibility !== 'object' || Array.isArray(artifact.compatibility)) throw new Error('A complete versioned artifact is required.');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function lucyMcpConfig({ baseUrl, tokenEnv, token, permissions = ['memory:read', 'memory:propose'], allowInsecureLoopback = false, artifact }) {
|
|
242
|
+
validateLucyMcpOptions({ baseUrl, tokenEnv, token, permissions, allowInsecureLoopback });
|
|
243
|
+
validateArtifactConfig(artifact);
|
|
244
|
+
return {
|
|
245
|
+
command: artifact.node_path,
|
|
246
|
+
args: [artifact.entrypoint],
|
|
247
|
+
env: {
|
|
248
|
+
LUCY_MEMORY_TOKEN: envReference(tokenEnv, token),
|
|
249
|
+
LUCY_MEMORY_PERMISSIONS: permissions.join(','), LUCY_MEMORY_BASE_URL: baseUrl,
|
|
250
|
+
...(allowInsecureLoopback ? { LUCY_MEMORY_ALLOW_INSECURE_LOOPBACK: 'true' } : {}),
|
|
251
|
+
},
|
|
252
|
+
x_lucy_memory_artifact: {
|
|
253
|
+
version: artifact.version, artifact_digest: artifact.artifact_digest,
|
|
254
|
+
source_git_sha: artifact.source_git_sha, package_name: artifact.package_name,
|
|
255
|
+
package_version: artifact.package_version, source_repository: artifact.source_repository,
|
|
256
|
+
compatibility: artifact.compatibility, receipt_path: artifact.receiptPath,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function installConfig(configPath, options) {
|
|
262
|
+
assertHermesInstallerMutationSupported();
|
|
263
|
+
validateLucyMcpOptions(options);
|
|
264
|
+
const path = assertAbsolute(configPath, 'configPath');
|
|
265
|
+
const profilePath = isolatedProfile(options.profilePath, { create: true });
|
|
266
|
+
assertConfigInside(path, profilePath);
|
|
267
|
+
// Refuse unsafe legacy credentials before creating lock state, publishing an
|
|
268
|
+
// artifact, or copying the old configuration into a persistent backup.
|
|
269
|
+
validateContainedParent(path, profilePath, 'Hermes config');
|
|
270
|
+
const preflightStat = statRegularFile(path, 'Hermes config');
|
|
271
|
+
if (preflightStat) {
|
|
272
|
+
const preflight = yamlObject(readYamlDocument(path, 'Hermes config'), 'Hermes config');
|
|
273
|
+
assertNoLegacyRawLucyToken(preflight);
|
|
274
|
+
}
|
|
275
|
+
const preflightBackupPath = path + BACKUP_SUFFIX;
|
|
276
|
+
const preflightBackupStat = statRegularFile(preflightBackupPath, 'Hermes config backup');
|
|
277
|
+
if (preflightBackupStat) {
|
|
278
|
+
const preflightBackup = yamlObject(readYamlDocument(preflightBackupPath, 'Hermes config backup'), 'Hermes config backup');
|
|
279
|
+
assertNoLegacyRawLucyToken(preflightBackup);
|
|
280
|
+
}
|
|
281
|
+
const releaseLock = acquireConfigLock(profilePath);
|
|
282
|
+
try {
|
|
283
|
+
validateContainedParent(path, profilePath, 'Hermes config');
|
|
284
|
+
const configStat = statRegularFile(path, 'Hermes config');
|
|
285
|
+
if (configStat && (configStat.mode & 0o222) === 0) throw new Error('Hermes config is read-only.');
|
|
286
|
+
const backupPath = path + BACKUP_SUFFIX;
|
|
287
|
+
const backupStat = statRegularFile(backupPath, 'Hermes config backup');
|
|
288
|
+
const initialConfigText = configStat ? readRegularText(path, 'Hermes config') : null;
|
|
289
|
+
const initialBackupText = backupStat ? readRegularText(backupPath, 'Hermes config backup') : null;
|
|
290
|
+
const document = configStat ? parseYamlDocument(initialConfigText, 'Hermes config') : YAML.parseDocument('{}\n');
|
|
291
|
+
const current = yamlObject(document, 'Hermes config');
|
|
292
|
+
if (current.mcp_servers !== undefined && (typeof current.mcp_servers !== 'object' || Array.isArray(current.mcp_servers) || current.mcp_servers === null)) {
|
|
293
|
+
throw new Error('Hermes config mcp_servers must be a mapping.');
|
|
294
|
+
}
|
|
295
|
+
assertNoLegacyRawLucyToken(current);
|
|
296
|
+
if (backupStat) {
|
|
297
|
+
const backup = yamlObject(parseYamlDocument(initialBackupText, 'Hermes config backup'), 'Hermes config backup');
|
|
298
|
+
assertNoLegacyRawLucyToken(backup);
|
|
299
|
+
}
|
|
300
|
+
const artifact = installHermesLucyArtifact(profilePath, options.artifact);
|
|
301
|
+
const trustedNode = assertAbsolute(options.artifact.nodePath, 'Node executable');
|
|
302
|
+
if (artifact.node_path !== trustedNode) throw new Error('Published artifact Node path does not match the trusted Node executable.');
|
|
303
|
+
const activationArtifact = { ...artifact, node_path: trustedNode };
|
|
304
|
+
const activationStat = statRegularFile(path, 'Hermes config');
|
|
305
|
+
if (Boolean(activationStat) !== Boolean(configStat) || (activationStat && readRegularText(path, 'Hermes config') !== initialConfigText)) {
|
|
306
|
+
throw new Error('Hermes config changed during artifact publication; retry against the current config.');
|
|
307
|
+
}
|
|
308
|
+
const activationBackupStat = statRegularFile(backupPath, 'Hermes config backup');
|
|
309
|
+
if (Boolean(activationBackupStat) !== Boolean(backupStat)
|
|
310
|
+
|| (activationBackupStat && readRegularText(backupPath, 'Hermes config backup') !== initialBackupText)) {
|
|
311
|
+
throw new Error('Hermes config backup changed during artifact publication; retry against the current backup.');
|
|
312
|
+
}
|
|
313
|
+
releaseLock.assertHeld();
|
|
314
|
+
// The backup contains pre-install local state and is never regenerated on updates.
|
|
315
|
+
if (backupStat) clampRegularFilePrivate(backupPath, 'Hermes config backup');
|
|
316
|
+
else if (configStat) atomicWritePrivate(backupPath, initialConfigText, 'Hermes config backup');
|
|
317
|
+
document.setIn(['mcp_servers', 'lucy'], lucyMcpConfig({ ...options, artifact: activationArtifact }));
|
|
318
|
+
releaseLock.assertHeld();
|
|
319
|
+
atomicWritePrivate(path, serializeYaml(document), 'Hermes config');
|
|
320
|
+
return { path, backup: existsSync(backupPath) ? backupPath : null, artifact: activationArtifact };
|
|
321
|
+
} finally {
|
|
322
|
+
releaseLock();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function installHermesLucyConfig(configPath, options = {}) { return installConfig(configPath, options); }
|
|
327
|
+
export function updateHermesLucyConfig(configPath, options = {}) { return installConfig(configPath, options); }
|
|
328
|
+
|
|
329
|
+
export function rollbackHermesLucyConfig(configPath, { profilePath, receiptPath, sourcePath, expectedSourceGitSha, trustedHermesVersion, trustedNodePath } = {}) {
|
|
330
|
+
assertHermesInstallerMutationSupported();
|
|
331
|
+
const path = assertAbsolute(configPath, 'configPath');
|
|
332
|
+
const profile = isolatedProfile(profilePath);
|
|
333
|
+
assertConfigInside(path, profile);
|
|
334
|
+
const releaseLock = acquireConfigLock(profile);
|
|
335
|
+
try {
|
|
336
|
+
validateContainedParent(path, profile, 'Hermes config');
|
|
337
|
+
if (typeof trustedNodePath !== 'string' || !trustedNodePath) throw new Error('Trusted rollback Node executable is required.');
|
|
338
|
+
const trustedNode = assertAbsolute(trustedNodePath, 'Trusted rollback Node executable');
|
|
339
|
+
const receipt = verifyHermesLucyArtifactAgainstSource(assertAbsolute(receiptPath, 'receiptPath'), profile, { sourcePath, expectedSourceGitSha, trustedHermesVersion, trustedNodePath: trustedNode });
|
|
340
|
+
if (receipt.node_path !== trustedNode) throw new Error('Verified artifact Node path does not match the trusted Node executable.');
|
|
341
|
+
const activationArtifact = { ...receipt, receiptPath, node_path: trustedNode };
|
|
342
|
+
const configStat = statRegularFile(path, 'Hermes config', { required: true });
|
|
343
|
+
if ((configStat.mode & 0o222) === 0) throw new Error('Hermes config is read-only.');
|
|
344
|
+
const document = readYamlDocument(path, 'Hermes config');
|
|
345
|
+
const current = yamlObject(document, 'Hermes config');
|
|
346
|
+
if (!current.mcp_servers || typeof current.mcp_servers !== 'object' || Array.isArray(current.mcp_servers)) throw new Error('Hermes config has no MCP mapping.');
|
|
347
|
+
const lucy = current.mcp_servers.lucy;
|
|
348
|
+
if (!lucy?.env?.LUCY_MEMORY_TOKEN || !lucy?.env?.LUCY_MEMORY_BASE_URL) throw new Error('Hermes config has no managed Lucy entry.');
|
|
349
|
+
const ref = lucy.env.LUCY_MEMORY_TOKEN;
|
|
350
|
+
if (!/^\$\{[A-Z_][A-Z0-9_]*\}$/.test(ref)) throw new Error('Managed Lucy token is not an environment reference.');
|
|
351
|
+
document.setIn(['mcp_servers', 'lucy'], lucyMcpConfig({
|
|
352
|
+
baseUrl: lucy.env.LUCY_MEMORY_BASE_URL, tokenEnv: ref.slice(2, -1),
|
|
353
|
+
permissions: String(lucy.env.LUCY_MEMORY_PERMISSIONS ?? '').split(',').filter(Boolean),
|
|
354
|
+
allowInsecureLoopback: lucy.env.LUCY_MEMORY_ALLOW_INSECURE_LOOPBACK === 'true', artifact: activationArtifact,
|
|
355
|
+
}));
|
|
356
|
+
releaseLock.assertHeld();
|
|
357
|
+
atomicWritePrivate(path, serializeYaml(document), 'Hermes config');
|
|
358
|
+
return { path, artifact: activationArtifact };
|
|
359
|
+
} finally {
|
|
360
|
+
releaseLock();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function uninstallHermesLucyConfig(configPath, { profilePath } = {}) {
|
|
365
|
+
assertHermesInstallerMutationSupported();
|
|
366
|
+
const path = assertAbsolute(configPath, 'configPath');
|
|
367
|
+
const profile = isolatedProfile(profilePath);
|
|
368
|
+
assertConfigInside(path, profile);
|
|
369
|
+
const releaseLock = acquireConfigLock(profile);
|
|
370
|
+
try {
|
|
371
|
+
validateContainedParent(path, profile, 'Hermes config');
|
|
372
|
+
const backupPath = path + BACKUP_SUFFIX;
|
|
373
|
+
const configStat = statRegularFile(path, 'Hermes config');
|
|
374
|
+
const backupStat = statRegularFile(backupPath, 'Hermes config backup');
|
|
375
|
+
if (!configStat) { if (backupStat) readYamlDocument(backupPath, 'Hermes config backup'); return { restored: false }; }
|
|
376
|
+
if ((configStat.mode & 0o222) === 0) throw new Error('Hermes config is read-only.');
|
|
377
|
+
const document = readYamlDocument(path, 'Hermes config');
|
|
378
|
+
const current = yamlObject(document, 'Hermes config');
|
|
379
|
+
if (backupStat) readYamlDocument(backupPath, 'Hermes config backup');
|
|
380
|
+
let removed = false;
|
|
381
|
+
if (current.mcp_servers && typeof current.mcp_servers === 'object' && !Array.isArray(current.mcp_servers) && Object.hasOwn(current.mcp_servers, 'lucy')) {
|
|
382
|
+
document.deleteIn(['mcp_servers', 'lucy']); removed = true;
|
|
383
|
+
releaseLock.assertHeld();
|
|
384
|
+
atomicWritePrivate(path, serializeYaml(document), 'Hermes config');
|
|
385
|
+
} else {
|
|
386
|
+
releaseLock.assertHeld();
|
|
387
|
+
clampRegularFilePrivate(path, 'Hermes config');
|
|
388
|
+
}
|
|
389
|
+
if (backupStat) {
|
|
390
|
+
releaseLock.assertHeld();
|
|
391
|
+
removeRegularFile(backupPath, 'Hermes config backup');
|
|
392
|
+
}
|
|
393
|
+
return { restored: false, removed };
|
|
394
|
+
} finally {
|
|
395
|
+
releaseLock();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function hermesLucyStatus(configPath) {
|
|
400
|
+
const path = assertAbsolute(configPath, 'configPath');
|
|
401
|
+
// Status is verification-only: validate existing parents but never create or clamp them.
|
|
402
|
+
if (!validateExistingParent(path, 'Hermes config', { allowMissing: true })) return { installed: false };
|
|
403
|
+
if (!statRegularFile(path, 'Hermes config')) return { installed: false };
|
|
404
|
+
const lucy = yamlObject(readYamlDocument(path, 'Hermes config'), 'Hermes config').mcp_servers?.lucy;
|
|
405
|
+
return { installed: Boolean(lucy), hasToken: /^\$\{[A-Z_][A-Z0-9_]*\}$/.test(lucy?.env?.LUCY_MEMORY_TOKEN ?? ''), baseUrl: lucy?.env?.LUCY_MEMORY_BASE_URL ?? null, permissions: lucy?.env?.LUCY_MEMORY_PERMISSIONS ?? null, artifact: lucy?.x_lucy_memory_artifact?.artifact_digest ?? null };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Removes only the private pre-install YAML backup. Artifact and receipt caches are retained. */
|
|
409
|
+
export function resetHermesLucyBackup(configPath, { profilePath } = {}) {
|
|
410
|
+
assertHermesInstallerMutationSupported();
|
|
411
|
+
const path = assertAbsolute(configPath, 'configPath');
|
|
412
|
+
const profile = isolatedProfile(profilePath);
|
|
413
|
+
assertConfigInside(path, profile);
|
|
414
|
+
const releaseLock = acquireConfigLock(profile);
|
|
415
|
+
try {
|
|
416
|
+
validateContainedParent(path, profile, 'Hermes config');
|
|
417
|
+
releaseLock.assertHeld();
|
|
418
|
+
removeRegularFile(path + BACKUP_SUFFIX, 'Hermes config backup');
|
|
419
|
+
} finally {
|
|
420
|
+
releaseLock();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** @deprecated Use resetHermesLucyBackup; this legacy alias retains artifact and receipt caches. */
|
|
425
|
+
export const resetHermesLucyArtifacts = resetHermesLucyBackup;
|
package/src/mcp/bin.mjs
ADDED