Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d56bd65
fix(hermes): use PRAGMA temp_store=MEMORY in SessionDB to fix FK dele…
yanyunl1991 Aug 5, 2026
c1ff4ed
fix(hermes): update image-build-probes hash and test fixtures for sql…
yanyunl1991 Aug 5, 2026
6c31cf5
Merge remote-tracking branch 'origin/main' into fix/hermes-session-de…
yanyunl1991 Aug 5, 2026
4d35fb4
fix(test): add missing sqlite-temp-store fixture to three test suites…
yanyunl1991 Aug 5, 2026
6d3fd37
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
5b03565
fix(hermes): add sha256 gate and tighten already-patched check for sq…
yanyunl1991 Aug 5, 2026
5f2d0e0
Merge remote-tracking branch 'origin/fix/hermes-session-delete-sqlite…
yanyunl1991 Aug 5, 2026
48594fd
fix(test): register sqlite temp-store patcher SHA256 ARG as integrity…
yanyunl1991 Aug 5, 2026
a22e3f7
test(hermes): exercise session deletion through host CLI
apurvvkumaria Aug 5, 2026
2fc714e
merge: refresh PR branch from main
apurvvkumaria Aug 5, 2026
f17464f
fix(hermes): reject ambiguous SQLite temp-store patches
cv Aug 5, 2026
9f46b7e
merge(main): refresh PR #8308 after gateway teardown repair
cv Aug 5, 2026
044567e
merge(main): refresh PR #8308 after Shields status repair
cv Aug 5, 2026
a89b9be
docs(hermes): describe SQLite patch states
cv Aug 5, 2026
7494b29
merge: resolve conflicts with main
github-actions[bot] Aug 5, 2026
f8dd26b
merge(main): refresh PR #8308 after Hermes cron restore
cv Aug 5, 2026
169d578
merge: preserve concurrent PR #8308 refresh
cv Aug 5, 2026
e4a19fa
docs(hermes): use controlled product names
cv Aug 5, 2026
7b797fb
merge(main): refresh PR #8308 after llama.cpp runtime
cv Aug 5, 2026
7e85eff
merge(main): refresh PR #8308
apurvvkumaria Aug 5, 2026
64e403b
merge(main): refresh PR #8308
cjagwani Aug 5, 2026
3b633c3
test(hermes): surface config hash replay failures
cjagwani Aug 5, 2026
b41d428
merge: resolve conflicts with main
github-actions[bot] Aug 6, 2026
63b3428
merge(main): refresh Hermes session deletion fix
apurvvkumaria Aug 6, 2026
78e095c
merge(main): refresh Hermes session deletion fix
apurvvkumaria Aug 6, 2026
6f044ef
test(hermes): cover SQLite patcher integrity
apurvvkumaria Aug 6, 2026
3c5bdc4
merge(main): refresh Hermes session deletion fix
apurvvkumaria Aug 6, 2026
13ce1e7
merge(main): refresh Hermes session deletion fix
apurvvkumaria Aug 6, 2026
7e9a8d2
merge(main): refresh Hermes session deletion fix
apurvvkumaria Aug 6, 2026
0178597
merge(main): refresh Hermes session deletion fix
apurvvkumaria Aug 7, 2026
588d7c0
Merge branch 'main' into fix/hermes-session-delete-sqlite-cantopen-8301
cv Aug 7, 2026
3738371
Merge branch 'main' into fix/hermes-session-delete-sqlite-cantopen-8301
cv Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ COPY scripts/gateway-control.sh /usr/local/bin/nemoclaw-gateway-control
COPY scripts/managed-gateway-control.py /usr/local/lib/nemoclaw/managed-gateway-control.py
COPY agents/hermes/validate-env-secret-boundary.py /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py
COPY agents/hermes/patch-session-list-preview.py /usr/local/lib/nemoclaw/patch-hermes-session-list-preview.py
COPY agents/hermes/patch-hermes-sqlite-temp-store.py /usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py
COPY agents/hermes/patch-discord-recovery-permissions.py /usr/local/lib/nemoclaw/patch-hermes-discord-recovery-permissions.py
COPY agents/hermes/patch-profile-policy-defaults.py /usr/local/lib/nemoclaw/patch-hermes-profile-policy-defaults.py
COPY agents/hermes/managed_policy.py /usr/local/lib/nemoclaw/managed_policy.py
Expand Down Expand Up @@ -326,7 +327,7 @@ RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
/scripts/patch-bundled-npm-tar.mts \
&& chmod -R a+rX /src/lib/messaging

