From dc9e7d2d89f5748fe801eb5ca8a8dfec839699a3 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:26:37 +0000 Subject: [PATCH 1/3] Propose immutable public browser inputs for the accepted Rust artifact --- security/browser-headers/README.md | 20 +- .../rust-browser-validation.json | 14 +- security/browser-headers/rust-inventory.json | 22 +- .../rust-local-browser-packet.json | 16 +- .../rust-local-browser-plan.md | 75 +++--- security/browser-headers/rust-policy.json | 12 +- .../rust-preparation-proposal.md | 225 ++++++++++++++++++ .../browser-headers/rust-rule.candidate.json | 12 +- .../browser-headers/rust-stage-proposal.md | 42 ++-- 9 files changed, 336 insertions(+), 102 deletions(-) create mode 100644 security/browser-headers/rust-preparation-proposal.md 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..c28dbac3a4e --- /dev/null +++ b/security/browser-headers/rust-preparation-proposal.md @@ -0,0 +1,225 @@ +# 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. + +## 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 ownership label before removal; a pre-existing name is a refusal. +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. + +## 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. + +The official image acquisition is a daemon operation: the service CPU/memory +limits do not cap the Docker daemon. Before pulling, compare the public manifest +layer sizes with an eight-GiB maximum unpacked-base budget, confirm at least +20 GiB free disk and sample daemon/host use during acquisition. 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 + +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 +recorded ID 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. + +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. + +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 From f483d97bcb29625d2fcc2a49a3de2543032d6ab3 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:27:42 +0000 Subject: [PATCH 2/3] Clarify daemon transport and image storage acceptance limits --- .../browser-headers/rust-preparation-proposal.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/security/browser-headers/rust-preparation-proposal.md b/security/browser-headers/rust-preparation-proposal.md index c28dbac3a4e..39adddc3119 100644 --- a/security/browser-headers/rust-preparation-proposal.md +++ b/security/browser-headers/rust-preparation-proposal.md @@ -88,11 +88,17 @@ 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, compare the public manifest -layer sizes with an eight-GiB maximum unpacked-base budget, confirm at least -20 GiB free disk and sample daemon/host use during acquisition. Bound this phase +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 From 8f94c50a85577da5122f8af63dfe0df731243ee4 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:38:29 +0000 Subject: [PATCH 3/3] Bind the artifact handoff and per-attempt cleanup identity --- .../rust-preparation-proposal.md | 102 +++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/security/browser-headers/rust-preparation-proposal.md b/security/browser-headers/rust-preparation-proposal.md index 39adddc3119..095b0c3b9c1 100644 --- a/security/browser-headers/rust-preparation-proposal.md +++ b/security/browser-headers/rust-preparation-proposal.md @@ -35,6 +35,10 @@ 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 @@ -52,7 +56,8 @@ 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 ownership label before removal; a pre-existing name is a refusal. +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 @@ -64,6 +69,48 @@ 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 @@ -154,6 +201,41 @@ 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 @@ -168,7 +250,8 @@ 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 -recorded ID and verify absence before creating the readback container. +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 @@ -180,6 +263,11 @@ 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. @@ -208,6 +296,16 @@ 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