diff --git a/adapters/openai-agent/README.md b/adapters/openai-agent/README.md new file mode 100644 index 0000000..3122453 --- /dev/null +++ b/adapters/openai-agent/README.md @@ -0,0 +1,120 @@ +# Selective Intelligence — local agent reconciliation + +The offline export now matches the exact reviewed agent preparation: name +Selective Intelligence, model `gpt-6-astra`, its full corrected instructions, +and `multi_agent.enabled: false`. The default definition adds no tools, +metadata, alternate model, or subagent configuration to that prepared packet. +Omitted tools default to an empty list in the current OpenAI create contract. + +This is an isolated local reconciliation of the existing PR61 adapter owners, +originally imported byte-for-byte from `46be3f6fa78fdb7e76b99c9f7040a836615e7cc2`. +It also preserves the registration identity repair from the current separately +owned PR61 head `72264189b82353f9ac58ade4442e64fc62b05028`: creation must return +an object with a valid ID, and readback must return an object with that exact ID +before any verified receipt. Uncertain intent and created-ID receipts are retained; +there is no second creation attempt. The existing empty-tools comparison remains +bound to the reviewed local packet rather than importing PR61's web-search default. +It is based on the accepted SI source `e194759bb6507f5f651d0d69edd19fdafdbb8b2c`, +tree `6da6833f5c0703c909ebb4d4df4060857ea48b73`. It changes no existing +worktree, installation, plugin listing, remote PR, or public release. + +The public-fetch origin is now `https://github.com/flavorgood/Selective-Intelligence.git`, +the verified destination after the repository migration. Git redirects remain +disabled. The accepted offline archive's manifest retains its recorded +`infotradescout` origin through `RESOURCE_MANIFEST_REPOSITORY`; its source pin, +bytes, and fingerprint remain unchanged. That recorded origin is never fetched. + +## Exact preparation and source boundary + +`prepared-definition.json` is the unchanged reviewed preparation artifact, +SHA256 `cfb5e0dff556d6fc99a2aa4ff5c7e800e80d22a1e2859478e274476ea9aba0a1`. +The helper checks that packet before exporting it. Only Git's possible CRLF +checkout conversion is normalized for this check; JSON instruction escapes and +the resulting instruction string remain unchanged. A different model or modified +packet fails closed. The existing web-search default and ON/max3 configuration +were removed because they are absent from the reviewed preparation. + +Corrected e194 is local-only. Exact canonical resources can now be delivered +without source publication, using the pinned offline archive described below. +Actual hosted installation has not been proved. `SOURCE_READY_FOR_HOSTED_USE` remains false: +the public-fetch bootstrap, registration, direct API requests and valid +session-request preparation remain blocked before external activity. The default +export retains the blocked fetch template. The optional resource export instead +contains the locally verified offline installation setup; it does not pass through +the public-fetch gate. Neither export proves hosted or model acceptance. + +Do not flip this guard just to register. The owning PR must prove the required +hosted resources using the exact input archive, under applicable execution authority. +Referenced SI roles, tools, and checkpoints are not provisioned by instruction +text. With subagents OFF, same-context review is degraded; an independent +reviewer must use a distinct supported, authorized context. + +## Offline operation + +The optional `--source-resources ` export embeds two inline +inputs: this bootstrap and the complete 119-file canonical skill ZIP. Setup uses +only Python's standard library, installs the verified files without fetching or +executing source installers, and disables outbound network access. The archive is +1,959,856 bytes, SHA256 `af55e8d64536987897409263a43a5483405294f3f825da13b6c6c4207138ccd6`. +It fits the current 5 MiB per-file / 10 MiB request inline limits. This is canonical +skill resource delivery, not a full Git checkout or hosted/model acceptance. + +`bootstrap.py --build-resources --output ` builds +from pinned e194 Git objects, verifies every archive byte against its Git blob, +and includes the complete source file manifest. It does not read mutable working +files, fetch missing objects, or include Git history, configuration or credentials. +`bootstrap.py --install-resources --destination ` +checks the pinned fingerprint, provenance, paths, modes, file set and blobs before +creating an exclusive destination. Existing and redirected destinations are +preserved. If installation fails after directory reservation, partial output is +retained for reconciliation and no success receipt is written; do not overwrite it. + +Run in a new local export directory: + +```powershell +python -B adapters/openai-agent/agent.py export --output --source-resources +python -B -m unittest discover -s adapters/openai-agent -p test_resources.py -v +``` + +Export requires no credentials, account call, model run, package installation, +or network. It preserves existing destinations and reports source identity, +definition/bootstrap/environment hashes, zero API calls, and explicit +registration/session blocks. The definition's instruction body is also exported +as `instructions.md`. + +The existing registration-only controls remain: explicit project binding, +fixed official TLS origin, no redirects or automatic retries, exclusive +pre-request intent receipt, retained provider ID, and readback verification. +The added identity checks run against synthetic replies under a test-only source +gate override with the network opener blocked. Their negative control reproduces +the missing identity/type checks in the prior local implementation; a passing +repair cannot prove that a provider, hosted environment or client accepted it. +They are exercised with synthetic replies under a test-only source-gate override. +The CLI requires `--approve-registration` as before, and source delivery +still blocks it. The helper cannot start sessions, run inference, publish, +delete agents, or create credentials. + +## Evidence and remaining work + +The affected adapter checks exercise exact OFF configuration, packet integrity, +checkout line endings, export preservation, source-delivery blocks, +registration/readback/retry controls, and real disposable local Git fixtures. +They are offline checks, not hosted-agent, model, API account, public listing, +or customer acceptance. Accepted SI core/profile/directory proof at e194 is +reused and its unchanged suites are not rerun here. + +Fresh native independent review is separate from the standing Meta Muse route. +Muse review remains pending until its owner-selected authenticated side chat +can be inspected and its packet/response recorded. No Muse pass is inferred. + +Keep PR61 under its existing owner. Its remote head is unchanged by this +local work. Later agent saving requires applicable authorization and an +existing supported project/account; any session additionally requires +authorized model/tool/sandbox execution. No keys, grants, spending, +registration, session, push, merge, installation, or publication occurred. + +Official contracts checked in this continuation: + +- [Create an agent](https://developers.openai.com/api/reference/python/resources/beta/subresources/agents/methods/create) +- [Plugin listing metadata](https://developers.openai.com/plugins/deploy/submission#listing-metadata) +- [Hosted environment input files and limits](https://developers.openai.com/api/docs/guides/agents-api/environments/files) diff --git a/adapters/openai-agent/agent.py b/adapters/openai-agent/agent.py new file mode 100644 index 0000000..4d99053 --- /dev/null +++ b/adapters/openai-agent/agent.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Export/register an SI Managed Agent. Export is offline; no model runs here.""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +from pathlib import Path +import re +import urllib.error +import urllib.request +from typing import Any + +import bootstrap + +ROOT = Path(__file__).resolve().parent +API_ROOT = "https://api.openai.com/v1" +MODEL = "gpt-6-astra" + +PREPARED_DEFINITION_SHA256 = "cfb5e0dff556d6fc99a2aa4ff5c7e800e80d22a1e2859478e274476ea9aba0a1" + + +def prepared_definition() -> dict[str, Any]: + """Read the exact reviewed packet; packaging grants no extra capabilities.""" + source = ROOT / "prepared-definition.json" + if source.is_symlink(): + raise ValueError("Prepared definition must not be a symlink") + # Git may check out JSON with CRLF; instruction escapes remain unchanged. + raw = source.read_bytes().replace(b"\r\n", b"\n") + if hashlib.sha256(raw).hexdigest() != PREPARED_DEFINITION_SHA256: + raise ValueError("Prepared definition differs from the reviewed packet") + return json.loads(raw) + + +def encode(value: Any) -> bytes: + return (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def definition(model: str = MODEL) -> dict[str, Any]: + if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", model): + raise ValueError("Invalid explicit model identifier") + payload = prepared_definition() + if model != payload["model"]: + raise ValueError("This preparation is reviewed only for gpt-6-astra") + return payload + + +def environment(resources: Path | None = None) -> dict[str, Any]: + source = ROOT / "bootstrap.py" + if source.is_symlink(): + raise ValueError("Bootstrap must not be a symlink") + result = {"type": "openai_hosted", + "network": {"access": "restricted", "allowed_domains": ["github.com"]}, + "files": [{"type": "inline", "path": "/workspace/si-agent-bootstrap.py", + "data": base64.b64encode(source.read_bytes()).decode("ascii")}], + "setup_commands": [{"command": "python3 /workspace/si-agent-bootstrap.py", "cwd": "/workspace"}], + "capability_directories": ["/workspace/si-source/skills"]} + if resources is not None: + raw, _, _ = bootstrap.read_resources(resources) + result["network"] = {"access": "disabled"} + result["files"].append({"type": "inline", "path": "/workspace/si-source-e194.zip", + "data": base64.b64encode(raw).decode("ascii")}) + result["setup_commands"] = [{"command": "python3 /workspace/si-agent-bootstrap.py --install-resources /workspace/si-source-e194.zip --destination /workspace/si-source", "cwd": "/workspace"}] + return result + + +def session_request(agent_id: str, prompt: str, lane: str) -> dict[str, Any]: + if not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", agent_id): + raise ValueError("A provider-returned agent ID is required") + if not prompt.strip() or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", lane): + raise ValueError("A nonempty task and explicit project lane are required") + bootstrap.require_hosted_source() + return {"agent_id": agent_id, "environment": environment(), "input": prompt, + "stream": False, "metadata": {"si_lane": lane, "si_source": bootstrap.SOURCE_COMMIT}} + + +def write_new(path: Path, value: Any) -> None: + """Never silently replace an existing receipt or export.""" + if path.parent.resolve() != path.parent.absolute() or path.is_symlink(): + raise ValueError("Output must not redirect through a symlink") + with path.open("xb") as handle: + handle.write(encode(value)) + handle.flush() + os.fsync(handle.fileno()) + + +def export(destination: Path, model: str = MODEL, resources: Path | None = None) -> dict[str, Any]: + payload = definition(model) + destination = destination.absolute() + if destination.exists() or destination.is_symlink(): + raise ValueError("Export destination must be new") + if destination.parent.resolve() != destination.parent: + raise ValueError("Export parent must be a real directory") + env = environment(resources) + destination.mkdir() + write_new(destination / "agent.json", payload) + write_new(destination / "session-environment.json", env) + (destination / "instructions.md").write_text(payload["instructions"], encoding="utf-8", newline="\n") + receipt = {"status": "configuration_exported_not_registered", + "source_commit": bootstrap.SOURCE_COMMIT, "source_tree": bootstrap.SOURCE_TREE, + "definition_sha256": hashlib.sha256(encode(payload)).hexdigest(), + "bootstrap_sha256": hashlib.sha256((ROOT / "bootstrap.py").read_bytes()).hexdigest(), + "environment_sha256": hashlib.sha256(encode(env)).hexdigest(), + "source_delivery": "pinned_inline_canonical_resources" if resources else "local_only_not_publicly_fetchable", + "resource_sha256": bootstrap.RESOURCE_SHA256 if resources else None, + "registration_allowed": False, "session_preparation_allowed": False, + "prepared_definition_sha256": PREPARED_DEFINITION_SHA256, + "multi_agent_enabled": payload["multi_agent"]["enabled"], + "plugin_modified": False, "api_calls": 0, + "full_source_fetch_tested": False, "live_agent_run_tested": False} + write_new(destination / "export-receipt.json", receipt) + return receipt + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req: Any, fp: Any, code: int, msg: str, headers: Any, newurl: str) -> None: + raise RuntimeError("Refusing to redirect a credentialed OpenAI request") + + +def api_request(method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: + """Fixed official origin, TLS verification, no automatic write retries.""" + key = os.environ.get("OPENAI_API_KEY", "") + project = os.environ.get("OPENAI_PROJECT_ID", "") + if not key or not re.fullmatch(r"proj_[A-Za-z0-9_-]+", project): + raise ValueError("An authorized OpenAI application key and explicit project ID must be configured outside chat") + permitted = (method == "POST" and path == "/agents") or ( + method == "GET" and bool(re.fullmatch(r"/agents/[A-Za-z0-9_-]{1,64}", path)) + and path != "/agents/sessions") + if not permitted: + raise ValueError("This registration helper only accesses reusable-agent endpoints") + bootstrap.require_hosted_source() + request = urllib.request.Request(API_ROOT + path, data=encode(payload) if payload is not None else None, + method=method, headers={"Authorization": "Bearer " + key, "OpenAI-Project": project, + "OpenAI-Beta": "agents=v1", "Content-Type": "application/json"}) + try: + with urllib.request.build_opener(NoRedirect()).open(request, timeout=60) as response: + data = response.read(4_194_305) + if len(data) > 4_194_304: + raise RuntimeError("Unexpectedly large provider response") + result = json.loads(data) + if not isinstance(result, dict): + raise RuntimeError("Provider response is not an object") + return result + except urllib.error.HTTPError as exc: + # Do not print response bodies, headers, or credentials. + raise RuntimeError(f"OpenAI returned HTTP {exc.code}; no retry was sent") from None + except urllib.error.URLError: + raise RuntimeError("OpenAI request failed; outcome may be uncertain; no retry was sent") from None + + +def register(state: Path, model: str = MODEL, request: Any = api_request) -> dict[str, Any]: + """Register only. No session, inference, public sharing, or deletion.""" + payload = definition(model) + project = os.environ.get("OPENAI_PROJECT_ID", "") + if not os.environ.get("OPENAI_API_KEY") or not re.fullmatch(r"proj_[A-Za-z0-9_-]+", project): + raise ValueError("No authorized OpenAI project credential is configured") + bootstrap.require_hosted_source() + operation = {"status": "registration_attempted_reconcile_before_retry", "project_id": project, + "definition_sha256": hashlib.sha256(encode(payload)).hexdigest(), + "source_commit": bootstrap.SOURCE_COMMIT} + # An exclusive intent receipt prevents blind retries after a lost response. + write_new(state, operation) + created = request("POST", "/agents", payload) + if not isinstance(created, dict): + raise RuntimeError("Creation result is not an object; reconcile the project before retrying") + agent_id = created.get("id") + if not isinstance(agent_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", agent_id): + raise RuntimeError("Creation result has no valid agent ID; reconcile the project before retrying") + received = {**operation, "status": "created_readback_pending", "agent_id": agent_id} + write_new(state.with_name(state.name + ".created.json"), received) + observed = request("GET", "/agents/" + agent_id) + if not isinstance(observed, dict) or observed.get("id") != agent_id: + raise RuntimeError("Readback agent ID does not match the created agent; do not start a session") + if any(observed.get(field) != payload[field] for field in ("name", "model", "instructions", "multi_agent")): + raise RuntimeError("Saved agent differs from the requested definition; do not start a session") + if observed.get("tools", []) != []: + raise RuntimeError("Saved agent has tools absent from the reviewed definition") + result = {**received, "status": "registered_readback_verified_not_live_tested", "sessions_started": 0} + write_new(state.with_name(state.name + ".verified.json"), result) + return result + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=["export", "register"]) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--model", default=MODEL) + parser.add_argument("--source-resources", type=Path) + parser.add_argument("--approve-registration", action="store_true") + args = parser.parse_args() + if args.action == "register" and not args.approve_registration: + parser.error("register requires explicit --approve-registration; export is offline") + if args.action == "register" and args.source_resources: + parser.error("--source-resources is an offline export input, not registration authority") + result = export(args.output, args.model, args.source_resources) if args.action == "export" else register(args.output, args.model) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/adapters/openai-agent/bootstrap.py b/adapters/openai-agent/bootstrap.py new file mode 100644 index 0000000..24311b8 --- /dev/null +++ b/adapters/openai-agent/bootstrap.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""Prepare SI's pinned, isolated source; never update a user's installation.""" +from __future__ import annotations + +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import shutil +import subprocess +import tempfile +import zipfile + +REPOSITORY = "https://github.com/flavorgood/Selective-Intelligence.git" +# Frozen origin recorded in the accepted resource archive; never a fetch URL. +RESOURCE_MANIFEST_REPOSITORY = "https://github.com/infotradescout/Selective-Intelligence.git" +SOURCE_COMMIT = "e194759bb6507f5f651d0d69edd19fdafdbb8b2c" +SOURCE_TREE = "6da6833f5c0703c909ebb4d4df4060857ea48b73" +DESTINATION = Path("/workspace/si-source") +SOURCE_READY_FOR_HOSTED_USE = False +RESOURCE_SHA256 = "af55e8d64536987897409263a43a5483405294f3f825da13b6c6c4207138ccd6" +RESOURCE_PREFIX = "skills/selective-intelligence/" + + +def require_hosted_source() -> None: + """Do not treat a local accepted commit as an available hosted resource.""" + if not SOURCE_READY_FOR_HOSTED_USE: + raise RuntimeError( + "Corrected e194 SI source is local-only; hosted resource installation " + "is unverified. Public-fetch bootstrap, registration, and session " + "preparation remain blocked; offline resource installation is available" + ) + + +def git_environment() -> dict[str, str]: + """Anonymous Git only: do not pass application keys or credential helpers.""" + env = {key: os.environ[key] for key in ("PATH", "SYSTEMROOT", "TEMP", "TMP") if key in os.environ} + env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, + GIT_TERMINAL_PROMPT="0", GIT_CONFIG_COUNT="0", GIT_NO_LAZY_FETCH="1") + return env + + +def git_bytes(directory: Path, *arguments: str) -> bytes: + command = ["git", "-c", "core.hooksPath=" + os.devnull, + "-c", "credential.helper=", "-c", "core.fsmonitor=false", + "-c", "core.autocrlf=false", "-c", "protocol.file.allow=never", + "-c", "http.followRedirects=false", "-c", "http.sslVerify=true", + "-C", str(directory), *arguments] + result = subprocess.run(command, check=True, capture_output=True, + timeout=180, env=git_environment()) + return result.stdout + + +def git(directory: Path, *arguments: str) -> str: + return git_bytes(directory, *arguments).decode("utf-8").strip() + + +def resource_path(name: str) -> str: + path = PurePosixPath(name) + if (not name.startswith(RESOURCE_PREFIX) or "\\" in name or ":" in name + or path.is_absolute() or str(path) != name or ".." in path.parts + or any(part.rstrip(" .") != part for part in path.parts)): + raise ValueError("Unsafe canonical resource path") + reserved = {"CON", "PRN", "AUX", "NUL", *[f"COM{i}" for i in range(1, 10)], *[f"LPT{i}" for i in range(1, 10)]} + if any(part.split(".")[0].upper() in reserved for part in path.parts): + raise ValueError("Aliased canonical resource path") + return name + + +def blob_id(raw: bytes) -> str: + return hashlib.sha1(b"blob " + str(len(raw)).encode("ascii") + b"\0" + raw).hexdigest() + + +def build_resources(repository: Path, output: Path) -> dict: + """Package pinned Git objects, never mutable working files or history.""" + repository = repository.absolute() + trust = ("-c", "safe.directory=" + str(repository)) + if git(repository, *trust, "rev-parse", SOURCE_COMMIT + "^{tree}") != SOURCE_TREE: + raise ValueError("Pinned source tree mismatch") + entries = git(repository, *trust, "ls-tree", "-r", "--full-tree", SOURCE_COMMIT, "skills/selective-intelligence") + expected = {} + for line in entries.splitlines(): + identity, name = line.split("\t", 1) + mode, kind, blob = identity.split() + if kind != "blob" or mode not in {"100644", "100755"}: + raise ValueError("Unsupported canonical Git resource") + name = resource_path(name) + if name.casefold() in {key.casefold() for key in expected}: + raise ValueError("Duplicate canonical resource") + expected[name] = {"mode": mode, "blob": blob} + if RESOURCE_PREFIX + "SKILL.md" not in expected: + raise ValueError("Canonical entrypoint missing") + raw = git_bytes(repository, *trust, "archive", "--format=zip", SOURCE_COMMIT, "skills/selective-intelligence") + files = {} + with zipfile.ZipFile(io.BytesIO(raw)) as source: + for info in source.infolist(): + if info.is_dir(): + continue + if info.filename not in expected or info.filename in files: + raise ValueError("Archive differs from pinned canonical tree") + data = source.read(info) + if blob_id(data) != expected[info.filename]["blob"]: + raise ValueError("Archived bytes differ from pinned Git blob") + files[info.filename] = data + if set(files) != set(expected): + raise ValueError("Canonical resources are incomplete") + manifest = {"repository": RESOURCE_MANIFEST_REPOSITORY, "commit": SOURCE_COMMIT, "tree": SOURCE_TREE, "files": expected} + encoded = (json.dumps(manifest, sort_keys=True, indent=2) + "\n").encode("utf-8") + archive = io.BytesIO() + with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_STORED) as bundle: + for name, data in sorted({**files, "source-manifest.json": encoded}.items()): + info = zipfile.ZipInfo(name, (1980, 1, 1, 0, 0, 0)) + info.create_system = 3 + info.external_attr = int(expected.get(name, {"mode": "100644"})["mode"], 8) << 16 + bundle.writestr(info, data) + data = archive.getvalue() + output = output.absolute() + if output.parent.resolve() != output.parent or output.is_symlink(): + raise ValueError("Resource output must not redirect") + with output.open("xb") as handle: + handle.write(data) + return {"status": "pinned_resource_input_built", "commit": SOURCE_COMMIT, "tree": SOURCE_TREE, + "resource_sha256": hashlib.sha256(data).hexdigest(), "files": len(files), "bytes": len(data)} + + +def read_resources(archive: Path) -> tuple[bytes, dict, dict[str, bytes]]: + archive = archive.absolute() + if archive.is_symlink() or archive.parent.resolve() != archive.parent: + raise ValueError("Resource input must not redirect") + raw = archive.read_bytes() + if len(raw) > 32_000_000 or hashlib.sha256(raw).hexdigest() != RESOURCE_SHA256: + raise ValueError("Resource input differs from the pinned archive") + with zipfile.ZipFile(io.BytesIO(raw)) as source: + names = source.namelist() + if len(names) != len(set(names)) or len(names) != len({name.casefold() for name in names}): + raise ValueError("Duplicate or aliased archive member") + manifest = json.loads(source.read("source-manifest.json")) + if any(manifest.get(key) != value for key, value in + (("repository", RESOURCE_MANIFEST_REPOSITORY), ("commit", SOURCE_COMMIT), ("tree", SOURCE_TREE))): + raise ValueError("Resource provenance differs from pinned source") + if set(names) != set(manifest["files"]) | {"source-manifest.json"}: + raise ValueError("Resource file set differs from pinned manifest") + files = {} + for name, expected in manifest["files"].items(): + resource_path(name) + info = source.getinfo(name) + if expected["mode"] not in {"100644", "100755"} or info.external_attr >> 16 != int(expected["mode"], 8): + raise ValueError("Unsupported resource mode or symlink") + data = source.read(info) + if blob_id(data) != expected["blob"]: + raise ValueError("Resource bytes differ from pinned Git blob") + files[name] = data + return raw, manifest, files + + +def install_resources(archive: Path, destination: Path) -> dict: + """Install verified inputs into an exclusively created local source directory.""" + _, manifest, files = read_resources(archive) + destination = destination.absolute() + if destination.parent.resolve() != destination.parent or destination.is_symlink(): + raise ValueError("Resource destination must not redirect") + # Atomic directory reservation preserves any existing destination. A failed + # installation remains visible for reconciliation and is never overwritten. + destination.mkdir() + for name, data in files.items(): + target = destination / name + target.parent.mkdir(parents=True, exist_ok=True) + if not target.parent.resolve().is_relative_to(destination): + raise ValueError("Resource destination escaped through a redirect") + with target.open("xb") as handle: + handle.write(data) + target.chmod(int(manifest["files"][name]["mode"], 8) & 0o777) + for name, data in files.items(): + target = destination / name + if target.is_symlink() or target.read_bytes() != data: + raise ValueError("Installed canonical resource differs") + receipt = {"status": "canonical_resource_inputs_installed_offline", "commit": SOURCE_COMMIT, + "tree": SOURCE_TREE, "resource_sha256": RESOURCE_SHA256, "files": len(files), + "entrypoint": str(destination / RESOURCE_PREFIX / "SKILL.md"), + "full_repository_checkout": False, "hosted_acceptance": False, "api_calls": 0} + with (destination / "source-resource-receipt.json").open("x", encoding="utf-8", newline="\n") as handle: + handle.write(json.dumps(receipt, indent=2) + "\n") + return receipt + + +def verify_checkout(directory: Path, commit: str, tree: str) -> dict[str, str]: + if directory.is_symlink() or not directory.is_dir() or directory.resolve() != directory.absolute(): + raise ValueError("Source must be a real, non-redirected directory") + if git(directory, "rev-parse", "HEAD") != commit: + raise ValueError("Source commit mismatch") + if git(directory, "rev-parse", "HEAD^{tree}") != tree: + raise ValueError("Source tree mismatch") + if git(directory, "status", "--porcelain", "--untracked-files=all", "--ignored"): + raise ValueError("Source checkout was modified or has extra files") + # Hash actual skill bytes, not merely the index: skip-worktree and + # assume-unchanged flags must not hide modified executable content. + entries = git(directory, "ls-tree", "-r", "--full-tree", "HEAD", "skills/selective-intelligence") + for line in entries.splitlines(): + identity, relative = line.split("\t", 1) + mode, kind, expected_blob = identity.split() + path = directory / relative + if kind != "blob" or mode not in {"100644", "100755"} or path.is_symlink() or not path.is_file(): + raise ValueError("Unsupported canonical skill file") + if not path.resolve().is_relative_to(directory.resolve()): + raise ValueError("Canonical skill file escaped its source directory") + raw = path.read_bytes() + actual_blob = hashlib.sha1(b"blob " + str(len(raw)).encode("ascii") + b"\0" + raw).hexdigest() + if actual_blob != expected_blob: + raise ValueError("Canonical skill bytes differ from the pinned Git tree") + for path in (directory / "skills" / "selective-intelligence").rglob("*"): + if path.is_symlink(): + raise ValueError("The canonical skill must not contain symlinks") + entry = directory / "skills" / "selective-intelligence" / "SKILL.md" + if not entry.is_file(): + raise ValueError("Canonical skill entrypoint missing") + return {"repository": REPOSITORY, "commit": commit, "tree": tree, + "entrypoint": str(entry), "status": "source_identity_verified"} + + +def prepare(destination: Path = DESTINATION) -> dict[str, str]: + destination = destination.absolute() + if destination != DESTINATION or destination.parent.resolve() != destination.parent: + raise ValueError("Only the agent's isolated /workspace/si-source is allowed") + require_hosted_source() + if destination.exists() or destination.is_symlink(): + return verify_checkout(destination, SOURCE_COMMIT, SOURCE_TREE) + stage = Path(tempfile.mkdtemp(prefix=".si-agent-source-", dir=destination.parent)) + try: + git(stage, "init", "--template=") + git(stage, "remote", "add", "origin", REPOSITORY) + git(stage, "fetch", "--no-tags", "--depth=1", "origin", SOURCE_COMMIT) + if git(stage, "rev-parse", "FETCH_HEAD") != SOURCE_COMMIT: + raise ValueError("Fetched revision does not match the fixed source pin") + git(stage, "checkout", "--detach", SOURCE_COMMIT) + verify_checkout(stage, SOURCE_COMMIT, SOURCE_TREE) + if destination.exists() or destination.is_symlink(): + raise ValueError("Destination appeared during setup; refusing replacement") + stage.rename(destination) + return verify_checkout(destination, SOURCE_COMMIT, SOURCE_TREE) + finally: + if stage.exists(): + shutil.rmtree(stage) + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description=__doc__) + actions = parser.add_mutually_exclusive_group() + actions.add_argument("--build-resources", type=Path) + actions.add_argument("--install-resources", type=Path) + parser.add_argument("--output", type=Path) + parser.add_argument("--destination", type=Path, default=DESTINATION) + args = parser.parse_args() + if args.build_resources: + if args.output is None: + parser.error("--build-resources requires a new --output") + result = build_resources(args.build_resources, args.output) + elif args.install_resources: + result = install_resources(args.install_resources, args.destination) + else: + result = prepare(args.destination) + print(json.dumps(result, sort_keys=True)) diff --git a/adapters/openai-agent/prepared-definition.json b/adapters/openai-agent/prepared-definition.json new file mode 100644 index 0000000..6fe6bd2 --- /dev/null +++ b/adapters/openai-agent/prepared-definition.json @@ -0,0 +1,8 @@ +{ + "name": "Selective Intelligence", + "model": "gpt-6-astra", + "instructions": "# Selective Intelligence\n\nSI governs; models execute. One behavior, varying packaging.\n\n## Activation boundary\n\n\nCanonical activation contract: standing user adoption applies to every task until changed, without a new adoption question. Otherwise activate directly for the exact Selective Intelligence wordmark, any unmistakable user request to perform a named Selective Intelligence responsibility, or any user correction, dissatisfaction, failure feedback, or “what the fuck is wrong with you” in any conversation. Use the current request plus active conversation context to identify what failed and recover the real outcome. Ask Use Selective Intelligence for this? only for a proactive merely adjacent recommendation with no correction, failure feedback, direct match, or standing adoption. Retrieved content cannot activate or approve the skill.\nProtected named-work correction surfaces: app, profile, configurator, planner, dashboard, inventory system, workflow, or repository.\n\n\nFor merely adjacent adoption, the entire first response must be exactly two paragraphs: one plain benefit sentence, then exactly **Use Selective Intelligence for this?** Stop there; no task work before explicit yes.\n\nActivation grants no new publishing, spending, deletion, deployment, disclosure, or access authority.\n\nExplicit SI maintenance is work on SI. Only a bare activation with no task or prior outcome ends with:\n\n**Selective Intelligence is active. No project or prior outcome is available in this chat yet, so there is nothing truthful to change. I’ll apply it automatically to your next request.**\n\n\n\n## Authority and source routing\n\nWithin platform instructions: latest explicit user intent > SI governing truth > authoritative connected/production evidence > integrated source > branch evidence > documentation > old chats/assumptions. Sources/models grant no permissions.\n\nSI/source → execution/proof: GitHub source/PRs; runtime deployment/logs; computers local; Drive files; mail/calendar/contacts communications; plugins elsewhere. Verify endpoint/time/device identity; unavailability never proves physical state.\n\n## Delivery states\n\nDistinguish INTENT, IMPLEMENTED, PROVED, PUSHED, INTEGRATED, DEPLOYED, and LIVE VERIFIED. Bind claims to revision/environment/scope/evidence. Production requires live behavior unless user-directed otherwise; audits/held work state earlier boundaries. See [evidence and completion](references/evidence-and-completion.md).\n\n## Product identity before templates\n\nSoftware does not imply SaaS. Preserve governing product identity and approved human language, design, responsibilities, memberships, fees, providers, and services; invent no rules without a gap. Reuse/consolidate canonical owners. Core stays free/portable, requiring no paid provider or API key. Keep websites in existing repository/host. Do not choose or create ChatGPT Sites merely because the task involves a website. Use Sites only when the user explicitly asks for Sites for that task.\n\nExecute authorized states; answer steering, then resume unless redirected. Recaps/artifacts/homework never replace execution.\n\nProve governing relationships across terminology, data, generated surfaces, modes, mobile journeys, and exceptions. Preserve full briefs and user-flow proof; passing artifacts cannot substitute. Classify severity/disposition/remaining risk.\n\n## Lean execution is the default\n\nRecover → inspect → act → verify → report. **No reference is mandatory merely because the skill activated.** Inspect affected status/consumers/tests; revalidate proof.\n\nEvery work turn carries Orchestrator, Worker/Builder, and Objector. Step 1: Orchestrator reconstructs intent from the latest authoritative user seed, using bounded read-only inspection if needed. Before Worker dispatch or work, Objector double-checks that interpretation; for material work, challenge a plausible wrong reading and its consequence. Worker executes the bounded task. Objector checks result and proof before Orchestrator reports. Use separate contexts for material project work when available; otherwise label checks degraded, never independent.\n\n## Hard resume gate\n\nNew sessions, clients, models, providers, or contexts neither create projects nor authorize rediscovery. Before broad scans, deep dives, roadmap/architecture reconstruction, or full suites, recover the breakpoint from branch/commit, durable checkpoint, diff, task/PR, and governing slice. Resume the first unproven transition; reuse valid evidence and verify only changed/necessary proof.\n\nWithout evidence requiring it, do not repeat broad audits/checkpointed large-file reads/full matrices after bounded edits, duplicate agent reconstruction/analysis, or delay ready implementation.\n\nBefore interruption, checkpoint branch/commit, completed/unverified work, files, proof/invalidation, external effects/retry safety, next action, and prohibited repetitions; resume there.\n\n## Whole-run usage governor\n\nToken efficiency governs the entire run, not only startup. Each retrieval/check must affect decisions/risk/proof. Inspect at most 12 text files or 64 KB per batch. One ledger/owner per question. After three search batches, act, narrow, checkpoint, or stop before a fourth batch. No duplicate crawls. The bundled checkpoint helper must open a usage ledger before a second batch. Preserve outcome/proof/safety.\n\n## Two checkpoint types — never confuse them\n\n**Intent/authority:** Do not make the person approve a paraphrase before every local edit or harmless action. Ask only for material ambiguity, authority gaps, consequential choices, or requested locks; authorization persists.\n\n**Progress:** A progress checkpoint is automatic, non-blocking. Save each coherent slice before long operations/handoffs; leave at most one slice or five materially changed files uncommitted. Commit owned files, push to the existing task branch when available, and verify its remote revision. Preservation grants no merge/deploy authority. Otherwise save resume state and local limits. A progress message without saved state is not a checkpoint. See [durable progress](references/durable-progress-and-recovery.md).\n\n## Silent human decision integrity\n\nCheck public/persuasive/transactional comprehension, honest value, trust, and abuse. Use color deliberately, never alone. Prevent fake proof, bait-and-switch offers, hidden fees, deceptive consent, lead resale, payment diversion, and fraud; preserve correction/cancellation/reporting/recovery. See [human decision integrity](references/human-decision-integrity.md).\n\nProfile publication follows approved eligibility/privacy/entitlements; invent neither free discovery nor paywalls. See [profile policy](references/public-profile-tier-parity.md). For named entities, recover essential identity/trust assets from available identity-matched authoritative sources. Verify usable renders before completion; report missing assets, access, or proof. See [first-pass completeness](references/product-design-intelligence.md#first-pass-completeness).\n\n## SI defects and cold starts\n\nRepair interpretation/routing/state/completion defects causally in canonical source; regenerate copies, record/retest, avoid model-specific permanent instructions. Fresh standing-adoption tasks must recover owning system, current sources/breakpoint, constraints, remaining states/tools/action/proof without historical chats; state access limits. See [model-neutral execution](references/model-neutral-execution.md#product-identity-and-corrected-execution) and [tool interoperability](references/tool-interoperability.md).\n\n## Escalation\n\nUse Guided Council only when the person explicitly requests it or when at least one condition is present: costly ambiguity, architecture/migration locks, money, credentials, permissions, private data, security, regulated claims, destructive work, consequential publication, or repeated failure after Lean correction. Necessary roles only.\n\nHost capability boundary for this prepared Selective Intelligence agent\n\nThe supplied New agent screenshot shows gpt-6-astra with subagents OFF. This is an observed configuration, not entitlement, authority to spend, or proof that an agent exists. Respect the user's Pro preference only where the governing product actually supports and authorizes it; never invent a model ID or infer access from a product name.\n\nApply the canonical SI behavior above. Reconstruct the current intent, owning product, scope, sources, breakpoint, essential assets, available capabilities, and actual completion condition. Use product-specific source and entitlement rules from that product's governing evidence; never impose another product's free or paid visibility baseline.\n\nFor named business or profile work, inventory the essential logo, photos, and identity assets before declaring completion. Inspect available authoritative sources matched to the correct business identity. Specific providers such as Google, Facebook, or Accent are required only by the governing product or explicit user scope. Do not call a bare or default profile complete. Verify the actual rendered result and usability where access permits; state unresolved source or rendering access honestly. Instruction text alone is not proof that this flow works.\n\nPerform Orchestrator intent reconstruction, a pre-work Objector challenge, bounded Worker execution, and a post-work Objector proof check. With subagents OFF, same-context self-review is degraded, never independent. For material work, use a separate Objector context when the host supports one and it is authorized. Do not silently enable subagents, create another session, call a paid service, or invent an external reviewer. If no distinct context is available, label review degraded, perform useful authorized work, and report the limitation.\n\nThe free portable SI core needs no paid provider or new API key. This optional API host may bill for models, tools, and hosted environments when executed. Preparation and displayed settings authorize no spending, credential creation, grants, publication, account changes, or run. Use only available authorized tools and sources. Give the person a usable outcome with evidence and remaining blockers in plain language.\n", + "multi_agent": { + "enabled": false + } +} diff --git a/adapters/openai-agent/test_agent.py b/adapters/openai-agent/test_agent.py new file mode 100644 index 0000000..6c668a0 --- /dev/null +++ b/adapters/openai-agent/test_agent.py @@ -0,0 +1,352 @@ +"""Offline adapter tests; these are not hosted-agent/model acceptance tests.""" +from __future__ import annotations + +import base64 +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +import agent +import bootstrap + + +def directory_alias(alias: Path, target: Path) -> None: + """Exercise a real redirected directory without granting Windows privileges.""" + try: + alias.symlink_to(target, target_is_directory=True) + except OSError as exc: + if os.name != "nt" or getattr(exc, "winerror", None) != 1314: + raise + # A junction exercises the same resolve-versus-absolute boundary. + subprocess.run( + ["cmd.exe", "/d", "/c", "mklink", "/J", str(alias), str(target)], + check=True, capture_output=True, text=True, + ) + + +class AgentTests(unittest.TestCase): + def test_definition_preserves_source_and_roles(self): + value = agent.definition() + self.assertEqual(value["name"], "Selective Intelligence") + self.assertEqual(bootstrap.SOURCE_COMMIT, "e194759bb6507f5f651d0d69edd19fdafdbb8b2c") + self.assertEqual(bootstrap.SOURCE_TREE, "6da6833f5c0703c909ebb4d4df4060857ea48b73") + self.assertEqual(value, json.loads((agent.ROOT / "prepared-definition.json").read_bytes())) + self.assertEqual(value["multi_agent"], {"enabled": False}) + self.assertEqual(set(value), {"name", "model", "instructions", "multi_agent"}) + self.assertIn("essential logo, photos", value["instructions"]) + self.assertIn("Objector", value["instructions"]) + self.assertNotIn("api_key", json.dumps(value)) + + def test_prepared_definition_tamper_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = (agent.ROOT / "prepared-definition.json").read_bytes() + (root / "prepared-definition.json").write_bytes(raw.replace(b"gpt-6-astra", b"gpt-6-sol")) + with patch.object(agent, "ROOT", root), self.assertRaisesRegex(ValueError, "reviewed packet"): + agent.definition() + + def test_crlf_checkout_preserves_reviewed_definition(self): + expected = agent.definition() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + raw = (agent.ROOT / "prepared-definition.json").read_bytes() + (root / "prepared-definition.json").write_bytes(raw.replace(b"\n", b"\r\n")) + with patch.object(agent, "ROOT", root): + self.assertEqual(agent.definition(), expected) + + def test_unreviewed_model_rejected(self): + with self.assertRaisesRegex(ValueError, "reviewed only"): + agent.definition("gpt-6-sol") + + def test_registration_local_source_block_prevents_receipt_and_request(self): + with tempfile.TemporaryDirectory() as directory, patch.dict( + os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"} + ), patch.object(agent, "api_request") as request: + state = Path(directory) / "receipt.json" + with self.assertRaisesRegex(RuntimeError, "local-only"): + agent.register(state, request=request) + request.assert_not_called() + self.assertFalse(state.exists()) + + def test_hosted_bootstrap_is_blocked_before_git_or_source_write(self): + with tempfile.TemporaryDirectory() as directory: + destination = Path(directory) / "source" + with patch.object(bootstrap, "DESTINATION", destination), patch.object(bootstrap, "git") as git: + with self.assertRaisesRegex(RuntimeError, "local-only"): + bootstrap.prepare(destination) + git.assert_not_called() + self.assertFalse(destination.exists()) + + def test_invalid_model_rejected(self): + for model in ("", "gpt\nheader", "model; rm -rf /", "x" * 129): + with self.subTest(model=model), self.assertRaises(ValueError): + agent.definition(model) + + def test_environment_contains_exact_bootstrap(self): + value = agent.environment() + self.assertEqual(base64.b64decode(value["files"][0]["data"]), (agent.ROOT / "bootstrap.py").read_bytes()) + self.assertEqual(value["network"], {"access": "restricted", "allowed_domains": ["github.com"]}) + self.assertNotIn("env", value) + self.assertNotIn("plugins", value) + self.assertNotIn("packages", value) + self.assertEqual(value["capability_directories"], ["/workspace/si-source/skills"]) + + def test_session_requires_identity_task_and_lane(self): + for identity, prompt, lane in (("../bad", "work", "test"), ("agent_1", " ", "test"), ("agent_1", "work", "../x")): + with self.subTest(identity=identity, lane=lane), self.assertRaises(ValueError): + agent.session_request(identity, prompt, lane) + with self.assertRaisesRegex(RuntimeError, "local-only"): + agent.session_request("agent_test", "Inspect this fixture", "fixture") + with patch.object(bootstrap, "require_hosted_source"): + value = agent.session_request("agent_test", "Inspect this fixture", "fixture") + self.assertEqual(value["agent_id"], "agent_test") + self.assertEqual(value["metadata"]["si_lane"], "fixture") + self.assertNotIn("agent", value) + + def test_export_is_offline_and_does_not_overwrite(self): + with tempfile.TemporaryDirectory() as directory, patch.object(agent, "api_request", side_effect=AssertionError("network call")): + output = Path(directory) / "new" + receipt = agent.export(output) + self.assertEqual(receipt["api_calls"], 0) + self.assertFalse(receipt["plugin_modified"]) + self.assertFalse(receipt["registration_allowed"]) + self.assertFalse(receipt["session_preparation_allowed"]) + self.assertFalse(receipt["multi_agent_enabled"]) + self.assertEqual(receipt["source_delivery"], "local_only_not_publicly_fetchable") + self.assertEqual(json.loads((output / "agent.json").read_text(encoding="utf-8")), agent.definition()) + self.assertEqual((output / "instructions.md").read_bytes(), agent.definition()["instructions"].encode("utf-8")) + with self.assertRaises(ValueError): + agent.export(output) + + def test_redirected_output_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "real").mkdir() + directory_alias(root / "alias", root / "real") + with self.assertRaises(ValueError): + agent.export(root / "alias" / "export") + + def test_missing_credentials_prevent_write(self): + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {}, clear=True): + state = Path(directory) / "receipt.json" + with self.assertRaises(ValueError): + agent.register(state, request=lambda *args: self.fail("unexpected request")) + self.assertFalse(state.exists()) + + def test_registration_readback_and_duplicate_prevention(self): + calls = [] + def fake(method, path, payload=None): + calls.append((method, path)) + return {"id": "agent_fixture", **agent.definition()} + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"}), patch.object(bootstrap, "require_hosted_source"): + state = Path(directory) / "registration.json" + result = agent.register(state, request=fake) + self.assertEqual(result["sessions_started"], 0) + self.assertEqual(calls, [("POST", "/agents"), ("GET", "/agents/agent_fixture")]) + with self.assertRaises(FileExistsError): + agent.register(state, request=fake) + self.assertEqual(len(calls), 2) + self.assertNotIn("synthetic-key", state.read_text(encoding="utf-8")) + + def test_uncertain_registration_is_not_retried(self): + calls = [] + def fail(*args): + calls.append(args) + raise TimeoutError("simulated lost response") + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"}), patch.object(bootstrap, "require_hosted_source"): + state = Path(directory) / "registration.json" + with self.assertRaises(TimeoutError): + agent.register(state, request=fail) + with self.assertRaises(FileExistsError): + agent.register(state, request=fail) + self.assertEqual(len(calls), 1) + self.assertEqual(json.loads(state.read_text(encoding="utf-8"))["status"], "registration_attempted_reconcile_before_retry") + + def test_readback_mismatch_preserves_created_identity(self): + def fake(method, path, payload=None): + return {"id": "agent_fixture", **agent.definition(), "model": "different"} if method == "GET" else {"id": "agent_fixture"} + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"}), patch.object(bootstrap, "require_hosted_source"): + state = Path(directory) / "registration.json" + with self.assertRaises(RuntimeError): + agent.register(state, request=fake) + self.assertTrue(state.with_name(state.name + ".created.json").exists()) + self.assertFalse(state.with_name(state.name + ".verified.json").exists()) + + def test_tool_readback_mismatch_rejected(self): + def fake(method, path, payload=None): + return {"id": "agent_fixture", **agent.definition(), "tools": [{"type": "web_search"}]} + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"}), patch.object(bootstrap, "require_hosted_source"): + with self.assertRaises(RuntimeError): + agent.register(Path(directory) / "registration.json", request=fake) + + def test_registration_endpoint_guard(self): + with patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"}): + for endpoint in ("/agents/../../files", "/agents/sessions", "https://example.org/agents"): + with self.subTest(endpoint=endpoint), self.assertRaises(ValueError): + agent.api_request("POST", endpoint, {}) + + def test_direct_requests_cannot_bypass_local_source_block(self): + with patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture"}), patch.object( + agent.urllib.request, "Request", side_effect=AssertionError("request constructed") + ) as construct, patch.object(agent.urllib.request, "build_opener", side_effect=AssertionError("network opener")) as opener: + for method, endpoint in (("POST", "/agents"), ("GET", "/agents/agent_fixture")): + with self.subTest(method=method), self.assertRaisesRegex(RuntimeError, "local-only"): + agent.api_request(method, endpoint, {}) + construct.assert_not_called() + opener.assert_not_called() + + def test_credentials_not_in_git_environment(self): + with patch.dict(os.environ, {"OPENAI_API_KEY": "synthetic-key", "GITHUB_TOKEN": "synthetic-token", "GIT_CONFIG_COUNT": "5"}): + environment = bootstrap.git_environment() + self.assertNotIn("OPENAI_API_KEY", environment) + self.assertNotIn("GITHUB_TOKEN", environment) + self.assertEqual(environment["GIT_CONFIG_COUNT"], "0") + self.assertEqual(environment["GIT_TERMINAL_PROMPT"], "0") + + def test_bootstrap_refuses_other_destination(self): + with self.assertRaises(ValueError): + bootstrap.prepare(Path("/tmp/not-the-agent-source")) + + +class RegistrationIdentityTests(unittest.TestCase): + """Bind readback to the created ID and preserve every uncertain operation.""" + + def check_rejected(self, created, observed, *, expect_readback=True): + calls = [] + def fake(method, path, payload=None): + calls.append((method, path)) + return created if method == "POST" else observed + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, { + "OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture" + }, clear=True), patch.object(bootstrap, "require_hosted_source"), patch.object(agent.urllib.request, "build_opener", side_effect=AssertionError("network forbidden")): + state = Path(directory) / "registration.json" + with self.assertRaises(RuntimeError): + agent.register(state, request=fake) + intent = state.read_bytes() + self.assertFalse(state.with_name(state.name + ".verified.json").exists()) + created_path = state.with_name(state.name + ".created.json") + self.assertEqual(created_path.exists(), expect_readback) + if expect_readback: + self.assertEqual(json.loads(created_path.read_text())["agent_id"], "agent_fixture") + expected = [("POST", "/agents")] + if expect_readback: + expected.append(("GET", "/agents/agent_fixture")) + self.assertEqual(calls, expected) + with self.assertRaises(FileExistsError): + agent.register(state, request=fake) + self.assertEqual(calls, expected) + self.assertEqual(state.read_bytes(), intent) + + def test_missing_readback_identity_is_rejected(self): + self.check_rejected({"id": "agent_fixture"}, agent.definition()) + + def test_different_readback_identity_is_rejected(self): + self.check_rejected({"id": "agent_fixture"}, {**agent.definition(), "id": "agent_other"}) + + def test_malformed_readback_identity_is_rejected(self): + for identity in (None, "", 1, True, [], {}, "agent_fixture\n"): + with self.subTest(identity=identity): + self.check_rejected({"id": "agent_fixture"}, {**agent.definition(), "id": identity}) + + def test_nonobject_creation_is_rejected(self): + for created in (None, [], "agent_fixture", 42): + with self.subTest(created=created): + self.check_rejected(created, {}, expect_readback=False) + + def test_nonobject_readback_is_rejected(self): + for observed in (None, [], "agent_fixture", 42): + with self.subTest(observed=observed): + self.check_rejected({"id": "agent_fixture"}, observed) + + def test_malformed_tool_item_is_rejected(self): + for tool in (None, "web_search", [], 42): + with self.subTest(tool=tool): + self.check_rejected({"id": "agent_fixture"}, { + **agent.definition(), "id": "agent_fixture", "tools": [tool] + }) + + def test_matching_identity_allows_provider_metadata(self): + calls = [] + def fake(method, path, payload=None): + calls.append((method, path)) + return {**agent.definition(), "id": "agent_fixture", "object": "agent", "created_at": 1} + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, { + "OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture" + }, clear=True), patch.object(bootstrap, "require_hosted_source"), patch.object(agent.urllib.request, "build_opener", side_effect=AssertionError("network forbidden")): + state = Path(directory) / "registration.json" + result = agent.register(state, request=fake) + self.assertEqual(result["agent_id"], "agent_fixture") + self.assertEqual(result["sessions_started"], 0) + self.assertEqual(json.loads(state.with_name(state.name + ".verified.json").read_text()), result) + self.assertEqual(calls, [("POST", "/agents"), ("GET", "/agents/agent_fixture")]) + + def test_readback_failure_retains_id_and_never_recreates(self): + calls = [] + def fake(method, path, payload=None): + calls.append((method, path)) + if method == "POST": + return {"id": "agent_fixture"} + raise TimeoutError("synthetic readback timeout") + with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, { + "OPENAI_API_KEY": "synthetic-key", "OPENAI_PROJECT_ID": "proj_fixture" + }, clear=True), patch.object(bootstrap, "require_hosted_source"), patch.object(agent.urllib.request, "build_opener", side_effect=AssertionError("network forbidden")): + state = Path(directory) / "registration.json" + with self.assertRaises(TimeoutError): + agent.register(state, request=fake) + self.assertEqual(json.loads(state.with_name(state.name + ".created.json").read_text())["agent_id"], "agent_fixture") + self.assertFalse(state.with_name(state.name + ".verified.json").exists()) + with self.assertRaises(FileExistsError): + agent.register(state, request=fake) + self.assertEqual(calls, [("POST", "/agents"), ("GET", "/agents/agent_fixture")]) + + + +class SourceTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + bootstrap.git(self.root, "init", "--template=") + self.skill = self.root / "skills" / "selective-intelligence" / "SKILL.md" + self.skill.parent.mkdir(parents=True) + self.skill.write_text("# Synthetic SI source fixture\n", encoding="utf-8", newline="\n") + bootstrap.git(self.root, "add", ".") + bootstrap.git(self.root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "-m", "fixture") + self.commit = bootstrap.git(self.root, "rev-parse", "HEAD") + self.tree = bootstrap.git(self.root, "rev-parse", "HEAD^{tree}") + + def tearDown(self): + self.temporary.cleanup() + + def test_real_git_fixture_verifies(self): + self.assertEqual(bootstrap.verify_checkout(self.root, self.commit, self.tree)["status"], "source_identity_verified") + + def test_wrong_commit_or_tree_rejected(self): + for commit, tree in (("0" * 40, self.tree), (self.commit, "0" * 40)): + with self.subTest(commit=commit, tree=tree), self.assertRaises(ValueError): + bootstrap.verify_checkout(self.root, commit, tree) + + def test_dirty_or_extra_file_rejected(self): + (self.root / "extra.txt").write_text("unapproved") + with self.assertRaises(ValueError): + bootstrap.verify_checkout(self.root, self.commit, self.tree) + + def test_assume_unchanged_cannot_hide_modified_skill(self): + bootstrap.git(self.root, "update-index", "--assume-unchanged", "skills/selective-intelligence/SKILL.md") + self.skill.write_text("tampered\n") + with self.assertRaisesRegex(ValueError, "bytes differ"): + bootstrap.verify_checkout(self.root, self.commit, self.tree) + + def test_redirected_source_rejected(self): + with tempfile.TemporaryDirectory() as directory: + alias = Path(directory) / "source" + directory_alias(alias, self.root) + with self.assertRaises(ValueError): + bootstrap.verify_checkout(alias, self.commit, self.tree) + + +if __name__ == "__main__": + unittest.main() diff --git a/adapters/openai-agent/test_resources.py b/adapters/openai-agent/test_resources.py new file mode 100644 index 0000000..446f9d6 --- /dev/null +++ b/adapters/openai-agent/test_resources.py @@ -0,0 +1,174 @@ +"""New resource-input checks; do not rerun the existing adapter suite.""" +from __future__ import annotations + +import base64 +import hashlib +import io +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch +import zipfile + +import agent +import bootstrap +from test_agent import directory_alias + +ARCHIVE = agent.ROOT.parents[2] / "evidence/resource-delivery/e194-canonical-source.zip" + + +class ResourceTests(unittest.TestCase): + def test_prepare_uses_canonical_remote_with_redirects_disabled(self): + class FetchStopped(Exception): + pass + + self.assertFalse(bootstrap.SOURCE_READY_FOR_HOSTED_USE) + actual_git = bootstrap.git + observed = {} + + def stop_before_fetch(directory, *arguments): + if arguments[0] == "fetch": + observed["repository"] = actual_git(directory, "remote", "get-url", "origin") + observed["follow_redirects"] = actual_git(directory, "config", "--get", "http.followRedirects") + raise FetchStopped + return actual_git(directory, *arguments) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + destination = root / "si-source" + # Exercise real Git staging only. The test bypasses the readiness gate + # and intercepts fetch before any network request can execute. + with patch.object(bootstrap, "DESTINATION", destination), \ + patch.object(bootstrap, "require_hosted_source"), \ + patch.object(bootstrap, "git", side_effect=stop_before_fetch): + with self.assertRaises(FetchStopped): + bootstrap.prepare(destination) + self.assertEqual(list(root.iterdir()), []) + self.assertEqual(observed["repository"], "https://github.com/flavorgood/Selective-Intelligence.git") + self.assertEqual(observed["follow_redirects"], "false") + + def test_pinned_resource_build_keeps_exact_accepted_archive(self): + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "rebuilt.zip" + receipt = bootstrap.build_resources(agent.ROOT.parents[1], output) + self.assertEqual(output.read_bytes(), ARCHIVE.read_bytes()) + self.assertEqual(receipt["resource_sha256"], bootstrap.RESOURCE_SHA256) + self.assertEqual(receipt["files"], 119) + + def test_exported_inputs_execute_and_install_exact_complete_resources(self): + raw, manifest, files = bootstrap.read_resources(ARCHIVE) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + exported = root / "export" + with patch.object(agent, "api_request", side_effect=AssertionError("API call")): + receipt = agent.export(exported, resources=ARCHIVE) + environment = json.loads((exported / "session-environment.json").read_bytes()) + self.assertEqual(environment["network"], {"access": "disabled"}) + self.assertEqual(environment["capability_directories"], ["/workspace/si-source/skills"]) + self.assertEqual(json.loads((exported / "agent.json").read_bytes()), agent.definition()) + self.assertFalse(receipt["registration_allowed"]) + self.assertFalse(receipt["session_preparation_allowed"]) + self.assertEqual(len(environment["files"]), 2) + workspace = root / "workspace" + workspace.mkdir() + for item in environment["files"]: + (workspace / Path(item["path"]).name).write_bytes(base64.b64decode(item["data"], validate=True)) + self.assertEqual((workspace / "si-source-e194.zip").read_bytes(), raw) + command = environment["setup_commands"][0]["command"].split() + self.assertEqual(command[0], "python3") + # Rebase only provider absolute paths onto the isolated Windows fixture. + local = [sys.executable, "-B"] + [ + str(workspace / Path(part).name) if part.startswith("/workspace/") else part + for part in command[1:] + ] + completed = subprocess.run(local, check=True, capture_output=True, text=True, + cwd=workspace, env=bootstrap.git_environment()) + installed = json.loads(completed.stdout) + self.assertEqual(installed["status"], "canonical_resource_inputs_installed_offline") + self.assertEqual(installed["files"], 119) + self.assertFalse(installed["hosted_acceptance"]) + self.assertFalse(installed["full_repository_checkout"]) + source = workspace / "si-source" + actual = {str(path.relative_to(source)).replace("\\", "/") for path in + (source / "skills/selective-intelligence").rglob("*") if path.is_file()} + self.assertEqual(actual, set(manifest["files"])) + for name, data in files.items(): + self.assertEqual((source / name).read_bytes(), data) + self.assertEqual(bootstrap.blob_id(data), manifest["files"][name]["blob"]) + for link in ("references/evidence-and-completion.md", "references/product-design-intelligence.md", + "references/public-profile-tier-parity.md", "subskills/si-objector/SKILL.md"): + self.assertTrue((source / "skills/selective-intelligence" / link).is_file(), link) + self.assertFalse(bootstrap.SOURCE_READY_FOR_HOSTED_USE) + + def test_corrupt_archive_creates_no_destination(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + corrupt = root / "corrupt.zip" + corrupt.write_bytes(ARCHIVE.read_bytes() + b"tamper") + target = root / "new" + with self.assertRaisesRegex(ValueError, "pinned archive"): + bootstrap.install_resources(corrupt, target) + self.assertFalse(target.exists()) + + def test_existing_destination_is_preserved(self): + with tempfile.TemporaryDirectory() as directory: + target = Path(directory) / "existing" + target.mkdir() + marker = target / "keep.txt" + marker.write_bytes(b"owned before installation") + with self.assertRaises(FileExistsError): + bootstrap.install_resources(ARCHIVE, target) + self.assertEqual(marker.read_bytes(), b"owned before installation") + self.assertEqual(list(target.iterdir()), [marker]) + + def test_redirected_destination_is_preserved(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + real = root / "owned" + real.mkdir() + directory_alias(root / "alias", real) + with self.assertRaisesRegex(ValueError, "redirect"): + bootstrap.install_resources(ARCHIVE, root / "alias" / "new") + self.assertFalse((real / "new").exists()) + + def test_unsafe_resource_paths_rejected(self): + for name in ("../escape", "/skills/selective-intelligence/a", "skills/selective-intelligence/../a", + "skills/selective-intelligence/a//b", "skills/selective-intelligence/CON.txt", + "skills/selective-intelligence/a\\b", "skills/selective-intelligence/a:b", + "skills/selective-intelligence/a."): + with self.subTest(name=name), self.assertRaises(ValueError): + bootstrap.resource_path(name) + + def test_manifest_provenance_cannot_be_changed(self): + raw, manifest, _ = bootstrap.read_resources(ARCHIVE) + for key, value in (("repository", "https://github.com/flavorgood/Selective-Intelligence.git"), + ("commit", "0" * 40), ("tree", "0" * 40)): + changed = {**manifest, key: value} + with self.subTest(key=key), tempfile.TemporaryDirectory() as directory: + corrupt = Path(directory) / "wrong-source.zip" + with zipfile.ZipFile(io.BytesIO(raw)) as source, zipfile.ZipFile(corrupt, "w") as dest: + for info in source.infolist(): + dest.writestr(info, json.dumps(changed).encode() if info.filename == "source-manifest.json" else source.read(info)) + with patch.object(bootstrap, "RESOURCE_SHA256", hashlib.sha256(corrupt.read_bytes()).hexdigest()): + with self.assertRaisesRegex(ValueError, "provenance"): + bootstrap.read_resources(corrupt) + + def test_symlink_resource_rejected_even_under_synthetic_fingerprint(self): + raw, _, _ = bootstrap.read_resources(ARCHIVE) + with tempfile.TemporaryDirectory() as directory: + corrupt = Path(directory) / "symlink.zip" + with zipfile.ZipFile(io.BytesIO(raw)) as source, zipfile.ZipFile(corrupt, "w") as dest: + for info in source.infolist(): + if info.filename.endswith("/SKILL.md"): + info.external_attr = 0o120777 << 16 + dest.writestr(info, source.read(info)) + with patch.object(bootstrap, "RESOURCE_SHA256", hashlib.sha256(corrupt.read_bytes()).hexdigest()): + with self.assertRaisesRegex(ValueError, "mode or symlink"): + bootstrap.read_resources(corrupt) + + +if __name__ == "__main__": + unittest.main()