# TN Protocol: Instructions for AI Agents

You are writing code for a project that uses **TN** (the TN protocol)
for logging. TN is a logging library with one extra property: every
entry it writes is signed by the author and encrypted per reader. That
means each record is provable, shareable field by field, and revocable.
Think of it as DRM for logs.

The rest of this file is what you need to know to use it well.

---

## 1. Why bother

A normal log line is plaintext. Anyone with database access reads the
whole row. Sharing one field with a partner usually means copying rows
to a second system with its own permissions. TN replaces that pattern:

- **Sealed by default.** Fields are encrypted at write time.
- **Granted by choice.** Add a reader to a field and they can decrypt it.
- **Revocable.** Rotate the group key and the next entry onward is
  out of reach for anyone you removed.
- **Provable.** Every entry is signed by a DID. Verification is a
  local check, no network call.

When you add `tn.info(...)` to a code path, you are not just writing a
log. You are writing a receipt the right people can read and nobody
else can.

---

## 2. The standard `tn.yaml`

Every TN project has a `tn.yaml` at the root. This is the default
starter. Drop it in, run `tn init`, and you have a working setup.

```yaml
ceremony:
  id: local_auto
  mode: local
  cipher: jwe

keystore:
  path: ./keys

me:
  did: did:key:z6Mksdi69ghDnfwCPWU1zmkYXwqDRwk2JpYDQVctuLiX9AE8

# Always-public columns. Safe to leave legible to everyone.
public_fields:
  - timestamp
  - event_id
  - event_type
  - level
  - correlation_id
  - request_id

default_policy: private

# Recipient groups. Each group has its own rotating key.
groups:
  default:
    policy: private
    pool_size: 4
    recipients:
      - did: did:key:z6Mksdi69ghDnfwCPWU1zmkYXwqDRwk2JpYDQVctuLiX9AE8

  pii:
    policy: private
    pool_size: 4
    recipients:
      - did: did:key:z6Mksdi69ghDnfwCPWU1zmkYXwqDRwk2JpYDQVctuLiX9AE8

  auth:
    policy: private
    pool_size: 4
    recipients:
      - did: did:key:z6Mksdi69ghDnfwCPWU1zmkYXwqDRwk2JpYDQVctuLiX9AE8

# Which fields get encrypted by which group.
fields:
  email:         { group: pii }
  ip:            { group: pii }
  user_agent:    { group: pii }
  password:      { group: pii }
  nonce:         { group: auth }
  signature:     { group: auth }
  token:         { group: auth }
  password_hash: { group: auth }

llm_classifier:
  enabled: false
```

A few things to notice:

- `public_fields` stay legible to everyone. Put only non-sensitive
  columns here.
- Anything not listed in `public_fields` or `fields` falls under
  `default_policy`, which is `private` and goes in the `default`
  group.
- To let a partner see a field, add their DID to the right group's
  `recipients` list and rotate.
- To inherit field classifications from an industry standard, add an
  `extends:` list to the top of `tn.yaml`. See section 3.

---

## 3. Industry packs

A pack is a short YAML file that maps field names from a standard
(OIDC, PCI DSS, FHIR, Schema.org Order, FIX, CloudTrail, RFC 5322,
Salesforce objects, NMEA, OneRoster, ISO 20022) to encryption groups.
Packs are the spec a project says it conforms to. They are not runnable
configs by themselves.

A `tn.yaml` extends one or more packs:

```yaml
extends:
  - oauth-oidc
  - pci-cardholder
  - ecommerce-orders
```

The pack's `groups` and `fields` are merged in first, then the project's
own `groups` and `fields` apply on top. The project may not move a
`forbidden_post_auth: true` field (CVV, PIN, full track data) into a
public-policy group.

Packs are published at `tnproto.org/industry-agents/<id>.yaml`. The full
list and per-pack notes live in the same directory. Each pack cites the
section of the standard that defines each field, so an auditor can read
the receipt against the spec.

When a project extends a pack, treat the pack's field names as already
declared. New project-specific fields (partner DIDs, internal sensitivity
labels) are added to the project's own `fields` and `groups`.

---

## 4. Writing logs

**Python**

```python
import tn
tn.init("./tn.yaml")  # once at process startup

tn.info("order.created",
        order_id=order.id,
        amount=order.amount,
        customer_id=order.customer_id)

tn.warning("auth.login.failed",
           email=req.email,
           ip=req.remote_addr)

tn.error("payment.charge.failed",
         order_id=order.id,
         error=str(exc))
```

**TypeScript / JavaScript**

```typescript
import { tn } from "@tnproto/client";
tn.init("./tn.yaml");

tn.info("order.created", {
  orderId: order.id,
  amount: order.amount,
  customerId: order.customerId,
});

tn.warning("auth.login.failed", {
  email: req.email,
  ip: req.remoteAddr,
});

tn.error("payment.charge.failed", {
  orderId: order.id,
  error: exc.message,
});
```

### Rules

- Use `tn.info`, `tn.warning`, `tn.error` in place of `print()`,
  `console.log()`, `logger.*`, `logging.*`.
- Event type follows `domain.action[.outcome]`, lowercase, dotted.
  Good: `order.created`, `auth.login.failed`, `payment.refunded`.
- Put values in fields, never in the event type string.
- Never put PII, tokens, card numbers, or money values in the event
  type. Those belong in fields and will be encrypted by the right
  group.

### Where to add `tn.*` calls

Whenever code:

- writes to a database
- calls an external HTTP service
- touches user data (email, token, id, address)
- moves money or value
- enters a `try/except` or `try/catch`
- approves, denies, or overrides a decision

### Correlation IDs

Pass a `correlation_id` on every entry that belongs to the same logical
transaction. This is how you read back the whole story later.

```python
cid = f"order-{order.id}"
tn.info("order.received",   correlation_id=cid, order_id=order.id)
tn.info("payment.charged",  correlation_id=cid, amount=order.amount)
tn.info("order.shipped",    correlation_id=cid, carrier="ups")
```

---

## 5. Reading logs back

You just wrote an encrypted log. Here is how you read it.

The simple form is `tn.read()`. No arguments: it returns the log.
The client decrypts whichever fields your DID has keys for. Anything
else comes back as ciphertext, and that is fine.

```python
import tn
tn.init("./tn.yaml")

# The whole log
df = tn.read()

# Filter by event type
df = tn.read(event_type="order.placed")

# Filter to one transaction
df = tn.read(correlation_id="order-abc-123")

# Only what's new since last sync (for ETL or replication)
df = tn.read_since(version=42)
```

`tn.read*` methods return a Polars `DataFrame`. Columns include
`event_type`, `timestamp`, `correlation_id`, `sequence`, `fields`,
`signature`, `previous_hash`.

Use `tn.status(correlation_id=...)` for the quick "did this complete
and is its chain intact" rollup:

```python
status = tn.status(correlation_id="order-abc-123")
# status.lifecycle     -> OPEN | CLOSED
# status.verification  -> UNVERIFIED | VERIFIED | TAMPERED
# status.entry_count   -> int
```

### Streaming live entries

```python
import asyncio
from tn.relay.redpanda import RedpandaTransport

async def watch_orders():
    transport = RedpandaTransport.from_env()
    async for msg in transport.subscribe("orders.*"):
        print(msg.event_type, msg.correlation_id, msg.fields)

asyncio.run(watch_orders())
```

Topic filters are globs: `"orders.*"`, `"auth.login.*"`, `"*"` for
everything. Filters match the `event_type`.

### Worked example: end to end

