@rightkit/release 0.2.161 → 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/disk-policy.json +3 -1
- package/disk-policy.mjs +15 -3
- 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.mjs +3 -3
- package/package.json +1 -1
- package/rightkit-versions.json +4 -3
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/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 {
|
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-versions.json
CHANGED
|
@@ -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",
|
|
@@ -202,7 +202,8 @@
|
|
|
202
202
|
"0.2.156",
|
|
203
203
|
"0.2.158",
|
|
204
204
|
"0.2.159",
|
|
205
|
-
"0.2.160"
|
|
205
|
+
"0.2.160",
|
|
206
|
+
"0.2.161"
|
|
206
207
|
],
|
|
207
208
|
"@rightkit/qa": [
|
|
208
209
|
"0.1.0",
|
|
@@ -512,7 +513,7 @@
|
|
|
512
513
|
"@rightkit/qa": "0.4.8",
|
|
513
514
|
"@rightkit/qa-chrome": "0.1.6",
|
|
514
515
|
"@rightkit/react-shell": "0.6.4",
|
|
515
|
-
"@rightkit/release": "0.2.
|
|
516
|
+
"@rightkit/release": "0.2.161",
|
|
516
517
|
"@rightkit/settings-ui": "0.4.4",
|
|
517
518
|
"@rightkit/shell": "0.1.1",
|
|
518
519
|
"@rightkit/tauri": "0.2.1",
|