@rightkit/release 0.4.0 → 0.5.0
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.
- package/README.md +59 -7
- package/adhoc-entitlements.mjs +8 -0
- package/adoption-evidence.mjs +650 -0
- package/advisory-exceptions.mjs +371 -0
- package/apple-profile-create.mjs +113 -10
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1079 -0
- package/binary-license-notices.mjs +161 -0
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +39 -0
- package/deterministic-scans.mjs +966 -0
- package/development-signing.mjs +2 -1
- package/host-upload-refusal.mjs +90 -0
- package/local-publish.mjs +142 -19
- package/local-qualify.mjs +728 -0
- package/local-runtime.mjs +319 -0
- package/lock-graph.mjs +51 -0
- package/managed-cargo.mjs +4 -1
- package/ownership-approval.mjs +23 -6
- package/ownership-dependency-allowlist.json +2 -0
- package/ownership-mandatory-set.json +158 -68
- package/ownership-mandatory-set.mjs +82 -6
- package/package.json +6 -2
- package/pipeline-admission.mjs +375 -0
- package/pipeline-config.mjs +100 -0
- package/pipeline-generators.json +4 -0
- package/pipeline-generators.mjs +137 -0
- package/pipeline-receipts.mjs +302 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/production-mode.mjs +42 -0
- package/publication-gate.mjs +94 -32
- package/publish-cargo.mjs +24 -7
- package/publish-swift.mjs +12 -3
- package/qa-control-scan.mjs +4 -0
- package/release-config.mjs +330 -0
- package/release-switch-scan.mjs +44 -5
- package/release.mjs +14 -4
- package/rightkit-ownership.json +57 -0
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +191 -131
- package/runtime-budget-classes.json +8 -0
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +128 -31
- package/scan-digest.mjs +107 -0
- package/sign-qa-bundle.mjs +210 -0
- package/stage-sidecar.mjs +99 -15
- package/swap-helper-bundle.mjs +7 -3
- package/workflow-yaml.mjs +177 -0
- package/bin/darwin/rightkit-swap.sign.json +0 -6
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
32
32
|
|
|
33
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
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.
|
|
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. Two daemon host features are declarations rather than bans: `rightkit-daemon` `remote` is refused (`remote-feature-undeclared`) unless the app sets `releaseSwitches.remoteHost: true`, and the loopback feature `remote-loopback` (CodeRight's loopback HTTP + bearer host) is refused (`remote-loopback-undeclared`) unless the app sets `releaseSwitches.remoteLoopback: true`. Neither declaration covers the other; a `remote` reached only through `remote-loopback` in the `cargo tree` indentation is judged by the loopback rule, any other path to `remote` (a direct request, `testing`) still needs `remoteHost`, and a tree without indentation counts every `remote` as direct. Both values must be booleans.
|
|
36
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
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
38
|
|
|
@@ -69,14 +69,14 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
69
69
|
| Build (SwiftPM apps) | `swift-package` (`windows-stage`, `msix`, `mac-app`) (config schema 1: `windows.{exe,out,searchDirs,include,dependencies,publisher,assets}`, `mac.{executable,bundleId,out,frameworks,resources,entitlements}`) |
|
|
70
70
|
| Install | `install-app` |
|
|
71
71
|
| Publish | `publish`, `upload`, `publish-update`, `github`, `mirror-root-artifact` |
|
|
72
|
-
| Signing | `sign-windows`, `sign-updater`, `signing-agent`, `create-mac-updater`, `sign-dev-artifact`, `sign-candidate` |
|
|
72
|
+
| Signing | `sign-windows`, `sign-updater`, `signing-agent`, `create-mac-updater`, `sign-dev-artifact`, `sign-candidate`, `sign-qa-bundle` |
|
|
73
73
|
| Engine locator | `engine-locate --engine cutright\|genright [--sha256 <hex>] [--json]`: the verified route to the installed CutRight or GenRight engine, also importable as `locateEngine` from `engine-locator.mjs`. The path is fixed (`<LocalAppData or ~/Library/Application Support>/Orthic Labs/<App>/current/bin/<engine>`), must be a regular file (no symlink or junction), and must pass signature and identity checks (macOS: strict codesign, Developer ID Team ID, fixed Identifier, hardened runtime, not ad hoc; Windows: valid Authenticode with the Orthic Labs signer). An optional sha256 pins the exact build. There is no environment variable, path option or PATH search, so release builds never trust a caller-supplied engine path. Exit 1 with a typed `ENGINE_*` code on any failure |
|
|
74
74
|
| Candidate signing | `sign-candidate --platform win\|mac --artifact <dir> --candidate-receipt <receipt.json> --out <binding.json> [--manifest <sign.json>] [--publisher <subject>] [--dry-run]`: signs the exact unsigned candidate bytes in place instead of rebuilding from source. The candidate receipt (`{ schema: 1, files: [{ path, sha256 }] }`) is checked first and any mismatch refuses; Windows signs through Azure Artifact Signing (`--publisher` pins the MSIX signer subject), macOS through Developer ID with fixed identifiers from `--manifest`. Writes a binding receipt (`rebuilt: false`, candidate sha -> signed sha per file). Never builds, notarizes or publishes |
|
|
75
75
|
| Dev artifact signing | `sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]`: Developer ID signs an existing CI dev artifact per file with fixed identifiers (macOS). It verifies the recorded hashes in the payload metadata (default `payload.json`, `{ schema: 1, files: [{ path, sha256 }] }`) first, signs each sign-manifest entry (`file` relative to the artifact, `identifier` required, nested code before its bundle) through the dev-signing identity (missing Developer ID: `DEV_MAC_SIGNING_DEVELOPER_ID_REQUIRED`, no ad hoc fallback), verifies with `codesign --verify --strict`, reads back Identifier and TeamIdentifier, refreshes the metadata hashes, and writes evidence JSON. Never builds, notarizes or publishes. The payload metadata must sit outside every signed entry (`SIGN_DEV_ARTIFACT_METADATA_IN_SIGNED_ENTRY`). An optional declarative `refresh` in the sign manifest recomputes digests that other JSON files record about signed bytes: `{ "refreshRoot": "<dir inside the artifact>", "refresh": [{ "file", "pointer" (JSON pointer to an existing string), "compute": "sha256-file" \| "sha256-tree-v1" \| "verify-sha256-file", "of", "format"?, "currentMatches"? }] }` (`file` and `of` relative to `refreshRoot`; `sha256-tree-v1` = sha256 over the sorted relative paths, each as path, NUL, 8-byte big-endian length, bytes; `verify-sha256-file` writes nothing and refuses a digest that changed; `format` embeds the digest in a locator string through one `{value}`; `currentMatches` refuses to overwrite a value of another kind, such as a signed attestation locator). Order is enforced: operations run after the entries they read are signed and before the first entry that contains a refreshed file, so that bundle (the wrapper) signs last over the final bytes; an operation may not read what a later one writes. JSON is rewritten in place with key order kept, and a file the writer would have to reformat is refused. Evidence gains a `refresh` list |
|
|
76
76
|
| Checks | `deps`, `hardening`, `harden`, `hardeningscan`, `cargo-target` |
|
|
77
|
-
| Ownership | `ownership`, `ownership-approval`, `ownership-report` |
|
|
77
|
+
| Ownership | `ownership`, `ownership-approval`, `ownership-report`, `ownership-mandatory`, `adoption-evidence` |
|
|
78
78
|
| Test | `test` (the app's declared suites) |
|
|
79
|
-
| Legal | `license-notices npm` |
|
|
79
|
+
| Legal | `license-notices npm`, `license-notices cargo`, `license-notices binaries` |
|
|
80
80
|
| Other | `dev`, `doctor`, `suite-doctor`, `qa`, `dmg`, `cache`, `apple`, `inventory`, `model`, `addon`, `hooks`, `lsclean`, `generate-dmg-background`, `publish cargo`, `publish npm`, `publish swift` |
|
|
81
81
|
|
|
82
82
|
## Declared test suites, web build, npm notices and artifact mirroring
|
|
@@ -135,6 +135,26 @@ its declared MIT or ISC license, labelled as a fallback; any other license witho
|
|
|
135
135
|
app version, the `pnpm-lock.yaml` sha256 and `generated-by=right-release license-notices npm`, so equal inputs give equal bytes.
|
|
136
136
|
`--check` writes nothing and exits 1 when the file differs. `--output` and `--product-name` override the config.
|
|
137
137
|
|
|
138
|
+
### `right-release license-notices cargo`
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
legal: { cargoNotices: {
|
|
142
|
+
output: "legal/THIRD_PARTY_CARGO_LICENSES.html",
|
|
143
|
+
template: "legal/about.hbs",
|
|
144
|
+
config: "legal/about.toml", // optional cargo-about config
|
|
145
|
+
manifestPath: "src-tauri/Cargo.toml", // optional Cargo manifest
|
|
146
|
+
features: ["desktop"], // or allFeatures: true
|
|
147
|
+
} }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Runs `cargo about generate --locked --fail` through managed Cargo, with declared config, manifest, features & template.
|
|
151
|
+
Inputs & output stay inside app root. Missing inputs, unresolved licenses, command failure or empty output fail generation.
|
|
152
|
+
`--output` overrides config; `--check` writes nothing & exits 1 when generated bytes differ.
|
|
153
|
+
`rightkit-legal rust-licenses` delegates to this route.
|
|
154
|
+
|
|
155
|
+
`license-notices binaries` generates notices for declared native binaries; `--receipt` checks staged non-catalog programs
|
|
156
|
+
against declared coverage. Both routes accept `--config`, `--output` & `--check`.
|
|
157
|
+
|
|
138
158
|
### Mirroring an artifact (`mirror-root-artifact`)
|
|
139
159
|
|
|
140
160
|
```js
|
|
@@ -168,6 +188,8 @@ Audit ad hoc or on a weekly schedule with `right-release qa audit [--app <dir>]
|
|
|
168
188
|
|
|
169
189
|
`right-release publish cargo --crate <name> --local` (and the batch form `--crates a,b,c | --crates-file <file>`) publishes from the Mac **or from Windows** under the committed local-build grant (`.rightkit-local-build.json`, `allow: "build-test-publish"`, naming this repository); `publish npm --package @rightkit/<name> --local` stays Mac only (the npm token comes from the macOS Keychain and `@rightkit/release` ships a signed helper), and `--local` refuses any other host (Linux) and any host without the grant. The gate is the same on both hosts: the unit's own `ci/gate.config.json` commands run with zero failures and no skipped evidence, the artifact sha256 is recorded, and the existing `enforcePublicationGate` and publisher run, including the registry readback. The evidence carries one receipt for the publishing host: `hostPolicy: "darwin-only"` with a `darwin` run on the Mac, `hostPolicy: "win32-only"` (plus a `hostPolicyReason`) with a `win32` run on Windows; the gate requires that host's receipt and `isDarwinOnlyPolicy`, `isWin32OnlyPolicy` and `singleHostOfPolicy` in `publication-gate.mjs` are the one definition. A crate whose `ci/gate.config.json` `os` list lacks the publishing host (`macos` or `windows`) is refused. Windows publishes with the credential Cargo stores itself (`~/.cargo/credentials.toml`): no `--token` is passed, read or logged. A runtime-budget crate needs the measurement of the publishing host only, under the same evidence policy that the budget file may not contradict.
|
|
170
190
|
|
|
191
|
+
`ci/gate.config.json` `rust.<crate>.consumerRows` adds gate rows that check the crate through a separate Cargo workspace: `{ "manifestPath": "ci/consumers/<name>/Cargo.toml", "args": [...], "kind": "check", "targets": ["<triple>"], "locked": true }` runs `cargo check --manifest-path <manifestPath> --locked --target <triple> <args>` once per target, after the crate's own checks, and records each as an evidence step (so `--from-evidence` requires it). Only `manifestPath` is required; unknown fields are refused. Clippy with `-D warnings` runs on every `featureSets` entry, not only the first.
|
|
192
|
+
|
|
171
193
|
## Local app install (macOS)
|
|
172
194
|
|
|
173
195
|
Agents must not run `osascript`, `open`, `screencapture`, or `notifyutil -p` directly. Install a signed build through:
|
|
@@ -178,7 +200,7 @@ right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id d
|
|
|
178
200
|
|
|
179
201
|
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.
|
|
180
202
|
|
|
181
|
-
The shipped `bin/darwin/rightkit-swap` is Developer ID signed.
|
|
203
|
+
The shipped `bin/darwin/rightkit-swap` is Developer ID signed. Local publish adds it via `right-release publish npm --package @rightkit/release --local` on the Mac (managed `rightkit cargo build --release -p rightkit-fs --bin rightkit-swap`, `codesign --options runtime --timestamp`, strict verify). CI qualification adds it via `publish.yml` build-swap → sign-swap → pack (`docs/CI.md`); sign-swap needs `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_KEYCHAIN_PASSWORD` & `APPLE_DEVELOPER_ID` in the `release` environment, & stays blocked until root provisions the certificate pair. Exporting that identity requires an Aqua login session: SSH export fails `errSecInteractionNotAllowed` (-25308) & the Aqua signing agent exposes only codesign/updater/notarize/staple, no key export — do not retry either route. `install-app` refuses a helper that exists but is not validly signed (`SWAP_HELPER_UNSIGNED`, naming the fix); with no helper at all it uses the journaled two-rename fallback and logs it (`swap-fallback`, `replaced {swap}`).
|
|
182
204
|
|
|
183
205
|
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.
|
|
184
206
|
|
|
@@ -212,6 +234,36 @@ RightKit QA is a mandatory component: every Orthic Labs app adopts it. `assertQa
|
|
|
212
234
|
- **Warned:** a `@rightkit/qa`, `@rightkit/qa-chrome` or `rightkit-qa` pin behind the latest published version in `rightkit-versions.json` (`[right-release] QA warning: <app> runs RightKit QA behind the latest published version: <pin> -> <latest> (<file>)`). **Refused** when the lag is a breaking bump (`breakingBump` of `rightkit-drift`: a minor bump below 1.0, a major from 1.0). `@rightkit/qa-chrome` is lag-checked but its presence is not enforced.
|
|
213
235
|
- **Exempt:** only a repository that is not an app. `qa-adoption-exemptions.json` (`nonAppRepositories`: `{ repositoryIdentity, kind: "library" | "tooling", reason }`, matched against the git `origin`) is RightKit-owned, so a change needs a RightKit release; the RightKit source tree is recognised by `packages/release/package.json` named `@rightkit/release`. There is no per-app opt-out, config key or environment switch.
|
|
214
236
|
|
|
237
|
+
## Ownership commands
|
|
238
|
+
|
|
239
|
+
`right-release ownership [--app <dir>] [--json]` scans app copies of shared capabilities; `--staged` scans staged index.
|
|
240
|
+
`ownership enroll`, `approve-key` & `revoke-key` manage native signing keys. `ownership trust-sync` advances protected trust
|
|
241
|
+
to committed key log through authenticated approval; `trust-sync --check` previews required approval & `trust-compare --json`
|
|
242
|
+
reads current digest.
|
|
243
|
+
|
|
244
|
+
`right-release ownership-mandatory [--workspace <dir>] [--app <name>]... [--amendment v2.1] [--enforce] [--json]`
|
|
245
|
+
checks Table 1 owner declarations & minimum versions. Default mode reports findings; `--enforce` applies current mandatory
|
|
246
|
+
policy. M21 and M23-M25 (issue tracker `@rightkit/issues-tracker`, end-to-end journeys via `rightkit-qa` / `@rightkit/qa`; both unpublished; M24 applies to every profile, M25 to desktop and apple) remain pending until qualified owner receipts authorize signed policy activation through authenticated
|
|
247
|
+
trust-sync. Their `pendingOwners` & M22 `{ role, entrypoints }` applicability records are report metadata; neither changes
|
|
248
|
+
enforcement. D4 is ratified; pending activation does not mean pending ratification.
|
|
249
|
+
|
|
250
|
+
## Adoption evidence (`adoption-evidence`, PK17-6)
|
|
251
|
+
|
|
252
|
+
`right-release adoption-evidence --manifest <published-release-manifest.json> [--packets <dir>] [--app <dir> [--adoption <file>] [--target <target>]] [--json]` validates the handoff manifest (packet 01 section 6) and, with `--app`, an app's adoption record. Exit 0 no findings, 1 findings, 2 usage error. CHANGELOG and deletion-list prose never satisfy a check; every check reads a hash, a lockfile, a saved `cargo tree` or a JSON receipt.
|
|
253
|
+
|
|
254
|
+
- **Manifest:** schema; package field rules per event (`publish`, `qualify-existing`); capability `satisfied` recomputed from its packages (`fetchedConsumer.ok`, passing qualification rows, evidence paths); contract rollups; every task `ready` and `readyForAdoption` re-derived (an N/A or unresolved task is never ready, a deferred slice is never satisfied); the selected snapshot hash and the evidence snapshots when the manifest's directory is present; duplicate JSON keys are refused; and the AP id set must equal the ids extracted from the frozen packets `02-*.md`..`07-*.md` (`- [ ] AP-XX-nn` lines) exactly (`ap-missing`, `ap-extra`, `packet-duplicate-task`; no packets is `packets-missing`). `--packets` defaults to the directory above the manifest's directory.
|
|
255
|
+
- **Adoption record** (`<app>/rightkit-adoption.json`, schema 1): `app`, `manifestSha256` (the manifest file's hash; a stale record is `adoption-stale`), `target`, optional `locks`, `cargoTree { file, sha256 }` and `tasks.<AP-id>.adopted[] { capability, packages, entrypoints, receipt { file, sha256 } }`. A task must be `ready` in the manifest and every capability it requires must be adopted. Each adopted package must be pinned by the lockfile at exactly the manifest version and integrity from the crates.io registry (`Cargo.lock`), the npm registry (`pnpm-lock.yaml` v9) or the tag commit (`Package.resolved`), with no other version of the name in the lock. For cargo, the saved `cargo tree -e features` must show that exact version, every declared feature active and published, and the declared feature set equal to a passing qualification row for the target's host. The active target must be published and qualified.
|
|
256
|
+
- **Receipt** (JSON only, schema 1): `kind` `test` or `run`, `app`, `capability`, one of the adopted `entrypoint`s (a real file inside the app, optional `#symbol` that must appear in it), `packages` naming the pinned `<id>@<version>`, `target`, a `command`, `exitCode: 0`, `finishedAt`, and `lockfileSha256.<cargo|pnpm|swift>` equal to the current lockfile (a receipt from before the last lock change is refused).
|
|
257
|
+
|
|
258
|
+
## QA-only ad hoc bundle signing (`sign-qa-bundle`)
|
|
259
|
+
|
|
260
|
+
`right-release sign-qa-bundle --app <abs .app> --qa-root <abs dir> [--entitlements <abs plist>] [--out <evidence.json>]` ad hoc signs an assembled QA bundle (for example an Accessibility QA copy) that lives inside a QA scratch root. It exists because `codesign --force --deep --sign -` sent through the signing agent (SSH or non-Aqua session) is refused by design; those refusals are unchanged and this command never calls the agent.
|
|
261
|
+
|
|
262
|
+
- macOS only. Refused before any command runs: a relative path, `/Applications` or `~/Applications` (lexical or resolved), a bundle whose real path is not inside the real QA root (links are resolved first), a QA root that is `/`, the home directory or an install location, and a target that is not a `.app` directory.
|
|
263
|
+
- Nested code is signed inside-out, deepest first (`Contents/Frameworks/*` frameworks and dylibs, other `*.dylib`, `*.xpc`, nested `*.app`, `Contents/Library/LoginItems`, extra `Contents/MacOS` executables), then the outer bundle, each as `/usr/bin/codesign --force --sign - [--entitlements <reduced>] <item>`. Never `--deep`, never a real identity. `codesign --verify --strict` runs on the bundle last.
|
|
264
|
+
- `--entitlements` is reduced like ad hoc development signing (only `com.apple.security.cs.*` and `com.apple.security.device.audio-input` survive) and applies to the outer bundle only.
|
|
265
|
+
- The receipt (returned, and written with `--out`) records `signature: "adhoc"`, `releaseArtifact: false`, `qaOnly: true` and the items signed in order. The result is never notarized, sealed, published or installed; use the Developer ID paths for anything that ships.
|
|
266
|
+
|
|
215
267
|
## Entry points
|
|
216
268
|
|
|
217
269
|
Bin: `right-release` (`cli/right-release.mjs`). The package has no `exports` map. The CLI is the supported surface.
|
|
@@ -220,7 +272,7 @@ Bin: `right-release` (`cli/right-release.mjs`). The package has no `exports` map
|
|
|
220
272
|
|
|
221
273
|
| Dependency | Range | Note |
|
|
222
274
|
|---|---|---|
|
|
223
|
-
| `smol-toml` | `1.9.
|
|
275
|
+
| `smol-toml` | `1.9.1` (exact) | Used to parse lock and manifest files |
|
|
224
276
|
|
|
225
277
|
Peer dependencies: none.
|
|
226
278
|
|
|
@@ -401,7 +453,7 @@ Any app that ships more than the ffmpeg sidecar (MailRight: `mailright-engine`,
|
|
|
401
453
|
|
|
402
454
|
- **`cmake` (Orthic-Labs/rightkit#489):** `{ kind: "cmake", name: "whisper-cli", repo: "https://github.com/ggml-org/whisper.cpp", commit: "<40 hex>", target?: "whisper-cli", output?: "bin/whisper-cli", cmakeArgs?: ["-DBUILD_SHARED_LIBS=OFF"], platformArgs?: { mac: ["-DGGML_METAL=ON"], win: [...] }, generator?, smoke?: ["--help"], sha256?, dest?, clean?, platforms? }`. It fetches exactly that commit into the fixed `.right-release/cmake/<name>/src` (`git init`, `fetch --depth 1 origin <commit>`, detach; a checkout already at the commit is reused, one with local edits or at another commit is refused with `STAGE_SIDECAR_SOURCE_MISMATCH`), configures and builds `target` in `.right-release/cmake/<name>/build` (`CMAKE_OSX_ARCHITECTURES` is set from the target; `universal-apple-darwin` builds `arm64;x86_64` and checks `lipo -archs`), finds the executable (`output`, then `Release/` layouts, `.exe` on Windows), optionally pins its sha256, stages it as `<name>-<triple>[.exe]` and smoke-runs it when the target can run on this host. `cmakeArgs` and `platformArgs` accept only `-DNAME=value` definitions. Toolchain controls (#691, all optional, defaults unchanged): `cmakeBin: "<name or path>"` runs that cmake (a bare name is looked up on PATH, a path with separators is resolved against the app root); `generator` is per entry and `platformGenerators: { win: "Visual Studio 17 2022" }` overrides it for one platform (`-G`); `architecture: "x64"` (any `-A` platform: `x64`, `Win32`, `ARM64`) adds `-A <arch>` after `-G`. With no `cmakeBin`, a Windows host uses `cmake` from PATH (`where cmake`) when there is one, else the Visual Studio-bundled cmake (`vswhere.exe` under `%ProgramFiles(x86)%\Microsoft Visual Studio\Installer`, `-latest -products * -requires Microsoft.VisualStudio.Component.VC.CMake.Project -property installationPath`, then `Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`), else plain `cmake`. The receipt row carries `cmakeBin` (when not plain `cmake`) and `generator`. There is no environment-variable override: the staging recipe has none for any other tool.
|
|
403
455
|
- **`prebuilt` (#490):** verifies a host-built binary already staged at `<stageDir>/<name>-<triple>` (or copied from `from`) against an app manifest, never building or downloading: `manifest: { file, entry: "binaries.{platform}", sha256?: "sha256", approvalSha256?: "approval_sha256", approvalAlgorithm?: "approval_hash_algorithm" }` (`{platform}` is `macos-aarch64`, `windows-x86_64`, ...). The raw sha256 must match; when an approval hash is named it must match too, with `raw-sha256` or `macho-codesign-strip-linkedit-v1` (`macho-approval-hash.mjs`: the sha256 of a thin Mach-O with its code signature removed and `__LINKEDIT` normalised, so a signed and an unsigned copy of one build agree; macOS only, `codesign --remove-signature` on a private copy). A missing file, entry or digest fails closed. With `from: { aarch64: "<path>", x86_64: "<path>" }` the entry also works on `universal-apple-darwin` (#692): each arch is verified against its own manifest entry (`macos-aarch64`, `macos-x86_64`; the approval algorithm default is the macOS one), then both are lipo-merged by the same helper `ffmpeg`, `onnxruntime` and `cmake` use into `<name>-universal-apple-darwin` (both archs required, `STAGE_SIDECAR_NOT_UNIVERSAL` otherwise; nothing is merged after a failed slice). With `from` omitted the inputs are the staged `<name>-aarch64-apple-darwin` and `<name>-x86_64-apple-darwin`; a plain string `from` is refused for universal. On a thin target an object `from` picks the entry for that arch. The row carries `platform: "macos-universal"` and per-arch `slices`.
|
|
404
|
-
- **`remote` archives (#491):** `extract: "<member file name>"` (with `memberSha256?`, `executable?`) unpacks one member of a zip or tgz after the archive digest passes, so a Windows zip needs no `command` entry.
|
|
456
|
+
- **`remote` archives (#491):** `extract: "<member file name>"` (with `memberSha256?`, `executable?`) unpacks one member of a zip or tgz after the archive digest passes, so a Windows zip needs no `command` entry. A bare name must be the base name of exactly one member; a name with `/` (`bin/x64-win/DirectML.dll`) must equal exactly one member path (case-sensitive), for archives that repeat a base name in several directories. Absolute, drive-prefixed, backslash, empty, `.`/`..` segment and glob-character paths are refused, and the output stays at `dest`.
|
|
405
457
|
- **`ffmpeg` mixed variants, fetch and probes (#491):** `variants: { ffmpeg: "export-ass" }` lets one entry take the export-ass ffmpeg with the decode ffprobe (a tool not named uses `variant`). `sources: { ffmpeg: { url, sha256, member }, ffprobe: {...} }` downloads the bytes the catalog only pins: the archive is digest-checked, the member is unpacked and its sha256 must equal the APPROVED catalog row's content digest, so a swapped archive can never stage. A source may instead be a local path (#692): `sources: { ffmpeg: { path: "../vendor/ffmpeg-lgpl", member?: "ffmpeg" } }` (also per arch: `{ aarch64: { path }, x86_64: { path } }`). `path` is relative to the app root and may step out of it; it names the binary itself or a directory holding `<tool>[.exe]` or `bin/<tool>[.exe]` (`member` renames the file). Nothing is downloaded and there is no archive digest, but the file must still equal the APPROVED catalog row content digest (`STAGE_SIDECAR_HASH_MISMATCH` otherwise); notices, probe and universal lipo are unchanged; a missing file is `STAGE_SIDECAR_BINARY_NOT_FOUND`. `probe: true` runs `-version` and `-encoders` on a staged binary this host can run: the configuration must say `--disable-gpl` and `--disable-nonfree` and must not enable GPL, nonfree, libx264, libx265, libxvid or libfdk-aac, and ffmpeg may not list those encoders (`STAGE_SIDECAR_FFMPEG_PROBE_FAILED`; another architecture or OS is recorded as `skipped-cross-target`). The catalog has no Windows `export-ass` row yet (only `export-ass-zimg`); that row belongs to the rightkit-ffmpeg crate.
|
|
406
458
|
- **`universal-apple-darwin` for `ffmpeg` and `onnxruntime` (#492):** each arch row is staged thin under `.right-release/slices/<triple>/` and the files are lipo-merged into `<stageDir>/<tool>-universal-apple-darwin` (onnxruntime: into `resourceDir`), requiring both archs and every row approved (onnxruntime also accepts `pinned`). For `ffmpeg`, `{arch}`, `{triple}` and `{stageDir}` in the `stage` command's arguments name the slice (it runs once per slice), `sources` need the per-arch form `{ ffmpeg: { aarch64: {...}, x86_64: {...} } }`, one source offer lists both slices, and the receipt rows carry `slices`. A merged file identical to the staged one is not rewritten.
|
|
407
459
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Ad hoc signatures cannot carry profile-restricted entitlements (iCloud, keychain-access-groups, application-identifier):
|
|
2
|
+
// AMFI would kill the app at launch. Ad hoc mode keeps only the hardened-runtime relaxations and audio input.
|
|
3
|
+
// Dependency-free so the QA ad hoc signer and development signing share one reduction.
|
|
4
|
+
export const isAdhocSafeEntitlement = (key) => key.startsWith("com.apple.security.cs.") || key === "com.apple.security.device.audio-input";
|
|
5
|
+
|
|
6
|
+
export function adhocSafeEntitlements(entitlements) {
|
|
7
|
+
return Object.fromEntries(Object.entries(entitlements ?? {}).filter(([key]) => isAdhocSafeEntitlement(key)));
|
|
8
|
+
}
|