diff --git a/docs/persimmons-juice-v0.md b/docs/persimmons-juice-v0.md new file mode 100644 index 0000000..9cb4f84 --- /dev/null +++ b/docs/persimmons-juice-v0.md @@ -0,0 +1,130 @@ +# Persimmons Juice v0 + +Juice derives a Gitea PAT in the unlocked extension from an encrypted Master +credential and writes only that derived PAT into a separate named repository +vault. The product page sends source ID, target ID/name and selected scopes. +Source passwords never pass through the product page, Juice backend or MCP. +The extension's existing in-memory key and capture/fill crypto are unchanged. + +Supported source origins are exactly `https://teahub.xyz` and +`https://git.persimmons.app`. Other origins display “Juice adapter not available +yet.” Extension host permissions and connect-src enumerate those providers; +externally_connectable remains only the Persimmons top frame. The service worker +checks the live owner connection, initialization and unlock before decrypting. +Provider calls use Basic auth, omit cookies, refuse redirects and have timeouts. +The product receives no PAT response. No new MCP surface exists. + +Choices are `read:repository`, `read:issue`, and `write:issue` (which includes +issue reads). These are **provider API-class-wide scopes**, bounded by the source +account's authority, not permissions limited to the target repo or a single +source repository. No expiry is promised. `all`, admin, user, organization, +package and repository-write scopes are not accepted. The modal and every Juice +card state this scope boundary. + +Backend operations are POST `/api/juice/{list,prepare,write,remove,cleanup}`. +Only the exact pinned extension Origin can prepare, submit a PAT or confirm +provider cleanup; the product page can list safe metadata or remove a target +secret. All operations require a live cookie session, matching X-Vault-Owner, +and a locally recorded named vault whose immutable repo ID, live owner and +privacy are checked against dedicated Gitea. There is no bearer fallback, +caller-selected provider host, secret-read operation or raw source input. + +Actions secrets are named `PERSIM_JUICE_`. Safe operation metadata +uses the same name in Actions variables, which Gitea normalizes to uppercase. +The backend projects only its strict metadata schema. PATs enter only the +write-only secret PUT body, never metadata, sessions, logs, receipts or returns. +The dedicated runtime token adds `write:repository` via the existing independent +Persimmons launch/bootstrap workflow. Its versioned root-only token file keeps +bootstrap resumable; the previous bootstrap token is retained for rollback. +Actions workflow execution remains disabled. + +An operation reserves metadata before creating `persimmons-juice-` on the +source provider. This deterministic name permits cleanup even if the create +response is lost. Write failure triggers best-effort source revocation and +target removal. Unconfirmed cleanup returns ANDON and leaves a visible ledger +entry. Worker loss leaves pending metadata for explicit Revoke; it does not +persist a plaintext PAT or automatically retry token creation. + +Rotate first writes a new operation/secret, then removes and revokes the old +operation. Interrupted rotation can leave two provider tokens, represented by +separate cards; the result explicitly directs Revoke on the old card. Revoke +always attempts target removal first. Without an available unlocked source, +the target is absent but provider revocation remains unknown. Revoke can be +retried after reconnecting/unlocking; revocation tombstones remain visible. +Metadata describing a stored token records the last confirmed operation, not +continuous provider health monitoring. + +Inspected before implementation: live Teahub Gitea **1.27.3** OpenAPI, dedicated +Gitea **1.27.3** authenticated API, and pinned source: +[API router](https://github.com/go-gitea/gitea/blob/v1.27.3/routers/api/v1/api.go), +[token create/delete](https://github.com/go-gitea/gitea/blob/v1.27.3/routers/api/v1/user/app.go), +and [Gitea API scope documentation](https://docs.gitea.com/development/api-usage/). + +Validation commands: + +``` +python persimmons/build.py +python -m unittest tests.test_persimmons_host tests.test_persimmons_gitea tests.test_persimmons_ux tests.test_juice_target +node --test persimmons-extension/tests/*.test.mjs +node tests/juice-provider-integration.mjs +node tests/persimmons-host-browser.cjs +``` + +The provider probe uses a disposable generated non-admin account, checks scoped +read/write enforcement, confirms Actions secrets cannot be read, and revokes its +PAT. Credentials stay in process memory and never enter output or files. The +browser harness retains capture/fill/lock/signout acceptance and exercises Gitea +capture, drag, target secret/metadata, rotation and revoke in a fresh profile. +Deployment is restricted to `persimmons-launch.yml` after local checks pass. + +## Accepted live receipt — 2026-10-01 + +Implemented in `dfa10102d94f1c962bebc042c80a7de577430897`; final deployed runtime +is **`a2ea1c770db8300c0da75db5894e6220ead8ccb4`**. Both releases used only the +independent Persimmons launch workflow. [Final launch run 1022](https://teahub.xyz/matcha/teahub/actions/runs/1022), +job 1024, completed successfully, including contract tests, provider authority, +deployment and HTTP acceptance. Before both publishes/deploys, explicit fetches +proved behind=0; dispatch receipts had HEAD=origin/main, ahead=0 and dirty=false. +No delegation occurred. + +Local checks passed: **42 host/Gitea/UX/Juice Python tests**, **8 encrypted-vault +and legacy Juice/MCP regression tests**, and **24 extension tests across nine +files**. The launch workflow repeated the host/Gitea/UX/Juice and extension suites. +The disposable provider integration confirmed Gitea 1.27.3, read:repository +access, denied repository writes, write-only secrets, uppercase metadata names, +and failed authentication after revocation. Its accepted provider fixture was +`juice-probe-93b0fee3e1977e07`, repo ID 10; provider PAT and target test secret were +removed. Disposable test users/private repositories remain as test evidence. + +Final browser acceptance used Chromium **153.0.8010.12**, extension **0.5.0**, +and a fresh profile with **no UI asset interception**. User +`browser-17b3e5a0b314d214` (non-admin ID 20) created private repo `work-test`, +immutable repo ID **19**. The actual Gitea login form was captured into Master, +then a native mouse drag emitted dragstart/drop/dragend and opened the modal. +The target stayed open after success. Source credential plaintext was neither +sent to the product page nor exposed in metadata. + +The extension worker independently verified two newly issued PATs could read +the intended private repository and both returned 401 after their respective +revocations. Only boolean counters left that verification closure. The real +backend accepted two extension-only PAT writes. Target Actions secret listing +exposed names only; secret GET was unavailable. Rotate removed/revoked the old +operation and installed a working replacement. Revoke left **zero provider PATs +and zero target secrets**. Safe revocation metadata remains in Actions variables. +The accepted run removed its Master ciphertext, logged out, and removed its +browser profile. The preserved WordPress synthetic capture/fill test passed with +zero form submissions; lock/unlock and logout connection clearing also passed. +The harness pregrants the exact optional Persimmons host permission and does not +claim to automate Chromium's native permission prompt. Timed auto-lock was +selected through the existing extension setting to keep the vault unlocked +while interacting with the page. + +Live `/healthz` returned the exact final runtime source above. Live artifact +bytes matched the local deterministic build: + +- app.js SHA-256: `4fc7ab21660660f0d6a3e958c7c54c65bd6d941d12f753a035c7d89d2a074960` +- extension.zip SHA-256: `fefa7eeda05543b5216316e8792d32c7358a0f57284ed231490219ef8dd8eb2d` + +This receipt-only follow-up does not change deployed runtime bytes. Provider +scope limitations, lack of automatic expiry, and explicit partial-failure +handling remain as documented above.