diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml
index 492053c..1a63357 100644
--- a/.github/workflows/pr-gate.yml
+++ b/.github/workflows/pr-gate.yml
@@ -35,7 +35,7 @@ jobs:
if ($LASTEXITCODE) { throw 'Invalid package version.' }
"value=$version" >> $env:GITHUB_OUTPUT
managed-packages:
- needs: [allocate-version, native, secret-scan, repository-hooks, design-policy, licence-policy, provenance-policy, reference-policy]
+ needs: [allocate-version, native, secret-scan, repository-hooks, design-policy, licence-policy, provenance-policy, reference-policy, runtime-policy]
uses: ./.github/workflows/package-validation.yml
with:
version: ${{ needs.allocate-version.outputs.version }}
@@ -145,6 +145,33 @@ jobs:
if-no-files-found: error
retention-days: 30
+ runtime-policy:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
+ with:
+ global-json-file: global.json
+ - name: Reject runtime, assignment and retired-scaffold drift
+ run: python -m unittest discover -s eng -p test_runtime_ownership.py -v
+ - name: Verify current owner and immutable family snapshots
+ run: python eng/runtime_ownership.py --evaluate-managed
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
+ if: always()
+ with:
+ name: runtime-policy-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }}
+ path: artifacts/evidence/runtime-ownership.json
+ if-no-files-found: error
+ retention-days: 30
+
provenance-policy:
strategy:
fail-fast: false
@@ -176,7 +203,7 @@ jobs:
ci:
if: always()
- needs: [allocate-version, managed-packages, native, secret-scan, repository-hooks, design-policy, licence-policy, provenance-policy, reference-policy]
+ needs: [allocate-version, managed-packages, native, secret-scan, repository-hooks, design-policy, licence-policy, provenance-policy, reference-policy, runtime-policy]
runs-on: ubuntu-latest
env:
CI_NEEDS: ${{ toJSON(needs) }}
diff --git a/README.md b/README.md
index 1b34f9c..1e81aa1 100644
--- a/README.md
+++ b/README.md
@@ -108,3 +108,6 @@ There are no Git submodules. Contracts publishes independently from its own revi
[AGPL-3.0-only](LICENSE). See [NOTICE.md](NOTICE.md) and the
[dependency register](docs/compliance/third-party-license-register.md) for attribution.
+
+[Runtime and source ownership](docs/runtime-ownership.md) records the current nine-owner
+policy, bounded runtime checks and retained bootstrap/scaffold dispositions.
diff --git a/docs/platform-bootstrap.md b/docs/platform-bootstrap.md
index 49e57bb..f51cbc1 100644
--- a/docs/platform-bootstrap.md
+++ b/docs/platform-bootstrap.md
@@ -1,5 +1,9 @@
# DesktopPlatform extraction and package pipeline
+This is historical extraction evidence at the source revisions below. The current
+[runtime and ownership policy](runtime-ownership.md), reviewed on 2026-09-20, governs
+repository/runtime assignments; the old Design snapshot is not current authority.
+
## Initial extraction plan
Base: `99bfe7d695ed0d65a0d035af7d219fc9b86100f5` in `ArcForges/DesktopPlatform`.
@@ -28,7 +32,7 @@ Removal is an ownership extraction, not a claim that new product or proto implem
Shared placeholder libraries are retained for follow-on implementation but are not release artifacts.
The first publication supported build policy. The subsequent [native package release](native-package-release.md)
admits the four existing Windows ABIs with their real native closure and package-consumer validation.
-This bootstrap does not close every WP02 obligation across all ten repositories or the WP06 native gates.
+This bootstrap does not close every WP02 obligation across all nine implementation repositories or the WP06 native gates.
## Local validation evidence
diff --git a/docs/runtime-ownership.md b/docs/runtime-ownership.md
new file mode 100644
index 0000000..9a72971
--- /dev/null
+++ b/docs/runtime-ownership.md
@@ -0,0 +1,70 @@
+# Runtime and source ownership
+
+WP00.05 implements the [current Design profile](https://github.com/ArcForges/ArcForges-Design/blob/e2dd78058ce2d4bd1a8434a34d049bbc1158eacb/docs/architecture/30-runtime-and-source-ownership-policy.md).
+The registry is `eng/policy/runtime-ownership.json`. Nine implementation owners
+are independent; Design is documentation authority, and ArcChat is an embedded
+assistant/companion feature. Policy does not create another runtime package.
+
+```text
+python -m unittest discover -s eng -p test_runtime_ownership.py -v
+python eng/runtime_ownership.py --evaluate-managed
+```
+
+Default verification reads this checkout and fetches the eight other owners at
+their exact recorded commits into disposable isolated checkouts. It validates the
+fixed Design identity, real historical extraction tree, all current project and
+source inventories, declared runtime configuration, and the Contracts naming scan.
+It never builds adjacent source. Contracts and Mobile do not import AGPL tooling.
+Pass all nine `--repository Owner=absolute-root` arguments for a fresh family audit;
+`--design-root` may use a local Design Git checkout containing the bound commit.
+Reports retain actual source commit/dirty state, file/project counts, policy digest
+and findings under ignored `artifacts/evidence/`. A current family audit is required
+after merges; immutable snapshots alone cannot establish that current main conforms.
+
+The project-role inventory covers every existing build manifest, including tooling,
+tests, native IDE adapters and runtime-package containers. Source files additionally
+require the existing complete provenance assignment. Licence/reference checks remain
+independent. Registered old product trees cannot be restored into DesktopPlatform;
+the excluded Notes canvas/slides paths remain rejected even with a new inventory row.
+
+The AOT source check follows explicit local imports and nearest automatic directory
+build files, rejects false/conditional required declarations and unknown dynamic
+imports, and distinguishes libraries, hosts and build/test programs. It does not
+implement an MSBuild evaluator. `--evaluate-managed` additionally invokes the pinned
+SDK only for this owner's production hosts/libraries in Debug and Release; their
+actual evaluated property must agree. Existing native/managed package-consumer CI
+continues to prove execution. JavaScript IDE adapters cannot inherit .NET runtime
+properties. The runtime check parses project/dependency/deployment inputs, not
+arbitrary historical prose; its bounded checks do not prove whole-program behavior.
+
+The registered AI Hello Workflow requires its direct Workers AI binding, one exact
+Workflow and no alternate Container/environment. Cloud has one Container bridge,
+and Web has React static assets. The Kotlin Android bootstrap keeps its observed
+prerelease identity, JVM 21 and development-only preview. These registrations must
+be reviewed when their scheduled product work changes the actual structure.
+They never authorize another agent runtime, production Node host, iOS deliverable,
+old database provider, or unregistered project/source owner.
+
+## Reviewed claim corrections
+
+Reviewed on 2026-09-20 against the pinned Design authority. Each correction preserves
+existing Hello/probe APIs, published package identities and runtime behavior.
+
+| Owner and path | Previous claim | Accepted replacement and verification |
+|---|---|---|
+| Design WP00.05 | Ten implementation repositories | Nine owners; Design remains documentation authority. Full corpus preview and Design PR29 review. |
+| DesktopPlatform `docs/platform-bootstrap.md` | Ten-repository completion boundary; original Design pin could appear current | Nine current owners; extraction results explicitly revision-bound. Historical results preserved. |
+| DesktopPlatform ContentSandbox project comment | Four desktop heads and removed layout document | Parent-bound Native AOT helper scaffold; architecture27 and WP11/13 own complete behavior. Source property/evaluated build and existing CI checked. |
+| Contracts npm proto/API-client READMEs | React Native/Hermes remained a mobile delivery target | TypeScript Web; Android uses generated Kotlin gRPC-Web. Packaged README bytes and published candidates are checked after merge. |
+| Contracts `docs/architecture.md` | Native gRPC could appear an alternative current public business transport | Binary gRPC-Web; existing native Hello fixtures explicitly compatibility-only. No API or fixture removal. |
+| Contracts `docs/bootstrap-plan.md` | Original RN direction could escape its historical scope | Source-bound original plan explicitly reviewed as historical; results preserved, current authority linked. |
+| Cloud README/bootstrap scope | PostgreSQL remained a future target | D1 business transactions; current Hello remains stateless. Existing AOT image/real protocol and main Cloudflare checks apply. |
+| AI README | Cloud owned PostgreSQL transactions | Cloud owns D1 transactions; sole Workflow unchanged. Source/bundle tests and main real model/tool/model gate remain required. |
+
+The registry records old monorepo groups against extraction commit
+`99bfe7d695ed0d65a0d035af7d219fc9b86100f5`, together with their current owners and
+dispositions. Eleven shared placeholders remain non-packable, and the helper is
+still a scaffold. Full business schemas, assistant implementations, all native
+functional families/RIDs, four Web surfaces, Android identity/toolchain migration
+and commercial operation remain at their named stages. Existing snapshot evidence
+does not close those gates or require recreating already absent legacy source.
diff --git a/eng/policy/runtime-ownership.json b/eng/policy/runtime-ownership.json
new file mode 100644
index 0000000..a0149d7
--- /dev/null
+++ b/eng/policy/runtime-ownership.json
@@ -0,0 +1,467 @@
+{
+ "schemaVersion": 1,
+ "reviewedOn": "2026-09-20",
+ "design": {
+ "commit": "e2dd78058ce2d4bd1a8434a34d049bbc1158eacb",
+ "path": "docs/architecture/30-runtime-and-source-ownership-policy.md",
+ "sha256": "3be292a0fffc23dbdb0f7d1d0fe41ad250e03661d02fee79e4d4c58b8af33e2b"
+ },
+ "owners": [
+ {
+ "repository": "DesktopPlatform",
+ "runtime": "dotnet-nativeaot-libraries-and-native-cabi",
+ "sourceCommit": "76b40214fe018fc663dd393f812f1ecf89249cdd",
+ "projects": [
+ {
+ "path": "CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcgraphics-metal-abi/CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcmedia-ffmpeg-abi/CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcmedia-ffmpeg-abi/windows/arcmedia_ffmpeg_abi.vcxproj",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcslate-color-abi/CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcslate-color-abi/windows/arcslate_color_abi.vcxproj",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcslate-image-abi/CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcslate-image-abi/windows/arcslate_image_abi.vcxproj",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcslate-otio-abi/CMakeLists.txt",
+ "role": "native-build"
+ },
+ {
+ "path": "native/arcslate-otio-abi/windows/arcslate_otio_abi.vcxproj",
+ "role": "native-build"
+ },
+ {
+ "path": "src/Build/ArcForges.Build.Policy/ArcForges.Build.Policy.csproj",
+ "role": "package-container"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Application.Abstractions/ArcForges.Application.Abstractions.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Desktop.Experience/ArcForges.Desktop.Experience.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Desktop.Graphics/ArcForges.Desktop.Graphics.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Desktop.Preview/ArcForges.Desktop.Preview.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Desktop.RichContent/ArcForges.Desktop.RichContent.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Desktop.Text/ArcForges.Desktop.Text.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Foundation/ArcForges.Foundation.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.NativeInterop/ArcForges.NativeInterop.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Observability/ArcForges.Observability.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Persistence.Sqlite/ArcForges.Persistence.Sqlite.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/BuildingBlocks/ArcForges.Security/ArcForges.Security.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj",
+ "role": "aot-host"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Abstractions/ArcForges.Native.Abstractions.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Colour.Runtime.win-x64/ArcForges.Native.Colour.Runtime.win-x64.csproj",
+ "role": "package-container"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Colour/ArcForges.Native.Colour.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Image.Runtime.win-x64/ArcForges.Native.Image.Runtime.win-x64.csproj",
+ "role": "package-container"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Image/ArcForges.Native.Image.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Media.Runtime.win-x64/ArcForges.Native.Media.Runtime.win-x64.csproj",
+ "role": "package-container"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Media/ArcForges.Native.Media.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Otio.Runtime.win-x64/ArcForges.Native.Otio.Runtime.win-x64.csproj",
+ "role": "package-container"
+ },
+ {
+ "path": "src/Native/ArcForges.Native.Otio/ArcForges.Native.Otio.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "tests/ArchitectureTests/ArcForges.Tests.ArchitectureTests.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "tests/NativeAbiTests/ArcForges.Tests.NativeAbiTests.csproj",
+ "role": "test-or-build-tool"
+ }
+ ]
+ },
+ {
+ "repository": "Contracts",
+ "runtime": "proto-generated-clients",
+ "sourceCommit": "d716045854e2e401a1a536ba2ee5c62ad1c4e8ab",
+ "projects": [
+ {
+ "path": "build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "eng/Codegen/Codegen.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "eng/central-signing/build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "package.json",
+ "role": "typescript-build"
+ },
+ {
+ "path": "src/public/dotnet/ArcForges.Contracts.PublicApi/ArcForges.Contracts.PublicApi.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/public/kotlin/contract-fixtures/build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "src/public/kotlin/contracts-client/build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "src/public/kotlin/contracts-connect-client/build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "src/public/kotlin/contracts-proto/build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "src/public/ts/api-client/package.json",
+ "role": "typescript-build"
+ },
+ {
+ "path": "src/public/ts/proto/package.json",
+ "role": "typescript-build"
+ },
+ {
+ "path": "tests/public/HelloClient/HelloClient.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "tests/public/HelloHost/HelloHost.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "tests/public/KotlinClient/build.gradle.kts",
+ "role": "contract-or-test-build"
+ },
+ {
+ "path": "tests/public/KotlinConnectClient/build.gradle.kts",
+ "role": "contract-or-test-build"
+ }
+ ]
+ },
+ {
+ "repository": "ArcNotes",
+ "runtime": "avalonia-nativeaot",
+ "sourceCommit": "e40423a1b14ce8341de35748cc2a093c7c9b77a7",
+ "projects": [
+ {
+ "path": "eng/ArcForges.Repository/ArcForges.Repository.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "src/ArcForges.ArcNotes.Core/ArcForges.ArcNotes.Core.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/ArcForges.ArcNotes/ArcForges.ArcNotes.csproj",
+ "role": "aot-host"
+ },
+ {
+ "path": "tests/ArcForges.ArcNotes.Tests/ArcForges.ArcNotes.Tests.csproj",
+ "role": "test-or-build-tool"
+ }
+ ]
+ },
+ {
+ "repository": "ArcScope",
+ "runtime": "avalonia-nativeaot",
+ "sourceCommit": "d247dcff36fd1123a70e5e59967a9b2294a2eeac",
+ "projects": [
+ {
+ "path": "eng/ArcForges.Repository/ArcForges.Repository.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "src/ArcForges.ArcScope.Core/ArcForges.ArcScope.Core.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/ArcForges.ArcScope/ArcForges.ArcScope.csproj",
+ "role": "aot-host"
+ },
+ {
+ "path": "tests/ArcForges.ArcScope.Tests/ArcForges.ArcScope.Tests.csproj",
+ "role": "test-or-build-tool"
+ }
+ ]
+ },
+ {
+ "repository": "ArcSlate",
+ "runtime": "avalonia-nativeaot",
+ "sourceCommit": "b0d255f54fb560a534cc493d5645ca4bc7b4bd0e",
+ "projects": [
+ {
+ "path": "eng/ArcForges.Repository/ArcForges.Repository.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "src/ArcForges.ArcSlate.Core/ArcForges.ArcSlate.Core.csproj",
+ "role": "aot-library"
+ },
+ {
+ "path": "src/ArcForges.ArcSlate/ArcForges.ArcSlate.csproj",
+ "role": "aot-host"
+ },
+ {
+ "path": "tests/ArcForges.ArcSlate.Tests/ArcForges.ArcSlate.Tests.csproj",
+ "role": "test-or-build-tool"
+ }
+ ]
+ },
+ {
+ "repository": "Cloud",
+ "runtime": "dotnet-nativeaot-cloudflare-container",
+ "sourceCommit": "4571ec8692235485712d4c4e3ef4886c162f2574",
+ "projects": [
+ {
+ "path": "package.json",
+ "role": "typescript-build"
+ },
+ {
+ "path": "src/ArcForges.Cloud/ArcForges.Cloud.csproj",
+ "role": "aot-host"
+ },
+ {
+ "path": "tests/ArcForges.Cloud.Consumer/ArcForges.Cloud.Consumer.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "tests/ArcForges.Cloud.Tests/ArcForges.Cloud.Tests.csproj",
+ "role": "test-or-build-tool"
+ },
+ {
+ "path": "tests/kotlin-consumer/build.gradle.kts",
+ "role": "contract-or-test-build"
+ }
+ ]
+ },
+ {
+ "repository": "AI",
+ "runtime": "cloudflare-workflow-workers-ai",
+ "sourceCommit": "944edfe88718fc5f72a42ea0bed1c495307c22df",
+ "projects": [
+ {
+ "path": "package.json",
+ "role": "typescript-build"
+ }
+ ]
+ },
+ {
+ "repository": "Web",
+ "runtime": "react-typescript-static",
+ "sourceCommit": "84939ca1fde0f0653d2cb4d8b8f9e5dd1057abb5",
+ "projects": [
+ {
+ "path": "ArcForges.Web.esproj",
+ "role": "javascript-ide"
+ },
+ {
+ "path": "apps/site/package.json",
+ "role": "typescript-build"
+ },
+ {
+ "path": "package.json",
+ "role": "typescript-build"
+ },
+ {
+ "path": "packages/ui/package.json",
+ "role": "typescript-build"
+ }
+ ]
+ },
+ {
+ "repository": "Mobile",
+ "runtime": "kotlin-compose-android",
+ "sourceCommit": "5031d837d2e7bf9dd1b681c837c942d2b74dc65e",
+ "projects": [
+ {
+ "path": "app/build.gradle.kts",
+ "role": "android-build"
+ },
+ {
+ "path": "build.gradle.kts",
+ "role": "android-build"
+ },
+ {
+ "path": "shared/build.gradle.kts",
+ "role": "android-build"
+ }
+ ]
+ }
+ ],
+ "retiredScaffolds": [
+ {
+ "path": "src/ArcChat",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "retired",
+ "owner": "DesktopPlatform",
+ "reason": "Standalone host retired; reusable embedded assistant features remain assigned to WP14-17."
+ },
+ {
+ "path": "src/ArcNotes",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "extracted",
+ "owner": "ArcNotes",
+ "reason": "Independent native product. Edgeless and Slides scaffolds are excluded, without a future hook."
+ },
+ {
+ "path": "src/ArcScope",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "extracted",
+ "owner": "ArcScope",
+ "reason": "Independent acquisition/measurement product; no product source in shared platform."
+ },
+ {
+ "path": "src/ArcSlate",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "extracted",
+ "owner": "ArcSlate",
+ "reason": "Independent media/timeline product; native mechanism packages remain platform-owned."
+ },
+ {
+ "path": "src/Cloud",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "replaced",
+ "owner": "Cloud",
+ "reason": "Native AOT Container with D1/R2 authority; no old JIT/database or in-host Harness placement."
+ },
+ {
+ "path": "src/Mobile",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "replaced",
+ "owner": "Mobile",
+ "reason": "Kotlin Android replaces .NET/RN delivery. Existing prerelease identity and JVM development preview remain until WP30."
+ },
+ {
+ "path": "src/Web",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "replaced",
+ "owner": "Web",
+ "reason": "React/TypeScript browser outputs replace .NET browser host."
+ },
+ {
+ "path": "src/Contracts",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "extracted",
+ "owner": "Contracts",
+ "reason": "Authored proto and generated clients replace old authored JSON DTO wire authority; Hello compatibility retained."
+ },
+ {
+ "path": "src/SDK",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "extracted",
+ "owner": "Contracts",
+ "reason": "Public SDK/CLI owner; full behavior remains WP03 and subsequent consumer gates."
+ },
+ {
+ "path": "src/Extensions",
+ "observedRepository": "DesktopPlatform",
+ "sourceCommit": "99bfe7d695ed0d65a0d035af7d219fc9b86100f5",
+ "disposition": "extracted",
+ "owner": "DesktopPlatform",
+ "reason": "Shared sandbox/extension mechanisms; catalogue business authority remains Cloud under WP41."
+ }
+ ],
+ "bootstrapBoundaries": {
+ "DesktopPlatform": "Eleven non-packable shared placeholders and ContentSandbox scaffold are not capability acceptance; native packages expose existing probes. WP01/04/07-17/53 retain their full obligations.",
+ "Contracts": "Hello schema and native-gRPC compatibility fixtures retained. Public business clients select binary gRPC-Web; WP03 completes production schemas and retires native-only Kotlin client at the first business release.",
+ "ArcNotes": "Hello native UI/transport only. Five-RID bootstrap is not full platform or Notes behavior acceptance.",
+ "ArcScope": "Hello native UI/transport only. Five-RID bootstrap is not full platform or acquisition behavior acceptance.",
+ "ArcSlate": "Hello native UI/transport only. Five-RID bootstrap is not full platform or media behavior acceptance.",
+ "Cloud": "Hello anonymous stateless AOT service. Full D1 modules and Container production profile remain WP21 onward.",
+ "AI": "Private Workflow model/tool/model Hello. Full sole production Harness remains WP52.",
+ "Web": "Site Hello only; account/chat/operations remain required at their scheduled producers.",
+ "Mobile": "Android Hello with io.github.arcforges.mobile; com.arcforges.mobile migration and stable toolchain reconciliation remain WP30 before production. JVM preview is development-only."
+ }
+}
diff --git a/eng/provenance/files.json b/eng/provenance/files.json
index f406011..98480b0 100644
--- a/eng/provenance/files.json
+++ b/eng/provenance/files.json
@@ -43,6 +43,7 @@
"docs/platform-bootstrap.md",
"docs/provenance.md",
"docs/reference-baselines.md",
+ "docs/runtime-ownership.md",
"eng/build/desktop-aot.props",
"eng/build/desktop-rids.props",
"eng/cmake/LicenceBoundary.cmake",
@@ -65,6 +66,7 @@
"eng/policy/licence-boundary.json",
"eng/policy/reference-baselines.json",
"eng/policy/reuse-policy.json",
+ "eng/policy/runtime-ownership.json",
"eng/provenance/NOTICE.txt",
"eng/provenance/artifact-profiles/native-win-x64-r1.json",
"eng/provenance/artifact-profiles/native-win-x64-r2.json",
@@ -188,9 +190,11 @@
"eng/provenance/reference-inputs.json",
"eng/provenance/template.json",
"eng/reference_baselines.py",
+ "eng/runtime_ownership.py",
"eng/test_design_policy.py",
"eng/test_licence_boundary.py",
"eng/test_reference_baselines.py",
+ "eng/test_runtime_ownership.py",
"global.json",
"native/CMakeLists.txt",
"native/arcgraphics-metal-abi/CMakeLists.txt",
diff --git a/eng/runtime_ownership.py b/eng/runtime_ownership.py
new file mode 100644
index 0000000..37bee09
--- /dev/null
+++ b/eng/runtime_ownership.py
@@ -0,0 +1,373 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+"""WP00.05 read-only runtime/source audit; not product or deployment acceptance."""
+
+from __future__ import annotations
+
+import argparse
+from datetime import date, datetime, timezone
+import hashlib
+import importlib.util
+import json
+from pathlib import Path, PurePosixPath
+import re
+import subprocess
+import sys
+import tempfile
+import xml.etree.ElementTree as ET
+
+import licence_boundary as inventory
+
+ROOT = Path(__file__).resolve().parents[1]
+POLICY = 'eng/policy/runtime-ownership.json'
+RUNTIMES = {
+ 'DesktopPlatform': 'dotnet-nativeaot-libraries-and-native-cabi',
+ 'Contracts': 'proto-generated-clients',
+ 'ArcNotes': 'avalonia-nativeaot', 'ArcScope': 'avalonia-nativeaot', 'ArcSlate': 'avalonia-nativeaot',
+ 'Cloud': 'dotnet-nativeaot-cloudflare-container', 'AI': 'cloudflare-workflow-workers-ai',
+ 'Web': 'react-typescript-static', 'Mobile': 'kotlin-compose-android',
+}
+RETIRED = {'ArcChat': ('retired', 'DesktopPlatform'), 'ArcNotes': ('extracted', 'ArcNotes'),
+ 'ArcScope': ('extracted', 'ArcScope'), 'ArcSlate': ('extracted', 'ArcSlate'),
+ 'Cloud': ('replaced', 'Cloud'), 'Mobile': ('replaced', 'Mobile'), 'Web': ('replaced', 'Web'),
+ 'Contracts': ('extracted', 'Contracts'), 'SDK': ('extracted', 'Contracts'),
+ 'Extensions': ('extracted', 'DesktopPlatform')}
+TRUE = {'PublishAot', 'IsAotCompatible'}
+HOSTS = {'DesktopPlatform': 'src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj',
+ 'Cloud': 'src/ArcForges.Cloud/ArcForges.Cloud.csproj',
+ **{name: f'src/ArcForges.{name}/ArcForges.{name}.csproj' for name in ('ArcNotes', 'ArcScope', 'ArcSlate')}}
+
+
+def require(value, message):
+ if not value:
+ raise ValueError(message)
+
+
+def fields(value, names):
+ require(isinstance(value, dict) and set(value) == set(names.split()), 'invalid schema fields')
+
+
+def path(value):
+ require(isinstance(value, str) and value and '\\' not in value and ':' not in value,
+ 'invalid policy path')
+ p = PurePosixPath(value)
+ require(not p.is_absolute() and '..' not in p.parts and value == p.as_posix(), 'unsafe policy path')
+ return value
+
+
+def digest(value, length=40):
+ require(isinstance(value, str) and re.fullmatch('[0-9a-f]{' + str(length) + '}', value), 'invalid digest')
+
+
+def document(root, relative):
+ return json.loads(inventory.read(root, path(relative)), object_pairs_hook=inventory.unique)
+
+
+def role(owner, relative):
+ if relative.endswith('.csproj'):
+ if relative.startswith(('tests/', 'eng/')):
+ return 'test-or-build-tool'
+ require(relative.startswith('src/'), 'unassigned managed source project')
+ if '/Build/' in relative or '.Runtime.' in relative:
+ require(owner == 'DesktopPlatform', 'unassigned runtime package')
+ return 'package-container'
+ if owner == 'Cloud' or (owner in {'ArcNotes', 'ArcScope', 'ArcSlate'} and '.Core/' not in relative) or '/DesktopHelpers/' in relative:
+ require(HOSTS.get(owner) == relative, 'unassigned runtime host')
+ return 'aot-host'
+ require(owner in {'DesktopPlatform', 'Contracts', 'ArcNotes', 'ArcScope', 'ArcSlate'}, 'unassigned managed runtime')
+ return 'aot-library'
+ if relative.endswith('.esproj'):
+ require(owner == 'Web', 'unassigned JavaScript IDE project')
+ return 'javascript-ide'
+ if relative.endswith('.vcxproj') or relative.endswith('CMakeLists.txt'):
+ require(owner == 'DesktopPlatform', 'native build outside its owner')
+ return 'native-build'
+ if relative.endswith('package.json'):
+ require(owner in {'Contracts', 'Cloud', 'AI', 'Web'}, 'unassigned npm runtime')
+ return 'typescript-build'
+ if relative.endswith('build.gradle.kts'):
+ require(owner in {'Contracts', 'Cloud', 'Mobile'}, 'unassigned Gradle runtime')
+ return 'android-build' if owner == 'Mobile' else 'contract-or-test-build'
+ raise ValueError('unassigned project kind: ' + relative)
+
+
+def validate_policy(value):
+ fields(value, 'schemaVersion reviewedOn design owners retiredScaffolds bootstrapBoundaries')
+ require(type(value['schemaVersion']) is int and value['schemaVersion'] == 1, 'invalid schema version')
+ require(date.fromisoformat(value['reviewedOn']).isoformat() == value['reviewedOn'], 'invalid review date')
+ fields(value['design'], 'commit path sha256')
+ digest(value['design']['commit'])
+ digest(value['design']['sha256'], 64)
+ require(value['design']['path'] == 'docs/architecture/30-runtime-and-source-ownership-policy.md', 'wrong Design authority')
+ require(isinstance(value['owners'], list), 'invalid owners')
+ seen = set()
+ for row in value['owners']:
+ fields(row, 'repository runtime sourceCommit projects')
+ owner = row['repository']
+ require(owner in RUNTIMES and owner not in seen, 'unknown or duplicate owner')
+ seen.add(owner)
+ require(row['runtime'] == RUNTIMES[owner], 'runtime selection changed: ' + owner)
+ digest(row['sourceCommit'])
+ require(isinstance(row['projects'], list) and row['projects'], 'missing project ownership')
+ projects = set()
+ for project in row['projects']:
+ fields(project, 'path role')
+ relative = path(project['path'])
+ require(relative not in projects, 'duplicate project')
+ projects.add(relative)
+ require(project['role'] == role(owner, relative), 'project role does not match owner')
+ require(seen == set(RUNTIMES), 'missing repository owner')
+ require(isinstance(value['bootstrapBoundaries'], dict) and set(value['bootstrapBoundaries']) == seen,
+ 'missing bootstrap boundary')
+ require(all(isinstance(x, str) and x.strip() for x in value['bootstrapBoundaries'].values()), 'empty bootstrap boundary')
+ require(isinstance(value['retiredScaffolds'], list), 'missing retired dispositions')
+ seen = set()
+ for row in value['retiredScaffolds']:
+ fields(row, 'path observedRepository sourceCommit disposition owner reason')
+ relative = path(row['path'])
+ group = relative.removeprefix('src/')
+ require(relative == 'src/' + group and group in RETIRED and group not in seen, 'invalid retired path')
+ seen.add(group)
+ require((row['disposition'], row['owner']) == RETIRED[group], 'retired owner/disposition changed')
+ require(row['observedRepository'] == 'DesktopPlatform' and row['sourceCommit'] == '99bfe7d695ed0d65a0d035af7d219fc9b86100f5',
+ 'historical extraction identity changed')
+ require(isinstance(row['reason'], str) and row['reason'].strip(), 'missing disposition reason')
+ require(seen == set(RETIRED), 'missing historical disposition')
+
+
+def managed_inputs(root, project, files):
+ """Walk explicit local imports and nearest automatic directory build inputs.
+
+ Conditions cannot make required AOT declarations optional. This intentionally
+ rejects unsupported dynamic imports rather than claiming to evaluate MSBuild.
+ """
+ result = []
+ visited = set()
+
+ def visit(relative, conditional=False):
+ identity = (relative, conditional)
+ require(relative in files, 'unregistered build input: ' + relative)
+ if identity in visited:
+ return
+ visited.add(identity)
+ tree = ET.fromstring(inventory.read(root, relative))
+ require(tree.tag == 'Project', 'unexpected MSBuild namespace/root')
+
+ def walk(element, condition):
+ condition = condition or bool(element.get('Condition')) or element.tag in {'Choose', 'When', 'Otherwise'}
+ result.append((relative, element, condition))
+ if element.tag == 'Import':
+ target = element.get('Project', '')
+ require(target and not any(c in target for c in '$@*?;'), 'dynamic import requires review: ' + relative)
+ resolved = (root / PurePosixPath(relative).parent / target.replace('\\', '/')).resolve()
+ require(resolved.is_relative_to(root.resolve()), 'build import escapes owner')
+ visit(resolved.relative_to(root.resolve()).as_posix(), condition)
+ for child in element:
+ walk(child, condition)
+ walk(tree, conditional)
+
+ for name in ('Directory.Build.props', 'Directory.Build.targets'):
+ parent = PurePosixPath(project).parent
+ while True:
+ candidate = (parent / name).as_posix()
+ if candidate in files:
+ visit(candidate)
+ break
+ if parent == PurePosixPath('.'):
+ break
+ parent = parent.parent
+ visit(project)
+ return result
+
+
+def check_managed(root, project, selected_role, files):
+ rows = managed_inputs(root, project, files)
+ needed = {'PublishAot'} if selected_role == 'aot-host' else {'IsAotCompatible'} if selected_role == 'aot-library' else set()
+ observed = set()
+ framework = False
+ for relative, element, conditional in rows:
+ if selected_role in {'aot-host', 'aot-library'} and element.tag in {'TargetFramework', 'TargetFrameworks'}:
+ require(element.tag == 'TargetFramework' and (element.text or '').strip() == 'net10.0' and not conditional,
+ 'unregistered managed framework: ' + project)
+ framework = True
+ if selected_role == 'aot-host' and element.tag == 'IsAotCompatible':
+ require((element.text or '').strip().lower() == 'true' and not conditional, 'host AOT compatibility override')
+ if element.tag in needed:
+ require((element.text or '').strip().lower() == 'true' and not conditional,
+ 'required AOT property absent, false or conditional: ' + project + ': ' + element.tag)
+ observed.add(element.tag)
+ if selected_role == 'aot-library' and element.tag == 'OutputType':
+ require((element.text or '').strip().lower() == 'library', 'unassigned executable: ' + project)
+ if selected_role == 'javascript-ide':
+ require(element.tag not in TRUE | {'TargetFramework', 'TargetFrameworks'}, 'IDE adapter inherited managed runtime')
+ require(observed == needed, 'missing AOT declaration: ' + project)
+ require(not needed or framework, 'missing managed framework: ' + project)
+ return {'path': project, 'role': selected_role, 'requiredProperties': sorted(needed),
+ 'sourceInputs': sorted({x[0] for x in rows})}
+
+
+def check_runtime_configuration(root, owner, files):
+ for relative in files:
+ p = PurePosixPath(relative)
+ if p.suffix in {'.csproj', '.props', '.targets', '.esproj'}:
+ tree = ET.fromstring(inventory.read(root, relative))
+ require('BlazorWebAssembly' not in tree.get('Sdk', ''), 'retired browser runtime')
+ for element in tree.iter():
+ if element.tag in {'UseMaui', 'UseWPF', 'UseWindowsForms'}:
+ require((element.text or '').strip().lower() == 'false', 'retired UI runtime')
+ if element.tag in {'PackageReference', 'PackageVersion'}:
+ name = (element.get('Include') or element.get('Update') or '').lower()
+ require(not name.startswith(('reactnative', 'electronnet', 'microsoft.aspnetcore.components.webassembly')),
+ 'retired dependency: ' + name)
+ if owner == 'Cloud':
+ require(not name.startswith(('npgsql', 'microsoft.entityframeworkcore.sqlserver', 'mysql')),
+ 'retired Cloud database provider: ' + name)
+ elif p.name == 'package.json':
+ package = document(root, relative)
+ for section in ('dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'):
+ for name, version in package.get(section, {}).items():
+ names = [name]
+ if str(version).startswith('npm:'):
+ alias = str(version)[4:]
+ end = alias.find('@', 1)
+ names.append(alias if end < 0 else alias[:end])
+ for dependency in names:
+ require(not dependency.lower().startswith(('react-native', '@react-native/', 'hermes-engine', 'electron', 'ollama')),
+ 'retired runtime dependency: ' + dependency)
+ elif p.name in {'build.gradle.kts', 'build.gradle', 'settings.gradle.kts', 'settings.gradle'} and owner == 'Mobile':
+ text = inventory.read(root, relative)
+ text = re.sub(r'/\*.*?\*/|//[^\n]*', '', text, flags=re.S)
+ require(not re.search(r'\b(?:iosArm64|iosX64|iosSimulatorArm64|macosArm64|macosX64)\s*\(', text), 'unsupported Mobile product target')
+ require('com.facebook.react' not in text and 'nativeDistributions' not in text, 'retired Mobile runtime/distribution')
+ if owner in {'AI', 'Cloud', 'Web'}:
+ config = document(root, 'wrangler.json')
+ require(not config.get('env'), 'unregistered runtime environment override')
+ if owner == 'AI':
+ require(config.get('ai', {}).get('binding') == 'AI', 'missing direct Workers AI binding')
+ workflows = config.get('workflows')
+ require(isinstance(workflows, list) and len(workflows) == 1 and workflows[0] == {
+ 'name': 'arcforges-ai-hello', 'binding': 'HELLO_AGENT', 'class_name': 'HelloAgentWorkflow'},
+ 'missing or extra model-loop Workflow binding')
+ require(config.get('main') == 'src/index.ts' and not config.get('containers'), 'incorrect AI host')
+ else:
+ require(not config.get('ai') and not config.get('workflows'), 'AI runtime outside its owner')
+ if owner == 'Cloud':
+ require(config.get('main') == 'worker/index.ts' and len(config.get('containers', [])) == 1,
+ 'missing or extra Cloud host')
+ require(config['containers'][0].get('class_name') == 'CloudContainer', 'incorrect Cloud Container owner')
+ else:
+ require(not config.get('main') and not config.get('containers') and config.get('assets', {}).get('directory') == './apps/site/build/client',
+ 'Web must deploy its static browser candidate')
+ require('react' in document(root, 'apps/site/package.json').get('dependencies', {}), 'missing React browser runtime')
+ if owner == 'Mobile':
+ app = inventory.read(root, 'app/build.gradle.kts')
+ app = re.sub(r'/\*.*?\*/|//[^\n]*', ' ', app, flags=re.S)
+ require(re.search(r'\bapplicationId\s*=\s*"io.github.arcforges.mobile"', app), 'prerelease identity changed before WP30')
+ require('libs.plugins.compose.compiler' in app and re.search(r'\bcompose\s*=\s*true', app), 'missing Android Compose runtime')
+ require('JavaVersion.VERSION_21' in app and 'JvmTarget.JVM_21' in app, 'Android compiler target changed')
+
+
+def audit(root, row):
+ owner = row['repository']
+ files = inventory.inventory(root)
+ before = {'commit': inventory.git(root, 'rev-parse', 'HEAD'), 'dirty': bool(inventory.git(root, 'status', '--porcelain'))}
+ require(inventory.git(root, 'remote', 'get-url', 'origin').removesuffix('.git') == 'https://github.com/ArcForges/' + owner,
+ 'incorrect repository origin: ' + owner)
+ declared = document(root, 'eng/provenance/files.json')
+ require(declared.get('repository') == owner and len(declared['firstParty']) == len(set(declared['firstParty'])), 'invalid source owner')
+ require(not set(declared['firstParty']) & set(declared['reused']), 'duplicate source assignment')
+ require(set(files) == set(declared['firstParty']) | set(declared['reused']), 'unassigned or stale source inventory: ' + owner)
+ licence = inventory.audit(root, owner)
+ require({p['path'] for p in licence['projects']} == {p['path'] for p in row['projects']}, 'unassigned or stale project: ' + owner)
+ if owner == 'DesktopPlatform':
+ require(not any(p.startswith(tuple('src/' + x + '/' for x in RETIRED)) for p in files), 'retired scaffold restored')
+ require(not any('/ArcNotes.Edgeless/' in '/' + p or '/ArcNotes.Slides/' in '/' + p for p in files), 'excluded Notes scaffold restored')
+ managed = []
+ for project in row['projects']:
+ if project['path'].endswith(('.csproj', '.esproj')):
+ managed.append(check_managed(root, project['path'], project['role'], set(files)))
+ check_runtime_configuration(root, owner, files)
+ require(before == {'commit': inventory.git(root, 'rev-parse', 'HEAD'), 'dirty': bool(inventory.git(root, 'status', '--porcelain'))}, 'source state changed during audit')
+ return {'repository': owner, **before, 'files': len(files), 'projects': len(row['projects']),
+ 'runtime': row['runtime'], 'managedSourceChecks': managed, 'result': 'passed'}
+
+
+def checkout(parent, owner, commit):
+ target = parent / owner
+ subprocess.run(['git', 'init', '-q', str(target)], check=True)
+ subprocess.run(['git', '-C', str(target), 'remote', 'add', 'origin', 'https://github.com/ArcForges/' + owner + '.git'], check=True)
+ subprocess.run(['git', '-C', str(target), '-c', 'core.autocrlf=false', 'fetch', '--quiet', '--depth=1', 'origin', commit], check=True)
+ subprocess.run(['git', '-C', str(target), '-c', 'core.autocrlf=false', 'checkout', '--quiet', '--detach', 'FETCH_HEAD'], check=True)
+ require(inventory.git(target, 'rev-parse', 'HEAD') == commit, 'fetched snapshot identity mismatch')
+ return target
+
+
+def naming(roots):
+ source = roots['Contracts'] / 'eng/check_naming.py'
+ spec = importlib.util.spec_from_file_location('owned_naming', source)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ policy = module.load_policy()
+ return [module.scan_repository(root, owner, policy) for owner, root in roots.items()]
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--repository', action='append', help='Owner=absolute-root; supply all nine for fresh family audit')
+ parser.add_argument('--design-root', type=Path)
+ parser.add_argument('--evaluate-managed', action='store_true', help='evaluate only this DesktopPlatform checkout')
+ parser.add_argument('--report', type=Path, default=ROOT / 'artifacts/evidence/runtime-ownership.json')
+ args = parser.parse_args()
+ result = {'substep': 'WP00.05', 'checkedAt': datetime.now(timezone.utc).isoformat(),
+ 'evidenceClass': 'source-runtime-and-ownership-policy', 'repositories': [],
+ 'limitations': 'No product, deployment, store or commercial completion inferred; evaluated builds and real candidates are separate evidence.'}
+ try:
+ value = document(ROOT, POLICY)
+ validate_policy(value)
+ result['policySha256'] = hashlib.sha256(inventory.read(ROOT, POLICY).encode()).hexdigest()
+ with tempfile.TemporaryDirectory(prefix='arcforges-runtime-') as directory:
+ temporary = Path(directory)
+ design = args.design_root.resolve() if args.design_root else checkout(temporary, 'ArcForges-Design', value['design']['commit'])
+ body = subprocess.check_output(['git', '-C', str(design), 'show', value['design']['commit'] + ':' + value['design']['path']])
+ require(hashlib.sha256(body.replace(b'\r\n', b'\n')).hexdigest() == value['design']['sha256'], 'Design identity mismatch')
+ roots = {}
+ if args.repository:
+ for entry in args.repository:
+ owner, separator, location = entry.partition('=')
+ require(separator and owner in RUNTIMES and owner not in roots and Path(location).is_absolute(), 'invalid selected root')
+ roots[owner] = Path(location).resolve()
+ require(set(roots) == set(RUNTIMES), 'fresh audit requires all nine roots')
+ else:
+ roots = {row['repository']: ROOT if row['repository'] == 'DesktopPlatform' else checkout(temporary, row['repository'], row['sourceCommit']) for row in value['owners']}
+ historical = value['retiredScaffolds'][0]['sourceCommit']
+ tree = set(inventory.git(roots['DesktopPlatform'], 'ls-tree', '--name-only', historical, 'src/').splitlines())
+ require({'src/' + group for group in RETIRED} <= tree, 'historical disposition source does not resolve')
+ result['historicalDispositionSource'] = historical
+ for row in value['owners']:
+ result['repositories'].append(audit(roots[row['repository']], row))
+ result['naming'] = naming(roots)
+ require(all(not row['findings'] for row in result['naming']), 'naming policy failed')
+ if args.evaluate_managed:
+ managed = []
+ row = next(x for x in value['owners'] if x['repository'] == 'DesktopPlatform')
+ for project in row['projects']:
+ if project['role'] not in {'aot-host', 'aot-library'}:
+ continue
+ for configuration in ('Debug', 'Release'):
+ prop = 'PublishAot' if project['role'] == 'aot-host' else 'IsAotCompatible'
+ output = subprocess.check_output(['dotnet', 'msbuild', project['path'], '-getProperty:' + prop, '-p:Configuration=' + configuration, '-verbosity:quiet'], cwd=ROOT, text=True).strip()
+ require(output.lower() == 'true', 'evaluated runtime mismatch: ' + project['path'])
+ managed.append({'path': project['path'], 'configuration': configuration, prop: output})
+ result['evaluatedManaged'] = managed
+ result['result'] = 'passed'
+ code = 0
+ except (ValueError, OSError, KeyError, TypeError, ET.ParseError, subprocess.CalledProcessError) as error:
+ result.update(result='failed', error=str(error))
+ code = 1
+ args.report.parent.mkdir(parents=True, exist_ok=True)
+ args.report.write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
+ print(json.dumps({'result': result['result'], 'repositories': len(result['repositories']), 'error': result.get('error')}))
+ return code
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/eng/test_runtime_ownership.py b/eng/test_runtime_ownership.py
new file mode 100644
index 0000000..ae3fb25
--- /dev/null
+++ b/eng/test_runtime_ownership.py
@@ -0,0 +1,196 @@
+# SPDX-License-Identifier: AGPL-3.0-only
+"""Independent invalid runtime/ownership fixtures; no provider or model calls."""
+
+import copy
+import json
+from pathlib import Path
+import subprocess
+import tempfile
+import unittest
+
+import runtime_ownership as policy
+
+
+class RuntimeOwnershipTests(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix='arcforges-runtime-test-')
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name).resolve()
+ self.files = set()
+
+ def write(self, path, value):
+ target = self.root / path
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_text(value if isinstance(value, str) else json.dumps(value), encoding='utf-8')
+ self.files.add(path)
+
+ def check(self, owner):
+ policy.check_runtime_configuration(self.root, owner, self.files)
+
+ def test_registry_rejects_missing_duplicate_and_extra_owner(self):
+ original = policy.document(policy.ROOT, policy.POLICY)
+ policy.validate_policy(original)
+ for mutation in ('missing', 'duplicate', 'unknown', 'runtime', 'role', 'digest', 'field', 'retired'):
+ with self.subTest(mutation=mutation):
+ value = copy.deepcopy(original)
+ if mutation == 'missing': value['owners'].pop()
+ if mutation == 'duplicate': value['owners'].append(value['owners'][0])
+ if mutation == 'unknown': value['owners'][0]['repository'] = 'ArcChat'
+ if mutation == 'runtime': value['owners'][0]['runtime'] = 'managed-jit'
+ if mutation == 'role': value['owners'][0]['projects'][0]['role'] = 'test-or-build-tool'
+ if mutation == 'digest': value['design']['commit'] = 'main'
+ if mutation == 'field': value['allowEverything'] = True
+ if mutation == 'retired': value['retiredScaffolds'].pop()
+ with self.assertRaises(ValueError): policy.validate_policy(value)
+
+ def test_unsafe_policy_paths_and_ambiguous_json(self):
+ for path in ('../outside', '/absolute', 'C:/absolute', 'src\\app', 'a/../b', './src'):
+ with self.subTest(path=path), self.assertRaises(ValueError): policy.path(path)
+ self.write('duplicate.json', '{"owner":1,"owner":2}')
+ with self.assertRaises(ValueError): policy.document(self.root, 'duplicate.json')
+
+ def test_complete_aot_host_then_false_missing_and_conditional_values(self):
+ project = 'src/Host/Host.csproj'
+ for properties, passes in [('true', True), ('', False),
+ ('false', False),
+ ('true', False),
+ ('truefalse', False)]:
+ self.write(project, 'net10.0' + properties + '')
+ if passes: policy.check_managed(self.root, project, 'aot-host', self.files)
+ else:
+ with self.assertRaises(ValueError): policy.check_managed(self.root, project, 'aot-host', self.files)
+
+ def test_false_override_in_automatic_targets_is_not_hidden_by_true_project(self):
+ self.write('src/Host.csproj', 'true')
+ self.write('Directory.Build.targets', 'false')
+ with self.assertRaisesRegex(ValueError, 'AOT property'):
+ policy.check_managed(self.root, 'src/Host.csproj', 'aot-host', self.files)
+
+ def test_imports_are_followed_and_cannot_escape_or_be_conditional(self):
+ self.write('aot.props', 'net10.0true')
+ self.write('src/Host.csproj', '')
+ policy.check_managed(self.root, 'src/Host.csproj', 'aot-host', self.files)
+ for imported in ('../../outside.props', '$(External)/aot.props', 'missing.props'):
+ self.write('src/Host.csproj', '')
+ with self.assertRaises(ValueError): policy.check_managed(self.root, 'src/Host.csproj', 'aot-host', self.files)
+ self.write('src/Host.csproj', '')
+ with self.assertRaises(ValueError): policy.check_managed(self.root, 'src/Host.csproj', 'aot-host', self.files)
+
+ def test_library_cannot_become_unassigned_executable(self):
+ self.write('Library.csproj', 'trueExe')
+ with self.assertRaisesRegex(ValueError, 'unassigned executable'):
+ policy.check_managed(self.root, 'Library.csproj', 'aot-library', self.files)
+ with self.assertRaisesRegex(ValueError, 'unassigned runtime host'):
+ policy.role('Cloud', 'src/AnotherHost/AnotherHost.csproj')
+
+ def test_javascript_ide_never_inherits_managed_runtime(self):
+ self.write('Web.esproj', 'true')
+ with self.assertRaisesRegex(ValueError, 'IDE adapter'):
+ policy.check_managed(self.root, 'Web.esproj', 'javascript-ide', self.files)
+
+ def test_old_ui_and_database_manifests_fail(self):
+ for xml, owner in [('', 'Web'),
+ ('true', 'ArcNotes'),
+ ('', 'Cloud')]:
+ self.write('app.csproj', xml)
+ with self.assertRaisesRegex(ValueError, 'retired'): self.check(owner)
+
+ def test_obsolete_npm_dependencies_include_aliases(self):
+ for name, version in [('react-native', '1.0.0'), ('alias', 'npm:react-native@1.0.0'),
+ ('alias', 'npm:@react-native/runtime'), ('hermes-engine', '1.0.0'), ('electron', '1.0.0')]:
+ self.write('package.json', {'dependencies': {name: version}})
+ with self.assertRaisesRegex(ValueError, 'retired runtime'): self.check('AI')
+
+ def workflow(self):
+ return {'main': 'src/index.ts', 'ai': {'binding': 'AI'}, 'workflows': [
+ {'name': 'arcforges-ai-hello', 'binding': 'HELLO_AGENT', 'class_name': 'HelloAgentWorkflow'}]}
+
+ def test_exact_workflow_binding_and_direct_ai_are_required(self):
+ self.write('wrangler.json', self.workflow())
+ self.check('AI')
+ for mutation in ('missing', 'second', 'binding', 'ai', 'container', 'environment'):
+ value = self.workflow()
+ if mutation == 'missing': value['workflows'] = []
+ if mutation == 'second': value['workflows'].append(dict(value['workflows'][0]))
+ if mutation == 'binding': value['workflows'][0]['binding'] = 'OTHER'
+ if mutation == 'ai': del value['ai']
+ if mutation == 'container': value['containers'] = [{}]
+ if mutation == 'environment': value['env'] = {'production': {'workflows': [{}]}}
+ self.write('wrangler.json', value)
+ with self.subTest(mutation=mutation), self.assertRaises(ValueError): self.check('AI')
+
+ def test_cloud_cannot_acquire_an_ai_loop_or_second_host(self):
+ base = {'main': 'worker/index.ts', 'containers': [{'class_name': 'CloudContainer'}]}
+ self.write('wrangler.json', base)
+ self.check('Cloud')
+ for value in ({**base, 'ai': {'binding': 'AI'}}, {**base, 'workflows': [{}]},
+ {**base, 'containers': []}, {**base, 'containers': [{}, {}]}):
+ self.write('wrangler.json', value)
+ with self.assertRaises(ValueError): self.check('Cloud')
+
+ def test_web_requires_react_static_assets_not_runtime_server(self):
+ self.write('apps/site/package.json', {'dependencies': {'react': '19.3.0'}})
+ self.write('wrangler.json', {'assets': {'directory': './apps/site/build/client'}})
+ self.check('Web')
+ self.write('wrangler.json', {'assets': {'directory': './apps/site/build/client'}, 'main': 'server.js'})
+ with self.assertRaises(ValueError): self.check('Web')
+
+ def test_mobile_retired_targets_and_false_comment_declarations(self):
+ for text in ('kotlin { iosArm64() }', 'plugins { id("com.facebook.react") }', 'nativeDistributions {}'):
+ self.write('build.gradle.kts', text)
+ with self.assertRaises(ValueError): self.check('Mobile')
+ self.write('build.gradle.kts', '// no extra target\n')
+ self.write('app/build.gradle.kts', '// applicationId = "io.github.arcforges.mobile"\n// compose = true\n')
+ with self.assertRaisesRegex(ValueError, 'identity'): self.check('Mobile')
+
+ def fixture(self):
+ subprocess.run(['git', 'init', '-q', str(self.root)], check=True)
+ subprocess.run(['git', '-C', str(self.root), 'remote', 'add', 'origin', 'https://github.com/ArcForges/DesktopPlatform.git'], check=True)
+ subprocess.run(['git', '-C', str(self.root), '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.invalid',
+ '-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-qm', 'fixture'], check=True)
+ project = 'src/BuildingBlocks/Library/Library.csproj'
+ self.write(project, 'net10.0trueAGPL-3.0-onlyAGPL')
+ self.write('eng/policy/licence-boundary.json', {'schemaVersion': 1, 'repository': 'DesktopPlatform',
+ 'spdxLicense': 'AGPL-3.0-only', 'licenceBoundary': 'AGPL', 'projects': [{'path': project, 'kind': 'msbuild'}]})
+ self.register()
+ return {'repository': 'DesktopPlatform', 'runtime': policy.RUNTIMES['DesktopPlatform'],
+ 'projects': [{'path': project, 'role': 'aot-library'}]}
+
+ def register(self):
+ self.files.add('eng/provenance/files.json')
+ self.write('eng/provenance/files.json', {'schemaVersion': 1, 'repository': 'DesktopPlatform',
+ 'firstParty': sorted(self.files), 'reused': {}, 'artifacts': []})
+
+ def test_real_git_inventory_rejects_new_unassigned_source(self):
+ row = self.fixture()
+ self.assertEqual(policy.audit(self.root, row)['result'], 'passed')
+ self.write('src/BuildingBlocks/Library/Unexpected.cs', 'class Unexpected {}')
+ with self.assertRaisesRegex(ValueError, 'unassigned'): policy.audit(self.root, row)
+
+ def test_aot_flag_does_not_allow_old_or_missing_framework(self):
+ for framework in ('', 'net8.0', 'net10.0;net8.0'):
+ self.write('Host.csproj', 'true' + framework + '')
+ with self.assertRaisesRegex(ValueError, 'framework'):
+ policy.check_managed(self.root, 'Host.csproj', 'aot-host', self.files)
+
+ def test_retired_path_fails_even_if_inventory_claims_ownership(self):
+ row = self.fixture()
+ self.write('src/ArcChat/Program.cs', 'class Program {}')
+ self.register()
+ with self.assertRaisesRegex(ValueError, 'retired scaffold'): policy.audit(self.root, row)
+
+ def test_new_project_fails_even_if_provenance_is_registered(self):
+ row = self.fixture()
+ self.write('src/BuildingBlocks/Other/Other.csproj', '')
+ self.register()
+ with self.assertRaises(ValueError): policy.audit(self.root, row)
+
+ def test_missing_tracked_file_is_never_omitted(self):
+ row = self.fixture()
+ subprocess.run(['git', '-C', str(self.root), 'add', '.'], check=True)
+ (self.root / row['projects'][0]['path']).unlink()
+ with self.assertRaises(ValueError): policy.audit(self.root, row)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj b/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj
index bc97005..63f22e0 100644
--- a/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj
+++ b/src/DesktopHelpers/ArcForges.ContentSandbox/ArcForges.ContentSandbox.csproj
@@ -1,9 +1,9 @@