ARG NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256=61fec509d9c0229650f71d1fc53d103a39fd830976213dbdef0d222592981d03
ARG NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256=89f530da6a8296c8bab449a2a324d8efedca1a90b04a5e0f57cf9203ed2011cd
# hadolint ignore=DL4006
RUN printf '%s %s\n' \
"$NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256" /opt/nemoclaw-hermes-config/image-build-probes.py \
Expand Down Expand Up @@ -361,7 +362,7 @@ RUN chmod -R a+rX /opt/nemoclaw-blueprint/
# profile hook, bashrc hook, or root-owned helper mode. Remove it once the
# minimum supported Hermes sandbox base tag guarantees those artifacts and
# test/sandbox-rlimit-hooks.test.ts covers that base.
RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-managed-startup-hold /usr/local/bin/nemoclaw-managed-bootstrap /usr/local/lib/nemoclaw/sandbox-init.sh /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py /usr/local/lib/nemoclaw/patch-hermes-session-list-preview.py /usr/local/lib/nemoclaw/patch-hermes-discord-recovery-permissions.py /usr/local/lib/nemoclaw/patch-hermes-profile-policy-defaults.py /usr/local/lib/nemoclaw/seed-hermes-dashboard-config.py /usr/local/lib/nemoclaw/hermes-runtime-config-guard.py /usr/local/lib/nemoclaw/finalize-tirith-marker.py /usr/local/lib/nemoclaw/hermes-mcp-config-transaction.py \
RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-managed-startup-hold /usr/local/bin/nemoclaw-managed-bootstrap /usr/local/lib/nemoclaw/sandbox-init.sh /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py /usr/local/lib/nemoclaw/patch-hermes-session-list-preview.py /usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py /usr/local/lib/nemoclaw/patch-hermes-discord-recovery-permissions.py /usr/local/lib/nemoclaw/patch-hermes-profile-policy-defaults.py /usr/local/lib/nemoclaw/seed-hermes-dashboard-config.py /usr/local/lib/nemoclaw/hermes-runtime-config-guard.py /usr/local/lib/nemoclaw/finalize-tirith-marker.py /usr/local/lib/nemoclaw/hermes-mcp-config-transaction.py \
&& chown root:root /usr/local/bin/nemoclaw-gateway-control /usr/local/lib/nemoclaw/gateway-supervisor.sh /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/share/nemoclaw/state-lock-plan.json /usr/local/lib/nemoclaw/managed-gateway-control.py /usr/local/lib/nemoclaw/build-hermes-mcp-digest.py /usr/local/lib/nemoclaw/hermes-cron-restore-control.py /usr/local/lib/nemoclaw/openshell-child-visible-credentials.v0.0.85.json \
&& chmod 700 /usr/local/bin/nemoclaw-gateway-control /usr/local/lib/nemoclaw/hermes-cron-restore-control.py \
&& chmod 500 /usr/local/lib/nemoclaw/state-dir-guard.py /usr/local/lib/nemoclaw/managed-gateway-control.py \
Expand Down Expand Up @@ -500,6 +501,27 @@ RUN /usr/bin/python3 -I /usr/local/lib/nemoclaw/patch-hermes-session-list-previe
&& HERMES_HOME="$(mktemp -d)" /opt/hermes/.venv/bin/python -I \
/opt/nemoclaw-hermes-config/image-build-probes.py session-preview

