7.0 KiB
Persimmons v0 productization task
Accepted baseline
Persimmons already supports create, list, rotate, revoke, and delete. Stored credential values remain write-only after save; there is no reveal endpoint. The browser uses the existing signed-in Teahub session. Agent-safe listing exposes metadata and availability only.
Keep the existing Gitea Actions secret/variable substrate. Do not create a new vault service.
Product model
Persimmons is a home for useful P's for humans + agents. Credentials are the first object type: passwords, PATs, API keys, and later OAuth connections. Leave room for people/contacts later, but do not build contacts now.
A Persimmon is a reusable object/connection with an explicit storage/authority context.
Vault model
Use Gitea's existing hierarchy:
- My vault = authenticated user's Actions scope.
- Repository vault = one explicitly selected accessible repository's Actions scope.
Backend storage must match the displayed vault. Never silently broaden repository authority.
Live Gitea 1.27.3 OpenAPI has user-level Actions variables and write/delete user-level Actions secrets, plus repository-level variables/secrets.
Existing legacy Persimmons records in matcha/persimmons remain repository-vault records.
Public connection metadata must include vault kind and repository when applicable.
Browser/API behavior
Add a bounded authenticated repository-discovery route for the creation UI.
Browser Persimmons routes must execute with the signed-in user's Gitea authority, not a broader service identity.
The list route may accept an explicit repository. Without one, return My vault plus the legacy matcha/persimmons vault.
Create accepts an explicit vault choice. Rotate/revoke/delete require the same vault context and must reject a mismatch.
Keep the agent-facing Persimmons list read-only and value-free; allow an optional repository context if useful.
First-run UX
Fresh users see the product before the schema:
Persimmons 🍊 permissions, without the ceremony.
Store useful things once so you and your agents can reuse them without repeatedly passing sensitive values around.
[ + Add ]
Your vault No Persimmons yet.
Creation progressively asks:
- What are you connecting? Gitea / Cloudflare / Fly / Other.
- Where should it be available? My vault / A repository.
- Only minimum provider-specific fields.
Gitea: username + PAT. Cloudflare: useful label/account + API token. Fly: useful label/account + token. Other: label/account + credential type + value.
Scopes are semantic controls/chips, not raw implementation strings by default. If provider introspection is not implemented, do not imply scopes were detected.
Expiry and notes live under Advanced.
After creation, return to the vault view.
Identity rule
No personal operator name or username in UI defaults, examples, fixtures, docs, screenshots, or generated copy. Use blank or provider-neutral placeholders.
Security boundary
Human/browser may provide a new value -> protected Gitea store receives it -> Persimmons exposes metadata/capabilities/availability -> stored value is never revealed back to humans or agents.
No reveal/copy-after-storage endpoint.
Acceptance
- My vault lifecycle green.
- Repository vault lifecycle green.
- Repository scope cannot be silently changed.
- Legacy repo records remain listable.
- List output never contains written credential values.
- Fresh-user landing explains Persimmons before fields.
- Personal username default removed.
- Vault location is obvious before save.
- Provider-specific progressive form.
- Advanced metadata is secondary.
- Live MCP health, broker auth/CORS, repo discovery, and public 🍊 page pass.
- Existing lifecycle remains green.
- UI source in matcha/persimmons and hosted tea/web/🍊 snapshot stay identical.
- README documents vault model, useful P's / people-later direction, and future contextual-use seam.
Out of scope
No contacts implementation, generalized authorization engine, OAuth framework, reveal service, or new vault backend.
Multi-user isolation
Persimmons authority is caller-scoped at three separate boundaries.
- Identity isolation — the browser caller is the Gitea user resolved from that browser's signed-in Gitea web session. MCP callers are resolved from their own Gitea OAuth token. Tea does not substitute the service Gitea token for Persimmons user operations.
- Resource isolation — repository discovery uses the caller's Gitea web session, then re-probes each candidate repository's Actions settings. Every explicit repository list/create/rotate/revoke/delete path validates the selected repository again with the same caller authority. Guessing a repository name does not bypass this check.
- Execution/authority isolation — browser mutations post back through the same caller cookie + Gitea CSRF token; agent-safe reads use the caller OAuth bearer. Effective authority is
backend capability ∩ caller authorization, never Tea service authority alone.
The runtime admission gate is currently ALLOWED_GITEA_USERS=matcha,chai. This is a manual onboarding/configuration assumption that narrows who may use Tea; it does not broaden any admitted caller's Gitea authority. General multi-user admission policy is future platform work.
A Gitea administrator can legitimately see repositories owned by other users because that is part of the administrator's Gitea authority. Persimmons must preserve that authority rather than hide such repositories merely because their owner differs from the caller.
Isolation tests must include:
- two callers with different writable-repository sets;
- collaborator visibility where authorized;
- guessed-repository read denial;
- guessed-repository mutation denial before any write occurs;
- per-session My-vault separation;
- safe legacy secret projection without secret values.
Juice policy metadata v0
Squeeze (POST /tea/site/chat/persimmons/juice/create) accepts an existing
source_id, a nonempty subject (up to 200 characters), scopes (1–40 strings,
up to 100 characters each), and optional future expires_at with timezone.
It defaults to the caller's My vault; supply repository: "team/project" for
an explicit repository vault. The source must have active connection metadata
in that same vault and be available. Existing secrets without connection
metadata cannot be squeezed in v0. Credential values are never read.
Juice is stored separately in PERSIM_JUICE_<id> Actions variables, with no
secret object. GET /tea/site/chat/persimmons/juice/list accepts optional
repository and source_id; POST /tea/site/chat/persimmons/juice/revoke
accepts id and the same vault selection. Revocation remains visible in lists
and leaves the source credential intact. Agents have only the read-only
persimmons_juice_list(repository?, source_id?) MCP tool.
Policy metadata is persisted and revocable, but v0 does not yet mediate downstream service execution or enforce scopes/expiry on those services. A future executor must consume Juice plus its source internally, check policy and source availability, and never reveal the source credential to an agent.