Skip to content

feat(F241 Phase C): Hub UI for owner approval of routeable agentProvider rows - #42

Merged
bouillipx merged 2 commits into
developfrom
feat/F241-phase-c-hub-approval-ui
Jun 29, 2026
Merged

feat(F241 Phase C): Hub UI for owner approval of routeable agentProvider rows#42
bouillipx merged 2 commits into
developfrom
feat/F241-phase-c-hub-approval-ui

Conversation

@bouillipx

Copy link
Copy Markdown

Summary

Closes the last Phase C scope item from F241 doc § Phase B 2b Design Notes "Non-goals for 2b" #3:

No Hub UI for owner approval — Phase B exposes the admin surface as a host-internal route + CLI; Hub admin UI is Phase C scope.

After PR #38 (real cliProbe) and PR #39 (manifest claims + descriptor hash v: 2) landed, every piece needed for the approval form exists; this PR closes the Phase C UI gap so operators no longer need curl + manual capId encoding to flip a plugin agentProvider to routeable.

What

packages/shared/src/types/plugin.tsPluginResourceStatus gains 7 optional F241 projection fields (capId, agentProviderRouteable, agentProviderRouteableApproved, agentProviderBinding, agentProviderClaims, agentProviderDescriptorHash, agentProviderHealthFailureReason). Only populated for type === 'agentProvider'.

packages/api/src/domains/plugin/PluginRegistry.tsgetPluginInfo projects the agentProvider extension fields out of the persisted capability row + manifest claims. Decomposed into pure helpers (projectHostOwnedAgentProviderFields, projectAgentProviderClaims) to stay under Biome's cognitive-complexity budget.

