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
13 changes: 12 additions & 1 deletion .github/workflows/python-wire-runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
- "independent/python/wire_runtime.py"
- "independent/python/WIRE_RUNTIME.md"
- "scripts/check-python-wire-runtime.mjs"
- "protocol/wireProtocol.js"
- "schema/wire-envelope.schema.json"
- "independent/python/verify_evidence_capsule.py"
- "scripts/check-evidence-capsule-python.mjs"
- "independent/python/verify_evidence_archive.py"
Expand Down Expand Up @@ -61,6 +63,8 @@ on:
- "independent/python/wire_runtime.py"
- "independent/python/WIRE_RUNTIME.md"
- "scripts/check-python-wire-runtime.mjs"
- "protocol/wireProtocol.js"
- "schema/wire-envelope.schema.json"
- "independent/python/verify_evidence_capsule.py"
- "scripts/check-evidence-capsule-python.mjs"
- "independent/python/verify_evidence_archive.py"
Expand Down Expand Up @@ -125,7 +129,14 @@ jobs:
with:
python-version: "3.12"
- name: Verify reference-produced artifacts with standalone Python
run: node scripts/check-python-wire-runtime.mjs
run: node scripts/check-python-wire-runtime.mjs > "${RUNNER_TEMP}/aml-wire-envelope-cross-runtime.json"
- name: Upload wire envelope comparison
uses: actions/upload-artifact@v4
with:
name: aml-wire-envelope-cross-runtime-${{ github.sha }}
path: ${{ runner.temp }}/aml-wire-envelope-cross-runtime.json
if-no-files-found: error
retention-days: 30
- name: Install Python Ed25519 verifier dependency
run: python -m pip install 'cryptography>=42,<51'
- name: Verify evidence capsules across JavaScript and Python
Expand Down
1 change: 1 addition & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ The public trust registry is `BRAND_TRUST_ROOTS.json`. Production private key ma
- `acceptWireEnvelope(guard, envelope, options?)`

Canonical serialization and protocol vectors help independent implementations converge on the same hashes and signatures.
`validateWireEnvelope` rejects missing or unknown fields, repeated or malformed capabilities, invalid optional metadata, and malformed or expired timestamps. The portable timestamp profile requires a calendar-valid ISO date-time with an explicit timezone and at most millisecond precision. Callers that previously relied on permissive acceptance of other shapes should normalize their envelopes before upgrading. The [JavaScript/Python comparison](independent/python/WIRE_RUNTIME.md) records the selected shared cases.

## HTTP service

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@

