observed_clean / never_observed /
degraded) and the freshness fields (valid_until,
stale_if_error, key_id) are the contract; producers add
sources without breaking consumers.
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 — 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 —
- Coverage honesty — for every source, whether the PIP actually looked, and whether "no data" means clean, blind spot, or failure.
- Freshness contract — how long the facts are current, and what the consumer may do when a refresh fails.
- 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, reasonlens_missing. - A block reporting
available: falsewith a failure-class reason (timeout, HTTP 5xx, backend error, misconfiguration) →degraded. Any otheravailable: falsereason →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, reasonno_observed_window. A count with no recency is not an observation. summaryandobserved_atMUST benullunless the state isobserved_clean.
2. Wire shape
2.1 The coverage block
{
"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 thevalid_untilwindow. 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) 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:
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:
- Every bundle carries all sources, each in exactly one tri-state, ordered and named consistently across responses.
summary/observed_atare null outsideobserved_clean;reasonpresent outsideobserved_clean.valid_until,stale_if_error,key_idare always present.- The rollup counts equal the per-source states.
- 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.
6. Comment
Issues and discussion: [email protected] — subject attribute-bundle.
Window closes 2026-10-24.
POST data.tunnelmind.ai/v1/verify/{node}
(REST + NDJSON stream) — the coverage block ships on every verdict and is
committed inside the signed receipt payload. Background:
the PIP whitepaper.Verify receipts offline with @tunnelmindai/receipt-verify (Apache-2.0).
Discuss via the standards hub at /standards. Public comment ends 2026-10-24.