Reach the Commons through the gateway, with no PAT #252

Merged
stephen merged 2 commits from feat/me-commons into main 2026-08-13 22:07:46 +00:00
Owner

Implements the recommendation in fleet's docs/DESIGN-fj-fjord-operated-forge-auth.md (fleet#410). fj read only /me/forge-instances, which by design excludes the Commons, so there was no instance id and no gateway route to construct. The phone apps have called /me/commons since before fj existed; this is fj catching up, not a new mechanism.

Verified live

$ fj instances --host https://fjord.sh
  ID                                    NAME           SLUG           REGION      PLAN     STATE  URL
  e35d518d-…  avenue         avenue         eu-central  team     ready  https://avenue.fjord.host
  819eadfe-…  rasterhub.com  rasterhub-com                       ready  https://rasterhub.com
  28a5d919-…  Fjord Commons  commons        shared      commons  ready  https://commons.fjord.host

No PAT involved. That is the addressing problem closed.

What I ran versus reasoned about, since it matters:

Command Status
fj instances Run live, output above
fj api repos/… against Commons Not run. Needs fj instances use commons, which writes default_instance_id into the shared hosts.toml on this box. The brief said not to disturb the stored session other lanes depend on, so I stopped.
fj pr create Not run. Mutates Commons; the brief makes it the operator's to run.

The two unrun ones share one prerequisite and one verification:

fj instances use commons --host https://fjord.sh
fj api repos/fjord/assay --host https://fjord.sh      # read-only, proves routing
fj instances use <previous-id> --host https://fjord.sh # restore if desired

fj instances proves discovery and the gateway id; fj api proves routing. They are separable and only the first is done.

The revocation condition

fj cannot extend the window, because it never holds anything minted. The gateway mints the per-user forge token server-side and attaches it to the proxied call; nothing minted reaches a client. fj stores only the platform bearer it already had before this change, and the Commons response is fetched on every invocation and never persisted, since a cached joined would offer an instance the account may since have lost.

The minted-token lifetime is not observable from fj, and I have not invented a number. The mint happens inside the platform, between it and Paragon; the response fj sees is the proxied Forgejo payload, which carries nothing about the token's expiry. The revocation window is therefore still exactly what the design said it was, the minted-token lifetime plus the gateway cache TTL, both of which live in fjord-platform and Paragon and remain unmeasured. That measurement is still open, and this change neither improves nor worsens it.

Where fj differs from fjord-android

One place, deliberately. The app silently omits a Commons the account has not joined. fj withholds the instance identically, but keeps the CommonsInfo so it can say why: fj instances prints a dimmed line, and fj instances use commons reports "has not joined" or "this platform does not advertise a Commons" instead of "no instance matches". A CLI has no join button to notice, so silence reads as "no such forge".

Not a divergence but worth noting: both platform calls run concurrently under one try_join! and share a single refresh, since they use the same bearer and a 401 from either means the same expired session.

Not regressed

resolve_fjord still errors first on a PAT-only host, so that message is unchanged. --host with a PAT still routes directly to the forge, which is the path the three queued assay branches use today; nothing here touches it.

Checks

cargo build clean, cargo clippy --all-targets clean, cargo fmt applied, 772 tests pass including 7 new ones: joined, not joined, missing gateway id, 404, {"commons": null}, 401 mapping to the SessionExpired marker, and the gateway path built for the Commons being the ordinary one.

No token or bearer appears in this PR, in the code, or in any test fixture. hosts.toml on this box was compared before and after and is byte-identical.

Implements the recommendation in fleet's `docs/DESIGN-fj-fjord-operated-forge-auth.md` (fleet#410). `fj` read only `/me/forge-instances`, which by design excludes the Commons, so there was no instance id and no gateway route to construct. The phone apps have called `/me/commons` since before `fj` existed; this is `fj` catching up, not a new mechanism. ## Verified live ``` $ fj instances --host https://fjord.sh ID NAME SLUG REGION PLAN STATE URL e35d518d-… avenue avenue eu-central team ready https://avenue.fjord.host 819eadfe-… rasterhub.com rasterhub-com ready https://rasterhub.com 28a5d919-… Fjord Commons commons shared commons ready https://commons.fjord.host ``` No PAT involved. That is the addressing problem closed. **What I ran versus reasoned about**, since it matters: | Command | Status | | --- | --- | | `fj instances` | **Run live**, output above | | `fj api repos/…` against Commons | **Not run.** Needs `fj instances use commons`, which writes `default_instance_id` into the shared `hosts.toml` on this box. The brief said not to disturb the stored session other lanes depend on, so I stopped. | | `fj pr create` | **Not run.** Mutates Commons; the brief makes it the operator's to run. | The two unrun ones share one prerequisite and one verification: ``` fj instances use commons --host https://fjord.sh fj api repos/fjord/assay --host https://fjord.sh # read-only, proves routing fj instances use <previous-id> --host https://fjord.sh # restore if desired ``` `fj instances` proves discovery and the gateway id; `fj api` proves routing. They are separable and only the first is done. ## The revocation condition **`fj` cannot extend the window, because it never holds anything minted.** The gateway mints the per-user forge token server-side and attaches it to the proxied call; nothing minted reaches a client. `fj` stores only the platform bearer it already had before this change, and the Commons response is fetched on every invocation and never persisted, since a cached `joined` would offer an instance the account may since have lost. **The minted-token lifetime is not observable from `fj`, and I have not invented a number.** The mint happens inside the platform, between it and Paragon; the response `fj` sees is the proxied Forgejo payload, which carries nothing about the token's expiry. The revocation window is therefore still exactly what the design said it was, the minted-token lifetime plus the gateway cache TTL, both of which live in fjord-platform and Paragon and remain unmeasured. That measurement is still open, and this change neither improves nor worsens it. ## Where `fj` differs from fjord-android One place, deliberately. The app silently omits a Commons the account has not joined. `fj` withholds the instance identically, **but keeps the `CommonsInfo` so it can say why**: `fj instances` prints a dimmed line, and `fj instances use commons` reports "has not joined" or "this platform does not advertise a Commons" instead of "no instance matches". A CLI has no join button to notice, so silence reads as "no such forge". Not a divergence but worth noting: both platform calls run concurrently under one `try_join!` and share a single refresh, since they use the same bearer and a 401 from either means the same expired session. ## Not regressed `resolve_fjord` still errors first on a PAT-only host, so that message is unchanged. `--host` with a PAT still routes directly to the forge, which is the path the three queued assay branches use today; nothing here touches it. ## Checks `cargo build` clean, `cargo clippy --all-targets` clean, `cargo fmt` applied, **772 tests pass** including 7 new ones: joined, not joined, missing gateway id, 404, `{"commons": null}`, 401 mapping to the `SessionExpired` marker, and the gateway path built for the Commons being the ordinary one. No token or bearer appears in this PR, in the code, or in any test fixture. `hosts.toml` on this box was compared before and after and is byte-identical.
Reach the Commons through the gateway, with no PAT
Some checks failed
Forseti review / forseti review (advisory) (pull_request_target) Successful in 3m20s
ci / check (pull_request) Successful in 11m2s
ci / live-e2e (pull_request) Successful in 2m3s
ci / coverage (pull_request) Failing after 2m49s
ae06ee5066
fj read only /me/forge-instances, which by design does not include the
Commons, so there was no instance id and no gateway route to construct.
The phone apps have called /me/commons since before fj existed. This is
fj catching up rather than a new mechanism, per the recommendation in
fleet's DESIGN-fj-fjord-operated-forge-auth.md.

The synthesis mirrors fjord-android's CommonsInfo.toSwitchableInstance()
field for field, including all three refusals (no gateway id, no public
url, not joined). fj and the apps disagreeing about what the Commons is
would become someone's bug later, and the app is the older
implementation, so it is the reference.

Both platform calls now run concurrently and share one refresh: they
use the same bearer, so a 401 from either means the same expired
session, and refreshing twice for one expiry would be a second bug.

Cases that are handled rather than guessed:

- Not joined. The instance is withheld (offering it turns a clear "you
  have not joined" into a 403 from the gateway at the moment someone
  opens a PR) but the CommonsInfo survives, so `fj instances` prints
  why it is not listed and `fj instances use commons` says so instead
  of "no instance matches".
- Endpoint absent. 404 maps to Ok(None), which is today's behaviour
  exactly. An older platform, or one with the feature off, is not an
  error.
- PAT-only host. Untouched: resolve_fjord still errors first, so the
  existing message is unchanged.
- Selection. `fj instances use commons` persists the gateway id like
  any other instance, and `--host` with a PAT still routes directly,
  so the path three assay branches use today is not disturbed.

On the design's condition: fj cannot extend the revocation window
because it never holds anything minted. The gateway mints the per-user
forge token server-side and attaches it to the proxied call; no token
reaches the client. fj stores only the platform bearer it already had.
The Commons response is fetched every time and never persisted, since
a cached "joined" would offer an instance the account may have lost.

The minted-token lifetime is therefore NOT observable from fj, and no
number is invented here. See the PR body.

Superseded by Forseti review for 0caf5330fa30.

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#252
  • Head SHA: ae06ee506654
  • Review job: sha256:2c0326c767cbcc050ce2e2318b6551a83df9dd6c7cf10074a2b933787eca76f7
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Model tokens: 67050 in / 11735 out (32306 from cache) ≈ $0.4177
  • Token source: GITHUB_TOKEN
  • Runner: d79aaa8d3891
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/476
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":252,"head_sha":"ae06ee50665438071f6be33a6be35a75895c1ce8","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:9bdab3b2de5bf03a","review_job_key":"sha256:2c0326c767cbcc050ce2e2318b6551a83df9dd6c7cf10074a2b933787eca76f7","base_sha":"9bb83baa15e05cc647b91a35fd41f62ca855fb35","role":"summary","status":"superseded"} --> > Superseded by Forseti review for `0caf5330fa30`. ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#252` - Head SHA: `ae06ee506654` - Review job: `sha256:2c0326c767cbcc050ce2e2318b6551a83df9dd6c7cf10074a2b933787eca76f7` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Model tokens: `67050 in / 11735 out` (`32306` from cache) ≈ `$0.4177` - Token source: `GITHUB_TOKEN` - Runner: `d79aaa8d3891` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/476
forgejo-actions approved these changes 2026-08-13 21:38:09 +00:00
Dismissed
forgejo-actions left a comment

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#252
  • Head SHA: ae06ee506654
  • Review job: sha256:2c0326c767cbcc050ce2e2318b6551a83df9dd6c7cf10074a2b933787eca76f7
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Model tokens: 67050 in / 11735 out (32306 from cache) ≈ $0.4177
  • Token source: GITHUB_TOKEN
  • Runner: d79aaa8d3891
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/476
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":252,"head_sha":"ae06ee50665438071f6be33a6be35a75895c1ce8","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:9bdab3b2de5bf03a","review_job_key":"sha256:2c0326c767cbcc050ce2e2318b6551a83df9dd6c7cf10074a2b933787eca76f7","base_sha":"9bb83baa15e05cc647b91a35fd41f62ca855fb35","role":"summary","status":"current"} --> ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#252` - Head SHA: `ae06ee506654` - Review job: `sha256:2c0326c767cbcc050ce2e2318b6551a83df9dd6c7cf10074a2b933787eca76f7` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Model tokens: `67050 in / 11735 out` (`32306` from cache) ≈ `$0.4177` - Token source: `GITHUB_TOKEN` - Runner: `d79aaa8d3891` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/476
Drop the stray coverage attribute the deleted wrapper left behind
All checks were successful
Forseti review / forseti review (advisory) (pull_request_target) Successful in 1m35s
ci / check (pull_request) Successful in 10m21s
ci / live-e2e (pull_request) Successful in 2m8s
ci / coverage (pull_request) Successful in 2m56s
0caf5330fa
The nightly coverage build failed with "multiple coverage attributes":
src/cli/instances.rs carried two #[cfg_attr(coverage_nightly,
coverage(off))] for one function, so the file never compiled under
--cfg coverage_nightly and the job died before measuring anything.

It came from deleting the old fetch_instances wrapper in the previous
commit. Its attribute survived the deletion and glued itself to the
next item, which already had one.

The attribute was not the only orphan. The wrapper's doc comment
survived too, so fetch_instances_with_commons was documented as
talking to /me/forge-instances "NOT through Client" and said nothing
about the Commons call it actually makes. Both are fixed together
rather than just the line that broke the build: a stale doc comment
attached to the wrong function is the same deletion mistake and the
compiler cannot see it.

The remaining attribute sits immediately before the fn, matching the
other three in this file. The whole file and both other touched files
were checked for further repeats; there are none.

Verified with `make coverage-strict` locally rather than by pushing and
waiting: exit 0, TOTAL 82.89% lines against a floor the job enforces.
The attribute is inert on stable, which is why cargo check, clippy and
772 tests all passed while the nightly job did not.

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#252
  • Head SHA: 0caf5330fa30
  • Review job: sha256:95063ad9253244491f0b11f4f17d3c0cd50de7643913494f79f2ac6402562e1c
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Model tokens: 67809 in / 7549 out (32306 from cache) ≈ $0.2912
  • Token source: GITHUB_TOKEN
  • Runner: daf75cfeba3b
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/478
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":252,"head_sha":"0caf5330fa303b36fa4530b7a597ed73825d66fb","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:a4ea6e67569afe9c","review_job_key":"sha256:95063ad9253244491f0b11f4f17d3c0cd50de7643913494f79f2ac6402562e1c","base_sha":"9bb83baa15e05cc647b91a35fd41f62ca855fb35","role":"summary","status":"current"} --> ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#252` - Head SHA: `0caf5330fa30` - Review job: `sha256:95063ad9253244491f0b11f4f17d3c0cd50de7643913494f79f2ac6402562e1c` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Model tokens: `67809 in / 7549 out` (`32306` from cache) ≈ `$0.2912` - Token source: `GITHUB_TOKEN` - Runner: `daf75cfeba3b` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/478
forgejo-actions left a comment

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#252
  • Head SHA: 0caf5330fa30
  • Review job: sha256:95063ad9253244491f0b11f4f17d3c0cd50de7643913494f79f2ac6402562e1c
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Model tokens: 67809 in / 7549 out (32306 from cache) ≈ $0.2912
  • Token source: GITHUB_TOKEN
  • Runner: daf75cfeba3b
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/478
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":252,"head_sha":"0caf5330fa303b36fa4530b7a597ed73825d66fb","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:a4ea6e67569afe9c","review_job_key":"sha256:95063ad9253244491f0b11f4f17d3c0cd50de7643913494f79f2ac6402562e1c","base_sha":"9bb83baa15e05cc647b91a35fd41f62ca855fb35","role":"summary","status":"current"} --> ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#252` - Head SHA: `0caf5330fa30` - Review job: `sha256:95063ad9253244491f0b11f4f17d3c0cd50de7643913494f79f2ac6402562e1c` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Model tokens: `67809 in / 7549 out` (`32306` from cache) ≈ `$0.2912` - Token source: `GITHUB_TOKEN` - Runner: `daf75cfeba3b` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/478
Sign in to join this conversation.
No description provided.