Repository navigation
feat: agent remote configuration (overlay, apply modes, safeguards) - #95
ccf-lisa[bot] wants to merge 49 commits into
Conversation
…ctor The G0 declared/runtime refactor must keep agentConfigurationHash byte-identical (it feeds the _agent label fallback and the agent evidence UUID). Record the values from the current loader first so the refactor commit can prove it.
Pin compliance-framework/api to the in-flight-agent-config branch head (52e315b) for pkg/agentconfig and pkg/agentconfig/regocheck. The config now has two forms (R4): agentconfig.Config is the declared form that is decoded, validated and (later) merged, classified and reported; the existing private structs stay the runtime form, built by one toRuntime conversion so agentConfigurationHash stays byte-identical (golden test). - cmd/config.go: loadBase builds a fresh viper per load, decodes with viper's weak decoder exactly as before (R51), decodes policy_bundles outside viper (yaml/json/toml), records env-sourced plugin pointers (R25) and validates with the shared rules. - R34: file-origin problems that only logged before (a bad plugin schedule) become reported warnings and the plugin is skipped; every other validation error stays fatal. - R9: protocol_version 0 means auto; the explicit-0 file check stays. - plugins.*.enabled: disabled plugins are dropped from the runtime. - internal.IsOCI delegates to agentconfig.IsOCISource (R3). - Remove the unused queryBundles field and the OPA v0 rego import; fix the verbosity comment.
…loads (G1) - internal/agentstate: per-config state dir (.compliance-framework/state/<sha256(abs config path)[:16]>, 0700) with a persisted instance ID; --state-dir/CCF_STATE_DIR and --instance-id/CCF_INSTANCE_ID override it (R31). An unwritable dir keeps the ID in memory with one WARN. The resolved dir, its source and the ID are logged at startup (R52). - The heartbeat uses the stable ID instead of a fresh UUID per run. - cmd/reconciler.go: one serialized reconciler builds a complete, prefetched candidate before cancelling the running config (R32). An invalid file or a download failure no longer panics or exits; the running config keeps going. A run that fails on its own after a reload falls back to the previous config. Each load uses a fresh viper; the watcher only signals. - AgentRunner: Run/runDaemon return errors instead of os.Exit; Prefetch downloads without touching the running locations; a per-source protocol cache survives reloads; UpdateConfig reuses the SDK client when the api block is unchanged; reloads let in-flight runs drain for up to 5 minutes (R33). - Plugins no longer inherit CCF_API_AUTH_* (SkipHostEnv plus a filtered host environment, R26). - Drop the no-op global viper.BindPFlag calls; run tests with -race.
G2 and G3 ship together (R35). Reporting (G2): - remote_config mode comes from the file (normalized: apply_safe with auth, off without; CCF_REMOTE_CONFIG_MODE is bound, R30). - cmd/report.go builds the config report: redacted UNRESOLVED base and effective with the same env-sourced masked pointers the digest uses (R24, R25, R55), daemon, attempted/applied revision, status/reason, unsafe changes, R34 warnings, remote-config (snake_case), truncation to 3.5 MiB. Sent at startup, when it changes, after a send error and every 24h. 404/401/403 back off 10 min, 409 pauses reports for 1h, 413 resends truncated (R8, R36). - The heartbeat carries config_revision (0 = file only) and config_digest whenever the mode is not off (R11, R45). - main sets the reported agent version (goreleaser main.version). Pull and apply (G3): - internal/agentstate cache (0600, checksummed, bound to api.url + client_id) keeps the fetched, applied and rejected revisions. The ETag is opaque and sent back verbatim (R7). - prepare: ValidateOverlay (the only strict decode, R27/R51) with FieldError codes mapped to reasons (R43), then the Classify gate (forbidden rejects in every mode, R23), Merge, Validate with the R34 origin partition, ResolveEnv on plugins.*.config only (R24), then Prefetch. Nothing touches the network before Classify passes. - A rejected revision is remembered per (ETag, base fingerprint); a failed one is retried after 1m doubling to 10m. - Startup ladder: fetched, then applied, then file only (R32); one-shot fetches, applies, reports and runs once (R37). - Evidence carries agent-config-revision when an overlay is applied, through a new runner.WithEvidenceProps option (R38).
Re-pin compliance-framework/api to the approved PR #465 head (aa005f7; only PolicyOnlyChange changed, the agent-facing contract did not). internal/inlinepolicy materializes policy_bundles (file or overlay) into write-once dirs under <state>/inline/<bundle>/<tree digest>: - R17 order: extends tree (regular files only, symlinks skipped with a warning), delete, modules, then data merge-patched onto the root data file and written as data.json (a base data.yaml is converted); - R18: root-relative paths, re-checked after Clean; only data.json / data.yaml / data.yml data files (authored: error, vendor: warning); - Check runs per (plugin, policy path), the compile unit plugins use (R21): the same policyeval.NewFromBundlePath prepare path as policy-manager, a walk of every rule reachable from authored rules for policyeval.DeniedBuiltins refs (including with ... as http.send, R19/R20), and the Rego tests with bundle data + policy_data (authored failures reject, vendor failures warn); - GC keeps the active dirs plus the 5 newest per bundle (startup only). prepare runs regocheck first, then materialize + check; errors give rejected/policy-errors with located errors, warnings are reported. DownloadPolicies/runPlugin use the materialized dir for inline:<name> and never download it. Reports list inline bundles with their extends tree and inventory OCI/local policy paths.
- configuration.md: remote_config with R29 defaults, the modes and the
apply_safe classification table, set locally only (R30); enabled;
policy_bundles (extends/delete/modules/data, root-relative paths,
data-file names, no cross-bundle imports); ${env:} in plugins.*.config
only; env-sourced masking; tolerated bad schedules; weak file typing vs
string-only overlays; the viper case/dot caveat; state dir and ID.
- running_as_a_service.md: WorkingDirectory/StateDirectory, persistent
state, CCF_STATE_DIR for containers, CCF_INSTANCE_ID for jobs.
- ADR 0003: declared vs runtime forms, prepare-then-cancel, the agent as
Classify authority, per-path compile unit and transitive denied
builtins (with the residual policy_data risk), plugin env filter,
opaque ETag, the R34 origin rule.
- README: pointer.
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
api #465 merged api main (#464 evaluation artifacts), so the pinned module now carries both pkg/agentconfig and the SDK Artifact client the agent already uses. The tree builds and tests without a go.work.
gusfcarvalho
left a comment
There was a problem hiding this comment.
Findings are inline, reviewed at b9c1cad. build, vet, test -race and gofmt are clean locally, and CI is green. There are 6 blocking threads:
- A denied builtin reachable from authored Rego still executes during the
_test.regorun. This is security-relevant and was probe-confirmed. - File configs that load on
mainare now fatal (R34/R51). - A no-op revision is never recorded as applied.
- A remembered-rejected overlay is re-prepared on every poll.
- SIGTERM is lost during the 5-minute reload drain (R33).
- The agent-run evidence for startup download failures is gone.
The rest are non-blocking suggestions and one owner question. The overall shape (prepare-then-cancel, the agent-side Classify gate, opaque ETags, env filtering and redaction) looks solid.
Check ran the bundle's Rego tests even after the transitive walk had found a denied builtin reachable from authored code, so an authored _test.rego calling a vendor helper that wraps http.send made the agent issue the request before rejecting the revision (D17, HLD §8). Check now returns before the tests when the walk reports anything, and the tests themselves run sandboxed: modules are compiled under policyeval.SandboxCapabilities with every denied builtin ref rewritten to a stub that always errors. Vendor modules that only reference a denied builtin off the authored path still compile; their tests fail as warnings.
The shared Validate made some file values fatal that main accepted:
- verbosity: -1 (hclog Warn, a quieter agent);
- a literal ${env:X} in plugins.*.labels / policy_data (an opaque string);
- an unset ${env:X} in plugins.*.config (main passed the literal through).
File-origin negative verbosity and env-location errors outside
policy_bundles are now warn-only: reported in warnings, value unchanged,
no plugin skip. Per the owner's R60 decision, an unset variable the file
references (per pointer and variable) is a warning and the literal reaches
the plugin unchanged; an unset variable an overlay introduces still fails
with failed/env-missing. Overlay-origin validation stays strict.
Files without policy_bundles no longer go through the YAML->JSON decode,
so YAML that JSON cannot represent (.nan, .inf) loads as on main.
WalkDir does not follow a symlinked root, so a local extends pointing at a symlink (a versioned directory, a ConfigMap mount) materialized without its vendor policies and only warned. readTree now resolves the root with EvalSymlinks (in-tree symlinks are still skipped), and an extends tree with no .rego file fails with ErrResolve (download-failed, retried with backoff) instead of silently dropping every vendor policy.
- A revision (or file edit) whose effective config equals the running one is adopted in place: the running runtime keeps going, its sync metadata (now atomic) gets the new applied revision, and the reconciler's record gets the new base/overlay. Before, it was never recorded active, so every poll re-prepared it and heartbeat/report kept the stale applied-revision. - reconcile returns early when the remaining target is remembered-rejected for the current base (last-known-good), instead of re-preparing it (and re-running inline policy checks) on every poll (§5.4). - The rejected memory and failed backoff key on the ETag, or on revision + sha256(overlay) when a response had no ETag. - run() binds the fallback before notifying onRunFailed; file-only candidates that fail to prepare or to run enter the failed backoff, and a successful prepare of the backed-off target keeps growing the delay, so a bad file edit no longer flaps every poll. - A startup download failure of the file-only config calls AgentRunner.ReportStartupFailure (marks the plugins, sends the startup-failure agent evidence) before exiting 1, as Run did on main. - A SIGINT/SIGTERM during the 5-minute reload drain is no longer lost: it switches to the 30s shutdown path and exits (R33). - Each prepare network step (Prefetch, extends, report inventory) is bounded by prepareNetworkTimeout (5m) and maps to failed/download-failed. - Inline GC also runs after every swap. Cron jobs capture config, client and logger once per setup. Race tests assert the final config and stop rc.run.
gusfcarvalho
left a comment
There was a problem hiding this comment.
Re-review of b9c1cad..0c1a8c7: all 15 threads from my previous review are addressed, and I verified each fix. This supersedes my REQUEST_CHANGES.
- Security:
Checknow returns before the tests when a denied builtin is reachable. The tests also run underSandboxCapabilities, with every denied builtin rewritten to a stub that errors. I re-ran my probe: 0 requests reached a local server across authored→vendor helper,with f as http.send, dynamicdata[p][q]refs,walk(data), a vendor test callinghttp.send, and, with the walk bypassed entirely, direct/http["send"]/withcalls andopa.runtime. - R34/R51/R60: a file with
verbosity: -1, a literal${env:}in labels/policy_data, or an unset${env:X}in config (whole or embedded) loads, and the plugin gets exactly the literal valuesmainpasses. All are reported as warnings and no plugin is skipped. - Reconciler: a no-op revision is adopted in place: one Prefetch, then applied-revision/report/heartbeat = 2, same runtime, clean under
-race. The remembered-rejected early return, the SIGTERM handling during the drain, the restored startup-failure evidence, the empty-ETag key, the notify-after-bind ordering, the file-only backoff, the bounded network phase and GC after each swap all look correct.
build, vet, test -race and gofmt are clean locally, and CI is green. One non-blocking note for the symlink follow-up is inline.
|
PR approved. Marking ready for e2e. |
Round 2 of api#465: artifact-digest on the policy bundle report entries and the policy contract check (policyeval.CheckContract / ValidateResult) wired into regocheck and Execute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The configuration report is about to upload the policy trees it names as artifacts, the same artifacts evidence references for playback. For both to land on the same artifact, and for a tree to be uploaded once, they need one archiver and one uploader. - internal/policytree: ReadTree (resolves a symlinked root, skips inner symlinks, as inline materialization and OPA do) and TarFiles (sorted, fixed headers), TarDirectory = both. A symlinked policy root, which the old tarDirectory refused, now archives like the directory it points to. - runner.ArtifactUploader: the per-digest cache, retries and old-API backoff, now exported, keyed per API and safe to share. Endpoint binds it to one API; NewApiHelper takes it with WithArtifactUploader (and keeps a private one without it). - WithPolicyPaths resolves each path when the helper is created, so the bundle artifact is the tree the run started with even if the path is a symlink swapped afterwards (inline stable paths, R67). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The e2e round showed revisions that compile but would record no evidence
(an override without a title), and a compile error in a vendor test that
did not say the override caused it.
R63, after the compile and the tests pass, on the materialized tree:
- static: policyeval.CheckContract on the vendor-only packages only
(warnings; regocheck already checked the authored modules), and a
warning for an authored package that more than one non-test module
defines (each records evidence for the whole package);
- dynamic: a dry run on {} plus policy_data through policyeval.Execute
and policy-manager's GetRiskTemplates, the calls plugins make, under
SandboxCapabilities with denied builtins rewritten to erroring stubs.
Decode errors and Result.Issues become located PolicyErrors with the
contract codes: errors in packages that contain an authored module,
warnings in vendor-only ones. Conflicts on {} and a missing title whose
title rule depends on the input are warnings. A package that fails to
evaluate is left out of the next attempt so it does not hide others.
R65: a compile error in a vendor file whose package an authored module
also defines carries a hint (keep the rule or delete the test); it stays
an error. Includes the exact e2e repro.
policy-manager gains NewWithEvaluator and a RiskTemplateError that names
the package and file. Test fixtures gain titles: authored packages
without one are now rejected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policy-manager seeds evidence UUIDs with the policy file path, and inline bundles lived in tree-digest directories, so every edit of a bundle gave every package in it a new evidence identity and broke history. Materialized trees stay write-once and content-addressed, now at <name>/<digest>/bundle; Materialized.Path is <name>/current/bundle, the same for every revision. Activate points <name>/current at a tree with a temporary symlink renamed over it. The symlink is an intermediate path component on purpose: OPA's bundle loader does not descend into a symlinked root directory and would silently load nothing. The swap is atomic for path lookups on Linux, but neither a snapshot for a reader walking the tree nor atomic on macOS APFS, so callers swap only when no plugin of the previous configuration runs, and serialize Activate with GC. GC never removes the tree current points to, and cleans up stale temporary links. Without symlink support (Windows without the privilege) Path is the tree itself, as before. A tree directory in the pre-R67 layout is rebuilt rather than reused. readTree is policytree.ReadTree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te packages (R62, R66, R67) R67: plugins receive each inline bundle's stable path; the runtime keeps the materialized trees separately (inlineTrees), and the candidate identity still hashes the trees, so any tree change restarts plugins. The run loop points the stable paths at a candidate's trees in start(), only after the previous configuration's run returned (after the reload drain), and back at the fallback's trees when it falls back; a failure to activate is a run failure. GC and activation share a lock, and GC keeps the trees of the running, pending, starting and fallback candidates as well as whatever current points to. R62: the reconciler and every plugin run share one artifact uploader (remoteAPI.UploadArtifact). At report time, so outside mode off and only for the candidate that runs, it uploads each reported tree (inline bundles, the trees they extend, OCI and local sources) once, memoized by tree digest per API and bounded by the remote request timeout, and fills artifact-digest on the bundle and its extends. Failures leave the field empty and never reject or fail; a tree the API refused for good is not retried, a transient failure is retried with the next report, a local tree changed since its inventory is not uploaded under the old digest. The field survives report truncation. R66: a plugin using an inline bundle that loads a policy package from two of its policy paths gets a warning naming both paths. Policy errors are de-duplicated before they are reported: exact repeats across plugins, and an API-side warning superseded by an agent error for the same bundle, path and code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- run loop: start(c, fallback) sets the starting and fallback candidates together before activating, so GC keeps both trees throughout a swap and a fallback. - writeOnce: a completion marker next to bundle/; a pre-R67 directory whose vendor tree has a top-level bundle/ is rebuilt, not reused. - contract: a dry-run timeout is a warning (vendor packages are evaluated too); an authored test alone does not make a vendor package authored; the duplicate-module warning points at the authored file. - artifacts: a local tree edited since its inventory is not re-read on every poll. - R66 wording; sortedMapKeys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- contract: skip policy-manager's risk-template error only when this package's risk templates were already reported, not on any invalid-type. - docs: an authored non-test module makes a package authored. - Activate doc comment reflowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 2 (design §13: R62, R63, R65, R66, R67)API pin bumped to api#465 round 2 ( R62: one channel for policy sources
R63: policy contract (agent is authoritative)
R65: a compile error in a vendor file whose package an override also defines gets the hint R66: if a plugin uses an inline bundle and the same package appears in two of its policy paths, the agent emits a R67: stable evidence identity
UI wire changes
Behaviour change: an authored package with no Checks: 🤖 Generated with Claude Code |
gusfcarvalho
left a comment
There was a problem hiding this comment.
Round-2 (§13) re-review of 0c1a8c7..f88bde9: approve. build, vet, test -race and gofmt are clean locally, and CI is green. What I verified:
- R67: I confirmed independently that OPA v1.14.1 loads 0 modules from a symlinked root, 1 from the real dir and 1 from
current/bundle, so the<name>/current/bundledeviation is justified.TestActivate_EvidenceIdentityIsStable_R67goes through policy-managerExecute/GenerateResultsand fails on empty results, so it proves that modules load through the stable path and that an unchanged package keeps its UUID. The swap happens only instart()after the previous run returned (and on fallback), underinlineMu. GC runs on the same lock, keeps active, pending, starting and fallback plus thecurrenttarget, and runs on the materializing goroutine. - R62: both upload paths use
policytree.ReadTreeandTarFiles, so the tars are identical. Uploads run at report time and never feed status. The memo resets when the remote client is rebuilt. - R63: the dry run uses sandboxed copies with stubs and
SandboxCapabilities, only after the reachability check and tests. 0 requests reached a probe server across authored→vendor helper,with … as http.send, a vendor test, a vendor violation rule andrisk_templatescallinghttp.send. Severity is an error only for authored non-test packages; timeouts are bounded warnings. The wording-based classification can only fail open (a warning), never reject wrongly, and tests pin the codes at this API pin. - R65/R66: the e2e repro test and the duplicate-package test are both in place.
One non-blocking owner question is inline (bundle data.json in artifacts vs Redact). The ConfigMap follow-up thread stays open, as agreed.
Conflicts in runner/result.go and cmd/agent.go: keep the shared ArtifactUploader, WithArtifactUploader, WithPolicyPaths symlink resolution and WithEvidenceProps alongside main's WithSources. Semantic integration (design 13.4): policy sources are keyed by the exact path plugins receive, including an inline bundle's stable current/bundle path, and an inline bundle records _policy_source = inline:<name> with the stored bundle's artifact digest, or its tree digest when the bundle could not be stored.
A module that declares policy_id writes to a stream that follows the policy, not where its bundle lives: policyeval.SeedPath replaces the policy_file seed and, when the plugin labels carry one, the _policy_path seed. A policy_id equal to the legacy policy file reproduces the legacy UUID, so an override can continue the vendor stream. Without a policy_id the seed is unchanged byte for byte (golden test). Evidence keeps the real _policy_path label and gains _policy_id.
gusfcarvalho
left a comment
There was a problem hiding this comment.
Re-reviewed bce3ad3: M1 (view Ensure errors now only fail that plugin's runs; blast-radius + restart test), M2 (_inline), R88 removal, shared policyeval/regocheck helpers, and the per-(bundle, plugin) policy-stream-forked warning for unshadowed extends bundles all look right. CI green. Note for release: pluginlib.MinPolicyID = v0.9.0 must match the release that ships this PR. Dev stacks need a state wipe (R67 migration removed).
gusfcarvalho
left a comment
There was a problem hiding this comment.
One logic flaw found while bringing the RC stack up on bce3ad3 (pre-existing, not from this round): a remembered rejection isn't re-reported after restart, so the instance shows 'pending'. Small fix inline.
startup skipped a fetched overlay remembered as rejected but set neither the attempted revision nor the outcome, so with a 304 the API showed the instance as pending until a new revision. Rebuild the outcome from the persisted RejectedRecord (now also keeping Unsafe and up to 100 PolicyErrors, optional fields), and keep it as the outcome when the applied overlay is re-prepared.
gusfcarvalho
left a comment
There was a problem hiding this comment.
Re-reviewed e8afe72: remembered rejections are re-reported at startup and kept through 304/fallback re-prepares (unsafe + policy errors persisted, bounded); restart test covers both paths. CI green.
…streams The API dropped authored policy_id (api#465): path shadowing already keeps vendor evidence streams with every plugin build, so a declared identity is no longer needed. - policy-manager: evidence seeding and labels are back to main's (no SeedPath branch, no _policy_id label). The seed golden test moves to evidence_seed_test.go; the policy_id tests are removed. - inlinepolicy: ModuleIdentity has no PolicyID; SeedOf is the plain (plugin path joined with the module path, plugin path) pair, which is what SeedPath returned without an id. OverrideStreams only reports policy-package-changed; policy-stream-forked is the unshadowed-bundle warning alone. Materialized.PolicyIDRules is gone. - cmd: no duplicate-policy-id (duplicate-policy-identity stays), no plugin-lib-policy-id-unsupported; the plugin library check is the set-form violation gate (MinViolationSet, v0.7.1) alone. - pluginlib: MinPolicyID is removed. - Docs and AGENTS.md no longer describe policy_id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tity checks Inline policy bundles (policy_bundles in the config file or overlay, inline:<name> policy entries) are removed, with everything that only served them: - cmd: inline bundle materialization and activation, path shadowing and plugin views, the duplicate-policy-identity check, the policy_bundles decoding of the config file (and its TOML/JSON path), and the set-form violation gate, which only judged inline bundles. Plugins run in the agent's working directory again. - internal/inlinepolicy and internal/policyview are deleted. - runner: WithPolicyRoot, the symlink resolution of policy paths and Source.BundleArtifact are removed; evidence records the policy source as on main. The shared artifact uploader and the _policy_path label fallback stay. - policy-manager: NewWithEvaluator and RiskTemplateError are removed; policy-manager.go equals main. The evidence seed golden test stays. The report keeps its policy-bundles inventory of the OCI and local sources each instance loads, and plugins[] with each plugin's lib-version. The inventory moves to policytree.Inventory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing produces policy errors any more, so the plumbing goes: applyError, the candidate's policy warnings, the report's policy-errors and the rejected record's policy_errors in the remote config cache. pluginlib keeps only the build-info version reader the plugins report uses; MinViolationSet and AtLeast (and golang.org/x/mod) are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API drops inline policy bundles, policy errors and the policy contract check from pkg/agentconfig and pkg/policyeval. An overlay that still sets policy_bundles is now rejected as unknown-field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting
Remove the inline policy bundle, path shadowing, policy identity, plugin
compatibility and policy error content from the configuration guide, ADR 0003,
policy_artifacts.md, AGENTS.md, the README and the service guide. Document what
stays: the report's policy-bundles inventory with artifact digests ("Sources for
the UI"), plugins[].lib-version as diagnostics, and the _policy_path label
fallback for _policy_source. ADR 0003 records that inline bundles are out of
scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The config report no longer inventories the policy sources (policy-bundles[] with tree digests and file lists), and the reconciler no longer uploads policy trees as artifacts at report time. The API removed the matching report fields and the artifact file routes. The evaluation-time policy bundle upload (evidence playback) is restored to main: each plugin run's API helper has its own uploader again, and the shared process-wide ArtifactUploader/ArtifactEndpoint and internal/policytree, which only existed to share code with the report-time upload, are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up compliance-framework/api#465 at b53be8f: agentconfig redaction now also masks by value (URL passwords, private keys, password= assignments, known token formats) and masks literal text mixed with ${env:...} under secret-like keys, and remote_config.mode defaults to report. The report and heartbeat digests come from agentconfig.Redact/Digest, so they follow the API. The report tests now pin that a dsn mixing literal text and placeholders is masked and that a password in a URL is masked by value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With api.auth credentials and no remote_config.mode, the API's agentconfig.RemoteConfig.Normalize now defaults the mode to report instead of apply_safe: the agent reports its configuration but never fetches or applies an overlay (not even a cached applied one) until the host owner sets apply_safe or apply_all. The agent delegates the default to the API package; this pins it with a test (with and without a remote_config block, after an overlay was applied under apply_safe) and updates the README, configuration docs and ADR 0003. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the report-time policy-bundles inventory, tree digests and policy tree uploads from the configuration docs and ADR 0003, and restore the evaluation-time upload description in policy_artifacts.md to main. Report truncation now only drops base. Point the redaction description at the API's pkg/agentconfig as the source of truth and summarize it: secret-like keys, secret-looking values, and literal text mixed with placeholders under secret-like keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing in favour of a stack of smaller PRs (stack #112). The top of the stack is byte-identical to this branch merged with
Review threads on this PR stay here for reference. The branch 🤖 Generated with Claude Code |
Summary
The agent side of agent remote configuration:
remote_config.mode(off,report,apply_safe,apply_all), behind the sharedClassifysafeguards. With credentials and no mode set, the mode isreport: applying is opt-in;An overlay can change
plugins.<p>.config,policy_data,labels,schedule,enabled,source,policies(OCI or local sources),policy_behavior,agent_evidenceandverbosity. Inline policy bundles are out of scope: an overlay that setspolicy_bundlesis rejected withunknown-field.Dependency: this pins compliance-framework/api#465 (head
b53be8f), then gets re-pinned to the API release tag. Ship order: api#465, then this PR, then compliance-framework/ui#318.What's in it
Config, state and reloads
api/pkg/agentconfig.Configis the declared form, and the existing structs remain the runtime form. The config hash is byte-identical, pinned by golden tests.--state-dir/CCF_STATE_DIRand--instance-id/CCF_INSTANCE_ID.Remote config (
cmd/reconciler.go,cmd/report.go)prepare:ValidateOverlay;Classify/WillApplymode gate (forbidden keys are rejected in every mode);Merge;Validate;${env:}resolution inplugins.*.config;rejected, with the reason and unsafe list. Failed applies back off from 1 to 10 minutes.plugins[]withlib-version, read from each plugin binary's Go build info (diagnostics only, nothing is gated on it);pkg/agentconfig(Redact,Digest): secret-like keys and secret-looking values (URL passwords, PEM private keys,password=assignments, known token formats), plus literal text mixed with${env:}under secret-like keys. The agent does not re-implement it;base.config_revision/config_digest.agent-config-revision.Evidence (
runner)main(runner/policy_artifacts.gois byte-identical tomain). There is no report-time policy bundle inventory and no configuration-time artifact upload._policy_sourcefor old plugins: plugins that send no policy evaluations still get_policy_source/_policy_digest, keyed from the evidence's_policy_pathlabel.Plugin environment
CCF_API_AUTH_*.policy-manager(used by plugins built on this lib)main. Evidence seeding is pinned by the newevidence_seed_test.go.Docs
docs/configuration.md: remote config (default modereport), the classification table, redaction, plugin library versions, and the state directory.docs/adr/0003-remote-config-overlay.md.docs/policy_artifacts.md,docs/running_as_a_service.md,AGENTS.md, README.Notable decisions
remote_config.modewith credentials isreport, notapply_safe.policy-bundles[]and the API dropped the artifact file routes.schedulein the file skips that plugin, and a negativeverbosityor a literal${env:}outsideplugins.*.configis only a warning (R34). An unset variable that the file references is passed through unchanged (R60).Testing
GOWORK=offagainst apib53be8f, all of these are clean:go build,go vet,make test(race and coverage), gofmt,make check-opa-version,make build, aGOOS=windowsbuild, and golangci-lint v2.11.4 on the diff frommain.dsnmixing literal text and placeholders, a password in a URL), truncation, and pluginlib-versions.remote_configblock) run inreport: they reportnot-applicable, never fetch, and don't apply a cached applied overlay.Release notes
api.authcredentials and noremote_config.modenow run inreportmode: they report their configuration but no longer fetch or apply remote configuration (including a cached overlay) unlessremote_config.mode(orCCF_REMOTE_CONFIG_MODE) is set toapply_safeorapply_all.pkg/agentconfigfor the reported digest to match.policy-bundlesfield is gone (the API ignores it if an older agent build sends it).Known limitations
PolicyEvaluation, so their evidence has no input or policy artifacts and can't be played back.cache-corrupt.🤖 Generated with Claude Code