Skip to content

feat(agent): complete Go agent orchestration and entrypoint - #379

Open
rice-riley wants to merge 1 commit into
agent-mocks-219from
agent-go-main-219
Open

feat(agent): complete Go agent orchestration and entrypoint#379
rice-riley wants to merge 1 commit into
agent-mocks-219from
agent-go-main-219

Conversation

@rice-riley

@rice-riley rice-riley commented Jul 29, 2026

Copy link
Copy Markdown
Member

Description

Completes the final orchestration for the Go agent rewrite and activates it through the production entrypoint.

  • Adds the agent runtime with typed exit codes, signal-aware cancellation, environment configuration, and current and legacy invocation formats.
  • Orchestrates package preparation, configuration loading, lifecycle steps and checks, completion flags, upgrade history, logging, and cleanup.
  • Runs decoded interrupts with per-resource completion markers and boot-ID-based node restart completion.
  • Streams step and interrupt output while retaining collision-safe host log files when enabled.
  • Replaces the placeholder executable with the signal-aware agent entrypoint.
  • Documents invocation forms, runtime behavior, environment variables, logging, signals, and exit statuses.

Depends on #408.
Closes #219

Checklist

  • I am familiar with the Contributing Guidelines.
  • My commits are signed off (git commit -s) per the DCO.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@rice-riley rice-riley self-assigned this Jul 29, 2026
@rice-riley
rice-riley requested a review from a team July 29, 2026 00:04
@github-actions github-actions Bot added doc Documentation change (PR path label; doc issues use the Documentation type) component/operator Skyhook operator (controller-manager) component/agent Skyhook agent (package executor) component/ci CI workflows, GitHub Actions, and repo tooling labels Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds a Go agent runtime with CLI argument parsing, environment configuration, validation, filesystem preparation, lifecycle step execution, interrupt handling, cancellation, logging, flags, and history updates. The CLI now handles SIGTERM and returns agent exit codes. Tests cover runtime behavior, package preparation, lifecycle execution, cancellation, interrupt markers, reboot retries, and retained logs. Documentation describes command forms, environment variables, markers, logging, and exit statuses.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: ayuskauskas, lockwobr

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation covers most requirements, but removes the greeting while issue #219 requires the exact Python-compatible startup banner. Restore the exact Python-compatible startup banner and verify all four CLI forms, chroot-exec routing, and compatibility behavior required by #219.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: completing Go agent orchestration and enabling the production entrypoint.
Description check ✅ Passed The description directly covers the orchestration, entrypoint, interrupt handling, logging, testing, and documentation changes.
Out of Scope Changes check ✅ Passed The code, tests, entrypoint, interrupt handling, and documentation changes directly support the orchestration and compatibility objectives in issue #219.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent-go-main-219

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/go/cmd/agent/main.go`:
- Around line 34-35: Rename the local variable nodewright_agent to idiomatic
mixedCaps naming, such as nodewrightAgent, and update its Run invocation
accordingly; leave the surrounding agent.New and execution flow unchanged.

In `@agent/go/internal/agent/agent.go`:
- Around line 249-263: Update normalizeRuntime to default stateRoot and logRoot
when they are empty, using the documented default root values before returning
the normalized runtimeConfig. Keep the existing dataDir, stdout, stderr, and
logger normalization unchanged so runRequest receives complete roots for
flags.NewLayout.
- Around line 241-247: Update envBool to parse environment values with
strconv.ParseBool instead of comparing only against defaultTrueValue, accepting
standard boolean forms while rejecting invalid values such as whitespace-padded
or arbitrary strings. When parsing fails, emit a warning and return the provided
fallback value so misconfigured default-true variables remain enabled rather
than silently becoming false.

In `@agent/go/internal/agent/interrupt.go`:
- Around line 107-155: The interrupt execution flow duplicates closeLog error
wrapping and errors.Join handling across failure branches. Extract a local
closeLogErr helper near closeLog that closes the log and returns the
consistently wrapped error, then reuse it in the log-retention and
execution-configuration failure paths (and the third matching branch) while
preserving the existing joined failure status.

In `@agent/go/internal/agent/package_test.go`:
- Around line 32-42: Update the “copies the container resolver into the mounted
root” test to check whether /etc/resolv.conf exists before calling
copyResolverConfig; skip the spec when the source file is absent, while
preserving the existing copy and content assertions when it is available.

In `@agent/go/internal/step/shared.go`:
- Around line 173-179: Update the environment initialization in runStep so a nil
input becomes an allocated writable map before assigning version and directory
values. Preserve cloning for non-nil environments and keep the existing
assignments to previousVersionEnv, currentVersionEnv, STEP_ROOT, and SKYHOOK_DIR
unchanged.

In `@agent/README.md`:
- Around line 74-80: Update the environment-variable documentation in the listed
README section to distinguish required variables from optional variables with
documented defaults: keep OVERLAY_FRAMEWORK_VERSION and SKYHOOK_RESOURCE_ID
required, and identify SKYHOOK_DATA_DIR, SKYHOOK_ROOT_DIR, and SKYHOOK_LOG_DIR
as defaulted. Correct the typos in “environment,” “this the,” and
“configuration” while preserving the documented defaults and descriptions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: e70aee63-f105-40f2-ac45-92205647c2b6

📥 Commits

Reviewing files that changed from the base of the PR and between bcef42d and f7b3432.

⛔ Files ignored due to path filters (1)
  • agent/go/go.sum is excluded by !**/*.sum
📒 Files selected for processing (39)
  • agent/README.md
  • agent/go/.mockery.yaml
  • agent/go/Makefile
  • agent/go/cmd/agent/main.go
  • agent/go/deps.mk
  • agent/go/go.mod
  • agent/go/internal/agent/agent.go
  • agent/go/internal/agent/agent_test.go
  • agent/go/internal/agent/interrupt.go
  • agent/go/internal/agent/interrupt_test.go
  • agent/go/internal/agent/package.go
  • agent/go/internal/agent/package_test.go
  • agent/go/internal/agent/request_test.go
  • agent/go/internal/agent/runtime_test.go
  • agent/go/internal/agent/steps.go
  • agent/go/internal/agent/steps_test.go
  • agent/go/internal/config/mock/SchemaValidator.go
  • agent/go/internal/config/validate_test.go
  • agent/go/internal/execution/execution.go
  • agent/go/internal/flags/flags.go
  • agent/go/internal/flags/flags_test.go
  • agent/go/internal/history/history.go
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/mock/Store.go
  • agent/go/internal/hostfs/copy.go
  • agent/go/internal/hostfs/copy_test.go
  • agent/go/internal/hostfs/hostfs.go
  • agent/go/internal/hostfs/hostfs_test.go
  • agent/go/internal/interrupts/mock/Interrupt.go
  • agent/go/internal/step/mock/Step.go
  • agent/go/internal/step/regular_step.go
  • agent/go/internal/step/regular_step_test.go
  • agent/go/internal/step/shared.go
  • agent/go/internal/step/shared_linux_test.go
  • agent/go/internal/step/shared_test.go
  • agent/go/internal/step/step.go
  • agent/go/internal/step/step_test.go
  • agent/go/internal/step/upgrade_step.go
  • agent/go/internal/step/upgrade_step_test.go
💤 Files with no reviewable changes (2)
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/history.go

Comment thread agent/go/cmd/agent/main.go Outdated
Comment thread agent/go/internal/agent/agent.go Outdated
Comment thread agent/go/internal/agent/agent.go
Comment thread agent/go/internal/agent/interrupt.go
Comment thread agent/go/internal/agent/package_test.go
Comment thread agent/go/internal/step/shared.go Outdated
Comment thread agent/README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/go/internal/agent/agent_test.go`:
- Around line 265-279: Extend the cancellation tests around New().Run with a
multi-step stage that cancels the context while the first step is executing.
Assert the active step aborts and the subsequent step does not start, covering
the per-iteration cancellation guard in steps.go rather than only validateRun’s
pre-flight ctx.Err() check.
- Around line 287-309: The test around orchestrator{}.runRequest assumes
/etc/resolv.conf exists when runtime.copyResolver is true. Make the
resolver-copy verification hermetic by injecting a controlled resolver source
through the available configuration path, or conditionally skip the resolver
assertion when the host file is absent, while preserving the existing success
and other file assertions.

In `@agent/go/internal/agent/interrupt.go`:
- Around line 181-199: Update configFromResourceID to parse resource IDs with an
unambiguous fixed prefix or encoding so package names containing underscores
remain intact, while still extracting the package version correctly. Preserve
validatePathComponent checks and the existing error behavior for malformed IDs.

In `@agent/go/internal/agent/steps_test.go`:
- Around line 129-142: The always-run completion-flag test only verifies
execution and must also verify the mandated warning. In the test setup around
runSteps, capture logger output with a bytes.Buffer instead of discarding it,
then assert the buffer contains the warning emitted when a completion flag
exists while runtime.alwaysRunStep is enabled.

In `@agent/go/internal/flags/flags.go`:
- Around line 136-150: Extract the shared path-resolution and validation
sequence from fileStore.Remove, Write, and Check into a small helper method.
Have each method call the helper and preserve the existing wrapped error context
and returned validated path, keeping their file-operation behavior unchanged.

In `@agent/go/internal/hostfs/hostfs.go`:
- Around line 225-263: Extract the shared temp-file write, explicit permission
adjustment, rename, and cleanup flow from writeFile and copyRegularFile into one
rooted-write helper. Ensure the helper calls root.Chmod with the requested mode
after creating the file so both paths produce identical permissions despite
umask masking, then update both callers to use it while preserving their
existing error and atomic-replacement behavior.

In `@agent/go/Makefile`:
- Around line 38-41: Update the license-fmt target in the agent/go Makefile so
format_license.py scopes changed-file processing to agent/go rather than the
repository root. Keep repo-wide formatting out of this component target; if
needed, place it in the top-level Makefile target instead.

In `@agent/README.md`:
- Line 87: Update the SKYHOOK_NODE_ORDER description in the README to insert
“is” after the variable name, so the sentence begins with “SKYHOOK_NODE_ORDER is
a zero-indexed monotonic position” while preserving the remaining wording.
- Line 65: Update the environment-variable description in the README to refer to
the agent rather than the controller, while preserving the existing meaning and
surrounding documentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: e7f7d403-1151-43cc-8774-ae7116bd5861

📥 Commits

Reviewing files that changed from the base of the PR and between f7b3432 and 5a86acf.

