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
91 changes: 83 additions & 8 deletions apps/pdf-viewer-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ <h2>Feature Gate Demo</h2>
<button id="btn-pdf-sign" class="btn-deny" disabled>Sign PDF</button>
<span id="gate-pdf-sign"></span>
</div>
<div class="feature-block">
<button id="btn-pdf-export" class="btn-deny" disabled>Export PDF (denied by policy)</button>
<span id="gate-pdf-export"></span>
</div>
<div id="feature-output"></div>
</div>

Expand Down Expand Up @@ -187,7 +191,17 @@ <h2>Feature Gate Demo</h2>
request(id, owner, opts = {}) { return this._emit(id, 'requested', owner, opts); }
negotiate(id, owner, opts = {}) { return this._emit(id, 'negotiating', owner, opts); }
setAvailable(id, owner, opts = {}) { return this._emit(id, 'available', owner, opts); }
enable(id, owner, pdr = null, ev = []) { return this._emit(id, 'enabled', owner, { policyDecisionRef: pdr, evidenceRefs: ev }); }
// enable REQUIRES a policyDecisionRef — see packages/capability-ledger/src/index.js:
// 'enabled' is the only state that authorises use, so recording it without naming
// the decision that authorised it produces a grant indistinguishable from an
// adjudicated one. The inline copy previously accepted `pdr = null`, drifting from
// the packaged behaviour it exists to mirror.
enable(id, owner, pdr, ev = []) {
if (typeof pdr !== 'string' || pdr.trim() === '') {
throw new Error(`capability "${id}" cannot be enabled without a policyDecisionRef`);
}
return this._emit(id, 'enabled', owner, { policyDecisionRef: pdr, evidenceRefs: ev });
}
deny(id, owner, pdr = null, ev = []) { return this._emit(id, 'blocked_by_policy', owner, { policyDecisionRef: pdr, evidenceRefs: ev }); }
degrade(id, owner, ev = []) { return this._emit(id, 'degraded', owner, { evidenceRefs: ev }); }
setUnsupportedByRuntime(id, owner, ev = []) { return this._emit(id, 'unsupported_by_runtime', owner, { evidenceRefs: ev }); }
Expand All @@ -214,7 +228,27 @@ <h2>Feature Gate Demo</h2>
getState(id) { return this._receipts.get(id)?.state ?? null; }
getReceipt(id) { return this._receipts.get(id) ?? null; }
getAll() { return Array.from(this._receipts.values()); }

// isEnabled returns a value a caller may ignore; every caller that forgets it
// fails OPEN. Left in for UI-state readouts (button styling, gate label) where
// the boolean IS the answer being asked. For actual use-gates, prefer
// assertEnabled, which throws and cannot be silently ignored.
isEnabled(id) { return this.getState(id) === 'enabled'; }

// Fail-closed gate — parity with packages/capability-ledger/src/index.js.
// Distinguishes 'blocked_by_policy' (deny receipt exists) from 'not in ledger',
// so a refusal is diagnosable and cites its policyDecisionRef when there is one.
assertEnabled(id) {
const state = this.getState(id);
if (state === 'enabled') return this.getReceipt(id);
const receipt = this.getReceipt(id);
const because = receipt?.policyDecisionRef ? ` (policy decision: ${receipt.policyDecisionRef})` : '';
throw new Error(
state == null
? `capability "${id}" is not in the ledger; refusing use of an undeclared capability`
: `capability "${id}" is "${state}", not "enabled"${because}; refusing use`,
);
}
}

// ── Bootstrap ledger with PDF viewer demo capabilities ────────────────
Expand All @@ -239,6 +273,16 @@ <h2>Feature Gate Demo</h2>
['plugin:ink-sign:not-installed'],
);

// pdf-export: denied by policy. Its click handler exercises the case the
// register item was open on: a deny() receipt exists, isEnabled reports false
// and drops silently, assertEnabled throws citing the policyDecisionRef.
ledger.declare('pdf-export', 'UI');
ledger.deny(
'pdf-export', 'policy',
'policy:deny-pdf-export:dlp-v2',
['audit:dlp-review:2026-07'],
);

// live-collab: unsupported by server
ledger.declare('live-collab', 'UI');
ledger.setUnsupportedByServer(
Expand Down Expand Up @@ -305,27 +349,58 @@ <h2>Feature Gate Demo</h2>
const enabled = ledger.isEnabled(capabilityId);
const state = ledger.getState(capabilityId) ?? 'unknown';

btn.disabled = !enabled;
// Buttons stay clickable even when the capability is not enabled: the
// enforcement is at the click site (assertEnabled), and letting the click
// fire is what surfaces the diagnostic — the point of closing the
// "deny records but blocks nothing" register item.
btn.disabled = false;
btn.className = enabled ? 'btn-ok' : 'btn-deny';
gate.textContent = enabled
? '✓ enabled'
: `✗ blocked — state: ${state}`;
gate.style.color = enabled ? 'var(--color-enabled)' : 'var(--color-blocked)';
}

