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
4 changes: 4 additions & 0 deletions loopx/control_plane/capabilities/external_evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,10 @@ export function evaluateExternalEvidenceAdmission(params: JsonObject): JsonObjec
const admittedRefs = decision.admitted_source_refs === undefined
? []
: boundedStrings(decision.admitted_source_refs, "decision.admitted_source_refs", 64, 2048);
requireThat(
new Set(admittedRefs).size === admittedRefs.length,
"decision.admitted_source_refs must be unique",
);
const availableRefs = new Set(sources.map((source) => source.source_ref as string));
requireThat(
admittedRefs.every((sourceRef) => availableRefs.has(sourceRef)),
Expand Down
81 changes: 81 additions & 0 deletions tests/capabilities/test_external_evidence_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import json
import subprocess
import sys
import tempfile
from pathlib import Path

import pytest

from loopx.capabilities.external_research import cli
from loopx.control_plane.effect_runtime import restart_effect_runtime


def _print_payload(payload, _format, _renderer):
Expand Down Expand Up @@ -349,3 +353,80 @@ def test_source_cli_discovers_inventory_without_claiming_readiness(
assert payload["summary"]["connector_count"] == payload["summary"]["provider_count"]
assert payload["summary"]["ready_count"] == 0
assert payload["truth_contract"]["execution_observed"] is False


@pytest.mark.parametrize("duplicate", [
"https://example.com/original", " https://example.com/original ",
])
def test_source_cli_rejects_duplicate_admission_and_accepts_corrected_input(
tmp_path: Path, monkeypatch, duplicate: str,
) -> None:
for variable in ("TMPDIR", "TEMP", "TMP"):
monkeypatch.setenv(variable, str(tmp_path))
monkeypatch.setattr(tempfile, "tempdir", str(tmp_path))
monkeypatch.setenv("LOOPX_USAGE_PING", "0")

def run(*args: str):
result = subprocess.run(
[sys.executable, "-m", "loopx.entrypoint", "--runtime-root",
str(tmp_path / "runtime"), "--registry", str(tmp_path / "registry.json"),
"external-evidence", *args, "--format", "json"],
cwd=Path(__file__).resolve().parents[2], capture_output=True,
text=True, encoding="utf-8", check=False, timeout=30,
)
return result.returncode, json.loads(result.stdout)

def save(name: str, payload: dict) -> str:
path = tmp_path / name
path.write_text(json.dumps(payload), encoding="utf-8")
return str(path)

try:
providers = save("providers.json", {"providers": [{
"provider_id": "host:external-research", "provider_kind": "method",
"protocol": "external_evidence_research_v0", "declared": True,
"installed": True, "enabled": True, "ready": True,
"unavailable_reason": None,
}]})
code, plan = run(
"plan", "--objective", "Inspect public evidence", "--user-activity",
"Choose a provider", "--decision", "Whether to adopt",
"--evidence-kind", "current_behavior", "--provider-inventory-json", providers,
)
assert code == 0, plan
source_ref = "https://example.com/original"
receipt = {
"schema_version": "loopx_external_evidence_receipt_v0",
"plan_id": plan["plan_id"], "request_id": plan["request"]["request_id"],
"provider_id": "host:external-research", "provider_kind": "method",
"status": "succeeded", "summary": "Synthetic public evidence",
"completed_at": "2026-09-28T00:01:00Z", "sources": [{
"source_ref": source_ref, "source_family": "public-fixture",
"basis": "observed", "finding": "The fixture supports the decision.",
"accessed_at": "2026-09-28T00:00:00Z",
"content_digest": "sha256:" + "a" * 64,
}],
}
admit_args = (
"admit", "--plan-json", save("plan.json", plan),
"--receipt-json", save("receipt.json", receipt),
"--decision", "admit", "--reason", "Direct evidence",
"--admit-source", source_ref,
)
code, rejected = run(*admit_args, "--admit-source", duplicate)
assert code == 1, rejected
assert rejected["status"] == "invalid_request"
assert "decision.admitted_source_refs must be unique" in rejected["error"]
assert "admission_id" not in rejected

code, admission = run(*admit_args)
assert code == 0, admission
assert admission["admitted_source_refs"] == [source_ref]
code, retirement = run(
"retire", "--admission-json", save("admission.json", admission),
"--downstream-source", source_ref,
)
assert code == 0, retirement
assert retirement["status"] == "retire_ready"
finally:
restart_effect_runtime()
49 changes: 49 additions & 0 deletions tests/control_plane_ts/external_evidence_research.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,55 @@ test("admits exact source refs and exposes only compact provenance", () => {
assert.equal(Object.hasOwn(projection, "raw_content"), false);
});

for (const duplicate of [
"https://example.com/original",
" https://example.com/original ",
]) {
test(`admission rejects duplicate source refs after normalization: ${JSON.stringify(duplicate)}`, () => {
const currentPlan = plan();
assert.throws(
() => evaluateExternalEvidenceAdmission({
plan: currentPlan,
receipt: receipt(currentPlan),
decision: {
disposition: "admit",
reason: "Direct evidence must yield a readable admission.",
admitted_source_refs: ["https://example.com/original", duplicate],
},
}),
/decision\.admitted_source_refs must be unique/,
);
});
}

test("distinct source selections remain readable through retirement", () => {
const currentPlan = plan();
const currentReceipt = receipt(currentPlan);
const first = "https://example.com/original";
const second = "https://example.com/second";
currentReceipt.sources.push({ ...currentReceipt.sources[0], source_ref: second });
for (const refs of [[second], [second, first]]) {
const admission = evaluateExternalEvidenceAdmission({
plan: currentPlan,
receipt: currentReceipt,
decision: {
disposition: "admit",
reason: "Selected sources answer the question.",
admitted_source_refs: refs,
},
});
assert.deepEqual(admission.admitted_source_refs, refs);
assert.equal(
projectExternalEvidenceRetirement({ admission, downstream_source_refs: [] }).status,
"retained",
);
assert.equal(
projectExternalEvidenceRetirement({ admission, downstream_source_refs: refs }).status,
"retire_ready",
);
}
});

test("admission fails closed on stale plan identity and local file provenance", () => {
const currentPlan = plan();
assert.throws(
Expand Down
Loading