@rightkit/release 0.2.150 → 0.2.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE-APACHE +201 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +119 -8
  4. package/build-release.mjs +16 -2
  5. package/cli/right-release.mjs +18 -5
  6. package/committed-source.mjs +19 -1
  7. package/crate-graph.mjs +121 -0
  8. package/development-signing.mjs +8 -0
  9. package/development.mjs +46 -13
  10. package/devtools-policy.json +5 -0
  11. package/devtools-policy.mjs +119 -0
  12. package/direct-bootstrap.mjs +9 -3
  13. package/dmg-build.mjs +19 -5
  14. package/dmg-layout.mjs +2 -2
  15. package/dmg-spec.mjs +97 -0
  16. package/execution-policy.mjs +17 -1
  17. package/github-release.mjs +25 -7
  18. package/github-upload-gate.mjs +84 -0
  19. package/hardeningscan.mjs +3 -0
  20. package/inventory-check.mjs +4 -4
  21. package/mac-bundle-signing.mjs +1 -1
  22. package/mac-sign-core.mjs +97 -0
  23. package/mac-signing-matrix.mjs +186 -0
  24. package/macos-notarization.mjs +18 -2
  25. package/msix-appinstaller.mjs +4 -1
  26. package/msix-config.mjs +5 -1
  27. package/msix-repair-guard.mjs +24 -0
  28. package/msix-repair-guard.ps1 +356 -0
  29. package/notarize-macos.mjs +4 -3
  30. package/ownership-catalog-config.json +6 -7
  31. package/ownership-command.mjs +69 -8
  32. package/ownership-evidence.mjs +1 -1
  33. package/ownership-key-authority.mjs +1 -1
  34. package/ownership-key-command.mjs +15 -3
  35. package/ownership-key-elevation.mjs +11 -2
  36. package/ownership-mandatory-set.json +420 -0
  37. package/ownership-mandatory-set.mjs +273 -0
  38. package/ownership.mjs +2 -2
  39. package/package.json +15 -3
  40. package/packaged-app-identity.mjs +88 -0
  41. package/preflight.mjs +25 -3
  42. package/prune-r2.mjs +28 -3
  43. package/publication-gate.mjs +49 -10
  44. package/publish-cargo.mjs +21 -1
  45. package/publish-swift.mjs +78 -141
  46. package/publish-update.mjs +24 -2
  47. package/qa-build-features.mjs +216 -0
  48. package/qa-control-scan.mjs +435 -0
  49. package/registry-parity.mjs +18 -1
  50. package/release-invocation.mjs +2 -1
  51. package/release-state.mjs +9 -0
  52. package/release-switch-registry.json +33 -0
  53. package/release-switch-scan.mjs +452 -0
  54. package/release.mjs +324 -26
  55. package/renamed-away.mjs +29 -0
  56. package/rightkit-ownership.json +682 -709
  57. package/rightkit-sync.mjs +8 -4
  58. package/rightkit-version-policy.mjs +18 -0
  59. package/rightkit-versions.json +212 -127
  60. package/runtime-budget-classes.json +4 -0
  61. package/runtime-budgets.mjs +870 -0
  62. package/runtime-bundle-scan.mjs +61 -0
  63. package/sign-macos.mjs +28 -30
  64. package/sign-updater.mjs +5 -2
  65. package/swift-mac-dmg.mjs +7 -2
  66. package/swift-package-command.mjs +1 -1
  67. package/upload-release.mjs +10 -37
package/LICENSE-APACHE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/LICENSE-MIT ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adrian D'souza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -11,6 +11,54 @@ Portable release CLI and SDK for Right Suite products: builds, signs, and publis
11
11
 
12
12
  Signing and publication rules are in the workspace file `docs/rules/release-signing.md`. Public releases use RightKit CI. Private builds run on the native host.
13
13
 
