Skip to content

fix!: the authority is asked about the operation dispatched, never the decoded path (#75 review) - #76

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/75-review
Oct 25, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/75-review

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Fix-forward for the independent review of #75 (r1). One objective: the route the authority checks is the operation the runtime runs, and nothing #75 removed is silently lost.

Findings and fixes

  • F1 (blocker, security): an encoded slash bypassed the route policy. wrapRequest asked the authority about r.Method+" "+r.URL.Path, the decoded path. Go's ServeMux dispatches on the escaped path, so GET /v1/page/pages/secret%2Fhistory ran the /pages/{id} operation (Requires page.page:read) but was checked against the /pages/{id}/history row (AnyCheckedCaller). Fix, as the reviewer proposed: mountOperations gives wrapRequest the dispatched operation's own row, method + " " + servedRoute(path). The local authority's admitMethod looks up m.Methods[method] exactly before it tries any template match (authority/vision state.go:202), so the row is found by name.
    • Test: TestAnEncodedSlashIsCheckedAsTheOperationItRuns is the reviewer's reproduction, over real HTTP through boot(). A caller holding only the history row gets 403 for /pages/secret%2Fhistory, the page handler does not run, and the authority is asked about GET /v1/page/pages/{id}. With the old line put back the test fails: 200 {"ran":"page-handler"}.
  • F2 (major): overlapping routes were refused as ambiguous. Same root, same fix. Test: TestOverlappingRoutesAreEachCheckedAgainstTheirOwnRow declares a subtree /v1/page/pages/ and /v1/page/pages/{id}. Each request is checked against exactly its own row and served. With the old line put back it fails: matches more than one declared route. The stand-in authority now models the real one: exact row first, then template matching with {name=**}, and ambiguity refused.
  • F3 (major): go.mod pins authority commits that exist only on the unmerged authority/vision. Not re-pinned, as asked. Recorded in the CHANGELOG and in the README's dependency-pins note: this closes when authority#1 merges with a merge commit (so both commits stay reachable from main) and this runtime re-pins to that merge or a tag. No runtime release is cut before then.
  • F4 (major): InterfaceInfo.BasePath. Chosen: delete the field. That is the strongest form of "refused by name": a backend still setting it fails to compile, naming BasePath. Why not check routes under the base path: the authority edge forwards a solution's requests on /v1/<id> unchanged (authority/vision edge.go:126), and the passthrough is mounted at /v1/<id> too. A request therefore never arrives under /solutions/<id>, and no non-empty base path can match one. Checking with it would only make the rows and the checks agree on a path nothing serves.
    • core-solutions users: only excel-extract (BasePath: "/solutions/excel-extract", on main and on authority/wiki-on-the-authority). It pins solution-runtime-go v0.0.13 and keeps working unchanged. When it moves to this (breaking) runtime, it has to carry /v1/excel-extract in its operation paths anyway, as wiki and proposals did when they dropped their base path. The compile error points straight at that change. wiki on main still sets one, but its authority branch has already removed it.
    • Not changed: the sub-point about the exported PassthroughOperations returning paths relative to the solution's route. Ten core-solutions tests use it to validate declarations, its doc already says the paths are relative, and InterfaceArtifact is the function that publishes them prefixed. Changing its signature is a separate objective.
  • F5 (minor): an InvalidArgument from the authority came back as a 503 and its reason was lost. refusalStatus now maps InvalidArgument/OutOfRange to 400. viewer() returns them as ErrRequestInvalid with the authority's message, which is answered 400 on a handler route and invalid_argument on a relayed call. An issue refused that way becomes a WorkContextRefusal of 400 carrying its reason. Unavailable/DeadlineExceeded stay 503. Test: TestAMalformedRequestIsRefusedWithTheAuthoritysReason.
  • F6 (minor): the stream relay lost its first-message bound. relayStream now cuts the module's stream off at the method's Timeout if no first message has arrived, using a timer that cancels a derived stream context, as StreamFirstAnswerWithin did. After the first message, only MaxStreamDuration applies. Test: added to TestAStreamEndsAsTheModuleOrItsBoundsSay. A silent module is cut off at 200 ms with "did not start its stream within 200ms". With the timer disabled, the test waits out the full 1-minute duration and fails.

What was reused

  • servedRoute, the function that already spells every published row. The check now uses the same spelling the artifact publishes, so there is one spelling.
  • The authority's own exact-row lookup (admitMethod). No route matcher was added to the runtime.
  • The existing refusalStatus / sentinel-error pattern for F5.
  • The removed StreamFirstAnswerWithin design (AfterFunc plus a late flag) for F6.

Classification

Fix at the owner for F1, F2, F4, F5 and F6. F3 is a recorded open gap whose fix belongs to the authority's merge; it is deliberately not re-pinned here.

Checks

  • go vet ./...: ok. go test -race -count=1 ./...: ok (129 s). gofmt -l .: clean.
  • Not verified: the cell host's Check (saas accounts) doing an exact-row lookup the way the local authority does. That is the reviewer's open question, and I had no live host to check it against.

No release (the release agent is holding releases until this merges).

🤖 Generated with Claude Code

…e decoded path

Review fixes for #75 (r1):

- F1 (blocker, security) and F2: wrapRequest asked the authority about
  r.Method+" "+r.URL.Path, the decoded path, while the mux dispatches on the
  escaped one, so /pages/secret%2Fhistory ran /pages/{id} under the
  /pages/{id}/history row; and a subtree beside {id} matched two rows, which
  the authority refuses as ambiguous. mountOperations now hands wrapRequest
  the dispatched operation's own row, verb+" "+servedRoute(path), which the
  authority finds by exact name.
- F4: InterfaceInfo.BasePath is deleted. The CLI prefixes every row with it
  while the edge forwards on /v1/<id> and the runtime checks routes as
  served, so no non-empty value matched a request.
- F5: InvalidArgument/OutOfRange from the authority's Check is 400 with the
  authority's reason (ErrRequestInvalid), not 503 "try again".
- F6: a relayed stream's first message is bounded by the method's Timeout
  again.
- F3 is recorded in the CHANGELOG and README: closes when authority#1 merges
  with a merge commit and the runtime re-pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit e53e044 into main Oct 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant