Skip to content

feat(accounts): serve the published host interfaces as adapters — rung 1, with the gap list - #1217

Merged
antoinetoussaint-byte merged 138 commits into
mainfrom
authority-interface
Oct 26, 2026
Merged

antoinetoussaint-byte merged 138 commits into
mainfrom
authority-interface

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Directory Work Context admission

Fix at the Accounts owner: DirectoryService's three Connect reads now accept a person-owned Work Context addressed to saas-directory. A consuming module obtains it through the existing installed module_actor operation binding and forwards it with the generated DirectoryService client. Admission reuses source-operation signature, audience, expiry, live seal/revision and delegation verification. The shared directory handlers retain requireAuth, current organization membership and verified tenant confinement; bearer admission is unchanged. Directory facts remain SaaS-owned and no RPC or fact is added to the authority interface. No wire contract or generated file changed.

Generated-client regressions cover the installed-module exchange and all three reads, foreign-organization refusal even for a member of both organizations, expiry, forgery, wrong audience, current membership and revocation, delegation revocation/unavailability, ambiguous carriers, spoofed identity headers and unchanged bearer admission. This adds no separate authority store or verifier.

Latest local qualification for b961f6f2: Accounts go vet ./... passes. The full go test -race -json -count=1 -timeout=35m ./... run found only two audience-set assertions needing the new built-in audience; complete adapters and business race reruns pass after those updates. Combined final evidence is 52 tested packages, 6,933 passing tests/subtests, 15 existing conditional skips, and no race reports. All new directory tests execute without skips. Naming, boundaries, whitespace, the new commit identity, prohibited-file and lockfile/token checks pass. This is the current directory-change qualification; the earlier validation notes below record the preceding work's status.

Accounts implements the authority interface through its existing identity, Work Context, signed-delivery, installation, scope-grant and audit owners. Signed module and solution policies are persisted with their exact manifest generation and enforced by Check. Datasources remain SaaS-owned.

The consent replay fix is implemented, but this PR still requires a coordinated update in codefly-dev/authority: publish the new Install approval fields and update the conformance caller, then copy the published proto/digest and advance the conformance pin here. That repository is outside this task's authorized scope. The provenance assertion and conformance suite remain intact and currently fail on this dependency.

Consent replay fix

Both human Install RPCs require an approval containing the immutable target ID, reviewed generation, expected installation revision (zero for creation), and a UUID operation ID. The frontend submits the selected catalogue generation; host-owned fixture setup resolves its own explicit approval.

Accounts compares the reviewed generation with applied presence while holding the binding/target locks, and compares the installation revision under the existing installation lock in the consent transaction. A durable receipt binds the original request and actor to the resulting revision. Unchanged retries return the installation without repeating the lifecycle audit; stale generation, stale revision, missing approval and conflicting operation reuse have named refusals. Neither replay after a wider delivery nor replay after narrowing can grant authority. A revoked approval cannot recreate its installation.

Classification: fixes at the existing Accounts installation and contract owners. Reused seams: the target catalogue, organization admin check, installation transaction, lifecycle revision/revocation trigger, tenant RLS, generated clients, and existing generation scripts. No second authority store, compatibility fallback, boundary exception, or weakened test was added.

Earlier review findings retained

  • X1: formatted installation scope implementation.
  • X2: explicit admin re-install records consent; non-admin attempts leave it unchanged; host fixture creation consents. The remedy now names the exact RPC and approval fields.
  • X3: not-installed, consent-required and unreadable-authority refusals remain distinct and log their cause with credentials redacted.
  • X4: conflicting delivery prints stored and offered manifest, payload and authority-document digests.
  • X5: pre-renumber local/validation databases must be recreated; docs and migration refusals state this.
  • X6: the conformance listener holds :0 and reads back its allocated port.
  • B2: source-read-sdk contains only ModuleCapabilitiesService and retains TestContractContainsOnlyRequiredDescriptors. Its generated provenance is refreshed for the new contract. TypeScript SDK is 0.3.39.

Required toolchain and validation

Released Codefly CLI v0.1.215 is required, matching scripts/ci/install-codefly.sh; proto companion 0.0.18 and Go 1.27.3 were used. The pinned CLI successfully provisions the Accounts dependency graph with its Codefly-owned egress dependency. Generated outputs are retained in full.

  • No-change proto regeneration was clean before editing. Contract/runnable checks and both SDK reproducibility checks pass with v0.1.215.
  • Generated Connect replay tests pass against real Codefly-managed Postgres, including initial/re-consent retries, missing approval, operation conflicts, non-admin refusal, replay after delivery and replay after narrowing through both human RPCs.
  • Full Accounts race run passes every package except adapters; its only failures are the two external contract/conformance dependencies described below. Business and infra race suites pass. Accounts vet passes.
  • Source-read SDK race suite and vet pass, including the exact descriptor-closure test.
  • Frontend typecheck and all 107 TypeScript SDK tests pass.
  • All module-tools tests pass. Store migration suite passes with MIGRATION_CONSENT_REPLAY=1.
  • Naming, boundaries, migration pairing and whitespace checks pass; lockfiles and go.sum contain no x-access-token entries.

