Repository navigation
Merge NetworkofNetwork into development [#72] - #109
Merged
Merged
Conversation
…86) * feat: add federation fields to the Participant registry schema [#71] Federation needs the registry to record peer networks: our own entry builds the Network Descriptor (#74) and admitting a peer writes one (#75). - role accepts a list as well as a single value, so one participant can serve several layers; a single string stays valid for existing rows - federationRole (self/source/requester/both): our admission decision, never read from a peer's Descriptor; added to indexFields - revision: the Descriptor revision accepted; only increases - schemaPacks: the peer's declared schema pack URLs, https only - projectionTtl: seconds we may retain a peer's data; 0 means live query All fields are optional and no existing rule is tightened, so stored records keep their osid and stay editable. * fix: let a node's keys be edited by declaring the registry's system fields [#71] On a PUT, Sunbird RC merges osid and the os* audit fields into each stored key before re-validating. PublicKey did not declare them, so every update of a node failed, including the revision bump that refreshes an admitted peer. Declare those five fields and keep additionalProperties false, so typos and unknown key fields are still refused. osOwner is left out: the registry never sets it on a key. * fix: drop self from federationRole, identify our own entry from config [#71] A network recognises its own entry from its configuration, so a stored self value is redundant. federationRole now only records our decision about a peer: source, requester or both. * feat: make federationRole a per-direction admission record [#71] federationRole is now an object with optional source and requester halves instead of a source/requester/both string; both halves present replaces "both". Each half carries its own schemaPacks (moved from the top level), descriptorRevision, admittedAt and updatedAt, since the design doc stores admission independently for each direction. federationRole drops out of indexFields, as it is no longer a scalar. * docs: shorten federation field descriptions [#71] * fix: require at least one schema pack per federation direction [#71] An agreement that names no packs covers nothing, yet the registry accepted one, so the mistake only showed up later as refused calls. Both directions now need a schemaPacks list with at least one entry. * fix: address review on federation fields [#71] * fix: sync the registry chart schema [#71] * docs: add ADR-0002 for the federation admission record [#71] * docs: record how the registry validates updates in ADR-0002 [#71] * fix: address second review on federation fields [#71] * fix: validate the registry schema in CI and verify the role migration [#71] The registry-schema workflow only compared the two copies of Participant.json, so two identically wrong files passed and nothing in the repo ever ran a payload through the schema. scripts/check-registry-schema.py now does both: the copies still have to match, and sixteen cases in scripts/registry-schema-cases.json assert what the schema accepts and refuses -- an empty federationRole, projectionTtl on source but not requester, a suspended direction without suspendedAt, a typo in a member name, role as a bare string. The role migration in the chart CHANGELOG was written but never run. It has now been rehearsed end to end against sunbird-rc-core v2.0.0, the image the quick-start stack runs: a 0.4.1 row reads but refuses its next update, the script rewrites it without losing its keys, and the rewritten row can then be admitted as a federation peer. The CHANGELOG records the error you see before the rewrite and the check to run after it. projectionTtl is accepted both at the entry's root and under source. The transition was described in ADR-0002 and owned by nobody; #102 owns it now, and the ADR and the schema point at it. * test: cover an ordinary node in the registry schema cases [#71] The cases covered peers and upstreams but not the row the registry holds most of: one of our own consumers or providers, a node with a baseUrl and no federationRole. Add it, plus the matching refusal of a node reached over plain http, which nothing exercised either. * docs: correct what the registry validates on an update [#71] ADR-0002 said Sunbird re-validates the whole merged document on a PUT. It does not: it validates the request body, then merges. Checked on a live sunbird-rc-core v2.0.0 with the same row twice -- a body of just federationRole was refused by a rule reading type and role, and the same body carrying type and role passed. The migration note followed from the same mistake. An update that does not mention role succeeds against a row holding the 0.4.1 string and leaves the string in place, so the first write after the upgrade does not force the rewrite. Say so, and say the rewrite has to be run deliberately. --------- Co-authored-by: Manjunath Davanam <manjunathdavanam@gmail.com>
…nAgriNet/network-adapter#74] (#82) * fix: seed schema packs from openagrinet.github.io [OpenAgriNet/network-adapter#74] * feat: seed one registry entry for the network [OpenAgriNet/network-adapter#74] * feat: host the Network Descriptor, JWKS and schema packs [OpenAgriNet/network-adapter#74] * fix: keep the descriptor revision on the registry entry, publish every key [OpenAgriNet/network-adapter#74] * fix: address review on descriptor hosting [OpenAgriNet/network-adapter#74] * docs: add ADR-0003 and ADR-0004 for the network identity and schema-pack list [OpenAgriNet/network-adapter#74]
* feat: admit, refresh and suspend a peer network [OpenAgriNet/network-adapter#75] * fix: suspend a direction by status in one write [OpenAgriNet/network-adapter#75] * fix: file a peer's discovery URL as discoveryUrl, not baseUrl [OpenAgriNet/network-adapter#75] * fix: address review on peer admission [OpenAgriNet/network-adapter#75]
Federation needs the registry to record peer networks: our own entry builds the Network Descriptor (#74) and admitting a peer writes one (#75). - role accepts a list as well as a single value, so one participant can serve several layers; a single string stays valid for existing rows - federationRole (self/source/requester/both): our admission decision, never read from a peer's Descriptor; added to indexFields - revision: the Descriptor revision accepted; only increases - schemaPacks: the peer's declared schema pack URLs, https only - projectionTtl: seconds we may retain a peer's data; 0 means live query All fields are optional and no existing rule is tightened, so stored records keep their osid and stay editable.
…ields [#71] On a PUT, Sunbird RC merges osid and the os* audit fields into each stored key before re-validating. PublicKey did not declare them, so every update of a node failed, including the revision bump that refreshes an admitted peer. Declare those five fields and keep additionalProperties false, so typos and unknown key fields are still refused. osOwner is left out: the registry never sets it on a key.
#71] A network recognises its own entry from its configuration, so a stored self value is redundant. federationRole now only records our decision about a peer: source, requester or both.
federationRole is now an object with optional source and requester halves instead of a source/requester/both string; both halves present replaces "both". Each half carries its own schemaPacks (moved from the top level), descriptorRevision, admittedAt and updatedAt, since the design doc stores admission independently for each direction. federationRole drops out of indexFields, as it is no longer a scalar.
An agreement that names no packs covers nothing, yet the registry accepted one, so the mistake only showed up later as refused calls. Both directions now need a schemaPacks list with at least one entry.
The federated half of the catalog crawler, in both deployment modes. Off by default, so a stack that admits no network behaves as it did. No dbDsn: a database is only needed by work that keeps a record between passes, and crawling other networks keeps none.
…ss [#72] The crawler runs on the network adapter, and that adapter serves /publish itself in both modes -- as its own mount in single, as an action on / in multi, whose routing already lists publish. Sending a multi-mode crawl to the provider adapter made it travel back out and in again for nothing.
…we answer [#72] quick-start ran entirely unsigned: no mount signed outbound, none verified inbound. With admission now writing agreements into the registry, nothing read them -- the agreement check hangs off a verified caller, and there was none. The consumer and publish mounts sign on the way out. The network and provider mounts verify, then run checkCrossNetworkRequester, which refuses a caller we never agreed to answer and, on a discover, one asking outside the agreed packs. Every mount already configured the signer, signValidator and keyManager it needed; only the step lists were missing. Needs an adapter image carrying checkCrossNetworkRequester (network-adapter#78).
A verifier filters the registry by participantId and then walks that record's keys matching on THEIR osid. Naming the entry's osid instead resolved no key, so every signed request failed as AUT_SUBSCRIBER_NOT_FOUND -- invisible until quick-start started signing, because nothing had looked a key up before. So setup.py renders the key's osid, publish signs the descriptor with the osid of the key it actually used, and admission files a peer's key under the kid rather than putting the kid on the entry around it. A rotated key is a new kid, and refresh now follows it. The entry-osid immutability guard went with it: the entry osid is no longer what a signature names. The duplicate-osid guard went too -- a verifier filters by participantId first, so two participants holding the same key osid cannot collide.
… token [#72] The registry is reached only from inside the stack -- bound to loopback, never on the proxy -- so a bearer token bought nothing and cost a container, a realm import, eleven environment variables and two published ports. It now runs with authentication_enabled=false and no identity provider. Both scripts call it plainly: setup.py loses token() and the bearer threaded through every write, oanfed loses the Authorization header. Checked that the registry still boots with the SSO settings absent -- the warning about the security filter chain applied only with authentication on -- and that an unauthenticated create returns SUCCESSFUL. The README says plainly what now keeps the registry private: the loopback binding and its absence from oan-edge, and nothing else. A proxy host pointed at it would publish a writable registry outright.
Everything a second stack would collide over is now a variable: the compose project, thirteen container names and two docker networks. STACK_NAME and STACK_PREFIX default to what they have always been, so one network is unchanged. Ports were already variables. stack.sh inspected two containers by bare name; it asks container() now. Step 1 waits for the registry's own healthcheck. docker compose up -d waits for a service's dependencies and never for the service itself, and the registry used to depend on Keycloak being healthy -- minutes, by which time it was ready too. Removing Keycloak removed that accidental wait, and setup.py started racing the registry's first boot and dying on a connection reset. Appendix O walks two networks end to end, Appendix B says what differs between a laptop and anywhere reachable, and two failures that cost real time are in Appendix F: SunbirdRC cannot load its schemas twice, and a transient pack fetch reads as a schema error. Tested with two stacks side by side, each answering a discover with its own catalog, sharing no container, project, network or volume.
…make [#72] oanfed.py grew admit, refresh and suspend, and nothing wrapped them -- an operator had to assemble the flags by hand, and the README never said how. Four targets now fill them from .env and name only what is actually a decision: the peer, the direction, and the packs the agreement covers. RESOLVE and CA_FILE are there for two networks on one machine. A missing or wrong value says which one and shows the form, rather than reaching argparse.
make setup did everything in one pass: keys, five participants, four bindings and every adapter config. A wrong value in .env was therefore a wrong row in a registry that creates only, and the blast radius was the whole network. It splits into make keys, make register-network, make register-provider PROVIDER=, and make configure-adapter. make up now stops at the containers -- it writes nothing and starts no adapter, because an adapter config is a bind-mounted file and Docker answers a missing one by creating a directory. configure-adapter is what starts them, so a rendered config is never left unpicked-up. configure-adapter renders from the registry's own ProviderSchema rows rather than from .env, so the binding key a provider step is configured with and the one it is called with are the same string. A provider with no row contributes no block. That took the four capability blocks out of the two adapter templates and into config/adapters/providers/<name>.yaml.tmpl, one per provider, holding the part the registry deliberately does not: how to authenticate to it. It also asks for the deployment mode and whether to crawl other networks, and writes both answers back to .env -- settings that were only ever an environment variable prefixed to the command. Neither is asked when it was given as an environment variable, or when there is no terminal. Three template comments said signing was off, which stopped being true when quick-start started signing. MODE= never worked despite the help text saying so. configure-adapter had no wait for the registry, so a stack still starting gave a traceback rather than the message that names the cause.
SchemaRegistry was the only entity whose root set additionalProperties false. The registry merges its own osCreatedAt, osUpdatedAt, osCreatedBy and osUpdatedBy into a record before validating it, so every PUT was rejected over fields the registry itself had added -- a 500 no caller could avoid. Sending a body with none of them in it failed the same way. Measured against a live registry: Participant and ProviderSchema both update cleanly and neither sets it. A capability's name, version and pack URL were permanent for no stated reason.
admit filed another network's key under the kid its descriptor signs with, and then refused its own write: "the registry did not store net-b.oan.local's key as 1-a74e3ca6-...". Sunbird prepends "1-" to any osid supplied on create, including a nested one, so passing the kid whole stored 1-1-a74e3ca6-... and no signature naming the kid would have resolved to a key. The entry's osid already had this strip; the key's did not, because the kid moved from the entry to the key without it. Found on two networks admitting each other, not in the tests -- the registry double kept a supplied nested osid verbatim, which is not what the registry does. It now prepends, as measured. Both directions now store a key osid equal to the other network's own kid.
The descriptor advertises NETWORK_BASE_URL + /discover. In single mode /discover matched the `/` mount -- the consumer tier, whose steps are validateSchema, addRoute, sign. No validateSign, no checkCrossNetworkRequester. The tier that checks both sat at /_internal/network/, a path the descriptor never named and the edge explicitly denied. So another network's call arrived at the one door that checks nothing, while the door that checks was sealed off. Measured on two networks admitting each other: an unsigned POST to /discover was 500 with no authentication attempted, the same payload to /_internal/network/discover was 401 AUT_SIGNATURE_MISSING. The three tiers are now /consumer/, /network/ and /provider/ in BOTH deployment modes, as networkofnetwork already had them, and NETWORK_BASE_URL ends in /network either way. The edge denies /consumer and /crawl in place of /_internal; /network and /provider reach the adapter normally and both check the signature and the agreement. The crawler's push target is now /network/publish, one address in both modes rather than an action on / in multi and a mount in single. Verified on a clean stack, single and multi: unsigned to /network/discover and /provider/select is 401, /discover is 404, and all three containers start with no restarts.
ADR-0002 records why the tiers moved to /consumer/, /network/ and /provider/, and how the paths callers see stay what ADR-0001 promised: the edge rewrites /discover onto /network/ and the transaction actions onto /provider/, so the mount names are ours and nobody else carries them. NETWORK_BASE_URL goes back to no path. The chart banned snippet, rewrite and regex annotations outright, because one could reach /_internal or /publish. That ban also forbids the mapping this needs. It is replaced by a check on the rewrite TARGET, refusing only /publish and /consumer -- the two mounts that take unsigned calls. Anything else is the deployment's business, so a mount added later needs no chart release. No ingress controller is named: Kong, ingress-nginx and ALB spell a rewrite differently and the annotations come from values. quick-start does the same job in nginx, but on the individual proxy host rather than server_proxy.conf: that file is included in every server block and the upstream differs per host -- in multi mode /select belongs to provider-adapter and /discover to network-adapter, which no shared rule can express. The shared file keeps what is true everywhere, denying every mount by name. Appendix B carries the per-host rule. Verified: the descriptor advertises /discover again, the adapter answers 401 on /network/discover and /provider/select, 404 on /discover, and the chart accepts a rewrite onto /network/ while refusing one onto /publish.
Nothing in the charts served .well-known, so a cluster could neither tell another network who it was nor read anyone else's answer. Federation existed only in quick-start. A Job signs the descriptor, JWKS and schema packs from this network's registry entry and writes them into a ConfigMap; a small server mounts that ConfigMap and answers for exactly those three paths. Kong routes to it. The Job mounts the SAME Secret the adapter reads. oanfed refuses to sign with a key the registry does not publish for this network, so sharing it is what guarantees the descriptor names the key that signs traffic -- a mismatch there surfaces much later, as another network failing to verify anything we send. The private key exists only inside the Job; what reaches the ConfigMap is public. Each run publishes the next revision, read from what the ConfigMap already holds. oanfed refuses a revision that does not increase on the one served, and so does every network holding the old one, so the number cannot come from a value in git that a re-run would repeat. Admissions are values, one Job each, so who we agreed to talk to is reviewable rather than a command somebody ran once. They need no key: admission reads the other network's public documents and writes a row in our registry. oanfed's open_registry imported quick-start's setup.py and read .env, which a Job has neither of. It now takes REGISTRY_URL and BECKN_NETWORK_ID from the environment when both are set and falls back to setup.py otherwise, so the same file runs in both places. The server holds nothing secret and serves nothing else: 404 for any other path, and the Ingress template refuses one outside /.well-known/.
] The Advanced-tab rule covered discover, select, init, confirm and status. The Beckn v2 spec has nine request paths a network sends us -- track, cancel, update, rate and support as well -- and publish, which goes to the network tier like discover does. The on_* callbacks are deliberately absent. A callback returns to whoever SENT the request, so it belongs to the consumer tier, which takes unsigned calls and is never public. Only select answers today: ensure_binding writes one action per ProviderSchema row and that action is select. The other eight are routed anyway, because the path belongs to this network either way -- a 404 from the mount that would serve it says "not bound yet", where no route at all says "wrong address".
make host-descriptor writes .well-known/ per deployment -- a descriptor, a public key and a pack list that belong to one network, not to the repo.
1390 lines, of which 1008 were fifteen appendices. Nobody getting started reads that, and the detail they held sits next to the thing it describes anyway: POCRA's rate limit and the shape budget in .env.example, the startup order in stack.sh, what the registry holds in setup.py. 93 lines now: what you need, the commands in order, how to check it worked, talking to another network, and the five things that actually break. The old text is in git history. Three files pointed at appendix letters that no longer exist.
The observability docs name every container whose logs land in ClickHouse. Replacing Keycloak with the registry left five sentences starting lowercase.
Its Appendix H pointer is about telemetry, not about this work.
They name Keycloak as a log source. That it is gone is a telemetry change, not this one.
Suspending the last direction left the participant active with its keys, so it passed signature validation and the admission step exactly as before. An inactive participant is refused before any federation rule is read.
The crawler pushed to /network/publish, whose first step is validateSign, carrying no Authorization header at all. Every crawled catalog was refused. /publish signs and forwards it on, and lives on the provider adapter in multi mode.
The rule that makes another network's /discover reach the adapter lived only in a textarea in NPM's database. It follows the npm-advanced paste-job pattern now, and the README says to paste it.
All three writes printed the status and carried on, so make reported success with the registry half filled and the first symptom was a crawl collecting nothing.
…d network's address [#72] Four references survived the README cut and point at nothing. The second-network list also left out NETWORK_BASE_URL, which is the address the descriptor publishes.
EXPLORE.md listed sunbird-registry-keycloak as a live service, including a query filtered on it, after the registry stopped needing it.
A direction is dropped by clearing all of them and writing back what stays, because the registry merges. A failure in that window left the network with no admission and surfaced as the write's own error.
Four docstrings and the script header argued for decisions instead of saying what the code does. One had lost its subject and verb to an edit nobody noticed, which is the case for moving them.
our_schema_packs took a whole argparse namespace to read one field. The record peer_record returns had two names two lines apart, and file_peer's read-back is its own step.
… changed [#72] The rebase pulled in non-conflicting hunks from upstream -- discoveryUrl on Participant, a mkdir in stack.sh, the registry chart. This branch's behaviour is the reviewed one, so its versions stand.
The record, the cases and ADR-0002 described a schema this branch does not carry. They say what the code does now, including that suspend writes a federationRole the schema refuses.
release-0.2.0 is where the networkId one-or-many and discover pagination live; the beckn tag predates both.
configure-adapter demanded every provider's token URL, so a deployment serving one capability was refused over a credential it would never use. token_url reads a value only when that provider is being served. Also fixes a NameError in registered_providers: an earlier rename left the loop variable as `p` while the comprehension read `provider`.
Federation locally needed TLS that nothing here provided: the registry pins baseUrl to ^https://, oanfed refuses to admit a network whose discovery URL is not https, and the crawler verifies with Go's default trust store and has no way to skip it. So a local run could not federate at all. - network-certs.sh: one CA outside the repo, shared by every network on the machine, and a certificate per network. Issued by `make up`, re-issued when BECKN_NETWORK_ID changes. - config/reverse-proxy/federation/: the front door, answering on the networkId. include.conf is mounted where NPM already includes it, so this needs no proxy host and no certificate in the admin UI. The rendered block lives beside it, out of the directory NPM chowns to root. - docker-compose.yml: the shared oan-federation bridge, NPM aliased to the networkId on it, and SSL_CERT_FILE on the network adapter alone. - stack.sh: `certs`, the prerequisites in preflight, rendering the front door during host-descriptor, and recording where each network answers so admit and verify need no address. FEDERATION.md is the whole run, start to finish. README no longer tells you to paste a proxy-host config: the front door serves /discover itself. .env.example also corrects every mapping URL, which pinned a tag that was never pushed and made every select fail, and pins the current image tags.
development registers PM-KISAN inline in the adapter templates, seeded in bulk. This branch moved every provider out into its own fragment, registered one at a time, so the rebase had nowhere to put it and would have dropped it. PM-KISAN does not fit the one-binding shape the other four use: two capabilities on two hosts, each answering several actions, and the OTP taken for a SchemeApplication is what a Grievance then presents. So: - providers/pmkisan.yaml.tmpl is one step carrying both binding keys, because they refer to each other. - provider_steps groups rows by provider, so a provider serving two capabilities renders one block rather than two. One binding each is unchanged. - ensure_actions_binding writes a binding that answers more than select, and takes a baseUrl for a capability living on another host. - PROVIDERS carries pmkisan's capabilities, paths and mappings. No capability schema rows: neither pack is published on network-specs yet. `make register-provider PROVIDER=pmkisan` is how it is registered now, and it refuses until both hosts are filled in.
A newcomer's first question is which provider to register and what it will demand. The README now answers both in a table, names pocra as the one that needs nothing, and warns about a mapping URL pinned to a tag that does not exist -- which breaks every select while naming something else. FEDERATION.md points at that table rather than repeating it.
manjudr
approved these changes
Oct 30, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Merges the federation work (#82, #86, #90, #97) into
development:Participant(chartregistry0.6.0)quick-start/FEDERATION.md)Breaking
roleinParticipantis now an array. Rewrite existing rows after upgrading; steps are incharts/registry/CHANGELOG.md..envfrom.env.example.Testing
test_oanfed.py: 40 tests passcheck-registry-schema.py: all 18 cases passCloses #72