diff --git a/tasks/extension-setup-wizard/00-working-protocol.md b/tasks/extension-setup-wizard/00-working-protocol.md new file mode 100644 index 0000000..cd9af06 --- /dev/null +++ b/tasks/extension-setup-wizard/00-working-protocol.md @@ -0,0 +1,35 @@ +# Working Protocol + +## Accepted Collaboration Contract + +- The agent may explore, investigate, reason, and run bounded experiments + without requesting permission. +- Source or durable project mutation begins only after product design, HLD, and + an implementation-ready plan are complete and Sir explicitly says to start. + Task-packet maintenance is exempt. +- Each conversation round carries one bounded decision, or one small coherent + batch whose cognitive load is comparable to one decision. +- Every round reports the previous result before advancing to the next question. +- Sir's statements are proposals and evidence, not automatic authority; the + agent must independently evaluate them and may disagree with reasons. +- The agent owns proposing coherent solutions. Sir reviews and steers those + proposals; the agent must not outsource design work as a series of tiny + questions. +- Decisions, evidence, open questions, and plan revisions remain synchronized + in this packet. +- The agent stops only for meaningful review, a material decision, missing + information that changes the design, or a mutation gate. + +## Current Mode + +Product design -> HLD -> implementation plan are complete. The packet is at +Sir's plan-review gate. No source implementation is authorized until a new +explicit start after that review. + +## Repository Boundary + +This packet lives in `ext-reg` only as the active cross-repository control +surface for the current task. It does not transfer setup ownership to the +Registry. Expected implementation owners are primarily `client-web` and +`core-py`; stable cross-unit product or technical truth may later be promoted to +the shared InKCre docs through its normal ownership workflow. diff --git a/tasks/extension-setup-wizard/10-current-system.md b/tasks/extension-setup-wizard/10-current-system.md new file mode 100644 index 0000000..d40034a --- /dev/null +++ b/tasks/extension-setup-wizard/10-current-system.md @@ -0,0 +1,79 @@ +# Current-system Evidence + +Evidence was read from each repository's `origin/main` on 2026-08-13. Exact +baselines were ext-reg `f8aebd0`, Core `63f57b2`, Client `4fdc083`, and +organization policy `81258c1`. The local +`client-web` and `core-py` working branches are not based on current main, and +the Core worktree also contains unrelated task-packet changes; this task did not +modify either worktree. + +## Deployment and Web UI + +- One deployment-wide `extensions` row stores canonical Extension Name, exact + version, enabled Peer IDs, nickname, config, and config schema. +- That row currently has no Extension `state` field. `sources.state` is the + existing deployment-durable precedent, used for cursors such as the latest + collected item. Its current whole-object getter/setter does not provide an + atomic cross-Peer mutation contract and should not be copied unchanged. +- `client-web` currently offers install, list, JSON config editing, exact version + change, per-current-Peer enable/disable, and uninstall. +- `WebExtensionHost` resolves the exact Registry Release, checks the Web Host + SDK range before fetching the Module Federation Distribution, runs its + lifecycle, then commits the current Peer enablement. +- The Twitter Web Distribution exposes its default lifecycle module and tweet + content component. Its lifecycle is effectively empty; there is no setup + view, setup protocol, or setup status. + +## Core Host and Twitter Runtime + +- Core's Python Host installs and consumes a native wheel, validates its Host + SDK association, and publishes its Extension-owned routes, Sources, and + Resolvers only while the current Core Peer is enabled. +- Twitter's Core Extension config selects `official` or `twikit` and currently + includes Twitter application credentials and alternative Twikit login + fields. +- With the official backend enabled, the Extension publishes: + - `GET /twitter/auth/authorize` + - `GET /twitter/auth/callback` + - `POST /twitter/bookmark` +- The callback exchanges the authorization code, fetches the Twitter user, and + returns the token response. +- The resulting access token, refresh token, user ID, and handle live only on + the in-process `OfficialAPI` singleton. Their intended persistence calls are + commented out. Core disable closes and forgets the singleton. +- The bookmark Source cannot collect without this authenticated client. Source + cursor state such as `latest_tweet_id` is already durable Source state, but + OAuth credentials are not. + +## Immediate Consequences + +1. Installation is not setup. +2. Per-Peer enablement is not setup completion either: it only makes the Host + runtime and routes available. +3. A successful OAuth callback currently proves only an ephemeral session, so a + wizard that marks setup complete at that point would lie after restart. +4. Creating a bookmark Source is a distinct action from authentication and is + necessary before scheduled collection can occur. +5. The setup experience crosses the Web Host, at least one Core Peer, the + Twitter Web Distribution, the Twitter Core Distribution, and shared + deployment state. The Registry is only the source of the two Distributions. + +## Implementation-readiness Evidence + +- The current Core installed-row projection is named `ExtensionState` even + though it has no Extension-produced state. Its SQL store, runtime record, + ExtensionBase config callbacks and exact PostgreSQL role/trigger boundaries + were traced for the HLD rename and mutation plan. +- Core currently logs raw query parameters, and JWT middleware cannot exempt a + dynamically contributed callback. Both exact change points are known. +- Client's `Client.list()` and authenticated request helper already provide the + command-endpoint discovery transport; no new peer registry is needed. +- `InkDialog` 1.2.2 has the required `showCancel`, `showConfirm`, + `closeOnScrim`, default slot and model event happy path; no dialog framework + spike remains. +- Authlib 1.7.2 was executed in isolation with HTTPX mock transport to prove the + planned S256/token/refresh path. PDM 2.27.0 resolved its production dependency + graph in a temporary Core copy. +- The configured SSH Docker provider was reached successfully on 2026-08-13 + (Docker engine/client 28.5.2, Compose 2.40.3). It is the exact-image path for + Client contract generation because this Mac has no local Docker CLI. diff --git a/tasks/extension-setup-wizard/20-product-design.md b/tasks/extension-setup-wizard/20-product-design.md new file mode 100644 index 0000000..20e8add --- /dev/null +++ b/tasks/extension-setup-wizard/20-product-design.md @@ -0,0 +1,132 @@ +# Product-design Working Model + +## Proposed Product Concept + +An **Extension Setup** is a resumable, Extension-specific user journey that +turns an already installed and enabled Extension Release into the Extension's +declared minimum usable state across the deployment. It is whole-Extension +setup, not setup of one individual feature or one Peer. It is not a Registry +lifecycle state and is not a synonym for installed, enabled, or running. + +A **Setup Wizard** is the accepted presentation of that journey in +`client-web`: an action on an Extension card opens a popup whose content is an +Extension-owned multi-step installer. The setup entry requires at least that +this Web Peer has enabled and loaded the Extension; Core Peer enablement is a +separate prerequisite that the wizard may observe or guide. The durable product +contract is still the whole-Extension outcome and observable progress, rather +than the popup's currently displayed step number. + +## Accepted Twitter Completion Direction + +Twitter setup is complete when the whole Extension has reached its declared +minimum usable state. For the current Twitter Extension, bookmark collection is +the first concrete proof of that baseline rather than a separately scoped +feature wizard. At minimum all of the following are true: + +1. `inkcre/twitter` is installed at one exact deployment version. +2. A selected compatible Core Peer is enabled for that Extension and exposes + the Twitter setup operations. +3. The Twitter application configuration required by the selected backend is + valid. +4. A Twitter account authorization is durable and can be restored by that Core + runtime after restart. +5. Every resource declared necessary for the Twitter Extension's initial useful + operation exists; in the first vertical slice this includes at least one + eligible Twitter bookmark Source. +6. A bounded Extension-level readiness check confirms the authenticated account + and required initial resources can begin useful work; it does not need to + wait for a full historical sync. + +The Web Peer is enabled to render the setup experience, but that is only a means +of presentation. The resulting setup facts are deployment-wide rather than +owned by that Web Peer. + +## Product Principles Derived from the Proposal + +- Display progress from observed domain facts rather than one manually toggled + `setup_complete` boolean. +- Resume at the first unsatisfied prerequisite after reload, reconnect, or + partial failure. +- A user may leave the wizard and return without losing successful steps. +- Setup may use one reachable Core Peer as a command/OAuth endpoint, but that + does not make setup or future Source execution Peer-specific. +- The wizard guides the user through registering an X developer App and entering + its Client ID and Client Secret. Deployment-wide `extensions.config` is the + accepted authority for these user-declared values. Deployment-wide + `extensions.state` owns Extension-produced account credentials, identity and + OAuth transactions. Both use InKCre's trusted authenticated-Peer security + boundary. +- The Extension defines one minimum setup baseline. Optional capabilities added + later may have their own configuration journeys without retroactively making + the whole Extension "not set up". + +## Accepted UI Ownership + +The narrowest reusable split is: + +- `client-web` owns the Extension-card entry and popup container, including + opening, closing, focus containment and mounting/unmounting its content. +- The enabled Web Distribution owns everything inside that popup: stepper, + Back/Next/Cancel/Finish controls, step sequence, content, field validation, + loading and recoverable errors, Core-facing commands, observed completion + facts, and the final Extension-level readiness projection. +- The Extension Host SDK owns the small contribution contract that connects + those two sides. It does not know Twitter fields, OAuth endpoints, Sources, or + database tables. + +This avoids hard-coding Twitter setup into `client-web` and avoids a premature +generic wizard engine. Extensions may need materially different setup flows; +their own Host SDK-bound UI is the correct place to express those differences. +Only popup behavior remains consistent across Extensions. + +The setup action is available once this Web Peer has successfully enabled and +loaded the Extension, because its Web Distribution contributes the popup +content. This is a minimum availability condition, not a claim that this Web +Peer performs the Extension's Core work. Installed-but-disabled cards should +explain that the Extension must first be enabled here instead of offering a +broken setup action. + +## Accepted Minimum Web Extension API + +The Web Host SDK adds one optional setup contribution to the already +loaded Web Extension module. Conceptually: + +```ts +interface WebExtensionModule { + initialize?(): Promise + activate?(): Promise + deactivate?(): Promise + dispose?(): Promise + + setup?: { + component: VueComponent + } +} +``` + +The Host renders `setup.component` inside its popup. The component owns its +entire flow and emits only a request to close the containing popup. A successful +Finish may request close, but it does not pass a `setup_complete` value back to +the Host; reopening the component reconstructs progress from Core-visible +facts. + +This API deliberately does not include `steps`, `currentStep`, validation, +progress persistence, OAuth, Core Peer selection, Source creation, or a generic +setup status. Those are Extension-owned semantics. HLD closes the exact Vue +type and `close` event in [Web Host Contribution and Wizard UI](34-hld-web-setup.md). + +## HLD Closure + +The formerly open implementation shapes are now closed in HLD: + +- the Web contribution is one Vue `Component` plus a `close` event; +- Twitter owns its exact config/state/setup protocol and Authlib OAuth client; +- Core owns one exact public callback contribution and database state + transactions; +- Finish explicitly enqueues the first bounded collection job; an unlikely duplicate + enqueue is acceptable for this single-user product; +- the first release does not expose a newest-versus-history choice. + +See [Canonical Extension State](32-hld-extension-state.md), +[Twitter Setup and OAuth Protocol](33-hld-twitter-protocol.md), and +[Web Host Contribution and Wizard UI](34-hld-web-setup.md). diff --git a/tasks/extension-setup-wizard/21-twitter-wizard.md b/tasks/extension-setup-wizard/21-twitter-wizard.md new file mode 100644 index 0000000..5fb4e84 --- /dev/null +++ b/tasks/extension-setup-wizard/21-twitter-wizard.md @@ -0,0 +1,131 @@ +# Twitter Wizard Proposal + +## External Product Constraints + +The current X OAuth documentation establishes these facts: + +- An X App must have OAuth 2.0 enabled and its redirect URI must exactly match a + callback URL configured in the Developer Console. +- Web applications are confidential clients and must keep their Client Secret + within the application's accepted trust boundary. InKCre's boundary permits + deployment-wide Extension config to contain it and authenticated Peers to + read that config; URLs and unauthenticated surfaces remain outside the + boundary. +- `offline.access` is required to receive a refresh token and restore user access + without another interactive login. +- Bookmark lookup requires a user access token with `bookmark.read`; `tweet.read` + and `users.read` support the returned posts and authenticated user. The current + Extension does not need `bookmark.write` to collect bookmarks. + +References: + +- [X OAuth 2.0 Authorization Code with PKCE](https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code) +- [X user access-token flow](https://docs.x.com/fundamentals/authentication/oauth-2-0/user-access-token) +- [X bookmarks lookup](https://docs.x.com/x-api/posts/bookmarks/quickstart/bookmarks-lookup) + +## Accepted First-release Product Boundary + +### OAuth App setup + +The user registers an X developer account/App, configures the callback URL, and +enters the resulting Client ID and Client Secret as part of the setup wizard. +The wizard must provide the exact callback URL and guidance needed to finish +that external registration. + +The credentials configure the deployment-wide Twitter Extension, not the Web +Peer that rendered the wizard. They are persisted in canonical Extension config +under the accepted authenticated-Peer security boundary. Account tokens and +identity produced by OAuth are persisted in canonical Extension state. The +wizard uses validated Twitter Core commands rather than inventing a second +persistence authority. Credentials still must not be placed in URLs, +unauthenticated responses or logs. + +### Supported account path + +The first release supports the official X OAuth backend only. Do not expose the +existing Twikit username/password/TOTP backend in this wizard. It is a separate, +less stable and higher-risk product path that would multiply credential and +recovery semantics before the official flow works end to end. + +Request only `tweet.read users.read bookmark.read offline.access`. Add write +scopes in a later Extension version only when an accepted user-facing feature +actually writes bookmarks. + +## Proposed Four-step Wizard + +### Step 1 — Prepare + +Explain what the Twitter Extension will do and the permissions it will request. +Then guide and inspect prerequisites: + +- this Web Peer is already enabled and the setup component is loaded; +- at least one reachable Core Peer can run the same installed Twitter Release; +- Twitter is enabled on at least one suitable Core Peer, or the user explicitly + authorizes enabling it there; +- the user has registered an X developer App with the exact callback URL; +- the user enters Client ID and Client Secret, which are submitted to Core and + reported only as configured/not configured afterward. + +If several suitable Core endpoints exist, one may be chosen as the command/OAuth +exchange endpoint. It does not scope setup to that Peer or assign permanent +ownership of the Source or its future jobs; Core's state/domain authorities +retain concurrency responsibility. + +### Step 2 — Connect account + +Show the exact scopes and start official X OAuth. After callback, display the +authenticated account identity and prove that its durable authorization can be +restored. Existing valid authorization is shown and may be reused; reconnect is +an explicit action rather than an automatic overwrite. + +### Step 3 — Bookmark collection + +List existing Twitter bookmark Sources. Let the user reuse one or create the +initial Source with the small set of user-relevant inputs: + +- nickname; +- collection schedule; + +The first release does not expose a newest-versus-history choice. It uses the +current bounded `full=false, result_limit=40` initial job. A later product +decision may add a history policy once collection semantics can support it +without misleading empty-state behavior. + +The UI disables the creation action while its request is pending. An unlikely +duplicate Source from a repeated or concurrent request is acceptable; Source +identity and cursor state remain Core-owned domain facts. + +### Step 4 — Review and start + +Show the chosen account, Source, and schedule. Run a bounded readiness command +that verifies the durable authorization and required Source state. On explicit +Finish, enqueue the first collection job and close when Core accepts the +command. A repeated Finish may enqueue another Job; the wizard does not wait for +a complete bookmark history sync. + +## Derived Resume Model + +On every open, the Twitter setup component reads one Extension-specific setup +projection and resumes at the first unsatisfied condition: + +```text +Core unavailable or disabled -> Prepare +OAuth App unavailable -> Prepare / operator action +No restorable account -> Connect account +No eligible bookmark Source -> Bookmark collection +Ready facts satisfied -> Review and start / already configured summary +``` + +Transient X or network failure is shown as an operational readiness error, not +by deleting already durable account or Source facts. Revoked/invalid refresh +credentials move the account back to “Reconnect required.” + +## Intentional Non-goals + +- No generic Host-owned wizard state machine. +- No Twikit setup path in the first release. +- No permanent Source-to-Core-Peer assignment. +- No second credential store alongside canonical Extension config. +- No wait for complete initial synchronization before the wizard may finish. +- No silent enablement of a Core Peer; enabling is an explicit user-authorized + action within Prepare. diff --git a/tasks/extension-setup-wizard/22-setup-authority-and-protocol.md b/tasks/extension-setup-wizard/22-setup-authority-and-protocol.md new file mode 100644 index 0000000..3689d5b --- /dev/null +++ b/tasks/extension-setup-wizard/22-setup-authority-and-protocol.md @@ -0,0 +1,111 @@ +# Setup Authority and Protocol + +## Corrected Security Boundary + +The canonical deployment-wide `extensions.config` is the accepted authority +for Extension configuration, including Secrets. Authenticated Peers are inside +InKCre's chosen trust boundary and may read it through the shared database +protocol. Current Twitter Client ID/Secret fields already follow this model. + +The earlier proposal for a second Core-only Extension secret store is rejected: +it would duplicate authority, add schema and lifecycle complexity, and impose a +security boundary the product has not chosen. + +## Accepted Authority Split + +### Extension config + +Twitter's deployment-wide Extension config owns: + +- OAuth App Client ID and Client Secret; +- other whole-Extension settings that are not Source-instance-specific. + +Twitter Core commands validate and update this config through the Extension Host +SDK's existing config authority. Multiple Core deployments restore the same +values from the shared database. No Peer ID participates in their identity. + +Schema/UI masking may improve accidental-disclosure UX, but does not change the +accepted authenticated-Peer access boundary. Credentials must still stay out of +URLs, unauthenticated responses and logs. + +### Extension state + +The canonical deployment-wide Extension state owns facts produced and evolved +by the Extension rather than declared by the user. For Twitter this includes: + +- connected-account access and refresh credentials; +- restorable account identity and token metadata; +- OAuth `state`, PKCE verifier, expiration, status and error category for each + short-lived authorization transaction. + +The fact that some values are Secrets does not decide whether they are config or +state. Authorship and lifecycle do: Client ID/Secret are user-declared config; +OAuth account credentials and transactions are Extension-produced state. Both +remain inside InKCre's accepted authenticated-Peer trust boundary. + +Extension state is not wizard UI progress. It must not contain `current_step` or +`setup_complete`. It persists across Core restart and Peer disable/re-enable, +and is shared by every Peer in the deployment. Source cursor and job state still +belong to the Source domain. + +### Sources + +Bookmark Source identity, schedule, config, cursor and jobs remain in the +existing Source domain. The wizard composes that domain and does not duplicate +Source facts inside Extension config. + +## Twitter Setup Projection + +The Twitter Core Distribution exposes one deployment-wide setup projection: + +```text +TwitterSetup + release/version + command_endpoint_status + oauth_app + status: missing | configured | invalid + callback_url + client_id? + account + status: disconnected | connected | reconnect_required + user_id? + handle? + bookmark_sources[] + id, nickname, schedule, latest_job_status? + readiness + status: incomplete | ready | temporarily_unavailable + blockers[] +``` + +This is a convenient validated projection over Extension config, Extension +state, runtime checks and Source facts. It contains no `current_step` or +manually stored `setup_complete`; the Web Distribution derives its display and +step. + +## Twitter Commands + +The Twitter-specific Core API provides these semantic commands. Their exact HTTP +routes and payload schemas are closed in +[Twitter Setup and OAuth Protocol](33-hld-twitter-protocol.md): + +1. **Configure OAuth App** — validate Client ID/Secret and replace the relevant + Extension config fields coherently. +2. **Begin account authorization** — create a short-lived state/PKCE transaction + in Extension state and return the X authorization URL plus opaque transaction + reference. +3. **Complete account authorization** — consume the callback, exchange the code, + validate account/scopes, and atomically persist account credentials, identity + and terminal transaction status in Extension state. +4. **Read authorization transaction** — return only its bounded polling + projection: pending, succeeded, failed or expired. +5. **Disconnect/reconnect account** — explicitly clear or replace account state; + reconfiguration never silently overwrites a working account. +6. **Create or select bookmark Source** — compose the Source authority through + its ordinary create/select operations; the Web UI prevents normal duplicate + submission. +7. **Finish and start** — validate deployment facts and enqueue an initial + collection job, returning readiness. Rare duplicate Jobs are acceptable. + +One reachable enabled Core Peer processes a command, but the command mutates +deployment-wide config, state and Source authorities. It is a transport +endpoint, not the semantic owner of setup. diff --git a/tasks/extension-setup-wizard/23-oauth-callback.md b/tasks/extension-setup-wizard/23-oauth-callback.md new file mode 100644 index 0000000..0069340 --- /dev/null +++ b/tasks/extension-setup-wizard/23-oauth-callback.md @@ -0,0 +1,82 @@ +# OAuth Callback Proposal + +## Rejected Alternative + +`postMessage` from the Core callback to the Twitter Web wizard is rejected. It +would require the Core HTTP origin to know an allowed `client-web` origin, +window-opener topology and a browser message contract. That browser-level +coupling is not justified merely to auto-advance a setup step. + +X currently documents authorization-code with PKCE and refresh-token as its +supported OAuth 2.0 grant types; it does not provide a device-code flow that +would remove the callback while retaining a native polling protocol. + +Reference: +[X OAuth 2.0 Authorization Code with PKCE](https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code). + +## Accepted Decoupled Flow + +Keep the OAuth callback inside the Twitter Core Distribution, but make it fully +standalone. The Twitter Web Distribution observes the Core transaction by +polling its ordinary Extension API. + +```text +Twitter wizard modal remains open + -> Begin authorization on one enabled Core command endpoint + <- authorization URL + opaque transaction reference + -> open X authorization in a separate window/tab +X redirects that window to the exact Twitter Core callback URL +Core validates state/PKCE, exchanges code, atomically updates extensions.state +Core marks transaction succeeded/failed in state and renders a standalone result page + +Meanwhile, independently: +Twitter wizard polls transaction reference on the chosen Core endpoint + -> pending | succeeded | failed | expired + -> on succeeded, reload deployment-wide setup projection + -> advance to the account summary +``` + +There is no communication from callback page to Web page. The callback page +does not know the Web origin, opener or popup. It simply tells the user that the +authorization completed and the window may be closed. + +## Product Experience + +- The wizard opens the authorization window and displays “Waiting for X…” with + Cancel and Retry actions. +- It polls at a modest interval and has a visible expiration deadline; no + permanent connection, SSE or WebSocket is required. +- Success normally advances without asking the user to click another button. +- If the authorization window cannot be opened, expose the authorization URL + as a deliberate “Open X” action. The user may return to the still-open wizard. +- If the wizard, tab or browser closes, the OAuth callback still completes. On + reopen, the wizard derives the connected account from `extensions.state` and + resumes correctly, even though the transient transaction has expired. +- The result page may offer a plain close-window button or textual instruction; + automatic close is optional and carries no product correctness. + +## Boundary Rules + +- Client ID/Secret and account tokens never appear in callback URLs or polling + responses. +- The transaction reference is opaque, TTL-bound and single-use; polling is + authenticated under the existing Peer/Core trust contract. +- X `state` is independently validated by Core and cannot be substituted with + the public transaction reference unless HLD proves equivalent entropy and + binding. +- Replay, mismatched state and expired flow fail without replacing existing + credentials. +- Cancellation stops Web polling but does not rely on canceling an already-open + X page. A later valid callback may still complete and will be reflected when + setup is reopened. +- One Core endpoint starts, completes and reports a transaction. The resulting + Extension state remains deployment-wide and future collection is not bound + to that Peer. + +## Why This Is the Smallest Coherent Choice + +Polling is a Twitter Web target -> Twitter Core target dependency, which the +Extension already needs for setup commands. It does not add a Core callback -> +`client-web` deployment dependency. It uses the provider's supported happy path, +keeps the confidential exchange in Core, survives lost browser UI, and avoids +building a generic callback broker or real-time notification subsystem. diff --git a/tasks/extension-setup-wizard/24-extension-state.md b/tasks/extension-setup-wizard/24-extension-state.md new file mode 100644 index 0000000..13158fc --- /dev/null +++ b/tasks/extension-setup-wizard/24-extension-state.md @@ -0,0 +1,123 @@ +# Extension State Proposal + +## Why Setup Changes the Earlier MVP Boundary + +The Registry MVP previously deferred general Extension state because no +accepted vertical slice required it. Twitter setup now supplies that need: +OAuth authorization produces account credentials and a resumable callback +transaction that must survive Core restart, Web closure and Peer handoff. These +facts are neither user-declared config nor Source-instance state. + +Core already has a Source precedent: `sources.state` is deployment-durable and +Source-owned. It confirms the product distinction, but its current whole-object +`get_state`/`set_state` implementation is not a concurrency contract to copy. +Sir has already established that shared-state concurrency belongs to Core's +state authority rather than Extension code or an Extension Host SDK. + +## Accepted Canonical Model + +Add one non-null JSON object to the existing canonical Extension installation +record: + +```text +extensions + name + version + enabled[] + nickname + config + config_schema + state JSON object, default {} +``` + +Do not add an `extension_states` table. One installed Extension already has one +deployment-wide identity and one state authority; a second one-to-one relation +would add joins and lifecycle without adding a distinct owner. + +The semantic split is: + +| Authority | Written by | Twitter examples | +| --- | --- | --- | +| Extension config | User/operator through validated Extension commands | OAuth App Client ID and Client Secret | +| Extension state | Extension through its Core Host SDK | account tokens and identity; OAuth transactions | +| Source config/state | Source domain | bookmark Source schedule/config and collection cursor | +| Runtime memory | one running Peer process | HTTP clients, locks, loaded module objects | + +Secrets may appear in config or state under the already accepted trust boundary; +secrecy does not define the domain category. + +## Core State Authority and Host SDK Boundary + +The Core state authority owns persistence, serialization and cross-Peer +concurrency, ultimately backed by database transactions and constraints. The +Extension Host SDK is only the typed interface through which Extension code asks +that authority to read or mutate state; it does not itself guarantee concurrency +or become the state owner. + +A concrete Extension declares both validated types conceptually: + +```python +class TwitterExtension( + ExtensionBase[TwitterConfig, TwitterState], + config_cls=TwitterConfig, + state_cls=TwitterState, +): + ... +``` + +The Extension receives typed config/state through `ExtensionBase` operations; +the SQL model and database session are not passed to Extension code. The Host +SDK delegates state mutation to one Core-owned semantic operation. HLD selects +the smallest authoritative implementation: a database transaction with exact-row +`SELECT ... FOR UPDATE`, described in +[Canonical Extension State](32-hld-extension-state.md). Extension code and the +Host SDK do not implement cross-Peer locking themselves. + +State is not a generic form surface, so the MVP does not persist a +`state_schema` merely for UI generation. The concrete Extension state type is +the validation and migration authority. The Twitter Web Distribution consumes +the Twitter setup projection and commands rather than treating raw state JSON +as its UI contract. + +## Twitter State Shape + +The product shape is intentionally narrower than an exact schema: + +```text +TwitterState + account? + access_token + refresh_token? + expires_at + scopes[] + user_id + handle + oauth_transactions + + provider_state_binding + pkce_verifier + status: pending | succeeded | failed | expired + created_at + expires_at + error_category? +``` + +Transactions are TTL-bounded and cleaned after a bounded observation window; +their opaque reference is not a credential and their polling projection never +returns provider state, PKCE or tokens. A successful callback commits account +state and terminal transaction status atomically so polling cannot observe +success without restorable credentials. + +## Lifecycle Invariants + +- State survives Peer disable/re-enable, Core restart and Web closure. +- It is not scoped to the Core endpoint that handled a command. +- Wizard step position and `setup_complete` are never stored in it. +- Extension readiness remains a projection over config, state, Sources and + runtime availability. +- Uninstall removes the Extension row and therefore its config/state, without + inspecting or deleting Sources. Unreachable Source types are handled by the + Source domain's existing lifecycle logic. +- Upgrade, rollback, reconfiguration and the first-release compatible state + contract are closed in the lifecycle proposal and HLD; a general migration + engine remains intentionally outside the task. diff --git a/tasks/extension-setup-wizard/25-state-and-setup-lifecycle.md b/tasks/extension-setup-wizard/25-state-and-setup-lifecycle.md new file mode 100644 index 0000000..f75a5fc --- /dev/null +++ b/tasks/extension-setup-wizard/25-state-and-setup-lifecycle.md @@ -0,0 +1,65 @@ +# Extension State and Setup Lifecycle Proposal + +## Accepted Product Model + +### Reopen and manage + +Once setup is ready, the same card action reopens the Extension-owned wizard as +a setup summary and management flow. It still derives truth from config, state, +Sources and runtime availability; it does not become a separate settings store. + +### OAuth App reconfiguration and reconnect + +- Changing Client ID/Secret while an account is connected must preview the + impact and require explicit confirmation. +- Committing different App credentials atomically updates config and invalidates + the account authorization and outstanding OAuth transactions that were bound + to the old App. Sources are preserved and readiness becomes incomplete until + reconnection succeeds. +- Reconnecting with unchanged App credentials does not replace a working account + when authorization begins. A successful callback atomically replaces account + state; failure or expiration leaves the previous working account intact. + +### Disconnect + +Disconnect is an explicit command. It clears account credentials and outstanding +OAuth transactions but preserves OAuth App config and Twitter Sources. The +Extension remains installed and enabled, while readiness returns to +`disconnected`; Core must not accept new Twitter collection work until a usable +account is restored. + +### Peer disable and re-enable + +Disabling a Web Peer removes the local setup entry because its Web Distribution +is no longer loaded. Disabling a Core Peer removes that command/runtime endpoint. +Neither operation deletes deployment-wide config, state or Sources. Re-enabling +a Core Peer restores from shared state and revalidates it; operational failure is +reported as readiness, not by erasing durable facts. + +### Upgrade and rollback + +The first MVP does not add a general Extension-state migration engine or a +Registry state-compatibility declaration. Core therefore has no objective, +side-effect-free way to prove that a different Distribution version can consume +non-empty state. Version change is allowed only when the canonical state is the +empty object; otherwise upgrade or rollback fails before changing the installed +version or state. Same-version install remains idempotent. The initial +`0.1.1 -> 0.2.0` Twitter cut is allowed because the newly added state column is +empty. A later task must define an explicit migration contract before preserving +non-empty state across versions; Core must not infer compatibility from Python +types by installing untrusted future bytes into the active interpreter. + +### Uninstall + +Uninstall still requires every Peer to be disabled. It removes the canonical +Extension row and therefore its config and state. It does not inspect, block on, +or delete Source records. Source is a separate authority and already owns the +behavior for records whose type is no longer reachable. Reinstalling a +compatible Distribution may make that type reachable again without reconstructing +Source records from Extension state. + +### OAuth transaction cleanup + +Pending and terminal OAuth transactions have bounded expiry and observation +windows. Cleanup removes only expired transaction entries. It does not remove a +working account, change wizard progress, or affect Sources. diff --git a/tasks/extension-setup-wizard/26-host-sdk-boundary.md b/tasks/extension-setup-wizard/26-host-sdk-boundary.md new file mode 100644 index 0000000..b011a4e --- /dev/null +++ b/tasks/extension-setup-wizard/26-host-sdk-boundary.md @@ -0,0 +1,77 @@ +# Setup and Host SDK Boundary Proposal + +## Accepted Product Boundary + +There is no language-neutral or Host-owned Setup Wizard API. Setup is an +Extension product concern assembled from two Peer-specific Extension APIs and a +Twitter-specific cross-target protocol. + +## Web Host SDK + +The generic Web contribution remains deliberately small: + +```ts +interface WebExtensionModule { + setup?: { + component: VueComponent + } +} +``` + +`client-web` detects the contribution on the already loaded Extension, shows the +card action, owns the popup shell, mounts the component and lets it request that +the shell close. It does not own steps, progress, readiness, OAuth, Core Peer +selection or Source commands. + +The setup component may use the ordinary `@inkcre/core` Web Peer APIs available +to an Extension. The Host SDK does not proxy every Core contribution through a +new setup context. + +## Core Host SDK + +Core's generic addition is deployment-wide Extension state, not a setup engine: + +```python +class ExtensionBase[Config, State]( + config_cls=..., + state_cls=..., +): + ... +``` + +The Host SDK gives Extension code typed config/state access without exposing the +SQL model. State mutations are delegated to Core's shared state authority; the +SDK does not guarantee concurrency. Existing lifecycle, route, Source and direct +Peer-internal contribution patterns remain Peer-specific Core Extension API. + +The Core Host SDK does not define OAuth providers, transactions, setup steps, +readiness blockers, bookmark Sources or a generic setup projection. + +## Twitter-owned Cross-target Protocol + +The Twitter Web and Core Distributions jointly own: + +- the setup projection; +- OAuth App validation and account authorization commands; +- authorization transaction polling; +- the standalone callback/result page; +- bookmark Source selection/creation; +- readiness and Finish/start semantics. + +This protocol uses the existing authenticated Web-to-Core Extension route and +Peer discovery/delegation facilities. It is not promoted to `client-web`, Core, +the Registry, or a generic OAuth/setup framework merely because two Twitter +Distributions consume it. + +## Resulting Topology + +```text +client-web card/popup shell + -> Twitter Web setup component + -> ordinary Web Peer/Core access facilities + -> Twitter Core setup API + -> Core config/state authorities + Source domain + X OAuth +``` + +Each layer owns one existing concern. The only new generic Host SDK surfaces are +the optional Web setup component and typed Core Extension state access. diff --git a/tasks/extension-setup-wizard/27-vertical-slice-and-acceptance.md b/tasks/extension-setup-wizard/27-vertical-slice-and-acceptance.md new file mode 100644 index 0000000..2f32daa --- /dev/null +++ b/tasks/extension-setup-wizard/27-vertical-slice-and-acceptance.md @@ -0,0 +1,98 @@ +# Minimum Vertical Slice and Deferred Black-box Acceptance + +## Accepted Product Slice + +The minimum slice is not “render four steps.” It must prove that an installed +Twitter Release becomes durably useful through its real Web and Core +Distributions, while preserving the accepted config/state/Source authorities. + +## Included Product Surface + +1. The canonical Extension record has deployment-wide validated state. +2. The Core Host SDK exposes typed state access while Core/database remains the + persistence and concurrency authority. +3. Twitter Core provides its setup projection, OAuth App/account/transaction + commands, standalone callback page, bookmark Source composition and bounded + readiness/Finish command. +4. Twitter Web contributes the four-step setup component. +5. `client-web` exposes the Extension-card action and popup shell after the local + Web Distribution is enabled. +6. The Registry publishes both updated Twitter Distributions under one exact + Extension Release version; the deployment consumes those native + Distributions through their existing Host SDKs. + +No generic wizard engine, OAuth broker, transaction service, notification +channel, new secret store, state table or Source ownership mechanism is part of +this slice. + +## Primary Happy-path Journey + +Starting from a fresh installed Twitter Release with the current Web Peer +enabled and no Core Peer enabled: + +1. The Extension card offers **Setup**; opening it mounts Twitter's stepper in + the `client-web` popup. +2. Prepare shows the exact callback URL and detects that Core is unavailable. + The user explicitly enables Twitter on one suitable Core Peer. +3. The user enters an X OAuth App Client ID/Secret. Reopening the wizard shows + the App as configured without returning the Secret. +4. Connect starts authorization and opens X in a separate window/tab. The Core + callback completes on its own origin and displays a standalone terminal + result. The still-open wizard advances only by polling its opaque transaction + reference. +5. Account credentials and identity survive closing/reopening the wizard and a + Core process restart. An alternate suitable Core Peer can read the same + deployment state; the account is not owned by the callback Peer. +6. The user reuses or creates one bookmark Source, chooses its user-facing + schedule/options, reviews the result and explicitly finishes. +7. Finish enqueues the initial collection job, and the first bounded collection + records at least one bookmark through the ordinary Source pipeline. Reopening + setup reports the Extension as ready from durable account/Source/Cron facts. + +## Required Behavioral Coverage + +- Invalid, expired, replayed or mismatched OAuth callback state never replaces + account state and yields a bounded transaction error. +- Closing the Web popup does not break an in-flight provider callback; reopening + derives connected status from durable state. +- A failed reconnect leaves an existing working account untouched. +- Core unavailability leaves config, state and Source facts unchanged and makes + readiness temporarily unavailable rather than incomplete. +- The Web UI disables Source creation and Finish while their request is pending. + Rare duplicate Source rows or initial Jobs are acceptable and recoverable in + this single-user product. +- Disabling/re-enabling either Peer preserves deployment setup facts; disabling + Web only removes the local entry, while disabling Core removes that command + endpoint. +- Uninstall deletes the Extension record without inspecting or deleting Source + records; the Source domain then exposes its existing unreachable-type behavior. +- Client Secret, PKCE verifier and tokens never appear in URLs, + polling/setup projections, unauthenticated responses or logs. OAuth `state` + appears only where the authorization-code protocol requires it—in the provider + authorization and callback URLs—and is excluded from application logs and + polling/setup projections. + +## Deferred Black-box Acceptance + +Black-box acceptance is explicitly deferred and owned by Sir. It is not a gate +for the implementation PRs in this task. The implementation still needs focused +unit, contract, integration and build evidence proportional to each repository, +but it must not create a provider simulator or real-X deployment workflow merely +to replace the deferred acceptance authority. + +When resumed, black-box acceptance should select its own deterministic/external +strategy and use the behavioral outcomes above as input rather than inheriting a +test harness chosen prematurely here. + +## Product-design Exit Gate + +Product design is complete with this slice accepted and black-box acceptance +deferred. HLD now specifies exact state authority calls, Twitter Web/Core schemas +and routes, callback addressing, provider client seam, database migration, +repository changes, delivery sequence and verification commands. The resulting +[implementation plan](40-implementation-plan.md) is at Sir's review gate before +source work may start. + +The authorized future implementation boundary ends when the relevant PRs are +ready for review. Merge, Release publication, deployment and black-box acceptance +remain outside this task unless separately authorized. diff --git a/tasks/extension-setup-wizard/30-decisions-and-questions.md b/tasks/extension-setup-wizard/30-decisions-and-questions.md new file mode 100644 index 0000000..d86bf7c --- /dev/null +++ b/tasks/extension-setup-wizard/30-decisions-and-questions.md @@ -0,0 +1,429 @@ +# Decision and Question Log + +## Accepted Inputs + +### I001 — Task identity + +The new task is named **Extension Setup Wizard**. + +### I002 — First vertical slice + +Use Twitter as the design and acceptance example. The motivating useful outcome +is collecting Twitter bookmarks, and OAuth login is at least one required setup +step. + +### I003 — Presentation Peer + +The guided experience should be available in `client-web`. + +### D001 — Whole-Extension scope + +Accepted. The wizard sets up the Extension as a whole, not only bookmark +collection. For Twitter, working bookmark collection is the initial concrete +proof of the Extension's minimum useful baseline. + +### D002 — Entry and availability + +Accepted. The setup entry is on the Extension card in the `client-web` +Extensions page and becomes available once at least this Web Peer has enabled +and loaded the Extension. This does not imply that any Core Peer is enabled and +does not scope setup to this Web Peer; setup is deployment-wide. + +### D003 — Interaction form + +Accepted. Setup opens in a popup and uses an ordered, multi-step installer-like +flow that guides the user one step at a time. + +### D004 — Truth-derived completion + +Accepted. Setup progress and completion come from durable, observable domain +facts. A one-time OAuth callback or a manually stored completion boolean is not +sufficient. + +### D005 — Popup versus stepper ownership + +Accepted. `client-web` owns only the popup container. The Extension's Web +Distribution owns the stepper and everything inside the popup, including its +navigation and step state. + +### D006 — User-configured OAuth App + +Accepted. The wizard guides the user through registering the X developer App +and entering Client ID and Client Secret. These configure the deployment-wide +Twitter Extension. + +### D007 — Extension config security boundary + +Accepted correction. Canonical deployment-wide `extensions.config` may contain +Secrets; authenticated Peers are inside InKCre's chosen access boundary. Do not +introduce a second Core-only Extension secret store. + +### D008 — No callback-to-Web browser coupling + +Accepted. The Core OAuth callback must not depend on the `client-web` origin, +opener relationship or a `postMessage` contract, even if removing that coupling +costs some automatic-return UX. + +### D009 — Decoupled OAuth observation + +Accepted. The Core callback renders a standalone result and never communicates +with the Web page. The Twitter Web Distribution polls a short-lived transaction +through the ordinary Twitter Core API and reloads the setup projection after a +terminal result. + +### D010 — Deployment-wide Extension state + +Accepted in principle. Setup introduces a real general Extension-state need: +Extension-produced durable facts such as Twitter account credentials and OAuth +transactions must survive restart and Peer handoff without being mislabeled as +user config or Source state. Reuse the canonical Extension installation row +rather than adding a second one-to-one state table; review the exact model in +[Extension State Proposal](24-extension-state.md). + +### D011 — Extension-state concurrency authority + +Accepted correction. The Extension Host SDK is a typed access interface, not +the concurrency authority. Core's shared state authority owns persistence and +cross-Peer serialization, backed by database transactions and constraints. The +Extension and Host SDK do not implement distributed locking. + +### D012 — Uninstall does not inspect Sources + +Accepted correction. Uninstall deletes the canonical Extension record and its +config/state directly. It neither checks nor deletes Sources. The Source domain +owns behavior for records whose type becomes unreachable. + +### D013 — Host SDK setup boundary + +Accepted. The Web Host SDK contributes and mounts an optional setup component; +the Core Host SDK provides typed Extension-state access while delegating to +Core's authority. Twitter's Web/Core Distributions own the entire setup +projection and protocol. No generic setup or OAuth engine is introduced. + +### D014 — Delivery stops at PR review + +Accepted. Black-box acceptance is deferred to Sir. This task's eventual source +delivery ends with relevant PRs ready for review; it does not merge, publish, +deploy or perform black-box acceptance without separate authorization. + +### D015 — Canonical state authority + +Implementation baseline. Add `extensions.state JSONB NOT NULL DEFAULT '{}'`; +Core/PostgreSQL owns row-lock transactions and direct-write restrictions. +Extension code receives typed inherited operations, not SQL rows/sessions. The +misnamed installed-row `ExtensionState` types are renamed to +`InstalledExtension`/`ExtensionStore`. + +### D016 — Exact public callback contribution + +Implementation baseline. A running Extension may claim an exact method/path +that it actually published. JWT bypass exists only while that claim is active +and is withdrawn with the runtime publication. Twitter claims only +`GET /twitter/auth/callback`. + +### D017 — Twitter OAuth library and state machine + +Implementation baseline. Use Authlib `AsyncOAuth2Client` with HTTPX, PKCE S256, +confidential-client Basic token authentication, read/offline scopes and a +durable transaction/account state machine. A temporary executable probe proved +the chosen library path and frozen dependency resolution. Callback exchange is +claimed once by `pending -> exchanging`; it is not lease-reclaimable because a +crashed process cannot know whether X consumed the one-use code. + +### D018 — Source and Finish semantics + +Superseded in part by D030. The first slice reuses a selected bookmark Source or +uses ordinary Source creation. Existing Sources remain independent and +selectable. Finish enqueues one bounded initial Job; rare duplicate Sources or +Jobs are acceptable. No history-mode choice or Source check is added to +uninstall. + +### D019 — Host and Extension versions + +Implementation baseline. Core Python and `@inkcre/core` advance to `0.1.1`; +both Twitter native Distributions advance together to Extension Release +`0.2.0` and require Host SDK `>=0.1.1 <0.2.0`. Existing Extension ranges remain +compatible. + +### D020 — Cross-repository delivery + +Implementation baseline. Prepare one Core PR and one dependent Client PR. Use +the configured SSH Docker provider to generate Client contract v3 from the exact +unmerged Core branch image. No ext-reg source PR, merge, publication, deployment +or black-box run belongs to this task. + +### D021 — Cross-Peer config/state freshness + +Implementation baseline after independent plan review. `ExtensionBase.config` +remains a compatibility snapshot, not shared authority. Freshness-sensitive +Twitter setup, OAuth, refresh and provider operations read validated current +config/state from Core before use. Official OAuth uses a fresh Authlib client per +bounded provider operation rather than a cross-operation singleton; only the +expert Twikit path may retain a config-bound local cache. No process-to-process +invalidation channel is introduced. + +### D022 — Review-ready cross-repository handoff + +Implementation baseline after independent plan review. The Client PR may be +reviewed using generated evidence from the exact unmerged Core branch image, +but remains Draft and explicitly merge-blocked on the Core PR. Its GitHub checks +continue to select protected stable Core and may remain dependency-blocked until +Core lands; no feature-image CI bypass is added. Generated contract and checks +must then be refreshed against the admitted Core revision before Client merge. +Review readiness is not false upstream admission. + +### D023 — One current OAuth flow + +Implementation baseline after sequence review. Beginning a new OAuth flow +atomically supersedes every older non-terminal flow for the same App. An older +callback may finish its external request but its conditional durable commit must +fail, so callback ordering cannot overwrite the user's newer choice. + +### D024 — Readiness belongs to the current authorization + +Implementation baseline after sequence review. A connected account records +an opaque `authorization_id`; only a non-failed collection job explicitly bound +to that ID can satisfy setup readiness. Reconnecting replaces the ID, and a +Finish/reconnect race cannot attribute an old authorization's job to the new +account. + +### D025 — Config writes are not lifecycle teardown + +Implementation baseline after multi-Peer review. Explicit Host/Extension config +operations persist immediately through Core authority and then update the local +snapshot. Base `on_close()` does not write config. A Peer disabling with an old +startup snapshot therefore cannot roll back a newer config committed elsewhere. + +### D026 — MVP state/version gate + +Implementation baseline after feasibility review. Without a declared migration +contract, Core cannot prove a different Distribution version understands +non-empty state without executing future bytes. Same-version install is +idempotent; a version change is allowed only while canonical state is `{}` and +otherwise fails before mutation. The initial Twitter `0.1.1 -> 0.2.0` cut remains +valid because the new column starts empty. + +### D027 — Setup exit remains Extension-owned + +Accepted during preview review. The Host popup continues to provide no cancel +or confirm action. Twitter renders one always-available Close action and emits +the existing contribution `close` event. The action is not restricted to the +terminal step, and closing aborts transient wizard work without rolling back +durable setup facts. + +### D028 — Registry origin is dynamically Client-overridable — Superseded + +The preview diagnosis correctly established the need for an operation-time +override and one origin snapshot, but its Client-specific source was tied to the +obsolete PR baseline. D029 replaces the authority order after the accepted Peer +cutover; no `ClientManager` compatibility path is retained. + +### D029 — Rebuild the setup slice on the admitted Peer baseline + +Accepted after preview-controller diagnosis. Core PR #52 is not made deployable +by teaching current `main` to accept a legacy Client image. Its old feature +branch is reconstructed from current Core `main`, then the setup capability is +ported semantically onto the current Peer, Extension Host, Source, Cron and Job +contracts. Client PR #68 is subsequently migrated from the technical `Client` +domain to the generated Peer contract and exact capability invocation. + +The effective Registry origin for one Host operation is: + +1. the executing Peer's non-empty `config.extension_registry_url`; +2. deployment config `extension.registry.extension_registry_url`; +3. the process setting/default. + +Each Host snapshots the result once for its native Release/Distribution +operation; Core specifically shares one snapshot across exact Release +resolution and Python Distribution acquisition. The OAuth callback URL comes +from the executing Core Peer's admitted public HTTP base URL. Twitter setup +commands are one Twitter-owned exact Peer capability; the public OAuth callback +remains the only unauthenticated route. No generic setup capability or fixed +Web-to-Core origin is introduced. + +### D030 — Browser identity and connection are Peer-native + +Accepted for the reconstruction batch. A browser origin generates and persists +one technical Peer UUID, migrating its prior Client UUID when present. Saving +meta configuration must first prove database/JWT connectivity, register that +Peer, save its owner configuration and renew its database-time lease. The +database registration function refreshes runtime-owned name, config schema and +capabilities without overwriting owner-authored config or labels. User-facing +surfaces continue to say “Client” where they describe the product rather than +the technical protocol node. + +### D031 — Selected-Client control is Peer topology dispatch + +Accepted. The Extensions selector lists Peer rows as product Clients. The +current browser executes its local Web Host lifecycle; a live selected Peer +advertising `core.extension.management.v1` receives an exact capability command; +any other selected Client is still controllable through the atomic durable +desired-state RPC, without an extra label/type restriction. Setup availability +continues to come only from this browser's running Web Distribution. + +### D032 — Twitter setup transport is one exact Peer capability + +Accepted by the Peer-native reconstruction. Twitter Web discovers live Core +Peers from `core.extension.management.v1` advertisements and invokes +`inkcre.twitter.setup.v1` on the exact selected Core Peer. It does not probe +arbitrary Client/Core origins, use postMessage, or introduce a generic setup +protocol. The public provider callback remains a standalone lifecycle-bound +Core route. + +### D033 — Generic management does not project Extension-produced state + +Implementation correction. `extensions.state` remains deployment-wide and may +contain credentials inside the accepted authenticated-Peer boundary, but the +generic Extension list/get/mutation protocol has no reason to transport it. +Core management models exclude it and the Web PostgREST adapter explicitly +selects only name, version, enabled Peers, nickname, config and config schema. +Schema stripping after an all-column response is insufficient because the bytes +would already have reached the browser. + +### D034 — Authorization reset disarms setup-owned collection + +Implementation correction. Replacing the OAuth App with confirmation, +disconnecting the account or reconciling a direct config change invalidates the +bound OAuth account/transactions and disables the setup-owned bookmark Cron. +The Source and Cron remain reusable for reconnect/Finish; this does not inspect +or delete Source-domain records during Extension uninstall and therefore does +not change D012. + +## Resolved Product Questions + +### Q001 — What does “Twitter setup complete” mean? — Resolved + +Use the Extension-wide minimum baseline in +[Product-design Working Model](20-product-design.md), ending in durable account +authorization, required initial resources including an eligible bookmark Source, +and a bounded Extension readiness check. + +Why this is the first question: screen structure, setup APIs, persistence, and +the relationship to enablement all depend on the terminal condition. Current +OAuth state is memory-only, so accepting “OAuth callback returned success” as +completion would create a misleading UX. + +### Q002 — Who owns the popup shell and Extension-specific content? — Resolved + +`client-web` owns only the popup container. The enabled Web Distribution owns +the complete setup UI and completion projection. The Extension Host SDK exposes +only the contribution boundary between them. + +### Q003 — What is the minimum Web Extension API contribution? + +Accepted. Add one optional setup Vue component to the Web +Extension module. The Host mounts it inside the popup and accepts a close +request. Do not add a generic step schema, Host-owned stepper, progress state, or +`setup_complete` callback. + +### Q004 — What is the Twitter first-release wizard? — Resolved + +Accept the four-step flow and first-release boundary in +[Twitter Wizard Proposal](21-twitter-wizard.md): Prepare, Connect account, +Bookmark collection, Review and start. Guide user-owned X App registration and +credential submission; request only read/offline scopes; support official X +OAuth only; reuse or create a Source; finish when readiness passes and Core +accepts the first collection job. + +### Q005 — Where do setup facts and commands live? — Resolved + +Use canonical `extensions.config` for user-declared OAuth App settings and +canonical `extensions.state` for Extension-produced account credentials and +OAuth transactions under the same accepted authenticated-Peer boundary. Keep +Source facts in the Source domain. Twitter exposes one validated setup +projection and Twitter-specific semantic commands; no credential is Peer-scoped +and no generic OAuth framework or second secret store is introduced. + +### Q006 — How does OAuth return to the wizard? — Resolved + +[OAuth Callback Proposal](23-oauth-callback.md) is accepted. The Twitter Core +callback completes the exchange and atomically updates account and transaction +state, but never communicates with the Web page. The Twitter wizard +independently polls that transaction through its Core API and reloads +authoritative setup facts. Reopening always resumes from durable deployment +facts. + +### Q007 — What is the minimum Extension state model? — Resolved + +[Extension State Proposal](24-extension-state.md) is accepted with D011's +authority correction. Add `extensions.state` as one +deployment-wide JSON object, validate and mutate it through the Core Host SDK, +and keep raw table/model details out of Extension code. Config is user-declared; +state is Extension-produced; both may contain Secrets inside the accepted trust +boundary. The Host SDK delegates writes to Core's concurrency authority. + +### Q008 — How do setup and state behave across lifecycle operations? — Resolved + +[Extension State and Setup Lifecycle Proposal](25-state-and-setup-lifecycle.md) +is accepted with D012's uninstall correction. +Reconfiguration invalidates only authorization bound to replaced OAuth App +credentials; reconnect failure preserves a working account; Peer disable keeps +deployment state; MVP upgrade/rollback rejects incompatible state instead of +inventing migrations; uninstall removes Extension config/state without +inspecting Sources. + +### Q009 — Which setup concepts belong in Host SDKs? — Resolved + +[Setup and Host SDK Boundary Proposal](26-host-sdk-boundary.md). The Web Host SDK +only contributes and mounts an optional setup component. The Core Host SDK only +adds typed access to deployment-wide Extension state and delegates persistence +to Core's authority. The wizard, projection, OAuth transaction protocol, Source +composition and readiness commands remain jointly owned by Twitter's Web and +Core Distributions. + +### D027 — Selected-Client enablement control — Accepted + +Restore the Extensions-page Client selector as an application-level management +surface. The card switch projects the selected Client's membership in the +canonical `extensions.enabled[]`: this browser Client is controlled through the +local Web Host SDK; an addressable remote Client is controlled through its +generic Host enable/disable API; an unaddressable remote Client is controlled +as durable desired state through the existing atomic state-port RPC. This +desired-state path applies to every unaddressable Client without an additional +type/label restriction. Its running process cannot be synchronously stopped, +and the UI must say that the change is realized on its next restore/reload. No +direct `enabled[]` edit is allowed. + +### D028 — Setup availability is not selector state — Accepted + +The selected Client controls only enablement. Whole-Extension setup remains a +contribution of the currently running Web Distribution. Selecting Core or any +other Client must not hide a setup contribution already loaded by this browser. + +### D029 — Restore all-Client Settings management — Accepted + +Restore the existing Client list in Settings beneath a separate all-deployment +Clients section. This follow-up manages existing Client metadata, REST API URL, +health and declared config; it does not introduce manual Client creation or +deletion. + +### D030 — Single-user setup command ROI boundary — Accepted + +Do not buy distributed idempotency for low-probability, low-harm duplicate +Source or initial Job creation. The Web wizard disables Source/Finish actions +while their request is pending. Core reuses ordinary Source creation, exposes +clear Cron create/update methods and runs the initial Job through the existing +run-now path. Rare duplicate rows or Jobs remain acceptable and manageable. + +### D031 — Browser Peer registration stays in the runtime — Accepted + +Do not add an `inkcre.register_peer` database RPC. `WebPeerRuntime` performs an +ordinary `peers` upsert containing only runtime-owned `id`、`name`、 +`config_schema` and `capabilities`; omitted owner-authored `config` and `labels` +remain unchanged. Core retains no browser-specific registration protocol. + +### Q010 — What is the minimum production-shaped vertical slice? — Resolved + +[Minimum Vertical Slice and Black-box Acceptance Proposal](27-vertical-slice-and-acceptance.md). +The slice must prove real Extension usefulness—durable OAuth, Source creation, +first bookmark collection, restart/Peer handoff and recovery—not merely render +the stepper. Black-box proof is deferred; implementation PRs retain focused +repository checks but do not invent a substitute provider harness. + +## Queued Questions + +No product, HLD, dependency, environment or repository-sequencing question is +queued. Sir authorized the Peer-native implementation batch; the Core and Web +source results now pass their full local checks. The only remaining admission +gate is mechanical generated-contract synchronization from the exact Core +feature image after separate commit/push authority makes that image available. diff --git a/tasks/extension-setup-wizard/31-hld-callback-ingress.md b/tasks/extension-setup-wizard/31-hld-callback-ingress.md new file mode 100644 index 0000000..2d85f46 --- /dev/null +++ b/tasks/extension-setup-wizard/31-hld-callback-ingress.md @@ -0,0 +1,111 @@ +# HLD 1 — OAuth Callback Ingress + +## Current Constraint + +Core's current `JWTMiddleware` runs before route dispatch and rejects every +request without a Peer JWT except a hard-coded health/docs list. X redirects a +browser with `code`/`state` query parameters and cannot attach InKCre's Peer JWT. +FastAPI route dependencies therefore cannot make the existing Twitter callback +reachable by themselves. + +The current request logger also records raw query parameters, which would leak +the authorization code and OAuth state into logs. + +## Rejected Shapes + +- Do not hard-code `/twitter/auth/callback` in Core middleware; Core must not know + a first-party Extension's route. +- Do not introduce a Core-wide OAuth callback broker; OAuth remains Twitter + protocol. +- Do not relay through `client-web`, `postMessage`, opener state or Web origin. +- Do not put a Peer JWT in a callback URL. + +## Core Extension API + +Add one generic route-visibility contribution, defaulting to none: + +```python +class PublicHTTPRoute(BaseModel): + method: Literal["GET", "POST"] + path: str # exact Extension-relative path, no parameters or wildcard + +class ExtensionBase: + @classmethod + def public_http_routes(cls) -> tuple[PublicHTTPRoute, ...]: + return () +``` + +Twitter declares only `GET /auth/callback`. During startup, `ExtensionHost` +combines the Extension runtime prefix with that relative path, verifies that one +published FastAPI route has the same exact method/path, and acquires a claim in a +Core-owned process-local `PublicHTTPRouteRegistry`. `JWTMiddleware` bypasses Peer +JWT validation only when the exact `(method, path)` is actively claimed. + +The registry is lock-protected, rejects a second owner of the same method/path, +and returns an idempotent owner-token claim handle. It is policy state for one +Core process, not deployment state and not persisted in the database. + +Startup first publishes the Extension routes, verifies an exact route match, +then acquires the public-route claim. A failed verification or conflicting claim +rolls back both the route publication and every other startup contribution. The +claim is never visible before the route it authorizes and never survives that +route. + +The claim is part of `ExtensionPublication`: startup rollback, disable and +shutdown remove it together with the route. No prefix, regex, arbitrary callback +function or Extension-managed middleware is exposed. Disabling Twitter during a +pending authorization makes the callback unavailable; the transaction expires +without changing account state. + +This is a Core Extension API feature, not a setup/OAuth API. It lets a running +Extension publish an exact unauthenticated inbound while Core retains routing, +lifecycle and authentication-policy authority. + +This shape is the implementation baseline. It introduces no callback broker or +Twitter hard-code and is fully withdrawn with the Extension publication. + +## Callback Address and Provider Client + +The selected Core Peer derives the callback URL from its authoritative +`CLIENT_BASE_URL`/registered `clients.rest_api_url` plus +`/twitter/auth/callback`. Setup fails with a prerequisite blocker when that Core +has no public base URL. The wizard displays this exact URL for X App registration; +it does not infer a callback from the Web origin. + +Twitter uses Authlib's `AsyncOAuth2Client` happy path rather than manually +constructing token requests: + +- `create_authorization_url(..., code_verifier=...)` with + `code_challenge_method="S256"`; +- persist the returned OAuth state and verifier in Extension state; +- reconstruct the client after callback and call `fetch_token(..., + authorization_response=..., code_verifier=...)`; +- use Authlib's token/refresh support and persist refreshed tokens through the + Core state authority. + +Authlib documents this explicit save/restore flow for async HTTPX clients and +PKCE. X requires exact callback matching, supports S256, and gives an +authorization code only a short exchange lifetime, so the callback performs the +exchange immediately rather than enqueueing it. + +References: + +- [Authlib async OAuth 2 client and PKCE](https://docs.authlib.org/en/1.7.1/oauth2/client/http/) +- [X OAuth 2 authorization code with PKCE](https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code) +- [X App callback requirements](https://docs.x.com/fundamentals/developer-apps) + +## Logging and Response + +Core stops logging raw query strings for all requests; method, path, trace ID, +status and duration remain. Twitter maps provider/callback failures to bounded +categories before logging. The callback returns a small standalone HTML result +page containing no code, state, token, Web origin or redirect-to-Web behavior. + +## Verification Boundary + +Focused Core tests must prove exact route matching, JWT enforcement on adjacent +paths/methods, claim cleanup on rollback/disable, callback reachability without a +JWT, query-value absence from logs, and account-state preservation on invalid or +replayed state. Twitter error tests also prove provider response bodies and token +values do not reach generic request logs. These are repository checks, not the +deferred black-box journey. diff --git a/tasks/extension-setup-wizard/32-hld-extension-state.md b/tasks/extension-setup-wizard/32-hld-extension-state.md new file mode 100644 index 0000000..454a8a3 --- /dev/null +++ b/tasks/extension-setup-wizard/32-hld-extension-state.md @@ -0,0 +1,177 @@ +# HLD 2 — Canonical Extension State + +## Persistence and Authority + +Add one column to the canonical deployment relation: + +```sql +ALTER TABLE inkcre.extensions + ADD COLUMN state jsonb NOT NULL DEFAULT '{}'::jsonb; + +ALTER TABLE inkcre.extensions + ADD CONSTRAINT ck_extensions_state_object + CHECK (jsonb_typeof(state) = 'object'); +``` + +The table continues to represent one installed Extension Release for the whole +deployment. `state` is deleted with that row on uninstall. Sources remain +independent and are neither inspected nor deleted. + +Core and PostgreSQL remain the state authority: + +- the authenticated PostgREST role may read `state` inside the accepted Peer + trust boundary, but cannot update it directly; +- a migration trigger rejects a non-empty `state` on direct insert, just as the + existing trigger rejects a non-empty `enabled` value; +- only the Core service role mutates state through Core-owned operations; +- every mutation locks the exact Extension row with `SELECT ... FOR UPDATE`, + validates the current and next values, and commits once; +- provider or other network I/O never runs while the row lock is held. + +The current Core type named `ExtensionState` is actually an installed-row +projection. Rename it to `InstalledExtension`, and rename +`ExtensionStateStore`/`SQLExtensionStateStore` to +`ExtensionStore`/`SQLExtensionStore`. The projection keeps installation fields +and does not expose raw Extension state through the generic Core management +API. This removes the future ambiguity of `state.state` without changing the +canonical table. + +## Core Host SDK Shape + +Existing Extensions receive an empty validated state by default. A stateful +Extension binds a second model type: + +```python +class ExtensionBase[ConfigT, StateT]( + config_cls=..., + state_cls=EmptyState, +): + @classmethod + def get_config(cls) -> ConfigT: ... + + @classmethod + def update_config(cls, next_config: ConfigT) -> ConfigT: ... + + @classmethod + def get_state(cls) -> StateT: ... + + @classmethod + def mutate_state( + cls, + transform: Callable[[StateT], StateT], + ) -> StateT: ... + + @classmethod + def mutate_config_and_state( + cls, + transform: Callable[[ConfigT, StateT], tuple[ConfigT, StateT]], + ) -> tuple[ConfigT, StateT]: ... +``` + +`ExtensionBase` validates both sides of each transform with the Extension's +declared models. It passes a pure dictionary transform to a narrow +`ExtensionRuntimeRecord`; the Core store executes that transform inside its +transaction. SQLModel rows, sessions and locks never cross into Extension code. +After a combined mutation commits, `ExtensionBase.config` is replaced with the +committed validated config. + +`ExtensionBase.config` remains the startup/last-local-commit snapshot needed by +existing Extensions; it is not a cross-Peer freshness guarantee. +`get_config()` reads and validates the current canonical row. Setup, OAuth, +token refresh and provider-client construction must call `get_config()` rather +than relying on the snapshot. This lets Core Peer B observe a credential change +committed through Core Peer A without making the Host SDK or process memory the +authority. + +Configuration persistence is command-time, not teardown-time. +`ExtensionBase.update_config()` validates, persists through the runtime record, +then replaces the local snapshot; the Host management route uses the same +semantic operation for a running Extension. `mutate_config_and_state()` behaves +likewise for its atomic pair. Base `on_close()` performs no unconditional config +write. Otherwise a disabling Core Peer could overwrite a newer config committed +by another Peer with its stale startup snapshot. + +The runtime record therefore adds these Core-owned callbacks: + +```text +read_config() -> JSON object +read_state() -> JSON object +mutate_state(transform(JSON object) -> JSON object) -> committed JSON object +mutate_config_and_state( + transform(config JSON, state JSON) -> (config JSON, state JSON) +) -> committed (config JSON, state JSON) +``` + +This is an Extension API surface because it is inherited from the Core Host +SDK. It is not a generic state service and does not promise that arbitrary +Extension code may retain a mutable state object between calls. + +## Transaction Semantics + +`SQLExtensionStore` owns fresh reads and transactional mutations in addition to +the existing install, config and enablement operations: + +```text +read_config(name) +read_state(name) +mutate_state(name, transform) +mutate_config_and_state(name, transform) +``` + +For a mutation, Core: + +1. begins a transaction; +2. selects `inkcre.extensions[name] FOR UPDATE`; +3. fails if the Extension is absent; +4. gives copies of the current JSON values to the pure transform; +5. validates that the result is a JSON object and writes it once; +6. commits and returns the committed value. + +The Python process-local runtime lock may still serialize lifecycle calls in one +Core process, but it is not the cross-Peer authority. PostgreSQL row locking is. +Extensions do not perform their own distributed locking. + +## Database Contract Cut + +This is an additive database migration, not another clean reset: + +- append one Alembic revision after the current head; +- bump `CORE_VERSION` and the root project version from `0.1.0` to `0.1.1`; +- bump `CONTRACT_REVISION` from `peer-database-runtime-v2` to + `peer-database-runtime-v3`; +- update metadata, deployment profile, runtime contract, schema artifact, + PostgREST checks and migration integrity; +- regenerate `client-web` database types from the exact Core branch image; +- keep `extension_installations` and `extension_peer_bindings` absent. + +The store's version-change transaction and the relation's update trigger both +require `OLD.state = '{}'` before `version` may change. The store check happens +under the same locked installed row and before writing the new version; the +trigger prevents a direct PostgREST update from bypassing it. There is no attempt +to import the incoming Distribution as a migration probe. Non-empty state makes +a different-version install fail closed; same version remains idempotent. + +All existing first-party Python Extension Host ranges +`>=0.1.0 <0.2.0` continue to accept Core `0.1.1`. Twitter `0.2.0` will require +`>=0.1.1 <0.2.0`, making the new state/callback API a pre-download +compatibility gate without reintroducing capability labels. + +## Focused Verification + +Core tests must prove: + +- migration default, JSON-object constraint, insert trigger and direct-update + denial for the authenticated role; +- state survives restart/Peer handoff and is removed with uninstall; +- a different-version install succeeds with empty state and is rejected without + mutation when state is non-empty; +- two Core sessions serialize state mutation through the database row lock; +- a failed validation or transform rolls back both config and state; +- a second Core process reads a config/state commit made by the first before a + freshness-sensitive provider operation, without restart or process messaging; +- disabling a Peer with an old startup snapshot cannot overwrite a newer config, + while explicit Extension/Host config updates persist immediately; +- existing Extensions receive `EmptyState` without source changes; +- Extension code sees typed config/state methods but no database row/session; +- the generated contract is revision v3 and the rejected installation/binding + relations remain absent. diff --git a/tasks/extension-setup-wizard/33-hld-twitter-protocol.md b/tasks/extension-setup-wizard/33-hld-twitter-protocol.md new file mode 100644 index 0000000..17d599b --- /dev/null +++ b/tasks/extension-setup-wizard/33-hld-twitter-protocol.md @@ -0,0 +1,300 @@ +# HLD 3 — Twitter Setup and OAuth Protocol + +## Core API + +The enabled Twitter Python Distribution publishes these authenticated routes: + +```text +GET /twitter/setup +PUT /twitter/setup/oauth-app +POST /twitter/setup/oauth-transactions +GET /twitter/setup/oauth-transactions/{transaction_id} +DELETE /twitter/setup/account +POST /twitter/setup/bookmark-source +POST /twitter/setup/finish +``` + +It also declares exactly one unauthenticated callback through the Core public +route contribution from [HLD 1](31-hld-callback-ingress.md): + +```text +GET /twitter/auth/callback +``` + +Command response contract: + +| Operation | Success | Relevant failures | +| --- | --- | --- | +| Setup projection | `200` projection | `409` runtime/config invariant, `503` transient dependency | +| Configure OAuth App | `200` refreshed projection | `409` reset confirmation required, `422` invalid input | +| Begin OAuth | `201` transaction reference/URL | `409` prerequisite missing, `503` transient provider preparation | +| Poll transaction | `200` bounded transaction projection | `404` unknown/pruned transaction | +| Disconnect | `204` | idempotent when already disconnected | +| Configure bookmark Source | `200` refreshed projection | `409` account/runtime prerequisite, `422` invalid schedule | +| Finish | `200` refreshed projection | `409` readiness/source mismatch | + +All authenticated error responses use the existing Core JSON error contract and +bounded messages. The public callback renders fixed HTML: `200` for success, +`400` for denied/invalid/expired input, and `502` for a bounded provider exchange +failure. It never returns a JSON exception or provider response body. + +The existing ad-hoc `/twitter/auth/authorize` and `/twitter/bookmark` endpoints +are removed in Twitter `0.2.0`. Setup commands replace them with one coherent, +resumable protocol. + +## Setup Projection + +`GET /twitter/setup` returns only Web-safe domain facts: + +```json +{ + "extension": { "name": "inkcre/twitter", "version": "0.2.0" }, + "core": { + "peer_id": "uuid", + "callback_url": "https://core.example/twitter/auth/callback" + }, + "oauth_app": { + "status": "missing | configured | invalid", + "client_id": "optional non-secret identifier", + "client_secret_configured": true + }, + "account": { + "status": "disconnected | connected | reconnect_required", + "user_id": "optional", + "handle": "optional", + "scopes": [] + }, + "bookmark_sources": [ + { + "id": 1, + "nickname": "Twitter bookmarks", + "collect_at": { "day_of_week": null, "hour": 0, "minute": 0 }, + "latest_job": { "id": 1, "status": "pending | running | finished | failed" } + } + ], + "readiness": { + "status": "incomplete | ready | temporarily_unavailable", + "blockers": [] + } +} +``` + +The projection never contains Client Secret, access/refresh token, provider +OAuth state, PKCE verifier or a stored wizard step. `callback_url` comes from +the selected Core's configured public `CLIENT_BASE_URL`; a missing/non-public +value is an explicit Prepare blocker. + +Readiness is fact-derived. `ready` requires a configured current App, a +restorable connected account bound to it, one selected eligible bookmark Source +and an enabled Cron whose Job template carries the current account's +`authorization_id`. Finish enqueues an initial Job after establishing those +facts, but Job history is not a second readiness authority. Provider/Core +availability problems are `temporarily_unavailable`; they do not erase durable facts. +The GET projection itself performs no X network call. Begin/callback and Finish +are the bounded operational checks; ordinary collection remains responsible for +later provider failures. + +## OAuth App Configuration + +`PUT /twitter/setup/oauth-app` accepts: + +```json +{ + "client_id": "...", + "client_secret": "...", + "confirm_account_reset": false +} +``` + +The command preserves unrelated Twitter config. Submitting the same credentials +is idempotent. Replacing credentials while an account or transaction exists +returns a conflict unless `confirm_account_reset` is true. A confirmed +replacement atomically updates config and clears the account and all OAuth +transactions; Sources are preserved. + +The first setup release configures the official X backend and does not expose +Twikit credentials. Existing lower-level config fields may remain for backward +compatibility, but the setup projection and wizard do not promise that path. + +`app_fingerprint` is a deterministic equality binding, not an authentication +credential: lowercase hex SHA-256 over UTF-8 +`"inkcre-twitter-oauth-app\0" + client_id + "\0" + client_secret`. It lets any +Core Peer detect direct config replacement and prevents a token/transaction +created for one App from being used with another. The digest is never exposed by +the setup projection. + +## Durable Twitter State + +The Twitter state model is deployment-wide: + +```text +TwitterState + account? + token Authlib token fields required for restore/refresh + user_id + handle + scopes[] + app_fingerprint + authorization_id opaque identity for this successful authorization + connected_at + reconnect_required + oauth_transactions: map[transaction_id, OAuthTransaction] + +OAuthTransaction + provider_state + pkce_verifier + app_fingerprint + redirect_uri + status: pending | exchanging | succeeded | failed | expired + created_at + expires_at + observe_until + error_category? +``` + +Transaction ID and provider `state` are independently generated. Pending +transactions expire after ten minutes; terminal records remain observable for +another ten minutes. Starting a new transaction atomically marks every older +`pending` or `exchanging` transaction for the same App as failed with the +bounded category `superseded`. Consequently an older in-flight callback cannot +overwrite a newer user choice. Every mutation prunes expired records and keeps +at most eight recent transactions. These values are protocol bounds, not UI +progress. + +Terminal transactions retain only their polling reference, status, timestamps +and bounded error category. Provider state and PKCE verifier are cleared on a +terminal transition; they are not needed for observation. A repeated callback +therefore receives the same bounded invalid/replayed result without recovering +or exposing the consumed secret material. + +## Authlib Happy Path + +Twitter uses `Authlib`'s `AsyncOAuth2Client` with HTTPX: + +- authorization endpoint `https://x.com/i/oauth2/authorize`; +- token endpoint `https://api.x.com/2/oauth2/token`; +- scopes exactly `tweet.read users.read bookmark.read offline.access`; +- PKCE `S256` with a separately persisted verifier; +- confidential-client `client_secret_basic` token authentication; +- a ten-second HTTPX timeout for token and user-info requests; +- `fetch_token(... authorization_response=..., code_verifier=...)` in the + callback; +- `update_token` to conditionally persist refreshed token state through the + Core state authority. + +A bounded temporary probe on 2026-08-13 exercised Authlib `1.7.2` with a real +HTTPX mock transport and proved S256 generation, Basic token authentication, +authorization-code exchange, automatic refresh and the async update callback. +PDM `2.27.0` also resolved `Authlib 1.7.2`, HTTPX `0.28.1`, cryptography and +joserfc into the current Core lock graph. Core and the Twitter wheel will both +declare the Authlib/HTTPX compatibility baseline because the Extension +consumer must not mutate Core-owned dependencies during enable. + +## Begin, Callback and Poll + +`POST /twitter/setup/oauth-transactions` requires configured App credentials and +a public callback URL. It supersedes an older non-terminal transaction, creates +one persisted pending transaction and returns: + +```json +{ + "transaction_id": "opaque Web polling reference", + "authorization_url": "https://x.com/i/oauth2/authorize?...", + "expires_at": "timestamp" +} +``` + +The callback looks up the transaction by provider `state`. One +Core-authoritative mutation changes `pending` to `exchanging`; only that exact +transaction may subsequently commit. Network exchange and `/2/users/me` lookup +happen outside the database lock. A second conditional mutation commits account +token, identity, `connected_at` and `succeeded` together only when the same +transaction, App fingerprint and `exchanging` state are still current. A newer +Begin command changes the older status before this commit and therefore wins. +Every successful authorization also receives a new random `authorization_id`; +token refresh preserves it, while reconnect replaces it. + +There is deliberately no exchange lease or reclaim protocol. Once an +authorization code may have been presented to X, a crashed process cannot prove +whether that one-use code was consumed. The transaction remains `exchanging` +until normal expiry and the user starts a new transaction. This is simpler and +more truthful than pretending the exchange can be recovered. + +Invalid, mismatched, expired or replayed callbacks cannot replace a working +account. Provider failures become bounded categories, not raw exception text. +If the process dies while exchanging, the transaction expires/reconnects; the +existing account remains. The callback responds with fixed standalone HTML and +no redirect or browser messaging. + +`GET .../{transaction_id}` returns only status, expiration and bounded error +category. It reports `exchanging` as pending to the Web experience. Web cancel +stops polling but does not mutate the provider transaction. + +An Authlib refresh callback performs a conditional state mutation against the +same App fingerprint and previous token. A stale refresh cannot overwrite a +newer account. An `invalid_grant` observed against the still-current token may +mark `reconnect_required`; other transient failures preserve account state. +Twitter converts Authlib/HTTPX/provider exceptions to its bounded domain error +categories before they reach generic Core or Source-job logging; provider bodies, +authorization headers and token values are never used as exception messages or +job-state errors. + +The official OAuth client is not a process singleton. Every provider operation +obtains fresh validated config/state, constructs one Authlib client, uses it for +that bounded operation and closes it. Multi-page collection may reuse the client +inside that one operation; a later operation reconstructs from durable state. +This follows Authlib's normal restore/close path and removes cross-Peer cache +invalidation, concurrent client replacement and stale startup-token problems. + +The retained expert-only Twikit backend may keep its existing process-local +session cache, but only while a binding over its relevant fresh validated config +still matches; otherwise it is closed/replaced. That cache is never an +authority. A cleanup failure is reported as runtime unavailability but does not +roll back or overwrite an already authoritative database commit. + +## Bookmark Source and Finish + +`POST /twitter/setup/bookmark-source` accepts +`{source_id?, nickname, collect_at}` using the existing `CollectAt` shape. The +first release supports one initial bookmark Source: + +- list/reuse every existing `extensions.twitter.bookmark.Source` in the setup + projection; +- if the user chooses Create, Core's existing `SourceManager.create()` creates + one ordinary Source row; +- the Web UI disables the action while the request is pending; rare duplicate + Source rows are acceptable and remain manageable through the Sources surface; +- when `source_id` is supplied, the command validates and selects that existing + Twitter bookmark Source without editing it; +- selecting an existing Source does not silently rename it. + +Configuring the selection creates or updates one disabled Cron template. Finish +enables that template and enqueues a Job. A Cron failure does not roll back or +delete the Source row. + +No newest-versus-history option is exposed in this release. The first job uses +the current bounded `full=false, result_limit=40` behavior. The bookmark +collector is corrected to handle an empty provider result without indexing the +first tweet. + +`POST /twitter/setup/finish` validates the current account and selected Source, +performs one bounded authenticated `/2/users/me` check, updates/enables the +ordinary Cron template and calls the existing `CronManager.run_now()`. A repeated +Finish may enqueue another Job; that low-probability duplicate is acceptable. +The captured `authorization_id` remains in the Job template, so reconnect makes +old work ineligible before provider execution. The wizard does not wait for a +historical sync. + +## Focused Verification + +Tests use injected Authlib transport/time/random seams and real database +transactions. They cover configure/reset confirmation; S256/scopes; +restart-safe begin/callback/poll; superseding overlapping flows; exchange claim +and replay; expiry/pruning; +failure preserving a working account; conditional refresh; projection redaction; +cross-Peer config/token freshness, per-operation official clients and Twikit +cache replacement; standalone +callback; ordinary Source/Cron creation and Finish enqueue; reconnect ignoring +pre-connection jobs; reconnect racing Finish; and empty bookmarks. +They do not introduce a fake public X deployment or replace Sir's deferred +black-box acceptance. diff --git a/tasks/extension-setup-wizard/34-hld-web-setup.md b/tasks/extension-setup-wizard/34-hld-web-setup.md new file mode 100644 index 0000000..6c1b8b0 --- /dev/null +++ b/tasks/extension-setup-wizard/34-hld-web-setup.md @@ -0,0 +1,152 @@ +# HLD 4 — Web Host Contribution and Wizard UI + +## Web Host SDK Contract + +`@inkcre/core` adds one optional contribution to the existing Module Federation +module: + +```ts +import type { Component } from 'vue' + +export interface ExtensionSetupContribution { + component: Component +} + +export interface ExtensionModule { + initialize?(): Promise + activate?(): Promise + deactivate?(): Promise + dispose?(): Promise + setup?: ExtensionSetupContribution +} +``` + +`WebExtensionHost.getSetupContribution(name)` returns the contribution only +while that exact Extension is running in the current Web Peer. It does not load +a disabled Remote merely to inspect setup. The running map stores the loaded +module beside its lifecycle object. + +The Host owns no setup context, steps, state, Core client or database port. The +component uses the ordinary Web Peer APIs already available to Extensions. +The generated database row will contain the new `state` column, but +`InstalledExtensionSchema` continues to project only installation/config facts; +the PostgREST adapter neither interprets nor writes raw Extension state. + +## Card and Popup Shell + +The Extension card derives setup availability from two facts: + +1. this Web Peer's UUID is present in the installed row's `enabled[]`; +2. the running module contributes `setup`. + +When both hold, the card shows **Setup**. When the Extension is disabled here, +the card explains that local enablement is required. The existing trusted +**Edit config** operation remains an expert surface; the wizard is the guided +product path but does not invent a new authorization boundary around config. + +An enabled row with no running module is not mislabeled as merely disabled: the +card uses the Host's existing runtime-error projection to report that setup is +unavailable. A running Extension with no setup contribution simply has no Setup +action. Twitter `0.2.0` is expected to contribute one; failure to do so is a +runtime/package defect, not a hidden fallback to Host-owned UI. + +The shell uses the existing `InkDialog` happy path: + +```text +showCancel=false +showConfirm=false +closeOnScrim=false +default slot = dynamic setup component +component close event -> dialog closes +``` + +The Twitter component owns Back/Next/Cancel/Finish. If local disable or unload +occurs, the shell clears the mounted dynamic component and awaits one Vue tick +before asking the Host to stop/dispose the Remote. Dialog animation is not the +lifecycle boundary. No generic stepper component or setup engine is introduced. + +## Core Endpoint Discovery + +The Twitter Web Distribution discovers command endpoints through the existing +`Client` model: + +1. call `Client.list()`; +2. keep clients with a `rest_api_url`; +3. probe authenticated `GET /extensions/inkcre/twitter` on each; +4. report reachable candidates and whether the shared installed row contains + that client UUID in `enabled[]`; +5. if none is enabled, require the user to select a candidate and explicitly + call `POST /extensions/inkcre/twitter/enable`; +6. use the selected enabled endpoint for `/twitter/setup` and setup commands. + +The UI does not guess from a client name or label. A Core endpoint is merely the +transport for deployment-wide config/state/Source authorities. If multiple +enabled endpoints are reachable, the user may choose one; the selection is +ephemeral and is not persisted as Extension ownership. + +## Twitter Component + +The Twitter Remote exports the normal lifecycle plus +`setup.component = TwitterSetupWizard`. The component owns the four steps: + +```text +Prepare -> Connect account -> Bookmark Source -> Review and start +``` + +On mount and after every successful command it reloads the setup projection and +derives the first unsatisfied step. Local `currentStep` only controls navigation +inside that mounted popup and is never a durable completion authority. +When multiple existing bookmark Sources are eligible, the component defaults to +the oldest stable Source ID and lets the user select another for Finish; this +selection is local because any eligible Source satisfies the whole-Extension +baseline. + +The component uses established dependencies only: + +- `@inkcre/core` `Client` for authenticated Core calls; +- existing `@inkcre/ui-web` form, input, dropdown and button components; +- a small accessible ordered-list step indicator styled inside Twitter; +- normal Vue state/composables; no form or state-machine framework is added. + +The existing `Client.request()` happy path gains one optional +`signal: AbortSignal`; it forwards that signal through the original request and +the existing one-time 401 refresh retry. That retry preserves the original +method, URL/query, body, signal and non-auth headers such as JSON +`Content-Type`, replacing only Authorization. Twitter does not fork +authentication, error parsing or `fetch` into a second HTTP client merely to +support polling and probe cancellation. + +The same parser recognizes a string FastAPI `detail` as the bounded API error +message, alongside its existing `message`/`error` cases. It never renders an +object/validation payload as text. This lets explicit Core enable/setup blockers +reach the wizard instead of collapsing to a generic HTTP status. + +The first slice may ship English copy owned by the Twitter Distribution rather +than adding a cross-Remote i18n protocol. Localization is presentation follow-up, +not a hidden setup authority. + +## OAuth Browser Flow + +After Begin returns, the component renders a deliberate **Open X** link with +`target="_blank" rel="noopener noreferrer"`. It does not depend on retaining an +opener. While mounted, it polls the opaque transaction every two seconds until +terminal/expired. Timers and requests use one `AbortController` and are stopped +on cancel, unmount or transaction replacement. + +Success reloads `/twitter/setup`; failure leaves a retry action and any previous +working account intact. Closing the dialog does not cancel the Core callback. +Reopening resumes from deployment facts, not the lost component instance. + +## Web Version and Tests + +`@inkcre/core` advances from `0.1.0` to `0.1.1`; existing Web Extensions remain +compatible, while Twitter `0.2.0` declares +`@inkcre/core >=0.1.1 <0.2.0`. The MF shared range follows that package version. + +Focused tests cover Host contribution availability/cleanup; card gating and +dialog ownership; Core discovery and explicit enable; +four-step resume; credential submission redaction; polling cleanup/retry; +Source selection/Finish; `Client.request` abort propagation; PostgREST omission +of `state`; 401 JSON-request preservation; and error preservation. +Existing native MF closure +checks must still prove `mf-manifest.json`, the Remote entry and all assets. diff --git a/tasks/extension-setup-wizard/35-hld-delivery.md b/tasks/extension-setup-wizard/35-hld-delivery.md new file mode 100644 index 0000000..f3bf779 --- /dev/null +++ b/tasks/extension-setup-wizard/35-hld-delivery.md @@ -0,0 +1,120 @@ +# HLD 5 — Versions, Repositories and Delivery + +## Version Plan + +Use versions as the pre-download Host SDK capability gate; do not reintroduce +generic capability labels: + +| Product | Current | Setup release | +| --- | --- | --- | +| Core service / Core Python Host SDK | `0.1.0` | `0.1.1` | +| `@inkcre/core` Web Host SDK | `0.1.0` | `0.1.1` | +| `inkcre/twitter` Extension Release | `0.1.1` | `0.2.0` | + +The Twitter Python and Module Federation Distributions both use Extension +version `0.2.0`. Their Host associations require their respective SDK +`>=0.1.1 <0.2.0`. Other existing Python Extensions keep +`>=0.1.0 <0.2.0` and remain compatible with Core `0.1.1`. + +A read-only production Registry probe on 2026-08-13 confirmed that +`inkcre/twitter@0.1.1` is published with both native associations and +`inkcre/twitter@0.2.0` returns 404. The chosen immutable Release identity is +therefore currently available; the probe performed no Registry mutation. Repeat +that exact read-only lookup before final PR handoff. If another actor has since +occupied `0.2.0`, stop and review a new cross-format version rather than +overwriting or silently bumping one Distribution. + +## Repository Ownership + +### `core-py` + +Owns: + +- the additive `extensions.state` migration and database contract v3; +- Core state transactions and direct-write restrictions; +- typed Core Host state APIs and exact public-route lifecycle; +- request-log redaction; +- Twitter Python setup/OAuth/Source protocol and wheel `0.2.0`; +- Core/Authlib/HTTPX dependency baseline and frozen lock; +- Python, migration, PostgreSQL and wheel verification. + +### `client-web` + +Owns: + +- Web Host setup contribution and `@inkcre/core 0.1.1`; +- Extension-card action and `InkDialog` shell; +- Twitter Web wizard and native MF Distribution `0.2.0`; +- database contract generated from the exact Core branch image; +- Vue/Host/component tests and native MF closure/build checks. + +### `ext-reg` + +Needs no source change. Its native Release already supports independently +published Python and Module Federation associations under the same Extension +name/version. This task packet is the only Registry-repository mutation for this +planning phase. + +## Existing CD Happy Path + +No new publisher workflow is required: + +- Core's `extension-publish.yml` detects the changed Twitter subtree, builds and + verifies the wheel, prepares the native Python association, uploads through + `/legacy/`, and publishes/reads back the exact Release. +- Client's checked-main delivery uploads the exact Twitter MF artifact, prepares + the native Module Federation association, publishes it and verifies its + Registry-hosted manifest/assets before Pages delivery. +- Registry permits the second native association to append to an already + published immutable Release, so merge order does not change Extension + identity. Operationally Core should merge/publish first, then Client, so the + wizard is not visible before a Core setup endpoint can exist. + +This implementation task stops before either workflow can run on `main`. +Publishing, deployment and black-box acceptance remain outside the authorized +boundary. + +## Cross-repository Contract Synchronization + +The Client PR must not hand-edit generated database truth or wait for a merge. +Use the already configured SSH Docker provider: + +1. build and start an exact Core feature-branch development image with + `scripts/dev_database.py ensure `; +2. record its local daemon tag and source revision; +3. point Client's checked SSH provider at the same Docker daemon; +4. run `pnpm contract:sync -- --image `; +5. immediately run `pnpm contract:check -- --image `; +6. stop the task-owned runtime after evidence is captured. + +`scripts/dev_database.py stop` removes the exact Compose project, volume, local +state and tunnel. The development image tag is source-revision-scoped rather +than instance-owned and may be reused by another task, so this plan neither +deletes it nor runs broad Docker prune operations. + +The machine has no local Docker CLI, but the configured SSH target and remote +Docker provider are available and have already supported this repository's +database-contract workflow. This is therefore an implementation step, not an +unresolved environment question. + +## PR Shape + +Prepare two new branches and two new PRs: + +1. **Core PR** — state authority, callback ingress, Twitter Python setup and + version/dependency changes. It is independently reviewable and fully green. +2. **Client PR** — exact generated v3 contract, Host contribution, popup shell, + Twitter wizard/MF `0.2.0`. It declares the Core PR as a dependency but uses + the exact unmerged Core branch image for generated evidence. + +Both PRs stop ready for review, but not with the same merge status. Core is a +normal independently green PR. Client is opened as Draft/reviewable and is +explicitly merge-blocked until Core lands. Client's GitHub `workspace` and E2E +checks intentionally resolve the protected `stable` Core image and therefore +remain an expected dependency gate while stable is still contract v2; the plan +does not weaken those workflows or add a feature-image bypass. Local exact +feature-image checks are attached as review evidence only. After Core is +admitted, Client must regenerate/check against that admitted Core revision +before becoming merge-ready. Do not merge, publish, release or deploy. Do not +open an ext-reg source PR unless implementation discovers an actual Registry +contract defect; the completed investigation has found none. diff --git a/tasks/extension-setup-wizard/40-implementation-plan.md b/tasks/extension-setup-wizard/40-implementation-plan.md new file mode 100644 index 0000000..437e045 --- /dev/null +++ b/tasks/extension-setup-wizard/40-implementation-plan.md @@ -0,0 +1,276 @@ +# Implementation Plan + +Sir authorized source implementation on 2026-08-13. Batches 1–5 are complete in +the two task worktrees; Batch 6 is complete through local review evidence and +cleanup. Commit, push, PR creation, publication, deployment, merge and real-X +black-box acceptance remain outside that authorization. + +The executed evidence and implementation refinements are recorded in +[Implementation result](60-implementation-result.md). This file remains the +pre-implementation plan rather than being rewritten into a retrospective. + +## Impact Handshake + +- **Invariant changed**: one canonical installed Extension gains + deployment-wide typed state; a running Extension may contribute one exact + public route; an enabled Web Extension may contribute setup UI. +- **Authorities preserved**: Registry owns Releases/Distributions; + Core/PostgreSQL owns installed config/state and concurrency; Source owns + Source rows/jobs; each Host owns runtime lifecycle; Twitter owns setup/OAuth. +- **Consumer impact**: Core database contract advances to v3; Core and Web Host + SDKs become `0.1.1`; Twitter becomes one cross-format `0.2.0` Release. +- **Failure posture**: callback and token refresh keep their required conditional + semantics. Ordinary Source/Cron/Job commands optimize the single-user happy + path; UI pending state handles normal double-submit and rare duplicate work is + accepted rather than buying a new idempotency subsystem. +- **Delivery boundary**: two new PRs ready for review; no merge, publication, + deployment or black-box acceptance. + +## Batch 1 — Core Database and Host SDK + +In a fresh `core-py` branch from current `origin/main`: + +1. Add `extensions.state`, the insert/update authority rules and database + contract v3 in: + - `app/schemas/extension/main.py`; + - a new append-only `migrations/versions/*.py`; + - `migrations/metadata.py`, `migrations/revision-integrity.json`; + - `app/database_contract/constants.py`, `roles.py`, readiness/protocol files; + - `deploy/profiles/production.json` and generated schema/OpenAPI artifacts. +2. Rename the misleading installed-row state types and add Core-owned fresh + config/state reads plus row-lock mutations in + `app/business/extension/state.py` and exports/callers. In the existing locked + install transaction, reject a different version when `state != {}`; do not + install/import the incoming Distribution as a migration probe. Extend the + database update trigger with the same rule so direct PostgREST cannot bypass + it. +3. Add the generic state model/type parameters and runtime callbacks in + `app/business/extension/main.py` and `runtime.py`; preserve + `ExtensionBase.config` as a compatibility snapshot while adding + `get_config()` for authoritative reads. Persist explicit config changes at + command time and remove the base `on_close()` stale-snapshot write. +4. Add the exact public-route registry/claim (one focused new module), connect it + to `ExtensionPublication`, and consume it in `JWTMiddleware`. +5. Remove raw query values from generic request logs. Twitter handles and maps + callback/provider failures before they reach generic logging. +6. Advance root `pyproject.toml`, `app/version.py` and `pdm.lock` to Core + `0.1.1`, adding the frozen Authlib/HTTPX baseline. +7. Update nearest durable Core extension/routing/deployment guidance only where + the new public API or state contract makes existing guidance false. + +Verification for this batch: + +```text +PDM 2.27.0 frozen lock check +migration history and offline SQL checks +focused state/public-route/middleware tests +config command-time persistence and stale-Peer-disable regression test +empty-state version-change acceptance and non-empty-state rejection test +disposable PostgreSQL role/trigger/concurrency tests +database schema/runtime-contract generation +full pdm run check +git diff --check +``` + +## Batch 2 — Twitter Python Distribution + +On the same Core branch: + +1. Advance `extensions/twitter/pyproject.toml` to `0.2.0`, require Core + `>=0.1.1 <0.2.0`, and declare Authlib/HTTPX. +2. Define validated config/state/setup DTOs in the Twitter package. +3. Replace the hand-written in-memory OAuth endpoints/client with the Authlib + service and durable transaction/account protocol. Claim callback exchange + once with `pending -> exchanging`; do not add a lease/reclaim path for a + potentially consumed authorization code. A new Begin supersedes older + non-terminal flows so late callbacks cannot replace the newer user choice. +4. Declare the exact public callback and implement standalone bounded responses. +5. Add setup projection, OAuth App, begin/poll/disconnect, bookmark Source and + Finish endpoints. +6. Reuse `SourceManager.create()`, add explicit `CronManager.create/update()` + operations, and use the existing `CronManager.run_now()` for Finish. Do not + add setup idempotency keys, advisory locks or Job deduplication. +7. Fix empty-bookmark collection and restore each bounded official API operation + from fresh durable config/state in a newly closed Authlib client; do not retain + an official cross-operation singleton. Keep Twikit outside the setup UI and + retain its local session only while its fresh config binding matches. +8. Give every successful account authorization an opaque `authorization_id`; + bind Finish's Cron/initial Job config and readiness to that ID, and fail the job + before provider work if reconnect raced with Finish. Then update Twitter/Core + tests and wheel metadata/entry-point probes. + +Verification for this batch: + +```text +injected Authlib transport/time/random focused tests +restart/replay/overlapping-flow/conditional refresh, cross-Peer freshness and redaction tests +per-operation official client close and Twikit config-cache replacement tests +real PostgreSQL Source/Cron operations and current-account job tests +Twitter Source empty-result/first-job tests +build and verify all six first-party wheels +load/start/close Twitter wheel from site-packages +full pdm run check +``` + +## Batch 3 — Exact Core Contract for Client + +1. Start a task-owned Core runtime through the configured SSH Docker provider + from the exact Core feature branch. +2. Record the image tag, source revision, migration head and contract revision. +3. In a fresh `client-web` branch, configure the same SSH provider and run + contract sync/check against that exact image. +4. Confirm generated `extensions` includes `state`, revision is v3, and no + installation/binding relations return. +5. Stop the task-owned Core runtime after Client verification. + +The official stop command removes the instance-owned Compose/volume/state/tunnel. +Do not delete the source-revision image tag or run a broad Docker prune: that +cache is not instance-owned and may serve another task. + +This batch is a hard gate before Web source integration, not a later cleanup. + +## Batch 4 — Web Host and Popup Shell + +In the Client branch: + +1. Advance `packages/core/package.json` to `0.1.1` and update the lock/workspace + contracts. +2. Add `ExtensionSetupContribution`, store the loaded module in the running + record, and expose setup only for a running Extension. +3. Extend Host tests for contribution lifecycle and absence while disabled. +4. Add optional `AbortSignal` propagation to the existing authenticated + `Client.request` path. Its one-time 401 retry preserves body, signal and all + non-auth headers while replacing Authorization; do not add a Twitter + fetch/auth wrapper. Accept bounded string FastAPI `detail` errors without + stringifying validation objects. +5. Add the Extension-card Setup action, disabled explanation and `InkDialog` + shell; unmount the dynamic component and await one Vue tick before + disable/unload, and leave the existing expert config editor intact. +6. Keep Host/popup free of Twitter/Core command semantics. + +Verification: + +```text +@inkcre/core type-check and Host tests +extension-card component tests in happy-dom +contract:check against the exact Core image +``` + +## Batch 5 — Twitter Web Distribution + +1. Advance `extensions/twitter/package.json` to Extension `0.2.0`, require + `@inkcre/core >=0.1.1 <0.2.0`, and update the lock. +2. Add Twitter-owned setup DTOs/API client and `TwitterSetupWizard.vue` plus + focused step components/styles. +3. Implement reachable-Core discovery, explicit Core enable, credential setup, + deliberate X link, bounded polling cleanup, Source selection/creation and + Finish. +4. Export the setup component beside the existing lifecycle from the MF entry. +5. Update native manifest/distribution tests for the new versions and bundle. +6. Use existing UI components and ordinary Vue state; add no generic wizard, + form framework, OAuth library or cross-Remote i18n protocol. + +Verification: + +```text +Twitter setup API/composable unit tests +Vue wizard resume/error/poll-cleanup tests +card -> dialog -> contributed component integration test +Twitter type-check/build and native mf-manifest closure check +full pnpm check +actionlint for unchanged delivery workflows +git diff --check +``` + +## Batch 6 — Review and PR Handoff + +1. Review both diffs for authority leakage, secret/log exposure, state/config + confusion, callback lifecycle and generated-contract provenance. +2. Repeat the read-only Registry lookup for `inkcre/twitter@0.2.0`; stop for + version review if it is no longer unused. +3. Rebase/check against current upstream before committing. +4. After separate commit/push authorization, create two new branches, commit only + task changes, push, and open two new PRs with the Core dependency recorded in + the Client PR. +5. Wait for repository checks and fix only failures attributable to these PRs. +6. Keep Core independently green. Open Client as Draft/reviewable and record that + its stable-Core workspace/E2E checks are an expected dependency gate, not a + failure to bypass. Its exact unmerged Core image is valid review evidence; + after Core lands, contract generation and checks must be refreshed against + the admitted revision before Client can become merge-ready. +7. Stop at both PRs ready for Sir's review. + +This handoff follows organization governance: each branch starts from its +repository's latest `main`; PR descriptions record intent, evidence, risk, +rollback, migration/delivery effects and the cross-repository dependency; +required checks are refreshed against the latest base; no direct `main` push, +production credential, canonical publication or merge is used. The two PRs are +cross-repository dependencies rather than one Git ancestry stack; merge order is +still Core first, then Client after refreshing its exact upstream evidence. +No Client workflow input or alternate image lane is added merely to make a +dependent Draft appear green before stable Core carries contract v3. + +Explicitly excluded from every batch: merge, Registry publication, production +deployment, real-X black-box acceptance and cleanup of user-owned unrelated +worktrees. + +## Concrete File and Test Map + +The exact implementation may split a deep module when that improves +readability, but it must remain within these owned surfaces. + +### Core branch `feat/extension-setup-wizard-core` + +| Concern | Primary files | Focused evidence | +| --- | --- | --- | +| State schema/authority | `app/schemas/extension/main.py`, `app/business/extension/state.py`, `app/database_contract/*`, new Alembic revision | `tests/migrations/test_extension_registry_schema.py`, peer-role/RPC tests, new `tests/test_extension_state.py` | +| Host state API | `app/business/extension/main.py`, `runtime.py`, package exports/routes | existing `tests/test_extension_registry_runtime.py` plus typed-state and cross-Peer config freshness cases | +| Public callback | new `app/business/extension/public_http.py`, `runtime.py`, `app/middleware.py` | new `tests/test_extension_public_http.py`, `tests/test_jwt_contract.py`, logging tests | +| Twitter setup | `extensions/twitter/{__init__,api,schema,bookmark}.py` plus a focused setup/OAuth module | `tests/extensions/test_twitter.py` and new setup-focused tests | +| Source/Cron/Finish | `app/business/{source/main,cron}.py`, Twitter setup flow | Source/Cron operation and Twitter setup tests | +| Versions/dependencies | root and Twitter `pyproject.toml`, `pdm.lock`, `app/version.py` | `tests/test_extension_distribution.py`, six-wheel probe | +| Generated/durable truth | migration integrity, deployment profile, schema/OpenAPI and nearest guidance | migration/schema/deployment-profile checks | + +### Client branch `feat/extension-setup-wizard-web` + +| Concern | Primary files | Focused evidence | +| --- | --- | --- | +| Generated contract | `packages/core/src/database/database.generated.ts`, `runtime-contract.generated.json` | exact-image `contract:sync` then `contract:check` | +| Web Host API | `packages/core/src/extension/{model,host,index,postgrest-state}.ts`, `packages/core/src/client/client.ts`, package manifest/lock | Host tests, Client abort/401-retry/error tests and PostgREST omission of raw `state` writes | +| Card/popup | Extension card Vue/TS/SCSS/docs and Extensions view | new Extension-card component spec | +| Twitter wizard | `extensions/twitter/src/index.ts` and new `src/setup/*` | setup API/state/component specs | +| MF version/delivery | Twitter/package/core manifests, lock and existing MF verifier fixtures | native MF build/closure tests and actionlint | + +## Exact Final Command Set + +Core, using repository-pinned PDM behavior: + +```text +uv tool run --from pdm==2.27.0 pdm lock --check +uv tool run --from pdm==2.27.0 pdm run check:migrations +uv tool run --from pdm==2.27.0 pdm run test +uv tool run --from pdm==2.27.0 pdm run check +uv tool run --from pdm==2.27.0 pdm run python scripts/dev_database.py ensure +git diff --check +``` + +Client, after resolving the exact remote-daemon Core image tag: + +```text +pnpm install --frozen-lockfile +pnpm contract:sync -- --image +pnpm contract:check -- --image +pnpm --filter @inkcre/core type-check +pnpm --filter @inkcre/client-web test +pnpm --filter @inkcre/ext-twitter type-check +pnpm --filter @inkcre/ext-twitter build +pnpm check +go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 .github/workflows/*.yml +git diff --check +``` + +Stop the task-owned Core runtime with +`scripts/dev_database.py stop ` after contract evidence. Any +command spelling that differs in the checked-out upstream must be reconciled +against that repository's own scripts before mutation; no ad-hoc replacement +check is acceptable. diff --git a/tasks/extension-setup-wizard/50-readiness-review.md b/tasks/extension-setup-wizard/50-readiness-review.md new file mode 100644 index 0000000..688401b --- /dev/null +++ b/tasks/extension-setup-wizard/50-readiness-review.md @@ -0,0 +1,179 @@ +# Implementation Readiness Review + +## Evidence Closed + +- Current upstream `origin/main` for ext-reg, Core, Client and organization + workflow policy was inspected from clean snapshots, not inferred from older + task branches. +- Product authority, config/state/Source boundaries, callback topology, Web/Core + Host SDK boundaries and delivery stop condition are accepted. +- Core database schema, role/trigger model, lifecycle publication, middleware, + Extension consumer, Source/job APIs and current Twitter implementation were + traced to concrete symbols. +- Client Host lifecycle, installed state port, `Client` peer discovery, card, + `InkDialog`, Twitter MF entry, native artifact checks and delivery workflows + were traced to concrete symbols. +- X OAuth requirements and Authlib's supported async PKCE/refresh path were + checked against primary documentation. +- An isolated Authlib `1.7.2`/HTTPX probe proved S256, authorization-code token + exchange, `client_secret_basic`, automatic refresh and `update_token`. +- A temporary PDM `2.27.0` resolution proved Authlib/HTTPX fit the existing Core + dependency graph without changing the real repository. +- The absence of local Docker was checked. The existing SSH Docker provider, + target alias and cross-repository contract-sync path were verified as the + planned exact-image mechanism. +- Existing first-party wheel and MF CD paths already support the same + `inkcre/twitter@0.2.0` Release; ext-reg requires no source change. +- A read-only production Registry probe confirmed `0.1.1` is the current + two-association Twitter Release and `0.2.0` is unused, so the immutable + version plan has no collision. +- Organization `CONTRIBUTING.md` and `GOVERNANCE.md` were re-read: the plan uses + latest-main focused branches, PR-only changes, repository checks, explicit + dependency/evidence/risk/rollback notes, and protected-main-only publication. + +## Independent Plan Review — 2026-08-13 + +The first closed draft was challenged against multi-Core execution, one-use +OAuth codes, Source cardinality, generic logging and cross-repository admission. +It was not accepted unchanged. The review found and corrected twelve material +problems: + +1. **Stale config/runtime cache across Core Peers.** State reads were fresh, but + `ExtensionBase.config` and `TwitterAPI.SINGLETON` were still process-local + snapshots. The corrected plan adds authoritative `get_config()` reads for + setup/provider operations, uses a fresh closed Authlib client for each + official provider operation, and limits the retained Twikit cache to matching + fresh config. +2. **False OAuth crash recovery.** An exchange lease could not determine whether + X had consumed the one-use authorization code. The corrected state machine + has one `pending -> exchanging` claim and no reclaim; a crash leaves that + transaction to expire while the user starts a new one. +3. **Accidental Source uniqueness.** `ensure_one` implied one Source per type, + which the Source product does not guarantee. The corrected Source authority + only ensures that at least one initial Source exists and preserves multiple + existing Sources. +4. **Review evidence mistaken for upstream admission.** An exact unmerged Core + image is valid for Client review, but not final merge authority. The Client PR + is now explicitly merge-blocked until Core lands and its generated contract + is refreshed against the admitted revision. +5. **Overlapping OAuth flows could reorder user intent.** A late older callback + could otherwise overwrite a newer authorization. Begin now supersedes prior + non-terminal flows and every callback commit rechecks the transaction state. +6. **Old collection evidence could produce false readiness.** A finished job + from a previous account could otherwise make a reconnect immediately ready. + A timestamp alone still races between provider validation and job creation. + Account state now carries an opaque `authorization_id`; Finish jobs and + readiness must match it, and a raced job fails before provider work. +7. **Teardown could roll config backward.** Base `on_close()` currently persists + its local config snapshot. A stale Core Peer could therefore overwrite a + newer OAuth App committed elsewhere merely by disabling. The plan moves + config persistence to explicit command time and makes base teardown read-only + with respect to config. +8. **State compatibility was not actually preflightable.** The draft promised + that an incoming Distribution would validate existing state before a version + change, but no migration metadata exists and loading future bytes would mutate + the active interpreter. The MVP now permits version change only for empty + state and rejects non-empty state before mutation. +9. **The planned polling cancellation API did not exist.** Current + `Client.request()` has no `AbortSignal`, so the wizard would otherwise fork + authentication and retry logic. The corrected plan extends that existing + deep client path and tests signal propagation through its 401 retry. +10. **The existing 401 retry drops JSON headers.** A refreshed OAuth-App PUT + could otherwise replay its body without `Content-Type`. The same Client + correction now preserves every non-auth request field and replaces only the + Authorization header. +11. **FastAPI blockers were hidden by the Client error parser.** Core uses a + bounded string `detail`, while Client only recognized `message`/`error`. + The corrected deep client path accepts string `detail` but does not stringify + structured validation input. +12. **The Client PR could not honestly be green before Core admission.** Its + GitHub checks deliberately resolve protected stable Core, not a feature + image. The corrected handoff keeps Core green, opens Client Draft/reviewable + with exact local evidence, leaves the stable check as an explicit dependency + gate and adds no CI bypass. + +The review also removed raw query values from generic logging, required Twitter +to bound provider exceptions before generic/Source-job logging, and made +public-route claim ordering/rollback explicit. It deliberately did not redesign +Core-wide exception observability. No new framework, service, table, callback +broker or cross-process invalidation channel was added. + +## Design Review Against Common Failure Modes + +### No hidden generic framework + +There is no generic setup engine, OAuth broker, transaction service, second +state table, capability matcher, Source ownership layer or notification system. +The only generic APIs are the two proven reusable needs: typed Extension state +and one optional Web setup contribution. OAuth and wizard semantics remain +Twitter-owned. + +### No authority inversion + +Core/PostgreSQL owns Extension-state transactions. Source concurrency remains +in the Source domain. Extension code receives typed operations, not database +objects. Web uses semantic Twitter commands and never interprets raw state. + +### No browser-origin coupling + +The provider calls a Core-owned exact callback. Web observes an ordinary +authenticated transaction endpoint. No opener, `postMessage`, redirect URI or +JWT binds Core to the Web origin. + +### No compatibility ambiguity + +Core and Web Host SDKs advance to `0.1.1`; Twitter advances both native +Distributions to `0.2.0` and requires the new Host versions before bytes are +loaded. Existing Extensions remain compatible. + +### No generated-contract shortcut + +Client generated types come from an exact unmerged Core branch image on the +configured SSH Docker daemon. They are not hand-edited and do not require an +early merge. + +### No delivery-scope expansion + +Two source PRs are sufficient. ext-reg source, publisher workflows, deployment +and black-box acceptance remain unchanged/out of scope. + +## Residual Risks With Chosen Behavior + +These are implementation risks with specified handling, not unanswered design +questions: + +- X may revoke/rotate credentials: preserve durable account facts and project + `reconnect_required`; never silently clear Sources. +- Two processes may attempt provider refresh: Core serializes state commits and + conditional writes reject stale token replacement; a transient loser fails + without erasing a newer account. +- A Core may die after claiming a callback exchange: the one-use code is not + reclaimed; that transaction expires and the user starts a new one while an + existing account remains. +- Registry/Core may be unavailable: wizard reports operational unavailability + and leaves durable config/state/Sources unchanged. +- First collection may fail: Finish can be retried and may enqueue another Job. + That duplicate is acceptable; durable authorization guards provider work. +- English-only first-slice copy is accepted as a presentation limitation rather + than introducing a cross-Remote localization contract. +- The trusted generic PostgREST/config API can still bypass Twitter's semantic + OAuth-App command. That existing operator boundary is intentionally retained; + the Twitter projection reports incompatible direct edits as invalid without + silently deleting the prior account. Tightening all Extension config writes + is a separate product contract. + +## Exit Assessment + +Product design, corrected HLD, repository/file plan, version plan, dependency +feasibility, exact contract generation path, focused verification and PR +handoff are all specified. The independent review found concrete defects and +the packet now incorporates their resolutions. There is no unfinished +investigation, exploratory branch, spike or schema/API choice required before +source implementation. + +The remaining gates are procedural only: + +1. Sir reviews this implementation proposal. +2. Sir gives the separate explicit start required before source mutation. +3. Commit/push/PR operations remain separately unauthorized until explicitly + requested. diff --git a/tasks/extension-setup-wizard/60-implementation-result.md b/tasks/extension-setup-wizard/60-implementation-result.md new file mode 100644 index 0000000..3cdf8a1 --- /dev/null +++ b/tasks/extension-setup-wizard/60-implementation-result.md @@ -0,0 +1,242 @@ +# Implementation Result + +## Outcome + +The setup-wizard source implementation is complete in two task worktrees and +published for review as Draft PRs: + +- Core: `feat/extension-setup-wizard-core` from + `63f57b26ed8685fa34a74516ade39e2af72218d9`; +- Client: `feat/extension-setup-wizard-web` from + `4fdc0832316d4489b9204b321cbe65b2c5ccb2e8`. + +Review heads: + +- Core commit `9f779b2`, + https://github.com/InKCre/core-py/pull/52; +- Client implementation commit `f25e684` plus the Client-management follow-up + commit `e1ee0e3`, + https://github.com/InKCre/client-web/pull/68; +- task packet initial commit `3fbb867aa6bdd00600bb811a97e22495b915707a`, + https://github.com/InKCre/ext-reg/pull/13. + +Admitted preview-controller corrections: + +- Client [PR #69](https://github.com/InKCre/client-web/pull/69) was merged as + `be2198f`. It separates the stable-Core + `Database contract` check from the preview-producing `Workspace contract` + without weakening production delivery or the intended merge gate, and it + serves the exact checked Twitter Module Federation snapshot plus a read-only + exact Release descriptor from the same PR preview origin. +- Core [PR #53](https://github.com/InKCre/core-py/pull/53) was merged as + `22f3f40`. A Core PR preview now includes both Core API and PostgREST apps on + the same isolated Neon branch and JWT trust boundary. + +No ext-reg service source change was necessary. The Registry continues to own +the Python wheel and Module Federation Distribution associations only. + +## Core Result + +- Added canonical deployment-wide `extensions.state JSONB`, database contract + v3 and migration head `c7d8e9f0a1b2`. +- Kept state mutation inside Core/PostgreSQL row-lock operations; authenticated + PostgREST may read state inside the accepted Peer boundary but cannot write it. +- Added typed fresh config/state operations to `ExtensionBase` without exposing + `ExtensionModel` to an Extension. +- Added lifecycle-bound exact public HTTP route claims; only the running + Twitter callback bypasses Peer JWT, and route withdrawal removes the bypass. +- Implemented durable Twitter OAuth App/account/transaction state with Authlib, + PKCE S256, standalone Core callback HTML, polling projection, disconnect, + Bookmark Source selection/creation and Finish/initial collection. +- Made provider token refresh conditional on the exact prior durable token; + provider OAuth/401 rejection marks that authorization as requiring reconnect + without erasing the account or its Sources. +- The earlier implementation serialized and deduplicated initial collection. + D030 supersedes that choice: current reconstruction uses ordinary Source + creation and Cron run-now, accepting rare duplicate work for better ROI. +- Advanced Core Host SDK to `0.1.1` and the Twitter Python wheel to `0.2.0`. +- Preserved Source authority: uninstall does not inspect/delete Sources, while a + deleted formerly selected Source makes setup incomplete instead of producing + false readiness. + +Implementation refinements from the HLD examples: + +- Twitter commands return a compact refreshed `TwitterSetupStatus` projection + rather than several command-specific wrapper objects; Begin remains `201`. +- Disconnect returns the refreshed projection (`200`) so Web does not need a + second read after a successful command. +- Selecting an existing Bookmark Source does not edit it implicitly; schedule + editing stays on the Sources surface. Creating a Source still accepts the + setup schedule. +- The exact public-route claim lives with the existing reversible runtime + publication primitives rather than in another shallow module. + +## Client Result + +- Added optional `ExtensionSetupContribution` to the running Web Extension + module contract. The Host exposes it only while that exact runtime is active. +- Added the Extension-card Setup action and `InkDialog` shell. Before disable, + the shell clears the contributed component and waits one Vue tick before the + Host unloads the Remote. +- Extended the existing authenticated `Client.request` path with `AbortSignal`, + preserved body/headers/signal through its one-time 401 retry and surfaced + bounded string FastAPI `detail` errors. +- Added the Twitter-owned four-step wizard: Prepare Core, configure OAuth App + and connect X, choose/create Bookmark Source, review/start. +- The wizard discovers reachable Core Peers, requires explicit enablement, + shows the selected Core callback URL, opens X deliberately in another tab and + polls only the opaque transaction projection. Closing/unmounting aborts all + wizard requests and polling. +- Advanced `@inkcre/core` to `0.1.1`, Twitter Web to `0.2.0`, and generated the + database types/runtime evidence from Core contract v3. + +## Verification Evidence + +Core: + +```text +PDM_IGNORE_ACTIVE_VENV=1 uv tool run --from pdm==2.27.0 pdm run check + migration checks: 31 + 23 passed + format/lint/type checks: passed, 0 diagnostics + pytest: 241 passed +git diff --check: passed +``` + +Client: + +```text +pnpm check + format/lint/type checks: passed + Vitest: 21 files, 90 tests passed + all workspace builds and package contract: passed +go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 .github/workflows/*.yml + passed +git diff --check: passed +``` + +Cross-repository contract: + +```text +Core development image -> contract revision peer-database-runtime-v3 +Client contract:sync -> passed +Client contract:check against exact task-built image + sha256:1159b60622cf3b24caa32a037d8c28ffaef79262523f7dd2565393e1b09d2dfd + -> passed +Generated extensions relation includes state; generated contract reports v3 +``` + +The contract artifact was exported from the migrated task-owned PostgreSQL +runtime using the same schema/role/runtime-contract packaging path as CI, then +embedded in the final task image. Checkout-only generated SQL/JSON staging files +were removed after Client verification, as required by the Core repository +contract. + +Read-only Registry check: + +```text +GET https://registry.inkcre.dev/v1/extensions/inkcre/twitter/releases/0.2.0 +404 {"detail":"public Release does not exist"} +``` + +## Preview Acceptance Evidence + +The bounded black-box gate requested after implementation is now complete: + +- Client PR #68 preview: + `https://preview-client-web-pr-68.inkcre-client-web.pages.dev`; +- Core PR #52 API preview: + `https://inkcre-core-py-pr-52-5989d607b441.herokuapp.com`; +- Core PR #52 PostgREST preview: + `https://inkcre-postgrest-pr-52-0c434df03cac.herokuapp.com`; +- the isolated preview database contains `inkcre/twitter@0.2.0`, enabled for + the preview Web Peer; +- the Client preview's same-origin exact Release and native + `mf-manifest.json` both return successfully; +- a cold browser load restores and activates the Twitter Remote, renders the + card action as `Set Up`, and opens `Set Up Twitter` with the four steps + `Prepare`, `Connect account`, `Bookmark Source`, and `Review and start`. + +The final cold-start defect was a UI/runtime ordering race: the Extensions view +could render before the app shell completed the initial Web Extension Host +restore. Client commit `f25e684` makes both callers share one startup Promise +and waits for that restore before listing cards. The full Client contract and +the live PR preview both passed after the correction. + +The Client PR's independent `Database contract` check still fails against the +admitted stable Core v2 contract. That is intentional evidence that #68 cannot +merge before Core #52 is admitted and the generated Client contract is +refreshed; it no longer prevents the review Preview from deploying. + +The Client-management follow-up was accepted against the same preview on +2026-08-14: + +- Client checks run `31786048471` consumed exact head + `e1ee0e3102c308ad4a93b7211f870f123d406f5c`; `Workspace contract`, action + syntax, dependency review, TypeScript shadow checks and client-webext E2E + passed; +- Pages preview run `31786198325` resolved that exact internal PR head, + downloaded its checked Web and Twitter artifacts, reverified the PR identity + before delivery and deployed Cloudflare Pages deployment + `5dd8bc7e-4019-49a4-8050-ceca424368e3`; +- the stable preview alias and its read-only `inkcre/twitter@0.2.0` exact + Release and native `mf-manifest.json` all returned `200`; the Release declared + `@inkcre/core >=0.1.1 <0.2.0`, while the manifest used the same-origin + immutable preview asset prefix and exposed two modules; +- the restored Settings page showed both this-browser configuration (including + Extension Registry URL) and all deployment Clients; +- the Extensions selector listed every registered Client, defaulted to this + browser and projected another unaddressable Web Client as `OFF` with the + explicit desired-state warning; +- through the UI, the sole enabled current Web Client was disabled, + `inkcre/twitter` was changed from `0.1.1` to `0.2.0`, and the current Web + Client was enabled again against the read-only preview Release; +- a subsequent cold reload restored `twitter@0.2.0`, rendered `Set Up`, and + opened the four-step `Set Up Twitter` dialog; selecting the disabled remote + Web Client changed its switch projection to `OFF` without hiding `Set Up`. + +The `client-web E2E` failure in the same source workflow is the same admitted +baseline split as the independent database-contract failure: those tests ask +the stable Core image for the former `clients` relation and receive `404`, while +this PR intentionally remains on the pre-Peer Client contract until Core #52 is +admitted. It did not weaken or bypass the preview artifact gate. + +## Cleanup and Handoff + +The task-owned Core Compose project, PostgreSQL volume, SSH tunnel/control +socket, runtime credentials/descriptors, generated contract staging files and +temporary Registry response were removed. The source-revision Docker image was +left as a normal reusable build cache, following the implementation plan; no +broad Docker prune was run. + +Remaining actions: + +1. Sir continues the wizard/provider acceptance from the live PR preview; +2. obtain Sir's review; do not merge Core #52 or Client #68 merely because the + bounded entry-point acceptance passed; +3. after Core admission, regenerate Client evidence against admitted Core and + satisfy the independent `Database contract` gate before Client admission; +4. publish `inkcre/twitter@0.2.0` and perform final delivery only under explicit + later authorization. + +## Client Selector and Settings Follow-up + +Continued acceptance exposed that the native-Distribution cutover had removed +the former Extensions-page Client selector and that an earlier Settings +refactor had orphaned the all-Client management components. The authorized +follow-up restores both without changing the canonical Extension row or Host +contracts: + +- the selected Client controls the card switch; +- this browser uses the local Web Host lifecycle; +- an addressable Client uses its generic Extension Host API; +- an unaddressable Client uses the existing atomic state-port RPC as durable + desired state, with an explicit warning that a running remote process cannot + be synchronously stopped; +- setup availability remains a contribution of this browser's running Web + Distribution rather than the selected Client's switch state; +- Settings again lists and edits all registered Clients beneath a separate + deployment scope. + +The restored Client card also replaces its historical nonexistent inline-input +`confirm` event with an explicit, schema-validated update of an existing Client. +No create/delete lifecycle was added. diff --git a/tasks/extension-setup-wizard/70-client-selector-and-settings-follow-up.md b/tasks/extension-setup-wizard/70-client-selector-and-settings-follow-up.md new file mode 100644 index 0000000..65a28b6 --- /dev/null +++ b/tasks/extension-setup-wizard/70-client-selector-and-settings-follow-up.md @@ -0,0 +1,207 @@ +# Client Selector and Client Settings Follow-up + +## Status + +Implementation, review fixes, commit, push and bounded PR-preview acceptance are +complete on Client PR #68. The exact source head is `e1ee0e3`; no Core, +Registry-service or database-contract source changed in this follow-up, and no +PR was merged. + +## Acceptance Finding + +The current Extensions page reports only a deployment-wide enabled count while +its switch always controls the current browser Client. During preview +acceptance, the current Web Client was enabled manually, so `Enabled on 1 +Peer(s)` does not prove that Core is the enabled Client. The UI currently gives +the user no way to identify and disable every member of `extensions.enabled[]` +before changing the shared version or uninstalling the Extension. + +This is a regression, not a new interaction model: + +- Client commit `784ae12` rendered a Client selector at the top of the + Extensions list, defaulted it to the current browser Client, and passed the + selected Client ID to every Extension card; +- Registry integration commit `c488ebe` retained a selected-Peer variant; +- native Module Federation cutover commit `cae553f` removed the selector and + reduced the page to the current browser Client; +- Client commit `784ae12` also rendered `ClientList` in Settings; +- commit `2b31ba9` removed that list while making environment configuration + read-only. Current Client configuration was later restored, but all-Client + management was not; +- `clientList.vue` and `clientCard.vue` still exist and are currently orphaned. + +## Product Behavior + +### Extensions page + +Add one Client selector above the Extension list. It defaults to the current +browser Client and lists every row returned by `Client.list()`. Each option +shows the Client nickname and a short identity hint; the current browser Client +is explicitly identified. + +For every card: + +- switch state is `extension.enabled.includes(selectedClient.id)`; +- selecting another Client changes only the switch projection, not the shared + Extension row or version controls; +- the total enabled count remains visible because version change and uninstall + are deployment-wide operations and remain disabled until `enabled[]` is + empty; +- a successful enable/disable response replaces the displayed Extension row, + so the selector, count, version gate and uninstall gate stay coherent; +- a selector change never starts or stops a runtime by itself. + +### Enable/disable dispatch + +The page-level application coordinator owns topology dispatch: + +1. When the selected Client is this browser Client, call the existing + `WebExtensionHost.enable/disable`. This preserves Module Federation load, + lifecycle compensation and the atomic database RPC. +2. When another selected Client has `rest_api_url`, call its generic Host API: + `POST /extensions/{namespace}/{name}/enable|disable`, using the existing + authenticated `Client.request` path and `InstalledExtensionSchema`. +3. When another selected Client has no management endpoint, update its durable + desired enablement through the existing atomic + `set_extension_peer_enabled` state-port operation. Do not call a runtime + lifecycle that this browser does not own. + +For an unaddressable Client, the switch therefore means desired state: + +- enable is realized when that browser next starts or restores its Web Host; +- disable prevents its next restore, but an already-open remote browser may + continue running until it reloads or closes because it has no management + endpoint; +- the UI labels this limitation instead of claiming synchronous runtime + control. + +The database write still goes through the RPC rather than directly updating the +array. The database remains the concurrency authority and verifies that the +selected Client exists. + +This coordinator belongs to `client-web`; `WebExtensionHost` remains the Host +SDK for this browser only and remains unaware of database tables or remote +Client topology. No new Core route, schema, RPC or Registry contract is needed. + +### Setup entry is independent of the selected Client + +The selected Client controls only the card switch. The setup wizard is a +whole-Extension experience contributed by the running Web Distribution, so its +availability remains: + +`getExtensionHost().getSetupContribution(extension.name) != null` + +It must not be derived from the selected Client's enabled flag. Consequently, +selecting Core can show and control Core enablement without hiding a setup +entry already supplied by the current Web runtime. + +### Settings page + +Restore the existing all-Client list below two clearly separated scopes: + +1. this browser's bootstrap and Client configuration; +2. all registered Clients in the connected deployment. + +The all-Client section lists, refreshes, health-checks and edits Client name, +REST API URL and declared configuration through the existing Client model. It +does not add manual Client creation or deletion: Clients register themselves, +and this follow-up restores management of existing records rather than +introducing a second registration lifecycle. + +## Source Change Map + +Client PR #68 branch only: + +- `apps/client-web/src/views/extensions/extensions.vue` and + `extensions.scss`: load Clients, own selected Client, render selector and pass + selected/current identities to cards; +- a small `client-web` application coordinator near the Extensions view: + dispatch current-Web runtime operations, remote-Host operations, or remote-Web + desired-state operations without expanding `WebExtensionHost`; +- retain one initialized `ExtensionStatePort` behind the application + integration seam so the coordinator can invoke the existing atomic RPC for a + remote Client; do not expose SQL or PostgREST details to cards; +- `apps/client-web/src/components/extension/extensionCard/*`: separate + selected-Client switch state from current-Web setup contribution, expose + mutation availability/reason, and consume returned rows; +- `apps/client-web/src/views/settings/settings.vue` and `settings.scss`: restore + `ClientList` under an explicit all-Client section; +- existing `clientList/*` and `clientCard/*`: only focused correctness and UX + repairs needed by restored use; +- English and Simplified Chinese messages for selector identity, unavailable + management endpoint and Settings section labels; +- focused unit/view tests. Generated database files remain untouched. + +Core PR #52, Registry source and database schema are out of scope. + +## Verification Plan + +Focused tests must prove: + +1. selector defaults to the current browser Client and projects `enabled[]` for + the selected Client; +2. selecting the current browser Client dispatches through + `WebExtensionHost.enable/disable`; +3. selecting an addressable remote Client calls the exact generic Core Host + route and applies the returned Extension row; +4. an unaddressable remote Client uses only the atomic state-port RPC, + performs no local lifecycle call, and reports desired-state semantics; +5. selecting Core does not hide the current Web setup contribution; +6. version and uninstall guards continue to use the complete `enabled[]`; +7. Settings renders and refreshes the all-Client list and edits Client metadata + and configuration without conflating it with this browser's bootstrap form. + +Repository gates after the coherent edit batch: + +- focused Vitest suites for the view, card, coordinator and Client list; +- `pnpm --filter @inkcre/client-web type-check`; +- `pnpm check`; +- `git diff --check`. + +No live preview, merge, Registry publication or database mutation is implied by +source implementation. + +## Implementation Result + +- The Extensions page now defaults a Client selector to this browser, displays + every registered Client and projects each card switch from the selected + Client's membership in `enabled[]`. +- The application coordinator dispatches current-browser lifecycle, remote Host + API, or unreachable-Client desired-state RPC operations and verifies that the + returned row changed the selected Client. +- Failure to load the wider Client list now degrades to current-browser-only + management instead of hiding the Extension management surface. +- Setup contribution availability is independent of selector state; disabling + another Client does not close or stop this browser's setup/runtime. +- Settings now separates this browser's bootstrap/configuration from all + deployment Clients. The restored list handles load failure and health-check + completion; Client metadata uses an explicit validated update action rather + than the former nonexistent inline `confirm` event or an upsert lifecycle. +- No Core, Registry, generated database contract or delivery automation changed. + +Verification: + +- focused follow-up suites: 6 files, 17 tests passed after final review fixes; +- `pnpm --filter @inkcre/client-web type-check`: passed; +- `pnpm check`: passed with 27 files / 110 tests plus every workspace build and + package contract; +- `pnpm type-check:ts7`: passed; +- `pnpm lint:type-aware` still reports only pre-existing diagnostics in unchanged + generated/core/runtime files; none is in this follow-up's source; +- `git diff --check`: passed. + +Preview acceptance: + +- Pages preview workflow run `31786198325`: passed, using the exact checked + artifact from Client workflow run `31786048471` at `e1ee0e3`; +- stable preview URL: + `https://preview-client-web-pr-68.inkcre-client-web.pages.dev`; +- Settings exposed this-browser Registry configuration and the restored + deployment-wide Client list; +- the selector exposed every registered Client and the no-endpoint Web Client + displayed desired-state semantics; +- UI migration `twitter@0.1.1 ON -> OFF -> 0.2.0 -> ON` completed against the + preview's read-only native Release; +- cold restore rendered `Set Up`; the four-step wizard opened; selecting the + disabled remote Web Client retained the setup entry while projecting that + selected Client as `OFF`. diff --git a/tasks/extension-setup-wizard/71-preview-acceptance-follow-up.md b/tasks/extension-setup-wizard/71-preview-acceptance-follow-up.md new file mode 100644 index 0000000..66bb43f --- /dev/null +++ b/tasks/extension-setup-wizard/71-preview-acceptance-follow-up.md @@ -0,0 +1,151 @@ +# Preview Acceptance Follow-up + +## Status + +Local implementation and repository verification are complete. The changes are +not committed or pushed, and no preview or other remote state was mutated. The +two changes below form one adjacent preview-acceptance batch. + +## Finding 1 — The setup popup cannot be exited early + +The popup shell correctly belongs to `client-web`, while the complete Twitter +setup experience belongs to the Twitter Web Distribution. The shell disables +its own cancel/confirm controls and mounts the contribution's `close` event. +Twitter already emits that event, but renders its Close button only on the last +step. Consequently the user cannot exit while loading or from steps 1–3. + +### Accepted correction + +Twitter owns one always-available Close action outside the step-specific +panels. The existing final-step-only Close action is removed rather than +duplicated. Closing unmounts the Twitter contribution through its existing +`close` event, which already aborts polling and in-flight wizard work; durable +configuration, state and Sources are not rolled back. + +Do not restore a Host-owned dialog button, change the Host contribution API, +or add a regression test for this presentation correction. Run the existing +Twitter wizard and repository checks. + +## Finding 2 — Core ignores the current Client's Registry override + +Core currently constructs the process-global `ExtensionHost` with two objects +whose Registry origin is copied from `settings.extension_registry_url` at +module import time: + +- `RegistryReleaseClient`, used for the exact Release descriptor; +- `PipDistributionConsumer`, used for the Python Simple index and wheel. + +Neither path reads `clients.config.extension_registry_url`. Editing that Client +configuration therefore cannot affect install, enable or cold restore, and the +reported `Registry could not resolve inkcre/twitter@0.2.0` is expected whenever +the process-level origin points at a Registry without that Release. + +### Accepted precedence and scope + +For the current Client-based branch, the effective Registry origin is: + +1. the current Core Client's non-empty `config.extension_registry_url`; +2. the existing process setting/default. + +The Client override is read from authoritative storage at the start of every +Registry-backed operation, so an operator's saved change applies without a +Core restart to the next install or enable. An invalid non-empty override fails +with a configuration error rather than silently falling back. + +When Peer/deployment configuration is admitted later, only the resolver's +precedence expands: + +`Client override > deployment Registry URL > process fallback`. + +This batch does not backport the origin/main deployment/Peer configuration +model. + +### Per-operation consistency + +One operation snapshots one effective origin. Exact Release resolution and the +subsequent Simple-index/wheel acquisition must consume that same value; they +must not independently reread mutable Client configuration and accidentally +mix two Registries. + +The implementation therefore makes the origin an explicit per-operation input +to the Release resolver and Distribution consumer. `ExtensionHost` owns the +dynamic Client-aware provider and takes the snapshot before resolution. The +existing process setting remains a fallback, not a separately active Registry +lane. + +## Source Change Map + +### Client PR #68 + +- `extensions/twitter/src/components/twitterSetupWizard/twitterSetupWizard.vue`: + move the Extension-owned Close action outside step-specific content; +- `twitterSetupWizard.scss`: only the minimal alignment needed for that action; +- no Host dialog, contribution API, locale or test changes. + +### Core PR #52 + +- `app/business/extension/main.py`: resolve the current Client-aware origin for + each Registry-backed operation and retain one snapshot through resolution and + acquisition; +- `app/business/extension/release.py`: validate/normalize an explicit Registry + origin and accept it per exact Release request; +- `app/business/extension/distribution.py`: accept that same explicit origin for + Simple-index/wheel acquisition instead of retaining a constructor-time + origin; +- existing Core Host tests/fakes are updated for the interface; focused tests + lock Client override, fallback, invalid override and same-operation origin + consistency. + +No database schema, Registry service, deployment configuration, generated +contract, delivery workflow or public Release changes are part of this batch. + +## Verification Plan + +- run the existing Twitter wizard suite without adding a new presentation + regression test; +- run focused Core Host/Release/Distribution tests, including dynamic Client + override and one-operation origin consistency; +- run `pnpm check` in Client and the pinned full Core repository check; +- run `git diff --check` in both repositories; +- do not commit, push or mutate previews without separate authorization. + +## Implementation Result + +- Twitter now renders one Extension-owned Close action after the conditional + wizard content, so it remains available while loading and on every step. The + prior final-step-only action was removed. No Host code, contribution API, + locale or test changed. +- Core resolves the current Client's Registry override dynamically for each + Registry-backed operation. Empty/missing override falls back to the process + setting; a malformed non-empty override fails closed. +- Release resolution and Python Distribution acquisition now require the + Registry origin as an explicit per-operation argument. Neither concrete + consumer retains a second constructor-time Registry authority. +- One Host operation invokes its provider once and passes that exact normalized + origin to both consumers. Existing tests/fakes were adapted, and focused Core + tests cover override/fallback/invalid values and the one-snapshot invariant. +- Client `pnpm check` passed: 27 test files / 110 tests plus every workspace + type-check, build and package contract. +- Core focused checks passed: 49 tests and 0 type diagnostics. The full contract + passed under the repository-required isolated PDM 2.27.0: 254 tests, format, + lint, type, migration and lock checks. +- `git diff --check` passed in both source repositories. The first attempt to + run Core's full check with the host PDM 2.28.0 stopped at the intentional + version guard before any project check; rerunning with PDM 2.27.0 passed. + +## Preview Controller Diagnosis — Superseded + +Core PR #52's failing Preview application run `31774435213` successfully built +the candidate image, initialized the preview database, published both Heroku +apps and configured PostgREST. It failed only when the current main-owned +controller invoked `/app/scripts/configure_peer_runtime.py` inside the old +Client-based candidate image. + +The prior recommendation to admit Core PR #62 is rejected. A fallback that +skips Peer convergence would make current main's delivery controller support a +candidate that cannot satisfy the repository's admitted Peer runtime contract. +The correct repair is to reconstruct PR #52 from current main and port the +feature onto Peer authority. The resulting image contains +`configure_peer_runtime.py`, registers the exact Peer, publishes its capability +snapshot and passes the existing controller without a legacy branch. See +[Peer-native PR reconstruction](72-peer-native-pr-reconstruction.md). diff --git a/tasks/extension-setup-wizard/72-peer-native-pr-reconstruction.md b/tasks/extension-setup-wizard/72-peer-native-pr-reconstruction.md new file mode 100644 index 0000000..cad6432 --- /dev/null +++ b/tasks/extension-setup-wizard/72-peer-native-pr-reconstruction.md @@ -0,0 +1,192 @@ +# Peer-native PR Reconstruction + +## Result of the Preview Diagnosis + +Core PR #52 head `5804f73` is based on the old technical Client domain and does +not contain `app.business.peer`, `inkcre.peers` or +`scripts/configure_peer_runtime.py`. Current Core main already admitted the +hard Client-to-Peer cutover, exact capability advertisement, deployment config, +global Job/Cron runtime and a materially revised Source/Extension Host. + +The failed preview therefore revealed a stale feature baseline. It did not +justify making current main's controller conditionally skip its own Peer +contract. Core PR #62 is not part of the solution. + +## Chosen Integration Shape + +Keep Core PR #52 and Client PR #68 as the review surfaces, but rebuild their +contents on admitted upstream truth: + +```text +current core-py main (Peer authority) + + Extension state/setup/Twitter 0.2.0 + + Peer/deployment Registry origin resolution + -> exact Core feature image and database contract + -> client-web PR #68 contract sync + + Web Peer runtime/SDK + + Peer selector/settings and capability invocation + + Twitter setup contribution +``` + +Do not merge old main into PR #52 and mechanically resolve thousands of lines, +and do not cherry-pick the old 35-file setup commit over current main. Both +would resurrect deleted Client, Source scheduler and Host structures. Build a +clean integration branch from current `origin/main`, reapply the accepted +product behavior against current APIs, verify it, then update PR #52's existing +remote head with `--force-with-lease` against its observed old SHA. Preserve the +old remote/local heads as recovery refs until the rewritten PR passes. + +The two local JWT commits on the old worktree are not replayed: current main PR +#61 already owns the unified signing authority. The uncommitted Client-based +Registry resolver is retained only as behavioral evidence and is reimplemented +through Peer/deployment authority. + +## Core PR #52 — Exact Implementation Plan + +### 1. Canonical Extension state on current schema + +- Add `extensions.state JSONB NOT NULL DEFAULT '{}'` with a new append-only + revision from current main's migration head; do not reuse the old branch's + `c7d8e9f0a1b2` revision. +- Advance the current database contract/protocol projections and exact role/ + trigger checks. Preserve `peers`, `configs`, Jobs, Crons and every unrelated + relation. +- Port the accepted `InstalledExtension`/`ExtensionStore` naming, typed + config/state reads and Core-owned row-lock mutation callbacks. +- Keep config writes explicit and remove stale config persistence from base + `on_close()`. Reject version change when state is non-empty at both Core and + database authority boundaries. + +### 2. Peer-native Host and callback lifecycle + +- Keep `PeerManager.get_current_peer_ref()` as the only runtime identity for + enable, disable and cold restore. +- Port the exact public-route claim into the current publication snapshot so + only `GET /twitter/auth/callback` bypasses Peer JWT while Twitter is running; + teardown withdraws the claim. +- Retain the current Extension management capability + `core.extension.management.v1`; do not revive direct Client management. +- Add one Twitter-owned typed capability, `inkcre.twitter.setup.v1`, backed by a + fixed authenticated Peer HTTP inbound. It carries discriminated setup + commands/results for status, OAuth App save, begin/poll/disconnect, bookmark + Source selection and Finish. It is advertised only while Twitter is running + and is withdrawn before teardown. The public OAuth callback is not sent + through this capability. + +### 3. Registry and callback address authority + +Add one deployment-wide Extension Registry config contract and an +owner-specific override in each Host Peer. The Core resolution path is: + +```text +CorePeerConfig.extension_registry_url (non-empty override) + > configs[extension.registry].extension_registry_url + > settings.extension_registry_url +``` + +Validate every configured value as one HTTP(S) origin. Resolve it at the start +of each Registry-backed Host operation and pass one immutable snapshot through +the exact Release request and pip/Simple acquisition. A malformed explicit +override fails closed. + +The Twitter redirect URI is derived from the executing Core Peer's validated +`CorePeerConfig.http_public_base_url`, which preview/production delivery already +converges. Do not restore `CLIENT_BASE_URL`, infer a request origin or add a +Web/Core `postMessage` channel. + +### 4. Twitter setup on current Source, Cron and Job authorities + +- Port the Authlib PKCE/account/transaction state machine onto the current + Twitter/graph code; do not overwrite current bookmark graph production with + the old branch implementation. +- Keep the Twitter setup wire concept `collect_at`, but project it to the + current Cron domain rather than restoring removed `sources.collect_at`. + Twitter state records only selected `bookmark_source_id` and the selected + setup Cron reference; Source, Cron and Job rows remain their own authorities. +- Reuse ordinary Source creation, add explicit Core Cron create/update methods, + and use the existing run-now operation for the initial Job. UI pending state + handles normal double-submit; rare duplicate Sources/Jobs are accepted for + this single-user product. +- Bind Cron and initial Job parameters to the current opaque + `authorization_id`. Reconnect makes prior work ineligible; Finish rebinds the + selected schedule and enqueues one bounded collection Job. +- Uninstall deletes only the Extension row. It does not inspect or delete + Sources, Crons or Jobs; their domains retain unreachable-type behavior. +- Advance Core Host SDK and Twitter Python Distribution to the already accepted + versions and re-run the six-wheel/site-packages lifecycle proof. + +### 5. Core verification + +Run focused pure and disposable-PostgreSQL tests for: + +- state migration, role/trigger authority and non-empty-state version gate; +- Peer override/deployment/process Registry precedence and one-operation + snapshot; +- exact public callback claim and Twitter setup capability advertisement/ + withdrawal; +- OAuth restart/replay/overlapping transactions and bounded error redaction; +- Source creation, Cron schedule projection, Finish enqueue and reconnect + authorization races; +- current Twitter graph collection, empty results and wheel lifecycle. + +Then run repository-pinned full checks, migration integrity, offline SQL, +schema/runtime contract generation and `git diff --check`. Build the exact final +Core image and run its preview database/readiness contract before updating the +remote PR head. + +## Client PR #68 — Dependent Peer Migration Plan + +After the exact Core feature image is green: + +1. Sync generated database/runtime contracts from that image. The technical + relation is `peers`; no `clients` compatibility type/table remains. +2. Replace the technical `Client` active record with `Peer`/`PeerRef` and a + small Web Peer runtime that registers this browser's generated Peer ID, + publishes an empty capability snapshot, renews its lease while active and + stops renewing on shutdown so database-time expiry owns liveness. Its + owner-specific config schema retains `extension_registry_url`, using the + same Peer-override/deployment-default/library-fallback order as Core. + Product/repository wording such as `client-web` and ordinary user-facing + “Client” may remain where it does not describe the technical runtime node. +3. Implement the shared Peer HTTP v1 consumer on the library happy path: + discover live exact capability advertisements, issue the normalized + authenticated envelope and preserve the protocol's no-replay boundary. +4. Rework the selector/control coordinator: + - current Web Peer -> local Web Host lifecycle; + - selected Peer advertising `core.extension.management.v1` -> exact + capability invocation; + - selected Peer without that capability -> atomic desired-state RPC only. +5. Rework Settings to list/manage Peer rows. Core-owned + `extension_registry_url` appears through the Core Peer config schema; the + deployment default remains the deployment-config resource, not browser meta + config. +6. Rework Twitter candidate discovery to use live + `core.extension.management.v1` Peers and `inkcre.twitter.setup.v1`, not + `Client.list()`, `rest_api_url` or probing arbitrary origins. The wizard + keeps its current four-step/close UI and talks only through the typed Twitter + capability. +7. Re-run generated-contract checks, Web Peer/protocol/selector/settings/setup + tests, Twitter MF build/closure, full `pnpm check`, actionlint and + `git diff --check`. + +## Delivery and PR Sequence + +1. Reconstruct and verify Core locally. +2. With separate commit/push authorization, force-with-lease update existing + Core PR #52; wait for its repository, database and Peer preview checks. +3. Build the exact checked Core head and sync Client PR #68 to it. +4. Verify Client locally, then ordinary commit/push under separate authority; + wait for its checks and Pages preview. +5. Run the bounded cross-preview gate: browser Peer registration, Extension + list/selector, enable Twitter on the chosen Core Peer, setup contribution, + callback URL and wizard entry. Real provider OAuth remains Sir-owned unless + separately delegated. +6. Close PR #62 as superseded only with explicit remote-state authorization. + Do not merge any PR in this batch. + +## Readiness Decision + +Investigation, topology review and implementation planning are complete. The +source batch is ready to begin only after Sir explicitly authorizes this +Peer-native reconstruction. Source edits do not imply permission to commit, +rewrite/push PR #52, push PR #68, close PR #62, deploy previews or merge. diff --git a/tasks/extension-setup-wizard/73-peer-native-implementation-progress.md b/tasks/extension-setup-wizard/73-peer-native-implementation-progress.md new file mode 100644 index 0000000..14c7cc0 --- /dev/null +++ b/tasks/extension-setup-wizard/73-peer-native-implementation-progress.md @@ -0,0 +1,112 @@ +# Peer-native Implementation Progress + +## Batch Status + +Sir authorized source implementation after approving the reconstruction plan in +`72-peer-native-pr-reconstruction.md`. The implementation is active in two new, +uncommitted worktrees; the obsolete Client-based PR worktrees remain untouched +as recovery/reference surfaces: + +- Core: `feat/extension-setup-wizard-core-peer`, based on current Core main; +- Client: `feat/extension-setup-wizard-web-peer`, based on the admitted + synchronized Peer branch. + +No commit, push, PR rewrite, Release publication, preview deployment or merge is +authorized by this source batch. + +## Core Result So Far + +- Added canonical deployment-wide `extensions.state` plus database-authority + config/state mutation and non-empty-state version gates. Core runtime alone + receives column-level state-update authority; generic management responses + exclude Extension-produced state. +- Ported Twitter 0.2.0 OAuth App/account/PKCE transaction, standalone callback, + Bookmark Source, Cron and initial Job setup onto current Peer/Source/Cron/Job + authorities. +- Published `inkcre.twitter.setup.v1` only while Twitter is running and retained + `core.extension.management.v1` for exact selected-Peer control. +- Added operation-time Registry resolution: + executing Core Peer override, deployment config, then process/product fallback. +- Browser registration uses an ordinary `peers` upsert from the Web runtime. + Its payload contains only runtime-owned `id`、`name`、`config_schema` and + `capabilities`, so owner-authored `config` and `labels` remain untouched. +- Advanced the Core Host SDK to 0.1.1 and the Twitter wheel to 0.2.0. +- Redacted OAuth query material from request diagnostics, reconciled a direct + deployment config change before Twitter setup becomes reachable, and disabled + the setup-owned Cron whenever the OAuth App/account is replaced or + disconnected. The Source and Cron records remain reusable and uninstall still + does not inspect Source-domain records. + +Latest pinned full evidence: + +```text +uv tool run --from pdm==2.27.0 pdm run check + foundation/lock/migrations/format/lint/type checks: passed + pyrefly: 0 diagnostics + pytest: 488 passed, 41 skipped +``` + +## Client Result So Far + +- Meta config now generates and persists one browser-origin Peer UUID and + migrates a legacy Client UUID if present; `INKCRE_CLIENT_ID` is no longer a + required environment/config input. +- Saving Settings validates JWT/database connectivity, registers the Web Peer, + starts its lease runtime, saves owner config and replaces the local bootstrap + only after the whole candidate connection succeeds. Reconnect/reset stops the + prior runtime, and the Settings recovery route does not cold-start Extensions + before the Peer override is available. +- The Web Peer runtime performs the runtime-field-only upsert, renews a + database-time lease and stops renewal on shutdown. +- Registry origin resolution is current Peer override, deployment config, then + `https://registry.inkcre.dev`, with one origin snapshot per exact Release read. +- The Extensions page restores the product-facing Client selector over Peer + rows. Current browser uses the local Web Host, an advertised remote Host uses + exact capability delegation, and every other Client uses the atomic durable + desired-state RPC. +- Web Extension API now has one optional setup component contribution. The Host + exposes it only while the current Web runtime is active; setup availability is + independent of the selected Client. +- Twitter Web 0.2.0 owns the four-step wizard and its always-available Close + action. It discovers live Core Peers and sends every management/setup command + through exact Peer capabilities; no Client REST origin probing or postMessage + dependency remains. +- Generic Extension list/get/mutation responses now request an explicit + management projection and never fetch `extensions.state` into the browser. + Registry/provider URLs reject non-HTTP(S) schemes and embedded authority + credentials. + +Latest full evidence: + +```text +pnpm check + format/lint/database/runtime/workspace contracts: passed + type checks: all 6 workspaces passed + Vitest: 36 files, 136 tests passed + builds and browser package contract: passed +actionlint 1.7.12: passed +git diff --check: passed +``` + +The added coverage includes Web Peer registration/lease, bootstrap transaction, +Registry authority, exact selected-Client control modes, setup contribution +lifecycle, Twitter capability transport, selector projection and the explicit +Core-enable wizard transition. It deliberately does not add the rejected +Twitter Close-button regression test. + +## Remaining Cross-Repository Gate + +1. The Client generated database files still describe the prior exact Core + image. Local Docker is unavailable and the configured SSH Docker provider is + unavailable. The Core feature migration head is now `c6d7e8f9a0b1`; generated + files remain deliberately untouched until an exact image exists. +2. After separate commit/push authority produces the exact Core feature image, + sync the Client database/runtime evidence from that image and rerun + `pnpm check` before Client merge. There is no temporary `register_peer` type seam. + +All temporary PostgreSQL clusters and failed type-generation output created by +the rehearsal were stopped and moved to Trash. No unrelated local database was +touched. + +Black-box provider acceptance remains Sir-owned and deferred. The source batch +ends at PR-ready-for-review evidence, not merge or publication. diff --git a/tasks/extension-setup-wizard/74-setup-ux-and-protocol-review.md b/tasks/extension-setup-wizard/74-setup-ux-and-protocol-review.md new file mode 100644 index 0000000..08c8525 --- /dev/null +++ b/tasks/extension-setup-wizard/74-setup-ux-and-protocol-review.md @@ -0,0 +1,201 @@ +# Setup UX and Protocol Review + +## Why This Review Exists + +Black-box acceptance of Twitter `0.2.0` reached the Bookmark Source step and +exposed a connected group of defects rather than isolated presentation polish: + +- the step says **Choose a Bookmark Source**, but when no Source exists it + silently replaces selection with a nickname text field; +- the schedule editor exposes unexplained `Daily hour` and `Minute` numbers; +- the wizard uses raw `select`/`input` elements and hard-coded colours instead + of the application's UI package and theme tokens; +- button loading indicators do not work because `InkButton` owns `isLoading`, + while the implementation passes an unknown `loading` prop; +- setup Source/Cron/Job work is routed through the Twitter Core Distribution + even though these are ordinary shared-database domains already available to + the Web Peer; +- the accepted semantic HTTP design was replaced during Peer reconstruction by + one `POST /twitter/setup` endpoint with a seven-way `action` discriminator. + +The final point is implementation drift. HLD 3 specified semantic routes. The +Peer-native implementation compressed them into an action dispatcher without +recording or reviewing that change. + +## Corrected Authority Boundary + +### Twitter Core Distribution + +The Core Distribution keeps only work that requires Twitter's private durable +state, provider interaction, or callback ingress: + +- coherent OAuth App config replacement and account reset; +- begin and observe OAuth authorization transactions; +- callback code exchange, current-user lookup and token persistence; +- disconnect account; +- a Web-safe OAuth/account projection. + +It does not list, create or select Sources; create or edit Crons; create Jobs; +or store selected Source/Cron IDs in Extension state. + +### Twitter Web Distribution + +The Web Distribution owns setup orchestration and uses ordinary Web Peer APIs +for the ordinary deployment resources: + +- list and create `extensions.twitter.bookmark.Source` rows; +- list, create and update the selected Source's ordinary collection Cron; +- enable the Cron and create the initial ordinary collection Job on Finish; +- derive setup readiness from OAuth status plus current Source/Cron facts. + +This is direct use of existing InKCre authorities, not a second Source/Cron +implementation. Normal duplicate submission is prevented by pending UI state. +Rare duplicate Sources or Jobs remain acceptable for this single-user product. + +### Twitter Extension state + +State retains only Extension-produced OAuth/account facts. The rejected +`bookmark_source_id` and `bookmark_cron_id` copies are removed. Closing and +reopening the wizard rediscovers Source/Cron truth from their own relations. +When exactly one eligible Source exists it may be preselected; multiple Sources +require an explicit visible choice. + +The current `authorization_id` in bookmark Job parameters exists only to reject +a low-probability reconnect/old-job race. It forces generic Source/Cron creation +back through the private Core Extension state. The correction removes it from +the Web-facing collect config. At execution time the Source reads the current +Twitter account from Extension state. This deliberately trades that rare race +guard for a substantially simpler and more truthful domain boundary. + +## Peer-native OAuth API + +Peer HTTP v1 advertises one fixed method and URL per capability. Rather than +expanding that generic protocol with caller-selected paths, Twitter advertises +a small set of fixed semantic operations: + +| Capability | HTTP operation | Request body | +| --- | --- | --- | +| `inkcre.twitter.setup.status.v1` | `GET /twitter/setup` | none | +| `inkcre.twitter.oauth-app.configure.v1` | `PUT /twitter/setup/oauth-app` | OAuth App fields and reset confirmation | +| `inkcre.twitter.oauth.begin.v1` | `POST /twitter/setup/oauth-transactions` | none | +| `inkcre.twitter.oauth.transaction.read.v1` | `POST /twitter/setup/oauth-transaction` | `{transaction_id}` | +| `inkcre.twitter.oauth.disconnect.v1` | `DELETE /twitter/setup/account` | none | + +The fixed POST for transaction observation is the one bounded adaptation from +the earlier dynamic GET route. It avoids adding variable paths to the generic +Peer transport. Each request has one schema and one meaning; there is no +`action` field or internal dispatcher. Discovery of the status capability is +the setup-availability signal. The remaining capabilities are advertised by the +same running Twitter publication and invoked only by the Twitter Web target. + +Cross-target coupling does not disappear: the two Twitter Distributions still +share an Extension-owned protocol. The correction removes accidental coupling +to Source/Cron/Job implementation and prevents one endpoint from becoming an +unversioned command bus. + +## Corrected Bookmark Source Experience + +Step 3 becomes **Set up bookmark collection** and has two explicit sections. + +### Source + +- If eligible Sources exist, show an `InkDropdown` labelled **Bookmark Source** + with an explicit **Create a new Source** choice/action. +- If none exist, show a clear empty state: **No Bookmark Sources yet**, explain + that setup can create one, and offer **Create Bookmark Source**. +- Creation reveals a separately titled **New Bookmark Source** form with a + short description and an `InkInput` for nickname. It is never presented as a + selection field. +- Selecting an existing Source does not rename it. + +### Collection schedule + +- Show the section only after a Source is selected or created. +- Use the established `InkPicker` time happy path rather than separate numeric + hour/minute inputs. +- Label it **Collect bookmarks daily at** and state that execution uses the Core + deployment timezone. If `core.cron` configuration is readable, show the exact + IANA timezone; otherwise say **Core deployment timezone**, not an invented + local-browser time. +- Reuse/update the selected Source's ordinary collection Cron when one exists; + otherwise create one disabled. Finish enables it and runs one initial Job. + +## Wizard-wide UX Correction + +- Use `InkButton :is-loading` and one explicit pending operation identifier; + disable conflicting navigation/actions while that operation runs. +- Show authorization polling as its own waiting state rather than an idle link + plus text. +- Keep Twitter's always-available Close action. Closing aborts browser work and + does not mutate durable setup facts. +- Add Back/Continue semantics for locally visited steps while still deriving + resumability from durable facts on mount. +- Replace raw form controls with `InkDropdown`, `InkInput`, `InkPicker`, and + normal `InkButton` happy paths. +- Keep the stepper inside Twitter, but use application theme tokens and quiet + progress styling instead of hard-coded blue/grey circles. Do not introduce a + generic Host wizard framework or a new UI dependency. +- Add concise titles and descriptions to OAuth App, account authorization, + Source, schedule and Finish sections. English Extension-owned copy remains + acceptable for this slice. + +## Version and Delivery Consequences + +Published Distribution bytes are immutable, so this correction is not a +replacement of `inkcre/twitter@0.2.0`: + +- Twitter Python Distribution becomes `0.2.1`; +- Twitter Module Federation Distribution becomes `0.2.1`; +- `@inkcre/core` Web package becomes `0.1.2` if the ordinary Cron update method + is added there; the MF association then requires `@inkcre/core >=0.1.2 <0.2.0`; +- Core Host SDK itself needs no new generic API or version for this correction. + +Publication, preview delivery, installed-version change and remote mutation +remain separately authorized actions. + +## Implementation Batches + +### Batch A — Core Twitter protocol contraction + +1. Remove Source/Cron/Job DTOs, setup state fields, action union and dispatcher + from the Twitter package. +2. Publish the five semantic fixed Peer HTTP inbounds and routes above. +3. Keep callback/config/account/transaction behavior and safe provider errors. +4. Remove `authorization_id` from bookmark collect config and resolve the + current account at operation start. +5. Advance the Python Distribution to `0.2.1` and update focused route, + capability, state and bookmark collection tests. + +No new generic Core business API, database migration, lock, transaction helper, +Source cleanup or Job deduplication is introduced. + +### Batch B — Web domain composition and UX + +1. Replace the action client with methods targeting the fixed semantic + capabilities. +2. Compose Sources, Crons and the initial Job through existing `@inkcre/core` + Web Peer models; add only the missing ordinary Cron update method. +3. Rework Step 3 into explicit select-versus-create and schedule sections. +4. Correct every loading binding and operation interlock. +5. Restyle the Twitter-owned progress/navigation and replace raw controls with + `@inkcre/ui-web` happy paths. +6. Advance Web package/Distribution versions and native MF metadata. + +### Batch C — Verification and review handoff + +- Core focused OAuth/capability/bookmark tests, six-wheel verification and full + pinned `pdm run check`; +- Web setup API/domain tests, behavioral Vue tests for empty/existing Source, + pending actions, resume and Finish, then full `pnpm check` and native MF + closure; +- no strict HTML/CSS snapshot validation and no new Close-button regression + test; +- update the task packet with exact evidence, then stop at PR ready for review + unless later remote actions are explicitly authorized. + +## Review Gate + +This document is a proposed correction produced from black-box evidence. It +does not authorize source edits. Before implementation, confirm that the +corrected authority split, five fixed capabilities, removal of +`authorization_id`, and `0.2.1` immutable Release are accepted together. diff --git a/tasks/extension-setup-wizard/packet.md b/tasks/extension-setup-wizard/packet.md new file mode 100644 index 0000000..d0487dd --- /dev/null +++ b/tasks/extension-setup-wizard/packet.md @@ -0,0 +1,133 @@ +# Extension Setup Wizard + +- **Objective**: Design a coherent post-install setup experience for InKCre + Extensions, using `inkcre/twitter` as the first vertical slice: guide a user + from an installed Release to a verifiably usable deployment-wide + whole-Extension setup through `client-web`, without collapsing Registry, + deployment, Peer runtime, Extension configuration, durable Extension state, + Source configuration/state, and runtime memory into one concept. +- **Guardrails**: This packet is a cross-repository control surface, not durable + product authority; the Extension Registry continues to own Releases and + Distributions only; do not change source code or project state before an + implementation-ready plan and Sir's explicit start; treat Sir's statements + and agent proposals as reviewable hypotheses; discuss one bounded product + question at a time; report the previous result before advancing; group small + adjacent questions; record decisions continuously; do not force different + Host SDKs into one setup or lifecycle model; do not claim setup completion + when required credentials or runtime state disappear on restart. +- **Verification**: Product design defines the setup entry point, completion + condition, ownership of every step and value, interruption/resume behavior, + multi-Peer behavior, and failure/recovery UX. HLD then provides exact + language-neutral cross-Peer contracts plus Web/Core-specific Host SDK shapes, + and an implementation plan maps repositories, files, migrations, tests, + delivery order, and review evidence before any source mutation starts. + The Twitter vertical slice must eventually prove install -> per-Peer enable -> + card entry -> popup wizard -> OAuth App configuration -> durable account OAuth + -> required initial resources -> Extension readiness -> restart/resume, with + no false "complete" state. +- **Current Truth**: Product design and the setup/OAuth/Extension-state boundary + remain accepted. Old-baseline Core PR + [#52](https://github.com/InKCre/core-py/pull/52) and Client PR + [#68](https://github.com/InKCre/client-web/pull/68) proved the four-step wizard + and reached a visible setup popup in preview, but they are not mergeable + upstream truth. Core main has since admitted the hard Client-to-Peer cutover, + deployment config, capability advertisement and new Source/Cron/Job runtime; + PR #52 lacks those surfaces. Client main still carries stale generated + Client-domain projection and PR #68 builds its selector/settings/setup + transport on `Client.list()` and `rest_api_url`. The local always-available + Twitter Close correction is valid. The local Client-aware Registry-origin + correction proved the one-operation snapshot requirement but its authority + source is obsolete. Both source PRs now require the Peer-native reconstruction + in D029 before further preview acceptance or merge review. +- **Latest Preview Findings**: Twitter's setup contribution emits the correct + close event but exposes its Close action only on the final step; the accepted + correction is an always-available Twitter-owned action, with no Host button + and no new regression test. Core #52 also freezes the Registry origin from + process settings. The first local correction read the old current Client, but + that implementation is now superseded by the admitted Peer baseline: + `executing Core Peer override > deployment config > process fallback`, with + one origin snapshot for Release and wheel consumption. +- **Latest Implementation Result**: Sir authorized the Peer-native source batch. + Core and Client now have uncommitted reconstruction worktrees; the accepted + setup/state/Peer behavior is implemented and both repositories' full local + checks are green. Generic Web management reads no longer fetch secret-bearing + Extension state, and reconfiguration/disconnect disarms setup-owned bookmark + collection without deleting Source-domain records. Exact Client + generated-contract sync still requires an exact Core feature image because + the pinned generator needs an unavailable container runtime; this remains the + declared dependent-PR admission gate. The latest Impact Handshake removed the + unnecessary `register_peer` RPC/D7 migration in favor of a runtime-field-only + Web upsert, and simplified Source/Cron/Job setup for the single-user happy path: + ordinary Source create, explicit Cron create/update, and non-deduplicating + run-now. See + [Peer-native implementation progress](73-peer-native-implementation-progress.md). +- **Latest UX and Protocol Review**: Black-box acceptance reached the Bookmark + Source step and exposed a connected design defect. The UI conflates choosing + and creating a Source, exposes unexplained hour/minute fields, uses raw + controls and hard-coded stepper styling, and passes the nonexistent + `InkButton.loading` prop instead of `isLoading`. More importantly, the + Peer-native implementation drifted from the accepted semantic routes to one + seven-way `action` dispatcher and delegated ordinary Source/Cron/Job work to + the Twitter Core Distribution. The proposed correction keeps Core Twitter + OAuth-only, composes ordinary Source/Cron/Job rows directly in the Twitter Web + target, removes copied Source/Cron IDs and the low-ROI `authorization_id` + binding from Extension state/job config, and publishes five fixed semantic + Peer capabilities. See + [Setup UX and protocol review](74-setup-ux-and-protocol-review.md). +- **UX and Protocol Correction Result**: Sir accepted the connected correction + and authorized implementation, commit and push. Core Twitter `0.2.1` now + publishes five fixed OAuth/account capabilities, keeps only OAuth/account + facts in Extension state, and no longer imports Source/Cron/Job authorities + from setup. Bookmark collection resolves the current account at execution + instead of carrying `authorization_id` in Job parameters. Web Twitter `0.2.1` + now composes ordinary Source/Cron/Job models directly; `@inkcre/core` `0.1.2` + adds the missing ordinary Cron update operation. Step 3 explicitly separates + existing Source selection from new Source creation, uses the UI package's + dropdown/time picker/button happy paths, provides operation-specific loading + states, and uses application design tokens. Core's pinned full check passed + with 489 tests/41 skips; Web's full check passed with 138 tests and all + workspace/MF builds. The source branches are ready for commit, push and + Preview delivery under the authorization already recorded in this turn. +- **Preview Controller Resolution**: Core PR #52 run `31774435213` exposed a + baseline mismatch, not a controller compatibility requirement. The old PR + branch lacks the admitted Peer implementation and delivery script while Core + main owns both. PR #62's legacy-image fallback is rejected. Reconstruct Core + #52 from current main, port setup to Peer-native contracts, then migrate + Client #68 to the exact generated Peer contract and capability transport. +- **Delivery Boundary**: Previous preview evidence remains useful but no longer + proves merge readiness. Black-box acceptance is paused until Peer-native Core + #52 and dependent Client #68 pass their exact contracts and previews. No + public Twitter Release, production delivery, PR merge or PR #62 closure is + implied. +- **Next Step**: Commit and push the three coherent repository batches, observe + Core and Client Preview delivery, publish the immutable Twitter `0.2.1` + native associations through the existing checked-main/Registry path as + required by Preview, then hand Sir the exact Web Preview entry for acceptance. + Do not merge either PR. + +## Supporting Material + +- [Working protocol](00-working-protocol.md) +- [Current-system evidence](10-current-system.md) +- [Product-design working model](20-product-design.md) +- [Twitter wizard proposal](21-twitter-wizard.md) +- [Setup authority and protocol proposal](22-setup-authority-and-protocol.md) +- [OAuth callback proposal](23-oauth-callback.md) +- [Extension state proposal](24-extension-state.md) +- [Extension state and setup lifecycle proposal](25-state-and-setup-lifecycle.md) +- [Setup and Host SDK boundary proposal](26-host-sdk-boundary.md) +- [Minimum vertical slice and black-box acceptance proposal](27-vertical-slice-and-acceptance.md) +- [HLD 1 — OAuth callback ingress](31-hld-callback-ingress.md) +- [HLD 2 — Canonical Extension state](32-hld-extension-state.md) +- [HLD 3 — Twitter setup and OAuth protocol](33-hld-twitter-protocol.md) +- [HLD 4 — Web Host contribution and wizard UI](34-hld-web-setup.md) +- [HLD 5 — Versions, repositories and delivery](35-hld-delivery.md) +- [Implementation plan](40-implementation-plan.md) +- [Implementation readiness review](50-readiness-review.md) +- [Implementation result](60-implementation-result.md) +- [Client selector and Client Settings follow-up](70-client-selector-and-settings-follow-up.md) +- [Preview acceptance follow-up](71-preview-acceptance-follow-up.md) +- [Peer-native PR reconstruction](72-peer-native-pr-reconstruction.md) +- [Peer-native implementation progress](73-peer-native-implementation-progress.md) +- [Setup UX and protocol review](74-setup-ux-and-protocol-review.md) +- [Decision and question log](30-decisions-and-questions.md)