Skip to content

feat: agent remote configuration (overlay, apply modes, safeguards) - #465

Open
ccf-lisa[bot] wants to merge 36 commits into
mainfrom
lisa/in-flight-agent-config
Open

ccf-lisa[bot] wants to merge 36 commits into
mainfrom
lisa/in-flight-agent-config

Conversation

@ccf-lisa

@ccf-lisa ccf-lisa Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The API side of agent remote configuration. Each agent gets an API-stored overlay over its file config (RFC 7396 JSON Merge Patch). Each instance applies the overlay according to its own remote_config.mode and reports back what it runs.

Scope: config overlay plus safeguards. An overlay can set verbosity, agent_evidence and plugins.<p>.{enabled, protocol_version, schedule, source, policies, config, labels, policy_data, policy_behavior}. Plugin policies entries are vendor OCI sources or local paths; authoring policy modules or bundles through the overlay is out of scope (policy_bundles is rejected as an unknown field).

Ship order: this PR first, then compliance-framework/agent#95, then compliance-framework/ui#318. The agent pins this branch's pseudo-version.

What's in it

pkg/agentconfig: the shared config contract (also imported by the agent)

  • Model and merge:
    • the declared Config types;
    • Merge / MergePatch / StripLocked, using RFC 7396 merge patch;
    • the locked keys api, daemon and remote_config can never be overlaid;
    • DiffJSON, and RFC 6901 pointers.
  • Validation:
    • ValidateOverlay strictly checks an overlay alone, with rules O1–O10 and exact pointers and codes;
    • Validate / ValidateEditable check a merged config;
    • 5-field cron schedules.
  • Safety:
    • RemoteConfig.Normalize: an agent with credentials and no remote_config.mode runs in report (it reports but never applies); without credentials the mode is forced to off. Remote apply is opt-in via apply_safe or apply_all;
    • Classify / WillApply decide what each mode (report, apply_safe, apply_all) applies, using trusted_sources, overridable_config_flags and allow_local_sources. Re-enabling a plugin disabled in the base is unsafe unless its source is trusted (reenables-plugin); policy_data, policy_behavior, labels, schedule and removing a plugin are safe by design.
  • Integrity:
    • Redact / WithMaskedPointers mask values as exactly •••• (always the whole value, never part of it), under plugins.*.config and plugins.*.policy_data, plus api.url when it holds a secret. Digest and the server-side RedactDocument apply the same rules:
      • by key name, split into words at separators and camelCase: the stems password, passwd, passphrase, secret, token, credential, apikey, privatekey, accesskey, connectionstring/connstring/connstr, sessionid, authorization and authheader match anywhere. key(s), pass, pwd, auth, dsn and cookie(s) match as the last word or its suffix (sshkey, dbpass, basicauth), ignoring trailing format words (_pem, _b64, _hex, …). Ordinary words such as monkey and bypass are excluded. So keyword, key_id, pass_rate and auth_method are not masked, while tokens_per_minute is (a deliberate false positive);
      • by content, whatever the key: URLs with a password in their userinfo (also inside DSNs and longer strings), PEM private keys, password=… assignments, and high-confidence token formats adapted from gitleaks' MIT rules (AWS key IDs, GitHub, GitLab, Slack, Google API keys, Stripe, JWTs, SendGrid, npm, PyPI, OpenAI, Anthropic, Hugging Face, DigitalOcean, Shopify, Terraform Cloud, Vault, Azure AD client secrets, age). No entropy heuristics;
      • ${env:…} placeholders: a value is kept verbatim only if nothing but whitespace and :;,|/@=& is left once its placeholders are removed. Literal text mixed with a placeholder is masked under a secret-like key, and otherwise checked by content. Booleans are never masked by key;
    • Digest;
    • opaque agent ETags, and admin If-Match parsing;
    • ${env:NAME} references, resolved by the agent only in plugins.*.config.
  • Wire types:
    • OverlayDocument;
    • Report, carrying plugins[] with lib-version;
    • the status and reason vocabularies.

Storage, authz and pruning

  • Tables:
    • ccf_agent_config_revisions: append-only, unique (agent_id, revision), revert creates a new revision; deleting an agent deletes its revisions;
    • ccf_agent_instances: one row per reporting instance, capped per agent (non-prunable rows); when full, the oldest stale instance is replaced, and a 409 only happens when every counted instance is fresh.
  • Authz:
    • new actions agent:configure (overlay writes) and agent:sync (the agent fetching its overlay and reporting);
    • with the builtin driver, only admins can use the agent resource;
    • GET /admin/agents[/:id] now needs agent:read; writes and keys still need admin:manage.
  • Pruning: an hourly agent_instance_prune job, driven by the CCF_AGENT_* settings.

Routes

  • Agent:
    • GET /api/agent/config: opaque ETag, 304, X-CCF-Remote-Config: 1;
    • PUT /api/agent/instances/:instanceId/config-report: validated, size-capped and re-redacted (base, effective, and free text: error, warnings, plugin sources, remote-config), NUL rejected, 409 at the instance cap;
    • the heartbeat now carries config_revision / config_digest.
  • Admin:
    • GET|PUT /admin/agents/:id/config: If-Match required (428 without it, 409 when stale);
    • POST …/config/preview (needs agent:configure): a dry run, with overlay errors and a per-instance diff, change classification and will-apply result; an invalid overlay returns only its errors;
    • GET …/config/revisions[/:rev] and POST …/config/revisions/:rev/revert;
    • GET …/instances[/:instanceId].
    • PUT, preview and revert bodies are capped at 1 MiB (4 × MaxOverlayBytes), so an oversized overlay gets a 422 with the size error rather than a 413.
  • SDK: client.AgentConfig.Get/Report, with the remote-config errors and header plumbing.
  • Swagger: regenerated for the new routes only.