**A counterexample made the contract stronger:** [Inspect the key-order lab](https://aruintelligence.github.io/aml-core/key-order-lab.html?case=two-numeric-keys). Numeric-looking JSON keys exposed a real proposal-hash disagreement between the project's JavaScript and Python code paths. The Python wire order is fixed without changing existing JavaScript digests. A deterministic, deliberately adversarial 519-case regression records 439 mismatches under the prior Python ordering and zero after the fix; the seven selected examples show exact old and new bytes. This remains project-maintained evidence for a restricted JSON subset.

The [wire-envelope cross-runtime check](independent/python/WIRE_RUNTIME.md) now compares 19 selected valid and malformed cases in JavaScript and Python. It catches extra fields, duplicate capabilities, and ambiguous timestamps before applications accept a session; CI publishes the compact comparison report. The numeric canonicalization limits described there still apply.

[![One action produced two hashes when Python sorted numeric-looking keys differently from JavaScript; the fixed Python path now matches.](docs/key-order-counterexample.svg)](https://aruintelligence.github.io/aml-core/key-order-lab.html)

**A spent grant must stay spent:** [Watch the local replay counterexample](https://aruintelligence.github.io/aml-core/grant-directory-lab.html?scenario=writable-root). A misconfigured writable directory let the former one-use file store accept the same test grant twice after its claim directory was replaced. The store now rejects weak ownership, permissions, and symlinks before issuance or consumption. Four before/after scenarios run the pinned historical module and corrected module in CI. This project-authored POSIX simulation does not involve a second operating-system user or real tool side effect.
Expand Down
2 changes: 2 additions & 0 deletions docs/INTEROPERABILITY_STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ The current mechanism is a hash commitment/inclusion proof. It is deliberately *

`createWireEnvelope()` wraps a typed payload in an explicit protocol/version/capability header. This creates a stable boundary for future cross-runtime transport.

`validateWireEnvelope()` now applies the required fields, unknown-field, capability uniqueness, and optional metadata checks used by the [Python wire verifier](../independent/python/WIRE_RUNTIME.md). Their 19-case mutation report is saved by CI. Both apply a calendar-valid timestamp subset with an explicit timezone and up to millisecond precision; callers with looser timestamps or extra properties must normalize them before validation. The cross-runtime result covers those fixtures, not arbitrary JSON numeric canonicalization.

## Why this matters

A standard becomes much more credible when two independently written systems can:
Expand Down
4 changes: 2 additions & 2 deletions independent/python/WIRE_RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ python3 independent/python/wire_runtime.py negotiate protocol/discovery.json pro
python3 independent/python/wire_runtime.py verify passport path/to/passport.json --now 2030-01-01T12:00:00Z
```

The first command creates artifacts with the JavaScript runtime, checks the same artifacts and mutations with Python, and removes temporary vectors. The Python module itself is standalone. Its CLI exits 0 for a valid artifact or accepted session and 1 for an invalid artifact or rejected session, with a JSON report on stdout.
The first command creates artifacts with the JavaScript runtime, compares 19 selected envelope cases against Python, checks five other artifact classes and mutations, prints a compact JSON report, and removes temporary vectors. CI uploads that report as `aml-wire-envelope-cross-runtime-<commit>`. The Python module itself is standalone. Its CLI exits 0 for a valid artifact or accepted session and 1 for an invalid artifact or rejected session, with a JSON report on stdout.

## Interoperability boundaries found

- RFC 0005 and 0006 describe object-key sorting but do not define numeric JSON serialization or the locale used for selective-disclosure claim ordering. This implementation accepts only safe integers in hashed JSON and lowercase ASCII-letter disclosure keys; other values fail explicitly. This is a documented subset, not a complete cross-language canonicalization contract.
- `aml-wire/1` JSON Schema requires a unique capability list, payload property, and no unknown fields. The JavaScript `validateWireEnvelope` helper checks only the protocol, header, allowed kind, and expiry. Python applies the stricter schema-shaped checks; this can reject an envelope the helper accepts.
- JavaScript and Python now agree on the selected envelope structure, capability, metadata, kind, and time mutation cases in `scripts/check-python-wire-runtime.mjs`. Both reject unknown fields and duplicate capabilities. The portable timestamp profile accepts a calendar-valid ISO date-time with a timezone and at most three fractional second digits. The published schema describes this subset; the validators should still be checked on any newly supported data shapes.
- The passport schema excludes extra fields, while the JavaScript verifier hashes whatever additional fields appear. Python rejects extra fields. Both implementations reject modified hash-bound preferences and expired passports on the tested subset.
- A disclosure commitment proves that the revealed values and hidden leaf hashes fit one root. It does not prove the hidden values, the holder's identity, or the truth of the claims.
- Causal graph verification proves the integrity of declared parent links and topology; it does not prove that the declared causal relationship is true.
Expand Down
15 changes: 12 additions & 3 deletions independent/python/wire_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
HEX = re.compile(r"^[0-9a-f]{64}$")
LOWER_KEY = re.compile(r"^[a-z]+$")
VERSION = re.compile(r"^[0-9]+(?:\.[0-9]+)*$")
WIRE_TIME = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,3})?(?:Z|[+-][0-9]{2}:[0-9]{2})\Z")
MAX_SAFE_INTEGER = 2**53 - 1


Expand Down Expand Up @@ -73,6 +74,14 @@ def parse_time(value):
return None


def parse_wire_time(value):
# A shared millisecond ISO subset avoids Date.parse accepting normalized
# calendar dates or Python accepting fractions JavaScript would truncate.
if not isinstance(value, str) or not WIRE_TIME.fullmatch(value):
return None
return parse_time(value)


def compare_versions(left, right):
if left == right:
return 0
Expand Down Expand Up @@ -124,13 +133,13 @@ def verify_envelope(envelope, *, now=None, allowed_kinds=()):
if envelope.get(key) is not None and not isinstance(envelope[key], str):
return failure("invalid_" + key)
for key in ("issued_at", "expires_at"):
if envelope.get(key) is not None and parse_time(envelope[key]) is None:
if envelope.get(key) is not None and parse_wire_time(envelope[key]) is None:
return failure("invalid_" + key)
if now is not None:
current = parse_time(now)
current = parse_wire_time(now)
if current is None:
return failure("invalid_now")
expiry = parse_time(envelope.get("expires_at"))
expiry = parse_wire_time(envelope.get("expires_at"))
if expiry is not None and current >= expiry:
return failure("expired")
return {"valid": True, "reason": None, "kind": envelope["kind"], "version": envelope["version"]}
Expand Down
50 changes: 47 additions & 3 deletions protocol/wireProtocol.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import { highestCommonProtocolVersion } from "./versionOrder.js";

const REQUIRED_ENVELOPE_FIELDS = ["protocol", "version", "kind", "capabilities", "payload"];
const OPTIONAL_ENVELOPE_FIELDS = ["session_id", "nonce", "issued_at", "expires_at"];
const ENVELOPE_FIELDS = new Set([...REQUIRED_ENVELOPE_FIELDS, ...OPTIONAL_ENVELOPE_FIELDS]);
const WIRE_TIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-](\d{2}):(\d{2}))$/;

function wireTimestamp(value) {
if (typeof value !== "string") return null;
const match = WIRE_TIME.exec(value);
if (!match) return null;
const [, year, month, day, hour, minute, second, , , zoneHour, zoneMinute] = match;
const y = Number(year), m = Number(month), d = Number(day);
const leap = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if (y < 1 || m < 1 || m > 12 || d < 1 || d > days[m - 1] || Number(hour) > 23 ||
Number(minute) > 59 || Number(second) > 59 ||
(zoneHour !== undefined && (Number(zoneHour) > 23 || Number(zoneMinute) > 59))) return null;
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : null;
}

export function createWireEnvelope({
kind,
payload,
Expand All @@ -25,10 +45,34 @@ export function createWireEnvelope({
}

export function validateWireEnvelope(envelope, { allowedKinds = [], now = null } = {}) {
if (!envelope || envelope.protocol !== "aml-wire/1") return { valid: false, reason: "invalid_protocol" };
if (!envelope.version || !envelope.kind) return { valid: false, reason: "missing_header" };
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope) ||
envelope.protocol !== "aml-wire/1") return { valid: false, reason: "invalid_protocol" };
if (REQUIRED_ENVELOPE_FIELDS.some(field => !Object.hasOwn(envelope, field) || envelope[field] === undefined) ||
Object.keys(envelope).some(field => !ENVELOPE_FIELDS.has(field))) {
return { valid: false, reason: "invalid_structure" };
}
if (typeof envelope.version !== "string" || !envelope.version ||
typeof envelope.kind !== "string" || !envelope.kind) return { valid: false, reason: "missing_header" };
const caps = envelope.capabilities;
if (!Array.isArray(caps) || caps.some(cap => typeof cap !== "string") ||
caps.length !== new Set(caps).size) return { valid: false, reason: "invalid_capabilities" };
if (allowedKinds.length && !allowedKinds.includes(envelope.kind)) return { valid: false, reason: "unsupported_kind" };
if (now && envelope.expires_at && new Date(now) >= new Date(envelope.expires_at)) return { valid: false, reason: "expired" };
for (const field of ["session_id", "nonce"]) {
if (envelope[field] != null && typeof envelope[field] !== "string") {
return { valid: false, reason: `invalid_${field}` };
}
}
for (const field of ["issued_at", "expires_at"]) {
if (envelope[field] != null && wireTimestamp(envelope[field]) === null) {
return { valid: false, reason: `invalid_${field}` };
}
}
if (now !== null && now !== undefined) {
const current = wireTimestamp(now);
if (current === null) return { valid: false, reason: "invalid_now" };
const expiry = envelope.expires_at == null ? null : wireTimestamp(envelope.expires_at);
if (expiry !== null && current >= expiry) return { valid: false, reason: "expired" };
}
return { valid: true, reason: null, kind: envelope.kind, version: envelope.version };
}

Expand Down
4 changes: 2 additions & 2 deletions schema/wire-envelope.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"capabilities": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
"session_id": { "type": ["string", "null"] },
"nonce": { "type": ["string", "null"] },
"issued_at": { "type": ["string", "null"], "format": "date-time" },
"expires_at": { "type": ["string", "null"], "format": "date-time" },
"issued_at": { "type": ["string", "null"], "format": "date-time", "description": "Portable verifier profile: ISO time with timezone and at most millisecond precision; validators also check calendar validity.", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?(?:Z|[+-]\\d{2}:\\d{2})$" },
"expires_at": { "type": ["string", "null"], "format": "date-time", "description": "Portable verifier profile: ISO time with timezone and at most millisecond precision; validators also check calendar validity.", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?(?:Z|[+-]\\d{2}:\\d{2})$" },
"payload": {}
},
"additionalProperties": false
Expand Down
43 changes: 39 additions & 4 deletions scripts/check-python-wire-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,41 @@ try {
assert.equal(validateWireEnvelope(badEnvelope).valid, false);
verify("envelope", "bad-envelope", badEnvelope, false);
verify("envelope", "expired-envelope", envelope, false, ["--now", "2030-01-02T00:00:00Z"]);
const extraEnvelope = { ...envelope, unregistered_field: true };
assert.equal(validateWireEnvelope(extraEnvelope).valid, true);
assert.equal(verify("envelope", "extra-envelope", extraEnvelope, false).reason, "invalid_structure");
const envelopeCases = [
["valid-offset", {}, null, { now: "2030-01-01T11:00:00-01:00" }],
["valid-empty-caps", { capabilities: [] }, null],
["extra-field", { unregistered_field: true }, "invalid_structure"],
["missing-payload", { payload: undefined }, "invalid_structure"],
["missing-caps", { capabilities: undefined }, "invalid_structure"],
["duplicate-capability", { capabilities: ["policy-passports", "policy-passports"] }, "invalid_capabilities"],
["numeric-capability", { capabilities: [1] }, "invalid_capabilities"],
["non-array-capabilities", { capabilities: "policy-passports" }, "invalid_capabilities"],
["numeric-kind", { kind: 1 }, "missing_header"],
["numeric-version", { version: 1 }, "missing_header"],
["numeric-nonce", { nonce: 1 }, "invalid_nonce"],
["numeric-session", { session_id: 1 }, "invalid_session_id"],
["invalid-issued", { issued_at: "not-a-date" }, "invalid_issued_at"],
["invalid-calendar-day", { issued_at: "2030-02-30T00:00:00Z" }, "invalid_issued_at"],
["submillisecond-expiry", { expires_at: "2030-01-02T00:00:00.0001Z" }, "invalid_expires_at"],
["invalid-zone", { expires_at: "2030-01-02T00:00:00+24:00" }, "invalid_expires_at"],
["invalid-now", {}, "invalid_now", { now: "not-a-date" }],
["expired", {}, "expired", { now: "2030-01-02T00:00:00Z" }],
["unsupported-kind", {}, "unsupported_kind", { allowedKinds: ["other-kind"] }]
];
const envelopeResults = [];
for (const [name, changes, expectedReason, options = {}] of envelopeCases) {
const candidate = { ...envelope, ...changes };
const js = validateWireEnvelope(candidate, options);
const extra = [
...(options.now ? ["--now", options.now] : []),
...(options.allowedKinds || []).flatMap(value => ["--allowed-kind", value])
];
const python = verify("envelope", name, candidate, expectedReason === null, extra);
assert.deepEqual({ valid: js.valid, reason: js.reason },
{ valid: python.valid, reason: python.reason }, name);
assert.equal(js.reason, expectedReason, name);
envelopeResults.push({ id: name, valid: js.valid, reason: js.reason, runtimes_agree: true });
}

const passport = createPolicyPassport({
subject: "example", profile: "calm_default", preferences: { reduced_motion: true, attention_budget: 3 },
Expand Down Expand Up @@ -108,7 +140,10 @@ try {
assert.equal(verifyCausalExecutionGraph(orphanGraph).valid, false);
assert.equal(verify("graph", "orphan-graph", orphanGraph, false).reason, "missing_parent");

console.log("Python wire runtime: negotiation, five artifact classes, and tamper cases passed");
console.log(JSON.stringify({ protocol: "aml-wire-envelope-cross-runtime/1", passed: true,
envelope_case_count: envelopeResults.length, envelope_cases: envelopeResults,
other_checks: "negotiation, five artifact classes, and tamper cases passed",
claim_boundary: "Project-authored JavaScript/Python comparison of selected wire fixtures; not complete JSON interoperability or external validation." }, null, 2));
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}
Loading