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
11 changes: 7 additions & 4 deletions UPSTREAM.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
"broker/generality_test.go": "14b83f8dd8e25562cd5d099e7d0b9285d15d2874cddd42f3901303c0553ee596",
"clients/.gitignore": "e44a4a6d3c3287bc82212abbac01e1992247c3cb2b3e5bff8ecfd7ee73788bb5",
"clients/gen/main.go": "2a11e7de9fe7eb1ed8a19435ee5421923a399bc32834da94c0497b5d59728016",
"clients/python/examples/decision-request.py": "0debe9f27023bbcf1b0487a2b343825f061a1920b0f2d1e7d82b7ece2d6f6583",
"clients/python/examples/parity.py": "8d530dd043ba55b4369f4b3d9945c98bcd9276f84dace7517df4fb3f326cb431",
"clients/python/pyproject.toml": "b93eaaa71294f43cc50608a4a84d1dc8277fb4b69115476a188c55c89933a7b5",
"clients/python/src/interlock_protocol/__init__.py": "6aaeb0f5b560354c36b4e63993d1826d4b70f8120d02044629132156a45156d4",
"clients/python/src/interlock_protocol/canonical.py": "e0f3978baa8195fe49c0711b59c46c9a76469b4c65d259c8ffb926a6473fc3b6",
"clients/python/src/interlock_protocol/protocol.py": "8dd6225bf068269e14290841c2b0c5b88e101978814009e83f25a57f797bf4fb",
"clients/python/src/interlock_protocol/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"clients/schema/interlock.schema.json": "b637af7ab11b324f9b018656942552858143b412e5083d18285abadc6d056eba",
"clients/typescript/examples/decision-request.ts": "0a398c305f1fc89b7d0c70c68e34f56c170ba0ee456e0447103c0419c50ad159",
"clients/typescript/examples/parity.ts": "2d73ad5d3c9f2b87bf3755fd029f449afcfcc3dd820877f02ae516228196a1b1",
"clients/typescript/package.json": "d6b747a44a20a2377274e7f5cc12bf292fdec10f60fb7c099a17e3b3bd6f0837",
"clients/typescript/src/canonical.ts": "be376928b4563f998a5879201a9cc36407b9c13218ed6357722424e044d1247b",
Expand Down Expand Up @@ -68,19 +70,20 @@
"examples/exclusive-publish/policy.go": "d18df2ada6684bc1815d19b4918b7ba8b7b584d7fd75e9fc7f8c0562299cd94c",
"examples/generated-file-protection/policy.go": "0a9a5b33990adb770c74382bbab7e618582fd8c2ce6632deba5ca9cc652696ee",
"examples/release-manifest/policy.go": "d2f005addc2842f0ec235feb64315776aa936aa2e49bb82325867b3c9bcb691c",
"examples/release-manifest/publish_test.go": "25bad210cd30e1d0e2ef4f6328856d18919a5fcda3bfd6d72ce2d770ffeb4868",
"examples/release-manifest/publish_test.go": "7594fc7ff5a2abaf3216c74879c8d5873b636f8091d532c39d93971a276f4f4c",
"examples/repository-policy/policy.go": "8c7fe12c487c1369d5ce8ae24e647e050557cd2bf29c56d4010b782603debbd1",
"go.mod": "a9a846b064eac2e330c18198dae19044e438bff31dcb26d928cdb2f9b5cae3e1",
"install.ps1": "ada3f94562569929446dd6ca200f993854fa10081785e3ad1a09db285dacca6a",
"install.sh": "7d62cccc9b35280490c3332279743050957ab7e648b4b115c86211ba8995ab4e",
"interlock.go": "de4ff7ee8dd15ab12d0bea387b66ad6e7abfbb66553839ecc0befc32ec8fb606",
"interlock_test.go": "06b56780f7ae90fe12b055bf426aab8b87ed4fe174698a49d729bdc6484299e2",
"ir/ir.go": "20fe77a927c8f89ac90b5a806579854934ff5d3977a014c236d30377860ec310",
"ir/ir.go": "8765290a06278981beff40410092c63e0df3b1561e4cbd7e6c63217f1213390d",
"ir/ir_test.go": "64a962436ee5cab261093f8cb455ad92075683a7ef58997dac1e022e09d1a46a",
"proof/proof.go": "22a3baeee05bfb1e9f2be549a07b873ddc51d36037711ffef31e77ee161c24fb",
"proof/proof_test.go": "8151c10b3efc95e8eb66841ce979093a35263adaf0b6f337b1fe5a3184769123",
"protocol/protocol.go": "1d9cc6ba1d59635e9c88ab9a20058491c78d0024e5e87fd25ec87628271c728e",
"publish/publish.go": "2434a5c00da06f97a27aae77f6b9ca8a006027e37123eac627032cce0a7f3434",
"publish/publish.go": "8ce82d43f8912dac520bc75c3be7506bd2871e747caa6e752102e4ff27c0e4d7",
"publish/publish_test.go": "2794f054e3fb8aba3d4ea126c15be97d2cfdb59aabe82ecef252957a2f5e0faf",
"receipt/receipt.go": "8a21b9054b599965221e08fe158887759473d2f41d28dd3df42a1ffe1c55cb6e",
"receipt/receipt_test.go": "a215a205205573bb0fbf6d121aa761bc28510346b92e8bb72dd85ca90b6fa65c",
"scaffold/demo.go": "d496e57f31a17165a540f704704ae341e66641139795263bee2dcfb9ca1de7c7",
Expand All @@ -95,7 +98,7 @@
"generator": "operatorstack/interlock:project-upstream",
"schema_version": 1,
"source": {
"commit": "9ec0e3c0251ee23dd771b8a42d0f9f7b34d4d4d7",
"commit": "74a9630b632faae5e421c4046ff5a6d516c0f7dd",
"path": "labs/21-interlock",
"repository": "operatorstack/intelligence-flow"
}
Expand Down
150 changes: 150 additions & 0 deletions clients/python/examples/decision-request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
# decision-request.py shows the winning cross-language ergonomic in Python: a
# caller names the resource by its POLICY ID and gets a typed EffectRequest with
# the URI+kind resolved from the policy — never restating a resource's URI/kind and
# never hand-writing the request JSON. The request is emitted with the same
# canonical encoder the hash is computed from.
#
# Authority boundary — read this before extending the example. This client does
# NOT decide and does NOT enforce. It builds a typed, canonically-encoded request.
# The decision is made by the Go engine (behind the interlock-pitot controller),
# and the protected effect is performed by the Go broker. Porting the decision
# engine or the broker into Python is an explicit non-goal: the canonical IR is the
# execution language, Go is the trusted executable. So this file resolves a
# resource and shapes a request; it makes no allow/deny claim.
#
# Run: python examples/decision-request.py (Python >= 3.11)
# Exits non-zero on any mismatch.
from __future__ import annotations