updateGate('pdf-viewer', 'btn-pdf-view', 'gate-pdf-view');
updateGate('pdf-sign', 'btn-pdf-sign', 'gate-pdf-sign');
updateGate('pdf-viewer', 'btn-pdf-view', 'gate-pdf-view');
updateGate('pdf-sign', 'btn-pdf-sign', 'gate-pdf-sign');
updateGate('pdf-export', 'btn-pdf-export', 'gate-pdf-export');

const output = document.getElementById('feature-output');

// Use assertEnabled at the actual use-gate: an isEnabled check a caller can ignore
// is the same defect as no check. ledger.deny(...) records a 'blocked_by_policy'
// receipt, which isEnabled reports as false and drops silently — a policy refusal
// indistinguishable from a not-yet-declared capability. assertEnabled distinguishes
// them and cites the policyDecisionRef when there is one.
function gatedClick(capabilityId, onGranted) {
try {
ledger.assertEnabled(capabilityId);
onGranted();
} catch (err) {
output.textContent = `⛔ ${err.message}`;
output.style.color = 'var(--color-blocked)';
}
}

document.getElementById('btn-pdf-view').addEventListener('click', () => {
if (!ledger.isEnabled('pdf-viewer')) return;
output.textContent = '📄 PDF viewer opened (capability confirmed enabled by ledger).';
gatedClick('pdf-viewer', () => {
output.textContent = '📄 PDF viewer opened (capability confirmed enabled by ledger).';
output.style.color = '';
});
});

document.getElementById('btn-pdf-sign').addEventListener('click', () => {
if (!ledger.isEnabled('pdf-sign')) return;
output.textContent = '✍ PDF signed.';
gatedClick('pdf-sign', () => {
output.textContent = '✍ PDF signed.';
output.style.color = '';
});
});

document.getElementById('btn-pdf-export').addEventListener('click', () => {
gatedClick('pdf-export', () => {
output.textContent = '📤 PDF exported.';
output.style.color = '';
});
});
</script>
</body>
Expand Down
63 changes: 63 additions & 0 deletions schemas/mesh-transfer.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.sourceos.ai/sourceos-shell/mesh-transfer.schema.json",
"title": "MeshTransfer",
"description": "A single cross-device transfer over the SourceOS personal mesh (E3): Handoff, AirDrop-style drop, AirPlay-style cast, message, or a tenant sync delta. Every transfer is an egress-purpose act and MUST carry the consent decision that admitted it plus a hash-sealed receipt. See docs/contract-additions/e3-mesh-transport.md in sourceos-spec.",
"type": "object",
"additionalProperties": false,
"required": [
"transferId",
"specVersion",
"kind",
"fromDevice",
"toDevice",
"purpose",
"payloadClass",
"consent",
"receipt"
],
"properties": {
"transferId": { "type": "string", "pattern": "^urn:srcos:mesh-transfer:" },
"specVersion": { "type": "string", "const": "0.1" },
"kind": {
"type": "string",
"enum": ["handoff", "drop", "cast", "message", "sync"],
"description": "handoff=app-state continuation; drop=file/clipboard (AirDrop parity); cast=A/V stream (AirPlay parity); message=Matrix substrate; sync=memory-mesh/hellgraph-federated delta."
},
"fromDevice": { "type": "string", "pattern": "^urn:srcos:device:" },
"toDevice": { "type": "string", "pattern": "^urn:srcos:device:" },
"purpose": {
"type": "string",
"const": "egress",
"description": "A cross-device transfer always crosses a device boundary; its consent purpose is always egress."
},
"payloadClass": {
"type": "string",
"enum": ["clipboard", "file", "app-state", "av-stream", "graph-delta", "memory-delta"]
},
"payloadRef": { "type": "string", "description": "Opaque handle to the payload; the transport moves ciphertext, this record never inlines content." },
"consent": {
"type": "object",
"additionalProperties": false,
"required": ["policyDecisionRef", "grantedBy", "space"],
"properties": {
"policyDecisionRef": {
"type": "string",
"pattern": "^urn:srcos:policy-decision:",
"description": "The purpose_admissibility_gate decision that admitted this egress. Absent => refused fail-closed."
},
"grantedBy": { "type": "string", "description": "Authority that consented (human:* or a delegated agent grant)." },
"space": {
"type": "string",
"enum": ["kernel-space", "system-space", "user-space", "agent-space", "data-namespace"]
},
"region": { "type": "string", "description": "Jurisdiction toleration presented (GDPR Ch. V). Required when space=data-namespace." }
}
},
"receipt": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{8,64}$",
"description": "AutonomyAdmissionReceipt seal over this transfer. Absent => not receipted => refused."
}
}
}
68 changes: 68 additions & 0 deletions tests/test_mesh_transfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from tools.mesh_transfer import admit


