Skip to content

Feat/91 agrifacilities plugin - #15

Closed
kelvinprabhu wants to merge 16 commits into
feat/41-oan-adapter-pluginsfrom
feat/91-agrifacilities-plugin
Closed

Feat/91 agrifacilities plugin#15
kelvinprabhu wants to merge 16 commits into
feat/41-oan-adapter-pluginsfrom
feat/91-agrifacilities-plugin

Conversation

@kelvinprabhu

Copy link
Copy Markdown
Collaborator

note : its branch on top of the feat/9-mandi-plugin will have contents of both

Serves openagrinet:AgricultureFacility from POCRA's aggregator API, as a
provider step on the existing internal/upstream machinery.

One capability answers all four governed facility types — Krishi Vigyan Kendra,
Custom Hiring Centre, Warehouse, Soil Testing Facility. Which one a request
wants is a value in its payload, not a binding key, so there is one registry row
and one mapping file rather than four of each.

Closes beckn#91.

What is here

pkg/plugin/implementation/agrifacility/agrifacility.go       43 ln
pkg/plugin/implementation/agrifacility/prerequisites.go      17 ln   empty map
pkg/plugin/implementation/agrifacility/cmd/plugin.go        106 ln
pkg/plugin/implementation/agrifacility/README.md            118 ln
config/mappings/pocra/agriculture-facility.select.yaml      353 ln   the real work
config/oan-provider-adapter.yaml                            +10
install/build-plugins.sh                                     +1
.gitignore                                                  +13

authScheme: none — POCRA's search takes no credential, which is also why its
baseUrl can be published as it stands.

Schema pack conformance

Follows openagrinet:AgricultureFacility v0.1 Direct mode, from
OpenAgriNet/network-specs@schema-packs-v0.1.

Two of the pack's own mapping rules shaped the answer directly:

An adapter must not copy a request coordinate into location unless the
Provider confirms that the returned coordinate belongs to the facility.

POCRA returns no verified per-facility coordinate — the single gps in its
answer is a fixed stub unrelated to the point asked for — so the answer carries
address and no location. Direct mode permits that.

Query-relative distance, ranking, and price are not intrinsic facility
attributes. Distance belongs in result metadata.

Distance is parsed, used to order the resources nearest-first, and then dropped.
The ranking POCRA computed survives; nothing is invented.

Also absent, and deliberately: services, capacity for non-warehouses,
website, lastUpdatedAt. POCRA supplies none of them. Deriving services from
the facility type, or lastUpdatedAt from the time of the fetch, would assert
something nobody verified. contact.webUrl is POCRA's own site repeated on
every item, so it goes to source.sourceUri rather than website.

"Unknown", "N/A", "000000" and "-" are treated as absent, so a field is
omitted rather than published as a placeholder.

Adding a facility type

Three lines in the mapping — the forward table, the inverse table, and the
precondition's list of accepted values. No registry row, no config change, no
rebuild, live on the next mapping cache expiry.

Governed type POCRA code
CustomHiringCentre chc
KrishiVigyanKendra kvk
Warehouse warehouse
SoilTestingFacility soil_lab

That table is the only place POCRA's private vocabulary appears anywhere.

Pocra is tightly coupled with the Maharastra boundaries

Testing

mappings_test.go runs the shipped mapping file through the real
jsonmapper and the real step against a fake POCRA. It reads from
config/mappings/pocra/ rather than an inline copy, so it fails when what is
deployed fails.

live_test.go runs against the real API. It routes through a recording proxy so
one call answers two questions — what POCRA returned, and what the mapping made
of it — because 0 facilities is otherwise ambiguous between "POCRA had nothing"
and "the mapping dropped everything", and only one of those is a bug. It skips
rather than passes when the upstream had nothing to say, and the leak test skips
rather than passes when POCRA did not misbehave that run.

@kelvinprabhu
kelvinprabhu force-pushed the feat/91-agrifacilities-plugin branch from 0d30731 to 940255f Compare September 7, 2026 12:24
@kelvinprabhu
kelvinprabhu changed the base branch from feat/41-oan-adapter-plugins to feat/16-capability-schema-conformance September 8, 2026 06:56
@kelvinprabhu
kelvinprabhu force-pushed the feat/91-agrifacilities-plugin branch from 8915a56 to e0e6d15 Compare September 8, 2026 07:07
Comment thread config/mappings/pocra/agriculture-facility.select.yaml

@ameersohel45 ameersohel45 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the AgricultureFacility plugin and the POCRA mapping.

The mapping is careful work — the field-level reasoning is documented better than most production code, $clean scrubbing POCRA's "Unknown"/"N/A"/"000000" placeholders is right, refusing to synthesise a location from the search origin is right, and the PROVISIONAL marker on the unconfirmed query convention is honest. Fetching the pack from schema-packs-v0.1 instead of vendoring it is a good change, and I verified both pack URLs return 200. The plugin itself is 43 lines with an empty prerequisites map — exactly the shape the architecture promises for provider #3.

