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
12 changes: 6 additions & 6 deletions agents/hermes/runtime-config-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4754,17 +4754,17 @@ def _is_generated_api_server_key(value: str) -> bool:
def _placeholder_suffix_matches_env_key(suffix: str, env_key: str) -> bool:
if suffix == env_key:
return True
revision_match = re.fullmatch(r"v[0-9]+_(.+)", suffix)
revision_match = re.fullmatch(r"v[0-9]{1,20}_(.+)", suffix)
return revision_match is not None and revision_match.group(1) == env_key
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def _normalize_provider_placeholder_for_env_key(value: str, env_key: str) -> str | None:
def _provider_placeholder_for_env_key(value: str, env_key: str) -> str | None:
if not value.startswith(SCOPED_PLACEHOLDER_PREFIX):
return None
suffix = value[len(SCOPED_PLACEHOLDER_PREFIX) :]
if not _placeholder_suffix_matches_env_key(suffix, env_key):
return None
return f"{SCOPED_PLACEHOLDER_PREFIX}{env_key}"
return value


def _has_env_control_chars(value: str) -> bool:
Expand Down Expand Up @@ -4971,11 +4971,11 @@ def provider_placeholders(
for key in allowed_fallback_keys:
if key in replacements:
continue
normalized = _normalize_provider_placeholder_for_env_key(
placeholder = _provider_placeholder_for_env_key(
os.environ.get(key, ""), key
)
if normalized:
replacements[key] = (normalized, "")
if placeholder:
replacements[key] = (placeholder, "")
if not replacements:
return

Expand Down
97 changes: 86 additions & 11 deletions test/hermes-runtime-api-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ function runHermesRuntimeProviderPlaceholderRefresh(opts: {
runtimePlan?: unknown;
runtimePlanPathKind?: "regular" | "symlink" | "hardlink" | "groupWritable" | "worldWritable";
hashFileContent?: string;
locked?: boolean;
}) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-hermes-provider-placeholders-"));
const hermesHome = path.join(tmpDir, ".hermes");
Expand All @@ -307,6 +308,8 @@ function runHermesRuntimeProviderPlaceholderRefresh(opts: {
opts.hashFileContent === undefined
? writeHermesHash(hashPath, configPath, envPath)
: fs.writeFileSync(hashPath, opts.hashFileContent);
const lockEnvFile = opts.locked === true ? () => fs.chmodSync(envPath, 0o444) : undefined;
lockEnvFile?.();
const runtimePlanText = `${JSON.stringify(opts.runtimePlan, null, 2)}\n`;
const writeRuntimePlanPath = {
regular: () => fs.writeFileSync(runtimePlanPath, runtimePlanText),
Expand Down Expand Up @@ -579,7 +582,7 @@ describe("agents/hermes/start.sh runtime API server key", () => {
expect(run.strictHashValid).toBe(true);
});

it("normalizes versioned provider placeholders from the runtime env before refreshing .env", () => {
it("preserves the exact OpenShell provider placeholder generation in Hermes .env (#8893)", () => {
for (const envFile of [
"DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN\n",
"DISCORD_BOT_TOKEN=openshell:resolve:env:v111_DISCORD_BOT_TOKEN\n",
Expand All @@ -593,9 +596,8 @@ describe("agents/hermes/start.sh runtime API server key", () => {

expect(run.result.status, run.result.stderr).toBe(0);
expect(run.envFileContent).toContain(
"DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN\n",
"DISCORD_BOT_TOKEN=openshell:resolve:env:v222_DISCORD_BOT_TOKEN\n",
);
expect(run.envFileContent).not.toContain("v222_DISCORD_BOT_TOKEN");
expect(run.envFileContent).not.toContain("v111_DISCORD_BOT_TOKEN");
expect(run.strictHashValid).toBe(true);
}
Expand Down Expand Up @@ -630,7 +632,7 @@ describe("agents/hermes/start.sh runtime API server key", () => {
expect(run.envFileContent).toContain(`API_SERVER_KEY=${apiServerKey}\n`);
expect(run.envFileContent).toContain("UNRELATED_VALUE=stable-value\n");
expect(run.envFileContent).toContain(
"DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN\n",
"DISCORD_BOT_TOKEN=openshell:resolve:env:v222_DISCORD_BOT_TOKEN\n",
);
expect(run.envFileContent).not.toContain("API_SERVER_KEY=openshell:resolve:env:API_SERVER_KEY");
expect(run.envFileContent).not.toContain(
Expand Down Expand Up @@ -661,11 +663,10 @@ describe("agents/hermes/start.sh runtime API server key", () => {
[
"API_SERVER_PORT=18642",
"API_SERVER_HOST=127.0.0.1",
"DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN",
"DISCORD_BOT_TOKEN=openshell:resolve:env:v101_DISCORD_BOT_TOKEN",
"",
].join("\n"),
);
expect(run.envFileContent).not.toContain("openshell:resolve:env:v101_DISCORD_BOT_TOKEN");
expect(run.strictHashValid).toBe(true);
});

Expand Down Expand Up @@ -694,20 +695,31 @@ describe("agents/hermes/start.sh runtime API server key", () => {
expect(run.result.status, run.result.stderr).toBe(0);
expect(run.envFileContent).toBe(
[
"export DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN",
"export DISCORD_BOT_TOKEN=openshell:resolve:env:v222_DISCORD_BOT_TOKEN",
"API_SERVER_PORT=18642",
"",
].join("\n"),
);
expect(run.strictHashValid).toBe(true);
});

it("does not rewrite provider placeholders when .env is already canonical", () => {
it.each([
{
name: "canonical",
value: "openshell:resolve:env:DISCORD_BOT_TOKEN",
},
{
name: "revisioned",
value: "openshell:resolve:env:v101_DISCORD_BOT_TOKEN",
},
])("does not rewrite an exact $name runtime placeholder already persisted in .env (#8893)", ({
value,
}) => {
const hashFileContent = "sentinel\n";
const run = runHermesRuntimeProviderPlaceholderRefresh({
envFile: "DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN\n",
envFile: `DISCORD_BOT_TOKEN=${value}\n`,
envOverrides: {
DISCORD_BOT_TOKEN: "openshell:resolve:env:v101_DISCORD_BOT_TOKEN",
DISCORD_BOT_TOKEN: value,
},
runtimePlan: {
schemaVersion: 1,
Expand All @@ -722,10 +734,73 @@ describe("agents/hermes/start.sh runtime API server key", () => {
});

expect(run.result.status, run.result.stderr).toBe(0);
expect(run.envFileContent).toBe("DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN\n");
expect(run.envFileContent).toBe(`DISCORD_BOT_TOKEN=${value}\n`);
expect(run.strictHashContent).toBe(hashFileContent);
});

it.each([
{
name: "a placeholder for another environment key",
value: "openshell:resolve:env:v222_OTHER_TOKEN",
},
{
name: "an overlong credential revision",
value: `openshell:resolve:env:v${"1".repeat(21)}_DISCORD_BOT_TOKEN`,
},
{
name: "a raw credential",
value: "raw-discord-token",
},
])("ignores $name from the runtime environment (#8893)", ({ value }) => {
const originalEnv = "DISCORD_BOT_TOKEN=openshell:resolve:env:v111_DISCORD_BOT_TOKEN\n";
const run = runHermesRuntimeProviderPlaceholderRefresh({
envFile: originalEnv,
envOverrides: { DISCORD_BOT_TOKEN: value },
runtimePlan: {
schemaVersion: 1,
sandboxName: "test-sandbox",
agent: "hermes",
channels: [{ channelId: "discord", active: true, disabled: false }],
disabledChannels: [],
credentialBindings: [{ channelId: "discord", providerEnvKey: "DISCORD_BOT_TOKEN" }],
runtimeSetup: { nodePreloads: [], envAliases: [], secretScans: [] },
},
});

expect(run.result.status, run.result.stderr).toBe(0);
expect(run.envFileContent).toBe(originalEnv);
expect(run.envFileContent).not.toContain(value);
expect(run.result.stderr).not.toContain(value);
expect(run.strictHashValid).toBe(true);
});

it("requires a rebuild to replace a sealed canonical placeholder with its runtime generation (#8893)", () => {
const originalEnv = "DISCORD_BOT_TOKEN=openshell:resolve:env:DISCORD_BOT_TOKEN\n";
const run = runHermesRuntimeProviderPlaceholderRefresh({
envFile: originalEnv,
envOverrides: {
DISCORD_BOT_TOKEN: "openshell:resolve:env:v222_DISCORD_BOT_TOKEN",
},
runtimePlan: {
schemaVersion: 1,
sandboxName: "test-sandbox",
agent: "hermes",
channels: [{ channelId: "discord", active: true, disabled: false }],
disabledChannels: [],
credentialBindings: [{ channelId: "discord", providerEnvKey: "DISCORD_BOT_TOKEN" }],
runtimeSetup: { nodePreloads: [], envAliases: [], secretScans: [] },
},
locked: true,
});

expect(run.result.status).toBe(1);
expect(run.result.stderr).toContain(
"cannot update provider placeholders while shields are up; rebuild or recreate the sandbox",
);
expect(run.envFileContent).toBe(originalEnv);
expect(run.strictHashValid).toBe(true);
});

it("uses manifest runtime aliases for Hermes Slack provider placeholders", () => {
const run = runHermesRuntimeProviderPlaceholderRefresh({
envFile: [
Expand Down
Loading