Known failures: the pinned external conformance lifecycle caller omits approval, and TestCopiedInterfaceProtosMatchTheirRecordedDigests correctly rejects the unpublished interface change. The authority contract/conformance owner must publish the coordinated update before merge. Historical non-no-reply commits are preserved under the no-rewrite rule; any resulting history-policy failure belongs to the branch owner. No deployment or host release was exercised.

lukazhupa and others added 5 commits October 20, 2026 15:39
…policy sidecar; the gap list

accounts declares it implements the four published host interfaces (authority,
composition, installation, session at 0.1.0), carries their protos under
proto/ beside its own (bindings via codefly generate proto), and serves them as
adapters over what it already does: Keys and Check over the Work Context
authority; Issue by owner shape over StartTask, StartInstallationTask,
StartChildSession and ExchangeAudience; List over ListInstallations; SignIn over
fixture Authenticate. Where no path exists the adapter refuses by name:
workload-owned Issue (gateway workload endpoint), ApplyManifest (signed
delivery), Install and GrantToTeam (target and role resolvers).

The interface protos carry no saas.policy option on purpose; the host's policy
for them is business.InterfaceMethodPolicies, read by RPCPolicies when a
descriptor carries none, with the catalog descriptions beside it. The
solution-side InstallableService is not served.

Known red, by design of this branch (HOST_INTERFACE.md): the service catalog
assumes one proto package and Connect bindings are keyed by short service name,
so cmd/service-catalog refuses the four packages and the registrations cannot be
regenerated; the drift tests fail until both are generalised. go build and the
policy tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Naming gate, tree check on this branch: 733 forbidden names, all of them the interface packages' publisher name as it appears in the copied protos and in every generated file (code/pkg/gen/<publisher>/**, frontend/code/src/gen/<publisher>/**). The commit message and this PR are clean.

This is not something to allowlist. It is a design decision for the owner, recorded in the authority repository's README: the host's own contract cannot be published under a consumer's name if the host repository is to carry it. Either the interface publisher and package move to a neutral namespace owned by the host (so the authority repository publishes codefly.dev/...), or the host never carries these protos and consumes them only as a dependency resolved at render. Until that is decided, this branch stays a draft and the gate stays red by design.

antoinetoussaint-byte and others added 3 commits October 21, 2026 20:52
The published interfaces now carry the host's names (publisher codefly.dev,
packages host.*.v1 under proto/host/), so this repository carries no consumer
name in the copied protos or in anything generated from them. Policy sidecar,
adapters, bindings and the design note follow the rename. go build and the
policy tests pass; the catalog gaps of HOST_INTERFACE.md are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…laration

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed names; two comments

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Namespace settled: the interfaces now carry the host's names (publisher codefly.dev, packages host.*.v1 under proto/host/), regenerated with the CLI on both sides. Naming gate tree check on this branch: see the next line of CI; locally it went from 733 hits to the main baseline. Gap 7 of HOST_INTERFACE.md is closed; gaps 1 to 6 stand.

antoinetoussaint-byte and others added 2 commits October 21, 2026 20:55
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s; registrations resolve by full name

The service catalog has one api_package, the service's own, and now lists the
interface_packages it implements beside it (catalog.proto field 9). The
compiler accepts a procedure of either; the validator likewise; the frontend
catalog, the gateway route aliases and the mesh gates read the API package
only, since an implemented interface is not a frontend plugin's client and has
no compatibility alias. cmd/connect-registration resolves bindings by full
service name first (two services may share a short name across packages) and
imports each implemented package's generated Go and Connect packages under
derived aliases. The authorization matrix counts services by full name.

The adapters now enforce the floors their sidecar policies declare (org
member for a person-owned Issue and for List; org admin for Install and
GrantToTeam), which the lockstep test requires. The team grant names its
organisation, as the interface now does.

The naming gate skips generated/service-catalog.json, which crossed its 512 KiB
scan cap at 40 services: every string in it is a scanned proto identifier or a
scanned Go description. Same reasoning as its descriptor skip; a tool fix,
named for review.

