Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Thi
## [Unreleased]

### Added
- DeviceService contract v0.1 (FOG & CITIZEN PLANE W8.7 — the estate's first southbound device abstraction): `DeviceProfile` (what a device IS — protocol, typed protocol binding, and the exact metric set it produces with units, value types, operating ranges and protocol-native source addresses; digest-pinned by a `definitionDigest` the validator RECOMPUTES) and `DeviceReading` (one observation — deviceRef, metric, value, unit, quality, observedAt, carried on the MPCC `ConversationEvent` envelope with byte-identical sub-schemas, parity machine-enforced). EdgeX Foundry's lesson landed as a contract: ONE southbound interface, N protocol drivers — a driver speaks a protocol, it does not invent a vocabulary. **Normative invariant — a reading is ATTRIBUTABLE OR IT IS NOTHING**: `deviceRef`, `deviceProfileRef`, `profileDigest`, `metric`, `sourceAddress` and `unit` are all required, and the validator resolves every one of them across the example set (profile exists, digest matches the recomputed declaration, metric is declared, unit and source address agree, value is of the declared type and inside the declared range, `provenanceLinks` independently name both device and profile). Digest-pinning closes the retroactive-legalisation path — widening a range after the fact produces a new digest and orphans the readings it was meant to legalise, the `UpdateHealthProbe` construct applied to metrology. Quality is closed at five values with `unavailable` schema-bound to a null value plus a `NullAbsenceRecord` reference (the existing 12-kind MPCC absence taxonomy is reused, not re-invented); `stale` and `substituted` are normatively not `ok`. Simulated devices are a first-class labelled member of the protocol taxonomy (`protocol: "virtual"` ⇒ `synthetic:simulated-device`, enforced in both directions) — the `model-generated` admissibility rule applied to sensors. `observedAt`/`receivedAt`/`wallTime` stay distinct because `observedAt` → `receivedAt` is the southbound latency a twin's sync budget is spent on. Read-only at v0.1 (`access` closed to `"read"`): actuation must travel the `EffectRequest` → `EffectDecision` lifecycle. Distinct from `DeviceIdentity` (operator-workstation trust, bound via `identityRef`) and `TelemetryEvent` (agent-session diagnostics). Includes a BLE-GATT and a virtual profile plus measured and typed-absence reading examples, twenty-one negative conformance vectors (`fixtures/device-service/conformance.json`), a `validate-device-service-examples` target, and a normative spec note (`specs/device-service-contract.md`). Reference implementation: `device-service` in `SocioProphet/prophet-platform`.
- A/B fallback update contract v0.1 (TRUST FABRIC W9.2): `UpdateSlot` (one of exactly two slots, carrying the GPT priority-boot attribute triple `bootPriority`/`triesRemaining`/`successful`, the installed payload digest, and a deliberate `role` vs `currentlyRunning` split so the fallback slot stays describable during a trial boot), `UpdateTransaction` (one apply attempt: write target, pinned probe, per-attempt boot record naming where each failure fell back to, terminal `promoted`/`rolled-back`/`refused` outcome with a closed `rollbackReason` set), and `UpdateHealthProbe` (the digest-pinned promotion gate: non-empty check set with at least one blocking check, mandatory hardware+software watchdog pair enforced by two `contains` clauses, `evaluatedIn` closed to post-boot userspace, `onProbeUnavailable` closed to `fail`). **Normative invariant — the currently-good slot is never overwritten by the update being applied** — enforced three ways: by schema within a document (a top-level `not`/`anyOf` enumerating the two illegal `(fromSlot, toSlot)` pairs, plus `UpdateSlot`'s `state: writing` ⇒ `role: candidate`), by schema on the settle path (four if/then clauses pinning `settledOnSlot` to `fromSlot` on rollback/refusal and to `toSlot` on promotion), and across documents by the validator (a settled transaction's `preservedPayloadDigest` still equals the active slot's `payloadDigest`). The attempt budget follows the GPT attribute — decremented by the bootloader *before* control transfers, so a payload that hangs before userspace still consumes an attempt — and `refused` is terminal, which is what ends the boot loop. Includes a refused-update and a promoted-update example set telling two coherent end-to-end stories, fourteen negative conformance vectors (`fixtures/ab-update/conformance.json`), a `validate-ab-update-examples` target whose probe-digest check is *recomputed* rather than read back, and a normative spec note (`specs/ab-fallback-update-contract.md`). Reference implementation: `AbUpdateMachine` in `sourceos-boot`.
- `make validate` now also runs the duplicate schema `$id` guardrail, which previously ran only in CI — a local run could not reproduce the check that gates the PR.
- KnowledgeNugget contract v0.1 (the estate's L2 content grain): warrant-typed knowledge fragments generalizing the production IFM warrant-typed extraction — content-addressed source spans (`docRef` + span + `sha256-` content hash), a closed four-kind warrant taxonomy (`direct-quote`/`computed`/`inferred`/`model-generated`) with evidence refs and confidence, ontology type refs (`kkoTypeRefs`), normalized `canonicalPayload`, typed provenance chain links, and `wallTime`/`logicalTime` carried verbatim from the MPCC `ConversationEvent` envelope (parity machine-enforced). Normative: `model-generated` nuggets MUST stay visibly distinguishable downstream (admissibility discounting), and `computed`/`inferred` warrants must cite evidence (schema-enforced). Includes direct-quote and model-generated examples, negative conformance vectors (`fixtures/knowledge-nugget/conformance.json`), a `validate-knowledge-nugget-examples` target, and a normative spec note (`specs/knowledge-nugget-contract.md`).
Expand Down
8 changes: 6 additions & 2 deletions Makefile
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
.PHONY: validate validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-duplicate-schema-ids
.PHONY: validate validate-value-type validate-source-locator validate-sourceos-repo-manifest validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids

validate: validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-duplicate-schema-ids validate-value-type validate-source-locator validate-sourceos-repo-manifest
validate: validate-control-plane-examples validate-nlboot-examples validate-lattice-data-governai-examples validate-ops-history-examples validate-runtime-observability-examples validate-interpretability-examples validate-lifecycle-boundary-examples validate-svf-contracts validate-sync-cycle-receipts validate-onboarding-examples validate-runtime-causality-examples validate-agentic-os-examples validate-triparty-examples validate-labor-market-examples validate-supply-chain-risk-examples validate-reasoning-examples validate-mpcc-event-examples validate-knowledge-nugget-examples validate-semantic-action-examples validate-epistemic-kernel-examples validate-ab-update-examples validate-device-service-examples validate-duplicate-schema-ids validate-value-type validate-source-locator validate-sourceos-repo-manifest
@echo "OK: validate"

validate-source-locator:
Expand All@@ -17,6 +17,10 @@ validate-ab-update-examples:
# but was not reachable from `make validate`, so a local run could not reproduce
# the check that gates the PR. Wiring it in costs nothing and removes a way for
# local and CI verdicts to disagree.
validate-device-service-examples:
python3 -m pip install --user jsonschema >/dev/null
python3 tools/validate_device_service_examples.py

validate-duplicate-schema-ids:
python3 scripts/check_duplicate_schema_ids.py

Expand Down
81 changes: 81 additions & 0 deletions examples/device_profile.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
{
"id": "urn:srcos:device-profile:acme_th100_room_sensor_v1",
"type": "DeviceProfile",
"specVersion": "0.1.0",
"actorRef": "urn:srcos:agent:device_service",
"workspaceRef": "urn:srcos:workspace:citizen_home_demo",
"visibilityScope": [
"private"
],
"provenanceLinks": [
{
"rel": "declared_from",
"ref": "datasheet:acme-th-100-rev-c"
},
{
"rel": "conforms_to",
"ref": "org.bluetooth.service.environmental_sensing"
}
],
"policyLabels": [
"residence:interior",
"pii:none"
],
"riskLabels": [],
"deviceClass": "acme-th-100-room-sensor",
"manufacturer": "Acme Instruments",
"model": "TH-100",
"protocol": "ble-gatt",
"protocolBinding": {
"endpoint": "ble://C4:2F:90:11:8A:3D",
"securityMode": "pairing-bonded",
"parameters": {
"connectionIntervalMs": 1000,
"notifyOnly": true,
"gattService": "0000181a-0000-1000-8000-00805f9b34fb"
}
},
"metrics": [
{
"metric": "temperature",
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"valueType": "number",
"unit": "Cel",
"minimum": -40.0,
"maximum": 85.0,
"resolution": 0.01,
"samplePeriodMs": 1000,
"sourceAddress": "gatt://0000181a-0000-1000-8000-00805f9b34fb/00002a6e-0000-1000-8000-00805f9b34fb",
"access": "read"
},
{
"metric": "humidity.relative",
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"valueType": "number",
"unit": "%",
"minimum": 0.0,
"maximum": 100.0,
"resolution": 0.01,
"samplePeriodMs": 1000,
"sourceAddress": "gatt://0000181a-0000-1000-8000-00805f9b34fb/00002a6f-0000-1000-8000-00805f9b34fb",
"access": "read"
},
{
"metric": "battery.level",
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"valueType": "integer",
"unit": "%",
"minimum": 0,
"maximum": 100,
"resolution": 1,
"samplePeriodMs": 300000,
"sourceAddress": "gatt://0000180f-0000-1000-8000-00805f9b34fb/00002a19-0000-1000-8000-00805f9b34fb",
"access": "read"
}
],
"declaredAt": "2026-07-29T08:00:00.000Z",
"identityRef": "urn:srcos:device-identity:home_edge_node_01",
"adminState": "enabled",
"supersedesRef": null,
"definitionDigest": "sha256:24dfe44e340bc684c2c5618cfad534717c6b1f218b6bf7e94aa809afecab2813"
}
77 changes: 77 additions & 0 deletions examples/device_profile.virtual.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
{
"id": "urn:srcos:device-profile:srcos_virtual_room_sensor_v1",
"type": "DeviceProfile",
"specVersion": "0.1.0",
"actorRef": "urn:srcos:agent:device_service",
"workspaceRef": "urn:srcos:workspace:citizen_home_demo",
"visibilityScope": [
"private"
],
"provenanceLinks": [
{
"rel": "declared_from",
"ref": "prophet-platform:apps/device-service/profiles/virtual-room-sensor.json"
}
],
"policyLabels": [
"synthetic:simulated-device"
],
"riskLabels": [
"not-a-measurement"
],
"deviceClass": "srcos-virtual-room-sensor",
"manufacturer": "SourceOS",
"model": "virtual-room-sensor",
"protocol": "virtual",
"protocolBinding": {
"endpoint": "virtual://room-sensor",
"securityMode": "none",
"parameters": {
"seed": 20260729,
"samplePeriodMs": 1000
}
},
"metrics": [
{
"metric": "temperature",
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"valueType": "number",
"unit": "Cel",
"minimum": 5.0,
"maximum": 40.0,
"resolution": 0.01,
"samplePeriodMs": 1000,
"sourceAddress": "virtual://room-sensor/temperature",
"access": "read"
},
{
"metric": "humidity.relative",
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"valueType": "number",
"unit": "%",
"minimum": 10.0,
"maximum": 90.0,
"resolution": 0.01,
"samplePeriodMs": 1000,
"sourceAddress": "virtual://room-sensor/humidity.relative",
"access": "read"
},
{
"metric": "occupancy.detected",
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#States",
"valueType": "boolean",
"unit": "{bool}",
"minimum": null,
"maximum": null,
"resolution": null,
"samplePeriodMs": 1000,
"sourceAddress": "virtual://room-sensor/occupancy.detected",
"access": "read"
}
],
"declaredAt": "2026-07-29T08:00:00.000Z",
"identityRef": "urn:srcos:device-identity:home_edge_node_01",
"adminState": "enabled",
"supersedesRef": null,
"definitionDigest": "sha256:55d51880fb7d938c7d87f0a8190b6d1499ba2c00291dbe55b710be5fc93e6666"
}
57 changes: 57 additions & 0 deletions examples/device_reading.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
{
"id": "urn:srcos:device-reading:room_sensor_01_temperature_000117",
"type": "DeviceReading",
"specVersion": "0.1.0",
"actorRef": "urn:srcos:agent:device_service",
"workspaceRef": "urn:srcos:workspace:citizen_home_demo",
"branchRef": "urn:srcos:branch:home_main",
"visibilityScope": [
"private"
],
"wallTime": "2026-07-29T09:15:00.042Z",
"logicalTime": 117,
"causalParents": [],
"traceContext": {
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7"
},
"provenanceLinks": [
{
"rel": "declared_by",
"ref": "urn:srcos:device-profile:srcos_virtual_room_sensor_v1"
},
{
"rel": "produced_by",
"ref": "urn:srcos:device:room_sensor_01"
},
{
"rel": "ingested_by",
"ref": "prophet-platform:apps/device-service"
}
],
"policyLabels": [
"synthetic:simulated-device"
],
"riskLabels": [
"not-a-measurement"
],
"deviceRef": "urn:srcos:device:room_sensor_01",
"deviceProfileRef": "urn:srcos:device-profile:srcos_virtual_room_sensor_v1",
"profileDigest": "sha256:55d51880fb7d938c7d87f0a8190b6d1499ba2c00291dbe55b710be5fc93e6666",
"metric": "temperature",
"sourceAddress": "virtual://room-sensor/temperature",
"value": 21.37,
"unit": "Cel",
"quality": "ok",
"observedAt": "2026-07-29T09:15:00.000Z",
"receivedAt": "2026-07-29T09:15:00.042Z",
"sequenceRef": 117,
"nullAbsenceRef": null,
"qualityFlags": [],
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"rawPayload": {
"driver": "virtual",
"raw": 2137,
"scale": -2
}
}
56 changes: 56 additions & 0 deletions examples/device_reading.unavailable.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
{
"id": "urn:srcos:device-reading:th100_bed2_humidity_000418",
"type": "DeviceReading",
"specVersion": "0.1.0",
"actorRef": "urn:srcos:agent:device_service",
"workspaceRef": "urn:srcos:workspace:citizen_home_demo",
"branchRef": "urn:srcos:branch:home_main",
"visibilityScope": [
"private"
],
"wallTime": "2026-07-29T09:15:03.500Z",
"logicalTime": 418,
"causalParents": [
"urn:srcos:device-reading:th100_bed2_humidity_000417"
],
"traceContext": {
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "1a2b3c4d5e6f7081"
},
"provenanceLinks": [
{
"rel": "declared_by",
"ref": "urn:srcos:device-profile:acme_th100_room_sensor_v1"
},
{
"rel": "produced_by",
"ref": "urn:srcos:device:th100_bed2"
},
{
"rel": "ingested_by",
"ref": "prophet-platform:apps/device-service"
}
],
"policyLabels": [
"residence:interior",
"pii:none"
],
"riskLabels": [],
"deviceRef": "urn:srcos:device:th100_bed2",
"deviceProfileRef": "urn:srcos:device-profile:acme_th100_room_sensor_v1",
"profileDigest": "sha256:24dfe44e340bc684c2c5618cfad534717c6b1f218b6bf7e94aa809afecab2813",
"metric": "humidity.relative",
"sourceAddress": "gatt://0000181a-0000-1000-8000-00805f9b34fb/00002a6f-0000-1000-8000-00805f9b34fb",
"value": null,
"unit": "%",
"quality": "unavailable",
"observedAt": "2026-07-29T09:15:03.000Z",
"receivedAt": "2026-07-29T09:15:03.500Z",
"sequenceRef": 418,
"nullAbsenceRef": "urn:srcos:null-absence:th100_bed2_notify_timeout_000418",
"qualityFlags": [
"notify_window_missed"
],
"kkoTypeRef": "http://kbpedia.org/ontologies/kko#Quantity",
"rawPayload": null
}
24 changes: 24 additions & 0 deletions examples/null_absence_record.device_timeout.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
{
"id": "urn:srcos:null-absence:th100_bed2_notify_timeout_000418",
"type": "NullAbsenceRecord",
"specVersion": "0.1.0",
"kind": "timeout",
"observedAt": "2026-07-29T09:15:03.000Z",
"relatedEventRef": "urn:srcos:device-reading:th100_bed2_humidity_000418",
"relatedBranchRef": "urn:srcos:branch:home_main",
"relatedWorkspaceRef": "urn:srcos:workspace:citizen_home_demo",
"causalNotes": "GATT notification window for 00002a6f elapsed with no indication; the peripheral stayed connected, so this is a timeout and not a transport_failure. Recorded as a typed absence rather than re-reporting the prior humidity value, which would have been substituted data wearing an ok quality.",
"policyLabels": [
"residence:interior"
],
"provenanceLinks": [
{
"rel": "detected_by",
"ref": "prophet-platform:apps/device-service#ble-gatt"
}
],
"details": {
"expectedNextSequence": 419,
"notifyWindowMs": 3000
}
}
Loading
Loading