Notable decisions

  • Overlays are redacted for readers. GET …/config and GET …/config/revisions/:rev return the overlay verbatim only to callers with agent:configure (editors); everyone else gets it masked (supersedes R57). Secrets still belong in ${env:NAME} placeholders.
  • Masking is conservative. A masked base value hides its ${env:…} references from the API, so a preview against an instance whose base has a mixed value such as postgres://u:${env:PG_PASS}@h under dsn reports a reused reference as new-env-reference (unsafe). The agent classifies against its unredacted base, so what it applies is unaffected.
  • Removed during review:
    • inline policy bundles (authoring, overriding and removing policy modules through the overlay), with everything that only served them: the Rego checks, the static and evaluated policy contract check, policy-error reporting, and the narrower agent:configure-policy permission and policy-author role;
    • the automatic continuity design (R82), the optional authored policy ID (R74) and bundle age (bundles-first-seen);
    • after the security review: the artifact file routes (GET /api/artifacts/{digest}/files[/{path}]) and the report's policy-bundle inventory (policy-bundles, BundleTreeDigest, the policy_bundles instance column). Nothing read them. Reports that still send policy-bundles are accepted and the field is ignored. Evaluation-time bundle uploads and GET /api/artifacts/{digest} are unchanged from main.
  • Dependency pin: github.com/moby/go-archive is pinned to v0.1.0, because the go-containerregistry bump otherwise breaks testcontainers builds.

Testing

  • go build, go vet (with and without the integration tag), make test and golangci-lint are clean. make swag leaves no diff, and go mod tidy is a no-op.
  • Integration suites run on Postgres testcontainers (go test -tags integration -p 1 ./...): TestAgentCfgServiceIntegration, TestAgentConfigSyncAPI, TestAgentConfigAdminAPI (every admin route, plus builtin and Cedar authz matrices), the prune worker.
  • Redaction has table tests for key names (including negatives), URL credentials, PEM keys, each token format, placeholder rules, idempotency, RedactDocument parity and digest stability.

Release notes

  • Remote apply is now opt-in. Agents being upgraded no longer apply remote config unless remote_config.mode is set to apply_safe or apply_all. With credentials and no mode set, an agent only reports (report).
  • Builtin authz: agent service accounts may only register, ingest and sync on the agent resource (parity with Cedar).
  • Digests change for configs whose keys end in name, url, file, id and similar (no longer masked by key name; values are still content-checked), and for sources/policy entries holding credentials (now masked).
  • Config reports and previews mask more values: secrets are now detected by content as well as by key name, and literal text mixed with an ${env:…} placeholder under a secret-like key is masked.

Follow-ups

  • Purging one revision: deleting an agent removes its revisions; redacting a single revision in place is not implemented yet.

🤖 Generated with Claude Code

ccf-lisa Bot added 2 commits September 30, 2026 11:53
Implements the API side of agent remote configuration (LLD WPs A1-A4, design §12 R1-R56):

- A1 pkg/agentconfig (+ regocheck): declared config types, RFC 7396 merge with
  locked keys, strict overlay validation with FieldError codes, change-safety
  classification, redaction/digests, opaque agent ETags, bundle checks,
  PolicyOnlyChange and the agent<->API wire types. Adds go-containerregistry v0.21.2.
- A2 revisions + instances models/migrations, agentcfg service, instance cap and
  River prune job, authz vocabulary (configure, configure-policy, sync; policy-author
  role), builtin PDP gating of the agent resource, PEP AuthorizeAny, and per-route
  guards on /admin/agents (reads on agent:read).
- A3 agent routes GET /agent/config and PUT /agent/instances/:id/config-report,
  heartbeat config_revision/config_digest, SDK AgentConfig client.
- A4 admin config routes (get/put/preview/revisions/revert/instances), CORS for
  If-Match/If-None-Match/ETag, swagger.
- document the builtin agent-resource gate and the policy-author role
- preview derives its validation set from PreviewBases (one instance scan)
- single agentcfg.SettingsFromConfig mapping
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 08cd0854-8865-4ca2-bf7c-ab52648e6e1a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

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

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at 52e315b. I found no blocking issues. The four inline threads are non-blocking, and two of them ask for an owner decision.

What I checked:

  • Against design §12 (R1–R56) and the API LLD:
    • locked keys: stripped in Merge and rejected by ValidateOverlay;
    • classification: Classify/WillApply, including R23 and R24;
    • builtin PDP gate on agent (R39);
    • agent routes: agent JWT only, with cross-agent isolation from the JWT;
    • D18 and the R22 swap;
    • opaque ETag and 304 (R7);
    • If-Match handling: 428, 409 and 200 for no-op saves;
    • the 422 body shape (R6), and R48 validated;
    • redaction and the digest stored as sent (R55);
    • the instance cap (R37), heartbeat R11/R45, and pagination (R49).
  • Local runs: go build, go vet, go test ./... and go mod verify are clean. -tags integration passes for handler, agentcfg, authz and worker on testcontainers Postgres.
  • gofmt: it only flags internal/authz/cedar_test.go, which is already on main and not touched here.

GitHub Actions has not run on this PR (only CodeRabbit, which skipped it). The bot-authored run probably needs approval before merge.

Comment thread docs/authz-oss-cedar.md Outdated
Comment thread pkg/agentconfig/policyonly.go Outdated
Comment thread internal/api/handler/agent_config.go Outdated
Comment thread internal/api/handler/agent_config.go Outdated

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: R58 (extends bypass) and R59 (file-origin errors block saves); see threads

Comment thread pkg/agentconfig/policyonly.go Outdated
Comment thread internal/api/handler/agent_config.go
@ccf-lisa

