# TunnelMind Signed Attribute Bundle v1.0

**Status:** DRAFT published 2026-07-26 · 90-day public-comment window closes 2026-10-24.
**License:** CC BY 4.0.
**Canonical URL:** https://tunnelmind.ai/standards/attribute-bundle/v1
**Live producer:** `POST https://data.tunnelmind.ai/v1/verify/{node}` (REST + NDJSON stream)
**Conformance vector:** https://tunnelmind.ai/wp-mac/fixtures/verify-203.0.113.45.ndjson
**Companion:** [Receipt Format v1.0](https://tunnelmind.ai/standards/receipt-format/v1) — the
signed envelope this bundle rides in. The bundle is the *claim set*; the receipt is the
*proof wrapper*.

A **Signed Attribute Bundle** is the output shape of a Policy Information Point (PIP)
for the agentic internet: per-source facts about a network node, each labeled with an
explicit **coverage state**, plus a **freshness contract** and a **signature binding** —
so a Policy Decision Point (PDP) can consume the facts without ever being asked to trust
a verdict.

The design position: a PIP owes its PDP three guarantees no verdict score can carry —

1. **Coverage honesty** — for every source, whether the PIP actually *looked*, and
   whether "no data" means *clean*, *blind spot*, or *failure*.
2. **Freshness contract** — how long the facts are current, and what the consumer may
   do when a refresh fails.
3. **Verifiable provenance** — a signature a relying party can check offline.

## 1. Coverage tri-state (normative)

Every per-source entry MUST carry exactly one of three states. No synonyms, no fourth
state:

| State | Meaning |
|---|---|
| `observed_clean` | The source looked and holds a real observation of this node. |
| `never_observed` | The source has nothing to say — wrong node type for this source, or it looked and the node is not in its corpus. This is a correct answer, not a placeholder. An honest blind spot MUST NOT be dressed up as an observation or a failure. |
| `degraded` | The source SHOULD have answered but errored, timed out, or is misconfigured — or returned an observation with no recency window where one is required. A producer MUST NOT invent a value to avoid this state. |

**Classification rules (normative):**

- A missing or non-object source block → `degraded`, reason `lens_missing`.
- A block reporting `available: false` with a failure-class reason (timeout, HTTP 5xx,
  backend error, misconfiguration) → `degraded`. Any other `available: false` reason →
  `never_observed`. Producers SHOULD match the failure side (a short, enumerable set)
  rather than trying to enumerate every not-applicable reason.
- For observational sources (live sensors, as opposed to corpus lookups), an
  observation carrying neither a timestamp nor a count → `degraded`, reason
  `no_observed_window`. A count with no recency is not an observation.
- `summary` and `observed_at` MUST be `null` unless the state is `observed_clean`.

## 2. Wire shape

### 2.1 The `coverage` block

```jsonc
{
  "coverage": {
    "lenses": [
      {
        "lens": "scry",                        // producer-defined source id
        "check": "exit-node observations",     // what this source EXAMINES — self-describing, locked string
        "state": "observed_clean",
        "summary": "3 obs · scanner",          // human-scale digest of the observation, null unless observed_clean
        "observed_at": "2026-07-24T11:02:00Z"  // per-node recency when the source has one, else null
      },
      {
        "lens": "tracker",
        "check": "demand-side graph",
        "state": "never_observed",
        "summary": null,
        "observed_at": null,
        "reason": "not_in_corpus"              // present when state != observed_clean
      }
      // ... one entry per source, fixed order, ALWAYS all sources — a consumer
      // must never have to infer a missing source's state from its absence.
    ],
    "rollup": { "observed_clean": 3, "never_observed": 1, "degraded": 0 },
    "valid_until": "2026-07-26T06:05:00Z",     // when a consumer should re-poll
    "stale_if_error": 86400,                   // seconds the consumer MAY keep using this bundle if a re-poll fails
    "key_id": "tm-2026-06"                     // which published signing key backs the receipt
  }
}
```

**Freshness semantics (normative):**

- `valid_until` — RFC 3339 UTC. Until this instant the bundle is current; after it, a
  consumer SHOULD re-poll before relying on it.
- `stale_if_error` — integer seconds, deliberately longer than the `valid_until`
  window. If a re-poll fails, the consumer MAY keep using the last good bundle for up
  to this long instead of failing closed. Availability during an upstream blip beats a
  hard error — and the producer saying so explicitly is part of the contract.
- `key_id` — names the Ed25519 key (see the
  [published key bundle](https://tunnelmind.ai/.well-known/receipt-signing-key.json))
  that signs the receipt carrying this bundle. The rollup + freshness fields are
  committed inside the signed receipt payload, making coverage claims tamper-evident.

### 2.2 Streaming form (NDJSON)

Producers that stream results emit one JSON object per line. Per-source events carry
the coverage state inline so a consumer can act before the bundle completes:

```
{"event":"lens_result","lens":"scry","coverage_state":"observed_clean","observed_value":"3 obs · scanner · Familiar SFO-2","observed_at":"2026-07-24T11:02:00Z"}
{"event":"lens_result","lens":"sigil","coverage_state":"observed_clean","observed_value":"1204 edges · 2 unauthorized","observed_at":null}
{"event":"verdict","coverage":{...full block as §2.1...},"sig":"ed25519:9f3a…c17b"}
```

Ordering is NOT guaranteed — events are keyed by `lens`, and a consumer MUST tolerate
out-of-order arrival. The conformance vector linked above exercises this.

## 3. What this bundle is not

- **Not a trust score.** The producer reports what it saw, how completely, how fresh,
  and signs it. Whether the node is *trusted* is the PDP's decision, made under the
  consumer's own policy and risk tolerance.
- **Not best-effort JSON.** Every field above is load-bearing; a producer that cannot
  fill one truthfully MUST use the honest state (`never_observed` / `degraded`), never
  a guess.

## 4. Consuming from a PDP

OPA/Rego example — gate on coverage instead of trusting a score:

```rego
package agentgate

import rego.v1

# Fetch the bundle (or use OPA's external-data replication with the same shape)
bundle := http.send({
    "method": "POST",
    "url": sprintf("https://data.tunnelmind.ai/v1/verify/%s", [input.node]),
    "timeout": "5s",
}).body.data

# Deny on any degraded source — the PIP itself told us it failed
deny contains msg if {
    some l in bundle.coverage.lenses
    l.state == "degraded"
    msg := sprintf("PIP degraded on %s (%s)", [l.lens, l.reason])
}

# Require the sources YOUR policy cares about to have actually observed the node
required := {"scry", "ghostroute"}
deny contains msg if {
    some name in required
    some l in bundle.coverage.lenses
    l.lens == name
    l.state == "never_observed"
    msg := sprintf("no observation for required source %s", [name])
}

# Respect the freshness contract
deny contains "bundle expired" if {
    time.parse_rfc3339_ns(bundle.coverage.valid_until) < time.now_ns()
}
```

## 5. Conformance

A producer conforms if:

1. Every bundle carries all sources, each in exactly one tri-state, ordered and named
   consistently across responses.
2. `summary`/`observed_at` are null outside `observed_clean`; `reason` present outside
   `observed_clean`.
3. `valid_until`, `stale_if_error`, `key_id` are always present.
4. The rollup counts equal the per-source states.
5. Streamed events reconcile exactly with the final bundle (same states, keyed by
   source, order-independent).

The published vector (`verify-203.0.113.45.ndjson`, TEST-NET-3 documentation address)
is a complete worked example: real check strings, out-of-order emission, and a signed
bundle close. Verify receipts offline with
[`@tunnelmindai/receipt-verify`](https://www.npmjs.com/package/@tunnelmindai/receipt-verify).

## 6. Comment

Issues and discussion: `standards@tunnelmind.ai` — subject `attribute-bundle`.
Window closes 2026-10-24.
