diff --git a/harnesses/codex/config.yaml b/harnesses/codex/config.yaml index 8892097e6..b21a153b6 100644 --- a/harnesses/codex/config.yaml +++ b/harnesses/codex/config.yaml @@ -71,6 +71,12 @@ capabilities: sse: { support: "partial", reason: "Codex maps SSE to its single HTTP server type (url + http_headers)" } streamable_http: { support: "yes" } project_scope: { support: "no", reason: "Project-scoped MCP (.codex/mcp_servers.json) is not implemented yet; project entries are demoted to global" } +no_auth: + behavior: drop-to-shell + message: | + This agent started without credentials. + Run your Codex authentication setup. + Then run: python3 /home/scion/.scion/harness/capture_auth.py auth: default_type: api-key types: @@ -89,9 +95,3 @@ auth: OPENAI_API_KEY: api-key files: CODEX_AUTH: auth-file -no_auth: - behavior: drop-to-shell - message: | - This agent started without credentials. - Set CODEX_API_KEY or OPENAI_API_KEY, or authenticate interactively. - Then capture credentials: python3 ~/.scion/harness/capture_auth.py diff --git a/harnesses/opencode/config.yaml b/harnesses/opencode/config.yaml index 388caf64d..011054e0c 100644 --- a/harnesses/opencode/config.yaml +++ b/harnesses/opencode/config.yaml @@ -67,6 +67,12 @@ capabilities: sse: { support: "yes" } streamable_http: { support: "yes" } project_scope: { support: "no", reason: "OpenCode does not distinguish project-scoped MCP" } +no_auth: + behavior: drop-to-shell + message: | + This agent started without credentials. + Run your OpenCode authentication setup. + Then run: python3 /home/scion/.scion/harness/capture_auth.py auth: default_type: api-key types: @@ -85,9 +91,3 @@ auth: OPENAI_API_KEY: api-key files: OPENCODE_AUTH: auth-file -no_auth: - behavior: drop-to-shell - message: | - This agent started without credentials. - Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or authenticate interactively. - Then capture credentials: python3 ~/.scion/harness/capture_auth.py diff --git a/pkg/agent/run.go b/pkg/agent/run.go index a83087934..ced5bdbc8 100644 --- a/pkg/agent/run.go +++ b/pkg/agent/run.go @@ -341,8 +341,7 @@ func (m *AgentManager) Start(ctx context.Context, opts api.StartOptions) (*api.A var h api.Harness var harnessConfigRevision string var resolvedImpl string - var noAuthMessage string - var resolvedAuthMeta *config.HarnessAuthMetadata + var noAuthConfig *config.HarnessNoAuthConfig if harnessConfigName != "" { var resolveTemplatePaths []string if opts.Template != "" { @@ -369,14 +368,11 @@ func (m *AgentManager) Start(ctx context.Context, opts api.StartOptions) (*api.A } else { h = resolved.Harness resolvedImpl = resolved.Implementation + noAuthConfig = resolved.Config.NoAuthConfig if resolved.ConfigDir != nil { harnessConfigRevision = config.ComputeHarnessConfigRevision(resolved.ConfigDir.Path) } util.Debugf("harness resolution: implementation=%s harness=%q", resolved.Implementation, resolved.Config.Harness) - if opts.NoAuth && resolved.Config.NoAuth != nil { - noAuthMessage = resolved.Config.NoAuth.Message - } - resolvedAuthMeta = resolved.Config.Auth } } else { h = harness.New(harnessName) @@ -431,11 +427,7 @@ func (m *AgentManager) Start(ctx context.Context, opts api.StartOptions) (*api.A if !opts.NoAuth { auth = harness.GatherAuthWithEnv(authEnvOverlay, !opts.BrokerMode) if opts.BrokerMode { - if resolvedAuthMeta != nil { - harness.OverlayFileSecretsFromConfig(&auth, opts.ResolvedSecrets, resolvedAuthMeta) - } else { - harness.OverlayFileSecrets(&auth, opts.ResolvedSecrets) - } + harness.OverlayFileSecrets(&auth, opts.ResolvedSecrets) } util.Debugf("auth: gathered credentials — selectedType=%q, hasGeminiKey=%t, hasGoogleKey=%t, hasOAuth=%t, hasADC=%t, hasAnthropicKey=%t, hasClaudeOAuthToken=%t, hasClaudeAuthFile=%t, cloudProject=%q, gcpMetadataMode=%q, brokerMode=%t", auth.SelectedType, @@ -893,11 +885,16 @@ func (m *AgentManager) Start(ctx context.Context, opts api.StartOptions) (*api.A } return nil }(), - GitClone: opts.GitClone, - SharedDirs: effectiveSharedDirs, - BrokerMode: opts.BrokerMode, - NoAuth: opts.NoAuth, - NoAuthMessage: noAuthMessage, + GitClone: opts.GitClone, + SharedDirs: effectiveSharedDirs, + BrokerMode: opts.BrokerMode, + NoAuth: opts.NoAuth && noAuthConfig != nil && noAuthConfig.Behavior == "drop-to-shell", + NoAuthMessage: func() string { + if opts.NoAuth && noAuthConfig != nil && noAuthConfig.Behavior == "drop-to-shell" { + return noAuthConfig.Message + } + return "" + }(), Debug: util.DebugEnabled(), Resume: opts.Resume, MetadataInterception: hasMetadataInterception(agentEnv), diff --git a/pkg/config/schemas/settings-v1.schema.json b/pkg/config/schemas/settings-v1.schema.json index 10c11a643..af7e542ee 100644 --- a/pkg/config/schemas/settings-v1.schema.json +++ b/pkg/config/schemas/settings-v1.schema.json @@ -269,8 +269,8 @@ }, "auth_selected_type": { "type": "string", - "enum": ["api-key", "oauth-token", "auth-file", "vertex-ai"], - "description": "Authentication mechanism to use (e.g., api-key, oauth-token, vertex-ai, auth-file)." + "enum": ["api-key", "oauth-token", "auth-file", "vertex-ai", "none"], + "description": "Authentication mechanism to use (e.g., api-key, oauth-token, vertex-ai, auth-file, none)." }, "secrets": { "type": "array", @@ -328,14 +328,14 @@ "$ref": "#/$defs/harnessAuthMetadata", "description": "Declarative auth preflight metadata for broker and hosted dispatch." }, - "no_auth": { - "$ref": "#/$defs/harnessNoAuthConfig", - "description": "Defines behavior when an agent starts with --no-auth (no credentials)." - }, "mcp": { "$ref": "#/$defs/harnessMCPMapping", "description": "Declarative mapping for translating universal mcp_servers into the harness's native MCP config (used by scion_harness.apply_mcp_servers_simple). Harnesses with bespoke MCP formats (e.g. OpenCode) leave this empty and translate themselves in provision.py." }, + "no_auth": { + "$ref": "#/$defs/harnessNoAuthConfig", + "description": "Behavior when an agent starts without credentials (NoAuth mode)." + }, "dialect": { "type": "object", "description": "Optional hook dialect metadata.", @@ -350,11 +350,11 @@ "behavior": { "type": "string", "enum": ["drop-to-shell", "show-setup-instructions", "run-setup-wizard"], - "description": "How the container starts when no credentials are present." + "description": "What the harness should do when no credentials are provided." }, "message": { "type": "string", - "description": "Message displayed to the user when the agent starts without credentials." + "description": "Message to display to the user in no-auth mode." } }, "additionalProperties": false @@ -547,13 +547,13 @@ "type": { "type": "string" }, "description": { "type": "string" }, "target_suffix": { "type": "string" }, - "field": { "type": "string" }, "alternative_env_keys": { "type": "array", "items": { "type": "string" } }, "skipped_when_gcp_service_account_assigned": { "type": "boolean" }, - "required": { "type": "boolean" } + "required": { "type": "boolean" }, + "field": { "type": "string" } }, "additionalProperties": false }, diff --git a/pkg/config/settings_v1.go b/pkg/config/settings_v1.go index 15d4dcdc8..7d1f7aed6 100644 --- a/pkg/config/settings_v1.go +++ b/pkg/config/settings_v1.go @@ -331,9 +331,14 @@ type V1PluginEntry struct { // SelfManaged indicates the plugin manages its own process lifecycle. // The Hub connects to the plugin's RPC server rather than starting it. SelfManaged bool `json:"self_managed,omitempty" yaml:"self_managed,omitempty" koanf:"self_managed"` - // Address is the RPC address for self-managed plugins (e.g. "localhost:9090"). - // Required when SelfManaged is true. + // Address is the network address for self-managed or gRPC plugins. + // Required when SelfManaged is true or Mode is "grpc". Address string `json:"address,omitempty" yaml:"address,omitempty" koanf:"address"` + // Mode selects the plugin communication mode: "" or "plugin" (default go-plugin + // subprocess), "grpc" (standalone gRPC broker), "self-managed" (go-plugin RPC to + // an externally-managed process). When empty, falls back to SelfManaged for + // backward compatibility. + Mode string `json:"mode,omitempty" yaml:"mode,omitempty" koanf:"mode"` } // V1ServerHubConfig holds the Hub API server settings (when running scion-server). @@ -715,7 +720,7 @@ type HarnessConfigEntry struct { EnvTemplate map[string]string `json:"env_template,omitempty" yaml:"env_template,omitempty" koanf:"env_template"` Capabilities *api.HarnessAdvancedCapabilities `json:"capabilities,omitempty" yaml:"capabilities,omitempty" koanf:"capabilities"` Auth *HarnessAuthMetadata `json:"auth,omitempty" yaml:"auth,omitempty" koanf:"auth"` - NoAuth *HarnessNoAuthConfig `json:"no_auth,omitempty" yaml:"no_auth,omitempty" koanf:"no_auth"` + NoAuthConfig *HarnessNoAuthConfig `json:"no_auth,omitempty" yaml:"no_auth,omitempty" koanf:"no_auth"` MCP *HarnessMCPConfig `json:"mcp,omitempty" yaml:"mcp,omitempty" koanf:"mcp"` Dialect map[string]interface{} `json:"dialect,omitempty" yaml:"dialect,omitempty" koanf:"dialect"` } @@ -793,18 +798,10 @@ type HarnessAuthAutodetect struct { Files map[string]string `json:"files,omitempty" yaml:"files,omitempty" koanf:"files"` } -// HarnessNoAuthConfig defines what happens when an agent starts with -// --no-auth (no credentials injected). The behavior field determines -// how the container starts. +// HarnessNoAuthConfig defines harness behavior when an agent starts without credentials. type HarnessNoAuthConfig struct { - // Behavior controls the container startup when no credentials are present. - // Supported values: - // "drop-to-shell" — start the container but don't launch the harness CLI - // "show-setup-instructions" — launch normally but display setup instructions - // "run-setup-wizard" — execute a setup script before the main process Behavior string `json:"behavior,omitempty" yaml:"behavior,omitempty" koanf:"behavior"` - // Message is displayed to the user when the agent starts without credentials. - Message string `json:"message,omitempty" yaml:"message,omitempty" koanf:"message"` + Message string `json:"message,omitempty" yaml:"message,omitempty" koanf:"message"` } // HarnessMCPConfig is the declarative mapping that lets a harness's diff --git a/pkg/harness/claude/embeds/config.yaml b/pkg/harness/claude/embeds/config.yaml index 37a358bb5..205cddc4f 100644 --- a/pkg/harness/claude/embeds/config.yaml +++ b/pkg/harness/claude/embeds/config.yaml @@ -52,6 +52,12 @@ capabilities: auth_file: { support: "yes" } oauth_token: { support: "yes" } vertex_ai: { support: "yes" } +no_auth: + behavior: drop-to-shell + message: | + This agent started without credentials. + Run: claude login + Then run: python3 /home/scion/.scion/harness/capture_auth.py auth: default_type: api-key types: @@ -88,9 +94,3 @@ auth: files: CLAUDE_AUTH: auth-file gcloud-adc: vertex-ai -no_auth: - behavior: drop-to-shell - message: | - This agent started without credentials. - To authenticate, run: claude login - Then capture credentials: python3 ~/.scion/harness/capture_auth.py diff --git a/pkg/harness/gemini/embeds/config.yaml b/pkg/harness/gemini/embeds/config.yaml index f94e99a6d..1ee301529 100644 --- a/pkg/harness/gemini/embeds/config.yaml +++ b/pkg/harness/gemini/embeds/config.yaml @@ -51,6 +51,12 @@ capabilities: auth_file: { support: "yes" } oauth_token: { support: "no" } vertex_ai: { support: "yes" } +no_auth: + behavior: drop-to-shell + message: | + This agent started without credentials. + Run your Gemini authentication setup. + Then run: python3 /home/scion/.scion/harness/capture_auth.py auth: default_type: api-key types: @@ -85,9 +91,3 @@ auth: files: GEMINI_OAUTH_CREDS: auth-file gcloud-adc: vertex-ai -no_auth: - behavior: drop-to-shell - message: | - This agent started without credentials. - To authenticate, run: gcloud auth application-default login - Then capture credentials: python3 ~/.scion/harness/capture_auth.py diff --git a/pkg/hub/admin_maintenance.go b/pkg/hub/admin_maintenance.go index dafc17b5e..c484b8583 100644 --- a/pkg/hub/admin_maintenance.go +++ b/pkg/hub/admin_maintenance.go @@ -293,16 +293,6 @@ func (s *Server) resolveMaintenanceExecutor(key string) (MaintenanceExecutor, er return &RebuildContainerBinariesExecutor{ repoPath: mc.RepoPath, }, nil - case "build-harness-config-image": - log.Debug("Resolved build-harness-config-image executor", - "runtime_bin", mc.RuntimeBin, "registry", mc.ImageRegistry, "tag", mc.ImageTag) - return &BuildHarnessConfigImageExecutor{ - store: s.store, - storage: s.GetStorage(), - runtimeBin: mc.RuntimeBin, - registry: mc.ImageRegistry, - tag: mc.ImageTag, - }, nil default: return nil, fmt.Errorf("no executor registered for operation %q", key) } diff --git a/pkg/hub/handlers.go b/pkg/hub/handlers.go index e977c036e..200c499db 100644 --- a/pkg/hub/handlers.go +++ b/pkg/hub/handlers.go @@ -9251,6 +9251,10 @@ func (s *Server) buildAppliedConfig(req CreateAgentRequest, harnessConfig string ac.InlineConfig = req.Config } + if ac.HarnessAuth == "none" { + ac.NoAuth = true + } + return ac } diff --git a/pkg/hub/maintenance_executors.go b/pkg/hub/maintenance_executors.go index e7fd7fc05..cb303e3f4 100644 --- a/pkg/hub/maintenance_executors.go +++ b/pkg/hub/maintenance_executors.go @@ -27,7 +27,6 @@ import ( scionruntime "github.com/GoogleCloudPlatform/scion/pkg/runtime" "github.com/GoogleCloudPlatform/scion/pkg/secret" - "github.com/GoogleCloudPlatform/scion/pkg/storage" "github.com/GoogleCloudPlatform/scion/pkg/store" "github.com/GoogleCloudPlatform/scion/pkg/util/logging" ) @@ -429,162 +428,6 @@ func (e *RebuildContainerBinariesExecutor) Run(ctx context.Context, logger io.Wr return nil } -// BuildHarnessConfigImageExecutor builds a container image from a harness-config's Dockerfile. -type BuildHarnessConfigImageExecutor struct { - store store.Store - storage storage.Storage - runtimeBin string - registry string - tag string -} - -func (e *BuildHarnessConfigImageExecutor) Run(ctx context.Context, logger io.Writer, params map[string]string) error { - log := logging.Subsystem("hub.maintenance.build-harness-config-image") - - harnessConfigID := params["harness_config_id"] - if harnessConfigID == "" { - return fmt.Errorf("missing required parameter: harness_config_id") - } - - tag := e.tag - if tag == "" { - tag = "latest" - } - if v := params["tag"]; v != "" { - tag = v - } - - registry := e.registry - if v := params["registry"]; v != "" { - registry = v - } - registry = strings.TrimSuffix(registry, "/") - - hc, err := e.store.GetHarnessConfig(ctx, harnessConfigID) - if err != nil { - return fmt.Errorf("failed to load harness-config %q: %w", harnessConfigID, err) - } - - hasDockerfile := false - for _, f := range hc.Files { - if f.Path == "Dockerfile" { - hasDockerfile = true - break - } - } - if !hasDockerfile { - return fmt.Errorf("harness-config %q does not contain a Dockerfile", hc.Name) - } - - if e.storage == nil { - return fmt.Errorf("storage not configured") - } - - tmpDir, err := os.MkdirTemp("", "scion-build-*") - if err != nil { - return fmt.Errorf("failed to create temp directory: %w", err) - } - defer os.RemoveAll(tmpDir) - - fmt.Fprintf(logger, "Materializing %d file(s) from harness-config %q...\n", len(hc.Files), hc.Name) - for _, f := range hc.Files { - objectPath := hc.StoragePath + "/" + f.Path - reader, _, err := e.storage.Download(ctx, objectPath) - if err != nil { - return fmt.Errorf("failed to download %q from storage: %w", f.Path, err) - } - - destPath := filepath.Join(tmpDir, f.Path) - if !strings.HasPrefix(destPath, tmpDir+string(os.PathSeparator)) { - _ = reader.Close() - return fmt.Errorf("invalid file path %q: escapes build directory", f.Path) - } - if dir := filepath.Dir(destPath); dir != tmpDir { - if err := os.MkdirAll(dir, 0o755); err != nil { - _ = reader.Close() - return fmt.Errorf("failed to create directory for %q: %w", f.Path, err) - } - } - - outFile, err := os.Create(destPath) - if err != nil { - _ = reader.Close() - return fmt.Errorf("failed to create file %q: %w", f.Path, err) - } - _, err = io.Copy(outFile, reader) - _ = reader.Close() - _ = outFile.Close() - if err != nil { - return fmt.Errorf("failed to write file %q: %w", f.Path, err) - } - - if f.Mode != "" { - mode := os.FileMode(0o644) - if _, err := fmt.Sscanf(f.Mode, "%o", &mode); err == nil { - _ = os.Chmod(destPath, mode) - } - } - } - - baseImage := "scion-base:" + tag - if registry != "" { - baseImage = registry + "/scion-base:" + tag - } - fmt.Fprintf(logger, "Base image: %s\n", baseImage) - - runtimeBin := e.runtimeBin - if runtimeBin == "" { - runtimeBin = scionruntime.DetectContainerRuntime() - } - if runtimeBin == "" { - return fmt.Errorf("no container runtime found (tried docker, podman)") - } - - imageName := hc.Slug - if imageName == "" { - imageName = hc.Name - } - outputImage := imageName + ":" + tag - fmt.Fprintf(logger, "Building %s from harness-config %q...\n", outputImage, hc.Name) - log.Debug("Starting container build", - "image", outputImage, "base_image", baseImage, - "runtime", runtimeBin, "harness_config", hc.Name) - - cmd := exec.CommandContext(ctx, runtimeBin, "build", - "--build-arg", "BASE_IMAGE="+baseImage, - "-t", outputImage, - tmpDir) - cmd.Stdout = logger - cmd.Stderr = logger - if err := cmd.Run(); err != nil { - return fmt.Errorf("build failed: %w", err) - } - - if params["push"] == "true" && registry != "" { - pushImage := registry + "/" + outputImage - fmt.Fprintf(logger, "Tagging %s as %s...\n", outputImage, pushImage) - tagCmd := exec.CommandContext(ctx, runtimeBin, "tag", outputImage, pushImage) - tagCmd.Stdout = logger - tagCmd.Stderr = logger - if err := tagCmd.Run(); err != nil { - return fmt.Errorf("tag failed: %w", err) - } - - fmt.Fprintf(logger, "Pushing %s...\n", pushImage) - pushCmd := exec.CommandContext(ctx, runtimeBin, "push", pushImage) - pushCmd.Stdout = logger - pushCmd.Stderr = logger - if err := pushCmd.Run(); err != nil { - return fmt.Errorf("push failed: %w", err) - } - outputImage = pushImage - } - - fmt.Fprintf(logger, "\nBuild complete: %s\n", outputImage) - log.Info("Build complete", "image", outputImage, "harness_config", hc.Name) - return nil -} - // UpdateCheckResult contains the result of a check-for-updates operation. type UpdateCheckResult struct { UpdateAvailable bool `json:"update_available"` diff --git a/pkg/hub/server.go b/pkg/hub/server.go index f02a8d1f8..21faeca77 100644 --- a/pkg/hub/server.go +++ b/pkg/hub/server.go @@ -390,6 +390,8 @@ type RemoteCreateAgentRequest struct { // CreatorName is the human-readable identity of who created this agent. // Injected as the SCION_CREATOR environment variable in the agent container. CreatorName string `json:"creatorName,omitempty"` + // NoAuth indicates the agent should start without any injected credentials. + NoAuth bool `json:"noAuth,omitempty"` // Attach indicates the agent should start in interactive attach mode (not detached). Attach bool `json:"attach,omitempty"` // ProvisionOnly indicates the agent should be provisioned (dirs, worktree, templates) @@ -433,10 +435,6 @@ type RemoteCreateAgentRequest struct { // (e.g. "shared", "per-agent", "worktree-per-agent"). Threaded from the // Hub so the broker can branch dispatch without re-deriving from labels. WorkspaceMode string `json:"workspaceMode,omitempty"` - - // NoAuth indicates the agent should start with zero injected credentials. - // When true, the broker skips credential injection. - NoAuth bool `json:"noAuth,omitempty"` } // ResolvedSecret represents a secret resolved by the Hub for projection into an agent container. diff --git a/pkg/runtime/common.go b/pkg/runtime/common.go index e07f7424b..047500979 100644 --- a/pkg/runtime/common.go +++ b/pkg/runtime/common.go @@ -428,21 +428,16 @@ func buildCommonRunArgs(config RunConfig) ([]string, error) { // Get command from harness var harnessArgs []string - if config.Harness != nil { - harnessArgs = config.Harness.GetCommand(config.Task, config.Resume, config.CommandArgs) - } else { - return nil, fmt.Errorf("no harness provided") - } - - // When NoAuth is set, drop to a shell instead of launching the harness CLI. - // TODO: Only drop-to-shell is currently implemented. show-setup-instructions - // and run-setup-wizard are defined in config but not yet handled. if config.NoAuth { if config.NoAuthMessage != "" { - harnessArgs = []string{"sh", "-c", fmt.Sprintf("echo %s; exec bash", shellQuote(config.NoAuthMessage))} + harnessArgs = []string{"sh", "-c", fmt.Sprintf("printf '%%s\\n' %s; exec bash", shellQuote(config.NoAuthMessage))} } else { harnessArgs = []string{"bash"} } + } else if config.Harness != nil { + harnessArgs = config.Harness.GetCommand(config.Task, config.Resume, config.CommandArgs) + } else { + return nil, fmt.Errorf("no harness provided") } // Build tmux-wrapped command — use POSIX single-quote escaping so that diff --git a/pkg/runtime/k8s_runtime.go b/pkg/runtime/k8s_runtime.go index 12bf60ea9..b856d5efe 100644 --- a/pkg/runtime/k8s_runtime.go +++ b/pkg/runtime/k8s_runtime.go @@ -881,7 +881,13 @@ func (r *KubernetesRuntime) buildPod(namespace string, config RunConfig) (*corev // Command Resolution var cmd []string var harnessArgs []string - if config.Harness != nil { + if config.NoAuth { + if config.NoAuthMessage != "" { + harnessArgs = []string{"sh", "-c", fmt.Sprintf("printf '%%s\\n' %s; exec bash", shellQuote(config.NoAuthMessage))} + } else { + harnessArgs = []string{"bash"} + } + } else if config.Harness != nil { harnessArgs = config.Harness.GetCommand(config.Task, config.Resume, config.CommandArgs) } else { // Fallback if no harness (though RunConfig implies there should be one or defaults) diff --git a/pkg/runtimebroker/handlers.go b/pkg/runtimebroker/handlers.go index 55c1921c2..ef72eee70 100644 --- a/pkg/runtimebroker/handlers.go +++ b/pkg/runtimebroker/handlers.go @@ -592,8 +592,8 @@ func (s *Server) createAgent(w http.ResponseWriter, r *http.Request) { CreatorName: req.CreatorName, ResolvedEnv: req.ResolvedEnv, ResolvedSecrets: req.ResolvedSecrets, - Attach: req.Attach, NoAuth: req.NoAuth, + Attach: req.Attach, WorkspaceMode: req.WorkspaceMode, HTTPRequest: r, }) @@ -2326,6 +2326,7 @@ func (s *Server) finalizeEnv(w http.ResponseWriter, r *http.Request, id string) CreatorName: origReq.CreatorName, ResolvedEnv: pending.MergedEnv, ResolvedSecrets: origReq.ResolvedSecrets, + NoAuth: origReq.NoAuth, Attach: origReq.Attach, HTTPRequest: r, }) diff --git a/pkg/runtimebroker/start_context.go b/pkg/runtimebroker/start_context.go index cd951acfa..e93f53b0f 100644 --- a/pkg/runtimebroker/start_context.go +++ b/pkg/runtimebroker/start_context.go @@ -76,8 +76,8 @@ type startContextInputs struct { ResolvedSecrets []api.ResolvedSecret // Behavior - Attach bool NoAuth bool + Attach bool // WorkspaceMode is the resolved workspace sharing mode for the project // (e.g. "worktree-per-agent"). Threaded from CreateAgentRequest so the diff --git a/pkg/runtimebroker/types.go b/pkg/runtimebroker/types.go index 46e477d02..7c6f0a2e6 100644 --- a/pkg/runtimebroker/types.go +++ b/pkg/runtimebroker/types.go @@ -286,6 +286,8 @@ type CreateAgentRequest struct { // CreatorName is the human-readable identity of who created this agent. // Injected as the SCION_CREATOR environment variable in the agent container. CreatorName string `json:"creatorName,omitempty"` + // NoAuth indicates the agent should start without any injected credentials. + NoAuth bool `json:"noAuth,omitempty"` // Attach indicates the agent should start in interactive attach mode (not detached). Attach bool `json:"attach,omitempty"` // ProvisionOnly indicates the agent should be provisioned (dirs, worktree, templates) @@ -326,10 +328,6 @@ type CreateAgentRequest struct { // (e.g. "shared", "per-agent", "worktree-per-agent"). Threaded from the // Hub so the broker can branch dispatch without re-deriving from labels. WorkspaceMode string `json:"workspaceMode,omitempty"` - - // NoAuth indicates the agent should start with zero injected credentials. - // When true, the broker skips credential injection. - NoAuth bool `json:"noAuth,omitempty"` } // UnmarshalJSON implements custom unmarshaling to support legacy grove fields. diff --git a/pkg/sciontool/telemetry/pipeline_health_test.go b/pkg/sciontool/telemetry/pipeline_health_test.go index b37755650..9ea158716 100644 --- a/pkg/sciontool/telemetry/pipeline_health_test.go +++ b/pkg/sciontool/telemetry/pipeline_health_test.go @@ -7,7 +7,6 @@ package telemetry import ( "context" "errors" - "os" "testing" "time" @@ -16,10 +15,10 @@ import ( func TestPipeline_HealthGauge_Registers(t *testing.T) { clearTelemetryEnv() - os.Setenv(EnvEnabled, "true") - os.Setenv(EnvCloudEnabled, "false") - os.Setenv(EnvGRPCPort, "54401") - os.Setenv(EnvHTTPPort, "54402") + t.Setenv(EnvEnabled, "true") + t.Setenv(EnvCloudEnabled, "false") + t.Setenv(EnvGRPCPort, "54401") + t.Setenv(EnvHTTPPort, "54402") defer clearTelemetryEnv() cfg := &Config{ @@ -54,10 +53,10 @@ func TestPipeline_HealthGauge_Registers(t *testing.T) { func TestPipeline_HealthGauge_StopsOnStop(t *testing.T) { clearTelemetryEnv() - os.Setenv(EnvEnabled, "true") - os.Setenv(EnvCloudEnabled, "false") - os.Setenv(EnvGRPCPort, "54403") - os.Setenv(EnvHTTPPort, "54404") + t.Setenv(EnvEnabled, "true") + t.Setenv(EnvCloudEnabled, "false") + t.Setenv(EnvGRPCPort, "54403") + t.Setenv(EnvHTTPPort, "54404") defer clearTelemetryEnv() cfg := &Config{ diff --git a/pkg/store/entadapter/maintenance_store.go b/pkg/store/entadapter/maintenance_store.go index 65268e81b..7172e90d3 100644 --- a/pkg/store/entadapter/maintenance_store.go +++ b/pkg/store/entadapter/maintenance_store.go @@ -74,12 +74,6 @@ var defaultSeedOperations = []store.MaintenanceOperation{ Description: "Rebuilds scion and sciontool binaries for Linux containers (make container-binaries). Only available when SCION_DEV_BINARIES is set. Binaries are written to .build/container/ in the source checkout.", Category: store.MaintenanceCategoryOperation, }, - { - Key: "build-harness-config-image", - Title: "Build Harness Config Image", - Description: "Builds a container image from a harness-config's bundled Dockerfile. The base image is resolved from the configured image registry.", - Category: store.MaintenanceCategoryOperation, - }, } // ============================================================================ diff --git a/web/src/components/pages/agent-configure.ts b/web/src/components/pages/agent-configure.ts index 7ea2bd46e..1e47871d7 100644 --- a/web/src/components/pages/agent-configure.ts +++ b/web/src/components/pages/agent-configure.ts @@ -836,6 +836,7 @@ export class ScionPageAgentConfigure extends LitElement { OAuth Token (env var) Vertex Model Garden Harness credential file + No Authentication ${this.authMethod && this.isUnsupported(selectedAuthCap || undefined) ? html`
${this.supportReason(selectedAuthCap || undefined)}
` diff --git a/web/src/components/pages/agent-create.ts b/web/src/components/pages/agent-create.ts index ce592a36f..f545662c3 100644 --- a/web/src/components/pages/agent-create.ts +++ b/web/src/components/pages/agent-create.ts @@ -1180,6 +1180,7 @@ private selectBrokerForProject(): void { OAuth Token (env var) Vertex Model Garden Harness credential file + No Authentication
Override the authentication method for the harness.
diff --git a/web/src/components/pages/harness-config-detail.ts b/web/src/components/pages/harness-config-detail.ts index 825bbd6eb..7f8f80b5a 100644 --- a/web/src/components/pages/harness-config-detail.ts +++ b/web/src/components/pages/harness-config-detail.ts @@ -70,37 +70,8 @@ export class ScionPageHarnessConfigDetail extends LitElement { @state() private editorInitialPreview = false; - @state() - private hasDockerfile = false; - - @state() - private buildDialogOpen = false; - - @state() - private buildRunning = false; - - @state() - private buildTag = 'latest'; - - @state() - private buildPush = false; - - @state() - private buildLog = ''; - - @state() - private buildStatus = ''; - - @state() - private buildRunId = ''; - - @state() - private buildError = ''; - private fileBrowserDataSource: FileBrowserDataSource | null = null; private fileEditorDataSource: FileEditorDataSource | null = null; - private buildPollTimer: ReturnType | null = null; - private buildPollErrors = 0; static override styles = css` :host { @@ -184,52 +155,6 @@ export class ScionPageHarnessConfigDetail extends LitElement { margin-bottom: 0.5rem; } - .header-actions { - margin-left: auto; - } - - .build-log-section { - margin-top: 1.5rem; - } - .build-log-section h3 { - font-size: 0.95rem; - font-weight: 600; - margin: 0 0 0.5rem; - display: flex; - align-items: center; - gap: 0.5rem; - } - .build-log { - background: var(--sl-color-neutral-50); - border: 1px solid var(--sl-color-neutral-200); - border-radius: var(--sl-border-radius-medium); - padding: 1rem; - font-family: var(--sl-font-mono); - font-size: 0.8rem; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-all; - max-height: 400px; - overflow-y: auto; - } - - .build-status-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - font-size: 0.75rem; - font-weight: 500; - } - .build-status-badge.running { color: var(--sl-color-primary-600); } - .build-status-badge.completed { color: var(--sl-color-success-600); } - .build-status-badge.failed { color: var(--sl-color-danger-600); } - - .build-error { - color: var(--sl-color-danger-600); - font-size: 0.85rem; - margin-top: 0.5rem; - } - .error-state, .loading-state { text-align: center; @@ -289,7 +214,6 @@ export class ScionPageHarnessConfigDetail extends LitElement { throw new Error(await extractApiError(response, `HTTP ${response.status}`)); } this.harnessConfig = (await response.json()) as HarnessConfig; - this.hasDockerfile = this.harnessConfig.files?.some(f => f.path === 'Dockerfile') ?? false; dispatchPageTitle( this, this.harnessConfig.displayName || this.harnessConfig.name || this.harnessConfigId, @@ -369,7 +293,7 @@ export class ScionPageHarnessConfigDetail extends LitElement { )} - ${this.renderHeader()} ${this.renderFilesSection()} ${this.renderBuildDialog()} ${this.renderBuildLog()} + ${this.renderHeader()} ${this.renderFilesSection()} `; } @@ -384,19 +308,6 @@ export class ScionPageHarnessConfigDetail extends LitElement { >

${hc.displayName || hc.name}

${hc.harness ? html`${hc.harness}` : ''} - ${this.hasDockerfile ? html` -
- - - ${this.buildRunning ? 'Building...' : 'Build Image'} - -
- ` : nothing} ${hc.description ? html`

${hc.description}

` : ''}
@@ -450,182 +361,6 @@ export class ScionPageHarnessConfigDetail extends LitElement {
`; } - // ── Build Image ── - - private openBuildDialog(): void { - this.buildTag = 'latest'; - this.buildPush = false; - this.buildError = ''; - this.buildDialogOpen = true; - } - - private renderBuildDialog() { - return html` - (this.buildDialogOpen = false)} - > - (this.buildTag = (e.target as HTMLInputElement).value)} - > -
- (this.buildPush = (e.target as HTMLInputElement).checked)}> - Push to registry after building - - ${this.buildError ? html`

${this.buildError}

` : nothing} - - Build - - (this.buildDialogOpen = false)}> - Cancel - -
- `; - } - - private async startBuild(): Promise { - this.buildDialogOpen = false; - this.buildRunning = true; - this.buildLog = ''; - this.buildStatus = 'running'; - this.buildError = ''; - this.buildPollErrors = 0; - - try { - const response = await apiFetch( - '/api/v1/admin/maintenance/operations/build-harness-config-image/run', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - params: { - harness_config_id: this.harnessConfigId, - tag: this.buildTag || 'latest', - push: this.buildPush ? 'true' : 'false', - }, - }), - }, - ); - - if (!response.ok) { - const errMsg = await extractApiError(response, `HTTP ${response.status}`); - this.buildError = errMsg; - this.buildRunning = false; - this.buildStatus = 'failed'; - return; - } - - const result = await response.json(); - if (!result?.runId) { - this.buildError = 'Build started but no run ID was returned'; - this.buildRunning = false; - this.buildStatus = 'failed'; - return; - } - this.buildRunId = result.runId; - this.startBuildPolling(); - } catch (err) { - this.buildError = err instanceof Error ? err.message : 'Failed to start build'; - this.buildRunning = false; - this.buildStatus = 'failed'; - } - } - - private startBuildPolling(): void { - if (this.buildPollTimer) return; - this.buildPollErrors = 0; - void this.pollBuildStatus(); - } - - private stopBuildPolling(): void { - if (this.buildPollTimer) { - clearTimeout(this.buildPollTimer); - this.buildPollTimer = null; - } - } - - private async pollBuildStatus(): Promise { - if (!this.buildRunId) return; - - try { - const resp = await apiFetch( - `/api/v1/admin/maintenance/operations/build-harness-config-image/runs/${this.buildRunId}`, - ); - if (!resp.ok) { - this.buildPollErrors++; - if (this.buildPollErrors >= 5) { - this.buildRunning = false; - this.buildStatus = 'failed'; - this.buildError = 'Lost connection to build'; - this.stopBuildPolling(); - } else if (this.buildRunning) { - this.buildPollTimer = setTimeout(() => void this.pollBuildStatus(), 3000); - } - return; - } - - this.buildPollErrors = 0; - const run = await resp.json(); - this.buildLog = run.log ?? ''; - this.buildStatus = run.status ?? ''; - void this.updateComplete.then(() => this.scrollBuildLog()); - - if (run.status !== 'running') { - this.buildRunning = false; - this.stopBuildPolling(); - if (run.status === 'completed') { - await this.loadHarnessConfig(); - } - } else if (this.buildRunning) { - this.buildPollTimer = setTimeout(() => void this.pollBuildStatus(), 3000); - } - } catch { - this.buildPollErrors++; - if (this.buildPollErrors >= 5) { - this.buildRunning = false; - this.buildStatus = 'failed'; - this.buildError = 'Lost connection to build'; - this.stopBuildPolling(); - } else if (this.buildRunning) { - this.buildPollTimer = setTimeout(() => void this.pollBuildStatus(), 3000); - } - } - } - - private scrollBuildLog(): void { - const el = this.renderRoot?.querySelector('.build-log'); - if (el) { - el.scrollTop = el.scrollHeight; - } - } - - private renderBuildLog() { - if (!this.buildLog && !this.buildRunning) return nothing; - - const statusClass = this.buildStatus === 'completed' ? 'completed' : this.buildStatus === 'running' ? 'running' : 'failed'; - - return html` -
-

- Build Output - - ${this.buildStatus === 'running' - ? html` Running` - : this.buildStatus} - -

-
${this.buildLog}
-
- `; - } - - override disconnectedCallback(): void { - super.disconnectedCallback(); - this.stopBuildPolling(); - } } declare global {