Catalog, registrations, authz catalog, routes, matrix, topology and frontend
catalog regenerated. go build, the business, adapters and cataloggen tests and
the naming gate pass. HOST_INTERFACE.md: gaps 1, 2 and 9 closed; 3 to 6 and 8
stand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Gaps 1 and 2 are closed on the branch: the catalog carries the implemented interface packages beside its API package, registrations resolve by full service name, and the whole generate sweep runs. Business, adapters and cataloggen tests pass; the naming gate is clean (it now skips the generated catalog, which had crossed its size cap at 40 services — a tool fix named in HOST_INTERFACE.md for review). Next: the in-process harness to run the interface conformance suite against accounts, then gaps 3 to 6.

antoinetoussaint-byte and others added 4 commits October 21, 2026 21:40
…th emits host_binding_applied itself

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…follow the published interface

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… this package already has

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…against accounts; the digest check on the copied protos

The harness boots the test store, seeds the dev-admin fixture, wires the sign-in
path as the business tests do, configures the Work Context authority against the
store, serves the Connect listener on an ephemeral port and runs the published
interface suite (github.com/codefly-dev/authority/libraries/conformance/go,
descriptor-driven, linking no generated package) over gRPC. Opt-in through
HOST_INTERFACE_CONFORMANCE=1 because it boots a store.

Green on the first run: Keys; sign-in; an unknown fixture token refused by name.
Red, recorded as the gap list in HOST_INTERFACE.md: a module must be installed
per organisation before it is an audience; the host has no effective-permissions
read for an empty ask; an unknown audience is refused PermissionDenied where the
contract says NotFound; workload Issue, ApplyManifest, Install, GrantToTeam as
before. Adapters: a fresh task id per person-owned Issue; the session returns the
access token as the bearer; List is an administrator's read over
ListOrganizationInstallations; an unknown fixture person is NotFound.

The copied interface protos carry DIGESTS.sha256 and a test that fails by file
name when either side drifts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

The rung 1 harness is on the branch (HOST_INTERFACE_CONFORMANCE=1 go test ./pkg/adapters/ -run TestAccountsPassesTheHostInterfaceConformanceSuite). First run: Keys, sign-in and the unknown-token refusal are green; the red lines are the gap list, now in HOST_INTERFACE.md with three new gaps the host exposed (a module must be installed per organisation before it is an audience; no effective-permissions read for an empty ask; refusal code for an unknown audience). The suite was changed only to state the contract more precisely, never to pass.

… composed modules are installed everywhere; a hidden refusal names its reason

Composition installs a module at every organisation (contract decision of
2026-10-22): business.ComposedModuleInstallations answers the live module
authority and the seal store for a composed module's installation, held by
every member, and wraps the Postgres store for everything else. A control-plane
membership read, WorkContextOrgMember, sits beside the other seal reads. The
harness wires it; production keeps its reads until the registry derives these
rows itself.

An uncomposed audience is refused NotFound by name before any mint, through
ModulePrincipalID against the derived registry. Check is authenticated by the
token it checks and is public like Keys. mapWorkContextError takes the context
and logs the one case the caller learns nothing, "cannot issue Work Context",
which until now had its reason recorded nowhere.

Suite: 4 of 10 green (Keys, sign-in, the whole person line, every refusal with a
reason). The six red lines are each a host change of their own and are the gap
list in HOST_INTERFACE.md, with one new gap: the host authenticates a child hop
by the owner's bearer where the contract authenticates the holder of the parent
token.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Suite against accounts: 4 of 10 green now, including the whole person line (issued, checked, refused elsewhere). What it took, each at the owning place: composed modules are installed at every organisation (business.ComposedModuleInstallations + a control-plane membership read beside the seal reads), an uncomposed audience is NotFound by name, Check is public by the token it checks, and the host's catch-all cannot issue Work Context now logs the reason it used to record nowhere. The six remaining red lines are each a host change of their own; HOST_INTERFACE.md lists them, with one new gap: the host authenticates a child hop by the owner's bearer where the contract authenticates the holder of the parent token. Regular tests of business, adapters, infra and cataloggen pass. Hand-written size is about 800 lines, over the 500 target; the one responsibility is that accounts serves the four host interfaces.

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the regular test run had three failures that my pipe hid behind a green exit code. All three were counted tripwires moved by Check becoming public (impersonation-restricted set 62 to 61 in two tests, public routes 56 to 57); the baselines are updated with the reason in their comments and the three tests pass. business and infra passed as stated.

antoinetoussaint-byte and others added 3 commits October 22, 2026 00:20
Check is authenticated by the token it checks, so it leaves the
impersonation-restricted set (62 to 61) and joins the public routes (56 to 57).
Each baseline says why in its comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…edential alone; an operation context names its binding

