feat(agent): embedded json schema and step validation for agent go re…#287
Conversation
57e56f8 to
bf6263b
Compare
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe agent now sets a default slog text logger at startup. The Go config package adds embedded JSON schemas, schema-backed load/dump logic, and cross-step validation for step modes and file paths. Internal step types now expose script paths through Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 26-31: The startup logging setup in main is introducing a global
slog default, which should be removed. Update main to avoid calling
slog.SetDefault and keep logging on the repo’s logr path by using
controller-runtime’s log.FromContext(ctx) / logr.Logger pattern instead of
process-wide logging state. Use the existing entrypoint in main and any
downstream logger wiring to pass an explicit logger boundary rather than relying
on hidden global defaults.
In `@agent/go/internal/config/config.go`:
- Around line 62-96: In Load, the config’s declared root_dir in cfg.RootDir is
used separately from the stepRootDir argument when calling step.Validate, which
can cause confusing path resolution drift. Add a brief guard or comment near
materialize and the step.Validate call to make the intended relationship
explicit, and if they are meant to match, assert that cfg.RootDir and
stepRootDir agree before validating steps.
In `@agent/go/internal/step/validate.go`:
- Around line 165-179: The checkStepFilesExist helper is treating every os.Stat
failure as a missing step, which masks permission or I/O problems. Update
checkStepFilesExist to distinguish os.IsNotExist(err) from other errors when
checking stepPath(s) under rootDir: only append truly missing paths to missing,
and return non-existence errors immediately so real filesystem failures are not
reported as “do not exist.”
🪄 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: 2aaf549d-d134-43cf-b4db-d096c52fa0e3
⛔ Files ignored due to path filters (1)
agent/go/go.sumis excluded by!**/*.sum
📒 Files selected for processing (9)
agent/go/cmd/agent/main.goagent/go/go.modagent/go/internal/config/config.goagent/go/internal/config/config_test.goagent/go/internal/config/schema.goagent/go/internal/config/schemas/v1/skyhook-agent-schema.jsonagent/go/internal/config/schemas/v1/step-schema.jsonagent/go/internal/step/validate.goagent/go/internal/step/validate_test.go
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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/config/config_test.go`:
- Around line 61-71: The current schema compilation tests cover valid and
unknown versions, but they do not verify byte-for-byte parity with the Python
source schemas. Add the schema drift golden test to config_test.go alongside
compiledSchemaFor/schema.V1 coverage so the embedded schema output is compared
against the expected Python-derived golden content and will fail on any
byte-level drift.
In `@agent/go/internal/config/config.go`:
- Around line 102-139: Dump currently serializes values that Load will later
reject because it does not validate the caller-supplied stage keys or the final
wire representation. Update Dump to validate stage values in the modes map and
ensure the marshaled wireConfig is accepted by Load before returning, using the
existing Dump and Load paths as the source of truth. Focus on the Dump function,
wireConfig construction, and any stage/scheme validation helpers so bogus
step.Stage values or invalid packageVersion inputs fail early.
In `@agent/go/internal/config/schemas/v1/skyhook-agent-schema.json`:
- Around line 7-20: The v1 schema contract is missing the root_dir field even
though Load, Dump, and the test fixture treat it as part of the config shape.
Update skyhook-agent-schema.json to declare root_dir in the properties for the
schema alongside schema_version, package_name, and package_version, and make it
required so malformed configs cannot pass validation; use the existing config
handling in Load/Dump to keep the schema aligned with the serialized model.
In `@agent/go/internal/step/validate.go`:
- Around line 137-159: The missing-check warning in warnMissingCheckPairs is
matching against all check stages, which lets an unrelated check suppress a
warning for the paired step. Update the logic so each step only looks for its
corresponding check stage derived from expectedCheckName(stepPath(s)) and
compare against the matching Stage rather than the aggregated checkPaths map.
Keep the warning behavior in warnMissingCheckPairs and use stepPath,
expectedCheckName, and IsStepStage to locate the affected flow.
- Around line 90-101: The check-only guard in checkNonCheckStagePresent is too
loose because it treats an empty non-check stage slice as present. Update the
logic to verify that at least one stage in steps has a non-empty []Step, rather
than only checking for the map key. Keep the existing ApplyToCheck and error
message behavior, but make the presence check ignore empty slices so configs
like apply: [] do not bypass validation.
- Around line 165-176: `checkStepFilesExist` currently trusts
`filepath.Join(rootDir, stepPath(s))`, so absolute paths and `../` traversal can
escape the package root. Add a guard before `os.Stat` that resolves each
`stepPath(s)` to a clean relative path and rejects any entry that is absolute or
climbs outside `rootDir`, then only validate paths that stay under the root.
Keep the existing missing-file collection and error reporting, but ensure
`stepPath(s)` is validated as an in-root path before it is checked.
🪄 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: 7af7fa4d-118e-438f-be3d-0293b1fead93
⛔ Files ignored due to path filters (1)
agent/go/go.sumis excluded by!**/*.sum
📒 Files selected for processing (9)
agent/go/cmd/agent/main.goagent/go/go.modagent/go/internal/config/config.goagent/go/internal/config/config_test.goagent/go/internal/config/schema.goagent/go/internal/config/schemas/v1/skyhook-agent-schema.jsonagent/go/internal/config/schemas/v1/step-schema.jsonagent/go/internal/step/validate.goagent/go/internal/step/validate_test.go
lockwobr
left a comment
There was a problem hiding this comment.
Review
The layer is functionally correct and well-tested: the Load flow is sound, Dump determinism holds, the migrate-over-the-Python-bug fix is a real improvement, and the tests hit the genuine edge cases (round-trip, missing required field, unknown version, warn-don't-fail). Nice work on the coverage.
That said, the throughline of this review is: it reads as a transliteration of the Python agent rather than a Go rewrite. The flow, the decomposition, and several type choices are shaped to mirror skyhook_agent instead of being idiomatic Go, and the comments say so out loud ("Order mirrors Python's config.load", "Mirrors Python's Mode(mode)", "matching the Python agent's warn-don't-fail"). Behavior is expressed as package-level functions reading and mutating shared structs, plus a package-global cache, which is the procedural Python-module shape. The Go-native version pushes behavior onto types and depends on small interfaces at the seams. None of the items below are optional polish; I'd want them addressed before merge.
1. Validation is in the wrong place, and it's fragmented
Cross-step validation lives in internal/step/validate.go, but these are document-level invariants ("there must be defined steps", "apply stages must have checks", "UpgradeStep placement") — they're about the assembled config, not about the step package. Validation is currently scattered across three layers:
internal/config/schema.go— JSON schema validationinternal/step/validate.go— cross-step rules- the step types' own
Validate()methods
So "where does a config get validated?" can't be answered from one place. Suggest moving the cross-step rules into config (where the whole document is materialized and where Load already orchestrates), leaving step to own only what's intrinsic to a step (ParseStage, Stage consts, per-type Validate()). Bonus: it resolves the naming overload between the internal/schema package (version arithmetic) and config/schema.go (actual JSON-schema validation) — two different "schema" concepts a reader has to disambiguate.
2. Lean on interfaces at the seams (the Go-native version of this design)
embeddedLoader (config/schema.go:119) is already a clean example of this done right: it satisfies jsonschema's loader interface and the compiler depends on the abstraction. Do more of that at the seams that earn it.
a. Schema validation behind a SchemaValidator interface, and kill the package global. config.validate reaches straight into santhosh-tekuri/jsonschema/v6 through a package-level schemaCache sync.Map (schema.go:59, :83). That global mutable cache is the anti-interface smell. Model it as a value that owns its cache as a field and satisfies a small interface:
type SchemaValidator interface {
Validate(data []byte, v schema.SchemaVersion) error
}config.Load then accepts that interface (or holds one on a Loader struct) instead of calling a package function backed by a global. The compiler+cache becomes an injectable value, tests can supply a fake instead of round-tripping the real embedded schemas, and the sync.Map/sync.Once machinery (heavier than the problem needs for compile-time-embedded files with one version today) collapses into struct state.
b. migrate should be a registry/interface if it's really "the seam future migrations slot into." It's currently a single function hardcoded to the identity transform (config.go:149), described in its own comment as a seam. The idiomatic expression of that is a Migration interface (or map[schema.SchemaVersion]Migration) you register into and range over — not a function you'll later grow a switch inside. If migrations are genuinely coming, build the seam as the type now; if not, drop the "seam" framing and just inline the version check.
c. Widen Step with Path(). The stepPath type switch (internal/step/validate.go:195) returns "" in its default case — duck-typing ported into Go, and a latent bug: a future Step type makes checkStepFilesExist call os.Stat(filepath.Join(rootDir, "")), which stats rootDir itself, succeeds, and a missing step file silently passes validation. The interface (step.go:30) only exposes Encode() and its doc comment even tells callers to type-switch. Both concrete types already carry an exported Path field, so adding Path() string deletes the switch, the silent default, and the bug. Behavior on the type, not a function switching over concrete types.
What I would not do: introduce a Validator interface for the cross-step checkX rules just to have one. Those are better as a []rule (func values) you range over — composable and individually testable without the ceremony of an interface. The test for "should this be an interface" is whether there's a real second implementation or a dependency seam to mock (schema validation: yes; migrations: yes if coming; a fixed list of internal checks: no).
3. Other Pythonic constructs to make Go-native
String-keyed "sentinel" errors built for Python text-parity — interrupts.go:28, step.go:100. Designing error messages to match Python's strings across the language boundary couples Go callers to Python's wording. If callers need to branch, use typed/sentinel errors with errors.Is. If they don't, drop the parity constraint. Either way the Go side shouldn't be pinned to Python's prose.
*bool/*string absent-vs-present probing — step.go:153, interrupts.go:65. This reproduces Python's None/KeyError distinction. Sometimes justified (genuine tri-state), but worth confirming each one needs three states rather than a zero-value default; several read as reflex ports of the Python shape.
4. Smaller idiom fixes (also should-fix)
- Validation error precedence —
step/validate.go:69runs before:72, so a misplacedUpgradeStepwith noapply-checkreports the generic pairing error instead of the specific placement one. Run placement before pairing. - Comment density — several comments narrate the code or just record Python parity. The ones explaining a genuine surprise (
json.Numberhandling, thejson:"-"rationale onConfig.Modes) stay; the parity narration should go, especially once the structure stops mirroring Python.
Nit
schemas/v1/step-schema.jsonhas trailing whitespace / no trailing newline.
bf6263b to
713be68
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/interrupts/interrupts.go`:
- Around line 57-71: In Decode, base64 decoding and json.Unmarshal currently
drop the underlying error and return only errInvalidSerializedInterrupt, which
removes useful diagnostics. Update the Decode function to wrap the original
decode/unmarshal error while preserving the sentinel behavior so callers can
still branch with errors.Is on errInvalidSerializedInterrupt. Keep the changes
localized to the Decode path and ensure both the base64.StdEncoding.DecodeString
and json.Unmarshal failure cases retain their original error context.
🪄 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: dfde1d3f-9574-46a1-bc84-4835704dd968
⛔ Files ignored due to path filters (1)
agent/go/go.sumis excluded by!**/*.sum
📒 Files selected for processing (16)
agent/go/cmd/agent/main.goagent/go/go.modagent/go/internal/config/config.goagent/go/internal/config/config_test.goagent/go/internal/config/schemas/v1/skyhook-agent-schema.jsonagent/go/internal/config/schemas/v1/step-schema.jsonagent/go/internal/config/validate.goagent/go/internal/config/validate_test.goagent/go/internal/interrupts/interrupts.goagent/go/internal/interrupts/interrupts_test.goagent/go/internal/step/regular_step.goagent/go/internal/step/regular_step_test.goagent/go/internal/step/step.goagent/go/internal/step/step_test.goagent/go/internal/step/upgrade_step.goagent/go/internal/step/upgrade_step_test.go
713be68 to
45124e5
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
agent/go/internal/config/config_test.go (1)
61-71: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winPlease add the schema-drift golden test from the PR scope.
These specs prove the embedded schemas compile, but they still won't fail if the embedded JSON drifts byte-for-byte from the Python source schemas, which was one of the stated objectives for this work.
🤖 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/config_test.go` around lines 61 - 71, Add the missing schema-drift golden test to the existing schema compilation specs so the embedded JSON is compared byte-for-byte against the Python source schemas, not just compiled successfully. Update the "schema compilation" Describe block in config_test.go to include a golden-style assertion using compileSchema and the embedded schema constants (for example schema.V1), and make sure the test fails when the embedded schema content drifts from the source fixture rather than only on invalid JSON or version errors.agent/go/internal/config/schemas/v1/skyhook-agent-schema.json (1)
7-20: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAdd
root_dirto the v1 schema contract.
Config.MarshalJSON/UnmarshalJSONand the test fixtures all treatroot_diras part of the wire format, but this schema neither declares nor requires it. That lets malformed configs pass schema validation even though the serialized model includes the field.Suggested fix
"properties": { "schema_version": { "description": "Version of the schema", "type": "string" }, + "root_dir": { + "description": "Root directory containing step files", + "type": "string" + }, "package_name": { "description": "The name of the skyhook package", "type": "string" }, @@ "required": [ "schema_version", + "root_dir", "package_name", "package_version", "expected_config_files", "modes" ]Also applies to: 106-112
🤖 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/schemas/v1/skyhook-agent-schema.json` around lines 7 - 20, The v1 schema contract is missing the root_dir field even though Config.MarshalJSON, Config.UnmarshalJSON, and the test fixtures already treat it as part of the wire format. Update the schema definition in skyhook-agent-schema.json to declare root_dir alongside schema_version, package_name, and package_version, and make it a required property so malformed configs are rejected during validation.
🤖 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/config/config.go`:
- Around line 21-24: The config loading and validation flow still depends on
*slog.Logger and falls back to slog.Default(), so update the API to use
context.Context and logr instead. Thread the reconcile context through Load and
validateModes, and switch logging to log.FromContext(ctx) at the call sites in
config.go and validate.go. Remove the logger parameter/defaulting pattern and
ensure the existing load/validation paths get the context from their callers.
In `@agent/go/internal/config/validate.go`:
- Around line 299-308: expectedCheckName is deriving the suffix from the full
path, so dots in parent directories produce incorrect check names and false
warnings. Update expectedCheckName to compute the name from the basename only
(for example by separating directory and base first, then applying the “_check”
suffix to the base), and keep warnMissingCheckPairs using the corrected result
so paths like dir.v1/foo map to dir.v1/foo_check.
In `@agent/go/internal/step/step_test.go`:
- Around line 121-133: The ParseStage spec currently depends on the Stages
registry, so it only checks that ParseStage and Stages agree instead of
verifying the full expected set. Update the ParseStage test in step_test.go to
assert against an explicit list of known stage values rather than ranging over
Stages, using ParseStage and the existing stage constants/types to validate each
expected stage. Keep the unknown-stage assertion as-is so registry drift is
caught if a new Stage is added but not registered.
In `@agent/go/internal/step/step.go`:
- Around line 56-71: The stage registry is currently exposed as the exported
slice Stages, which lets other packages mutate the runtime validation set used
by ParseStage. Make the registry immutable by hiding the underlying list behind
an unexported variable or by returning a copied slice from an accessor, and
update any uses of Stages in step.go to go through the safe immutable path so
accepted wire keys cannot be changed at runtime.
---
Duplicate comments:
In `@agent/go/internal/config/config_test.go`:
- Around line 61-71: Add the missing schema-drift golden test to the existing
schema compilation specs so the embedded JSON is compared byte-for-byte against
the Python source schemas, not just compiled successfully. Update the "schema
compilation" Describe block in config_test.go to include a golden-style
assertion using compileSchema and the embedded schema constants (for example
schema.V1), and make sure the test fails when the embedded schema content drifts
from the source fixture rather than only on invalid JSON or version errors.
In `@agent/go/internal/config/schemas/v1/skyhook-agent-schema.json`:
- Around line 7-20: The v1 schema contract is missing the root_dir field even
though Config.MarshalJSON, Config.UnmarshalJSON, and the test fixtures already
treat it as part of the wire format. Update the schema definition in
skyhook-agent-schema.json to declare root_dir alongside schema_version,
package_name, and package_version, and make it a required property so malformed
configs are rejected during validation.
🪄 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: 088678d9-90b0-4de9-a791-0da0149a3f51
⛔ Files ignored due to path filters (1)
agent/go/go.sumis excluded by!**/*.sum
📒 Files selected for processing (16)
agent/go/cmd/agent/main.goagent/go/go.modagent/go/internal/config/config.goagent/go/internal/config/config_test.goagent/go/internal/config/schemas/v1/skyhook-agent-schema.jsonagent/go/internal/config/schemas/v1/step-schema.jsonagent/go/internal/config/validate.goagent/go/internal/config/validate_test.goagent/go/internal/interrupts/interrupts.goagent/go/internal/interrupts/interrupts_test.goagent/go/internal/step/regular_step.goagent/go/internal/step/regular_step_test.goagent/go/internal/step/step.goagent/go/internal/step/step_test.goagent/go/internal/step/upgrade_step.goagent/go/internal/step/upgrade_step_test.go
45124e5 to
cb84d95
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
agent/go/internal/step/step_test.go (1)
146-159: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the defaulting test actually omit
idempotence.Line 147 still sends
idempotence:false, so this spec doesn't exercise the absent-field fallback toAuto; it only proves the legacy bool form decodes. Removing the field here, or splitting out a separate legacy-bool case, will catch regressions in the real defaulting path.Suggested change
- data := []byte(`{"path":"foo.sh","idempotence":false,"upgrade_step":false}`) + data := []byte(`{"path":"foo.sh","upgrade_step":false}`)🤖 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/step_test.go` around lines 146 - 159, The defaulting spec in Decode is still including the legacy idempotence field, so it does not verify the missing-field fallback. Update the “applies defaults to a minimal payload” test in step_test.go to omit idempotence entirely when constructing the JSON passed to Decode, and keep the assertion that RegularStep.Idempotence defaults to Auto. If you still want coverage for the legacy boolean form, add a separate test case for that Decode behavior.
♻️ Duplicate comments (3)
agent/go/internal/config/config_test.go (1)
61-71: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winAdd the schema-drift golden test from the PR scope.
These assertions only prove the embedded schemas compile. The PR objective also calls for drift protection against the Python source schemas, and nothing here would fail if the embedded JSON diverged while still compiling. Based on PR objectives, the work should include schema drift protection via a golden 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/config/config_test.go` around lines 61 - 71, The current schema compilation tests in compileSchema only verify embedded schema validity and do not protect against drift from the Python source schemas. Add the schema-drift golden test from the PR scope alongside the existing Describe("schema compilation") coverage so it compares the generated embedded schema output against the Python source schema artifacts. Use the existing compileSchema and schema.V1/schema.SchemaVersion symbols as the anchor, and ensure the new test fails when embedded JSON diverges even if it still compiles.agent/go/internal/config/schemas/v1/skyhook-agent-schema.json (1)
7-20: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAdd
root_dirto the v1 schema contract.
agent/go/internal/config/config.goserializesroot_dir, andagent/go/internal/config/config_test.gotreats it as part of the required wire shape, but this schema neither declares nor requires it. That lets malformed configs pass schema validation while leavingConfig.RootDirunset afterLoad.Suggested fix
"properties": { "schema_version": { "description": "Version of the schema", "type": "string" }, + "root_dir": { + "description": "Root directory containing step files", + "type": "string" + }, "package_name": { "description": "The name of the skyhook package", "type": "string" }, @@ "required": [ "schema_version", + "root_dir", "package_name", "package_version", "expected_config_files", "modes" ]Also applies to: 106-112
🤖 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/schemas/v1/skyhook-agent-schema.json` around lines 7 - 20, The v1 schema contract is missing the root_dir field, so configs can validate without populating Config.RootDir. Update the schema object in skyhook-agent-schema.json to declare root_dir alongside the existing properties, and mark it as required so the contract matches what config.go serializes and config_test.go expects. Keep the change aligned with the schema’s existing property naming and validation style.agent/go/internal/config/config.go (1)
21-24: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftUse
context.Context/logrhere instead of plumbing*slog.Logger.
Loader.Loadnow bakes*slog.Loggerinto the config API, andvalidateModes/warnMissingCheckPairscontinue the same contract. That conflicts with the repo logging guideline and makes this layer harder to compose with the rest of the Go code. As per coding guidelines,Use controller-runtime's logr.Logger obtained via log.FromContext(ctx) for logging; do not introduce slog or stdlib logandPlumb the reconcile ctx through instead of creating context.Background() inside the reconcile path.#!/bin/bash set -euo pipefail rg -n 'log/slog|\*slog\.Logger|slog\.Default\(\)' agent/go/internal/config agent/go/cmd/agentAlso applies to: 135-145
🤖 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/config.go` around lines 21 - 24, `Loader.Load`, `validateModes`, and `warnMissingCheckPairs` are still tied to `*slog.Logger`, which violates the repo logging contract and makes the config package harder to compose. Refactor these APIs to accept `context.Context` and pull a controller-runtime `logr.Logger` via `log.FromContext(ctx)` at the call sites, then thread the reconcile context through instead of introducing `context.Background()` or any `slog` dependency in this path.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/config/config.go`:
- Around line 167-199: Dump currently only validates schema and misses the
cross-step checks that Load performs, so configs can serialize successfully and
still fail on reload. Update Loader.Dump to run the same validateModes logic
used by Load before marshaling, using the same inputs from modes and
expectedConfigFiles so cases like empty steps, missing paired check/apply
stages, or misplaced UpgradeStep are rejected up front. Keep the fix localized
to Dump and reference the existing Load/validateModes behavior to ensure the
Dump→Load contract stays consistent.
---
Outside diff comments:
In `@agent/go/internal/step/step_test.go`:
- Around line 146-159: The defaulting spec in Decode is still including the
legacy idempotence field, so it does not verify the missing-field fallback.
Update the “applies defaults to a minimal payload” test in step_test.go to omit
idempotence entirely when constructing the JSON passed to Decode, and keep the
assertion that RegularStep.Idempotence defaults to Auto. If you still want
coverage for the legacy boolean form, add a separate test case for that Decode
behavior.
---
Duplicate comments:
In `@agent/go/internal/config/config_test.go`:
- Around line 61-71: The current schema compilation tests in compileSchema only
verify embedded schema validity and do not protect against drift from the Python
source schemas. Add the schema-drift golden test from the PR scope alongside the
existing Describe("schema compilation") coverage so it compares the generated
embedded schema output against the Python source schema artifacts. Use the
existing compileSchema and schema.V1/schema.SchemaVersion symbols as the anchor,
and ensure the new test fails when embedded JSON diverges even if it still
compiles.
In `@agent/go/internal/config/config.go`:
- Around line 21-24: `Loader.Load`, `validateModes`, and `warnMissingCheckPairs`
are still tied to `*slog.Logger`, which violates the repo logging contract and
makes the config package harder to compose. Refactor these APIs to accept
`context.Context` and pull a controller-runtime `logr.Logger` via
`log.FromContext(ctx)` at the call sites, then thread the reconcile context
through instead of introducing `context.Background()` or any `slog` dependency
in this path.
In `@agent/go/internal/config/schemas/v1/skyhook-agent-schema.json`:
- Around line 7-20: The v1 schema contract is missing the root_dir field, so
configs can validate without populating Config.RootDir. Update the schema object
in skyhook-agent-schema.json to declare root_dir alongside the existing
properties, and mark it as required so the contract matches what config.go
serializes and config_test.go expects. Keep the change aligned with the schema’s
existing property naming and validation style.
🪄 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: ed115967-74cc-4791-b256-ff9523c32517
⛔ Files ignored due to path filters (1)
agent/go/go.sumis excluded by!**/*.sum
📒 Files selected for processing (16)
agent/go/cmd/agent/main.goagent/go/go.modagent/go/internal/config/config.goagent/go/internal/config/config_test.goagent/go/internal/config/schemas/v1/skyhook-agent-schema.jsonagent/go/internal/config/schemas/v1/step-schema.jsonagent/go/internal/config/validate.goagent/go/internal/config/validate_test.goagent/go/internal/interrupts/interrupts.goagent/go/internal/interrupts/interrupts_test.goagent/go/internal/step/regular_step.goagent/go/internal/step/regular_step_test.goagent/go/internal/step/step.goagent/go/internal/step/step_test.goagent/go/internal/step/upgrade_step.goagent/go/internal/step/upgrade_step_test.go
cb84d95 to
6b8634d
Compare
6b8634d to
b602830
Compare
…write Signed-off-by: Riley Rice <[email protected]>
bff9310 to
68c2c5c
Compare
feat(agent): config loader with embedded JSON schemas + stage/step validation (#214)
Description
Adds the config layer of the Go agent rewrite: a new
internal/configpackage thatloads, validates, and serializes a package
config.json, plus a newinternal/stagepackage for the lifecycle-stage vocabulary and cross-step validation rules. This is the
layer every later agent module (#215, #217, #219) builds on.
Validation follows the same split the Python agent uses: the JSON schema owns the
declarative half (required fields, the semver/name
patterns, types) andhand-written Go owns the structural/cross-step rules (no defined steps, apply
stages without checks,
UpgradeStepplacement, step files existing on disk, check-pairwarnings). The v1 schemas are embedded so the binary stays a single static artifact.
Builds on #213 (
internal/{schema,step,interrupts}, already onmain). No changes tothe Python agent.
Architecture
The package-config domain is split into three packages, each with one job:
internal/stage— the lifecycle-stage vocabulary: theStagetype, the stageconstants,
ParseStage,IsStepStage, and theApplyToCheck/CheckToApplypairingmaps. Names are unprefixed (
stage.Apply,stage.ApplyCheck) since the package namealready namespaces them.
internal/step— the step types: theStepinterface,RegularStep,UpgradeStep,Idempotence, andDecode/Encode. No dependency onstage(thestage → steprelationship lives inconfigasmap[stage.Stage][]step.Step).internal/config— assembles the two: loads/validates/dumps the whole documentand owns the cross-step rules that no single step can enforce.
What's in here
internal/config/config.go—Configstruct + aLoaderwithLoad/Dump.Configholds typedModes(map[stage.Stage][]step.Step); customMarshalJSON/UnmarshalJSONroute themodessection throughstep.Decode/step.Encodeso thestep.Stepinterface stays the contract.Loader.Load(data, stepRootDir, *slog.Logger): read declared version →schema-validate (which also rejects unknown versions) → materialize
modesviastage.ParseStage+step.Decode→ run the cross-step rules.Loader.Dump: emits the latest schema version; deterministic output (struct fieldorder + stdlib-sorted map keys), each step re-encoded through
step.Encode, and nowruns the same structural cross-step rules Load does so a dumped config can't
serialize cleanly yet fail on reload.
internal/config/validate.go— the single home for config validation. Holds boththe
SchemaValidatorinterface + the embedded-schema implementation(
//go:embed schemas/v1/*.json, validated viagithub.com/santhosh-tekuri/jsonschema/v6)and the cross-step document rules (
validateModes,checkModeRules,warnMissingCheckPairs,checkStepFilesExist).internal/config/schemas/v1/*.json— copies of the Python agent's v1 schemas;these become the Go module's authoritative copy at cutover ([FEA]: Cutover: flip default to Go, delete Python, flatten dir, update docs #222).
internal/stage/stage.go— theStagetype, constants, registry (stage.All),pairing maps,
IsStepStage, andParseStage(which rejects unknown mode keys — theschema does not forbid extras).
internal/step—Stepgained aPath()method so callers read a step's paththrough the interface instead of type-switching on the concrete types.
cmd/agent/main.go— establishes the agent'ssloglogging seam.Closes #214.
New dependency
github.com/santhosh-tekuri/jsonschema/v6(+ transitivegithub.com/dlclark/regexp2) —the agent's first runtime dependency. Native JSON Schema Draft 2020-12 with a custom
URLLoaderhook, which is what lets us resolve the cross-file$refstraight out of theembed FS. Per AGENTS.md, flagging it explicitly as the one new pattern this PR introduces.
Rejected alternatives: pure-Go hand-rolled validation (loses the schema as the single
source of truth),
google/jsonschema-go(less ergonomic embed/$refwiring at the time),and
omissis/go-jsonschema(a code generator — it would own the type layer and collidewith the hand-built
steptypes from #213).Notable design decisions
Calling these out for review, since several depart from a literal port of the Python agent:
config/validate.go). Cross-step rules sit withLoad/Dump, where the whole document is materialized — not scattered across thesteppackage. This also removes the "schema" name overload:internal/schemaisversion arithmetic, and JSON-schema validation no longer has a separate
schema.go.internal/stagepackage.Stageis one of the three core vocabconcepts (Status/State/Stage); giving it its own package keeps
stepto just the steptypes and matches how the operator treats
Stageas first-class. Names are unprefixed(
stage.Apply) since the package name carries the namespace.SchemaValidatorinterface + injectableLoader. Schema validation sits behind asmall interface that
Loaderholds, instead of a package-level function backed by aglobal cache. The validator is stateless (the embedded schemas are tiny and
single-version), and tests can inject a fake — no
sync.Map/sync.Oncemachinery.migratefunction. With only v1 defined, schema validation already rejectsunknown/newer versions, so the version check is inlined into
Loadrather than kept asa speculative migration seam. (This also drops the Python
migrate()bug where itreturns aValidationErrorinstead of raising it.)StageandStepare exposed through behavior, not type switches.Step.Path()replaced a
stepPathtype switch whosedefaultreturned""— a latent bug where afuture step type would silently stat the root dir and pass file validation. The
RegularStepdata field is namedScriptPathso the type can exposePath().Dumpenforces the cross-step contract.Dumpnow runs the structural rules(
checkModeRules: empty steps, missing paired checks, misplacedUpgradeStep) beforemarshaling, so
Dump→Loadstays consistent. The on-disk step-file check isintentionally excluded —
Dumprecords a package-relative root, not a path it can stat.fmt.Errorf("…: %w"), matchinginterrupts.go,instead of porting Python's
StepError/ValueErrorhierarchy.interrupts.Decodereturns an
errors.Is-able sentinel and wraps the underlying base64/JSON error fordiagnostics.
$refresolution uses a customURLLoader(keyed by URL basename over theembed FS) instead of
AddResource. The v1 schemas use relative$id/$refvalueswhose URI math makes
AddResourceresolve to doubled-up paths; the loader keepsresolution entirely inside the embed FS.
slog, not controller-runtimelogr. The agent is a standalonecontainer entrypoint with no reconcile loop or controller-runtime dependency, so
slog(withslog.Default()fallback when anillogger is passed) is the right seamhere — the operator's
logrcontract does not apply to this module.dump()output. The Go agent isn't wiredinto the operator e2e suite until cutover ([FEA]: Cutover: flip default to Go, delete Python, flatten dir, update docs #222), so dump/log string parity isn't
load-bearing yet.
Dumptargets deterministic + schema-valid + round-trippable.Testing
cd agent/go && make test— 5 Ginkgo suites, 110 specs, all passing.cd agent/go && make lint— golangci-lint clean (0 issues).cd agent/go && make build— builds with theslogseam.go mod tidyis stable.Coverage includes: valid load; missing required field; unknown schema version;
non-semver
package_version; missingschema_version;Dump→Loadround-trip;Dumprejecting configs that would fail Load's cross-step rules; an injected fakeSchemaValidator(provingLoaddepends on the interface, not the embedded schemas);embedded-schema compilation; every cross-step rule (no steps, only-check stages,
apply-without-check,
UpgradeStepmisplacement, missing step files, path-escaperejection, and the non-fatal check-pair warning asserted via a buffer-backed
slog.Logger);expectedCheckNameincluding dotted parent directories;stage.ParseStageagainst an explicit stage list; and
step.Decodedefaulting (including the absentidempotence→Autofallback and the legacy boolean form).Out of scope (follow-ups)
teestreaming andrun_step#217).Load/ validation ([FEA]: Controllermain/agent_main/ SIGTERM + CLI parsing and entrypoint banner #219).root_dirfield + Go↔Python schema drift test —root_diris in the wireformat but unvalidated, and the Python source schema omits it too; aligning that
belongs with the source-of-truth schema, not a unilateral divergence here.
Files
Checklist
git commit -s) per the DCO.