5afa2d832e5aa0702f179e30a60003a5ad0f8ffc
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5afa2d832e |
step 3/4: sidecar routing keys become handles — NOT YET VERIFIED LIVE
Committed before restarting, deliberately: this changes the registration wire, and the restart that proves it also bounces officer-claude-code, which is what the session doing the work runs on. An uncommitted 25-file protocol change is worse to inherit than one marked unverified. `capabilities: ['music']` → `handles: ['music']` on the registration message, across 20 sidecars, both plugins, the connector, the registry and the protocol type. findSidecarByCapability → findSidecarHandling, waitForCapability → waitForHandler. The name: a sidecar already has `handleCommand`, so the list is literally what it handles. `provider` was rejected — 390 existing uses and it already means websocket door. `serves` was rejected — collides with HTTP serving, which sidecars also do. THIS IS A BREAKING WIRE CHANGE with no compatibility shim. A platform expecting `handles` reads `undefined` from a sidecar sending `capabilities`, registers it with an empty list, and every sendCommand finds nobody — chat, terminal and music all fail with "No sidecar handling X is connected". So the whole estate has to restart together; there is no rolling upgrade. If it goes wrong: `git revert HEAD` and `pm2 restart all` again. The registry is in-memory and nothing about this touches the database, so a revert is complete. Found a fifth meaning of the word on the way, correctly named and untouched: the pty sidecar's terminfo capability queries (XTGETTCAP escape sequences). That is now five — permissions, the item store, routing keys, Lightning wallet features, and terminfo. tsgo clean, 797 tests, 787 pass, same 7. Not exercised against a live sidecar. |
||
|
|
e13128846b |
offscale is a plugin
headscale leaves the platform. 45 files move to plugins/offscale/ and the
platform stops knowing it exists.
api/router.ts the thin auth-gated proxy, now at /api/offscale
sidecar/ 18 files, the whole headscale contract and its admin keys
db/ schema + queries, offscale_servers
web/ 26 files as panels and a layout — no screen, per the rule
removed from the platform: the hono mount, the `headscale` capability, the
App.tsx route pair, the screen and its barrel, the AppRegistry spread, the
officerdev re-exports, the dock tile, the page-title rule, and both database
barrels. tsgo is clean and nothing references it.
the imports tell the story of what the plugin↔host API actually is. the sidecar
takes @@/sidecar/protocol, @@/sidecar/connect, @@/data-path and
@@/officer-url.mjs; the queries take officerdb/db and officerdb/crypto; the
schema takes officerdb/auth/schema for the one reference a plugin may make; the
web half takes useClient, copyToClipboard, WorkspaceView and TerminalView from
the officerdev barrel. all of it resolves because a plugin lives inside the repo
— no publishing, no version negotiation.
AND IT FOUND A REAL BUG IN THE INSTALLER. createSidecarProxy learns its port
from a one-shot `<name>:server` event and subscribes when the plugin's router is
first imported — at mount. install started the sidecar BEFORE mounting, so the
announcement fired into a void: process online, routes mounted, every request
answering `503 sidecar not available` until something forced a reconnect. it
would have hit every plugin with an http sidecar. `example` never caught it
because it has no listener to announce.
install and enable now mount before starting; disable still unmounts before
stopping. neither direction leaves a mounted route in front of a sidecar that
cannot be reached.
verified live: /api/offscale/_officer/servers answers {"servers":[]}, /offscale
and /offscale/nodes serve, the old /api/headscale is 404, the offscale
capability is registered from the manifest, and officer-offscale is online.
757 pass, same 10 pre-existing failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3c7f52ab77 |
PORT is read in one place, and it has no default
officer-url.mjs is now the only file in the tree that touches process.env.PORT.
Twenty-two others read it and supplied their own default; a value with
twenty-two sources is not configuration, it is twenty-two things to keep in sync,
and they had already drifted three ways.
It throws when PORT is unset rather than guessing. A default only covers the case
where .env was never loaded — which is not a machine anyone wants running,
because POSTGRES_URL is missing in the same breath. What the default bought was a
process that starts, binds somewhere unexpected, and fails later for a reason
that does not name the cause. Same posture as jwt.ts with JWT_SECRET.
It is .mjs, not .ts, and that is the whole reason this could be one file. pm2
launches officer-pty with node (ecosystem.config.cjs) and everything else with
bun; node cannot import TypeScript, so a .ts module would have left the pty
sidecar holding the only surviving copy of the default — precisely the thing
being removed. allowJs is already on, so the TS callers still get types. Verified
both runtimes import it, and that PUBLIC_URL-style overrides still work.
It also exports API_URL and OFFICER_API_URL, because nineteen sidecars were
independently building `ws://127.0.0.1:${PORT}` and two more were building the
http form. Those are one listener described in two protocols — no sidecar binds
anything — so they belong beside the port rather than being rediscovered per
file.
server.tsx now takes PORT as a number, so Number(PORT) at the serve site is gone.
Not typechecked (empty node_modules, frozen installs). Every edited file parses
under `bun build --no-bundle`; node and bun both load the new module; the unset
and non-numeric paths were exercised; the pm2 profile still loads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e72cae4830 |
one default port, and it is 9000
Every PORT fallback in the tree now says 9000. There were three answers to one question, each defensible where it was written and none of them visible from the others: server.tsx and 19 sidecars 5000 a default from before there was an installer user-instance.ts 9010 what scripts/setup-old/setup.sh really wrote .env.example 9000 what we told people to write 5000 goes first because macOS binds it — AirPlay Receiver has owned it since Monterey, so a dev server there fails to bind or gets shadowed by something that answers. All 22 sites moved together, which is the point. Changing the app alone would have turned a consistent-but-wrong default into a split one: the app on 9000 while nineteen sidecars still dialled 5000. 9010 was the interesting one. It was the only value that ever matched a real machine, because it is what the old installer wrote — and it was in the single file whose disagreement would have broken chat alone, with nothing else looking wrong. Its own comment records the same bug being fixed once already, within the file, by a change that left it disagreeing with everything outside it. Note what these defaults actually are: the sidecars bind nothing. user-instance.ts has no listener at all — it builds ws:// and http:// URLs that both address the app's single listener. So every one of these numbers is a guess at where the app is, for a value that .env always supplies. Worth removing rather than aligning, which is a separate change. Not typechecked (empty node_modules, frozen installs). Every edited file parses under `bun build --no-bundle`; the pm2 profile loads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3f071c0b24 |
the install root is derived, not configured
Seven variables out of .env. DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are gone from the code entirely; PUBLIC_URL, PUBLIC_BUILD_ENV, JWT_SECRET and VAULT_STORE_KEY are no longer written by the setup script. data-path.ts now derives OFFICER_ROOT as dirname(process.cwd()), with data/, capabilities/ and dockers/ as fixed names under it. The direction used to run the other way — DATA_PATH from env, then OFFICER_ROOT = dirname(DATA_PATH) in app-store/paths.ts — which meant three environment variables that had to agree with each other and with the tree on disk. Eight files re-read process.env.DATA_PATH independently, each with its own `?? cwd()/data` fallback. They import the one value now, which is what made removing it safe: otherwise each would have derived its own and drifted. Three things this turned up. The cwd pin in ecosystem.profile.cjs was broken. It set `cwd: __dirname` under a comment asserting "__dirname is the repo root — this file sits beside ecosystem.config.cjs", which stopped being true when these files moved into ecosystem-files/. It walks up to the platform's package.json now, which holds wherever the file lives. That was a live bug before this change and a load-bearing one after it, since cwd now decides where the install is. assertInstallLayout joins the other two boot assertions. A wrong cwd does not error — it computes a plausible root somewhere else and writes managed homes and agent runs into it, so the install looks empty and the data looks lost with nothing naming the cause. It throws before serve(), first of the three, because a wrong answer there makes the other two check the wrong files. getOwnerHomeDir captures homedir() once at module load rather than per call. Measured on bun 1.3.10: both os.homedir() and os.userInfo().homedir return $HOME when set rather than reading passwd, and user-instance.ts assigns process.env.HOME on its way to spawning an agent. A lazy read would have returned the owner's home on the first call and a member's afterwards. data-path.ts imports only node builtins, so it is evaluated before any of that runs. JWT_SECRET and VAULT_STORE_KEY leaving .env means an install made by this script does not boot — jwt.ts throws at module load without one. That is the agreed sequencing: they move to the SQLite store (docs/secret-store.md), and writing them here meanwhile would create a second origin for a secret the store then has to be reconciled with. Said plainly in .env.example and in lib/env.sh rather than left to be discovered. Not typechecked: node_modules is empty here and installs are frozen. Every edited file parses under `bun build --no-bundle`; the profile loads and pins the right cwd; assertInstallLayout was exercised from both the repo and /tmp; the setup section was run and writes five variables. Prettier was NOT run — 3.9.6 via bunx is not the pinned resolution and reformatted unrelated unions and line wraps in six files, so those were reverted and the edits re-applied by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8cf210eae5 |
caldav: json door for the web ui
step 3 of docs/nextcloud-replacement.md. collections, events and contacts as plain json, so the browser never has to parse multistatus xml to draw a list. this talks dav to radicale over loopback rather than reading its on-disk format. the storage layout is radicale's private business and changes between versions; propfind is its supported interface and costs one in-process hop. parsing the storage directly would be faster and would break silently on upgrade, which is a bad trade for a calendar. three bugs found and fixed while verifying, all of which fail quietly rather than loudly: calendar-data and address-data are NOT webdav live properties. rfc 4791 and 6352 define them as report-only, and radicale correctly returns an empty prop for them under propfind — so the first version returned a 207 full of nothing, which reads exactly like "your calendar is empty". the principal resource matched the calendar test, because `<C:calendar-home-set/>` satisfies /calendar\b/. the principal showed up in the list as a calendar called "1". the tag has to be required to end. the internal fetcher omitted X-Script-Name, so the ui was handed /1/work/ for the same collection a phone sees as /dav/1/work/, and nothing downstream could have matched them up. ical.ts is deliberately small: it unfolds lines and pulls out the fields a list shows. it does NOT expand rrule or resolve vtimezone — radicale owns correctness there, and rrule is passed through raw so the ui can say "repeats" without either of us pretending to know when. verified against the running stack with a real vevent and a real vcard, then the fixtures were removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ccd104a28b |
caldav/carddav: officer-caldav sidecar and the /dav door
first two steps of docs/nextcloud-replacement.md — the half that has to work on
a phone, because that is the half that cannot be faked.
radicale is supervised by the sidecar rather than reimplemented. nextcloud does
not implement caldav either; it vendors sabre/dav. icalendar and vcard are a
weekend, but sync-collection, rrule expansion, vtimezone and ctag/etag are not,
and when they are subtly wrong a phone does not error — it silently stops
syncing, or silently duplicates every event.
two doors, because a browser should not speak dav:
/dav/* top-level, http basic against a scoped app password, every
verb and every dav header forwarded verbatim. this is what
davx5 and ios talk to. same reasoning as /api/vault being
mounted outside protectedRouter.
/api/caldav/* the ordinary sidecar proxy, for officer's own ui. json.
the shared proxy factory could not carry the dav door: it forwards three
headers and dav dies without Depth, and it derives the user from a jwt a phone
cannot hold. so it is a separate file, per that factory's own instruction never
to grow per-app logic.
new `dav_app_passwords` — a phone cannot do jwt, and the alternative is the
account password living in a phone's account manager. argon2, shown once,
revocable per device, and accepted ONLY by /dav.
.well-known/caldav and carddav redirect to the dav root. they are most of what
makes adding an account feel transparent, and they need naming explicitly in
server.tsx or the SPA `/*` fallback answers the phone with html.
verified end to end against the running stack: 401 + WWW-Authenticate
unauthenticated; 207 with calendar-access and addressbook advertised; MKCALENDAR,
PUT and GET of a real VEVENT; calendar-query and sync-collection REPORTs; MKCOL,
PUT and GET of a real vCard. X-Script-Name is set because radicale otherwise
generates hrefs at / and the client follows them into the SPA.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|