diff --git a/.trellis/spec/backend/index.md b/.trellis/spec/backend/index.md index 19a5546a1..8b1f1c5e1 100644 --- a/.trellis/spec/backend/index.md +++ b/.trellis/spec/backend/index.md @@ -16,9 +16,10 @@ error matrices, tests, paths, and security rules belong in the linked owner. 3. Read the focused feature contract that owns the data, filesystem, process, network, secret, or IPC behavior being changed. Managed Auth is split into [core/vault/migration](./managed-auth.md), - [provider login sessions](./managed-auth-login.md), and - [consumer projection](./managed-auth-consumers.md); cite only the owner of - the behavior being changed. + [provider login sessions](./managed-auth-login.md), + [consumer projection](./managed-auth-consumers.md), and + [managed account proxy binding](./managed-account-proxy.md); cite only the + owner of the behavior being changed. 4. For platform or delivery work, also read the matching Windows/macOS and CI/release governance contracts. @@ -70,6 +71,7 @@ and reuse of Grok's npm mirrors and ordinary-user execution boundary. | [Managed Auth Core](./managed-auth.md) | Account metadata, Credential Sessions, SecretRef vault admission, JSON migration, refresh ownership, and Proxy token resolution. | | [Managed Auth Login](./managed-auth-login.md) | Backend login sessions, OpenAI browser/Device Code, xAI Device Code, cancellation, reopen, and grant admission. | | [Managed Auth Consumers](./managed-auth-consumers.md) | Codex/Grok/OpenCode connection observation, native projection gates, readback, ownership transfer, and restart evidence. | +| [Managed Account Proxy](./managed-account-proxy.md) | Explicit OpenAI/xAI account binding to local Agent Providers, target handoff, and `fyagent_proxy` overview projection. | | [Deep-Link Import Security](./deeplink-import-security.md) | Untrusted deep-link parsing, confirmation, import capabilities, and side-effect limits. | | [Change Plan Typed Executor](./change-plan-executor.md) | Typed plans, idempotency, execution phases, compensation, and partial results. | | [Codex Provider Configuration](./codex-provider-configuration.md) | Codex provider/auth projection, writer serialization, backup, rollback, and readback. | diff --git a/.trellis/spec/backend/local-proxy-pipeline.md b/.trellis/spec/backend/local-proxy-pipeline.md index 85ad13828..440f2e109 100644 --- a/.trellis/spec/backend/local-proxy-pipeline.md +++ b/.trellis/spec/backend/local-proxy-pipeline.md @@ -14,6 +14,12 @@ Provider switching, live-config backup/restore, and crash recovery are owned by [Local Proxy Service and Takeover](./proxy-runtime.md). This file owns the HTTP engine after that service has admitted and configured it. +[Managed Account Proxy](./managed-account-proxy.md) owns which explicit +OpenAI/xAI account, model and Agent target may create the managed Provider and +how its local route appears in the account overview. This pipeline consumes the +admitted Provider/account lineage; it does not select an account or publish +connection summaries. + Provider-specific adapters may add narrower authentication or wire-format rules, but they must enter and leave through this pipeline rather than creating a parallel HTTP server, retry loop, health ledger, or usage logger. @@ -108,6 +114,34 @@ socket, database handle, or raw response-success override. can prove failure. Unknown or malformed bodies are not rewritten into a fabricated success. +### Managed subscription Responses policy + +- OpenAI/xAI managed credentials pin vendor origin and Responses protocol before + editable metadata. API-key and official native-client auth paths retain their + own adapters. Resolve the actual bound upstream model before final headers. +- `providers/managed_responses::prepare_request(provider, endpoint, body)` runs + after editable overrides. OpenAI generation requests force `store=false`, + remove `max_output_tokens`, and deduplicate encrypted-reasoning inclusion. + Compact requests have a separate schema and must not receive generation-only + fields. Existing Claude conversion handles a forced upstream SSE response + even when its downstream caller requested non-streaming JSON. +- xAI reuses namespace/sanitize owners, normalizes the route model, hoists + text-only system/developer content into instructions, removes unsupported + reasoning/encrypted replay and retention fields, and maps a legacy response + format only when no explicit text policy exists. Preserve function calls, + call IDs, arguments and empty function-result outputs; reject unsupported + instruction content instead of silently deleting it. +- Request-time vault resolution retains the admitted account lineage. A first + upstream 401 drops its response before a single same-account refresh/replay + inside the existing attempt and permit. Reuse the buffered request, replace + protected bearer/routing headers and bound response-header waits. A second + 401 is terminal. Do not create another failover loop or retry a different + account/API-key balance after subscription 401/403 or credential failure. +- Shared OpenAI compatibility headers apply to all consuming Agents. Final + xAI token-auth, model-override and client headers replace caller copies. + Neither upstream credentials nor raw OAuth/upstream error bodies may escape + into renderer results, Agent configuration or diagnostics. + ### Response, streaming, and usage - Rebuilt bodies remove hop-by-hop headers and stale entity headers. Streaming @@ -163,6 +197,12 @@ socket, database handle, or raw response-success override. - Forwarder tests cover retryable/terminal classification, permit settlement, non-streaming body-read failure, first-stream-chunk priming/replay, semantic 2xx failures, timeout/cancellation, and protected overrides. +- Managed subscription tests cover both providers and all three CLI targets, + official host/path assertions before mock I/O, complete stream/tool terminal + events, tool-result replay, final policy idempotence, preserved native auth, + exact one-401 retry and no default-account/API-key fallback. HTTP fixtures + must honor the actual outbound stream flag; a JSON mock is not valid evidence + for an adapter expecting `response.completed` in SSE. - Response/SSE tests cover LF/CRLF, split multibyte UTF-8, incomplete/trailing events, rebuilt headers, disconnect/drop settlement, and bounded diagnostics. - Usage tests cover protocol parsers, deduplication, model resolution, cache diff --git a/.trellis/spec/backend/macos-system-commit.md b/.trellis/spec/backend/macos-system-commit.md index e3fe1a84a..0770decbd 100644 --- a/.trellis/spec/backend/macos-system-commit.md +++ b/.trellis/spec/backend/macos-system-commit.md @@ -153,7 +153,10 @@ this table or the admitted Agent lifecycle; Claude Code is CLI-only under - Formal signing requires both nested binaries. `FYAGENT_ALLOW_APP_ONLY_SIGN=1` is local/diagnostic only. - `.build/` and `dist/` under `src-tauri/macos-privileged-helper/` are - gitignored build outputs. + gitignored build outputs. Development scratch is `.build-development`. + Copy the current SwiftPM `release/` product first; a leftover + `apple/Products` tree from a previous Xcode driver must not win, or the + embedded helper `CFBundleVersion` will fail the stale-version check. - WorkBuddy's expected bundle ID is `com.tencent.workbuddy.mac`. Keep `agent_install/desktop.rs` `DESKTOP_PRODUCTS` / `macos_bundle_id_for`, `macos_system_commit/policy.rs`, and privileged helper `Policy.swift` equal. @@ -177,6 +180,7 @@ this table or the admitted Agent lifecycle; Claude Code is CLI-only under | Renderer sends path/URL/command/Authorization bytes | Reject at Agent/Codex IPC; helper never sees it | | Swift package added as a Cargo workspace member | `version:check` / workspace contract fails | | Formal `sign-app` without both nested binaries | Fail; do not sign an app-only bundle | +| Current SwiftPM `release/` product and leftover `apple/Products` both exist | Copy the current SwiftPM product; the legacy Xcode tree must not win | | `sudo`, AppleScript admin, `AuthorizationExecuteWithPrivileges`, or setuid | Forbidden; tests and review reject | ## 5. Good / Base / Bad Cases @@ -186,9 +190,13 @@ this table or the admitted Agent lifecycle; Claude Code is CLI-only under `authorization_required` while production is disabled. - Base: portable Swift tests and Rust `macos_system_commit` tests cover the product table, ABI layout, and fail-closed port without Blessing a helper. +- Good: a driver switch leaves an old universal Xcode product under + `apple/Products`, but packaging embeds the just-built SwiftPM `release/` + helper and its current `CFBundleVersion` passes verification. - Bad: enabling `production_enabled()` because the helper compiled; labeling `~/Applications` success as a system install; adding Claude to the helper - table without a reviewed slot; sending a filesystem path over XPC. + table without a reviewed slot; sending a filesystem path over XPC; or + selecting an older Xcode product merely because it is already universal. ## 6. Tests Required @@ -204,6 +212,8 @@ this table or the admitted Agent lifecycle; Claude Code is CLI-only under - `tests/releaseWorkflow.test.ts`: `build-macos` runs `build-macos-privileged-helper.sh`, `embed-macos-privileged-helper.sh`, and `verify-macos-privileged-helper.sh --structure-only` before `sign-app`. +- `tests/miseTaskContract.test.ts`: the helper build copies the current + SwiftPM `release/` product before leftover `apple/Products`. - Negative: no renderer path/URL/command; no `sudo` / `osascript` elevation; Cargo workspace members stay `[".", "user-helper"]`. - Signed/notarized `/Applications` HIL is required before flipping @@ -258,6 +268,24 @@ macos_bundle_id: "com.tencent.workbuddy.mac", // lockstep with Policy.swift #### Wrong +```bash +find_named_artifact() { + try "$SCRATCH_PATH/apple/Products/Release/$name" + try "$SCRATCH_PATH/release/$name" +} +``` + +#### Correct + +```bash +find_named_artifact() { + try "$SCRATCH_PATH/release/$name" # current driver output + try "$SCRATCH_PATH/apple/Products/Release/$name" # compatibility fallback +} +``` + +#### Wrong + ```swift if try !commit.isDefined() { try commit.createOrUpdateDefinition( diff --git a/.trellis/spec/backend/managed-account-proxy.md b/.trellis/spec/backend/managed-account-proxy.md new file mode 100644 index 000000000..fc5e9c587 --- /dev/null +++ b/.trellis/spec/backend/managed-account-proxy.md @@ -0,0 +1,320 @@ +# Managed Account Proxy Binding and Overview Contract + +## 1. Scope / Trigger + +Read this contract before changing how an explicitly selected OpenAI/xAI +managed account becomes a local Agent Provider, how that Provider is handed to +the existing Proxy runtime, or how the `fyagent_proxy` connection is projected +into the Managed Auth overview. + +Primary implementation boundaries are: + +- `src-tauri/src/commands/provider.rs` for + `bind_managed_proxy_provider` and the retained xAI compatibility command; +- `src-tauri/src/services/provider/managed_proxy.rs` for account admission, + stable Provider construction and target-specific bind results; +- `src-tauri/src/commands/xai_oauth.rs` for the bounded xAI CLI model + suggestions; +- the `fyagent_proxy` reconciliation and wire-summary helpers in + `src-tauri/src/services/managed_auth/service.rs`. + +[Managed Auth Core](./managed-auth.md) owns identities, credential metadata, +SecretRef admission, exact-account lookup, refresh ownership and access-token +resolution. [Proxy Runtime](./proxy-runtime.md) owns the loopback listener, +target mutation locks, Agent live-file projection, readback, compensation and +the read-only route observer. [Local Proxy Pipeline](./local-proxy-pipeline.md) +owns upstream request shaping, same-account 401 replay, streaming and usage. +This document composes those owners; it does not authorize a second token +store, refresh loop, listener, target writer or retry state machine. + +The Renderer counterpart is +[Managed Account Subscriptions](../frontend/managed-account-subscriptions.md). +The strict account/connection DTO parser remains in +[Renderer Managed Accounts](../frontend/managed-auth.md). + +## 2. Signatures + +The generalized Tauri command accepts exactly one nested request: + +```text +bind_managed_proxy_provider({ request: { + app: "claude" | "codex" | "grokbuild", + accountId: "ma1:" + 32 lowercase hex, + modelId: string +}}) -> BindManagedProxyResult +``` + +The retained compatibility façade is xAI-only and additionally admits the +existing Claude Desktop draft workflow: + +```text +bind_xai_managed_provider({ request: { + app: "claude" | "claude-desktop" | "codex", + accountId: "ma1:" + 32 lowercase hex, + modelId: string +}}) -> BindManagedProxyResult +``` + +`modelId` is 1–128 ASCII bytes, begins with an alphanumeric byte and otherwise +contains only alphanumeric, `.`, `_`, `-` or `:`. The native command repeats +that validation; the compatibility façade does not turn a non-xAI account into +an xAI binding. + +Both commands return the exact wire shape: + +```text +BindManagedProxyResult { + providerId: string, + providerName: string, + app: submitted app, + alreadyBound: boolean, + activated: boolean +} +``` + +The closed error body contains only `code`: + +```text +invalid_request +account_unavailable +provider_conflict +apply_failed_rolled_back +rollback_partial_state_unknown +``` + +`activated` is true exactly for successful Claude Code or Grok Build binds. +Codex and compatibility Claude Desktop are saved drafts and return false. + +The xAI suggestion command is: + +```text +get_xai_oauth_models({ accountId }) -> Vec +``` + +It validates the same explicit overview identity and current vault credential. +It is a bounded list of documented CLI route suggestions, not a subscription +catalog or entitlement response. + +The Proxy-owned observation consumed by overview reconciliation is: + +```text +observe_managed_account_route(authKind, resolvedAccountKey, isDefault) + -> Option +``` + +Proxy Runtime owns how this value is established. This contract owns its +mapping into the complete Managed Auth wire snapshot: + +```text +Some(true) -> connected / official_subscription +Some(false) -> disconnected / none / connection_unavailable +None -> checking / unknown / observer_unavailable +``` + +## 3. Contracts + +### Exact account and credential admission + +- `accountId` is the public identity from the current Managed Auth overview. + Native code resolves that exact identity; it never selects the default + account or a similarly labelled account for the caller. +- The resolved credential must be `Ready`, use + `purpose=proxy_upstream`, `consumer=fyagent_proxy` and + `refresh_owner=fyagent`. Native Codex/Grok/OpenCode credentials are not + eligible even when they aggregate onto the same account card. +- Admission includes reading the matching SecretRef bundle. A removed, + revoked, stale-generation or unavailable vault session returns + `account_unavailable`; a new vault-created binding never falls back to an + in-memory legacy JSON account. +- Binding never returns a token, SecretRef, credential ID or refresh lineage. + Managed Auth Core remains the only owner that resolves or refreshes upstream + access material at request time. + +### Stable Provider identity and compatibility + +- Native code constructs the Provider ID from the resolved account/model + binding and target; caller-supplied display text never becomes an ID. OpenAI + and xAI identities remain distinct even when their labels or model IDs match. +- OpenAI Codex Providers use the custom `fyagent_chatgpt` slot and + `codex_oauth`; they must not overwrite or impersonate the reserved native + `openai` Provider. xAI keeps its existing `xai` / `xai_oauth` compatibility. +- An existing Provider with the same stable ID must match the complete + persisted binding definition. Its saved presentation name is retained so a + public account rename or user rename does not break idempotency. A mismatched + definition returns `provider_conflict`; it is never overwritten in place. +- `alreadyBound` reports whether that matching Provider row existed before the + operation. It does not claim that the listener is running, the Agent live + file adopted it, or an upstream request succeeded. + +### Target-specific application + +- Claude Code and Grok Build enter the existing Provider transaction and + Proxy managed-activation path. Positive return requires the target-specific + write/readback and runtime commit to finish; `activated=true` is not emitted + after a merely saved row. +- Codex saves or reuses the Provider draft without changing the current + Provider marker. The user continues through the existing Auth source + workspace and typed Change Plan before any live switch. This command does + not mount a second Codex apply flow. +- The old xAI command retains Claude Desktop draft compatibility. The generic + command rejects `claude-desktop`; the Models route exposes no Desktop action + until its own authoritative draft readback and application path exist. +- Agent-facing configuration receives the loopback endpoint, selected model + and existing local proxy marker/placeholder. Upstream OAuth access/refresh + tokens never enter Provider JSON returned to the Renderer, a Change Plan, + Claude/Codex/Grok live files or native Agent auth files. +- Existing native auth material, MCP configuration, permissions and unrelated + target fields survive application and restoration. Automatic failover is + disabled for the managed target so an expired subscription cannot silently + spend against an API-key Provider. +- The shared listener/target lock order, snapshots, backup ownership, + compensation and state-unknown result are owned by Proxy Runtime. Binding + cannot publish a Provider success before that owner returns an authoritative + result. + +### Model suggestions and upstream evidence + +- xAI suggestion lookup uses the exact selected account and current SecretRef + admission, then returns reviewed CLI route IDs. It never sends the session + token to the API-key `/models` endpoint. +- OpenAI has no equivalent subscription catalog in this feature. The Renderer + requires explicit manual model input; native code does not guess a default. +- A suggestion, saved Provider, adopted listener or successful local readback + is not evidence of account entitlement, quota, every model's availability or + a successful upstream generation. + +### Route observation and overview projection + +- Proxy Runtime observes the same effective current Provider authority used by + forwarding, not only a database current marker. `Some(true)` requires an + owned running loopback listener and at least one matching effective Provider + whose target live configuration has adopted that listener. +- A stopped listener, unadopted target or nonmatching Provider yields + `Some(false)`. Lock contention, unreadable state or an inconsistent + transition yields `None`. Observation is read-only and never probes the + upstream service. +- Reconciliation may retain `accountId` on a named `fyagent_proxy` slot even + when observation is false. That represents the selected saved credential, + not current request routing. +- `requestMode=none` always serializes + `requestProviderLabel=null`. A stale label must be cleared before the + complete overview crosses IPC. +- Each account's `connectedConsumerCount` is the number of unique + `connections[].consumer` values whose `accountId` names that account. It + includes a disconnected/non-routing `fyagent_proxy` slot and does not count + duplicate rows for the same consumer twice. +- The complete overview must satisfy the strict Renderer cross-reference and + count parser. Backend reconciliation never hides a named connection merely + to make the count smaller and never fabricates connected state from + credential readiness alone. + +## 4. Validation & Error Matrix + +| Condition | Required result | +| --- | --- | +| Generic request names an unsupported app, malformed account ID/model ID, or extra field | Reject before Provider, listener, file or credential mutation. | +| Compatibility request selects a non-xAI account | `account_unavailable`; do not reinterpret it as a generic bind. | +| Explicit account is absent, not Ready, wrong-purpose, wrong-consumer, native-owned or unreadable from the vault | `account_unavailable`; no default/legacy fallback. | +| Stable Provider ID exists with a different account/model/config definition | `provider_conflict`; preserve the existing row and live state. | +| Claude/Grok application fails and every snapshot is restored/read back | `apply_failed_rolled_back`; no activation claim. | +| Provider/target/listener compensation cannot establish the baseline | `rollback_partial_state_unknown`; preserve recovery evidence and block optimistic retry. | +| Codex draft save/readback/current-marker verification fails but restoration is confirmed | `apply_failed_rolled_back`; current source remains unchanged. | +| Codex draft restoration cannot be confirmed | `rollback_partial_state_unknown`; do not hand off as a usable draft. | +| xAI suggestions are empty or unavailable | Return failure/empty evidence; do not manufacture a model or query an API-key catalog. | +| Proxy observation is `Some(true)` | Project connected + official subscription only for the named account slot. | +| Proxy observation is `Some(false)` | Keep the named account if present, project disconnected + none, clear the provider label. | +| Proxy observation is `None` | Project checking + unknown with observer-unavailable evidence; no connected claim. | +| `requestMode=none` still has a provider label, or account count omits a named proxy slot | Backend/Renderer contract test fails; the complete snapshot is not accepted. | + +## 5. Good / Base / Bad Cases + +- **Good:** the user chooses one ready xAI account and model for Claude Code; + native admission resolves that exact proxy credential, reuses or creates the + stable Provider, the existing transaction adopts one loopback listener, and + the returned overview confirms the named route without exposing a token. +- **Good:** an OpenAI account creates a Codex `fyagent_chatgpt` draft. The + current Codex Provider is unchanged, and Auth presents the existing + preview/apply workspace for explicit confirmation. +- **Base:** the account remains saved while FyAgent's listener is stopped. The + `fyagent_proxy` connection still names the account, is disconnected with + `requestMode=none`, has no provider label and still contributes one unique + consumer to the account count. +- **Base:** route observation is temporarily locked. The overview says + checking/unknown rather than connected or disconnected, and no upstream + availability claim is made. +- **Bad:** bind the default account when another account ID was submitted, + store an OAuth token in Provider/live configuration, overwrite the native + Codex `openai` slot, call a Codex draft active, or drop a non-routing proxy + connection so the summary count appears cleaner. + +## 6. Tests Required + +- `services/managed_auth/subscription_tests.rs` covers both OpenAI/xAI + accounts, all three CLI targets, compatibility Desktop draft behavior, + wrong-purpose admission, provider conflicts, stable/distinct identities, + saved-name idempotency, Codex current-marker preservation, target + application and compensation. +- Managed activation integration tests cover port conflict, concurrent targets, + listener ownership, preimages/backups, native auth/MCP preservation and + partial rollback. Those assertions follow + [Proxy Runtime](./proxy-runtime.md); they must not weaken target readback to + accommodate an ephemeral test port. +- `subscription_transport_tests.rs` covers stopped, adopted and unknown route + observation against the effective forwarding owner. Request replay/SSE + assertions remain owned by + [Local Proxy Pipeline](./local-proxy-pipeline.md). +- Managed Auth overview tests prove a stopped proxy clears its provider label, + preserves the named account slot and recomputes unique consumer counts after + observation. Renderer parser tests accept that exact snapshot and reject a + leftover label or under-count. +- `xaiSubscriptionPort.test.ts` asserts exact nested command payloads, strict + request/result keys, public `ma1` identity, target/result agreement, closed + errors, old/new command compatibility, Grok activation and browser + native-only behavior. +- `XaiSubscriptionSection.test.tsx` and browser subscription cases cover + explicit account/model selection, changed account invalidation, target-local + confirmation/readback, Codex handoff and OpenAI manual input. Synthetic IPC + and local loopback fixtures are not live subscription or quota evidence. +- Secret-negative assertions cover Provider rows, Change Plans, Agent files, + overview DTOs, errors and logs. Native Agent auth bytes must remain unchanged + through bind, switch and restore. + +## 7. Wrong vs Correct + +Wrong: + +```text +bind(accountId?) + -> missing account means use default + -> save OAuth token in Agent config + -> set Codex current Provider + -> return activated=true +``` + +Correct: + +```text +validate exact {app, accountId, modelId} + -> resolve the exact ready proxy-purpose credential + vault bundle + -> construct/verify one stable Provider with no upstream secret + -> Claude/Grok: existing transaction + listener/config readback + -> Codex: draft only, current marker unchanged, existing Change Plan handoff +``` + +Wrong: + +```text +listener stopped + -> remove fyagent_proxy from the account count + -> requestMode=none, requestProviderLabel="openai" + -> Renderer rejects the complete overview +``` + +Correct: + +```text +listener stopped + -> keep the named fyagent_proxy account binding + -> disconnected, requestMode=none, requestProviderLabel=null + -> count unique named consumers, including fyagent_proxy +``` diff --git a/.trellis/spec/backend/managed-auth.md b/.trellis/spec/backend/managed-auth.md index cc62fd926..07371a94a 100644 --- a/.trellis/spec/backend/managed-auth.md +++ b/.trellis/spec/backend/managed-auth.md @@ -25,6 +25,8 @@ Related owners: OpenAI browser/Device Code, xAI Device Code, cancellation and reopen. - [Managed Auth Consumers](./managed-auth-consumers.md) owns Codex/Grok/OpenCode connection observation, native projection gates, readback and restart state. +- [Managed Account Proxy](./managed-account-proxy.md) owns explicit OpenAI/xAI + binding to local Agent Providers and the `fyagent_proxy` route projection. This is the first production consumer of `services::secret`. Do not introduce a second keyring, a plaintext JSON token authority, or a `shared` refresh owner. @@ -134,37 +136,41 @@ returns a refresh token or SecretRef. ## 3. Contracts -### Grok subscription binding to local Agent providers - -- `bind_xai_managed_provider` accepts exactly `{ app, accountId, modelId }`. - `accountId` is the public overview identity, not a default or caller-supplied - legacy token-store ID. Binding resolves an xAI `proxy_upstream` / - `fyagent_proxy` credential, requires `ready` and `refresh_owner=fyagent`, and - reads the matching SecretRef bundle before Provider mutation. -- Native Grok/OpenCode lineages are not eligible even when the same account - identity is ready. No upstream access/refresh token is copied into a Provider, - renderer, Change Plan, or Agent auth file. The existing proxy resolver remains - the only refresh owner and rejects credentials whose current status changed. -- The binding uses a stable target/account/model Provider identity; an existing - row with a different definition fails `provider_conflict` instead of being - overwritten. A new source name includes a short public account label and a - stable identity digest so equal model/display names remain distinguishable. - A saved name is preserved and excluded from binding identity; renaming the - source or changing an account's display name does not break idempotency. - Claude Code activates through the existing Provider transaction; - Codex saves a draft and uses Change Plan; Claude Desktop saves a draft for its - existing dedicated profile application, with `activated=false`. -- Result fields remain `providerId`, `providerName`, `app`, `alreadyBound`, - `activated`. Errors contain only a closed `code`: `invalid_request`, - `account_unavailable`, `provider_conflict`, `apply_failed_rolled_back`, or - `rollback_partial_state_unknown`. The last code never means restored. -- `get_xai_oauth_models(accountId)` checks the same explicit overview identity - and vault bundle, then returns the documented `grok-build` route suggestion. - No subscription catalog endpoint is established: do not send session tokens - to the API-key `/models` endpoint or label suggestions as account entitlement. -- New vault-created binding IDs never fall back to an in-memory legacy JSON - account when the vault account disappears or becomes unavailable. - +### Proxy account boundary + +- This core exposes exact-account credential admission and access-material + resolution only. Provider identity, target activation/draft behavior, model + suggestions and overview route projection are owned by + [Managed Account Proxy](./managed-account-proxy.md). +- A proxy consumer must present the public account identity and resolve a + `Ready` `proxy_upstream` / `fyagent_proxy` credential owned by FyAgent. Native + consumer lineages are not interchangeable, and a missing vault credential + never falls back to a default or plaintext legacy account. +- No binding owner may receive a refresh token or SecretRef. The only exported + request-time material is the zeroizing access token and optional routing + subject described below. + +### Proxy access-material refresh + +- `resolve_access_material` returns only a zeroizing access token, optional + routing subject and private admitted credential lineage. The internal + `refresh_rejected_access(&AccessMaterial)` uses that same lineage and rejected + token; it never rereads a default account to recover an in-flight request. +- A credential lock coalesces expiry refresh and simultaneous 401 refreshes. + Reuse an already-refreshed token only within the same credential, identity, + provider, purpose, consumer, SecretRef and authentication epoch, still owned + by FyAgent and Ready. Verify bundle identity and generation before use. +- Refresh HTTP is bounded to 30 seconds. Recheck the exact generation and + lineage after HTTP, on both success and failure, before CAS or status writes. + Logout, relogin, deletion or native ownership transfer makes late work stale; + do not return an unrelated newer session as fallback. Verify committed vault + readback after CAS; a failed CAS is not success. +- HTTP 400 with an exact `invalid_grant` (or xAI `invalid_token`) code, or a + definitive refresh 401/403, requires reauthentication. Network failure, + timeout, throttling, server failure and + malformed responses preserve the credential for an explicit retry. Missing + refresh material when renewal is needed requires reauthentication. The + legacy xAI refresh policy and native consumer owners are not rewritten. ### Identity versus credential session @@ -400,6 +406,15 @@ Required assertions: Copilot v1 does not block Codex finalize; - vault unavailable does not seal JSON; - stale generation cannot overwrite; resolver rejects native refresh owners; + concurrent expiry/401 callers share one same-lineage refresh; deletion, + relogin and ownership changes reject both late success and terminal failure; + terminal errors require reauth while transient errors retain Ready; +- exact proxy-account lookup rejects defaults, missing vault sessions, + non-proxy purposes and native refresh owners. Provider binding, route + observation and transport replay assertions are owned by + [Managed Account Proxy](./managed-account-proxy.md), + [Proxy Runtime](./proxy-runtime.md) and + [Local Proxy Pipeline](./local-proxy-pipeline.md); - set-default accepts only a ready credential under exact revision; removal preview/apply recomputes impact and rejects stale preview IDs; native secrets are deleted before credential/identity metadata; diff --git a/.trellis/spec/backend/proxy-runtime.md b/.trellis/spec/backend/proxy-runtime.md index 8a7dca460..3e7e8a44b 100644 --- a/.trellis/spec/backend/proxy-runtime.md +++ b/.trellis/spec/backend/proxy-runtime.md @@ -16,7 +16,9 @@ persistence mechanics remain owned by [Database Persistence](./database-persistence.md). Upstream OAuth access tokens for Codex/xAI/Copilot come from [Managed Auth Core](./managed-auth.md) when a fyagent-owned vault session -exists; the forwarder must not grow a second token store. +exists; the forwarder must not grow a second token store. Explicit account +admission, stable Provider binding and `fyagent_proxy` overview projection are +owned by [Managed Account Proxy](./managed-account-proxy.md). ## 2. Signatures @@ -61,20 +63,14 @@ backup body, or replacement routing implementation. ## 3. Contracts -### Managed Grok activation - -- `xai_oauth` inference is pinned to the official CLI session route - `https://cli-chat-proxy.grok.com/v1/chat/completions`. Ordinary API-key - providers retain `api.x.ai`. Claude Messages and Codex Responses reuse the - existing Chat converters, including streaming tool calls; Codex's tool - catalog uses the matching `ProxyChat` profile. -- After model mapping and header overrides, write `X-XAI-Token-Auth: - xai-grok-cli` and `x-grok-model-override` from the final outbound model. - Replace incoming copies; JSON `model` alone does not select the CLI route. - The integration fixture must assert vendor host/path before redirecting I/O - to loopback. Its streaming tests inspect tool arguments and terminal events - in both downstream protocols; synthetic success is not real quota evidence. -- Claude Code/Codex Grok subscription activation composes the existing Provider +### Managed subscription activation + +- Vendor origins, protected headers, Responses transforms, same-account 401 + refresh/replay, downstream conversion and streaming terminal events are + owned by [Local Proxy Pipeline](./local-proxy-pipeline.md). Runtime activation + receives an already admitted managed Provider and must not duplicate those + request semantics. +- Claude Code/Codex/Grok Build subscription activation composes the existing Provider transaction with the one `ProxyService`. Acquire the target mutation lock, then the shared managed-activation guard, then any listener-start guard. Hold activation ownership from snapshots through commit or compensation; @@ -87,8 +83,10 @@ backup body, or replacement routing implementation. backup; otherwise capture the outgoing native configuration. Do not backfill an outgoing API key into the new managed Provider. - Write/read back the local endpoint using the target owner. Preserve Claude - permissions/unrelated environment and Codex native auth/MCP/unrelated source - configuration. Set only the selected target enabled; disable its automatic + permissions/unrelated environment and Codex/Grok native auth/MCP/unrelated + configuration. Grok uses the existing model-selection TOML patch; malformed + existing content is not treated as an empty file. Set only the selected + target enabled; disable its automatic failover so an expired subscription cannot silently use a paid API source. - Failure restores files, row/current selection, backup, and target proxy flags. Stop a newly created listener only when no other takeover uses it; never stop @@ -99,6 +97,21 @@ backup body, or replacement routing implementation. cannot stop the subsequently committed target's listener. - Existing quit/restore/next-start behavior stays authoritative; this feature does not add a daemon, Docker, cloud service, or system-wide proxy. +- Listener integration tests normally bind port zero and read the bound port. + A deliberate stop/restart across a confirmed Change Plan must instead reuse + the endpoint confirmed by that plan: allocate through the real ProxyService, + then retain that port for the restart fixture. Port zero after restart may + correctly fail the target-projection check; never weaken that check merely + to make an ephemeral-port fixture pass. +- `observe_managed_account_route(authKind, legacyAccountId, isDefault)` is a + read-only `Option` snapshot: true means an owned, bound loopback + listener and at least one matching effective current Provider with adopted + live configuration. False means stopped/unadopted; None means locked, + unreadable or inconsistent observation. Reuse the same effective Provider + selection owner as forwarding, not the database marker alone. Observation + does not probe upstream or imply quota/model availability. Mapping this + result to account counts, request mode and provider label is owned by + [Managed Account Proxy](./managed-account-proxy.md). ### Command and state ownership @@ -223,6 +236,10 @@ backup body, or replacement routing implementation. - Recovery tests cover clean shutdown, interrupted takeover, missing/uncertain backup, recognized local-proxy state, unrecognized user state, and idempotent rerun. +- Managed subscription tests cover lock ordering across concurrent targets, + listener reuse, port-conflict compensation, preservation of target auth/MCP, + exact endpoint readback and effective-Provider route observation. They do not + re-specify token refresh or HTTP replay. - Breaker administration tests cover application-scoped refresh/reset and prove health reads do not consume permits; HTTP attempt behavior remains covered by [Local Proxy Pipeline](./local-proxy-pipeline.md). diff --git a/.trellis/spec/frontend/grok-subscription.md b/.trellis/spec/frontend/grok-subscription.md index 38b9907d7..a294d9425 100644 --- a/.trellis/spec/frontend/grok-subscription.md +++ b/.trellis/spec/frontend/grok-subscription.md @@ -1,131 +1,9 @@ -# Managed Grok Subscriptions +# Managed Account Subscriptions Compatibility Router -## 1. Scope / Trigger +This historical filename is retained so archived Trellis tasks and review +links remain resolvable. The current OpenAI/xAI account picker, binding DTO, +target readback, error and test contract is +[Managed Account Subscriptions](./managed-account-subscriptions.md). -Read before changing the Grok account/model picker or managed subscription -binding. `pages/models/XaiSubscriptionSection.tsx` is the single picker; it -composes the current Models panels and Managed Auth overview, not a second -authentication page. Agent Grok model entries link to the Claude/Codex Models -target with the existing validated Agent-return tuple. Login and account -recovery use `/auth?view=accounts`. - -General write/draft/secret rules remain in [Models](./models.md); native -authority stays in [Managed Auth](../backend/managed-auth.md), -[Proxy Runtime](../backend/proxy-runtime.md) and -[Change Plan Executor](../backend/change-plan-executor.md). - -## 2. Signatures - -`ProvidersPort.bindXaiManaged` accepts the request and returns the result below. -`ProvidersPort.fetchXaiManagedModels(accountId)` uses the existing -`WorkBuddyFetchModelsResult` shape for discovery; sharing that shape does not -admit a WorkBuddy subscription workflow. - -```ts -interface BindXaiManagedRequest { - app: "claude" | "claude-desktop" | "codex"; - accountId: string; // Explicit overview ma1 identity, never legacy/default ID. - modelId: string; -} -interface BindXaiManagedResult { - providerId: string; - providerName: string; - app: "claude" | "claude-desktop" | "codex"; - alreadyBound: boolean; - activated: boolean; -} -``` - -## 3. Contracts - -- The overview is Query-owned under `featureKeys.managedAuthOverview` and - pauses automatic reads while hidden. The user explicitly selects an xAI - account. Only `health=ready` is selectable; health does not prove the native - proxy-purpose credential exists, so native revalidates it during discovery - and binding. No default-account fallback or renderer-held credential exists. -- Account selection reads `get_xai_oauth_models` with that exact overview - identity and renders the existing selectable model chips. Native validates - the vault credential and returns documented CLI route suggestions; it does - not send the subscription token to the API-key `/models` catalog. The UI - labels these as official-example options, not an account entitlement list, - and labels this integration experimental. Changing accounts clears the old - selection/list; no model is hardcoded in the renderer or selected implicitly. - A failed/empty options read exposes manual input. Model IDs are 1–128 ASCII - characters, start with an alphanumeric character and otherwise admit only - alphanumeric, `.`, `_`, `-` and `:`. Discovery is not entitlement/use proof. -- The bind request has exactly the three keys above. Native response parsing - checks exact keys, the submitted target identity, bounded provider ID/name, - boolean fields and `activated === (app === "claude")`. Invalid or unknown - responses fail closed; no raw native diagnostic enters product copy. -- Claude Code confirmation discloses the native write targets and shares the - Provider panel's synchronous write guard. A positive result requires native - application plus provider-summary/current-ID and managed-auth rereads. - Any failed reread or unknown write result blocks further writes to that - target through the existing Models parent block. Other targets remain usable. - This target block still applies when switching targets unmounts the picker - while a bind is pending; mounted guards may suppress only local UI updates. -- Codex binding saves a draft only. The result links to the existing Auth - `consumer=codex&view=connections` source workspace, where the user selects - the named saved source, previews and confirms the existing Change Plan. - Models never mounts another source-switch workspace. This picker exposes - only Claude Code and Codex CLI binding. The native contract retains Desktop - draft compatibility, but Models provides no Desktop action until its own - authoritative saved-source readback and application path are integrated. -- Bind errors are the closed `{code}` values `invalid_request`, - `account_unavailable`, `provider_conflict`, `apply_failed_rolled_back`, and - `rollback_partial_state_unknown`. The last value and malformed failures - block writes; known preflight/confirmed-restoration failures retain a safe - retry path. Once binding returned, subsequent owner-read errors are always - unconfirmed regardless of their error shape. -- Successful binding invalidates/rereads the affected Provider summary and - managed-auth overview. Account/login changes reuse the same overview key. - The UI states that using the subscription requires FyAgent running in the - background, and separates saved config from actual calls/quota use. -- WorkBuddy has no subscription picker: CLI route suggestions do not describe - models supported by its configured API-key service. Its existing service/key - input, model discovery and save-plan behavior stay intact. - -## 4. Validation & Error Matrix - -These focused cases extend the general write, secret and lifecycle matrix in -[Models](./models.md). - -| Condition | Required renderer behavior | -| --- | --- | -| No explicit account/model, or selected account is no longer ready | Disable confirmation; never select the default account or another model. | -| Discovery fails or returns no usable IDs | Offer explicit manual input; do not manufacture a model or change the upstream source. | -| Bind result names another target, has excess fields, or contradicts activation semantics | Reject the result and mark target state unconfirmed. | -| Native rejects unavailable account/conflict, or confirms restoration | Show the closed failure and retain an explicit retry/recovery entry. | -| Native cannot confirm restoration, or either post-bind owner read fails | Block further target writes; no optimistic success or automatic write retry. | -| Codex draft is saved | State that it is a draft and expose the existing Auth source-plan continuation. | -| Claude Code picker is open | Offer only its own target; do not save a Desktop source then read the Claude Code summary. | -| WorkBuddy is selected | Keep its service/key workflow; do not offer subscription route suggestions. | - -## 5. Good / Base / Bad Cases - -Good: a user chooses an existing xAI account and a suggested model, confirms -Claude's native file disclosure, and sees applied copy only after native and -owner readback. Base: suggestions are unavailable, so the user enters a model ID -and native still revalidates the selected account. Bad: use the first/default -account, resurrect `auth_get_status`, or call a draft a working subscription. - -## 6. Tests Required - -`XaiSubscriptionSection.test.tsx` covers explicit account/model selection, -changed/expired accounts, native failure/readback, per-target draft/application -copy, navigation and hidden reads. Models Page coverage verifies that WorkBuddy -has no unsupported subscription picker. `xaiSubscriptionPort.test.ts` covers -exact vault identity payloads, invalid fields, target/result agreement, closed -errors and browser native-only behavior. - -`tests/browser/xai-subscription.spec.ts` covers the real renderer's Claude -confirmation and Codex source preview/apply using synthetic IPC fixtures. -These are not native-file, live-subscription or Windows evidence. - -## 7. Wrong vs Correct - -Wrong: `bindXaiManaged({ app: "codex", accountId: defaultAccountId })` followed -by immediate "已切换" copy. Correct: pass the exact selected -`{app, accountId, modelId}`, reread the saved source, then hand off to Auth's -existing preview/apply workspace. A model fetch or saved draft is never live -usage evidence. +Do not add new behavior here. Update the focused owner and its backend +[Managed Account Proxy](../backend/managed-account-proxy.md) boundary instead. diff --git a/.trellis/spec/frontend/index.md b/.trellis/spec/frontend/index.md index 27c52bc12..5bb7fd6e2 100644 --- a/.trellis/spec/frontend/index.md +++ b/.trellis/spec/frontend/index.md @@ -42,17 +42,21 @@ focused feature owner. Apply [Component Guidelines](./component-guidelines.md), | [External Agent Auth](./agent-auth.md) | Native auth observations, session ownership and safe handoff. | | [Managed Auth](./managed-auth.md) | Accounts/connections/request sources, login and impact confirmation. | | [Models](./models.md) | Drafts, connectivity, native save and existing model workflows. | -| [Managed Grok Subscriptions](./grok-subscription.md) | Explicit account/model binding, native readback and subscription scope. | +| [Managed Account Subscriptions](./managed-account-subscriptions.md) | OpenAI/xAI proxy binding, target readback and subscription scope. | | [Assignments](./assignments.md) | Shared seven-target selection and serialized mutations. | | [Skills](./skills.md) | Discovery, installed items, backups and assignment. | | [MCP](./mcp.md) | Catalog/launch validation, CRUD, installation and assignment. | +| [FDE MCP Catalogue](./mcp-fde-catalog.md) | China-oriented recipes, restricted tools, env credentials and discovery. | | [Prompts and Memory](./prompts-memory.md) | Native content CRUD, editor/dirty state and directory operations. | +| [Prompt Presets](./prompt-presets.md) | Static FDE catalogue, preview, draft copying and disabled library saves. | ## Historical discovery routers -[Shell](./shell.md), [Agents and Models](./agent-models.md) and -[Skills and MCP](./skills-mcp.md) only point to focused contracts. They are not -alternative implementations. New work cites the focused owner. +[Shell](./shell.md), [Agents and Models](./agent-models.md), +[Skills and MCP](./skills-mcp.md), and the historical +[Grok subscription filename](./grok-subscription.md) only point to focused +contracts. They are not alternative implementations. New work cites the +focused owner. ## Quality Check diff --git a/.trellis/spec/frontend/managed-account-subscriptions.md b/.trellis/spec/frontend/managed-account-subscriptions.md new file mode 100644 index 000000000..983b98971 --- /dev/null +++ b/.trellis/spec/frontend/managed-account-subscriptions.md @@ -0,0 +1,146 @@ +# Managed Account Subscriptions + +## 1. Scope / Trigger + +Read before changing the OpenAI/xAI account/model picker or managed subscription +binding. `pages/models/XaiSubscriptionSection.tsx` is the single picker; it +composes the current Models panels and Managed Auth overview, not a second +authentication page. Agent Grok model entries link to the Claude/Codex Models +target with the existing validated Agent-return tuple. Login and account +recovery use `/auth?view=accounts`. + +General write/draft/secret rules remain in [Models](./models.md). Native +binding and overview authority stays in +[Managed Account Proxy](../backend/managed-account-proxy.md); credential and +refresh authority stays in [Managed Auth Core](../backend/managed-auth.md), +listener/application transactions in +[Proxy Runtime](../backend/proxy-runtime.md), and Codex confirmation in +[Change Plan Executor](../backend/change-plan-executor.md). + +## 2. Signatures + +`ProvidersPort.bindManagedProxy` accepts the request and returns the result below. +`ProvidersPort.fetchXaiManagedModels(accountId)` uses the existing +`WorkBuddyFetchModelsResult` shape for discovery; sharing that shape does not +admit a WorkBuddy subscription workflow. + +```ts +interface BindManagedProxyRequest { + app: "claude" | "codex" | "grokbuild"; + accountId: string; // Explicit overview ma1 identity, never legacy/default ID. + modelId: string; +} +interface BindManagedProxyResult { + providerId: string; + providerName: string; + app: "claude" | "codex" | "grokbuild"; + alreadyBound: boolean; + activated: boolean; +} +``` + +The native command is `bind_managed_proxy_provider`. The old +`bindXaiManaged`/`bind_xai_managed_provider` facade remains xAI-only and retains +Desktop draft compatibility; both use the same response/error parser and native +binding owner. Browser ports remain native-only. + +## 3. Contracts + +- The overview is Query-owned under `featureKeys.managedAuthOverview` and + pauses automatic reads while hidden. The user explicitly selects an OpenAI/xAI + account. Only `health=ready` is selectable; health does not prove the native + proxy-purpose credential exists, so native revalidates it during discovery + and binding. No default-account fallback or renderer-held credential exists. +- xAI account selection reads `get_xai_oauth_models` with that exact overview + identity and renders the existing selectable model chips. Native validates + the vault credential and returns documented CLI route suggestions; it does + not send the subscription token to the API-key `/models` catalog. The UI + labels these as official-example options, not an account entitlement list, + and labels this integration experimental. Changing accounts clears the old + selection/list; no model is hardcoded in the renderer or selected implicitly. + A failed/empty options read exposes manual input. Model IDs are 1–128 ASCII + characters, start with an alphanumeric character and otherwise admit only + alphanumeric, `.`, `_`, `-` and `:`. Discovery is not entitlement/use proof. +- OpenAI selection offers explicit manual model ID input. Do not guess a + model, reuse another account's options, or send subscription credentials to + an API-key catalog. Account labels distinguish ChatGPT from Grok; changing + accounts or targets clears stale model selection. +- The bind request has exactly the three keys above. Native response parsing + checks exact keys, the submitted target identity, bounded provider ID/name, + boolean fields and `activated === (app === "claude" || app === "grokbuild")`. Invalid or unknown + responses fail closed; no raw native diagnostic enters product copy. +- Claude Code/Grok Build confirmation discloses native write targets and shares the + Provider panel's synchronous write guard. A positive result requires native + application plus provider-summary/current-ID and managed-auth rereads. + Any failed reread or unknown write result blocks further writes to that + target through the existing Models parent block. Other targets remain usable. + This target block still applies when switching targets unmounts the picker + while a bind is pending; mounted guards may suppress only local UI updates. +- Codex binding saves a draft only. The result links to the existing Auth + `consumer=codex&view=connections` source workspace, where the user selects + the named saved source, previews and confirms the existing Change Plan. + Models never mounts another source-switch workspace. This picker exposes + Claude Code, Codex and Grok Build binding. The native contract retains Desktop + draft compatibility, but Models provides no Desktop action until its own + authoritative saved-source readback and application path are integrated. +- Bind errors are the closed `{code}` values `invalid_request`, + `account_unavailable`, `provider_conflict`, `apply_failed_rolled_back`, and + `rollback_partial_state_unknown`. The last value and malformed failures + block writes; known preflight/confirmed-restoration failures retain a safe + retry path. Once binding returned, subsequent owner-read errors are always + unconfirmed regardless of their error shape. +- Successful binding invalidates/rereads the affected Provider summary and + managed-auth overview. Account/login changes reuse the same overview key. + The UI states that using the subscription requires FyAgent running in the + background, and separates saved config from actual calls/quota use. +- WorkBuddy has no subscription picker: CLI route suggestions do not describe + models supported by its configured API-key service. Its existing service/key + input, model discovery and save-plan behavior stay intact. + +## 4. Validation & Error Matrix + +These focused cases extend the general write, secret and lifecycle matrix in +[Models](./models.md). + +| Condition | Required renderer behavior | +| --- | --- | +| No explicit account/model, or selected account is no longer ready | Disable confirmation; never select the default account or another model. | +| Discovery fails or returns no usable IDs | Offer explicit manual input; do not manufacture a model or change the upstream source. | +| Bind result names another target, has excess fields, or contradicts activation semantics | Reject the result and mark target state unconfirmed. | +| Native rejects unavailable account/conflict, or confirms restoration | Show the closed failure and retain an explicit retry/recovery entry. | +| Native cannot confirm restoration, or either post-bind owner read fails | Block further target writes; no optimistic success or automatic write retry. | +| Codex draft is saved | State that it is a draft and expose the existing Auth source-plan continuation. | +| Claude Code picker is open | Offer only its own target; do not save a Desktop source then read the Claude Code summary. | +| WorkBuddy is selected | Keep its service/key workflow; do not offer subscription route suggestions. | + +## 5. Good / Base / Bad Cases + +Good: a user chooses an existing xAI account and a suggested model, confirms +Claude's native file disclosure, and sees applied copy only after native and +owner readback. Base: suggestions are unavailable, so the user enters a model ID +and native still revalidates the selected account. Bad: use the first/default +account, resurrect `auth_get_status`, or call a draft a working subscription. + +## 6. Tests Required + +`XaiSubscriptionSection.test.tsx` covers both providers' explicit account/model selection, +changed/expired accounts, native failure/readback, per-target draft/application +copy, navigation and hidden reads. Models Page coverage verifies that WorkBuddy +has no unsupported subscription picker. `xaiSubscriptionPort.test.ts` covers +exact vault identity payloads, invalid fields, target/result agreement, closed +errors, old/new command compatibility, Grok activation and browser native-only +behavior. OpenAI selection must not call xAI/API-key model discovery. Models +Page tests retain target-switch isolation and WorkBuddy's non-subscription scope. + +`tests/browser/xai-subscription.spec.ts` covers the real renderer's Claude/Grok +confirmation, OpenAI manual selection and Codex source preview/apply using +synthetic IPC fixtures. +These are not native-file, live-subscription or Windows evidence. + +## 7. Wrong vs Correct + +Wrong: `bindManagedProxy({ app: "codex", accountId: defaultAccountId })` followed +by immediate "已切换" copy. Correct: pass the exact selected +`{app, accountId, modelId}`, reread the saved source, then hand off to Auth's +existing preview/apply workspace. A model fetch or saved draft is never live +usage evidence. diff --git a/.trellis/spec/frontend/managed-auth.md b/.trellis/spec/frontend/managed-auth.md index 038b21e51..b901c53ac 100644 --- a/.trellis/spec/frontend/managed-auth.md +++ b/.trellis/spec/frontend/managed-auth.md @@ -21,9 +21,11 @@ External Agent-owned authentication remains under refresh ownership and recovery semantics are owned by [Managed Auth Core](../backend/managed-auth.md); provider login sessions by [Managed Auth Login](../backend/managed-auth-login.md); and software projection -by [Managed Auth Consumers](../backend/managed-auth-consumers.md). This -frontend contract does not make browser fixtures or mock IPC into native -authentication evidence. +by [Managed Auth Consumers](../backend/managed-auth-consumers.md). Explicit +OpenAI/xAI binding plus `fyagent_proxy` route projection is owned by +[Managed Account Proxy](../backend/managed-account-proxy.md). This frontend +contract owns strict wire parsing and presentation; it does not make browser +fixtures or mock IPC into native authentication evidence. ## 2. Signatures @@ -180,7 +182,11 @@ listed in bounded labels, canonical opaque IDs/revisions and valid timestamps. - The overview parser validates cross-resource references, unique IDs, connected-account counts, at most eight uniquely identified active sessions, - and provider/consumer compatibility. The backend admits at most one + and provider/consumer compatibility. `connectedConsumerCount` equals the + number of unique `connections[].consumer` values whose `accountId` matches + that account, including a `fyagent_proxy` slot that still names the account + while `authStatus=disconnected` and `requestMode=none`. `requestMode=none` + rejects a leftover `requestProviderLabel`. The backend admits at most one non-terminal session per provider, while one OpenAI and one xAI session may coexist. A malformed reference rejects the complete snapshot. - Login snapshots never expose authorization URLs, callback URLs, OAuth code, @@ -298,6 +304,8 @@ listed in | Overview has an extra token/path/raw-error field | Reject the complete response. | | Connection references a missing account/provider | Reject the complete response. | | Account says two connections but only one references it | Reject the complete response. | +| `requestMode=none` still carries `requestProviderLabel` | Reject the complete response. | +| Disconnected `fyagent_proxy` still names the account and the count includes that consumer | Accept; disconnected routing is not permission to drop the named slot from the count. | | Device verification URI has query, fragment, wrong host or non-HTTPS scheme | Reject the login snapshot. | | More than eight active sessions or a duplicate session ID appears | Reject the overview/session chain. | | OpenAI and xAI each have one active session | Accept both; the UI follows the selected opaque session ID and must not merge them. | @@ -339,6 +347,12 @@ listed in OAuth sections as a second account owner. Only retained native compatibility commands remain; mutation commands fail with `legacy_auth_mutation_disabled`. A compatibility API is not evidence of an existing renderer picker or login UI. +- **Good:** a saved official account whose local proxy is not routing still + parses: `fyagent_proxy` may be `disconnected`/`none` with a null label, and + `connectedConsumerCount` still includes that named slot. +- **Bad:** treat a non-routing proxy as absent from the count, or keep a + provider label on `requestMode=none`. The page then shows only + 「无法加载账号与认证」 / 「请稍后重试。」. ## 6. Tests Required @@ -354,6 +368,9 @@ Required assertions include: - all valid closed overview/session/mutation variants and strict rejection of unknown keys, invalid references, malformed revisions and forbidden fields; + disconnected `fyagent_proxy` with `requestMode=none` and a null label is + accepted when the account count includes that named slot; leftover labels + and under-counted summaries are rejected; - maximum-eight/unique session parsing plus backend per-provider single-flight and cross-provider coexistence; - Tauri command/payload mapping and request/response identity binding; diff --git a/.trellis/spec/frontend/mcp-fde-catalog.md b/.trellis/spec/frontend/mcp-fde-catalog.md new file mode 100644 index 000000000..6da477eee --- /dev/null +++ b/.trellis/spec/frontend/mcp-fde-catalog.md @@ -0,0 +1,111 @@ +# FDE MCP Catalogue Recipes + +## 1. Scope / Trigger + +Read before changing FDE discovery membership or China-oriented cloud/data +recipes. [MCP](./mcp.md) owns installation, targets, secret display and native +mutation behavior; this document owns the catalogue-only additions. + +## 2. Signatures and owners + +`src/pages/mcp/catalog.ts` remains the single recipe owner. Its existing +`McpCatalogItem.build(values, apps, platform): McpServer` validates required +fields/targets and uses `npxSpec` for Windows/macOS launch differences. +`McpCatalogCategory` and `McpCatalogFilterId` admit `fde`; Discovery intersects +this category with ordinary search. No new installer, Port or endpoint exists. + +The `fde` filter contains exactly these 20 catalogue IDs in catalogue order: + +```text +amap +feishu +dingtalk +yunxiao +gitee +tencent-docs +tapd +aliyun-websearch +yuque +apifox +antv-chart +edgeone-pages +cloudbase +aliyun-dms +aliyun-dataworks +aliyun-ack +aliyun-rds +aliyun-cloudops +dbhub +starrocks +``` + +## 3. Contracts + +- The FDE filter uses the exact ordered membership above: twelve existing + domestic mapping/collaboration/API/deployment entries plus eight cloud/data + recipes. Do not clone an ID to create a second catalogue or admit an entry + merely because it has a China-related tag. A membership change is a reviewed + contract change with an exact regression update. Other install-mode filters + retain their existing meaning. No-field configuration does not imply no login. +- New FDE recipes are `provenance: official`, `maturity: verify`, linked to + upstream setup documentation and carry actual runtime/auth/risk notes. + Configuration review is not authentication, availability, audit certification + or a guarantee of mainland connectivity. Revalidate upstream when editing. +- Credentials/DSNs use password fields and `env`, never argv, search or card + text. `CONNECTION_STRING` for DMS identifies an authorized database rather + than carrying its password. Optional STS values are omitted when blank. + +| ID / launcher | Required configuration and conservative defaults | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cloudbase` / npx `@cloudbase/cloudbase-mcp@latest` | Vendor login and environment selection occur later; cloud privilege, no automatic login/deploy. | +| `aliyun-dms` / uvx `alibabacloud-dms-mcp-server@latest` | `ALIBABA_CLOUD_ACCESS_KEY_ID`, `ALIBABA_CLOUD_ACCESS_KEY_SECRET`, required `CONNECTION_STRING`, optional `ALIBABA_CLOUD_SECURITY_TOKEN`; write privilege, not a SQL firewall. | +| `aliyun-dataworks` / npx `alibabacloud-dataworks-mcp-server@latest` | Alibaba AK pair, `REGION`, fixed `TOOL_NAMES=ListProjects`; read-only tool selection, not general DataWorks write access. | +| `aliyun-ack` / uvx `alibabacloud-ack-mcp-server@latest` | `ACCESS_KEY_ID`, `ACCESS_KEY_SECRET`, `KUBECONFIG_MODE=ACK_PRIVATE`; no `--allow-write`, requires cluster network/RAM/RBAC. | +| `aliyun-rds` / uvx `alibabacloud-rds-openapi-mcp-server@latest` | Alibaba AK pair, optional STS, `SERVER_TRANSPORT=stdio`, `ENABLE_WRITE_TOOLS=false`; still cloud privilege, not guaranteed read-only. | +| `aliyun-cloudops` / uvx `alibaba-cloud-ops-mcp-server@latest` | Alibaba AK pair; fixed `--transport stdio --env domestic --services ecs --visible-tools ECS_DescribeInstances`; do not add broad/local execution tools. | +| `dbhub` / npx `@bytebase/dbhub@latest --transport stdio` | Required secret `DSN`; Node >=22.5. Do not invent legacy `--readonly`; database account permissions must enforce read-only. | +| `starrocks` / `uv run --with mcp-server-starrocks mcp-server-starrocks --mode stdio` | Required secret `STARROCKS_URL`; optional `STARROCKS_SSL_CA` sets both `STARROCKS_SSL_VERIFY_CERT=true` and `STARROCKS_SSL_VERIFY_IDENTITY=true`. Missing CA does not guarantee secure transport. | + +Tool-selection flags complement server-side permissions; instructions and +privilege labels do not enforce authorization. DMS, DBHub and StarRocks can +write SQL; RDS and CloudBase can change cloud resources. The catalogue does not +execute these packages to validate connectivity or request customer secrets. +Package resolution remains upstream-owned; no SDK/runtime dependency is added +to FyAgent's own lockfiles by adding a recipe. + +## 4. Validation & Error Matrix + +| Condition | Required behavior | +| ------------------------------------------------------------ | ----------------------------------------------------------------------------- | +| Required field missing / no target | Existing builder throws `UserFacingError` before native installation. | +| Optional STS or CA blank | Omit optional env keys, do not create fictional credentials/TLS state. | +| Platform is Windows | npx uses existing `cmd /c` adapter; uv/uvx retain their documented argv. | +| Database account can write | Keep write/cloud risk even when the intended workflow only queries. | +| Upstream only provides source/binary or publisher is unclear | Do not invent an installable npm/PyPI recipe or mark community code official. | +| FDE membership/order differs from the exact 20-ID set | Catalogue contract test fails; review the admission/removal explicitly. | +| FDE filter + search | Both apply; unrelated catalogue entries are excluded. | + +## 5. Good / Base / Bad Cases + +Good: a restricted CloudOps inventory configuration is built for one chosen +Agent, then validated by the customer in a test account. Base: a DBHub DSN +stays in env, with database permissions and TLS independently configured. +Bad: claim a prose “read only” instruction prevents SQL writes, or equate a +successfully saved CloudBase config with a successful login/deployment. + +## 6. Tests Required + +`tests/renderer/features/mcpCatalog.test.ts` freezes the exact ordered 20-ID +FDE membership and covers all eight new IDs, category, official/verify +metadata, nonempty docs/risk, required fields, zero targets, macOS/Windows +identity generation and no private values in argv/search. +Assert exact env keys, restricted tool selectors, STS omission and conditional +CA verification. `featurePages.test.tsx` checks FDE/search intersection while +retaining install-mode and ordinary install/assignment regressions. +These deterministic tests do not replace authenticated customer-side smoke. + +## 7. Wrong vs Correct + +Wrong: DSN in `--dsn` argv, an unverified `--readonly` flag, or an unrestricted +CloudOps toolset labeled read-only. Correct: DSN in env, accurately stated +database permissions, and the documented inventory-only CloudOps selector. diff --git a/.trellis/spec/frontend/mcp.md b/.trellis/spec/frontend/mcp.md index c10aa53e0..2ff2bc92d 100644 --- a/.trellis/spec/frontend/mcp.md +++ b/.trellis/spec/frontend/mcp.md @@ -26,6 +26,9 @@ live-file ordering, import, and conflict semantics are owned by QoderWork/TRAE external MCP preflight is not part of this management page; it uses `ExternalMcpPort` in the relevant Agent configuration flow. +FDE discovery membership and cloud/data command/env defaults are owned by +[FDE MCP Catalogue](./mcp-fde-catalog.md); do not duplicate those recipes here. + ## 2. Signatures The current management Port is exactly: diff --git a/.trellis/spec/frontend/models.md b/.trellis/spec/frontend/models.md index 577e70603..7fd50f958 100644 --- a/.trellis/spec/frontend/models.md +++ b/.trellis/spec/frontend/models.md @@ -69,6 +69,7 @@ interface ProvidersPort { checkReachability(baseUrl: string): Promise; checkModel(request: ModelProbeRequest): Promise; bindXaiManaged(request: BindXaiManagedRequest): Promise; + bindManagedProxy(request: BindManagedProxyRequest): Promise; fetchXaiManagedModels(accountId: string): Promise; } @@ -212,14 +213,14 @@ an apply instruction. The page sanitizes returned warning codes against the closed `CodexProviderMutationWarning` union. -### Existing Grok subscription to a local Agent +### Existing account subscription to a local Agent -[Managed Grok Subscriptions](./grok-subscription.md) owns the explicit +[Managed Account Subscriptions](./managed-account-subscriptions.md) owns the explicit account/model picker, binding DTOs, target-local failure/readback rules and -subscription regression matrix. Read it when changing `bindXaiManaged`, +subscription regression matrix. Read it when changing `bindManagedProxy`, `bindXaiManaged`, `fetchXaiManagedModels` or `XaiSubscriptionSection`. It extends this Models contract rather than duplicating authentication or Change Plan ownership. -Claude Code applies only after confirmation and authoritative rereads; Codex +Claude Code/Grok Build apply only after confirmation and authoritative rereads; Codex binding remains a draft before the existing Auth source-plan flow. WorkBuddy keeps its API-key workflow, and a saved source is not live entitlement evidence. diff --git a/.trellis/spec/frontend/prompt-presets.md b/.trellis/spec/frontend/prompt-presets.md new file mode 100644 index 000000000..a8610e131 --- /dev/null +++ b/.trellis/spec/frontend/prompt-presets.md @@ -0,0 +1,117 @@ +# Prompt Preset Catalogue + +## 1. Scope / Trigger + +Read before changing the static FDE catalogue, its browser, or copying a preset +into an application library. Native CRUD, live-file authority and seven-app +identity remain in [Prompts and Memory](./prompts-memory.md). + +## 2. Signatures and owners + +`src/pages/prompts/presets.ts` owns `FDE_PROMPT_PRESETS`, the exact six-value +category order and: + +```ts +type PromptPresetCategory = + | "delivery" + | "business" + | "industry" + | "public" + | "services" + | "engineering"; +``` + +`PROMPT_PRESET_CATEGORIES` presents those IDs in that order as `交付基础`, +`企业流程`, `产业运营`, `公共与专业服务`, `行业服务`, and `工程运维`. +The catalogue contains exactly 30 stable scenarios: five in each category. + +The public search boundary is: + +```ts +searchPromptPresets( + query: string, + category: PromptPresetCategory | "all", +): readonly PromptPreset[]; +``` + +`PromptPreset` has stable `id`, `name`, `category`, `description`, `tags`, +`inputs`, `approach`, `deliverables`, `evaluation`, `example`, and composed +`content`. `PresetBrowser.tsx` owns search/filter/preview selection only; +`Page.tsx` owns application, editor, discard intent and native mutations. +There is no new Port, native command, preference or persistence schema. + +## 3. Contracts + +- The catalogue is original static content, not an application library, a + vendor-endorsed template or a measured model benchmark. Each scenario has + specific inputs, implementation choices, deliverables, acceptance cases and + an example. The common contract covers evidence, untrusted external text, + tool approval and data minimization without demanding a fixed report format. +- Search matches every whitespace-separated, case-insensitive term across the + scenario `name`, `description`, category label, `tags`, `inputs`, `approach`, + `deliverables`, and `evaluation`, intersected with the chosen category. + `example`, composed shared boilerplate, credentials and native user records + are not hidden search authorities. +- Use shared `FeatureTabs` / `FeatureTabPanel` with `layout="workspace"`, + `FeatureSearch`, `FeatureList`, `SplitPanes` and `Button`. The preset panel + mounts on selection and unmounts on exit. The library/editor stays mounted + while browsing, so its draft is not discarded by a view-only tab change. +- Preview/search is available without native access. `使用此预设` requires + an existing successful library query and no active mutation. Browsing or + selecting a catalogue row never invokes a native write. +- Applying a preset creates a new draft containing copies of name, + description and content. It does not use the catalogue ID as a library ID, + mutate an existing row, save, enable, install MCPs or interpolate secrets. +- Replacing a dirty draft uses the existing `preset` discard intent and + shared ConfirmDialog. Cancel preserves the draft; confirm copies the preset + and returns to the library. App/route changes retain their existing guards. + Capture the Use button's origin with its persistent preset-tab return anchor: + the button unmounts on confirmation, and the shared dialog redirects an + inactive tab anchor to its currently active sibling instead of stealing focus. +- Save uses `prompt-` plus 16 cryptographically random bytes encoded as 32 hex + characters for new renderer-created records (`crypto.getRandomValues`), + preserves existing IDs on edit and sets new records `enabled: false`. + Do not require the secure-context-only `crypto.randomUUID` API or use a + timestamp as uniqueness. Repeated copies cannot replace an enabled record. + `upsert` and authoritative rereads are unchanged; enable remains a separate + explicit operation. + +## 4. Validation & Error Matrix + +| Condition | Required behavior | +| -------------------------------------------- | ------------------------------------------------------------------------- | +| No native library data / initial read failed | Browsing works, use is disabled; no synthetic native success. | +| Category ID/order or per-category count drifts | Catalogue contract test fails; do not silently reclassify saved presets. | +| Search/category matches nothing | Empty result with clear-filter action; no stale preview. | +| Browse while editor is dirty | Preserve the editor and its route blocker. | +| Apply while dirty | Confirm before replacing; cancel keeps user content. | +| Use or save while busy | Existing write lock prevents duplicate native operations. | +| Save fails | Retain editable draft and error; no enable or success claim. | +| Save succeeds, reread fails | Retain the existing uncertainty warning, not an invented effective state. | +| Copy same preset again | Independent disabled record with a new opaque ID. | + +## 5. Good / Base / Bad Cases + +Good: preview an environmental-report preset, edit project details, save a new +disabled Claude record, then separately choose whether to enable it. Base: +browser preview can read all templates but cannot save to a desktop library. +Bad: auto-seed every application, overwrite a preset-named existing record, or +invoke a suggested integration merely because its name appears in prompt text. + +## 6. Tests Required + +`tests/renderer/pages/prompts/presets.test.ts` checks the exact six category +IDs/order, five scenarios per category, 30 unique IDs/content bodies, complete +scenario fields, exact search-field intersections and shared safety boundaries. +`Page.test.tsx` covers zero-write preview, dirty cancellation/confirmation, +repeated copies, all seven libraries, disabled saves and native-only browsing. +Keep existing failed-save/readback, route guards and live-file tests. +`tests/browser/fde-presets.spec.ts` checks bounded readable preview, filters, +empty results and native-only behavior in Chromium/WebKit. Static structure +and browser fixtures do not establish model output quality or native writes. + +## 7. Wrong vs Correct + +Wrong: `upsert(app, { ...preset, id: preset.id, enabled: true })` on selection. +Correct: create an unsaved draft, explicitly save with a fresh 128-bit opaque ID and +`enabled: false`, then let the existing enable/readback workflow act separately. diff --git a/.trellis/spec/frontend/prompts-memory.md b/.trellis/spec/frontend/prompts-memory.md index 4be019aab..e5c2a31a3 100644 --- a/.trellis/spec/frontend/prompts-memory.md +++ b/.trellis/spec/frontend/prompts-memory.md @@ -14,7 +14,8 @@ directly; all effects cross `FeaturePorts` and the existing `src/shared/platform/**` boundary. Reuse is the default: Prompts and Memory share `FeatureSearch`, `FeatureList`, and Memory type `FeatureTabs`. New chrome that the other page will need goes in `src/shared/ui` on the -first commit. See [Frontend Reuse](./reuse.md). +first commit. See [Frontend Reuse](./reuse.md). Static FDE catalogue and +draft-only selection are owned by [Prompt Presets](./prompt-presets.md). The selected-Agent `提示词` section may present the existing prompt library, enable one entry, and link to `/prompts`, but it remains a consumer of this @@ -126,6 +127,9 @@ paths above. UUID primitive. A timestamp is display metadata, not uniqueness: repeated imports must not replace an enabled row or change its live projection. Existing persisted IDs remain valid and are not migrated. +- Renderer-created entries also use `prompt-` plus a fresh 128-bit random + hex ID rather than timestamps; existing records retain IDs when edited. Preset + selection creates an unsaved draft and new saves remain disabled. - The live-file inspector is collapsed by default under the editor. It reports the current native file content and is not an editable second source of truth. Do not keep it as a third always-open column that steals @@ -192,6 +196,8 @@ The fixed resource mapping is: completed but refresh failed; do not announce synchronized state. - Application, document, tab, daily-file, and route transitions share the same dirty-discard confirmation flow. Do not use `window.confirm`. + The Prompts library/preset tabs only change the view and preserve the editor; + applying a preset, not browsing it, requires dirty-discard confirmation. - Prompts select the application with `CatalogMasterDetail`, not a `