Two blocking findings below, both reproduced against the shipped mapping through the real mapper, and both failing the entire response rather than dropping one facility. One should-fix on the schema cache. The rest are minor.

Comment thread config/mappings/pocra/agriculture-facility.select.yaml Outdated
Comment thread config/mappings/pocra/agriculture-facility.select.yaml Outdated
Comment thread pkg/plugin/implementation/AgricultureFacility/schemacache_test.go
Comment thread config/mappings/pocra/agriculture-facility.select.yaml Outdated
Comment thread pkg/plugin/implementation/AgricultureFacility/schemacache_test.go Outdated
Comment thread pkg/plugin/implementation/AgricultureFacility/prerequisites.go Outdated
@ameersohel45
ameersohel45 force-pushed the feat/16-capability-schema-conformance branch 2 times, most recently from d6033a8 to 6f3ab95 Compare September 8, 2026 08:06
@manjudr

manjudr commented Sep 8, 2026

Copy link
Copy Markdown
Member

Code review

Found 3 issues:

  1. A provider step's answer gets signed before it is validated, so an invalid answer can be NACK'd with a signature computed over the wrong body. ackSignerStep.RunOnResponse (no-route path) signs ctx.ResponseBody verbatim and writes the Signature header straight onto the live http.ResponseWriter header map, before sendResponse calls verifyEnvelope a few lines later. If a provider step's answer isn't a valid JSON object (e.g. a mapping bug that emits a bare scalar like 28.5), sendResponse correctly switches to sendNack — but sendNack/nack() never clears or recomputes Signature, so the NACK actually sent to the caller carries a signature computed over the discarded scalar, not over the NACK body. This is the exact failure mode the neighboring empty-body branch's own comment describes avoiding ("ackSigner signs the body it expects to be written; NACKing later would ship a signature over the [wrong] body") — but that fix only covers the empty-body case, not the invalid-non-empty-body case. No existing test covers this: TestSendResponseNacksAScalarInsteadOfSigningIt calls sendResponse directly and never runs ackSignerStep.RunOnResponse first, so it can't catch a stale Signature header.

// No routing — ONIX writes the ACK directly. Run response steps here
// with resp=nil (publisher path semantics).
for _, step := range h.responseSteps {
if err = step.RunOnResponse(stepCtx, nil); err != nil {
log.Errorf(stepCtx, err, "%T.RunOnResponse():%v", step, err)
// ackSignerStep itself failed — sign the NACK body if
// a different signing mechanism is available, or send unsigned.
h.signNackResponse(stepCtx, err)
responseBody = sendNack(stepCtx, wrapped, err)
return
}
}
responseBody = sendResponse(stepCtx, wrapped)
return

// Publisher / no-route path: ONIX writes the ACK — build the deterministic
// body that sendAck will write so the digest matches.
// A step that answered supplies the body; otherwise rebuild the deterministic
// ACK. Either way this signs exactly what sendResponse will write -- signing
// the ACK while sending an answer would put a valid signature over the wrong
// bytes.
ackBody := ctx.ResponseBody
if len(ackBody) == 0 {
built, err := buildAckBody(ctx.ProtocolVersion, ctx.MessageID)
if err != nil {
return fmt.Errorf("ackSigner: failed to build ack body: %w", err)
}
ackBody = built
}
// signBodyAndSetHeader writes to ctx.RespHeader which IS the http.ResponseWriter
// header map — the Signature header will be flushed when WriteHeader is called.
return a.signBodyAndSetHeader(ctx, ackBody)

  1. config/local-simple.yaml's cache.config.addr is changed from redis:6379 to localhost:6379 in all four module blocks, while the sibling registry.config.url stays at the container hostname http://registry:3030/subscribers. This file is loaded by install/docker-compose-adapter.yml and install/docker-compose-adapter-beckn-one.yml (README's "Automated Setup (Recommended)"), where redis and onix-adapter run as separate containers on the shared beckn_networklocalhost inside the adapter container will not reach the redis container. redis:6379 was set deliberately for this reason by an earlier commit (979763b). The cache config reads addr directly from this YAML (not from the REDIS_ADDR env var docker-compose also sets), so this isn't a dead value — it breaks Redis connectivity for anyone following the recommended setup. This file also isn't touched by anything else in this PR (the new provider plugins are wired entirely through config/provider-adapter.yaml), so it reads as an unrelated local-dev change that leaked in.

cache:
id: cache
config:
addr: localhost:6379
schemaValidator:
id: schemavalidator

  1. upstream.go's redactString only redacts credentials for authScheme: query; for basic and header schemes it returns the logged text unmodified. The function's own comment says redaction exists because "a provider that rejects a request often quotes it back, credential and all" — but that risk isn't scheme-specific, and this PR ships WeatherObservation (mausamgram) configured with authScheme: basic in config/provider-adapter.yaml. A non-2xx response from that provider echoing its Authorization header back in an error body would be logged verbatim at Warn level via explain(body).

