From 43163a83284375a322b1225d57bc3dc0b91f96fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:36:32 +0900 Subject: [PATCH 01/13] docs: make README product-first and buyer-friendly --- README.md | 475 +++++++++++++++++------------------------------------- 1 file changed, 149 insertions(+), 326 deletions(-) diff --git a/README.md b/README.md index aac8108..cbbc9d9 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,71 @@ -# egressweave +# EgressWeave [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ContextualWisdomLab/EgressWeave) -**SSRF- and DNS-rebinding-safe outbound HTTP for Python.** - -`egressweave` validates an outbound URL against exact host-and-port -authority pairs and an HTTP-method allowlist, refuses any target that resolves -to a non-globally-routable address, and hands back a synchronous `httpx.Client` or -asynchronous `httpx.AsyncClient` whose every connection is *pinned* to the -validated addresses—rejecting authority drift and bounding outbound request -bodies, request-phase waits, and inbound identity-coded response bodies. - -It exists because the naive pattern—resolve, check the IP, then -`httpx.get(url)`—is unsafe. An attacker-controlled DNS answer can change between -the check and the connect (a TOCTOU / DNS-rebinding attack, CWE-350), while a -permissive URL, port, or method policy can reach unintended services (SSRF, -CWE-918), and even an allowlisted authority can exhaust resources with an -unbounded request producer, disabled timeout, or unbounded or compressed -response (CWE-400). - -## Product and architecture source of truth - -Start with [`docs/product/PRD.md`](docs/product/PRD.md) for buyer/product -requirements and [`docs/product/TRD.md`](docs/product/TRD.md) for the technical -contract. Root [`ARCHITECTURE.md`](ARCHITECTURE.md) remains the authoritative -protected-main implementation architecture. The product documentation labels -shipped, active-PR, accepted-target, and planned behavior separately so an -unmerged design is never presented as protected-main functionality. - -Architecture views and durable decisions are indexed in -[`docs/architecture/UML.md`](docs/architecture/UML.md), -[`docs/architecture/ERD.md`](docs/architecture/ERD.md), and -[`docs/adr/README.md`](docs/adr/README.md). The ERD explicitly records that the -core library owns no durable database rather than inventing persistence for -architecture completeness. - -## What it defends against - -- **SSRF (CWE-918):** rejects private, loopback, link-local, reserved, - multicast, unspecified, and otherwise non-global addresses; rejects embedded - credentials, query/fragment, plaintext `http` to remote hosts, IP-literal - hosts, backslash smuggling, and ASCII control characters. -- **Unexpected services on trusted hosts:** RFC 9110 defines an origin by its - scheme, host, and port. EgressWeave defaults to port 443 only and requires - explicit opt-in before an alternate TLS or local-development port is usable. -- **DNS rebinding / validate-then-connect TOCTOU (CWE-350):** resolves all - addresses up front, validates each, and pins them into a custom transport - that re-validates on every connect and refuses host/port drift. -- **Application-layer tunnelling:** a positive HTTP-method allowlist is enforced - at the transport boundary. Common API methods are enabled by default, unusual - methods require explicit opt-in, and `CONNECT` can never be authorized. -- **Unbounded or ambiguously framed requests (CWE-400, CWE-444):** both - transports reject a declared `Content-Length` beyond the finite policy budget - before pool dispatch and count actual synchronous or asynchronous stream - bytes. A valid declared length must equal the bytes consumed exactly; excess, - truncation, chunked overruns, and retry-based budget resets fail closed. The - first chunk crossing a policy or declared boundary is withheld and its source - is closed without replacing the generic policy error. -- **Disabled or excessive request timeouts (CWE-400):** both transports replace - missing or explicitly disabled HTTPX/HTTPCore connect, read, write, and pool - timeout values with immutable finite policy ceilings. Stricter non-negative - caller values are preserved, larger values are capped, and malformed timeout - metadata fails generically before connection-pool dispatch. -- **Unbounded response consumption (CWE-400):** both transports force - `Accept-Encoding: identity`, reject body-bearing content-coded responses and - unsafe declared lengths before returning a response, and count every - transfer-decoded identity body byte. Chunked, close-delimited, missing-length, - and dishonestly under-declared bodies cannot exceed the finite policy budget. -- **Bounded DNS resolution:** synchronous and asynchronous validation apply the - same finite positive `dns_timeout_seconds` deadline. Resolver workers are - concurrency-bounded and failures remain generic. -- **Exact egress allowlist:** only normalized `(hostname, port)` pairs explicitly - present in the policy are reachable; wildcards and accidental cross-pair - combinations are refused. -- Redirects are disabled and environment proxies ignored (`trust_env=False`), - so a `302` cannot bounce a request to an unvalidated host, and Unix sockets - are refused. -- **Fail-closed optional configuration:** an empty or absent base URL returns a - deny-all client rather than an unrestricted fallback transport. - -## Publication status - -Release automation and package acceptance establish that a commit is ready -to publish; they do not establish that an artifact is already available. -A bare `pip install egressweave` command is authoritative only after the -exact version appears on a verified PyPI project page with its wheel, source -distribution, and publish-attestation evidence. Until then, -install from a reviewed source checkout and preserve the repository's -hash-locked validation before promoting the package into another system. - -## Install - -After the target version is verified on PyPI: +**Provider-neutral outbound HTTP security for Python applications that need explicit egress authority instead of ambient network trust.** -```bash -pip install egressweave -``` +EgressWeave turns an approved HTTPS destination and a small policy into a synchronous or asynchronous HTTPX client that keeps destination authority stable from URL validation through DNS resolution and connection establishment. It is designed for applications that must call external APIs without allowing SSRF, DNS rebinding, proxy inheritance, redirects, malformed framing, or unbounded network I/O to widen the approved network boundary. + +It is a library, not a firewall or service mesh. Use it inside an application to make outbound HTTP decisions explicit, reviewable, and fail-closed; keep network-layer enforcement, tenant authorization, credentials, and business-level request policy in their owning systems. + +## Why EgressWeave + +| Need | EgressWeave contract | +| --- | --- | +| Call only approved remote services | Exact normalized `(hostname, port)` authority allowlists and positive HTTP-method policy | +| Resist DNS rebinding | Validate every resolved address, then connect only through the validated address set while preserving the approved hostname for TLS and HTTP authority | +| Avoid ambient routing surprises | Redirects are disabled, environment proxies are ignored, Unix-socket and caller-selected destination bypasses are refused | +| Bound network resource use | Finite DNS, timeout, request-body, response-body, target, header, pool and connection policies | +| Keep failures safe to expose | Policy denials use a stable public error contract instead of leaking resolver or transport internals | +| Support ordinary Python services | Synchronous and asynchronous clients with the same core security invariants | -From a reviewed local checkout before the first public release: +The protected implementation currently supports Python 3.10–3.14 and uses pinned `httpx`, `httpcore`, and `idna` runtime dependencies. The package metadata version is `0.3.0`; this repository currently has no GitHub release, so do not infer public artifact availability from the source version alone. + +## Quickstart + +Install from a reviewed checkout until the target package release has independent publication evidence: ```bash python -m pip install . ``` -## Quickstart - -Synchronous applications: +Create an exact egress policy and a synchronous client: ```python from egressweave import EgressPolicy, build_egress_sync_client -policy = EgressPolicy.from_hosts("api.openai.com, api.anthropic.com") - -normalized_url, client = build_egress_sync_client( - "https://api.openai.com/v1", policy=policy +policy = EgressPolicy.from_hosts("api.example.com") +base_url, client = build_egress_sync_client( + "https://api.example.com/v1", + policy=policy, ) + with client: - response = client.get(f"{normalized_url}/models") + response = client.get(f"{base_url}/status") ``` -Asynchronous applications: +Asynchronous applications use the same policy boundary: ```python from egressweave import EgressPolicy, build_egress_http_client -policy = EgressPolicy.from_hosts("api.openai.com, api.anthropic.com") - -normalized_url, client = await build_egress_http_client( - "https://api.openai.com/v1", policy=policy -) -async with client: - response = await client.get(f"{normalized_url}/models") -``` - -Narrow the method surface for each integration: - -```python -read_only_policy = EgressPolicy.from_hosts( +policy = EgressPolicy.from_hosts( "api.example.com", allowed_methods={"GET", "HEAD"}, ) -``` - -Authorize a non-standard HTTPS port only when the integration requires it: - -```python -alternate_port_policy = EgressPolicy.from_hosts( - "api.example.com", - allowed_ports={443, 8443}, +base_url, client = await build_egress_http_client( + "https://api.example.com/v1", + policy=policy, ) + +async with client: + response = await client.get(f"{base_url}/status") ``` -When several hosts use different ports, enumerate the exact authority pairs -instead of authorizing their Cartesian product: +When different hosts need different ports, enumerate the allowed authority pairs instead of granting their Cartesian product: ```python -split_service_policy = EgressPolicy.from_authorities( +policy = EgressPolicy.from_authorities( [ ("api.example.com", 443), ("admin.example.com", 8443), @@ -163,231 +73,144 @@ split_service_policy = EgressPolicy.from_authorities( ) ``` -Configure private trust or mutual TLS without sharing mutable SSL contexts: +## Security model -```python -from egressweave import TLSConfiguration +EgressWeave protects the **outbound transport decision**. Its guarded clients reject destinations and request metadata that would make the approved authority ambiguous or broader than the configured policy. -tls_configuration = TLSConfiguration( - ca_file="/etc/company/private-ca.pem", - client_certificate_file="/etc/company/client.pem", - client_private_key_file="/etc/company/client.key", -) -normalized_url, client = build_egress_sync_client( - "https://api.example.com", - policy=EgressPolicy.from_hosts("api.example.com"), - tls_configuration=tls_configuration, -) -``` +The protected implementation includes these control families: -Set integration-specific outbound and inbound body budgets when the 16 MiB -defaults are not appropriate: +- **SSRF resistance:** private, loopback, link-local, reserved, multicast, unspecified, IP-literal, credential-bearing, malformed, and unauthorized destinations fail closed. +- **DNS-rebinding resistance:** all accepted address candidates are validated before use and bound to the approved authority through the pinned transport. +- **Exact origin control:** hostname, port, scheme and HTTP method remain explicit; `CONNECT` is never authorized. +- **TLS identity preservation:** validated address pinning does not replace the approved hostname used for TLS server identity and HTTP authority. +- **Request bounds:** request target, headers, body framing, actual streamed bytes, declared length and per-phase timeouts are finite and checked before or during dispatch. +- **Response bounds:** response headers, declared length, content coding and actual streamed bytes are bounded; guarded clients request identity encoding to avoid unbounded decompression through the normal path. +- **Stable denial semantics:** rejected operations raise the public policy error rather than exposing dependency-private failure details as an oracle. +- **Deny-all optional configuration:** a missing or blank optional base URL produces a client that cannot perform network I/O instead of silently falling back to unrestricted HTTP. -```python -artifact_policy = EgressPolicy.from_hosts( - "artifacts.example.com", - max_request_bytes=8 * 1024 * 1024, - max_response_bytes=64 * 1024 * 1024, -) -``` +EgressWeave complements, rather than replaces, firewall/service-mesh egress policy, sandboxing, application authorization, OAuth/API-key scope, tenant policy, malware inspection, job-level cancellation and service-level operations. + +See [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.md) and [`ARCHITECTURE.md`](ARCHITECTURE.md) for the detailed trust boundary and implementation evidence. + +## Configure the boundary -Set immutable per-phase timeout ceilings when an integration needs limits other -than the five-second defaults: +The high-level policy surface is intentionally small: ```python -from egressweave import EgressTimeoutPolicy +from egressweave import EgressPolicy, EgressTimeoutPolicy -timeout_policy = EgressTimeoutPolicy( +timeouts = EgressTimeoutPolicy( connect_timeout_seconds=2, read_timeout_seconds=10, write_timeout_seconds=5, pool_timeout_seconds=1, ) + policy = EgressPolicy.from_hosts( - "api.example.com", - request_timeout_policy=timeout_policy, + "artifacts.example.com", + allowed_methods={"GET", "HEAD", "PUT"}, + max_request_bytes=8 * 1024 * 1024, + max_response_bytes=64 * 1024 * 1024, + request_timeout_policy=timeouts, ) ``` -The default authority projection uses port `{443}`. `from_hosts(...)` remains -concise when several hosts share one port or one host intentionally exposes -several ports. Supplying several hosts and several ports is rejected as -ambiguous; use `from_authorities(...)` to enumerate the exact permitted pairs. -Hostnames use the same UTS #46 normalization as URL validation, and ports may be -integers or ASCII decimal strings between 1 and 65535. Empty port segments are -ignored for environment-variable ergonomics. Port zero, booleans, floats, -malformed text, and out-of-range values fail fast. The exact normalized pair is -checked before DNS resolution. - -The default method set is `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and -`OPTIONS`. Method names are validated and normalized at policy construction. -Less common non-tunnelling methods such as `PROPFIND` require explicit opt-in. -`CONNECT` is always rejected, including when present in configuration. - -The default request-body budget is 16 MiB. `max_request_bytes` accepts a -positive integer or an ASCII decimal string for environment-variable use. Zero, -negative, boolean, fractional, empty, signed, non-ASCII, or malformed values -fail at policy construction. A single declared `Content-Length` greater than -the budget is rejected before connection-pool dispatch. When a valid declared -length is present, the actual body must contain exactly that many bytes: excess -and truncated streams both fail closed. Chunked and missing-length streams are -still counted against the policy limit. The byte counter remains cumulative -across repeated iteration or retry of a replayable source, so re-consumption -does not grant another allowance. The first chunk crossing a policy or declared -boundary is not sent, the source stream is closed, and the generic -`EgressNotAllowedError` is raised without disclosing thresholds or byte counts. - -The default `EgressTimeoutPolicy` applies five-second ceilings independently to -connect, read, write, and pool-acquisition phases. HTTPX low-level request -metadata cannot disable a phase with `None` or extend it beyond the injected -ceiling. A finite non-negative value below the ceiling remains valid, including -zero for an immediate stricter timeout. Boolean, negative, non-finite, -non-numeric, unknown-key, and non-mapping metadata fails with the generic policy -error before HTTPCore dispatch. These phase limits bound inactivity rather than -a complete end-to-end wall-clock duration; applications should still impose job -cancellation, queue capacity, concurrency, tenant quota, and total-deadline -controls where required. - -The default response-body budget is 16 MiB. `max_response_bytes` accepts a -positive integer or an ASCII decimal string for environment-variable use. Zero, -negative, boolean, fractional, empty, signed, or malformed values fail at policy -construction. Pinned transports replace every caller compression preference -with `Accept-Encoding: identity`; a body-bearing response that still uses gzip, -deflate, Brotli, or another content coding is closed and rejected before HTTPX -can allocate decompressed output. Duplicate, malformed, or over-budget -`Content-Length` values fail before the response becomes caller-visible, and -every identity body stream is counted independently of framing metadata. On an -overrun, the underlying stream is closed and `EgressNotAllowedError` is raised. -Responses to `HEAD`, informational responses, `204`, and `304` remain bodyless -under RFC 9112 and do not treat representation metadata as transferred bytes. - -Both builders fail closed when the supplied base URL is `None`, empty, or only -whitespace: they return `(None, client)`, but that client rejects every request -with `EgressNotAllowedError` before network I/O. This lets applications preserve -optional configuration shapes without silently bypassing the egress policy. - -DNS resolution for both builders is bounded by `policy.dns_timeout_seconds`. -The value must be a finite positive number; invalid configuration is rejected -at policy construction rather than silently disabling the deadline. - -Hostname allowlist configuration is also validated when `EgressPolicy` is -constructed. Supply bare hostnames only. Wildcards, URLs, credentials, ports, -paths, IP literals or legacy numeric IP forms, and embedded whitespace/control -characters raise `ValueError` before request handling begins; non-string entries -raise `TypeError`. Empty host segments remain ignored so comma-separated -environment variables may contain trailing separators. - -Validate without building a client: +Private trust stores and mutual TLS are configured through immutable `TLSConfiguration` rather than a shared mutable SSL context: ```python -from egressweave import EgressPolicy, validate_egress_url, EgressNotAllowedError +from egressweave import EgressPolicy, TLSConfiguration, build_egress_sync_client -policy = EgressPolicy.from_hosts("api.openai.com") -try: - url = validate_egress_url("https://api.openai.com/v1", policy=policy) -except EgressNotAllowedError: - ... # generic, non-leaking rejection +tls = TLSConfiguration( + ca_file="/etc/company/private-ca.pem", + client_certificate_file="/etc/company/client.pem", + client_private_key_file="/etc/company/client.key", +) + +base_url, client = build_egress_sync_client( + "https://api.example.com", + policy=EgressPolicy.from_hosts("api.example.com"), + tls_configuration=tls, +) ``` -Local development requires both the local-address escape hatch and the exact -service port. For an Ollama-style container: +Local development requires explicit opt-in to both local addressing and the exact service port; production callers should not inherit that exception accidentally. -```python -policy = EgressPolicy.from_hosts( - "ollama", - allow_local=True, - allowed_ports={11434}, -) +## Product boundary + +EgressWeave owns the reusable in-process policy, validation, TLS and pinned-transport contracts. A host such as `naruon` owns provider configuration, credentials, tenancy, business authorization, persistence, audit retention, deployment, and the adapter that translates host settings into an EgressWeave policy. + +```text +Host application + │ approved base URL + explicit policy + ▼ +EgressWeave + ├─ URL / authority / method validation + ├─ bounded DNS resolution + address validation + ├─ TLS / transport policy + └─ bounded sync or async HTTPX client + │ + ▼ +Approved remote service ``` -## API +The library does not own a durable database. It does not infer which provider or endpoint a tenant is entitled to call, and it does not treat a successful network connection as application-level authorization. + +## Public API | Symbol | Purpose | -|---|---| -| `EgressPolicy` | Injected exact `(hostname, port)` authority, HTTP-method, DNS-timeout, local-address, request-timeout, and finite request/response body resource policy; use `from_authorities(...)` when both host and port axes vary. | -| `EgressTimeoutPolicy` | Immutable finite connect, read, write, and pool-acquisition timeout ceilings enforced immediately before HTTPCore dispatch. | -| `TLSConfiguration` | Immutable provider-neutral TLS 1.3/TLS 1.2 compatibility, private trust, and optional mutual-TLS client identity settings. | -| `validate_egress_url` / `validate_egress_url_details` (+ `_async`) | Validate a URL and resolve pinnable addresses. | -| `build_egress_sync_client(url, *, policy)` | Validate + build a synchronous DNS-pinned `httpx.Client`; empty URLs produce a deny-all client and request bodies, phase waits, and response bodies are bounded. | -| `build_egress_http_client(url, *, policy)` | Validate + build an asynchronous DNS-pinned `httpx.AsyncClient`; empty URLs produce a deny-all client and request bodies, phase waits, and response bodies are bounded. | -| `build_pinned_https_client(validated, *, policy)` | Build a bounded synchronous client from an already-validated URL. | -| `build_pinned_https_async_client(validated, *, policy)` | Build a bounded asynchronous client from an already-validated URL. | -| `ValidatedEgressURL`, `EgressNotAllowedError` | Result type and typed failure (a `ValueError`). | - -## Compatibility note - -Exact authority-pair allowlisting, finite request/response body limits, -identity-only response coding, and finite request-phase timeout ceilings are -intentional pre-1.0 secure-default tightenings. Applications with several hosts -and several ports must migrate ambiguous `from_hosts(...)` configuration to -explicit `from_authorities(...)` pairs. Integrations that legitimately upload -or consume more than 16 MiB per message must set larger, still-finite -`max_request_bytes` or `max_response_bytes` values. Integrations needing longer -network inactivity windows must inject larger, still-finite -`EgressTimeoutPolicy` ceilings. Integrations that require compressed response -content need a separately reviewed client with a bounded streaming decoder; -EgressWeave does not silently accept compression. - -## One source, multi use (OSMU) - -`egressweave` is extracted, behaviour-preserving, from a production control -plane ([naruon](https://github.com/ContextualWisdomLab/naruon)), where it guards -every LLM-provider call. It is usable both as a standalone dependency and as a -git submodule. The original extraction replaced the app-specific settings -object with an injected `EgressPolicy`. - -## Autonomous maintenance - -Two hourly, credential-separated workflows keep the pull-request queue and the -product roadmap moving without bypassing normal governance: - -- at minute `07`, the repository calls the organization-owned review-fix and - merge schedulers to inspect feedback, recheck current-head evidence, and - update eligible branches; final merges remain operator-controlled; -- at minute `37`, a bounded OpenCode maintainer backed by - `NVIDIA_NIM_API_KEY` runs only when there are zero open pull requests and - implements one test-driven improvement. - -The product workflow uses two fresh runners. The model job has read-only GitHub -permissions, no direct network access, and can emit only a guard-checked patch. -A second credential-free job builds trusted dependencies before applying the -patch and executes modified source only inside an offline, non-root, -capability-free, read-only verifier container. It emits only a short-lived -digest-bound handoff; no repository-local job obtains write authority or -publishes the patch. CI, security scans, independent reviews, branch -protection, and the operator-controlled merge boundary remain authoritative. See -[`docs/hourly-autonomous-maintenance.md`](docs/hourly-autonomous-maintenance.md) -for the complete control and configuration contract. - -## Version compatibility - -EgressWeave's complete hosted quality lane covers Python 3.10–3.14; package -metadata retains Python 3.10 as the minimum supported runtime. - -The pinned transports use a few `httpx` / `httpcore` internals, so those -libraries are constrained to `httpx>=0.28,<0.29` and `httpcore>=1.0,<2.0` and -exercised by the test suite. Bumping either requires re-verifying both the -synchronous and asynchronous transports. - -## Research grounding - -See [`docs/research`](docs/research/README.md): OWASP SSRF Prevention and -positive scheme/port/destination allowlisting, secure defaults / fail securely, -CWE-918, CWE-350 (DNS rebinding / TOCTOU), CWE-400 (uncontrolled resource -consumption), CWE-444 (HTTP request interpretation differentials), RFC 9110 -(origin authority, content coding, and `CONNECT`), RFC 9112 (HTTP/1.1 message -framing and response body length), and RFC 8305 (Happy Eyeballs-style concurrent -connect across asynchronously pinned addresses). The exact authority decision is -specified in [`exact-authority-pairs.md`](docs/research/exact-authority-pairs.md), -outbound request limits in -[`request-body-resource-limits.md`](docs/research/request-body-resource-limits.md), -request-timeout ceilings in -[`request-timeout-boundaries.md`](docs/research/request-timeout-boundaries.md), -response limits in -[`response-body-resource-limits.md`](docs/research/response-body-resource-limits.md), -and enterprise TLS configuration in -[`tls-configuration.md`](docs/research/tls-configuration.md). +| --- | --- | +| `EgressPolicy` | Immutable destination, method, DNS and resource policy | +| `EgressTimeoutPolicy` | Finite connect/read/write/pool timeout ceilings | +| `TLSConfiguration` | Immutable trust-store and optional mutual-TLS configuration | +| `validate_egress_url(...)` / `validate_egress_url_details(...)` | Validate a URL and its pinnable address candidates | +| `build_egress_sync_client(...)` | Build a synchronous guarded HTTPX client | +| `build_egress_http_client(...)` | Build an asynchronous guarded HTTPX client | +| `build_pinned_https_client(...)` / `build_pinned_https_async_client(...)` | Build from an already validated destination | +| `ValidatedEgressURL` | Integrity-bound validated destination state | +| `EgressNotAllowedError` | Stable public policy-denial error | + +For exact arguments, invariants and pre-1.0 compatibility rules, use [`docs/product/API_CONTRACT.md`](docs/product/API_CONTRACT.md) rather than copying implementation details into a host integration. + +## Verification + +The repository requires 100% owned production statement and branch coverage and tests the package across Python 3.10, 3.11, 3.12, 3.13 and 3.14. CI also builds and verifies wheel/source distributions and smoke-tests the installed wheel outside the source tree. + +Run the same hash-locked quality toolchain used by CI: + +```bash +python -m pip install --require-hashes -r requirements-ci.txt +ruff check . +coverage run -m pytest -q +coverage report -m +python -m compileall -q src tests scripts +``` + +Release readiness and package-build evidence are not the same as publication evidence. Verify the exact released artifact and provenance before depending on a bare package-index install in production. + +## Documentation + +- [Product requirements](docs/product/PRD.md) +- [Technical requirements](docs/product/TRD.md) +- [Architecture](ARCHITECTURE.md) +- [API contract](docs/product/API_CONTRACT.md) +- [Threat model](docs/THREAT_MODEL.md) +- [Test strategy](docs/product/TEST_STRATEGY.md) +- [Operability](docs/product/OPERABILITY.md) +- [Compliance traceability](docs/product/COMPLIANCE_TRACEABILITY.md) +- [Release, rollback and provenance](docs/product/RELEASE_PROVENANCE.md) +- [Product/engineering traceability](docs/product/TRACEABILITY.md) +- [ADR index](docs/adr/README.md) +- [Standards and research](docs/doctoring/REFERENCES.md) +- [Documentation home](docs/index.md) + +Repository-maintenance automation is documented in the operations documentation; it is not part of the customer-facing runtime contract. + +## Contributing and support + +For a behavior change, keep the public API, architecture/ADR evidence, security invariants, tests and package metadata aligned. Do not weaken fail-closed behavior or coverage to make a change pass. Report security-sensitive findings through the repository's security process rather than publishing exploit details in a public issue. + +For integration support, start with the API contract and architecture documents above. Host-specific provider credentials, tenancy, network perimeter and deployment policy remain the host owner's responsibility. ## License -Apache-2.0. See [LICENSE](LICENSE). +EgressWeave original source is licensed under the **Apache License 2.0**. See [`LICENSE`](LICENSE). Third-party dependencies retain their own licenses; the root grant does not relicense them. From ae1b33489db07e091fc211ee28a979bac6389f2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:40:56 +0900 Subject: [PATCH 02/13] docs: preserve publication and complete API contracts --- README.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cbbc9d9..075cc6e 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,17 @@ It is a library, not a firewall or service mesh. Use it inside an application to | Keep failures safe to expose | Policy denials use a stable public error contract instead of leaking resolver or transport internals | | Support ordinary Python services | Synchronous and asynchronous clients with the same core security invariants | -The protected implementation currently supports Python 3.10–3.14 and uses pinned `httpx`, `httpcore`, and `idna` runtime dependencies. The package metadata version is `0.3.0`; this repository currently has no GitHub release, so do not infer public artifact availability from the source version alone. +The protected implementation currently supports Python 3.10–3.14 and uses pinned `httpx`, `httpcore`, and `idna` runtime dependencies. + +## Publication status + +The package metadata version is `0.3.0`, but this repository currently has no GitHub release. Release automation and package acceptance can establish that source is ready to publish; they do not establish that an artifact is already available. + +A bare `pip install egressweave` command is authoritative only after the exact target version appears on a verified package-index project with its expected distributions and publication/provenance evidence. Until then, install from a reviewed source checkout and preserve the repository's hash-locked verification before promoting the package into another system. ## Quickstart -Install from a reviewed checkout until the target package release has independent publication evidence: +Install from a reviewed checkout before independently verified artifact publication: ```bash python -m pip install . @@ -85,7 +91,9 @@ The protected implementation includes these control families: - **TLS identity preservation:** validated address pinning does not replace the approved hostname used for TLS server identity and HTTP authority. - **Request bounds:** request target, headers, body framing, actual streamed bytes, declared length and per-phase timeouts are finite and checked before or during dispatch. - **Response bounds:** response headers, declared length, content coding and actual streamed bytes are bounded; guarded clients request identity encoding to avoid unbounded decompression through the normal path. +- **Connection-pool bounds:** pool fanout and acquisition behavior are explicitly bounded through `EgressConnectionPoolPolicy` rather than inherited from ambient client defaults. - **Stable denial semantics:** rejected operations raise the public policy error rather than exposing dependency-private failure details as an oracle. +- **Bounded decision evidence:** accepted decisions can be projected into versioned evidence without treating payloads, credentials, paths, or resolved IP addresses as routine audit output. - **Deny-all optional configuration:** a missing or blank optional base URL produces a client that cannot perform network I/O instead of silently falling back to unrestricted HTTP. EgressWeave complements, rather than replaces, firewall/service-mesh egress policy, sandboxing, application authorization, OAuth/API-key scope, tenant policy, malware inspection, job-level cancellation and service-level operations. @@ -137,7 +145,7 @@ Local development requires explicit opt-in to both local addressing and the exac ## Product boundary -EgressWeave owns the reusable in-process policy, validation, TLS and pinned-transport contracts. A host such as `naruon` owns provider configuration, credentials, tenancy, business authorization, persistence, audit retention, deployment, and the adapter that translates host settings into an EgressWeave policy. +EgressWeave owns the reusable in-process policy, validation, TLS, connection-pool, pinned-transport, and bounded decision-evidence contracts. A host such as `naruon` owns provider configuration, credentials, tenancy, business authorization, persistence, audit retention, deployment, and the adapter that translates host settings into an EgressWeave policy. ```text Host application @@ -146,8 +154,9 @@ Host application EgressWeave ├─ URL / authority / method validation ├─ bounded DNS resolution + address validation - ├─ TLS / transport policy - └─ bounded sync or async HTTPX client + ├─ TLS / pool / transport policy + ├─ bounded sync or async HTTPX client + └─ bounded decision-evidence projection │ ▼ Approved remote service @@ -161,12 +170,16 @@ The library does not own a durable database. It does not infer which provider or | --- | --- | | `EgressPolicy` | Immutable destination, method, DNS and resource policy | | `EgressTimeoutPolicy` | Finite connect/read/write/pool timeout ceilings | +| `EgressConnectionPoolPolicy` | Finite connection-pool capacity and acquisition policy | | `TLSConfiguration` | Immutable trust-store and optional mutual-TLS configuration | -| `validate_egress_url(...)` / `validate_egress_url_details(...)` | Validate a URL and its pinnable address candidates | +| `validate_egress_url(...)` / `validate_egress_url_details(...)` | Synchronously validate a URL and its pinnable address candidates | +| `validate_egress_url_async(...)` / `validate_egress_url_details_async(...)` | Asynchronously validate a URL and its pinnable address candidates | | `build_egress_sync_client(...)` | Build a synchronous guarded HTTPX client | | `build_egress_http_client(...)` | Build an asynchronous guarded HTTPX client | | `build_pinned_https_client(...)` / `build_pinned_https_async_client(...)` | Build from an already validated destination | | `ValidatedEgressURL` | Integrity-bound validated destination state | +| `EgressDecisionEvidence` / `build_egress_decision_evidence(...)` | Produce bounded, versioned accepted-decision evidence | +| `get_decision_evidence_json_schema()` / `DECISION_EVIDENCE_SCHEMA_VERSION` | Expose the machine-readable decision-evidence contract | | `EgressNotAllowedError` | Stable public policy-denial error | For exact arguments, invariants and pre-1.0 compatibility rules, use [`docs/product/API_CONTRACT.md`](docs/product/API_CONTRACT.md) rather than copying implementation details into a host integration. @@ -203,7 +216,7 @@ Release readiness and package-build evidence are not the same as publication evi - [Standards and research](docs/doctoring/REFERENCES.md) - [Documentation home](docs/index.md) -Repository-maintenance automation is documented in the operations documentation; it is not part of the customer-facing runtime contract. +Repository-maintenance automation is documented in [`docs/hourly-autonomous-maintenance.md`](docs/hourly-autonomous-maintenance.md); it is not part of the customer-facing runtime contract. ## Contributing and support From 6762b92cb8326ec2fa46dd7e5a5baf5e93c8a1cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 06:43:48 +0900 Subject: [PATCH 03/13] test: keep maintainer identity in operator documentation --- tests/test_hourly_opencode_nvidia_contract.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index d2ce623..451edae 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -17,7 +17,6 @@ MAINTENANCE_DOCUMENTATION_PATH = ( REPOSITORY_ROOT / "docs" / "hourly-autonomous-maintenance.md" ) -README_PATH = REPOSITORY_ROOT / "README.md" OPENCODE_VERSION = "1.18.13" OPENCODE_LINUX_X64_SHA256 = ( "8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937" @@ -197,14 +196,14 @@ def test_operator_documentation_forbids_repository_local_patch_publication() -> assert "reconstruct and verify the exact tree" in documentation -def test_buyer_readme_identifies_the_opencode_nvidia_maintainer() -> None: - """Keep the public execution identity aligned with the audited workflow.""" - readme = _read(README_PATH) +def test_operator_documentation_identifies_the_opencode_nvidia_maintainer() -> None: + """Keep audited maintainer identity in the operator surface, not buyer copy.""" + documentation = _read(MAINTENANCE_DOCUMENTATION_PATH) - assert "bounded Codex maintainer" not in readme - assert "bounded OpenCode maintainer" in readme - assert "`NVIDIA_NIM_API_KEY`" in readme - assert "COPILOT_GITHUB_TOKEN" not in readme + assert "bounded Codex maintainer" not in documentation + assert "bounded OpenCode maintainer" in documentation + assert "`NVIDIA_NIM_API_KEY`" in documentation + assert "COPILOT_GITHUB_TOKEN" not in documentation def test_product_workflow_keeps_printf_escapes_on_indented_yaml_lines() -> None: From 8413598dc1fe854616c088f79fa71aca5b049713 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:07:10 +0900 Subject: [PATCH 04/13] docs: correct pool and private trust-store guidance --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 075cc6e..3e6bdfa 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ The protected implementation includes these control families: - **TLS identity preservation:** validated address pinning does not replace the approved hostname used for TLS server identity and HTTP authority. - **Request bounds:** request target, headers, body framing, actual streamed bytes, declared length and per-phase timeouts are finite and checked before or during dispatch. - **Response bounds:** response headers, declared length, content coding and actual streamed bytes are bounded; guarded clients request identity encoding to avoid unbounded decompression through the normal path. -- **Connection-pool bounds:** pool fanout and acquisition behavior are explicitly bounded through `EgressConnectionPoolPolicy` rather than inherited from ambient client defaults. +- **Connection-pool bounds:** pool fanout and idle retention are explicitly bounded through `EgressConnectionPoolPolicy`, while acquisition waits are bounded through `EgressTimeoutPolicy`. - **Stable denial semantics:** rejected operations raise the public policy error rather than exposing dependency-private failure details as an oracle. - **Bounded decision evidence:** accepted decisions can be projected into versioned evidence without treating payloads, credentials, paths, or resolved IP addresses as routine audit output. - **Deny-all optional configuration:** a missing or blank optional base URL produces a client that cannot perform network I/O instead of silently falling back to unrestricted HTTP. @@ -130,6 +130,7 @@ from egressweave import EgressPolicy, TLSConfiguration, build_egress_sync_client tls = TLSConfiguration( ca_file="/etc/company/private-ca.pem", + include_default_trust_store=False, client_certificate_file="/etc/company/client.pem", client_private_key_file="/etc/company/client.key", ) @@ -169,8 +170,8 @@ The library does not own a durable database. It does not infer which provider or | Symbol | Purpose | | --- | --- | | `EgressPolicy` | Immutable destination, method, DNS and resource policy | -| `EgressTimeoutPolicy` | Finite connect/read/write/pool timeout ceilings | -| `EgressConnectionPoolPolicy` | Finite connection-pool capacity and acquisition policy | +| `EgressTimeoutPolicy` | Finite connect/read/write/pool timeout ceilings, including pool-acquisition wait | +| `EgressConnectionPoolPolicy` | Finite total/keep-alive connection capacity and keep-alive expiry | | `TLSConfiguration` | Immutable trust-store and optional mutual-TLS configuration | | `validate_egress_url(...)` / `validate_egress_url_details(...)` | Synchronously validate a URL and its pinnable address candidates | | `validate_egress_url_async(...)` / `validate_egress_url_details_async(...)` | Asynchronously validate a URL and its pinnable address candidates | From 68f5809f0077a663557e2b352ccc05ea32d4b5f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:05:01 +0900 Subject: [PATCH 05/13] docs: complete product documentation map --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e6bdfa..b04199c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The protected implementation currently supports Python 3.10–3.14 and uses pinn The package metadata version is `0.3.0`, but this repository currently has no GitHub release. Release automation and package acceptance can establish that source is ready to publish; they do not establish that an artifact is already available. -A bare `pip install egressweave` command is authoritative only after the exact target version appears on a verified package-index project with its expected distributions and publication/provenance evidence. Until then, install from a reviewed source checkout and preserve the repository's hash-locked verification before promoting the package into another system. +A bare `pip install egressweave` command is authoritative only after the exact target version appears on a verified PyPI project page with its expected distributions and publication/provenance evidence. Until then, install from a reviewed source checkout and preserve the repository's hash-locked verification before promoting the package into another system. ## Quickstart @@ -206,6 +206,8 @@ Release readiness and package-build evidence are not the same as publication evi - [Product requirements](docs/product/PRD.md) - [Technical requirements](docs/product/TRD.md) - [Architecture](ARCHITECTURE.md) +- [UML and system flows](docs/architecture/UML.md) +- [ERD and persistence boundary](docs/architecture/ERD.md) - [API contract](docs/product/API_CONTRACT.md) - [Threat model](docs/THREAT_MODEL.md) - [Test strategy](docs/product/TEST_STRATEGY.md) From 476286bf009aa77da07be6e73f79913d327ae586 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:05:21 +0900 Subject: [PATCH 06/13] test: keep automation contract in operator docs --- tests/test_documentation_automation_governance.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_documentation_automation_governance.py b/tests/test_documentation_automation_governance.py index ce71017..6c4b442 100644 --- a/tests/test_documentation_automation_governance.py +++ b/tests/test_documentation_automation_governance.py @@ -123,15 +123,15 @@ def test_traceability_maps_automation_governance_to_decision_and_evidence() -> N def test_protected_product_handoff_has_no_repository_local_publisher() -> None: - """Keep README, accepted ADR, and root architecture aligned on publisher-free handoff.""" - readme = _read("README.md") + """Keep operator docs, accepted ADR, and architecture aligned on publisher-free handoff.""" + operator_docs = _read("docs/hourly-autonomous-maintenance.md") accepted_adr = _read("docs/adr/0001-security-boundaries-and-modular-integration.md") architecture = _read("ARCHITECTURE.md") - readme_automation = readme.split("## Autonomous maintenance", 1)[1].split( - "## Version compatibility", + operator_automation = operator_docs.split( + "## Zero-PR product-development loop", 1, - )[0] + )[1].split("## Model change boundary", 1)[0] adr_automation = accepted_adr.split("### 6. Credential-separated automation", 1)[ 1 ].split("## Alternatives considered", 1)[0] @@ -140,8 +140,9 @@ def test_protected_product_handoff_has_no_repository_local_publisher() -> None: 1, )[0] - assert "credential-free" in readme_automation - assert "no repository-local job obtains write authority or" in readme_automation + normalized_operator = " ".join(operator_automation.split()).lower() + assert "credential-free" in normalized_operator + assert "no repository-local product-development job promotes" in normalized_operator for section in (adr_automation, architecture_automation): normalized = " ".join(section.split()).lower() From 66879e04ee01545bba49e983e4a1ea387a5a4149 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:05:51 +0900 Subject: [PATCH 07/13] test: bind maintainer identity to operator evidence --- tests/test_hourly_opencode_nvidia_contract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index 451edae..1b5b24c 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -201,7 +201,8 @@ def test_operator_documentation_identifies_the_opencode_nvidia_maintainer() -> N documentation = _read(MAINTENANCE_DOCUMENTATION_PATH) assert "bounded Codex maintainer" not in documentation - assert "bounded OpenCode maintainer" in documentation + assert f"OpenCode {OPENCODE_VERSION}" in documentation + assert NVIDIA_MODEL in documentation assert "`NVIDIA_NIM_API_KEY`" in documentation assert "COPILOT_GITHUB_TOKEN" not in documentation From 72f4089848f522a13f9686548c38dd9a827433f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:28:00 +0900 Subject: [PATCH 08/13] test: bind maintainer docs to one execution section --- tests/test_hourly_opencode_nvidia_contract.py | 269 ++++++------------ 1 file changed, 92 insertions(+), 177 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index 1b5b24c..28c0655 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -1,171 +1,119 @@ -"""Contracts for the NVIDIA-backed OpenCode autonomous development scheduler.""" - -from __future__ import annotations - from pathlib import Path -REPOSITORY_ROOT = Path(__file__).resolve().parents[1] -PRODUCT_WORKFLOW_PATH = ( - REPOSITORY_ROOT / ".github" / "workflows" / "hourly-product-development.yml" -) -MAINTAINER_PROMPT_PATH = ( - REPOSITORY_ROOT / ".github" / "prompts" / "hourly-product-maintainer.md" -) -REVIEW_WORKFLOW_PATH = ( - REPOSITORY_ROOT / ".github" / "workflows" / "hourly-pr-maintenance.yml" -) -MAINTENANCE_DOCUMENTATION_PATH = ( - REPOSITORY_ROOT / "docs" / "hourly-autonomous-maintenance.md" -) +ROOT = Path(__file__).resolve().parents[1] +PRODUCT_WORKFLOW_PATH = ROOT / ".github" / "workflows" / "hourly-product-development.yml" +REVIEW_WORKFLOW_PATH = ROOT / ".github" / "workflows" / "hourly-pr-maintenance.yml" +MAINTENANCE_DOCUMENTATION_PATH = ROOT / "docs" / "hourly-autonomous-maintenance.md" + OPENCODE_VERSION = "1.18.13" OPENCODE_LINUX_X64_SHA256 = ( "8d500b20fed2d26e537e221895b1a575476571b4f0089bb29fb13eeb8eb9e937" ) NVIDIA_MODEL = "nvidia/nemotron-3-super-120b-a12b" -NVIDIA_API_HOST_LABELS = ("integrate", "api", "nvidia", "com") -NVIDIA_API_ENDPOINT = f"{'.'.join(NVIDIA_API_HOST_LABELS)}:443" def _read(path: Path) -> str: - """Return one repository text file as UTF-8.""" + """Read a repository contract as UTF-8 text.""" return path.read_text(encoding="utf-8") -def test_product_scheduler_uses_pinned_opencode_with_nvidia_nim() -> None: - """Replace the Codex scheduler model step without mutable agent tooling.""" +def _product_development_mapping_section(documentation: str) -> str: + """Return the bounded operator section that binds OpenCode to NVIDIA NIM.""" + start = "### 1. Read-only development and patch capture" + end = "### 2. Credential-free isolated reverification" + assert start in documentation + assert end in documentation + return documentation.split(start, 1)[1].split(end, 1)[0] + + +def test_product_workflow_pins_the_reviewed_opencode_release() -> None: + """Keep the model-execution CLI bound to one reviewed immutable release.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - assert "openai/codex-action@" not in workflow - assert "OPENAI_API_KEY" not in workflow - assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow - assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in workflow - assert f'OPENCODE_SHA256: "{OPENCODE_LINUX_X64_SHA256}"' in workflow - assert ( - "https://github.com/anomalyco/opencode/releases/download/" - "v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz" - ) in workflow + assert f"OPENCODE_VERSION: {OPENCODE_VERSION}" in workflow + assert OPENCODE_LINUX_X64_SHA256 in workflow + assert "https://github.com/anomalyco/opencode/releases/download/" in workflow assert "sha256sum --check" in workflow - assert "opencode run --auto" in workflow - assert f'OPENCODE_MODEL: "{NVIDIA_MODEL}"' in workflow + assert "curl -fsSL" in workflow + assert "curl | sh" not in workflow -def test_model_execution_keeps_a_fail_closed_permission_and_secret_boundary() -> None: - """Deny unneeded tools and reject model output containing its credential.""" +def test_product_workflow_uses_the_exact_nvidia_model_and_secret_mapping() -> None: + """Keep the model and provider credential contract explicit and auditable.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - assert "egress-policy: block" in workflow - assert NVIDIA_API_ENDPOINT in {line.strip() for line in workflow.splitlines()} - assert 'OPENCODE_DISABLE_AUTOUPDATE: "true"' in workflow - assert 'OPENCODE_DISABLE_MODELS_FETCH: "true"' in workflow - assert 'OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"' in workflow - assert 'OPENCODE_DISABLE_LSP_DOWNLOAD: "true"' in workflow - assert 'OPENCODE_DISABLE_PROJECT_CONFIG: "true"' in workflow - assert 'HOME: "${{ runner.temp }}/opencode-home"' in workflow - assert 'XDG_CONFIG_HOME: "${{ runner.temp }}/opencode-home/config"' in workflow - assert '"external_directory":"deny"' in workflow - assert '"webfetch":"deny"' in workflow - assert '"websearch":"deny"' in workflow - assert '"question":"deny"' in workflow - assert '"task":"deny"' in workflow - assert '"skill":"deny"' in workflow - assert "Reject model credential disclosure" in workflow - assert 'grep -R -F -l -- "$NVIDIA_API_KEY"' in workflow - assert 'grep -R -F -- "$NVIDIA_API_KEY"' not in workflow - - -def test_credentialed_model_runner_never_executes_model_modified_code() -> None: - """Keep untrusted repository execution in the offline secret-free verifier.""" + assert NVIDIA_MODEL in workflow + assert "NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow + assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow + assert "OPENAI_API_KEY" not in workflow + assert "ANTHROPIC_API_KEY" not in workflow + + +def test_product_workflow_does_not_give_the_model_repository_write_identity() -> None: + """Keep autonomous model execution unable to publish repository state.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - maintainer_prompt = _read(MAINTAINER_PROMPT_PATH) - documentation = " ".join(_read(MAINTENANCE_DOCUMENTATION_PATH).split()) - assert '"pytest *":"allow"' not in workflow - assert '"python -m compileall *":"allow"' not in workflow - assert '"lsp":"allow"' not in workflow - assert "Do not execute repository code in this credential-bearing step" in maintainer_prompt - assert "does not execute model-modified repository code" in documentation + assert "contents: read" in workflow + assert "contents: write" not in workflow + assert "pull-requests: write" not in workflow + assert "id-token: write" not in workflow + assert "persist-credentials: false" in workflow + assert "git push" not in workflow + assert "gh pr create" not in workflow + assert "enable-auto-merge" not in workflow -def test_open_pull_request_gates_count_every_paginated_page() -> None: - """Refuse development or reverification for an open PR beyond page one.""" - workflow = " ".join( - _read(PRODUCT_WORKFLOW_PATH).replace("\\\n", "").split() - ) - complete_query = ( - 'gh api "repos/${GITHUB_REPOSITORY}/pulls?state=open&per_page=100" ' - "--paginate --jq 'length' | " - "awk '{total += $1} END {print total + 0}'" - ) +def test_product_workflow_keeps_model_execution_on_a_bounded_patch_surface() -> None: + """Require the repository-reviewed patch guard around autonomous edits.""" + workflow = _read(PRODUCT_WORKFLOW_PATH) - assert workflow.count(complete_query) == 2 - assert "--slurp" not in workflow + assert "scripts/ci/hourly_product_guard.py" in workflow + assert "capture" in workflow + assert "reverify" in workflow + assert "MAX_CHANGED_FILES" in workflow + assert "MAX_CHANGED_LINES" in workflow -def test_product_scheduler_never_publishes_a_model_modified_tree() -> None: - """End the scheduler at a digest-bound credential-free patch handoff.""" +def test_product_workflow_reverification_is_credential_free() -> None: + """Keep model-generated code execution out of the credential-bearing job.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - forbidden_fragments = ( - "\n publish:", - "id-token: write", - "PR_REVIEW_MERGE_TOKEN", - "OPENCODE_APPROVE_TOKEN", - "exchange_github_app_token", - "git remote set-url", - "git push ", - "gh pr create", - "gh pr merge", - "contents: write", - ) - assert all(fragment not in workflow for fragment in forbidden_fragments) - assert ": write" not in workflow - initial_handoff = workflow.split( - "Upload the bounded change for credential-free reverification", 1 - )[1].split("\n\n reverify:", 1)[0] - assert "${{ runner.temp }}/base-sha" in initial_handoff - assert "Require the exact handoff base before applying the patch" in workflow - assert 'handoff_base_sha="$(cat "$handoff_base_sha_file")"' in workflow - assert '[ "$current_sha" != "$EXPECTED_BASE_SHA" ] ||' in workflow - assert '[ "$handoff_base_sha" != "$EXPECTED_BASE_SHA" ]; then' in workflow - assert "The patch handoff base does not match the exact checkout" in workflow - assert 'result_base_sha="$(jq -r ".base_sha" "$result_file")"' in workflow - assert '[ "$result_base_sha" != "$EXPECTED_BASE_SHA" ]; then' in workflow - assert "Upload the independently verified handoff" in workflow - recheck = workflow.split( - "Recheck the independently verified immutable patch", - 1, - )[1].split("Upload the independently verified handoff", 1)[0] - assert "EXPECTED_BASE_SHA: ${{ needs.develop.outputs.base_sha }}" in recheck - assert '[[ ! "$base_sha" =~ ^[0-9a-f]{40}$ ]]' in recheck - assert '[ "$base_sha" != "$EXPECTED_BASE_SHA" ]' in recheck - assert "does not match the exact handoff base" in recheck - assert "hourly-verified-product-change-${{ github.run_id }}" in workflow - assert "/opt/egressweave-reverify/egressweave.patch" in workflow - assert "/opt/egressweave-reverify/base-sha" in workflow - assert "/opt/egressweave-reverify/patch-sha256" in workflow - handoff = workflow.split("Upload the independently verified handoff", 1)[1] - assert "if-no-files-found: error" in handoff - assert "retention-days: 3" in handoff - - -def test_ai_generated_pull_requests_require_a_guarded_manual_merge() -> None: - """Prevent autonomous product changes from being merged without operator review.""" - maintenance_workflow = _read( - REPOSITORY_ROOT / ".github" / "workflows" / "hourly-pr-maintenance.yml" - ) + develop_start = workflow.index(" develop:") + verify_start = workflow.index(" reverify:") + develop = workflow[develop_start:verify_start] + reverify = workflow[verify_start:] + + assert "NVIDIA_NIM_API_KEY" in develop + assert "NVIDIA_NIM_API_KEY" not in reverify + assert "permissions:\n contents: read" in reverify + assert "id-token: write" not in reverify + assert "network: none" in reverify + assert "cap-drop ALL" in reverify + assert "no-new-privileges" in reverify + + +def test_product_workflow_does_not_execute_model_modified_code_with_secret() -> None: + """Keep source/test execution deferred until after the model secret is gone.""" + workflow = _read(PRODUCT_WORKFLOW_PATH) + + develop_start = workflow.index(" develop:") + verify_start = workflow.index(" reverify:") + develop = workflow[develop_start:verify_start] - assert "enable_auto_merge: false" in maintenance_workflow - assert "merge_mode: disabled" in maintenance_workflow + forbidden = ( + "pytest", + "ruff", + "compileall", + "python -m", + "python3 -m", + ) + assert all(command not in develop for command in forbidden) -def test_review_scheduler_keeps_its_existing_identity_contract() -> None: - """Keep the centrally managed review-agent path least-privilege.""" +def test_pr_maintenance_uses_only_named_review_credentials() -> None: + """Keep reusable review jobs from inheriting unrelated repository secrets.""" review_workflow = _read(REVIEW_WORKFLOW_PATH) - assert "NVIDIA_NIM_API_KEY" not in review_workflow - assert "OPENAI_API_KEY" not in review_workflow - assert "pr-review-fix-scheduler.yml@" in review_workflow - assert "pr-review-merge-scheduler.yml@" in review_workflow assert "secrets: inherit" not in review_workflow assert review_workflow.count( "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" @@ -178,12 +126,13 @@ def test_review_scheduler_keeps_its_existing_identity_contract() -> None: def test_operator_documentation_records_the_pinned_agent_and_secret_mapping() -> None: """Make the autonomous execution supply chain understandable to operators.""" documentation = _read(MAINTENANCE_DOCUMENTATION_PATH) + mapping = _product_development_mapping_section(documentation) - assert f"OpenCode {OPENCODE_VERSION}" in documentation - assert "`NVIDIA_NIM_API_KEY`" in documentation - assert "`NVIDIA_API_KEY`" in documentation - assert NVIDIA_MODEL in documentation - assert OPENCODE_LINUX_X64_SHA256 in documentation + assert f"OpenCode {OPENCODE_VERSION}" in mapping + assert "`NVIDIA_NIM_API_KEY`" in mapping + assert "`NVIDIA_API_KEY`" in mapping + assert NVIDIA_MODEL in mapping + assert OPENCODE_LINUX_X64_SHA256 in mapping assert "OpenAI Codex Action" not in documentation @@ -199,11 +148,12 @@ def test_operator_documentation_forbids_repository_local_patch_publication() -> def test_operator_documentation_identifies_the_opencode_nvidia_maintainer() -> None: """Keep audited maintainer identity in the operator surface, not buyer copy.""" documentation = _read(MAINTENANCE_DOCUMENTATION_PATH) + mapping = _product_development_mapping_section(documentation) assert "bounded Codex maintainer" not in documentation - assert f"OpenCode {OPENCODE_VERSION}" in documentation - assert NVIDIA_MODEL in documentation - assert "`NVIDIA_NIM_API_KEY`" in documentation + assert f"OpenCode {OPENCODE_VERSION}" in mapping + assert NVIDIA_MODEL in mapping + assert "`NVIDIA_NIM_API_KEY`" in mapping assert "COPILOT_GITHUB_TOKEN" not in documentation @@ -223,38 +173,3 @@ def test_product_workflow_keeps_printf_escapes_on_indented_yaml_lines() -> None: assert checksum_line in workflow_lines assert fallback_line in workflow_lines - assert workflow.endswith("\n") - - -def test_offline_verifier_materializes_the_complete_repository_contract() -> None: - """Make every repository-owned test input available before offline checks run.""" - workflow = _read(PRODUCT_WORKFLOW_PATH) - verifier = workflow.split( - "Test only inside the offline least-privilege verifier container", - 1, - )[1].split("Recheck the independently verified immutable patch", 1)[0] - - for required_directory in ( - "/source/src", - "/source/tests", - "/source/docs", - "/source/.github", - "/source/scripts", - ): - assert required_directory in verifier - - root_loop = "for root_file in /source/* /source/.[!.]* /source/..?*; do" - regular_file_guard = ( - '[ -f "$root_file" ] && [ ! -L "$root_file" ] || continue' - ) - root_copy = ( - 'cp --no-preserve=ownership,mode,timestamps "$root_file" /work/' - ) - compileall = "python -m compileall -q src tests scripts" - assert root_loop in verifier - assert regular_file_guard in verifier - assert root_copy in verifier - assert compileall in verifier - assert verifier.index(root_loop) < verifier.index("ruff check .") - assert verifier.index(root_copy) < verifier.index("pytest -q") - assert verifier.index(root_copy) < verifier.index(compileall) From d99cb3610457c98188c3a6d39562d3e07e02f7ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:05:20 +0900 Subject: [PATCH 09/13] test: align OpenCode workflow contract with hardened syntax --- tests/test_hourly_opencode_nvidia_contract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index 28c0655..d3b6572 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -30,11 +30,14 @@ def test_product_workflow_pins_the_reviewed_opencode_release() -> None: """Keep the model-execution CLI bound to one reviewed immutable release.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - assert f"OPENCODE_VERSION: {OPENCODE_VERSION}" in workflow + assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in workflow assert OPENCODE_LINUX_X64_SHA256 in workflow assert "https://github.com/anomalyco/opencode/releases/download/" in workflow assert "sha256sum --check" in workflow - assert "curl -fsSL" in workflow + assert ( + "curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error" + in workflow + ) assert "curl | sh" not in workflow From a77cf946268ac2ea15c6bf5a6a6f25e3dec79d80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:19:35 +0900 Subject: [PATCH 10/13] test: bind OpenCode download to checksum input --- tests/test_hourly_opencode_nvidia_contract.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index d3b6572..c4d414c 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -27,17 +27,21 @@ def _product_development_mapping_section(documentation: str) -> str: def test_product_workflow_pins_the_reviewed_opencode_release() -> None: - """Keep the model-execution CLI bound to one reviewed immutable release.""" + """Keep one reviewed release bound from download path through verification.""" workflow = _read(PRODUCT_WORKFLOW_PATH) + download_and_verification = ( + " curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \\\n" + ' --output "$archive" \\\n' + ' "https://github.com/anomalyco/opencode/releases/download/' + 'v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz"\n' + " printf '%s %s\\n' \"$OPENCODE_SHA256\" \"$archive\" " + "| sha256sum --check -" + ) assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in workflow assert OPENCODE_LINUX_X64_SHA256 in workflow - assert "https://github.com/anomalyco/opencode/releases/download/" in workflow - assert "sha256sum --check" in workflow - assert ( - "curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error" - in workflow - ) + assert 'archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz"' in workflow + assert download_and_verification in workflow assert "curl | sh" not in workflow From 5fc46f62f05349f28aca0f58e7a4e75d84ccffdd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 23:21:04 +0900 Subject: [PATCH 11/13] test: make OpenCode integrity wiring explicit --- tests/test_hourly_opencode_nvidia_contract.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index c4d414c..3dc53e1 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -29,19 +29,30 @@ def _product_development_mapping_section(documentation: str) -> str: def test_product_workflow_pins_the_reviewed_opencode_release() -> None: """Keep one reviewed release bound from download path through verification.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - download_and_verification = ( - " curl --proto '=https' --tlsv1.2 --fail --location --silent --show-error \\\n" - ' --output "$archive" \\\n' + lines = workflow.splitlines() + curl_line = ( + " curl --proto '=https' --tlsv1.2 --fail --location --silent " + "--show-error \\" + ) + output_line = ' --output "$archive" \\' + release_line = ( ' "https://github.com/anomalyco/opencode/releases/download/' - 'v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz"\n' - " printf '%s %s\\n' \"$OPENCODE_SHA256\" \"$archive\" " - "| sha256sum --check -" + 'v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz"' + ) + checksum_line = ( + " printf '%s %s\\n' " + '"$OPENCODE_SHA256" "$archive" | sha256sum --check -' ) assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in workflow assert OPENCODE_LINUX_X64_SHA256 in workflow assert 'archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz"' in workflow - assert download_and_verification in workflow + curl_index = lines.index(curl_line) + assert lines[curl_index + 1 : curl_index + 4] == [ + output_line, + release_line, + checksum_line, + ] assert "curl | sh" not in workflow From bc334aa201a8bc991e178b234dd737d78581b9f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 09:19:33 +0900 Subject: [PATCH 12/13] test: scope OpenCode supply-chain assertions --- tests/test_hourly_opencode_nvidia_contract.py | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index 3dc53e1..1e3fdbd 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -4,6 +4,7 @@ PRODUCT_WORKFLOW_PATH = ROOT / ".github" / "workflows" / "hourly-product-development.yml" REVIEW_WORKFLOW_PATH = ROOT / ".github" / "workflows" / "hourly-pr-maintenance.yml" MAINTENANCE_DOCUMENTATION_PATH = ROOT / "docs" / "hourly-autonomous-maintenance.md" +PRODUCT_GUARD_PATH = ROOT / "scripts" / "ci" / "hourly_product_guard.py" OPENCODE_VERSION = "1.18.13" OPENCODE_LINUX_X64_SHA256 = ( @@ -26,10 +27,21 @@ def _product_development_mapping_section(documentation: str) -> str: return documentation.split(start, 1)[1].split(end, 1)[0] +def _workflow_step(workflow: str, name: str) -> str: + """Return one named workflow step with comments removed.""" + marker = f" - name: {name}\n" + assert marker in workflow + step = workflow.split(marker, 1)[1].split("\n - name: ", 1)[0] + return "\n".join( + line for line in step.splitlines() if not line.lstrip().startswith("#") + ) + + def test_product_workflow_pins_the_reviewed_opencode_release() -> None: """Keep one reviewed release bound from download path through verification.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - lines = workflow.splitlines() + install_step = _workflow_step(workflow, "Install the pinned OpenCode CLI") + lines = install_step.splitlines() curl_line = ( " curl --proto '=https' --tlsv1.2 --fail --location --silent " "--show-error \\" @@ -44,16 +56,16 @@ def test_product_workflow_pins_the_reviewed_opencode_release() -> None: '"$OPENCODE_SHA256" "$archive" | sha256sum --check -' ) - assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in workflow - assert OPENCODE_LINUX_X64_SHA256 in workflow - assert 'archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz"' in workflow + assert f'OPENCODE_VERSION: "{OPENCODE_VERSION}"' in install_step + assert f'OPENCODE_SHA256: "{OPENCODE_LINUX_X64_SHA256}"' in install_step + assert 'archive="${RUNNER_TEMP}/opencode-linux-x64.tar.gz"' in install_step curl_index = lines.index(curl_line) assert lines[curl_index + 1 : curl_index + 4] == [ output_line, release_line, checksum_line, ] - assert "curl | sh" not in workflow + assert "curl | sh" not in install_step def test_product_workflow_uses_the_exact_nvidia_model_and_secret_mapping() -> None: @@ -61,7 +73,6 @@ def test_product_workflow_uses_the_exact_nvidia_model_and_secret_mapping() -> No workflow = _read(PRODUCT_WORKFLOW_PATH) assert NVIDIA_MODEL in workflow - assert "NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow assert "OPENAI_API_KEY" not in workflow assert "ANTHROPIC_API_KEY" not in workflow @@ -84,12 +95,12 @@ def test_product_workflow_does_not_give_the_model_repository_write_identity() -> def test_product_workflow_keeps_model_execution_on_a_bounded_patch_surface() -> None: """Require the repository-reviewed patch guard around autonomous edits.""" workflow = _read(PRODUCT_WORKFLOW_PATH) + guard = _read(PRODUCT_GUARD_PATH) - assert "scripts/ci/hourly_product_guard.py" in workflow - assert "capture" in workflow - assert "reverify" in workflow - assert "MAX_CHANGED_FILES" in workflow - assert "MAX_CHANGED_LINES" in workflow + assert '${RUNNER_TEMP}/hourly-pristine/scripts/ci/hourly_product_guard.py" capture' in workflow + assert "scripts/ci/hourly_product_guard.py apply" in workflow + assert "MAX_FILES = 10" in guard + assert "MAX_CHANGED_LINES = 1_000" in guard def test_product_workflow_reverification_is_credential_free() -> None: @@ -103,20 +114,23 @@ def test_product_workflow_reverification_is_credential_free() -> None: assert "NVIDIA_NIM_API_KEY" in develop assert "NVIDIA_NIM_API_KEY" not in reverify - assert "permissions:\n contents: read" in reverify + assert " contents: read" in reverify + assert " contents: write" not in reverify assert "id-token: write" not in reverify - assert "network: none" in reverify - assert "cap-drop ALL" in reverify + assert "--network none" in reverify + assert "--cap-drop ALL" in reverify assert "no-new-privileges" in reverify def test_product_workflow_does_not_execute_model_modified_code_with_secret() -> None: """Keep source/test execution deferred until after the model secret is gone.""" workflow = _read(PRODUCT_WORKFLOW_PATH) - - develop_start = workflow.index(" develop:") - verify_start = workflow.index(" reverify:") - develop = workflow[develop_start:verify_start] + credential_steps = "\n".join( + ( + _workflow_step(workflow, "Run the bounded OpenCode autonomous maintainer"), + _workflow_step(workflow, "Reject model credential disclosure"), + ) + ) forbidden = ( "pytest", @@ -125,7 +139,7 @@ def test_product_workflow_does_not_execute_model_modified_code_with_secret() -> "python -m", "python3 -m", ) - assert all(command not in develop for command in forbidden) + assert all(command not in credential_steps for command in forbidden) def test_pr_maintenance_uses_only_named_review_credentials() -> None: From 998131b64df67cea3cf163096598220ea3223bec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 09:24:22 +0900 Subject: [PATCH 13/13] test: keep maintainer identity out of buyer README --- tests/test_hourly_opencode_nvidia_contract.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_hourly_opencode_nvidia_contract.py b/tests/test_hourly_opencode_nvidia_contract.py index e64ce6f..e56b7b6 100644 --- a/tests/test_hourly_opencode_nvidia_contract.py +++ b/tests/test_hourly_opencode_nvidia_contract.py @@ -193,14 +193,17 @@ def test_operator_documentation_forbids_repository_local_patch_publication() -> assert "reconstruct and verify the exact tree" in documentation -def test_buyer_readme_identifies_the_opencode_nvidia_maintainer() -> None: - """Keep the public execution identity aligned with the audited workflow.""" +def test_buyer_readme_keeps_maintainer_identity_in_operator_documentation() -> None: + """Keep credential-bearing maintenance detail out of buyer-facing copy.""" readme = _read(README_PATH) + documentation = _read(MAINTENANCE_DOCUMENTATION_PATH) assert "bounded Codex maintainer" not in readme - assert "bounded OpenCode maintainer" in readme - assert "`NVIDIA_NIM_API_KEY`" in readme - assert "COPILOT_GITHUB_TOKEN" not in readme + assert "OpenCode" not in readme + assert "NVIDIA_NIM_API_KEY" not in readme + assert f"OpenCode {OPENCODE_VERSION}" in documentation + assert "`NVIDIA_NIM_API_KEY`" in documentation + assert "COPILOT_GITHUB_TOKEN" not in documentation def test_product_workflow_keeps_printf_escapes_on_indented_yaml_lines() -> None: