atris 3.82.5 → 3.82.8
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/bin/atris.js +7 -2
- package/commands/security-review.js +18 -0
- package/commands/sentinel-bench.js +172 -0
- package/commands/sentinel-fix.js +243 -0
- package/commands/sentinel-powers.js +97 -11
- package/lib/security-audit.js +3 -0
- package/lib/sentinel-agents.js +75 -41
- package/lib/sentinel-bench.js +556 -0
- package/lib/sentinel-cloud.js +2 -1
- package/lib/sentinel-events.js +32 -10
- package/lib/sentinel-fix.js +777 -0
- package/lib/sentinel-guard-install.js +10 -4
- package/lib/sentinel-guard.js +142 -83
- package/lib/sentinel-improve.js +827 -0
- package/lib/sentinel-rules.js +108 -0
- package/lib/sentinel-rules.json +237 -0
- package/lib/sentinel-watch.js +940 -98
- package/package.json +1 -1
package/bin/atris.js
CHANGED
|
@@ -144,7 +144,8 @@ const jsonRequested = process.argv.slice(2).includes('--json');
|
|
|
144
144
|
const dryRunRequested = updateArgs.includes('--dry-run');
|
|
145
145
|
const skipUpdateCheck = Boolean(process.env.ATRIS_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER || helpRequested || jsonRequested);
|
|
146
146
|
// Local-only commands make no registry call and start no background install.
|
|
147
|
-
|
|
147
|
+
// sentinel prints its own one-line notice at the end of the run instead.
|
|
148
|
+
if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'update', 'mcp', 'recap', '99', 'sentinel'].includes(updateCommand)))) {
|
|
148
149
|
updateCheckPromise = checkForUpdates()
|
|
149
150
|
.then((updateInfo) => {
|
|
150
151
|
if (updateInfo) {
|
|
@@ -634,6 +635,8 @@ function showHelpAll() {
|
|
|
634
635
|
console.log(' slop - Deterministic slop detector: frontend/prose tells, dead code (slop dead --exports)');
|
|
635
636
|
console.log(' sentinel - Security audit: engines hunt for holes, a different model verifies each one');
|
|
636
637
|
console.log(' free checks: sentinel agents, sentinel watch, sentinel guard install');
|
|
638
|
+
console.log(' exam and self-improvement: sentinel bench, sentinel improve');
|
|
639
|
+
console.log(' fix a confirmed finding on its own branch: sentinel fix <id>');
|
|
637
640
|
console.log(' re-check old findings against today\'s code: sentinel recheck');
|
|
638
641
|
console.log(' voice - Scan binary voice tells or judge reply shape and plainness');
|
|
639
642
|
console.log(' recover - Explain stopped missions; --apply performs safe recovery only');
|
|
@@ -3376,8 +3379,10 @@ if (command === 'guide') {
|
|
|
3376
3379
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
3377
3380
|
} else if (command === 'sentinel') {
|
|
3378
3381
|
// Sentinel: the full security audit (engines hunt, another model verifies). Same as `security-review audit`.
|
|
3382
|
+
// A stale install gets one plain upgrade line at the end of every sentinel command.
|
|
3383
|
+
const sentinelNotice = require('../commands/sentinel-powers').startUpdateNotice(process.argv.slice(3));
|
|
3379
3384
|
Promise.resolve().then(() => require('../commands/security-review').securityReviewCommand(['audit', ...process.argv.slice(3)]))
|
|
3380
|
-
.then((code) => exitAfterStdoutDrain(typeof code === 'number' ? code : 0))
|
|
3385
|
+
.then(async (code) => { try { await sentinelNotice.print(); } catch {} exitAfterStdoutDrain(typeof code === 'number' ? code : 0); })
|
|
3381
3386
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
3382
3387
|
} else if (command === 'security-review' || command === 'secure') {
|
|
3383
3388
|
// Security review: deterministic secrets/PII/code-risk scan (no LLM). Exit 1 = HIGH finding,
|
|
@@ -747,6 +747,11 @@ async function auditCommand(argv, deps = {}) {
|
|
|
747
747
|
const run = { agents: powers.agentsCommand, guard: powers.guardCommand, watch: powers.watchCommand, flush: powers.flushCommand }[argv[0]];
|
|
748
748
|
return run(argv.slice(1), deps);
|
|
749
749
|
}
|
|
750
|
+
if (argv[0] === 'bench' || argv[0] === 'improve') {
|
|
751
|
+
const sb = require('./sentinel-bench');
|
|
752
|
+
return (argv[0] === 'bench' ? sb.benchCommand : sb.improveCommand)(argv.slice(1), deps);
|
|
753
|
+
}
|
|
754
|
+
if (argv[0] === 'fix') return require('./sentinel-fix').fixCommand(argv.slice(1), deps);
|
|
750
755
|
if (argv[0] === 'upload') return auditUploadCommand(argv.slice(1), deps);
|
|
751
756
|
if (argv[0] === 'recheck') return auditRecheckCommand(argv.slice(1), deps);
|
|
752
757
|
if (argv[0] === 'schedule') return auditScheduleCommand(argv.slice(1), deps);
|
|
@@ -854,12 +859,25 @@ function printAuditHelp() {
|
|
|
854
859
|
atris sentinel agents --home also check ~/.claude and ~/.codex
|
|
855
860
|
atris sentinel watch [path] known attacks in your dependencies (osv database)
|
|
856
861
|
atris sentinel watch --fix update affected packages on a new branch, never pushed
|
|
862
|
+
atris sentinel watch --fix --allow-major also take fixes that jump to a new major version
|
|
857
863
|
atris sentinel guard install [--global] stop risky agent commands before they run
|
|
858
864
|
atris sentinel guard test see which sample commands the guard stops
|
|
859
865
|
|
|
860
866
|
results go to your atris dashboard when you are logged in. add --no-upload
|
|
861
867
|
to keep them on this machine. secret values and file contents never leave.
|
|
862
868
|
|
|
869
|
+
the exam and the loop that improves the sentinel (in an atris checkout):
|
|
870
|
+
|
|
871
|
+
atris sentinel bench [--summary] score the guard and agent check on a seen and a held-out half
|
|
872
|
+
atris sentinel improve [--engine claude] one engine proposes a change, the exam decides if it stays
|
|
873
|
+
atris sentinel improve --schedule run the improve loop nightly
|
|
874
|
+
|
|
875
|
+
turning a confirmed finding into a fix:
|
|
876
|
+
|
|
877
|
+
atris sentinel fix list confirmed findings you can fix
|
|
878
|
+
atris sentinel fix <id> [path] smallest fix plus a regression test on a new branch
|
|
879
|
+
atris sentinel fix help how a fix is proved before it is committed
|
|
880
|
+
|
|
863
881
|
reviews on a timer:
|
|
864
882
|
|
|
865
883
|
atris sentinel schedule --every day [path] quick review once a day, saved to your account
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris sentinel bench and atris sentinel improve.
|
|
4
|
+
//
|
|
5
|
+
// atris sentinel bench [--summary] [--json] [--review] [--sets guard,agents]
|
|
6
|
+
// atris sentinel bench --split <file> --set guard|agents|review
|
|
7
|
+
// atris sentinel improve [--engine claude] [--offline] [--json]
|
|
8
|
+
// atris sentinel improve --schedule [--every 24] [--engine claude] | --unschedule
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function readFlag(argv, name) {
|
|
13
|
+
const i = argv.indexOf(name);
|
|
14
|
+
if (i !== -1 && argv[i + 1] !== undefined && !String(argv[i + 1]).startsWith('--')) return argv[i + 1];
|
|
15
|
+
const inline = argv.find((a) => String(a).startsWith(`${name}=`));
|
|
16
|
+
return inline ? inline.slice(name.length + 1) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function checkFlags(argv, flags, values) {
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = String(argv[i]);
|
|
22
|
+
const name = a.split('=')[0];
|
|
23
|
+
if (values.includes(name)) { if (!a.includes('=')) i += 1; continue; }
|
|
24
|
+
if (flags.includes(a)) continue;
|
|
25
|
+
throw new Error(`unknown option: ${a}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function benchHelp() {
|
|
30
|
+
console.log(`
|
|
31
|
+
atris sentinel bench, a fixed exam for the sentinel
|
|
32
|
+
|
|
33
|
+
atris sentinel bench score the guard and the agent check
|
|
34
|
+
atris sentinel bench --summary totals only, no item lines
|
|
35
|
+
atris sentinel bench --review also run the review set on engines (costs engine time)
|
|
36
|
+
atris sentinel bench --json raw numbers for scripts
|
|
37
|
+
atris sentinel bench --split <file> --set guard|agents|review
|
|
38
|
+
split new items into the seen half (in the repo)
|
|
39
|
+
and the sealed held-out half (on this machine).
|
|
40
|
+
the plaintext input is replaced with an encrypted copy
|
|
41
|
+
|
|
42
|
+
the seen half lives in atris/sentinel-bench. the held-out half lives in
|
|
43
|
+
~/.atris/sentinel-holdout, encrypted with a key in your keychain. its
|
|
44
|
+
items are never printed. only lib/sentinel-rules.json of the checkout is
|
|
45
|
+
judged, always with this atris's own code. every number
|
|
46
|
+
is raw: attacks blocked, everyday commands wrongly blocked, problems
|
|
47
|
+
found, false alarms. there is no blended score.
|
|
48
|
+
`);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function benchCommand(argv, deps = {}) {
|
|
53
|
+
if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') return benchHelp();
|
|
54
|
+
try { checkFlags(argv, ['--summary', '--json', '--review'], ['--split', '--set', '--sets', '--target', '--seen', '--hunter', '--verifier', '--budget']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
55
|
+
const bench = require('../lib/sentinel-bench');
|
|
56
|
+
const env = deps.env || process.env;
|
|
57
|
+
const root = path.resolve(readFlag(argv, '--target') || deps.root || process.cwd());
|
|
58
|
+
const seenDir = path.resolve(readFlag(argv, '--seen') || bench.seenDirOf(root));
|
|
59
|
+
const holdoutDir = bench.defaultHoldoutDir(env);
|
|
60
|
+
const split = readFlag(argv, '--split');
|
|
61
|
+
if (split) {
|
|
62
|
+
const set = readFlag(argv, '--set');
|
|
63
|
+
if (!set) { console.error(' --split needs --set guard, agents or review'); return 2; }
|
|
64
|
+
let res;
|
|
65
|
+
try { res = bench.splitCorpus(path.resolve(split), set, { seenDir, holdoutDir, env }); } catch (e) { console.error(` could not split: ${e.message}`); return 1; }
|
|
66
|
+
if (argv.includes('--json')) { console.log(JSON.stringify({ ok: true, ...res }, null, 2)); return 0; }
|
|
67
|
+
console.log(`\n ${res.seen} items went to the seen half: ${path.relative(process.cwd(), res.seen_file) || res.seen_file}`);
|
|
68
|
+
console.log(` ${res.holdout} items went to the held-out half on this machine`);
|
|
69
|
+
if (res.skipped) console.log(` ${res.skipped} items were already in a half and were skipped`);
|
|
70
|
+
if (res.invalid) console.log(` ${res.invalid} lines were not valid items and were skipped`);
|
|
71
|
+
console.log(` the plaintext input was replaced with an encrypted copy at ${res.sealed_source}. no readable held-out item is left behind\n`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const review = argv.includes('--review');
|
|
75
|
+
const setsRaw = readFlag(argv, '--sets');
|
|
76
|
+
const sets = setsRaw ? setsRaw.split(',').map((s) => s.trim()).filter(Boolean) : ['guard', 'agents', ...(review ? ['review'] : [])];
|
|
77
|
+
const bad = sets.filter((s) => !bench.SETS.includes(s));
|
|
78
|
+
if (bad.length) { console.error(` unknown set: ${bad.join(', ')}. use guard, agents or review`); return 2; }
|
|
79
|
+
const budget = readFlag(argv, '--budget');
|
|
80
|
+
const reviewOpts = review ? {
|
|
81
|
+
hunter: readFlag(argv, '--hunter') || 'codex',
|
|
82
|
+
verifier: readFlag(argv, '--verifier') || 'claude',
|
|
83
|
+
...(budget ? { budget: Number(budget) } : {}),
|
|
84
|
+
log: argv.includes('--json') ? () => {} : (m) => console.error(` ${m}`),
|
|
85
|
+
...(deps.runJobs ? { runJobs: deps.runJobs } : {}),
|
|
86
|
+
} : null;
|
|
87
|
+
let report;
|
|
88
|
+
try { report = await bench.runBench({ target: root, seenDir, sets, review: reviewOpts, env }); } catch (e) { console.error(` the exam did not run: ${e.message}`); return 1; }
|
|
89
|
+
const pub = bench.publicReport(report, { summary: argv.includes('--summary') });
|
|
90
|
+
if (argv.includes('--json')) console.log(JSON.stringify({ ok: true, ...pub }, null, 2));
|
|
91
|
+
else console.log(bench.renderReport(pub, { holdoutDir: holdoutDir.replace(require('os').homedir(), '~') }));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function improveHelp() {
|
|
96
|
+
console.log(`
|
|
97
|
+
atris sentinel improve, one exam-checked change to the sentinel
|
|
98
|
+
|
|
99
|
+
atris sentinel improve [--engine claude] propose one change and judge it
|
|
100
|
+
atris sentinel improve --offline skip the published advisory lookup
|
|
101
|
+
atris sentinel improve --schedule [--every 24] [--engine claude]
|
|
102
|
+
run it on a timer, nightly by default
|
|
103
|
+
atris sentinel improve --unschedule turn the timer off
|
|
104
|
+
|
|
105
|
+
claude proposes one change to lib/sentinel-rules.json on a new branch,
|
|
106
|
+
sealed off from the held-out exam. the sentinel bench, fixed code with no
|
|
107
|
+
model, scores it before and after. it is kept only when
|
|
108
|
+
held-out attack catch holds or rises, wrong blocks and false alarms do
|
|
109
|
+
not rise and no seen item that passed now fails. a kept change waits on
|
|
110
|
+
its branch for you. nothing is merged, pushed or released.
|
|
111
|
+
`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function improveCommand(argv, deps = {}) {
|
|
116
|
+
if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') return improveHelp();
|
|
117
|
+
try { checkFlags(argv, ['--json', '--offline', '--schedule', '--unschedule'], ['--engine', '--every']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
118
|
+
const improve = require('../lib/sentinel-improve');
|
|
119
|
+
const env = deps.env || process.env;
|
|
120
|
+
const engine = readFlag(argv, '--engine') || 'claude';
|
|
121
|
+
if (!/^[a-z][a-z0-9:_-]{0,30}$/.test(engine)) { console.error(` unknown engine: ${engine}`); return 2; }
|
|
122
|
+
const refused = require('../lib/sentinel-improve').refusedEngine(engine);
|
|
123
|
+
if (refused && !argv.includes('--unschedule')) { console.error(` ${refused}`); return 2; }
|
|
124
|
+
const root = deps.root || process.cwd();
|
|
125
|
+
if (argv.includes('--schedule') || argv.includes('--unschedule')) {
|
|
126
|
+
const { installKeep, removeKeep } = require('../lib/campaign-keep');
|
|
127
|
+
const everyHours = Number(readFlag(argv, '--every') || 24);
|
|
128
|
+
if (!Number.isFinite(everyHours) || everyHours <= 0) { console.error(' --every needs a number of hours'); return 2; }
|
|
129
|
+
let top;
|
|
130
|
+
try { top = require('child_process').execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { console.error(' run this inside the atris checkout the loop should improve'); return 2; }
|
|
131
|
+
const plan = improve.schedulePlan({ root: top, engine, everyHours, env, atrisBin: deps.atrisBin });
|
|
132
|
+
if (argv.includes('--unschedule')) {
|
|
133
|
+
const removed = (deps.removeKeep || removeKeep)(plan);
|
|
134
|
+
if (!removed.ok) { console.error(` could not turn the timer off: ${removed.error}`); return 1; }
|
|
135
|
+
console.log(` sentinel improve timer: ${removed.removed === false ? 'was not on' : 'off'} for ${top}`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
const space = require('../lib/worktree-space');
|
|
139
|
+
const bin = deps.atrisBin || path.join(__dirname, '..', 'bin', 'atris.js');
|
|
140
|
+
if (!deps.atrisBin && (space.isAgentCopyPath(bin) || space.isScratchPath(bin) || space.isAgentCopyPath(top))) {
|
|
141
|
+
console.error(' refusing: this atris or this checkout lives in a folder that gets removed. install the timer from a lasting checkout');
|
|
142
|
+
return 2;
|
|
143
|
+
}
|
|
144
|
+
const installed = (deps.installKeep || installKeep)(plan);
|
|
145
|
+
if (!installed.ok) { console.error(` could not turn the timer on: ${installed.error}`); return 1; }
|
|
146
|
+
console.log(` sentinel improve timer: on for ${top}, every ${everyHours} ${everyHours === 1 ? 'hour' : 'hours'} with ${engine}`);
|
|
147
|
+
console.log(` log: ${plan.log_path}`);
|
|
148
|
+
console.log(' each run leaves a receipt in ~/.atris/sentinel-improve/receipts. kept changes wait on their branch for you');
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
let res;
|
|
152
|
+
try {
|
|
153
|
+
res = await improve.improve({ root, engine, env, offline: argv.includes('--offline'), log: argv.includes('--json') ? () => {} : (m) => console.log(` ${m}`), ...(deps.improveOpts || {}) });
|
|
154
|
+
} catch (e) { console.error(` improve did not run: ${e.message}`); return 1; }
|
|
155
|
+
if (argv.includes('--json')) {
|
|
156
|
+
console.log(JSON.stringify({ ok: !res.error, kept: res.kept, branch: res.branch, error: res.error || null, decision: res.decision, receipt: res.receipt }, null, 2));
|
|
157
|
+
return res.error ? 1 : 0;
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
if (res.error) console.log(` stopped: ${res.error}`);
|
|
161
|
+
else if (res.kept) console.log(` kept. the change waits on branch ${res.branch} for your review`);
|
|
162
|
+
else console.log(' rejected. the branch was removed');
|
|
163
|
+
if (res.decision) {
|
|
164
|
+
for (const g of res.decision.gains) console.log(` gain: ${g}`);
|
|
165
|
+
for (const n of res.decision.notes || []) console.log(` note: ${n}`);
|
|
166
|
+
for (const r of res.decision.reasons) console.log(` stop: ${r}`);
|
|
167
|
+
}
|
|
168
|
+
console.log(` receipt: ${res.receipt.replace(require('os').homedir(), '~')}\n`);
|
|
169
|
+
return res.error ? 1 : 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { benchCommand, improveCommand };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris sentinel fix [finding-id|--all-open] [path]
|
|
4
|
+
//
|
|
5
|
+
// Turns a confirmed sentinel finding into a reviewed fix on its own branch.
|
|
6
|
+
// The work lives in lib/sentinel-fix.js; this file reads flags, prices atris
|
|
7
|
+
// model calls before any spend, prints the landing and sends one event.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
function tildePath(p, home = require('os').homedir()) {
|
|
13
|
+
return p && p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseFixArgs(argv) {
|
|
17
|
+
const opts = { positionals: [], allOpen: false, verify: null, builder: null, verifier: null, engine: null, yes: false, pr: false, upload: true, timeoutMs: null };
|
|
18
|
+
const takes = (i, flag) => {
|
|
19
|
+
if (argv[i + 1] === undefined || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
20
|
+
return argv[i + 1];
|
|
21
|
+
};
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const arg = argv[i];
|
|
24
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, null];
|
|
25
|
+
const value = () => (inline !== null ? inline : takes(i++, flag));
|
|
26
|
+
if (flag === '--all-open') opts.allOpen = true;
|
|
27
|
+
else if (flag === '--verify') opts.verify = value();
|
|
28
|
+
else if (flag === '--builder') opts.builder = value();
|
|
29
|
+
else if (flag === '--verifier') opts.verifier = value();
|
|
30
|
+
else if (flag === '--engine') opts.engine = value();
|
|
31
|
+
else if (flag === '--yes' || flag === '-y') opts.yes = true;
|
|
32
|
+
else if (flag === '--pr') opts.pr = true;
|
|
33
|
+
else if (flag === '--no-upload') opts.upload = false;
|
|
34
|
+
else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
|
|
35
|
+
else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
|
|
36
|
+
else opts.positionals.push(arg);
|
|
37
|
+
}
|
|
38
|
+
if (opts.engine !== null && !['atris', 'local'].includes(opts.engine)) throw new Error('--engine must be atris or local');
|
|
39
|
+
if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
|
|
40
|
+
if (opts.verify !== null && !opts.verify.trim()) throw new Error('--verify needs a command');
|
|
41
|
+
// fix <id> <path>, fix <path>, fix <id>, fix --all-open <path>
|
|
42
|
+
const [a, b] = opts.positionals;
|
|
43
|
+
if (opts.positionals.length > 2) throw new Error('give at most a finding id and a path');
|
|
44
|
+
if (opts.allOpen) {
|
|
45
|
+
if (opts.positionals.length > 1) throw new Error('--all-open takes only a path');
|
|
46
|
+
opts.path = a || null;
|
|
47
|
+
} else if (opts.positionals.length === 2) {
|
|
48
|
+
opts.id = a;
|
|
49
|
+
opts.path = b;
|
|
50
|
+
} else if (a !== undefined) {
|
|
51
|
+
let isDir = false;
|
|
52
|
+
try { isDir = fs.statSync(a).isDirectory(); } catch {}
|
|
53
|
+
if (isDir) opts.path = a; else opts.id = a;
|
|
54
|
+
}
|
|
55
|
+
return opts;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printFixHelp() {
|
|
59
|
+
console.log(`
|
|
60
|
+
atris sentinel fix, a reviewed fix for a confirmed finding
|
|
61
|
+
|
|
62
|
+
atris sentinel fix list confirmed findings you can fix
|
|
63
|
+
atris sentinel fix <id> [path] fix one finding on its own branch
|
|
64
|
+
atris sentinel fix --all-open [path] fix every finding with no fix branch yet
|
|
65
|
+
atris sentinel fix <id> --verify "make test"
|
|
66
|
+
run this test command (default: npm test,
|
|
67
|
+
pytest or go test, whichever the repo uses)
|
|
68
|
+
atris sentinel fix <id> --builder claude --verifier codex
|
|
69
|
+
which engine writes the fix and which checks it
|
|
70
|
+
atris sentinel fix <id> --engine atris atris models write and check, after a price
|
|
71
|
+
atris sentinel fix <id> --pr push the branch and open a pull request
|
|
72
|
+
|
|
73
|
+
it needs a clean working tree. the fix is built in a temp copy on a new
|
|
74
|
+
branch named sentinel/fix-<id> made from the branch you are on. your own
|
|
75
|
+
checkout and branch are never changed. nothing is merged. nothing is
|
|
76
|
+
pushed unless you add --pr.
|
|
77
|
+
|
|
78
|
+
builder and checker may be codex, claude or an atris model. atris models
|
|
79
|
+
always show a price and wait for a yes.
|
|
80
|
+
|
|
81
|
+
a fix is committed only when all three hold:
|
|
82
|
+
the new test fails on the old code
|
|
83
|
+
the full test suite passes with the fix
|
|
84
|
+
a second engine reads the fixed source and finds the attack no longer works
|
|
85
|
+
|
|
86
|
+
the new test must fail on a real failed check. a missing module, a syntax
|
|
87
|
+
error or a timeout does not count. otherwise nothing is committed and the
|
|
88
|
+
attempt is saved as a patch under ~/.atris/sentinel/fixes.
|
|
89
|
+
|
|
90
|
+
the new tests run with a temp home, no tokens or keys in the environment
|
|
91
|
+
and push turned off for the temp copy. they still run as you and are not a
|
|
92
|
+
sandbox. read the new test before you run it on code you do not trust.
|
|
93
|
+
`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function askYesNo(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const rl = require('readline').createInterface({ input, output });
|
|
100
|
+
rl.question(question, (answer) => { rl.close(); resolve(/^\s*y(es)?\s*$/i.test(String(answer || ''))); });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sevOf(r) {
|
|
105
|
+
return (r.severity && r.severity.overall) || 'unrated';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function fixCommand(argv, deps = {}) {
|
|
109
|
+
if (argv[0] === 'help' || argv.includes('--help') || argv.includes('-h')) return printFixHelp();
|
|
110
|
+
const fix = deps.fixLib || require('../lib/sentinel-fix');
|
|
111
|
+
const env = deps.env || process.env;
|
|
112
|
+
let opts;
|
|
113
|
+
try { opts = parseFixArgs(argv); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
114
|
+
|
|
115
|
+
const start = path.resolve(opts.path || deps.root || process.cwd());
|
|
116
|
+
const top = fix.git(start, ['rev-parse', '--show-toplevel'], { allowFail: true });
|
|
117
|
+
if (!top) { console.error(` ${tildePath(start)} is not a git repository. a fix needs a branch to go on`); return 2; }
|
|
118
|
+
const root = top;
|
|
119
|
+
|
|
120
|
+
const { records } = fix.confirmedFindings(root);
|
|
121
|
+
if (!records.length) { console.log('\n no confirmed findings for this repository. run atris sentinel first\n'); return 0; }
|
|
122
|
+
|
|
123
|
+
if (!opts.id && !opts.allOpen) {
|
|
124
|
+
console.log('\n confirmed findings you can fix\n');
|
|
125
|
+
for (const r of records) {
|
|
126
|
+
const has = fix.branchExists(root, fix.branchFor(r));
|
|
127
|
+
console.log(` ${fix.shortId(r.fingerprint)} ${sevOf(r).padEnd(8)} ${r.title}${has ? ' (fix branch exists)' : ''}`);
|
|
128
|
+
}
|
|
129
|
+
console.log(`\n fix one with: atris sentinel fix ${fix.shortId(records[0].fingerprint)}\n`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let targets;
|
|
134
|
+
if (opts.allOpen) {
|
|
135
|
+
targets = records.filter((r) => !fix.branchExists(root, fix.branchFor(r)));
|
|
136
|
+
if (!targets.length) { console.log('\n every confirmed finding already has a fix branch\n'); return 0; }
|
|
137
|
+
} else {
|
|
138
|
+
const r = fix.matchFinding(records, opts.id);
|
|
139
|
+
if (!r) { console.error(` no confirmed finding matches ${opts.id}. run atris sentinel fix to list them`); return 2; }
|
|
140
|
+
targets = [r];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (fix.changedPaths(root).length) { console.error(' this repository has uncommitted changes. commit or stash them first so the fix branch holds only the fix'); return 2; }
|
|
144
|
+
const from = fix.git(root, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFail: true });
|
|
145
|
+
if (!from || from === 'HEAD') { console.error(' check out a branch first. a fix branch is made from the branch you are on'); return 2; }
|
|
146
|
+
|
|
147
|
+
const testCommand = opts.verify || fix.detectTestCommand(root);
|
|
148
|
+
if (!testCommand) { console.error(' no test command found. name one with --verify "<command>"'); return 2; }
|
|
149
|
+
|
|
150
|
+
let engines;
|
|
151
|
+
try { engines = fix.resolveFixEngines({ engine: opts.engine, builder: opts.builder, verifier: opts.verifier, env, available: deps.available }); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
152
|
+
|
|
153
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
154
|
+
const hosted = fix.isHosted(engines.builder) || fix.isHosted(engines.verifier);
|
|
155
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
156
|
+
let hostedRun = deps.hostedRun || null;
|
|
157
|
+
let allowed = new Set();
|
|
158
|
+
if (hosted) {
|
|
159
|
+
if (!token) { console.error(' fixes on atris models need a login. run atris login first'); return 2; }
|
|
160
|
+
allowed = require('../lib/security-audit').hostedAllowedFiles(root, []);
|
|
161
|
+
const rows = [];
|
|
162
|
+
if (fix.isHosted(engines.builder)) {
|
|
163
|
+
const sizes = targets.map((r) => Buffer.byteLength(fix.builderPrompt(r, { testCommand, hosted: true, fileList: [...allowed].slice(0, 400), source: fix.hostedSourceBlock(root, fix.citedFiles(r), allowed, 64 * 1024) })));
|
|
164
|
+
const max = Math.max(4096, ...sizes) + 1024;
|
|
165
|
+
rows.push({ role: 'builder', model: engines.builder, calls: targets.length, input_tokens: max, output_tokens: fix.HOSTED_OUTPUT_TOKENS.builder, max_input_bytes: max });
|
|
166
|
+
}
|
|
167
|
+
if (fix.isHosted(engines.verifier)) rows.push({ role: 'verifier', model: engines.verifier, calls: targets.length, input_tokens: fix.HOSTED_CHECK_MAX_BYTES, output_tokens: fix.HOSTED_OUTPUT_TOKENS.verifier, max_input_bytes: fix.HOSTED_CHECK_MAX_BYTES });
|
|
168
|
+
let quote;
|
|
169
|
+
try { quote = await cloud.quoteEstimate({ calls: rows }, { token }); } catch (e) { console.error(` nothing was spent. ${e.message}`); return 2; }
|
|
170
|
+
console.log(`\n ${cloud.describeQuote(quote).replace(/^this review/, 'this fix')}\n`);
|
|
171
|
+
let yes = opts.yes;
|
|
172
|
+
if (!yes) {
|
|
173
|
+
const input = deps.input || process.stdin;
|
|
174
|
+
if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return 1; }
|
|
175
|
+
yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: process.stdout });
|
|
176
|
+
if (!yes) { console.log(' nothing was spent'); return 1; }
|
|
177
|
+
}
|
|
178
|
+
if (!hostedRun) {
|
|
179
|
+
const spend = cloud.spendGuard({ quoteCall: deps.quoteCall });
|
|
180
|
+
spend.max = quote.credits;
|
|
181
|
+
spend.limits = quote.limits;
|
|
182
|
+
const runner = cloud.atrisJobRunner({ token, spend });
|
|
183
|
+
hostedRun = (jobs) => runner(jobs, { root, concurrency: 1, timeoutMs: opts.timeoutMs || 20 * 60 * 1000 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const log = deps.log || ((m) => console.log(` · ${m}`));
|
|
188
|
+
const ctx = {
|
|
189
|
+
from,
|
|
190
|
+
testCommand,
|
|
191
|
+
engines,
|
|
192
|
+
env,
|
|
193
|
+
allowed,
|
|
194
|
+
hostedRun,
|
|
195
|
+
runBuilder: deps.runBuilder || fix.defaultRunBuilder,
|
|
196
|
+
runChecker: deps.runChecker || fix.defaultRunChecker,
|
|
197
|
+
timeouts: { build: opts.timeoutMs || 20 * 60 * 1000, check: opts.timeoutMs || 10 * 60 * 1000, test: 20 * 60 * 1000 },
|
|
198
|
+
log,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
console.log(`\n ◉ sentinel fix · ${engines.builder} writes the fix · ${engines.verifier} checks it · tests: ${testCommand}`);
|
|
202
|
+
const results = [];
|
|
203
|
+
for (const record of targets) {
|
|
204
|
+
console.log(`\n ${fix.shortId(record.fingerprint)} · ${record.title}`);
|
|
205
|
+
const res = await fix.fixOne(root, record, ctx);
|
|
206
|
+
results.push({ record, res });
|
|
207
|
+
if (res.ok) {
|
|
208
|
+
console.log(` prepared a fix on a new branch: ${res.branch}`);
|
|
209
|
+
console.log(` the new test failed on the old code and passes with the fix. the full test suite passes. ${engines.verifier} checked from source that the attack no longer works`);
|
|
210
|
+
console.log(` review it with: git diff ${from}...${res.branch}`);
|
|
211
|
+
if (opts.pr) {
|
|
212
|
+
const pr = fix.openPr(root, res, record, { run: deps.run, verifier: engines.verifier });
|
|
213
|
+
res.pr = pr;
|
|
214
|
+
if (pr.ok) console.log(` opened a pull request${pr.url ? `: ${pr.url}` : ''}. nothing was merged`);
|
|
215
|
+
else console.log(` no pull request was opened: ${pr.reason}${pr.pushed ? '' : '. nothing was pushed'}`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(' nothing was pushed');
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
console.log(` no fix was committed: ${res.reason}`);
|
|
221
|
+
if (res.branch === null || res.branch === undefined) console.log(` you are on ${from}${res.patch ? `. the attempt is saved at ${tildePath(res.patch, env.HOME || undefined)}` : ''}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const done = results.filter((x) => x.res.ok);
|
|
226
|
+
if (done.length) {
|
|
227
|
+
const events = require('../lib/sentinel-events');
|
|
228
|
+
let upload;
|
|
229
|
+
if (!opts.upload) upload = { skipped: 'no-upload' };
|
|
230
|
+
else if (!token) upload = { skipped: 'not-logged-in' };
|
|
231
|
+
else {
|
|
232
|
+
try { upload = await events.postEvents(done.map((x) => fix.fixEvent(root, x.record, x.res)), { token, base: deps.base }); } catch (e) { upload = { ok: false, reason: e.message }; }
|
|
233
|
+
}
|
|
234
|
+
if (upload.skipped === 'no-upload') console.log('\n kept on this machine. nothing was sent (--no-upload)');
|
|
235
|
+
else if (upload.skipped === 'not-logged-in') console.log('\n not logged in. nothing was sent to your dashboard');
|
|
236
|
+
else if (!upload.ok) console.log(`\n warning: could not tell your atris dashboard: ${upload.reason}. the fix itself is done`);
|
|
237
|
+
else console.log('\n told your atris dashboard. no code was sent');
|
|
238
|
+
}
|
|
239
|
+
console.log('');
|
|
240
|
+
return results.every((x) => x.res.ok) ? 0 : 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { fixCommand, parseFixArgs };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// atris sentinel agents [path] [--home] check an agent setup
|
|
7
7
|
// atris sentinel guard install|uninstall|status|test [--global]
|
|
8
|
-
// atris sentinel watch [path] [--fix]
|
|
8
|
+
// atris sentinel watch [path] [--fix] [--allow-major] known attacks in dependencies
|
|
9
9
|
// atris sentinel flush send queued guard events
|
|
10
10
|
//
|
|
11
11
|
// An upload problem is a warning line. It never changes the exit code.
|
|
@@ -50,6 +50,34 @@ function uploadLine(res, what) {
|
|
|
50
50
|
return ` sent ${res.sent - (res.resolved || 0)} ${what} to your atris dashboard${resolved}. no file contents or secret values were sent`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// ---------------------------------------------------------------- update
|
|
54
|
+
|
|
55
|
+
// A stale global install misses sentinel fixes. Every sentinel command
|
|
56
|
+
// ends with one line when npm has a newer atris. The check starts with the
|
|
57
|
+
// command and gets a short wait at the end. It never blocks or fails a run.
|
|
58
|
+
function startUpdateNotice(argv = [], { env = process.env, check, timeoutMs = 1500, packageRoot = path.join(__dirname, '..') } = {}) {
|
|
59
|
+
const off = env.ATRIS_SKIP_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER
|
|
60
|
+
|| argv.some((a) => ['--json', '--help', '-h', 'help', '--quiet'].includes(a));
|
|
61
|
+
if (off) return { print: async () => null };
|
|
62
|
+
const updates = require('../utils/update-check');
|
|
63
|
+
let pending;
|
|
64
|
+
try { pending = Promise.resolve((check || updates.checkForUpdates)()).catch(() => null); } catch { pending = Promise.resolve(null); }
|
|
65
|
+
return {
|
|
66
|
+
async print(log = console.log) {
|
|
67
|
+
let timer;
|
|
68
|
+
const late = new Promise((r) => { timer = setTimeout(() => r(null), timeoutMs); if (timer.unref) timer.unref(); });
|
|
69
|
+
const info = await Promise.race([pending, late]);
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
if (!info || !info.needsUpdate) return null;
|
|
72
|
+
let how = 'npm install -g atris@latest';
|
|
73
|
+
try { if (updates.isGitCheckout(packageRoot)) how = 'atris upgrade'; } catch {}
|
|
74
|
+
const line = ` a newer atris is out: ${info.latest}. this one is ${info.installed}. update with: ${how}`;
|
|
75
|
+
log(line);
|
|
76
|
+
return line;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
// ---------------------------------------------------------------- agents
|
|
54
82
|
|
|
55
83
|
async function runAgents(root, { home = false, upload = true, deps = {} } = {}) {
|
|
@@ -83,37 +111,92 @@ async function agentsCommand(argv, deps = {}) {
|
|
|
83
111
|
|
|
84
112
|
// ---------------------------------------------------------------- watch
|
|
85
113
|
|
|
114
|
+
// A threat that is gone here counts as fixed only on the repo's default
|
|
115
|
+
// branch, or when the default branch no longer holds the affected version.
|
|
116
|
+
// On a side branch it stays open on the dashboard.
|
|
117
|
+
function holdForBranch(branch) {
|
|
118
|
+
if (!branch || branch.onDefault) return null;
|
|
119
|
+
const live = branch.liveFingerprints;
|
|
120
|
+
if (!live) return () => true;
|
|
121
|
+
return (fp) => live.has(fp);
|
|
122
|
+
}
|
|
123
|
+
|
|
86
124
|
async function runWatchCheck(root, { upload = true, deps = {} } = {}) {
|
|
87
125
|
const watch = require('../lib/sentinel-watch');
|
|
88
126
|
const events = require('../lib/sentinel-events');
|
|
89
|
-
const result = await watch.runWatch(root, { base: deps.osvBase || process.env.ATRIS_OSV_BASE || watch.OSV_BASE, ...(deps.request ? { request: deps.request } : {}) });
|
|
127
|
+
const result = await watch.runWatch(root, { base: deps.osvBase || process.env.ATRIS_OSV_BASE || watch.OSV_BASE, ...(deps.request ? { request: deps.request } : {}), ...(deps.maxLockfileBytes ? { maxBytes: deps.maxLockfileBytes } : {}) });
|
|
90
128
|
const evs = watch.toEvents(result, { repo: deps.repo });
|
|
129
|
+
let branch = null;
|
|
130
|
+
try { branch = watch.branchState(root, { run: deps.run }); } catch { branch = { git: true, onDefault: false, liveFingerprints: null }; }
|
|
91
131
|
let res;
|
|
92
132
|
if (result.offline) res = { ok: true, skipped: 'offline', sent: 0 };
|
|
93
133
|
else {
|
|
94
134
|
try {
|
|
95
|
-
|
|
135
|
+
// Per lockfile: an earlier threat is fixed only when every lockfile it
|
|
136
|
+
// was found in was read this time and none of them holds it now. One
|
|
137
|
+
// whose lockfile was not read (depth, cap, size, error, moved or
|
|
138
|
+
// deleted) stays open. An old record with no lockfile list needs a
|
|
139
|
+
// complete scan.
|
|
140
|
+
const read = new Set(result.lockfiles.map((l) => l.file));
|
|
141
|
+
const branchHold = holdForBranch(branch);
|
|
142
|
+
const why = { unread: 0, branch: 0 };
|
|
143
|
+
// Locations not read this run are kept by reportFindings itself. Here:
|
|
144
|
+
// an old record with no lockfile list plus the branch rule.
|
|
145
|
+
const hold = (fp, prev) => {
|
|
146
|
+
const lfs = prev && Array.isArray(prev.lockfiles) && prev.lockfiles.length ? prev.lockfiles : null;
|
|
147
|
+
if (!lfs && result.incomplete) { why.unread += 1; return true; }
|
|
148
|
+
if (branchHold && branchHold(fp)) { why.branch += 1; return true; }
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
res = await events.reportFindings(root, 'threat', evs, { token: upload ? tokenFor(deps) : '', base: deps.base, env: deps.env || process.env, upload, hold, meta: watch.lockfilesByFingerprint(result), read });
|
|
152
|
+
res.heldUnread = why.unread + (res.heldLocations || 0);
|
|
153
|
+
res.heldBranch = why.branch;
|
|
96
154
|
} catch (e) { res = { ok: false, reason: e.message }; }
|
|
97
155
|
}
|
|
98
|
-
return { result, events: evs, upload: res };
|
|
156
|
+
return { result, events: evs, upload: res, branch };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function heldLines(upload, branch) {
|
|
160
|
+
const L = [];
|
|
161
|
+
const u = (upload && upload.heldUnread) || 0;
|
|
162
|
+
const b = (upload && upload.heldBranch) || 0;
|
|
163
|
+
if (u) L.push(` ${u} earlier threat${u === 1 ? ' was' : 's were'} in a lockfile this check did not read. ${u === 1 ? 'it stays' : 'they stay'} open on your dashboard`);
|
|
164
|
+
if (b) {
|
|
165
|
+
const target = (branch && branch.defaultBranch) || 'the default branch';
|
|
166
|
+
L.push(` fixed on this branch, not yet live: ${b} threat${b === 1 ? '' : 's'}. ${b === 1 ? 'it stays' : 'they stay'} open on your dashboard until the fix is merged into ${target}`);
|
|
167
|
+
}
|
|
168
|
+
return L;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// One or two lines per package left for a person. Never cut short.
|
|
172
|
+
function manualLines(manual) {
|
|
173
|
+
const L = [];
|
|
174
|
+
if (!manual || !manual.length) return L;
|
|
175
|
+
L.push(` left for you to update (${manual.length}):`);
|
|
176
|
+
for (const a of manual) {
|
|
177
|
+
const to = a.fixed ? ` to ${a.fixed}` : '';
|
|
178
|
+
L.push(` ${a.name} ${a.version}${to} in ${a.lockfile || a.ecosystem}: ${a.why || 'not updated'}`);
|
|
179
|
+
}
|
|
180
|
+
return L;
|
|
99
181
|
}
|
|
100
182
|
|
|
101
183
|
async function watchCommand(argv, deps = {}) {
|
|
102
184
|
let opts;
|
|
103
|
-
try { opts = parseFlags(argv, ['fix', 'json', 'no-upload']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
185
|
+
try { opts = parseFlags(argv, ['fix', 'json', 'no-upload', 'allow-major']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
104
186
|
let root;
|
|
105
187
|
try { root = resolveRoot(opts, deps); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
106
188
|
const watch = require('../lib/sentinel-watch');
|
|
107
|
-
const { result, upload } = await runWatchCheck(root, { upload: !opts.noUpload, deps });
|
|
189
|
+
const { result, upload, branch } = await runWatchCheck(root, { upload: !opts.noUpload, deps });
|
|
108
190
|
let fix = null;
|
|
109
191
|
if (opts.fix && !result.offline && result.affected.length) {
|
|
110
|
-
try { fix = watch.applyFix(root, result, {
|
|
192
|
+
try { fix = watch.applyFix(root, result, { run: deps.run, allowMajor: Boolean(opts.allowMajor) }); } catch (e) { fix = { ok: false, reason: String(e.message || e).split('\n')[0] }; }
|
|
111
193
|
}
|
|
112
194
|
if (opts.json) {
|
|
113
|
-
console.log(JSON.stringify({ ok: !result.affected.some((a) => a.severity === 'critical' || a.severity === 'high'), ...result, upload, fix }, null, 2));
|
|
195
|
+
console.log(JSON.stringify({ ok: !result.affected.some((a) => a.severity === 'critical' || a.severity === 'high'), ...result, upload, branch: branch ? { ...branch, liveFingerprints: undefined } : null, fix }, null, 2));
|
|
114
196
|
} else {
|
|
115
197
|
console.log(watch.renderReport(result));
|
|
116
198
|
if (upload.skipped !== 'offline') console.log(uploadLine(upload, 'the threats'));
|
|
199
|
+
for (const l of heldLines(upload, branch)) console.log(l);
|
|
117
200
|
if (fix) {
|
|
118
201
|
console.log('');
|
|
119
202
|
if (!fix.ok) console.log(` the fix did not run: ${fix.reason}`);
|
|
@@ -124,9 +207,12 @@ async function watchCommand(argv, deps = {}) {
|
|
|
124
207
|
console.log(` go back with: git checkout ${fix.from || 'main'}`);
|
|
125
208
|
console.log(' nothing was pushed');
|
|
126
209
|
}
|
|
127
|
-
|
|
128
|
-
console.log(`
|
|
210
|
+
for (const te of fix.toolErrors || []) {
|
|
211
|
+
console.log(` ${te.tool} could not update ${te.lockfile}. it said:`);
|
|
212
|
+
for (const l of te.lines) console.log(` ${l}`);
|
|
213
|
+
if (te.hint) console.log(` ${te.hint}`);
|
|
129
214
|
}
|
|
215
|
+
for (const l of manualLines(fix.manual)) console.log(l);
|
|
130
216
|
}
|
|
131
217
|
console.log('');
|
|
132
218
|
}
|
|
@@ -281,4 +367,4 @@ async function runScheduledFreeChecks(root, deps = {}) {
|
|
|
281
367
|
return out;
|
|
282
368
|
}
|
|
283
369
|
|
|
284
|
-
module.exports = { agentsCommand, guardCommand, watchCommand, flushCommand, runScheduledFreeChecks, runAgents, runWatchCheck };
|
|
370
|
+
module.exports = { agentsCommand, guardCommand, watchCommand, flushCommand, runScheduledFreeChecks, runAgents, runWatchCheck, startUpdateNotice };
|