diff --git a/.github/workflows/client-v1-conformance.yml b/.github/workflows/client-v1-conformance.yml index fe407260..c67ec1dc 100644 --- a/.github/workflows/client-v1-conformance.yml +++ b/.github/workflows/client-v1-conformance.yml @@ -1185,7 +1185,7 @@ jobs: @('scripts\phase1-macos-keychain.mjs', 5091, 'ab0c2dd08cf606d9502f5da206175707d471d99f484e8c8c79b5b08a5772b9a4'), @('scripts\phase1-process-supervisor.mjs', 3820, '16b51fb1a33b4bfef98daca549aacf5dc2d2c098cfbd664753b69c940d1e6f6c'), @('scripts\phase1-schema-v2-evidence.mjs', 52505, '0aede2ab3abd76fabf5ac61d64d2dbaaffa497c8647b82236403de16a47751c8'), - @('scripts\phase1-schema-v2-producer.mjs', 233186, '2e5fe8808230ad318aa86408bbad2004dbe4fd1c074f88de11fc581b227e0a51'), + @('scripts\phase1-schema-v2-producer.mjs', 233304, 'afe3509fc333b32b7af9a6de443db480ce7c1362aee82712713faa4c042fd3bc'), @('scripts\process-owned-artifact-root.mjs', 13061, '103cc789f12a6bbde16b2414aecf05813d9d28a2c40c7d6eaa2073b86e8e5d77'), @('scripts\supervised-exec.mjs', 2875, 'a5edfd985b934d3b46247a0da3141682c411d30bb582edf87ae7b29791dad65b'), @('scripts\supervisor-status.mjs', 854, 'ac332ca7b6b040ecc846088bb3a6ad5e7112a0454eb3ea71d2a819d55e64254e') @@ -1910,7 +1910,7 @@ jobs: - name: Prepare trusted Unix supervisor if: matrix.platform != 'win32-x64' shell: bash - run: "set -euo pipefail\nnode --input-type=module <<'EOF'\nimport { createHash } from 'node:crypto';\nimport { lstatSync, readFileSync } from 'node:fs';\nconst expected = new Map([\n ['scripts/contract-canary.mjs', [40618, 'a4c2fe0a5eb6a5ff4653de5374c34c0fb46907c6806a5d23b86d8b37206ef958']],\n ['scripts/executable-resolution.mjs', [9154, '31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430']],\n ['scripts/owned-temp-directory.mjs', [7762, '95f546ef9ed614f2a0f55d356ddfc54c943fc53b595b4eebebfcbd4db68e5c0b']],\n ['scripts/phase1-artifact-secret-scan.mjs', [21183, 'be0ec302b9c4372f232d6bd1efcba873fd3380cc5de7f756cd0b9eeeec07222a']],\n ['scripts/phase1-conformance-lock.mjs', [48960, '92f981c43f75bc65c81e9e9ee16084aae658617b929d451a9db0d7c9e6bedbe2']],\n ['scripts/phase1-conformance.mjs', [219462, 'fd90e35308ea6b8dd0488cb874d00d7ad40aa16b33e38b46a9a76c3ea13d5056']],\n ['scripts/phase1-evidence-contract.mjs', [15088, '24180ae03835fa6aac45559682adb3c1e626bab76466eddc55b9e2300f0a2b7f']],\n ['scripts/phase1-evidence-runtime.mjs', [6572, 'b09a0d20abc7ebbc6289c57c985fd42b8a4b5d196895bc035b8fca40281d9315']],\n ['scripts/phase1-linux-secret-service.mjs', [4270, 'ddf834c6f57853c5116b4b1f345952a218ff0687c5d741737c68e20bc2ecda92']],\n ['scripts/phase1-macos-keychain.mjs', [5091, 'ab0c2dd08cf606d9502f5da206175707d471d99f484e8c8c79b5b08a5772b9a4']],\n ['scripts/phase1-process-supervisor.mjs', [3820, '16b51fb1a33b4bfef98daca549aacf5dc2d2c098cfbd664753b69c940d1e6f6c']],\n ['scripts/phase1-schema-v2-evidence.mjs', [52505, '0aede2ab3abd76fabf5ac61d64d2dbaaffa497c8647b82236403de16a47751c8']],\n ['scripts/phase1-schema-v2-producer.mjs', [233186, '2e5fe8808230ad318aa86408bbad2004dbe4fd1c074f88de11fc581b227e0a51']],\n ['scripts/process-owned-artifact-root.mjs', [13061, '103cc789f12a6bbde16b2414aecf05813d9d28a2c40c7d6eaa2073b86e8e5d77']],\n ['scripts/supervised-exec.mjs', [2875, 'a5edfd985b934d3b46247a0da3141682c411d30bb582edf87ae7b29791dad65b']],\n ['scripts/supervisor-status.mjs', [854, 'ac332ca7b6b040ecc846088bb3a6ad5e7112a0454eb3ea71d2a819d55e64254e']],\n ['scripts/phase1-linux-secret-service.sh', [5650, '83ce19c0dd6da5002f6853fa37addb4fc2d39f3d17beee1b1c39e1fce232b476']],\n ['scripts/unix-artifact-handoff.c', [18704, '2a003f9aa1d1886b9a593371a73cb65fe3a4a8b703f1c59fec8a27694367b7fc']],\n ['scripts/unix-producer-command.sh', [3223, 'ce9ec2ff00947f3ec0db53f144c99d34bc27de6085062d00dccff7c934c2e3c8']],\n ['scripts/unix-producer-supervisor.sh', [29424, 'b73036415744c80ed27d5667f255ceea149096ca517b47c93a154299802206ff']],\n]);\nfor (const [path, [size, digest]] of expected) {\n const stats = lstatSync(path);\n const bytes = readFileSync(path);\n if (\n !stats.isFile() ||\n stats.isSymbolicLink() ||\n stats.nlink !== 1 ||\n bytes.byteLength !== size ||\n createHash('sha256').update(bytes).digest('hex') !== digest\n ) {\n throw new Error('Trusted Unix supervisor source bytes do not match');\n }\n}\nconsole.log('Frozen harness module graph verified.');\nEOF\nbroker_root=\"/tmp/opencoven-unix-broker\"\n(umask 077 && mkdir \"$broker_root\")\ncc -std=c11 -D_DARWIN_C_SOURCE -Wall -Wextra -Werror -O2 \\\n scripts/unix-artifact-handoff.c \\\n -o \"$broker_root/unix-artifact-handoff\"\nchmod 500 \"$broker_root/unix-artifact-handoff\"\n" + run: "set -euo pipefail\nnode --input-type=module <<'EOF'\nimport { createHash } from 'node:crypto';\nimport { lstatSync, readFileSync } from 'node:fs';\nconst expected = new Map([\n ['scripts/contract-canary.mjs', [40618, 'a4c2fe0a5eb6a5ff4653de5374c34c0fb46907c6806a5d23b86d8b37206ef958']],\n ['scripts/executable-resolution.mjs', [9154, '31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430']],\n ['scripts/owned-temp-directory.mjs', [7762, '95f546ef9ed614f2a0f55d356ddfc54c943fc53b595b4eebebfcbd4db68e5c0b']],\n ['scripts/phase1-artifact-secret-scan.mjs', [21183, 'be0ec302b9c4372f232d6bd1efcba873fd3380cc5de7f756cd0b9eeeec07222a']],\n ['scripts/phase1-conformance-lock.mjs', [48960, '92f981c43f75bc65c81e9e9ee16084aae658617b929d451a9db0d7c9e6bedbe2']],\n ['scripts/phase1-conformance.mjs', [219462, 'fd90e35308ea6b8dd0488cb874d00d7ad40aa16b33e38b46a9a76c3ea13d5056']],\n ['scripts/phase1-evidence-contract.mjs', [15088, '24180ae03835fa6aac45559682adb3c1e626bab76466eddc55b9e2300f0a2b7f']],\n ['scripts/phase1-evidence-runtime.mjs', [6572, 'b09a0d20abc7ebbc6289c57c985fd42b8a4b5d196895bc035b8fca40281d9315']],\n ['scripts/phase1-linux-secret-service.mjs', [4270, 'ddf834c6f57853c5116b4b1f345952a218ff0687c5d741737c68e20bc2ecda92']],\n ['scripts/phase1-macos-keychain.mjs', [5091, 'ab0c2dd08cf606d9502f5da206175707d471d99f484e8c8c79b5b08a5772b9a4']],\n ['scripts/phase1-process-supervisor.mjs', [3820, '16b51fb1a33b4bfef98daca549aacf5dc2d2c098cfbd664753b69c940d1e6f6c']],\n ['scripts/phase1-schema-v2-evidence.mjs', [52505, '0aede2ab3abd76fabf5ac61d64d2dbaaffa497c8647b82236403de16a47751c8']],\n ['scripts/phase1-schema-v2-producer.mjs', [233304, 'afe3509fc333b32b7af9a6de443db480ce7c1362aee82712713faa4c042fd3bc']],\n ['scripts/process-owned-artifact-root.mjs', [13061, '103cc789f12a6bbde16b2414aecf05813d9d28a2c40c7d6eaa2073b86e8e5d77']],\n ['scripts/supervised-exec.mjs', [2875, 'a5edfd985b934d3b46247a0da3141682c411d30bb582edf87ae7b29791dad65b']],\n ['scripts/supervisor-status.mjs', [854, 'ac332ca7b6b040ecc846088bb3a6ad5e7112a0454eb3ea71d2a819d55e64254e']],\n ['scripts/phase1-linux-secret-service.sh', [5650, '83ce19c0dd6da5002f6853fa37addb4fc2d39f3d17beee1b1c39e1fce232b476']],\n ['scripts/unix-artifact-handoff.c', [18704, '2a003f9aa1d1886b9a593371a73cb65fe3a4a8b703f1c59fec8a27694367b7fc']],\n ['scripts/unix-producer-command.sh', [3223, 'ce9ec2ff00947f3ec0db53f144c99d34bc27de6085062d00dccff7c934c2e3c8']],\n ['scripts/unix-producer-supervisor.sh', [29424, 'b73036415744c80ed27d5667f255ceea149096ca517b47c93a154299802206ff']],\n]);\nfor (const [path, [size, digest]] of expected) {\n const stats = lstatSync(path);\n const bytes = readFileSync(path);\n if (\n !stats.isFile() ||\n stats.isSymbolicLink() ||\n stats.nlink !== 1 ||\n bytes.byteLength !== size ||\n createHash('sha256').update(bytes).digest('hex') !== digest\n ) {\n throw new Error('Trusted Unix supervisor source bytes do not match');\n }\n}\nconsole.log('Frozen harness module graph verified.');\nEOF\nbroker_root=\"/tmp/opencoven-unix-broker\"\n(umask 077 && mkdir \"$broker_root\")\ncc -std=c11 -D_DARWIN_C_SOURCE -Wall -Wextra -Werror -O2 \\\n scripts/unix-artifact-handoff.c \\\n -o \"$broker_root/unix-artifact-handoff\"\nchmod 500 \"$broker_root/unix-artifact-handoff\"\n" - name: Compute reviewed Unix tool path id: unix-tool-path if: matrix.platform != 'win32-x64' diff --git a/docs/phase1-conformance.md b/docs/phase1-conformance.md index ae233292..8ab4b83d 100644 --- a/docs/phase1-conformance.md +++ b/docs/phase1-conformance.md @@ -1997,7 +1997,7 @@ revision authorities can therefore have different workflow hashes: | File | Bytes | SHA-256 | | --- | ---: | --- | -| `.github/workflows/client-v1-conformance.yml` | 177,934 | `73f665797b6d21de149949ca532bb814ac114a3682cff14c004919d95e3be2bb` | +| `.github/workflows/client-v1-conformance.yml` | 177,934 | `fb80c8c010c061c358a87507b98a2f29c3485e8ee02d2f52f4708742b5c4a0c8` | | `scripts/contract-canary.mjs` | 40,618 | `a4c2fe0a5eb6a5ff4653de5374c34c0fb46907c6806a5d23b86d8b37206ef958` | | `scripts/executable-resolution.mjs` | 9,154 | `31e3c412ff8c835f14522f36a59e91f4a4ba82913210ae8e3b4455217503f430` | | `scripts/owned-temp-directory.mjs` | 7,762 | `95f546ef9ed614f2a0f55d356ddfc54c943fc53b595b4eebebfcbd4db68e5c0b` | @@ -2010,7 +2010,7 @@ revision authorities can therefore have different workflow hashes: | `scripts/phase1-macos-keychain.mjs` | 5,091 | `ab0c2dd08cf606d9502f5da206175707d471d99f484e8c8c79b5b08a5772b9a4` | | `scripts/phase1-process-supervisor.mjs` | 3,820 | `16b51fb1a33b4bfef98daca549aacf5dc2d2c098cfbd664753b69c940d1e6f6c` | | `scripts/phase1-schema-v2-evidence.mjs` | 52,505 | `0aede2ab3abd76fabf5ac61d64d2dbaaffa497c8647b82236403de16a47751c8` | -| `scripts/phase1-schema-v2-producer.mjs` | 233,186 | `2e5fe8808230ad318aa86408bbad2004dbe4fd1c074f88de11fc581b227e0a51` | +| `scripts/phase1-schema-v2-producer.mjs` | 233,304 | `afe3509fc333b32b7af9a6de443db480ce7c1362aee82712713faa4c042fd3bc` | | `scripts/process-owned-artifact-root.mjs` | 13,061 | `103cc789f12a6bbde16b2414aecf05813d9d28a2c40c7d6eaa2073b86e8e5d77` | | `scripts/supervised-exec.mjs` | 2,875 | `a5edfd985b934d3b46247a0da3141682c411d30bb582edf87ae7b29791dad65b` | | `scripts/supervisor-status.mjs` | 854 | `ac332ca7b6b040ecc846088bb3a6ad5e7112a0454eb3ea71d2a819d55e64254e` | @@ -2765,6 +2765,32 @@ Fresh reviewed source binding, SDK rebinding, and protected validation are required before attributing the Windows failure or claiming a repaired run. +### Unsupported custody installation + +`secure_store_unavailable` covered two unrelated causes on the installation +preflight. `KeyringError::Unavailable` is returned both by a secure store that +is genuinely unavailable and by `CredentialCustody::installation_id`'s default +trait body, which a custody implementation reaches only by never overriding it. +`InstallationStage::classify` already rewrites `Unavailable` into +`installation_lock_unavailable`, `installation_entry_unavailable`, +`installation_read_unavailable`, `installation_write_unavailable` and +`installation_persistence_unavailable`, so those five stages were already +distinguishable; the default trait body was the remaining unclassified path. + +It now returns the fixed code `installation_custody_unsupported`, published as +`phase1.native-scenarios.native-preflight-installation-custody-unsupported`. +The outer launcher derives its native-stage allowlist from the producer +registry, so the identifier survives extraction without a second edit. Only the +fixed identifier is published: no message, stack, path, credential or +subprocess output is added. + +Protected run +[35100084575](https://github.com/OpenCoven/chat/actions/runs/35100084575) +reported `native-preflight-installation-secure-store-unavailable` on Windows +while Linux and macOS passed. This change does not repair that failure and does +not establish its cause; it separates the two causes so the next protected run +attributes it. + ### Unexpected installation RPC failures Protected run `34964120550` used Chat `d84195c61b86598e691ccd47163e46a15b154417` diff --git a/phase1-conformance.lock.json b/phase1-conformance.lock.json index 36dd71aa..f1370cab 100644 --- a/phase1-conformance.lock.json +++ b/phase1-conformance.lock.json @@ -18,11 +18,11 @@ }, "harness": { "repository": "OpenCoven/chat", - "revision": "7dda439daa59ee350ff12cd195ff169b52e1085c" + "revision": "fe58cb1d4353b44fc61884ad76802ce924571526" }, "harnessAuthority": { - "revision": "7dda439daa59ee350ff12cd195ff169b52e1085c", - "tree": "347c0b5300498d612f5411c1aee3372731ffc4f1", + "revision": "fe58cb1d4353b44fc61884ad76802ce924571526", + "tree": "7dc95a786f06770651b4755478727e8843f09400", "files": [ { "path": "scripts/phase1-conformance.mjs", @@ -81,8 +81,8 @@ }, { "path": "scripts/phase1-schema-v2-producer.mjs", - "blob": "76c8a0e31e55c8c1478dcb9657a1570d697b155c", - "sha256": "2e5fe8808230ad318aa86408bbad2004dbe4fd1c074f88de11fc581b227e0a51" + "blob": "08cebe6bad068657b9ee00efa2755cb8bd065066", + "sha256": "afe3509fc333b32b7af9a6de443db480ce7c1362aee82712713faa4c042fd3bc" }, { "path": "scripts/phase1-linux-secret-service.mjs", @@ -146,8 +146,8 @@ }, { "path": ".github/workflows/client-v1-conformance.yml", - "blob": "fe4072609b4c3e967881f93d82f86de0c0d4f23a", - "sha256": "73f665797b6d21de149949ca532bb814ac114a3682cff14c004919d95e3be2bb" + "blob": "c67ec1dc4e0acb797ef4f2f2f0a2d50fde5f96ed", + "sha256": "fb80c8c010c061c358a87507b98a2f29c3485e8ee02d2f52f4708742b5c4a0c8" } ], "productionDeltas": [ @@ -193,8 +193,8 @@ }, { "path": "src-tauri/src/keyring.rs", - "blob": "b235ab40e02265ea3e9fbaedf0a842e7fc627f4f", - "sha256": "ba9be1f65792ae988ff64ca1348fc9ec4438925e4f4822f3c2d3057eee5d0568" + "blob": "11bd10925b3e18a27fd55fccbada1d0fb67db781", + "sha256": "72e1363f2f241e435136237731f3d84ca5a4ba9dad00f784027146331dcd8abc" }, { "path": "src-tauri/src/lib.rs", diff --git a/scripts/phase1-schema-v2-producer.mjs b/scripts/phase1-schema-v2-producer.mjs index 76c8a0e3..08cebe6b 100644 --- a/scripts/phase1-schema-v2-producer.mjs +++ b/scripts/phase1-schema-v2-producer.mjs @@ -116,6 +116,7 @@ const nativeLaunchPublicationFailures = new WeakMap(); const nativeRpcFailureCategories = new WeakMap(); const nativeInstallationResponseCategories = new Map([ ['secure_store_unavailable', 'secure-store-unavailable'], + ['installation_custody_unsupported', 'custody-unsupported'], ['installation_lock_unavailable', 'lock-unavailable'], ['installation_entry_unavailable', 'entry-unavailable'], ['installation_read_unavailable', 'read-unavailable'], @@ -173,6 +174,7 @@ const schemaV2NativeFailureStages = new Set([ 'native-preflight-custody-proof', 'native-preflight-installation-rpc', 'native-preflight-installation-secure-store-unavailable', + 'native-preflight-installation-custody-unsupported', 'native-preflight-installation-lock-unavailable', 'native-preflight-installation-entry-unavailable', 'native-preflight-installation-read-unavailable', diff --git a/src-tauri/src/keyring.rs b/src-tauri/src/keyring.rs index b235ab40..11bd1092 100644 --- a/src-tauri/src/keyring.rs +++ b/src-tauri/src/keyring.rs @@ -82,6 +82,8 @@ pub(crate) enum KeyringError { Unavailable, #[cfg(feature = "phase1-conformance")] InstallationUnavailable(InstallationStage), + #[cfg(feature = "phase1-conformance")] + CustodyInstallationUnsupported, Failure, #[cfg(feature = "phase1-conformance")] CleanupGrantRejected, @@ -162,6 +164,10 @@ impl KeyringError { }, true, ), + #[cfg(feature = "phase1-conformance")] + Self::CustodyInstallationUnsupported => { + NativeDiagnostic::new("installation_custody_unsupported", true) + } Self::Failure => NativeDiagnostic::new("keychain_failure", true), #[cfg(feature = "phase1-conformance")] Self::CleanupGrantRejected => NativeDiagnostic::new("cleanup_grant_rejected", false), @@ -287,8 +293,13 @@ pub(crate) enum CredentialSlot { } pub(crate) trait CredentialCustody: Send + Sync { + // A custody implementation that does not override this is a distinct + // condition from an unavailable secure store; keep them separable. fn installation_id(&self) -> Result { - Err(KeyringError::Unavailable) + #[cfg(feature = "phase1-conformance")] + return Err(KeyringError::CustodyInstallationUnsupported); + #[cfg(not(feature = "phase1-conformance"))] + return Err(KeyringError::Unavailable); } fn read(&self, instance_id: &str, origin: &str) -> Result; @@ -2349,6 +2360,63 @@ mod tests { )); } } + #[cfg(feature = "phase1-conformance")] + #[test] + fn unsupported_custody_installation_is_distinct_from_an_unavailable_store() { + // A custody type that does not override installation_id must not be + // reported as an unavailable secure store; the two are separate causes. + struct BareCustody; + impl super::CredentialCustody for BareCustody { + fn read(&self, _: &str, _: &str) -> Result { + Err(KeyringError::Failure) + } + fn read_for_pairing_update( + &self, + _: &str, + _: &str, + ) -> Result { + Err(KeyringError::Failure) + } + fn store_if_current( + &self, + _: &str, + _: &str, + _: Option<&super::Credential>, + _: &str, + _: &str, + ) -> Result { + Err(KeyringError::Failure) + } + fn replace_stale_if_current( + &self, + _: &str, + _: &str, + _: &super::Credential, + _: &str, + _: &str, + ) -> Result { + Err(KeyringError::Failure) + } + fn delete_if_matches( + &self, + _: &str, + _: &str, + _: &super::Credential, + ) -> Result { + Err(KeyringError::Failure) + } + } + let error = super::CredentialCustody::installation_id(&BareCustody) + .expect_err("the default custody implementation must fail"); + assert!(matches!( + error, + KeyringError::CustodyInstallationUnsupported + )); + let diagnostic = error.diagnostic(); + assert_eq!(diagnostic.code, "installation_custody_unsupported"); + assert_ne!(diagnostic.code, KeyringError::Unavailable.diagnostic().code); + assert!(diagnostic.retryable); + } #[cfg(unix)] use super::{ acquire_mutation_lock_detailed_with_timeout_at, acquire_mutation_lock_with_timeout_at, diff --git a/src/native-preflight-diagnostics.test.ts b/src/native-preflight-diagnostics.test.ts index a4011e5b..bff5d52a 100644 --- a/src/native-preflight-diagnostics.test.ts +++ b/src/native-preflight-diagnostics.test.ts @@ -68,6 +68,7 @@ test.each([ 'installation-rpc', 'installation-id', 'installation-secure-store-unavailable', + 'installation-custody-unsupported', 'installation-lock-unavailable', 'installation-entry-unavailable', 'installation-read-unavailable', diff --git a/src/phase1-conformance-lock.test.ts b/src/phase1-conformance-lock.test.ts index cd0a87d2..ba3fa535 100644 --- a/src/phase1-conformance-lock.test.ts +++ b/src/phase1-conformance-lock.test.ts @@ -80,8 +80,8 @@ const committedHarnessAuthority = JSON.parse( readFileSync(resolve(projectRoot, 'phase1-conformance.lock.json'), 'utf8'), ).harnessAuthority; const expectedBehaviorAuthority = { - revision: '7dda439daa59ee350ff12cd195ff169b52e1085c', - tree: '347c0b5300498d612f5411c1aee3372731ffc4f1', + revision: 'fe58cb1d4353b44fc61884ad76802ce924571526', + tree: '7dc95a786f06770651b4755478727e8843f09400', files: [ { path: 'scripts/owned-temp-directory.mjs', @@ -105,8 +105,8 @@ const expectedBehaviorAuthority = { }, { path: 'scripts/phase1-schema-v2-producer.mjs', - blob: '76c8a0e31e55c8c1478dcb9657a1570d697b155c', - sha256: '2e5fe8808230ad318aa86408bbad2004dbe4fd1c074f88de11fc581b227e0a51', + blob: '08cebe6bad068657b9ee00efa2755cb8bd065066', + sha256: 'afe3509fc333b32b7af9a6de443db480ce7c1362aee82712713faa4c042fd3bc', }, { path: 'scripts/unix-producer-supervisor.sh', @@ -135,8 +135,8 @@ const expectedBehaviorAuthority = { }, { path: '.github/workflows/client-v1-conformance.yml', - blob: 'fe4072609b4c3e967881f93d82f86de0c0d4f23a', - sha256: '73f665797b6d21de149949ca532bb814ac114a3682cff14c004919d95e3be2bb', + blob: 'c67ec1dc4e0acb797ef4f2f2f0a2d50fde5f96ed', + sha256: 'fb80c8c010c061c358a87507b98a2f29c3485e8ee02d2f52f4708742b5c4a0c8', }, { path: 'scripts/process-owned-artifact-root.mjs',