Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions buildkitd.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[registry."host.bridge.internal:5000"]
http = true

[registry."localhost:5000"]
http = true

[registry."host.docker.internal:5000"]
http = true
32 changes: 32 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Changes between `d59f9451` and `HEAD`

## Features

1. Full OpenCode Harness Support (event bridge via `scion-plugin.js`, `opencode` hook dialect, `config.yaml` capability updates, `provision.py` enhancements, heartbeat mechanism)
2. `OPENCODE_API_KEY` auth support across the auth pipeline (auto-detection, `AuthConfig`, `ResolveAuth`, `isAuthEnvKey`, `RequiredAuthEnvKeys`)
3. `"none"` auth type for harness authentication (CLI flags, schema, capabilities, auth secrets/keys validation)
4. `--source` CLI flag for specifying git source branch/tag/commit for agent workspaces (propagated through `StartOptions`, `ScionConfig`, Hub API types, `CreateWorktree`, runtime broker)
5. Default branch detection via `git ls-remote --symref` in the Hub (`pkg/hub/default_branch.go`) with remote probing and fallback chain
6. `manifest.json` written to object storage during resource bootstrap and hash-match paths (with `writeManifestToStorage` helper)
7. Harness config sync status display (`in-sync`, `local-outdated`, `hub-only`, `storage-stale`) with content hash comparison
8. `manifest.json` upload to signed URL during `scion harness-config sync`
9. `--force-harness-configs` server flag and `RefreshDefaultTemplates` that preserves user customizations by default
10. `disable_local_auth` settings field to skip local auth sources in broker-like scenarios
11. Docker network mode configuration via `settings.yaml` (`runtimes.<name>.docker.network`) mapped to `--network` flag
12. `scion exec` command for executing arbitrary commands inside agent containers (local and Hub modes)
13. WebSocket `MaxMessageSize` increased from 64KB to 10MB in `wsprotocol` and hub control channel
14. Hub `session-end` handler forwards `assistant_text` as outbound `"assistant-reply"` message
15. `internal/testgit` test helper package with `TestMain` setup in 7 packages to suppress macOS keychain prompts during tests
16. `opencode.jsonc` support in `mapEmbedFileToHomePath` and provision script
17. New scripts: `buildkitd.toml`, `local-scion-server.sh`, `proxy-host-to-docker.sh`, `test/Makefile`, `test/multi-agent-collaboration-tests.md`

## Bugfixes

