From 8b382ba5c7feb14a0563f79ee28d89647d96f411 Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Tue, 11 Aug 2026 11:40:17 -0400 Subject: [PATCH 1/2] Adopt Component Model map and implements features for 0.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WASI Subgroup voted on 2026-08-06 to adopt the Component Model map type (πŸ—ΊοΈ) and the implements/external-id annotations (🏷️), both by full consensus. Record the adoption, note where that record fits in the release process, and state the required feature baseline in each released specification overview. CI now validates each changed proposal against exactly the adopted feature set, so proposals can use the newly adopted features and cannot quietly depend on an un-adopted one. This requires wasm-tools 1.249.0 or later for `cm-implements`. Closes #942 Closes #943 Signed-off-by: Bailey Hayes --- .github/RELEASE.md | 4 + .github/actions/install-tools/action.yml | 2 +- .github/scripts/README.md | 15 +++ .github/scripts/cm-features.js | 137 +++++++++++++++++++++++ .github/scripts/validate-proposals.js | 53 ++++++++- .github/workflows/publish.yml | 8 ++ CONTRIBUTING.md | 10 +- docs/ComponentModelFeatures.md | 41 +++++++ docs/Release.md | 3 + specifications/wasi-0.3.0/Overview.md | 15 +++ 10 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 .github/scripts/cm-features.js create mode 100644 docs/ComponentModelFeatures.md diff --git a/.github/RELEASE.md b/.github/RELEASE.md index 858178bc..9061b9f2 100644 --- a/.github/RELEASE.md +++ b/.github/RELEASE.md @@ -42,3 +42,7 @@ Feature gates queued for stabilization (`@unstable` β†’ `@since`) must have passed their phase 3 vote, and any new dependence on a Component Model feature must have been adopted by a WASI Subgroup vote, before the release is cut. Both processes are documented in [CONTRIBUTING.md](../CONTRIBUTING.md). + +Adopted Component Model features are recorded in [Component Model features in +WASI](../docs/ComponentModelFeatures.md); features newly adopted for a release +must be called out in that release's notes. diff --git a/.github/actions/install-tools/action.yml b/.github/actions/install-tools/action.yml index 252bc6e7..2e190117 100644 --- a/.github/actions/install-tools/action.yml +++ b/.github/actions/install-tools/action.yml @@ -9,7 +9,7 @@ inputs: wasm-tools-version: description: 'Version of wasm-tools to install' required: false - default: '1.244.0' + default: '1.256.0' runs: using: 'composite' diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 5cafc890..fe8afc16 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -1,5 +1,20 @@ # CI Scripts +## cm-features.js + +Reads the Component Model adoption record in `docs/ComponentModelFeatures.md`, +which is the single source of truth for which gated features WASI requires. + +- `publish.yml` runs it to write the "Component Model features" section of each + released `specifications/wasi-/Overview.md`. +- `validate-proposals.js` imports `readRecord` from it to build the + `wasm-tools validate` feature flags. + +```bash +# Print the section for a given version +node .github/scripts/cm-features.js 0.3.1 +``` + ## validate-proposals.js Validates WIT definitions for changed proposals. Used by the CI workflow. diff --git a/.github/scripts/cm-features.js b/.github/scripts/cm-features.js new file mode 100644 index 00000000..ea19fd5c --- /dev/null +++ b/.github/scripts/cm-features.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node + +// Emit the "Component Model features" section of a specification overview. +// +// The adoption record in docs/ComponentModelFeatures.md is the source of +// truth: it lists every Component Model gated feature the WASI Subgroup has +// voted to adopt, and the release it was adopted for. Adoption is cumulative, +// so the section for a given version lists every feature adopted in that +// version or an earlier one. +// +// Prerelease versions (`0.3.0-rc-*`) are treated as their base version, since +// a release candidate carries the same feature baseline as the release it is +// a candidate for. +// +// Usage: node cm-features.js [record-path] + +const fs = require('fs'); +const path = require('path'); + +const RECORD = path.join(__dirname, '..', '..', 'docs', 'ComponentModelFeatures.md'); +const HEADING = '## Adopted features'; + +// The record marks the default (ungated) features with an em dash in the gate +// column. They are not a gated feature, so they are described in prose below +// rather than listed as a row. +const UNGATED = 'β€”'; + +function parseVersion(version) { + const base = version.split('-')[0]; + const parts = base.split('.').map((part) => Number(part)); + if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) { + throw new Error(`not a version: '${version}'`); + } + return parts; +} + +function compareVersions(a, b) { + const left = parseVersion(a); + const right = parseVersion(b); + for (let i = 0; i < 3; i++) { + if (left[i] !== right[i]) return left[i] - right[i]; + } + return 0; +} + +// Rows of the adoption table, in document order, as {version, gate, feature}. +function readRecord(recordPath = RECORD) { + const lines = fs.readFileSync(recordPath, 'utf-8').split('\n'); + const start = lines.indexOf(HEADING); + if (start === -1) { + throw new Error(`${recordPath}: no '${HEADING}' heading`); + } + + const rows = []; + for (let i = start + 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('## ')) break; + if (!line.startsWith('|')) continue; + // Cells are read positionally, so a row missing its trailing pipe would + // silently lose the last character of its final cell. + if (!line.endsWith('|')) { + throw new Error(`${recordPath}:${i + 1}: table row does not end with '|'`); + } + + const cells = line.slice(1, -1).split('|').map((cell) => cell.trim()); + if (cells.length !== 4) { + throw new Error(`${recordPath}:${i + 1}: expected 4 columns, found ${cells.length}`); + } + // Skip the header and its separator, in any of the alignment styles. + if (cells[0] === 'WASI version') continue; + if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue; + + rows.push({ version: cells[0], gate: cells[1], feature: cells[2] }); + } + + if (rows.length === 0) { + throw new Error(`${recordPath}: no rows under '${HEADING}'`); + } + return rows; +} + +// The section is appended to a partially written overview, ahead of that +// file's link definitions, so it both starts and ends with a blank line: a +// link definition on the line directly below a paragraph would be parsed as +// part of that paragraph. +function render(version, rows) { + const adopted = rows + .filter((row) => row.gate !== UNGATED) + .filter((row) => compareVersions(row.version, version) <= 0); + + const out = ['', '## Component Model features', '']; + + if (adopted.length === 0) { + out.push( + 'Implementing this version of the specification requires the default (ungated)', + '[Component Model][cm] features. No [gated features][gates] have been adopted.', + '', + '' + ); + return out.join('\n'); + } + + out.push( + 'Implementing this version of the specification requires the default (ungated)', + '[Component Model][cm] features, plus the [gated features][gates] adopted by the', + 'WASI Subgroup up to and including this version:', + '', + '| Gate | Feature | Adopted in |', + '| --- | --- | --- |' + ); + for (const row of adopted) { + out.push(`| ${row.gate} | ${row.feature} | ${row.version} |`); + } + out.push( + '', + 'These are required whether or not an API in this version uses them. See', + '[Component Model features in WASI][features] for the full adoption record.', + '', + '' + ); + return out.join('\n'); +} + +function main() { + const [version, recordPath = RECORD] = process.argv.slice(2); + if (!version) { + console.error('Usage: node cm-features.js [record-path]'); + process.exit(1); + } + process.stdout.write(render(version, readRecord(recordPath))); +} + +if (require.main === module) { + main(); +} + +module.exports = { RECORD, UNGATED, readRecord, render, compareVersions }; diff --git a/.github/scripts/validate-proposals.js b/.github/scripts/validate-proposals.js index 724e83d8..d058c0bb 100644 --- a/.github/scripts/validate-proposals.js +++ b/.github/scripts/validate-proposals.js @@ -2,7 +2,45 @@ const { execSync } = require('child_process'); const fs = require('fs'); +const os = require('os'); +const path = require('path'); const { validateDirectory, formatErrors } = require('./validate-since'); +const { readRecord, UNGATED } = require('./cm-features'); + +// `wasm-tools` feature name for each Component Model gate WASI has adopted. +// Adding a row to docs/ComponentModelFeatures.md without an entry here is a +// hard error rather than a silently un-enforced feature. +const GATE_FEATURES = { + 'πŸ”€': 'cm-async', + 'πŸ—ΊοΈ': 'cm-map', + '🏷️': 'cm-implements', +}; + +// Emoji gates carry a variation selector (U+FE0F) inconsistently across +// sources, so compare them without it. +const bare = (gate) => gate.replace(/\uFE0F/g, ''); + +// The adopted Component Model features, as `wasm-tools` CLI flags. Enabling +// them is what keeps proposals from depending on a feature the Subgroup has +// not voted to adopt: an un-adopted gate stays off and fails validation. Note +// these are added to whatever `wasm-tools` enables by default, so a gate that +// is un-adopted here but on by default there is not caught. +const adoptedFeatureFlags = () => { + const lookup = new Map(Object.entries(GATE_FEATURES).map(([g, f]) => [bare(g), f])); + const features = new Set(); + for (const { gate, version } of readRecord()) { + if (gate === UNGATED) continue; + const feature = lookup.get(bare(gate)); + if (!feature) { + throw new Error( + `docs/ComponentModelFeatures.md adopts '${gate}' for ${version}, but ` + + `GATE_FEATURES in ${path.basename(__filename)} has no wasm-tools feature name for it` + ); + } + features.add(feature); + } + return [...features].map((feature) => `-f ${feature}`).join(' '); +}; const parseFiles = (filesJson) => { if (!filesJson || filesJson === 'null') return []; @@ -48,6 +86,10 @@ if (toValidate.length === 0) { let failed = false; +const featureFlags = adoptedFeatureFlags(); +const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wasi-validate-')); +console.log(`Component Model features: ${featureFlags || '(none adopted)'}`); + for (const proposal of toValidate) { const witDir = ((proposal) => `proposals/${proposal}/wit`)(proposal); console.log(`::group::Validating ${proposal}`); @@ -81,9 +123,16 @@ for (const proposal of toValidate) { // Validate wasm encoding console.log(' Validating wasm encoding...'); - if (!run(`wasm-tools component wit "${witDir}" --wasm -o /dev/null`)) { + const encoded = path.join(outDir, `${proposal}.wasm`); + if (!run(`wasm-tools component wit "${witDir}" --wasm -o "${encoded}"`)) { console.log(`::error::wasm encoding failed for ${proposal}`); failed = true; + } else if (!run(`wasm-tools validate ${featureFlags} "${encoded}"`)) { + // `component wit --wasm` does not enforce Component Model feature gates, + // so the encoded package is validated separately against the features + // WASI has adopted. + console.log(`::error::Component Model validation failed for ${proposal}`); + failed = true; } // Validate @since annotations @@ -99,6 +148,8 @@ for (const proposal of toValidate) { } } +fs.rmSync(outDir, { recursive: true, force: true }); + if (failed) { console.log('\n❌ Validation failed'); process.exit(1); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8436f92e..e6c511b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -243,10 +243,18 @@ jobs: - [wasi:filesystem@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Ffilesystem?tag=${VERSION}) - [wasi:cli@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fcli?tag=${VERSION}) - [wasi:http@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fhttp?tag=${VERSION}) + EOF + + # Which Component Model gated features this version requires, from + # the adoption record in docs/ComponentModelFeatures.md. + node .github/scripts/cm-features.js "$VERSION" >> "$SPEC_DIR/Overview.md" + cat >> "$SPEC_DIR/Overview.md" << EOF [cm]: https://github.com/WebAssembly/component-model [wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md [wasm-oci]: https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact + [gates]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features + [features]: https://github.com/WebAssembly/WASI/blob/main/docs/ComponentModelFeatures.md EOF # Remove leading whitespace from heredoc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73d04a37..38935141 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -212,8 +212,10 @@ followed: evaluates whether the feature is genuinely stable and whether the iteration and feedback phase has been sufficient. 1. Once the SG votes to adopt the feature, stable (`@since`-gated) WASI APIs in - the next release may depend on it. The release notes for that version must - call out the newly adopted Component Model feature. + the next release may depend on it. The adoption is recorded in + [Component Model features in WASI](docs/ComponentModelFeatures.md), and the + release notes for that version must call out the newly adopted Component + Model feature. **Note:** Not all Component Model features are surfaced in WASI WIT definitions. Some, such as new canonical ABI features, have no WIT-level @@ -221,7 +223,9 @@ followed: 1. From that release onward, the feature is part of the baseline set of Component Model features required to implement that WASI version, matching the feature's status in the Component Model's [gated features] documentation - (for example, WASI 0.3 adopts the features gated by πŸ”€ async). + (for example, WASI 0.3 adopts the features gated by πŸ”€ async). A PR to the + Component Model repository updates that documentation to record which WASI + release adopted the feature. [Component Model]: https://github.com/WebAssembly/component-model [gated features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features diff --git a/docs/ComponentModelFeatures.md b/docs/ComponentModelFeatures.md new file mode 100644 index 00000000..1fc39ee2 --- /dev/null +++ b/docs/ComponentModelFeatures.md @@ -0,0 +1,41 @@ +# Component Model features in WASI + +WASI APIs are defined in terms of the [Component Model], which develops new WIT +syntax, types, and canonical ABI functionality behind [gated features]. Stable +(`@since`-gated) WASI APIs may only depend on a gated feature after the WASI +Subgroup has voted to adopt it, following the process described in +[CONTRIBUTING.md](../CONTRIBUTING.md#adopting-component-model-features). + +This document records which features have been adopted, and the release they +were adopted for. Adoption is cumulative: implementing a given WASI version +requires the default (ungated) Component Model features, plus every feature +adopted in that version and in all earlier ones. + +Adopting a feature does not mean that any WASI API uses it. It means that WASI +APIs in that release and later *may* use it, and that runtimes and toolchains +which implement that WASI version must implement the feature regardless of +whether it is used yet. + +## Adopted features + +This table is the source for the "Component Model features" section of each +released [specification overview](../specifications). + +| WASI version | Gate | Feature | Adopted | +| --- | --- | --- | --- | +| 0.2.0 | β€” | The default (ungated) Component Model features | WASI 0.2 vote (predates this process) | +| 0.3.0 | πŸ”€ | `async` lift/lower, `future` and `stream` | WASI 0.3 vote (predates this process) | +| 0.3.1 | πŸ—ΊοΈ | The `map` type | [2026-08-06](https://github.com/WebAssembly/meetings/blob/main/wasi/2026/WASI-08-06.md) ([#943](https://github.com/WebAssembly/WASI/issues/943)) | +| 0.3.1 | 🏷️ | `implements` and `external-id` annotations on plain-named interface imports and exports | [2026-08-06](https://github.com/WebAssembly/meetings/blob/main/wasi/2026/WASI-08-06.md) ([#942](https://github.com/WebAssembly/WASI/issues/942)) | + +Features which have not been adopted are listed under [gated features] in the +Component Model Explainer. WASI proposals may experiment with them in +prerelease versions (such as `0.3.0-rc-*` releases), but stable releases must +remain implementable without them. + +CI enforces this: each changed proposal is encoded to a component and validated +with the features in the table above enabled, so a proposal that depends on an +un-adopted feature fails validation. + +[Component Model]: https://github.com/WebAssembly/component-model +[gated features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features diff --git a/docs/Release.md b/docs/Release.md index 60d9d681..ec935a62 100644 --- a/docs/Release.md +++ b/docs/Release.md @@ -37,5 +37,8 @@ workflow which begin the release process. This process has the following steps: 1. Trigger the Release WASI workflow by running `gh workflow run release.yml -f prev_version=0.3. -f next_version=0.3.`. 1. A PR updating all version numbers is created. This is blocked on CI and review ([example](https://github.com/WebAssembly/WASI/pull/924)). 2. Create a GitHub release by running `gh release create "v0.3." --generate-notes`. + If the release adopts new Component Model features, the generated notes must + be edited to call them out. See [Component Model features in + WASI](ComponentModelFeatures.md). 3. Once the release has been created, a second PR will be automatically created to populate the `specifications/` dir ([example](https://github.com/WebAssembly/WASI/pull/925)). 4. Once this final PR is merged the release process is complete. diff --git a/specifications/wasi-0.3.0/Overview.md b/specifications/wasi-0.3.0/Overview.md index 3f5af04f..471571de 100644 --- a/specifications/wasi-0.3.0/Overview.md +++ b/specifications/wasi-0.3.0/Overview.md @@ -16,6 +16,21 @@ layout][wasm-oci]: - [wasi:cli@0.3.0](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fcli?tag=0.3.0) - [wasi:http@0.3.0](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fhttp?tag=0.3.0) +## Component Model features + +Implementing this version of the specification requires the default (ungated) +[Component Model][cm] features, plus the [gated features][gates] adopted by the +WASI Subgroup up to and including this version: + +| Gate | Feature | Adopted in | +| --- | --- | --- | +| πŸ”€ | `async` lift/lower, `future` and `stream` | 0.3.0 | + +These are required whether or not an API in this version uses them. See +[Component Model features in WASI][features] for the full adoption record. + [cm]: https://github.com/WebAssembly/component-model [wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md [wasm-oci]: https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact +[gates]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features +[features]: https://github.com/WebAssembly/WASI/blob/main/docs/ComponentModelFeatures.md From 9308c756d923456adada218daee733b2b87a437e Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Tue, 11 Aug 2026 13:43:45 -0400 Subject: [PATCH 2/2] Update specifications/wasi-0.3.0/Overview.md Co-authored-by: Luke Wagner --- specifications/wasi-0.3.0/Overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifications/wasi-0.3.0/Overview.md b/specifications/wasi-0.3.0/Overview.md index 471571de..63392700 100644 --- a/specifications/wasi-0.3.0/Overview.md +++ b/specifications/wasi-0.3.0/Overview.md @@ -24,7 +24,7 @@ WASI Subgroup up to and including this version: | Gate | Feature | Adopted in | | --- | --- | --- | -| πŸ”€ | `async` lift/lower, `future` and `stream` | 0.3.0 | +| πŸ”€ | `async`, `future` and `stream` | 0.3.0 | These are required whether or not an API in this version uses them. See [Component Model features in WASI][features] for the full adoption record.