14
+ ## No test-automation surface ships
15
+
16
+ Release and update builds (`right-release build`, `publish-update`) must not contain any test-automation or debugging surface; the development lane may. Layers, in the order they run:
17
+
18
+ 1. **Source (before anything is built; `right-release build --dry-run` only loads the config and does not reach this layer)** - `qa-build-features.mjs` refuses a build command, a package.json script it runs, or an app `Cargo.toml` that enables `devtools`, `qa-native`, `qa-control`, `dev-native` or `dev-inspect` (also `--all-features`, and `devtools` on the `tauri` dependency, `qa-control` on a non-optional `rightkit-qa-driver`, `dev-inspect` on a non-optional `rightkit-tauri-shell`, a non-optional `tauri-plugin-wdio-webdriver`, or any of them reachable from the default features).
19
+ 2. **Pre-pack payload scan** - `qa-control-scan.mjs` over the raw EXE (Windows NSIS and portable), the macOS signing set and `notarize.app` (the `.app` going into the DMG; `dmg build` also scans its staging directory), the MSIX `stageDir` before packing, and `target.qaControlScan`. Mach-O, PE and ELF files are matched against five rules: `qa-control-server` (marker `RIGHTKIT-QA-CONTROL-SERVER-PRESENT`), `shell-dev-inspect` (marker `RIGHTKIT-SHELL-DEV-INSPECT-COMPILED`, written by rightkit-tauri-shell's `dev-inspect` feature; no allow-list lifts it), `wdio-webdriver` (2 of 3 of `tauri-plugin-wdio-webdriver`, `tauri_plugin_wdio_webdriver`, `wdio-webdriver:default`), `remote-debugging` (`--remote-debugging-port`, `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`) and `tauri-devtools` (`plugin:webview|internal_toggle_devtools`, `toggleDevtoolsHotkey`); each signature also as UTF-16LE. A scan that finds no native binary fails.
20
+
21
+ Web-bundle mock scan: `release` also runs `assertNoWebBundleMocks` over the frontend dist (`target.webBundleScan`, else `build.frontendDist` of `src-tauri/tauri.conf.json`) before packaging when it already exists (also under `--dry-run`) and after the package command, where a missing or empty dist fails. `right-release bundle-scan` (Node/Python runtime roots) and `identity-scan` (needs the app's declared cdhash/team/identifier) are not automatic: both need app-specific inputs, so each app runs them from its own `checks`.
22
+ 3. **Packed MSIX** - the finished `.msix` is read through its ZIP central directory (stored and deflate entries, nested ZIPs to depth 3, 1 GiB per entry, 8 GiB inflated per container; encrypted, unsupported-method or oversized entries are findings).
23
+ 4. **Hardening targets** (final artifacts) and **every sealed file** (`verifySealedRelease`, used by every seal and upload path) run the same rules, including ZIP containers.
24
+
25
+ Not coverable after packing: an NSIS exe (LZMA payload) and a compressed DMG. They are covered only by layer 2, so the release fails when no pre-pack scan saw a native binary, and the log names each unreadable installer. Not coverable at all: assets embedded compressed in the app, resources an NSIS installer packs that were never in the scanned set (list them in `target.qaControlScan`), and any surface without a rule in `QA_RULES`. The `wdio-webdriver` rule is weaker than the marker rule (signatures chosen without the plugin source). Full list: `crates/rightkit-qa-driver/README.md`, "QA control never ships".
26
+
27
+ ### DevTools policy (owner's fixed design)
28
+
29
+ Every Right app has absolutely NO access to DevTools; the single exception is ViewRight, which may inspect the HTML content it opens, never its own UI or shell. The rules above therefore refuse `devtools` (and rightkit-tauri-shell's `content-devtools`) for every app, with one narrow allowance (`devtools-policy.mjs`): an app whose right-release config declares `devtools: "content-only"`, **and** that is listed in `devtools-policy.json` (`contentOnlyApps`, today only `viewright`), **and** whose Cargo manifest depends on `rightkit-tauri-shell` with `content-devtools`, **and** whose `build.rs` declares an application ACL manifest. Then the source layer accepts `devtools` / `content-devtools`, and the binary layer accepts the `tauri-devtools` signatures only in a binary that also carries `RIGHTKIT-SHELL-DEVTOOLS-CONTENT-ONLY-COMPILED` (written by rightkit-tauri-shell when `content-devtools` is compiled). `--all-features`, `qa-native`, `qa-control`, `dev-native`, `dev-inspect` and every other rule stay refused. The allowance is recorded in the sealed manifest (`devtools`) and re-checked against the allow-list when a sealed file is verified. Any other app declaring it is refused (`devtools-policy.test.mjs`).
30
+
31
+ ## Release switch scan (M21/M23, owner decision D4, ratified)
32
+
33
+ `release-switch-scan.mjs` proves that release code cannot use an environment variable, argument, settings key or build flag to select a test mode, data root, executable, library, model or identity. The runtime half is `rightkit_daemon::mode`: a build without the `test-mode` cargo feature reads no override selector at all. Registry: `release-switch-registry.json` (forbidden features, the `RIGHTKIT-TEST-MODE-COMPILED` marker, deny names with their class). Apps add their own deny names under `releaseSwitches.forbid`. Selector classes: S1 test activation, S2 authority or identity override, S3 executable/library/model, S4 internal state or data root (S1-S4: forbidden, no allow-list), S5 user-content location (allowed, user gesture), S6 diagnostics/tuning (allowed, bounded value).
34
+
35
+ 1. **Feature closure (authoritative)** - `release.mjs` runs `cargo tree -e features,no-dev --locked --target <triple>` in the app repository (`resolveFeatureClosure`; forwards the build command's `--features`/`--no-default-features`; manifest resolves `targets.<os>.cargoManifest` -> `releaseSwitches.manifest` -> `src-tauri/Cargo.toml` -> `Cargo.toml` and must exist; features are also read from package.json scripts the hooks run (up to three levels) plus `releaseSwitches.features`; no manifest fails with `release-switch-manifest-missing` unless the app sets `releaseSwitches.rust: false` (the only skip); a cargo failure fails a real release, a dry run reports it) and passes it to `featureClosureFindings(...)` and `commandSwitchFindings(argv)` refuse `test-mode`, `qa-native`, `dev-native`, `qa-control`, `dev-inspect` anywhere in the resolved graph (finding id `forbidden-feature <name>`). The binary marker below is secondary evidence only. `qa-build-features.mjs` also lists `test-mode` as a forbidden feature, including on a non-optional `rightkit-daemon` dependency.
36
+ 2. **Inventory completeness** - `sourceSwitchFindings` walks app source (Rust, TS/JS, Swift, C; not tests, fixtures, `node_modules`, `target`) and rejects an environment read whose name is not in `switches.json` (`unregistered-read`), a non-literal read in a file not listed under `dynamicReads` (`dynamic-read`), an S1-S4 entry without `compiledOut` (`not-compiled-out`), and a malformed inventory (`inventory-invalid`). Argument flags are enforced for deny-listed flags only.
37
+ 3. **Binary** - `scanSwitchPaths` / `bufferSwitchFindings` over Mach-O, PE and ELF files: the marker (`test-mode-capability`) and any deny name from S1-S4 (`release-switch-names`), each as Latin-1 and UTF-16LE. The receipt holds the sha256 and size of every scanned file; the caller adds source commit, target triple and resolved features.
38
+
39
+ `assertNoReleaseSwitches({ treeText, commands, sourceRoot, inventoryText, binaryPaths, appForbid })` runs every layer it is given input for. Limits: packed NSIS/DMG and compressed MSIX members are covered only by the pre-pack scan of the staged payload, symlinks are skipped, and browser JS is checked at the source layer only. `release.mjs` runs the source layer once before packaging (inventory at `releaseSwitches.inventory` or `switches.json`; a missing inventory fails a non-dry-run release) and the binary layer wherever it runs the QA-control binary scan; a finding stops the release. Trust model: a release build reads no override switch at all; the test capability is compiled out (`test-mode` cargo feature, refused by `qa-build-features.mjs`), never hidden behind a flag.
40
+
41
+ ### `switches.json` (one per app)
42
+
43
+ ```json
44
+ {
45
+ "schema": 1,
46
+ "app": "viewright",
47
+ "switches": [
48
+ { "name": "HEARDRIGHT_ENGINE_TEST_MODE", "class": "S1", "channel": "env",
49
+ "anchor": "src/test_mode.rs:97", "compiledOut": "feature:test-mode", "note": "" },
50
+ { "name": "LIGHTCRAFT_LOG", "class": "S6", "channel": "env", "valueType": "enum" },
51
+ { "name": "--library", "class": "S5", "channel": "argv", "userGesture": true }
52
+ ],
53
+ "dynamicReads": [ { "file": "src/x.rs", "reason": "why a literal name is impossible" } ]
54
+ }
55
+ ```
56
+
57
+ - `name` (required, unique): environment variable, argument flag, settings key or `VITE_*` name.
58
+ - `class` (required): `S1`..`S6`. `channel` (required): `env`, `argv`, `settings`, `preference` or `frontend`.
59
+ - S1-S4 need `compiledOut`: `feature:<name>`, `cfg:test` or `cfg:debug_assertions` (the read exists only there). S5 needs `userGesture: true`. S6 needs `valueType` of `bool`, `int` or `enum`.
60
+ - `anchor` and `note` are informational. Fixtures: `fixtures/release-switches/` (`app/` is a clean example).
61
+
14
62
  ## Commands
15
63
 
16
64
  `right-release` maps these subcommands to the modules in this package:
@@ -24,6 +72,7 @@ Signing and publication rules are in the workspace file `docs/rules/release-sign
24
72
  | Signing | `sign-windows`, `sign-updater`, `signing-agent`, `create-mac-updater` |
25
73
  | Checks | `deps`, `hardening`, `harden`, `hardeningscan`, `cargo-target` |
26
74
  | Ownership | `ownership`, `ownership-approval`, `ownership-report` |
75
+ | Other | `dev`, `doctor`, `suite-doctor`, `qa`, `dmg`, `cache`, `apple`, `inventory`, `model`, `addon`, `hooks`, `lsclean`, `generate-dmg-background`, `publish cargo`, `publish npm`, `publish swift` |
27
76
 
28
77
  ## QA receipts (warn-and-record)
29
78
 
@@ -42,9 +91,23 @@ Agents must not run `osascript`, `open`, `screencapture`, or `notifyutil -p` dir
42
91
  right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]
43
92
  ```
44
93
 
45
- It mounts the image read-only and hidden (`-nobrowse`), verifies the app (`codesign --verify --deep --strict`, `spctl --assess`, `stapler validate`, bundle id, and Team ID against the installed copy; a Team ID change is refused) before touching the installed copy, quits the running instance with SIGTERM (15 s, then SIGKILL), moves the old bundle to `~/.Trash/<App> <timestamp>.app`, copies the new one with `ditto` (restoring the old bundle on failure), re-verifies, and relaunches only if it was running or `--relaunch` is given, using `open -g -j -b` (background, hidden, never activated). `--json` emits one lifecycle event per line (`start`, `verified`, `quit`, `replaced`, `relaunched`, `done`/`failed`). Win32 exits with "not implemented on win32". Exit codes: 0 ok, 1 failed, 2 usage, 3 unsupported platform.
94
+ It mounts the image read-only and hidden (`-nobrowse`), verifies the app (`codesign --verify --deep --strict`, `spctl --assess`, `stapler validate`, bundle id, and Team ID against the installed copy; a Team ID change is refused) before touching the installed copy, stages a `ditto` copy beside it in the destination and verifies that copy, quits the running instance with SIGTERM (15 s, then SIGKILL), swaps the staged copy in (one atomic exchange when the `rightkit-swap` helper is found, otherwise two journaled renames; the old bundle is restored on failure), re-verifies the installed copy, only then moves the old bundle to `~/.Trash/<App> <timestamp>.app`, and relaunches only if it was running or `--relaunch` is given, using `open -g -j -b` (background, hidden, never activated). `--json` emits one lifecycle event per line (`start`, `verified`, `quit`, `replaced`, `relaunched`, `done`/`failed`, among others). `right-release install-app --recover [--dest <dir>] [--app-name <App.app>] [--json]` rolls an interrupted install forward or back from its journal; every install runs that recovery first. Win32 exits with "not implemented on win32". Exit codes: 0 ok, 1 failed, 2 usage, 3 unsupported platform.
95
+
96
+ Development builds (from `@rightkit/ci` `devArtifact`, Developer ID-signed but not notarized) install with `--dev` and exactly one of `--dmg`, `--zip <path.zip>` (extracted with `ditto -x -k`), or `--app <path.app>`: `right-release install-app --dev --zip dev-mac.zip`. `--dev` still requires `codesign --verify --deep --strict`, a Team ID, no Team ID change, and a bundle-id match; it skips only `spctl` and `stapler`, removes `com.apple.quarantine` from the new bundle only, prints `DEV BUILD (not notarized)`, and sets `dev: true` on every JSON event.
97
+
98
+ ## Runtime budget gate (crates)
99
+
100
+ `publish cargo` runs the `runtime budget` step (`runtime-budgets.mjs`) right after the secret scan. Full contract, schema and refusal table: `docs/RUNTIME-BUDGETS.md`.
101
+
102
+ What the gate enforces, exactly:
103
+ - **Three classes, fail-closed.** always-on (measured budget at publish), `continuous = "held-resource"` with a `resources` table (structural check at publish, installed-app measurement at adoption via `appRuntimeBudgetAdoptionCheck`, wired into `collectPreflight`), and `continuous = false` (bounded per call).
104
+ - **Membership is fail-closed.** `Cargo.toml` `[package].name` must equal `--crate`. Every crate needs `runtime-budget.json`, `[package.metadata.rightkit] continuous = false` with a `continuous_reason`, or `continuous = "held-resource"` with a `resources` table; a crate with none of them is refused. The 11 crates in `RUNTIME_BUDGET_CRATES` cannot declare `continuous = false`. A `continuous = false` crate is refused when its non-test `src` matches a pattern in `CONTINUOUS_PATTERNS` (`thread-spawn`, `task-spawn`, `runtime-builder`, `sleep`, `timer`, `listener`, `watcher`, `event-loop`, `audio-stream`, `onnx-session`, `ggml`, `conn-pool`, `resident-static`, `detached-child`) and the reason does not contain that id.
105
+ - **A continuous crate needs a measured budget.** Declared ceilings and `requiredScale`; a status pointer to a `pub` item exposing resident memory; per-host measurements of the published version, each backed by raw bench output under `runtime-budget.evidence/` (`runtime-budgets.mjs record`) whose sha256 and `RESULT budget` line the gate re-checks; every observed value within its ceiling and non-zero resident memory; a fresh, in-ceiling, at-scale measurement on each required host (macOS and Windows, or macOS alone only when the release evidence records `hostPolicy: "darwin-only"` with a reason, which the budget file may not contradict); a README section equal to the one generated from the JSON.
106
+ - **Freshness** covers the crate's `src`, `benches`, `Cargo.toml`, `build.rs`, the `src`, `Cargo.toml` and `build.rs` of every workspace `path` dependency, new untracked files there, and the crate's `Cargo.lock` dependency closure, against the measured commit (which must be in the checkout and an ancestor of HEAD).
46
107
 
47
- Development builds (from `@rightkit/git` `devArtifact`, Developer ID-signed but not notarized) install with `--dev` and exactly one of `--dmg`, `--zip <path.zip>` (extracted with `ditto -x -k`), or `--app <path.app>`: `right-release install-app --dev --zip dev-mac.zip`. `--dev` still requires `codesign --verify --deep --strict`, a Team ID, no Team ID change, and a bundle-id match; it skips only `spctl` and `stapler`, removes `com.apple.quarantine` from the new bundle only, prints `DEV BUILD (not notarized)`, and sets `dev: true` on every JSON event.
108
+ What it does not do: run the bench, prove the bench ran, detect `[profile]`, `[patch]` or toolchain changes, or find continuous work outside the scanned patterns. `runtimeBudgetAdoptionCheck` adds the need for a fresh `installed` measurement naming the app; crates with `publish = false` never reach `publish cargo`, so their adoption must call it.
109
+
110
+ A real `publish cargo --crate <name>` refuses with `RUNTIME_BUDGET_MISSING` listing every problem; `--dry-run` prints `[FAIL] runtime-budget: ...` and continues.
48
111
 
49
112
  ## Entry points
50
113
 
@@ -54,7 +117,7 @@ Bin: `right-release` (`cli/right-release.mjs`). The package has no `exports` map
54
117
 
55
118
  | Dependency | Range | Note |
56
119
  |---|---|---|
57
- | `smol-toml` | `1.7.0` (exact) | Used to parse lock and manifest files |
120
+ | `smol-toml` | `1.9.0` (exact) | Used to parse lock and manifest files |
58
121
 
59
122
  Peer dependencies: none.
60
123
 
@@ -75,20 +138,68 @@ Swift (SwiftPM) apps are a first-class project type. `projectType: "swiftpm"` (c
75
138
 
76
139
  `updater.kind: "appinstaller"` (default remains the Tauri minisign flow) generates `msix.appInstaller.out` (https `uri`/`msixUri`); trust is the MSIX Authenticode signature, so no `.sig` is produced and `publish-update` records the sealed package digest.
77
140
 
78
- Never cancel an MSIX deployment from app or updater code. Cancelling `AddPackageAsync` while it is in `RegistrationChanged` leaves the package `Modified, NeedsRemediation` (0x80073CF8, AppX event 751); the app then cannot activate, so the OnLaunch feed check never runs and cannot self-heal. Measured recovery (SwiftRight SliceApp R5 W08, 2026-10-10): `Add-AppxPackage -AppInstallerFile <feed uri>` restores launch by deploying the feed's newer version, with user data intact; `-RegisterByFamilyName`, `Reset-AppxPackage` and relaunching do not. Recovery when the feed offers no newer version is unverified, so publish the next version to the feed before directing a user to that command.
141
+ Portable macOS targets (`macos-developer-id-notarized-portable-v1`) whose `notarize.file` is an archive of bare executables set `notarize.staple: false`: the archive is notarized (Accepted) and recorded, nothing is stapled. Portable Windows and macOS targets with no installer or updater block seal their archives from `targets.<platform>.artifacts`.
142
+
143
+ Never cancel an MSIX deployment from app or updater code. Cancelling `AddPackageAsync` while it is in `RegistrationChanged` leaves the package `Modified, NeedsRemediation` (0x80073CF8, AppX event 751); the app then cannot activate, so the OnLaunch feed check normally does not run and the package does not self-heal (in the tuple 08 runs one launch attempt in five was followed by a feed update to the newer version; the trigger was not isolated). Measured recovery (SwiftRight SliceApp R5 W08 tuple 08, 2026-10-10, one run per method, user data intact in every recovering case): Settings › Apps › Advanced options › Repair recovers a feed install (with or without a newer version on the feed) and a path install with no feed; `Add-AppxPackage -Path <same-version .msix>` recovers; `Add-AppxPackage -AppInstallerFile <feed uri>` recovers with or without a newer version on the feed. `Reset-AppxPackage`, `-RegisterByFamilyName`, the platform's own `RegisterByPackageFullName` (runs on every launch attempt, reports success, package stays NeedsRemediation) and relaunching do not. Not measured: Repair with the install source gone.
79
144
 
80
145
  macOS Swift apps use `right-release swift-package mac-app` (Info.plist from config, resources, frameworks, entitlements on the bundle only; nested code signed inside-out before the app).
81
146
 
82
- Build a SwiftPM app with `right-release build --platform mac|win` (add `--config <path>` when the config is not `right-release.config.mjs`). Build is tier-neutral and never uploads, so it rejects `--tier` and takes no `--upload`/`--no-upload`; the old `release --tier patch --no-upload` form no longer works. Pick the tier only at upload: `right-release upload --release <sealed-id> --platform mac|win --tier patch|update`.
147
+ Build a SwiftPM app with `right-release build --platform mac|win` (add `--config <path>` when the config is not `right-release.config.mjs`). Build is tier-neutral and never uploads, so it rejects `--tier` and takes no `--upload`/`--no-upload`; the old `release --tier patch --no-upload` form no longer works. Pick the tier only at upload: `right-release upload --release <sealed-id> --platform mac|win --tier patch|update` (`--tier` is required; it never builds). `upload` (and `publish`, its alias) requires `distribution.provider: "github-releases"` and `distribution.repository` in the release config; a product distributed through R2 and RightApps publishes with `right-release publish-update --release <sealed-id> --platform mac|win --tier patch|update` (or `RIGHT_RELEASE_TIER`), which requires `distribution.provider: "r2-rightapps"`.
83
148
 
84
149
  Set `targets.mac.notarize.appBeforePackage: true` (with `notarize.app` naming the `.app`) to notarize and staple the app itself right after signing and before the package step copies it into the DMG, so an app copied out of the DMG passes `stapler validate` offline. The DMG named by `notarize.file` is still notarized and stapled afterward; the app ticket receipt is `macos-app-notarization.json`. Without the flag, behavior is unchanged.
85
150
 
86
- For `projectType: "swiftpm"` on mac with `appBeforePackage: true` and no `targets.mac.package`, right-release builds the DMG itself (same `dmg-build.mjs` as Tauri) at the package step, after the `.app` is signed, notarized and stapled: it runs `xcrun stapler validate` on the app (refusing to pack an unstapled one), builds the DMG at `notarize.file` (must end `.dmg`), Developer ID signs it (receipt `macos-dmg-signing.json`), then notarizes and staples the DMG as before. Options go in `targets.mac.dmg`: `{ volname, background, iconSize, appPosition, applicationsPosition, window, volumeIcon, out }` (`volname` defaults to the app name). A prePackage hook must no longer build the DMG: preflight fails when its command or arguments mention `dmg` or `hdiutil` (heuristic), because that DMG would carry the unstapled app. Keep a custom DMG flow with `targets.mac.dmg: false` or a `targets.mac.package` step (both run after the staple and skip the automatic build).
151
+ For `projectType: "swiftpm"` on mac with `appBeforePackage: true` and no `targets.mac.package`, right-release builds the DMG itself (same `dmg-build.mjs` as Tauri) at the package step, after the `.app` is signed, notarized and stapled: it runs `xcrun stapler validate` on the app (refusing to pack an unstapled one), builds the DMG at `notarize.file` (must end `.dmg`), Developer ID signs it (receipt `macos-dmg-signing.json`), then notarizes and staples the DMG as before. Options go in `targets.mac.dmg`: `{ volname, background, iconSize, appPosition, applicationsPosition, window, volumeIcon, out }` (`volname` defaults to the app name). A prePackage hook must no longer build the DMG: the config is refused (at build time and by `doctor`) when its command or arguments mention `dmg` or `hdiutil` (heuristic), because that DMG would carry the unstapled app. Keep a custom DMG flow with `targets.mac.dmg: false` or a `targets.mac.package` step (both run after the staple and skip the automatic build).
87
152
 
88
153
  `sign.prePackageFiles` entries may be `.app` bundles (directories). Nested code (Mach-O files, frameworks, XPC services, plug-ins) is signed first, inside-out and without entitlements, then the bundle is signed; the receipt records a tree digest of the bundle. Any other directory is rejected with a clear error.
89
154
 
155
+ ### Per-file signing matrix (macOS)
156
+
157
+ By default every code item is signed with the hardened runtime and no entitlements, so a bundle that needs entitlements on some of its code (an app, a sandboxed extension) declares them per file. Both forms below are optional and backwards compatible; a config of plain path strings signs exactly as before.
158
+
159
+ - `sign.prePackageFiles[]` entries are a path string or `{ file, entitlements?, hardenedRuntime?, options? }`. `entitlements` is a plist path relative to the release config directory. For a `.app` entry the settings apply to the bundle signature.
160
+ - `sign.nestedCode` is a map from a path inside the bundle (`Contents/...`, forward slashes) to `{ entitlements?, hardenedRuntime?, options? }`. It needs exactly one `.app` entry in `sign.prePackageFiles` to resolve against. Nested code the map does not name keeps the defaults.
161
+ - `options` are extra codesign `--options` names (`expires`, `hard`, `host`, `kill`, `library`; `runtime` is always on). `hardenedRuntime` may be `true` or omitted: `false` is a config error because a notarized target cannot ship code without it.
162
+
163
+ Signing order stays inside-out: all nested code, deepest path first (a nested bundle after everything inside it), then the `sign.prePackageFiles` entries in declared order. A `nestedCode` path is signed once, with its own settings, in that sequence; it must exist, must not be a symlink or leave the bundle, and must not be the bundle's main executable (that is signed with the bundle). Every configuration problem found is reported at once, before anything is signed.
164
+
165
+ `right-release doctor` validates the matrix (unknown keys, malformed paths, unsupported option names, entitlement files that are missing, outside the config directory or not property lists); `right-release build --dry-run` only loads the config and does not. Whether a `nestedCode` path exists inside the built bundle is checked at sign time, before the first codesign call. The receipt records, per signed item, the entitlements file digest and the `--options` value, and the build refuses a receipt that disagrees with the plan. Steps that carry settings reach `sign-macos.mjs` as a JSON manifest (`--manifest`, written beside the receipt); steps with only defaults keep the positional form.
166
+
167
+ Worked example, the Pulse bundle (hardened-runtime helpers with no entitlements, a sandboxed Finder Sync extension with its own entitlements, the app with the app entitlements; replaces `@electron/osx-sign` `optionsForFile`):
168
+
169
+ ```js
170
+ sign: {
171
+ prePackageFiles: [
172
+ { file: "dist/staging/Pulse.app", entitlements: "release/entitlements.plist", hardenedRuntime: true },
173
+ ],
174
+ nestedCode: {
175
+ "Contents/Helpers/PulseHelper": { hardenedRuntime: true },
176
+ "Contents/Helpers/pulse-elevate": { hardenedRuntime: true },
177
+ "Contents/Helpers/smartctl": { hardenedRuntime: true },
178
+ "Contents/PlugIns/PulseFinder.appex": { hardenedRuntime: true, entitlements: "mac/Notch/FinderExtension/PulseFinder.entitlements" },
179
+ },
180
+ },
181
+ ```
182
+
183
+ Nested code that the map does not name (frameworks, other helpers) is signed first with the defaults; the appex's own `Contents/MacOS` executable is signed before the appex, and the appex signature then carries the extension entitlements. The Finder extension's entitlements never reach the app and the app's Apple Events entitlement never reaches the extension.
184
+
185
+ ### DMG from an appdmg-style spec
186
+
187
+ `right-release dmg build --app <.app> --out <dmg> --spec <appdmg.json>` (and `targets.mac.dmg.spec` for SwiftPM apps that right-release packs itself) renders an appdmg-style JSON file through the same headless hdiutil pipeline; explicit flags and `targets.mac.dmg` options win over the spec, and the spec's `title` is the volume name (on the command line, `--volname` is required when the spec has no `title`). Relative `background` and `icon` paths resolve against the spec file's directory. Pulse's `release/appdmg.json` works unchanged:
188
+
189
+ ```json
190
+ { "title": "Pulse", "icon-size": 128, "window": { "size": { "width": 640, "height": 420 } },
191
+ "contents": [ { "x": 180, "y": 210, "type": "file", "path": "Pulse.app" },
192
+ { "x": 460, "y": 210, "type": "link", "path": "/Applications" } ] }
193
+ ```
194
+
195
+ Supported: `title`, `icon` (`.icns` volume icon), `background` (PNG or TIFF), `icon-size`, `window.size`, `window.position`, `format: "UDZO"`, `filesystem: "HFS+"`, and `contents` limited to the `.app` (`file`, its name must match `--app`) and a `link` to `/Applications`, where `x`/`y` are icon centres. Unsupported fields (`background-color`, `code-sign`, `license`, other formats or filesystems, extra files or links, `position` entries, anything unknown) fail with `DMG_SPEC_UNSUPPORTED` instead of being ignored. `code-sign` is refused because the release pipeline signs the DMG in its own step (receipt `macos-dmg-signing.json`).
196
+
197
+ ## One gate for GitHub uploads
198
+
199
+ `right-release upload` and `right-release github --release` share one gate (`github-upload-gate.mjs`): both require `--tier patch|update`, the primary checkout with a clean tree, a sealed release built from the current HEAD with matching ownership evidence, and a release config that declares `distribution.provider: "github-releases"` and `distribution.repository` (`--repo` may only repeat it; the `origin` remote is never consulted). The gate makes no network call and also runs on `--dry-run`. Private products publish with `publish-update` and `distribution.provider: "r2-rightapps"`. `github --addon` publishes headless add-ons and takes no tier. Not gated the same way: `github --addon` (no tier), `addon upload` (no provider requirement), and the direct R2 uploaders `upload-large.mjs` and `pub-upload.mjs` (no tier, provider or clean-tree check).
200
+
90
201
  ## Known issues
91
202
 
92
- - `smol-toml` is pinned exactly. A consumer that adds another `smol-toml` gets a second copy (npm audit 2026-10-08, release finding).
93
- - Eleven apps pin this package at different versions, from 0.2.109 to 0.2.135 (npm audit 2026-10-08).
203
+ - `smol-toml` is pinned exactly. A consumer that depends on a different `smol-toml` version gets a second copy.
204
+ - Apps pin this package at different versions; `docs/ADOPTION.md` lists each pin and marks the stale ones.
94
205
  - Prepublish runs the test suite, the Windows signer integration test and the registry parity check.
package/build-release.mjs CHANGED
@@ -25,6 +25,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
26
26
  import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
27
27
  import { admitSourceQa } from "./release-qa-audit.mjs";
28
+ import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
28
29
  import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
29
30
  import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
30
31
  import { createTargetBridge } from "./target-bridge.mjs";
@@ -49,7 +50,7 @@ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
49
50
  // and test-only edits and would force a cold Rust rebuild of every app each time.
50
51
  // Cached target directories must not outlive changes to these signing helpers.
51
52
 
52
- const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "swift-mac-dmg.mjs", "mac-bundle-signing.mjs", "macos-file-evidence.mjs", "swift-mac-app.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs", "msix-build.mjs", "msix-plan.mjs"];
53
+ const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "dmg-spec.mjs", "swift-mac-dmg.mjs", "mac-bundle-signing.mjs", "mac-signing-matrix.mjs", "mac-sign-core.mjs", "macos-file-evidence.mjs", "swift-mac-app.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs", "msix-build.mjs", "msix-plan.mjs"];
53
54
  const PIPELINE_FINGERPRINT = createHash("sha256")
54
55
  .update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
55
56
  .digest("hex")
@@ -106,7 +107,9 @@ const configPath = path.resolve(configName);
106
107
  const invocationRoot = path.dirname(configPath);
107
108
  const repoRoot = git(invocationRoot, ["rev-parse", "--show-toplevel"]);
108
109
  assertPrimaryReleaseCheckout(repoRoot);
109
- const relativeConfig = path.relative(repoRoot, configPath).replaceAll("\\", "/");
110
+ // Native real paths: Git reports the long form of a Windows short name (RUNNER~1).
111
+ const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
112
+ const relativeConfig = path.relative(nativeReal(repoRoot), nativeReal(configPath)).replaceAll("\\", "/");
110
113
  if (relativeConfig.startsWith("../")) fail("release config must live inside the Git repository");
111
114
  const appRoot = path.dirname(configPath);
112
115
  const layout = resolveReleaseLayout({ repoRoot, configPath });
@@ -477,6 +480,15 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
477
480
  addSource(resolveArtifactPath(artifact.file), "updater");
478
481
  if (target.updater?.kind !== "appinstaller") addSource(resolveArtifactPath(artifact.signature), "updater-signature");
479
482
  }
483
+ // Portable contracts ship bare archives (zip / tar.gz) with no installer or
484
+ // updater block: `target.artifacts` entries (paths or { file }) seal with the
485
+ // "portable" role.
486
+ for (const artifact of target.artifacts ?? []) {
487
+ const file = typeof artifact === "string" ? artifact : artifact?.file;
488
+ if (typeof file !== "string" || !file) fail("target.artifacts entries must be paths or { file }");
489
+ addSource(resolveArtifactPath(file), "portable");
490
+ }
491
+ if (sources.size === 0) fail(`nothing to seal: declare installer.artifacts, updater.artifacts or artifacts for the ${platform} target`);
480
492
  for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
481
493
  const temp = `${sealedDir}.tmp-${process.pid}`;
482
494
  if (existsSync(temp)) rmSync(temp, { recursive: true, force: true });
@@ -513,6 +525,8 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
513
525
  commit,
514
526
  ownershipEvidence,
515
527
  qa: qaAdmission,
528
+ // DevTools allowance (owner's fixed design): { mode: "denied" } for every app; content-only only for an allow-listed app.
529
+ devtools: devtoolsEvidence(resolveDevtoolsAllowance({ config, root: configRoot })),
516
530
  platform,
517
531
  cacheKey,
518
532
  // Portable contracts sign only raw executables — installer/embedding
@@ -27,7 +27,10 @@ const commands = new Map([
27
27
  ["ownership", "ownership-command.mjs"],
28
28
  ["ownership-approval", "ownership-approval-command.mjs"],
29
29
  ["ownership-report", "ownership-report-command.mjs"],
30
+ ["ownership-mandatory", "ownership-mandatory-set.mjs"],
30
31
  ["swift-package", "swift-package-command.mjs"],
32
+ ["bundle-scan", "runtime-bundle-scan.mjs"],
33
+ ["identity-scan", "packaged-app-identity.mjs"],
31
34
  ]);
32
35
 
33
36
  const args = process.argv.slice(2);
@@ -170,8 +173,8 @@ function printHelp() {
170
173
  console.log(`right-release <command> [options]
171
174
 
172
175
  Commands:
173
- dmg build --app <.app> --out <dmg> --volname <name> [--background <png>]
174
- [--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h]
176
+ dmg build --app <.app> --out <dmg> --volname <name> [--background <png>] [--spec <appdmg.json>]
177
+ [--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h] [--window-position x,y]
175
178
  Build branded UDZO image without Finder
176
179
  install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id <id>] [--dest /Applications]
177
180
  [--relaunch|--no-relaunch] [--json]
@@ -182,6 +185,10 @@ Commands:
182
185
  Incremental unsigned build & optional local install
183
186
  swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [--dry-run] [--no-sign] [--sign] [--notarize] [--profile <name>]
184
187
  Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
188
+ bundle-scan [--allow-name <basename>]... <bundle-root>...
189
+ Fail when a bundle carries Node/Python runtimes or script sources
190
+ identity-scan --target <path> [--identifier <id>] [--team <OU>] [--cdhash <hex>] [--arch <arch>]...
191
+ Assert codesign Identifier/Team/cdhash/lipo archs; reject ad-hoc
185
192
  upload --release <id> --platform mac|win --tier patch|update
186
193
  Upload one sealed release; never builds
187
194
  qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa and write the sealed QA receipt (a missing receipt warns and is recorded; it never blocks)
@@ -195,7 +202,8 @@ Commands:
195
202
  Publish the gated tarball with the Keychain token
196
203
  (npm-rightkit-publish) and verify registry bytes
197
204
  publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
198
- publish swift [--repo <git-url>] [--dry-run] Test, tag, resolve a fresh exact-pin consumer, and publish RightKitSwift to its git repo
205
+ publish swift [--dry-run] [--evidence <file>] [--workspace <dir>]
206
+ Check main, a clean tree, and the publication evidence, then tag RightKitSwift at its VERSION on origin
199
207
  inventory check [--json] [--warn-only] [--md <file>] [--no-cache]
200
208
  Registry, account, dependency, app & hygiene drift (public endpoints)
201
209
  model promote --authority <app-key> --config <file> [--dry-run]
@@ -215,6 +223,7 @@ Commands:
215
223
  ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
216
224
  ownership revoke-key <fingerprint> Revoke one active device key
217
225
  ownership trust-sync Advance this host's protected trust to the committed key log
226
+ ownership trust-sync --check Read-only: say whether a sync would ask for approval, and for what (exit 3 if so)
218
227
  ownership trust-compare [--json] Print this host's protected-trust digest (Mac is the reference)
219
228
  ownership-approval --draft <input.json> [--out <draft.json>]
220
229
  Draft an exception for native owner approval
@@ -222,6 +231,8 @@ Commands:
222
231
  Verify exact signed exception coverage
223
232
  ownership-report --workspace <dir> [--markdown <file>] [--json-out <file>]
224
233
  Report cross-app ownership & pending requests
234
+ ownership-mandatory [--workspace <dir>] [--app <name>]... [--amendment v2.1] [--enforce] [--json]
235
+ Check each app declares every Table 1 owner at its required version (report by default)
225
236
  cache status [--json] Inspect the shared local build cache
226
237
  cache prune [--dry-run|--apply] [--json] Plan or apply safe shared-target pruning
227
238
  cache migrate --config <file> [--dry-run|--apply] [--json]
@@ -232,8 +243,10 @@ Commands:
232
243
  generate-dmg-background <options> Generate the branded multi-resolution DMG background
233
244
  mirror-root-artifact --file <path> --package-root <dir>
234
245
  Persist artifact in RIGHT_RELEASE_ARTIFACT_DIR
235
- github --release <id> --platform mac|win [--repo owner/repo] [--dry-run]
236
- Attach sealed signed artifacts to a verified GitHub Release
246
+ github --release <id> --tier patch|update --platform mac|win [--config <file>] [--repo owner/repo] [--dry-run]
247
+ Same checks and publisher as upload (explicit tier, github-releases provider + configured repository)
248
+ github --addon <config> --platform mac|win [--repo owner/repo] [--dry-run]
249
+ Attach a sealed headless add-on to an immutable GitHub Release
237
250
 
238
251
  Direct flags are treated as: right-release build <flags>.
239
252
  Build is tier-neutral. Upload requires an explicit tier. Dev builds are incremental and unsigned.`);
@@ -194,8 +194,26 @@ export function assertCommittedCheckoutClean(root, { expectedCommit, expectedTre
194
194
  const output = runGit(path.resolve(root), ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
195
195
  encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv(),
196
196
  });
197
- if (stripOwnLockRecords(Buffer.from(output).toString("utf8")).length) throw new CommittedSourceIncompleteError("committed ownership: protected release checkout has tracked or untracked changes");
197
+ const dirty = stripOwnLockRecords(Buffer.from(output).toString("utf8"));
198
+ if (dirty.length) {
199
+ const error = new CommittedSourceIncompleteError("committed ownership: protected release checkout has tracked or untracked changes");
200
+ // The check name and every offending path travel with the error so the CLI can name them.
201
+ error.blocked = { check: "clean-checkout", paths: statusPaths(dirty) };
202
+ throw error;
203
+ }
198
204
  return true;
199
205
  }
200
206
 
207
+ // Paths from NUL-terminated porcelain v1 records. A rename or copy record is followed by its source path as its own record.
208
+ function statusPaths(status) {
209
+ const records = status.split("\0").filter(Boolean);
210
+ const paths = [];
211
+ for (let index = 0; index < records.length; index += 1) {
212
+ const record = records[index];
213
+ paths.push(record.slice(3));
214
+ if (/^[RC]/.test(record) || /^.[RC]/.test(record)) index += 1;
215
+ }
216
+ return paths;
217
+ }
218
+
201
219
  export const committedOwnershipLimitsForTest = Object.freeze({ MAX_TREE_BYTES, MAX_TOTAL_BLOB_BYTES, MAX_BLOB_BYTES });