import json
import sys
from pathlib import Path

try:
from interlock_protocol.canonical import canonical_bytes
from interlock_protocol.protocol import EffectRequest, Policy, TargetResource
except ModuleNotFoundError: # pragma: no cover - convenience for uninstalled runs
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
from interlock_protocol.canonical import canonical_bytes
from interlock_protocol.protocol import EffectRequest, Policy, TargetResource

# The corpus lives three directories up from clients/python/examples.
CORPUS = Path(__file__).resolve().parent.parent.parent.parent / "conformance" / "compat" / "v0.1.0"


def resolve_resource(policy: Policy, resource_id: str) -> TargetResource:
"""Python mirror of Go's ir.Policy.ResolveResource.

Name the resource once (in the policy) and look up its URI+kind here, rather
than restating them at the call site. It fails closed exactly like Go: an
unknown id raises (no default) and a duplicate id raises (no arbitrary pick),
because a silently-wrong target is the one mistake a policy engine must never
make.
"""
matches = [r for r in policy["resources"] if r["id"] == resource_id]
if not matches:
raise ValueError(
f'interlock: resource id "{resource_id}" not declared in policy "{policy["policy_id"]}"'
)
if len(matches) > 1:
raise ValueError(
f'interlock: resource id "{resource_id}" declared more than once in policy "{policy["policy_id"]}"'
)
return {"kind": matches[0]["kind"], "uri": matches[0]["uri"]}


def build_effect_request(
policy: Policy,
*,
run_id: str,
request_id: str,
actor: str,
operation: str,
resource_id: str,
claimed_policy_hash: str | None = None,
evidence: list | None = None,
source: str = "interlock-python-client",
fidelity: str = "brokered",
) -> EffectRequest:
"""Assemble the typed EffectRequest, resolving the resource by id.

Pure: no I/O, no decision. This is the exact value that would be canonically
encoded and sent to the Go controller. Observation defaults to a brokered
decision-transport reading, matching the Go client (client.EffectRequest).
"""
req: EffectRequest = {
"protocol": "interlock.effect.v1",
"request_id": request_id,
"run_id": run_id,
"actor": actor,
"operation": operation,
"resource": resolve_resource(policy, resource_id),
"observation": {"source": source, "fidelity": fidelity},
}
if claimed_policy_hash is not None:
req["claimed_policy_hash"] = claimed_policy_hash
if evidence is not None:
req["evidence"] = evidence
return req