# Hermes v0.19.0 SessionDB does not set PRAGMA temp_store=MEMORY, so SQLite
# falls back to file-based temp storage for FK constraint processing (for
# example, the ON DELETE CASCADE on session_model_usage -> sessions). The
# OpenShell sandbox execution context used by
# `nemohermes <sandbox> sessions delete <id>` restricts the temp-file creation
# syscalls that SQLite's FK engine relies on, causing every
# `DELETE FROM sessions` with FK enforcement to raise
# `sqlite3.OperationalError: unable to open database file` (#8301). Inserting
# PRAGMA temp_store=MEMORY before PRAGMA foreign_keys=ON ensures FK processing
# uses in-memory storage. Docker execution permits the file-based storage path.
ARG NEMOCLAW_HERMES_SQLITE_TEMP_STORE_PATCHER_SHA256=2db46b6cd358f4d181999bbddbc0d35d035a67ee1092adb2e2c16a894c695e13
# hadolint ignore=DL4006
RUN printf '%s %s\n' \
"$NEMOCLAW_HERMES_SQLITE_TEMP_STORE_PATCHER_SHA256" /usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py \
| sha256sum -c - \
|| { echo "ERROR: patch-hermes-sqlite-temp-store.py hash mismatch (update NEMOCLAW_HERMES_SQLITE_TEMP_STORE_PATCHER_SHA256)" >&2; exit 1; }
RUN /usr/bin/python3 -I /usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py \
&& grep -q 'PRAGMA temp_store=MEMORY' /opt/hermes/hermes_state.py \
&& HERMES_HOME="$(mktemp -d)" /opt/hermes/.venv/bin/python -I \
/opt/nemoclaw-hermes-config/image-build-probes.py session-delete
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Hermes v0.19.0 forces the gateway-created Discord recovery ledger to 0600,
# which prevents sandbox-owned online backup and makes a sandbox-restored
# database unwritable by the gateway. The final layout uses a gateway:sandbox
Expand Down
15 changes: 15 additions & 0 deletions agents/hermes/image-build-probes.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ def verify_session_preview() -> None:
assert rows[0]["preview"] == "NEMOCLAW_PREVIEW_LATEST", rows


def verify_session_delete() -> None:
from hermes_state import SessionDB

db = SessionDB()
session_id = "nemoclaw-session-delete-smoke"
db.create_session(session_id, "cli")
db.append_message(session_id, "user", "probe message 1")
db.append_message(session_id, "assistant", "probe reply")
deleted = db.delete_session(session_id)
assert deleted, f"delete_session returned {deleted!r}"
rows = db.list_sessions_rich(limit=10)
assert not any(r["id"] == session_id for r in rows), "session still present after delete"


def verify_discord_recovery_source() -> None:
source = Path("/opt/hermes/plugins/platforms/discord/recovery.py").read_text(
encoding="utf-8"
Expand Down Expand Up @@ -389,6 +403,7 @@ def reopen_probe(conn):
"langfuse-credentials": verify_langfuse_credentials,
"neutral-platform-inertness": verify_neutral_platform_inertness,
"profile-policy": verify_profile_policy,
"session-delete": verify_session_delete,
"session-preview": verify_session_preview,
}

Expand Down
90 changes: 90 additions & 0 deletions agents/hermes/patch-hermes-sqlite-temp-store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Patch SessionDB.__init__ to use in-memory temp store for SQLite FK processing.