1. Restored missing `StageCaptureAuthAssets` call in `pkg/agent/provision.go` that was inadvertently disabled by broken indentation
2. Fixed shebang lines (`#!/bin/bash` -> `#!/usr/bin/env bash`) in 3 image-build shell scripts for portability
3. Fixed inline config `Source` field not being propagated in `create.go` and `common.go` CLI paths
4. Removed redundant `GIT_ASKPASS=echo` env var in `remote_templates.go` (duplicated with `GIT_TERMINAL_PROMPT=0`)
5. Fixed `AuthSelectedType` not falling back to `Auth.DefaultType` when empty in `LoadHarnessConfigDir`
6. Consolidated duplicate default-branch fallback logic in `populateAgentConfig` into a single `resolveDefaultBranch` call
7. Resource validation now produces aggregate/summary issues instead of per-file noise when storage is empty or has mismatches
8. Server startup changed from `UpdateDefaultTemplates(true, ...)` to `RefreshDefaultTemplates(..., force=false)` to avoid clobbering user harness-config customizations
10 changes: 8 additions & 2 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ var (
noAuth bool
attach bool
branch string
source string
workspace string
runtimeBrokerID string
harnessConfigFlag string
Expand Down Expand Up @@ -379,10 +380,10 @@ func RunAgent(cmd *cobra.Command, args []string, resume bool) error {
// Validate --harness-auth value
if harnessAuthFlag != "" {
switch harnessAuthFlag {
case "api-key", "oauth-token", "auth-file", "vertex-ai":
case "api-key", "oauth-token", "auth-file", "vertex-ai", "none":
// valid
default:
return fmt.Errorf("invalid --harness-auth value %q: must be one of api-key, oauth-token, auth-file, vertex-ai", harnessAuthFlag)
return fmt.Errorf("invalid --harness-auth value %q: must be one of api-key, oauth-token, auth-file, vertex-ai, none", harnessAuthFlag)
}
}

Expand Down Expand Up @@ -450,6 +451,7 @@ func RunAgent(cmd *cobra.Command, args []string, resume bool) error {

// Apply inline config overrides to CLI options
effectiveBranch := branch
effectiveSource := source
effectiveTask := strings.TrimSpace(task)
effectiveHarnessConfig := harnessConfigFlag
effectiveHarnessAuth := harnessAuthFlag
Expand All @@ -458,6 +460,9 @@ func RunAgent(cmd *cobra.Command, args []string, resume bool) error {
if effectiveBranch == "" && inlineCfg.Branch != "" {
effectiveBranch = inlineCfg.Branch
}
if effectiveSource == "" && inlineCfg.Source != "" {
effectiveSource = inlineCfg.Source
}
if effectiveTask == "" && inlineCfg.Task != "" {
effectiveTask = inlineCfg.Task
}
Expand Down Expand Up @@ -500,6 +505,7 @@ func RunAgent(cmd *cobra.Command, args []string, resume bool) error {
Detached: detached,
NoAuth: noAuth,
Branch: effectiveBranch,
Source: effectiveSource,
Workspace: workspace,
InlineConfig: inlineCfg,
}
Expand Down
12 changes: 9 additions & 3 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ arguments are provided, an empty prompt.md is created for later editing.`,
// Validate --harness-auth value
if harnessAuthFlag != "" {
switch harnessAuthFlag {
case "api-key", "oauth-token", "auth-file", "vertex-ai":
case "api-key", "oauth-token", "auth-file", "vertex-ai", "none":
// valid
default:
return fmt.Errorf("invalid --harness-auth value %q: must be one of api-key, oauth-token, auth-file, vertex-ai", harnessAuthFlag)
return fmt.Errorf("invalid --harness-auth value %q: must be one of api-key, oauth-token, auth-file, vertex-ai, none", harnessAuthFlag)
}
}

Expand Down Expand Up @@ -89,6 +89,7 @@ arguments are provided, an empty prompt.md is created for later editing.`,

// Apply inline config overrides to CLI options
effectiveBranch := branch
effectiveSource := source
effectiveTask := task
effectiveHarnessConfig := harnessConfigFlag
effectiveImage := agentImage
Expand All @@ -97,6 +98,9 @@ arguments are provided, an empty prompt.md is created for later editing.`,
if effectiveBranch == "" && inlineCfg.Branch != "" {
effectiveBranch = inlineCfg.Branch
}
if effectiveSource == "" && inlineCfg.Source != "" {
effectiveSource = inlineCfg.Source
}
if effectiveTask == "" && inlineCfg.Task != "" {
effectiveTask = inlineCfg.Task
}
Expand All @@ -117,6 +121,7 @@ arguments are provided, an empty prompt.md is created for later editing.`,
Image: effectiveImage,
ProjectPath: projectPath,
Branch: effectiveBranch,
Source: effectiveSource,
Workspace: workspace,
InlineConfig: inlineCfg,
}
Expand Down Expand Up @@ -322,12 +327,13 @@ func init() {
createCmd.Flags().StringVarP(&templateName, "type", "t", "", "Template to use")
createCmd.Flags().StringVarP(&agentImage, "image", "i", "", "Container image to use (overrides template)")
createCmd.Flags().StringVarP(&branch, "branch", "b", "", "Git branch to use for the agent workspace")
createCmd.Flags().StringVar(&source, "source", "", "Source branch/tag/commit for the agent workspace (defaults to repo default branch)")
createCmd.Flags().StringVarP(&workspace, "workspace", "w", "", "Host path to mount as /workspace")
createCmd.Flags().StringVar(&runtimeBrokerID, "broker", "", "Preferred runtime broker ID or name")
createCmd.Flags().StringVar(&harnessConfigFlag, "harness-config", "", "Named harness configuration to use")
createCmd.Flags().StringVar(&harnessConfigFlag, "harness", "h", "Named harness configuration to use (alias for --harness-config)")

createCmd.Flags().StringVar(&harnessAuthFlag, "harness-auth", "", "Override auth method for the harness (api-key, oauth-token, auth-file, vertex-ai)")
createCmd.Flags().StringVar(&harnessAuthFlag, "harness-auth", "", "Override auth method for the harness (api-key, oauth-token, auth-file, vertex-ai, none)")

// Template resolution flags for Hub mode (Section 9.4)
createCmd.Flags().BoolVar(&uploadTemplate, "upload-template", false, "Automatically upload local template to Hub if not found")
Expand Down
108 changes: 108 additions & 0 deletions cmd/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"context"
"fmt"
"os"
"time"

"github.com/GoogleCloudPlatform/scion/pkg/api"
"github.com/GoogleCloudPlatform/scion/pkg/runtime"
"github.com/spf13/cobra"
)

var execTimeout int

// execCmd represents the exec command
var execCmd = &cobra.Command{
Use: "exec <agent> -- <command> [args...]",
Short: "Execute a command inside an agent container",
Long: `Execute a command inside a running agent's container.

In hub mode, the command is dispatched through the hub to the runtime broker
that owns the agent. In local mode, the command runs directly on the local
container runtime (Docker, Podman, or Apple Container).

This works across all runtime backends — the CLI abstracts away the
differences between docker exec, podman exec, container exec, and kubectl exec.`,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: getAgentNames,
RunE: func(cmd *cobra.Command, args []string) error {
agentName := api.Slugify(args[0])

// Everything after -- is the command to execute
command := args[1:]
if len(command) == 0 {
return fmt.Errorf("no command specified")
}

// Check if Hub is enabled
hubCtx, err := CheckHubAvailabilityForAgent(projectPath, agentName, false)
if err != nil {
return err
}

if hubCtx != nil {
return execViaHub(hubCtx, agentName, command)
}

return execLocal(agentName, command)
},
}

func execLocal(agentName string, command []string) error {
rt := runtime.GetRuntime(projectPath, profile)
output, err := rt.Exec(context.Background(), agentName, command)
if err != nil {
return fmt.Errorf("failed to execute command in agent '%s': %w", agentName, err)
}
fmt.Print(output)
return nil
}

func execViaHub(hubCtx *HubContext, agentName string, command []string) error {
PrintUsingHub(hubCtx.Endpoint)

projectID, err := GetProjectID(hubCtx)
if err != nil {
return wrapHubError(err)
}

timeout := execTimeout
if timeout <= 0 {
timeout = 30
}

ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()

resp, err := hubCtx.Client.ProjectAgents(projectID).Exec(ctx, agentName, command, timeout)
if err != nil {
return wrapHubError(fmt.Errorf("failed to execute command in agent '%s': %w", agentName, err))
}

fmt.Print(resp.Output)
if resp.ExitCode != 0 {
os.Exit(resp.ExitCode)
}
return nil
}

func init() {
execCmd.Flags().IntVarP(&execTimeout, "timeout", "t", 30, "Timeout in seconds for command execution")
rootCmd.AddCommand(execCmd)
}
70 changes: 57 additions & 13 deletions cmd/harness_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package cmd

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand All @@ -26,6 +28,7 @@ import (
"github.com/GoogleCloudPlatform/scion/pkg/config"
"github.com/GoogleCloudPlatform/scion/pkg/harness"
"github.com/GoogleCloudPlatform/scion/pkg/hubclient"
"github.com/GoogleCloudPlatform/scion/pkg/transfer"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -87,22 +90,51 @@ var harnessConfigListCmd = &cobra.Command{
Status: "active",
})
if err == nil {
// Merge Hub results (avoid duplicates by name)
localNames := make(map[string]bool)
for _, e := range entries {
localNames[e.Name] = true
// Build hub map for comparison
hubMap := make(map[string]*hubclient.HarnessConfig)
for i := range hubResp.HarnessConfigs {
hubMap[hubResp.HarnessConfigs[i].Name] = &hubResp.HarnessConfigs[i]
}
for _, hc := range hubResp.HarnessConfigs {
if !localNames[hc.Name] {
entries = append(entries, hcEntry{
Name: hc.Name,
Harness: hc.Harness,
Source: "hub",
ID: hc.ID,
Status: hc.Status,
})

// Update local entries with hub status and validate storage
for i := range entries {
if hubHC, exists := hubMap[entries[i].Name]; exists {
entries[i].ID = hubHC.ID
entries[i].Status = hubHC.Status

// Validate storage to check for stale content
report, err := hubCtx.Client.HarnessConfigs().Validate(context.Background(), hubHC.ID)
if err == nil && len(report.Issues) > 0 {
// Storage has issues
entries[i].Source = "local+hub (storage-stale)"
} else {
// Storage is valid, compare local vs DB
files, err := transfer.CollectFiles(entries[i].Path, nil)
if err == nil {
localHash := transfer.ComputeContentHash(files)
if localHash == hubHC.ContentHash {
entries[i].Source = "local+hub (in-sync)"
} else {
entries[i].Source = "local+hub (local-outdated)"
}
} else {
entries[i].Source = "local+hub (error)"
}
}
delete(hubMap, entries[i].Name)
}
}

// Add hub-only entries
for name, hc := range hubMap {
entries = append(entries, hcEntry{
Name: name,
Harness: hc.Harness,
Source: "hub-only",
ID: hc.ID,
Status: hc.Status,
})
}
}
}
}
Expand Down Expand Up @@ -672,6 +704,18 @@ func syncHarnessConfigToHub(hubCtx *HubContext, name, localPath, scope, scopeID,
}
}

// Upload manifest.json to storage if ManifestURL is provided
if uploadResp.ManifestURL != "" {
fmt.Println("Uploading manifest.json...")
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return fmt.Errorf("failed to marshal manifest: %w", err)
}
if err := transfer.NewClient(nil).UploadFileWithMethod(ctx, uploadResp.ManifestURL, "PUT", nil, bytes.NewReader(manifestBytes)); err != nil {
return fmt.Errorf("failed to upload manifest.json: %w", err)
}
}

// Finalize
fmt.Println("Finalizing harness-config...")
hc, err := hubCtx.Client.HarnessConfigs().Finalize(ctx, hcID, manifest)
Expand Down
2 changes: 1 addition & 1 deletion cmd/sciontool/commands/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func init() {
rootCmd.AddCommand(hookCmd)

hookCmd.Flags().StringVar(&hookDialect, "dialect", "claude",
"Harness dialect for event parsing (claude, gemini, codex)")
"Harness dialect for event parsing (claude, gemini, codex, opencode)")
hookCmd.Flags().StringVar(&hookData, "data", "",
"Additional data for subcommands")

Expand Down
7 changes: 7 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ var (
// Server daemon flags
serverStartForeground bool
stopForce bool
serverNoBrowser bool

// Hosted mode flag (replaces former "production" mode)
hostedMode bool

// Force overwrite harness-configs on startup
forceHarnessConfigs bool
)

const (
Expand Down Expand Up @@ -268,6 +272,9 @@ func init() {
// Runtime Broker auto-provide flag
serverStartCmd.Flags().BoolVar(&serverAutoProvide, "auto-provide", false, "Automatically add runtime broker as provider for new projects")

// Harness-config force flag
serverStartCmd.Flags().BoolVar(&forceHarnessConfigs, "force-harness-configs", false, "Force overwrite of harness-config files on startup (discards user customizations)")

// Web Frontend flags
serverStartCmd.Flags().BoolVar(&enableWeb, "enable-web", false, "Enable the web frontend")
serverStartCmd.Flags().IntVar(&webPort, "web-port", 8080, "Web frontend port")
Expand Down
3 changes: 2 additions & 1 deletion cmd/server_foreground.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ func runServerStart(cmd *cobra.Command, args []string) error {
// In workstation mode, refresh the default template and harness-configs
// from the binary's embeds. Hosted mode bootstraps directly into the Hub
// via BootstrapBundledResources, bypassing local ~/.scion materialization.
if err := config.UpdateDefaultTemplates(true, harness.EmbedOnlyHarnesses()); err != nil {
// The forceHarnessConfigs flag controls whether to overwrite user customizations.
if err := config.RefreshDefaultTemplates(harness.EmbedOnlyHarnesses(), forceHarnessConfigs); err != nil {
log.Printf("Warning: failed to refresh default templates: %v", err)
}
}
Expand Down
Loading