ModulePrefixOfWorkload names the one composed module a workload credential
authenticates, through the execution reviewer and the existing reverse lookup
from a reviewed workload to the principal declaring it; no caller types a module
name. The interface's Issue, for a workload, is the host's operation context to
an installed audience when a binding is named, and the module's own context to
this surface otherwise; an undeclared binding is NotFound by name.

The interface's Issue is authenticated by the request's shape (a person by the
bearer, a workload by its credential, a child hop by the parent token and the
actor's credential), so both interceptors let a bearer-less call of that one
method reach the adapter, which refuses a bearer-less person by name;
a bearer, when present, is verified as on every other method.

An operation context now names the binding it exercises: the seal source
resolves module-operation/<principal>/<binding> from the installed grant at the
revision of its terms, and the operation-task mint seals it. Until now a module
operation context carried no binding, which is what left every runtime hop
"stale" at the revision check.

A composed installation's scope is the declared grant itself, since the manifest
is the ceiling. Suite: 5 of 10 green (Keys, sign-in, the person line, every
refusal with a reason, the workload line).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… and the actor's credential

childHop verifies the parent as issued by this host, names the acting module
from its credential, re-reads the owner's authority as the owner (the parent
token is what proves the owner), refuses a widening hop as PermissionDenied and
derives the hop with core; the host's own child session is authorized by the
owner's bearer, which the module acting does not hold. The hop names no binding:
core keeps the owner's seal installation on a hop and requires a named binding to
be granted within it, and a module's binding is granted within its own
installation, an open decision recorded on the interface repository.

The bearer-less pass-through on the interface's Issue applies only to a call
carrying no credential at all; a forwarded identity is admitted as before. The
operation-task mint names a binding only when the authority has one.

The harness composes documents, org-directory (the page module, one workload,
one binding to documents) and model. Suite: 6 of 10 green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Suite against accounts: 6 of 10 green (Keys, sign-in, the person line, every refusal with a reason, the workload line, the child hop). New since the last comment, each at the owning place: a workload is named from its credential alone through the execution reviewer; an operation context now names its binding in the token (the seal source resolves module-operation/<principal>/<binding>), which is the fix behind runtime hops reading as stale; a child hop by the parent token and the actor's credential; the interface's Issue is authenticated by the request's shape, with the bearer-less pass-through limited to a call carrying no credential at all. One open decision on core recorded on the interface repository: a cross-module hop cannot name its binding, since core requires it within the owner's seal installation. Remaining red: the effective-permissions read, ApplyManifest on the internal listener, Install and GrantToTeam resolvers, and the fixture's installations, listed in HOST_INTERFACE.md. Regular tests of adapters, business, infra and cataloggen pass on this head. Hand-written size is about 1,020 lines.

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Handoff written: obin-ai/handbook branch context, design/host-authority-interface/handoff-2026-10-22.md. It names where each branch stands, how to run the suite on both sides, the four red lines in order (ApplyManifest behind the composition interface first, now that the interface carries the signed delivery), and the decision that went back to core. This branch is clean at 3cda3afb0.

Both sides added a resolver for a module's binding id to the seal source:
the installed-binding ids (installed-content/, installed-actor/) and the
module-operation/ id. Their prefixes are disjoint, so the seal source tries
the installed ids first, then the module-operation id, then a source
delegation. The installed read path now passes its context to the error
mapper, which logs the refusal reason. AUTHZ_MATRIX.md is regenerated.
ApplyManifestRequest carries the delivery as signed: the manifest document,
the DSSE envelope over it, and the delivering workload's credential.
Regenerated with codefly generate proto over the template's saas and host
inputs; DIGESTS.sha256 updated. The adapter behind it is the next change.
StartTask takes operation_bindings: binding ids of the audience module. The
host resolves each one the way the module's delegated exchange will
(ModuleOperationAudience: the binding as installed at the organization,
narrowed by that installation), adds its invoke scopes beside
authority_scopes, keeps each resource only if the person holds it, and
refuses by name a binding the person holds nothing under. So a consumer
names the binding and never its resource ids, which come from the
installation. Direct human-owned mints only.
antoinetoussaint-byte and others added 15 commits October 26, 2026 00:02
…to installed organisations

Every issue from one delegation carried the delegation's id as its hop id,
so the journal kept only the first issue's binding and scope set and held
every later one to it (r136 B1). A delegated hop is now recorded under the
delegation within the task its issue opened, and the readers ask for that
row and the hop's own id. The unit-test journal keeps the first row on a
conflict, as Postgres does. Journal and chain-read refusals log the store
error (r136 m3).

