diff --git a/security/browser-headers/README.md b/security/browser-headers/README.md index 235c23be145..4b5189d345d 100644 --- a/security/browser-headers/README.md +++ b/security/browser-headers/README.md @@ -9,8 +9,8 @@ The candidate is disabled and both approval flags remain false. ## Reviewed input and source prerequisite `rust-policy.json` binds the accepted Rust 4.12.0 public crate and the -[protected documentation run](https://github.com/durable-workflow/sdk-rust/actions/runs/41954374074), -artifact 13382143373. Its ZIP SHA-256 is recorded with its tooling revision. +[protected documentation run](https://github.com/durable-workflow/sdk-rust/actions/runs/41959739724), +artifact 13384761426. Its ZIP SHA-256 is recorded with its tooling revision. The full input contains 484 regular files and 372 HTML pages. The earlier artifact's `help.html` and `settings.html` contained executable @@ -18,7 +18,7 @@ links and inline history-back handlers. The generator refuses them. The merged [SDK source correction](https://github.com/durable-workflow/sdk-rust/pull/207) provides a safe crate-index fallback and an external history listener. The sole automatic protected build and deployment for the current proposal are successful -at tooling revision `2b45fd7b12a6e9460666045a3c7f644860caea3b`. Public package/source +at tooling revision `46cf36514f7a39e819213771abf9efc82321c062`. Public package/source identities remain unchanged. Eight live identity, navigation, help/settings and API files matched this artifact on November 5, 2026. The [current staging proposal](rust-stage-proposal.md) records their hashes and the @@ -27,11 +27,13 @@ separate acceptance that remains pending. The current inventory describes that accepted published artifact. `source_preparation` is null. `history/` retains the earlier local derivative and the accepted SDK #207 artifact's browser result as historical evidence only. -The derivative was not a public artifact. SDK #212 retains every page and asset -byte from the prior SDK #209 artifact. -Only the build identity changes. The executable script hashes and generated -rule are unchanged. That static comparison does not carry forward the earlier -browser pass. Current compatibility is unrun and the rule remains disabled. +The derivative was not a public artifact. Compared with accepted SDK #212, +SDK #211 changes exactly `__build.json`, `help.html` and `settings.html`. The two +utility pages only replace `workflow_testing` with `worker_latency` in generated +`data-current-crate` metadata. All 372 page-resource records, executable script +hashes and the parsed rule are unchanged. That static comparison does not carry +forward the earlier browser pass. Current compatibility is unrun and the rule +remains disabled. ## Policy and inventory @@ -115,6 +117,8 @@ exact input bindings, ownership, proposed checks and rollback decision. separately reviewable, unrun local browser/telemetry packet. Normal CI exercises only its inert source controls. The optional browser entrypoint requires its own exact run admission; it makes no provider or production request. +`rust-preparation-proposal.md` supplies the next separately reviewed acquisition +and immutable-image custody design. No acquisition, seal or prepared image exists. ## Rust-only staged acceptance and rollback diff --git a/security/browser-headers/rust-browser-validation.json b/security/browser-headers/rust-browser-validation.json index 156c8e4ac4a..6393037f6be 100644 --- a/security/browser-headers/rust-browser-validation.json +++ b/security/browser-headers/rust-browser-validation.json @@ -3,16 +3,16 @@ "scope": "Current artifact source proposal only; browser, telemetry, TLS/HSTS and provider acceptance pending", "input_artifact": { "repository": "durable-workflow/sdk-rust", - "run_id": 41954374074, - "artifact_id": 13382143373, - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b", - "zip_bytes": 4071521, - "zip_sha256": "15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95" + "run_id": 41959739724, + "artifact_id": 13384761426, + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062", + "zip_bytes": 4067287, + "zip_sha256": "ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada" }, "source_preparation": null, "compatibility_accepted": false, "historical_result": "history/rust-sdk207-browser-validation.json", - "next_action": "Security reviews the exact current-artifact proposal; General separately admits bounded compatibility and any later provider stage.", - "inventory_sha256": "01e0872a2d03726d52092362f7654285a81635f92f2f5bd30ecace505d14c71d", + "next_action": "Security reviews the exact current-artifact and preparation/custody proposal; General separately decides controller implementation, preparation and any later browser/provider stage.", + "inventory_sha256": "ad4b2134cf4bdb42b7dfabd58ba145567c8b4facd9d9652f9af5c61604beb60a", "csp_sha256": "b002ffc9f95abc8145319d8a045aef9f0184d323d81ff9aec5a9de54d2f2d710" } diff --git a/security/browser-headers/rust-inventory.json b/security/browser-headers/rust-inventory.json index 5e3504f5f17..2315ad7d33e 100644 --- a/security/browser-headers/rust-inventory.json +++ b/security/browser-headers/rust-inventory.json @@ -3,11 +3,11 @@ "origin": "https://rust.durable-workflow.com", "input_artifact": { "repository": "durable-workflow/sdk-rust", - "run_id": 41954374074, - "artifact_id": 13382143373, - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b", - "zip_bytes": 4071521, - "zip_sha256": "15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95" + "run_id": 41959739724, + "artifact_id": 13384761426, + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062", + "zip_bytes": 4067287, + "zip_sha256": "ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada" }, "source_preparation": null, "build_identity": { @@ -16,7 +16,7 @@ "version": "4.12.0", "crate_sha256": "620a8287af73c363dac61f0a91f20971a39f3b8cdc9513fbea85cdbd95a74ba4", "source_revision": "4b2440a5618ccc1c98960ea2e1e62127042c3590", - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b" + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062" }, "files": [ { @@ -27,7 +27,7 @@ { "path": "__build.json", "bytes": 332, - "sha256": "6338f8427799ac2cf9b0c2a1c08fcf1889b8887761f42c2d748dc0e0cfad8a8a" + "sha256": "3e8d1827d1df8bf3c47ee8f0efcbd2f3692cb1bd8741f40c1271ef5c74f4a183" }, { "path": "activity_options/all.html", @@ -1601,8 +1601,8 @@ }, { "path": "help.html", - "bytes": 3059, - "sha256": "a42b770a5c7d5743fb8c3d58cab134336aa9f81bffbc4b442f4ae259b2a678ae" + "bytes": 3057, + "sha256": "6169059db0e7b8c17267d47d59780af8ba387556a49ddc19d4841a0b296177de" }, { "path": "index.html", @@ -1791,8 +1791,8 @@ }, { "path": "settings.html", - "bytes": 3007, - "sha256": "3fc76ea7eba9ad08821dca786bf35a746a12c619af2020f1a45f452f1dee6a91" + "bytes": 3005, + "sha256": "977ba0a22dbb3352c07d82c18569203bd5b4636c2f2aa20fc55d5e7bc302cfb0" }, { "path": "src/activity_options/activity_options.rs.html", diff --git a/security/browser-headers/rust-local-browser-packet.json b/security/browser-headers/rust-local-browser-packet.json index 30c3f04bb43..354aa50bdf5 100644 --- a/security/browser-headers/rust-local-browser-packet.json +++ b/security/browser-headers/rust-local-browser-packet.json @@ -3,11 +3,11 @@ "run_status": "not-run", "input_artifact": { "repository": "durable-workflow/sdk-rust", - "run_id": 41954374074, - "artifact_id": 13382143373, - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b", - "zip_bytes": 4071521, - "zip_sha256": "15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95" + "run_id": 41959739724, + "artifact_id": 13384761426, + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062", + "zip_bytes": 4067287, + "zip_sha256": "ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada" }, "playwright_version": "1.64.0", "expected_chromium_version": "156.0.8078.4", @@ -27,9 +27,9 @@ "rust-browser.test.cjs": "0b2f731db13e4f095230673bfda3d97a36f71c5ef62b8f3702a7978740d17d61", "rust-browser-inputs.cjs": "43bb050185f05187e7d94a40d73b4fae965276696fe7356e6609b319c6b120ee", "rust-browser-source.test.cjs": "deafc5ce08806c148b189a48f11012cc0c03600122dd8fe34eb01e223f381952", - "rust-policy.json": "c2441f7b0bf7aebc0c4ee491cda0470170231bc8baf14b4c2163552b8d2fd79c", - "rust-inventory.json": "01e0872a2d03726d52092362f7654285a81635f92f2f5bd30ecace505d14c71d", - "rust-rule.candidate.json": "90b37562aabf6ed8619fbb488ad764137731bec976d8e2ff5e7bfa382fba5634", + "rust-policy.json": "49eeb47bcb9e437f9830a2f93c89ced80cb2dfe84306ebfcc122fdf3975d0231", + "rust-inventory.json": "ad4b2134cf4bdb42b7dfabd58ba145567c8b4facd9d9652f9af5c61604beb60a", + "rust-rule.candidate.json": "a20518e8341d94445cad433acb99e7e6e9ca42c837092248b83e10cf07540e08", "runtime/package.json": "a2cd697a8ed17ab196e4eac1bcb15ceec6da243a600a8f2584c8aabdadfc3c9d", "runtime/package-lock.json": "959580e91aae43c61d0bbfcf11c21eb52ecafcfadb088f166b322fd3988a47ae" } diff --git a/security/browser-headers/rust-local-browser-plan.md b/security/browser-headers/rust-local-browser-plan.md index 1b39da08754..0f5fc7ca86e 100644 --- a/security/browser-headers/rust-local-browser-plan.md +++ b/security/browser-headers/rust-local-browser-plan.md @@ -11,36 +11,38 @@ production operation is proposed here. ## Inputs and provenance `rust-local-browser-packet.json` binds the executable/data closure and exact -runtime. Its artifact tuple is the accepted SDK #212 input already recorded in -`rust-policy.json`: artifact 13382143373, ZIP SHA-256 -`15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95`, +runtime. Its artifact tuple is the accepted SDK #211 input already recorded in +`rust-policy.json`: artifact 13384761426, ZIP SHA-256 +`ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada`, 484 files / 372 HTML pages / 49 inline-script hashes. Public Rust 4.12.0 crate -and source identities are preserved; tooling matches the accepted SDK #212 -build. The parsed rule, policy sources, approval flags and unrun status remain -unchanged. Input provenance fields now bind this artifact. +and source identities are preserved; tooling matches the accepted SDK #211 +build. Only build identity and two utility-page metadata attributes change from +#212; all 372 page records and executable hashes stay equal. The parsed rule, +policy sources, approval flags and unrun status remain unchanged. Input +provenance fields now bind this artifact. The proposed runtime is the existing Playwright 1.64.0 generation, pinned to the official **linux/amd64** platform manifest in the packet. Registry metadata was read without pulling or running that browser image. `runtime/package-lock.json` -records the two exact npm archives and integrity values. Only package-lock resolution, advisory metadata and inert source checks were -performed during this preparation. No -Playwright package, browser image or browser was executed for the source packet. - -After separate admission, prepare a dedicated public dependency directory with -`runtime/package.json` and its lock. Use `npm ci --ignore-scripts --no-audit ---no-fund` with `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`. Do not run lifecycle hooks, -`playwright install`, `npx` downloads or an image build. Verify the approved image -manifest/config, library version and lock integrity. Mount only its prepared -`node_modules` directory into the browser cell. No home directory, credentials, -Docker socket or unrelated repository is an input. An unavailable dependency, -different platform or runtime mismatch stops the attempt for review. +records the two exact npm archives and integrity values. Earlier source review +resolved the lock, read advisory/registry metadata and ran inert source checks. +This retarget performed only data/source and raw HTTPS checks. No Playwright +package, browser image or browser was executed for this source packet. + +The next preparation decision requires separate review of +[`rust-preparation-proposal.md`](rust-preparation-proposal.md): fixed public +archive acquisition, no npm or package execution, and verified data-only +construction of an image containing all three public input trees. It replaces +the earlier suggested npm-ci path. No image or dependency has been acquired, +prepared or sealed. A separately reviewed controller and cleanup implementation +is required before any acquisition/preparation admission. Before execution, independently verify the ZIP digest and safe regular-file membership, recheck the accepted protected-run/deployment identity and compare live `__build.json` with the exact artifact. Those are finite read-only checks. Stop if the live input has changed, the artifact has expired, or a required input is unavailable. Do not dispatch a replacement build or silently retarget the -packet. The input API expires this ZIP on November 6, 2026 at 02:19:51 UTC. +packet. The input API expires this ZIP on November 6, 2026 at 03:10:05 UTC. ## What the proposed check exercises @@ -79,7 +81,7 @@ Playwright, launches a browser or runs the optional browser entrypoint. requires verification outside Node before any packet code is loaded. The in-process check cannot authenticate code that is already executing. The exact current packet SHA-256 is -`2060309bf48d3e179abf55c0f2e1ffbc4908fcd7227b0cc4eb11089afde71f2a`. +`c6c735624906c4680d162ba989ebd55f00a8124e6d8ca72f781f8fee646ae281`. Before any separate preparation/run admission, General and Security must bind an approved write-preventing mechanism and exclusive owner for all three public @@ -111,7 +113,7 @@ assert not root.is_relative_to(Path.home()) manifest_path = root / 'rust-local-browser-packet.json' assert stat.S_ISREG(manifest_path.lstat().st_mode) manifest_bytes = manifest_path.read_bytes() -expected = '2060309bf48d3e179abf55c0f2e1ffbc4908fcd7227b0cc4eb11089afde71f2a' +expected = 'c6c735624906c4680d162ba989ebd55f00a8124e6d8ca72f781f8fee646ae281' assert hashlib.sha256(manifest_bytes).hexdigest() == expected manifest = json.loads(manifest_bytes) assert manifest['run_status'] == 'not-run' and len(manifest['files']) == 8 @@ -143,17 +145,13 @@ This check is an integrity prerequisite. It does not create a seal or approve an attempt. Record the separate write-prevention/custody proof and recheck the mounts and cached image immediately before creating the admitted cell. -Dependency preparation is separately held. Its future admitted environment -must have a container-only empty home, distinct empty user/global npm config -files, a task-private cache, a public registry and an explicit environment -allowlist. No host home/profile, `.npmrc`, token, credential directory or secret -proxy setting may be inherited. Set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` and -`--ignore-scripts`; use the exact lock and verify both archive integrity values -and library version. Do not import Playwright during preparation. Enumerate and -hash the resulting dependency tree, reject external links, then establish its -approved immutable/exclusive custody before any browser code resolves it. -No such environment, dependency tree or seal has been created by this source -retarget. Missing preparation/image/custody proof leaves execution held. +Dependency/image acquisition and custody are separately held. The new preparation +proposal specifies explicit credential-free transport, exact archive integrity, +safe dependency closure, a sealed memory descriptor for transfer, never-started +assembler/readback containers and a content-addressed derived image. Those +mechanisms do not exist yet and require review/admission. Preparation alone +would load no Node/Playwright code. The external hash check above remains a +prerequisite and does not itself create immutable custody. ## Proposed execution and cleanup @@ -166,9 +164,16 @@ There is no automatic retry, host IPC, privileged mode, extra capability or published port. The writable tmpfs and private shared memory count within the container's memory cap. This is a proposed budget, not a reservation. -The following is the exact proposed launch shape after admission, the external -pre-load hash check and accepted custody proof above. Assign the -three absolute input paths from the reviewed public-input preparation; reject +The following retains the earlier mount-based launch shape as historical design +only. It is **not eligible for a next browser attempt**: the new preparation +proposal replaces all three host input mounts with a verified immutable image. +After separately accepted preparation, a source successor must bind the actual +derived image ID and zero-input-mount launch shape before browser admission. +No derived image ID exists, so this document cannot authorize that launch. + +The earlier shape required the external pre-load hash check and accepted custody +proof above. It assigned three absolute input paths from reviewed public-input +preparation and rejected commas/newlines in those paths. `packet_directory` contains only this public packet. `runtime_modules` is the prepared public dependency tree. Do not point either at a home or credential directory. The pinned image must already exist; diff --git a/security/browser-headers/rust-policy.json b/security/browser-headers/rust-policy.json index 583b527041c..8d0b0dd68bc 100644 --- a/security/browser-headers/rust-policy.json +++ b/security/browser-headers/rust-policy.json @@ -4,15 +4,15 @@ "version": "4.12.0", "source_revision": "4b2440a5618ccc1c98960ea2e1e62127042c3590", "crate_sha256": "620a8287af73c363dac61f0a91f20971a39f3b8cdc9513fbea85cdbd95a74ba4", - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b" + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062" }, "input_artifact": { "repository": "durable-workflow/sdk-rust", - "run_id": 41954374074, - "artifact_id": 13382143373, - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b", - "zip_bytes": 4071521, - "zip_sha256": "15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95" + "run_id": 41959739724, + "artifact_id": 13384761426, + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062", + "zip_bytes": 4067287, + "zip_sha256": "ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada" }, "sources": { "script": [ diff --git a/security/browser-headers/rust-preparation-proposal.md b/security/browser-headers/rust-preparation-proposal.md new file mode 100644 index 00000000000..095b0c3b9c1 --- /dev/null +++ b/security/browser-headers/rust-preparation-proposal.md @@ -0,0 +1,329 @@ +# Public dependency and immutable image preparation proposal + +Status: **proposal only; acquisition, preparation, sealing and execution are not +admitted or performed**. General owns the next preparation decision after +Security review. Documentation owns the proposed preparation and cleanup. +The later browser attempt, provider operations and production telemetry each +retain their separate gates under [issue 268](https://github.com/durable-workflow/.github/issues/268). + +This proposes a specific replacement for the mutable host input mounts in the +earlier browser plan. It packages the public artifact, exact source packet and +two locked dependencies in a derived Docker image, then identifies that image +by its full local content ID. No prepared image exists. The existing browser, +helper, tests, dependency lock, official base and disabled response rule remain +unchanged. Preparation would execute no Node, npm, package lifecycle, Playwright +or browser code. A source merge does not admit these operations. + +## Subject and execution requirements + +The [actual SDK #211 owner receipt](https://github.com/durable-workflow/sdk-rust/pull/211#issuecomment-6648721461) +binds main `46cf36514f7a39e819213771abf9efc82321c062`, protected push run +41959739724, deployment 7564108107, artifact 13384761426, 4,067,287 bytes, +ZIP SHA-256 `ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada`. +Its 484 files / 372 HTML pages retain the published 4.12.0 package/source tuple. +Against #212, exactly `__build.json`, `help.html` and `settings.html` change. +The utility-page delta only substitutes `worker_latency` for `workflow_testing` +in `data-current-crate`. Script hashes, origins and the parsed rule stay equal. +The earlier #212-bound packet is historical; no browser result carries. +This artifact expires November 6, 2026 at 03:10:05 UTC. Drift or expiry stops +preparation for a fresh subject review; there is no replacement dispatch. + +The admitted native-amd64 Linux host needs an existing local Docker daemon, +trusted isolated Python with `memfd_create`, `MFD_ALLOW_SEALING` and the four +seal constants below, and its existing user systemd manager. Verify those +capabilities and fresh machine/daemon capacity before admission/use. Record +machine-specific placement facts in the existing operational channel rather +than this public source. No image was pulled, descriptor sealed or unit created. +Changing host requires fresh evidence. No vault or Cloud credential is needed. +The GitHub Actions ZIP endpoint requires GitHub authentication despite its +public contents. The selected route below accepts a separately downloaded, +explicitly reviewed ZIP. The preparation controller receives no GitHub +credential and has no authenticated artifact-fetch fallback. + +## Admission boundary and independent lifetime + +The proposed next decision covers only one acquisition/preparation sequence, +with at most one owned stopped container at a time. It does not admit the later +browser cell. Before acquisition, a separately reviewed controller and cleanup +implementation must have an exact source SHA and data-only fault controls. +They must not import packet modules. This document is not that implementation. + +Use the existing user systemd manager for a transient service named +`durable-workflow-github-io-rust-preparation`, with `RuntimeMaxSec=600`, +`TimeoutStopSec=15`, `KillMode=control-group`, `Restart=no`, one CPU, +`MemoryMax=1G`, `TasksMax=64`, and `UMask=0077`. Its host-side controller owns +all subprocesses and a fixed allowlist of container names and recorded IDs. +Register independently supervised cleanup before the first pull/create. The +service's stop cleanup must remove owned containers even if the controller or +agent dies between create and ID recording. It must verify exact name, creation +window and the full owner/role/nonce identity below before removal; a pre-existing +name is a refusal. Creation time alone never grants deletion authority. +Do not rely on the agent's shell trap or a guardian started after creation. + +Read back the actual service limits, cleanup registration and first fresh +CPU/RAM/disk sample before declaring ready. Its cleanup remains active across +the whole sequence. A lost supervisor, timeout, unexpected object, missing +observation or cleanup failure stops and records nonpassing evidence. No retry, +new helper, sudo, daemon configuration or system service is included. If the +existing user-manager mechanism cannot be qualified, return the exact gap to +General before doing acquisition. Later browser lifetime/cleanup needs its own +reviewed controller and separate admission. + +## Authenticated artifact handoff, without credential delegation + +Select the predownloaded-ZIP route only. Documentation's existing trusted +source-review transport already fetched the exact SDK artifact with its normal +GitHub authorization. That read-only QA download is not a prepared input or +immutable seal. The preparation controller must not call `gh`, read its token, +use an implicit cache or attempt an anonymous/API/redirect fallback. + +Before any preparation admission, bind one exact regular ZIP in the private +operational handoff: canonical absolute path outside home/credential trees, +owner UID/GID, file and parent-directory modes, device/inode, exact 4,067,287 +bytes, the full API ZIP digest above, protected run/artifact/deployment/expiry, +and the independent 484-file inventory/owner receipt. The handoff must be +acknowledged by General and the independent reviewer. No path/owner/mode field +may remain unbound at admission. A different path or owner is a new handoff, +not permission to search caches or download another copy. Host-specific paths +and account details stay in that existing private operational record. + +The admitted controller walks the named path through held directory descriptors +using no-follow directory/file opens, verifies every component and rejects +symlinks. It compares `fstat` owner, mode, device/inode and size with the reviewed +handoff, then reads at most the exact byte count plus one from that same held +descriptor. Hash and inspect those captured bytes; never re-open the path for +subsequent parsing/copy. A concurrent edit either reproduces the exact accepted +buffer or fails its fixed digest. Owner-write permission or a prior hash does +not establish immutable custody. + +Verify the ZIP digest, single safe `artifact.tar`, all safe unique regular-file +and directory members, exact 484 path/size/hash rows, build identity and expiry +before declassifying the captured bytes as public input. The public-input tar +below is constructed from those same authenticated buffers and later sealed. +No credential, API response header, download URL or QA cache metadata enters it. +A trusted source owner must independently refresh the run/deployment/artifact +metadata and live build receipt before admission; that normal credentialed +metadata read stays outside the credential-free preparation controller. + +Missing, changed or expired handoff, failed no-follow/owner/mode checks, digest +or inventory drift, or unavailable fresh metadata stops before acquisition or +Docker allocation. Do not silently choose another source or dispatch a new +build. No handoff is declared accepted, file permission changed or seal made +by this source proposal. + +## Credential-isolated public acquisition + +Create task-private transport state outside home and credential directories +only after admission. Every host subprocess gets an explicit environment: +fixed system `PATH`, task-only empty home, locale, task-only empty Docker config +and the fixed Docker endpoint. Do not inherit proxy variables, `.docker`, +credential helpers, `.npmrc`, netrc, SSH agent, tokens, profiles or repository +metadata. Do not print complete environments or authentication configuration. +There is no npm process or configuration lookup in this proposal. + +Acquire the exact public official linux/amd64 base, without running it: + +```text +docker image pull --platform linux/amd64 + mcr.microsoft.com/playwright@sha256:bc72a8df40831ded821e3cbc79c318c21b44d6e51dff9fd89ace7a8249c5b462 +``` + +The controller supplies the empty Docker config and clean environment above. +Use no login, custom registry, daemon proxy change, tag selection, image build, +push or automatic retry. Independently compare the downloaded platform manifest +and config to the reviewed digest and config +`847f71bbb3f17fc1032d50d75bf6fb3fdcf188b16dff81f24667fe9ae9409e01`. +Record the base image ID, architecture, OS, layer list and size. Reject unexpected +volumes, on-build actions, platform, configuration or symlinked input prefixes. +Separately verify that the daemon's existing transport uses no credential-bearing +proxy for this public path. Clean client environment/configuration does not +establish that fact. An unknown or credential-dependent daemon path stops for +General's decision; do not inspect or print credential values or reconfigure it. + +The official image acquisition is a daemon operation: the service CPU/memory +limits do not cap the Docker daemon. Before pulling, record the public manifest's +compressed layer sizes separately from the eventual unpacked image size, confirm +at least 20 GiB free disk and sample daemon/host use during acquisition. Stop +qualification if the resulting unpacked base exceeds eight GiB; this is an +acceptance budget, not an enforced daemon storage quota. Bound this phase +to 240 seconds. A client timeout is not proof that daemon transfer stopped; +independently reconcile the pull, partial cache and disk afterward. If that +completion cannot be established, hold later steps and name the retained cache +and owner. Do not prune shared layers or change daemon settings to recover. + +Using only the trusted host Python standard library, fetch these two fixed +public URLs with TLS verification, no ambient proxy/authentication, no netrc, +no redirects, 30-second request deadlines and a 16-MiB compressed limit each: + +- `https://registry.npmjs.org/playwright/-/playwright-1.64.0.tgz` +- `https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.0.tgz` + +Authenticate both complete byte buffers against the exact SHA-512 integrity +values in the externally authenticated `runtime/package-lock.json`. Do not +resolve versions, alter the lock, query a private registry or use a host npm +cache. HTTP failure, redirect, integrity/graph drift or unavailable archive +stops the sequence. An online package install is not a fallback. + +## Construct dependencies without loading their code + +In trusted isolated Python, inspect each authenticated archive as data. Accept +only unique regular files and directories under its single `package/` prefix. +Reject absolute/traversal/backslash/NUL paths, links, devices, sparse entries +or special permission bits. Ignore archive ownership and normalize it below. +Limit the combined dependency +closure to 10,000 files / 64 MiB unpacked. Parse package JSON as data and require +exact names/versions, the sole Playwright-to-core 1.64.0 dependency, no other +required/optional graph, and the reviewed lock graph of exactly two packages. +Do not execute package scripts or import either library. + +Map those exact files to `/runtime/node_modules/playwright/` and +`/runtime/node_modules/playwright-core/`. Generate no npm `.bin` links or shim +scripts; the harness resolves the library through `NODE_PATH`, and requires no +package CLI. Record every relative path, size, content hash and normalized +mode. This deliberately replaces the earlier suggested npm-ci preparation. +It makes no npm-install-equivalence claim and requires its own source review. + +Authenticate the nine-file packet closure with the externally reviewed manifest +hash before using its code/data, and match all 484 artifact files to the accepted +inventory. Combine only `/packet`, `/site` and `/runtime/node_modules` in one +canonical public tar, at most 128 MiB. Reset owners to UID/GID 1000, directories +to 0555 and files to 0444 plus only already-reviewed executable bits; preserve +file bytes. Refuse links, extra roots, duplicate names and shadow modules. +No host home, config, cache, credentials, logs or controller source enters it. + +The controller writes that complete verified tar to a Linux anonymous memory +file, adds `F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL`, and reads +back all four seals plus the full tar hash before any Docker copy. The same +sealed descriptor, rewound, supplies Docker's standard input. A mutable path +is never re-opened for this transfer. Seals close the validation-to-copy race; +they are not a claim that a mutable worktree has become immutable. No such +descriptor, tar or dependency tree has been made by this proposal. + +## Content-addressed custody and independent readback + +Before allocation, the controller generates `secrets.token_hex(32)` in trusted +host Python: exactly 32 random bytes / 64 lowercase hexadecimal characters per +attempt. Persist the full nonce with the role/name allowlist in a task-private +0700 directory / 0600 regular record, and make it available to the independent +cleanup service before any create. Do not derive it from time, PID, branch, +returned ID or fixed name. Do not publish it while allocation/cleanup is active. +No nonce or ownership record has been generated for this proposal. + +Both container creates must use these literal label keys and values: + +| Label | Value | +| --- | --- | +| `com.durable-workflow.agent` | `durable-workflow-github-io` | +| `com.durable-workflow.attempt` | The privately retained 64-hex attempt nonce | +| `com.durable-workflow.purpose` | `rust-custody-assemble` or `rust-custody-verify`, matching the fixed role name | + +Preflight exact-name absence and absence of an existing owner/nonce allocation +before every create. A foreign name collision refuses without inspecting or +removing that foreign container. Validate returned IDs against `^[0-9a-f]{64}$` +before assigning a trusted ID. Independently rediscover exactly one allocation +by exact name plus all three labels. Inspect only that discovered owned ID and +require returned/discovered equality, effective image/config/labels, no mounts +and never-started state before copy or readback. Malformed/missing/mismatched +create output records failure and prohibits copy/commit; it must not prevent +cleanup of a separately discovered and fully verified owned allocation. + +Cleanup may remove only a recorded or independently rediscovered ID whose +exact role name and all three owner/nonce/role labels match the private record. +It repeats those checks immediately before removal. A missing ID record or +controller death still uses that bounded exact-label/name discovery. No broad +name/prefix/label deletion, time-based ownership or foreign-candidate inspection +is permitted. Unexpected multiplicity or identity drift is a failure, never +authority to delete an unverified object. Independently record exact owned +survivors and foreign-object preservation at terminal cleanup. + +Create `durable-workflow-github-io-rust-custody-assemble` from the exact cached +base using `--pull=never`, network none, no ports/mounts/volumes, one CPU, +512 MiB, 64 PIDs, `--init`, no added capabilities, no-new-privileges and command +`/bin/false`. Inspect its exact base ID, no mounts/volumes and `State=created` +before copying. **Never start it.** Stream the sealed tar to `docker cp - +:/`, without symlink following. Check the copy result and bounded +writable-layer size; commit this never-started container to a new local image. +Do not publish it or identify it by a mutable tag. + +Docker commit does not include mounted volumes, which is why this assembler +must have none. Record the returned full `sha256:` image ID, config digest and +rootfs diff IDs; the original base chain must be an exact prefix with only the +new public-input layer added. Keep the image's default command `/bin/false`, +with no executable preparation hook. Remove the assembler normally by its +verified ID using the identity procedure above, and verify absence before +creating the readback container. + +Create `durable-workflow-github-io-rust-custody-verify` from that full derived +image ID with `--pull=never --read-only`, the same restrictions and no input +mounts. Inspect its actual image/config and never-started state. Stream each +of the three input trees out via `docker cp : -`. Parse those tar +streams with trusted Python; never extract container-controlled paths onto the +host. Independently require exact path/mode/content equality to the sealed +public-input inventory, the external nine-file packet check and all artifact +and dependency hashes. Reject extra modules, symlinks, mounts or shadowing. +Remove the readback container and verify absence. + +Bind any derived-image cleanup to its full returned and independently read-back +content ID and the creation receipt, never a tag or broad label query. A missing +or mismatched image ID stops image deletion; it does not widen container cleanup +authority. The shared/base image and layers remain outside deletion authority. + +The resulting receipt names the base manifest/config, derived image ID/layer +chain, sealed tar hash, three tree inventories, external-verifier identity, +resource observations, acquisition/cleanup results and actual SDK subject. +The immutable image ID binds the snapshot; a later browser cell must select +that exact ID with a read-only root filesystem and **zero host input mounts**. +The base's existing browser binaries remain unchanged. Mutable staging trees +can then be removed, since execution would not read them. + +Docker copy/commit are also daemon operations outside the controller's cgroup. +Their finite input budget and the independent whole-sequence deadline are +required alongside fresh host/daemon and disk samples. An unfinished operation +does not qualify merely because its client exited. Reconcile it before cleanup +or acceptance, and return an unresolved daemon operation to General. + +This relies on the existing trusted host kernel and Docker daemon. It provides +custody against ordinary host-tree edits and unprivileged subject writes; it +does not protect against a malicious root/Docker administrator. Documentation +owns the new objects, coordinates exclusive use and records any other writer +as a stop condition. No new privilege or cross-agent branch ownership follows. + +## Decision and completion record + +Security first reviews this source design, exact current packet and proposed +custody boundary. General then decides whether to commission the exact +controller/fault controls and admit one named preparation attempt. Preparation +acceptance requires the complete independent readback and cleanup receipt; it +cannot be inferred from a successful pull, package hash or image commit. + +The required controller's data-only predecessor-fail/successor-pass controls must +cover the exact artifact handoff and buffer boundary; path/owner/mode/symlink, +ZIP/tar traversal/link/special/duplicate/limit and sealed-descriptor mutation; +base-prefix symlinks; create-output corruption or loss; foreign-name/nonce +collision; every allocation/ID-recording/controller/supervisor-death edge; +continuing daemon work after copy/commit timeout; image/config/layer mismatch; +readback mismatch; and exact survivor/cleanup accounting. The controller source, +passing controls and independent review are required before a preparation +decision. They are not implemented or claimed by this documentation change. + +Retain at most the reviewed base and one derived image under Documentation's +named cache ownership pending that decision. Record exact IDs and sizes; +remove owned transient containers, public transport state and sealed descriptors +at terminal cleanup. Never prune or remove another owner's/shared image. +Preparation failure preserves a bounded diagnostic receipt and stops; no retry +or browser follows. No registry publication, secret or paid resource is needed. + +After accepted preparation, a separate source packet must bind the actual +derived image ID and update the browser launch shape from three host mounts +to these immutable in-image paths. Keep the existing two-test, one-browser, +network-none, two-CPU / two-GiB / 256-PID and 300-second limits. Qualify independent +cleanup and obtain separate browser admission before loading any packet Node +module. Candidate/compatibility/rollout flags remain false and current browser +status remains not-run throughout this preparation proposal. + +Technical references: +[Docker's content-addressed image store](https://docs.docker.com/reference/cli/docker/image/pull/), +[tar streaming and stopped-container copy](https://docs.docker.com/reference/cli/docker/container/cp/), +[commit and excluded mounted volumes](https://docs.docker.com/reference/cli/docker/container/commit/), +[Python anonymous memory files](https://docs.python.org/3/library/os.html#os.memfd_create), +[systemd service deadlines and cleanup](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html). diff --git a/security/browser-headers/rust-rule.candidate.json b/security/browser-headers/rust-rule.candidate.json index 3b66f5f9ee0..b6813d0c8ed 100644 --- a/security/browser-headers/rust-rule.candidate.json +++ b/security/browser-headers/rust-rule.candidate.json @@ -2,15 +2,15 @@ "schema": "durable-workflow.browser-security-rule-candidate/v1", "rollout_approved": false, "phase": "http_response_headers_transform", - "inventory_sha256": "01e0872a2d03726d52092362f7654285a81635f92f2f5bd30ecace505d14c71d", + "inventory_sha256": "ad4b2134cf4bdb42b7dfabd58ba145567c8b4facd9d9652f9af5c61604beb60a", "compatibility_accepted": false, "input_artifact": { "repository": "durable-workflow/sdk-rust", - "run_id": 41954374074, - "artifact_id": 13382143373, - "tooling_revision": "2b45fd7b12a6e9460666045a3c7f644860caea3b", - "zip_bytes": 4071521, - "zip_sha256": "15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95" + "run_id": 41959739724, + "artifact_id": 13384761426, + "tooling_revision": "46cf36514f7a39e819213771abf9efc82321c062", + "zip_bytes": 4067287, + "zip_sha256": "ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada" }, "source_preparation": null, "rule": { diff --git a/security/browser-headers/rust-stage-proposal.md b/security/browser-headers/rust-stage-proposal.md index 31acbebe83e..4899b0df3dd 100644 --- a/security/browser-headers/rust-stage-proposal.md +++ b/security/browser-headers/rust-stage-proposal.md @@ -15,7 +15,7 @@ credentials or API were used. A source merge does not clear those gates. ## Exact input -The [Rust owner receipt](https://github.com/durable-workflow/sdk-rust/pull/212#issuecomment-6648011712) +The [Rust owner receipt](https://github.com/durable-workflow/sdk-rust/pull/211#issuecomment-6648721461) binds the following accepted automatic protected build: | Input | Value | @@ -23,24 +23,24 @@ binds the following accepted automatic protected build: | Package | Published `durable-workflow` 4.12.0 | | Public source | `4b2440a5618ccc1c98960ea2e1e62127042c3590` | | Crate SHA-256 | `620a8287af73c363dac61f0a91f20971a39f3b8cdc9513fbea85cdbd95a74ba4` | -| Tooling | `2b45fd7b12a6e9460666045a3c7f644860caea3b` | -| Protected run | [41954374074](https://github.com/durable-workflow/sdk-rust/actions/runs/41954374074), push, attempt 1 | -| Successful deployment | `7563384998` | -| Pages artifact | `13382143373`, 4,071,521-byte ZIP | -| ZIP SHA-256 | `15f1e2745a0b268e1093820b3486c4910e8d1bf7b63062e562200586d3beef95` | +| Tooling | `46cf36514f7a39e819213771abf9efc82321c062` | +| Protected run | [41959739724](https://github.com/durable-workflow/sdk-rust/actions/runs/41959739724), push, attempt 1 | +| Successful deployment | `7564108107` | +| Pages artifact | `13384761426`, 4,067,287-byte ZIP | +| ZIP SHA-256 | `ea80b0d3191da525152b8f02ff2e3253a29ed93a723dfeae4c344eefb392cada` | | Extracted inventory | 484 regular files, 372 HTML pages, 49 executable inline-script hashes | -| Inventory file SHA-256 | `01e0872a2d03726d52092362f7654285a81635f92f2f5bd30ecace505d14c71d` | -| Policy file SHA-256 | `c2441f7b0bf7aebc0c4ee491cda0470170231bc8baf14b4c2163552b8d2fd79c` | -| Candidate file SHA-256 | `90b37562aabf6ed8619fbb488ad764137731bec976d8e2ff5e7bfa382fba5634` | - -The downloaded ZIP matched its GitHub API digest. Extraction rejected absolute -paths, parent traversal and non-regular members. Compared with #472's immutable -SDK #207 subject, only `__build.json`, `help.html` and `settings.html` differ. -The two HTML changes replace `data-current-crate="activity_options"` with -`data-current-crate="workflow_testing"`. All other files and every inventoried -script/style/handler/origin record are identical. Public package/source identity -is preserved. SDK #212 retains every SDK #209 page and asset byte. Only its -`__build.json` changes from the accepted #476/#477 subject. The historical browser +| Inventory file SHA-256 | `ad4b2134cf4bdb42b7dfabd58ba145567c8b4facd9d9652f9af5c61604beb60a` | +| Policy file SHA-256 | `49eeb47bcb9e437f9830a2f93c89ced80cb2dfe84306ebfcc122fdf3975d0231` | +| Candidate file SHA-256 | `a20518e8341d94445cad433acb99e7e6e9ca42c837092248b83e10cf07540e08` | + +The downloaded ZIP matched its GitHub API digest. Membership inspection rejected +absolute paths, parent traversal and non-regular members. Compared with the +accepted SDK #212 / Docs #478 subject, exactly `__build.json`, `help.html` and +`settings.html` differ. The two utility pages only replace +`data-current-crate="workflow_testing"` with `data-current-crate="worker_latency"`. +All other files and every inventoried script/style/handler/origin record are +identical. Public package/source identity is preserved. The old #212 tuple and +its accepted source reviews remain historical in #478. The historical browser pass remains in `history/rust-sdk207-browser-validation.json`; it does not qualify this artifact. @@ -50,14 +50,14 @@ absent. This is a finite identity sample, not a live site-wide compatibility run | Path at `https://rust.durable-workflow.com/` | SHA-256 | | --- | --- | -| `__build.json` | `6338f8427799ac2cf9b0c2a1c08fcf1889b8887761f42c2d748dc0e0cfad8a8a` | +| `__build.json` | `3e8d1827d1df8bf3c47ee8f0efcbd2f3692cb1bd8741f40c1271ef5c74f4a183` | | `index.html` | `b2e918903002c3d863aab6eb77c44a01af5ece95d26bb433120d396cae055fae` | | `durable_workflow/index.html` | `8747b578d0749a534ae67416aeb79bca02b7e50c9c8194f85aae883cc99f0e20` | | `durable_workflow/struct.Client.html` | `1c4a15c7383e28a3af199e55ee217a157165f45e131dd6e708ef5788661606b1` | | `durable_workflow/struct.WorkflowHandle.html` | `4eceaae93e24c0b31a83d7cb95737ddc9e51fae025e271a025fd5185e49ac31c` | | `navigation.js` | `718db7a749882b290853d988ba0681b44f97fe651e4ebff078b186ff021a3a37` | -| `help.html` | `a42b770a5c7d5743fb8c3d58cab134336aa9f81bffbc4b442f4ae259b2a678ae` | -| `settings.html` | `3fc76ea7eba9ad08821dca786bf35a746a12c619af2020f1a45f452f1dee6a91` | +| `help.html` | `6169059db0e7b8c17267d47d59780af8ba387556a49ddc19d4841a0b296177de` | +| `settings.html` | `977ba0a22dbb3352c07d82c18569203bd5b4636c2f2aa20fc55d5e7bc302cfb0` | ## Exact candidate