Metadata-Version: 2.4
Name: agience-origin
Version: 0.1.5
Summary: Agience Origin — the identity authority (IdP): principals, identities, grants, service identity and the trust anchors peers verify against.
Author: Ikailo Inc.
License: AGPL-3.0-only
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: agience-prism[trust]>=0.1.2
Requires-Dist: authlib==1.6.9
Requires-Dist: bcrypt==5.0.0
Requires-Dist: cryptography==46.0.7
Requires-Dist: webauthn>=2.5.0
Requires-Dist: aiosmtplib==3.0.2
Requires-Dist: fastapi==0.135.1
Requires-Dist: starlette==0.49.3
Requires-Dist: itsdangerous==2.2.0
Requires-Dist: uvicorn[standard]==0.37.0
Requires-Dist: python-multipart==0.0.26
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: pydantic==2.12.0
Requires-Dist: pydantic[email]==2.12.0
Requires-Dist: pydantic-core==2.41.1
Requires-Dist: annotated-types==0.7.0
Requires-Dist: email-validator==2.2.0
Requires-Dist: sqlalchemy<3.0,>=2.0
Requires-Dist: alembic>=1.13
Requires-Dist: pyyaml>=6.0
Requires-Dist: boto3==1.40.50
Requires-Dist: botocore==1.40.50
Requires-Dist: httpx==0.28.1
Requires-Dist: anyio==4.11.0
Requires-Dist: typing-extensions==4.15.0
Requires-Dist: python-dateutil==2.9.0.post0
Requires-Dist: python-jose==3.5.0
Provides-Extra: test
Requires-Dist: pytest==8.4.2; extra == "test"
Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
Requires-Dist: pytest-xdist>=3.5.0; extra == "test"
Requires-Dist: pytest-timeout>=2.2.0; extra == "test"
Dynamic: license-file

# Agience Origin

