feat(connections): one Advanced disclosure, ranked default, armed write gate

Completes PAP-659 plan steps C0/C1/C2 and C6/C7 on this worktree.

- Fold the key step's own Advanced settings into the same disclosure that
  holds the access controls, so every connector has exactly one Advanced in
  the same place rather than two on one screen.
- Keep the disclosure open when what is inside it is load-bearing: a
  non-default method in use, or a selection the connector still needs.
- Restore first-attempt vs retry semantics on the single primary action: the
  first press creates or resumes, and only a retry pays for the defensive
  re-read that recovers a create whose response was lost.
- Show the resolved default on the one-time Cloud enrolment screen too, and
  state every reason the default cannot apply rather than only the first.
- Show a loading state when a deep link arrives before the gallery resolves
  its definition; the deleted Access step used to carry that wait.
- C6a: recommendedDefaultsForApp now returns
  askFirstRiskLevels ["write","destructive","high","critical"] instead of [],
  arming the approval gate that was already built but had nothing behind it.
- C6b/C7: the Permissions tab gains a per-group bulk control and shows each
  action's risk level, so a tool the classifier misfiled is visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Opus 5 committed 2026-09-28 21:41:36 -07:00
1 parent c2205a88b4
commit b63aef204c
6 files changed
+220 -133

No files matched your search

+2 -2
View File
@@ -617,12 +617,12 @@ describe("AppDefinition catalog", () => {
APP_DEFINITIONS.find((app) => app.slug === "hugging-face")?.methods[0]
?.defaults?.scopesHint,
).toEqual(["read-mcp"]));
it("defaults every new connection action to allowed", () => {
it("defaults reads to allowed and state-changing actions to ask first", () => {
for (const app of APP_DEFINITIONS)
for (const method of app.methods)
expect(recommendedDefaultsForApp(app, method.key)).toEqual({
access: "all_agents",
askFirstRiskLevels: [],
askFirstRiskLevels: ["write", "destructive", "high", "critical"],
});
});
it("defaults explicit read/write capability groups to their write-capable method", () => {
+22 -5
View File
@@ -246,16 +246,33 @@ export function resolveConnectionMethodServerUrl(
}
}
/**
* Risk levels a fresh connection gates behind human approval (PAP-659 C6a).
*
* The runtime gate and its approval card already exist; what was missing was a
* default with anything behind it. Reads stay Allowed so the constant read
* traffic an agent does is never interrupted; anything that changes the
* provider's state asks first, and one click on the Permissions tab turns that
* off per connection.
*
* Two risk vocabularies are in use — `read|write|destructive` from the tool
* classifier and `low|medium|high|critical` from curated catalogs — so both
* are named rather than assuming callers normalise them.
*/
export const DEFAULT_ASK_FIRST_RISK_LEVELS = [
"write",
"destructive",
"high",
"critical",
] as const;
export function recommendedDefaultsForApp(app: AppDefinition, methodKey?: string | null): Record<string, unknown> {
// Keep the parameters in the public contract: callers resolve defaults for a
// concrete app/method even though the initial policy is now uniform. This is
// an open default, not an approval bypass: connection finalization remains a
// configure-authorized, audited operation, and Ask first stays available as
// an operator-selected policy for any action after the connection is made.
// concrete app/method even though the initial policy is uniform.
void app;
void methodKey;
return {
access: "all_agents",
askFirstRiskLevels: [],
askFirstRiskLevels: [...DEFAULT_ASK_FIRST_RISK_LEVELS],
};
}
+1
View File
@@ -329,6 +329,7 @@ export {
getRecommendedConnectionMethod,
isConnectableAppSlug,
isAppStoreVisibleSlug,
DEFAULT_ASK_FIRST_RISK_LEVELS,
recommendedDefaultsForApp,
resolveConnectionMethodServerUrl,
} from "./app-definitions.js";