ccf-lisa Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Review pass (aa005f7) addressed R58, R59, R57 and the D18 audit. API change for the UI LLD (R59): preview instances[].warnings (always []) and 422 errors.instances[].warnings now carry validation errors that already exist in the instance's own file (Merge(base, {})). They are non-blocking. instances[].errors now holds only errors the overlay introduces, and only those force invalid-config. R58: a configure-policy-only save gets 403 when it adds or changes policy_bundles.*.extends to a source the validation bases don't already use, unless the change is the R22 swap at the same index. The UI's client-side policy-only check should mirror this rule.

@ccf-lisa
ccf-lisa Bot requested a review from gusfcarvalho September 30, 2026 15:24

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed 52e315b..aa005f7. R58 and R59 are implemented correctly, so this approval supersedes my CHANGES_REQUESTED review.

  • R58: extendsChangeAllowed runs for every base, including diffs that touch only /policy_bundles. A new or changed extends must be in usedSources(base) or be the source replaced by an accepted R22 swap at the same index; removing or keeping extends is still allowed, and with no bases only the swap can add one. The unit tests cover each of these cases, and the Cedar policy-author tests check that an unknown extends gets 403 and an already-used source gets 201.
  • R59: splitIntroduced compares errors against Merge(base, {}) on (Path, Code, Message). Only errors the overlay introduces block a save or force invalid-config; file-origin errors are returned as warnings, never null. The integration test confirms a base with a bad cron still accepts {"verbosity":1} with 201, and preview shows the warning with will-apply: true.
  • R57 docs and swagger, the D18 denial audit record, and the policy-author revert test all look good.
  • Local verification: build, vet and unit tests are green. With -tags integration, the handler, agentcfg, authz and worker suites pass. gofmt flags only internal/authz/cedar_test.go, which is already like that on main.

GitHub Actions still hasn't run on this bot-authored PR, so the workflow needs approval before merge.

@ccf-lisa

ccf-lisa Bot commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

PR approved. Marking ready for e2e.

