@rightkit/release 0.2.160 → 0.2.162
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/cargo-contract.mjs +10 -2
- package/disk-policy.json +3 -1
- package/disk-policy.mjs +15 -3
- package/local-publish.mjs +12 -0
- package/ownership-approval.mjs +11 -4
- package/ownership-command.mjs +39 -8
- package/ownership-dependency-allowlist-seed.mjs +1 -1
- package/ownership-evidence.mjs +5 -4
- package/ownership-key-command.mjs +5 -54
- package/ownership-mandatory-set.json +3 -3
- package/ownership.mjs +3 -3
- package/package.json +1 -1
- package/rightkit-ownership.json +1 -1
- package/rightkit-versions.json +21 -15
package/cargo-contract.mjs
CHANGED
|
@@ -181,11 +181,19 @@ function readToml(file, label) {
|
|
|
181
181
|
return JSON.parse(result.stdout);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// Not product code: SwiftPM `.build` (dependency checkouts and build products), throwaway probe dirs (`*-probe`, `pdfprobe2`), `tmp`, and
|
|
185
|
+
// `tasks/handoffs` (handoff probes and their manifests). They are never built by the product, so they are not gated.
|
|
186
|
+
export function isExcludedScanDirectory(name, parentName = "") {
|
|
187
|
+
if (SKIP_DIRECTORIES.has(name) || name === ".build") return true;
|
|
188
|
+
if (/probe\d*$/i.test(name) || name === "tmp") return true;
|
|
189
|
+
return name === "handoffs" && parentName === "tasks";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function findCargoManifests(root) {
|
|
185
193
|
const found = [];
|
|
186
194
|
const visit = (dir) => {
|
|
187
195
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
188
|
-
if (entry.isDirectory() &&
|
|
196
|
+
if (entry.isDirectory() && isExcludedScanDirectory(entry.name, path.basename(dir))) continue;
|
|
189
197
|
const full = path.join(dir, entry.name);
|
|
190
198
|
if (entry.isDirectory()) visit(full);
|
|
191
199
|
else if (entry.isFile() && entry.name === "Cargo.toml") found.push(full);
|
package/disk-policy.json
CHANGED
|
@@ -47,7 +47,9 @@
|
|
|
47
47
|
{"id": "leftover-node-modules-medium-packages", "platform": "mac", "path": "/Volumes/D/claude/rightkit/packages/*/node_modules.medium-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
48
48
|
{"id": "leftover-legion-snap", "platform": "mac", "path": "/Volumes/D/claude/legion-snap-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
49
49
|
{"id": "leftover-legion-final", "platform": "mac", "path": "/Volumes/D/claude/legion-final-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
50
|
-
{"id": "leftover-pulse-bak", "platform": "win", "path": "${LOCALAPPDATA}\\Programs\\Pulse.bak-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"}
|
|
50
|
+
{"id": "leftover-pulse-bak", "platform": "win", "path": "${LOCALAPPDATA}\\Programs\\Pulse.bak-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
51
|
+
{"id": "qa-evidence-runs", "platform": "mac", "path": "/Volumes/D/claude/*/.cache/rightkit-qa/*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true},
|
|
52
|
+
{"id": "qa-evidence-runs", "platform": "win", "path": "D:\\claude\\*\\.cache\\rightkit-qa\\*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true}
|
|
51
53
|
],
|
|
52
54
|
"protectedSegments": ["Application Support", "Keychains", "Keychain", "HeardRightNext", "Credentials", ".ssh", ".rightkit-managed", ".rightkit-swept"]
|
|
53
55
|
}
|
package/disk-policy.mjs
CHANGED
|
@@ -187,14 +187,25 @@ export function windowsLockProbe(candidate) {
|
|
|
187
187
|
return false;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// True when git tracks anything at or under `target` (a tracked file is never swept). Not in a repository: not tracked.
|
|
191
|
+
export function gitTracksPath(target) {
|
|
192
|
+
const top = spawnSync("git", ["-C", path.dirname(target), "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
193
|
+
if (top.status !== 0) return false;
|
|
194
|
+
const listed = spawnSync("git", ["-C", String(top.stdout).trim(), "ls-files", "-z", "--", target], { encoding: "utf8" });
|
|
195
|
+
if (listed.status !== 0) return true; // cannot prove untracked: keep it
|
|
196
|
+
return String(listed.stdout).length > 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function planSweep({ policy, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), staleDays, openPaths, inUse, tracked = gitTracksPath, size = (p) => pathSize(p, { platform }) } = {}) {
|
|
191
200
|
const key = platformKey(platform);
|
|
192
|
-
const
|
|
201
|
+
const effectiveDays = staleDays ?? policy.staleDays;
|
|
193
202
|
const candidates = [];
|
|
194
203
|
const skipped = [];
|
|
195
204
|
const seen = new Set();
|
|
196
205
|
const lockCheck = inUse ?? (key === "win" ? windowsLockProbe : (p) => inUseByOpenPaths(p, openPaths));
|
|
197
206
|
for (const writer of policy.writers.filter((w) => w.platform === key && w.action === "sweep")) {
|
|
207
|
+
// `--days N` overrides every writer; otherwise a writer may carry its own window (QA evidence: 3 days).
|
|
208
|
+
const cutoff = now - (staleDays ?? writer.staleDays ?? policy.staleDays) * 86400_000;
|
|
198
209
|
let roots; let literal;
|
|
199
210
|
try { roots = writerMatches(writer, { env, home }); literal = literalRoot(expandPolicyPath(writer.path, { env, home })); } catch { continue; }
|
|
200
211
|
const units = [];
|
|
@@ -216,12 +227,13 @@ export function planSweep({ policy, platform = process.platform, env = process.e
|
|
|
216
227
|
if (st.isSymbolicLink()) { skipped.push({ ...base, reason: "symlink" }); continue; }
|
|
217
228
|
if (!isInsideRoot(unit, root)) { skipped.push({ ...base, reason: "outside-root" }); continue; }
|
|
218
229
|
if (isProtectedPath(unit, policy)) { skipped.push({ ...base, reason: "protected" }); continue; }
|
|
230
|
+
if (writer.skipTracked && tracked(unit)) { skipped.push({ ...base, reason: "tracked" }); continue; }
|
|
219
231
|
if (hasFreshContent(unit, cutoff)) { skipped.push({ ...base, reason: "fresh" }); continue; }
|
|
220
232
|
if (lockCheck(unit)) { skipped.push({ ...base, reason: "in-use" }); continue; }
|
|
221
233
|
candidates.push({ ...base, root, bytes: size(unit), mtime: new Date(st.mtimeMs).toISOString() });
|
|
222
234
|
}
|
|
223
235
|
}
|
|
224
|
-
return { schema: 1, platform: key, staleDays, candidates, skipped, totalBytes: candidates.reduce((s, c) => s + c.bytes, 0) };
|
|
236
|
+
return { schema: 1, platform: key, staleDays: effectiveDays, candidates, skipped, totalBytes: candidates.reduce((s, c) => s + c.bytes, 0) };
|
|
225
237
|
}
|
|
226
238
|
|
|
227
239
|
export const HOLDING_DIR = ".rightkit-swept";
|
package/local-publish.mjs
CHANGED
|
@@ -79,6 +79,17 @@ async function defaultLoadWorkspaceModules(workspaceRoot) {
|
|
|
79
79
|
return { results, checkNpmTarball: upload.checkNpmTarball };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Feature sets that must at least compile, beyond the tested ones. A crate whose tested sets already
|
|
84
|
+
* include `--all-features` needs none. `checkFeatureSets` (ci/gate.config.json) lists them explicitly for
|
|
85
|
+
* crates with mutually exclusive features; otherwise one `--all-features` check runs. Mirrored in scripts/ci/gate.mjs.
|
|
86
|
+
*/
|
|
87
|
+
export function allFeatureCheckSets(entry = {}) {
|
|
88
|
+
const tested = entry.featureSets ?? [[]];
|
|
89
|
+
if (tested.some((features) => features.includes("--all-features"))) return [];
|
|
90
|
+
return entry.checkFeatureSets ?? [["--all-features"]];
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
/** The CI gate's commands for one unit (scripts/ci/gate.mjs npmGate/rustGate), mirrored exactly. */
|
|
83
94
|
export function localTestPlan({ kind, unit, dir, config, manifest }) {
|
|
84
95
|
if (kind === "npm") {
|
|
@@ -100,6 +111,7 @@ export function localTestPlan({ kind, unit, dir, config, manifest }) {
|
|
|
100
111
|
if (entry.alsoSerial) steps.push({ command: "cargo", args: ["test", "-p", unit, ...features, "--no-fail-fast", "--", "--test-threads=1", "--show-output"], test: true });
|
|
101
112
|
}
|
|
102
113
|
steps.push({ command: "cargo", args: ["clippy", "-p", unit, "--no-deps", "--all-targets", ...featureSets[0], "--", "-D", "warnings"], test: false });
|
|
114
|
+
for (const features of allFeatureCheckSets(entry)) steps.push({ command: "cargo", args: ["check", "-p", unit, "--all-targets", ...features], test: false });
|
|
103
115
|
return { id: `rust:${unit}`, featureLabels: featureSets.map((features) => features.join(" ") || "default"), steps };
|
|
104
116
|
}
|
|
105
117
|
|
package/ownership-approval.mjs
CHANGED
|
@@ -285,15 +285,22 @@ function versionAtLeast(actual, minimum) {
|
|
|
285
285
|
}
|
|
286
286
|
return true;
|
|
287
287
|
}
|
|
288
|
+
// The protected policy bundle (scanner source, catalog, registry, dependency allowlist) is still fingerprinted and a pin
|
|
289
|
+
// that differs from the running bundle is reported, but only as a warning: trust-sync, the human-approved way to
|
|
290
|
+
// re-pin it, is retired (Adrian, decisions.md), so no check refuses on a missing or stale pinned bundle.
|
|
291
|
+
export const TRUST_SYNC_RETIRED = "right-release ownership trust-sync is retired: no release, ownership or QA check needs a human-approved policy bundle any more. The bundle is still fingerprinted; `right-release ownership trust-compare` reports drift as a warning with a semantic diff.";
|
|
292
|
+
|
|
293
|
+
export const POLICY_BUNDLE_DRIFT_WARNING = "ownership policy bundle (scanner source, catalog, registry or dependency allowlist) differs from the bundle pinned in protected trust (warning only; trust-sync is retired)";
|
|
288
294
|
export function validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest, policyBundleDigest } = {}) {
|
|
289
295
|
const problems = [];
|
|
290
|
-
|
|
296
|
+
const warnings = [];
|
|
297
|
+
if (!trust) return { ok: false, problems: ["external ownership trust unavailable"], warnings };
|
|
291
298
|
if (!versionAtLeast(scannerVersion, trust.minimumScannerVersion)) problems.push("scanner version missing, malformed, or below protected minimum");
|
|
292
299
|
if (!versionAtLeast(catalogVersion, trust.minimumCatalogVersion)) problems.push("catalog version missing, malformed, or below protected minimum");
|
|
293
300
|
if (trust.minimumCatalogDigest && trust.minimumCatalogDigest !== catalogDigest) problems.push("catalog digest below protected pinned digest");
|
|
294
|
-
// Trust written before the bundle existed has no policyBundle
|
|
295
|
-
if (trust.policyBundle?.digest && policyBundleDigest !== undefined && trust.policyBundle.digest !== policyBundleDigest)
|
|
296
|
-
return { ok: problems.length === 0, problems };
|
|
301
|
+
// Trust written before the bundle existed has no policyBundle: nothing to compare.
|
|
302
|
+
if (trust.policyBundle?.digest && policyBundleDigest !== undefined && trust.policyBundle.digest !== policyBundleDigest) warnings.push(POLICY_BUNDLE_DRIFT_WARNING);
|
|
303
|
+
return { ok: problems.length === 0, problems, warnings };
|
|
297
304
|
}
|
|
298
305
|
function protectedLocation(file, platform, { runner = spawnSync, inspect = lstatSync, stat = statSync, requireNonElevated = true } = {}) {
|
|
299
306
|
const problems = [];
|
package/ownership-command.mjs
CHANGED
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
//
|
|
5
5
|
// right-release ownership [--app <dir>] [--json]
|
|
6
6
|
// right-release ownership [--app <dir>] --audit-json (Legion governance.capability-ownership provider)
|
|
7
|
-
// right-release ownership trust-compare [--json] (print this host's protected-trust digest)
|
|
7
|
+
// right-release ownership trust-compare [--json] (print this host's protected-trust digest; policy-bundle drift is a warning, never a block)
|
|
8
|
+
// right-release ownership trust-sync (retired: prints why and exits 0)
|
|
8
9
|
// right-release ownership --write-manifest [--rightkit <rightkit>] [--approve-new-capabilities]
|
|
9
10
|
// --approve-new-capabilities permits new ids during local generation only.
|
|
10
11
|
// Existing owner changes are always refused; installation needs protected authorization.
|
|
11
12
|
import { realpathSync } from "node:fs";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { formatTable } from "./ownership.mjs";
|
|
15
|
+
import { SCANNER_VERSION, formatTable } from "./ownership.mjs";
|
|
15
16
|
import { writeOwnershipCatalog } from "./ownership-catalog.mjs";
|
|
16
17
|
import { createOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
17
18
|
import { spawnSync } from "./background-process.mjs";
|
|
18
19
|
import { main as stagedMain } from "./ownership-staged.mjs";
|
|
19
20
|
import { loadTrustedExternal } from "./ownership-approval.mjs";
|
|
20
|
-
import { trustComparison } from "./ownership-policy-bundle.mjs";
|
|
21
|
+
import { diffPolicyBundles, runningPolicyBundle, trustComparison } from "./ownership-policy-bundle.mjs";
|
|
22
|
+
import { POLICY_BUNDLE_DRIFT_WARNING, TRUST_SYNC_RETIRED } from "./ownership-approval.mjs";
|
|
21
23
|
import { isBlockingFinding } from "./ownership-verdict.mjs";
|
|
22
24
|
|
|
23
25
|
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -27,14 +29,41 @@ function flag(args, name) {
|
|
|
27
29
|
return i >= 0 ? args[i + 1] : undefined;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** Semantic drift of the bundle pinned in protected trust against the bundle this checkout runs; null when none is pinned or they match. */
|
|
33
|
+
export function policyBundleDrift(trust, { runningBundle = () => runningPolicyBundle({ scannerVersion: SCANNER_VERSION }) } = {}) {
|
|
34
|
+
const pinned = trust?.policyBundle;
|
|
35
|
+
if (!pinned?.digest) return null;
|
|
36
|
+
let running;
|
|
37
|
+
try { running = runningBundle(); } catch (error) { return { warning: `policy bundle could not be computed: ${error.message}`, diff: null }; }
|
|
38
|
+
if (running.digest === pinned.digest) return null;
|
|
39
|
+
return { warning: POLICY_BUNDLE_DRIFT_WARNING, pinnedDigest: pinned.digest, runningDigest: running.digest, diff: diffPolicyBundles(pinned, running) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function driftLines(drift) {
|
|
43
|
+
const lines = [`WARNING: ${drift.warning}`];
|
|
44
|
+
if (drift.diff) {
|
|
45
|
+
const { filesAdded, filesRemoved, filesChanged, scannerVersion, catalogChanged, registryChanged, allowlistChanged, qaFloorChanged } = drift.diff;
|
|
46
|
+
if (scannerVersion) lines.push(` scanner version: ${scannerVersion.from} -> ${scannerVersion.to}`);
|
|
47
|
+
for (const [label, list] of [["files added", filesAdded], ["files removed", filesRemoved], ["files changed", filesChanged]]) if (list.length) lines.push(` ${label}: ${list.join(", ")}`);
|
|
48
|
+
const moved = Object.entries({ catalog: catalogChanged, registry: registryChanged, allowlist: allowlistChanged, "qa floor": qaFloorChanged }).filter(([, changed]) => changed).map(([name]) => name);
|
|
49
|
+
if (moved.length) lines.push(` changed digests: ${moved.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
/** Print this host's protected-trust digest. The Mac's digest is the reference; see docs/OWNERSHIP-ENFORCEMENT.md. */
|
|
31
|
-
export function trustCompare(args, { print, trustLoader = loadTrustedExternal } = {}) {
|
|
55
|
+
export function trustCompare(args, { print, trustLoader = loadTrustedExternal, runningBundle } = {}) {
|
|
32
56
|
let state;
|
|
33
57
|
try { state = trustLoader(); } catch (error) { print(`[right-release] trust-compare: protected trust unreadable (${error.message})`); return 1; }
|
|
34
58
|
if (!state?.trust) { print("[right-release] trust-compare: no protected trust on this host (not enrolled)"); return 1; }
|
|
35
59
|
const { digest, components } = trustComparison(state.trust);
|
|
36
|
-
|
|
37
|
-
|
|
60
|
+
const drift = policyBundleDrift(state.trust, runningBundle ? { runningBundle } : undefined);
|
|
61
|
+
if (args.includes("--json")) print(JSON.stringify({ platform: process.platform, digest, components, ...(drift ? { warnings: [drift] } : {}) }, null, 2));
|
|
62
|
+
else {
|
|
63
|
+
print(`trust digest (${process.platform}): ${digest}`);
|
|
64
|
+
for (const [name, value] of Object.entries(components)) print(` ${name}: ${typeof value === "object" && value !== null ? JSON.stringify(value) : value ?? "none"}`);
|
|
65
|
+
if (drift) for (const line of driftLines(drift)) print(line);
|
|
66
|
+
}
|
|
38
67
|
return 0;
|
|
39
68
|
}
|
|
40
69
|
|
|
@@ -45,7 +74,8 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
45
74
|
}
|
|
46
75
|
if (args[0] === "trust-compare") return trustCompare(args, { print, trustLoader });
|
|
47
76
|
if (args.includes("--staged")) return stagedMain(args, { print });
|
|
48
|
-
if ([
|
|
77
|
+
if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
|
|
78
|
+
if (["enroll", "approve-key", "revoke-key"].includes(args[0])) {
|
|
49
79
|
const { main: keyMain } = await import("./ownership-key-command.mjs");
|
|
50
80
|
return keyMain(args, { runner: gitRunner, print });
|
|
51
81
|
}
|
|
@@ -79,7 +109,7 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
79
109
|
: result.coverage.length
|
|
80
110
|
? { type: "ownership-coverage", message: `${result.coverage.length} ownership coverage gap(s)` }
|
|
81
111
|
: undefined;
|
|
82
|
-
print(JSON.stringify({ ok: checked.ok, ...(checked.enrollmentPending ? { enrollmentPending: true, warning: checked.warning } : {}), ...(error ? { error } : {}), ...(blocked ? { blocked } : {}), rows: result.rows, violations: result.violations, covered: result.covered, problems: [...result.problems, ...checked.floor.problems], coverage: result.coverage, metadata: result.metadata }, null, 2));
|
|
112
|
+
print(JSON.stringify({ ok: checked.ok, ...(checked.enrollmentPending ? { enrollmentPending: true, warning: checked.warning } : {}), ...(error ? { error } : {}), ...(blocked ? { blocked } : {}), rows: result.rows, violations: result.violations, covered: result.covered, problems: [...result.problems, ...checked.floor.problems], ...(checked.warnings ? { warnings: checked.warnings } : {}), coverage: result.coverage, metadata: result.metadata }, null, 2));
|
|
83
113
|
} else {
|
|
84
114
|
print(`RightKit ownership: ${path.basename(app)}`);
|
|
85
115
|
print(formatTable(result.rows));
|
|
@@ -88,6 +118,7 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
88
118
|
}
|
|
89
119
|
for (const p of result.problems) print(` ${checked.enrollmentPending ? "DIAGNOSTIC" : "EXCEPTION"} ${p}`);
|
|
90
120
|
print(verdictLine(checked, result, blocked));
|
|
121
|
+
for (const warning of checked.warnings ?? []) print(` WARNING ${warning}`);
|
|
91
122
|
}
|
|
92
123
|
// CI lanes may pass `--advisory-when-pending`: before approval keys are enrolled the scan asserts
|
|
93
124
|
// nothing, so findings print as diagnostics and the step exits 0. Release lanes never pass it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Propose or regenerate dependency-allowlist baselines from each registered repository's CURRENT direct
|
|
3
|
-
// dependencies. This only edits the committed proposal file; it
|
|
3
|
+
// dependencies. This only edits the committed proposal file; it is advisory until a protected trust copy carries it (trust-sync is retired).
|
|
4
4
|
// node ownership-dependency-allowlist-seed.mjs --workspace <dir> [--write]
|
|
5
5
|
// Existing approvals are never removed (union with the current file); block-tier catalog dependencies
|
|
6
6
|
// and RightKit packages are never recorded as approvals.
|
package/ownership-evidence.mjs
CHANGED
|
@@ -78,7 +78,8 @@ function createEvidence(repoRoot, {
|
|
|
78
78
|
// When a protected registry exists, key enrollment controls signing
|
|
79
79
|
// authority; scanner and catalog floors still apply during pending enrollment.
|
|
80
80
|
// Step 2: the protected trust pins the policy bundle (catalog + registry + dependency allowlist + scanner version +
|
|
81
|
-
// scanner source digests).
|
|
81
|
+
// scanner source digests). It is fingerprinted and compared for drift only: a pin that differs, or a bundle that cannot be
|
|
82
|
+
// computed, is a warning, never a block (trust-sync is retired).
|
|
82
83
|
let policyBundleDigest;
|
|
83
84
|
let bundleProblem = null;
|
|
84
85
|
if (trustState.trust?.policyBundle?.digest) {
|
|
@@ -88,7 +89,7 @@ function createEvidence(repoRoot, {
|
|
|
88
89
|
const floor = trustState.trust
|
|
89
90
|
? validateOwnershipFloor(trustState.trust, { scannerVersion, catalogVersion, catalogDigest, ...(policyBundleDigest !== undefined ? { policyBundleDigest } : {}) })
|
|
90
91
|
: { ok: false, problems: ["protected ownership floor is unavailable until trust registry enrollment"] };
|
|
91
|
-
|
|
92
|
+
floor.warnings = [...(floor.warnings ?? []), ...(bundleProblem ? [bundleProblem] : [])];
|
|
92
93
|
// Real paths on both sides: Git's root is resolved (macOS /private/var), callers may pass /var or /tmp.
|
|
93
94
|
const real = (p) => { try { return realpathSync.native(p); } catch { return path.resolve(p); } };
|
|
94
95
|
const requestedRoot = path.relative(real(root), real(scanRoot)).replaceAll("\\", "/") || ".";
|
|
@@ -152,7 +153,7 @@ function createEvidence(repoRoot, {
|
|
|
152
153
|
ok: scans.every(({ scanned }) => scanned.ok),
|
|
153
154
|
findings: scans.flatMap(({ scanned, prefix }) => (scanned.findings ?? []).map((entry) => rebase(entry, prefix))),
|
|
154
155
|
violations: scans.flatMap(({ scanned, prefix }) => (scanned.violations ?? []).map((entry) => rebase(entry, prefix))),
|
|
155
|
-
problems: [...scans.flatMap(({ scanned }) => scanned.problems ?? []), ...evidenceDiagnostics, ...(enrollmentPending ? floor.problems :
|
|
156
|
+
problems: [...scans.flatMap(({ scanned }) => scanned.problems ?? []), ...evidenceDiagnostics, ...(enrollmentPending ? floor.problems : [])],
|
|
156
157
|
rows: scans.flatMap(({ scanned, root: registered }) => (scanned.rows ?? []).map((row) => ({ ...row, root: registered }))),
|
|
157
158
|
covered: scans.flatMap(({ scanned, prefix }) => (scanned.covered ?? []).map((entry) => rebase(entry, prefix))),
|
|
158
159
|
coverage: contentCoverage,
|
|
@@ -182,7 +183,7 @@ function createEvidence(repoRoot, {
|
|
|
182
183
|
checkedAt: new Date().toISOString(),
|
|
183
184
|
};
|
|
184
185
|
const warning = enrollmentPending ? "WARNING: ownership approval enrollment is pending; findings remain backlog diagnostics and no protected evidence or approvals are asserted" : undefined;
|
|
185
|
-
return { ok: !enrollmentPending && result.ok && floor.ok && contentCoverage.length === 0, ...(enrollmentPending ? { enrollmentPending: true, warning } : {}), evidence, result, floor, contentCoverage };
|
|
186
|
+
return { ok: !enrollmentPending && result.ok && floor.ok && contentCoverage.length === 0, ...(enrollmentPending ? { enrollmentPending: true, warning } : {}), ...(floor.warnings.length ? { warnings: floor.warnings } : {}), evidence, result, floor, contentCoverage };
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
/** Create scan evidence bound to committed source, catalog, and signed exceptions. */
|
|
@@ -5,10 +5,9 @@ import os from "node:os";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawnSync, spawnSyncInteractive, windowsPowerShellEnvironment } from "./background-process.mjs";
|
|
7
7
|
import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "./ownership-key-log.mjs";
|
|
8
|
-
import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
8
|
+
import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, TRUST_SYNC_RETIRED, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
9
9
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
10
|
-
import { buildMacInvocation,
|
|
11
|
-
import { summarizeRequestFile } from "./ownership-trust-summary.mjs";
|
|
10
|
+
import { buildMacInvocation, hasTerminal } from "./ownership-key-elevation.mjs";
|
|
12
11
|
|
|
13
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
14
13
|
const LOG = ".rightkit/ownership-keys.json";
|
|
@@ -321,7 +320,7 @@ function buildWindowsElevationInvocation(nodeExe, trustScript, requestPath, dige
|
|
|
321
320
|
const parentScript = `$ErrorActionPreference='Stop'; try { $env:NODE_OPTIONS=$null; $env:NODE_PATH=$null; $p=Start-Process -FilePath ${quote(powershell)} -ArgumentList ${quote(`-NoProfile -EncodedCommand ${childEncoded}`)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode } catch { exit 1 }`;
|
|
322
321
|
return { parentExecutable: powershell, parentArgs: ["-NoProfile", "-EncodedCommand", Buffer.from(parentScript, "utf16le").toString("base64")], childScript };
|
|
323
322
|
}
|
|
324
|
-
export const __testOnly = Object.freeze({ elevate, buildWindowsElevationInvocation, assertSafeMetadataParents, detectMinimizeHook,
|
|
323
|
+
export const __testOnly = Object.freeze({ elevate, buildWindowsElevationInvocation, assertSafeMetadataParents, detectMinimizeHook, installCheckpoint });
|
|
325
324
|
function trustState() { return loadTrustedExternal(); }
|
|
326
325
|
function currentLogTrust(root, log, trust = trustState(), authority, injected = false) {
|
|
327
326
|
if (!trust.trust?.rootFingerprint) throw new Error("protected root trust is not enrolled on this host");
|
|
@@ -353,10 +352,10 @@ export async function main(args, deps = {}) {
|
|
|
353
352
|
const print = deps.print ?? console.log;
|
|
354
353
|
const app = path.resolve(flag(args, "--app") ?? process.cwd());
|
|
355
354
|
const root = deps.root ?? repoRoot(app, run);
|
|
355
|
+
if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
|
|
356
356
|
try {
|
|
357
357
|
if (!platform) throw new Error(`ownership key management unsupported platform: ${process.platform}`);
|
|
358
358
|
// Delegate BEFORE taking the command lock: the desktop-session child needs that same lock.
|
|
359
|
-
if (args[0] === "trust-sync" && !args.includes("--check") && needsDesktopDelegation({ hostPlatform: deps.hostPlatform, env: deps.env, run, elevatedCheck: deps.elevatedCheck })) return delegateTrustSync(deps, run, print);
|
|
360
359
|
const helper = deps.helper ?? helperFor(run);
|
|
361
360
|
const lockPath = deps.lockPath ?? path.join(os.tmpdir(), "rightkit-key-command.lock");
|
|
362
361
|
let lockFd;
|
|
@@ -371,8 +370,7 @@ export async function main(args, deps = {}) {
|
|
|
371
370
|
if (verb === "enroll") return await enroll(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
372
371
|
if (verb === "approve-key") return await approve(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
373
372
|
if (verb === "revoke-key") return await revoke(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
374
|
-
|
|
375
|
-
throw new Error("expected enroll, approve-key, revoke-key, or trust-sync");
|
|
373
|
+
throw new Error("expected enroll, approve-key, or revoke-key (trust-sync is retired)");
|
|
376
374
|
} finally {
|
|
377
375
|
closeSync(lockFd);
|
|
378
376
|
rmSync(lockPath, { force: true });
|
|
@@ -508,53 +506,6 @@ async function resumeRootEnrollment(log, d) {
|
|
|
508
506
|
print(`Ownership root enrollment recovery completed: ${pin}; active keys: ${state.keys.length}`);
|
|
509
507
|
return 0;
|
|
510
508
|
}
|
|
511
|
-
function delegateTrustSync(deps, run, print) {
|
|
512
|
-
const result = delegateToDesktop({ run, command: deps.desktopCommand ?? [process.execPath, ...(process.argv[1] ? [path.resolve(process.argv[1])] : []), ...process.argv.slice(2)], ...(deps.desktopOptions ?? {}) });
|
|
513
|
-
if (result.output) print(result.output.trimEnd());
|
|
514
|
-
if (result.status !== 0) throw new Error(`desktop-session trust-sync failed (exit ${result.status}); see output above`);
|
|
515
|
-
return 0;
|
|
516
|
-
}
|
|
517
|
-
/** Advance this host's protected checkpoint to the committed key log (after another host approved or revoked a key). */
|
|
518
|
-
async function trustSync(_args, d) {
|
|
519
|
-
d = { ...d, label: "RightKit ownership trust-sync", gate: trustSyncGate(d) };
|
|
520
|
-
const log = readLog(d.root);
|
|
521
|
-
const pin = log.events[0]?.payload?.rootFingerprint;
|
|
522
|
-
const state = verifyKeyLog(log, { rootFingerprint: pin });
|
|
523
|
-
if (!state.ok) throw new Error(`committed key log is invalid: ${state.problems.join("; ")}`);
|
|
524
|
-
const trust = (d.trustLoader ?? trustState)();
|
|
525
|
-
if (!trust?.trust?.rootFingerprint) throw new Error("protected root trust is not enrolled on this host; run ownership enroll");
|
|
526
|
-
if (trust.trust.rootFingerprint !== pin) throw new Error("committed key log root does not match this host's protected trust");
|
|
527
|
-
const baseline = d.baseline ?? parse(readFileSync(path.join(here, "ownership-default-policy.json"), "utf8"), "ownership default policy");
|
|
528
|
-
if (_args.includes("--check")) {
|
|
529
|
-
// Read-only: say whether a sync would ask for approval, and for what. Asks nothing, elevates nothing, writes no trust.
|
|
530
|
-
const request = makeTrustRequest(log, pin, baseline, d.authority);
|
|
531
|
-
try {
|
|
532
|
-
const summary = (d.summarize ?? summarizeRequestFile)({ file: request.file, digest: request.digest, existing: trust.trust, ...(d.summaryInputs ?? {}) });
|
|
533
|
-
if (!summary.changed) { d.print("trust already current: no approval needed"); return 0; }
|
|
534
|
-
d.print(summary.lines.join("\n"));
|
|
535
|
-
d.print("approval needed: trust-sync on this host would ask for the change above");
|
|
536
|
-
return 3;
|
|
537
|
-
} finally { await import("node:fs").then(({ unlinkSync }) => unlinkSync(request.file)).catch(() => {}); }
|
|
538
|
-
}
|
|
539
|
-
const installed = await installCheckpoint(log, pin, baseline, d);
|
|
540
|
-
if (installed === false) { d.print("trust already current"); return 0; }
|
|
541
|
-
postVerify(d.root, log, d);
|
|
542
|
-
d.print(`Ownership trust synced to key-log sequence ${log.events.length - 1}; active keys: ${state.keys.length}`);
|
|
543
|
-
return 0;
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Informed-consent gate: summarize the exact request bytes (digest-checked) against the installed trust,
|
|
547
|
-
* skip when nothing changes, otherwise require explicit approval before any elevation prompt.
|
|
548
|
-
*/
|
|
549
|
-
function trustSyncGate(d) {
|
|
550
|
-
return (request) => {
|
|
551
|
-
const existing = (d.trustLoader ?? trustState)()?.trust ?? null;
|
|
552
|
-
const summary = (d.summarize ?? summarizeRequestFile)({ file: request.file, digest: request.digest, existing, ...(d.summaryInputs ?? {}) });
|
|
553
|
-
if (!summary.changed) return false;
|
|
554
|
-
(d.confirm ?? confirmTrustChange)({ lines: summary.lines, run: d.run, interactiveRun: d.run.interactive, print: d.print, hostPlatform: d.hostPlatform, ...(d.confirmOptions ?? {}) });
|
|
555
|
-
return true;
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
509
|
async function installCheckpoint(log, rootFingerprint, baseline, d) {
|
|
559
510
|
const request = makeTrustRequest(log, rootFingerprint, baseline, d.authority);
|
|
560
511
|
try {
|
|
@@ -375,10 +375,10 @@
|
|
|
375
375
|
"profile": "desktop",
|
|
376
376
|
"owners": [
|
|
377
377
|
{
|
|
378
|
-
"name": "rightkit-
|
|
378
|
+
"name": "rightkit-agent-tools",
|
|
379
379
|
"ecosystem": "cargo",
|
|
380
|
-
"min": "0.
|
|
381
|
-
"registryMax": "0.
|
|
380
|
+
"min": "0.1.2",
|
|
381
|
+
"registryMax": "0.1.2"
|
|
382
382
|
}
|
|
383
383
|
]
|
|
384
384
|
}
|
package/ownership.mjs
CHANGED
|
@@ -562,7 +562,7 @@ function scanInternal(appRoot, manifest, { inventory = fileInventory(appRoot), t
|
|
|
562
562
|
if (blocked.has(`${d.ecosystem}:${d.name}`) || evaluateDependency(allowlistPolicy.allowlist, repositoryIdentity, { ecosystem: d.ecosystem, name: d.name, kind: sourceKind(d.source) }, { owners }).allowed) continue;
|
|
563
563
|
findings.push({ fingerprint: ALLOWLIST_FINDING_ID, capability: "dependency-allowlist", package: "RightKit dependency allowlist", tier: allowlistPolicy.tier, file: d.file, line: 0, kind: "dependency", ecosystem: d.ecosystem, name: d.name,
|
|
564
564
|
alias: d.alias, section: d.section, requirement: d.requirement, features: d.features, defaultFeatures: d.defaultFeatures, local: d.local, source: d.source, resolvedVersion: d.resolvedVersion,
|
|
565
|
-
text: `${d.ecosystem} dependency ${d.name} is not in ${ALLOWLIST_FILE}; propose it there
|
|
565
|
+
text: `${d.ecosystem} dependency ${d.name} is not in ${ALLOWLIST_FILE}; propose it there (advisory until protected trust carries the allowlist; trust-sync is retired)`, provenance: d.source });
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
568
|
const blockRules = manifest.capabilities.flatMap((c) => c.fingerprints.filter((f) => f.type === "dependency" && findingPolicy(c, f).tier === "block").map((f) => ({ c, f })));
|
|
@@ -598,8 +598,8 @@ function shippedAllowlist() {
|
|
|
598
598
|
try { const parsed = JSON.parse(readFileSync(shippedAllowlistFile, "utf8")); return validateAllowlist(parsed).ok ? parsed : null; } catch { return null; }
|
|
599
599
|
}
|
|
600
600
|
/**
|
|
601
|
-
* Which allowlist applies and how hard. The protected trust copy
|
|
602
|
-
* registered in trust and blocks.
|
|
601
|
+
* Which allowlist applies and how hard. The protected trust copy governs every repository
|
|
602
|
+
* registered in trust and blocks. When no trust copy applies, or while approval enrollment is pending, the shipped proposal
|
|
603
603
|
* is advisory and only for repositories it has a baseline for.
|
|
604
604
|
*/
|
|
605
605
|
export function dependencyAllowlistPolicy(trust, repositoryIdentity, { pending = false, shipped } = {}) {
|
package/package.json
CHANGED
package/rightkit-ownership.json
CHANGED
|
@@ -2193,7 +2193,7 @@
|
|
|
2193
2193
|
"Declaration-only: the lifecycle, bundle writer, redaction, protocol and reader are not an ownership fingerprint.",
|
|
2194
2194
|
"Windows capture lives in this crate (src/windows, cfg(windows)) and is served out of process by rightkit-record-helper in serve mode.",
|
|
2195
2195
|
"rightkit-child-process owns helper spawning, supervision and restart; this crate only speaks the protocol over the streams it is given.",
|
|
2196
|
-
"The macOS helper
|
|
2196
|
+
"The macOS ScreenCaptureKit helper (helpers/macos, Swift) ships with this crate sources and implements the same protocol; the crate never sends input. Capturing needs the OS Screen Recording permission on macOS."
|
|
2197
2197
|
]
|
|
2198
2198
|
},
|
|
2199
2199
|
{
|
package/rightkit-versions.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"stagedNpm": {
|
|
16
16
|
"@rightkit/app-update": "0.2.5",
|
|
17
17
|
"@rightkit/ax": "0.3.2",
|
|
18
|
-
"@rightkit/ci": "0.2.
|
|
18
|
+
"@rightkit/ci": "0.2.45",
|
|
19
19
|
"@rightkit/doc-render": "0.1.6",
|
|
20
20
|
"@rightkit/hooks": "0.1.2",
|
|
21
21
|
"@rightkit/i18n": "0.1.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@rightkit/qa": "0.4.8",
|
|
32
32
|
"@rightkit/qa-chrome": "0.1.6",
|
|
33
33
|
"@rightkit/react-shell": "0.6.4",
|
|
34
|
-
"@rightkit/release": "0.2.
|
|
34
|
+
"@rightkit/release": "0.2.162",
|
|
35
35
|
"@rightkit/settings-ui": "0.4.4",
|
|
36
36
|
"@rightkit/tauri": "0.2.1",
|
|
37
37
|
"@rightkit/tauri-shell": "0.1.1",
|
|
@@ -201,7 +201,9 @@
|
|
|
201
201
|
"0.2.155",
|
|
202
202
|
"0.2.156",
|
|
203
203
|
"0.2.158",
|
|
204
|
-
"0.2.159"
|
|
204
|
+
"0.2.159",
|
|
205
|
+
"0.2.160",
|
|
206
|
+
"0.2.161"
|
|
205
207
|
],
|
|
206
208
|
"@rightkit/qa": [
|
|
207
209
|
"0.1.0",
|
|
@@ -330,7 +332,8 @@
|
|
|
330
332
|
"0.1.0"
|
|
331
333
|
],
|
|
332
334
|
"@rightkit/ci": [
|
|
333
|
-
"0.2.43"
|
|
335
|
+
"0.2.43",
|
|
336
|
+
"0.2.44"
|
|
334
337
|
]
|
|
335
338
|
},
|
|
336
339
|
"cargo": {
|
|
@@ -365,7 +368,7 @@
|
|
|
365
368
|
"rightkit-contracts": "0.1.5",
|
|
366
369
|
"rightkit-crash": "0.3.0",
|
|
367
370
|
"rightkit-credentials": "0.2.0",
|
|
368
|
-
"rightkit-daemon": "0.2.
|
|
371
|
+
"rightkit-daemon": "0.2.3",
|
|
369
372
|
"rightkit-db": "0.4.0",
|
|
370
373
|
"rightkit-device": "0.1.3",
|
|
371
374
|
"rightkit-embed": "0.4.0",
|
|
@@ -388,8 +391,8 @@
|
|
|
388
391
|
"rightkit-ownership": "0.2.1",
|
|
389
392
|
"rightkit-pdf": "0.3.0",
|
|
390
393
|
"rightkit-playback": "0.2.1",
|
|
391
|
-
"rightkit-qa": "0.
|
|
392
|
-
"rightkit-qa-driver": "0.
|
|
394
|
+
"rightkit-qa": "0.4.0",
|
|
395
|
+
"rightkit-qa-driver": "0.4.0",
|
|
393
396
|
"rightkit-review": "0.2.0",
|
|
394
397
|
"rightkit-sandbox": "0.4.0",
|
|
395
398
|
"rightkit-search": "0.2.1",
|
|
@@ -435,7 +438,7 @@
|
|
|
435
438
|
"rightkit-control": "0.3.0",
|
|
436
439
|
"rightkit-crash": "0.3.0",
|
|
437
440
|
"rightkit-credentials": "0.2.0",
|
|
438
|
-
"rightkit-daemon": "0.2.
|
|
441
|
+
"rightkit-daemon": "0.2.3",
|
|
439
442
|
"rightkit-db": "0.4.0",
|
|
440
443
|
"rightkit-device": "0.1.3",
|
|
441
444
|
"rightkit-embed": "0.4.0",
|
|
@@ -463,8 +466,8 @@
|
|
|
463
466
|
"rightkit-pdf": "0.3.0",
|
|
464
467
|
"rightkit-playback": "0.2.1",
|
|
465
468
|
"rightkit-process": "0.4.0",
|
|
466
|
-
"rightkit-qa": "0.
|
|
467
|
-
"rightkit-qa-driver": "0.
|
|
469
|
+
"rightkit-qa": "0.4.0",
|
|
470
|
+
"rightkit-qa-driver": "0.4.0",
|
|
468
471
|
"rightkit-qts": "0.2.6",
|
|
469
472
|
"rightkit-qts-ggml": "0.1.2",
|
|
470
473
|
"rightkit-qts-ggml-sys": "0.1.2",
|
|
@@ -493,7 +496,7 @@
|
|
|
493
496
|
"@rightkit/app-shell": "0.6.2",
|
|
494
497
|
"@rightkit/app-update": "0.2.5",
|
|
495
498
|
"@rightkit/ax": "0.3.2",
|
|
496
|
-
"@rightkit/ci": "0.2.
|
|
499
|
+
"@rightkit/ci": "0.2.45",
|
|
497
500
|
"@rightkit/doc-render": "0.1.6",
|
|
498
501
|
"@rightkit/git": "0.2.42",
|
|
499
502
|
"@rightkit/hooks": "0.1.2",
|
|
@@ -510,7 +513,7 @@
|
|
|
510
513
|
"@rightkit/qa": "0.4.8",
|
|
511
514
|
"@rightkit/qa-chrome": "0.1.6",
|
|
512
515
|
"@rightkit/react-shell": "0.6.4",
|
|
513
|
-
"@rightkit/release": "0.2.
|
|
516
|
+
"@rightkit/release": "0.2.161",
|
|
514
517
|
"@rightkit/settings-ui": "0.4.4",
|
|
515
518
|
"@rightkit/shell": "0.1.1",
|
|
516
519
|
"@rightkit/tauri": "0.2.1",
|
|
@@ -610,7 +613,8 @@
|
|
|
610
613
|
"0.2.11",
|
|
611
614
|
"0.2.12",
|
|
612
615
|
"0.2.14",
|
|
613
|
-
"0.2.16"
|
|
616
|
+
"0.2.16",
|
|
617
|
+
"0.3.0"
|
|
614
618
|
],
|
|
615
619
|
"rightkit-sandbox": [
|
|
616
620
|
"0.1.0",
|
|
@@ -743,13 +747,15 @@
|
|
|
743
747
|
],
|
|
744
748
|
"rightkit-daemon": [
|
|
745
749
|
"0.2.0",
|
|
746
|
-
"0.2.1"
|
|
750
|
+
"0.2.1",
|
|
751
|
+
"0.2.2"
|
|
747
752
|
],
|
|
748
753
|
"rightkit-job-queue": [
|
|
749
754
|
"0.3.0"
|
|
750
755
|
],
|
|
751
756
|
"rightkit-qa-driver": [
|
|
752
|
-
"0.3.0"
|
|
757
|
+
"0.3.0",
|
|
758
|
+
"0.3.1"
|
|
753
759
|
]
|
|
754
760
|
}
|
|
755
761
|
}
|