⛔ Files ignored due to path filters (1)
  • agent/go/go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • agent/README.md
  • agent/go/.mockery.yaml
  • agent/go/Makefile
  • agent/go/cmd/agent/main.go
  • agent/go/deps.mk
  • agent/go/go.mod
  • agent/go/internal/agent/agent.go
  • agent/go/internal/agent/agent_test.go
  • agent/go/internal/agent/interrupt.go
  • agent/go/internal/agent/interrupt_test.go
  • agent/go/internal/agent/package.go
  • agent/go/internal/agent/package_test.go
  • agent/go/internal/agent/steps.go
  • agent/go/internal/agent/steps_test.go
  • agent/go/internal/config/mock/SchemaValidator.go
  • agent/go/internal/config/validate_test.go
  • agent/go/internal/execution/execution.go
  • agent/go/internal/flags/flags.go
  • agent/go/internal/flags/flags_test.go
  • agent/go/internal/history/history.go
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/mock/Store.go
  • agent/go/internal/hostfs/copy.go
  • agent/go/internal/hostfs/copy_test.go
  • agent/go/internal/hostfs/hostfs.go
  • agent/go/internal/hostfs/hostfs_test.go
  • agent/go/internal/interrupts/mock/Interrupt.go
  • agent/go/internal/step/mock/Step.go
  • agent/go/internal/step/regular_step.go
  • agent/go/internal/step/regular_step_test.go
  • agent/go/internal/step/shared.go
  • agent/go/internal/step/shared_linux_test.go
  • agent/go/internal/step/shared_test.go
  • agent/go/internal/step/step.go
  • agent/go/internal/step/step_test.go
  • agent/go/internal/step/upgrade_step.go
  • agent/go/internal/step/upgrade_step_test.go
💤 Files with no reviewable changes (2)
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/history.go

Comment thread agent/go/internal/agent/agent_test.go
Comment thread agent/go/internal/agent/agent_test.go
Comment thread agent/go/internal/agent/interrupt.go
Comment thread agent/go/internal/agent/steps_test.go
Comment thread agent/go/internal/flags/flags.go
Comment thread agent/go/internal/hostfs/hostfs.go
Comment thread agent/go/Makefile
Comment thread agent/README.md Outdated
Comment thread agent/README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/go/deps.mk`:
- Around line 58-60: Update the addlicense target to validate that ADDLICENSE is
an executable installed binary and that its reported version matches
ADDLICENSE_VERSION; reinstall it via the existing go install command whenever
validation fails, including for stale or incorrect versions.

In `@agent/go/internal/agent/agent_test.go`:
- Around line 488-501: Update writeCancellationPackageFixture so the shell
script written for the apply step replaces the infinite no-op busy loop with
sleep 3600, while preserving the existing marker-file behavior and script
permissions.

In `@agent/go/internal/agent/package_test.go`:
- Around line 67-78: Add unit-test coverage in the existing package tests for
the remaining branches of prepareHost and ensurePackageData: reject an
ExpectedConfigFiles path escaping the configmaps directory, reject an expected
config path that is not a regular file, and cover ensurePackageData with an
existing non-directory copyRoot, a missing dataDir, and legacy node-files
copying. Use the existing Ginkgo/Gomega helpers and assert the relevant errors
and filesystem outcomes.

In `@agent/go/internal/agent/package.go`:
- Line 41: Align the path argument order between ensurePackageData and
prepareHost by using the same leading-parameter order in both functions, then
update the prepareHost call in agent.go and its specifications in
package_test.go to match. Keep the resulting order consistent so copyRoot and
rootMount cannot be accidentally swapped.

In `@agent/go/internal/agent/steps_test.go`:
- Around line 252-278: Update the test around runSteps to replace the discarded
runtime.stdout writer with a buffer, then assert that the captured stdout
contains the same output written to the retained log file. Preserve the existing
success and log-directory assertions, ensuring the test verifies both
destinations when runtime.writeLogs is enabled.

In `@agent/go/internal/config/validate_test.go`:
- Around line 173-178: Update the test setup around Loader.Load and the
validator mock to use malformed JSON instead of validConfigJSON for both the
Validate expectation and the load input. Keep the sentinel validation error and
Once expectation, ensuring the test fails if decoding occurs before
SchemaValidator.Validate.

In `@agent/go/internal/flags/flags.go`:
- Around line 159-164: Restore atomic publication for the state-file write in
the flags flow by writing through a temporary file under the same root and
renaming it into place only after a successful write, using a rooted
atomic-replace helper shared by flags and control files. Update the relevant
marker-writing path around hostfs.WriteFile, and add a failure-path test proving
a failed marker write leaves no completion flag that causes a later Check to
skip the step.

In `@agent/go/internal/flags/layout.go`:
- Around line 155-162: The collision allocation in CreateLogFile must preserve
suffix ordering when lower suffixes were deleted: avoid reusing a lower suffix
while a higher suffix for the same timestamp remains, and update CleanupOldLogs
in logs.go so retention does not rely solely on the reusable suffix to determine
same-timestamp age. Add a regression test covering cleanup, creating another log
at the same timestamp, and verifying the new log is retained. Update
agent/go/internal/flags/layout.go lines 155-162 and
agent/go/internal/flags/logs.go lines 82-90; add the test in the appropriate
affected test file.

In `@agent/go/internal/history/history.go`:
- Around line 157-160: Replace the direct hostfs.WriteFile call in the history
ledger save method with a rooted atomic-write operation that writes and syncs a
temporary regular file, renames it only after successful completion, and syncs
the containing directory. Preserve the existing error wrapping and add a
failure-path test verifying the prior ledger remains readable when replacement
fails.

In `@agent/go/Makefile`:
- Around line 54-56: Update the license validation loop in license-header-check
to remove the generated-source exemption based on the “Code generated ... DO NOT
EDIT.” marker. Ensure every file from license_files is checked for the
SPDX-License-Identifier: Apache-2.0 header.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 5ef277fc-96ce-49b5-8f87-b9b605500d8c

📥 Commits

Reviewing files that changed from the base of the PR and between 5a86acf and 82de1d8.

⛔ Files ignored due to path filters (1)
  • agent/go/go.sum is excluded by !**/*.sum
📒 Files selected for processing (41)
  • agent/README.md
  • agent/go/.mockery.yaml
  • agent/go/Makefile
  • agent/go/cmd/agent/main.go
  • agent/go/deps.mk
  • agent/go/go.mod
  • agent/go/internal/agent/agent.go
  • agent/go/internal/agent/agent_test.go
  • agent/go/internal/agent/interrupt.go
  • agent/go/internal/agent/interrupt_test.go
  • agent/go/internal/agent/package.go
  • agent/go/internal/agent/package_test.go
  • agent/go/internal/agent/steps.go
  • agent/go/internal/agent/steps_test.go
  • agent/go/internal/config/mock/SchemaValidator.go
  • agent/go/internal/config/validate_test.go
  • agent/go/internal/execution/execution.go
  • agent/go/internal/flags/flags.go
  • agent/go/internal/flags/flags_test.go
  • agent/go/internal/flags/layout.go
  • agent/go/internal/flags/layout_test.go
  • agent/go/internal/flags/logs.go
  • agent/go/internal/flags/logs_test.go
  • agent/go/internal/history/history.go
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/mock/Store.go
  • agent/go/internal/hostfs/copy.go
  • agent/go/internal/hostfs/copy_test.go
  • agent/go/internal/hostfs/hostfs.go
  • agent/go/internal/hostfs/hostfs_test.go
  • agent/go/internal/interrupts/mock/Interrupt.go
  • agent/go/internal/step/mock/Step.go
  • agent/go/internal/step/regular_step.go
  • agent/go/internal/step/regular_step_test.go
  • agent/go/internal/step/shared.go
  • agent/go/internal/step/shared_linux_test.go
  • agent/go/internal/step/shared_test.go
  • agent/go/internal/step/step.go
  • agent/go/internal/step/step_test.go
  • agent/go/internal/step/upgrade_step.go
  • agent/go/internal/step/upgrade_step_test.go

Comment thread agent/go/deps.mk
Comment on lines +58 to +60
.PHONY: addlicense
addlicense: $(LOCALBIN) ## Download addlicense locally if necessary.
test -s $(ADDLICENSE) || GOBIN=$(LOCALBIN) go install github.com/google/addlicense@$(ADDLICENSE_VERSION)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '23,24p;52,60p' agent/go/deps.mk

fd -HI -t f '^addlicense$' . -E .git | while IFS= read -r binary; do
  if test -x "$binary"; then
    printf 'executable: %s\n' "$binary"
  else
    printf 'not executable: %s\n' "$binary"
  fi
done

Repository: NVIDIA/nodewright

Length of output: 648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- deps.mk context ---'
cat -n agent/go/deps.mk | sed -n '1,90p'

printf '%s\n' '--- addlicense references ---'
rg -n --glob '!**/.git/**' '\b(addlicense|ADDLICENSE|ADDLICENSE_VERSION)\b' agent/go Makefile .github 2>/dev/null || true

printf '%s\n' '--- local tool definitions ---'
rg -n --glob '!**/.git/**' 'LOCALBIN|MOCKERY_VERSION|ADDLICENSE_VERSION' . | head -120

Repository: NVIDIA/nodewright

Length of output: 11534


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re
import stat

deps = Path("agent/go/deps.mk").read_text()
makefile = Path("agent/go/Makefile").read_text()

target = re.search(
    r"^addlicense:.*\n(?P<recipe>\s+test -s \$\(ADDLICENSE\).*?)$",
    deps,
    re.MULTILINE,
).group("recipe")

assert "test -s $(ADDLICENSE)" in target
assert "ADDLICENSE_VERSION" in target
assert "test -x" not in target
assert "version" not in target

license_use = re.search(r"^\s*\$\(license_files\).*?\$\(ADDLICENSE\)", makefile, re.MULTILINE)
assert license_use

candidate = Path("agent/go/Makefile")
print(f"nonempty_candidate={candidate.stat().st_size > 0}")
print(f"executable_candidate=bool(mode & stat.S_IXUSR)={bool(candidate.stat().st_mode & stat.S_IXUSR)}")
print("addlicense_target_accepts_nonempty_nonexecutable=true")
print("addlicense_target_checks_declared_version=false")
print("license_targets_execute_ADDLICENSE=true")
PY

Repository: NVIDIA/nodewright

Length of output: 374


Validate the installed addlicense binary.

test -s $(ADDLICENSE) accepts any nonempty path, including non-executable files. The license targets then fail when they execute it. The check also accepts stale versions and bypasses ADDLICENSE_VERSION. Require an executable binary that matches the declared version, or reinstall it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/deps.mk` around lines 58 - 60, Update the addlicense target to
validate that ADDLICENSE is an executable installed binary and that its reported
version matches ADDLICENSE_VERSION; reinstall it via the existing go install
command whenever validation fails, including for stale or incorrect versions.

Comment thread agent/go/internal/agent/agent_test.go
Comment thread agent/go/internal/agent/package_test.go
rootOverlayDirName = "root_dir"
)