@ccf-lisa
ccf-lisa Bot requested a review from gusfcarvalho September 30, 2026 15:38

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-approving at 9a512a6, the merge of main (#464, evaluation artifacts). The conflict resolutions are correct, and CI is green.

  • Conflicts (.env.example, cmd/root.go, internal/config/config.go, api.go imports): both sides were kept.
  • sdk/client.go: both clients are kept, with our stricter header loop. It still drops a caller's Authorization and canonicalizes via Del/Add; Artifact.Upload's Content-Type override still works.
  • Swagger: regenerated. It adds exactly #464's artifact paths and definitions.
  • Non-conflicted files from main are byte-identical to 181097e.
  • Semantics:
    • the manifest adds artifact alongside policy-author and agent sync;
    • the builtin PDP's agent-resource special case doesn't touch artifact;
    • migrator Up/Down ordering is consistent;
    • the new Cedar cases check that policy-author can read artifacts but not ingest them.
  • Local: build, vet and unit tests are green. With -tags integration, handler, agentcfg, authz and worker pass.
  • CI: check-diff, lint, unit-tests and integration-tests all pass.

ccf-lisa Bot and others added 7 commits October 1, 2026 06:52
The policy contract checker (R63, pkg/policyeval) must apply the same
limits, hint vocabulary and template label-key rule the risk template
service enforces on save. The service lives under internal/ next to gorm,
which the agent must not pull in through pkg/policyeval, so the pure rules
move to a dependency-free package and the service uses them from there.
A parity test pins the hint rule to the service's own check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pkg/policyeval/contract.go checks what a compliance_framework package
must produce for the agent to record evidence and submit risk templates.

- CheckContract(modules) []Issue is the static layer on parsed modules:
  missing title, contract keys defined as functions or with contains,
  literal type mismatches (text keys, labels, risk_templates), violation
  as an object rule or a complete non-collection, literal violations that
  are not objects or have non-string fields, and risk templates the API
  would reject (rules shared through pkg/risktemplate). Warnings cover an
  empty or conditional-only title, no violation rule, violations without
  an id, violation_ids no literal violation produces, and packages
  defined by more than one non-test module.
- ValidateResult(Result) []Issue is the dynamic layer. Execute stores its
  issues on the new Result.Issues field; its error semantics are
  unchanged.
- regocheck runs the static layer on authored overlay modules. Type and
  shape problems are errors that block a save (R54); a missing title is
  a warning when the bundle extends a source or patches a bundle an
  instance's file defines (WithPartialBundles). PolicyError gains an
  optional code.
- Playback returns the static issues at the top level and the dynamic
  ones per result; neither fails the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI's Override must pre-fill the vendor Rego, and the API never sees
vendor sources. The #464 artifact store is the single channel: agents
upload each policy tree they load and name it in their config report.

- agentconfig.PolicyBundleReport and PolicyExtendsReport gain
  artifact-digest (omitempty). PUT config-report checks its format only
  (artifact.ValidDigest), not that the artifact exists. Agents keep it
  when they drop files to fit the report size.
- GET /api/artifacts/{digest}/files lists a policy bundle artifact:
  {digest, treeDigest, files[{path, sha256, size, package?}]}, where
  treeDigest is agentconfig.BundleTreeDigest over the files (the digest
  config reports use) and package comes from the OPA AST (v1, then v0).
- GET /api/artifacts/{digest}/files/{path} returns
  {path, package?, sha256, source} for one file, up to 1 MiB of UTF-8
  text (422 otherwise).
- Both sit on the artifact read group (same middleware and artifact:read
  guard): 400 malformed digest, 404 unknown digest or path, 415 for an
  artifact that is not a policy bundle. Responses are immutable: the ETag
  is the digest of the body, If-None-Match gets 304.
- internal/artifact gains WalkBundleTar/ReadBundleFiles next to
  ReadBundleTar, and ModulePackage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scope (R72)

- docs/artifacts.md: the policy bundle file routes and their shapes,
  config reports naming artifacts (artifact-digest), and who can read
  artifacts: every role holds artifact:read, including ssp-subscriber,
  so inline Rego uploaded as artifacts is readable beyond agent:read.
  Documented, not narrowed (owner decision pending).
- The same note in the manifest roles comment (next to R40/R57) and in
  docs/authz-oss-cedar.md.
- pkg/agentconfig/digest.go: replace the stale evidence-v3 G1.1
  reference (superseded by #464) with how the tree digest relates to the
  artifact file listing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- contract: match violation_ids like the API (trimmed, case-insensitive)
- contract: drop an unused var and satisfy staticcheck
- regocheck: word the missing-title downgrade for merged bundles too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- regocheck: a bundle with a module that does not parse is incomplete,
  so a title that module may hold is not also reported missing as an error
- contract: one violation-missing-id warning with a count, not one per
  violation numbered in map order
- docs: only agents that report artifact digests upload at apply time

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs: evidence props carry bundle digests too; rewrap a paragraph

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ccf-lisa

ccf-lisa Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Round 2: e2e feedback (design §13: R62, R63, R72)

New commits on top of 9a512a6 (main has not moved, so no merge was needed):

Commit What
bc4f6db refactor: share risk template rules in pkg/risktemplate
4b799b7 feat: policy contract checker (R63)
29d926e feat: policy bundle file routes and report artifact digests (R62)
e8d1883 docs: artifact file routes, report artifact digests and artifact:read scope (R72)
491f415, b6609f7, 6c801a3 self-review passes 1 to 3

R62: vendor sources through the artifact store

  • agentconfig.PolicyBundleReport and PolicyExtendsReport gain artifact-digest (omitempty). PUT /api/agent/instances/{id}/config-report checks only its format (artifact.ValidDigest) and returns 400 if it is malformed. It does not check that the artifact exists. Agents keep the field when they drop files to fit the report.
  • GET /api/artifacts/{digest}/files returns {digest, treeDigest, files[{path, sha256, size, package?}]}.
    • treeDigest is agentconfig.BundleTreeDigest over the files, so a config report's digest for the same tree matches it.
    • sha256 is plain hex, the same as PolicyFileReport.
    • package comes from the OPA AST: Rego v1 is tried first, then v0.
  • GET /api/artifacts/{digest}/files/{path} returns {path, package?, sha256, source}. Files over 1 MiB, or that are not UTF-8, get 422. A / in the path may be escaped as %2F.
  • Both routes are on the existing artifact read group (same middleware and artifact:read guard). They return:
    • 400 for a malformed digest;
    • 404 for an unknown digest or path;
    • 415 for a non-bundle media type.
  • Responses are immutable: Cache-Control: private, max-age=31536000, immutable. The ETag is the digest of the response body, and If-None-Match gets 304.
  • internal/artifact gains WalkBundleTar / ReadBundleFiles (next to ReadBundleTar) and ModulePackage.

R63: policy contract checker (pkg/policyeval/contract.go)

  • CheckContract(map[string]*ast.Module) []Issue is the static layer.
    • Errors:
      • a missing title;
      • a contract key defined as a function or with contains;
      • a literal type mismatch in title, description, remarks, skip_reason, labels or risk_templates;
      • violation[k] := v, or a complete violation that is not a collection;
      • a literal violation that is not an object, or that has a non-string id, title, description or remarks;
      • a risk template the API would reject: required name, title and statement, hint enums, threat refs, dedupe_label_keys ⊆ label_schema, template label keys, duplicate names.
    • Warnings:
      • an empty or conditional-only title;
      • no violation rule;
      • a violation without an id;
      • violation_ids that no literal violation produces (matched trimmed and case-insensitively, like the API);
      • more than one non-test module per package.
  • ValidateResult(Result) []Issue is the dynamic layer. Execute stores its result in the new Result.Issues field, and its error semantics are unchanged.
  • Issue is {file, row, col, package, severity, code, message}.
  • regocheck runs the static layer on the authored overlay modules.
    • Type and shape problems are errors and block the save (R54).
    • A missing title is a warning, not an error, when vendor or file modules could complete the package. That covers a bundle that extends a source, a patch of a bundle that a validation base's file defines (regocheck.WithPartialBundles), or a bundle with a module that does not parse.
    • PolicyError gains code (omitempty).
  • Playback returns the static issues as top-level issues and the dynamic ones as results[].issues. Neither fails the request.
  • The risk template service's limits, hint vocabulary and template label-key rule moved to the dependency-free pkg/risktemplate, which the service now uses. A parity test pins the hint rule to the service's own check.

R72: docs

  • docs/artifacts.md covers:
    • the new routes;
    • config reports that reference artifacts;
    • who can read artifacts: artifact:read is held by every role, including ssp-subscriber, so inline Rego is readable beyond agent:read. This is documented, not narrowed.
  • The same note is in the manifest roles comment next to R40/R57 and in docs/authz-oss-cedar.md.
  • The stale evidence-v3 G1.1 comment in pkg/agentconfig/digest.go is fixed.

Deviations (also in the lisa-design git note)

  • The listing field is treeDigest (design §13 and the artifact API's camelCase), not tree-digest.
  • No violation rule is a warning everywhere, as in the R63 table. Only a missing title becomes an error, and only for self-contained authored bundles.
  • risk_templates as a set, or defined with contains, is an error (R63), even though OPA would return it as an array.

Verification

  • go build, go vet (with and without -tags integration) and go test ./... pass.
  • go test -tags integration ./internal/api/handler/... ./pkg/... ./internal/artifact/... passes.
  • golangci-lint reports 0 issues; make swag and go mod tidy leave no diff.

For agent#95 and ui#318

regocheck.ValidatePolicyBundles now includes the contract layer. Agents that bump the pin get the static R63 check in prepareInline without calling CheckContract again on the same modules.

@ccf-lisa
ccf-lisa Bot requested a review from gusfcarvalho October 1, 2026 10:19

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving the §13 round, 9a512a6..6c801a3. I found nothing blocking.

  • File routes (R62):
    • {path} is only compared, by exact equality, against the stored canonical tar's entry names; nothing touches the filesystem. So .. and %2F can't escape, and an unescaped path that doesn't match gets 404.
    • The routes use the existing artifact:read guard, and the digest is format-checked.
    • A single file over 1 MiB, or one that isn't valid UTF-8, gets 422. Listings carry no file contents, and the whole artifact is already capped by CCF_ARTIFACT_MAX_BYTES.
    • ETags are immutable. The broader artifact:read scope is documented as R72.
  • Contract checker (R63):
    • It mirrors Execute's package selection (compliance_framework.*, non-test modules).
    • Computed values are left to the dynamic check, and it handles v0 violation[x] {} as well as v1 contains/[x] if.
    • I ran a v0/v1 vendor-style probe: sprintf/concat values, comprehension-built ids, a default+conditional title, and a ccf_libs helper. It produced no false issues.
    • Severities follow §13. missing-title is downgraded to a warning for extends bundles, patched file bundles, and bundles with a module that doesn't parse.
    • ValidateResult only fills Result.Issues, so Execute's errors are unchanged.
  • pkg/risktemplate: a behaviour-preserving extraction. The limits, hint rule and template label-key walk are the same, and a parity test pins the hint rule.
  • Recorded deviations, all fine:
    • (a) treeDigest is the name R62 itself specifies;
    • (b) and (c) match the R63 table;
    • (d) is conservative, because standalone mode can't know which bundles the agent's file defines;
    • (e) no SDK consumer needs the file routes.
  • Verification:
    • Locally with GOWORK=off: build, vet and unit tests are green. With -tags integration, internal/api/handler/..., pkg/... and internal/artifact/... pass.
    • CI on 6c801a3: check-diff, lint, unit-tests and integration-tests all pass.

ccf-lisa Bot added 5 commits October 1, 2026 11:07
policyeval: Policy.ID from a module's policy_id, SeedPath for the agent's
evidence seeds, ValidPolicyID; CheckContract checks policy_id (R75:
invalid-policy-id, duplicate-policy-id) and ValidateResult reports an
invalid evaluated one. Playback results carry policyId.

agentconfig: PolicyBundleReport.PluginPath (R77) and Report.Plugins with
lib-version (R76), stored on the instance (new plugins column) and returned
in instance summaries and details; PolicyCode* constants for the agent's
new codes. Docs: policy-identity.md, playback.md, artifacts.md.
@ccf-lisa

ccf-lisa Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

This PR is now split into a GitHub stack (stack #484) of 15 PRs, each at most ~1000 changed lines (without docs/go.sum). The top layer's tree is identical to this branch at 6b970f0, including the fixes for Ian's findings.

  1. feat(agentconfig): config model and RFC 7396 overlay merge (1/15) #469 agentconfig: config model and overlay merge (852)
  2. feat(agentconfig): JSON diff, env references, plugin sources and ETags (2/15) #470 agentconfig: diff, env refs, sources, ETags (862)
  3. feat(agentconfig): change-safety classification and wire types (3/15) #471 agentconfig: change classification (934)
  4. feat(agentconfig): secret detection, redaction and digests (4/15) #472 agentconfig: secret detection and redaction (1100, size-exception)
  5. feat(agentconfig): overlay validation (5/15) #473 agentconfig: overlay validation (921)
  6. feat(sdk): agent config client (6/15) #474 sdk: agent config client (557)
  7. feat(agentcfg): append-only agent config revision store (7/15) #475 agentcfg: revision store (772)
  8. feat(agentcfg): agent instance store (8/15) #476 agentcfg: instance store (986)
  9. feat(agentcfg): validation bases, instance pruning and agent deletion (9/15) #477 agentcfg: validation bases, pruning, agent deletion (649)
  10. feat(authz): agent configure/sync actions and per-route agent guards (10/15) #478 authz: configure/sync actions, agent route guards (279)
  11. feat(api): agent-facing config overlay route and heartbeat instance registration (11/15) #479 api: agent GET config + heartbeat registration (490)
  12. feat(api): agent instance config reports (12/15) #480 api: agent config reports (1089, size-exception)
  13. feat(api): admin agent config read and save (13/15) #481 api: admin config read/save (1180, size-exception)
  14. feat(api): admin agent config preview, revision history and revert (14/15) #482 api: admin preview, revisions, revert (655)
  15. feat(api): admin agent instances, CORS and authz coverage (15/15) #483 api: admin instances, CORS, authz coverage (468)

I'm leaving this PR open for its review history; close it once the stack is reviewed.

gusfcarvalho pushed a commit that referenced this pull request Oct 5, 2026
Fifteenth and last layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/instances lists an agent's reporting instances with derived status, sync status and per-status counts, and GET …/instances/{instanceId} returns one with its redacted base/effective config; CORS allows If-Match/If-None-Match and exposes ETag for the UI; the server recovers handler panics as 500s; builtin/Cedar authz matrices over every admin agent-config route, and the Cedar docs. The tree now matches #465.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccf-lisa Bot added a commit that referenced this pull request Oct 6, 2026
Fifteenth and last layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/instances lists an agent's reporting instances with derived status, sync status and per-status counts, and GET …/instances/{instanceId} returns one with its redacted base/effective config; CORS allows If-Match/If-None-Match and exposes ETag for the UI; the server recovers handler panics as 500s; builtin/Cedar authz matrices over every admin agent-config route, and the Cedar docs. The tree now matches #465.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccf-lisa Bot added a commit that referenced this pull request Oct 6, 2026
Fifteenth and last layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/instances lists an agent's reporting instances with derived status, sync status and per-status counts, and GET …/instances/{instanceId} returns one with its redacted base/effective config; CORS allows If-Match/If-None-Match and exposes ETag for the UI; the server recovers handler panics as 500s; builtin/Cedar authz matrices over every admin agent-config route, and the Cedar docs. The tree now matches #465.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccf-lisa Bot added a commit that referenced this pull request Oct 6, 2026
Fifteenth and last layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/instances lists an agent's reporting instances with derived status, sync status and per-status counts, and GET …/instances/{instanceId} returns one with its redacted base/effective config; CORS allows If-Match/If-None-Match and exposes ETag for the UI; the server recovers handler panics as 500s; builtin/Cedar authz matrices over every admin agent-config route, and the Cedar docs. The tree now matches #465.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccf-lisa Bot added a commit that referenced this pull request Oct 6, 2026
Fifteenth and last layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/instances lists an agent's reporting instances with derived status, sync status and per-status counts, and GET …/instances/{instanceId} returns one with its redacted base/effective config; CORS allows If-Match/If-None-Match and exposes ETag for the UI; the server recovers handler panics as 500s; builtin/Cedar authz matrices over every admin agent-config route, and the Cedar docs. The tree now matches #465.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(agentconfig): config model and RFC 7396 overlay merge

First layer of the agent remote-configuration stack (split from #465): the declared agent config types, JSON helpers and canonical encoding, RFC 6901 pointers, and Merge/MergePatch/StripLocked of an API overlay onto the agent's file config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(agentconfig): say where the design IDs cited in comments are defined

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(agentconfig): decode config documents in place

decodeAny and decodeConfig decoded through a json.Decoder, which copies its
input through a buffer it grows by doubling: about 4 MiB of allocations per
decode of a 1 MiB document, and a config save decodes a reported base about
six times. Decode with json.Unmarshal instead, which reads the bytes in
place, keeping the Decoder's results exactly:
- decodeAny decodes into numberValue, which builds the same map[string]any,
  []any and json.Number values as a UseNumber Decoder. Documents nested
  deeper than 12 levels (Unmarshal re-scans each level) and input Unmarshal
  rejects still go through the Decoder, so its errors are unchanged.
- decodeConfig unmarshals the struct and takes policy_data, the one
  free-form field, from the UseNumber decoding it already has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
#470)

Second layer of the agent remote-configuration stack (split from #465): pointer-level JSON diff, ${env:NAME} references and the forbidden-name rule, plugin source kinds (OCI via go-containerregistry, local), trusted-source and overridable-flag matching for remote_config, cron schedule parsing, and opaque revision/admin ETags.

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
…#471)

* feat(agentconfig): change-safety classification and wire types

Third layer of the agent remote-configuration stack (split from #465): Classify/WillApply decide which overlay changes an agent applies under each remote_config mode (forbidden locked keys, unsafe new sources and re-enabled plugins, sources of enabled plugins already in use, trusted sources, overridable flags), plus the agent<->API wire types and FieldError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentconfig): classify a re-enabled plugin's policies and env references

Re-enabling a disabled plugin only checked its source, so its policy
entries and ${env:} references, which no enabled plugin uses, were
never classified and apply_safe could run an untrusted or local policy
the host had disabled. They are now classified like a new plugin's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentconfig): classify a re-enabled plugin's local source like a new one

Re-enabling a plugin the base disables only checked trusted_sources, so a
local-path source was just unsafe/reenables-plugin and apply_all applied it
even with allow_local_sources=false. Run the kept local source through the
source rules too: forbidden/local-source-not-allowed unless apply_all with
allow_local_sources (then unsafe/new-local-source).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(agentconfig): secret detection, redaction and digests

Fourth layer of the agent remote-configuration stack (split from #465): secret detection by key name and by value (linear URL-password scan, token patterns, scheme-less DSNs), Redact/RedactDocument/ScrubSecretText, and Digest over the redacted canonical config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentconfig): scale the linear-scan time budget under the race detector

TestContainsSecretValueLinear asserted a fixed 2s budget, which the race
detector's instrumentation exceeds (about 5s). Keep the linearity check but
scale the budget x10 when built with -race (raceEnabled, set by build tag).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(agentconfig): overlay validation

Fifth layer of the agent remote-configuration stack (split from #465): ValidateOverlay (strict decode, size, locked keys, plugin names, env references, schedules incl. the TZ= prefix guard, NUL characters) and the ValidationErrors/FieldError vocabulary. pkg/agentconfig is complete after this layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(agentconfig): generate a conformance golden file from the rule tables

The UI re-implements MatchTrustedSource, MatchOverridableConfigFlag,
KindOf/IsOCISource, PluginNamePattern, ParseSchedule and the per-field
apply_safe outcome of Classify + WillApply, and tested them against a
hand-copied fixture pinned to an old api commit, so a rule change failed on
neither side.

Hoist those test tables to package-level vars and generate
testdata/conformance.json from them (in the UI fixture's shape), with every
expected value computed by the real functions. TestConformanceGolden fails
when the file is stale; regenerate it with
  go test ./pkg/agentconfig -run TestConformanceGolden -update
The apply_safe cases are a new table (applySafeCases) whose field state is
derived from Classify + WillApply over probe overlays. The UI fixture's
drift cases are added to the Go tables (two '%' registries for KindOf,
'*/5 * * * *' and '@hourly' for ParseSchedule), plus a re-enabled local
plugin source case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
Sixth layer of the agent remote-configuration stack (split from #465): sdk.Client.AgentConfig fetches the agent's overlay (ETag/If-None-Match) and submits instance config reports; Heartbeat gains config_revision/config_digest; extra request headers never override Authorization.

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(agentcfg): append-only agent config revision store

Seventh layer of the agent remote-configuration stack (split from #465): the ccf_agent_config_revisions model (append-only hooks, unique (agent, revision)), migrations, the CCF_AGENT_* settings, and the agentcfg service's revision API: Current, GetRevision, ListRevisions (paginated) and CreateRevision (agent-row lock, optimistic expected revision), plus DeleteRevisionsForAgent for agent deletion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentcfg): take Settings defaults from config.DefaultAgentsConfig

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(agentcfg): agent instance store

Eighth layer of the agent remote-configuration stack (split from #465): the ccf_agent_instances model and the agentcfg instance API: UpsertReport (config reports) and TouchFromHeartbeat, the per-agent instance cap (prune-eligible rows don't count; the oldest stale instance is replaced; a short-lived cap-reached cache for heartbeats), ListInstances/GetInstance, and the derived status/sync-status/staleness rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(agentcfg): state the real per-instance bound of ListInstances

The comment implied the unpaginated list was cheap because normalizeReport
bounds the summary columns. State the actual worst case: about 3 MiB per
instance across the capped columns, times the capped plus not-yet-pruned
rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentcfg): paginate ListInstances and count instances from scalar columns

ListInstances loaded every instance's summary columns (about 3 MiB each in
the worst case), so one agent credential could inflate the admin instance
list past 1 GiB. It now returns one page (at most InstancesPageLimit = 25
rows, same order: last_seen_at DESC, instance_id) and the total.
CountInstances counts every instance by freshness, status and sync status
from the scalar columns only, using IsStale, DeriveStatus and
DeriveSyncStatus, so fleet-wide counts no longer need the full list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
… (9/15) (#477)

* feat(agentcfg): validation bases, instance pruning and agent deletion

Ninth layer of the agent remote-configuration stack (split from #465): ValidationBases (the instances a save validates against, R48) and the bounded PreviewBases set, the River job that prunes stale instances (CCF_AGENT_INSTANCE_PRUNE_*), and deleting an agent now deletes its instances and config revisions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentcfg): take the instance mode from the validated report mode

reportedRemote preferred the reported remote-config block's mode, which
the API does not validate, over the report's validated top-level mode,
so preview could classify with a different or unknown mode than the one
ValidationBases selected on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(worker): take the prune schedule fallback from config.DefaultAgentsConfig

* fix(agentcfg): load and decode each distinct validation base once

ValidationBases loaded base_config for every fresh apply-mode instance (up to
the instance cap, each up to 4 MiB), so a save's cost followed the instance
count. Group the set by base content in SQL (SHA-256 of the jsonb text),
load and decode one base per group in the same read-only snapshot, and share
it across the group's instances. Each instance is still returned with its own
fields and remote-config, plus a BaseKey so callers validate a base once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agentcfg): list instances by page in the prune test

ListInstances is paginated now (#476); TestPruneInstances reads its six
instances from the first page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
…478)

Tenth layer of the agent remote-configuration stack (split from #465): the agent resource gains configure (write an agent's overlay) and sync (an agent fetching its overlay and reporting) in the manifest and Cedar roles; the builtin PDP requires the admin check for users on agent:* and lets agent service accounts only register, ingest and sync (R39); /admin/agents list/get need agent:read while writes and key routes stay admin:manage (R40).

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
…egistration (11/15) (#479)

* feat(api): agent-facing config overlay route and heartbeat instance registration

Eleventh layer of the agent remote-configuration stack (split from #465): GET /api/agent/config returns the authenticated agent's current overlay with an opaque ETag (If-None-Match -> 304), agent JWT only and agent:sync; authenticated heartbeats refresh the instance's last-seen time and, with config_digest/config_revision, register the instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: heartbeats register instances under heartbeat:ingest

Say in the heartbeat route description and next to the agent role's sync
grant that removing sync does not stop heartbeats carrying config_digest
from registering instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): answer a 304 config poll without loading the overlay

GetConfig loaded the whole current revision (SELECT *, overlay included)
before comparing If-None-Match, although nearly every poll is a 304. With an
If-None-Match header, read only the revision head (id, revision, created_at;
same indexed query) through agentcfg.CurrentHead, compute the ETag and
return 304 on a match. The overlay is loaded only on a miss, and the
response is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(api): agent instance config reports

Twelfth layer of the agent remote-configuration stack (split from #465): PUT /api/agent/instances/{instanceId}/config-report stores an instance's mode, applied/attempted revision, status, redacted base/effective configs, digest, plugins, unsafe changes and warnings. The server validates and bounds the report, rejects NUL characters, re-redacts base/effective, masks secrets in free-text fields (error, warnings, plugin sources, unsafe values, remote-config) and returns 409 at the instance cap. The JSON body reader accepts application/json with parameters (415/413 otherwise).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): mask report secrets before truncating free text

normalizeReport cut error, warning messages, unsafe values and plugin
sources to their length caps before scrubReportText ran, so a secret
straddling a cap no longer matched and its prefix was stored. Masking
now runs after the count caps and before the length caps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): cap the remote-config block of a config report

normalizeReport never bounded remote-config, so a report with 100k
trusted_sources (7 MB) was stored with truncated=false, and ListInstances
returns that column for every instance. Cap it like the other summary
columns, after the scrub: at most 100 trusted_sources and 100
overridable_config_flags, each at most 256 bytes JSON-encoded (an over-long
entry is dropped, since a cut glob pattern can widen trust), mode at 32 and
poll_interval at 64 bytes. The encoded block stays within 64 KiB even when
every character is escaped, and the report is marked truncated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): bound a config report's summary after JSON escaping

warnings[].code, unsafe[].safety and unsafe[].reason had no length cap, and
the other caps are on raw bytes, which JSON escaping can grow sixfold ('<',
'&' and control characters encode as \u00XX). One report could make its
instance's listed summary about 21 MB, and a page of 25 instances about
500 MiB.

normalizeReport now cuts code, safety and reason to 64 bytes (cut, not
rejected: a newer agent may send values this API does not know) and keeps
the summary fields (hostname, agent-version, error, warnings, unsafe,
plugins, remote-config) within 3 MiB as encoding/json encodes them with
HTML escaping, the way the instance list does. Over that budget it cuts
the error text to 8 KiB encoded and drops the last entry of the largest
list until the report fits, and marks the report truncated. A plain-text
report at every cap (about 2.9 MB) fits, so it is stored unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(api): admin agent config read and save

Thirteenth layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/config returns the current overlay (verbatim only to agent:configure holders, redacted otherwise, fail-closed) with an admin ETag, and GET …/config/revisions/{rev} returns one revision the same way; PUT saves a new revision with If-Match (428 without, 409 on conflict, 200 for a no-op), validating the overlay on its own and against the instances a save validates against (only errors the overlay introduces block; file-origin errors are warnings). Needs agent:configure to write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): drop the hand-synced BodyLimit on agent config PUT; readJSONBody enforces the limit

* fix(api): validate each distinct reported base once on save

validateCandidate merged and validated the overlay against every instance
of the validation set. ValidationBases now groups instances by base content
(BaseKey): validate each distinct base once and attribute its errors to
every instance in the group, so the 422 body still lists each instance and
the cost of a save follows the distinct bases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): reject a stray closing brace after a strict JSON body

decodeStrict checked for trailing data with dec.More(), which reports false
for a trailing '}' or ']', so {"overlay":{}}} was accepted. Require the next
token to be io.EOF instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): reject a NUL in a revision comment with a 400

normalizeComment let a NUL character through, and Postgres cannot store one
in a text column, so PUT (and revert) failed with a 500 from the insert.
Reject it as a 400, like the overlay (O11) and report NUL checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
…4/15) (#482)

* feat(api): admin agent config preview, revision history and revert

Fourteenth layer of the agent remote-configuration stack (split from #465): POST /api/admin/agents/{id}/config/preview validates a candidate overlay and shows, per instance, the redacted effective config, its diff, classified changes and whether the agent would apply it (bounded to 50 instances / 16 MiB of reported config, validated first; omitted-instances counts the rest; needs agent:configure); GET …/config/revisions lists revisions (paginated, newest first); POST …/revisions/{rev}/revert saves an old overlay as a new revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): keep preview within its bounds for validated instances

PreviewBases loaded and decoded every validated instance's base through
ValidationBases, and Preview validated the overlay against all of them
only to discard the per-instance results, so the 50-instance / 16 MiB
preview bound did not cover the validation set. Preview now validates
the overlay alone, and PreviewBases reads the validation set as
instance ids only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(agentcfg): drop the removed Validation field from PreviewSet docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(api): a NUL in a revert comment is a 400

Revert shares normalizeComment with PUT, which now rejects a NUL character;
cover the revert route so it cannot regress to a 500 from the insert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gusfcarvalho pushed a commit that referenced this pull request Oct 6, 2026
* feat(api): admin agent instances, CORS and authz coverage

Fifteenth and last layer of the agent remote-configuration stack (split from #465): GET /api/admin/agents/{id}/instances lists an agent's reporting instances with derived status, sync status and per-status counts, and GET …/instances/{instanceId} returns one with its redacted base/effective config; CORS allows If-Match/If-None-Match and exposes ETag for the UI; the server recovers handler panics as 500s; builtin/Cedar authz matrices over every admin agent-config route, and the Cedar docs. The tree now matches #465.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): return the agent instance list as GenericDataListResponse

* docs: add an operator guide to agent remote configuration

docs/agent-remote-config.md covers the agent and admin routes with their
status codes, the ETag/If-Match and If-None-Match flows, apply modes and
change classification, trusted_sources / overridable_config_flags /
allow_local_sources, instances (cap, pruning, report bounds), who sees
redacted or verbatim overlays, and the CCF_AGENT_* settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): paginate the admin agent instance list

GET /api/admin/agents/{id}/instances returned every instance's summary,
about 3 MiB each in the worst case, so one agent credential could inflate
it past 1 GiB. It now takes page (default 1) and limit (default 25, max 25;
a larger limit is capped, as ParseParams does elsewhere) and returns one
page. An invalid page or limit is a 400, as on the revision list.

meta keeps desired-revision and counts, which cover all of the agent's
instances (agentcfg.CountInstances, scalar columns only), and adds page,
limit, total and total-pages. Swagger and the operator guide describe the
paging and the per-page bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): test the instance page bound with escaped report text

TestInstancesWorstCasePageSize now builds its reports with capsReport and
normalizeReport, over every cap, once with plain text and once with text
that JSON escapes ('<', '&', control characters), and checks that a page of
25 stays within 25 times the per-report summary budget plus the fixed
fields. The swagger description and the operator guide describe the bound
after escaping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants