Metadata-Version: 2.4
Name: agience-origin
Version: 0.1.2
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.0
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

**Identity, authority.**

Origin says who. It is the identity and authorization authority — the trust anchor the rest of the
system verifies against, and the entity everything else asks "who is this, and may they?"

As the OIDC issuer it mints and verifies tokens, publishes the JWKS that peer services check
signatures against, and owns the passkey/OTP account and setup flows.

The shared foundation is **`agience-prism-py`** — Origin's only workspace dependency.

## Layout

| Path | Purpose |
|---|---|
| `src/origin/` | The FastAPI service: `api/`, `routers/`, `services/` (auth, verifier, key custody via Shamir + key-oracle), `models/`, `db/`, `scripts/` (operator commands), plus `alembic/` (migrations) and `tests/`. |
| `src/origin/web/` | The static auth UI (login / passkey / setup), served at `/`, `/login`, `/account`, `/reset-password` and `/verify-email`, and mounted for its assets at `/web`. |

`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.
`src/origin/tests/test_the_package_ships_what_it_serves.py` holds it there.

## Running it

Origin is an ordinary Python package. It needs no container, and installing it pulls its own
pinned dependencies:

    pip install ../agience-prism/py          # the trust floor
    pip install .                            # or -e ".[test]" to work on it
    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 for 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`. **Origin ships no key generator** — every loader in
`prism.trust.key_manager` raises rather than inventing a key it did not write, which is correct for
an authority and means an empty directory is a hard stop. Key material comes from the platform
installer (`agience-observe`), a KMS, or a one-shot key-init step. `.env.example` documents the
full set.

For a managed install, `agience-observe/package/manager` does all of the above: it finds a Python,
builds the virtualenv, installs Origin into it and supervises the process.

## Letting a peer verify Origin

Publishing `/.well-known/jwks.json` is **not** what makes a peer able to verify an Origin-signed
token. Agience peers read their trust **inline**, from `trust_anchors` in the
`authority.manifest.json` of their own keyset, and never fetch. Until Origin's public JWK is
physically present there under `trust_anchors.origin`, two healthy, mutually reachable services
answer **401** for every user token and nothing logs a reason: 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 — asserting a public key for a service whose
private key is elsewhere is a trust statement, not a convenience. Origin emits its half:

    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           # no install needed

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

It reads `KEYS_DIR/origin.public.pem`, 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 — and **writes nothing**. Placement is the operator's decision; a command that
installed trust in itself on a peer would be the one direction a trust anchor must never travel.

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

    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` is the template — copy it to `.env`. It states the in-code default for every value
and warns 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 rather than falling back to a wildcard.

## License

**Dual-licensed: AGPL-3.0-only *or* commercial.** See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE);
commercial and white-label terms in [`COMMERCIAL_LICENSE.md`](COMMERCIAL_LICENSE.md). Contributing:
[`CONTRIBUTING.md`](CONTRIBUTING.md) and [`CLA.md`](CLA.md).
