From 6cbb67e9a45fcaef55898b000eb52008cfabef5e Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Mon, 3 Aug 2026 12:20:11 -0400 Subject: [PATCH 1/7] :sparkles: Stage-aware Hub token revocation in harness (#74) Harness revokes Hub API token on exit for standalone runs and last workflow stage; intermediate stages skip. Reads KONVEYOR_WORKFLOW_STAGE, KONVEYOR_WORKFLOW_STAGE_COUNT, and HUB_TOKEN_ID from env. Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- CONTEXT.md | 5 +- .../74-stage-aware-token-revocation.yaml | 6 + ...6-hub-addon-pattern-for-agent-resources.md | 3 +- harness/cmd/migration-harness/main.go | 30 +++++ harness/cmd/migration-harness/main_test.go | 104 ++++++++++++++++++ harness/internal/config/config.go | 10 ++ harness/internal/config/config_test.go | 44 ++++++++ harness/internal/hub/client.go | 10 +- harness/internal/hub/client_test.go | 20 +++- 9 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 changes/unreleased/74-stage-aware-token-revocation.yaml diff --git a/CONTEXT.md b/CONTEXT.md index cdfdb540..fb8c9a11 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -150,8 +150,9 @@ pattern; and (2) a runtime data service that the harness calls (via a scoped API token) to fetch application metadata, decrypted git credentials, and analysis results — the same role Hub plays for addons today. At AgentRun create time, Hub mints a scoped token and -injects `HUB_BASE_URL`, `HUB_APP_ID`, and the token into the AgentRun's -env/envFrom, then creates the CR. Hub does not resolve application +injects `HUB_BASE_URL`, `HUB_APP_ID`, the token (`HUB_TOKEN`), and the +token's database ID (`HUB_TOKEN_ID`) into the AgentRun's env/envFrom, +then creates the CR. Hub does not resolve application data at create time — the harness resolves at runtime. Hub is fire-and-forget; it does not launch or manage agent workloads. diff --git a/changes/unreleased/74-stage-aware-token-revocation.yaml b/changes/unreleased/74-stage-aware-token-revocation.yaml new file mode 100644 index 00000000..24d60ecc --- /dev/null +++ b/changes/unreleased/74-stage-aware-token-revocation.yaml @@ -0,0 +1,6 @@ +kind: enhancement +description: > + Harness revokes its Hub API token on exit for standalone AgentRuns and + the last stage of an AgentWorkflowRun. Intermediate workflow stages + skip revocation so subsequent stages can reuse the shared token. Requires + Hub to inject HUB_TOKEN_ID alongside HUB_TOKEN in the run Secret. diff --git a/docs/adr/0006-hub-addon-pattern-for-agent-resources.md b/docs/adr/0006-hub-addon-pattern-for-agent-resources.md index e9afb492..72f6bce8 100644 --- a/docs/adr/0006-hub-addon-pattern-for-agent-resources.md +++ b/docs/adr/0006-hub-addon-pattern-for-agent-resources.md @@ -34,7 +34,8 @@ env var. When Hub receives a create request for an AgentRun or AgentWorkflowRun: 1. Mints a scoped API token with `AddonScopes` -2. Stores the token in a Kubernetes Secret +2. Stores the token and its database ID in a Kubernetes Secret + (`HUB_TOKEN`, `HUB_TOKEN_ID`) 3. Adds `HUB_BASE_URL`, `HUB_APP_ID`, and the token Secret to the CR's `spec.env` and `spec.envFrom` 4. Creates the CR via `client.Create()` diff --git a/harness/cmd/migration-harness/main.go b/harness/cmd/migration-harness/main.go index 5c3a87aa..e786b2f0 100644 --- a/harness/cmd/migration-harness/main.go +++ b/harness/cmd/migration-harness/main.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "path/filepath" + "strconv" "strings" "syscall" "time" @@ -65,6 +66,22 @@ func runStage(cmd *cobra.Command, args []string) error { return fmt.Errorf("hub resolution: %w", err) } + // Stage-aware token revocation: revoke the Hub API token on exit + // for standalone runs and the last workflow stage. Intermediate + // stages skip so subsequent stages can reuse the token. + if shouldRevokeToken(cfg) { + tokenID, _ := strconv.ParseUint(cfg.HubTokenID, 10, 64) + defer func() { + if err := hubClient.RevokeToken(uint(tokenID)); err != nil { + logging.Warn("hub token revocation: %v", err) + } else { + logging.Ok("hub token revoked") + } + }() + } else if cfg.HubTokenID == "" && cfg.HubToken != "" { + logging.Warn("HUB_TOKEN_ID not set — skipping token revocation (token will expire via TTL)") + } + if cfg.TargetBranch == creds.Branch { return fmt.Errorf("TARGET_BRANCH %q must differ from source branch", cfg.TargetBranch) } @@ -293,6 +310,19 @@ func resolveFromHub(cfg *config.Config) (*git.Credentials, *hub.Client, error) { return creds, hubClient, nil } +// shouldRevokeToken decides whether the harness should revoke the Hub API +// token on exit. Standalone AgentRuns always revoke. Workflow stages +// revoke only on the last stage so subsequent stages can reuse the token. +func shouldRevokeToken(cfg *config.Config) bool { + if cfg.HubTokenID == "" { + return false + } + if cfg.WorkflowStage == "" { + return true + } + return cfg.WorkflowStage == cfg.WorkflowStageCount +} + func fetchAndWriteAnalysis(hubClient *hub.Client, appIDStr string, workDir string) error { appID, err := hub.ParseAppID(appIDStr) if err != nil { diff --git a/harness/cmd/migration-harness/main_test.go b/harness/cmd/migration-harness/main_test.go index 543adae7..b74f904f 100644 --- a/harness/cmd/migration-harness/main_test.go +++ b/harness/cmd/migration-harness/main_test.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/konveyor/migration-harness/internal/config" ) func TestDiscoverSkills_NoSkills(t *testing.T) { @@ -69,3 +71,105 @@ func TestDiscoverSkills_EmptySkillFile(t *testing.T) { t.Errorf("expected 1 path (skill is mounted), got: %v", paths) } } + +func TestShouldRevokeToken(t *testing.T) { + tests := []struct { + name string + hubTokenID string + workflowStage string + workflowStageCount string + want bool + }{ + { + name: "no token ID — skip revocation", + want: false, + }, + { + name: "standalone run — revoke", + hubTokenID: "1", + want: true, + }, + { + name: "last workflow stage — revoke", + hubTokenID: "1", + workflowStage: "3", + workflowStageCount: "3", + want: true, + }, + { + name: "intermediate workflow stage — skip", + hubTokenID: "1", + workflowStage: "1", + workflowStageCount: "3", + want: false, + }, + { + name: "second of two stages — skip", + hubTokenID: "1", + workflowStage: "1", + workflowStageCount: "2", + want: false, + }, + { + name: "single-stage workflow — revoke", + hubTokenID: "1", + workflowStage: "1", + workflowStageCount: "1", + want: true, + }, + { + name: "stage set but count missing — skip", + hubTokenID: "1", + workflowStage: "1", + workflowStageCount: "", + want: false, + }, + { + name: "count set but stage missing — standalone", + hubTokenID: "1", + workflowStage: "", + workflowStageCount: "3", + want: true, + }, + { + name: "stage exceeds count — skip", + hubTokenID: "1", + workflowStage: "5", + workflowStageCount: "3", + want: false, + }, + { + name: "non-numeric stage — skip", + hubTokenID: "1", + workflowStage: "abc", + workflowStageCount: "3", + want: false, + }, + { + name: "non-numeric count — skip", + hubTokenID: "1", + workflowStage: "1", + workflowStageCount: "xyz", + want: false, + }, + { + name: "stage zero — skip", + hubTokenID: "1", + workflowStage: "0", + workflowStageCount: "3", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &config.Config{ + HubTokenID: tt.hubTokenID, + WorkflowStage: tt.workflowStage, + WorkflowStageCount: tt.workflowStageCount, + } + if got := shouldRevokeToken(cfg); got != tt.want { + t.Errorf("shouldRevokeToken() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/harness/internal/config/config.go b/harness/internal/config/config.go index ac9dc1c4..a90d53f8 100644 --- a/harness/internal/config/config.go +++ b/harness/internal/config/config.go @@ -19,11 +19,17 @@ type Config struct { HubBaseURL string HubToken string + HubTokenID string AppID string ACPSecretKey string TargetBranch string + // Workflow stage metadata, injected by the controller for + // AgentWorkflowRun stages. Both empty for standalone AgentRuns. + WorkflowStage string + WorkflowStageCount string + // Prompt context layers, composed by internal/prompt. AgentPrompt string WorkflowGuide string @@ -53,10 +59,14 @@ func LoadFromEnv() (*Config, error) { MaxTurns: DefaultMaxTurns, HubBaseURL: required["HUB_BASE_URL"], HubToken: os.Getenv("HUB_TOKEN"), + HubTokenID: os.Getenv("HUB_TOKEN_ID"), AppID: required["APP_ID"], ACPSecretKey: required["KONVEYOR_ACP_SECRET_KEY"], TargetBranch: required["TARGET_BRANCH"], + WorkflowStage: os.Getenv("KONVEYOR_WORKFLOW_STAGE"), + WorkflowStageCount: os.Getenv("KONVEYOR_WORKFLOW_STAGE_COUNT"), + AgentPrompt: os.Getenv("KONVEYOR_PROMPT"), WorkflowGuide: workflowGuideFromEnv(), StageInstructions: os.Getenv("KONVEYOR_INSTRUCTIONS"), diff --git a/harness/internal/config/config_test.go b/harness/internal/config/config_test.go index 34e78e45..06506846 100644 --- a/harness/internal/config/config_test.go +++ b/harness/internal/config/config_test.go @@ -23,6 +23,9 @@ func clearKonveyorEnv(t *testing.T) { "KONVEYOR_PLAYBOOK_INSTRUCTIONS", "KONVEYOR_WORKFLOW_GUIDE", "KONVEYOR_INSTRUCTIONS", + "KONVEYOR_WORKFLOW_STAGE", + "KONVEYOR_WORKFLOW_STAGE_COUNT", + "HUB_TOKEN_ID", } { t.Setenv(k, "") os.Unsetenv(k) @@ -188,3 +191,44 @@ func TestLoadFromEnvPrefersWorkflowGuide(t *testing.T) { t.Errorf("WorkflowGuide = %q, want the KONVEYOR_WORKFLOW_GUIDE value", cfg.WorkflowGuide) } } + +func TestLoadFromEnvReadsWorkflowStageMetadata(t *testing.T) { + clearKonveyorEnv(t) + setRequiredEnv(t) + t.Setenv("KONVEYOR_WORKFLOW_STAGE", "2") + t.Setenv("KONVEYOR_WORKFLOW_STAGE_COUNT", "3") + t.Setenv("HUB_TOKEN_ID", "99") + + cfg, err := LoadFromEnv() + if err != nil { + t.Fatalf("LoadFromEnv: %v", err) + } + if cfg.WorkflowStage != "2" { + t.Errorf("WorkflowStage = %q, want %q", cfg.WorkflowStage, "2") + } + if cfg.WorkflowStageCount != "3" { + t.Errorf("WorkflowStageCount = %q, want %q", cfg.WorkflowStageCount, "3") + } + if cfg.HubTokenID != "99" { + t.Errorf("HubTokenID = %q, want %q", cfg.HubTokenID, "99") + } +} + +func TestLoadFromEnvWorkflowStageFieldsOptional(t *testing.T) { + clearKonveyorEnv(t) + setRequiredEnv(t) + + cfg, err := LoadFromEnv() + if err != nil { + t.Fatalf("LoadFromEnv: %v", err) + } + if cfg.WorkflowStage != "" { + t.Errorf("WorkflowStage should be empty for standalone runs, got %q", cfg.WorkflowStage) + } + if cfg.WorkflowStageCount != "" { + t.Errorf("WorkflowStageCount should be empty for standalone runs, got %q", cfg.WorkflowStageCount) + } + if cfg.HubTokenID != "" { + t.Errorf("HubTokenID should be empty when not set, got %q", cfg.HubTokenID) + } +} diff --git a/harness/internal/hub/client.go b/harness/internal/hub/client.go index 83a00e29..3674579b 100644 --- a/harness/internal/hub/client.go +++ b/harness/internal/hub/client.go @@ -64,11 +64,17 @@ func ParseAppID(s string) (uint, error) { return uint(n), nil } +// RevokeToken revokes a Hub API token by its database ID. +func (c *Client) RevokeToken(id uint) error { + return c.rich.Token.Revoke(id) +} + // ClearEnv removes Hub credentials from the process environment so -// child processes (goose) cannot access them. Note: this does NOT -// revoke the Hub API token — it remains valid until its JWT expiry. +// child processes (goose) cannot access them. Token revocation is +// handled separately by the stage-aware logic in main. func ClearEnv() { os.Unsetenv("HUB_BASE_URL") os.Unsetenv("HUB_TOKEN") + os.Unsetenv("HUB_TOKEN_ID") os.Unsetenv("APP_ID") } diff --git a/harness/internal/hub/client_test.go b/harness/internal/hub/client_test.go index 2103f748..b607ce5f 100644 --- a/harness/internal/hub/client_test.go +++ b/harness/internal/hub/client_test.go @@ -1,6 +1,9 @@ package hub -import "testing" +import ( + "os" + "testing" +) func TestParseAppID(t *testing.T) { tests := []struct { @@ -28,3 +31,18 @@ func TestParseAppID(t *testing.T) { }) } } + +func TestClearEnvRemovesAllHubVars(t *testing.T) { + vars := []string{"HUB_BASE_URL", "HUB_TOKEN", "HUB_TOKEN_ID", "APP_ID"} + for _, k := range vars { + t.Setenv(k, "test-value") + } + + ClearEnv() + + for _, k := range vars { + if v := os.Getenv(k); v != "" { + t.Errorf("ClearEnv should unset %s, got %q", k, v) + } + } +} From 8f45db47c5af7a78fdd40761f0a4145d237fcf82 Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Mon, 3 Aug 2026 16:15:38 -0400 Subject: [PATCH 2/7] Fix /tmp volume, test scripts, and skill commit hygiene Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- hack/harness-test/setup.sh | 22 ++++++------- hack/harness-test/workflow-resources.yaml | 2 ++ hack/setup-e2e.sh | 2 ++ harness/cmd/migration-harness/main.go | 6 ++-- images/agent-base/Containerfile | 3 +- internal/controller/agentrun_controller.go | 12 +++++++ .../controller/agentrun_controller_test.go | 31 +++++++++++++++++++ skills/execute/SKILL.md | 2 +- skills/plan/SKILL.md | 3 +- skills/verify/SKILL.md | 4 +-- 10 files changed, 68 insertions(+), 19 deletions(-) diff --git a/hack/harness-test/setup.sh b/hack/harness-test/setup.sh index e80b943d..20a1be2f 100755 --- a/hack/harness-test/setup.sh +++ b/hack/harness-test/setup.sh @@ -29,14 +29,13 @@ kubectl create secret generic vertex-credentials \ --dry-run=client -o yaml | kubectl apply -f - echo " vertex-credentials created" -# Hub token (JWT signed with default key "tackle") -HUB_KEY="${HUB_KEY:-tackle}" -EXP=$(( $(date +%s) + 86400 )) -HEADER_B64=$(printf '{"typ":"JWT","alg":"HS512"}' | base64 | tr -d '=' | tr '+/' '-_' | tr -d '\n') -PAYLOAD_B64=$(printf '{"sub":"admin","scope":"*:*","exp":%d}' "$EXP" | base64 | tr -d '=' | tr '+/' '-_' | tr -d '\n') -SIGNATURE=$(printf '%s.%s' "$HEADER_B64" "$PAYLOAD_B64" | openssl dgst -sha512 -hmac "$HUB_KEY" -binary | base64 | tr -d '=' | tr '+/' '-_' | tr -d '\n') -HUB_TOKEN="${HEADER_B64}.${PAYLOAD_B64}.${SIGNATURE}" -echo " hub token generated (expires in 24h)" +# Hub token — from env or hub-local.sh. Hub must be running. +# Hub token — must be set in environment. +if [ -z "${HUB_TOKEN:-}" ]; then + echo "ERROR: HUB_TOKEN must be set. Export HUB_TOKEN from your Hub instance." + exit 1 +fi +echo " hub token set (HUB_TOKEN_ID=${HUB_TOKEN_ID:-})" echo "" echo "=== Building agent images ===" @@ -105,11 +104,12 @@ TIMESTAMP=$(date +%s) sed -e "s/__GCP_PROJECT_ID__/$GCP_PROJECT_ID/g" \ -e "s/__TIMESTAMP__/$TIMESTAMP/g" \ -e "s|__HUB_TOKEN__|$HUB_TOKEN|g" \ - "$SCRIPT_DIR/playbook-resources.yaml" | kubectl apply -f - -echo " AgentPlaybookRun: coolstore-migration-$TIMESTAMP" + -e "s|__HUB_TOKEN_ID__|${HUB_TOKEN_ID:-}|g" \ + "$SCRIPT_DIR/workflow-resources.yaml" | kubectl apply -f - +echo " AgentWorkflowRun: coolstore-migration-$TIMESTAMP" echo "" echo "=== Done ===" -echo "Watch the run: kubectl get agentplaybookrun coolstore-migration-$TIMESTAMP -w" +echo "Watch the run: kubectl get agentworkflowrun coolstore-migration-$TIMESTAMP -w" echo "Check pods: kubectl get pods" echo "View logs: kubectl logs -f coolstore-migration-${TIMESTAMP}-plan -c agent" diff --git a/hack/harness-test/workflow-resources.yaml b/hack/harness-test/workflow-resources.yaml index 8824681a..a622151f 100644 --- a/hack/harness-test/workflow-resources.yaml +++ b/hack/harness-test/workflow-resources.yaml @@ -162,6 +162,8 @@ spec: value: "http://host.containers.internal:8080" - name: HUB_TOKEN value: "__HUB_TOKEN__" + - name: HUB_TOKEN_ID + value: "__HUB_TOKEN_ID__" - name: APP_ID value: "1" - name: TARGET_BRANCH diff --git a/hack/setup-e2e.sh b/hack/setup-e2e.sh index 82f7b007..e27e370a 100755 --- a/hack/setup-e2e.sh +++ b/hack/setup-e2e.sh @@ -31,6 +31,8 @@ if [ "${CONTAINER_TOOL}" = "podman" ]; then export KIND_EXPERIMENTAL_PROVIDER=podman fi +echo "=== Source: $(git rev-parse --short HEAD) ($(git rev-parse --abbrev-ref HEAD)) ===" +echo "" echo "=== Building images ===" echo "Building controller image: ${IMG}" diff --git a/harness/cmd/migration-harness/main.go b/harness/cmd/migration-harness/main.go index e786b2f0..54c88e1f 100644 --- a/harness/cmd/migration-harness/main.go +++ b/harness/cmd/migration-harness/main.go @@ -73,13 +73,15 @@ func runStage(cmd *cobra.Command, args []string) error { tokenID, _ := strconv.ParseUint(cfg.HubTokenID, 10, 64) defer func() { if err := hubClient.RevokeToken(uint(tokenID)); err != nil { - logging.Warn("hub token revocation: %v", err) + logging.Warn("hub token revocation (id=%d): %v", tokenID, err) } else { - logging.Ok("hub token revoked") + logging.Ok("hub token revoked (id=%d)", tokenID) } }() } else if cfg.HubTokenID == "" && cfg.HubToken != "" { logging.Warn("HUB_TOKEN_ID not set — skipping token revocation (token will expire via TTL)") + } else if cfg.WorkflowStage != "" { + logging.Info("workflow stage %s/%s — skipping token revocation", cfg.WorkflowStage, cfg.WorkflowStageCount) } if cfg.TargetBranch == creds.Branch { diff --git a/images/agent-base/Containerfile b/images/agent-base/Containerfile index 81e6217e..f7823392 100644 --- a/images/agent-base/Containerfile +++ b/images/agent-base/Containerfile @@ -60,8 +60,7 @@ COPY --from=builder /migration-harness /opt/migration-harness/bin/migration-harn RUN mkdir -p /opt/skills /workspace /home/harness/.migration-harness \ && useradd -u 1001 -g 0 -d /home/harness -s /sbin/nologin harness \ && chown -R 1001:0 /home/harness /workspace \ - && chmod -R g=u /home/harness /workspace \ - && chmod 1777 /tmp + && chmod -R g=u /home/harness /workspace ENV HOME=/home/harness WORKDIR /workspace diff --git a/internal/controller/agentrun_controller.go b/internal/controller/agentrun_controller.go index 9c884e7c..0f6e4365 100644 --- a/internal/controller/agentrun_controller.go +++ b/internal/controller/agentrun_controller.go @@ -343,6 +343,18 @@ func (r *AgentRunReconciler) createSandbox( MountPath: "/workspace", }) + // Writable /tmp for tools that create temp files at runtime. + volumes = append(volumes, corev1.Volume{ + Name: "tmp", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "tmp", + MountPath: "/tmp", + }) + // Create the Sandbox CR. serviceEnabled := true sandbox := &sandboxv1beta1.Sandbox{ diff --git a/internal/controller/agentrun_controller_test.go b/internal/controller/agentrun_controller_test.go index f5070fa6..85a2df11 100644 --- a/internal/controller/agentrun_controller_test.go +++ b/internal/controller/agentrun_controller_test.go @@ -355,6 +355,37 @@ var _ = Describe("AgentRun Controller", func() { By("verifying restartPolicy is Never so failed stages are observable (#51)") Expect(sandbox.Spec.PodTemplate.Spec.RestartPolicy).To(Equal(corev1.RestartPolicyNever)) + By("verifying /tmp EmptyDir volume is present for writable temp space") + spec := sandbox.Spec.PodTemplate.Spec + var tmpVolFound bool + for _, v := range spec.Volumes { + if v.Name == "tmp" { + tmpVolFound = true + Expect(v.VolumeSource.EmptyDir).NotTo(BeNil()) + } + } + Expect(tmpVolFound).To(BeTrue(), "expected a 'tmp' EmptyDir volume") + + var tmpMountFound bool + for _, m := range spec.Containers[0].VolumeMounts { + if m.Name == "tmp" { + tmpMountFound = true + Expect(m.MountPath).To(Equal("/tmp")) + } + } + Expect(tmpMountFound).To(BeTrue(), "expected a volume mount for /tmp") + + By("verifying workspace EmptyDir volume is present") + var wsVolFound bool + for _, v := range spec.Volumes { + if v.Name == "workspace" { + wsVolFound = true + Expect(v.VolumeSource.EmptyDir).NotTo(BeNil()) + Expect(v.VolumeSource.EmptyDir.SizeLimit).NotTo(BeNil()) + } + } + Expect(wsVolFound).To(BeTrue(), "expected a 'workspace' EmptyDir volume") + By("verifying the single-key provider credential is injected as API_KEY") container := sandbox.Spec.PodTemplate.Spec.Containers[0] var apiKey *corev1.EnvVar diff --git a/skills/execute/SKILL.md b/skills/execute/SKILL.md index 584fa3e4..a843680f 100644 --- a/skills/execute/SKILL.md +++ b/skills/execute/SKILL.md @@ -41,7 +41,7 @@ For each step in PLAN.md, follow this exact sequence: 1. Read the target file 2. Apply transformations per the step's instructions and reference patterns 3. Write the modified file -4. Run: git add -A && git commit -m "" +4. Run: git add && git commit -m "" 5. Move to the next step immediately ``` diff --git a/skills/plan/SKILL.md b/skills/plan/SKILL.md index b9493e80..995f622b 100644 --- a/skills/plan/SKILL.md +++ b/skills/plan/SKILL.md @@ -234,4 +234,5 @@ Write `PLAN.md` to the project root with this structure: - Do NOT skip graphify — the graph is essential for later stages - Read selectively — the graph gives you most of what you need - Report which reference you used in the Goal section of PLAN.md -- When done, run: `git add PLAN.md graph.json && git commit -m "Add migration plan and code graph"` +- When done, run: `git add PLAN.md && git commit -m "Add migration plan"` +- Do NOT commit `graphify-out/`, `.goose/`, or other generated artifacts — they are gitignored diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md index abc480e7..a8c25886 100644 --- a/skills/verify/SKILL.md +++ b/skills/verify/SKILL.md @@ -36,7 +36,7 @@ For each compiler error: 2. Read the source file 3. Apply a minimal, conservative fix 4. Do NOT change code that is not related to the error -5. Run `git add -A && git commit -m "Fix: "` +5. Run `git add && git commit -m "Fix: "` Consult reference files from loaded migration skills at `/opt/skills/*/references/` for common error-fix mappings specific to @@ -72,7 +72,7 @@ to fix failing tests. Test failures are expected after a migration and are documented in the result, not fixed here. After running tests, commit any remaining changes: -`git add -A && git commit -m "Verify: build and test results"` +`git add && git commit -m "Verify: build and test results"` --- From 9df46f1a30db2b8c63bddc304174d7cbf18e0f5e Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Mon, 3 Aug 2026 16:23:09 -0400 Subject: [PATCH 3/7] Add changelog fragments for #91 and #92 Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- changes/unreleased/91-sandbox-tmp-emptydir.yaml | 5 +++++ changes/unreleased/92-skill-commit-hygiene.yaml | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 changes/unreleased/91-sandbox-tmp-emptydir.yaml create mode 100644 changes/unreleased/92-skill-commit-hygiene.yaml diff --git a/changes/unreleased/91-sandbox-tmp-emptydir.yaml b/changes/unreleased/91-sandbox-tmp-emptydir.yaml new file mode 100644 index 00000000..9c851b85 --- /dev/null +++ b/changes/unreleased/91-sandbox-tmp-emptydir.yaml @@ -0,0 +1,5 @@ +kind: bugfix +description: > + Mount an EmptyDir volume at /tmp in sandbox pods so tools can write + temp files at runtime. Removed stale chmod from Containerfile since + containerd overlay FS does not preserve image-layer permissions. diff --git a/changes/unreleased/92-skill-commit-hygiene.yaml b/changes/unreleased/92-skill-commit-hygiene.yaml new file mode 100644 index 00000000..e09254ac --- /dev/null +++ b/changes/unreleased/92-skill-commit-hygiene.yaml @@ -0,0 +1,5 @@ +kind: bugfix +description: > + Updated plan, execute, and verify skills to use targeted git add + instead of git add -A, preventing gitignored artifacts like + graphify-out/ from being committed to migration PRs. From 28e6b7a3c03f1d86549138438e229f0744a7d868 Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Mon, 3 Aug 2026 16:37:57 -0400 Subject: [PATCH 4/7] Extract tmpVolumeName constant to fix goconst lint Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- internal/controller/agentrun_controller.go | 6 ++++-- internal/controller/agentrun_controller_test.go | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/controller/agentrun_controller.go b/internal/controller/agentrun_controller.go index 0f6e4365..74c4c5c1 100644 --- a/internal/controller/agentrun_controller.go +++ b/internal/controller/agentrun_controller.go @@ -50,6 +50,8 @@ const ( // workspaceVolumeName is the name of the EmptyDir volume for the agent workspace. workspaceVolumeName = "workspace" + tmpVolumeName = "tmp" + // sandboxFinishedReasonSucceeded is the Sandbox condition reason for // success. Must match Agent Sandbox's SandboxReasonPodSucceeded constant. sandboxFinishedReasonSucceeded = "PodSucceeded" @@ -345,13 +347,13 @@ func (r *AgentRunReconciler) createSandbox( // Writable /tmp for tools that create temp files at runtime. volumes = append(volumes, corev1.Volume{ - Name: "tmp", + Name: tmpVolumeName, VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }) volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "tmp", + Name: tmpVolumeName, MountPath: "/tmp", }) diff --git a/internal/controller/agentrun_controller_test.go b/internal/controller/agentrun_controller_test.go index 85a2df11..bb5da9a2 100644 --- a/internal/controller/agentrun_controller_test.go +++ b/internal/controller/agentrun_controller_test.go @@ -359,7 +359,7 @@ var _ = Describe("AgentRun Controller", func() { spec := sandbox.Spec.PodTemplate.Spec var tmpVolFound bool for _, v := range spec.Volumes { - if v.Name == "tmp" { + if v.Name == tmpVolumeName { tmpVolFound = true Expect(v.VolumeSource.EmptyDir).NotTo(BeNil()) } @@ -368,7 +368,7 @@ var _ = Describe("AgentRun Controller", func() { var tmpMountFound bool for _, m := range spec.Containers[0].VolumeMounts { - if m.Name == "tmp" { + if m.Name == tmpVolumeName { tmpMountFound = true Expect(m.MountPath).To(Equal("/tmp")) } From f7363a3448af27dc707481077460fe5dd76dd9a5 Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Tue, 4 Aug 2026 08:34:32 -0400 Subject: [PATCH 5/7] Address CodeRabbit review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix HUB_APP_ID → APP_ID in CONTEXT.md and ADR 0006 - Validate token ID, stage, and count in shouldRevokeToken - Register token revocation defer before resolveFromHub - Set 1Gi SizeLimit on /tmp EmptyDir volume - Assert ReadOnly: false on /tmp mount in tests Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- CONTEXT.md | 6 +-- ...6-hub-addon-pattern-for-agent-resources.md | 6 +-- harness/cmd/migration-harness/main.go | 43 +++++++++++-------- harness/cmd/migration-harness/main_test.go | 25 ++++++++++- internal/controller/agentrun_controller.go | 4 +- .../controller/agentrun_controller_test.go | 2 + 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index fb8c9a11..c6adf17c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -65,7 +65,7 @@ gateway Service exists, creates a sandbox through the OpenShell gateway API, and tracks status to completion. Parameters are domain-agnostic — the controller passes them through without interpretation. For Konveyor-managed agents, Hub injects connectivity -info (`HUB_BASE_URL`, `HUB_APP_ID`, scoped API token) into the +info (`HUB_BASE_URL`, `APP_ID`, scoped API token) into the AgentRun's env at create time; the harness resolves application metadata from Hub at runtime. @@ -150,7 +150,7 @@ pattern; and (2) a runtime data service that the harness calls (via a scoped API token) to fetch application metadata, decrypted git credentials, and analysis results — the same role Hub plays for addons today. At AgentRun create time, Hub mints a scoped token and -injects `HUB_BASE_URL`, `HUB_APP_ID`, the token (`HUB_TOKEN`), and the +injects `HUB_BASE_URL`, `APP_ID`, the token (`HUB_TOKEN`), and the token's database ID (`HUB_TOKEN_ID`) into the AgentRun's env/envFrom, then creates the CR. Hub does not resolve application data at create time — the harness resolves at runtime. Hub is @@ -158,7 +158,7 @@ fire-and-forget; it does not launch or manage agent workloads. **Harness** — The Go binary entrypoint in the agent base image, analogous to the addon adapter (`shared/addon/adapter`) in Hub. In -Konveyor-managed mode (`HUB_BASE_URL` + `HUB_APP_ID` set), the harness +Konveyor-managed mode (`HUB_BASE_URL` + `APP_ID` set), the harness acts as a Hub client: resolves the application's git URL, branch, and decrypted credentials from Hub, clones the repo, and configures the workspace so the agent cannot push (credentials stay in the diff --git a/docs/adr/0006-hub-addon-pattern-for-agent-resources.md b/docs/adr/0006-hub-addon-pattern-for-agent-resources.md index 72f6bce8..e253ca2d 100644 --- a/docs/adr/0006-hub-addon-pattern-for-agent-resources.md +++ b/docs/adr/0006-hub-addon-pattern-for-agent-resources.md @@ -2,7 +2,7 @@ Hub's integration with the agent platform follows the established addon pattern rather than introducing smart resolution endpoints. Hub creates -AgentRun/AgentWorkflowRun CRs with `HUB_BASE_URL`, `HUB_APP_ID`, and a +AgentRun/AgentWorkflowRun CRs with `HUB_BASE_URL`, `APP_ID`, and a scoped API token injected as env/envFrom — then walks away (fire-and-forget). The harness resolves application metadata from Hub at runtime, the same way the addon adapter does for addon tasks today. @@ -36,7 +36,7 @@ When Hub receives a create request for an AgentRun or AgentWorkflowRun: 1. Mints a scoped API token with `AddonScopes` 2. Stores the token and its database ID in a Kubernetes Secret (`HUB_TOKEN`, `HUB_TOKEN_ID`) -3. Adds `HUB_BASE_URL`, `HUB_APP_ID`, and the token Secret to the CR's +3. Adds `HUB_BASE_URL`, `APP_ID`, and the token Secret to the CR's `spec.env` and `spec.envFrom` 4. Creates the CR via `client.Create()` @@ -56,7 +56,7 @@ Other resource types are listed unfiltered. ### Harness resolves at runtime The harness acts as a Hub client (analogous to the addon adapter). In -managed mode (`HUB_BASE_URL` + `HUB_APP_ID` set), it calls Hub's existing +managed mode (`HUB_BASE_URL` + `APP_ID` set), it calls Hub's existing REST API to resolve the application's git URL, branch, and decrypted credentials. In standalone mode, it reads from `KONVEYOR_PARAM_*` env vars and mounted Secrets. diff --git a/harness/cmd/migration-harness/main.go b/harness/cmd/migration-harness/main.go index 54c88e1f..b8d7812b 100644 --- a/harness/cmd/migration-harness/main.go +++ b/harness/cmd/migration-harness/main.go @@ -61,14 +61,9 @@ func runStage(cmd *cobra.Command, args []string) error { cloneDir = "/workspace/repo" } - creds, hubClient, err := resolveFromHub(cfg) - if err != nil { - return fmt.Errorf("hub resolution: %w", err) - } - - // Stage-aware token revocation: revoke the Hub API token on exit - // for standalone runs and the last workflow stage. Intermediate - // stages skip so subsequent stages can reuse the token. + // Stage-aware token revocation: register cleanup before Hub resolution + // so the token is revoked even if resolveFromHub fails partway. + hubClient := hub.NewClient(cfg.HubBaseURL, cfg.HubToken) if shouldRevokeToken(cfg) { tokenID, _ := strconv.ParseUint(cfg.HubTokenID, 10, 64) defer func() { @@ -84,6 +79,11 @@ func runStage(cmd *cobra.Command, args []string) error { logging.Info("workflow stage %s/%s — skipping token revocation", cfg.WorkflowStage, cfg.WorkflowStageCount) } + creds, err := resolveFromHub(cfg, hubClient) + if err != nil { + return fmt.Errorf("hub resolution: %w", err) + } + if cfg.TargetBranch == creds.Branch { return fmt.Errorf("TARGET_BRANCH %q must differ from source branch", cfg.TargetBranch) } @@ -275,25 +275,23 @@ func discoverSkills() (string, []string, error) { return combined.String(), matches, nil } -func resolveFromHub(cfg *config.Config) (*git.Credentials, *hub.Client, error) { +func resolveFromHub(cfg *config.Config, hubClient *hub.Client) (*git.Credentials, error) { logging.Header("Hub Resolution") appID, err := hub.ParseAppID(cfg.AppID) if err != nil { - return nil, nil, fmt.Errorf("invalid APP_ID %q: %w", cfg.AppID, err) + return nil, fmt.Errorf("invalid APP_ID %q: %w", cfg.AppID, err) } - hubClient := hub.NewClient(cfg.HubBaseURL, cfg.HubToken) - app, err := hubClient.FetchApp(appID) if err != nil { - return nil, nil, fmt.Errorf("fetch app: %w", err) + return nil, fmt.Errorf("fetch app: %w", err) } logging.Ok("app: %s (id=%d), repo: %s", app.Name, app.ID, app.Repository.URL) identity, err := hubClient.FetchGitCreds(appID) if err != nil { - return nil, nil, fmt.Errorf("fetch git creds: %w", err) + return nil, fmt.Errorf("fetch git creds: %w", err) } creds := &git.Credentials{ @@ -309,7 +307,7 @@ func resolveFromHub(cfg *config.Config) (*git.Credentials, *hub.Client, error) { logging.Ok("git identity: %s", identity.Name) } - return creds, hubClient, nil + return creds, nil } // shouldRevokeToken decides whether the harness should revoke the Hub API @@ -319,10 +317,21 @@ func shouldRevokeToken(cfg *config.Config) bool { if cfg.HubTokenID == "" { return false } - if cfg.WorkflowStage == "" { + if _, err := strconv.ParseUint(cfg.HubTokenID, 10, 64); err != nil { + return false + } + if cfg.WorkflowStage == "" && cfg.WorkflowStageCount == "" { return true } - return cfg.WorkflowStage == cfg.WorkflowStageCount + stage, err := strconv.ParseUint(cfg.WorkflowStage, 10, 64) + if err != nil || stage == 0 { + return false + } + count, err := strconv.ParseUint(cfg.WorkflowStageCount, 10, 64) + if err != nil || count == 0 { + return false + } + return stage <= count && stage == count } func fetchAndWriteAnalysis(hubClient *hub.Client, appIDStr string, workDir string) error { diff --git a/harness/cmd/migration-harness/main_test.go b/harness/cmd/migration-harness/main_test.go index b74f904f..5965a87f 100644 --- a/harness/cmd/migration-harness/main_test.go +++ b/harness/cmd/migration-harness/main_test.go @@ -125,11 +125,11 @@ func TestShouldRevokeToken(t *testing.T) { want: false, }, { - name: "count set but stage missing — standalone", + name: "count set but stage missing — skip", hubTokenID: "1", workflowStage: "", workflowStageCount: "3", - want: true, + want: false, }, { name: "stage exceeds count — skip", @@ -159,6 +159,27 @@ func TestShouldRevokeToken(t *testing.T) { workflowStageCount: "3", want: false, }, + { + name: "equal non-numeric values — skip", + hubTokenID: "1", + workflowStage: "abc", + workflowStageCount: "abc", + want: false, + }, + { + name: "equal zero values — skip", + hubTokenID: "1", + workflowStage: "0", + workflowStageCount: "0", + want: false, + }, + { + name: "non-numeric token ID — skip", + hubTokenID: "abc", + workflowStage: "", + workflowStageCount: "", + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/controller/agentrun_controller.go b/internal/controller/agentrun_controller.go index 74c4c5c1..6a97bbbc 100644 --- a/internal/controller/agentrun_controller.go +++ b/internal/controller/agentrun_controller.go @@ -349,7 +349,9 @@ func (r *AgentRunReconciler) createSandbox( volumes = append(volumes, corev1.Volume{ Name: tmpVolumeName, VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, + EmptyDir: &corev1.EmptyDirVolumeSource{ + SizeLimit: resource.NewQuantity(1*1024*1024*1024, resource.BinarySI), // 1Gi + }, }, }) volumeMounts = append(volumeMounts, corev1.VolumeMount{ diff --git a/internal/controller/agentrun_controller_test.go b/internal/controller/agentrun_controller_test.go index bb5da9a2..1d8f2b94 100644 --- a/internal/controller/agentrun_controller_test.go +++ b/internal/controller/agentrun_controller_test.go @@ -362,6 +362,7 @@ var _ = Describe("AgentRun Controller", func() { if v.Name == tmpVolumeName { tmpVolFound = true Expect(v.VolumeSource.EmptyDir).NotTo(BeNil()) + Expect(v.VolumeSource.EmptyDir.SizeLimit).NotTo(BeNil()) } } Expect(tmpVolFound).To(BeTrue(), "expected a 'tmp' EmptyDir volume") @@ -371,6 +372,7 @@ var _ = Describe("AgentRun Controller", func() { if m.Name == tmpVolumeName { tmpMountFound = true Expect(m.MountPath).To(Equal("/tmp")) + Expect(m.ReadOnly).To(BeFalse()) } } Expect(tmpMountFound).To(BeTrue(), "expected a volume mount for /tmp") From 0a68f0c9cb7a66a51e864d6230ac9bcbf31409b8 Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Tue, 4 Aug 2026 13:03:40 -0400 Subject: [PATCH 6/7] Address review feedback from djzager - Restore graph.json to plan skill commit (not gitignored, useful for audit) - Use concrete file path examples in skill git add instructions - Fix test case name: "first of two stages" not "second" - Return parsed tokenID from shouldRevokeToken to avoid double-parse Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- harness/cmd/migration-harness/main.go | 30 ++++++++++++---------- harness/cmd/migration-harness/main_test.go | 5 ++-- skills/execute/SKILL.md | 2 +- skills/plan/SKILL.md | 4 +-- skills/verify/SKILL.md | 4 +-- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/harness/cmd/migration-harness/main.go b/harness/cmd/migration-harness/main.go index b8d7812b..0069f012 100644 --- a/harness/cmd/migration-harness/main.go +++ b/harness/cmd/migration-harness/main.go @@ -64,10 +64,9 @@ func runStage(cmd *cobra.Command, args []string) error { // Stage-aware token revocation: register cleanup before Hub resolution // so the token is revoked even if resolveFromHub fails partway. hubClient := hub.NewClient(cfg.HubBaseURL, cfg.HubToken) - if shouldRevokeToken(cfg) { - tokenID, _ := strconv.ParseUint(cfg.HubTokenID, 10, 64) + if tokenID, revoke := shouldRevokeToken(cfg); revoke { defer func() { - if err := hubClient.RevokeToken(uint(tokenID)); err != nil { + if err := hubClient.RevokeToken(tokenID); err != nil { logging.Warn("hub token revocation (id=%d): %v", tokenID, err) } else { logging.Ok("hub token revoked (id=%d)", tokenID) @@ -311,27 +310,32 @@ func resolveFromHub(cfg *config.Config, hubClient *hub.Client) (*git.Credentials } // shouldRevokeToken decides whether the harness should revoke the Hub API -// token on exit. Standalone AgentRuns always revoke. Workflow stages -// revoke only on the last stage so subsequent stages can reuse the token. -func shouldRevokeToken(cfg *config.Config) bool { +// token on exit and returns the parsed token ID. Standalone AgentRuns +// always revoke. Workflow stages revoke only on the last stage so +// subsequent stages can reuse the token. +func shouldRevokeToken(cfg *config.Config) (uint, bool) { if cfg.HubTokenID == "" { - return false + return 0, false } - if _, err := strconv.ParseUint(cfg.HubTokenID, 10, 64); err != nil { - return false + tokenID, err := strconv.ParseUint(cfg.HubTokenID, 10, 64) + if err != nil { + return 0, false } if cfg.WorkflowStage == "" && cfg.WorkflowStageCount == "" { - return true + return uint(tokenID), true } stage, err := strconv.ParseUint(cfg.WorkflowStage, 10, 64) if err != nil || stage == 0 { - return false + return 0, false } count, err := strconv.ParseUint(cfg.WorkflowStageCount, 10, 64) if err != nil || count == 0 { - return false + return 0, false + } + if stage <= count && stage == count { + return uint(tokenID), true } - return stage <= count && stage == count + return 0, false } func fetchAndWriteAnalysis(hubClient *hub.Client, appIDStr string, workDir string) error { diff --git a/harness/cmd/migration-harness/main_test.go b/harness/cmd/migration-harness/main_test.go index 5965a87f..b6ab9c65 100644 --- a/harness/cmd/migration-harness/main_test.go +++ b/harness/cmd/migration-harness/main_test.go @@ -104,7 +104,7 @@ func TestShouldRevokeToken(t *testing.T) { want: false, }, { - name: "second of two stages — skip", + name: "first of two stages — skip", hubTokenID: "1", workflowStage: "1", workflowStageCount: "2", @@ -188,7 +188,8 @@ func TestShouldRevokeToken(t *testing.T) { WorkflowStage: tt.workflowStage, WorkflowStageCount: tt.workflowStageCount, } - if got := shouldRevokeToken(cfg); got != tt.want { + _, got := shouldRevokeToken(cfg) + if got != tt.want { t.Errorf("shouldRevokeToken() = %v, want %v", got, tt.want) } }) diff --git a/skills/execute/SKILL.md b/skills/execute/SKILL.md index a843680f..808beda2 100644 --- a/skills/execute/SKILL.md +++ b/skills/execute/SKILL.md @@ -41,7 +41,7 @@ For each step in PLAN.md, follow this exact sequence: 1. Read the target file 2. Apply transformations per the step's instructions and reference patterns 3. Write the modified file -4. Run: git add && git commit -m "" +4. Run: git add path/to/ModifiedFile.java && git commit -m "" 5. Move to the next step immediately ``` diff --git a/skills/plan/SKILL.md b/skills/plan/SKILL.md index 995f622b..0cccd029 100644 --- a/skills/plan/SKILL.md +++ b/skills/plan/SKILL.md @@ -234,5 +234,5 @@ Write `PLAN.md` to the project root with this structure: - Do NOT skip graphify — the graph is essential for later stages - Read selectively — the graph gives you most of what you need - Report which reference you used in the Goal section of PLAN.md -- When done, run: `git add PLAN.md && git commit -m "Add migration plan"` -- Do NOT commit `graphify-out/`, `.goose/`, or other generated artifacts — they are gitignored +- When done, run: `git add PLAN.md graph.json && git commit -m "Add migration plan"` +- Do NOT commit `graphify-out/`, `.goose/`, or other generated artifacts diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md index a8c25886..43b3eae4 100644 --- a/skills/verify/SKILL.md +++ b/skills/verify/SKILL.md @@ -36,7 +36,7 @@ For each compiler error: 2. Read the source file 3. Apply a minimal, conservative fix 4. Do NOT change code that is not related to the error -5. Run `git add && git commit -m "Fix: "` +5. Run `git add path/to/FixedFile.java && git commit -m "Fix: "` Consult reference files from loaded migration skills at `/opt/skills/*/references/` for common error-fix mappings specific to @@ -72,7 +72,7 @@ to fix failing tests. Test failures are expected after a migration and are documented in the result, not fixed here. After running tests, commit any remaining changes: -`git add && git commit -m "Verify: build and test results"` +`git add path/to/ChangedFile.java && git commit -m "Verify: build and test results"` --- From faa6dd9292af59900ba21687f91c35f845a4548e Mon Sep 17 00:00:00 2001 From: Savitha Raghunathan Date: Tue, 4 Aug 2026 13:10:16 -0400 Subject: [PATCH 7/7] Clean up minor review findings - Remove redundant stage <= count guard in shouldRevokeToken - Remove dead hubClient != nil check (client is always created now) - Remove stale duplicate comment in setup script - Log warning for malformed workflow metadata instead of treating as valid stage Assisted-By: Claude Code Signed-off-by: Savitha Raghunathan --- hack/harness-test/setup.sh | 1 - harness/cmd/migration-harness/main.go | 18 +++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/hack/harness-test/setup.sh b/hack/harness-test/setup.sh index 20a1be2f..c75bfcb2 100755 --- a/hack/harness-test/setup.sh +++ b/hack/harness-test/setup.sh @@ -29,7 +29,6 @@ kubectl create secret generic vertex-credentials \ --dry-run=client -o yaml | kubectl apply -f - echo " vertex-credentials created" -# Hub token — from env or hub-local.sh. Hub must be running. # Hub token — must be set in environment. if [ -z "${HUB_TOKEN:-}" ]; then echo "ERROR: HUB_TOKEN must be set. Export HUB_TOKEN from your Hub instance." diff --git a/harness/cmd/migration-harness/main.go b/harness/cmd/migration-harness/main.go index 0069f012..9bc6ba7b 100644 --- a/harness/cmd/migration-harness/main.go +++ b/harness/cmd/migration-harness/main.go @@ -74,8 +74,14 @@ func runStage(cmd *cobra.Command, args []string) error { }() } else if cfg.HubTokenID == "" && cfg.HubToken != "" { logging.Warn("HUB_TOKEN_ID not set — skipping token revocation (token will expire via TTL)") - } else if cfg.WorkflowStage != "" { - logging.Info("workflow stage %s/%s — skipping token revocation", cfg.WorkflowStage, cfg.WorkflowStageCount) + } else if cfg.HubTokenID != "" { + stage, sErr := strconv.ParseUint(cfg.WorkflowStage, 10, 64) + count, cErr := strconv.ParseUint(cfg.WorkflowStageCount, 10, 64) + if sErr == nil && cErr == nil && stage > 0 && count > 0 { + logging.Info("workflow stage %d/%d — skipping token revocation", stage, count) + } else { + logging.Warn("invalid workflow metadata (stage=%q, count=%q) — skipping token revocation", cfg.WorkflowStage, cfg.WorkflowStageCount) + } } creds, err := resolveFromHub(cfg, hubClient) @@ -135,10 +141,8 @@ func runStage(cmd *cobra.Command, args []string) error { if hasSkills { // 4b. Write analysis to workspace (if resolved from Hub) - if hubClient != nil { - if err := fetchAndWriteAnalysis(hubClient, cfg.AppID, cloneDir); err != nil { - logging.Warn("analysis fetch: %v", err) - } + if err := fetchAndWriteAnalysis(hubClient, cfg.AppID, cloneDir); err != nil { + logging.Warn("analysis fetch: %v", err) } // 4c. Commit harness-managed files so they survive on the branch @@ -332,7 +336,7 @@ func shouldRevokeToken(cfg *config.Config) (uint, bool) { if err != nil || count == 0 { return 0, false } - if stage <= count && stage == count { + if stage == count { return uint(tokenID), true } return 0, false