def main() -> int:
policy: Policy = json.loads(
(CORPUS / "policies" / "exclusive-publish.json").read_text(encoding="utf-8")
)

failures = 0

def check(ok: bool, msg: str) -> None:
nonlocal failures
print(f"{'PASS' if ok else 'FAIL'} {msg}")
if not ok:
failures += 1

# The win: the publisher publishes the "artifact" resource named by id. The URI
# and kind are never typed by the caller — they come from the policy.
req = build_effect_request(
policy,
run_id="run1",
request_id="r1",
actor="publisher",
operation="artifact.publish",
resource_id="artifact",
claimed_policy_hash="sha256:edf4ed0d10b1aa9c0c2a0301688b3c97e34f6c0fc78502f4303466adb4ea82b3",
evidence=[
{"kind": "staged_hash_match"},
{"kind": "receipt_status", "receipt": "deltawire.supervision.receipt.v1", "status": "released"},
],
)

check(
req["resource"]["uri"] == "repo://out/result.json" and req["resource"]["kind"] == "file",
"resource resolved from policy id (URI/kind never restated at call site)",
)
check(req["observation"]["fidelity"] == "brokered", "observation defaults to a brokered decision reading")
check(req["protocol"] == "interlock.effect.v1", "protocol tag stamped")

# Canonically encodable — the same deterministic bytes Go and TypeScript
# produce, ready to transport. Byte-for-byte cross-language parity is proven by
# parity.py against the frozen corpus.
wire = canonical_bytes(req)
check(len(wire) > 0, "typed request canonically encodes (no hand-written JSON)")

# Fail-closed: an undeclared resource id is an error, never a silent empty target.
threw = False
try:
build_effect_request(
policy, run_id="r", request_id="r", actor="publisher",
operation="artifact.publish", resource_id="nope",
)
except ValueError:
threw = True
check(threw, "unknown resource id fails closed (mirrors Go ir.ResolveResource)")

print("\nRESULT: PASS" if failures == 0 else f"\nRESULT: FAIL ({failures})")
return 0 if failures == 0 else 1


if __name__ == "__main__":
raise SystemExit(main())
131 changes: 131 additions & 0 deletions clients/typescript/examples/decision-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// decision-request.ts shows the winning cross-language ergonomic in TypeScript:
// a caller names the resource by its POLICY ID and gets a typed EffectRequest with
// the URI+kind resolved from the policy — never restating a resource's URI/kind and
// never hand-writing the request JSON. The request is emitted with the same
// canonical encoder the hash is computed from.
//
// Authority boundary — read this before extending the example. This client does
// NOT decide and does NOT enforce. It builds a typed, canonically-encoded request
// DTO. The decision is made by the Go engine (behind the interlock-pitot
// controller), and the protected effect is performed by the Go broker. Porting the
// decision engine or the broker into TypeScript is an explicit non-goal: the
// canonical IR is the execution language, Go is the trusted executable. So this
// file resolves a resource and shapes a request; it makes no allow/deny claim.
//
// Run: node examples/decision-request.ts (Node >= 22, native type stripping)
// Exits non-zero on any mismatch.

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { canonicalBytes } from "../src/canonical.ts";
import type {
EffectRequest,
Fidelity,
Operation,
Policy,
TargetResource,
} from "../src/protocol.ts";

// resolveResource is the TS mirror of Go's ir.Policy.ResolveResource: name the
// resource once (in the policy) and look up its URI+kind here, rather than
// restating them at the call site. It fails closed exactly like Go — an unknown id
// throws (no default) and a duplicate id throws (no arbitrary pick) — because a
// silently-wrong target is the one mistake a policy engine must never make.
function resolveResource(policy: Policy, id: string): TargetResource {
const matches = policy.resources.filter((r) => r.id === id);
if (matches.length === 0) {
throw new Error(`interlock: resource id "${id}" not declared in policy "${policy.policy_id}"`);
}
if (matches.length > 1) {
throw new Error(`interlock: resource id "${id}" declared more than once in policy "${policy.policy_id}"`);
}
return { kind: matches[0].kind, uri: matches[0].uri };
}

// DecisionInput is the small, honest set of facts a caller supplies. The resource
// is named by id; observation defaults to a brokered decision-transport reading,
// matching the Go client (client.EffectRequest).
interface DecisionInput {
runId: string;
requestId: string;
actor: string;
operation: Operation;
resourceId: string;
claimedPolicyHash?: string;
evidence?: EffectRequest["evidence"];
source?: string;
fidelity?: Fidelity;
}

