Commit Graph
9 Commits
Author SHA1 Message Date
officer 8125363e0b a real form, and the URL step no longer demands a token
Two things reported from a production instance.

The URL step was refused with "an access token is required the first time" — a
check that predates splitting the screen into two steps, and which made the
first step impossible to complete. Naming the instance is now its own step; a
member still needs a token, since setting one is all they do.

And the fields were a div, so pressing Enter did nothing and the button was the
only way to submit. That is a keyboard trap wearing a layout. Now a <form> with
onSubmit, with the other two buttons marked type="button" so they do not submit
it.

19 panel files across the platform have the same problem. Recorded in the
platform's TODO.md rather than swept blindly — a search box has no submit, and
a multi-field editor wants the useForm hook.

Also dropped the hint under the URL field, which repeated the sentence directly
above it.
2026-08-19 10:32:59 +00:00
officer d4efc52447 ask for the URL first, and offer what tea already knows
The form asked for a URL and a token together, on a fresh instance, while the
machine in front of it often had both on disk already — somebody had run `tea
login` by hand.

Two changes.

One thing at a time: until the platform knows where Gitea is, a token cannot be
checked, because there is nothing to check it against. So the owner names the
instance first, and only then does the page ask for — or discover — a token.
That step now probes /api/v1/version unauthenticated, since demanding a token
to validate a URL refuses a good URL for the wrong reason and sends whoever
typed it looking for a credential problem.

And it reads tea's own config when the platform has no instance yet, offering
the URL it finds. A machine that already has a working login should not be
asked to retype what it can see.

Members skip the first step entirely — the instance is the owner's.
2026-08-19 10:25:22 +00:00
officer bf27793b44 put the settings in the panel header
They were floating buttons over the top-right of the iframe, which was wrong
twice: they sit on top of Gitea's own controls, and an overlay on an
application that fills its whole panel is invisible until you already know it
is there. Reported as "I have no access to settings when the iframe is loaded",
which is exactly right.

The header is the platform's own furniture — always present, never competing
with the content underneath — and it is where photos and jellyfin put theirs.
It also names where you are, so /gitea/connection reads as "Gitea —
connection" rather than looking like the frame failed to load.
2026-08-19 10:11:47 +00:00
officer 921427c7a2 the token lives in tea's config, once, per user
It was stored encrypted in service_connections as well. Nothing needs it there
any more: the UI is an iframe carrying the browser's own Gitea session, so the
only consumer left was the health probe. Meanwhile tea keeps its own copy
regardless — it is what the agent uses, and what this plugin's affordance tells
the agent to use.

Two copies of one secret is a drift waiting to happen: rotate with `tea login`
and the database quietly keeps the dead one. So the token lives once, where it
is used, and saving now writes `tea login add --name officer` AS that account —
a member's token lands in a home only they can read. `secret: null` on save
clears whatever an earlier version stored, so an upgraded machine stops
carrying a copy nothing reads.

service_connections keeps the URL, which is the owner's and genuinely shared.

The settings page READS the existing config first, so on a machine where the
owner and their members already ran `tea login` by hand it says "tea is
configured as X" rather than demanding something they have. It also says when
an account has no OS user yet, since there is then nowhere to write one — the
web view works regardless.

Test runs `tea whoami` as that account. Deliberately not a call the platform
makes with a token it read: the point is to prove what the AGENT will get, and
those are now the same thing. `--name officer` so the platform never overwrites
a login somebody set up by hand.

Noted rather than hidden: `tea login add` takes the token in argv, visible in
ps while it runs. tea offers no stdin form, and the alternative is writing its
YAML by hand — a visible argument traded for a fragile format.
2026-08-19 10:08:34 +00:00
officer c31f95155f make the connection settings reachable at /gitea/connection
The form appeared only when nothing was configured, so once a token was saved
there was no way back to it — a member who mistyped theirs, or anyone rotating
one, had nowhere to go. On a single-user machine that is an annoyance; on a
company one it is the ordinary case, because every member has a token of their
own to set.

Uses the route pair the platform already generates for every plugin, so this
needed no platform change. A plug icon beside the open-in-new-tab button, as a
real Link rather than a button, because it is navigation and should be
cmd-clickable.

Saving returns to the frame — otherwise you are left looking at the form you
just submitted, which reads as nothing having happened.