// safe to do at info level.
func (s *Step) redactString(text string) string {
if s.config.AuthScheme != AuthSchemeQuery {
return text

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@manjudr

manjudr commented Sep 8, 2026

Copy link
Copy Markdown
Member

@kelvinprabhu - there are conflicts in the PR - please resolve them

@ameersohel45
ameersohel45 force-pushed the feat/16-capability-schema-conformance branch 2 times, most recently from c481e7a to 5fcfb2f Compare September 8, 2026 09:48
@kelvinprabhu
kelvinprabhu force-pushed the feat/91-agrifacilities-plugin branch from c408a41 to 1b902a7 Compare September 8, 2026 10:31
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🛡️ Trivy security scan (CRITICAL,HIGH,MEDIUM,LOW)

View full run

Go dependencies

No findings at CRITICAL,HIGH,MEDIUM,LOW.

Container image

No findings at CRITICAL,HIGH,MEDIUM,LOW.

@ameersohel45
ameersohel45 force-pushed the feat/16-capability-schema-conformance branch from 5fcfb2f to 9a1594f Compare September 8, 2026 10:38
ameersohel45 and others added 13 commits September 8, 2026 16:17
…comment [#16]

Both subscriberId occurrences become `<>`, with the value that was there kept
beside them as `[#16] was: provider-network-vistaar.da.gov.in`. A reference config
that ships one deployment's real network identity invites it being copied into
another, and the id is not reusable: the registry cannot update a published
key and its delete is soft, so a second deployment claiming the same id has no
way out of it.

Both, not one. The handler's id and the keyManager's have to agree -- the
second is what the signer puts in the Authorization header's keyId -- so
placeholdering only one would have produced a config that signs as a
participant it does not claim to be, which fails at a peer rather than here.

Nothing loads this file, so nothing breaks: it is a reference, and the
placeholder is the point. It parses as YAML, and the value reads back as the
string "<>" rather than tripping the parser.

Also cleared the organisation name from three comments in the same file, which
the earlier sweep missed because it only looked at Go comments and not YAML.

[#16] Please enter the commit message for your changes. Lines starting
[#16] with '#' will be ignored, and an empty message aborts the commit.
[#16]
[#16] Date:      Mon Sep 7 18:02:45 2026 +0530
[#16]
[#16] interactive rebase in progress; onto 983affb
[#16] Last commands done (6 commands done):
[#16]    reword 6385051 refactor: name the registry and binding packages for what they are [#16]
[#16]    reword 9ebb5c1 docs(config): placeholder the subscriber id, keep the old value in a comment [#16]
[#16] No commands remaining.
[#16] You are currently editing a commit while rebasing branch 'tmp/rebase-16' on '983affb'.
[#16]
[#16] Changes to be committed:
[#16]	modified:   config/provider-adapter.yaml
[#16]
…lity

- Introduced a new file `prerequisites.go` in the agrifacility implementation.
- Defined an empty prerequisites variable to handle upstream requirements for agriculture facility capabilities.
- Documented the rationale for keeping the payload explicit and avoiding unnecessary entries.
… the mapping scrubs

The README predated two changes and contradicted both: the placeholder list
omitted "-", which the warehouse BPP uses for a phone it does not have, and
nothing said @context is echoed from the request rather than restated.

Also states what conformance_test.go covers, and why the pack's own examples
are validated -- every other result in that file is worthless if the schema
was compiled too loosely to reject anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the convention a1db4e7 set for the other two: the package, the built
.so and the id a deployment names in providerSteps are all the capability's
own local name, so which plugin owns a binding key is readable from the key.
mandi became MandiPrice and weather became WeatherObservation; agrifacility
becomes AgricultureFacility.

A directory rename carries the id with it, since the .so basename IS the id.

docs/agrifacility-testing.md moves with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kelvinprabhu
kelvinprabhu force-pushed the feat/91-agrifacilities-plugin branch from 1b902a7 to bd0d4c4 Compare September 8, 2026 10:49
Base automatically changed from feat/16-capability-schema-conformance to feat/41-oan-adapter-plugins September 8, 2026 12:35
…Verify (upstream_test.go). Verified clean with go test -race -count=3 ./pkg/plugin/implementation/internal/upstream/....
@manjudr

manjudr commented Sep 8, 2026

Copy link
Copy Markdown
Member

@kelvinprabhu - This PR contains a Claude co-auther information and also contains unclear commit messages - can you please do the fresh PR with proper commits?

@manjudr manjudr closed this Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📊 Test Coverage: ✅ Passed — 91% of changed lines covered, min 80%

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.

3 participants