def _ok(**over):
t = {
"transferId": "urn:srcos:mesh-transfer:1",
"specVersion": "0.1",
"kind": "drop",
"fromDevice": "urn:srcos:device:laptop",
"toDevice": "urn:srcos:device:phone",
"purpose": "egress",
"payloadClass": "file",
"consent": {
"policyDecisionRef": "urn:srcos:policy-decision:abc",
"grantedBy": "human:michael",
"space": "user-space",
},
"receipt": "sha256:deadbeef",
}
t.update(over)
return t


def test_admits_a_fully_consented_receipted_drop():
d = admit(_ok())
assert d.allow is True
assert "admitted" in d.reason


def test_denies_missing_policy_decision():
t = _ok()
del t["consent"]["policyDecisionRef"]
assert admit(t).allow is False


def test_denies_missing_receipt():
assert admit(_ok(receipt=None)).allow is False
assert admit(_ok(receipt="notasha")).allow is False


def test_denies_non_egress_purpose():
assert admit(_ok(purpose="operate")).allow is False


def test_denies_unknown_kind():
assert admit(_ok(kind="teleport")).allow is False


def test_denies_same_device():
assert admit(_ok(toDevice="urn:srcos:device:laptop")).allow is False


def test_denies_non_device_endpoint():
assert admit(_ok(fromDevice="laptop")).allow is False


def test_data_namespace_requires_region():
t = _ok()
t["consent"]["space"] = "data-namespace"
assert admit(t).allow is False # no region
t["consent"]["region"] = "EU"
assert admit(t).allow is True


def test_denies_empty_and_garbage():
assert admit({}).allow is False
assert admit(None).allow is False
assert admit("nope").allow is False
80 changes: 80 additions & 0 deletions tools/mesh_transfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Fail-closed admission for personal-mesh transfers (E3).

A MeshTransfer (Handoff / drop / cast / message / sync) crosses a device boundary,
so it is always an ``egress``-purpose act. Following the shell's fail-closed
convention (a gate that grants only on an explicit policy decision — see
sourceos-shell#31), a transfer is admitted ONLY when it carries the consent
decision that admitted the egress AND a hash-sealed receipt. Anything missing,
malformed, or contradictory is refused; there is no "just this once" bypass.

Pure/stdlib-only so it validates the same way in CI, on device, and in tests.
The receiver surfaces a refusal as a pending prompt in the E11 pane rather than
silently succeeding.
"""

from __future__ import annotations

from typing import Any, Dict, NamedTuple

KINDS = {"handoff", "drop", "cast", "message", "sync"}
SPACES = {"kernel-space", "system-space", "user-space", "agent-space", "data-namespace"}


class Decision(NamedTuple):
allow: bool
reason: str


def _sealed(receipt: Any) -> bool:
return (
isinstance(receipt, str)
and receipt.startswith("sha256:")
and 8 <= len(receipt) - len("sha256:") <= 64
and all(c in "0123456789abcdef" for c in receipt[len("sha256:"):])
)


def admit(transfer: Dict[str, Any]) -> Decision:
"""Return a fail-closed Decision for one MeshTransfer record.

Deny (never raise) on any missing/contradictory field so a caller can only
proceed on an explicit ``allow``.
"""
if not isinstance(transfer, dict):
return Decision(False, "transfer is not an object")

kind = transfer.get("kind")
if kind not in KINDS:
return Decision(False, f"unknown transfer kind: {kind!r}")

# A cross-device transfer is egress by construction; refuse anything claiming otherwise.
if transfer.get("purpose") != "egress":
return Decision(False, "transfer purpose must be 'egress' (crosses a device boundary)")

for endpoint in ("fromDevice", "toDevice"):
val = transfer.get(endpoint)
if not (isinstance(val, str) and val.startswith("urn:srcos:device:")):
return Decision(False, f"{endpoint} is not a device URN")
if transfer["fromDevice"] == transfer["toDevice"]:
return Decision(False, "fromDevice == toDevice (not a transfer)")

consent = transfer.get("consent")
if not isinstance(consent, dict):
return Decision(False, "no consent block — refused fail-closed")

ref = consent.get("policyDecisionRef")
if not (isinstance(ref, str) and ref.startswith("urn:srcos:policy-decision:")):
return Decision(False, "no admitting policy decision — refused fail-closed")

space = consent.get("space")
if space not in SPACES:
return Decision(False, f"unknown consent space: {space!r}")

# GDPR Ch. V: leaving the data-namespace needs an explicit region toleration.
if space == "data-namespace" and not consent.get("region"):
return Decision(False, "data-namespace egress requires a region toleration (Ch. V)")

if not _sealed(transfer.get("receipt")):
return Decision(False, "no valid receipt seal — not receipted => refused")

return Decision(True, f"admitted: {kind} egress from {space} (decision {ref})")
Loading