atris 3.82.3 → 3.82.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/atris/atris.md +6 -0
- package/bin/atris.js +2 -0
- package/commands/aeo.js +15 -2
- package/commands/next.js +16 -6
- package/commands/now.js +6 -1
- package/commands/security-review.js +172 -17
- package/commands/sentinel-powers.js +284 -0
- package/commands/task.js +9 -2
- package/lib/agent-next.js +255 -0
- package/lib/first-minute.js +8 -4
- package/lib/member-context.js +11 -2
- package/lib/security-audit.js +526 -13
- package/lib/sentinel-agents.js +584 -0
- package/lib/sentinel-cloud.js +13 -5
- package/lib/sentinel-events.js +431 -0
- package/lib/sentinel-guard-install.js +163 -0
- package/lib/sentinel-guard.js +876 -0
- package/lib/sentinel-watch.js +535 -0
- package/lib/task-core.js +3 -3
- package/lib/task-db.js +12 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Mem0, Zep and Letta remember the users of a chat app. Atris remembers the projec
|
|
|
28
28
|
npx -y atris aeo scan yoursite.com --vs competitor.com
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
You get a score out of 100, each gap with its fix, a side-by-side score against a competitor, and a draft `llms.txt` you can publish. To see what ChatGPT actually says when buyers ask about your business, run the free check at https://atris.ai/aeo/check.
|
|
31
|
+
You get a score out of 100, each gap with its fix, a side-by-side score against a competitor, and a draft `llms.txt` you can publish. To see what ChatGPT actually says when buyers ask about your business, run the free check at https://atris.ai/aeo/check. Guide to both checks and the tools that run them: [docs/answer-engine-checks.md](docs/answer-engine-checks.md).
|
|
32
32
|
|
|
33
33
|
## Quick Start
|
|
34
34
|
|
package/atris/atris.md
CHANGED
|
@@ -149,6 +149,12 @@ The Mission anchors, goals point, a mission run commits, tasks prove, accepts la
|
|
|
149
149
|
|
|
150
150
|
## task source of truth
|
|
151
151
|
|
|
152
|
+
For the next agent step, use `atris next --agent <functional-member> --json`.
|
|
153
|
+
Known worker shells choose this view automatically; `--human` shows the
|
|
154
|
+
person's view without changing acceptance permission. An unchecked task
|
|
155
|
+
requires a goal, exit, first move and runnable check through `atris task plan`.
|
|
156
|
+
The recommendation itself never claims, executes, accepts or awards XP.
|
|
157
|
+
|
|
152
158
|
Use `atris task` as the source of truth for active work. It stores durable local
|
|
153
159
|
SQLite state plus append-only task events, and refreshes
|
|
154
160
|
`.atris/state/tasks.projection.json` for desktop/web UIs. `atris/TODO.md` is a
|
package/bin/atris.js
CHANGED
|
@@ -633,6 +633,8 @@ function showHelpAll() {
|
|
|
633
633
|
console.log(' verify - Validate work is done (tests, MAP.md, changes)');
|
|
634
634
|
console.log(' slop - Deterministic slop detector: frontend/prose tells, dead code (slop dead --exports)');
|
|
635
635
|
console.log(' sentinel - Security audit: engines hunt for holes, a different model verifies each one');
|
|
636
|
+
console.log(' free checks: sentinel agents, sentinel watch, sentinel guard install');
|
|
637
|
+
console.log(' re-check old findings against today\'s code: sentinel recheck');
|
|
636
638
|
console.log(' voice - Scan binary voice tells or judge reply shape and plainness');
|
|
637
639
|
console.log(' recover - Explain stopped missions; --apply performs safe recovery only');
|
|
638
640
|
console.log(' task - Local agent task plane (atomic claims, TODO import)');
|
package/commands/aeo.js
CHANGED
|
@@ -217,10 +217,23 @@ async function aeoScan(args) {
|
|
|
217
217
|
console.log('\n Gaps:');
|
|
218
218
|
for (const g of gaps) console.log(` ✗ ${g.title || g.key}: ${g.fix || ''}`);
|
|
219
219
|
}
|
|
220
|
+
// What an agent actually gets from live requests: plain text on request,
|
|
221
|
+
// crawler access, words without scripts, structured data, sitemap.
|
|
222
|
+
const access = data.agent_access;
|
|
223
|
+
if (access && Array.isArray(access.checks)) {
|
|
224
|
+
console.log(`\n Agent access: ${access.score}/${access.max_score ?? 100}`);
|
|
225
|
+
for (const c of access.checks) {
|
|
226
|
+
console.log(` ${c.passed ? '✓' : '✗'} ${c.title}${c.passed ? '' : `: ${c.fix || c.detail || ''}`}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
220
229
|
const comp = data.competitors || [];
|
|
221
230
|
if (comp.length) {
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
const withAccess = comp.some((c) => c.agent_access_score != null);
|
|
232
|
+
console.log(withAccess ? '\n Head-to-head (score, agent access):' : '\n Head-to-head:');
|
|
233
|
+
for (const c of comp) {
|
|
234
|
+
const extra = withAccess ? `${pad(String(c.agent_access_score ?? '?'), 4)} ` : '';
|
|
235
|
+
console.log(` ${pad(String(c.score ?? '?'), 4)} ${extra}${c.url || c.name}`);
|
|
236
|
+
}
|
|
224
237
|
}
|
|
225
238
|
if (data.draft_llms_txt) {
|
|
226
239
|
console.log('\n Draft llms.txt included, save it with:');
|
package/commands/next.js
CHANGED
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
} = require('../lib/next-moves');
|
|
20
20
|
|
|
21
21
|
function showHelp(log = console.log) {
|
|
22
|
-
log('Usage: atris next [--json]');
|
|
22
|
+
log('Usage: atris next [--agent <member>|--human] [--json]');
|
|
23
23
|
return 0;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -179,12 +179,17 @@ function executeCard(card, root, log = console.log) {
|
|
|
179
179
|
|
|
180
180
|
function actionToken(args = []) {
|
|
181
181
|
const list = Array.isArray(args) ? args : [];
|
|
182
|
-
|
|
182
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
183
|
+
const arg = list[i];
|
|
183
184
|
const text = String(arg || '').trim();
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
if (['--agent', '--as', '--task', '--goal-id', '--goal_id', '--tag', '--status', '--review-state', '--review_state'].includes(text)) {
|
|
186
|
+
if (list[i + 1] && !String(list[i + 1]).startsWith('-')) i += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!text || text.startsWith('-')) continue;
|
|
190
|
+
return text.toLowerCase();
|
|
191
|
+
}
|
|
192
|
+
return '';
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
function nextCommand(args = [], root = process.cwd(), { log = console.log } = {}) {
|
|
@@ -193,6 +198,11 @@ function nextCommand(args = [], root = process.cwd(), { log = console.log } = {}
|
|
|
193
198
|
|
|
194
199
|
const asJson = wantsJson(list);
|
|
195
200
|
const action = actionToken(list);
|
|
201
|
+
if (!['skip', 'no', 'yes'].includes(action)) {
|
|
202
|
+
const { buildAgentNext, printAgentNext } = require('../lib/agent-next');
|
|
203
|
+
const agent = buildAgentNext({ root, args: list });
|
|
204
|
+
if (agent) return printAgentNext(agent, { asJson, log });
|
|
205
|
+
}
|
|
196
206
|
|
|
197
207
|
if (!action || action === 'json') return speakNext(root, { asJson, log });
|
|
198
208
|
|
package/commands/now.js
CHANGED
|
@@ -612,7 +612,7 @@ function nowJsonPayload(root = process.cwd()) {
|
|
|
612
612
|
function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
|
|
613
613
|
const help = args.includes('--help') || args.includes('-h') || args[0] === 'help';
|
|
614
614
|
if (help) {
|
|
615
|
-
console.log('Usage: atris now [--init|--refresh|--all|--path] [--json]');
|
|
615
|
+
console.log('Usage: atris now [--init|--refresh|--all|--path] [--agent <member>|--human] [--json]');
|
|
616
616
|
console.log('');
|
|
617
617
|
console.log('Show the current operating truth for this workspace.');
|
|
618
618
|
console.log('');
|
|
@@ -632,6 +632,11 @@ function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
|
|
|
632
632
|
const asJson = args.includes('--json');
|
|
633
633
|
|
|
634
634
|
try {
|
|
635
|
+
if (!init && !refresh && !all && !pathOnly) {
|
|
636
|
+
const { buildAgentNext, printAgentNext } = require('../lib/agent-next');
|
|
637
|
+
const agent = buildAgentNext({ root, args });
|
|
638
|
+
if (agent) return printAgentNext(agent, { asJson, alias: 'next' });
|
|
639
|
+
}
|
|
635
640
|
if (asJson && !init && !refresh && !all && !pathOnly) {
|
|
636
641
|
const payload = nowJsonPayload(root);
|
|
637
642
|
printNowJson(payload);
|
|
@@ -457,13 +457,15 @@ function renderAuditLanding({ dir, meta, records }) {
|
|
|
457
457
|
if (hunters < 1) L.push(' this budget is too small to send a hunter; try --budget 8 or more');
|
|
458
458
|
else L.push(needed <= hunters ? ' one run can read every area' : ` about ${Math.ceil(needed / hunters)} runs to read every area`);
|
|
459
459
|
} else {
|
|
460
|
-
|
|
460
|
+
const waiting = require('../lib/security-audit').waitingOf(meta);
|
|
461
|
+
L.push(` ${confirmed.length + waiting.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
|
|
461
462
|
L.push(` read ${cov.fresh} areas this run, ${cov.carried} carried unchanged, ${cov.deferred || 0} left for the next run`);
|
|
462
463
|
L.push(` found by ${meta.engines.hunter}, checked by ${meta.engines.verifier}${meta.engines.hunter === meta.engines.verifier ? ' (same engine)' : ''} · ${meta.invocations.used} engine calls of ${meta.budget}${meta.credits_used ? ` · ${meta.credits_used} credits used` : ''}`);
|
|
463
464
|
for (const r of confirmed.slice(0, 6)) {
|
|
464
465
|
const sink = r.trace[r.trace.length - 1];
|
|
465
466
|
L.push(` ${r.severity.overall.padEnd(8)} ${r.title} (${sink.file}:${sink.line})`);
|
|
466
467
|
}
|
|
468
|
+
for (const w of waiting.slice(0, 6)) L.push(` ${w.severity.padEnd(8)} ${w.title} (last seen at ${w.location}, still open: ${w.why})`);
|
|
467
469
|
if (meta.run_status === 'incomplete') L.push(` incomplete: ${String(meta.incomplete_reason).replace(/_/g, ' ')}`);
|
|
468
470
|
}
|
|
469
471
|
L.push(` report: ${tildePath(path.join(dir, 'REPORT.md'))}`, '');
|
|
@@ -478,11 +480,28 @@ function askYesNo(question, { input = process.stdin, output = process.stdout } =
|
|
|
478
480
|
});
|
|
479
481
|
}
|
|
480
482
|
|
|
481
|
-
async function uploadAfterRun(dir, { token, quiet, cloud }) {
|
|
483
|
+
async function uploadAfterRun(dir, { token, quiet, cloud, root = null, env = process.env }) {
|
|
482
484
|
const res = await cloud.uploadRun(dir, { token });
|
|
483
485
|
const say = quiet ? (m) => console.error(m) : (m) => console.log(m);
|
|
484
486
|
if (res.ok) say(' saved the summary to your atris account (no source code was sent)');
|
|
485
487
|
else say(` warning: could not save the summary to your atris account: ${res.reason}. the review itself is done`);
|
|
488
|
+
if (res.ok && res.body) await sendFixedEvents(dir, res.body, { token, root, env, say });
|
|
489
|
+
return res;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Fixed findings also go to the events feed. The overview counts them as
|
|
493
|
+
// fixed today. A failure here never undoes the saved review.
|
|
494
|
+
async function sendFixedEvents(dir, body, { token, root, env, say }) {
|
|
495
|
+
if (!body.findings.some((f) => f.status === 'fixed')) return null;
|
|
496
|
+
let meta = null;
|
|
497
|
+
try { meta = JSON.parse(fs.readFileSync(path.join(dir, 'run-metadata.json'), 'utf8')); } catch {}
|
|
498
|
+
const target = root || (meta && meta.target) || process.cwd();
|
|
499
|
+
let res;
|
|
500
|
+
try {
|
|
501
|
+
res = await require('../lib/sentinel-events').reportReviewFixes(target, body, { token, env, foundAt: meta && meta.prior_finished_at });
|
|
502
|
+
} catch (e) { res = { ok: false, reason: e.message }; }
|
|
503
|
+
if (res.ok && res.fixed) say(` marked ${res.fixed} finding${res.fixed === 1 ? '' : 's'} fixed on your dashboard`);
|
|
504
|
+
else if (!res.ok) say(` warning: could not mark the fixed findings on your dashboard: ${res.reason}. run atris sentinel upload to try again`);
|
|
486
505
|
return res;
|
|
487
506
|
}
|
|
488
507
|
|
|
@@ -502,6 +521,7 @@ async function auditUploadCommand(argv, deps) {
|
|
|
502
521
|
const res = await cloud.uploadRun(dir, { token });
|
|
503
522
|
if (!res.ok) { console.error(` could not save ${path.basename(dir)}: ${res.reason}`); return 1; }
|
|
504
523
|
console.log(` saved ${path.basename(dir)} to your atris account (no source code was sent)`);
|
|
524
|
+
if (res.body) await sendFixedEvents(dir, res.body, { token, root: null, env: deps.env || process.env, say: (m) => console.log(m) });
|
|
505
525
|
return 0;
|
|
506
526
|
}
|
|
507
527
|
|
|
@@ -590,6 +610,7 @@ async function auditScheduleCommand(argv, deps) {
|
|
|
590
610
|
if (!installed.ok) { cloud.removeApproval(target, deps.homeDir); console.error(` could not install the timer: ${installed.error}`); return 1; }
|
|
591
611
|
console.log(` scheduled reviews are on for ${tildePath(target)}, once a ${every}`);
|
|
592
612
|
console.log(' each run uses the quick profile and saves its summary to your atris account');
|
|
613
|
+
console.log(' each run also checks your agent setup and dependencies for free');
|
|
593
614
|
if (approval.engine === 'local') console.log(' runs use only codex and claude on this machine. they never switch to atris models');
|
|
594
615
|
else console.log(` runs use atris models and stop at ${approval.max_credits} credits`);
|
|
595
616
|
console.log(` log: ${tildePath(plan.log_path)}`);
|
|
@@ -597,6 +618,119 @@ async function auditScheduleCommand(argv, deps) {
|
|
|
597
618
|
return 0;
|
|
598
619
|
}
|
|
599
620
|
|
|
621
|
+
// The quote-and-confirm step every run on atris models goes through: price
|
|
622
|
+
// the estimate and set the cap. Then a yes, --yes or a scheduled approval.
|
|
623
|
+
function spendConfirmer({ cloud, token, spend, capOverride, opts, deps, tell }) {
|
|
624
|
+
return async (estimate) => {
|
|
625
|
+
const quote = await cloud.quoteEstimate(estimate, { token });
|
|
626
|
+
spend.max = capOverride || quote.credits;
|
|
627
|
+
spend.limits = quote.limits;
|
|
628
|
+
tell(`\n ${cloud.describeQuote(quote)}\n`);
|
|
629
|
+
if (capOverride) tell(` this scheduled run may spend at most ${capOverride} credits, as approved`);
|
|
630
|
+
if (opts.yes) return true;
|
|
631
|
+
const input = deps.input || process.stdin;
|
|
632
|
+
if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return false; }
|
|
633
|
+
const yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: opts.json ? process.stderr : process.stdout });
|
|
634
|
+
if (!yes) tell(' nothing was spent');
|
|
635
|
+
return yes;
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function parseRecheckArgs(argv) {
|
|
640
|
+
const opts = { json: false, verifier: null, engine: null, yes: false, upload: true, timeoutMs: null, budget: null, target: null };
|
|
641
|
+
for (let i = 0; i < argv.length; i++) {
|
|
642
|
+
const arg = argv[i];
|
|
643
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, null];
|
|
644
|
+
const value = () => {
|
|
645
|
+
if (inline !== null) return inline;
|
|
646
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
647
|
+
return argv[++i];
|
|
648
|
+
};
|
|
649
|
+
if (flag === '--json') opts.json = true;
|
|
650
|
+
else if (flag === '--verifier') opts.verifier = value();
|
|
651
|
+
else if (flag === '--engine') opts.engine = value();
|
|
652
|
+
else if (flag === '--yes' || flag === '-y') opts.yes = true;
|
|
653
|
+
else if (flag === '--no-upload') opts.upload = false;
|
|
654
|
+
else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
|
|
655
|
+
else if (flag === '--budget') opts.budget = Number(value());
|
|
656
|
+
else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
|
|
657
|
+
else if (opts.target) throw new Error('give one folder to re-check');
|
|
658
|
+
else opts.target = arg;
|
|
659
|
+
}
|
|
660
|
+
if (opts.engine !== null && !['atris', 'local'].includes(opts.engine)) throw new Error('--engine must be atris or local');
|
|
661
|
+
if (opts.budget !== null && (!Number.isInteger(opts.budget) || opts.budget < 1 || opts.budget > 500)) throw new Error('--budget must be a whole number from 1 to 500');
|
|
662
|
+
if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
|
|
663
|
+
return opts;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const OUTCOME_WORDS = { fixed: 'fixed', open: 'still open', moved: 'still open, moved', unsure: 'unsure, kept open', held: 'kept open' };
|
|
667
|
+
|
|
668
|
+
function renderRecheckLanding(result) {
|
|
669
|
+
const { meta, records, dir } = result;
|
|
670
|
+
const a = meta.audit;
|
|
671
|
+
const byFp = new Map(records.map((r) => [r.fingerprint, r]));
|
|
672
|
+
const L = ['', ` ◉ sentinel re-check · ${meta.repo}`, ''];
|
|
673
|
+
L.push(` re-checked ${a.checked} open finding${a.checked === 1 ? '' : 's'} from ${path.basename(meta.prior_run)}`);
|
|
674
|
+
L.push(` ${a.fixed} fixed · ${a.open} still open · ${a.unchanged} unchanged with no call`);
|
|
675
|
+
for (const n of a.notes) {
|
|
676
|
+
const r = byFp.get(n.fingerprint);
|
|
677
|
+
const held = (a.held || []).find((h) => h.fingerprint === n.fingerprint);
|
|
678
|
+
const title = r ? r.title : held ? held.title : n.fingerprint;
|
|
679
|
+
const sev = r && r.severity ? r.severity.overall : held ? held.severity : '';
|
|
680
|
+
L.push(` ${(OUTCOME_WORDS[n.outcome] || n.outcome).padEnd(18)} ${sev ? `${sev} ` : ''}${title}`);
|
|
681
|
+
L.push(` ${''.padEnd(18)} ${n.why}`);
|
|
682
|
+
}
|
|
683
|
+
L.push(` checked by ${meta.engines.verifier} · ${meta.invocations.used} engine call${meta.invocations.used === 1 ? '' : 's'}${meta.credits_used ? ` · ${meta.credits_used} credits used` : ''}`);
|
|
684
|
+
if (meta.run_status === 'incomplete') L.push(' some findings got no answer. they stay open and wait for the next run');
|
|
685
|
+
L.push(` report: ${tildePath(path.join(dir, 'REPORT.md'))}`, '');
|
|
686
|
+
return L.join('\n');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// atris sentinel recheck [path]: only the auditor, on the last review's open
|
|
690
|
+
// findings. Same engine rules, same price question and cap on atris models,
|
|
691
|
+
// same plain-sentence upload.
|
|
692
|
+
async function auditRecheckCommand(argv, deps = {}) {
|
|
693
|
+
const audit = require('../lib/security-audit');
|
|
694
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
695
|
+
let opts;
|
|
696
|
+
try { opts = parseRecheckArgs(argv); } catch (e) { console.error(`sentinel recheck: ${e.message}`); return 2; }
|
|
697
|
+
const root = path.resolve(opts.target || deps.root || process.cwd());
|
|
698
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) { console.error(`sentinel recheck: ${root} is not a folder`); return 2; }
|
|
699
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
700
|
+
let engines;
|
|
701
|
+
try { engines = cloud.resolveEngines({ engine: opts.engine, verifier: opts.verifier, token, env: deps.env || process.env }); } catch (e) { console.error(`sentinel recheck: ${e.message}`); return 2; }
|
|
702
|
+
const verifier = engines.verifier || undefined;
|
|
703
|
+
const hosted = audit.isHostedEngine(verifier);
|
|
704
|
+
if (hosted && !token) { console.error('sentinel recheck: re-checks on atris models need a login; run atris login first'); return 2; }
|
|
705
|
+
const tell = opts.json ? (m) => console.error(m) : (m) => console.log(m);
|
|
706
|
+
if (hosted && engines.chosen === 'no_local_engine') tell(' no codex or claude found on this machine. this re-check runs on atris models');
|
|
707
|
+
const spend = hosted ? cloud.spendGuard({ quoteCall: deps.quoteCall }) : null;
|
|
708
|
+
let runJobs = deps.runJobs;
|
|
709
|
+
if (!runJobs && hosted) runJobs = cloud.atrisJobRunner({ token, spend });
|
|
710
|
+
const confirmSpend = hosted ? spendConfirmer({ cloud, token, spend, capOverride: null, opts, deps, tell }) : null;
|
|
711
|
+
let result;
|
|
712
|
+
try {
|
|
713
|
+
result = await audit.runRecheck({ root, verifier, runJobs, confirmSpend, budget: opts.budget ?? undefined, timeoutMs: opts.timeoutMs || undefined, log: opts.json ? () => {} : (m) => console.log(` · ${m}`) });
|
|
714
|
+
} catch (e) {
|
|
715
|
+
console.error(`sentinel recheck: ${e.message}`);
|
|
716
|
+
return 2;
|
|
717
|
+
}
|
|
718
|
+
if (result.nothing) {
|
|
719
|
+
if (opts.json) console.log(JSON.stringify({ ok: true, nothing: result.nothing }, null, 2));
|
|
720
|
+
else console.log(`\n ${result.nothing}. nothing to do\n`);
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
if (result.declined) return 1;
|
|
724
|
+
const fail = audit.shouldFailRun(result.dir, result.records);
|
|
725
|
+
if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
|
|
726
|
+
else console.log(renderRecheckLanding(result));
|
|
727
|
+
if (spend && spend.blocked) tell(` stopped at the approved limit of ${spend.max} credits. ${spend.blocked} model call${spend.blocked === 1 ? '' : 's'} did not run. those findings stay open`);
|
|
728
|
+
if (opts.upload && token) {
|
|
729
|
+
try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud, root, env: deps.env || process.env }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the re-check itself is done`); }
|
|
730
|
+
}
|
|
731
|
+
return fail ? 1 : 0;
|
|
732
|
+
}
|
|
733
|
+
|
|
600
734
|
async function auditCommand(argv, deps = {}) {
|
|
601
735
|
if (argv[0] === 'help' || argv.includes('-h') || argv.includes('--help')) return printAuditHelp();
|
|
602
736
|
const audit = require('../lib/security-audit');
|
|
@@ -608,7 +742,13 @@ async function auditCommand(argv, deps = {}) {
|
|
|
608
742
|
console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
|
|
609
743
|
return 0;
|
|
610
744
|
}
|
|
745
|
+
if (['agents', 'guard', 'watch', 'flush'].includes(argv[0])) {
|
|
746
|
+
const powers = require('./sentinel-powers');
|
|
747
|
+
const run = { agents: powers.agentsCommand, guard: powers.guardCommand, watch: powers.watchCommand, flush: powers.flushCommand }[argv[0]];
|
|
748
|
+
return run(argv.slice(1), deps);
|
|
749
|
+
}
|
|
611
750
|
if (argv[0] === 'upload') return auditUploadCommand(argv.slice(1), deps);
|
|
751
|
+
if (argv[0] === 'recheck') return auditRecheckCommand(argv.slice(1), deps);
|
|
612
752
|
if (argv[0] === 'schedule') return auditScheduleCommand(argv.slice(1), deps);
|
|
613
753
|
let opts;
|
|
614
754
|
try { opts = parseAuditArgs(argv); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
|
|
@@ -618,6 +758,11 @@ async function auditCommand(argv, deps = {}) {
|
|
|
618
758
|
if (opts.scheduled) {
|
|
619
759
|
const approval = cloud.readApproval(root, deps.homeDir);
|
|
620
760
|
if (!approval) { console.error('security-review audit: this folder has no approved schedule. nothing ran. run atris sentinel schedule again'); return 2; }
|
|
761
|
+
// The free checks run first, so a review that stops at its credit limit
|
|
762
|
+
// still leaves today's agent and dependency results.
|
|
763
|
+
try {
|
|
764
|
+
await require('./sentinel-powers').runScheduledFreeChecks(root, { ...deps, upload: opts.upload, log: opts.json ? (m) => console.error(m) : (m) => console.log(m) });
|
|
765
|
+
} catch (e) { console.error(` warning: the free checks did not finish: ${e.message}`); }
|
|
621
766
|
opts.hunter = null;
|
|
622
767
|
opts.verifier = null;
|
|
623
768
|
opts.engine = approval.engine;
|
|
@@ -636,19 +781,7 @@ async function auditCommand(argv, deps = {}) {
|
|
|
636
781
|
let runJobs = deps.runJobs;
|
|
637
782
|
const spend = hosted ? cloud.spendGuard({ quoteCall: deps.quoteCall }) : null;
|
|
638
783
|
if (!runJobs && hosted) runJobs = cloud.atrisJobRunner({ token, spend });
|
|
639
|
-
const confirmSpend =
|
|
640
|
-
const quote = await cloud.quoteEstimate(estimate, { token });
|
|
641
|
-
spend.max = capOverride || quote.credits;
|
|
642
|
-
spend.limits = quote.limits;
|
|
643
|
-
tell(`\n ${cloud.describeQuote(quote)}\n`);
|
|
644
|
-
if (capOverride) tell(` this scheduled run may spend at most ${capOverride} credits, as approved`);
|
|
645
|
-
if (opts.yes) return true;
|
|
646
|
-
const input = deps.input || process.stdin;
|
|
647
|
-
if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return false; }
|
|
648
|
-
const yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: opts.json ? process.stderr : process.stdout });
|
|
649
|
-
if (!yes) tell(' nothing was spent');
|
|
650
|
-
return yes;
|
|
651
|
-
};
|
|
784
|
+
const confirmSpend = spendConfirmer({ cloud, token, spend, capOverride, opts, deps, tell });
|
|
652
785
|
let result;
|
|
653
786
|
try {
|
|
654
787
|
result = await audit.runAudit({
|
|
@@ -661,12 +794,12 @@ async function auditCommand(argv, deps = {}) {
|
|
|
661
794
|
return 2;
|
|
662
795
|
}
|
|
663
796
|
if (result.declined) return 1;
|
|
664
|
-
const fail = audit.
|
|
797
|
+
const fail = !opts.planOnly && audit.shouldFailRun(result.dir, result.records);
|
|
665
798
|
if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
|
|
666
799
|
else console.log(renderAuditLanding(result));
|
|
667
800
|
if (spend && spend.blocked) tell(` stopped at the approved limit of ${spend.max} credits. ${spend.blocked} model call${spend.blocked === 1 ? '' : 's'} did not run and wait for the next run`);
|
|
668
801
|
if (opts.upload && token && !opts.planOnly) {
|
|
669
|
-
try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the review itself is done`); }
|
|
802
|
+
try { await uploadAfterRun(result.dir, { token, quiet: opts.json, cloud, root, env: deps.env || process.env }); } catch (e) { tell(` warning: could not save the summary to your atris account: ${e.message}. the review itself is done`); }
|
|
670
803
|
}
|
|
671
804
|
return fail ? 1 : 0;
|
|
672
805
|
}
|
|
@@ -703,9 +836,30 @@ function printAuditHelp() {
|
|
|
703
836
|
atris sentinel --no-upload keep this run on this machine only
|
|
704
837
|
atris sentinel upload [run-folder] save a past run (default: the last one)
|
|
705
838
|
|
|
839
|
+
re-checking old findings (every review does this first):
|
|
840
|
+
|
|
841
|
+
atris sentinel recheck [path] only re-check the last review's open findings
|
|
842
|
+
atris sentinel recheck --engine atris re-check on atris models, price shown first
|
|
843
|
+
|
|
844
|
+
a finding whose code did not change stays open without a call. a changed
|
|
845
|
+
one goes to the verifier, never the engine that found it. it is marked
|
|
846
|
+
fixed only with a yes and the line that now closes it. unsure stays open.
|
|
847
|
+
|
|
706
848
|
only the summary is saved: plain sentences, file:line locations and counts.
|
|
707
849
|
source code and code lines never leave in an upload.
|
|
708
850
|
|
|
851
|
+
free checks, no model and no credits:
|
|
852
|
+
|
|
853
|
+
atris sentinel agents [path] check agent instructions, settings, hooks, mcp and skills
|
|
854
|
+
atris sentinel agents --home also check ~/.claude and ~/.codex
|
|
855
|
+
atris sentinel watch [path] known attacks in your dependencies (osv database)
|
|
856
|
+
atris sentinel watch --fix update affected packages on a new branch, never pushed
|
|
857
|
+
atris sentinel guard install [--global] stop risky agent commands before they run
|
|
858
|
+
atris sentinel guard test see which sample commands the guard stops
|
|
859
|
+
|
|
860
|
+
results go to your atris dashboard when you are logged in. add --no-upload
|
|
861
|
+
to keep them on this machine. secret values and file contents never leave.
|
|
862
|
+
|
|
709
863
|
reviews on a timer:
|
|
710
864
|
|
|
711
865
|
atris sentinel schedule --every day [path] quick review once a day, saved to your account
|
|
@@ -714,6 +868,7 @@ function printAuditHelp() {
|
|
|
714
868
|
timed runs on atris models, at most 20 credits each
|
|
715
869
|
atris sentinel schedule --uninstall [path] stop the timer
|
|
716
870
|
|
|
871
|
+
every timed run also runs the free agents and watch checks.
|
|
717
872
|
the engine is picked when the timer goes on. on atris models you see the
|
|
718
873
|
price and say yes then. a timed run never switches to atris models later.
|
|
719
874
|
|