This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Canonical guide for AI coding assistants working on the main (v3.x) branch of rpc-go. The content is tool-neutral and applies to any agent (Claude Code, Codex, Cursor, Aider, Continue, Gemini CLI, GitHub Copilot, etc.). Edit this file; pointer files (AGENTS.md, .github/copilot-instructions.md) should reference it.
This file is the v3 (main) guide. It is not accurate for the 2.x.x maintenance branch.
main— v3.0.0 beta (this file). Refactored around Kong CLI, acommands/package hierarchy, theorchestrator/subprocess driver, and the new RESTful profile-export activation flow. Publishes to thebetachannel.2.x.x— v2.x maintenance. Different architecture: flatinternal/flags/parser,cleanenvfor config, nocommands//orchestrator/packages, legacy ws/wss-only remote activation. That branch will get its ownCLAUDE.md; do not port v3 guidance there and do not port v2 patterns here.
When in doubt about which branch you're on: go.mod declares module github.com/device-management-toolkit/rpc-go/v2 on both (the major in the module path is historical and will not be bumped for v3). The discriminators are package layout (internal/commands/, internal/orchestrator/, internal/cli/cli.go with Kong) and v3.0-changes.md at the repo root.
Remote Provisioning Client (RPC) is a single-binary tool — and optional C-shared library — that activates, configures, deactivates, and reports on Intel® AMT devices. It runs on the managed device itself, talking to AMT firmware locally through the HECI/MEI driver (admin/root required), and optionally to a remote server (RPS or Console) for orchestration or profile delivery. Same binary, three deployment shapes:
- Local CLI:
rpc <command>on the target device. Default invocation with no command runsamtinfo. - Subprocess orchestrator: the same binary re-invokes itself (
rpc activate --acm …,rpc configure cira …, …) to execute a multi-step profile end-to-end. See Profile orchestration. - C-shared library:
cmd/rpc/lib.goexportsrpcExecandrpcCheckAccessfor embedding in C/C++/.NET/etc. consumers (go build -buildmode=c-shared).
v3 is a breaking release. Three things changed at the surface (full detail in v3.0-changes.md):
- Flags require explicit
--long/-shortsyntax; bare-longno longer works. confignow means application configuration (the YAML that mirrors CLI flags, defaultconfig.yaml). The oldconfigv2flag is gone.- The old "configuration profile" concept is now called a profile and is passed as
--profile <file-or-name>. The flag does double duty: a path/extension-bearing string is a local profile file; a bare name is a legacy RPS profile name (requires--url wss://…).
Activation has three distinct code paths and the flag combinations choose between them — keep them straight when editing internal/commands/activate/:
| Mode | Trigger | Code path | What rpc-go does |
|---|---|---|---|
| Legacy remote (RPS) | --url wss://… + --profile <name> |
internal/rps/ |
Opens WebSocket to RPS; RPS sends WSMAN messages, rpc-go relays them to AMT. rpc-go is a dumb pipe. |
| HTTP profile fullflow | --url https://…/api/v1/admin/profiles/export/<name> |
internal/profile/fetcher.go + internal/orchestrator/ |
rpc-go authenticates, fetches the encrypted profile, decrypts, registers the device with Console, then orchestrates the steps itself by re-invoking rpc <subcmd>. |
| Local profile file | --profile path/to/profile.yaml (with --key if encrypted) |
internal/profile/local.go + internal/orchestrator/ |
Same orchestrator as HTTP fullflow, but the profile is loaded from disk. |
| Local direct | --local --ccm / --local --acm (no profile) |
internal/commands/activate/local.go |
Direct single-step activation using flags. No orchestrator. |
The HTTP-fullflow path is the v3 headline feature — orchestration moves from RPS to rpc-go, dramatically cutting time-to-activation. Until that DX is polished, the legacy ws/wss flow is still supported and must not regress.
Go 1.27+ required (per go.mod). Module path is github.com/device-management-toolkit/rpc-go/v2 — the /v2 is historical (kept across the v3 release) and all intra-repo imports use that prefix. Don't try to "fix" it.
HECI/MEI access requires administrator / root. Local AMT commands (amtinfo excepted in degraded mode) fail with IncorrectPermissions when not elevated; on interactive terminals the CLI offers to self-elevate.
# bash/zsh
go run ./cmd/rpc/main.go <command> [flags]
# PowerShell — note: chain with ';' not '&&'
go run .\cmd\rpc\main.go <command> [flags]A bare invocation defaults to amtinfo (internal/cli/cli.go:Execute injects the command when none is present), so rpc and rpc amtinfo behave the same. Help still works (rpc --help, rpc configure --help, etc.) because --help/-h short-circuits the default-command injection.
The .vscode/launch.json ships configurations for the common scenarios (activate via wss, activate local with config.yaml, amtinfo, deactivate, …). All use "asRoot": true and "buildFlags": ["-race"] — prefer those over hand-built go run lines when iterating on local AMT flows; you need elevation either way.
# Executable
go build -o rpc ./cmd/rpc/main.go # Linux/macOS
go build -o rpc.exe ./cmd/rpc/main.go # Windows
# C-shared library (requires CGO + a working gcc/MSVC toolchain)
CGO_ENABLED=1 go build -buildmode=c-shared -o librpc.so ./cmd/rpc # Linux
go build -buildmode=c-shared -o rpc.dll ./cmd/rpc # Windowsbuild.sh <version> is the release-time wrapper used by semantic-release; it cross-compiles the matrix the .releaserc.json ships as GitHub release assets (rpc_linux_x64.tar.gz, rpc_linux_x86.tar.gz, rpc_windows_x64.exe, rpc_windows_x86.exe, rpc_so_x64.tar.gz). Local dev doesn't need it.
go test ./... # whole suite
go test ./internal/commands/activate # one package
go test -run '^TestActivate_Local$' ./internal/commands/activate # one test
go test ./... -coverprofile=coverage.out -covermode=atomic # with coverage (matches CI)
go tool cover -func=coverage.out # coverage reportCI runs go test ./... -coverprofile=coverage.out -covermode=atomic; -race is not enabled at the repo level today (unlike sibling repos) — feel free to add it locally when chasing data-race symptoms but don't add it as a CI default in passing.
Tests rely on:
go.uber.org/mock(gomock) for interface mocks. Regenerate withmake mockafter editinginternal/interfaces/wsman.goorpkg/amt/commands.go.github.com/stretchr/testify/require+assertfor assertions.
Fuzz targets live in internal/cli/cli_fuzz_test.go (currently the deactivate command parsing surface — the historically panic-prone area). Standard helpers:
make fuzz-short # 30s per target — the PR-time smoke
make fuzz # 5m per target — local thorough run
make fuzz-regression # corpus replay only, no new inputs (1x execution)
go test -fuzz=^FuzzDeactivate$ -fuzztime=1m ./internal/cli # ad-hoc one targetSeed corpora live under internal/cli/testdata/fuzz/ and are committed. Crashes discovered in CI (.github/workflows/fuzz.yml) are uploaded as artifacts; commit reproducers back into the corpus when fixing.
make mockThe make mock target encodes the canonical mockgen -source <interface> -destination … invocations:
internal/interfaces/wsman.go→internal/mocks/wsman_mock.gointernal/amt/commands.go→internal/mocks/amt_mock.go(note: the legacyinternal/amt/path comes from v2 layout and is still referenced by the Makefile — confirm before editing)
Commit the regenerated mocks in the same PR as the interface change. Stale mocks are a top cause of "passes locally, fails CI."
CI rejects unformatted or unlinted code. The pre-push triplet:
gofumpt -l -w -extra ./ # format (one-time install: go install mvdan.cc/gofumpt@latest)
go vet ./... # vet
# Dockerized golangci-lint — same .golangci.yml CI uses
# bash/zsh:
docker run --rm -v .:/app -w /app golangci/golangci-lint:latest golangci-lint run -v --fix
# PowerShell:
docker run --rm -v ${pwd}:/app -w /app golangci/golangci-lint:latest golangci-lint run -v --fixWhat CI actually does: gofmt -s -l . | wc -l must be zero, then reviewdog/action-golangci-lint runs against ./.golangci.yml. gofumpt -extra is a strict superset of gofmt -s, so anything passing it also passes CI's formatter; linter binary versions aren't pinned identically, so occasional skew on bleeding-edge linters is possible.
.golangci.yml is strict:
- Style:
gofumpt(extra rules),gcifor import ordering,wsl_v5for whitespace/separator rules (allow-first-in-block: true,branch-max-lines: 2). - Bugs:
staticcheck,errcheck(check-type-assertions: true),errorlint(%wrequired on wraps),bodyclose,noctx,nakedret,ineffassign,predeclared,durationcheck,makezero. - Complexity:
cyclop15,funlen100 lines / 45 statements,gocognit16,nestif5. - Quality:
goconst(min-len 2, min-occurrences 3),mnd,misspell(US locale),unconvert,unparam,nlreturn,dogsled,nolintlint(require explanation + specific). - Exclusions:
err113,funlen,nestif,goconstare relaxed in*_test.go;paralleltestandgodotrelaxed inintegration-test/. gosecexcludes G115 (integer overflow) — already too noisy on our integer math.
mnd (magic-number detector) is on for arguments/case/condition/operation/return — extract constants when you find yourself fighting it. Don't //nolint without a // because: <reason> per nolintlint.
rpc-go is intentionally small and procedural. The layout is:
cmd/rpc/ // entry points
├─ main.go // executable: calls cli.Execute(os.Args)
└─ lib.go // C-shared library: rpcExec / rpcCheckAccess via cgo
internal/cli/cli.go // Kong parser construction + Execute / ExecuteWithAMT.
// Wires kong-yaml config file, BindToProvider for amt.Interface DI,
// default-to-amtinfo, self-elevation on IncorrectPermissions.
internal/commands/ // every CLI verb lives here
├─ base.go // AMTBaseCmd — the lifecycle base (see "Command lifecycle" below)
├─ auth.go // ServerAuthFlags — embedded into commands that talk to RPS/Console
├─ shared.go // commands.Context — DI struct passed to every Run()
├─ amtinfo.go / version.go / deactivate.go // simple verbs
├─ activate/ // activate <flags> — mode-detected (local / wss / https-profile / file)
│ ├─ activate.go // top-level Validate/Run that dispatches
│ ├─ local.go // local single-step activation
│ └─ remote.go // legacy ws/wss path that delegates into internal/rps
├─ configure/ // configure <sub> [...] — each tunable is its own subcommand
│ ├─ configure.go // ConfigureCmd composition + ConfigureBaseCmd (EnsureRuntime)
│ └─ mebx.go / amtpassword.go / amtfeatures.go / enableamt.go / disableamt.go /
│ cira.go / syncclock.go / wifisync.go / wireless.go / wired.go / tls.go /
│ proxy.go / synchostname.go
└─ diagnostics/ // diagnostics (alias: diag)
internal/orchestrator/ // profile driver — re-invokes the rpc binary per step
├─ orchestrator.go // ProfileOrchestrator.ExecuteProfile()
└─ executor.go // CLIExecutor (exec.Command-based) + ExecError + DirectExecutor (tests)
internal/profile/ // profile loading: local YAML, encrypted local, HTTP(S) fetch + auth
internal/rps/ // legacy WebSocket client to RPS (the ws/wss activation path)
internal/lm/ // Local Management Service / Engine connection helpers
internal/local/amt/ // adapter that builds a go-wsman-messages WSMAN client over LMS
internal/certs/ // TLS configs, embedded trusted store, Enterprise Assistant helper
internal/device/ // Console REST client (AddDevice / UpdateDevice / ClearMPSPassword)
internal/interfaces/ // WSMANer + other interfaces — the seam that gets mocked
internal/mocks/ // generated — do not hand-edit
pkg/ // public surface (importable by external consumers, e.g. the lib build)
├─ amt/ // amt.Interface — high-level HECI-backed AMT command wrapper
├─ heci/ // HECI/MEI driver bindings (Windows + Linux)
├─ pthi/ // Platform Trust Host Interface protocol
├─ utils/ // exit codes (utils.CustomError), prompts, elevation, paths, constants
├─ smb/ // SMB helper for fetching profiles from share paths
├─ network/ // DHCP/IP helpers
├─ upid/ // UPID handling
├─ hotham/ // Hotham (in-band AMT discovery / activation helper)
├─ version/ // build-injected version + git hash
└─ windows/ // win32 syscalls (elevation, console)
The composition root is internal/cli/cli.go:Execute. It constructs the Kong parser, binds the amt.Interface provider, parses, builds a commands.Context, and calls kctx.Run(appCtx) to dispatch into the selected command's Run(ctx) method.
We use github.com/alecthomas/kong for parsing. Kong's tag-driven model is non-obvious in places and the wider internet has plenty of stale examples — when in doubt, check the upstream docs and source at https://github.com/alecthomas/kong (kong.Context, kong.Visit, BindToProvider, Configuration, AfterApply). What this repo does specifically:
-
Top-level CLI is a struct.
internal/cli/cli.go:CLIembedsGlobals(shared flags) andcommands.ServerAuthFlags(shared auth flags), then declares one field per top-level command with acmd:""tag:type CLI struct { Globals commands.ServerAuthFlags AmtInfo commands.AmtInfoCmd `cmd:"" name:"amtinfo" help:"…"` Activate activate.ActivateCmd `cmd:"activate" help:"…"` Configure configure.ConfigureCmd `cmd:"configure" help:"…"` Deactivate commands.DeactivateCmd `cmd:"deactivate" help:"…"` Version commands.VersionCmd `cmd:"version" help:"…"` Diagnostics diagnostics.DiagnosticsCmd `cmd:"diagnostics" aliases:"diag" help:"…"` }
-
Subcommands nest by embedding command structs as fields with
cmd:"". E.g.ConfigureCmddeclaresMEBx MEBxCmd \cmd:"" name:"mebx" aliases:"setmebx"`,TLS TLSCmd `cmd:"" aliases:"tls,configuretls"`, etc. The Kong tree mirrors the struct tree. To add a newconfigureverb, add a field withcmd:""toconfigure.ConfigureCmdand implementValidate()/Run(ctx *commands.Context) error` on the struct. Don't manually register anywhere — Kong walks the tree. -
Shared flag groups are embedded structs.
Globals,commands.ServerAuthFlags, andcommands.AMTBaseCmdare all embedded. Flag tags (help:"…",name:"…",short:"…",env:"…",default:"…",enum:"…") live on the fields. Fields taggedkong:"-"are skipped by the parser (we use this onWSMan,ControlMode, etc. insideAMTBaseCmd— those are runtime-only state). -
Validation hook: any command that implements
Validate() erroris called by Kong after flag binding. Use it for cross-flag invariants (e.g.activatechecks--urlscheme + mode flag combinations). -
Lifecycle hook: any command that implements
AfterApply(deps…) erroris called by Kong afterValidate. Kong injects dependencies fromBindToProvider.AMTBaseCmd.AfterApply(amtCommand amt.Interface)is the canonical example — see below. MakeAfterApplyidempotent (we track this viaafterApplied bool); Kong can call it more than once across the tree. -
Dependency injection: in
cli.gowe dokong.BindToProvider(func() amt.Interface { return amtCommand }). Anywhere downstream that takesamt.Interfaceas a parameter (AfterApply,Run) gets the bound value. To add a new injected dependency, add aBindToProviderline and accept the type as a parameter; don't reach into globals. -
YAML defaults:
kong.Configuration(kongyaml.Loader, "config.yaml")makes every flag's default-resolvable fromconfig.yamlkeys with the same name as the flag (name:tag wins over field name). CLI args and env vars override YAML. The YAML loader is best-effort — a missing file is fine, a malformed file is fatal. -
Aliases go on the field tag (
aliases:"syncclock,synctime"). Use them liberally onconfiguresubcommands; we have many users on legacy command names from v2. -
Help:
kong.UsageOnError()makes parse errors print usage.cli.go:PrintHelpre-parses with--helpappended to render contextual help for partial command trees. -
The
name:"..."struct tag controls the CLI flag name;kong:"required"does not affect it. Kong resolves flag names in this order: (1)name:tag if present, (2) kebab-cased Go field name as fallback. Thekong:"required"directive only marks a flag as mandatory — it does not override thename:tag. Concrete example:configure.CIRACmd.MPSAddressis declared`help:"MPS Address" env:"MPS_ADDRESS" name:"mpsaddress" kong:"required"`and Kong exposes it as--mpsaddress(thename:tag wins). SimilarlyMPSCertwithname:"mpscert" kong:"required"exposes as--mpscert. The only authoritative source for a flag name isgo run ./cmd/rpc/main.go <command> --help— verify there before assuming kebab-case conversion applies.
For anything more exotic — custom mappers, alternate config loaders, hidden flags, BeforeReset, Resolvers — go read github.com/alecthomas/kong. The README has the authoritative tag table; kong/* source files have the runtime semantics that the README glosses over.
Almost every command that touches AMT embeds commands.AMTBaseCmd. Its AfterApply(amtCommand amt.Interface) error runs once per invocation and:
- Aborts early with
utils.IncorrectPermissionswhen not elevated (the outercli.Executethen offers to self-elevate on interactive TTYs). Commands that tolerate missing privileges setSkipWSMANSetup = trueand get a degraded path (this is howamtinfoworks without root). - Calls
amtCommand.GetControlMode()with retries (4 attempts, 4s backoff) so transient HECI busyness doesn't fail the whole command. - Reads
GetChangeEnabled()to detect TLS-enforced local ports (LocalTLSEnforced); this changes how WSMAN clients are dialed downstream. - Closes the MEI handle (
defer amtCommand.Close()) — holding it in the parent process blocks orchestrator subprocesses on Windows, see comment inorchestrator.go.
AMTBaseCmd does not set up the WSMAN client during AfterApply (we don't have the password yet). Instead each command's Run calls EnsureAMTPassword(ctx, cmd) (which prompts if needed, with confirm-on-typo when control mode 0) and then EnsureWSMAN(ctx) to lazily build the WSMAN client. ConfigureBaseCmd.EnsureRuntime is the standard helper that runs both. Call it at the top of every new configure subcommand Run — don't try to set up WSMAN earlier.
Commands that conditionally need a password override RequiresAMTPassword() bool (e.g. ActivateCmd returns false when --stopConfig is set; amtinfo returns false unless --userCert is asked for).
When activation is driven by a profile (HTTP fullflow or local file), internal/orchestrator/orchestrator.go:ProfileOrchestrator.ExecuteProfile runs the steps by re-invoking the same rpc binary as a subprocess for each step (internal/orchestrator/executor.go:CLIExecutor). The argv looks like rpc --password <new> configure cira --mpsaddress … --mpscert ….
This is unusual and intentional:
- Each step is isolated. A WSMAN session leak, an AMT firmware quirk, or a HECI lock won't bleed into the next step.
- Failures are typed by exit code.
ExecError.ExitCodematchesutils.CustomError.Code— the orchestrator gates retries on the code (e.g.AMTAuthenticationFailedtriggers the password-rotation fallback inexecuteWithPasswordFallback). Never substring-match subprocess output — verbose Digest logs make that unreliable. - Password rotation is interleaved. Mid-orchestration, if a step authenticates and fails with
AMTAuthenticationFailed, the orchestrator prompts (up to 3 attempts) for the current AMT password, callsrpc configure amtpassword --password <old> --newamtpassword <new>, and retries the original step. - Step order is fixed (
ExecuteProfilesequence): activation → MEBx → AMT features → wired → WiFi enable → wireless profiles (with--purgefirst) → TLS → CIRA → HTTP proxy. CIRA failures wrapErrCIRAConfigurationso callers can clear the MPS password from Console on failure.
When adding a new orchestrated step:
- Implement the underlying
configure <name>subcommand the normal way (struct ininternal/commands/configure/, validate, run, tests). - Add an
executeXxx()method onProfileOrchestratorthat callspo.baseArgs()then appends"configure", "<name>", "--flag", value, …and finishes withpo.executeWithPasswordFallback(args). - Splice it into
ExecuteProfile()in the correct position relative to AMT firmware dependencies. - Test it with
DirectExecutor(the test-onlyCommandExecutorthat swallows args) — never shell out in unit tests.
baseArgs() is the canonical argv prefix and already includes --skip-amt-cert-check and --password and propagates verbose. Use it.
The ws/wss path (activate --url wss://… --profile <name>, deactivate --url …, maintenance commands) opens a WebSocket to RPS and lets RPS decide what to do. rpc-go's role is to relay WSMAN messages between RPS and AMT and respond to specific protocol prompts. This path must keep working — many existing v2 integrators depend on it. Behaviour changes here need explicit approval; prefer adding new behaviour to the HTTP-profile path instead.
your command
└─ AMTBaseCmd lifecycle
├─ amt.Interface (pkg/amt) // pkg/heci + pkg/pthi over MEI/HECI ioctls
│ └─ GetControlMode, GetUUID, GetVersion, Close, …
└─ EnsureWSMAN → localamt.NewGoWSMANMessages(LMSAddress)
└─ go-wsman-messages WSMAN client targeting LMS on :16992/:16993
└─ internal/lm/ Local Management Service connection
└─ AMT firmware
Two distinct cert-check flags govern TLS verification on each side and they are not interchangeable:
--skip-cert-check/SkipCertCheck— applies to remote RPS/Console HTTPS/WSS connections.--skip-amt-cert-check/SkipAMTCertCheck— applies to the AMT/LMS TLS connection over local ports.
When wiring something new through Kong, keep them straight; conflating them lets remote certs slip past verification.
The library exports rpcExec(Input, **Output, **ErrOutput) int and rpcCheckAccess() int. Input is a CSV-quoted argv string; the C library hijacks stdout/stderr, runs cli.ExecuteWithAMT(args, amtCommand), restores the streams, and returns the exit code (utils.CustomError.Code). Two consequences:
- Don't write directly to
os.Stdoutfrom command code if you want the lib build to capture it — go through the existing logger or output helpers. - Keep
cli.Executeandcli.ExecuteWithAMTparallel.Executeis the executable entry point and wires self-elevation;ExecuteWithAMTis the lib entry point and skips elevation (the host process is responsible). New top-level behaviour added toExecutemust be reflected (or explicitly not reflected, deliberately) inExecuteWithAMT.
- Never hand-author WSMAN XML. All WSMAN goes through
github.com/device-management-toolkit/go-wsman-messages/v2. Use the existinginternal/interfaces/WSMANerseam andinternal/local/amtadapter. If a needed message isn't ingo-wsman-messages, fix it upstream rather than crafting raw XML here —go-wsman-messagesis a sibling repo in the device-management-toolkit org and accepts contributions. - Embed
AMTBaseCmdon every command that touches AMT; embedConfigureBaseCmdon everyconfiguresubcommand. Don't try to set up the WSMAN client outside the documentedEnsureRuntime/EnsureAMTPassword/EnsureWSMANflow — you'll race with the lifecycle and break the test mocks. - Don't substring-match subprocess output in the orchestrator. Use
ExecError.ExitCode(it matchesutils.CustomError.Code). Add a newutils.CustomErrorif you need a new signal. - Keep the two activation paths cleanly separated.
internal/rps/is the legacy ws/wss path and must keep working. The HTTP-fullflow path lives ininternal/profile/,internal/orchestrator/, andinternal/commands/activate/activate.go:runHttpProfileFullflow. Don't fork them with shared mutable state; if you need shared helpers put them ininternal/device/orinternal/profile/. - CLI tags and YAML keys must match.
kong-yamllooks up config keys by the flag'sname:tag (or kebab-cased field name whenname:is absent). When you rename a flag, auditconfig.yaml/config.sample.yamland anyenv:tags in the same change — environment variables override YAML and CLI overrides both. - Magic numbers and strings are linter-enforced.
mndandgoconst(≥3 occurrences, ≥2 chars) will catch the obvious cases. Constants live next to the consumer (e.g.configure.RemoteTLSInstanceId) or inpkg/utilswhen truly shared. %won every error wrap.errorlintis on. New sentinel errors are package-scopevar ErrFoo = errors.New("…")and get matched witherrors.Is/errors.As.- Mind
wsl_v5. Blank lines around blocks,if/returnclustering, branch-max-lines 2 — letgofumpt -extra+golangci-lint --fixdo the mechanical pass. - Tests do not run in parallel today (no
paralleltestlinter, no-racein CI). When writing fresh tests, default to subtests +tt := ttcapture so we can flip those on later without churn. - Cross-platform constraints are real. rpc-go ships on Linux and Windows. HECI/MEI implementations diverge per OS (
pkg/heci/heci_linux.govspkg/heci/heci_windows.go); elevation, console handling, and path conventions diverge too (pkg/utils/elevate*.go,pkg/windows/). New OS-specific behaviour goes behind build tags, not at runtime. - Admin-only is the default. HECI requires root/Administrator; commands that don't need HECI (
version) skipAMTBaseCmd; commands that prefer HECI but can degrade (amtinfowithout--userCert) setSkipWSMANSetup = trueand checkHECIAvailable. Don't paper over missing privileges with silent fallbacks — fail withutils.IncorrectPermissionsand letcli.Executehandle elevation. - Keep PRs small and scoped to one concern. A focused 50–150-line diff gets reviewed and merged; a 500-line "while I was in there" diff stalls and risks regressions in unrelated activation paths. Unrelated bug / dead-code / lint-nit / formatting drift you notice while working belongs in a separate PR. Do not scope-creep.
- Work in incremental phases — this is an agile team. Plan features as a sequence of small, independently-reviewable PRs rather than one big bang. If a PR grows past the point where a reviewer can hold it in their head (rough rule of thumb: a few hundred meaningful lines, or more than one logical concern), stop and break it into smaller PRs that stack. Each PR should leave
mainin a working state. - Order PRs around the semver release impact. Releases are automated by semantic-release (
.releaserc.json) from conventional commits. Onmain(the v3 prerelease channel):feat:cuts a minor prerelease,fix:/perf:cuts a patch,chore:is configured to cut a patch,BREAKING CHANGE:cuts a major, andrefactor:/docs:/test:/style:/build:/ci:cut no release. When a feature needs prerequisite plumbing (extracted helpers, internal API reshaping, test scaffolding), land those prerequisites first asrefactor:/test:/build:so they ship invisibly; the final user-visible PR is thefeat:that flips the switch and triggers the release. Don't bundle prerequisites into afeat:commit just to save PRs. - Touching
activate? Trace all four modes.runRemoteActivation(wss),runHttpProfileFullflow(https),runLocalProfileFullflow(file),runLocalActivation(--ccm/--acm). TheValidate()method inactivate.gois the gatekeeper — it disallows mode-flag combinations that would silently take the wrong path. Add invariants there before you add behaviour to arun*method. - Before declaring work done, all of these must be green:
go test ./...,gofumpt -l -w -extra ./(no diff),go vet ./..., and the Dockerizedgolangci-lint run(no remaining diagnostics). Runmake mockif you touched a mocked interface and commit the result. Runmake fuzz-regressionif you touchedinternal/cli/parsing.
Conventional Commits, semantic-release-driven. Format:
<type>(<scope>): <subject>
<body>
<footer>
Each line ≤72 characters; commitlint enforces body-max-line-length: 200. The header is mandatory; scope is optional but reviewers usually expect one.
Types (with release impact on main's beta channel):
| Type | Release |
|---|---|
feat |
minor (prerelease) |
fix |
patch |
perf |
patch |
chore |
patch (.releaserc.json rule) |
revert |
reverts a prior commit |
docs / style / refactor / test / build / ci |
no release |
any body containing BREAKING CHANGE: |
major |
Scopes are enforced by .github/commitlint.config.cjs:scope-enum. The valid list is:
lme, lms, lmx, utils, cli, rps, heci, pthi, sample, docker, deps, deps-dev, gh-actions, config, internal, local
Pick the most specific match. Use internal for commands/orchestrator/profile changes that don't fit a more specific scope. Use cli for Kong-level changes in internal/cli/. Multi-area changes are usually a sign the PR should be split (see small PRs).
Footer references the GitHub issue this commit closes, using a closing-keyword — Closes: #1234, Fixes: #1234, or Resolves: #1234 — so the issue auto-closes on merge. Breaking changes start with BREAKING CHANGE: in the footer.
PR practices:
- PR title follows the same header rules. A
semantic.ymlcheck enforces this. - PR author merges. Choose
Rebase and mergeorSquash and mergeto preserve linear history. - Update
config.sample.yaml/config.yamlwhen adding flags. They are the canonical user-facing reference for the YAML surface. - Update
v3.0-changes.mdif you introduce a v3 breaking change relative to v2 that users will hit on upgrade.