The read oracles answer only about organisations the caller is installed in:
CheckAuthorizationRevision and AuthorizeEvidenceRead refuse, before reading,
an organisation where the calling module has no installation, alike for one
that does not exist (r136 M3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an installation

A module refused as itself on the host interface is Unauthenticated with
the reason; a forged token stays PermissionDenied as the shared suite
requires (r136 m4). Delegate refuses a module not installed in the
grantor's organisation, by name (r136 m5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gated issues on real Postgres

The conformance test no longer waits for a switch CI never set and absorbs
the fresh-store test (r136 M2); the library is pinned at authority main
d091cc7a (core 8af4b787 with it). The harness reads the Connect server's
error and retries a port lost between probe and bind (r136 m1). New cases:
two issues from one delegation in both orders (r136 B1), and Delegate to an
uninstalled module (r136 m5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A renumbered ledger left a database at a version this source also has
under another name, and Up skipped a migration and failed dirty later
(r136 m2). The runner stamps every applied migration's name on its
version table and refuses a database whose names the ledger contradicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve actionable authority refusals, delivery digest evidence, migration recreation guidance, and held ephemeral conformance listeners. Regenerate narrowed client libraries with released CLI v0.1.215 and restore descriptor closure qualification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retain the Codefly-generated proto3 JSON field shape and operation metadata required by the pinned toolchain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Require target generation and installation revision on explicit approval,
retain an idempotent consent receipt, and refuse replay after delivery or
narrowing. Regenerate contracts and clients with Codefly v0.1.215.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ance

Pins the authority conformance library to conformance/consent-on-0.2
(codefly-dev/authority 6870db87), whose installation lifecycle case binds
Install to a reviewed ConsentApproval. The fixture's Review hook reads the
organization's catalogue offer (target id, applied generation) and the
installation listing's revision through the generated Connect clients.
DIGESTS.sha256 records the published installation.proto that now carries
ConsentApproval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…proto digest

codefly generate contracts saas-starter (CLI 0.1.215) copies
DIGESTS.sha256 into the exported accounts contracts; the previous commit
recorded the published installation.proto digest, so the copies follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page sends the reviewed target, generation and a fresh operation id with every install (7a4342a); the container tests still expected the request without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse source-operation verification and installed module actor bindings for
saas-directory. Keep organization membership and tenant scope checks in the
existing handlers, preserve bearer admission, and name verification refusals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retain revoked review state, advance revision for every fresh receipt, and
supersede pending installation work under the existing lifecycle fence.
Validate descendant scopes against the original journaled grant, follow all
host listing pages, and submit the listed revision from the install page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte marked this pull request as ready for review October 26, 2026 14:44
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 35a8aca Oct 26, 2026
24 checks passed
antoinetoussaint-byte added a commit that referenced this pull request Oct 26, 2026
…rage-through-the-service

Brings in #1217 (the published host interfaces as adapters, codefly 0.1.215).

Generated files conflicting on this branch's datasource.proto change (both
contract descriptors, the catalog, the module package's digests, the SDK's
contract, catalog, stamp and datasource_pb.ts) were taken from main and
regenerated with codefly 0.1.215 through the chain main documents; a second
run is a fixed point. The source-read SDK records the whole authority
contract's digest, so it was regenerated once with --force, as its README
says; only that digest line moves, and a run without --force is clean.

Hand-resolved:
- module.package.codefly.yaml: the authored object-storage provides entry
  main's copy drops is restored.
- objectstore client.go, client_test.go, guard_test.go: kept deleted. This
  branch replaces the host's store client with the composed service; main's
  mesh-egress admission for that client has no host dial to apply to here.
- DATABASE_AUTHORITY.md: main's tenant row plus datasource_object_manifests.
- saas-sdk 0.3.39 on main becomes 0.3.40; saas-ui's peer and dev floors, the
  frontend's exact pin, the README floor and the lockfile follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
antoinetoussaint-byte added a commit that referenced this pull request Oct 26, 2026
…ier 54

#1217 landed migrations 49 to 54, so this branch's unshipped 49 and 50 collide
with main's authority_delivery and installation_exposure_revision. They become
55 (datasource_object_manifests) and 56 (last_snapshot_items), content
unchanged, with the prose that names them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
crisfiorini-oss pushed a commit to crisfiorini-oss/module-saas-starter that referenced this pull request Oct 27, 2026
…ct-storage service; the host's store client is deleted (codefly-dev#1235)

* feat(accounts): the object-storage connector diffs the current listing against one the host retains

An object store answers only "what is there now". objectstore.FilesConnector
versions a source by the digest of its listing (key, ETag, size) under its
binding, the host retains the listing each version names in
datasource_object_manifests (migration 46, tenant-owned, newest four per
source), and Changes(from) diffs against it. It reaches the store through a
Bucket interface the object-storage service's per-call binding will implement
(codefly-dev/service-object-storage#108), and passes connectortest.RunFiles
over a scripted bucket. It is not yet what the host syncs through: the provider
stays registered non-conformant and runUploadSync keeps running.

The upload descriptor now states source-scoped readers in the catalog.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(accounts,saas-ui): a bucket source takes include and exclude key patterns, applied before any fetch

UploadDatasourceConfig gains include_patterns and exclude_patterns (bounded RE2
over the key relative to the prefix; exclude wins; empty include admits all)
and file_extensions with GitHub's semantics. AddSource and UpdateSource refuse
an invalid pattern by field (INVALID_ARGUMENT, a BadRequest violation, and a
message leading with the field path), never at sync. The store's listing
applies the filter before it counts against max_objects, so an excluded object
is never fetched, and the closing listing names only matches: narrowing the
filter through UpdateSource tombstones what it now excludes on the next sync.

The kit's object-storage connect form, with its shared collection field, moves
here from the catalog change and takes the key filter, placing a host refusal
under the field it names.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(accounts,saas-ui): the bucket key filter is the connector's Scope, not the listing client's

The owner's review of codefly-dev#1216: the match sat inside objectstore/client.go's
List, the file codefly-dev#1211 deletes, so it would have had to move again when the
object-storage service replaces the client. objectstore.Scope now holds the
host's InScope predicate (uploadKeyScope, as GitHub's SourceConfig.InScope)
and the object cap, and Window applies both to whatever listed the prefix:
an excluded key is dropped before it is counted, never fetched, and a cut
is reported. client.go returns the store's page whole; its cap moved into
Scope with the filter. UploadContentClient.List is back to main's shape.

The kit moves to 0.12.16 (saas-ui, and ui with it) and the SDK to 0.3.36,
past what v0.0.105 released; saas-ui's SDK floor rises to ^0.3.36 because
the form sends fields only that SDK carries. package-lock.json is
regenerated by npm.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(accounts): object-storage sources read through the composed object-storage service; the host's store client is deleted

The host composes service-object-storage and reads every object-storage
datasource through its per-call bucket binding (codefly-dev/service-object-storage#108,
pinned at 5708d5e): the binding names the source's endpoint, region, bucket and
prefix and carries its access key for that one call. objectstore.ServiceBucket
pages the listing (bounded) and reads each object at If-Match its listed ETag;
it asks Capabilities under the binding first and refuses a service that reports
no bound caps, since a release from before the binding would serve its own
bucket as the tenant's.

runUploadSync takes its snapshot through objectstore.FilesConnector and reads
it in batches; the source's key filter and object cap ride on SourceConfig.Scope
and are applied before the version is digested. The delivery and the closing
listing are unchanged. client.go, its tests and its egress guard are deleted.

The provider stays registered non-conformant, now for clause 8 alone: the
service's bound read takes one key per call.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(ci): the object-storage service is covered by the Codefly vendor image audit

Every topology service declares image coverage. The object-storage agent runs
its gateway and MinIO images, both pinned by the agent, and builds nothing in
this repository, so it is classified as Redis and Vault are.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(accounts): the object-storage dependency's generated client, from codefly sync service accounts

Declaring accounts' dependency on object-storage/grpc makes the go-grpc agent's
sync emit a client file for it under code/external, and the sync-drift phase
compares the tree against that output. Generated with CI's pinned codefly; the
same command on clean main changes nothing.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(accounts): object storage reads a batch in one call and is registered conformant

service-object-storage#111 at 33ba749 serves a batched bound read: GetMany takes
up to max_get_many_items keys, each at its listed ETag, and streams them back in
request order. ServiceBucket.Read now issues one GetMany per batch, refuses an
answer it cannot attribute (out of order, short of the listed size), maps
CHANGED and NOT_FOUND to ErrObjectChanged and TOO_LARGE to ErrItemTooLarge, and
refuses a service that reports no batch size as it refuses one that ignores the
binding.

The conformance suite now runs through ServiceBucket over an in-process fake of
the service's gRPC API, so clause 8 is measured on the calls the host makes:
120 objects cost one GetMany after the listing. The provider is registered
through connector.Budgeted, metered per source credential like GitHub;
GetDatasourceCatalog reports accepts_new_sources and AddSource takes a new
source with its prefix and key filter. The registration gate finds a
connector's suite in the package that implements it, through the meter.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(accounts): a vanished store never closes an object-storage sync, and a missing bucket is never an empty listing

The service at 33ba749 can answer a store-level failure inside GetMany as
NOT_FOUND for every object, ending the stream successfully. The host's
tombstone authority is the closing listing alone, sent only after a successful
listing in the same sync and the delivery of every object in it; a NOT_FOUND
read is ErrObjectChanged and fails the sync first. These tests pin both halves:
a sync whose every read is NOT_FOUND sends no listing, and a List the service
answers NOT_FOUND is the store's error, never an empty version.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(accounts): service-object-storage at eef1863, codefly-dev#111's head; the client package is unchanged

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(accounts): an object-storage source lists its literal prefix, admission needs a service that can read, and one credential spends one budget

From the review of codefly-dev#1235 at a3c4836:

- A source's prefix is a literal S3 key prefix, and the service normalizes a
  binding's prefix as a folder: "/reports/" listed "reports/", and "/" listed
  the whole bucket, so a populated source could read an empty scope and close
  with a listing that tombstones every entry. The binding now carries no
  prefix; every page filters on the literal prefix, which the service passes
  to the store unchanged, and keys travel whole. A listed key outside the
  prefix, or a page past the service's own page size, refuses the listing.
- Every call to the service has a deadline (30 s; five minutes for a batched
  read), so a store that answers and then stalls cannot hold a sync and its
  worker's lease.
- Admission needs a composed service that can read a bucket. work.go asks the
  service, unbound, whether it reads a batch (objectstore.CheckService); with
  none, or the released 0.0.18 agent, object storage is unavailable: the
  catalog says so and AddSource refuses with ErrObjectStorageUnavailable.
- An upload source's budget is keyed by its credential — a keyed digest of the
  store's origin and access key id, fixed at connect, as an API source's is —
  so sources sharing one key share one quota and one provider throttle. A sync
  a person asked for is interactive and may spend the reserved share.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style(accounts): the object-storage connector's two unchecked writes, which golangci-lint flagged on this branch

A write into a sha256 hash cannot fail, and the test fake's page-token parse
falls back to the first page; both discard the error explicitly.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(accounts): service-object-storage at a3157779, codefly-dev#111's fixed commit for the codefly-dev#1235 review

The service now fails a bound listing the store marks truncated without a
continuation token, keeps a binding's prefix literal, bounds every store
response to 5 MiB and deadlines each bound operation at 30 seconds. The
proto changes in comments only, so the client package is unchanged.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(accounts): the pinned object-storage service is held to the two invariants the host's deletions rest on; pin 358091a0

The closing listing of an object-storage sync authorizes deletions, and two
properties of it were the service's alone: that a bound listing's prefix is
used literally, and that a listing the store marks incomplete fails instead of
answering as complete. Both were wrong in the service once, and a fake of the
service cannot catch a regression in it.

These tests build cmd/service-object-storage from the module go.mod pins, run
it on an in-memory bucket of its own with a scripted S3 endpoint admitted as a
composed dependency, and read through ServiceBucket: the store must be asked
for "/reports/", "/" and "a//b/" exactly, and an IsTruncated page with no
continuation token must fail the listing, empty or not. They fail rather than
skip when the gateway cannot be built. Against the pre-fix service (33ba749)
the incomplete-listing test fails; against a service patched to strip a
leading slash, the prefix test fails.

The service pin moves to 358091a0, codefly-dev#111's head after its second review round.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style(accounts): the contract test's two unchecked closes, which golangci-lint flagged

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(accounts): a composed object-storage service that cannot serve a binding is refused at admission, not at every sync

Review r1's major 3 was that the catalog accepted object-storage sources, and
AddSource stored them, while the composed runtime (the released 0.0.18, which
knows no binding) failed every sync. The refusal now lives in the one place
object storage is wired: Service.SetObjectStorage asks the service whether it
serves a batched, bound read and refuses to wire one that does not, so the
catalog does not offer the provider and AddSource refuses it by name.

The test drives that wiring with a service answering as 0.0.18 does and
asserts the refusal at admission — catalog, AddSource, nothing stored or
audited — and that a service with the binding is admitted. Wiring whatever is
composed turns it red.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(accounts): object storage runs on service-object-storage v0.0.19, in both pins, and acts on the reason each refusal names

The composition selected agent 0.0.18, which has no binding, so on the
declared runtime the host refused object storage at admission. v0.0.19 is the
release of codefly-dev#111 (per-call binding, batched read, literal prefix, complete or
failing listings, bounded responses, per-operation deadlines); the agent
version moves to it with `codefly update service`, which rewrote only the
version token, and accounts' go.mod requires the same release.

