Repository navigation
feat: agent remote configuration (overlay, apply modes, safeguards) - #318
ccf-lisa[bot] wants to merge 43 commits into
Conversation
Types for the agent remote-configuration admin API, a single client (useAgentConfigApi) with camelCase stop paths for the opaque snake_case documents, If-Match writes and normalised errors, the jsonBody request transform (R13), configure/configure-policy/sync actions and the policy-author role (R40/R53), pure utilities (RFC 6901 pointers, RFC 7396 merge, overlay ops, provenance, diff, Go path.Match port, 5-field cron, YAML via js-yaml CORE_SCHEMA, client-side validation, file states and the policy-only check incl. R58), display vocabularies and an explicit fixture mode. Adds js-yaml and the CodeMirror 6 packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a Configuration tab to the agents page: the desired revision, a sync summary with problem chips, an instance picker (stale instances collapsed), per-instance mode and rejection details, file warnings (R41), and Effective (summary cards with provenance, or YAML), File and Overlay views with copy/download. Provenance is computed against the overlay of the revision each instance runs. R40: the route and nav entry move to agent:read; registration, keys and agent CRUD are hidden unless admin:manage, and keys are never requested for non-admins. The tab mounts only while active (TabPanel is not lazy) and waits for permission hydration before fetching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A right-side drawer edits the agent's overlay as one JSON Merge Patch
document, in a form or in YAML (CodeMirror 6), with debounced live
checks against the API preview (shields, trust hints), client-side
validation, and a review step: per-instance diffs of merge(base,
current) vs merge(base, draft) with safety tags, will-apply summary,
validated-only blocking (R48) and non-blocking file warnings (R59).
Saves use If-Match; 201/200 (R14), 409 (keep/discard), 422 (R6), 403
and 413 are handled. Clear overlay, a dirty-close guard, the policy-only
mode (U2.7) and the R57 notice recommending ${env:NAME} are included.
CodeMirror lives in async chunks; the Configuration tab and the editor
drawer are lazy too, so the AgentsView chunk barely grows. Fixture mode
loads its in-memory API on first use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a History view to the Configuration tab: revisions newest first with "Load more" (totalPages, R49), author, relative time, comment, size and Current / Revert-of badges; View and diffs (previous / current) of the overlays, lazily loaded and cached per revision; and Revert (agent:configure only) with an optional comment, handling 201, the 200 no-op, 409 (refresh + retry) and 422 (errors dialog). Specs share a synchronous stand-in for the async CodeMirror components. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the Policies section to the editor: bundle cards with provenance, age / origin and the "temporary" hint, Used-by wiring chips (whole-array writes), New bundle (root-relative main.rego skeleton) and Customize a bundle (extends + the R22 swap at the same index, sanitised and deduped names), Delete / Reset, and a file table with the R17 states (inherited, overridden, deleted, added, delete-missing, set, conflict) and actions (override with the vendor package pre-filled, delete via the whole delete array, undelete, restore, revert to vendor / drop file module, add file, delete by path when the vendor list is unknown). Modules open in a Rego CodeMirror editor with diagnostics merged from the preview, the last 422 and — only when still accurate — the instance report. Bundle data is edited as JSON, exclusive with a root data file (R18). The read-only Effective view shows the same file states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Never copy or send the report mask: whole-object editors (policy_data, bundle data) omit untouched masked leaves and null removed file keys; the HTTP client refuses a PUT/preview containing "••••" (R25). - Mirror the API's R59 rule client-side: problems already in an agent's file never block; a missing plugin source is only a hint standalone. - Align checks with the API: case-insensitive CCF_API_AUTH_, robfig cron parsing (no trim, time zones), interval trim/negative, empty extends, unparsable data files, lossless-only YAML number coercion; guard pointer writes against "__proto__". - R40: admin-only UI and the keys request wait for permission hydration. - Removals (makeAbsent, undelete) consider every known instance file. - Instance switching publishes detail and applied overlay together; stale loads are ignored; the File view is sanitized; provenance fallback is labelled. - Editor: external CodeMirror replacements are not undoable (and module editors are keyed per file), close guards see unparsed YAML, conflict reload / Save disabled during a conflict, comment kept, fresh preview after "Keep my draft", stale 422 errors cleared, Customize defaults reset on reopen, policy-only reset gated, R58 hint for new extends. - History: view errors, reload on new revisions, safe pagination. - A11y: focusable, announced icon hints; live check status is polite-live. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Validate the draft against the same instances the API validates a save against (fresh apply-mode, else the latest reported apply-mode one, else standalone) instead of every loaded base; the advisory policy-only check falls back to the same set. - The plugin source check covers every effective plugin, so file plugins that already lack a source stay non-blocking (R59 baseline). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Review stays disabled until the instance files have loaded. - The R58 hint for new bundle sources uses the validation bases, like the API's PolicyOnlyChange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
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 |
gusfcarvalho
left a comment
There was a problem hiding this comment.
Approving at 6a149fa. The four inline threads are all non-blocking; one is a product question (NEEDS-DECISION).
Verified locally:
vue-tsc --build,eslint src/,prettier --check src/, Vitest (168 files, 1497 tests) andvite buildall pass.- CI is green.
- The client matches the contract in api#465 (
agent_config.go):- stop paths and casing;
If-Match: "<rev>";- 200/201, 409
current-revision, 422 body keys read raw (not camelCased), 403, 413 and 428; validated/ R59warningstreated as non-blocking;PolicyOnlyChangeparity, including R22 and R58;totalPages.
Security checks:
- No
v-html; PrimeVue tooltips escape by default. - The mask is refused before PUT and preview.
__proto__is handled.- Keys and admin UI wait for permission hydration.
|
PR approved. Marking ready for e2e. |
gusfcarvalho
left a comment
There was a problem hiding this comment.
Blocking: R61 (policy-author Revert/Clear gating; owner decision); see thread
Revert and Clear overlay were configure-only in the UI while the API also
accepts configure-policy for policy-only changes (D18/R22/R58). Owner decision
R61: enable them whenever the API would accept the change.
- policyOnlyGate(bases, current, target) in utils/agent-config/policy-files.ts,
shared by the history panel and the drawer (first non-policy path, else a
generic R22/R58 reason).
- useAgentConfig.loadValidationBases(): validation-set bases (API
ValidationBases), memoised per load, failing closed.
- AgentConfigHistory: canRevert -> revertAccess + currentOverlay + loadBases;
per-revision decisions for the listed rows; openRevert guarded; 403 unchanged.
- AgentConfigEditorDrawer: Clear overlay gated by policyOnlyGate(validationBases,
original, {}) for policy-only users.
|
R61 is addressed in 8c873f0. The Divergences section of the PR body is now out of date: it says "Revert and Clear overlay require |
gusfcarvalho
left a comment
There was a problem hiding this comment.
R61 verified at 8c873f0 (incremental review of 6a149fa..8c873f0). This supersedes my REQUEST_CHANGES.
Checked:
policyOnlyGatewrapsisPolicyOnlyChangeandfirstNonPolicyPath(with a generic fallback for R22/R58 refusals), so its result matches the API's D18 check insave().- Revert:
fullis always enabled;noneis disabled;policy-onlyis decided per revision againstloadValidationBases(), which follows the API'sValidationBasesand is memoised per load. It fails closed: disabled while checking, and on any detail or revision load error.openRevertis guarded by the same decision.- Decisions reset when the desired overlay or the role changes.
- Clear overlay:
configureusers are always enabled; policy-only users go throughpolicyOnlyGate(validationBases, original, {})and are disabled while details load. - The 403 path is unchanged.
- No regression for
agent:configureusers.
Local checks: vue-tsc, eslint, prettier, Vitest (168 files, 1510 tests) and vite build pass; CI is green; the lockfile is unchanged. I removed the obsolete Divergences item from the PR body.
… helpers (R62, R63, R64, R71)
Wire and pure helpers for the round-2 rework (design §13):
- R62: PolicyBundleReport / extends gain artifactDigest; the config client
reads GET /api/artifacts/{digest}/files[/{path}] (bare bodies, per-segment
path encoding); fixture mode serves vendor sources for its digests.
vendorArtifactFor picks the digest (inline extends entry when it reports the
same source, else the direct source report) and explains a miss.
- R63: PolicyError.code; POLICY_ERROR_CODE_LABELS for the contract,
regocheck and agent codes; contractHints, a light line-based mirror of
CheckContract for modules authored in the browser (advisory only).
- R64: the module template (title, description, violation, labels) replaces
the bare package skeleton; vendorTestsFor finds the vendor tests of an
overridden package; the vendor-test tooltip says compile failures reject.
- R71: fieldAccess derives forbidden / restricted / editable per pointer from
each apply_safe instance's reported remote_config.
…e editor drawer (R69, R70, R71) Owner e2e feedback (design §13): the 'Edit configuration' drawer was too cramped and confusing with many plugins/policies. - R69: one pending-changes draft per agent (draftRegistry), kept for the session and shared by every view of the agent. useConfigWorkspace binds it with the instance bases, live preview and permission rules. The Effective view gets a pencil per editable field (plugin source, enabled, schedule, protocol, policies, config keys, labels, policy_data, verbosity, agent_evidence.*) opening a small inline editor, plus Add/Remove plugin. A sticky bar shows 'N pending changes · Review & save · Discard'; Review reuses the preview panel and saves ONE revision with If-Match and the 409 flow. Policy-only gating (R58/R61) disables Review. - R70: the structured drawer, its form sections and specs are removed; an 'Advanced: raw YAML' dialog edits the overlay and feeds the same draft. Clear overlay lives there, with the R61 gate. - R71: editable (pencil), restricted (shield naming the apply_safe instances that will not apply it and why) and forbidden (lock, muted, no pencil: api.*, daemon, remote_config.*) states; the YAML dialog marks forbidden keys and blocks Apply (the API's locked-key 422 stays authoritative). The Policies view these screens link to lands in the next commit.
…checks (R68, R62, R63, R64, R66) R68: new route /admin/agents/:id/policies (agent:read; editing with agent:configure or configure-policy), linked from the Configuration tab and the bundle summary. A full-page workspace: inline bundles and the sources plugins load | the bundle's file tree (inherited, overridden, added, deleted, dropped; View / Override / Delete / Restore / Add file) | a full-height CodeMirror editor and a validation panel running the preview. Every change goes to the shared pending-changes draft (R69); the Configuration tab and this view save together. Back links land on the agent's Configuration tab (?agent=&tab=config). R62/R64: Override fetches the current vendor file by artifact digest (the inline entry's extends digest when it reports the same source, else the direct source report) and pre-fills it. Without one (no digest, 404, or an extends the agents have not reported) it confirms, then starts from the module template instead of a body-less stub (§13.1). When vendor tests cover the overridden package, it offers to delete them. View shows a vendor file read-only. R63: API policy errors show their code labels inline (markers) and in the validation panel, next to browser contract hints (missing title, contract keys as functions, literal types, …) for authored modules. R66: Create a bundle from a source plugins use (or another source, or from scratch) and Assign to plugins: for a plugin that loads the extended source the default REPLACES it at the same index (R22, policy-only safe); adding alongside is explicit and warns about duplicate evidence. Unassigning a swapped bundle puts the source back.
Round 2: UI rework from the e2e feedback (design §13)New head What changed
Checks
Bundle sizes (
|
| chunk | before | after |
|---|---|---|
| AgentsView | 21.2 / 6.4 kB | 21.7 / 6.6 kB |
| AgentConfigTab | 126.5 / 41.3 kB | 72.8 / 20.9 kB |
| AgentConfigEditorDrawer | 86.2 / 26.2 kB | removed |
| shared workspace (js-yaml, validation, inline editors) | n/a | 101.9 / 35.8 kB |
| AgentPoliciesView (route) | n/a | 56.5 / 18.4 kB |
| ReviewSaveDialog (async) | n/a | 19.8 / 6.8 kB |
| RawOverlayDialog (async) | n/a | 5.1 / 2.3 kB |
| CodeEditor / CodeMergeView (async) | 46.4 / 29.3 kB | unchanged |
Deviations and notes
- The module template uses
"TODO: …"strings and afalseviolation body, so it parses and passes the contract checks as written. A literal{ … }body would block the save. - Pending drafts live in memory, not in sessionStorage. A reload starts clean, and
beforeunloadwarns first. - Restricted (🛡) only lists apply_safe restrictions. Report-mode and off-mode hosts are already called out by the instance notice.
- Deferred (low): a refresh re-fetches every instance detail; contract hints are recomputed for every bundle on each keystroke; the Policies route ignores an in-place
:idchange (nothing in the UI navigates that way).
The decisions are recorded in the lisa-design git note.
gusfcarvalho
left a comment
There was a problem hiding this comment.
Approving round 2 at cd6edba (incremental review of 8c873f0..cd6edba, §13 R62–R71). Nothing is blocking; I left two non-blocking threads.
Checks: npm ci, vue-tsc --build, eslint, prettier, Vitest (172 files, 1528 tests) and vite build all pass. CI is green. The merge of main (#316) is clean: no conflict hunks, and it only touches ControlStatementMetadata.vue.
Verified:
- Security:
- No
v-html/innerHTMLin the new code. Vendor Rego, API and report text render as text or in CodeMirror, and tooltips escape. "••••"is never sent:- inline text editors don't start from the mask and reject it as input;
- map rows show it only as a placeholder;
policy_dataand bundle data go throughreplacingPatch;- raw YAML Apply feeds a draft whose client issues block Review;
refuseMaskedstill guards PUT and preview.
- New writes go through
setAt/setOwn, so__proto__stays safe. - Drafts are keyed by user id and agent id.
- Forbidden keys:
canEditPointerrefuses them, raw YAML blocks Apply on locked keys, and client validation marks them as blocking.
- No
- R56: "file value" omits the key; "agent default", Auto and Remove write
null(viamakeAbsent); per-change undo goes throughrevertPointer.delete[]is written as the whole list, and the vendor-test delete offer adds the inherited tests the overridden package covers. - R62: Override uses
extends.artifact-digestwhen the report extends the same source, else the direct source's digest. A miss, 404 or error goes to a confirm and then the template. - R58/R61: configure-policy users can only edit
/policy_bundles/**and/plugins/*/policies. Review & save is gated byisPolicyOnlyChangeover the validation bases. Clear overlay in the raw dialog keepspolicyOnlyGate. - Regressions: none. The drawer is fully removed with no dead imports or routes. The new route is gated on
agent:read. The 409 flow (keep / discard / reload) and 422/403/413 handling carry over toReviewSaveDialog. Hydration is unchanged:readyfollowsensurePermissions.
Deviations:
- (a) The
"TODO: …"template with afalseviolation body is acceptable. It parses and passes R63, the confirm explains it, and the evidence is visibly titled "TODO…". It does mean an untouched override always passes; consider a warning hint while the TODO markers remain. - (b) In-memory drafts with a
beforeunloadguard are acceptable. They survive in-app navigation and keep overlay edits out of browser storage. - (c) Listing only apply_safe reasons on the restricted shield matches R71; report-mode and off-mode hosts are covered by the instance notice.
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer 1 of 21 in the stacked split of #318. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…AML helpers [2/21] (#323) * feat(agent-config): JSON merge-patch, JSON pointer, overlay ops and YAML helpers Layer 2 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): reject ambiguous YAML numbers and null roots; RFC 7396 provenance - parseYaml rejects numbers that would not be saved as typed (.inf/.nan, 0644/0x1F/0o17/+5, integers past 2^53) with the line and column, for every field and for mapping keys; quoting keeps them as text - an explicit null / ~ root is not a mapping; only comment-only text is {} - provenance: a null file value is present; under an array or scalar replacement, nulls are data and descendants are classified by the replacement Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
…g [3/21] (#324) * feat(agent-config): agent config types, glob matching and cron parsing Layer 3 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): time zone names are case-sensitive, as in Go Go's time.LoadLocation reads zoneinfo files by name, case-sensitively on the agent's host; Intl matches case-insensitively, so cron5 accepted TZ=utc and CRON_TZ=europe/london, which the API and agent reject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): type the preview's omitted-instances count ConfigPreview.omittedInstances mirrors the API's configPreviewResponse omitted-instances: the instances a bounded preview (50 instances / 16 MiB) left out. A save still validates against them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-config): type the paginated instance list's meta InstancesMeta gains page, limit, total and totalPages (api#476/#483: GET .../instances?page&limit, limit max 25). counts stay over every instance. The fields are optional: an unpaginated API returned one page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
… [4/21] (#325) * feat(agent-config): three-state field access over reporting instances Layer 4 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): follow Classify for re-enabled plugins and reused sources - apply_safe: re-enabling a plugin the host's file disables applies only with a trusted effective source, and re-checks the policies and ${env:} references it keeps (classify.go reenables-plugin) - apply_safe without trusted_sources: reusing a source the file already uses is Safe, so `source` is partial instead of read-only - usedSources skips disabled plugins, as the API does - registry authorities with % are local sources (Go url.Parse) - agentconfig-conformance.json: one table of expected results from the API's pkg/agentconfig tests, asserted against glob, cron5 and field-access so the copies cannot drift silently Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): forbidden-key tooltip text lives in tooltips.ts FORBIDDEN_TOOLTIP reads TOOLTIPS['agents.config.field.forbidden'], per docs/TOOLTIPS.md (UI-COMP-001). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-config): vendor the API's agentconfig conformance golden file The conformance fixture is now the API's generated golden file (pkg/agentconfig/testdata/conformance.json @ c0b3792), byte for byte, instead of a hand-copied table (CORE-DUP-001). - fixtures/README.md records the API commit and the update flow - scripts/sync-agentconfig-conformance.sh [ref] copies it (npm run sync:agentconfig-conformance); --check compares it, passing with a notice while the API has not published the file - CI job conformance-drift (pull-request.yml) runs the check against api@main; make reviewable does not (no network) - .prettierignore keeps prettier from reformatting the vendored file - the spec fails when the golden file gains a table it does not run The golden file adds the case "re-enable keeps a local plugin source", which field-access.ts already passes. The two case-mismatched time zones it drops stay as UI-only cases in cron5.spec.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
…rs [5/21] (#326) * feat(agent-config): overlay validation, config diff and display helpers Layer 5 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): block unstorable numbers; align large arrays by their common ends - validateOverlayClientSide blocks Infinity, NaN and integers past 2^53 anywhere in the overlay: JSON.stringify writes non-finite numbers as null, which RFC 7396 reads as deleting the key on every host - arrayElementChanges matches the common prefix and suffix before the LCS, so one insertion in an array past the LCS limit is still one addition (regression test at 600 x 601) - sanitizeForDisplay spec: client_secret removed from the copy only - plugin-name cases join the agentconfig conformance table Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
…[6/21] (#327) * feat(agent-config): instance status classification and test fixtures Layer 6 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): instance badge tooltips live in tooltips.ts The one-shot, truncated-report and file-warnings badge tooltips read TOOLTIPS['agents.config.instance.*'], per docs/TOOLTIPS.md (UI-COMP-001). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-config): sync summary from the API's fleet-wide counts The instance list becomes paginated (api#476/#483), so the loaded rows may be part of the fleet. summarizeSync takes meta.counts for the fleet-wide numbers (total, in sync / expected, stale, not reporting) and marks the summary partial when fewer rows are loaded than counts.total: report-only (rows only) is then unknown, and the problem chips cover the loaded rows. instancesMixed.meta carries the page fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): a complete instance list keeps the row-based sync summary The API counts sync status for every instance, stale ones included, so taking inSync / expected from meta.counts showed an offline agent as "In sync: 1/1". With every row loaded (the normal case, and always a single instance) the summary is computed from the rows as before, with only the total from counts; the counts are used only when the list is partial, where they are the only fleet-wide numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
#328) * feat(agent-config): policy_data model and minimal merge patches Layer 7 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(agent-config): one storable-number check, shared with validation policy-data.ts reuses validation.ts isStorableNumber and exports hasUnstorableNumber for the raw policy_data view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-config): listInstances takes a page query AgentConfigApi.listInstances(agentId, { page, limit }) follows the paginated instance list (api#476/#483); InstancesList is one page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
… review helpers [9/21] (#330) * feat(agent-config): agent config API client, configure permission and review helpers Layer 9 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-config): page through the agent's instances - listInstances sends ?page&limit (default 1 and 25, the API's max) and returns one page with its meta (page, limit, total, totalPages and the fleet-wide counts) - listAllInstances (instancePages.ts) is the one way to load every instance: pages in order until the last one, at most MAX_INSTANCE_PAGES (4 pages = 100 instances; a worst-case summary is about 3 MiB), each instance once. Past the cap, or when the list moved while paging, the result is marked partial Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): an instance list is partial only past the cap or when it moved listAllInstances marks the result partial when it stops at the page cap, or when a row comes back twice (the list moved under the pages, so another row was skipped), instead of comparing the rows with counts.total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
…21] (#331) * feat(agent-config): agent config state and the editing workspace Layer 10 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): edits wait until every instance's file is loaded The draft decides null-vs-omit and "back to the file value" against every known base. Editing was enabled before the background detail load finished, and a failed detail load was skipped silently, so an edit could drop an overlay entry another host's file still needs. - canEditPointer is false while a reporting instance's detail is missing; basesBlockedReason says why (loading, or which hosts failed) - failedBaseIds names the instances the last loadDetails could not load; loadDetails is the retry - a single-instance agent's only file is the selected instance's, so it never waits on the background load (and makes no extra request) - the workspaceHost test harness loads every detail, as the tab does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-config): load the instance list through the page helper; block edits on a partial fleet - useAgentConfig loads instances with listAllInstances; it exposes instancesPartial and instanceTotal (meta.counts.total), and the sync summary takes the API's fleet-wide counts - past the page cap, base-dependent edits are blocked through the existing gate (basesBlockedReason names the cap): field access and the draft would otherwise be computed over part of the fleet - a single-instance agent still makes one list request and never waits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-config): the fake API serves instances page by page helpers.ts pagedListInstances(items) answers listInstances as the paginated API does (25 a page, page fields, counts over every item); fakeApi uses it for the fixture fleet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
* feat(agent-config): shared configuration UI pieces Layer 11 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-config): the applied-instance notice case asserts the notice renders "shows no rejection details for an applied instance" only asserted absences, so it passed with no notice at all (CORE-TEST-002). It now first expects the notice and its mode badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
… picker [12/21] (#333) * feat(agent-config): field editor building blocks, header and instance picker Layer 12 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): editor tooltips live in tooltips.ts KeyValueEditor's key-case warning and the "Reset to file value" tooltip (KeyValueEditor, FieldHints) read TOOLTIPS['agents.config.*'], per docs/TOOLTIPS.md (UI-COMP-001). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agent-config): page the instance picker; say when the header covers part of the fleet - AgentInstancePicker renders 25 instances a page (fresh first, stale ones when shown) with Previous / Next, and opens on the selected instance's page - AgentConfigHeader says "Showing N of M instances" when the loaded rows are part of the fleet; its numbers come from the API's counts (instanceCount is the fleet total) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): the header says when the in-sync ratio comes from fleet counts On a partial instance list the in-sync ratio is the API's fleet-wide counts, which include stale instances: the header now reads "In sync: X/Y instances (incl. stale, from the API's fleet counts)". A complete list shows the row-based ratio as before (2/4 for the fixture fleet). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
…1] (#334) * feat(agent-config): save preview with diff rows and safety tags Layer 13 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): show what the save preview left out - the apply summary says how many instances the API's bounded preview omitted (omitted-instances): a save still validates against them - 422 errors of instances without a preview panel (an omitted one, or one that reported after the preview) are listed under "Other instances": they block Save, so they must be visible Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lay dialogs [14/21] (#335) * feat(agent-config): pending changes bar, review-and-save and raw overlay dialogs Layer 14 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): raw overlay dialog asks before discarding edits Escape no longer closes the dialog (inside the editor it only leaves the editor), and Cancel or x ask before discarding text that differs from what the dialog opened with: the tab unmounts the dialog on close, so the edits would be lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Layer 15 of 21 in the stacked split of #318. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(agent-config): structured policy_data tree editor Layer 16 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): raw policy_data JSON rejects numbers it cannot save as typed JSON.parse reads 1e999 as Infinity, which would be saved as null (an RFC 7396 delete of the key), and rounds integers past 2^53; the raw view now shows an error instead of applying them, like the structured add form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): policy_data lists warn when the files differ A policy_data list is written whole (RFC 7396), so an edit gives every instance the same list. Like the other editors (FieldHints), an editable list whose value differs between the instances' files now says "differs across instances" (useEditor().differsAcrossInstances, through the tree context). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): keep the policy_data node's value chain intact The "differs across instances" span sat between the container branch and its v-else-if chain, starting a new chain: every container node that did not differ also rendered the empty scalar value span. The container branch is now one template holding the summary and the hint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
* feat(agent-config): plugin summary card Layer 17 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agent-config): raw policy_data view refuses 1e999 and 20-digit integers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): plugin card removal for any name, once every file is loaded - pendingRemoval reads the draft's plugins as own properties (getOwn): a plugin named "constructor" (a valid name) read Object#constructor, so its removal showed no pending state and no Undo - Remove is disabled, with the workspace's reason, until every reporting instance's file is loaded: removal nulls the plugin where any file has it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
* feat(agent-config): plugin tabs and add-plugin dialog Layer 18 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-config): plugin tabs by own name; Add plugin never replaces a defined plugin - the tab's removal hint and the "Removed by overlay" card read plugins as own properties (getOwn), so a plugin named "constructor" is matched like any other - Add plugin refuses every name the saved overlay or the draft defines (non-null), not only the shown cards and loaded files: adding writes the whole plugin and replaced a saved definition this instance does not run yet - Add plugin waits, with the reason, until every reporting instance's file is loaded; the Effective view says files are loading, or names the ones that failed with a Retry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Layer 19 of 21 in the stacked split of #318. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(agents): Configuration tab on the Agents page Layer 20 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(agents): the Configuration tab counts the whole fleet The header's instance count is the API's fleet-wide total, not the loaded rows. Tab specs cover one instance (one list request, no paging notices) and a fleet past the page cap (fleet-wide header, paged picker, edits blocked with the cap named). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
* test(agents): integration specs for the Configuration tab Layer 21 of 21 in the stacked split of #318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agents): the reader case asserts the tab renders before its absences "a reader gets no pencils and no plugin actions" only asserted absences, so it passed when the tab failed to load (CORE-TEST-002). It now first expects the schedule field and the local-ssh card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agents): the synthetic apply_all instance has a loadable file "keeps the shield on the new plugin's tab" lists an extra instance (ip-all) the fixture API had no detail for. Its load failed silently before; now Add plugin waits for every reporting instance's file, so the spec serves ip-all's detail too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(agents): integration specs serve their instance lists page by page AddPluginGating and InlineEditing build their fleets with pagedListInstances, so meta (page fields, counts) matches the rows as the paginated API's would. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: ccf-lisa[bot] <286799724+ccf-lisa[bot]@users.noreply.github.com>
Summary
Agent remote configuration on the Agents page. Users with
agent:configurecan:The API is compliance-framework/api#465. Where the design and that code differ, this PR follows the code.
Important
Ship with or after compliance-framework/api#465.
/admin/agentsand its nav entry toagent:read.GET …/config) and shows an "unsupported" notice.What's in it
Configuration tab
Header: the desired revision and a sync summary, with chips for instances that have problems.
Instance picker: stale instances are collapsed.
Per-instance notice:
Views:
Plugins as tabs: one tab per plugin (ARIA tablist, arrow/Home/End keys, a scrollable strip for many plugins), so the selected plugin's card gets the full width. Tab labels show new / pending / removal / removed / disabled. Add plugin sits next to the strip and selects the new plugin's tab.
Inline editing on the Effective view: a ✏️ pencil on each editable field (source, schedule, enabled, labels,
policy_data, config keys, verbosity, agent evidence, policy sources as OCI references or local paths). Row actions (edit, remove, reset, reorder) sit right next to the item they act on.Three-state field access over the reporting instances (fresh and reported), mirroring the API's
Classify+WillApplyper instance:overridable_config_flags): read-only, with the reasons;api.*,daemonandremote_config.*, never changeable remotely.With no reporting instance yet, fields stay editable without a shield. Without
agent:configureeverything is read-only.Add-plugin gating: the same three states for adding
plugins.<name>: before a source is known, whether each instance could install a new plugin (apply_all; apply_safe withtrusted_sources, or only by reusing a source its file already uses); in the dialog, the concrete source is classified per instance (already used / trusted / untrusted / local). None → the action (or the dialog's Add) is disabled. A new plugin's tab, card and fields keep the shield.policy_data: a structured tree instead of a JSON blob: objects as collapsible groups, lists as lists, masked••••values and${env:NAME}references marked; deep or large containers start collapsed. Each key and list item has its own pencil / remove / add, and an edit keeps the value's JSON type. Type changes go through the Raw JSON view (a toggle). Both views record a minimal merge patch: only the changed pointers,nullfor a removed file key, a changed list written whole, and an entry is dropped when a value is set back to the file value. A list edit shows as one element change in the pending count, the inline markers, per-item undo and the review rows. Lists holding a masked value are locked in the tree (saving them would copy the mask).Pending-changes bar: edits collect in a per-agent draft behind a sticky bar ("N pending changes · Review & save · Discard").
Advanced: edit the raw overlay as YAML, using the same review flow.
Review & save:
merge(base, current)againstmerge(base, draft), with safety tags, a will-apply summary, and non-blocking file-origin warnings.Secrets notice: placeholders such as
${env:NAME}are recommended for secrets, because overlays are readable byagent:read.History: revisions with "Load more", view, diff against the previous or the current revision, and Revert.
Lib badge: each plugin card shows a neutral "agent vX" badge, the agent library its binary was built with (diagnostics only).
Layout: the Configuration tab has a single page scrollbar, and the pending-changes bar sticks to the bottom of the page scroll.
Validation
POST …/config/preview(debounced, 1.5 s). Its overlay errors and per-instance results appear at the matching fields.<<merge keys), masked••••values (never sent), and boolean coercion inconfig/labels; numbers there must be quoted. The plugin-name pattern (O6) is also checked client-side, mirroring the API'sPluginNamePattern.Access (R40)
agent:read. CRUD and keys are hidden unless the user hasadmin:manage, and keys are never fetched for non-admins.agent:configure.Follows the API's security-review contract
remote_config.modeis report by default (with credentials): the report-mode text says so, and the locked-keys panel shows it.••••) can appear under any key and at any depth; the UI recognises them by value everywhere.policyBundles,PolicyBundleReportandPolicyFileReportare removed; "Report truncated" now means the agent's file (base) was dropped.Notable decisions
policy_bundles, so there is no policy authoring UI; policies come from OCI or local sources.?fixturesdemo mode.Testing
make reviewable(prettier,vue-tsc --build,eslint src/, Vitest: 173 files, 1528 tests) andmake buildpass. The unit suite ran 8× in a row with no unhandled errors (specs that render PrimeVue tabs now unmount after each test).policy_dataedits (the API preview accepted them), element-level review rows, single page scroll.🤖 Generated with Claude Code