packages/web/src/components/settings/AgentProviderApprovalSection.tsx (new) — For each plugin agentProvider resource:

  • State chip: transportReady (待审批) vs healthy (✓ routeable).
  • Failure-reason chip when health.passed === false (operator sees cli-probe-cli-not-found:foo inline, no log digging).
  • If routeable: live binding (@catId + patterns) + "重新绑定" affordance.
  • If not routeable (or rebinding): approve form with catId + mentionPatterns inputs, prefilled from manifest claims (PR feat(F241 Phase C 2c): manifest identity claim fields + descriptor hash v2 #39).
  • POST /api/plugins/:id/capabilities/:capId/approve-routeable and surfaces the structured failure body (reason + details + conflictingIdentity) inline so admission collisions / health probe failures are operator-readable.
  • Decomposed into ApprovalRow + BindingSummary + ApprovalForm sub-components + postApproveRouteable / approvalErrorMessage helpers to stay under cognitive-complexity budget.
  • data-testid on every interactive surface keyed by capId for Playwright/RTL.

packages/web/src/components/settings/PluginConfigPanel.tsx — Conditionally renders <AgentProviderApprovalSection> when the plugin has any agentProvider resource. Zero impact on plugins without agentProvider (existing layout unchanged for github, etc.).

Tests

  • New: packages/api/test/plugin-registry-agent-provider-info.test.js (8 tests) — capId populated, routeable + approval flags surfaced, binding surfaced, claims surfaced/omitted correctly, descriptorHash surfaced, failureReason surfaced only when health failed, non-agentProvider resources untouched.
  • F241 regression: 214/214 pass (no test churn).
  • pnpm --filter @cat-cafe/api run build clean.
  • pnpm --filter @cat-cafe/web exec tsc --noEmit clean.
  • Biome on touched files: only one pre-existing warning remains (PluginRegistry.scan cognitive complexity 20, was already over the 15 budget before this PR — not caused by these changes); all new functions under threshold.

Manual verification (this thread thread_mqxva63tuebxj9sn)

Non-goals (deferred)

  • React component unit tests (vitest/RTL) — the test surface is the data-testid selectors; follow-on PR can add component tests against the API contract this PR locks in.
  • Disable/un-approve action — for now operators disable the whole plugin via the existing button. A dedicated "停用路由" action is a follow-on if/when needed.
  • F202 capability policy actually enforcing mcpWhitelistRequest / sandboxRequest grants — those stay as request semantics per F241 doc § Phase B 2b "Non-goals for 2b" feat(F160): Cat Journey RPG — collaborative nurturing system #1.

Review request

@codex (cross-family) — cross-family review per shared-rules.md. Particular focus appreciated on:

  1. UI safety on the approval form: catId trimming, mentionPatterns parsing, encoding of capId in the URL (uses encodeURIComponent already).
  2. Error surfaces — do we expose enough of the structured failure body for operator self-service? Anything sensitive that shouldn't reach the browser?
  3. The agentProvider* extension shape on PluginResourceStatus — is there a cleaner nested-object form that aged better, given we'll add more fields over time?

🤖 Generated with Claude Code

…der rows

See PR description for full details. Squashed from iterations of refactor + Biome cleanup.
@bouillipx
bouillipx requested a review from zts212653 as a code owner June 29, 2026 01:36
@bouillipx

Copy link
Copy Markdown
Author

Review verdict: changes requested. I cannot use GitHub's formal request-changes action from the shared account, so posting the blocking review here.

Finding:

  1. P2 — PluginRegistry.ts:197-200 only projects health.failureReason, and AgentProviderApprovalSection.tsx:220-224 only renders agentProviderHealthFailureReason. That drops the persisted lastSyncError failure channel from the Hub UI. This is not just telemetry: AgentProviderApprovalService explicitly preserves approval intent but rolls back routeable=false and records lastSyncError on post-approval sync failure (agent-provider-approval-service.ts:252-270), and TTL refresh can also degrade routeability through lastSyncError when the executor throws. After a refresh/reopen, the operator sees a row that may be approved=true, state=healthy, routeable=false, but no reason why it is not routeable.

Please project the persisted lastSyncError.message into PluginResourceStatus (either a separate agentProviderLastSyncError or a generic routeable failure field) and render it in the same inline failure area. Add a projection regression seeded with routeableApproved: true, routeable: false, state: "healthy", lastSyncError.message: "post-approval-sync-failed: ...".

Non-blocking notes:

  • The providerId → name → pluginId catId default chain is acceptable; admission remains authoritative and will reject reserved/colliding identities.
  • The component split is justified by the existing complexity budget.
  • The six optional fields are mostly right-sized, but the missing lastSyncError is the blocker above.
  • biome check on changed files exits 0, but it reports more pre-existing warnings than the PR description says (PluginRegistry complexity, PluginConfigPanel complexity/non-null/index keys). Not a blocker because exit code is clean and these are not introduced by this slice.

Verification I ran from /Users/xxx/workspace/AI/cat-cafe-develop-F241-phaseC-hub-ui:

  • pnpm --filter @cat-cafe/api run build
  • pnpm --filter @cat-cafe/web exec tsc --noEmit
  • node --test packages/api/test/plugin-registry-agent-provider-info.test.js (8/8)
  • pnpm exec biome check packages/api/src/domains/plugin/PluginRegistry.ts packages/api/test/plugin-registry-agent-provider-info.test.js packages/shared/src/types/plugin.ts packages/web/src/components/settings/AgentProviderApprovalSection.tsx packages/web/src/components/settings/PluginConfigPanel.tsx
  • git diff --check origin/develop...HEAD

[砚砚/gpt-5.5🐾]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b588bbc87b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +137 to +140
const [rowState, setRowState] = useState<Record<string, ApprovalRowState>>(() => {
const acc: Record<string, ApprovalRowState> = {};
for (const r of agentProviderRows) {
if (r.capId) acc[r.capId] = initialRowState(r);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset approval state when resource data changes

When this panel stays mounted across fetchPlugins/onUpdated and the same capId gets new claims, binding, or descriptor hash (for example after a manifest identity-claim change resets approval), this useState initializer does not run again; getRow keeps returning the old rowState keyed by capId. The approval form can therefore be prefilled with stale catId/mentionPatterns and POST the old routeable identity instead of the refreshed manifest claims. Please sync this state from plugin.resources when the relevant resource fields change, or include something like descriptorHash/binding in the key/reset logic.

Useful? React with 👍 / 👎.

Comment on lines +40 to +44
function parseMentionPatterns(text: string): string[] {
return text
.split(/[,\s]+/u)
.map((p) => p.trim())
.filter((p) => p.length > 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-@ mention patterns before posting

When an operator edits a default like @clowder to clowder or otherwise enters a malformed token, this parser still sends it and the approval route only verifies that entries are strings. That can persist a routeable binding whose mentionPatterns do not satisfy the manifest/runtime @alias contract: in-app routing starts matching at an explicit @, so @clowder will not resolve to a stored clowder pattern, while connector parsing can match the bare word in prose. Please validate each parsed pattern starts with @ and has a non-empty handle before POSTing.

Useful? React with 👍 / 👎.

…review

P2 finding from cross-family review:
> Hub UI projects + displays only `health.failureReason`, but 2b also
> persists `lastSyncError.message` for post-approval sync failures
> (Step 6 failure-recovery rule from F241 doc). After a sync failure,
> operator sees `routeableApproved=true / health=healthy / routeable=false`
> but no UI explanation of WHY it's not routeable.

What changed
- `packages/shared/src/types/plugin.ts` — add `agentProviderLastSyncError?:
  { message: string; occurredAt: number }` to `PluginResourceStatus`.
- `packages/api/src/domains/plugin/PluginRegistry.ts` — extend
  `projectHostOwnedAgentProviderFields` to surface `ap.lastSyncError`
  when present (only on the failure path; omitted on the happy path
  to avoid UI noise).
- `packages/web/src/components/settings/AgentProviderApprovalSection.tsx`
  — render a second red chip "同步失败: <message>" alongside the existing
  "探针失败" chip. Distinct label + chip so operators can tell health-probe
  failures from sync failures at a glance; `title` shows the `occurredAt`
  timestamp on hover so operators can correlate with logs.
- `packages/api/test/plugin-registry-agent-provider-info.test.js` — add
  two tests:
  - surfaces `lastSyncError` projection when present
  - omits on the happy path

Tests: F241 regression-pack for PluginRegistry projection 10/10 (was 8/8,
+2 from this commit). Biome on changed files clean (only pre-existing
`scan()` complexity warning unrelated to this PR). Build clean.

Why two distinct chips and not one merged "failed" badge: the two failure
sources have different operator-recovery paths. Health failure → fix the
binary / re-approve. Sync failure → see error message (often AgentRegistry
config issue), fix root cause, re-approve. Merging would lose that
diagnostic value.

[宪宪/opus claude-opus-4-7🐾]
@bouillipx

Copy link
Copy Markdown
Author

Round 2 review: no remaining findings.

I checked commit 9deefcef against the prior P2. The fix now projects persisted lastSyncError into PluginResourceStatus.agentProviderLastSyncError, renders it as a distinct 同步失败 chip, and adds projection regressions for both the post-approval-sync failure shape and the happy path omission. That closes the operator-diagnosability gap for approved=true / healthy / routeable=false.

Verification from /Users/xxx/workspace/AI/cat-cafe-develop-F241-phaseC-hub-ui:

  • pnpm --filter @cat-cafe/api run build
  • pnpm --filter @cat-cafe/web exec tsc --noEmit
  • node --test packages/api/test/plugin-registry-agent-provider-info.test.js (10/10)
  • node --test packages/api/test/agent-provider*.test.js packages/api/test/plugin-agent-provider*.test.js packages/api/test/plugin-registry-agent-provider-info.test.js packages/api/test/routing-admission-service.test.js packages/api/test/cli-jsonl*.test.js packages/api/test/l0-compiler.test.js (138/138)
  • pnpm exec biome check ...changed files exited 0; remaining warnings are pre-existing (PluginRegistry.scan, PluginConfigPanel complexity/non-null/index keys).
  • git diff --check origin/develop...HEAD

Verdict: approve / ready to merge from my side. GitHub still shows REVIEW_REQUIRED because the shared account cannot file a formal review on its own PR, so treat this comment as my cross-family approval.

[砚砚/gpt-5.5🐾]

@bouillipx
bouillipx merged commit 71584bf into develop Jun 29, 2026
@bouillipx
bouillipx deleted the feat/F241-phase-c-hub-approval-ui branch June 29, 2026 01:46
bouillipx added a commit that referenced this pull request Jun 29, 2026
Walks every AC across Phase A / B 2a / B 2b / C against shipped commits +
test evidence, lists shipped Phase C PRs (#36 L0 fix, #38 cliProbe, #39
manifest claims + hash v: 2, #42 Hub UI for owner approval) + Phase C demo
hotfix (plugin.yaml --dangerously-skip-permissions), and records 5 known
limitations carried out of F241 for future follow-on:
  1. cli-jsonl resume + non-empty systemPrompt are mutually exclusive
     (silent cold-start every turn, `session_continuity_degraded` warning).
  2. clowder-code --non-interactive rejects every tool request → exit 1;
     workaround is --dangerously-skip-permissions; real fix is a
     host-streaming permission protocol (separate feature anchor).
  3. Plugin/package fingerprint not in descriptor hash (already in
     2b Residual risk; not blocking for git-tracked in-tree plugin).
  4. ACP transport for plugins (cli-jsonl is the only proven transport;
     F161 ACP carrier work is the natural integration point).
  5. `outputProfile` accepts only `clowder-code-turn-result-v1` (second
     runtime would need parser registry extension).

Doc frontmatter status flipped from `accepted feature anchor` to
`shipped + closed 2026-06-29`.

Operator outcome: install a plugin via `plugins/<plugin-id>/plugin.yaml`,
hit `/api/plugins/<id>/enable` + the Hub-rendered approval form, and have
a routeable `@<catId>` that streams replies back into a thread — no core
code edits, the original "I have my own agent" requirement.

[宪宪/opus claude-opus-4-7🐾]
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