From 8acb99d1007b847c3b95422bef007c3934429b4f Mon Sep 17 00:00:00 2001 From: jenkins Date: Sat, 22 Aug 2026 22:52:15 -0300 Subject: [PATCH] hermes: make Claude setup-token access durable --- services/hermes/agent-deployment.yaml | 2 +- .../hermes/execution-worker-statefulset.yaml | 36 +++++++------- services/hermes/kustomization.yaml | 2 + services/hermes/scripts/ai_usage_exporter.py | 27 +++++++++-- services/hermes/scripts/claude | 9 ++-- services/hermes/scripts/claude_oauth_exec | 16 +++++++ .../hermes/scripts/execution_pool_worker.py | 22 +++++++-- .../hermes/scripts/stage_runtime_access.py | 10 ++-- .../test_hermes_ai_usage_exporter_coverage.py | 31 ++++++++++++ .../test_hermes_execution_pool_worker_v2.py | 9 ++-- testing/tests/test_hermes_runtime_access.py | 48 +++++++++++++++---- 11 files changed, 160 insertions(+), 52 deletions(-) create mode 100755 services/hermes/scripts/claude_oauth_exec diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml index 880763bf..50115045 100644 --- a/services/hermes/agent-deployment.yaml +++ b/services/hermes/agent-deployment.yaml @@ -25,7 +25,7 @@ spec: ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available - ai.bstein.dev/config-rev: "20260822-claude-oauth-self-heal-v1" + ai.bstein.dev/config-rev: "20260822-claude-setup-token-v2" prometheus.io/scrape: "true" prometheus.io/path: /metrics prometheus.io/port: "9010" diff --git a/services/hermes/execution-worker-statefulset.yaml b/services/hermes/execution-worker-statefulset.yaml index 33f13f23..e885e985 100644 --- a/services/hermes/execution-worker-statefulset.yaml +++ b/services/hermes/execution-worker-statefulset.yaml @@ -27,28 +27,17 @@ spec: ai.bstein.dev/role: fenced-execution-only ai.bstein.dev/scm-boundary: mediated-pr14-broker-with-completion-gates ai.bstein.dev/model-policy: Switchyard AUTO with cross-provider fallback - ai.bstein.dev/storage: separate durable RWO workspace and OAuth refresh ownership per ordinal + ai.bstein.dev/storage: durable workspace and ordinal-private Codex refresh state + ai.bstein.dev/config-rev: "20260822-claude-setup-token-v1" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: hermes-execution-worker vault.hashicorp.com/agent-inject-containers: stage-worker-access vault.hashicorp.com/agent-service-account-token-volume-name: vault-auth-token - vault.hashicorp.com/agent-inject-secret-claude-credentials-0: kv/data/atlas/hermes/agent-tokens - vault.hashicorp.com/agent-inject-perms-claude-credentials-0: "0600" - vault.hashicorp.com/agent-inject-template-claude-credentials-0: | + vault.hashicorp.com/agent-inject-secret-claude-oauth-token: kv/data/atlas/hermes/agent-tokens + vault.hashicorp.com/agent-inject-perms-claude-oauth-token: "0600" + vault.hashicorp.com/agent-inject-template-claude-oauth-token: | {{- with secret "kv/data/atlas/hermes/agent-tokens" -}} - {{ .Data.data.execution_worker_0_claude_credentials_json }} - {{- end }} - vault.hashicorp.com/agent-inject-secret-claude-credentials-1: kv/data/atlas/hermes/agent-tokens - vault.hashicorp.com/agent-inject-perms-claude-credentials-1: "0600" - vault.hashicorp.com/agent-inject-template-claude-credentials-1: | - {{- with secret "kv/data/atlas/hermes/agent-tokens" -}} - {{ .Data.data.execution_worker_1_claude_credentials_json }} - {{- end }} - vault.hashicorp.com/agent-inject-secret-claude-credentials-2: kv/data/atlas/hermes/agent-tokens - vault.hashicorp.com/agent-inject-perms-claude-credentials-2: "0600" - vault.hashicorp.com/agent-inject-template-claude-credentials-2: | - {{- with secret "kv/data/atlas/hermes/agent-tokens" -}} - {{ .Data.data.execution_worker_2_claude_credentials_json }} + {{ .Data.data.claude_oauth_token }} {{- end }} vault.hashicorp.com/agent-inject-secret-codex-auth-0: kv/data/atlas/hermes/agent-tokens vault.hashicorp.com/agent-inject-perms-codex-auth-0: "0600" @@ -133,6 +122,7 @@ spec: runAsGroup: 0 seccompProfile: {type: RuntimeDefault} volumeMounts: + - {name: claude-oauth-access, mountPath: /claude-oauth-access} - {name: workspace, mountPath: /workspace} - {name: provider-access, mountPath: /provider-access} - {name: coordinator, mountPath: /opt/coordinator, readOnly: true} @@ -188,6 +178,9 @@ spec: - {name: HOME, value: /worker-data/home} - {name: CODEX_HOME, value: /provider-access/codex} - {name: CLAUDE_CONFIG_DIR, value: /provider-access/claude} + - {name: CLAUDE_CODE_OAUTH_TOKEN_FILE, value: /claude-oauth-access/token} + - {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude_oauth_exec} + - {name: HERMES_CLAUDE_NATIVE_BIN, value: /worker-data/tools/bin/claude} - {name: HERMES_AUTO_ROUTER_PROFILE, value: agent} - {name: PYTHONPATH, value: /opt/hermes} - {name: PATH, value: /worker-data/tools/bin:/opt/coordinator:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin} @@ -201,12 +194,12 @@ spec: fieldPath: spec.nodeName startupProbe: exec: - command: [/bin/sh, -ec, "test -w /workspace && test -w /provider-access/codex/auth.json && test -w /provider-access/claude/.credentials.json"] + command: [/bin/sh, -ec, "test -w /workspace && test -w /provider-access/codex/auth.json && test -r /claude-oauth-access/token"] periodSeconds: 5 failureThreshold: 60 readinessProbe: exec: - command: [/bin/sh, -ec, "test -w /workspace && test -w /provider-access/codex/auth.json"] + command: [/bin/sh, -ec, "test -w /workspace && test -w /provider-access/codex/auth.json && test -r /claude-oauth-access/token"] periodSeconds: 10 securityContext: allowPrivilegeEscalation: false @@ -217,6 +210,7 @@ spec: runAsGroup: 10000 seccompProfile: {type: RuntimeDefault} volumeMounts: + - {name: claude-oauth-access, mountPath: /claude-oauth-access, readOnly: true} - {name: workspace, mountPath: /workspace} - {name: worker-data, mountPath: /worker-data} - {name: tools, mountPath: /worker-data/tools, readOnly: true} @@ -227,6 +221,8 @@ spec: requests: {cpu: 5m, memory: 128Mi, ephemeral-storage: 1Gi} limits: {cpu: "2", memory: 4Gi, ephemeral-storage: 8Gi} volumes: + - name: claude-oauth-access + emptyDir: {medium: Memory, sizeLimit: 1Mi} - name: worker-data emptyDir: {sizeLimit: 128Mi} - name: coordinator @@ -278,7 +274,7 @@ spec: resources: requests: storage: 2Gi - # OAuth refresh ownership is single-mounter by design: keep it ReadWriteOnce. + # Codex refresh state and Claude session state remain ordinal-private. - metadata: name: provider-access labels: diff --git a/services/hermes/kustomization.yaml b/services/hermes/kustomization.yaml index 995ab793..e5a4caef 100644 --- a/services/hermes/kustomization.yaml +++ b/services/hermes/kustomization.yaml @@ -65,6 +65,7 @@ configMapGenerator: - name: hermes-execution-pool namespace: hermes files: + - claude_oauth_exec=scripts/claude_oauth_exec - cli_lane_board.py=scripts/cli_lane_board.py - cli_lane_capabilities.py=scripts/cli_lane_capabilities.py - cli_lane_config.py=scripts/cli_lane_config.py @@ -105,6 +106,7 @@ configMapGenerator: - ai_usage_exporter.py=scripts/ai_usage_exporter.py - ai_usage_http.py=scripts/ai_usage_http.py - claude=scripts/claude + - claude_oauth_exec=scripts/claude_oauth_exec - claude_command_policy.py=scripts/claude_command_policy.py - cli_lane_board.py=scripts/cli_lane_board.py - cli_lane_capabilities.py=scripts/cli_lane_capabilities.py diff --git a/services/hermes/scripts/ai_usage_exporter.py b/services/hermes/scripts/ai_usage_exporter.py index d1729087..0b93b686 100644 --- a/services/hermes/scripts/ai_usage_exporter.py +++ b/services/hermes/scripts/ai_usage_exporter.py @@ -54,6 +54,10 @@ POLL_STARTUP_GRACE_SECONDS = 45 POLL_PROGRESS_BUDGET_SECONDS = 60 +class QuotaNotExposed(RuntimeError): + """The active provider credential cannot expose account quota.""" + + @dataclass(frozen=True) class Sample: """One Prometheus gauge sample.""" @@ -273,10 +277,20 @@ def _provider_authenticated(provider: str) -> bool: def query_claude() -> dict[str, Any]: """Read Claude account quota with the runtime OAuth token held only in memory.""" - document = json.loads(CLAUDE_CREDENTIALS.read_text(encoding="utf-8")) - token = document.get("claudeAiOauth", {}).get("accessToken") + try: + document = json.loads(CLAUDE_CREDENTIALS.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise QuotaNotExposed("Claude runtime credentials are incomplete") from error + oauth = document.get("claudeAiOauth", {}) + token = oauth.get("accessToken") if isinstance(oauth, dict) else None if not isinstance(token, str) or not token: - raise RuntimeError("Claude runtime credentials are incomplete") + raise QuotaNotExposed("Claude runtime credentials are incomplete") + expires_at = _number(oauth.get("expiresAt")) + if expires_at is not None: + if expires_at > 10_000_000_000: + expires_at /= 1000 + if expires_at <= time.time(): + raise QuotaNotExposed("Claude quota credential has expired") request = Request( CLAUDE_USAGE_URL, headers={ @@ -339,6 +353,13 @@ class Collector: samples = parse_claude_payload(query_claude()) else: raise ValueError("unknown provider") + except QuotaNotExposed: + self.record_failure( + provider, + started=started, + monotonic_started=monotonic_started, + ) + return except Exception as error: print( f"{provider} quota collection deferred: {type(error).__name__}", diff --git a/services/hermes/scripts/claude b/services/hermes/scripts/claude index 4803cd14..2acf2261 100755 --- a/services/hermes/scripts/claude +++ b/services/hermes/scripts/claude @@ -4,13 +4,10 @@ set -eu umask 077 config_dir="${CLAUDE_CONFIG_DIR:-/runtime-access/claude}" -oauth_token_file="${CLAUDE_CODE_OAUTH_TOKEN_FILE:-/claude-oauth-access/token}" -if [ -s "$oauth_token_file" ]; then - CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$oauth_token_file")" - export CLAUDE_CODE_OAUTH_TOKEN -fi +HERMES_CLAUDE_NATIVE_BIN="${HERMES_CLAUDE_NATIVE_BIN:-/opt/data/tools/bin/claude}" +export HERMES_CLAUDE_NATIVE_BIN -exec /opt/data/tools/bin/claude \ +exec /opt/coordinator/claude_oauth_exec \ --dangerously-skip-permissions \ --autocompact auto \ --settings "$config_dir/settings.json" \ diff --git a/services/hermes/scripts/claude_oauth_exec b/services/hermes/scripts/claude_oauth_exec new file mode 100755 index 00000000..82e62214 --- /dev/null +++ b/services/hermes/scripts/claude_oauth_exec @@ -0,0 +1,16 @@ +#!/bin/sh +# Inject the Vault-mounted Claude setup token only into the native CLI process. +set -eu +umask 077 + +native_bin="${HERMES_CLAUDE_NATIVE_BIN:-/opt/data/tools/bin/claude}" +oauth_token_file="${CLAUDE_CODE_OAUTH_TOKEN_FILE:-/claude-oauth-access/token}" + +test -x "$native_bin" +test -s "$oauth_token_file" +CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$oauth_token_file")" +test -n "$CLAUDE_CODE_OAUTH_TOKEN" +export CLAUDE_CODE_OAUTH_TOKEN +unset ANTHROPIC_API_KEY CLAUDE_API_KEY + +exec "$native_bin" "$@" diff --git a/services/hermes/scripts/execution_pool_worker.py b/services/hermes/scripts/execution_pool_worker.py index c98dfd56..e00a2eee 100644 --- a/services/hermes/scripts/execution_pool_worker.py +++ b/services/hermes/scripts/execution_pool_worker.py @@ -407,11 +407,23 @@ def readiness() -> None: for path in (ROOT, ROOT / "provider-state", cli_lane_runner.DATA_ROOT): if not path.is_dir() or not os.access(path, os.W_OK): raise ProtocolError(f"durable worker path is not writable: {path}") - for credential in ( - Path(os.environ.get("CODEX_HOME", "/runtime-access/codex")) / "auth.json", - Path(os.environ.get("CLAUDE_CONFIG_DIR", "/runtime-access/claude")) / ".credentials.json", - ): - if not credential.is_file() or not os.access(credential, os.W_OK): + credentials = ( + ( + Path(os.environ.get("CODEX_HOME", "/runtime-access/codex")) + / "auth.json", + os.W_OK, + ), + ( + Path( + os.environ.get( + "CLAUDE_CODE_OAUTH_TOKEN_FILE", "/claude-oauth-access/token" + ) + ), + os.R_OK, + ), + ) + for credential, access in credentials: + if not credential.is_file() or not os.access(credential, access): raise ProtocolError(f"subscription credential is unavailable or read-only: {credential.name}") cli_lane_runner.RESULT_SCHEMA_PATH.parent.mkdir(parents=True, exist_ok=True) atomic_json( diff --git a/services/hermes/scripts/stage_runtime_access.py b/services/hermes/scripts/stage_runtime_access.py index 627e88db..06dd5b9d 100644 --- a/services/hermes/scripts/stage_runtime_access.py +++ b/services/hermes/scripts/stage_runtime_access.py @@ -291,7 +291,7 @@ def _durable_link(runtime: Path, durable: Path, name: str) -> None: def stage_execution_worker() -> None: - """Bootstrap and preserve ordinal-private subscription refresh ownership.""" + """Stage the shared Claude setup token and ordinal-private Codex refresh state.""" provider_state = WORKER_ROOT / "provider-state" for path in ( provider_state, @@ -303,10 +303,10 @@ def stage_execution_worker() -> None: ordinal = int(os.environ.get("HERMES_WORKER_ORDINAL", "-1")) if ordinal not in range(3): raise RuntimeError("execution worker ordinal is invalid") - _bootstrap_json( - f"claude-credentials-{ordinal}", - PROVIDER_ACCESS_ROOT / "claude" / ".credentials.json", - ("claudeAiOauth", "refreshToken"), + _owned_directory(CLAUDE_OAUTH_ACCESS_ROOT) + _copy_secret( + "claude-oauth-token", + CLAUDE_OAUTH_ACCESS_ROOT / "token", ) _bootstrap_json( f"codex-auth-{ordinal}", diff --git a/testing/tests/test_hermes_ai_usage_exporter_coverage.py b/testing/tests/test_hermes_ai_usage_exporter_coverage.py index d9bf1336..6249235c 100644 --- a/testing/tests/test_hermes_ai_usage_exporter_coverage.py +++ b/testing/tests/test_hermes_ai_usage_exporter_coverage.py @@ -133,6 +133,37 @@ def test_provider_queries_validate_credentials_and_response_shape( assert mod.query_codex is mod.codex_query.query_codex +def test_expired_claude_quota_credential_is_not_sent_or_logged( + tmp_path, monkeypatch, capsys +): + mod = load_module() + credentials = tmp_path / "credentials.json" + credentials.write_text( + json.dumps( + { + "claudeAiOauth": { + "accessToken": "revoked-runtime-token", + "expiresAt": 1_700_000_000_000, + } + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(mod, "CLAUDE_CREDENTIALS", credentials) + monkeypatch.setattr(mod.time, "time", lambda: 1_800_000_000) + monkeypatch.setattr( + mod, + "urlopen", + lambda *_args, **_kwargs: pytest.fail("expired credentials must not be sent"), + ) + + collector = mod.Collector() + collector.refresh_provider("anthropic") + + assert collector._providers["anthropic"].fetch_success is False + assert "revoked-runtime-token" not in capsys.readouterr().out + + def test_provider_authentication_uses_only_fresh_non_secret_health(tmp_path, monkeypatch): mod = load_module() monkeypatch.setattr(mod, "PROVIDER_HEALTH_ROOT", tmp_path) diff --git a/testing/tests/test_hermes_execution_pool_worker_v2.py b/testing/tests/test_hermes_execution_pool_worker_v2.py index a1984e56..80a2c153 100644 --- a/testing/tests/test_hermes_execution_pool_worker_v2.py +++ b/testing/tests/test_hermes_execution_pool_worker_v2.py @@ -262,18 +262,19 @@ def test_readiness_checks_ordinal_paths_credentials_and_mediator(tmp_path, monke worker_root = tmp_path / "worker" data_root = tmp_path / "data" codex = tmp_path / "codex" - claude = tmp_path / "claude" - for path in (worker_root, worker_root / "provider-state", data_root, codex, claude): + claude_token = tmp_path / "claude-oauth/token" + for path in (worker_root, worker_root / "provider-state", data_root, codex): path.mkdir(parents=True, exist_ok=True) (codex / "auth.json").write_text("{}") - (claude / ".credentials.json").write_text("{}") + claude_token.parent.mkdir() + claude_token.write_text("setup-token") schema = tmp_path / "schema/result.json" monkeypatch.setattr(worker, "ORDINAL", 0) monkeypatch.setattr(worker, "ROOT", worker_root) monkeypatch.setattr(worker.cli_lane_runner, "DATA_ROOT", data_root) monkeypatch.setattr(worker.cli_lane_runner, "RESULT_SCHEMA_PATH", schema) monkeypatch.setenv("CODEX_HOME", str(codex)) - monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(claude)) + monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN_FILE", str(claude_token)) polled = [] monkeypatch.setattr(worker, "_poll", lambda: polled.append(True)) worker.readiness() diff --git a/testing/tests/test_hermes_runtime_access.py b/testing/tests/test_hermes_runtime_access.py index 8ec89cc4..4c2a126e 100644 --- a/testing/tests/test_hermes_runtime_access.py +++ b/testing/tests/test_hermes_runtime_access.py @@ -158,11 +158,10 @@ def test_execution_worker_and_mediator_separate_credentials_and_hmac( runtime = tmp_path / "runtime" worker = tmp_path / "worker" provider_access = tmp_path / "provider-access" + claude_oauth_access = tmp_path / "claude-oauth-access" pool_access = tmp_path / "pool-access" vault.mkdir() - (vault / "claude-credentials-1").write_text( - json.dumps({"claudeAiOauth": {"refreshToken": "claude-refresh"}}) - ) + (vault / "claude-oauth-token").write_text("claude-setup-token") (vault / "codex-auth-1").write_text( json.dumps({"tokens": {"refresh_token": "codex-refresh"}}) ) @@ -170,6 +169,7 @@ def test_execution_worker_and_mediator_separate_credentials_and_hmac( monkeypatch.setattr(stage, "RUNTIME_ROOT", runtime) monkeypatch.setattr(stage, "WORKER_ROOT", worker) monkeypatch.setattr(stage, "PROVIDER_ACCESS_ROOT", provider_access) + monkeypatch.setattr(stage, "CLAUDE_OAUTH_ACCESS_ROOT", claude_oauth_access) monkeypatch.setattr(stage, "POOL_ACCESS_ROOT", pool_access) monkeypatch.setenv("HERMES_WORKER_ORDINAL", "1") monkeypatch.setattr(stage.os, "chown", lambda *_args: None) @@ -181,6 +181,10 @@ def test_execution_worker_and_mediator_separate_credentials_and_hmac( assert not (runtime / "execution-pool-key").exists() assert not (runtime / "codex/sessions").exists() assert not (runtime / "claude/projects").exists() + assert (claude_oauth_access / "token").read_text().strip() == ( + "claude-setup-token" + ) + assert not (provider_access / "claude/.credentials.json").exists() assert (provider_access / "codex/auth.json").stat().st_mode & 0o777 == 0o600 refreshed = {"tokens": {"refresh_token": "provider-rotated"}} (provider_access / "codex/auth.json").write_text(json.dumps(refreshed)) @@ -203,13 +207,14 @@ def test_execution_worker_fails_closed_without_channel_credential( stage = _load("stage_runtime_access") vault = tmp_path / "vault" vault.mkdir() - (vault / "claude-credentials-0").write_text( - json.dumps({"claudeAiOauth": {"refreshToken": "claude-refresh"}}) - ) + (vault / "claude-oauth-token").write_text("claude-setup-token") monkeypatch.setattr(stage, "VAULT_ROOT", vault) monkeypatch.setattr(stage, "RUNTIME_ROOT", tmp_path / "runtime") monkeypatch.setattr(stage, "WORKER_ROOT", tmp_path / "worker") monkeypatch.setattr(stage, "PROVIDER_ACCESS_ROOT", tmp_path / "provider-access") + monkeypatch.setattr( + stage, "CLAUDE_OAUTH_ACCESS_ROOT", tmp_path / "claude-oauth-access" + ) monkeypatch.setenv("HERMES_WORKER_ORDINAL", "0") monkeypatch.setattr(stage.os, "chown", lambda *_args: None) monkeypatch.setattr(stage.os, "fchown", lambda *_args: None) @@ -359,7 +364,11 @@ def test_runtime_refresh_sync_restores_incomplete_credentials_from_vault( def test_claude_clients_use_the_vault_staged_long_lived_token(): wrapper = (SCRIPTS / "claude").read_text(encoding="utf-8") + oauth_exec = (SCRIPTS / "claude_oauth_exec").read_text(encoding="utf-8") deployment = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text()) + worker = yaml.safe_load( + (HERMES / "execution-worker-statefulset.yaml").read_text() + ) runner = next( item for item in deployment["spec"]["template"]["spec"]["containers"] @@ -367,9 +376,32 @@ def test_claude_clients_use_the_vault_staged_long_lived_token(): ) environment = {item["name"]: item["value"] for item in runner["env"]} - assert 'CLAUDE_CODE_OAUTH_TOKEN_FILE:-/claude-oauth-access/token' in wrapper - assert 'export CLAUDE_CODE_OAUTH_TOKEN' in wrapper + assert "/opt/coordinator/claude_oauth_exec" in wrapper + assert 'CLAUDE_CODE_OAUTH_TOKEN_FILE:-/claude-oauth-access/token' in oauth_exec + assert 'export CLAUDE_CODE_OAUTH_TOKEN' in oauth_exec assert environment["HERMES_CLAUDE_BIN"] == "/opt/coordinator/claude" + worker_pod = worker["spec"]["template"]["spec"] + worker_annotations = worker["spec"]["template"]["metadata"]["annotations"] + assert worker_annotations[ + "vault.hashicorp.com/agent-inject-secret-claude-oauth-token" + ] == "kv/data/atlas/hermes/agent-tokens" + assert not any("claude-credentials" in key for key in worker_annotations) + worker_container = worker_pod["containers"][0] + worker_environment = { + item["name"]: item["value"] + for item in worker_container["env"] + if "value" in item + } + assert worker_environment["HERMES_CLAUDE_BIN"] == ( + "/opt/coordinator/claude_oauth_exec" + ) + assert worker_environment["HERMES_CLAUDE_NATIVE_BIN"] == ( + "/worker-data/tools/bin/claude" + ) + oauth_volume = next( + item for item in worker_pod["volumes"] if item["name"] == "claude-oauth-access" + ) + assert oauth_volume["emptyDir"] == {"medium": "Memory", "sizeLimit": "1Mi"} def test_subprocess_patches_strip_and_redact_runtime_credentials(tmp_path: Path):