v0.0.19 also names every refusal's reason in a google.rpc.ErrorInfo, because
one code covers refusals that call for opposite actions. The host read
RESOURCE_EXHAUSTED as a throttle — so a listing too long, or a response too
large, blocked the credential for every source sharing it and retried the same
listing forever — and PERMISSION_DENIED as a refused credential even when the
guard refused the endpoint. storeError now acts on the reason: a too-long
listing or response is ErrListingIncomplete, an object too large is
ErrItemTooLarge, the guard's refusal is ErrDestinationRefused, and only
STORE_THROTTLED (or a throttle with no reason) is a rate limit, with the
store's RetryInfo. A contract test holds the released gateway to it: a page of
1,001 entries must read as an incomplete listing, never a rate limit; with the
code-only mapping it reads as one.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(accounts): a sync never empties an object-storage source that has files, each refusal gets the action its reason names, and one credential is one budget for every source

From review r2 of codefly-dev#1235 at 32eb6c8:

- g2 (owner decision): a closing snapshot with no files tombstones every entry
  mirrored from a source, and a service that wrongly answers "empty" — a
  rewritten scope, a discarded truncation — was proved twice through the real
  client and the production sync path. The host owns deletion authority, so
  the rule lives here, whatever any service version answers: a listing that
  comes back empty for a source whose last closing snapshot named objects is
  refused (ErrUploadEmptyListingRefused, job code
  datasource.upload_empty_listing_refused), closes nothing, and leaves the
  collection as it is. "Has files" is the count the host records when it sends
  a closing snapshot (datasource_sources.last_snapshot_items, migration 47);
  a source synced before it is unknown and treated as having files. The
  retained manifests cannot serve: every listing writes one, so a run of
  refused syncs would push the last non-empty one out.