Source-of-truth note for this localized Hermes runtime patch:
- Invalid state: Hermes v0.19.0 SessionDB does not set PRAGMA temp_store=MEMORY,
so SQLite falls back to file-based temp storage when processing FK constraints
(for example, the ON DELETE CASCADE on session_model_usage -> sessions). When
`hermes sessions delete` is invoked through OpenShell sandbox execution —
the code path used by `nemohermes <sandbox> sessions delete <id>` — the
process runs in a restricted environment where SQLite's temp-file creation
syscalls fail with
SQLITE_CANTOPEN, causing every `DELETE FROM sessions` with FK enforcement
enabled to raise `sqlite3.OperationalError: unable to open database file`
(#8301). The same command succeeds through Docker execution because that
context allows the file-based temp store.
- Value being patched: pinned/prebuilt `/opt/hermes/hermes_state.py`
`SessionDB.__init__` connection setup block; specifically, the statement
immediately following `apply_wal_with_fallback()` that enables FK enforcement.
`PRAGMA temp_store=MEMORY` is inserted before `PRAGMA foreign_keys=ON` so the
in-memory store is active before any FK-constrained write.
- Source-fix constraint: NemoClaw layers a sandbox image on top of the
published Hermes runtime; the source fix belongs upstream in Hermes, not in
NemoClaw's TypeScript or wrapper code.
- Regression evidence: on first application, this patcher accepts exactly one
unpatched connection setup block and no temp-store statement. A later
application accepts exactly one complete patched block with one temp-store
statement. Every other source shape fails without writing. The Dockerfile
checks for the inserted PRAGMA after patching. The image-build
`session-delete` behavior test creates a SessionDB, inserts a session with
messages, and calls `delete_session()` to confirm that SQLite does not raise
OperationalError.
- Removal condition: delete this patch when the pinned Hermes runtime natively
sets `PRAGMA temp_store=MEMORY` (or equivalent) in `SessionDB.__init__`.
"""

from __future__ import annotations

import argparse
from pathlib import Path

OLD = (
'apply_wal_with_fallback(self._conn, db_label="state.db")\n'
' self._conn.execute("PRAGMA foreign_keys=ON")'
)
NEW = (
'apply_wal_with_fallback(self._conn, db_label="state.db")\n'
' self._conn.execute("PRAGMA temp_store=MEMORY")\n'
' self._conn.execute("PRAGMA foreign_keys=ON")'
)
EXPECTED_OCCURRENCES = 1


def patch_file(path: Path) -> None:
source = path.read_text(encoding="utf-8")
old_count = source.count(OLD)
new_count = source.count('self._conn.execute("PRAGMA temp_store=MEMORY")')
patched_count = source.count(NEW)
if (
old_count == 0
and new_count == EXPECTED_OCCURRENCES
and patched_count == EXPECTED_OCCURRENCES
):
return
if old_count != EXPECTED_OCCURRENCES or new_count != 0:
raise SystemExit(
"ERROR: Hermes SessionDB.__init__ connection setup shape changed; "
f"expected {EXPECTED_OCCURRENCES} unpatched block and no temp-store "
f"statements; found {old_count} unpatched blocks, {new_count} temp-store "
f"statements, and {patched_count} complete patched blocks"
)
path.write_text(source.replace(OLD, NEW), encoding="utf-8")
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"path",
nargs="?",
default="/opt/hermes/hermes_state.py",
help="Hermes state module to patch",
)
args = parser.parse_args()
patch_file(Path(args.path))
return 0


if __name__ == "__main__":
raise SystemExit(main())
1 change: 1 addition & 0 deletions src/lib/onboard/managed-startup/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ export const MANAGED_STARTUP_PROFILE_EXCLUDED_DOCKER_INPUTS = {
{ input: "NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256", reason: "integrity-pin" },
{ input: "NEMOCLAW_HERMES_CRON_EXECUTIONS_SOURCE_SHA256", reason: "integrity-pin" },
{ input: "NEMOCLAW_HERMES_BACKUP_SOURCE_SHA256", reason: "integrity-pin" },
{ input: "NEMOCLAW_HERMES_SQLITE_TEMP_STORE_PATCHER_SHA256", reason: "integrity-pin" },
{ input: "NEMOCLAW_HERMES_DISCORD_RECOVERY_PATCHER_SHA256", reason: "integrity-pin" },
{ input: "NEMOCLAW_HERMES_LANGFUSE_PATCHER_SHA256", reason: "integrity-pin" },
{ input: "NEMOCLAW_HERMES_WRAPPER_SHA256", reason: "integrity-pin" },
Expand Down
26 changes: 26 additions & 0 deletions test/e2e/live/hermes-cli-adapter-live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// SPDX-License-Identifier: Apache-2.0

import { resultText, shellQuote } from "../fixtures/clients/command.ts";
import type { HostCliClient } from "../fixtures/clients/host.ts";
import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts";
import { expect } from "../fixtures/e2e-test.ts";
import { exportHermesSession, hermesLastActive } from "../fixtures/hermes-session.ts";

interface HermesCliAdapterLiveOptions {
env: NodeJS.ProcessEnv;
host: HostCliClient;
redactionValues: string[];
sandbox: SandboxClient;
sandboxName: string;
Expand All @@ -29,6 +31,7 @@ export function onlyNewHermesSessionId(before: Set<string>, after: Set<string>):

export async function assertHermesCliAdapterLiveContract({
env,
host,
redactionValues,
sandbox,
sandboxName,
Expand Down Expand Up @@ -119,6 +122,29 @@ export async function assertHermesCliAdapterLiveContract({
},
);

const deleteSession = await host.command(
"nemohermes",
[sandboxName, "sessions", "delete", seedSessionId],
{
artifactName: "phase-4-issue-8301-delete-session",
env,
redactionValues,
timeoutMs: 60_000,
},
);
expect(deleteSession.exitCode, resultText(deleteSession)).toBe(0);
const sessionsAfterDelete = await host.command("nemohermes", [sandboxName, "sessions", "list"], {
artifactName: "phase-4-issue-8301-sessions-after-delete",
env,
redactionValues,
timeoutMs: 60_000,
});
expect(sessionsAfterDelete.exitCode, resultText(sessionsAfterDelete)).toBe(0);
expect(
hermesSessionIds(resultText(sessionsAfterDelete)).has(seedSessionId),
stripAnsi(resultText(sessionsAfterDelete)),
).toBe(false);

const usageFilePath = `/tmp/nemoclaw-cli-adapter-usage-${Date.now()}.json`;
const sessionsBeforeGuardedUsage = await listDefaultSessions(
"phase-4-cli-adapter-sessions-before-guarded-usage",
Expand Down
1 change: 1 addition & 0 deletions test/e2e/live/hermes-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ test("hermes-e2e: install.sh onboards Hermes and proves health plus live inferen

await assertHermesCliAdapterLiveContract({
env: commandEnv(),
host,
redactionValues,
sandbox,
sandboxName: SANDBOX_NAME,
Expand Down
1 change: 1 addition & 0 deletions test/hermes-doctor-config-hash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ describe("Hermes doctor and config hash boundary", () => {
path.join(libDir, "gateway-supervisor.sh"),
path.join(libDir, "validate-hermes-env-secret-boundary.py"),
path.join(libDir, "patch-hermes-session-list-preview.py"),
path.join(libDir, "patch-hermes-sqlite-temp-store.py"),
discordRecoveryPatcherPath,
profilePolicyPatcherPath,
managedPolicyReaderPath,
Expand Down
6 changes: 6 additions & 0 deletions test/hermes-final-image-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const HERMES_INTEGRITY_FILES = [
source: "agents/hermes/image-build-probes.py",
target: "/opt/nemoclaw-hermes-config/image-build-probes.py",
},
{
arg: "NEMOCLAW_HERMES_SQLITE_TEMP_STORE_PATCHER_SHA256",
source: "agents/hermes/patch-hermes-sqlite-temp-store.py",
target: "/usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py",
},
{
arg: "NEMOCLAW_HERMES_WRAPPER_SHA256",
source: "agents/hermes/hermes-wrapper.py",
Expand Down Expand Up @@ -277,6 +282,7 @@ describe("Hermes final image layout", () => {
"COPY scripts/managed-gateway-control.py /usr/local/lib/nemoclaw/managed-gateway-control.py",
"COPY agents/hermes/validate-env-secret-boundary.py /usr/local/lib/nemoclaw/validate-hermes-env-secret-boundary.py",
"COPY agents/hermes/patch-session-list-preview.py /usr/local/lib/nemoclaw/patch-hermes-session-list-preview.py",
"COPY agents/hermes/patch-hermes-sqlite-temp-store.py /usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py",
"COPY agents/hermes/patch-discord-recovery-permissions.py /usr/local/lib/nemoclaw/patch-hermes-discord-recovery-permissions.py",
"COPY agents/hermes/patch-profile-policy-defaults.py /usr/local/lib/nemoclaw/patch-hermes-profile-policy-defaults.py",
"COPY agents/hermes/managed_policy.py /usr/local/lib/nemoclaw/managed_policy.py",
Expand Down
89 changes: 89 additions & 0 deletions test/hermes-sqlite-temp-store-patch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { afterEach, describe, expect, it } from "vitest";

const root = path.join(import.meta.dirname, "..");
const patcher = path.join(root, "agents", "hermes", "patch-hermes-sqlite-temp-store.py");
const dockerfile = fs.readFileSync(path.join(root, "agents", "hermes", "Dockerfile"), "utf8");
const fixtures: string[] = [];

const walSetup = 'apply_wal_with_fallback(self._conn, db_label="state.db")';
const tempStore = ' self._conn.execute("PRAGMA temp_store=MEMORY")';
const foreignKeys = ' self._conn.execute("PRAGMA foreign_keys=ON")';
const unpatchedSource = `${walSetup}\n${foreignKeys}\n`;
const patchedSource = `${walSetup}\n${tempStore}\n${foreignKeys}\n`;

function fixtureFile(source: string): string {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-hermes-sqlite-temp-store-"));
fixtures.push(fixture);
const stateModule = path.join(fixture, "hermes_state.py");
fs.writeFileSync(stateModule, source);
return stateModule;
}

function runPatcher(stateModule: string) {
return spawnSync("python3", ["-I", patcher, stateModule], {
encoding: "utf8",
timeout: 5000,
});
}

afterEach(() => {
for (const fixture of fixtures.splice(0)) {
fs.rmSync(fixture, { recursive: true, force: true });
}
});

describe("Hermes SQLite temp-store patch", () => {
it("inserts in-memory temp storage before foreign-key enforcement (#8301)", () => {
const stateModule = fixtureFile(unpatchedSource);

const result = runPatcher(stateModule);

expect(result.status, result.stderr).toBe(0);
expect(fs.readFileSync(stateModule, "utf8")).toBe(patchedSource);
});

it("accepts one already-patched connection block (#8301)", () => {
const stateModule = fixtureFile(patchedSource);

const result = runPatcher(stateModule);

expect(result.status, result.stderr).toBe(0);
expect(fs.readFileSync(stateModule, "utf8")).toBe(patchedSource);
});

it.each([
["duplicate", `${patchedSource}${tempStore}\n`],
["partial", `${walSetup}\n${tempStore}\n`],
["misplaced", `${tempStore}\n${unpatchedSource}`],
])("rejects a %s temp-store patch (#8301)", (_case, source) => {
const stateModule = fixtureFile(source);

const result = runPatcher(stateModule);

expect(result.status).toBe(1);
expect(result.stderr).toContain("Hermes SessionDB.__init__ connection setup shape changed");
expect(fs.readFileSync(stateModule, "utf8")).toBe(source);
});

it("binds the Hermes image to the reviewed patcher (#8301)", () => {
const digest = createHash("sha256").update(fs.readFileSync(patcher)).digest("hex");

expect(dockerfile).toContain(`ARG NEMOCLAW_HERMES_SQLITE_TEMP_STORE_PATCHER_SHA256=${digest}`);
expect(dockerfile).toContain(
"COPY agents/hermes/patch-hermes-sqlite-temp-store.py " +
"/usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py",
);
expect(dockerfile).toContain(
"RUN /usr/bin/python3 -I /usr/local/lib/nemoclaw/patch-hermes-sqlite-temp-store.py",
);
});
});
1 change: 1 addition & 0 deletions test/sandbox-provisioning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,7 @@ describe("Hermes sandbox provisioning", () => {
path.join(localLib, "sandbox-init.sh"),
path.join(localLib, "validate-hermes-env-secret-boundary.py"),
path.join(localLib, "patch-hermes-session-list-preview.py"),
path.join(localLib, "patch-hermes-sqlite-temp-store.py"),
path.join(localLib, "patch-hermes-discord-recovery-permissions.py"),
path.join(localLib, "patch-hermes-profile-policy-defaults.py"),
managedPolicyReaderPath,
Expand Down
Loading
Loading