@rightkit/release 0.2.161 → 0.2.163
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/cli/right-release.mjs +2 -2
- package/disk-command.mjs +70 -24
- package/disk-policy.json +13 -1
- package/disk-policy.mjs +122 -6
- package/disk-stable-root.mjs +168 -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.mjs +3 -3
- package/package.json +1 -1
- package/rightkit-versions.json +13 -8
- package/runtime-budget-classes.json +4 -0
package/cli/right-release.mjs
CHANGED
|
@@ -247,8 +247,8 @@ Commands:
|
|
|
247
247
|
disk check [--json] Per-writer sizes vs caps and free-space floor (exit 3 on warning)
|
|
248
248
|
disk sweep [--dry-run] [--days N] [--delete] [--json]
|
|
249
249
|
Move stale idle heavy data to the heavy volume (delete only with --delete)
|
|
250
|
-
disk run check, dry-run sweep, move sweep, warn (the scheduled job)
|
|
251
|
-
disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows)
|
|
250
|
+
disk run self-check, check, dry-run sweep, move sweep, warn (the scheduled job)
|
|
251
|
+
disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows), bound to the stable per-user copy
|
|
252
252
|
deps --check|--audit|--update Shared dependency lane
|
|
253
253
|
hardening <artifact...> Run the Right Suite hardening scan
|
|
254
254
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
package/disk-command.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
import { spawnSync } from "./background-process.mjs";
|
|
7
7
|
import { compareVersions } from "./rightkit-sync.mjs";
|
|
8
8
|
import {
|
|
9
|
-
DISK_EXIT_WARNING, applySweep, checkDisk, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy,
|
|
9
|
+
DISK_EXIT_WARNING, applySweep, checkDisk, ensureScratchRoots, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy,
|
|
10
10
|
} from "./disk-policy.mjs";
|
|
11
|
+
import { evaluateBinding, installStableRoot, readScheduleBinding, selfCheck } from "./disk-stable-root.mjs";
|
|
11
12
|
|
|
12
13
|
export const LAUNCHD_LABEL = "dev.rightkit.disk-hygiene";
|
|
13
14
|
export const WINDOWS_TASK = "RightKit Disk Hygiene";
|
|
@@ -18,6 +19,10 @@ export function renderCheck(result) {
|
|
|
18
19
|
const size = w.error ? `error: ${w.error}` : w.matches === 0 ? "absent" : `${formatBytes(w.bytes)} / cap ${formatBytes(w.capBytes)}`;
|
|
19
20
|
lines.push(` ${w.id.padEnd(28)} ${w.action.padEnd(6)} ${size}${w.over ? " OVER CAP" : ""}`);
|
|
20
21
|
}
|
|
22
|
+
if (result.unknown?.length) {
|
|
23
|
+
lines.push(`Unknown top-level entries (${result.unknown.length}):`);
|
|
24
|
+
for (const u of result.unknown) lines.push(` ${(u.symlink ? "symlink" : formatBytes(u.bytes)).padStart(9)} ${String(u.ageDays).padStart(4)}d ${u.path}`);
|
|
25
|
+
}
|
|
21
26
|
for (const w of result.warnings) lines.push(`WARNING: ${w}`);
|
|
22
27
|
return lines.join("\n");
|
|
23
28
|
}
|
|
@@ -162,48 +167,81 @@ Register-ScheduledTask -TaskName '${WINDOWS_TASK}' -Action $a -Trigger $t -Setti
|
|
|
162
167
|
return { platform: "win", script };
|
|
163
168
|
}
|
|
164
169
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
171
|
+
|
|
172
|
+
/** CLI to copy into the stable root: --cli / RIGHT_RELEASE_INSTALLED_CLI, else this very package when it is not a checkout
|
|
173
|
+
* (the usual `right-release disk schedule install` from the installed package), else the discovered installed package. */
|
|
174
|
+
export function sourceCli({ explicit, env = process.env, self = path.join(here, "cli", "right-release.mjs"), resolve = resolveInstalledCli } = {}) {
|
|
175
|
+
if (explicit || env.RIGHT_RELEASE_INSTALLED_CLI) return resolve({ explicit, env });
|
|
176
|
+
let real = self;
|
|
177
|
+
try { real = realpathSync(self); } catch { /* keep lexical */ }
|
|
178
|
+
if (existsSync(self) && !insideGitCheckout(real)) return self;
|
|
179
|
+
return resolve({ explicit, env });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function scheduleStatus({ platform, stdout, env = process.env, home = os.homedir() }) {
|
|
183
|
+
let binding = null;
|
|
184
|
+
let loaded = false;
|
|
185
|
+
try { binding = readScheduleBinding({ platform, home }); } catch (error) { stdout(`status: cannot read the schedule (${error.message})`); return 1; }
|
|
186
|
+
if (platform === "darwin") {
|
|
187
|
+
const r = spawnSync("launchctl", ["print", `gui/${process.getuid()}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
|
|
188
|
+
loaded = r.status === 0;
|
|
189
|
+
} else loaded = Boolean(binding);
|
|
190
|
+
if (!binding) { stdout("not installed"); return 0; }
|
|
191
|
+
const verdict = evaluateBinding(binding, { platform, env, home });
|
|
192
|
+
stdout(`${loaded ? "installed" : "plist present but not loaded"} (${binding.source})`);
|
|
193
|
+
stdout(` target: ${binding.cli ?? "(none)"}`);
|
|
194
|
+
if (verdict.ok) { stdout(" binding ok: stable root, target present"); return loaded ? 0 : 1; }
|
|
195
|
+
for (const p of verdict.problems) stdout(` PROBLEM: ${p}`);
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function schedule(sub, { stdout, cli, platform = process.platform, env = process.env }) {
|
|
200
|
+
if (!["install", "uninstall", "status"].includes(sub)) throw new Error("disk schedule install|uninstall|status");
|
|
201
|
+
if (sub === "status") return scheduleStatus({ platform, stdout, env });
|
|
202
|
+
const spec = scheduleSpec({ platform });
|
|
168
203
|
if (platform === "darwin") {
|
|
169
204
|
const uid = String(process.getuid());
|
|
170
205
|
if (sub === "install") {
|
|
171
|
-
|
|
206
|
+
const installed = installStableRoot({ cli: sourceCli({ explicit: cli, env }), platform, env });
|
|
207
|
+
stdout(`${installed.unchanged ? "kept" : "installed"} @rightkit/release ${installed.version ?? "?"} in ${installed.stable}`);
|
|
208
|
+
const bound = scheduleSpec({ platform, cli: installed.cli });
|
|
209
|
+
mkdirSync(path.dirname(bound.file), { recursive: true });
|
|
172
210
|
mkdirSync("/Volumes/D/claude/tmp/swept", { recursive: true });
|
|
173
|
-
|
|
211
|
+
ensureScratchRoots({ policy: readDiskPolicy(), platform, env });
|
|
212
|
+
writeFileSync(bound.file, bound.plist);
|
|
174
213
|
spawnSync("launchctl", ["bootout", `gui/${uid}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
|
|
175
|
-
const r = spawnSync("launchctl", ["bootstrap", `gui/${uid}`,
|
|
214
|
+
const r = spawnSync("launchctl", ["bootstrap", `gui/${uid}`, bound.file], { encoding: "utf8" });
|
|
176
215
|
if (r.status !== 0) throw new Error(`launchctl bootstrap failed: ${r.stderr}`);
|
|
177
|
-
stdout(`installed ${
|
|
178
|
-
} else
|
|
216
|
+
stdout(`installed ${bound.file} (daily 03:30), bound to ${installed.cli}`);
|
|
217
|
+
} else {
|
|
179
218
|
spawnSync("launchctl", ["bootout", `gui/${uid}/${LAUNCHD_LABEL}`], { encoding: "utf8" });
|
|
180
219
|
if (existsSync(spec.file)) unlinkSync(spec.file);
|
|
181
220
|
stdout("uninstalled");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
stdout(r.status === 0 ? `installed (${spec.file})` : "not installed");
|
|
185
|
-
} else throw new Error("disk schedule install|uninstall|status");
|
|
186
|
-
return;
|
|
221
|
+
}
|
|
222
|
+
return 0;
|
|
187
223
|
}
|
|
188
224
|
if (sub === "install") {
|
|
189
|
-
const
|
|
225
|
+
const installed = installStableRoot({ cli: sourceCli({ explicit: cli, env }), platform, env });
|
|
226
|
+
stdout(`${installed.unchanged ? "kept" : "installed"} @rightkit/release ${installed.version ?? "?"} in ${installed.stable}`);
|
|
227
|
+
ensureScratchRoots({ policy: readDiskPolicy(), platform, env });
|
|
228
|
+
const bound = scheduleSpec({ platform, cli: installed.cli });
|
|
229
|
+
const r = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", bound.script], { encoding: "utf8" });
|
|
190
230
|
if (r.status !== 0) throw new Error(`Register-ScheduledTask failed: ${r.stderr || r.stdout}`);
|
|
191
|
-
stdout(`installed scheduled task "${WINDOWS_TASK}" (daily 03:30, hidden)`);
|
|
192
|
-
} else
|
|
231
|
+
stdout(`installed scheduled task "${WINDOWS_TASK}" (daily 03:30, hidden), bound to ${installed.cli}`);
|
|
232
|
+
} else {
|
|
193
233
|
spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `Unregister-ScheduledTask -TaskName '${WINDOWS_TASK}' -Confirm:$false`], { encoding: "utf8" });
|
|
194
234
|
stdout("uninstalled");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
stdout(r.status === 0 ? `installed (${String(r.stdout).trim()})` : "not installed");
|
|
198
|
-
} else throw new Error("disk schedule install|uninstall|status");
|
|
235
|
+
}
|
|
236
|
+
return 0;
|
|
199
237
|
}
|
|
200
238
|
|
|
201
239
|
export async function runDiskCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, platform = process.platform, deps = {} } = {}) {
|
|
202
240
|
const [command, ...rest] = args;
|
|
203
241
|
if (command === "schedule") {
|
|
204
242
|
const at = rest.indexOf("--cli");
|
|
205
|
-
schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1] });
|
|
206
|
-
return { exitCode
|
|
243
|
+
const exitCode = schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1], platform, env });
|
|
244
|
+
return { exitCode };
|
|
207
245
|
}
|
|
208
246
|
const opts = parse(rest);
|
|
209
247
|
const policy = readDiskPolicy(opts.policy);
|
|
@@ -219,6 +257,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
219
257
|
return { exitCode: result.warn ? DISK_EXIT_WARNING : 0, result };
|
|
220
258
|
}
|
|
221
259
|
if (command === "sweep") {
|
|
260
|
+
if (!opts.dryRun) ensureScratchRoots({ policy, platform, env });
|
|
222
261
|
const p = plan();
|
|
223
262
|
if (opts.dryRun) { out(p, renderSweep(p)); return { exitCode: 0, plan: p }; }
|
|
224
263
|
const applied = applySweep({ plan: p, policy, platform, deleteMode: opts.deleteMode, ...deps.apply });
|
|
@@ -231,6 +270,13 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
231
270
|
return { exitCode: purged.results.some((r) => !r.ok) ? 1 : 0, purged };
|
|
232
271
|
}
|
|
233
272
|
if (command === "run") {
|
|
273
|
+
// Self-check first: a broken binding (non-stable target, missing package or dependency) is logged and fails the run.
|
|
274
|
+
const self = (deps.selfCheck ?? selfCheck)({ moduleDir: path.resolve(here, "."), platform, env, ...(deps.selfCheckOptions ?? {}) });
|
|
275
|
+
if (!self.ok) {
|
|
276
|
+
for (const problem of self.problems) stdout(`ERROR: disk run self-check: ${problem}`);
|
|
277
|
+
logEvent(logFile, { event: "disk.run.selfcheck.fail", problems: self.problems, version: self.version });
|
|
278
|
+
}
|
|
279
|
+
ensureScratchRoots({ policy, platform, env });
|
|
234
280
|
const before = check();
|
|
235
281
|
stdout(renderCheck(before));
|
|
236
282
|
const p = plan();
|
|
@@ -243,7 +289,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
|
|
|
243
289
|
const warn = after.warn;
|
|
244
290
|
logEvent(logFile, { event: warn ? "disk.floor.warn" : "disk.run.ok", warnings: after.warnings, freeBytes: after.floor.freeBytes, movedBytes: applied.movedBytes });
|
|
245
291
|
if (warn) stdout(`WARNING: ${after.warnings.join("; ")}`);
|
|
246
|
-
return { exitCode: warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged };
|
|
292
|
+
return { exitCode: !self.ok ? 2 : warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged, selfCheck: self };
|
|
247
293
|
}
|
|
248
294
|
throw new Error("disk usage: check|sweep [--dry-run|--delete] [--days N]|purge [--dry-run] [--days N]|run|schedule install|uninstall|status [--json]");
|
|
249
295
|
}
|
package/disk-policy.json
CHANGED
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
"mac": "/Volumes/D/claude/tmp/swept",
|
|
15
15
|
"win": "D:\\claude\\tmp\\swept"
|
|
16
16
|
},
|
|
17
|
+
"unknownStaleDays": 3,
|
|
18
|
+
"workspaceRoots": {
|
|
19
|
+
"mac": [{"path": "/Volumes/D/claude"}],
|
|
20
|
+
"win": [{"path": "D:\\claude"}]
|
|
21
|
+
},
|
|
22
|
+
"workspaceAllowlist": [".DS_Store", ".git", "node_modules", "tmp", "reports", "tasks", ".cache", "bin", "repos", "packages", "tests", "schemas", "assets", "migration", "archive", "catalog.db", "catalog.db-shm", "catalog.db-wal", "apple-skill-sources", "foundation-runs", "SEO", ".audit", ".commandcode", ".fastembed_cache", ".legion", ".model-bench", ".pnpm-store", ".pytest_cache", ".research", ".right-release", ".rightkit-managed", ".tether", ".venv-tools", ".vite"],
|
|
23
|
+
"scratchRoots": {
|
|
24
|
+
"mac": ["/Volumes/D/scratch"],
|
|
25
|
+
"win": ["D:\\scratch"]
|
|
26
|
+
},
|
|
17
27
|
"writers": [
|
|
18
28
|
{"id": "claude-session-dirs", "platform": "mac", "path": "/private/tmp/claude-501/*/*", "capBytes": 2147483648, "action": "sweep", "unit": "self"},
|
|
19
29
|
{"id": "rightkit-qa-tmp", "platform": "mac", "path": "/private/tmp/rightkit-qa", "capBytes": 2147483648, "action": "sweep", "unit": "children"},
|
|
@@ -47,7 +57,9 @@
|
|
|
47
57
|
{"id": "leftover-node-modules-medium-packages", "platform": "mac", "path": "/Volumes/D/claude/rightkit/packages/*/node_modules.medium-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
48
58
|
{"id": "leftover-legion-snap", "platform": "mac", "path": "/Volumes/D/claude/legion-snap-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
49
59
|
{"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"}
|
|
60
|
+
{"id": "leftover-pulse-bak", "platform": "win", "path": "${LOCALAPPDATA}\\Programs\\Pulse.bak-*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
|
|
61
|
+
{"id": "qa-evidence-runs", "platform": "mac", "path": "/Volumes/D/claude/*/.cache/rightkit-qa/*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true},
|
|
62
|
+
{"id": "qa-evidence-runs", "platform": "win", "path": "D:\\claude\\*\\.cache\\rightkit-qa\\*", "capBytes": 5368709120, "action": "sweep", "unit": "self", "staleDays": 3, "skipTracked": true}
|
|
51
63
|
],
|
|
52
64
|
"protectedSegments": ["Application Support", "Keychains", "Keychain", "HeardRightNext", "Credentials", ".ssh", ".rightkit-managed", ".rightkit-swept"]
|
|
53
65
|
}
|
package/disk-policy.mjs
CHANGED
|
@@ -56,7 +56,9 @@ export function walkSize(root) {
|
|
|
56
56
|
const dir = stack.pop();
|
|
57
57
|
let st;
|
|
58
58
|
try { st = lstatSync(dir); } catch { continue; }
|
|
59
|
-
|
|
59
|
+
// Allocated bytes; where the filesystem reports none for a non-empty file (NTFS keeps small files resident
|
|
60
|
+
// in the MFT, so Windows reports 0 blocks) the logical size is the best estimate.
|
|
61
|
+
total += st.blocks ? st.blocks * 512 : st.size;
|
|
60
62
|
if (!st.isDirectory()) continue;
|
|
61
63
|
let handle;
|
|
62
64
|
try { handle = opendirSync(dir); } catch { continue; }
|
|
@@ -115,7 +117,77 @@ export function writerMatches(writer, ctx) {
|
|
|
115
117
|
return expandGlob(pattern);
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
|
|
121
|
+
// ---- Workspace roots (allowlist) and scratch roots ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
const isWin = (key) => key === "win";
|
|
124
|
+
const fold = (value, key) => (isWin(key) ? value.toLowerCase() : value);
|
|
125
|
+
|
|
126
|
+
/** Top-level names git tracks in the repository rooted exactly at `root`; null when that cannot be proven
|
|
127
|
+
* (not a repository root, git missing or failing). Callers must then leave the root alone. */
|
|
128
|
+
export function workspaceTracked(root, { platform = process.platform, git = spawnSync } = {}) {
|
|
129
|
+
const key = platformKey(platform);
|
|
130
|
+
const top = git("git", ["-C", root, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
131
|
+
if (top.status !== 0) return null;
|
|
132
|
+
try {
|
|
133
|
+
// The native form resolves Windows 8.3 short names (C:\Users\RUNNER~1) to the long names git prints.
|
|
134
|
+
if (fold(realpathSync.native(String(top.stdout).trim()), key) !== fold(realpathSync.native(root), key)) return null;
|
|
135
|
+
} catch { return null; }
|
|
136
|
+
const listed = git("git", ["-C", root, "ls-files", "-z"], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
137
|
+
if (listed.status !== 0) return null;
|
|
138
|
+
const names = new Set();
|
|
139
|
+
for (const file of String(listed.stdout).split("\0")) if (file) names.add(fold(file.split("/")[0], key));
|
|
140
|
+
return names;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function scratchRoots(policy, { platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const p of policy.scratchRoots?.[platformKey(platform)] ?? []) { try { out.push(expandPolicyPath(p, { env, home })); } catch { /* unset variable */ } }
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Create missing scratch roots (only when their parent volume exists). Returns the roots that exist afterwards. */
|
|
150
|
+
export function ensureScratchRoots({ policy, platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
151
|
+
const ready = [];
|
|
152
|
+
for (const root of scratchRoots(policy, { platform, env, home })) {
|
|
153
|
+
try {
|
|
154
|
+
if (!existsSync(root) && existsSync(path.dirname(root))) mkdirSync(root);
|
|
155
|
+
if (existsSync(root)) ready.push(root);
|
|
156
|
+
} catch { /* volume not writable: the next run retries */ }
|
|
157
|
+
}
|
|
158
|
+
return ready;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Top-level entries of each workspace root that are neither tracked by the workspace repo, nested git checkouts,
|
|
162
|
+
* nor on the allowlist. Symlinks are listed (`symlink: true`) but sweep never touches them. */
|
|
163
|
+
export function scanWorkspaceRoots({ policy, platform = process.platform, env = process.env, home = os.homedir(), tracked = (r) => workspaceTracked(r, { platform }) } = {}) {
|
|
164
|
+
const key = platformKey(platform);
|
|
165
|
+
const allow = new Set((policy.workspaceAllowlist ?? []).map((n) => fold(n, key)));
|
|
166
|
+
const unknown = [];
|
|
167
|
+
const skipped = [];
|
|
168
|
+
for (const entry of policy.workspaceRoots?.[key] ?? []) {
|
|
169
|
+
let root;
|
|
170
|
+
try { root = expandPolicyPath(entry.path, { env, home }); } catch { continue; }
|
|
171
|
+
if (!existsSync(root)) continue;
|
|
172
|
+
const trackedNames = tracked(root);
|
|
173
|
+
if (!trackedNames) { skipped.push({ writer: "workspace-unknown", path: root, reason: "git-unavailable" }); continue; }
|
|
174
|
+
let names = [];
|
|
175
|
+
try { names = readdirSync(root); } catch { continue; }
|
|
176
|
+
const extra = new Set((entry.allow ?? []).map((n) => fold(n, key)));
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
const folded = fold(name, key);
|
|
179
|
+
if (allow.has(folded) || extra.has(folded) || trackedNames.has(folded)) continue;
|
|
180
|
+
const full = path.join(root, name);
|
|
181
|
+
let st;
|
|
182
|
+
try { st = lstatSync(full); } catch { continue; }
|
|
183
|
+
if (!st.isSymbolicLink() && existsSync(path.join(full, ".git"))) continue; // nested git checkout
|
|
184
|
+
unknown.push({ root, name, path: full, symlink: st.isSymbolicLink(), mtimeMs: st.mtimeMs });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { unknown, skipped };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function checkDisk({ policy, platform = process.platform, env = process.env, home = os.homedir(), size = (p) => pathSize(p, { platform }), space = freeSpace, now = Date.now(), scan = scanWorkspaceRoots } = {}) {
|
|
119
191
|
const key = platformKey(platform);
|
|
120
192
|
const floorPolicy = policy.floors[key];
|
|
121
193
|
const floor = evaluateFloor(floorPolicy, space(floorPolicy.path));
|
|
@@ -132,7 +204,11 @@ export function checkDisk({ policy, platform = process.platform, env = process.e
|
|
|
132
204
|
const warnings = [];
|
|
133
205
|
if (floor.warn) warnings.push(`${floor.label} free ${formatBytes(floor.freeBytes)} is below the ${formatBytes(floor.thresholdBytes)} floor`);
|
|
134
206
|
for (const w of writers) if (w.over) warnings.push(`${w.id} holds ${formatBytes(w.bytes)} over its ${formatBytes(w.capBytes)} cap`);
|
|
135
|
-
|
|
207
|
+
const scanned = scan({ policy, platform, env, home });
|
|
208
|
+
const unknown = scanned.unknown.map((u) => ({ root: u.root, name: u.name, path: u.path, symlink: u.symlink, bytes: u.symlink ? 0 : size(u.path), ageDays: Math.max(0, Math.floor((now - u.mtimeMs) / 86400_000)) }));
|
|
209
|
+
for (const u of unknown) warnings.push(`unknown top-level entry ${u.path} (${u.symlink ? "symlink" : formatBytes(u.bytes)}, ${u.ageDays}d old): track it, add it to workspaceAllowlist, or let sweep move it`);
|
|
210
|
+
for (const s of scanned.skipped) warnings.push(`workspace root ${s.path}: tracked paths cannot be read from git, so unknown entries are not evaluated or swept`);
|
|
211
|
+
return { schema: 1, platform: key, floor, writers, unknown, warnings, warn: warnings.length > 0 };
|
|
136
212
|
}
|
|
137
213
|
|
|
138
214
|
export function formatBytes(n) {
|
|
@@ -187,14 +263,25 @@ export function windowsLockProbe(candidate) {
|
|
|
187
263
|
return false;
|
|
188
264
|
}
|
|
189
265
|
|
|
190
|
-
|
|
266
|
+
// True when git tracks anything at or under `target` (a tracked file is never swept). Not in a repository: not tracked.
|
|
267
|
+
export function gitTracksPath(target) {
|
|
268
|
+
const top = spawnSync("git", ["-C", path.dirname(target), "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
269
|
+
if (top.status !== 0) return false;
|
|
270
|
+
const listed = spawnSync("git", ["-C", String(top.stdout).trim(), "ls-files", "-z", "--", target], { encoding: "utf8" });
|
|
271
|
+
if (listed.status !== 0) return true; // cannot prove untracked: keep it
|
|
272
|
+
return String(listed.stdout).length > 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
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 }), scan = scanWorkspaceRoots } = {}) {
|
|
191
276
|
const key = platformKey(platform);
|
|
192
|
-
const
|
|
277
|
+
const effectiveDays = staleDays ?? policy.staleDays;
|
|
193
278
|
const candidates = [];
|
|
194
279
|
const skipped = [];
|
|
195
280
|
const seen = new Set();
|
|
196
281
|
const lockCheck = inUse ?? (key === "win" ? windowsLockProbe : (p) => inUseByOpenPaths(p, openPaths));
|
|
197
282
|
for (const writer of policy.writers.filter((w) => w.platform === key && w.action === "sweep")) {
|
|
283
|
+
// `--days N` overrides every writer; otherwise a writer may carry its own window (QA evidence: 3 days).
|
|
284
|
+
const cutoff = now - (staleDays ?? writer.staleDays ?? policy.staleDays) * 86400_000;
|
|
198
285
|
let roots; let literal;
|
|
199
286
|
try { roots = writerMatches(writer, { env, home }); literal = literalRoot(expandPolicyPath(writer.path, { env, home })); } catch { continue; }
|
|
200
287
|
const units = [];
|
|
@@ -216,12 +303,40 @@ export function planSweep({ policy, platform = process.platform, env = process.e
|
|
|
216
303
|
if (st.isSymbolicLink()) { skipped.push({ ...base, reason: "symlink" }); continue; }
|
|
217
304
|
if (!isInsideRoot(unit, root)) { skipped.push({ ...base, reason: "outside-root" }); continue; }
|
|
218
305
|
if (isProtectedPath(unit, policy)) { skipped.push({ ...base, reason: "protected" }); continue; }
|
|
306
|
+
if (writer.skipTracked && tracked(unit)) { skipped.push({ ...base, reason: "tracked" }); continue; }
|
|
219
307
|
if (hasFreshContent(unit, cutoff)) { skipped.push({ ...base, reason: "fresh" }); continue; }
|
|
220
308
|
if (lockCheck(unit)) { skipped.push({ ...base, reason: "in-use" }); continue; }
|
|
221
309
|
candidates.push({ ...base, root, bytes: size(unit), mtime: new Date(st.mtimeMs).toISOString() });
|
|
222
310
|
}
|
|
223
311
|
}
|
|
224
|
-
|
|
312
|
+
// Allowlisted workspace roots: unknown top-level entries older than 3 days and idle.
|
|
313
|
+
const scanned = scan({ policy, platform, env, home });
|
|
314
|
+
skipped.push(...scanned.skipped);
|
|
315
|
+
const unknownCutoff = now - (staleDays ?? policy.unknownStaleDays ?? 3) * 86400_000;
|
|
316
|
+
const consider = (writerId, unit, root, cutoff) => {
|
|
317
|
+
if (seen.has(unit)) return;
|
|
318
|
+
seen.add(unit);
|
|
319
|
+
const base = { writer: writerId, path: unit };
|
|
320
|
+
let st;
|
|
321
|
+
try { st = lstatSync(unit); } catch { return; }
|
|
322
|
+
if (st.isSymbolicLink()) { skipped.push({ ...base, reason: "symlink" }); return; }
|
|
323
|
+
if (!isInsideRoot(unit, root)) { skipped.push({ ...base, reason: "outside-root" }); return; }
|
|
324
|
+
if (isProtectedPath(unit, policy)) { skipped.push({ ...base, reason: "protected" }); return; }
|
|
325
|
+
if (hasFreshContent(unit, cutoff)) { skipped.push({ ...base, reason: "fresh" }); return; }
|
|
326
|
+
if (lockCheck(unit)) { skipped.push({ ...base, reason: "in-use" }); return; }
|
|
327
|
+
candidates.push({ ...base, root, bytes: size(unit), mtime: new Date(st.mtimeMs).toISOString() });
|
|
328
|
+
};
|
|
329
|
+
for (const u of scanned.unknown) consider("workspace-unknown", u.path, u.root, unknownCutoff);
|
|
330
|
+
// Scratch roots: every child older than 3 days and idle (the same window).
|
|
331
|
+
for (const root of scratchRoots(policy, { platform, env, home })) {
|
|
332
|
+
let rst;
|
|
333
|
+
try { rst = lstatSync(root); } catch { continue; }
|
|
334
|
+
if (rst.isSymbolicLink()) { skipped.push({ writer: "scratch-root", path: root, reason: "symlink-root" }); continue; }
|
|
335
|
+
let names = [];
|
|
336
|
+
try { names = readdirSync(root); } catch { continue; }
|
|
337
|
+
for (const n of names) consider("scratch-root", path.join(root, n), root, unknownCutoff);
|
|
338
|
+
}
|
|
339
|
+
return { schema: 1, platform: key, staleDays: effectiveDays, candidates, skipped, totalBytes: candidates.reduce((s, c) => s + c.bytes, 0) };
|
|
225
340
|
}
|
|
226
341
|
|
|
227
342
|
export const HOLDING_DIR = ".rightkit-swept";
|
|
@@ -314,6 +429,7 @@ export function applySweep({ plan, policy, platform = process.platform, deleteMo
|
|
|
314
429
|
try {
|
|
315
430
|
// Re-verify at apply time: still the planned unit, not a symlink, still inside its root.
|
|
316
431
|
if (!c.root || lstatSync(c.path).isSymbolicLink() || !isInsideRoot(c.path, c.root)) throw new Error("refused: path is not a sweepable unit inside its target root");
|
|
432
|
+
if (c.writer === "workspace-unknown" && existsSync(path.join(c.path, ".git"))) throw new Error("refused: a nested git checkout is never swept");
|
|
317
433
|
if (deleteMode) { remove(c.path); results.push({ ...c, action: "delete", ok: true }); logEvent(logFile, { event: "disk.sweep.delete", ...c }); continue; }
|
|
318
434
|
// An item stays on its own volume: moving it elsewhere frees nothing on the volume that is short of space.
|
|
319
435
|
const holding = pick(c.path, { policy, platform, env, home, date });
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
|
|
6
|
+
// The scheduled disk job must never bind to a pnpm store, a pnpm global hash directory or a checkout: those move or vanish
|
|
7
|
+
// on upgrade. `disk schedule install` copies the installed package into this per-user root and binds the job to it.
|
|
8
|
+
// See docs/architecture/development-installed-product-boundary.md (installed bindings use a stable `current` root).
|
|
9
|
+
|
|
10
|
+
const pathFor = (platform) => (platform === "win32" || platform === "win" ? path.win32 : path.posix);
|
|
11
|
+
|
|
12
|
+
export function stableRootPath({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
|
|
13
|
+
if (platform === "win32" || platform === "win") {
|
|
14
|
+
if (!env.LOCALAPPDATA) throw new Error("disk: LOCALAPPDATA is not set");
|
|
15
|
+
return path.win32.join(env.LOCALAPPDATA, "Orthic Labs", "rightkit-release", "current");
|
|
16
|
+
}
|
|
17
|
+
return path.posix.join(home, "Library", "Application Support", "Orthic Labs", "rightkit-release", "current");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stableCliPath(stable, platform = process.platform) {
|
|
21
|
+
return pathFor(platform).join(stable, "node_modules", "@rightkit", "release", "cli", "right-release.mjs");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isUnderStable(file, stable, platform = process.platform) {
|
|
25
|
+
const win = platform === "win32" || platform === "win";
|
|
26
|
+
const n = (v) => (win ? v.toLowerCase().replace(/\//g, "\\") : v);
|
|
27
|
+
const prefix = n(stable).replace(/[\\/]+$/, "") + (win ? "\\" : "/");
|
|
28
|
+
return n(file).startsWith(prefix);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const readJson = (file) => { try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; } };
|
|
32
|
+
|
|
33
|
+
// Walk up from `from` looking for `node_modules/<name>` (or `<name>` directly inside a node_modules directory).
|
|
34
|
+
function findDependency(name, from) {
|
|
35
|
+
let dir = from;
|
|
36
|
+
for (;;) {
|
|
37
|
+
for (const candidate of [path.join(dir, "node_modules", name), path.basename(dir) === "node_modules" ? path.join(dir, name) : null]) {
|
|
38
|
+
if (candidate && existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
39
|
+
}
|
|
40
|
+
const up = path.dirname(dir);
|
|
41
|
+
if (up === dir) return null;
|
|
42
|
+
dir = up;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Runtime dependency directories of a package (transitive), as `{ name, dir }`, found beside the package (lexical path
|
|
47
|
+
* first, then its real path, so pnpm's symlinked layout works). */
|
|
48
|
+
export function collectDependencies(pkgRoot) {
|
|
49
|
+
const found = new Map();
|
|
50
|
+
const visit = (root) => {
|
|
51
|
+
const deps = Object.keys(readJson(path.join(root, "package.json"))?.dependencies ?? {});
|
|
52
|
+
for (const name of deps) {
|
|
53
|
+
if (found.has(name)) continue;
|
|
54
|
+
let real = root;
|
|
55
|
+
try { real = realpathSync(root); } catch { /* keep lexical */ }
|
|
56
|
+
const dir = findDependency(name, root) ?? findDependency(name, real);
|
|
57
|
+
if (!dir) throw new Error(`disk: dependency ${name} of ${root} was not found beside the installed package`);
|
|
58
|
+
found.set(name, dir);
|
|
59
|
+
visit(dir);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
visit(pkgRoot);
|
|
63
|
+
return [...found].map(([name, dir]) => ({ name, dir }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Copy the installed @rightkit/release package (and its dependencies) into the stable root. The new tree is built and
|
|
67
|
+
* smoke-tested in a staging directory next to `current`, then swapped in by rename; a failed swap restores the previous
|
|
68
|
+
* `current`. Returns `{ stable, cli, version, replaced, unchanged }`. */
|
|
69
|
+
export function installStableRoot({ cli, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), nodePath = process.execPath, smoke = true } = {}) {
|
|
70
|
+
const stable = stableRootPath({ platform, env, home });
|
|
71
|
+
const stableCli = stableCliPath(stable, platform);
|
|
72
|
+
let realCli = cli;
|
|
73
|
+
try { realCli = realpathSync(cli); } catch { /* keep lexical */ }
|
|
74
|
+
if (isUnderStable(cli, stable, platform) || isUnderStable(realCli, stable, platform)) {
|
|
75
|
+
return { stable, cli: stableCli, version: readJson(path.join(stable, "node_modules", "@rightkit", "release", "package.json"))?.version, replaced: false, unchanged: true };
|
|
76
|
+
}
|
|
77
|
+
const pkgRoot = path.resolve(path.dirname(cli), "..");
|
|
78
|
+
const pkg = readJson(path.join(pkgRoot, "package.json"));
|
|
79
|
+
if (pkg?.name !== "@rightkit/release") throw new Error(`disk: ${cli} is not inside an @rightkit/release package`);
|
|
80
|
+
const deps = collectDependencies(pkgRoot);
|
|
81
|
+
|
|
82
|
+
const parent = path.dirname(stable);
|
|
83
|
+
mkdirSync(parent, { recursive: true });
|
|
84
|
+
for (const name of readdirSync(parent)) {
|
|
85
|
+
if (/^current\.(staging|prev)-/.test(name)) try { rmSync(path.join(parent, name), { recursive: true, force: true }); } catch { /* in use: leave it */ }
|
|
86
|
+
}
|
|
87
|
+
const staging = path.join(parent, `current.staging-${process.pid}-${now}`);
|
|
88
|
+
const copy = (from, to, filter) => cpSync(from, to, { recursive: true, dereference: true, force: true, filter });
|
|
89
|
+
try {
|
|
90
|
+
const target = path.join(staging, "node_modules", "@rightkit", "release");
|
|
91
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
92
|
+
copy(pkgRoot, target, (src) => path.basename(src) !== "node_modules" && !src.endsWith(".test.mjs"));
|
|
93
|
+
for (const dep of deps) {
|
|
94
|
+
const to = path.join(staging, "node_modules", ...dep.name.split("/"));
|
|
95
|
+
mkdirSync(path.dirname(to), { recursive: true });
|
|
96
|
+
copy(dep.dir, to, (src) => path.basename(src) !== "node_modules");
|
|
97
|
+
}
|
|
98
|
+
writeFileSync(path.join(staging, "install.json"), `${JSON.stringify({ package: pkg.name, version: pkg.version, source: pkgRoot, installedAt: new Date(now).toISOString(), node: nodePath }, null, 2)}\n`);
|
|
99
|
+
const stagedCli = stableCliPath(staging, platform);
|
|
100
|
+
if (!existsSync(stagedCli)) throw new Error("disk: the copied package has no cli/right-release.mjs");
|
|
101
|
+
if (smoke) {
|
|
102
|
+
const r = spawnSync(nodePath, [stagedCli, "--version"], { encoding: "utf8" });
|
|
103
|
+
if (r.status !== 0) throw new Error(`disk: the staged copy does not run: ${r.stderr || r.stdout || r.error?.message}`);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
rmSync(staging, { recursive: true, force: true });
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
let replaced = false;
|
|
110
|
+
let prev;
|
|
111
|
+
try {
|
|
112
|
+
if (existsSync(stable) || isLink(stable)) { prev = path.join(parent, `current.prev-${process.pid}-${now}`); renameSync(stable, prev); replaced = true; }
|
|
113
|
+
renameSync(staging, stable);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (prev && !existsSync(stable)) try { renameSync(prev, stable); } catch { /* reported below */ }
|
|
116
|
+
rmSync(staging, { recursive: true, force: true });
|
|
117
|
+
throw new Error(`disk: could not replace ${stable}: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
if (prev) try { rmSync(prev, { recursive: true, force: true }); } catch { /* a later install removes it */ }
|
|
120
|
+
return { stable, cli: stableCli, version: pkg.version, replaced, unchanged: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isLink(p) { try { return lstatSync(p).isSymbolicLink(); } catch { return false; } }
|
|
124
|
+
|
|
125
|
+
/** Registered schedule target: the CLI path the launchd job or Windows task runs, or null when none is registered. */
|
|
126
|
+
export function readScheduleBinding({ platform = process.platform, home = os.homedir(), file, run = spawnSync, label = "RightKit Disk Hygiene", plistFile } = {}) {
|
|
127
|
+
if (platform === "darwin") {
|
|
128
|
+
const plist = plistFile ?? file ?? path.join(home, "Library", "LaunchAgents", "dev.rightkit.disk-hygiene.plist");
|
|
129
|
+
if (!existsSync(plist)) return null;
|
|
130
|
+
const m = /<key>ProgramArguments<\/key><array>\s*<string>([^<]*)<\/string>\s*<string>([^<]*)<\/string>/.exec(readFileSync(plist, "utf8"));
|
|
131
|
+
return m ? { node: m[1], cli: m[2], source: plist } : { node: null, cli: null, source: plist };
|
|
132
|
+
}
|
|
133
|
+
const r = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `$t = Get-ScheduledTask -TaskName '${label}' -ErrorAction SilentlyContinue; if (-not $t) { exit 3 }; $a = $t.Actions[0]; Write-Output $a.Execute; Write-Output $a.Arguments`], { encoding: "utf8" });
|
|
134
|
+
if (r.status !== 0) return null;
|
|
135
|
+
const [exe, ...rest] = String(r.stdout).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
136
|
+
const args = rest.join(" ");
|
|
137
|
+
const quoted = /"([^"]+)"/.exec(args);
|
|
138
|
+
return { node: exe ?? null, cli: quoted ? quoted[1] : (args.split(/\s+/)[0] || null), source: label };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Evaluate a binding against the stable root. */
|
|
142
|
+
export function evaluateBinding(binding, { platform = process.platform, env = process.env, home = os.homedir(), exists = existsSync } = {}) {
|
|
143
|
+
if (!binding) return { registered: false, ok: true, problems: [] };
|
|
144
|
+
const stable = stableRootPath({ platform, env, home });
|
|
145
|
+
const problems = [];
|
|
146
|
+
if (!binding.cli) problems.push("the schedule names no right-release CLI");
|
|
147
|
+
else {
|
|
148
|
+
if (!isUnderStable(binding.cli, stable, platform)) problems.push(`target ${binding.cli} is not under the stable root ${stable} (reinstall: right-release disk schedule install)`);
|
|
149
|
+
if (!exists(binding.cli)) problems.push(`target ${binding.cli} is MISSING`);
|
|
150
|
+
}
|
|
151
|
+
if (binding.node && !exists(binding.node)) problems.push(`node ${binding.node} is MISSING`);
|
|
152
|
+
return { registered: true, ok: problems.length === 0, problems, cli: binding.cli, node: binding.node, stable };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `disk run` self-check: this process's own package must be intact and a registered schedule must be bound to the stable root. */
|
|
156
|
+
export function selfCheck({ moduleDir, platform = process.platform, env = process.env, home = os.homedir(), binding = () => readScheduleBinding({ platform, home }), exists = existsSync } = {}) {
|
|
157
|
+
const problems = [];
|
|
158
|
+
const pkg = readJson(path.join(moduleDir, "package.json"));
|
|
159
|
+
if (!pkg) problems.push(`own package.json is unreadable at ${moduleDir}`);
|
|
160
|
+
else {
|
|
161
|
+
if (!exists(path.join(moduleDir, "cli", "right-release.mjs"))) problems.push("own cli/right-release.mjs is missing");
|
|
162
|
+
for (const dep of Object.keys(pkg.dependencies ?? {})) if (!findDependency(dep, moduleDir)) problems.push(`dependency ${dep} is not resolvable from ${moduleDir}`);
|
|
163
|
+
}
|
|
164
|
+
let bound;
|
|
165
|
+
try { bound = evaluateBinding(binding(), { platform, env, home, exists }); } catch (error) { bound = { registered: false, ok: false, problems: [`cannot read the schedule binding: ${error.message}`] }; }
|
|
166
|
+
problems.push(...bound.problems);
|
|
167
|
+
return { ok: problems.length === 0, problems, registered: bound.registered, version: pkg?.version };
|
|
168
|
+
}
|
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",
|
|
@@ -353,7 +354,7 @@
|
|
|
353
354
|
"rightkit-tauri": "0.1.0"
|
|
354
355
|
},
|
|
355
356
|
"stagedCargo": {
|
|
356
|
-
"rightkit-agent-tools": "0.1.
|
|
357
|
+
"rightkit-agent-tools": "0.1.3",
|
|
357
358
|
"rightkit-agents": "0.1.0",
|
|
358
359
|
"rightkit-app-settings": "0.2.0",
|
|
359
360
|
"rightkit-asr": "0.2.1",
|
|
@@ -382,7 +383,7 @@
|
|
|
382
383
|
"rightkit-llm": "0.2.1",
|
|
383
384
|
"rightkit-logs": "0.3.0",
|
|
384
385
|
"rightkit-mcp": "0.5.3",
|
|
385
|
-
"rightkit-models": "0.3.
|
|
386
|
+
"rightkit-models": "0.3.2",
|
|
386
387
|
"rightkit-oauth": "0.5.0",
|
|
387
388
|
"rightkit-ocr": "0.2.2",
|
|
388
389
|
"rightkit-onnx": "0.2.3",
|
|
@@ -422,7 +423,7 @@
|
|
|
422
423
|
},
|
|
423
424
|
"registry": {
|
|
424
425
|
"cargo": {
|
|
425
|
-
"rightkit-agent-tools": "0.1.
|
|
426
|
+
"rightkit-agent-tools": "0.1.3",
|
|
426
427
|
"rightkit-agents": "0.1.0",
|
|
427
428
|
"rightkit-app-settings": "0.2.0",
|
|
428
429
|
"rightkit-asr": "0.2.1",
|
|
@@ -455,7 +456,7 @@
|
|
|
455
456
|
"rightkit-logs": "0.3.0",
|
|
456
457
|
"rightkit-mcp": "0.5.3",
|
|
457
458
|
"rightkit-media": "0.2.6",
|
|
458
|
-
"rightkit-models": "0.3.
|
|
459
|
+
"rightkit-models": "0.3.2",
|
|
459
460
|
"rightkit-oauth": "0.5.0",
|
|
460
461
|
"rightkit-ocr": "0.2.2",
|
|
461
462
|
"rightkit-onnx": "0.2.3",
|
|
@@ -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.162",
|
|
516
517
|
"@rightkit/settings-ui": "0.4.4",
|
|
517
518
|
"@rightkit/shell": "0.1.1",
|
|
518
519
|
"@rightkit/tauri": "0.2.1",
|
|
@@ -539,7 +540,8 @@
|
|
|
539
540
|
"0.2.5",
|
|
540
541
|
"0.2.6",
|
|
541
542
|
"0.2.7",
|
|
542
|
-
"0.3.0"
|
|
543
|
+
"0.3.0",
|
|
544
|
+
"0.3.1"
|
|
543
545
|
],
|
|
544
546
|
"rightkit-audio": [
|
|
545
547
|
"0.1.0",
|
|
@@ -755,6 +757,9 @@
|
|
|
755
757
|
"rightkit-qa-driver": [
|
|
756
758
|
"0.3.0",
|
|
757
759
|
"0.3.1"
|
|
760
|
+
],
|
|
761
|
+
"rightkit-agent-tools": [
|
|
762
|
+
"0.1.2"
|
|
758
763
|
]
|
|
759
764
|
}
|
|
760
765
|
}
|