- g3: typed reasons reached distinct Go errors but one generic retry. Now a
  throttle is retried no earlier than the store's reset; an object over the
  service's cap is skipped and the sync goes on, surfacing it without a
  closing snapshot; a guard refusal or a rejected access key parks the source
  as degraded with a fixed reason and fails the job terminally; and
  LISTING_INCOMPLETE is the incomplete-listing disposition.
- g1: the budget key was written at connect and only with the host's link
  key, so existing and keyless sources kept a budget each. It is now derived
  from the source's endpoint origin and access key id on every use — plain
  configuration on the same row, no secret — so every source on one key
  shares one quota and one provider block, with no backfill.
- g4: the adapter comment no longer says the service normalizes a binding's
  prefix.

Each behaviour has the test that would have caught it, driven through the
real client and the job handler where the review's probes were; each was
mutated red.

Refs codefly-dev#1211

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(accounts): a refused object-storage source parks through the one park step, and a park that cannot be written retries

Round two's refusal parking re-implemented the GitHub public-source park's
control-plane write, and dropped its rule: when the degrade write fails, the
job retries instead of going terminal, so a source never stops with nothing on
it to say why. TestControlPlaneCallSites refused the copy as a new call site.

parkDatasourceSource now owns the write and the already-parked check; the
GitHub path and the object-storage path both call it, so the call-site list
changes by a rename only. A refused upload whose park fails returns the park
failure in place of the refusal and retries; TestUploadRefusalWhoseParkFailsRetries
holds it, mutated red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(frontend): kit 0.13.3 and saas-sdk 0.3.37, bumped from main's numbers