// buildEffectRequest assembles the typed EffectRequest, resolving the resource by
// id. It is pure: no I/O, no decision. This is the exact value that would be
// canonically encoded and sent to the Go controller.
function buildEffectRequest(policy: Policy, input: DecisionInput): EffectRequest {
return {
protocol: "interlock.effect.v1",
request_id: input.requestId,
run_id: input.runId,
actor: input.actor,
operation: input.operation,
resource: resolveResource(policy, input.resourceId),
observation: {
source: input.source ?? "interlock-ts-client",
fidelity: input.fidelity ?? "brokered",
},
...(input.claimedPolicyHash ? { claimed_policy_hash: input.claimedPolicyHash } : {}),
...(input.evidence ? { evidence: input.evidence } : {}),
};
}

// The corpus lives three directories up from clients/typescript/examples.
const CORPUS = join(import.meta.dirname, "..", "..", "..", "conformance", "compat", "v0.1.0");
const policy = JSON.parse(
readFileSync(join(CORPUS, "policies", "exclusive-publish.json"), "utf8"),
) as Policy;

let failures = 0;
function check(ok: boolean, msg: string): void {
console.log(`${ok ? "PASS" : "FAIL"} ${msg}`);
if (!ok) failures++;
}

// The win: the publisher publishes the "artifact" resource named by id. The URI
// and kind are never typed by the caller — they come from the policy.
const req = buildEffectRequest(policy, {
runId: "run1",
requestId: "r1",
actor: "publisher",
operation: "artifact.publish",
resourceId: "artifact",
claimedPolicyHash: "sha256:edf4ed0d10b1aa9c0c2a0301688b3c97e34f6c0fc78502f4303466adb4ea82b3",
evidence: [
{ kind: "staged_hash_match" },
{ kind: "receipt_status", receipt: "deltawire.supervision.receipt.v1", status: "released" },
],
});

check(
req.resource.uri === "repo://out/result.json" && req.resource.kind === "file",
"resource resolved from policy id (URI/kind never restated at call site)",
);
check(req.observation.fidelity === "brokered", "observation defaults to a brokered decision reading");
check(req.protocol === "interlock.effect.v1", "protocol tag stamped");

// The request is canonically encodable — the same deterministic bytes Go and
// Python produce, ready to transport. We assert it encodes without throwing and is
// non-empty; byte-for-byte cross-language parity is proven by parity.ts against the
// frozen corpus.
const wire = canonicalBytes(req);
check(wire.length > 0, "typed request canonically encodes (no hand-written JSON)");

// Fail-closed: an undeclared resource id is an error, never a silent empty target.
let threw = false;
try {
buildEffectRequest(policy, { runId: "r", requestId: "r", actor: "publisher", operation: "artifact.publish", resourceId: "nope" });
} catch {
threw = true;
}
check(threw, "unknown resource id fails closed (mirrors Go ir.ResolveResource)");

console.log(failures === 0 ? "\nRESULT: PASS" : `\nRESULT: FAIL (${failures})`);
process.exit(failures === 0 ? 0 : 1);
30 changes: 14 additions & 16 deletions examples/release-manifest/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@ func TestReleaseManifestPublishesViaFacade(t *testing.T) {
evidenceDir := filepath.Join(dir, "evidence")

res, err := publish.Publish(publish.Request{
Policy: policy,
RunID: "rel-run",
RequestID: "rel-1",
Actor: "release-bot",
ResourceURI: "repo://dist/release-manifest.json",
Kind: publish.KindFile,
StagedPath: staged,
TargetPath: target,
Policy: policy,
RunID: "rel-run",
RequestID: "rel-1",
Actor: "release-bot",
ResourceID: "manifest",
StagedPath: staged,
TargetPath: target,
// Target must not exist yet.
Upstream: []publish.Evidence{{
Schema: ReleaseAttestationSchema,
Expand Down Expand Up @@ -95,14 +94,13 @@ func TestReleaseManifestForeignSchemaFailsClosed(t *testing.T) {
target := filepath.Join(dir, "dist", "release-manifest.json")

_, err := publish.Publish(publish.Request{
Policy: policy,
RunID: "rel-run",
RequestID: "rel-2",
Actor: "release-bot",
ResourceURI: "repo://dist/release-manifest.json",
Kind: publish.KindFile,
StagedPath: staged,
TargetPath: target,
Policy: policy,
RunID: "rel-run",
RequestID: "rel-2",
Actor: "release-bot",
ResourceID: "manifest",
StagedPath: staged,
TargetPath: target,
Upstream: []publish.Evidence{{
// DeltaWire's schema — foreign to this tenant's policy.
Schema: "deltawire.supervision.receipt.v1",
Expand Down
Loading
Loading