Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
2 changes: 1 addition & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,7 @@ RUN node --experimental-strip-types \
ARG NEMOCLAW_HERMES_CLI_ADAPTER_SHA256=3c769f05292b840424a8feec9a9eb441e393a48bf0212bd824b0db8a33a66331
ARG NEMOCLAW_HERMES_WRAPPER_SHA256=20de8ca3316dcd1e606ffb34a8adfd82f39e558882d3fbfa221da699cae712cc
ARG NEMOCLAW_HERMES_CLI_ADAPTER_VALIDATOR_SHA256=db4046e79e513eab67b069a8eda20167b8b65529cf26842531d2ad673c670330
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=007f49a2d0e107adeacc63e67d541d3308e2964cb6fc1b74c1e38de78d799f76
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=adc35f1ed8248eb87a76357795649e6301c0030d2e9ad5aa11e2a9520fc4afaf
ARG NEMOCLAW_HERMES_TIRITH_FINALIZER_SHA256=a1e6b1c53ab297569abb87c29d15c294d729e46005bfd022136b4c447a791819
ARG NEMOCLAW_HERMES_CRON_RESTORE_CONTROLLER_SHA256=dbd32832383f174878c08529947f5d81911152e1516b6f960824db3c7f7ac1e9
# hadolint ignore=DL4006
Expand Down
1 change: 1 addition & 0 deletions agents/hermes/validate-env-secret-boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def _validate_directory_descriptor(path: str, fd: int) -> tuple[int, int, int, i
allowed.update(
{
(sandbox_uid, sandbox_gid, 0o700),
(sandbox_uid, sandbox_gid, 0o750),
(sandbox_uid, sandbox_gid, 0o3770),
}
)
Expand Down
49 changes: 49 additions & 0 deletions test/agents/hermes/hermes-env-secret-boundary-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,55 @@ except module.UnsafeEnvInputError:
});

describe("Hermes env secret-boundary namespace pinning", () => {
const validateHermesRootMode = (mode: number) => {
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-env-legacy-")));
const sandbox = path.join(root, "sandbox");
const hermes = path.join(sandbox, ".hermes");
const envPath = path.join(hermes, ".env");
fs.mkdirSync(hermes, { recursive: true });
fs.chmodSync(sandbox, 0o770);
fs.chmodSync(hermes, mode);
fs.writeFileSync(envPath, "SAFE=1\n", { mode: 0o640 });
fs.chmodSync(envPath, 0o640);
try {
const result = spawnSync(
"python3",
[
"-c",
`import importlib.util, os, sys
spec = importlib.util.spec_from_file_location("validator", sys.argv[1])
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.__file__ = module.INSTALLED_BOUNDARY_VALIDATOR
module.INSTALLED_ENV_ROOT = sys.argv[2]
module.INSTALLED_ENV_PATH = sys.argv[3]
module._sandbox_identity = lambda: (os.geteuid(), os.getegid())
raise SystemExit(module.validate_env_file(sys.argv[3]))`,
VALIDATOR,
sandbox,
envPath,
],
{ encoding: "utf-8", timeout: 5000 },
);
return result;
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
};

it("accepts the legacy 0750 Hermes root before startup repairs its mode (#11110)", () => {
const result = validateHermesRootMode(0o750);
expect(result.status, result.stderr).toBe(0);
});

it("rejects adjacent 0755 while admitting the legacy Hermes root mode (#11170)", () => {
const result = validateHermesRootMode(0o755);
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(
/\/sandbox\/\.hermes does not match a trusted owner\/group\/mode posture/u,
);
});

it("anchors installed validation at sandbox when Landlock denies opening root", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-env-landlock-root-"));
const sandbox = path.join(root, "sandbox");
Expand Down
12 changes: 11 additions & 1 deletion test/onboarding/onboard-fresh-create-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ runner.run = (command, opts = {}) => {
return "";
};
const retainedRegistryEntry = recoveryReentry && fs.existsSync(${JSON.stringify(payloadPath)})
? JSON.parse(fs.readFileSync(${JSON.stringify(payloadPath)}, "utf8")).currentRegistryEntry
? JSON.parse(fs.readFileSync(${JSON.stringify(payloadPath)}, "utf8")).recoveryRegistryEntry
: null;
const registryMutationCalls = [];
let checkpointReadCalls = 0;
Expand Down Expand Up @@ -575,6 +575,13 @@ if (${JSON.stringify(
return;
}
if (recoveryReentry === "fresh-same-registry-only") {
if (!retainedRegistryEntry?.pendingCreateIdentity) {
throw new Error("missing verified create checkpoint for registry-only recovery");
}
registry.save({
defaultSandbox: null,
sandboxes: { "my-assistant": retainedRegistryEntry },
});
onboardModule.onboardSession.saveSession(
onboardModule.onboardSession.createSession({
sessionId: "replacement-session",
Expand Down Expand Up @@ -935,6 +942,9 @@ if (${JSON.stringify(
assert.deepEqual(reentryPayload.retainedRecoveryRecords, []);
}

// Refusal reentries replace the payload. Restore the original verified
// checkpoint before modeling the registry-only process boundary.
fs.writeFileSync(payloadPath, JSON.stringify(payload));
const registryOnlyReentry = spawnSync(process.execPath, [scriptPath], {
cwd: repoRoot,
encoding: "utf-8",
Expand Down
Loading