func ensurePackageData(rootMount, copyRoot, dataDir string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Align the path parameter order across the package preparation functions.

ensurePackageData takes (rootMount, copyRoot, dataDir). prepareHost takes (copyRoot, rootMount, cfg). Both leading parameters are string, so the compiler cannot detect a swapped call. A swapped call to prepareHost would copy the host root overlay into the package directory instead of onto the host. Use the same order in both functions.

♻️ Proposed change
-func prepareHost(copyRoot, rootMount string, cfg config.Config) error {
+func prepareHost(rootMount, copyRoot string, cfg config.Config) error {

Update the caller in agent/go/internal/agent/agent.go and the spec in agent/go/internal/agent/package_test.go at Line 67 and Line 72 to match.

Also applies to: 79-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/package.go` at line 41, Align the path argument order
between ensurePackageData and prepareHost by using the same leading-parameter
order in both functions, then update the prepareHost call in agent.go and its
specifications in package_test.go to match. Keep the resulting order consistent
so copyRoot and rootMount cannot be accidentally swapped.

Comment on lines +252 to +278
It("composes command output with a retained log file", func() {
runtime.writeLogs = true
output := "step output\n"
value := newMockStep("apply.sh")
value.EXPECT().
Run(mock.Anything, mock.Anything).
Run(func(_ context.Context, cfg execution.Config) {
_, err := io.WriteString(cfg.Stdout(), output)
Expect(err).NotTo(HaveOccurred())
}).
Return(execution.StatusSuccess, nil).
Once()
cfg.Modes[stage.Apply] = []step.Step{value}

status, err := runSteps(
context.Background(), req, runtime, layout, cfg, flagStore, historyStore,
)

Expect(err).NotTo(HaveOccurred())
Expect(status).To(Equal(execution.StatusSuccess))
entries, err := os.ReadDir(filepath.Join(layout.LogDir(), cfg.PackageName, cfg.PackageVersion))
Expect(err).NotTo(HaveOccurred())
Expect(entries).To(HaveLen(1))
data, err := os.ReadFile(filepath.Join(layout.LogDir(), cfg.PackageName, cfg.PackageVersion, entries[0].Name()))
Expect(err).NotTo(HaveOccurred())
Expect(string(data)).To(Equal(output))
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert that step output reaches runtime.stdout.

This spec sets runtime.stdout to io.Discard, so it verifies only the retained log file. The contract requires both behaviors: output streams to stdout and stderr, and, when retention is enabled, output is also written under the log directory. Replace io.Discard with a buffer and assert the same content in both destinations.

As per coding guidelines, "step and interrupt output must stream to stdout/stderr and, when enabled, also be written under SKYHOOK_LOG_DIR."

♻️ Proposed change
 	It("composes command output with a retained log file", func() {
 		runtime.writeLogs = true
+		streamed := &bytes.Buffer{}
+		runtime.stdout = streamed
 		output := "step output\n"
 		Expect(err).NotTo(HaveOccurred())
 		Expect(string(data)).To(Equal(output))
+		Expect(streamed.String()).To(Equal(output))
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It("composes command output with a retained log file", func() {
runtime.writeLogs = true
output := "step output\n"
value := newMockStep("apply.sh")
value.EXPECT().
Run(mock.Anything, mock.Anything).
Run(func(_ context.Context, cfg execution.Config) {
_, err := io.WriteString(cfg.Stdout(), output)
Expect(err).NotTo(HaveOccurred())
}).
Return(execution.StatusSuccess, nil).
Once()
cfg.Modes[stage.Apply] = []step.Step{value}
status, err := runSteps(
context.Background(), req, runtime, layout, cfg, flagStore, historyStore,
)
Expect(err).NotTo(HaveOccurred())
Expect(status).To(Equal(execution.StatusSuccess))
entries, err := os.ReadDir(filepath.Join(layout.LogDir(), cfg.PackageName, cfg.PackageVersion))
Expect(err).NotTo(HaveOccurred())
Expect(entries).To(HaveLen(1))
data, err := os.ReadFile(filepath.Join(layout.LogDir(), cfg.PackageName, cfg.PackageVersion, entries[0].Name()))
Expect(err).NotTo(HaveOccurred())
Expect(string(data)).To(Equal(output))
})
It("composes command output with a retained log file", func() {
runtime.writeLogs = true
streamed := &bytes.Buffer{}
runtime.stdout = streamed
output := "step output\n"
value := newMockStep("apply.sh")
value.EXPECT().
Run(mock.Anything, mock.Anything).
Run(func(_ context.Context, cfg execution.Config) {
_, err := io.WriteString(cfg.Stdout(), output)
Expect(err).NotTo(HaveOccurred())
}).
Return(execution.StatusSuccess, nil).
Once()
cfg.Modes[stage.Apply] = []step.Step{value}
status, err := runSteps(
context.Background(), req, runtime, layout, cfg, flagStore, historyStore,
)
Expect(err).NotTo(HaveOccurred())
Expect(status).To(Equal(execution.StatusSuccess))
entries, err := os.ReadDir(filepath.Join(layout.LogDir(), cfg.PackageName, cfg.PackageVersion))
Expect(err).NotTo(HaveOccurred())
Expect(entries).To(HaveLen(1))
data, err := os.ReadFile(filepath.Join(layout.LogDir(), cfg.PackageName, cfg.PackageVersion, entries[0].Name()))
Expect(err).NotTo(HaveOccurred())
Expect(string(data)).To(Equal(output))
Expect(streamed.String()).To(Equal(output))
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/steps_test.go` around lines 252 - 278, Update the
test around runSteps to replace the discarded runtime.stdout writer with a
buffer, then assert that the captured stdout contains the same output written to
the retained log file. Preserve the existing success and log-directory
assertions, ensuring the test verifies both destinations when runtime.writeLogs
is enabled.

Source: Coding guidelines

Comment on lines +173 to +178
validator := configmock.NewMockSchemaValidator(GinkgoT())
validator.EXPECT().
Validate([]byte(validConfigJSON), schema.V1).
Return(sentinel).
Once()
loader := &Loader{validator: validator}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use malformed input to verify validation ordering.

At Line 175, validConfigJSON parses successfully. If Loader.Load decodes before calling Validate, the mock still returns sentinel, so this test passes. Use malformed JSON in both calls to prove that validation occurs before decoding.

Proposed test update
 It("surfaces the injected validator's error without parsing the document", func() {
+    input := []byte("{")
     sentinel := errors.New("boom from fake validator")
     validator := configmock.NewMockSchemaValidator(GinkgoT())
     validator.EXPECT().
-        Validate([]byte(validConfigJSON), schema.V1).
+        Validate(input, schema.V1).
         Return(sentinel).
         Once()
     loader := &Loader{validator: validator}

-    _, err := loader.Load([]byte(validConfigJSON), GinkgoT().TempDir(), nil)
+    _, err := loader.Load(input, GinkgoT().TempDir(), nil)
     Expect(err).To(MatchError(sentinel))
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
validator := configmock.NewMockSchemaValidator(GinkgoT())
validator.EXPECT().
Validate([]byte(validConfigJSON), schema.V1).
Return(sentinel).
Once()
loader := &Loader{validator: validator}
It("surfaces the injected validator's error without parsing the document", func() {
input := []byte("{")
sentinel := errors.New("boom from fake validator")
validator := configmock.NewMockSchemaValidator(GinkgoT())
validator.EXPECT().
Validate(input, schema.V1).
Return(sentinel).
Once()
loader := &Loader{validator: validator}
_, err := loader.Load(input, GinkgoT().TempDir(), nil)
Expect(err).To(MatchError(sentinel))
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/config/validate_test.go` around lines 173 - 178, Update the
test setup around Loader.Load and the validator mock to use malformed JSON
instead of validConfigJSON for both the Validate expectation and the load input.
Keep the sentinel validation error and Once expectation, ensuring the test fails
if decoding occurs before SchemaValidator.Validate.

Comment thread agent/go/internal/flags/flags.go
Comment thread agent/go/internal/flags/layout.go Outdated
Comment thread agent/go/internal/history/history.go
Comment thread agent/go/Makefile
Comment on lines +54 to +56
@wrong=$$($(license_files) | while read -r f; do \
grep -qE '^.{1,2} Code generated .* DO NOT EDIT\.$$' "$$f" && continue; \
head -20 "$$f" | grep -q 'SPDX-License-Identifier: Apache-2.0' || echo " $$f"; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- agent/go/Makefile ---'
cat -n agent/go/Makefile | sed -n '1,90p'
printf '%s\n' '--- related license checks ---'
rg -n -C 4 'license-header-check|license_files|addlicense|Code generated|SPDX-License-Identifier' agent/go Makefile .github 2>/dev/null | sed -n '1,240p'
printf '%s\n' '--- source files considered by the Makefile ---'
rg -n '^[[:space:]]*(license_files|GO_FILES|.*FILES).*=' agent/go/Makefile

Repository: NVIDIA/nodewright

Length of output: 20676


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import subprocess, re

files = subprocess.check_output(
    ["git", "ls-files", "--", "*.go", "*.py", "*.sh", "*.yaml", "*.yml",
     "Dockerfile", "*.Dockerfile"], text=True
).splitlines()
generated = []
missing_apache = []
for name in files:
    text = Path(name).read_text(errors="replace")
    first20 = "\n".join(text.splitlines()[:20])
    is_generated = any(
        re.search(r"^.{1,2} Code generated .* DO NOT EDIT\.$", line)
        for line in text.splitlines()
    )
    has_apache = "SPDX-License-Identifier: Apache-2.0" in first20
    if is_generated:
        generated.append((name, has_apache))
    if not has_apache:
        missing_apache.append((name, is_generated))

print(f"tracked license candidates: {len(files)}")
print(f"generated candidates: {len(generated)}")
for name, has_apache in generated:
    print(f"generated {'Apache-2.0' if has_apache else 'NON-Apache/missing'}: {name}")
print(f"missing Apache in first 20 lines: {len(missing_apache)}")
for name, is_generated in missing_apache:
    print(f"missing {'generated' if is_generated else 'non-generated'}: {name}")
PY
printf '%s\n' '--- repository policy references ---'
rg -n -i -C 3 'generated.*(license|SPDX)|SPDX.*generated|license.*generated|Apache-2\.0.*header|license header' \
  README.md CONTRIBUTING.md LICENSE scripts agent .github 2>/dev/null | sed -n '1,240p'
printf '%s\n' '--- workflow invocation context ---'
rg -n -C 4 'license-header-check|make .*lint|working-directory:.*agent/go|cd agent/go' .github agent 2>/dev/null | sed -n '1,240p'

Repository: NVIDIA/nodewright

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- exact license_files output from repository root ---'
git ls-files -- '*.go' '*.py' '*.sh' '*.yaml' '*.yml' 'Dockerfile' '*.Dockerfile' |
  awk 'BEGIN {n=0} {n++; if ($0 ~ /^agent\/go\//) agent++; if ($0 ~ /(^|\/)vendor\//) vendor++} END {printf "total=%d agent/go=%d vendor=%d\n", n, agent, vendor}'
printf '%s\n' '--- exact license_files output from agent/go ---'
(
  cd agent/go
  git ls-files -- '*.go' '*.py' '*.sh' '*.yaml' '*.yml' 'Dockerfile' '*.Dockerfile' |
    awk 'BEGIN {n=0} {n++; if ($0 ~ /^agent\/go\//) agent++; if ($0 ~ /(^|\/)vendor\//) vendor++} END {printf "total=%d agent/go=%d vendor=%d\n", n, agent, vendor}'
)
printf '%s\n' '--- generated files in agent/go candidates ---'
python3 - <<'PY'
from pathlib import Path
import subprocess, re
for cwd in (Path('.'), Path('agent/go')):
    files = subprocess.check_output(
        ["git", "-C", str(cwd), "ls-files", "--", "*.go", "*.py", "*.sh",
         "*.yaml", "*.yml", "Dockerfile", "*.Dockerfile"], text=True
    ).splitlines()
    generated = []
    missing = []
    for name in files:
        path = cwd / name
        if not path.is_file():
            continue
        lines = path.read_text(errors="replace").splitlines()
        is_generated = any(re.search(r"^.{1,2} Code generated .* DO NOT EDIT\.$", line)
                           for line in lines)
        has_apache = "SPDX-License-Identifier: Apache-2.0" in "\n".join(lines[:20])
        if is_generated:
            generated.append((name, has_apache))
            if not has_apache:
                missing.append(name)
    print(f"cwd={cwd}: candidates={len(files)}, generated={len(generated)}, generated_missing_apache={len(missing)}")
    print("  examples:", ", ".join(f"{n} ({'Apache' if a else 'missing'})" for n,a in generated[:8]))
    print("  missing examples:", ", ".join(missing[:8]))
PY
printf '%s\n' '--- workflow context only ---'
rg -n -C 5 'license-header-check|make .*lint|working-directory:.*agent/go|cd agent/go' .github agent 2>/dev/null |
  grep -v -E 'vendor|generated' | sed -n '1,180p'

Repository: NVIDIA/nodewright

Length of output: 5054


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- dependency version and license template ---'
cat -n agent/go/deps.mk | sed -n '1,42p'
cat -n scripts/license-header.tmpl | sed -n '1,80p'
printf '%s\n' '--- all generated agent/go files and first header lines ---'
python3 - <<'PY'
from pathlib import Path
import subprocess, re
files = subprocess.check_output(
    ["git", "ls-files", "--", "*.go", "*.py", "*.sh", "*.yaml", "*.yml",
     "Dockerfile", "*.Dockerfile"], cwd="agent/go", text=True
).splitlines()
for name in files:
    path = Path("agent/go") / name
    lines = path.read_text(errors="replace").splitlines()
    if any(re.search(r"^.{1,2} Code generated .* DO NOT EDIT\.$", line)
           for line in lines):
        print(f"{name}:")
        for i, line in enumerate(lines[:5], 1):
            print(f"  {i}: {line}")
PY
printf '%s\n' '--- license policy references excluding vendored files ---'
rg -n -i -C 3 'generated.*(license|SPDX)|SPDX.*generated|license.*generated|Apache-2\.0.*header|license header' \
  README.md CONTRIBUTING.md LICENSE scripts agent/go .github 2>/dev/null |
  grep -v '/vendor/' | sed -n '1,220p'

Repository: NVIDIA/nodewright

Length of output: 8406


🌐 Web query:

Google addlicense -check accepts any license marker SPDX behavior

💡 Result:

The Google addlicense tool does not automatically accept any arbitrary SPDX license identifier provided by a user [1][2]. The tool manages license templates through a internal mapping system [1][3]. Its behavior regarding SPDX identifiers is as follows: 1. Predefined Licenses: It has built-in support for specific licenses (Apache-2.0, MIT, BSD, MPL-2.0) [1][3]. When the -s flag is used, it appends an SPDX identifier to these recognized licenses, or uses a dedicated SPDX-only template if -s=only is specified [1][2]. 2. Unknown Licenses: If a user specifies a license type that is not in the internal licenseTemplate map, the tool will return an error (e.g., unknown license: "...") unless the -s (or -s=only) flag is used [1][2]. 3. SPDX-Only Mode: If the -s=only flag is enabled, the tool will use a generic SPDX template (tmplSPDX) even if the provided license type is unknown [1][2]. In this mode, the tool essentially treats the provided license argument as a string to be used for the SPDXID field [1][3]. Essentially, the tool does not validate that a provided string is a valid SPDX identifier against an external database; it simply trusts the string provided to the license argument or, when using -s=only with an unknown license, uses that argument as the identifier in the generated header [1][3]. Top results: [1][3][2][4]

Citations:


🌐 Web query:

github.com/google/addlicense v1.2.0 check flag source license marker check

💡 Result:

In the google/addlicense tool (version 1.2.0), the -check flag enables a check-only mode where the tool verifies the presence of license headers in source files [1][2]. When this flag is used, the tool does not modify any files; instead, it exits with a non-zero status code if it detects any files that are missing a valid license header [1][3]. The tool identifies whether a file already contains a license header through several methods: 1. Keyword Detection: It scans for terms such as "copyright," "mozilla public," or "spdx-license-identifier" [3]. 2. Header Analysis: It checks for existing license text and specifically validates the copyright holder if the -c flag is provided [4]. A recent update ensures that when -c is specified, the tool verifies that the copyright holder in the file matches the expected value, rather than merely checking for the presence of any license header [4]. 3. Special Handling: The tool is designed to preserve specific file structures, such as shebang lines, and it automatically ignores empty files during its checks [4][3]. In version 1.2.0, the tool continues to support recursive scanning of directories and can be configured with various flags—such as -ignore for file exclusion, -s for SPDX identifiers, and -l for license types—to refine how these headers are detected and verified [1][5][3].

Citations:


Keep generated source in Apache SPDX validation.

addlicense -check detects a generic license header, not specifically SPDX-License-Identifier: Apache-2.0. Remove the generated-source exemption so license-header-check enforces its stated Apache-2.0 requirement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/Makefile` around lines 54 - 56, Update the license validation loop
in license-header-check to remove the generated-source exemption based on the
“Code generated ... DO NOT EDIT.” marker. Ensure every file from license_files
is checked for the SPDX-License-Identifier: Apache-2.0 header.

Source: Coding guidelines

@rice-riley rice-riley changed the title feat(agent): added the final agent orchestration package for agent go… feat(agent): complete Go agent orchestration and entrypoint Aug 4, 2026
@rice-riley
rice-riley changed the base branch from main to agent-mocks-219 August 4, 2026 19:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
agent/go/internal/agent/agent_test.go (1)

488-501: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace the busy loop with sleep 3600.

The apply fixture script spins with while :; do :; done. The spec keeps this process alive for up to five seconds, so it consumes a full CPU core during the run. sleep 3600 produces the same blocking behavior without the CPU cost, and the marker file assertions stay valid.

♻️ Proposed change
-		[]byte("#!/bin/sh\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-started\"\nwhile :; do :; done\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-finished\"\n"),
+		[]byte("#!/bin/sh\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-started\"\nsleep 3600\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-finished\"\n"),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/agent_test.go` around lines 488 - 501, Update the
apply script created by writeCancellationPackageFixture to replace the
CPU-intensive infinite while loop with sleep 3600, preserving the existing
marker creation and blocking behavior.
agent/go/internal/agent/package.go (1)

41-41: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Align the path parameter order between ensurePackageData and prepareHost.

ensurePackageData takes (rootMount, copyRoot, dataDir). prepareHost takes (copyRoot, rootMount, cfg). Both leading parameters are string, so a swapped call compiles. Use the same order in both functions and update the caller in agent/go/internal/agent/agent.go at Line 366 and the specs in agent/go/internal/agent/package_test.go at Lines 67 and 72.

Also applies to: 79-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/package.go` at line 41, Align the parameter order of
ensurePackageData with prepareHost by using copyRoot before rootMount, then
update the call in prepareHost’s caller and the affected package tests to pass
arguments in that order. Preserve the existing behavior and parameter names
while ensuring all call sites match the unified order.
agent/go/internal/agent/steps_test.go (1)

252-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert that step output reaches runtime.stdout.

The spec sets runtime.stdout to io.Discard, so it verifies only the retained log file. The contract requires both destinations: output streams to stdout and stderr, and, when retention is enabled, output is also written under the log directory. Use a buffer and assert the same content in both places.

💚 Proposed change
 	It("composes command output with a retained log file", func() {
 		runtime.writeLogs = true
+		streamed := &bytes.Buffer{}
+		runtime.stdout = streamed
 		output := "step output\n"
 		Expect(err).NotTo(HaveOccurred())
 		Expect(string(data)).To(Equal(output))
+		Expect(streamed.String()).To(Equal(output))
 	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/steps_test.go` around lines 252 - 278, Update the
“composes command output with a retained log file” test to replace the discarded
runtime stdout with a buffer, then assert that the buffer contains the same step
output as the retained log file. Preserve the existing log-directory assertions
and use the test’s runtime stdout configuration to verify both destinations.
agent/go/internal/agent/package_test.go (1)

51-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the remaining prepareHost branches and for ensurePackageData.

This spec covers the overlay copy, one present expected-config file, and one missing expected-config file. Three branches of agent/go/internal/agent/package.go stay untested:

  • Line 88: prepareHost rejects a non-local ExpectedConfigFiles entry, such as ../escape.conf. This is a containment check on package-supplied input.
  • Line 99: prepareHost rejects an expected config file that is not a regular file.
  • Lines 41-64: ensurePackageData is not exercised. The relevant cases are a non-directory copyRoot, a missing dataDir, and the legacy node-files copy.

Based on learnings, code changes should include unit tests, followed by running tests and formatting before submitting a merge request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/package_test.go` around lines 51 - 78, Extend the
package test coverage around prepareHost and ensurePackageData. Add cases
verifying prepareHost rejects path-traversing ExpectedConfigFiles entries such
as ../escape.conf and rejects expected config paths that are not regular files;
add ensurePackageData cases for a non-directory copyRoot, a missing dataDir, and
copying legacy node files. Use the existing test symbols and assert each
expected error or resulting filesystem state, then run formatting and tests.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/go/internal/history/history.go`:
- Around line 35-39: Update the Go agent’s runSteps flow to set
OVERLAY_FRAMEWORK_VERSION to the required {package name}-{version} value before
executing steps, using the existing previous/current version data passed by
runSteps. Ensure the environment variable is available to history management
without changing the existing version arguments or history constants.

In `@agent/go/internal/step/upgrade_step.go`:
- Around line 104-107: Update UpgradeStep.Fingerprint to include a step-type
discriminator in the value passed to stepFingerprint, matching the corresponding
RegularStep fingerprint behavior while preserving all existing execution inputs.
Add a test proving identical inputs for RegularStep and UpgradeStep produce
different fingerprints and cannot share the same idempotency marker.

---

Duplicate comments:
In `@agent/go/internal/agent/agent_test.go`:
- Around line 488-501: Update the apply script created by
writeCancellationPackageFixture to replace the CPU-intensive infinite while loop
with sleep 3600, preserving the existing marker creation and blocking behavior.

In `@agent/go/internal/agent/package_test.go`:
- Around line 51-78: Extend the package test coverage around prepareHost and
ensurePackageData. Add cases verifying prepareHost rejects path-traversing
ExpectedConfigFiles entries such as ../escape.conf and rejects expected config
paths that are not regular files; add ensurePackageData cases for a
non-directory copyRoot, a missing dataDir, and copying legacy node files. Use
the existing test symbols and assert each expected error or resulting filesystem
state, then run formatting and tests.

In `@agent/go/internal/agent/package.go`:
- Line 41: Align the parameter order of ensurePackageData with prepareHost by
using copyRoot before rootMount, then update the call in prepareHost’s caller
and the affected package tests to pass arguments in that order. Preserve the
existing behavior and parameter names while ensuring all call sites match the
unified order.

In `@agent/go/internal/agent/steps_test.go`:
- Around line 252-278: Update the “composes command output with a retained log
file” test to replace the discarded runtime stdout with a buffer, then assert
that the buffer contains the same step output as the retained log file. Preserve
the existing log-directory assertions and use the test’s runtime stdout
configuration to verify both destinations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 0a2a2490-6208-4590-82c3-9fae9ec5eb1f

📥 Commits

Reviewing files that changed from the base of the PR and between 82de1d8 and 9a4aa8c.

⛔ Files ignored due to path filters (1)
  • agent/go/go.sum is excluded by !**/*.sum
📒 Files selected for processing (41)
  • agent/README.md
  • agent/go/.mockery.yaml
  • agent/go/Makefile
  • agent/go/cmd/agent/main.go
  • agent/go/deps.mk
  • agent/go/go.mod
  • agent/go/internal/agent/agent.go
  • agent/go/internal/agent/agent_test.go
  • agent/go/internal/agent/interrupt.go
  • agent/go/internal/agent/interrupt_test.go
  • agent/go/internal/agent/package.go
  • agent/go/internal/agent/package_test.go
  • agent/go/internal/agent/steps.go
  • agent/go/internal/agent/steps_test.go
  • agent/go/internal/config/mock/SchemaValidator.go
  • agent/go/internal/config/validate_test.go
  • agent/go/internal/execution/execution.go
  • agent/go/internal/flags/flags.go
  • agent/go/internal/flags/flags_test.go
  • agent/go/internal/flags/layout.go
  • agent/go/internal/flags/layout_test.go
  • agent/go/internal/flags/logs.go
  • agent/go/internal/flags/logs_test.go
  • agent/go/internal/history/history.go
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/mock/Store.go
  • agent/go/internal/hostfs/copy.go
  • agent/go/internal/hostfs/copy_test.go
  • agent/go/internal/hostfs/hostfs.go
  • agent/go/internal/hostfs/hostfs_test.go
  • agent/go/internal/interrupts/mock/Interrupt.go
  • agent/go/internal/step/mock/Step.go
  • agent/go/internal/step/regular_step.go
  • agent/go/internal/step/regular_step_test.go
  • agent/go/internal/step/shared.go
  • agent/go/internal/step/shared_linux_test.go
  • agent/go/internal/step/shared_test.go
  • agent/go/internal/step/step.go
  • agent/go/internal/step/step_test.go
  • agent/go/internal/step/upgrade_step.go
  • agent/go/internal/step/upgrade_step_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

♻️ Duplicate comments (4)
agent/go/internal/agent/agent_test.go (1)

488-501: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace the busy loop with sleep 3600.

The apply fixture script spins with while :; do :; done. The spec keeps this process alive for up to five seconds, so it consumes a full CPU core during the run. sleep 3600 produces the same blocking behavior without the CPU cost, and the marker file assertions stay valid.

♻️ Proposed change
-		[]byte("#!/bin/sh\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-started\"\nwhile :; do :; done\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-finished\"\n"),
+		[]byte("#!/bin/sh\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-started\"\nsleep 3600\n: > \"$NODEWRIGHT_AGENT_TEST_MARKER_DIR/first-finished\"\n"),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/agent_test.go` around lines 488 - 501, Update the
apply script created by writeCancellationPackageFixture to replace the
CPU-intensive infinite while loop with sleep 3600, preserving the existing
marker creation and blocking behavior.
agent/go/internal/agent/package.go (1)

41-41: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Align the path parameter order between ensurePackageData and prepareHost.

ensurePackageData takes (rootMount, copyRoot, dataDir). prepareHost takes (copyRoot, rootMount, cfg). Both leading parameters are string, so a swapped call compiles. Use the same order in both functions and update the caller in agent/go/internal/agent/agent.go at Line 366 and the specs in agent/go/internal/agent/package_test.go at Lines 67 and 72.

Also applies to: 79-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/package.go` at line 41, Align the parameter order of
ensurePackageData with prepareHost by using copyRoot before rootMount, then
update the call in prepareHost’s caller and the affected package tests to pass
arguments in that order. Preserve the existing behavior and parameter names
while ensuring all call sites match the unified order.
agent/go/internal/agent/steps_test.go (1)

252-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert that step output reaches runtime.stdout.

The spec sets runtime.stdout to io.Discard, so it verifies only the retained log file. The contract requires both destinations: output streams to stdout and stderr, and, when retention is enabled, output is also written under the log directory. Use a buffer and assert the same content in both places.

💚 Proposed change
 	It("composes command output with a retained log file", func() {
 		runtime.writeLogs = true
+		streamed := &bytes.Buffer{}
+		runtime.stdout = streamed
 		output := "step output\n"
 		Expect(err).NotTo(HaveOccurred())
 		Expect(string(data)).To(Equal(output))
+		Expect(streamed.String()).To(Equal(output))
 	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/steps_test.go` around lines 252 - 278, Update the
“composes command output with a retained log file” test to replace the discarded
runtime stdout with a buffer, then assert that the buffer contains the same step
output as the retained log file. Preserve the existing log-directory assertions
and use the test’s runtime stdout configuration to verify both destinations.
agent/go/internal/agent/package_test.go (1)

51-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the remaining prepareHost branches and for ensurePackageData.

This spec covers the overlay copy, one present expected-config file, and one missing expected-config file. Three branches of agent/go/internal/agent/package.go stay untested:

  • Line 88: prepareHost rejects a non-local ExpectedConfigFiles entry, such as ../escape.conf. This is a containment check on package-supplied input.
  • Line 99: prepareHost rejects an expected config file that is not a regular file.
  • Lines 41-64: ensurePackageData is not exercised. The relevant cases are a non-directory copyRoot, a missing dataDir, and the legacy node-files copy.

Based on learnings, code changes should include unit tests, followed by running tests and formatting before submitting a merge request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/package_test.go` around lines 51 - 78, Extend the
package test coverage around prepareHost and ensurePackageData. Add cases
verifying prepareHost rejects path-traversing ExpectedConfigFiles entries such
as ../escape.conf and rejects expected config paths that are not regular files;
add ensurePackageData cases for a non-directory copyRoot, a missing dataDir, and
copying legacy node files. Use the existing test symbols and assert each
expected error or resulting filesystem state, then run formatting and tests.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/go/internal/history/history.go`:
- Around line 35-39: Update the Go agent’s runSteps flow to set
OVERLAY_FRAMEWORK_VERSION to the required {package name}-{version} value before
executing steps, using the existing previous/current version data passed by
runSteps. Ensure the environment variable is available to history management
without changing the existing version arguments or history constants.

In `@agent/go/internal/step/upgrade_step.go`:
- Around line 104-107: Update UpgradeStep.Fingerprint to include a step-type
discriminator in the value passed to stepFingerprint, matching the corresponding
RegularStep fingerprint behavior while preserving all existing execution inputs.
Add a test proving identical inputs for RegularStep and UpgradeStep produce
different fingerprints and cannot share the same idempotency marker.

---

Duplicate comments:
In `@agent/go/internal/agent/agent_test.go`:
- Around line 488-501: Update the apply script created by
writeCancellationPackageFixture to replace the CPU-intensive infinite while loop
with sleep 3600, preserving the existing marker creation and blocking behavior.

In `@agent/go/internal/agent/package_test.go`:
- Around line 51-78: Extend the package test coverage around prepareHost and
ensurePackageData. Add cases verifying prepareHost rejects path-traversing
ExpectedConfigFiles entries such as ../escape.conf and rejects expected config
paths that are not regular files; add ensurePackageData cases for a
non-directory copyRoot, a missing dataDir, and copying legacy node files. Use
the existing test symbols and assert each expected error or resulting filesystem
state, then run formatting and tests.

In `@agent/go/internal/agent/package.go`:
- Line 41: Align the parameter order of ensurePackageData with prepareHost by
using copyRoot before rootMount, then update the call in prepareHost’s caller
and the affected package tests to pass arguments in that order. Preserve the
existing behavior and parameter names while ensuring all call sites match the
unified order.

In `@agent/go/internal/agent/steps_test.go`:
- Around line 252-278: Update the “composes command output with a retained log
file” test to replace the discarded runtime stdout with a buffer, then assert
that the buffer contains the same step output as the retained log file. Preserve
the existing log-directory assertions and use the test’s runtime stdout
configuration to verify both destinations.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 0a2a2490-6208-4590-82c3-9fae9ec5eb1f

📥 Commits

Reviewing files that changed from the base of the PR and between 82de1d8 and 9a4aa8c.

⛔ Files ignored due to path filters (1)
  • agent/go/go.sum is excluded by !**/*.sum
📒 Files selected for processing (41)
  • agent/README.md
  • agent/go/.mockery.yaml
  • agent/go/Makefile
  • agent/go/cmd/agent/main.go
  • agent/go/deps.mk
  • agent/go/go.mod
  • agent/go/internal/agent/agent.go
  • agent/go/internal/agent/agent_test.go
  • agent/go/internal/agent/interrupt.go
  • agent/go/internal/agent/interrupt_test.go
  • agent/go/internal/agent/package.go
  • agent/go/internal/agent/package_test.go
  • agent/go/internal/agent/steps.go
  • agent/go/internal/agent/steps_test.go
  • agent/go/internal/config/mock/SchemaValidator.go
  • agent/go/internal/config/validate_test.go
  • agent/go/internal/execution/execution.go
  • agent/go/internal/flags/flags.go
  • agent/go/internal/flags/flags_test.go
  • agent/go/internal/flags/layout.go
  • agent/go/internal/flags/layout_test.go
  • agent/go/internal/flags/logs.go
  • agent/go/internal/flags/logs_test.go
  • agent/go/internal/history/history.go
  • agent/go/internal/history/history_test.go
  • agent/go/internal/history/mock/Store.go
  • agent/go/internal/hostfs/copy.go
  • agent/go/internal/hostfs/copy_test.go
  • agent/go/internal/hostfs/hostfs.go
  • agent/go/internal/hostfs/hostfs_test.go
  • agent/go/internal/interrupts/mock/Interrupt.go
  • agent/go/internal/step/mock/Step.go
  • agent/go/internal/step/regular_step.go
  • agent/go/internal/step/regular_step_test.go
  • agent/go/internal/step/shared.go
  • agent/go/internal/step/shared_linux_test.go
  • agent/go/internal/step/shared_test.go
  • agent/go/internal/step/step.go
  • agent/go/internal/step/step_test.go
  • agent/go/internal/step/upgrade_step.go
  • agent/go/internal/step/upgrade_step_test.go
🛑 Comments failed to post (2)
agent/go/internal/history/history.go (1)

35-39: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find remaining producers of the version environment and upgrade arguments.
rg -n -C4 'OVERLAY_FRAMEWORK_VERSION|OVERLAY_PREVIOUS|UpgradeArguments|WithVersions' --type=go

Repository: NVIDIA/nodewright

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
fd -i 'history|version|upgrade|orches' agent/go
printf '%s\n' '--- history outline ---'
ast-grep outline agent/go/internal/history/history.go --match '$_' --view compact || true
printf '%s\n' '--- history source ---'
cat -n agent/go/internal/history/history.go
printf '%s\n' '--- version-related identifiers and literals ---'
rg -n -C3 'OVERLAY|UpgradeArguments|WithVersions|Versions|upgrade|history' agent/go --glob '*.go' || true
printf '%s\n' '--- repository diff summary ---'
git diff --stat

Repository: NVIDIA/nodewright

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- upgrade step implementation ---'
cat -n agent/go/internal/step/upgrade_step.go
printf '%s\n' '--- shared step execution ---'
cat -n agent/go/internal/step/shared.go
printf '%s\n' '--- orchestration version flow ---'
sed -n '250,430p' agent/go/internal/agent/agent.go
printf '%s\n' '--- all framework/version environment references ---'
rg -n -C3 'OVERLAY_FRAMEWORK_VERSION|PREVIOUS_VERSION|CURRENT_VERSION|WithVersions|Versions\{' . --glob '!agent/go/internal/step/*_test.go' --glob '!agent/go/internal/history/*_test.go'
printf '%s\n' '--- configuration fields ---'
rg -n -C3 'PackageName|PackageVersion' agent/go/internal/config agent/go/internal/agent --glob '*.go'

Repository: NVIDIA/nodewright

Length of output: 48588


Set OVERLAY_FRAMEWORK_VERSION in the Go agent.

runSteps already passes previous and current versions. No Go code sets OVERLAY_FRAMEWORK_VERSION to {package name}-{version}, which history management requires.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/history/history.go` around lines 35 - 39, Update the Go
agent’s runSteps flow to set OVERLAY_FRAMEWORK_VERSION to the required {package
name}-{version} value before executing steps, using the existing
previous/current version data passed by runSteps. Ensure the environment
variable is available to history management without changing the existing
version arguments or history constants.

Source: Learnings

agent/go/internal/step/upgrade_step.go (1)

104-107: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Determine whether step completion flags are keyed by Fingerprint.
set -euo pipefail

rg -n --type=go -C 10 'func .*fileStore\) Path\(' agent/go/internal/flags
rg -n --type=go -C 4 'Fingerprint\(\)' agent/go

Repository: NVIDIA/nodewright

Length of output: 6378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in agent/go/internal/step/step.go \
         agent/go/internal/step/regular_step.go \
         agent/go/internal/step/upgrade_step.go \
         agent/go/internal/step/fingerprint.go \
         agent/go/internal/flags/flags.go; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    ast-grep outline "$f" || true
  fi
done

echo "===== step fingerprint definitions/usages ====="
rg -n --type=go -C 8 'func stepFingerprint|stepFingerprint\(' agent/go/internal/step agent/go/internal/flags agent/go/internal/agent

echo "===== flag path and completion call sites ====="
sed -n '105,145p' agent/go/internal/flags/flags.go
rg -n --type=go -C 8 '\.Path\(.*step|flags\.(Get|Set|Has|Path)|Store\.(Get|Set|Path)|Flag' agent/go/internal/agent agent/go/internal/flags

Repository: NVIDIA/nodewright

Length of output: 48409


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== fingerprint helper ====="
sed -n '204,270p' agent/go/internal/step/shared.go

echo "===== step execution and flag checks ====="
sed -n '165,245p' agent/go/internal/agent/steps.go

echo "===== regular and upgrade constructors/defaults ====="
sed -n '29,115p' agent/go/internal/step/regular_step.go
sed -n '31,140p' agent/go/internal/step/upgrade_step.go

echo "===== stage assignment and upgrade-step decoding/configuration ====="
rg -n --type=go -C 5 'UpgradeStep|NewUpgradeStep|NewRegularStep|Modes\[|stage\.Upgrade' agent/go/internal/config agent/go/internal/step agent/go/internal/agent

Repository: NVIDIA/nodewright

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib
import json
import os

# This models the fields passed by both Fingerprint methods and the filename
# construction in fileStore.Path. It does not import or execute repository code.
path = "upgrade.sh"
arguments = []
returncodes = [0]
environment = {}
on_host = True

payload = {
    "path": path,
    "arguments": arguments,
    "returncodes": returncodes,
    "environment": environment,
    "on_host": on_host,
}
digest = hashlib.sha256(
    json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()

regular = digest
upgrade = digest
regular_filename = os.path.basename(path) + "-" + regular + ".flag"
upgrade_filename = os.path.basename(path) + "-" + upgrade + ".flag"

print("regular fingerprint:", regular)
print("upgrade fingerprint:", upgrade)
print("fingerprints equal:", regular == upgrade)
print("regular flag filename:", regular_filename)
print("upgrade flag filename:", upgrade_filename)
print("flag filenames equal:", regular_filename == upgrade_filename)
PY

Repository: NVIDIA/nodewright

Length of output: 587


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import hashlib
import json
import os

path = "upgrade.sh"
arguments = []
returncodes = [0]
environment = {}
on_host = True

payload = {
    "path": path,
    "arguments": arguments,
    "returncodes": returncodes,
    "environment": environment,
    "on_host": on_host,
}
digest = hashlib.sha256(
    json.dumps(payload, sort_keys=True, separators=(",", ":")).hexdigest()
).hexdigest()

regular_filename = os.path.basename(path) + "-" + digest + ".flag"
upgrade_filename = os.path.basename(path) + "-" + digest + ".flag"

print("fingerprints equal:", digest == digest)
print("flag filenames equal:", regular_filename == upgrade_filename)
PY

Repository: NVIDIA/nodewright

Length of output: 287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== runSteps loop and flag decision ====="
sed -n '44,165p' agent/go/internal/agent/steps.go

echo "===== flag Check implementation and decision ====="
sed -n '168,214p' agent/go/internal/flags/flags.go

echo "===== package/version inputs to flag store ====="
rg -n --type=go -C 6 'PackageVersion|NewStore\(|WithVersions|stage\.Upgrade|StageAlwaysRuns' agent/go/internal/agent agent/go/internal/flags agent/go/internal/config agent/go/internal/runtime

echo "===== uniqueness and placement validation ====="
rg -n --type=go -C 8 'duplicate|unique|same path|requireUpgradeStepPlacement|expectedCheckName|UpgradeStep' agent/go/internal/config/validate.go

Repository: NVIDIA/nodewright

Length of output: 39423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Read-only verifier of the relevant policy encoded in the inspected source.
allowed_upgrade_stages = {"upgrade", "upgrade-check"}
always_run_stages = {"config", "uninstall", "upgrade"}

for stage in ["apply", "upgrade", "upgrade-check"]:
    upgrade_allowed = stage in allowed_upgrade_stages
    stage_forces_run = stage in always_run_stages
    print(stage, "upgrade_allowed=", upgrade_allowed, "flag_can_skip=", not stage_forces_run)
PY

Repository: NVIDIA/nodewright

Length of output: 311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
allowed_upgrade_stages = {"upgrade", "upgrade-check"}
always_run_stages = {"config", "uninstall", "upgrade"}

for stage in ["apply", "upgrade", "upgrade-check"]:
    upgrade_allowed = stage in allowed_upgrade_stages
    stage_forces_run = stage in always_run_stages
    print(stage, "upgrade_allowed=", upgrade_allowed, "flag_can_skip=", not stage_forces_run)
PY

Repository: NVIDIA/nodewright

Length of output: 311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== stage definitions and execution dispatch ====="
rg -n --type=go -C 8 'var All|const \(|ApplyCheck|UpgradeCheck|runSteps\(|stage\.Apply|stage\.Upgrade' agent/go/internal/stage agent/go/internal/agent

echo "===== validation rules for duplicate paths or cross-stage identity ====="
rg -n --type=go -C 6 'Path\(\)|path.*duplicate|duplicate.*path|same.*path|map\[.*Path|requireApplyChecks|expectedCheckName' agent/go/internal/config

echo "===== flag namespace construction ====="
sed -n '80,125p' agent/go/internal/flags/flags.go

Repository: NVIDIA/nodewright

Length of output: 39850


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Model the relevant policy from the inspected source.
def decide(flag_exists, always_run, stage, idempotence="auto"):
    if not flag_exists:
        return True, "flag-missing"
    if always_run:
        return True, "always-run"
    if stage in {"config", "uninstall", "upgrade"}:
        return True, "stage-always-runs"
    if idempotence == "disabled":
        return True, "idempotence-disabled"
    return False, "already-completed"

print("upgrade stage with shared marker:", decide(True, False, "upgrade"))
print("apply stage with shared marker:", decide(True, False, "apply"))
print("upgrade-check stage execution:", "flag check is bypassed because it is a check stage")
PY

Repository: NVIDIA/nodewright

Length of output: 360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
def decide(flag_exists, always_run, stage, idempotence="auto"):
    if not flag_exists:
        return True, "flag-missing"
    if always_run:
        return True, "always-run"
    if stage in {"config", "uninstall", "upgrade"}:
        return True, "stage-always-runs"
    if idempotence == "disabled":
        return True, "idempotence-disabled"
    return False, "already-completed"

print("upgrade stage with shared marker:", decide(True, False, "upgrade"))
print("apply stage with shared marker:", decide(True, False, "apply"))
print("upgrade-check stage execution:", "flag check is bypassed because it is a check stage")
PY

Repository: NVIDIA/nodewright

Length of output: 360


Include the step type in the fingerprint.

RegularStep and UpgradeStep produce the same fingerprint for identical inputs, and fileStore.Path uses that fingerprint without the step type or stage. A shared marker can cause a later idempotent RegularStep in stage.Apply to be skipped after an UpgradeStep writes it. Add the discriminator and a cross-type collision test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/step/upgrade_step.go` around lines 104 - 107, Update
UpgradeStep.Fingerprint to include a step-type discriminator in the value passed
to stepFingerprint, matching the corresponding RegularStep fingerprint behavior
while preserving all existing execution inputs. Add a test proving identical
inputs for RegularStep and UpgradeStep produce different fingerprints and cannot
share the same idempotency marker.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
agent/go/internal/agent/package_test.go (1)

51-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a spec for the non-local ExpectedConfigFiles branch.

This spec covers the overlay copy, one present expected-config file, and one missing expected-config file. The containment check at agent/go/internal/agent/package.go Line 88 stays uncovered. That check validates package-supplied input, so a regression there would allow reads outside the configmaps directory.

💚 Proposed additional spec
It("rejects an expected config file outside the configmaps directory", func() {
	root := GinkgoT().TempDir()
	copyRoot := filepath.Join(root, "package")
	Expect(os.MkdirAll(filepath.Join(copyRoot, configMapsDirName), 0o755)).To(Succeed())

	err := prepareHost(copyRoot, root, config.Config{
		ExpectedConfigFiles: []string{filepath.Join("..", "escape.conf")},
	})

	Expect(err).To(MatchError(ContainSubstring("must be relative to the configmaps directory")))
})

As per coding guidelines, "Code changes should include unit tests".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/package_test.go` around lines 51 - 78, Add a unit
test alongside the existing prepareHost specs covering a non-local
ExpectedConfigFiles entry such as a parent-directory traversal path. Create the
configmaps directory, call prepareHost with that entry, and assert the error
contains “must be relative to the configmaps directory,” exercising the
containment validation in prepareHost.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/go/internal/agent/interrupt.go`:
- Around line 166-199: Move completion-marker creation for non-NodeRestartType
interrupts in the execution flow around value.Run so it occurs immediately after
successful execution, before closeLogErr or CleanupOldLogs failures are
returned; preserve failure reporting and avoid marking unsuccessful runs
complete. Add a regression test that forces log finalization to fail, then
verifies the subsequent invocation does not call Run, ensuring one marker is
written per interrupt type and resource ID.

In `@agent/go/internal/agent/steps.go`:
- Around line 203-227: Update the step execution flow around value.Run and log
closing so log retention always runs after runtime or close errors, rather than
returning immediately. Close the log, resolve the log pattern, and call
flags.CleanupOldLogs before returning, joining any retention failure with the
existing runErr and closeErr; preserve the current error context and failed
status. Add a regression test covering repeated runErr results and verifying old
logs are cleaned up.
- Around line 252-270: The check-stage flow must clear any existing
*_ALL_CHECKED marker before running current checks, so stale success state
cannot survive a failure. Update the relevant check execution function around
the checkResultsFlagName and completed-check flag writes to remove the marker
before checks begin, retain recreation only after all checks pass, and add a
regression test covering a successful run followed by a failed run.

---

Duplicate comments:
In `@agent/go/internal/agent/package_test.go`:
- Around line 51-78: Add a unit test alongside the existing prepareHost specs
covering a non-local ExpectedConfigFiles entry such as a parent-directory
traversal path. Create the configmaps directory, call prepareHost with that
entry, and assert the error contains “must be relative to the configmaps
directory,” exercising the containment validation in prepareHost.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 5f306cba-b719-463c-aad6-dace908798bc

📥 Commits

Reviewing files that changed from the base of the PR and between 9a4aa8c and 9d34850.

📒 Files selected for processing (10)
  • agent/README.md
  • agent/go/cmd/agent/main.go
  • agent/go/internal/agent/agent.go
  • agent/go/internal/agent/agent_test.go
  • agent/go/internal/agent/interrupt.go
  • agent/go/internal/agent/interrupt_test.go
  • agent/go/internal/agent/package.go
  • agent/go/internal/agent/package_test.go
  • agent/go/internal/agent/steps.go
  • agent/go/internal/agent/steps_test.go

Comment on lines +166 to +199
status, runErr := value.Run(ctx, runConfig)
closeErr := closeLogErr()
var cleanupErr error
if runtime.writeLogs {
cleanupErr = flags.CleanupOldLogs(logFiles, flags.DefaultLogRetention)
}
if runErr != nil || closeErr != nil || cleanupErr != nil {
if runErr != nil {
runErr = fmt.Errorf("running interrupt %q: %w", interruptType, runErr)
}
if cleanupErr != nil {
cleanupErr = fmt.Errorf("cleaning old interrupt logs: %w", cleanupErr)
}
return execution.StatusFailed, errors.Join(
runErr,
closeErr,
cleanupErr,
)
}
if status != execution.StatusSuccess {
return execution.StatusFailed, nil
}

if interruptType != interrupts.NodeRestartType {
if err := hostfs.CreateFile(
req.rootMount,
completeMarker,
[]byte(time.Now().UTC().Format(time.RFC3339Nano)),
markerFileMode,
); err != nil {
return execution.StatusFailed, fmt.Errorf("marking interrupt complete: %w", err)
}
}
return execution.StatusSuccess, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Write the ordinary completion marker before post-run log failures return.

For a non-NodeRestartType interrupt, value.Run can return execution.StatusSuccess with no error, but closeLogErr or flags.CleanupOldLogs can return before completeMarker is written. The next invocation then executes the completed interrupt again.

Write the completion marker after successful interrupt execution and before reporting post-run log failures. Add a regression test that forces a log-finalization failure and verifies that the next invocation does not call Run.

As per coding guidelines, the orchestration layer must write one completion marker per interrupt type and resource ID after successful execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/interrupt.go` around lines 166 - 199, Move
completion-marker creation for non-NodeRestartType interrupts in the execution
flow around value.Run so it occurs immediately after successful execution,
before closeLogErr or CleanupOldLogs failures are returned; preserve failure
reporting and avoid marking unsuccessful runs complete. Add a regression test
that forces log finalization to fail, then verifies the subsequent invocation
does not call Run, ensuring one marker is written per interrupt type and
resource ID.

Source: Coding guidelines

Comment on lines +203 to +227
if runErr != nil || closeErr != nil {
if runErr != nil {
runErr = fmt.Errorf("running step %q: %w", value.Path(), runErr)
}
if closeErr != nil {
closeErr = fmt.Errorf("closing log for step %q: %w", value.Path(), closeErr)
}
return execution.StatusFailed, errors.Join(
runErr,
closeErr,
)
}
if runtime.writeLogs {
logFiles, err := layout.LogFilePattern(cfg, value.Path())
if err != nil {
return execution.StatusFailed, fmt.Errorf(
"resolving log retention for step %q: %w",
value.Path(),
err,
)
}
if err := flags.CleanupOldLogs(logFiles, flags.DefaultLogRetention); err != nil {
return execution.StatusFailed, fmt.Errorf("cleaning old logs for step %q: %w", value.Path(), err)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run log retention after runtime errors.

CreateLogFile creates a log before value.Run. If value.Run returns runErr, this branch returns before CleanupOldLogs runs. A repeated runtime error then retains one new host log per invocation without a retention pass.

Close the log, run retention, and join any retention error before returning the step error. Add a regression test for repeated runErr results.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/steps.go` around lines 203 - 227, Update the step
execution flow around value.Run and log closing so log retention always runs
after runtime or close errors, rather than returning immediately. Close the log,
resolve the log pattern, and call flags.CleanupOldLogs before returning, joining
any retention failure with the existing runErr and closeErr; preserve the
current error context and failed status. Add a regression test covering repeated
runErr results and verifying old logs are cleaned up.

Comment on lines +252 to +270
if err := flagStore.Write(
filepath.Join(layout.FlagDir(), checkResultsFlagName),
[]byte(strings.Join(lines, "\n")),
); err != nil {
return execution.StatusFailed, fmt.Errorf("writing check results for stage %q: %w", currentStage, err)
}
if failed {
return execution.StatusFailed, nil
}
if err := flagStore.Write(
filepath.Join(layout.FlagDir(), string(currentStage)+"_ALL_CHECKED"),
nil,
); err != nil {
return execution.StatusFailed, fmt.Errorf(
"writing completed-check flag for stage %q: %w",
currentStage,
err,
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear an existing all-checked marker before each check run.

A passing run creates *_ALL_CHECKED. A later failed run returns at Line 258 without removing that existing marker. A prior success can therefore remain visible after the current checks fail.

Remove the marker before executing the check stage. Recreate it only after every current check succeeds. Add a success-then-failure regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/go/internal/agent/steps.go` around lines 252 - 270, The check-stage
flow must clear any existing *_ALL_CHECKED marker before running current checks,
so stale success state cannot survive a failure. Update the relevant check
execution function around the checkResultsFlagName and completed-check flag
writes to remove the marker before checks begin, retain recreation only after
all checks pass, and add a regression test covering a successful run followed by
a failed run.

@rice-riley
rice-riley force-pushed the agent-go-main-219 branch 2 times, most recently from 825f656 to f9d6fef Compare August 6, 2026 19:33

@lockwobr lockwobr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cross-review summary

Automated multi-reviewer analysis of d03e5844, cross-reviewed to consensus with adversarial verification of every confirmed finding. Nothing was run against a host or a cluster and no tests were executed; every claim comes from reading the pinned commit alongside the Python agent it replaces.

No CI ran on this commit. .github/workflows/agent-go-ci.yaml triggers only on pull_request targeting main, and this PR targets agent-mocks-219, so no Go build, test, vet or lint executed. The sole reported check is an automated review. That is worth fixing independently of anything below, because a stacked branch is exactly where a rewrite of this size wants a test signal.

Findings fall into two groups. Confirmed ones reached agreement across independent reviewers and then survived a fresh reviewer whose only job was to refute them. Unadjudicated ones carry a single reporter's position or a genuine reviewer split, noted per item. Two further findings were dropped after verification, and are not reproduced here.

The theme

Every confirmed finding is the same class: a divergence from the Python agent's on-disk or environment contract. Individually each is small. Together they mean cross-agent state continuity is not preserved: interrupt completion markers, step completion flags, symlink policy and boolean env parsing all changed shape. A node that has run the Python agent and then runs the Go agent does not recognise its own prior work.

That may well be deliberate, and the argument for it is reasonable: the Go agent is not the container entrypoint yet (containers/agent.Dockerfile still runs python -m skyhook_agent.controller), so nothing regresses today. But the drop-in-replacement framing implies a rolling image swap, and under a rolling swap these become live at once. Whichever way it goes, it should be a stated decision rather than an emergent one, because the failure modes are silent: a second reboot on an already-interrupted node, or a package that wedges on every stage.

Inline comments carry the findings that anchor to changed lines. Two more follow, both in files this PR does not touch but does put on the execution path.


Unadjudicated: reviewers split

agent/go/internal/interrupts/node_restart.go:62 - reboot success is not verified

One reviewer raised this and another disagreed, so treat it as an open technical question rather than a defect.

The claim: node_restart returns StatusSuccess on an ordinary exit-0 from reboot, which bypasses the boot-ID verification this PR adds. :51-55 return success only for the SIGTERM-during-shutdown case; :59-61 fail on signal or non-zero; :62 is the plain exit-0 path.

On systemd hosts reboot returns 0 as soon as the shutdown job is enqueued. In that window the agent exits 0, the init container completes, and the operator treats the interrupt stage as done with no verified reboot. Because a completed init container is not re-run, the retained .pending marker is never consumed and the boot-ID check never fires. interrupt.go:160-163 sets retainPendingMarker = true precisely so completion is decided "by comparing host boot IDs on the next invocation" - but that next invocation has to happen.

Two things would settle it:

  • Is there a guarantee across supported host images that reboot inside the chroot never returns 0 before shutdown terminates the init container? This is the hinge for severity, and it was not found in the pinned docs.
  • Does any later invocation (interrupt-check, post-interrupt) actually re-run the node_restart path after the node returns? If it does, prepareNodeRestartMarker eventually promotes the pending marker and the exposure narrows to the window before the operator advances the stage.

agent/go/internal/step/shared.go:184 - step scripts are never chmodded executable

Single reporter, not cross-evaluated.

shared.go:184-191 builds the step command with no command.WithPermissions(...). command.go:98 defaults Permissions to 0, and both runners only chmod when it is non-zero (command_runner.go:36, chroot_command_runner.go:54). The Python agent chmods +x on every normal step: chroot_exec.py:62-64 runs os.chmod(cmds[0], ... S_IXGRP|S_IXUSR|S_IXOTH) whenever no_chmod is false, and run_step calls _run without it (controller.py:255, invoked at :330-337). Interrupts are consistent between the two.

So any package whose step scripts lack the execute bit in the container image installs fine today and starts failing with a permission error on apply/config/upgrade/post-interrupt once the agent image switches. Silent behavior removal rather than a documented tightening.


Open questions

  • Should agent-go-ci run for PRs targeting stacked branches? As written it only fires against main, so this entire rewrite merged with no Go test signal.
  • Was the strconv.ParseBool widening intentional? "1", "t", "T" are true in Go and false in Python, independent of the fallback issue in the inline comment. If intentional it belongs in agent/CHANGELOG.md and the README as a documented break.
  • Was dropping the separate stderr log file intentional? Python writes <step>-<ts>.log.err alongside the stdout log and prefixes lines with [out]/[err] plus per-line timestamps (controller.py:160-201); the Go path MultiWriters both streams into one raw .log. No fixture asserts on it, so nothing breaks today.
  • Nothing prunes interrupts/flags/<resourceID>/. The .pending marker is retained once node-restart execution begins and is only consumed by a later invocation for the same resource ID. Across generations those directories accumulate on the host, and neither the operator nor the agent removes them.
  • Can os.Root see host procfs? boot_id is read through the mounted host root. The interrupt pod mounts hostPath / with MountPropagation: HostToContainer, so /root/proc should be host procfs, but no lane could verify the submount is visible to a rooted read without executing something. If it is not, node-restart completion never promotes.
  • Interrupt log filenames also change (interrupts/<type>-<ts>.log versus interrupts/<type>_<i>-<ts>.log). No consumer reads host log filenames, so this is noted rather than raised.
  • Log timestamps gain nanosecond precision. CleanupOldLogs parses both formats so retention survives, but external tooling matching the old exact shape will not.

interruptsDirName,
interruptFlagsDirName,
runtime.resourceID,
string(interruptType)+".complete",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed finding, highest severity here. The completion marker name does not match the agent that ships today, so a completed interrupt is invisible after an agent swap.

This builds <stateRoot>/interrupts/flags/<resourceID>/<type>.complete and line 73 gates re-execution on that exact name. The Python agent builds the same directory (controller.py:492) but names the file per interrupt command index:

interrupt_id = f"{interrupt._type()}_{i}"        # controller.py:503
... f"{interrupt_dir}/{interrupt_id}.complete"   # controller.py:484

So the on-disk names are node_restart_0.complete, service_restart_0.complete, service_restart_1.complete. Only NoOp uses the bare type (controller.py:497) - which is why k8s-tests/operator-agent/interrupt/chainsaw-test.yaml:46 asserts no_op.complete and passes, and would not catch this.

SKYHOOK_RESOURCE_ID is <name>-<uid>-<generation>_<pkg>_<version>, stable across pod restarts, so this is not a race. A node that already rebooted under the Python agent presents no node_restart_0.complete that this code looks for, and the reboot interrupt runs a second time on an already-interrupted node. Same class for service_restart and restart_all_services, which additionally lose Python's per-command resume granularity.

The reboot path is the one case that always spans two agent invocations for a single resource ID, so it is the most likely to be hit in practice.

One reviewer refuted a version of this on the grounds that no Go agent image exists yet, which is true and is why nothing regresses today. It does not change the analysis for the moment the entrypoint switches. If the intent is that hosts start from a clean state format, that is defensible - but then it needs to be stated, and probably needs the old markers cleared rather than ignored.

time.Now().UTC().Format(time.RFC3339Nano),
configuredStep.Idempotence() == step.Disabled,
)
if _, err := flagStore.Mark(configuredStep, message); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed finding. Step completion flags use a different naming scheme than the Python agent, so every step re-runs on hosts carrying Python-era flags.

This line (flagStore.Mark) and line 88 (flagStore.Check) are the first code paths that actually exercise flags.Store. flags.go:118 names the file:

base(step.Path()) + "-" + <sha256 fingerprint> + ".flag"

The Python agent names it <step.path>_<base64(f"{arguments}_{returncodes}")> with no suffix (controller.py:246), under the identical directory <stateRoot>/flags/<pkg>/<version>/.

Same directory, no overlap in filenames. When a live cluster's agent image switches, no existing flag matches, so every previously-completed apply and post-interrupt step re-runs once on every node. (Config, uninstall and upgrade always re-run anyway, so those are unaffected.) How much that hurts depends on how genuinely idempotent the packaged steps are - but avoiding exactly this re-run is what the flag is for.

The naming code in flags.go predates this PR. This PR is what puts it on the execution path, which is why it surfaces here.

}

func prepareHost(copyRoot, rootMount string, cfg config.Config) error {
if err := hostfs.CopyTreeIfExists(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed finding, plus a second unadjudicated mechanism on the same line. Both are consequences of the same symlink policy, but they fail at different points and need separate fixes.

1. Symlinks in the source tree (confirmed). prepareHost here and ensurePackageData (lines 52, 60) route every copy through hostfs.CopyTreeIfExists, and hostfs.CopyTree rejects any symlink outright:

// agent/go/internal/hostfs/copy.go:72-74
return fmt.Errorf("copy source %q is a symbolic link", path)

The reference implementation uses shutil.copytree(..., dirs_exist_ok=True) with the default symlinks=False (controller.py:557,561,575), which dereferences and copies the target's contents. So a package that ships a symlink anywhere under root_dir/ - a common way to alias a config file - installs cleanly under the Python agent and fails every apply/config/upgrade/post-interrupt stage under the Go agent with copying package root overlay: ... is a symbolic link, wedging the package on the node. The same applies to legacy package data copied from SKYHOOK_DATA_DIR. Note the operator populates the host copy dir with cp -r, which preserves symlinks.

2. Symlinks in the destination path (unadjudicated, single reporter). Distinct and arguably worse. The per-entry work goes through hostfs.ensureDirectories (hostfs.go:335-345) and inspect (hostfs.go:348-360), both of which Lstat every path component and return path component %q is a symbolic link.

On every usr-merged distro - Ubuntu 20.04+, Debian 12, RHEL 8+ - /bin, /sbin, /lib and /lib64 are symlinks into /usr. Individual files such as /etc/localtime are symlinks too. So a root_dir overlay targeting a unit file under /lib/systemd/system, a binary under /bin or /sbin, or a replacement /etc/localtime - all documented uses - fails with copying package root overlay: path component "lib" is a symbolic link on essentially every mainstream host.

If the strictness is deliberate hardening rather than an oversight, the destination case still needs an answer: there is no way to write to /lib on a usr-merged system without traversing a symlink.

return value
}

func envBool(name string, fallback bool, logger *slog.Logger) bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed finding. envBool returns the caller's default when strconv.ParseBool fails, and lines 227-228 pass fallback=true for both SKYHOOK_AGENT_WRITE_LOGS and COPY_RESOLV.

The Python agent evaluates both as os.getenv(...).lower() == "true" (controller.py:74 and :680), so any value other than "true" yields false - including the empty string, "yes", "off", or a typo. agent/skyhook-agent/tests/test_controller.py:1471 asserts that behavior.

So the two contracts disagree in opposite directions on the same input. A user who disabled host log retention or resolv.conf copying with a value the Python agent accepted as false silently gets the feature re-enabled under the Go agent: host log files reappear, and the container's /etc/resolv.conf is written to the host. The only trace is a warning log.

Worth noting that agent_test.go:167-180 codifies the divergence by asserting COPY_RESOLV=" true " yields copyResolver=true, so this reads as intentional. If it is, the safer shape is to keep the permissive parse but make the fallback false for these two variables, so an unparseable value never turns a feature on. Either way it belongs in the changelog: silently re-enabling a host-mutating behavior on upgrade is the kind of break that is hard to diagnose from the node.

if err != nil {
return fmt.Errorf("resolving host resolver path: %w", err)
}
if err := hostfs.CopyFile(rootMount, source, destination); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unadjudicated, single reporter. Lower severity than the sibling comment above, but the same root cause.

copyResolverConfig calls hostfs.CopyFile(rootMount, "/etc/resolv.conf", <rootMount>/etc/resolv.conf), and copyRegularFile rejects a symlinked destination (copy.go:126-128); os.Root would refuse the traversal regardless. On systemd-resolved hosts /etc/resolv.conf is a symlink to ../run/systemd/resolve/stub-resolv.conf. Python uses shutil.copyfile (controller.py:682), which opens through the symlink and succeeds.

agent.go:228 defaults copyResolver to true and agent.go:338-342 runs it before anything else, so on a mainstream host this aborts every stage, including uninstall, with copying resolver configuration: ... is a symbolic link.

Kept at minor only because the operator injects COPY_RESOLV="false" for both package and interrupt pods, so the supported path never reaches it. What does reach it is the manual and legacy invocation forms this PR documents in agent/README.md, where the documented default is enabled.

Signed-off-by: Riley Rice <rrice@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/agent Skyhook agent (package executor) component/ci CI workflows, GitHub Actions, and repo tooling component/operator Skyhook operator (controller-manager) doc Documentation change (PR path label; doc issues use the Documentation type)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA]: Controller main / agent_main / SIGTERM + CLI parsing and entrypoint banner

2 participants