Main moved @codefly-dev/ui and @codefly-dev/saas-ui to 0.13.2 and released
saas-sdk 0.3.36 without this branch's key-pattern fields, so both carry this
branch's changes under versions the registry already serves. The kit moves
together to 0.13.3; saas-sdk to 0.3.37, which saas-ui's peer floor and the
frontend's exact pin now name, since 0.3.37 is the first release carrying an
upload source's include and exclude patterns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(store): renumber the object-storage migrations above main's frontier 48

codefly-dev#1254 landed migration 48, so this branch's unshipped 46 and 47 now sit below
the reference frontier and the migration reference gate refuses them. They
become 49 (datasource_object_manifests) and 50 (last_snapshot_items), content
unchanged, with the prose that names them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(store): renumber the object-storage migrations above main's frontier 54

codefly-dev#1217 landed migrations 49 to 54, so this branch's unshipped 49 and 50 collide
with main's authority_delivery and installation_exposure_revision. They become
55 (datasource_object_manifests) and 56 (last_snapshot_items), content
unchanged, with the prose that names them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(store): renumber the object-storage migrations above main's frontier 55

Main's hop admission keys took 55, colliding with this branch's 55. The
two unshipped migrations become 56 (datasource_object_manifests) and 57
(last_snapshot_items), content unchanged, with the prose that names them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(store): renumber the object-storage migrations above main's frontier 58

Main's authority core took 56 to 58, colliding with this branch's 56 and
57. The two unshipped migrations become 59 (datasource_object_manifests)
and 60 (last_snapshot_items), content unchanged, with the prose that
names them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte removed the working Claimed by a lazybox agent; cleared when the session ends. label Nov 2, 2026
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.

2 participants