feat(agent): port enums and steps for agent go rewrite#235
Conversation
|
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:
📝 WalkthroughWalkthroughAdds a schema package with SchemaVersion type, ParseSchemaVersion, Compare, Highest, LatestFrom, and tests; and a step package defining Step/Stage/Idempotence, RegularStep with functional options/defaulting/Encode, UpgradeStep embedding with a no-arguments invariant and encoder, Decode dispatch that selects concrete step types from JSON, and Ginkgo/Gomega tests covering defaults, validation, decoding, and round-trip serialization. Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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/schema/schema.go`:
- Around line 93-110: The comparator currently discards parse errors from
parseVersionNumber and always returns -1/1, and ParseSchemaVersion returns
non‑canonical numeric inputs; update both to preserve canonical values and
consistent ordering: in the comparator (where
parseVersionNumber(a)/parseVersionNumber(b) are called) check and
propagate/handle parse errors instead of ignoring them—if either parse fails,
fall back to a deterministic lexical comparison of the SchemaVersion strings; in
ParseSchemaVersion call parseVersionNumber(normalized) and, when it succeeds,
convert the numeric result to the canonical SchemaVersion string (use a small
helper like formatSchemaVersion or similar) and return that canonical value, and
when parseVersionNumber fails return the parse error (don’t swallow it). Ensure
you reference and use parseVersionNumber and ParseSchemaVersion (and add
formatSchemaVersion if needed) so serialized values and comparator behavior stay
consistent.
🪄 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: fa64f032-24fc-48df-bab5-9e8373b25c00
📒 Files selected for processing (10)
agent/go/internal/schema/schema.goagent/go/internal/schema/schema_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.goagent/go/internal/step/wire.goagent/go/internal/step/wire_test.go
|
Zero-value In Python, The implicit contract is "always construct via Two ways to address it, either is fine:
If you'd rather keep the current design, at minimum I'd add a golden-bytes test for |
|
aNum, _ := parseVersionNumber(a)
bNum, _ := parseVersionNumber(b)
if aNum < bNum {
return -1
}
return 1Two issues here:
The numeric-equal fix is one line — add Worth adding tests for both cases (parse-equal-but-string-different, and an invalid input) — the latter will fail today, which is exactly the point. |
|
@rice-riley this PR has been inactive for 21 days. Do you need help finishing it, or should we close it for now? Feel free to reopen anytime. |
Signed-off-by: Riley Rice <[email protected]>
65b8e63 to
b38b809
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
agent/go/internal/schema/schema.go (1)
126-136:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCanonicalize parsed versions to
v{number}format and validate version numbers are >= 1.
ParseSchemaVersioncurrently returns the normalized input without re-formatting the numeric portion. Inputs like"v01"return as"v01"instead of the canonical"v1", allowing non-canonical representations to propagate into storage, serialization, and cross-system contracts. This breaks the implicit assumption thatSchemaVersionvalues are in canonicalv{N}form and can cause downstream inconsistencies.Additionally, the function does not validate that version numbers are >= 1, so invalid versions like
"v0"or"v-1"would be accepted.Capture the parsed number, validate it is >= 1, and return the canonical
SchemaVersion(fmt.Sprintf("v%d", n))representation.🔧 Proposed fix
func ParseSchemaVersion(value string) (SchemaVersion, error) { normalized := SchemaVersion(strings.ToLower(strings.TrimSpace(value))) if normalized == Latest { return normalized, nil } - if _, err := parseVersionNumber(normalized); err != nil { + n, err := parseVersionNumber(normalized) + if err != nil { return "", fmt.Errorf("invalid schema version %q: %w", value, err) } - return normalized, nil + if n < 1 { + return "", fmt.Errorf("invalid schema version %q: must be >= v1", value) + } + return SchemaVersion(fmt.Sprintf("v%d", n)), nil }🤖 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/schema/schema.go` around lines 126 - 136, ParseSchemaVersion currently returns the lowercased/trimmed input (e.g., "v01") and doesn't enforce version >= 1; update it to call parseVersionNumber(normalized) to obtain the integer n, validate n >= 1, and then return the canonical SchemaVersion using fmt.Sprintf("v%d", n) (instead of returning normalized). Keep error wrapping the same when parseVersionNumber fails and return an explicit error if n < 1. Ensure references: ParseSchemaVersion, parseVersionNumber, SchemaVersion, and Latest are used as described.
🤖 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/schema/schema_test.go`:
- Around line 80-83: Add two new test cases in schema_test.go targeting
ParseSchemaVersion: one that verifies canonicalization (call ParseSchemaVersion
with "v01" and "V002", assert no error and that returned value equals
SchemaVersion("v1") and SchemaVersion("v2") respectively) and another that
verifies rejection of non-positive versions (call ParseSchemaVersion with "v0"
and "v-1" and assert an error is returned; for "v0" also assert the error
message contains text like "must be >= v1"). Use the existing test style (It
blocks and Expect assertions) so they integrate with the current tests for
ParseSchemaVersion.
In `@agent/go/internal/step/regular_step.go`:
- Around line 54-59: The Encode method in RegularStep should wrap returned
errors with context instead of returning bare errors: when calling
s.Idempotence.Validate() (in RegularStep.Encode) wrap any returned error using
fmt.Errorf with a descriptive prefix like "validate idempotence: %w", and when
json.Marshal(s) fails wrap that error similarly (e.g. "marshal RegularStep:
%w"); keep the existing s.applyDefaults() call and ensure you import fmt if not
already imported.
- Around line 54-60: The Encode method on RegularStep must ensure the
discriminator is fixed: after calling s.applyDefaults() and validating
Idempotence (in RegularStep.Encode), set s.UpgradeStep = false unconditionally
before calling json.Marshal(s) so the marshalled payload cannot be coerced into
an upgrade step by a caller that previously set the exported field.
In `@agent/go/internal/step/step.go`:
- Around line 169-170: The call to u.Validate() inside Decode returns the bare
err (return nil, err) which breaks the repo's error-wrapping rule; update Decode
so that when u.Validate() fails you wrap the error with context (e.g., using
fmt.Errorf("upgrade step validation failed: %w", err)) before returning, and add
the fmt import if needed; locate the u.Validate() call in the Decode function in
step.go and replace the bare return with a wrapped error that references the
validation failure.
In `@agent/go/internal/step/upgrade_step_test.go`:
- Around line 103-143: Add a new unit case that constructs an UpgradeStep value
directly (not via NewUpgradeStep) and calls UpgradeStep.Encode() to verify the
wire contains the discriminator `"upgrade_step":true`; specifically, create an
UpgradeStep instance (using the UpgradeStep struct with RegularStep fields set
appropriately), call its Encode method and Expect the dumped bytes to
ContainSubstring(`"upgrade_step":true`), placing the test alongside the existing
Describe("UpgradeStep.Encode", ...) cases to pin ownership of the discriminator
independent of NewUpgradeStep.
In `@agent/go/internal/step/upgrade_step.go`:
- Around line 44-45: The call to u.Validate() in NewUpgradeStep returns a bare
err; update NewUpgradeStep to wrap that validation error with context using
fmt.Errorf and %w (e.g., include "NewUpgradeStep" or "upgrade step validation"
in the message) before returning so callers get call-site context; ensure fmt is
imported if not already.
- Around line 69-74: UpgradeStep.Encode currently just validates and delegates
to RegularStep.Encode, allowing a directly-constructed UpgradeStep to serialize
with the discriminator "upgrade_step": false; update UpgradeStep.Encode to
ensure the discriminator is forced to true before encoding by setting the
UpgradeStep-specific discriminator flag on the embedded RegularStep (or
otherwise set the field used by RegularStep.Encode that controls "upgrade_step")
and then call Validate and RegularStep.Encode (i.e. in UpgradeStep.Encode, set
the discriminator to true, call u.Validate(), then return
u.RegularStep.Encode()).
---
Duplicate comments:
In `@agent/go/internal/schema/schema.go`:
- Around line 126-136: ParseSchemaVersion currently returns the
lowercased/trimmed input (e.g., "v01") and doesn't enforce version >= 1; update
it to call parseVersionNumber(normalized) to obtain the integer n, validate n >=
1, and then return the canonical SchemaVersion using fmt.Sprintf("v%d", n)
(instead of returning normalized). Keep error wrapping the same when
parseVersionNumber fails and return an explicit error if n < 1. Ensure
references: ParseSchemaVersion, parseVersionNumber, SchemaVersion, and Latest
are used as described.
🪄 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: d05e7484-5db8-4136-bf4e-96f94a252be2
📒 Files selected for processing (8)
agent/go/internal/schema/schema.goagent/go/internal/schema/schema_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
b38b809 to
e4b3643
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
agent/go/internal/step/regular_step.go (1)
35-40:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPreserve
on_hostdefault parity for directRegularStepliterals.Line 39 uses
OnHost bool, and Lines 136-139 explicitly skip defaulting it. That meansRegularStep{Path: "foo.sh"}.Encode()emits"on_host":false, which diverges from Python’s default-true wire behavior and the parity objective. Please switch to an unset-capable representation (for example*bool+ defaulting) or prevent direct literal construction so defaulting can be enforced uniformly.Also applies to: 136-139
🤖 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/regular_step.go` around lines 35 - 40, The RegularStep struct's OnHost bool causes direct literals like RegularStep{Path:"foo.sh"} to encode "on_host": false breaking parity; change OnHost to a pointer (*bool) on the struct (symbol: OnHost in type RegularStep) and update RegularStep.Encode (and any defaulting logic tied to Idempotence) to treat nil as "default true" when serializing, ensuring existing defaulting at lines handling default values (previously skipping OnHost) still applies; alternatively, prevent direct literal construction by making the struct unexported and providing a constructor that sets the default, but prefer the *bool + default-in-Encode approach for minimal disruption.
🤖 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/step/upgrade_step.go`:
- Around line 72-74: UpgradeStep.Encode currently returns the bare error from
u.Validate(), losing call-site context; change the return to wrap the validation
error with context (e.g., return nil, fmt.Errorf("upgrade step validation
failed: %w", err)) so callers see which step failed, and add/import fmt if not
present; refer to UpgradeStep.Encode and u.Validate when making the change.
---
Duplicate comments:
In `@agent/go/internal/step/regular_step.go`:
- Around line 35-40: The RegularStep struct's OnHost bool causes direct literals
like RegularStep{Path:"foo.sh"} to encode "on_host": false breaking parity;
change OnHost to a pointer (*bool) on the struct (symbol: OnHost in type
RegularStep) and update RegularStep.Encode (and any defaulting logic tied to
Idempotence) to treat nil as "default true" when serializing, ensuring existing
defaulting at lines handling default values (previously skipping OnHost) still
applies; alternatively, prevent direct literal construction by making the struct
unexported and providing a constructor that sets the default, but prefer the
*bool + default-in-Encode approach for minimal disruption.
🪄 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: 7d126f11-6e1b-47e9-9dd4-f97cc7875e1f
📒 Files selected for processing (8)
agent/go/internal/schema/schema.goagent/go/internal/schema/schema_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
e4b3643 to
bb3f44a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/step/regular_step.go`:
- Around line 34-49: The exported RegularStep struct currently has OnHost bool
which serializes false for zero-value literals and breaks Python parity; change
OnHost to a pointer (*bool) with the json tag `on_host,omitempty`, update
NewRegularStep to initialize OnHost to pointer-to-true, and modify applyDefaults
(and any code referencing OnHost) to treat nil as “unset” and set the pointer to
true when applying defaults; update tests to use NewRegularStep (or set &true)
where they construct RegularStep literals so the field is omitted/round-trips
correctly.
In `@agent/go/internal/step/step_test.go`:
- Around line 224-236: The test for RegularStep zero-value should assert the
OnHost behavior to document the wire discrepancy: update the test (the It block
that constructs bare := RegularStep{Path: "foo.sh"} and calls bare.Encode() /
Decode()) to either assert that the encoded wire form contains on_host:false or
assert that after round-trip the decoded rs.OnHost is false; reference
RegularStep, Encode, Decode and the OnHost field so reviewers can locate and
verify the added assertion.
🪄 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: 06d0a09e-9432-4de3-b73e-2cc66821b069
📒 Files selected for processing (8)
agent/go/internal/schema/schema.goagent/go/internal/schema/schema_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
lockwobr
left a comment
There was a problem hiding this comment.
One thing that is not a blocker, we can come back later to make things more uniform once we get closer to parity.
* ci: publish keyless release attestations (#232) (#240) * ci: publish keyless release attestations * ci: drop nvcr release attestations * ci: narrow release attestation identity * ci: gate attestations to release tags * docs: show immutable release subjects * ci: attest helm chart releases --------- Signed-off-by: AnouarMohamed <[email protected]> Co-authored-by: Anouar Mohamed <[email protected]> * chore(docs): update docs around release and location of helm chart (#237) (#239) * fix(test): uninstall downgraded package before cleanup in downgrade-after-uninstall (#241) The test left [email protected] freshly installed at chainsaw's automatic CLEANUP, where the new delete-time uninstall finalizer (from #200) must run uninstall pods on every node before releasing the CR. That exceeds chainsaw's default cleanup window, causing "context deadline exceeded". Add a final uninstall-v1 step that flips uninstall.apply=true on the downgraded version and waits for nodeState to empty, so the CR has no installed packages when cleanup deletes it. Signed-off-by: Alex Yuskauskas <[email protected]> * fix: deadlock in webhook controller when upgrading from old versions (#243) * fix: close StageInterrupt trap + harden core e2e pool (#242) * fix(test): harden core e2e pool against finalizer-cleanup races and improve diagnosability Two changes across the six core-pool chainsaw tests: 1. Add `timeouts.cleanup: 120s` to every core-pool test. Chainsaw's default cleanup window (~30s) is too tight for the project's CR finalizer, which must uncordon nodes, remove labels/annotations, and GC package pods before releasing. Same failure mode that hit downgrade-after-uninstall (PR #241). 2. Add `catch:` blocks to `depends-on` and `simple-skyhook`, mirroring the pattern used by the other four core tests. When these tests flake, CI now captures the Node, Skyhook CR, and package Pods for diagnosis instead of failing without artifacts. No assert logic changes — this is a foundation pass to remove a known flake class and make future flakes diagnosable. Signed-off-by: Alex Yuskauskas <[email protected]> * fix(operator): close StageInterrupt trap when configUpdates signal decays A package whose only interrupt is a `configInterrupts` entry (no top-level `interrupt` block) could get stuck at `StageInterrupt/StateSkipped` permanently when `Status.ConfigUpdates` cleared or never persisted (e.g. due to a 409 on the spec patch). The state machine queried the dynamic `HasInterrupt(config)` signal at four points, and once that signal decayed to false, the package was untouchable: `ProgressSkipped` wouldn't promote it, `NextStage` wouldn't advance it past Interrupt, and `GetComplete`/`IsPackageComplete` wouldn't count it complete. Decouple progression-past-Interrupt from the dynamic signal: - `NextStage`: add `StageInterrupt → StagePostInterrupt` to the no-interrupt default map. The with-interrupt full-replacement map is preserved as-is — PR #200 deliberately omits `StageUninstall → StageApply` from it so with-interrupt uninstalls route via `StageUninstallInterrupt`; collapsing the maps would silently re-enable that transition. - `ProgressSkipped`: drop the `HasInterrupt` gate. The only writer of `StateSkipped` at `StageInterrupt` is `ProcessInterrupt`'s budget-contention branch, which already decided the package needed an interrupt to schedule. Stage alone is sufficient. - `GetComplete`, `IsPackageComplete`: treat `StagePostInterrupt` as unconditionally terminal. The only way to reach it is via the interrupt cycle; gating the terminal check on a signal that can decay is redundant with the entry gate and only becomes load-bearing when the trap fires. Reproducer in `skyhook_types_test.go` exercises all three sites. Surfaced by the `chainsaw/config-skyhook` "update while running" step, which patches the spec mid-flight and concurrently triggers the `HandleMigrations` and `processSkyhooksPerNode` 409 conflict paths — those stretch convergence enough that ConfigUpdates can be lost between the package reaching StageInterrupt and ProgressSkipped firing. Signed-off-by: Alex Yuskauskas <[email protected]> * refactor(operator): GetComplete delegates to IsPackageComplete GetComplete and IsPackageComplete encoded the same terminal-state logic twice. Have GetComplete iterate the spec packages and call IsPackageComplete for each, and move the explanatory comment to IsPackageComplete where the predicate lives. Behavior preserved: the prior implementation iterated node state and filtered to spec packages by name+version match; the new one iterates spec packages and looks them up in node state by unique name (name|version). Same set of packages either way. Signed-off-by: Alex Yuskauskas <[email protected]> --------- Signed-off-by: Alex Yuskauskas <[email protected]> --------- Signed-off-by: AnouarMohamed <[email protected]> Signed-off-by: Alex Yuskauskas <[email protected]> Co-authored-by: Anouar Mohamed <[email protected]> Co-authored-by: ayuskauskas <[email protected]>
bb3f44a to
91f8f6c
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
agent/go/internal/step/regular_step.go (1)
34-49:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftZero-value
RegularStepliteral breaks Python wire parity foron_host.
RegularStepis exported, allowing direct construction:RegularStep{Path: "foo.sh"}. Because Go's bool zero-value isfalseandapplyDefaultsintentionally skipsOnHost(lines 136-138), encoding a zero-value literal produces"on_host":false, violating the stated goal of "byte parity with Python" (where the default ison_host=true).The documented constructor
NewRegularStepseedsOnHost=true(line 124) and works correctly, but the exported struct allows the broken path. Several tests construct literals directly (e.g., regular_step_test.go lines 28, 34, etc.), though they don't verifyOnHostbehavior and may mask this discrepancy.Mitigation options:
- Make
RegularStepunexported (regularStep) to force use ofNewRegularStep.- Change
OnHostto*boolsoapplyDefaultscan detect unset and default totrue.- Document that direct literal construction is unsupported and add a runtime guard or lint rule.
🤖 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/regular_step.go` around lines 34 - 49, Change the OnHost field to a pointer so unset literals don't serialize "on_host":false: update RegularStep.OnHost from bool to *bool (and add `omitempty` to its json tag), update NewRegularStep to allocate and set a true pointer, and modify applyDefaults (and any callers) to treat nil as "unset" and set it to true when defaulting; run and adjust tests (e.g., regular_step_test.go) to construct or inspect the pointer/dereferenced value as needed.
🤖 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.
Duplicate comments:
In `@agent/go/internal/step/regular_step.go`:
- Around line 34-49: Change the OnHost field to a pointer so unset literals
don't serialize "on_host":false: update RegularStep.OnHost from bool to *bool
(and add `omitempty` to its json tag), update NewRegularStep to allocate and set
a true pointer, and modify applyDefaults (and any callers) to treat nil as
"unset" and set it to true when defaulting; run and adjust tests (e.g.,
regular_step_test.go) to construct or inspect the pointer/dereferenced value as
needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: d6689325-5d5a-44c1-809b-fe4db8347ebf
📒 Files selected for processing (8)
agent/go/internal/schema/schema.goagent/go/internal/schema/schema_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
Description
Ports the
StepandSchemaVersiondata types from Python (agent/skyhook-agent/) to two new Go packages underagent/go/internal/, building on the interrupts port that landed earlier. Wire format and lifecycle semantics are preserved so Go and Python agents agree on the bytes.internal/step/mirrorsskyhook_agent/step.py:Stepis a Go interface with eight getters plus anEncode() ([]byte, error)method. Two implementations satisfy it:RegularStep(the default; Python's plainStep) andUpgradeStep(Python'sclass UpgradeStep(Step)), which embedsRegularStepand enforces Python's "no arguments" invariant via its ownValidate.NewRegularStep(path, opts...)/NewUpgradeStep(path, opts...)withWithName,WithArguments,WithReturncodes,WithEnv,WithOnHost,WithIdempotence,WithRequiresInterrupt. Defaults (Name from Path, Arguments=[], Returncodes=[0], Env={}, OnHost=true, Idempotence=Auto) are applied in one place viaapplyDefaults.Decode(data []byte) (Step, error)reads the JSON wire payload and returns either aRegularSteporUpgradeStepbased on theupgrade_stepdiscriminator, propagating the Python-parityUpgradeStepinvariant error.Encodelives on the interface, so dispatch is by Go type:RegularStep.Encodeemitsupgrade_step:false,UpgradeStep.Encodeemitsupgrade_step:trueafter validating its invariant. A pinning test guards the embedding override.Mode(11 values, includingInterrupt,PostInterrupt, etc.), theApplyToCheck/CheckToApplymaps, thenonStepModesset,IsStepMode, and theIdempotenceenum withValidate.internal/schema/portsSchemaVersionfromskyhook_agent/enums.py:SchemaVersiontyped string withV1andLatestconstants.Latestsorts above any concrete version, matching Python'sSortableEnum.Compare,Highest,ParseSchemaVersion, andLatestFromhelpers replace Python's operator-overload comparison andget_latest_schema.Deliberate divergences from Python are documented at the divergence site:
step.Idempotence.Validateuses"X is not a valid idempotence value"(Go is more specific than Python's"X is not a valid mode").step.Decodeis intentionally looser than Python'sStep.load: Python requiresidempotencein the payload; Go treats it as optional and defaults toAuto.step.RegularStep.RequiresInterruptis populated byDecodebut intentionally dropped byEncode, mirroring Python'sStep.dumpwhich also omits it.Out of scope (continues the "pure data types" boundary): the agent runtime (
controller.py,chroot_exec.py), the cluster-awareStepsvalidator, and the JSON-schema validation pipeline fromconfig.py.Closes #213
Checklist
git commit -s) per the DCO.