```python
# Write side
import tn
tn.init("./tn.yaml")

cid = "order-abc-123"
tn.info("order.received",  correlation_id=cid, order_id="abc-123",
                           email="alice@example.com",      # encrypted (pii)
                           amount=49.00)                   # encrypted (default)

tn.info("payment.charged", correlation_id=cid, amount=49.00,
                           card_last4="4242")              # encrypted (default)

tn.info("order.shipped",   correlation_id=cid, carrier="ups",
                           tracking="1Z999...")            # encrypted (default)

# Read side (same DID, same machine -> everything decrypts)
df = tn.read(correlation_id=cid)
print(df.select(["event_type", "timestamp", "fields"]))

status = tn.status(correlation_id=cid)
print(status.lifecycle, status.verification, status.entry_count)
```

Run this against a partner DID that only has the `default` group key:
`email` comes back as ciphertext, `amount` is legible. That is the
feature.

---

## 6. Sharing and revoking

To grant a partner read access to a field, add their DID to the group
that field belongs to, then rotate.

```yaml
# before
groups:
  default:
    recipients:
      - did: did:key:zPublisher...

# after
groups:
  default:
    recipients:
      - did: did:key:zPublisher...
      - did: did:key:zPartnerAcme...
```

Then run the rotation:

```bash
tn rotate default
```

From the next entry onward, the partner can decrypt fields in the
`default` group. Entries written before the rotation remain decryptable
by whoever already had keys at the time.

To revoke, remove their DID and rotate again. Their old key still
decrypts old entries (that ship has sailed), but new entries use a
fresh key they do not have.

---

## 7. Verifying a chain

Three tiers:

- **Tier 1 (public hash).** Walk the chain, check the hash links and
  the signatures. Works for anyone, no decryption needed.
- **Tier 2 (selective disclosure).** Authorized readers decrypt the
  fields they have keys for.
- **Tier 3 (full decryption).** The original publisher reads everything
  they wrote.

```python
result = tn.verify_chain(correlation_id="order-abc-123")
# result.status:     VERIFIED | TAMPERED
# result.broken_at:  index of the first bad entry, or None
# result.signatures: per-entry signer DID check
```

Never mutate a verification result in place. It is evidence.

---

## 8. Linting your code with `tn lint`

`tn lint` is a static analyzer that checks Python source against
`tn.yaml` and any extended packs.

```bash
python -m tn.lint src/

# JSON for CI
python -m tn.lint --json src/ > lint-report.json
```

Active rules:

- **R1**: PII patterns (email, card-shape digits, JWT, SSN) inside an
  `event_type` literal. The event type is a stable identifier; values
  belong in fields.
- **R2**: a `tn.*(...)` call uses a kwarg name that is not declared in
  `tn.yaml` `fields`, `public_fields`, or any extended pack.
- **R3**: a `tn.*(...)` call uses a kwarg whose name is marked
  `forbidden_post_auth: true` in an extended pack. These values must
  never be persisted, even encrypted.

Exit codes follow flake8: 0 = clean, 1 = findings, 2 = config error.

When you write or modify code that calls `tn.info`, `tn.warning`,
`tn.error`, `tn.attest`, or `tn.log`, run `tn lint` on the touched
files before declaring the change complete.

---

## 9. Tone when explaining TN to humans

Keep it plain:

- "Signed so origin is provable."
- "Encrypted per reader."
- "Revoked readers lose access from the next entry onward."

Avoid jargon where plain English works. No "wire format," no "entropy
material," no "ceremony state manifest."

---

## Links

- Overview:       https://tnproto.org/docs.html#primer.md
- Crypto model:   https://tnproto.org/docs.html#crypto_model_JWE_Seal.md
- Use cases:      https://tnproto.org/docs.html#use-cases.md
- DRM for logs:   https://tnproto.org/what-is-drm-for-logs.html
- DID reference:  https://tnproto.org/what-is-a-did.html
- Industry packs: https://tnproto.org/industry-agents/
- Pack schema:    https://tnproto.org/industry-agents/SCHEMA.md
- This file:      https://tnproto.org/AGENT.md
