Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude/skills/refresh-generated-api/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ does not carry:
- **`SOURCE.txt`** — the release tag you checked out (or exact PR-head commit
before a release), and the module version from upstream's
`module/module.package.codefly.yaml`. CI resolves release pins through
`refs/tags/` so a same-named branch cannot select the source.
`refs/tags/` so a same-named branch cannot select the source. The same ref
is the gateway `conformance/hostgateway` mints through, at the
`(auth gateway: …)` path recorded there.
- **`CHANGELOG.md`** — under the unreleased heading. `### Added` for new
services, facades, and fields; `### Removed (breaking)` for anything the
regeneration deleted, naming the symbols a consumer will fail to compile
Expand All @@ -109,6 +111,20 @@ buf generate --template "$repo/buf.gen.yaml" -o /tmp/regen-check
diff -r /tmp/regen-check/gen "$repo/gen"
```

Moving the ref also moves the broker contract `moduleauthority` is held to.
Mint through the new ref's real gateway before the PR, not after CI says so
(`conformance/hostgateway/AGENTS.md`):

```bash
conformance/hostgateway/run.sh "$host_checkout" -race
```

Read a red run before fixing it. If the test file does not compile at the new
ref, the host renamed what the file binds to: re-bind those calls and accessors,
and leave the assertions alone. If it compiles and an assertion fails, the
broker contract moved: fix the client. `conformance/hostgateway/AGENTS.md`
("When it goes red") tells the two apart.

Silence is the proof. This also catches a stale `SOURCE.txt`: if the diff is
non-empty for a ref you did not change, the recorded ref is not the one the
committed tree came from, and that is worth its own issue rather than a quiet
Expand Down
77 changes: 77 additions & 0 deletions .github/actions/host-source/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: host source
description: >-
Check out codefly-dev/module-saas-starter, read-only, at the ref SOURCE.txt
records: the accounts proto path SOURCE.txt records, and its auth gateway
path when asked. One reading of SOURCE.txt for every job that needs the host.
inputs:
sdk:
description: Where this repository is checked out.
default: sdk
path:
description: Where to check the host out.
default: source
gateway:
description: Also check out the auth gateway path SOURCE.txt records.
default: "false"
outputs:
ref:
description: The git ref checked out (refs/tags/<tag> or a 40-hex commit).
value: ${{ steps.source.outputs.ref }}
proto:
description: The accounts public proto path, relative to the host checkout.
value: ${{ steps.source.outputs.proto }}
gateway:
description: The auth gateway Go module path, relative to the host checkout.
value: ${{ steps.source.outputs.gateway }}
runs:
using: composite
steps:
- name: read the recorded source ref
id: source
shell: bash
env:
SOURCE_TXT: ${{ inputs.sdk }}/SOURCE.txt
WITH_GATEWAY: ${{ inputs.gateway }}
run: |
set -euo pipefail
line=$(grep -m1 -oE 'codefly-dev/module-saas-starter@(v[0-9]+\.[0-9]+\.[0-9]+|[0-9a-f]{40})$' "$SOURCE_TXT" || true)
if [ -z "$line" ]; then
echo "::error file=SOURCE.txt::no 'codefly-dev/module-saas-starter@<release tag or 40-hex sha>' ref found"
exit 1
fi
proto=$(grep -m1 -oE 'accounts public proto: [^)]+' "$SOURCE_TXT" | sed 's/^accounts public proto: //' || true)
if [ -z "$proto" ]; then
echo "::error file=SOURCE.txt::no '(accounts public proto: <path>)' line found"
exit 1
fi
gateway=$(grep -m1 -oE 'auth gateway: [^)]+' "$SOURCE_TXT" | sed 's/^auth gateway: //' || true)
if [ "$WITH_GATEWAY" = "true" ] && [ -z "$gateway" ]; then
echo "::error file=SOURCE.txt::no '(auth gateway: <path>)' line found"
exit 1
fi
ref=${line#*@}
if [[ "$ref" == v* ]]; then
ref="refs/tags/$ref"
fi
paths=$proto
if [ "$WITH_GATEWAY" = "true" ]; then
paths=$(printf '%s\n%s' "$proto" "$gateway")
fi
{
echo "ref=$ref"
echo "proto=$proto"
echo "gateway=$gateway"
echo "paths<<EOF"
echo "$paths"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "source: codefly-dev/module-saas-starter@${line#*@} ($(echo $paths))"
# The source repo is public, so the default token is enough.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: codefly-dev/module-saas-starter
ref: ${{ steps.source.outputs.ref }}
path: ${{ inputs.path }}
sparse-checkout: ${{ steps.source.outputs.paths }}
sparse-checkout-cone-mode: false
persist-credentials: false
57 changes: 27 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,36 +55,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: sdk
- name: read the recorded source ref
id: source
run: |
set -euo pipefail
line=$(grep -m1 -oE 'codefly-dev/module-saas-starter@(v[0-9]+\.[0-9]+\.[0-9]+|[0-9a-f]{40})$' sdk/SOURCE.txt || true)
if [ -z "$line" ]; then
echo "::error file=SOURCE.txt::no 'codefly-dev/module-saas-starter@<release tag or 40-hex sha>' ref found"
exit 1
fi
proto=$(grep -m1 -oE 'accounts public proto: [^)]+' sdk/SOURCE.txt | sed 's/^accounts public proto: //')
if [ -z "$proto" ]; then
echo "::error file=SOURCE.txt::no '(accounts public proto: <path>)' line found"
exit 1
fi
ref=${line#*@}
if [[ "$ref" == v* ]]; then
ref="refs/tags/$ref"
fi
echo "ref=$ref" >> "$GITHUB_OUTPUT"
echo "proto=$proto" >> "$GITHUB_OUTPUT"
echo "source: codefly-dev/module-saas-starter@${line#*@} ($proto)"
# The source repo is public, so the default token is enough.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: codefly-dev/module-saas-starter
ref: ${{ steps.source.outputs.ref }}
path: source
sparse-checkout: ${{ steps.source.outputs.proto }}
sparse-checkout-cone-mode: false
persist-credentials: false
- id: source
uses: ./sdk/.github/actions/host-source
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: sdk/go.mod
Expand Down Expand Up @@ -113,3 +85,28 @@ jobs:
git --no-pager diff --stat
git --no-pager diff | head -n 400
exit 1

# The module broker contract, held against the host's REAL auth-gateway
# handlers at the ref SOURCE.txt records — not only against
# moduleauthority's fake, which once still accepted a header the host had
# stopped reading. run.sh compiles conformance/hostgateway's test into the
# host's gateway package by overlay; the checkout is never modified, and
# only accounts' gRPC mints are faked, as the host's own gateway tests do.
host-gateway-conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: sdk
- id: source
uses: ./sdk/.github/actions/host-source
with:
gateway: "true"
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: sdk/go.mod
cache-dependency-path: |
sdk/go.sum
source/${{ steps.source.outputs.gateway }}/go.sum
- name: mint through the real gateway
run: sdk/conformance/hostgateway/run.sh source -race -v
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ Fleet standard — [handbook#68](https://github.com/obin-ai/handbook/issues/68).
descriptor has meant a text-rewritten module path, not a bad protobuf
release.
- **Say what you did not verify.** Unverified is not the same as working. CI
(below) runs the build walk and the `gen/` drift check, nothing more — if
you could not exercise something else, the PR says so.
(below) runs the build walk, the `gen/` drift check and the host-gateway
mint, nothing more — if you could not exercise something else, the PR says so.

## Build and test

Expand All @@ -79,7 +79,9 @@ export data for the boundary gate to run. There is no lint config —
The same workflow's `gen-drift` job regenerates `gen/` from the ref in
`SOURCE.txt` with this repo's `buf.gen.yaml` (buf itself pinned in the
workflow) and fails on any difference — the reproduction check from the
`refresh-generated-api` skill, run for you.
`refresh-generated-api` skill, run for you. Its `host-gateway-conformance` job
mints through the host's *real* gateway handlers at that same ref
(`conformance/hostgateway/run.sh <host checkout>`; see the `AGENTS.md` there).

## Where things live

Expand All @@ -92,6 +94,7 @@ workflow) and fails on any difference — the reproduction check from the
| `workcontext/` | callee-side verification: HTTP, Connect, gRPC middleware plus scope checks |
| `moduleauthority/` | a module's own authority: gateway broker mints; direct Accounts authority or explicitly selected gateway authority transport |
| `gen/` | generated output. Regenerated, never edited |
| `conformance/hostgateway/` | `moduleauthority` minting through the host's real gateway at the `SOURCE.txt` ref |
| `internal/apiboundary` | the public-API boundary gate, with fixtures that prove it still looks |
| `internal/agentcontext` | the budget and frontmatter contract for this file and the skills |

Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
# Changelog

## Unreleased — the broker contract, held against the host's real gateway

- **Fix (moduleauthority).** A source mint's HTTP 412 returns
`ErrDelegationMissing` again, wrapping the same `*BrokerError`: the source
has no active delegation, and a person must reconnect it. It is decided by
status alone, as 403 → `ErrDelegationInvalid` is — the v0.0.94 gateway
answers 412 on that path only for `DELEGATION_MISSING` (any other
FailedPrecondition is a 502) — so no response body is read.
`ErrDelegationMissing` is no longer deprecated. `ErrDelegationRevoked`
stays deprecated: telling it apart would need the body.
- **Behaviour change.** `moduleauthority.New` refuses an `OrganizationID` that
is not a canonical (lowercase, hyphenated) UUID with `ErrInvalidCredentials`
— accounts' own rule — whichever mints the client goes on to make. Who
changes behaviour:
- a client that mints module Work Contexts or operation contexts used to
fail at its first such mint with an untyped HTTP 400; it now fails at
construction;
- a client that only mints source operation contexts, which never send the
field, used to work with such a value; it now fails at construction.
Leave `OrganizationID` empty there, or correct it.
An uppercase, braced, `urn:uuid:`, unhyphenated or space-padded UUID is
refused; the empty string still means "no organization".
- Documented: a missing or wrong organization (a shared module naming none, a
tenant-pinned module naming another) is refused as `ErrInvalidCredentials`
(HTTP 401) on the module Work Context mint and `ErrPermissionDenied`
(HTTP 403) on the operation mint (`ErrInvalidCredentials` doc, README).
- **CI.** New `host-gateway-conformance` job: `conformance/hostgateway/run.sh`
mints with the SDK's real client through module-saas-starter's real
auth-gateway handlers at the `SOURCE.txt` ref, faking only accounts' gRPC
mints. The host checkout comes from a `host-source` composite action now
shared with `gen-drift`; `SOURCE.txt` records the gateway path beside the
proto path. The runner fails unless all eight expected tests are listed,
run and pass. It also fails if the build would give the gateway a grpc,
protobuf or sdk-go/workcontext whose source differs from the host's pin,
and it resolves nothing at `@latest`: `moduleauthority`'s imports must all
be declared by the SDK's go.mod (checked `-mod=readonly` before `go get`
adds it), and everything after that one `go get` runs `-mod=readonly`.
- `moduleauthority`'s fake gateway follows the real one's prefix rule, check
order, and indifference to the legacy module-secret header; its remaining,
deliberate divergences are listed in `host_fake_test.go`.

## Unreleased — callable sources

- Reject unsupported HTTP response encodings before reading the body or
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,16 @@ The SDK surfaces are:
per organization, since each client caches its own module Work Context. A
tenant-pinned module may omit the selector or name its own tenant. The host
checks the module envelope and live installation; selecting an organization
grants no authority. Source mints ignore this selector and derive their
grants no authority. The selector must be a canonical UUID (lowercase,
hyphenated), as accounts requires; `New` refuses any other form with
`ErrInvalidCredentials` at construction, whichever mints the client will
make, rather than letting the first work or operation mint fail as an
untyped 400. A client that only mints source operation contexts leaves it
empty. A missing or wrong organization — a shared module that names none, or a
tenant-pinned module that names another — is refused as unproven
credentials: `ErrInvalidCredentials` (HTTP 401) from `ModuleWorkContext`, and
`ErrPermissionDenied` (HTTP 403) from `MintModuleOperationContext`. The host
does not say that the organization was the cause. Source mints ignore this selector and derive their
organization from the source's delegation. Deployment and identity requirements
are in [MODULE_INSTALLATION.md](https://github.com/codefly-dev/module-saas-starter/blob/v0.0.94/module/MODULE_INSTALLATION.md)
and [Establishing what a caller is running](https://github.com/codefly-dev/module-saas-starter/blob/v0.0.94/module/services/accounts/AGENTS.md#establishing-what-a-caller-is-running).
Expand Down Expand Up @@ -475,7 +484,10 @@ compatible dependency floor with SDK and consumer builds when refreshing it.
The generation plugin versions remain pinned.
Record the release tag in `SOURCE.txt`; CI checks out that tag through
`refs/tags/` and verifies that regeneration leaves no diff. Before a host
release, an exact PR-head commit may be used for development.
release, an exact PR-head commit may be used for development. The same ref is
the host whose real auth-gateway handlers `moduleauthority` must mint through
in CI (`conformance/hostgateway/run.sh <host checkout>`), so moving it moves
the broker contract too.

Regeneration and matching SDK releases are owned by `module-saas-starter` and
this repository. Until release automation covers both, follow the
Expand Down Expand Up @@ -596,8 +608,12 @@ with `errors.As`. It contains only `StatusCode` and `Classification`
and headers are excluded entirely; classification never depends on reading a
body. The 401, 403 and 503 sentinels remain `ErrInvalidCredentials`,
`ErrPermissionDenied` (or `ErrDelegationInvalid` for source mints), and
`ErrExecutionUnestablished`. Body-based delegation distinctions are gone: source
403s all return `ErrDelegationInvalid`; 412s carry only `BrokerError`.
`ErrExecutionUnestablished`. A source mint's 412 is `ErrDelegationMissing`: the
source has no active delegation to this module, and a person must connect or
reconnect it — minting again will not help. The gateway answers 412 there only
for a missing delegation, so the status alone decides it. Body-based delegation
distinctions are gone: source 403s all return `ErrDelegationInvalid`, whether
the delegation was revoked or is otherwise unusable.
`BrokerError.Path` and `BrokerError.Reason` have been removed.

Transport and response read failures return `*moduleauthority.BrokerTransportError`
Expand Down
1 change: 1 addition & 0 deletions SOURCE.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
gen/ generated from codefly-dev/module-saas-starter@v0.0.94
(accounts public proto: module/services/accounts/proto)
(auth gateway: module/services/auth-gateway/code)

module version: 0.1.0 (module/module.package.codefly.yaml)
This SDK's release is matched to the saas-starter module version above.
Expand Down
Loading
Loading