[![PyPI](https://img.shields.io/pypi/v/agience-origin)](https://pypi.org/project/agience-origin/)
[![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](pyproject.toml)
[![License](https://img.shields.io/pypi/l/agience-origin)](LICENSE)
[![CI](https://github.com/Agience/agience-origin/actions/workflows/build.yml/badge.svg)](https://github.com/Agience/agience-origin/actions/workflows/build.yml)
[![Sponsor](https://img.shields.io/badge/Sponsor-Agience-EA4AAA?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/Agience)

**Identity and authority — the OIDC issuer.**

Origin is the trust anchor peers verify against, and the service everything else asks *who is this,
and may they?* It mints and verifies tokens, publishes the JWKS that peers check signatures against,
and owns the passkey, OTP and account-setup flows.

## Running it

An ordinary Python package — no container, and installing it pulls its own pinned dependencies.

```bash
pip install agience-origin
KEYS_DIR=/path/to/keys python -m uvicorn origin.main:app --host 127.0.0.1 --port 8080
```

It applies its own migrations at startup, so the first boot creates the database. Requires Python
3.11 or newer.

Bind loopback and put a reverse proxy in front of anything public — a service on the public
interface answers past whatever header and path rules the proxy applies.

`KEYS_DIR` must already contain `origin.private.pem`, `origin.public.pem`, `encryption.key` and
`inbound_nonce.secret`. Every key loader raises rather than inventing a key it did not write, which
is correct for an authority and makes an empty directory a hard stop. Key material comes from a
platform installer, a KMS, or a one-shot key-init step; [`.env.example`](.env.example) documents the
full set.

## Letting a peer verify Origin

Agience peers read their trust **inline**, from `trust_anchors` in the `authority.manifest.json` of
their own keyset. Publishing `/.well-known/jwks.json` is therefore not what makes a peer able to
verify an Origin-signed token: until Origin's public JWK is physically present under
`trust_anchors.origin`, two healthy and mutually reachable services answer **401** for every user
token, with nothing to log — from the peer's side there is no mismatch, there is simply no such
issuer.

A peer's own key init writes only its own anchor, since asserting a public key for a service whose
private key is elsewhere is a trust statement rather than a convenience. Origin emits its half:

```bash
origin-emit-anchor                                    # the mergeable fragment
origin-emit-anchor --format anchor --uri https://origin.example.com
origin-emit-anchor --format jwks --keys-dir /path/to/keys
python -m origin.scripts.emit_trust_anchor            # straight from a checkout
```

`--format` chooses `fragment` (a mergeable `{"trust_anchors": {"origin": …}}`, the default), `anchor`
(the value alone, for placing at `trust_anchors.origin`), or `jwks`. `--keys-dir` defaults to
`$KEYS_DIR` and `--uri` to `config.AUTHORITY_ISSUER`.

It reads `KEYS_DIR/origin.public.pem` and produces the JWK through the same `get_jwk_public()` that
serves `/.well-known/jwks.json`, so what you place is byte-identical to what Origin publishes, `kid`
included. It writes nothing: placement is the operator's decision, and a command that installed
trust in itself on a peer would send a trust anchor the one direction it must never travel.

To place it, merge into the peer's manifest:

```bash
origin-emit-anchor --format anchor > /tmp/origin-anchor.json
python - <<'EOF'
import json, pathlib
m = pathlib.Path("/path/to/peer/keys/authority.manifest.json")
doc = json.loads(m.read_text())
doc.setdefault("trust_anchors", {})["origin"] = json.load(open("/tmp/origin-anchor.json"))
m.write_text(json.dumps(doc, indent=2) + "\n")
EOF
```

Then restart the peer. A wrong `kid` or a re-encoded modulus fails as the same silent 401, which is
why the JWK is emitted rather than transcribed.

## Configuration

[`.env.example`](.env.example) is the template — copy it to `.env`. It states the in-code default for
every value and marks where an unset variable is itself a decision: `KEYS_DIR` unset means the
process does not boot, and `ORIGIN_ALLOWED_ORIGINS` unset derives the CORS allow-list from the
issuer, `ORIGIN_URI` and the facet bases.

## Layout

| path | what it is |
|---|---|
| [`src/origin/main.py`](src/origin/main.py) | the FastAPI app, and the startup that runs its own migrations |
| [`src/origin/routers/`](src/origin/routers/) | `auth` · `otp` · `passkey` · `setup` · `oracle` · `server_credentials` · `system` |
| [`src/origin/services/`](src/origin/services/) | `auth_service` and `auth_verifier`, key custody through `shamir` and `key_oracle`, `passkey_service`, `otp_service`, `person_service`, `oidc_providers`, `platform_settings_service`, `guess_budget` |
| [`src/origin/models/`](src/origin/models/) · [`src/origin/db/`](src/origin/db/) · [`src/origin/api/`](src/origin/api/) | the entities, the store and the request/response models |
| [`src/origin/alembic/`](src/origin/alembic/) | the migrations, applied at boot |
| [`src/origin/scripts/`](src/origin/scripts/) | the operator commands — `emit_trust_anchor` |
| [`src/origin/web/`](src/origin/web/) | the static auth UI served at `/`, `/login`, `/account`, `/reset-password` and `/verify-email`, with its assets mounted at `/web` |
| [`src/origin/tests/`](src/origin/tests/) | the suite, including the check that the package ships what it serves |

`web/` lives **inside** the package, beside the module that serves it: `main.py` resolves it as
`Path(__file__).resolve().parent / "web"`, which gives one answer in both places the code runs — a
checkout and an installed distribution. A path that climbs out of the package resolves against the
repository layout, which only a checkout has.

## Star history

<a href="https://www.star-history.com/?repos=Agience%2Fagience-origin&type=date&legend=top-left">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=Agience/agience-origin&type=date&theme=dark&legend=top-left" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=Agience/agience-origin&type=date&legend=top-left" />
   <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=Agience/agience-origin&type=date&legend=top-left" />
 </picture>
</a>

Security issues: email **connect@agience.ai** rather than opening a public issue.

Dual-licensed — see [`LICENSE`](LICENSE), [`COMMERCIAL_LICENSE.md`](COMMERCIAL_LICENSE.md),
[`NOTICE`](NOTICE) and [`CLA.md`](CLA.md).

## Declaration of generative AI use

The author used Anthropic's Claude Opus (versions 4.8 and 5) in the preparation of this work. Its
contribution was to write code, and to generate and validate content. The ideas, the construction
and the claims are the author's. No other generative AI tool was used. The author reviewed and
edited all output and takes full responsibility for the content of this publication.
