Skip to content

feat(client): policy & audit UI — surface the per-contact + agent policy engine#27

Merged
hyhmrright merged 3 commits into
mainfrom
feat/policy-audit-client-ui
Jun 14, 2026
Merged

feat(client): policy & audit UI — surface the per-contact + agent policy engine#27
hyhmrright merged 3 commits into
mainfrom
feat/policy-audit-client-ui

Conversation

@hyhmrright

Copy link
Copy Markdown
Owner

What

The F2 ask_user gate (#24) and F4 per-contact policy engine (#26) shipped server-side with no client surface, so the policy engine was invisible to users. This wires three already-tested-but-unconsumed endpoints into UI as one coherent "policy & audit" cluster. packages/client only — no backend change, no migration.

  • Per-contact standing-policy editor — a contact detail panel opened from the contacts list reads GET /contacts/:id and saves the per-contact default decision (allow / ask_user / deny, or inherit) via POST /contacts/:id/policies. Existing rules shown read-only (full rule editor deferred, TODO).
  • Agent-level default policy editor — a new Settings "policy" tab reads GET /agents/me and writes the agent default via PUT /agents/me/policies.
  • Permission decision history — a new Settings "history" tab lists past decisions from GET /permissions/history (action + decision + decided-at).

Correctness

  • Both editors use the engine PolicyOverrides vocabulary from @confer/shared ({default?, rules?} with allow/ask_user/deny) — never the AgentFacts policyConfigSchema (do-not-bridge). The client type is the only shape guard for PUT /me/policies (server takes z.record).
  • Policy writes are whole-object PUT semantics (preserve existing rules, swap only default).
  • PATCH/POST /policies responses omit the peer join; the store merges them into the cached contact preserving peer (mergePeerlessUpdate) so peer info survives a save.
  • History renders raw permission rows (no peer_name/description — unlike /pending); null-safe.
  • i18n added across zh/en/ja with real translations (structural parity is build-enforced via Resources = typeof zh).

Review notes

Pure client over already-tested endpoints → no a2a-contract-reviewer, no migration-reviewer. Reviewer-QA confirmed: all six cross-boundary shapes match the live gateway handlers field-by-field, peer-join reconciliation correct, policy vocabulary pure engine-shape, whole-object semantics preserve rules, i18n complete. One review round pruned an unconsumed patchContact store method + 5 unused i18n keys (speculative surface).

Verified

  • client bun run build (strict tsc -b + vite build): PASS
  • client store tests: 105/0 (incl. setContactPolicy engine-shape body + peer preservation; loadHistory)
  • lint clean (247 files)
  • Deployed client; http://localhost/ → 200.

🤖 Generated with Claude Code

hyhmrright and others added 2 commits June 14, 2026 12:12
…icy engine

The F2 ask_user gate and F4 per-contact policy engine shipped server-side with
no client surface, so the policy engine was invisible to users. This wires three
already-tested-but-unconsumed endpoints into UI, as one coherent policy & audit
cluster (packages/client only, no backend change, no migration):

- Per-contact standing-policy editor: a contact detail panel opened from the
  contacts list reads GET /contacts/:id and saves the per-contact default
  decision (allow/ask_user/deny, or inherit) via POST /contacts/:id/policies.
  Existing rules shown read-only (full rule editor deferred, TODO).
- Agent-level default policy editor: a new Settings "policy" tab reads
  GET /agents/me and writes the agent default via PUT /agents/me/policies.
- Permission decision history: a new Settings "history" tab lists past decisions
  from GET /permissions/history (action + decision + decided-at).

Correctness:
- Both editors use the engine PolicyOverrides vocabulary from @confer/shared
  ({default?, rules?} with allow/ask_user/deny) — never the AgentFacts
  policyConfigSchema (do-not-bridge); the client type is the only shape guard
  for PUT /me/policies (server takes z.record).
- Policy writes are whole-object PUT semantics (preserve existing rules, swap
  only default).
- PATCH/POST-policies responses omit the peer join; the store merges them into
  the cached contact preserving peer (mergePeerlessUpdate) so peer info survives
  a save.
- i18n added across zh/en/ja (structural parity is build-enforced).

Verified: client `bun run build` (strict tsc -b + vite) PASS, client store
tests 105/0, lint clean (247 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guard openDetail race

Code-review follow-ups (all packages/client):
- permissions store: loadHistory no longer silently swallows failures. The
  history tab is user-triggered, so a fetch failure now sets `historyError`
  (surfaced in PermissionHistory) instead of rendering an empty list that reads
  as "no history". Corrected the stale "endpoint might not exist yet" comment
  (the endpoint shipped in PR #24). loadPending left as the background poll it is.
- contacts store: openDetail discards a stale GET response if the user switched
  to another contact mid-fetch (cancellation guard on selectedContactId), so a
  slow response can't overwrite the newer selection.
- PermissionHistory: drop the dead `decision === 'allow'` branch (the backend
  only writes allow_once/allow_always); use `startsWith('allow')`.
- lib/policy: make decisionFromSelect module-private (only mergePolicyDefault
  uses it) — no speculative public surface.

Verified: client `bun run build` PASS, store tests 105/0, lint clean (247 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hyhmrright

Copy link
Copy Markdown
Owner Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

F5 is the first client code to import @confer/shared. The client tsconfig had no
`paths`, so CI's `npx tsc --noEmit` (flat, not `tsc -b`) could not resolve the
workspace package and failed with TS2307 on PolicyEditor/lib-policy/contacts/
settings. Local `bun run build` masked it (node_modules symlink + prior dist).

Mirror the root tsconfig: map @confer/shared to ../shared/src so resolution is
explicit and build-state-independent — the same mechanism the backend
type-check already uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hyhmrright hyhmrright merged commit e60d4b6 into main Jun 14, 2026
1 check passed
@hyhmrright hyhmrright deleted the feat/policy-audit-client-ui branch June 14, 2026 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant