The Docker smoke-test ran the server container under the fixed name
familiar-smoke. On the self-hosted runner the job shares the host
Docker daemon, so a container left behind by a killed or re-run job
(or a concurrent dev push + PR) collides on that name and 'docker run'
fails with exit 125.
Derive the name from the commit SHA, remove any stale container of that
name before starting, and thread the name through the smoke-test and
cleanup steps.
The smoke test probed the server over the network, which fails on the
self-hosted runner: the job step runs in a container that shares the host
Docker socket but sits on a different network, so it can reach neither a
published host port (-p lands on the runner host) nor the server
container's bridge IP (different network → curl connect hangs, so the job
looked stuck rather than failing).
Probe from inside the server container with `docker exec` instead: it goes
through the Docker socket and runs in the server's own netns, where
127.0.0.1:8080 always works regardless of runner topology. The image ships
python + httpx, so no curl or extra image pull is needed. Drop the now
unnecessary -p publish.
The compose service already joins the external `proxy` network and
exposes :8080; the reverse proxy owns TLS and routing. Remove the
Traefik router labels (and the now-unused FAMILIAR_DOMAIN /
TRAEFIK_CERTRESOLVER env vars) and document Pangolin — which discovers
the service on the shared network — as the proxy instead. nginx/Caddy
guidance is unchanged.
- doc/deployment.md: run familiar as a remote MCP server — secure defaults,
bearer vs OAuth modes, the full env-var reference, one-time login / token-store
provisioning, the published images, docker compose, and TLS reverse-proxy
blocks for nginx (proxy_buffering off + long read timeout), Caddy, and Traefik.
- doc/clients/: one page per client with exact working steps — Claude Code,
VS Code (.vscode/mcp.json with a masked token input, and the workspace
.mcp.json header gotcha), Mistral Le Chat, Claude Desktop + Cowork (mcp-remote
bridge, per-OS config paths), and Claude web (oauth mode + external IdP, with
"use Desktop + bearer" as the simpler alternative). Plus a clients/ index.
- README: a "Connect a client" matrix (client -> auth -> guide) and a compact
"Deploy with Docker" section linking to doc/deployment.md; oauth mode noted.
- doc/README.md index: rows for Deployment and Client setup.
- doc/getting-started.md: pointer from the HTTPS quickstart to the full docs.
- CLAUDE.md: FAMILIAR_AUTH_MODE / FAMILIAR_OAUTH_* / FAMILIAR_ALLOW_INSECURE_BIND
and /healthz in the transport note; the split ci.yml / docker.yml workflows.
Deployment docs live under the existing doc/ tree (the repo uses doc/, not a
separate docs/) to keep a single documentation directory.
New .gitea/workflows/docker.yml (alongside the untouched ci.yml wheel publish).
Runs lint + test, then builds the image and smoke-tests it — non-root uid,
/healthz 200, MCP 401 without a token and 200 with a valid bearer. On a merge to
main/dev it pushes to git.hiddenden.cafe/hiddenden/familiar (main -> :latest +
:main, dev -> :dev, plus :sha-<12>) via the existing REGISTRY_TOKEN, then does a
scoped cleanup. The two workflows are independent and don't interfere.
- docker/Dockerfile: multi-stage uv wheel build; runtime runs `familiar serve`
as a non-root user (uid 1000) on port 8080, /data volume for the encrypted
token store, and a HEALTHCHECK against /healthz. No token is baked in — serve
refuses to start without FAMILIAR_HTTP_TOKEN, so it's supplied at run time.
- docker-compose.yml: joins an external `proxy` network (Traefik labels for TLS,
nginx/Caddy documented as alternatives), read-only rootfs, tmpfs /tmp,
no-new-privileges, cap_drop ALL, non-root, /healthz healthcheck. Pulls the
published FAMILIAR_IMAGE with a local build as fallback; writes default off.
- .env.example: every FAMILIAR_* knob incl. the bearer token, auth mode, the
OAuth resource-server settings, and the insecure-bind escape hatch.
- .dockerignore: keep local state (data/, *.enc, key) and dev files out.
Extend the HTTP transport (from the remote-MCP work) so it's production-ready.
- /healthz: an unauthenticated GET liveness route that returns only
{status, server, version} — no account data, no token state. For Docker and
reverse-proxy probes; on stdio the route is never served.
- Fail-closed bind: refuse to start without authentication, and make the
message distinguish a loopback bind from a public (0.0.0.0) one. The only
override is the explicit FAMILIAR_ALLOW_INSECURE_BIND=true escape hatch, which
permits an unauthenticated bind (with a loud warning).
- FAMILIAR_AUTH_MODE=oauth: run as an OAuth 2.1 resource server. A JWTVerifier
validates external-issuer JWTs (JWKS from FAMILIAR_OAUTH_JWKS_URI, else
discovered from FAMILIAR_OAUTH_ISSUER's OIDC metadata) against issuer +
audience, wrapped in a RemoteAuthProvider that serves
/.well-known/oauth-protected-resource so OAuth-only clients (Claude web) can
discover the authorization server. familiar is a resource server only — no
authorization server, no DCR. bearer mode is unchanged and stays the default.
Tests cover healthz (200, no leak), fail-closed vs escape hatch, and oauth
(valid JWT 200; missing/expired/wrong-audience 401 + WWW-Authenticate; the
protected-resource metadata), plus the new config readers.
- README + getting-started: a "Serve over HTTPS (remote MCP)" section with the
`familiar serve` command, the FAMILIAR_HTTP_* env vars, the write-off-by-
default note, an nginx location block (proxy_buffering off, long read
timeout), the headless-login / copy-the-store options, and the steps to add
it as a Mistral custom connector.
- architecture: describe the stdio/HTTP transport split, the bearer-auth gate,
and the HTTP write default.
- CLAUDE.md: add the new commands and env vars to Commands + Architecture.
- 401 without/with a bad token (incl. WWW-Authenticate: Bearer), 200 with a
valid one, driven through the ASGI app in-process.
- configure_http refuses to start when the token is unset or blank.
- FAMILIAR_HTTP_* config defaults and overrides; http_token() strip/empty.
- HTTP write default disabled by default, env still wins, stdio stays enabled.
- parse_callback (URL / bare code / empty) and the manual_login exchange flow.
Add a `familiar serve` subcommand that runs the FastMCP streamable-HTTP
transport for use as a remote MCP server (e.g. a Mistral custom connector),
while stdio stays the default and is completely unchanged.
- config: FAMILIAR_HTTP_HOST/PORT/PATH (default 127.0.0.1:8080/mcp) and a
live http_token() reader for FAMILIAR_HTTP_TOKEN.
- server: configure_http()/serve_http() attach a StaticTokenVerifier so a
missing/wrong bearer token gets 401 + WWW-Authenticate: Bearer and a valid
one passes the handshake. Refuses to start (SystemExit) without a token.
- writes: in HTTP mode the write default flips to disabled unless
FAMILIAR_ENABLE_WRITES is explicitly truthy; guard() logic is untouched.
- auth: manual_login() + parse_callback() power `login --no-browser`, printing
the SSO URL and reading the pasted callback from stdin (no local browser).
- bump fastmcp pin to >=3.0.0 for the streamable-HTTP + verifier API.
Implements issue #51. Adds a Fuzzwork SDE downloader/importer and
expands the offline lookup layer beyond item types.
- sde.py: SDE_URL constant (FAMILIAR_SDE_URL override) and
async download_sde() that streams + bz2-decompresses the dump
incrementally to sde_path(), resetting the cached connection.
- sde.py: system_by_id/system_by_name/station_by_id offline lookups,
all degrading to None when the table/file is missing.
- universe.py: get_system/get_station prefer the SDE when available,
falling back to ESI otherwise (signatures unchanged).
- __main__.py: `familiar sde download|status|path` subcommands.
- tests: download_sde via injected MockTransport, new lookups + None
fallbacks, and SDE-vs-ESI fast-path/fallback for get_system/station.
- README: short offline-SDE setup note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Claude Desktop JSON in the README rendered as one cramped line on the
package page — format it as a proper multi-line block. Bump version to
0.0.2 so the published package "About" picks up the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add src/familiar/sde.py backing type lookups from a local SQLite SDE with
graceful fallback to ESI when absent. lookup.resolve_id prefers the SDE for
inventory_types; universe.get_item serves SDE-backed fields with no network
call. Register a read-only sde_status() MCP tool. Tested against an in-memory
SQLite fixture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Invert the global write toggle so writes are ENABLED by default and the
user opts OUT, while keeping all other safety intact (per-call confirm=true
dry-run preview and the on-disk audit log are unchanged).
- writes.py: use a `writes_disabled` flag file instead of `writes_enabled`;
`writes_enabled()` returns True unless the flag exists (env var still
honored either way); `set_writes(False)` creates the flag, `set_writes(True)`
removes it. Update user-facing notes.
- tests: default is now ENABLED; adjust toggle/disabled tests (set disabled
explicitly via set_writes(False) or FAMILIAR_ENABLE_WRITES=0).
- README.md / CLAUDE.md: document writes are on by default, opt out via
`familiar writes disable`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EsiClient now caches GET responses honoring the Expires header and backs off
when the rolling ESI error window is nearly exhausted (X-Esi-Error-Limit-*).
README gains Setup / Connect-to-Claude / enabling-writes sections.
Closes#16. SDE bulk-load split out to its own follow-up (#51).
135 tests, coverage 90%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document common commands, the register(mcp) tool-module pattern, the ESI/
auth/charutil/write-safety architecture, test conventions, and the dev-only
git/CI workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add read-only PI tools: get_planets (colonies), get_planet_layout
(pins/links/routes), get_corp_customs_offices. ESI exposes no PI write
endpoint despite the manage_planets scope name, so these are reads.
Closes#29. 132 tests, coverage 90%, 84 tools.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fleet: get_fleet (read, handles not-in-fleet); guarded invite_to_fleet and
update_fleet. UI: guarded set_waypoint and open_window (market/info/contract).
All writes go through write-safety.
Closes#27, #28. 128 tests, coverage 89%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contacts: read character/corp/alliance contacts; guarded add/remove of
character contacts. Fittings: read saved fits; guarded create/delete.
EsiClient.delete now takes params. All writes go through write-safety.
Closes#25, #26. 120 tests, coverage 89%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add read-only tools: faction-warfare stats (char + corp), freelance jobs
(char + corp), corp projects, character activities, and access lists.
FW endpoints are stable; the newer projects/freelance/activities/access
paths are best-effort and flagged for live confirmation.
Closes#34, #35, #36. 106 tests, coverage 91%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add read-only corp tools: members, member tracking, divisions, titles,
medals, standings (corp-level, need corp roles), plus the character's own
corp roles. Closes#32.
95 tests, coverage 91%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add read-only killmail tools: recent character and corp killmails (refs
enriched with system/victim/attacker detail) plus zKillboard PvP stats via
a tiny injectable zkill client. New charutil.character_targets helper for
token-less per-character iteration.
85 tests, coverage 93%. Closes#30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add writes.py: the single gate every write tool must pass. A write happens
only when confirm=true AND writes are enabled (off by default); otherwise a
dry-run preview is returned. Every executed write is appended to an audit
log. New `familiar writes [enable|disable|status]` CLI and a read-only
write_status MCP tool.
81 tests, coverage 92%. Closes#22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add read-only structure tools: search_structures (per character),
get_structure and get_structure_market (single structure via one token),
and get_corp container logs. New charutil.one_token helper picks a single
authenticated token (named character or first).
31 tools total, 75 tests, coverage 92%. Closes#21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add read-only industry tools (industry jobs, blueprints, mining ledger;
corp jobs + blueprints) and trade tools (contracts, own market orders;
corp contracts + orders). New charutil helper runs a fetcher over one or
all characters, with a corp variant that resolves each character's corp id.
account._run now reuses it.
27 tools total, 70 tests, coverage 91%. Closes#18 and #19; covers most of #20.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expand config.SCOPES to the complete character + corp scope set (64 read,
9 write), split into READ_SCOPES / WRITE_SCOPES so a caller can opt into a
read-only subset via FAMILIAR_SCOPES. Update the README: familiar is no
longer "read-only by design" — reads are free, writes are opt-in, previewed,
and audited.
Part of #17. Note: changing scopes requires re-running `familiar login` per
character to re-consent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the token store to hold many characters keyed by character_id,
migrating the previous single-character file transparently. `familiar login`
now adds a character; `familiar status` lists all; `familiar logout [name|id]`
removes one or all.
Every account tool gains an optional `character` (name or id): omitted means
all logged-in characters, given means just that one. Results are uniform
{"characters": [...]}. get_auth refreshes per character.
58 tests, coverage 89%. Closes#9
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo already has a REGISTRY_TOKEN secret (Gitea access token with
package scope); the workflow referenced a non-existent PACKAGE_REGISTRY_TOKEN
and fell back to the auto GITHUB_TOKEN, which is not granted package-write,
so every upload was rejected. Point twine at REGISTRY_TOKEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gitea's PyPI registry rejects twine's --skip-existing flag
(UnsupportedConfiguration), which failed every publish run. Remove it and
instead tolerate an "already exists" conflict in the shell, so dev (unique
.devN versions) always publishes and a main re-run of an existing release
is treated as success rather than a hard failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setting `permissions: packages: write` alone dropped every other scope to
none, so actions/checkout could not clone the private repo and the publish
job failed before building. Add contents:read, and use github.actor (more
reliably populated than gitea.actor) as the twine username.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a thin async ESI client (httpx, descriptive User-Agent, uniform errors,
mockable transport) plus name<->id resolution. New read-only tools:
- universe: resolve_names, get_item, get_system, get_station, get_region, get_route
- market: list_hubs, get_market_price, get_market_orders, compare_hubs
Trade-hub IDs (Jita/Amarr/Dodixie/Rens/Hek) are hardcoded constants. Market
aggregation (best buy/sell, spread, order book) lives in pure helpers. 30 unit
tests with mocked ESI; coverage 99%. SDE bulk-load deferred to a later phase.
Closes#2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lint (ruff) and test (pytest, fail under 80% coverage) on every PR and
push to main/dev. On a merge to main/dev, build the sdist+wheel and
publish to the Gitea PyPI registry — main as a clean release, dev as a
unique .devN pre-release. No Docker; runs on uv.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Set up the familiar package (src layout, uv/hatchling) with a FastMCP
server exposing a read-only ping health tool. Add pytest smoke tests with
80% coverage gate. Flesh out the README with the project vision and roadmap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>