From 452eb54dd1b9d47c0c76f6c736b0d08eabbdc6fd Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Mon, 5 Oct 2026 07:40:53 -0300 Subject: [PATCH 1/4] feat(agent-config): structured policy_data tree editor Layer 16 of 21 in the stacked split of compliance-framework/ui#318. Co-Authored-By: Claude Opus 5.5 --- .../config/effective/PolicyDataAddForm.vue | 139 ++++++ .../config/effective/PolicyDataNode.vue | 414 ++++++++++++++++++ .../config/effective/PolicyDataSection.vue | 211 +++++++++ .../config/effective/PolicyDataTree.vue | 54 +++ .../config/effective/policyDataContext.ts | 25 ++ 5 files changed, 843 insertions(+) create mode 100644 src/components/agents/config/effective/PolicyDataAddForm.vue create mode 100644 src/components/agents/config/effective/PolicyDataNode.vue create mode 100644 src/components/agents/config/effective/PolicyDataSection.vue create mode 100644 src/components/agents/config/effective/PolicyDataTree.vue create mode 100644 src/components/agents/config/effective/policyDataContext.ts diff --git a/src/components/agents/config/effective/PolicyDataAddForm.vue b/src/components/agents/config/effective/PolicyDataAddForm.vue new file mode 100644 index 00000000..155db8c0 --- /dev/null +++ b/src/components/agents/config/effective/PolicyDataAddForm.vue @@ -0,0 +1,139 @@ + + + diff --git a/src/components/agents/config/effective/PolicyDataNode.vue b/src/components/agents/config/effective/PolicyDataNode.vue new file mode 100644 index 00000000..93367c3f --- /dev/null +++ b/src/components/agents/config/effective/PolicyDataNode.vue @@ -0,0 +1,414 @@ + + + diff --git a/src/components/agents/config/effective/PolicyDataSection.vue b/src/components/agents/config/effective/PolicyDataSection.vue new file mode 100644 index 00000000..5fa00843 --- /dev/null +++ b/src/components/agents/config/effective/PolicyDataSection.vue @@ -0,0 +1,211 @@ + + + diff --git a/src/components/agents/config/effective/PolicyDataTree.vue b/src/components/agents/config/effective/PolicyDataTree.vue new file mode 100644 index 00000000..19c803a9 --- /dev/null +++ b/src/components/agents/config/effective/PolicyDataTree.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/components/agents/config/effective/policyDataContext.ts b/src/components/agents/config/effective/policyDataContext.ts new file mode 100644 index 00000000..3346a214 --- /dev/null +++ b/src/components/agents/config/effective/policyDataContext.ts @@ -0,0 +1,25 @@ +// What the structured policy_data tree needs from its owner (PolicyDataSection): per-pointer +// edit rights and access notes, pending markers, and the edits themselves. Without a provider +// (read-only contexts) the tree only displays. + +import type { InjectionKey } from 'vue'; + +export interface PolicyDataTreeContext { + /** Whether the value at `ptr` may be edited (agent:configure and R71 access). */ + canEdit(ptr: string): boolean; + /** R71 note for `ptr` when it differs from the section's own state ('' otherwise). */ + accessNote( + ptr: string, + ): { state: 'restricted' | 'readonly'; text: string } | null; + /** The draft changes exactly this pointer (an element of a list included). */ + changed(ptr: string): boolean; + /** Undo the pending change at `ptr`. */ + revert(ptr: string): void; + /** Set the effective value at `ptr` (arrays are passed whole). */ + set(ptr: string, value: unknown): void; + /** Remove the key at `ptr`. */ + remove(ptr: string): void; +} + +export const POLICY_DATA_TREE_KEY: InjectionKey = + Symbol('policy-data-tree'); From b839112eb64241a38e4e7978a42d38660bce0479 Mon Sep 17 00:00:00 2001 From: "ccf-lisa[bot]" <286799724+ccf-lisa[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:30:14 -0300 Subject: [PATCH 2/4] 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 --- .../agents/config/effective/PolicyDataSection.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/agents/config/effective/PolicyDataSection.vue b/src/components/agents/config/effective/PolicyDataSection.vue index 5fa00843..13df1b7f 100644 --- a/src/components/agents/config/effective/PolicyDataSection.vue +++ b/src/components/agents/config/effective/PolicyDataSection.vue @@ -90,6 +90,7 @@ import { type PlainObject, } from '@/utils/agent-config/merge-patch'; import { diffOps } from '@/utils/agent-config/policy-data-patch'; +import { hasUnstorableNumber } from '@/utils/agent-config/policy-data'; import { accessTooltip, fieldAccess } from '@/utils/agent-config/field-access'; import type { Provenance } from '@/utils/agent-config/provenance'; import ProvenanceBadge from '../ProvenanceBadge.vue'; @@ -192,6 +193,12 @@ function onInput(value: string) { error.value = 'Policy data must be a JSON object.'; return; } + // 1e999 parses as Infinity (saved as null, an RFC 7396 delete) and big integers lose digits. + if (hasUnstorableNumber(parsed)) { + error.value = + 'A number cannot be saved as typed (not finite, or more digits than 2^53); quote it to keep it as text.'; + return; + } // Only what really changed, at the pointers that changed. const ops = diffOps(ptr.value, shown.value, parsed); // A new null at a key would record a deletion (RFC 7396); nulls inside arrays are kept. From 3fd15f650bed2d876a9e3567a3198108a964b539 Mon Sep 17 00:00:00 2001 From: "ccf-lisa[bot]" <286799724+ccf-lisa[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:19:14 -0300 Subject: [PATCH 3/4] 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 --- .../__tests__/PolicyDataDiffers.spec.ts | 89 +++++++++++++++++++ .../config/effective/PolicyDataNode.vue | 16 ++++ .../config/effective/PolicyDataSection.vue | 4 + .../config/effective/policyDataContext.ts | 2 + 4 files changed, 111 insertions(+) create mode 100644 src/components/agents/config/__tests__/PolicyDataDiffers.spec.ts diff --git a/src/components/agents/config/__tests__/PolicyDataDiffers.spec.ts b/src/components/agents/config/__tests__/PolicyDataDiffers.spec.ts new file mode 100644 index 00000000..d7a57ab1 --- /dev/null +++ b/src/components/agents/config/__tests__/PolicyDataDiffers.spec.ts @@ -0,0 +1,89 @@ +// policy_data lists whose value differs between the instances' files carry the same +// "differs across instances" warning as the other editors (FieldHints): a list edit writes one +// list for every instance. +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + enableAutoUnmount, + flushPromises, + mount, + type VueWrapper, +} from '@vue/test-utils'; +import { resetAgentDrafts } from '@/composables/agent-config/draftRegistry'; +import type { AgentInstanceDetail, ConfigDoc } from '@/types/agent-config'; +import { + baseConfig, + configRev7, + instanceDetailA, + instanceIds, +} from '@/composables/agent-config/__tests__/fixtures'; +import { clone } from '@/utils/agent-config/merge-patch'; +import { + ADMIN, + READER, + fakeApi, + globalWith, + piniaWith, + workspaceHost, +} from './helpers'; + +vi.mock('@/components/code-editor', () => import('./codeEditorMock')); + +import PolicyDataSection from '../effective/PolicyDataSection.vue'; + +enableAutoUnmount(afterEach); + +const PD = '/plugins/local-ssh/policy_data'; +const POLICY_DATA = { + max_auth_tries: 4, + users: ['root', 'admin'], + ports: [22], +}; + +function detailWith(policyData: Record): AgentInstanceDetail { + const base = clone(baseConfig) as ConfigDoc; + base.plugins!['local-ssh']!.policy_data = clone(policyData); + return { ...instanceDetailA, base, effective: base }; +} + +async function mountSection(perms: Record) { + // ip-b's file has another user list and another max_auth_tries; the ports agree. + const api = fakeApi({ + getConfig: vi.fn().mockResolvedValue({ ...configRev7, overlay: {} }), + getInstance: vi.fn(async (_a: string, id: string) => + id === instanceIds.b + ? detailWith({ ...POLICY_DATA, max_auth_tries: 6, users: ['root'] }) + : detailWith(POLICY_DATA), + ), + }); + const host = workspaceHost(api, PolicyDataSection, () => ({ + plugin: 'local-ssh', + reported: POLICY_DATA, + provenance: 'file', + })); + const wrapper = mount(host, { global: globalWith(piniaWith(perms)) }); + await flushPromises(); + return wrapper; +} + +const differs = (w: VueWrapper, rel: string) => + w.find(`[data-test="pd-differs-${PD}/${rel}"]`); + +describe('policy_data: lists that differ across instances', () => { + beforeEach(() => resetAgentDrafts()); + + it('warns on a list whose value differs between the files', async () => { + const w = await mountSection(ADMIN); + expect(w.find(`[data-test="pd-node-${PD}/users"]`).exists()).toBe(true); + expect(differs(w, 'users').text()).toBe('differs across instances'); + // A list the files agree on, and a scalar (written at its own pointer), carry no warning. + expect(w.find(`[data-test="pd-node-${PD}/ports"]`).exists()).toBe(true); + expect(differs(w, 'ports').exists()).toBe(false); + expect(differs(w, 'max_auth_tries').exists()).toBe(false); + }); + + it('is an editing hint: readers do not see it', async () => { + const w = await mountSection(READER); + expect(w.find(`[data-test="pd-node-${PD}/users"]`).exists()).toBe(true); + expect(differs(w, 'users').exists()).toBe(false); + }); +}); diff --git a/src/components/agents/config/effective/PolicyDataNode.vue b/src/components/agents/config/effective/PolicyDataNode.vue index 93367c3f..cb4ef95c 100644 --- a/src/components/agents/config/effective/PolicyDataNode.vue +++ b/src/components/agents/config/effective/PolicyDataNode.vue @@ -59,6 +59,12 @@ class="text-xs text-gray-500 dark:text-slate-400" >{{ summary }} + differs across instances