Repository navigation
feat(tier3): support Claude Code on Agent Platform and add active preflight probe - #140
kweinmeister wants to merge 33 commits into
Conversation
300fcbb to
8fcc379
Compare
Add Vertex AI Anthropic endpoint routing and ADC authentication for Claude Code. Enforce security boundaries rejecting cluster infrastructure kwargs in skill configs. Harden sensitive credential redaction across CLI, environment, and process args. Support task.path resolution and stdio MCP env forwarding in local agents. Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
Implement an active 1-token preflight probe against Vertex AI Agent Platform OpenAPI endpoints to verify credentials, permissions, and model availability with minimal latency. - Detect Vertex AI OpenAPI base URLs and extract project/location metadata. - Send a 1-token chat/completions probe using HTTPStatus enum values. - Fall back to Google Cloud ADC when an explicit API key is not supplied. - Classify successful probes as VERIFIED and fail fast as FATAL on 401/403/404. - Add unit tests in test_harbor_runtime_preflight covering 200, 401, 403, 404, 429, timeouts, and redirects. Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
8fcc379 to
fea99b9
Compare
- Address PR review feedback for GKE execution and Vertex AI routing. - Decouple live agent Claude/Vertex provider routing from evaluator configs. - Support Google Application Default Credentials (ADC) for Vertex AI OpenAPI endpoints with automatic 401 token refresh in LLMClient. - Harden CLI environment kwargs parsing to fail fast on credentials and malformed input. - Expand GKE infrastructure kwargs blocklist in evals config and improve credential redaction precision. - Normalize multi-path KUBECONFIG resolution and ensure path resolution consistency. - Add packaging dependency guards and comprehensive unit and regression tests. Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
# Conflicts: # CHANGELOG.md
… handling - Enforce safe allowlist for Harbor environment kwargs in skill evals config. - Sanitize runner constructor kwargs and expand sensitive key detection to prevent credential leaks in CLI argv. - Maintain fatal preflight checks for Vertex OpenAPI judge while gracefully handling Claude Vertex Workload Identity. - Implement in-process access token refresh for Google ADC on 401 Unauthorized responses. - Cache and validate multi-path KUBECONFIG files via KubeConfigMerger with fail-fast error handling. - Replace external kubectl process checks with direct Kubernetes API health check. - Inject OPENAI_API_KEY into SkillSpector child process environment when using Vertex OpenAPI with ADC. Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
11777f6 to
c06cfb3
Compare
…DC refresh Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
62520ca to
cc110a1
Compare
|
@kweinmeister - Please address the final 2 review comments: #140 (comment) and #140 (comment) |
…tripping Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
Head branch was pushed to by a user without write access
|
@rng1995 Both comments have been addressed and replied to, and I ran a live GKE evaluation against the updated branch to verify everything end-to-end. Ready for another look whenever you have a chance! |
Signed-off-by: Karl Weinmeister <kweinmeister@google.com> # Conflicts: # .gitleaks.toml # src/skillevaluator/utils/redaction.py
717a262 to
8e89753
Compare
Signed-off-by: Karl Weinmeister <kweinmeister@google.com> # Conflicts: # src/skillevaluator/tier3/eval_core/secret_redaction.py # src/skillevaluator/tier3/harbor/templates/eval.py
Signed-off-by: Karl Weinmeister <kweinmeister@google.com> # Conflicts: # CHANGELOG.md # src/skillevaluator/tier3/harbor/adapter.py
harbor[gke] pulls in kubernetes -> requests-oauthlib -> oauthlib 3.3.1, which is affected by GHSA-hj66-6f7g-4r5v and GHSA-xpv3-w29h-x7cv and fails the Dependency review check. Both are fixed only in oauthlib 4.0.0, so add a fixed floor to the tier3 extra (matching the existing mcp/pyjwt floors), re-lock, and assert the floor in the packaging test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Resolve conflicts with NVIDIA#145 (LLM judge retries and structured schemas): - inference/client.py: keep the ADC token refresh on 401 inside the OpenAI-compatible branch of _invoke_provider, now wrapping _call_with_schema_fallback, under retry_call_with_backoff. - templates/eval.py: keep the Vertex OpenAPI helpers next to main's schema builders, and apply the in-process ADC refresh on 401 around _urlopen_with_schema_fallback, rebuilding the request with the new token. - tests/test_tier3_public_runtime.py and CHANGELOG.md: keep both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
rng1995
left a comment
There was a problem hiding this comment.
@kweinmeister Thank you for your patience and your continued work on this PR, and for the live GKE validation.
I re-reviewed 2436c36:
- MCP placeholder stripping: fixed. With
SKILLEVALUATOR_ALLOWED_MCP_HOSTSandSKILLEVALUATOR_ALLOWED_MCP_SECRETSset,Bearer ${DEDICATED_MCP_TOKEN}andBearer $DEDICATED_MCP_TOKENare accepted through source validation,_load_mcp_servers,validate_skillevaluatorsand Claude registration. Operator-owned references, literal tokens and JWTs, unclosed${VAR,${VAR:-literal},$${...}and unapproved variables are rejected. I've resolved that thread. - GKE metadata isolation: one gap remains. The isolation check runs inside the skill-built image, so a skill can spoof it on clusters that don't enforce NetworkPolicy. Details are in the existing thread.
- New question: whether the metadata-blocking policy also blocks DNS on Dataplane V2 / Autopilot. Details are inline.
I also pushed two maintainer commits to keep the branch mergeable:
1a138baadds anoauthlib>=4.0.0floor. TheDependency reviewcheck was failing becauseharbor[gke]pulls inkubernetes→requests-oauthlib→oauthlib3.3.1 (GHSA-hj66-6f7g-4r5v and GHSA-xpv3-w29h-x7cv, both fixed only in 4.0.0).c9ab383resolves the conflicts with #145. The ADC 401 token refresh inLLMClientand the verifier template now wraps #145's retry and schema-fallback calls, and your ADC refresh tests pass through the new path.
Validation: Ruff is clean, the full suite passes locally (9,972 passed, 18 skipped), and all 17 CI checks passed on c9ab383. CI on the latest update from main is running. No live GKE evaluation was performed on my side.
Requesting changes for the isolation gap only.
|
@rng1995 @chrisknvidia Both comments have been addressed and replied to:
Synced the branch with |
rng1995
left a comment
There was a problem hiding this comment.
@kweinmeister Thank you for your patience and the quick, thorough follow-up, and for validating it on a live Autopilot cluster.
I re-reviewed 7601351:
- DNS egress: fixed. I've resolved that thread.
- Direct-mode isolation: the companion probe works as intended: SkillEvaluator-owned image, shared network namespace, hardened script, correct exec routing, fail-closed handling. Details are in the existing thread.
Requesting changes for:
- [P1] The DinD probe always passes (inline on the
wgetline). BusyBoxwgetin thedocker:dindimage rejects--no-proxy, so compose-mode pods are always reported as isolated. - [P2] The ephemeral-storage cap applies outside Autopilot (inline). It silently lowers
storage_mb/--override-storage-mbabove 10176Mi on every unprivileged direct-mode pod. - The window before the probe runs (existing thread). A skill-replaced
sleepcan reach the metadata server before the probe runs; an init container would close this. - Merge blockers:
- The
DCOcheck fails because7601351has noSigned-off-byline. All earlier commits on the branch are signed. - The branch now conflicts with
main(#161 landed) intests/test_tier3_public_runtime.py.
- The
Non-blocking, worth a look:
SKILLEVALUATOR_GKE_METADATA_PROBE_IMAGEisn't in the runner's Harbor subprocess allowlist, so operators can't override the image throughskill-eval. The defaultpython:3.12-slimis a mutable Docker Hub tag. Forwarding the variable and pinning a digest would help clusters without Docker Hub egress (today they fail closed). The variable and the companion container also aren't documented yet.- The probe counts a connection that is accepted but answers after the 2s timeout as "isolated", on both the Python and curl paths. Treating a successful TCP connect as reachable would be stricter.
- The
wgetbranch has no regression test. A BusyBox-style case would have caught item 1.
Validation: Ruff passes, focused GKE/runner/adapter/preflight tests pass (788), and the full local suite passes (10,020 passed, 18 skipped). All CI checks pass except DCO. No live GKE run on my side.
| "done; exit 0; " | ||
| "elif command -v wget >/dev/null 2>&1; then " | ||
| f"for u in {sh_urls}; do " | ||
| f'out=$(wget --no-proxy -S -T {_METADATA_PROBE_HTTP_TIMEOUT_SEC} --header="Metadata-Flavor: Google" ' |
There was a problem hiding this comment.
[P1] BusyBox wget has no --no-proxy option. Its long options are continue, quiet, server-response, output-document, output-file, directory-prefix, proxy (-Y on/off), user-agent, timeout, header, post-data, spider, no-check-certificate and post-file.
In compose mode the probe runs in Harbor's docker:28.3.3-dind container. That image is Alpine with no python3 or curl, so the script reaches this branch. BusyBox treats --no-proxy as an unknown option: it prints its usage text and exits 1. That output isn't empty and has no HTTP/, so the loop finishes with exit 0 ("isolated") whether or not 169.254.169.254 is reachable. Before this commit, the same branch (without --no-proxy) returned 42 for a reachable endpoint.
The script already unsets every proxy variable, so dropping --no-proxy is enough. BusyBox also accepts -Y off if you want the explicit form; GNU wget doesn't. It would also help to make any wget failure other than "connection refused" or "timed out" exit 43 instead of falling through to exit 0, and to add a BusyBox-style case to the probe test.
A quick live check: docker run --rm docker:28.3.3-dind wget --no-proxy -S -O /dev/null http://example.com; echo $?
There was a problem hiding this comment.
@rng1995 Dropped --no-proxy from the wget branch (since proxy env vars are already unset at the start of the script) and updated the fallback to exit 43 unless wget fails with an expected connection-refused/timeout error. Also added a BusyBox wget test case.
| return amount | ||
|
|
||
|
|
||
| def _adjust_autopilot_ephemeral_storage_for_probe(main_container: Any) -> None: |
There was a problem hiding this comment.
[P2] Despite the name, this cap has no Autopilot check. It runs for every unprivileged direct-mode pod on any GKE cluster (call site at line 662) and lowers both the requests and limits ephemeral-storage of main to 10176Mi. A skill with harbor.resources.storage_mb: 20480, or an operator passing --override-storage-mb 20480 on a GKE Standard cluster, silently gets a 10176Mi limit and can be evicted for exceeding it.
Could you apply this only when the cluster is Autopilot (for example by detecting it during preflight, or with an explicit operator flag), or fail with a clear message instead of lowering an explicit request?
There was a problem hiding this comment.
@rng1995 Added Autopilot cluster detection (_is_autopilot_cluster, with an optional SKILLEVALUATOR_GKE_AUTOPILOT / --ek autopilot override) and scoped the 10176Mi adjustment so it only applies to non-GPU/TPU pods on Autopilot when main requests the default 10Gi. Explicit storage overrides above 10Gi and GKE Standard clusters are left untouched.
|
@kweinmeister I rechecked the current PR head ( |
…DNS egress - Run metadata reachability probe from dedicated companion container (skillevaluator-metadata-probe) instead of evaluated task container - Harden probe script against proxy environment variables, urllib fallback, and binary shims - Add port 53 UDP/TCP DNS egress rule for Cloud DNS and kube-dns pods - Remove ::/0 from egress policy to prevent Cilium Dataplane V2 from bypassing IPv4 metadata exclusions - Add unit and seam tests for probe companion, Autopilot storage adjustment, and shim resistance Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
…ot pod lifecycle - Add pre-start metadata isolation init container (skillevaluator-metadata-probe-init) alongside the runtime companion probe container (skillevaluator-metadata-probe) and fail fast in _wait_for_pod_ready if the init probe exits non-zero or fails to pull - Remove unsupported --no-proxy flag from wget fallback for BusyBox/DinD images and require expected connection-refused/timeout output before reporting isolation success - Scope GKE Autopilot 10176Mi ephemeral-storage adjustment to non-GPU/TPU pods on Autopilot clusters when main storage is in (10176Mi, 10240Mi] - Fail fast in _check_pod_terminated and _wait_for_container_exec_ready when a GKE pod is deleted or preempted (404 Not Found or GKE Warden missing-pod WebSocket error) - Avoid false-positive secret redaction on hyphenated case IDs followed by colons in progress/summary output while preserving equals-sign and standalone colon redaction - Document SKILLEVALUATOR_GKE_METADATA_PROBE_IMAGE and SKILLEVALUATOR_GKE_AUTOPILOT Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
7601351 to
1277658
Compare
|
@chrisknvidia @rng1995 Resolved the merge conflict in |
Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
Summary
Adds support for routing Claude Code through Google Cloud Agent Platform (Vertex AI) endpoints, hardens Harbor GKE runtime execution boundaries, and introduces an active 1-token preflight probe for Agent Platform OpenAPI endpoints.
Why this change is needed:
CLAUDE_CODE_USE_VERTEX=1) and authenticate via Application Default Credentials (ADC).--env-mode gke) requires enforcing security boundaries to reject cluster infrastructure settings in skill configs in favor of host CLI flags/environment variables, while ensuring credentials passed in--ekkeys/values are redacted from process listings without dropping rate limits or token configs.SKILLEVALUATOR_GKE_ALLOW_WORKLOAD_IDENTITY=0), enforces defense-in-depth metadata isolation via a pod-scoped egressNetworkPolicy(preserving UDP/TCP port 53 DNS), a pre-startskillevaluator-metadata-probe-initinit container, and a runtimeskillevaluator-metadata-probecompanion container, with automatic GKE Autopilot10Giephemeral-storage budgeting and fast failure on pod preemption.Verification
make lintmake testmake buildRelease Impact
CHANGELOG.md