feat(opencode): add vertex-ai auth support#275
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new update command and API endpoint to re-import harness configurations from their source URLs, updates the image build executor to sync newly-built images back to storage and the database, and adds fallback support for viewing plain-text broker logs when Cloud Logging is unavailable. It also adds vertex-ai authentication support to the opencode harness, refines message channel tagging, and upgrades several dependencies. The review feedback highlights critical robustness issues in the opencode provisioner where candidates could be None or not a dictionary, causing AttributeErrors. Additionally, the reviewer suggests falling back to environment variables when resolving GCP secrets and returning resolution errors in cmd/build.go instead of silently ignoring them.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def _env_secret_files(candidates: dict[str, Any]) -> dict[str, str]: | ||
| """Map of env-var name -> container path of its 0600 secret value file.""" | ||
| raw = candidates.get("env_secret_files") or {} | ||
| out: dict[str, str] = {} | ||
| if not isinstance(raw, dict): | ||
| return out |
There was a problem hiding this comment.
If candidates is None or not a dictionary (which can happen if the manifest does not contain the "candidates" key or if it is null), calling candidates.get will raise an AttributeError and crash the provisioning process. Adding a type check ensures robustness.
def _env_secret_files(candidates: dict[str, Any] | None) -> dict[str, str]:
"""Map of env-var name -> container path of its 0600 secret value file."""
if not isinstance(candidates, dict):
return {}
raw = candidates.get("env_secret_files") or {}
out: dict[str, str] = {}
if not isinstance(raw, dict):
return out| def _read_secret(env_secret_files: dict[str, str], name: str) -> str: | ||
| """Read the 0600 secret value file for an env var. Returns "" on miss.""" | ||
| path = env_secret_files.get(name) | ||
| if not path: | ||
| return "" | ||
| real = _expand(path) | ||
| try: | ||
| with open(real, "r", encoding="utf-8") as f: | ||
| return f.read().rstrip("\r\n") | ||
| except OSError: | ||
| return "" |
There was a problem hiding this comment.
When running in environments where GCP credentials/project/location are passed directly as environment variables (such as local development, CI/CD, or standard Kubernetes pods) rather than staged secret files, env_secret_files will not contain entries for these variables. Falling back to os.environ.get(name, "") ensures the provisioner can still resolve these values correctly.
| def _read_secret(env_secret_files: dict[str, str], name: str) -> str: | |
| """Read the 0600 secret value file for an env var. Returns "" on miss.""" | |
| path = env_secret_files.get(name) | |
| if not path: | |
| return "" | |
| real = _expand(path) | |
| try: | |
| with open(real, "r", encoding="utf-8") as f: | |
| return f.read().rstrip("\r\n") | |
| except OSError: | |
| return "" | |
| def _read_secret(env_secret_files: dict[str, str], name: str) -> str: | |
| """Read the 0600 secret value file for an env var. Returns env var value on miss.""" | |
| path = env_secret_files.get(name) | |
| if not path: | |
| return os.environ.get(name, "") | |
| real = _expand(path) | |
| try: | |
| with open(real, "r", encoding="utf-8") as f: | |
| return f.read().rstrip("\r\n") | |
| except OSError: | |
| return os.environ.get(name, "") |
| gcp_meta_mode = str(candidates.get("gcp_metadata_mode") or "").strip() | ||
| vertex_not_blocked = gcp_meta_mode != "block" | ||
| has_vertex = has_vertex_project and has_vertex_location and vertex_not_blocked |
There was a problem hiding this comment.
Similarly, candidates might be None or not a dictionary, which would cause candidates.get("gcp_metadata_mode") to raise an AttributeError. We should safely handle this case.
gcp_meta_mode = ""
if isinstance(candidates, dict):
gcp_meta_mode = str(candidates.get("gcp_metadata_mode") or "").strip()
vertex_not_blocked = gcp_meta_mode != "block"
has_vertex = has_vertex_project and has_vertex_location and vertex_not_blocked| var gp string | ||
| if projectPath != "" { | ||
| if resolved, err := config.GetResolvedProjectDir(projectPath); err == nil { | ||
| gp = resolved | ||
| } | ||
| } else if resolved, err := config.GetResolvedProjectDir(""); err == nil { | ||
| gp = resolved | ||
| } |
There was a problem hiding this comment.
If projectPath is explicitly provided but cannot be resolved, we should return the error rather than silently ignoring it and continuing with an empty gp. This keeps the behavior consistent with cmd/harness_config_update.go.
var gp string
if projectPath != "" {
resolved, err := config.GetResolvedProjectDir(projectPath)
if err != nil {
return fmt.Errorf("failed to resolve project path %q: %w", projectPath, err)
}
gp = resolved
} else if resolved, err := config.GetResolvedProjectDir(""); err == nil {
gp = resolved
}* codex harness: project template instructions * codex harness: drop unused system prompt file * codex harness: harden instruction projection * codex harness: polish instruction projection output --------- Co-authored-by: Scion Agent (codex-harness-template-dev) <agent@scion.dev>
* codex harness: enable notification hooks * codex harness: escape otel toml values * codex harness: rely on bundled dialect mapping --------- Co-authored-by: Scion Agent (codex-harness-hooks-dev) <agent@scion.dev>
…m#489) Co-authored-by: Scion Agent (codex-dialect-core-dev) <agent@scion.dev>
…pability (GoogleCloudPlatform#490) - Extract session_id from .conversationId on PreInvocation/PostInvocation - Extract tool_input from .toolCall.args on PreToolUse - Remove false tool_name extraction from PostToolUse (field not in payload) - Declare max_model_calls as supported (model-start/model-end are wired) - Bump PROVISION_VERSION Co-authored-by: Scion Agent (antigravity-hooks-dev) <agent@scion.dev>
8f710f1 to
153d6f5
Compare
…atform#491) Co-authored-by: Scion Agent (codex-harness-template-dev) <agent@scion.dev>
Co-authored-by: Scion Agent (codex-harness-hooks-dev) <agent@scion.dev>
Add vertex-ai as a third auth type for the opencode harness, matching the Claude harness pattern where vertex-ai is the lowest-priority fallback after direct credentials (api-key > auth-file > vertex-ai). Autodetects when GCP project + location env vars are present and gcp_metadata_mode is not "block". When selected, writes VERTEXAI_PROJECT and VERTEX_LOCATION to outputs/env.json.
The gcp_metadata_mode field is never written to auth-candidates.json by the Go staging layer, making the guard inert. Add a comment noting it is reserved for future use rather than removing it, since the concept is actively used elsewhere in the system (e.g. claude_code harness).
72c114a to
75c368d
Compare
Summary
vertex-aias a third auth type for the opencode harness, matching the Claude harness patterngcp_metadata_modeis not"block"VERTEXAI_PROJECTandVERTEX_LOCATIONtooutputs/env.jsonapi-key>auth-file>vertex-ai(vertex-ai is the lowest-priority fallback)Test plan
py_compile)go test ./pkg/harness/...)GOOGLE_CLOUD_PROJECT+GOOGLE_CLOUD_REGIONenv vars → selects vertex-aiANTHROPIC_API_KEY+ vertex vars → selects api-key (higher priority)gcp_metadata_mode: "block"→ skips vertex-ai in autodetectvertex-aiselection without project/location → error message