QriusAI Nonprofit Agent Harness
An open agent harness for the nonprofit sector. You write the agent. It gives you everything around the agent.
Built on Google's Agent Development Kit, and genuinely usable without it: OpenAI, Anthropic, Groq, Together, OpenRouter, vLLM and local models through Ollama all work out of the box, with no extra install.
Status: alpha. Everything described below works and is covered by tests. The interfaces will still change while the version stays below 1.0, so pin an exact version if you depend on them.
Why the Foundation built this
QriusAI Foundation works on AI capacity building for nonprofits. Across that work one pattern keeps repeating. A nonprofit team knows exactly what it needs an agent to do, because nobody understands their programme better than they do. What stops them is never the idea. It is the six weeks of scaffolding around the idea: sign-in, file handling, job state, cost control, an approval step, somewhere to put the output.
So they either do not build it, or they pay someone to build a single tool that only that someone can maintain.
We would rather give away the scaffolding.
We are not trying to build the tools the sector needs. We are building the blocks the sector can build them from. A harness is infrastructure. It is deliberately not a product, not a platform you sign up for, and not something that routes your work through us. You fork it, you run it, you own it, and the capability stays with you.
This is what we think AI capacity building has to mean eventually. Training helps. Tools help. But a sector that can only use what it is given stays dependent on whoever gives it. A sector with its own building blocks does not.
This is not a design sketch
The Foundation runs the Curriculum Agent for nonprofits: an organisation uploads its own training material, an agent restructures it into something teachable, and a person reviews every change before anything is released. You can see it at qriusai.org/harness.
The harness in this repository is that system's machinery, rewritten for public use rather than invented for it. The review gate exists because real output had to be signed off by a real person. The budget ceilings exist because the bill was real. The citation checking exists because a generated fact reaching a funder report is a specific thing to be afraid of, not a hypothetical.
What is here is the scaffolding, not the agents. The Curriculum Agent and the Foundation's other work stay in its own repositories. This is the part worth giving away.
See it run
On 2026-10-17 the Foundation ran this harness against Claude Haiku 5.5, Sonnet 5.5, Opus 5.5 and Fable 5.1, on six openly licensed training and programme documents of up to 385,000 tokens and three published IATI activities: 183 harness runs, USD 85.36. Every output is committed with the harness's own receipt (tokens, cost, review state, the verdict on every quote). docs/showcase.md has the cost per model, what the citation check caught and got wrong, a 3,966-item quote-checking test set, budget ceilings tripped on purpose, and how to reproduce it all with your own key.
What we looked for, and did not find
Before writing this we searched for one already. Two angles: open-source agent projects scoped to the nonprofit sector, and agent projects built on Google's ADK for nonprofit use.
We found real, useful work in each direction separately. There are nonprofit-specific single-purpose tools, for grant writing and for board governance. There are ADK projects built by and for individual organisations. There are excellent general-purpose agent frameworks that any sector can build on.
What we did not find was something at the intersection: a reusable harness, scoped to this sector, that a nonprofit could fork and build several different agents on.
We want to be precise about what that is and is not. It is an absence of evidence from two search angles, not an exhaustive audit of everything that exists. So the honest claim is "we found no comparable project", not "this is the first in the world." If you know of one, please open an issue. We would genuinely like to know, and we would rather link to it than duplicate it.
What makes this a nonprofit harness
Plenty of frameworks will help you call a model. Very few assume the things this sector has to assume.
Someone is accountable for every output. A generated training handout, funder summary, or community-facing message cannot go out because a model produced it. Review is the default here, not an add-on, and every release records who approved it.
A claim has to be traceable to a source. An invented statistic in a funder report is the failure that costs an organisation its credibility. Cited quotes are checked against the source document before a reviewer ever sees them, and that check costs nothing to run.
The budget is fixed and small. Grant money does not stretch because a retry loop misbehaved. Every run carries a ceiling on tokens and calls by default, and on cost once you set one and price your models. A model with no price is reported as unknown cost, never as free.
Nobody chose their model provider. Credits arrive donated, from whichever platform offered them, and some data is not allowed to leave the building at all. Eight providers work out of the box, including local models, and switching is one environment variable.
There is no platform team. It has to run with no cloud account before it runs with one. A fresh clone works offline, for free, in one command.
If your agent does not need those things, use a general framework. This one is scoped on purpose.
Quickstart
No cloud account, no API key, no spend:
pip install nonprofit-agent-harness
harness check
That prints the settings in force. Nothing is configured yet, and it still runs, because the default provider is offline and deterministic.
To work on the harness itself, or to run the bundled examples:
git clone https://github.com/QriusAI-Foundation/nonprofit-agent-harness
cd nonprofit-agent-harness
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,documents]"
harness run examples.summarizer:SummarizerAgent path/to/report.pdf
The examples are not packaged, deliberately. They are reference material rather than library code, and your agents live in your own project.
Continuous integration installs the package with no extras and runs the example on every commit, so the offline promise stays true rather than being an aspiration.
Point it at a real model when you are ready. Any of these work, and no provider needs an extra install except Google:
# OpenAI, or Groq, Together, OpenRouter with the same adapter
HARNESS_PROVIDER=openai HARNESS_MODEL=gpt-4o-mini OPENAI_API_KEY=...
# Claude
HARNESS_PROVIDER=anthropic ANTHROPIC_API_KEY=...
# A local model. Nothing leaves the machine.
HARNESS_PROVIDER=ollama HARNESS_MODEL=llama3.1
# Vertex AI, which uses the service account and so has no API key at all
HARNESS_PROVIDER=google GOOGLE_CLOUD_PROJECT=...
Everything except Google and Anthropic is one adapter speaking the OpenAI chat API, so
any server implementing it works by setting OPENAI_BASE_URL, named alias or not. That
matters for two situations this sector actually hits: donated credits on whichever
platform offered them, and data that is not allowed to leave your own hardware.
Switching provider changes nothing else. Agents, review, budget ceilings and verification behave identically, because the model layer is the only thing that moved.
Writing an agent
One method. The harness owns the rest.
from nonprofit_harness import Agent, AgentResult, RunContext
class BoardBriefAgent(Agent):
name = "board-brief"
description = "Turns programme documents into a short brief for trustees."
def run(self, ctx: RunContext) -> AgentResult:
artifacts = []
for document in ctx.inputs:
response = ctx.provider.generate(
f"Write a 200-word brief for trustees:\n\n{document.text}",
system="Plain language. Short sentences. No invented facts.",
)
artifacts.append(self.artifact("brief", response.text, title=document.name))
return AgentResult(artifacts=artifacts, summary=f"{len(artifacts)} brief(s)")
ctx is the only thing your agent touches. It never reaches for a database handle, a
global client, or an environment variable of its own. That is why the same agent runs
offline in a test and on Cloud Run in production without changing, and why it cannot
skip a budget ceiling or approve its own output.
See docs/writing-an-agent.md for the full guide.
What you get around it
| Piece | What it does |
|---|---|
| Review gate | Artifacts land in pending_review. Nothing is released until a person approves it, and every decision records who made it, including automatic ones. |
| Claim verification | Cited quotes are checked against the source. Free and offline. An optional cross-check has several models re-derive the claim from its evidence alone. |
| Budget ceilings | Per-run limits on calls, tokens, and cost. Each call's output is capped to what is left before it is sent and counted in full after. Token and call limits work with no pricing; a cost limit refuses a model it has no price for. One call can still overshoot by a little; docs/configuration.md says how much. |
| Readiness scoring | A scoring engine for AI-readiness instruments: per-question direction, excluded options, weighted dimensions, geometric-mean aggregation, tier bands. |
| Documents | PDF, DOCX, and text in, plain text out. Agents never see bytes. |
| Sector data | An IATI adapter: search activities, read their published XML into full results, and resolve codes to readable names from bundled codelists. |
| Results and indicators | The sector's own arithmetic: achievement against target, progress from baseline, disaggregation, budget utilisation. Deterministic, no model calls. |
| Providers | OpenAI, Anthropic, Groq, Together, OpenRouter, vLLM, Ollama, and Gemini. Only Gemini needs an extra install. |
| Storage | In-memory by default. Firestore and Cloud Storage behind the same interface. |
| Auth | Google Sign-In verification plus the harness's own session tokens. Off by default. |
| Redaction | Optional masking of direct identifiers before text reaches a model. |
| Credential hygiene | Every provider's error text is scrubbed of API keys before it is raised, logged, or returned. |
| Run recovery | A run stranded by a stopped process is failed at startup, so a poller gets an answer instead of waiting forever. |
| HTTP API and CLI | FastAPI surface for runs, uploads, review, readiness and webhooks, plus a harness command. |
Verifying what an artifact claims
Attach claims to an artifact and the harness checks them before anyone reviews it:
from nonprofit_harness import Citation, Claim
self.artifact(
"brief", text,
claims=[
Claim(
statement="The programme reached twelve villages.",
citations=[Citation(text="reached twelve villages", locator="p.3")],
)
],
)
Layer one costs nothing. Every quoted span must actually appear in one of the run's own input documents. No model call, no network, no spend. This is what catches an invented citation, and it runs by default.
This check finds quotes. It does not decide whether the quote supports the claim. A verdict from layer one says the citation resolved, with "support not checked"; only layer two can report a claim as supported.
The matching rules are fixed and small:
| Quote compared with the source | Result |
|---|---|
Same words after case and whitespace are folded, on word boundaries ("reached 5" does not match "reached 50") |
exact |
A number, a negation (not, never, without, ...), or the order of content words differs |
refused |
Only punctuation, an added or dropped article or that, or a swap such as in/during, at most 1 per 5 words and never more than 3 |
approximate |
| Most words line up in order, but some other word differs | flagged for a person |
A faithful quote that its surroundings change: never just before it, or a number cut from 5-10 or 5 million |
flagged for a person |
"reached ninety villages" shares two words in three with "reached twelve villages",
so plain word overlap would wave it through, and a swapped figure is precisely the
fabrication that does damage. A flagged quote holds the artifact for review like a
missing one; it is never counted as resolved.
Layer two costs model calls and is opt-in. Several reviewers re-derive the claim from its evidence alone and have to agree:
export HARNESS_VERIFY_PASSES=3
export HARNESS_VERIFY_MODELS=model-a,model-b,model-c
Use different models rather than the same one several times. Reviewers that share an architecture tend to share its blind spots, which makes their agreement much weaker evidence than it looks. A reviewer that cannot decide abstains rather than guessing.
Results reach the person doing the review, not a silent filter:
{ "ok": false, "summary": "3 claim(s): 2 resolved (support not checked), 1 citation not found" }
A failed claim never discards work and never fails the run. It flags the artifact so a
reviewer knows where to look. One consequence worth knowing: an agent that sets
requires_review = False still cannot auto-release an artifact whose claims failed.
Opting out of review says the output is routine. A citation that does not resolve is
evidence that it is not.
Did the programme work
The sector's own arithmetic, written down once. Deterministic, and free to run.
from nonprofit_harness.results import Baseline, Indicator, achievement
got = achievement(indicator)
got.explain() # "Enrolment: 50.0% (70 from a baseline of 60 towards 80)"
Three things this refuses to get wrong, each of which produces a confident and incorrect number if you do the obvious thing:
- Direction. Forty disease cases against a target of fifty is a success, not an
80% shortfall. The IATI
ascendingflag says which way is good, and it is honoured. - Baselines. Enrolment moving 60 to 70 against a target of 80 is 50% of the intended change, not 87.5%. Measuring from zero credits a programme with what was already true before it started.
- What may be added. Percentages do not sum, ordinal codes are labels, and publishers can mark their own data as not aggregatable. Totals refuse rather than filtering quietly, because a total that dropped half its inputs looks complete.
Nothing returns a bare number. Every figure carries how it was reached, and anything that cannot be calculated says why instead of guessing. See docs/results.md.
Reading the sector's own data
Funders and implementing organisations publish activity data to the IATI standard. The harness reads it and turns it into ordinary harness input:
from nonprofit_harness.datasources import IatiClient
client = IatiClient(cache={}) # IATI_API_KEY, free
activities = client.search_activities(reporting_org="XM-DAC-41114", rows=5)
run = runner.start("my-agent", org_id="org_1",
inputs=[a.to_document() for a in activities])
Fetching happens on the caller's side, never inside the agent. An agent that could fetch its own data would reach past its inputs, could not be tested offline, and could not have its claims checked against a known set of sources.
The free IATI tier allows 100 calls a week, so pass a cache.
Codes arrive resolved, because Sectors: 11220 is precise and useless:
Recipient countries: Palestine, State of (PS)
Sectors: Primary education (11220)
Activity status: Closed (4)
The lists are bundled, so that costs no network, no key, and none of the call budget. A publisher's own narrative always wins over the codelist, and a sector reported against a publisher's own numbering is left as a bare code rather than given a DAC name that would be real and wrong.
Published results come back as the results model above, ready to compute on:
results = client.results("NL-KVK-41236410-6970")
period = results[0].indicators[0].periods[0]
dimension_totals(period.actuals, "gender") # {'female': 2965.0, 'male': 2049.0}
dimension_totals(period.actuals, "age") # {'18+': 2692.0, 'under 18': 2322.0}
That reads the activity's published XML, which keeps the nesting. There is also a search path over the Datastore's flattened rows, which is how you find activities in the first place, but it loses which indicator belongs to which result and which slice belongs to which number, and says so in its warnings. One call either way, so prefer the XML.
See docs/datasources.md and docs/results.md.
Readiness scoring
The engine is open. The questionnaire is data you supply.
from nonprofit_harness.readiness import Instrument, score
instrument = Instrument.from_json("my-instrument.json")
result = score(instrument, {"d1": "spreadsheets", "d2": "dont_know"})
result.status # "complete", "partial", or "incomplete"
result.overall # 0.0 to 1.0, or None when incomplete
result.dimension_scores # per dimension
result.tier # whichever band the overall lands in, or "" when incomplete
result.excluded_questions # what was left out of scoring, and why
result.to_dict()["coverage"] # answered, scored, excluded, unanswered per dimension
Three design choices worth knowing about, because they change the numbers:
- "Don't know" removes a question from scoring rather than scoring it zero. In this sector, "don't know" usually means a thin back office, not low capability. Scoring it as zero systematically marks down the smallest organisations.
- Dimensions combine with a geometric mean. An organisation with excellent tooling and no data governance is not "average". It is blocked on data governance, and an arithmetic mean hides that.
- No overall score without every dimension. The overall score and tier are issued
only when every dimension has at least one scored answer. Otherwise the result is
incomplete:overallisNone,tieris empty, andmissing_dimensionsnames what is missing. One answer cannot produce a "High". When every dimension is covered but some questions are blank, the result ispartialand says how many. "Don't know" counts as answered but not scored, socoveragereportsexcludedandunansweredseparately. A value selected twice in a multi-select question counts once, andduplicates_droppedlists the repeats.
A small example instrument ships in src/nonprofit_harness/readiness/instruments/. It
is a demonstration, not a validated index. Bring your own.
The Foundation authors and stewards a nonprofit AI readiness index of its own. That index is meant as a neutral standard for the sector rather than a QriusAI product, which is exactly why the scoring engine is here in the open and the questionnaire is just data.
Run it as a service
uvicorn nonprofit_harness.main:app --reload
HARNESS_AGENTS=examples.summarizer:SummarizerAgent uvicorn nonprofit_harness.main:app
GET /healthz
GET /v1/agents
POST /v1/uploads
POST /v1/runs create a run
GET /v1/runs list them
GET /v1/runs/{run_id}
GET /v1/review/queue what is waiting on a person
POST /v1/review/{run_id}/artifacts/{artifact_id}/approve
POST /v1/review/{run_id}/artifacts/{artifact_id}/reject
POST /v1/review/{run_id}/approve-all
GET /v1/review/{run_id}/released 409 while anything is pending
GET /v1/readiness/instrument
POST /v1/readiness/score
POST /v1/auth/google exchange a Google credential
GET /v1/auth/me
GET /v1/admin/config settings in force, plus warnings
GET /v1/admin/orgs
PUT /v1/admin/members/{email} assign a user to an org
POST /v1/webhooks/{source}
Interactive docs at /docs.
The command line
harness check # settings in force, and any warnings
harness run mypackage.agents:BriefAgent report.pdf # run an agent over files
harness score answers.json # score readiness answers
harness run adds the working directory to the import path, so an agent in your own
project works without installing it. Pass --option key=value to reach ctx.options.
Configuration
Everything is environment variables. Defaults are offline, free, and reviewed. See docs/configuration.md for the full reference.
| Variable | Default | Meaning |
|---|---|---|
HARNESS_PROVIDER |
echo |
echo, openai, anthropic, groq, ollama, google, and more |
HARNESS_MODEL |
provider default | Model name |
HARNESS_STORAGE |
memory |
memory or gcp |
HARNESS_MAX_COST_USD |
unset | Per-run cost ceiling (needs HARNESS_PRICES) |
HARNESS_PRICES |
unset | Model rates, model=input/output in USD per 1M tokens |
HARNESS_MAX_TOKENS |
200000 |
Per-run token ceiling |
HARNESS_MAX_CALLS |
50 |
Per-run model-call ceiling |
HARNESS_VERIFY_CLAIMS |
true |
Check cited quotes against the source (free) |
HARNESS_VERIFY_PASSES |
0 |
Cross-check passes per claim (costs model calls) |
HARNESS_VERIFY_MODELS |
empty | Comma-separated models to rotate across passes |
HARNESS_REDACT_INPUTS |
false |
Mask identifiers before sending text |
HARNESS_RUN_TIMEOUT_SECONDS |
3600 |
How long before a stranded run is treated as abandoned |
HARNESS_AUTH_REQUIRED |
false |
Require a session token |
HARNESS_GOOGLE_CLIENT_ID |
unset | For Google Sign-In |
HARNESS_JWT_SECRET |
unset | Session signing secret, 32 bytes or more |
HARNESS_ADMIN_EMAILS |
empty | Comma-separated allowlist, exact match |
HARNESS_DEFAULT_ORG |
org_default |
Org a first-time Google user joins; empty means enrolled users only |
HARNESS_CORS_ORIGINS |
empty | Comma-separated browser origins |
HARNESS_AGENTS |
empty | Comma-separated module:ClassName to register |
Provider keys use each vendor's usual name: OPENAI_API_KEY, OPENAI_BASE_URL,
ANTHROPIC_API_KEY, GOOGLE_API_KEY or GOOGLE_CLOUD_PROJECT. Data sources use
IATI_API_KEY.
Ceilings accept none, unlimited, or off to switch off. 0 means zero, not
"unset". Run harness check to print what is actually in force, along with the
problems that would bite in production.
Deploying
deployment/ has a Dockerfile and Terraform for Cloud Run, configured to scale to zero
so an idle deployment costs nothing. See docs/deployment.md, and
read docs/security.md before exposing an instance publicly.
Use it as a starter template
This repo doubles as an agent-starter-pack remote template:
uvx agent-starter-pack create my-agent -a github.com/QriusAI-Foundation/nonprofit-agent-harness
Documentation
| Document | What it covers |
|---|---|
| docs/writing-an-agent.md | The agent contract in depth, options, claims, testing |
| docs/datasources.md | Reading the sector's open data, starting with IATI |
| docs/results.md | Results, indicators, and the arithmetic that gets them wrong |
| docs/configuration.md | Every setting, what it does, what it costs |
| docs/showcase.md | Real runs on Claude models: costs, receipts, what the checks caught |
| docs/deployment.md | Cloud Run and Terraform, start to finish |
| docs/security.md | Threat model, what is and is not protected |
| docs/ARCHITECTURE.md | Why the pieces are shaped the way they are |
| CONTRIBUTING.md | Setup, conventions, what we are looking for |
Project status
Honest about where this is:
Works and is tested. The agent contract, review gate, claim verification, budget ceilings, the results and indicator arithmetic, the readiness engine, the IATI data source, eight model providers, document extraction, in-memory and GCP storage, auth, the HTTP API, and the CLI. 536 tests, all offline, on Python 3.11 through 3.13.
Audited. An independent source review of 0.3.0 (September 2026) reported thirteen findings: unauthenticated output routes, membership not enforced, agent-supplied approval state, non-atomic reviews, citation matching that accepted altered quotes, inflatable readiness scores, results arithmetic across scopes, startup-only recovery, unbounded extraction, and a budget ceiling that overstated its protection. All thirteen are closed in 0.4.0; the changelog names each fix. The reviewer's caution still stands: these are source-level guarantees with offline tests behind them, not a pilot with a partner organisation's data.
Seen running. docs/showcase.md records 183 real runs against Claude models on 2026-10-17, with every token, cost, and citation outcome written by the harness itself, and a 3,966-item test set for the citation check.
Known gaps. Per-run ceilings bound a single run rather than a total, so a deployment open to the public needs quota and rate limiting on top. Runs still execute in-process with a lease and a periodic recovery pass; high volumes want a real queue. Firestore transactions and pagination are tested against a fake client, not a real Firestore. A queued run that never started is not recovered. PDF line-break hyphens can make a correct quote look altered.
Not started. An evaluation harness beyond the citation corpus. Logframe and theory of change structures, which are mostly narrative and so offer little for code to check.
Contributing
See CONTRIBUTING.md. Issues and pull requests are welcome, especially from people running agents in actual nonprofit settings. That is the feedback this is hardest to get and the most useful to have.
License and credit
Apache License 2.0. You may fork it, change it, and use it commercially.
Attribution is required, and the license enforces it rather than asking. Section 4(d) says any derivative work must carry forward the notice in NOTICE. Keep that file and you are compliant.
About QriusAI Foundation
QriusAI Foundation is the public brand of KRIAAJEE ENVIRONMENTAL FOUNDATION, a Section 8 nonprofit. The Foundation works on AI literacy and AI capacity building with nonprofits, schools, and colleges, including in places where the digital divide is widest.
This harness is part of that work. Software is one of the ways capability transfers, and open source is the version of it that does not depend on us still being here.
Metadata
Release files for nonprofit-agent-harness 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| nonprofit_agent_harness-0.4.0.tar.gz | 185.9 kB | With provenance | Details for nonprofit_agent_harness-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| nonprofit_agent_harness-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for nonprofit_agent_harness-0.4.0-py3-none-any.whl |
Total release size: 335.2 kB
Release files / nonprofit_agent_harness-0.4.0.tar.gz
| Download URL | nonprofit_agent_harness-0.4.0.tar.gz |
|---|---|
| Size | 185.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
51a9b7c41f44aafbb636c1f67a99818de0f2203bf7dbd6694691192392785cf6
|
|
BLAKE2b-256 checksum How to use checksums |
04e4722031e55b68fef4698cb97ae4805a9344f2cf6fd118ae3b0da58d0d3bdd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / nonprofit_agent_harness-0.4.0-py3-none-any.whl
| Download URL | nonprofit_agent_harness-0.4.0-py3-none-any.whl |
|---|---|
| Size | 149.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bcc60484e8fbaa3f166ec8cc855cd80fa7d59e85d477fb903d55f0b64a8ce14f
|
|
BLAKE2b-256 checksum How to use checksums |
442b1c04a8db61fed63e65c6b7d1be5216a477f9d4bb299bc2dcb200bfb65106
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logSecurity for nonprofit-agent-harness 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for nonprofit_agent_harness-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.