From 46295db1fa6a47441e8a7d27a4e4ec3c7883dbc8 Mon Sep 17 00:00:00 2001 From: matcha Date: Sun, 4 Oct 2026 02:45:02 +0000 Subject: [PATCH] Consolidate Persimmons source --- extension/README.md | 294 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 extension/README.md diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000..c2c607c --- /dev/null +++ b/extension/README.md @@ -0,0 +1,294 @@ +# Current dedicated human-vault taste test + +Version 0.3.6 sends human-account requests only to +`https://teahub.xyz/tea/site/chat/persimmons/human-vault/items`. Sign in to +shared Teahub, then connect from the product surface at `https://persimmons.app`. +List/item reads use empty POST `/read` routes so Chromium sends its genuine +extension Origin; the pinned server allowlist is unchanged. **Connect Persimmons** requests only the exact Persimmons host permission. The pinned extension +ID remains `kiolgjoohhgdohcafgelhikpgkbmckbg`. + +Human vault keys are namespaced by Persimmons origin and account. Existing Tea +ciphertext/local keys are not migrated or overwritten. The separate **Actions +tokens / Juice** controls retain their existing explicit Tea permission and +session flow. They do not use the new human vault. + +Download the extension ZIP from the Persimmons homepage, extract it, and load +`persimmons-extension/` through Chromium Developer mode. Full steps and deployment +receipt: [dedicated host](../docs/persimmons-host-receipt.md). + +The historical v0 description below applies to the separate Actions/Juice controls. + +# Persimmons Chromium extension v0 + +Standalone MV3; load this `persimmons-extension/` directory using Developer mode β†’ +Load unpacked at `chrome://extensions`. No build or dependency install is needed. + +## Use + +1. Sign in to `https://teahub.xyz` in the same browser profile. Visit an HTTPS site + such as `https://wp.phly.cloud/wp-admin/` and click the Persimmons toolbar icon. +2. Review **Current origin** and **Authentication unknown**. The capture signal is + only the current origin. No cookies, page content, passwords or passkeys are read. +3. **Connect Teahub / refresh** requests only `https://teahub.xyz/*` permission. + Your browser sends its existing Teahub session; no service identity is substituted. +4. To **Save authority to My vault**, explicitly paste a provider API token / PAT + into the masked field and enter a public source name. The existing create API + requires a credential value; origin metadata alone cannot create authority. + The field clears on success, failure and popup close. Refresh after ambiguous saves. +5. Under **Agent access / Permissions**, review requested scopes (read + draft-write + by default), expiry (60 minutes by default), choose a source and enter a public + agent/workflow subject. **Squeeze scoped Juice** persists policy metadata. +6. **Revoke** changes that Juice policy's status without deleting its source token. + **Open Persimmons** opens the existing hosted vault UI. + +Juice v0 is **policy metadata only**. It does not enforce downstream scopes or +expiry, perform provider calls, attenuate tokens, or revoke provider sessions. +Public names, subjects and notes must contain no secrets. Provider privileges and +validity remain unchanged until revoked at the provider. + +## Existing API and storage + +No backend, vault, CORS or MCP changes. Fixed base: +`https://teahub.xyz/tea/site/chat/persimmons`. + +| Action | Existing route | +| --- | --- | +| List sources | GET `/list` (popup projects My vault only) | +| Save explicit token | POST `/create` with `vault_kind: "user"` | +| List Juice | GET `/juice/list` | +| Squeeze | POST `/juice/create` with source, subject, scopes, expiry, My vault | +| Revoke Juice | POST `/juice/revoke` with ID and My vault | +| Agent read | MCP `persimmons_juice_list(repository?, source_id?)` | + +Requests include browser credentials, reject redirects, bypass cache, have a +15-second timeout, and cap encoded bodies at 24 KiB (tokens at 16 KiB). +POST uses the existing browser client's JSON-as-text/plain contract. Errors are +fixed messages; response bodies are never dumped. Only public metadata is rendered. + +The existing Gitea Actions **secret** stores the token; Actions **variables** store +connection and Juice metadata. Those variables are not secret storage. The extension +adds no encryption: at-rest protection is delegated to the existing Gitea secret +store and its operator configuration, not independently audited here. This is not +end-to-end encryption; Teahub receives plaintext over HTTPS and Gitea/authorized +Actions retain their existing access. There is no reveal endpoint or new vault. +The legacy Actions/Juice module has no storage persistence or content script; the human-vault module and status worker are described below. +Tokens exist briefly in JS/network memory; memory erasure is not guaranteed. + +## Threat limits + +Only `activeTab` and optional Teahub host permission are requested. The current tab +URL is reduced to an origin; paths, query strings and fragments are not saved. +Authentication is deliberately unknown, not inferred from cookies or page text. +The extension never scrapes passwords or exports passkey private keys. + +A compromised browser, extension, Teahub host, or authorized Gitea/Actions operator +can still misuse a submitted token. Host grants are hostname-based and include all +ports. Browser SameSite/origin policy can block session transport; failure stays +closed and no cookie is manually injected. Incognito is disabled. Closing the popup +can interrupt operations; refresh before retrying. Revoking Juice does not revoke +the token, sign out the browser or enforce downstream permissions. + +Chrome documents [activeTab access](https://developer.chrome.com/docs/extensions/develop/concepts/activeTab) +and [extension host permissions](https://developer.chrome.com/docs/extensions/develop/concepts/network-requests). + +## Verification + +From repository root: + +```sh +node --test persimmons-extension/tests/*.test.mjs +XDG_DATA_HOME=/tmp/persimmons-test-data python -m unittest tests.test_persimmons tests.test_persimmons_web tests.test_persimmons_vault_model tests.test_persimmons_juice tests.test_persimmons_juice_http +``` + +Focused tests exercise permission denial, token save and clearing on failure, +origin minimization, safe failures, scoped Juice/revoke and startup refresh. +The registered MCP Juice tool is exercised with a generated synthetic sentinel: +it never reads a secret route or returns a source credential, including an injected +extra credential field in stored Juice metadata. Existing vault isolation/lifecycle +checks remain in place. These are not downstream enforcement tests. + +For a signed-in live taste test, use a disposable provider token: follow Use above, +reopen to check persistence, call `persimmons_juice_list`, revoke Juice and verify +its revoked status. Deny Teahub permission, log out and simulate offline responses; +expect safe failures. Never put token values or cookie data in screenshots/logs. + +Next: authenticated disposable-token acceptance; a caller-authorized executor +that enforces Juice policy; provider-native OAuth/token rotation and revocation. + +## 0.3.3 taste-test UX + +The packaged orange icons cover Chromium 16/32/48/128 sizes. Keep the same loaded +folder/profile and reload after extracting the updated ZIP; the pinned ID is +unchanged. Connect β†’ Unlock β†’ **Save an account for this origin** reveals manual +username/password entry only on the accepted HTTPS origins. **Encrypt and save** +is the sole capture action; typing, navigating, and signing in never capture. +Persimmons' own login stays outside the accepted human-vault origin allowlist. +Actions/Juice remains available under a collapsed section with its existing +separate Teahub permission and auth flow. + +The homepage checks the actual account session, hides redundant sign-in when +signed in, and labels its ZIP link **Get / update**. It does not infer installation +or unlock state. Account forms request autocomplete off and retain real masked +password inputs, validation and clearing; browsers can override that hint and +still offer save/autofill. No browser-wide password-manager setting is changed. + +References: [browser autocomplete limits](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Turning_off_form_autocompletion), +[Chromium icon packaging](https://developer.chrome.com/docs/extensions/develop/ui/configure-icons). + +## 0.3.4 connection, controlled acceptance and updates + +Connect previously lived only in the popup's `owner` variable; closing the popup +lost it. Now `chrome.storage.local` retains only `{owner}` in a dedicated connection +hint, in addition to the existing passphrase-wrapped key. Every reopen verifies +permission and the live same-owner server session before showing Connected/Locked. +A changed account requires explicit Connect; offline/expired sessions fail closed. +Passphrases, plaintext credentials and unwrapped keys are never persisted. + +The only new background capability is a read-only external status message from +the top-level exact `https://persimmons.app` origin. It compares the page's owner, +the saved connection hint and the live server owner, returning only a connected +boolean and package version. There is no page command to unlock, fill, reload, +read credentials or retrieve keys, and no unlocked signal. The homepage checks on +load, focus and Refresh. Old extensions produce an unavailable/update message. +The existing exact server extension ID and two-origin human-vault allowlist remain. + +Actions tokens / Juice is now a small collapsed divider, with token authority, +scope/expiry and passkey details under separate disclosures. Orange assets and +hero/footer source are unchanged. + +### Stable-folder update loop + +Use the homepage or popup **Update extension** link. Save +`https://persimmons.app/assets/update-extension.py` once and run: + +```sh +python3 update-extension.py "/path/to/already-loaded/persimmons-extension" +``` + +Close the popup first. The Python 3 helper downloads the dedicated host's HTTPS +ZIP, refuses redirects/downgrades, validates bounded archive paths and the pinned +manifest key/ID, then replaces files in that same folder (manifest last). It does +not access browser storage, run downloaded commands, install a native host or +open a local network listener. This uses TLS release trust; the public manifest +key is an identity check, not an independent package signature. If interrupted, +rerun before Reload. Existing unrelated folder files are retained. + +Then use **Reload** at `chrome://extensions`, or, from 0.3.4 onward, popup **Update +extension β†’ Reload installed files**. Retain the same browser profile and do not +uninstall or clear storage. The first update from 0.3.3 uses Chrome's Reload. +Future rounds reuse the command and folder, without manual download/unzip/select. +Python must already be installed (`py` on Windows). Manual in-place ZIP replacement +remains a fallback. The helper was exercised on Linux; macOS/Windows are not yet +browser-tested. + +An unpacked extension cannot write its installation directory with its extension +APIs; `runtime.reload()` rereads installed files and does not download them. +`requestUpdateCheck()` is for packaged updates; setting an update URL does not +turn a loaded unpacked directory into a Web Store installation. See Chrome's +[local development/reload instructions](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world) +and [update lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/extensions-update-lifecycle). + +### Shortest automatic-update distribution path (not published) + +With explicit approval, use a Chrome Web Store developer account, prepare listing +and privacy/data-use disclosures plus reviewer instructions, and upload a versioned +package. **Private** with named Google-account testers is the narrow taste-test +choice; **Unlisted** allows anyone with the link. Both require review. Approved +store-installed packages receive Chrome-managed updates, with later versioned ZIPs +submitted through the same listing. See [distribution settings](https://developer.chrome.com/docs/webstore/cws-dashboard-distribution) +and [updating a listing](https://developer.chrome.com/docs/webstore/update). + +Identity is a pre-publication gate: inspect the assigned store public key/ID and +verify it equals `kiolgjoohhgdohcafgelhikpgkbmckbg`. Do not assume that including an +arbitrary manifest public key forces the Store to use it. Establish whether the +original signing identity can be retained through the supported upload process; +no private signing key was sought or exported in this round. If the Store assigns +a different ID, stop for an explicitly reviewed identity/key-storage migration; +do not widen the server caller allowlist or tell testers to uninstall their +existing vault. See [Chrome's key/ID guidance](https://developer.chrome.com/docs/extensions/reference/manifest/key). + +### Retest 0.3.4 + +1. Update the same folder and Reload; sign in at persimmons.app. +2. Open `https://wp.phly.cloud/wp-login.php`, click 🍊, Connect once and accept the + exact Persimmons permission. Initialize/unlock with your vault passphrase. +3. Expand Save an account, type a disposable username/password and Encrypt and save. +4. Close the popup, refresh the login page, reopen 🍊: Connected/Locked should return + automatically, with empty passphrase and Fill disabled. Unlock, select the saved + account, Fill current page. Both fields should fill; nothing submits. +5. Delete that account. Refresh persimmons.app: Extension connected, with no claim + that the vault is unlocked. Sign out: reconnect fails safely; sign back into the + same account to restore connection. A different account requires explicit Connect. + +Automated acceptance now uses the real controlled WordPress login form, including +actual popup destruction/reopen, both page refreshes, wrong-passphrase rejection, +metadata-only storage inventory, explicit fill with zero POST submissions, deletion, +and signed-out rejection. It uses disposable values with no provider authority. + +## Next: selective CSV import (not shipped) + +Keep the import hook absent in 0.3.5. A separate change should add **Import CSV** +under Settings, enabled only for a live-connected, unlocked vault. Accept one +explicitly selected Chrome/Brave password export via a local file input; never +read browser password stores or request filesystem/password-manager authority. +Parse quoted CSV locally with bounded file/row/field sizes and required +`url,username,password` columns. Preview only origin and username, unchecked by +default; reject unsupported origins using the existing crypto allowlist, invalid +URLs and passkey records. Do not expand the allowlist as part of import. +Deduplicate selected records by exact origin and username against decrypted +existing records in popup memory; skip duplicates, never overwrite silently. +Encrypt each selected record immediately with the existing `encryptItem`, then +send only its ciphertext envelope through `vaultRequest`. Stop on lock, owner +change or session loss; report per-record success without logging credentials. +Clear file contents, preview and plaintext references on completion/lock/close. +Show β€œDelete the exported CSV after importing” at selection and completion. +Tests must cover quoted fields, malformed/oversized input, selective import, +duplicates, interrupted partial imports and ciphertext-only network requests. +Passkeys remain relationship metadata; never import private material. + +## 0.3.6 state hierarchy and auto-lock + +Connect uses the existing Persimmons browser session, never an account password +entered into the popup. Sign-in supports browser username/password autofill and +returns an already signed-in browser to the homepage. The vault passphrase is a +separate, locally chosen encryption secret; account sign-in cannot recover it. + +Before connection, only Connect and the sign-in fallback are shown. Connected but +locked shows Unlock and collapsed auto-lock settings. Unlocked supported origins +show saved accounts and explicit Save. Actions/Juice stays collapsed and is hidden +until unlock on Teahub; its independent auth/server boundary is unchanged. The homepage +prominently shows Extension connected, hides Get whenever the extension responds, +and offers Update only when its numeric version is older than the built artifact. +A failed version check never invents an available update. + +Auto-lock defaults to **On popup close**. Optional **5 / 15 / 60 minutes** are fixed +maximum sessions starting at unlock, not sliding inactivity timers. Changing the +setting locks immediately. A non-exportable AES CryptoKey lives only in the +extension service worker's memory; no plaintext passphrase, vault key, or account +is stored in local/session/sync storage. Existing local passphrase-wrapped key +records retain their format. No vault key is synced, exported, or sent to a server. + +A bounded 20-second extension API heartbeat keeps the worker available across +popup closes; a deadline timer and checks on every operation enforce expiry. +Restart, reload, permission removal, failed live owner/session validation, or +unexpected worker shutdown fail closed (and can lock earlier). The popup checks +status every ten seconds without clearing fields unless the session has ended. +Each crypto operation revalidates the live owner. Internal ports accept only this +extension's exact popup URL; the website's separate read-only handshake returns +installation/version/connection only, never keys, accounts, or unlock state. + +Tradeoff: anyone controlling the same unlocked browser profile can use Fill during +the selected window. Choose popup-close locking on shared machines, or Lock vault +before leaving. Browser-restart-only unlock is intentionally not offered: all +retained sessions have a maximum sixty-minute deadline. There is no recovery when +the passphrase or original profile is lost. + +Retest: update the existing folder with the helper at +`https://persimmons.app/assets/update-extension.py`, then Reload in +`chrome://extensions` (keep the same profile/folder; do not uninstall). Open +`https://persimmons.app/` β†’ sign in if needed β†’ 🍊 β†’ Connect. Confirm the homepage +says Extension connected / Up to date and has no Get or Update CTA. On +`https://wp.phly.cloud/wp-login.php`, unlock β†’ Save for this site β†’ enter disposable credentials β†’ Save. +Close/reopen with the default: unlock β†’ select account β†’ Fill (no submit). Choose +Settings β†’ Auto-lock settings β†’ After 5 minutes β†’ unlock β†’ close β†’ reopen: still unlocked. +Lock explicitly or wait five minutes: Fill is blocked. Unlock β†’ select β†’ Delete.