The two halves were already right: the panel shows only the token field when
isOwner is false, and getResolvedServiceCredentials gives a member the owner's
URL with their OWN secret, never the owner's.
2026-08-19 09:58:30 +00:00
officer 9d219e9c8c drop the sandbox attribute, which was doing nothing
Gitea needs allow-same-origin to keep a session and allow-scripts for the
interface to work, and those two together give back everything sandbox takes
away. The browser says so on every load: "an iframe which has both
allow-scripts and allow-same-origin can escape its sandboxing."

So it was decoration that emitted a warning nobody can act on, which is how
people learn to ignore warnings. What actually stands here is that this is
Gitea's own interface, on the platform's own origin, through a proxy the
platform wrote.
2026-08-19 09:20:40 +00:00
officer 80b9cd02e7 say when a stored token cannot be decrypted
The frame answered {"error":"internal error"} on every request. The cause was
AES-GCM failing with "Unsupported state or unable to authenticate data" — the
token in service_connections was encrypted under a key that no longer exists,
because the platform mints its encryption key on first use and does not back it
up. The same loss took slskd, memos and gitea credentials on this project's own
server earlier today.

It presented as a broken sidecar. connectionState reported `configured: true`
because a ROW exists, so the panel skipped the form and went to the frame,
where every call threw. Nothing offered a way to fix it, and the connection
form was unreachable precisely because it only appears when unconfigured.

Now a named failure. `UnreadableCredentials` is distinguished from every other
error — a database that is down keeps its own message, or a real fault would be
reported as "re-enter your token" — and the panel shows the form saying the key
changed, nothing else is lost, and the URL is still there.

Verified by the platform itself refusing to decrypt: this is the state that
machine has been in all day.
2026-08-19 09:00:03 +00:00
officer b1b9e2afc3 a way to actually connect it
Deleting the old UI took its connection panel with it, so the plugin installed,
opened, and had nowhere to type a URL. The frame would have loaded and answered
503 on every request inside it — which reads as Gitea being down rather than as
nobody having said where Gitea is.

The panel now asks for the connection FIRST and shows the form when there is
none.

Two halves, and which one you get depends on who you are: the URL is the
owner's, one instance per server, and everyone else supplies only a token and
inherits it. Not a UI choice — service_connections stores a member's row with a
null URL for exactly this reason, so "members never see the instance URL" is a
property of the schema rather than a filter somebody has to remember.

Defaults to http://localhost:3000 and says why in the field: the sidecar
fetches SERVER-side, so localhost means the machine Officer runs on. It needs
no TLS and never leaves the box, where the public URL takes the same traffic
out and back.
2026-08-19 08:36:05 +00:00
officer f680e25f82 gitea: the plugin, rendering Gitea's own interface
Eleventh plugin out, and the first that is not a move. The seventeen files that
reimplemented a fraction of Gitea's interface were deleted rather than carried
across — each one a promise to keep pace with a project that moves faster than
this plugin will.

That removed a platform change nobody now has to build. useGiteaLocation read
:owner/:name/:tab/:item from five route patterns and a plugin gets two, so
extracting as-is would have needed the plugin system to declare extra routes.
Nothing inside an iframe needs a route, so the requirement went with the screen.

No regression window: gitea's router had been commented out of hono.ts since
2026-08-13 with api: [], and officer-gitea was running nowhere. The screen
rendered and every request 404'd.

The proxy is what makes the iframe possible, not decoration. Officer and Gitea
are different registrable domains, so a frame pointed straight at the instance
gets a login page whose cookie the browser drops as third-party. Through
Officer's origin it is first-party and survives; X-Frame-Options and CSP are
stripped, safe only because the frame is the origin the browser already
authenticated.

/_web/* forwards the browser's own cookies and injects NOTHING. The sidecar
holds an API token and injecting it there would show every user the owner's
view of every repository. It is used only by /_api/*.

Two doors, because an iframe's sub-resources carry no Authorization header:
the ordinary routes go through the platform's gate, and /_web/* is declared
unauthenticated and authenticates itself with a minted, path-scoped cookie —
32 random bytes against the user who asked, not dev-proxy's constant =1 which
anyone knowing the cookie name can set. Verifying it also sets `user`, because
createSidecarProxy fills X-Officer-User from it and the sidecar refuses a
request without one.

Ships an affordance: git for what git does, tea for pull requests and issues.
Nothing copies it into capabilities/ yet — gitea is that mechanism's first
customer.
2026-08-19 07:58:55 +00:00