Skip to content

feat(tier3): support Claude Code on Agent Platform and add active preflight probe - #140

Open
kweinmeister wants to merge 33 commits into
NVIDIA:mainfrom
kweinmeister:fix/harbor-claude-vertex-routing
Open

kweinmeister wants to merge 33 commits into
NVIDIA:mainfrom
kweinmeister:fix/harbor-claude-vertex-routing

Conversation

@kweinmeister

@kweinmeister kweinmeister commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • When evaluating skills with Claude Code via Harbor, users need to route requests to Google Cloud Vertex AI Anthropic endpoints (CLAUDE_CODE_USE_VERTEX=1) and authenticate via Application Default Credentials (ADC).
  • Harbor GKE execution mode (--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 --ek keys/values are redacted from process listings without dropping rate limits or token configs.
  • In unprivileged GKE mode (SKILLEVALUATOR_GKE_ALLOW_WORKLOAD_IDENTITY=0), enforces defense-in-depth metadata isolation via a pod-scoped egress NetworkPolicy (preserving UDP/TCP port 53 DNS), a pre-start skillevaluator-metadata-probe-init init container, and a runtime skillevaluator-metadata-probe companion container, with automatic GKE Autopilot 10Gi ephemeral-storage budgeting and fast failure on pod preemption.
  • Endpoint validation previously lacked active connectivity checks for Vertex AI OpenAPI endpoints, leading to delayed failures during evaluation runs. The new active 1-token chat/completions probe verifies credentials, permissions, and model availability upfront and fails fast on 401/403/404.

Verification

  • I am familiar with the Contributing Guidelines
  • Added or updated focused tests
  • Updated documentation for user-visible changes
  • Ran make lint
  • Ran make test
  • Ran make build
  • Did not add credentials, private datasets, or proprietary benchmark content

Release Impact

  • No user-visible release note needed
  • Updated CHANGELOG.md

@kweinmeister kweinmeister changed the title feat(tier3): support Claude Code on Vertex AI and add active preflight probe feat(tier3): support Claude Code on Agent Platform and add active preflight probe Sep 13, 2026
@kweinmeister
kweinmeister force-pushed the fix/harbor-claude-vertex-routing branch from 300fcbb to 8fcc379 Compare September 14, 2026 20:02
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>
@kweinmeister
kweinmeister force-pushed the fix/harbor-claude-vertex-routing branch from 8fcc379 to fea99b9 Compare September 14, 2026 20:18
Comment thread src/skillevaluator/tier3/evals_config.py
Comment thread src/skillevaluator/tier3/harbor/runner.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runner.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runtime_preflight.py
Comment thread src/skillevaluator/tier3/harbor/runner.py
Comment thread src/skillevaluator/cli.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runner.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runtime_preflight.py
Comment thread src/skillevaluator/tier3/evals_config.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runner.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runtime_preflight.py Outdated
Comment thread src/skillevaluator/provider_config.py
Comment thread src/skillevaluator/tier3/harbor/runner.py Outdated
Comment thread src/skillevaluator/tier3/harbor/runner.py Outdated
Comment thread src/skillevaluator/provider_config.py
kweinmeister and others added 4 commits September 16, 2026 14:41
- 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>
… 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>
@kweinmeister
kweinmeister force-pushed the fix/harbor-claude-vertex-routing branch from 11777f6 to c06cfb3 Compare September 16, 2026 21:42
@kweinmeister
kweinmeister force-pushed the fix/harbor-claude-vertex-routing branch from 62520ca to cc110a1 Compare September 29, 2026 00:49
@rng1995

rng1995 commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

@kweinmeister - Please address the final 2 review comments: #140 (comment) and #140 (comment)

@rng1995
rng1995 enabled auto-merge (squash) September 29, 2026 18:48
…tripping

Signed-off-by: Karl Weinmeister <kweinmeister@google.com>
auto-merge was automatically disabled September 29, 2026 22:51

Head branch was pushed to by a user without write access

@kweinmeister

Copy link
Copy Markdown
Contributor Author

@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
@kweinmeister
kweinmeister force-pushed the fix/harbor-claude-vertex-routing branch from 717a262 to 8e89753 Compare September 30, 2026 22:25
kweinmeister and others added 5 commits October 1, 2026 05:50
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 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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_HOSTS and SKILLEVALUATOR_ALLOWED_MCP_SECRETS set, Bearer ${DEDICATED_MCP_TOKEN} and Bearer $DEDICATED_MCP_TOKEN are accepted through source validation, _load_mcp_servers, validate_skillevaluators and 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:

  • 1a138ba adds an oauthlib>=4.0.0 floor. The Dependency review check was failing because harbor[gke] pulls in kubernetes → requests-oauthlib → oauthlib 3.3.1 (GHSA-hj66-6f7g-4r5v and GHSA-xpv3-w29h-x7cv, both fixed only in 4.0.0).
  • c9ab383 resolves the conflicts with #145. The ADC 401 token refresh in LLMClient and 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.

Comment thread src/skillevaluator/tier3/harbor/gke_environment.py
@kweinmeister

Copy link
Copy Markdown
Contributor Author

@rng1995 @chrisknvidia Both comments have been addressed and replied to:

  1. The metadata reachability probe now runs from a dedicated companion container (skillevaluator-metadata-probe) instead of the evaluated task container, and the probe script is hardened against dead proxies and /bin/true shims.
  2. Added port 53 DNS egress rules for Cloud DNS and kube-dns pods, and removed ::/0 which was causing Cilium Dataplane V2 to bypass IPv4 metadata exclusions.

Synced the branch with main and verified end-to-end locally and on live GKE Autopilot, including a full Tier 3 eval run with claude-code and google-cloud-developer. Ready for another look when you have a chance!

@kweinmeister
kweinmeister requested a review from rng1995 October 4, 2026 18:25

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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:

  1. [P1] The DinD probe always passes (inline on the wget line). BusyBox wget in the docker:dind image rejects --no-proxy, so compose-mode pods are always reported as isolated.
  2. [P2] The ephemeral-storage cap applies outside Autopilot (inline). It silently lowers storage_mb / --override-storage-mb above 10176Mi on every unprivileged direct-mode pod.
  3. The window before the probe runs (existing thread). A skill-replaced sleep can reach the metadata server before the probe runs; an init container would close this.
  4. Merge blockers:
    • The DCO check fails because 7601351 has no Signed-off-by line. All earlier commits on the branch are signed.
    • The branch now conflicts with main (#161 landed) in tests/test_tier3_public_runtime.py.

Non-blocking, worth a look:

  • SKILLEVALUATOR_GKE_METADATA_PROBE_IMAGE isn't in the runner's Harbor subprocess allowlist, so operators can't override the image through skill-eval. The default python:3.12-slim is 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 wget branch 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" '

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 $?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@chrisknvidia

Copy link
Copy Markdown
Collaborator

@kweinmeister I rechecked the current PR head (7601351) against main (6bab56e), and there is still a merge conflict in tests/test_tier3_public_runtime.py. GitHub also reports the PR as conflicting. Could you please resolve this conflict against the latest main and push the updated branch so we can finish the review?

…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>
@kweinmeister
kweinmeister force-pushed the fix/harbor-claude-vertex-routing branch from 7601351 to 1277658 Compare October 5, 2026 20:01
@kweinmeister

kweinmeister commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

@chrisknvidia @rng1995 Resolved the merge conflict in tests/test_tier3_public_runtime.py against main, signed off the commits for DCO, and addressed the latest review comments on the metadata init probe, BusyBox wget, and Autopilot storage scoping. Ready for another look when you have a chance!

@kweinmeister
kweinmeister requested a review from rng1995 October 5, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants