hermes: make Claude setup-token access durable
This commit is contained in:
parent
5e0b0d9cd6
commit
8acb99d100
@ -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"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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__}",
|
||||
|
||||
@ -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" \
|
||||
|
||||
16
services/hermes/scripts/claude_oauth_exec
Executable file
16
services/hermes/scripts/claude_oauth_exec
Executable file
@ -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" "$@"
|
||||
@ -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(
|
||||
|
||||
@ -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}",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user