atris 3.82.2 → 3.82.4
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 +1 -0
- package/commands/aeo.js +15 -2
- package/commands/next.js +16 -6
- package/commands/now.js +6 -1
- package/commands/security-review.js +239 -8
- package/commands/sentinel-powers.js +284 -0
- package/commands/task.js +9 -2
- package/lib/agent-next.js +255 -0
- package/lib/campaign-keep.js +1 -0
- package/lib/first-minute.js +13 -6
- package/lib/member-context.js +11 -2
- package/lib/security-audit.js +191 -12
- package/lib/sentinel-agents.js +584 -0
- package/lib/sentinel-cloud.js +561 -0
- package/lib/sentinel-events.js +320 -0
- package/lib/sentinel-guard-install.js +163 -0
- package/lib/sentinel-guard.js +856 -0
- package/lib/sentinel-watch.js +529 -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,7 @@ 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');
|
|
636
637
|
console.log(' voice - Scan binary voice tells or judge reply shape and plainness');
|
|
637
638
|
console.log(' recover - Explain stopped missions; --apply performs safe recovery only');
|
|
638
639
|
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);
|
|
@@ -123,10 +123,10 @@ function applyBaselineOptions(root, rawFindings, opts) {
|
|
|
123
123
|
|
|
124
124
|
function securityReviewCommand(argv = []) {
|
|
125
125
|
const sub = argv[0];
|
|
126
|
+
if (sub === 'audit') return auditCommand(argv.slice(1));
|
|
126
127
|
if (sub === 'help' || argv.includes('-h') || argv.includes('--help')) return printHelp();
|
|
127
128
|
if (sub === 'rules') return printRules();
|
|
128
129
|
if (sub === 'hook' || sub === 'install-hook') return installHook();
|
|
129
|
-
if (sub === 'audit') return auditCommand(argv.slice(1));
|
|
130
130
|
|
|
131
131
|
const root = process.cwd();
|
|
132
132
|
let opts;
|
|
@@ -402,7 +402,7 @@ function installHook() {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
function parseAuditArgs(argv) {
|
|
405
|
-
const opts = { profile: 'standard', scope: [], json: false, planOnly: false, budget: null, out: null, hunter: null, verifier: null, timeoutMs: null };
|
|
405
|
+
const opts = { profile: 'standard', scope: [], json: false, planOnly: false, budget: null, out: null, hunter: null, verifier: null, timeoutMs: null, engine: null, yes: false, upload: true, scheduled: false };
|
|
406
406
|
const takes = (i, flag) => {
|
|
407
407
|
if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
408
408
|
return argv[i + 1];
|
|
@@ -420,6 +420,10 @@ function parseAuditArgs(argv) {
|
|
|
420
420
|
else if (flag === '--hunter') opts.hunter = value();
|
|
421
421
|
else if (flag === '--verifier') opts.verifier = value();
|
|
422
422
|
else if (flag === '--timeout') opts.timeoutMs = Number(value()) * 1000;
|
|
423
|
+
else if (flag === '--engine') opts.engine = value();
|
|
424
|
+
else if (flag === '--yes' || flag === '-y') opts.yes = true;
|
|
425
|
+
else if (flag === '--no-upload') opts.upload = false;
|
|
426
|
+
else if (flag === '--scheduled') opts.scheduled = true;
|
|
423
427
|
else if (arg.startsWith('-')) throw new Error(`unknown flag: ${arg}`);
|
|
424
428
|
else opts.scope.push(arg);
|
|
425
429
|
}
|
|
@@ -427,6 +431,7 @@ function parseAuditArgs(argv) {
|
|
|
427
431
|
if (!PROFILES.includes(opts.profile)) throw new Error(`--profile must be one of ${PROFILES.join(', ')}`);
|
|
428
432
|
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');
|
|
429
433
|
if (opts.timeoutMs !== null && (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs < 10000)) throw new Error('--timeout is in seconds, at least 10');
|
|
434
|
+
if (opts.engine !== null && !['atris', 'local'].includes(opts.engine)) throw new Error('--engine must be atris or local');
|
|
430
435
|
return opts;
|
|
431
436
|
}
|
|
432
437
|
|
|
@@ -454,7 +459,7 @@ function renderAuditLanding({ dir, meta, records }) {
|
|
|
454
459
|
} else {
|
|
455
460
|
L.push(` ${confirmed.length} confirmed${sevText ? ` (${sevText})` : ''} · ${needs.length} need a fact from you · ${rejected.length} disproved`);
|
|
456
461
|
L.push(` read ${cov.fresh} areas this run, ${cov.carried} carried unchanged, ${cov.deferred || 0} left for the next run`);
|
|
457
|
-
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}`);
|
|
462
|
+
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` : ''}`);
|
|
458
463
|
for (const r of confirmed.slice(0, 6)) {
|
|
459
464
|
const sink = r.trace[r.trace.length - 1];
|
|
460
465
|
L.push(` ${r.severity.overall.padEnd(8)} ${r.title} (${sink.file}:${sink.line})`);
|
|
@@ -465,33 +470,215 @@ function renderAuditLanding({ dir, meta, records }) {
|
|
|
465
470
|
return L.join('\n');
|
|
466
471
|
}
|
|
467
472
|
|
|
468
|
-
|
|
473
|
+
// Ask once on a terminal. Anything but y or yes is a no.
|
|
474
|
+
function askYesNo(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
475
|
+
return new Promise((resolve) => {
|
|
476
|
+
const rl = require('readline').createInterface({ input, output });
|
|
477
|
+
rl.question(question, (answer) => { rl.close(); resolve(/^\s*y(es)?\s*$/i.test(String(answer || ''))); });
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function uploadAfterRun(dir, { token, quiet, cloud }) {
|
|
482
|
+
const res = await cloud.uploadRun(dir, { token });
|
|
483
|
+
const say = quiet ? (m) => console.error(m) : (m) => console.log(m);
|
|
484
|
+
if (res.ok) say(' saved the summary to your atris account (no source code was sent)');
|
|
485
|
+
else say(` warning: could not save the summary to your atris account: ${res.reason}. the review itself is done`);
|
|
486
|
+
return res;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function auditUploadCommand(argv, deps) {
|
|
490
|
+
const audit = require('../lib/security-audit');
|
|
491
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
492
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
493
|
+
if (!token) { console.error(' log in first with atris login. then run atris sentinel upload again'); return 2; }
|
|
494
|
+
const arg = argv.find((a) => !a.startsWith('-'));
|
|
495
|
+
let dir = arg ? path.resolve(arg) : null;
|
|
496
|
+
if (!dir) {
|
|
497
|
+
const last = audit.latestRun(deps.root || process.cwd());
|
|
498
|
+
if (!last) { console.error(' no sentinel review has run for this repo yet'); return 2; }
|
|
499
|
+
dir = last.dir;
|
|
500
|
+
}
|
|
501
|
+
if (!fs.existsSync(path.join(dir, 'run-metadata.json'))) { console.error(` ${tildePath(dir)} is not a sentinel run folder`); return 2; }
|
|
502
|
+
const res = await cloud.uploadRun(dir, { token });
|
|
503
|
+
if (!res.ok) { console.error(` could not save ${path.basename(dir)}: ${res.reason}`); return 1; }
|
|
504
|
+
console.log(` saved ${path.basename(dir)} to your atris account (no source code was sent)`);
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Price one quick run on atris models for a folder without calling any
|
|
509
|
+
// model: the audit plans, the estimate is quoted, and the plan is dropped.
|
|
510
|
+
async function quoteQuickRun(target, { hunter, verifier, token, cloud }) {
|
|
511
|
+
const audit = require('../lib/security-audit');
|
|
512
|
+
let estimate = null;
|
|
513
|
+
await audit.runAudit({
|
|
514
|
+
root: target, profile: 'quick', hunter, verifier,
|
|
515
|
+
runJobs: async () => { throw new Error('no model call while pricing a schedule'); },
|
|
516
|
+
confirmSpend: async (est) => { estimate = est; return false; },
|
|
517
|
+
});
|
|
518
|
+
if (!estimate) throw new Error('could not plan a review for this folder');
|
|
519
|
+
return cloud.quoteEstimate(estimate, { token });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function auditScheduleCommand(argv, deps) {
|
|
523
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
524
|
+
const { installKeep, removeKeep } = require('../lib/campaign-keep');
|
|
525
|
+
const uninstall = argv.includes('--uninstall');
|
|
526
|
+
let every = 'day';
|
|
527
|
+
let target = null;
|
|
528
|
+
let engine = null;
|
|
529
|
+
let maxCredits = null;
|
|
530
|
+
let yes = false;
|
|
531
|
+
for (let i = 0; i < argv.length; i++) {
|
|
532
|
+
const a = argv[i];
|
|
533
|
+
const [flag, inline] = a.startsWith('--') && a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, null];
|
|
534
|
+
const value = () => (inline !== null ? inline : argv[++i]);
|
|
535
|
+
if (flag === '--every') every = value();
|
|
536
|
+
else if (flag === '--engine') engine = value();
|
|
537
|
+
else if (flag === '--max-credits') maxCredits = Number(value());
|
|
538
|
+
else if (flag === '--yes' || flag === '-y') yes = true;
|
|
539
|
+
else if (flag === '--uninstall' || flag === '--install') continue;
|
|
540
|
+
else if (a.startsWith('-')) { console.error(` unknown flag: ${a}`); return 2; }
|
|
541
|
+
else target = a;
|
|
542
|
+
}
|
|
543
|
+
if (!cloud.SCHEDULE_EVERY[every]) { console.error(' use --every day or --every week'); return 2; }
|
|
544
|
+
if (engine !== null && engine !== 'atris' && engine !== 'local') { console.error(' --engine must be atris or local'); return 2; }
|
|
545
|
+
if (maxCredits !== null && (!Number.isInteger(maxCredits) || maxCredits < 1)) { console.error(' --max-credits must be a whole number of credits, at least 1'); return 2; }
|
|
546
|
+
target = path.resolve(target || deps.root || process.cwd());
|
|
547
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) { console.error(` ${target} is not a folder`); return 2; }
|
|
548
|
+
const atrisBin = deps.atrisBin || path.resolve(__dirname, '..', 'bin', 'atris.js');
|
|
549
|
+
const plan = cloud.schedulePlan({ target, every, homeDir: deps.homeDir, platform: deps.platform, atrisBin, envPath: deps.envPath });
|
|
550
|
+
const keepDeps = deps.spawn ? { spawn: deps.spawn } : {};
|
|
551
|
+
if (uninstall) {
|
|
552
|
+
// Same folder makes the same timer name, so --every does not matter here.
|
|
553
|
+
const removed = removeKeep(plan, keepDeps);
|
|
554
|
+
if (!removed.ok) { console.error(` could not remove the timer: ${removed.error}`); return 1; }
|
|
555
|
+
cloud.removeApproval(target, deps.homeDir);
|
|
556
|
+
console.log(` scheduled reviews ${removed.removed === false ? 'were not on' : 'are off'} for ${tildePath(target)}`);
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
const space = require('../lib/worktree-space');
|
|
560
|
+
if (!deps.atrisBin && (space.isAgentCopyPath(atrisBin) || space.isScratchPath(atrisBin))) {
|
|
561
|
+
console.error(` refusing: this atris runs from ${path.dirname(path.dirname(atrisBin))}, a folder that gets removed. install the timer from a lasting atris install`);
|
|
562
|
+
return 2;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// The engine is decided now, with the owner here, and written down. A
|
|
566
|
+
// scheduled run never picks again.
|
|
567
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
568
|
+
let engines;
|
|
569
|
+
try { engines = cloud.resolveEngines({ engine, token, env: deps.env || process.env }); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
570
|
+
let approval;
|
|
571
|
+
if (engines.mode === 'atris') {
|
|
572
|
+
let quote;
|
|
573
|
+
try { quote = await quoteQuickRun(target, { hunter: engines.hunter, verifier: engines.verifier, token, cloud }); } catch (e) { console.error(` no timer was set. ${e.message}`); return 2; }
|
|
574
|
+
const cap = maxCredits || quote.credits;
|
|
575
|
+
console.log(`\n each scheduled review runs on atris models (no codex or claude needed)`);
|
|
576
|
+
console.log(` ${cloud.describeQuote(quote)}`);
|
|
577
|
+
console.log(` each run may spend at most ${cap} credits, once a ${every}. a run that reaches that stops\n`);
|
|
578
|
+
if (!yes) {
|
|
579
|
+
const input = deps.input || process.stdin;
|
|
580
|
+
if (!input.isTTY && !deps.input) { console.error(' no timer was set. add --yes to approve this spend without the question'); return 1; }
|
|
581
|
+
const ok = await (deps.ask || askYesNo)(` approve up to ${cap} credits per scheduled run? (y/N) `, { input, output: process.stdout });
|
|
582
|
+
if (!ok) { console.log(' no timer was set'); return 1; }
|
|
583
|
+
}
|
|
584
|
+
approval = { engine: 'atris', hunter: engines.hunter, verifier: engines.verifier, max_credits: cap, quoted_credits: quote.credits, every, approved_at: new Date().toISOString() };
|
|
585
|
+
} else {
|
|
586
|
+
approval = { engine: 'local', every, approved_at: new Date().toISOString() };
|
|
587
|
+
}
|
|
588
|
+
cloud.writeApproval(target, approval, deps.homeDir);
|
|
589
|
+
const installed = installKeep(plan, keepDeps);
|
|
590
|
+
if (!installed.ok) { cloud.removeApproval(target, deps.homeDir); console.error(` could not install the timer: ${installed.error}`); return 1; }
|
|
591
|
+
console.log(` scheduled reviews are on for ${tildePath(target)}, once a ${every}`);
|
|
592
|
+
console.log(' each run uses the quick profile and saves its summary to your atris account');
|
|
593
|
+
console.log(' each run also checks your agent setup and dependencies for free');
|
|
594
|
+
if (approval.engine === 'local') console.log(' runs use only codex and claude on this machine. they never switch to atris models');
|
|
595
|
+
else console.log(` runs use atris models and stop at ${approval.max_credits} credits`);
|
|
596
|
+
console.log(` log: ${tildePath(plan.log_path)}`);
|
|
597
|
+
console.log(` stop with: atris sentinel schedule --uninstall ${tildePath(target)}`);
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function auditCommand(argv, deps = {}) {
|
|
469
602
|
if (argv[0] === 'help' || argv.includes('-h') || argv.includes('--help')) return printAuditHelp();
|
|
470
603
|
const audit = require('../lib/security-audit');
|
|
604
|
+
const root = deps.root || process.cwd();
|
|
471
605
|
if (argv[0] === 'status' || argv[0] === 'last') {
|
|
472
|
-
const last = audit.latestRun(
|
|
606
|
+
const last = audit.latestRun(root);
|
|
473
607
|
if (!last || !last.meta) { console.log('\n no sentinel audit has run for this repo yet\n'); return 0; }
|
|
474
608
|
if (argv.includes('--json')) { console.log(JSON.stringify({ dir: last.dir, meta: last.meta, findings: last.records }, null, 2)); return 0; }
|
|
475
609
|
console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
|
|
476
610
|
return 0;
|
|
477
611
|
}
|
|
612
|
+
if (['agents', 'guard', 'watch', 'flush'].includes(argv[0])) {
|
|
613
|
+
const powers = require('./sentinel-powers');
|
|
614
|
+
const run = { agents: powers.agentsCommand, guard: powers.guardCommand, watch: powers.watchCommand, flush: powers.flushCommand }[argv[0]];
|
|
615
|
+
return run(argv.slice(1), deps);
|
|
616
|
+
}
|
|
617
|
+
if (argv[0] === 'upload') return auditUploadCommand(argv.slice(1), deps);
|
|
618
|
+
if (argv[0] === 'schedule') return auditScheduleCommand(argv.slice(1), deps);
|
|
478
619
|
let opts;
|
|
479
620
|
try { opts = parseAuditArgs(argv); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
|
|
621
|
+
const cloud = deps.cloud || require('../lib/sentinel-cloud');
|
|
622
|
+
// A timer run does only what the owner approved at install.
|
|
623
|
+
let capOverride = null;
|
|
624
|
+
if (opts.scheduled) {
|
|
625
|
+
const approval = cloud.readApproval(root, deps.homeDir);
|
|
626
|
+
if (!approval) { console.error('security-review audit: this folder has no approved schedule. nothing ran. run atris sentinel schedule again'); return 2; }
|
|
627
|
+
// The free checks run first, so a review that stops at its credit limit
|
|
628
|
+
// still leaves today's agent and dependency results.
|
|
629
|
+
try {
|
|
630
|
+
await require('./sentinel-powers').runScheduledFreeChecks(root, { ...deps, upload: opts.upload, log: opts.json ? (m) => console.error(m) : (m) => console.log(m) });
|
|
631
|
+
} catch (e) { console.error(` warning: the free checks did not finish: ${e.message}`); }
|
|
632
|
+
opts.hunter = null;
|
|
633
|
+
opts.verifier = null;
|
|
634
|
+
opts.engine = approval.engine;
|
|
635
|
+
if (approval.engine === 'atris') { opts.yes = true; capOverride = Number(approval.max_credits); }
|
|
636
|
+
}
|
|
637
|
+
const token = deps.token !== undefined ? deps.token : cloud.authToken();
|
|
638
|
+
let engines;
|
|
639
|
+
try { engines = cloud.resolveEngines({ engine: opts.engine, hunter: opts.hunter, verifier: opts.verifier, token, env: deps.env || process.env }); } catch (e) { console.error(`security-review audit: ${e.message}`); return 2; }
|
|
640
|
+
const hunter = engines.hunter || undefined;
|
|
641
|
+
const verifier = engines.verifier || undefined;
|
|
642
|
+
const hosted = audit.isHostedEngine(hunter) || audit.isHostedEngine(verifier);
|
|
643
|
+
if (hosted && !token) { console.error('security-review audit: reviews on atris models need a login; run atris login first'); return 2; }
|
|
480
644
|
const log = opts.json ? () => {} : (msg) => console.log(` · ${msg}`);
|
|
645
|
+
const tell = opts.json ? (m) => console.error(m) : (m) => console.log(m);
|
|
646
|
+
if (hosted && engines.chosen === 'no_local_engine' && !opts.planOnly) tell(' no codex or claude found on this machine. this review runs on atris models');
|
|
647
|
+
let runJobs = deps.runJobs;
|
|
648
|
+
const spend = hosted ? cloud.spendGuard({ quoteCall: deps.quoteCall }) : null;
|
|
649
|
+
if (!runJobs && hosted) runJobs = cloud.atrisJobRunner({ token, spend });
|
|
650
|
+
const confirmSpend = async (estimate) => {
|
|
651
|
+
const quote = await cloud.quoteEstimate(estimate, { token });
|
|
652
|
+
spend.max = capOverride || quote.credits;
|
|
653
|
+
spend.limits = quote.limits;
|
|
654
|
+
tell(`\n ${cloud.describeQuote(quote)}\n`);
|
|
655
|
+
if (capOverride) tell(` this scheduled run may spend at most ${capOverride} credits, as approved`);
|
|
656
|
+
if (opts.yes) return true;
|
|
657
|
+
const input = deps.input || process.stdin;
|
|
658
|
+
if (!input.isTTY && !deps.input) { console.error(' nothing was spent. add --yes to run without this question'); return false; }
|
|
659
|
+
const yes = await (deps.ask || askYesNo)(' spend these credits? (y/N) ', { input, output: opts.json ? process.stderr : process.stdout });
|
|
660
|
+
if (!yes) tell(' nothing was spent');
|
|
661
|
+
return yes;
|
|
662
|
+
};
|
|
481
663
|
let result;
|
|
482
664
|
try {
|
|
483
665
|
result = await audit.runAudit({
|
|
484
|
-
root
|
|
485
|
-
hunter
|
|
666
|
+
root, profile: opts.profile, scope: opts.scope, budget: opts.budget ?? undefined, out: opts.out,
|
|
667
|
+
hunter, verifier, planOnly: opts.planOnly, runJobs, confirmSpend,
|
|
486
668
|
timeoutMs: opts.timeoutMs || undefined, log,
|
|
487
669
|
});
|
|
488
670
|
} catch (e) {
|
|
489
671
|
console.error(`security-review audit: ${e.message}`);
|
|
490
672
|
return 2;
|
|
491
673
|
}
|
|
674
|
+
if (result.declined) return 1;
|
|
492
675
|
const fail = audit.shouldFailAudit(result.records);
|
|
493
676
|
if (opts.json) console.log(JSON.stringify({ ok: !fail, dir: result.dir, meta: result.meta, findings: result.records }, null, 2));
|
|
494
677
|
else console.log(renderAuditLanding(result));
|
|
678
|
+
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`);
|
|
679
|
+
if (opts.upload && token && !opts.planOnly) {
|
|
680
|
+
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`); }
|
|
681
|
+
}
|
|
495
682
|
return fail ? 1 : 0;
|
|
496
683
|
}
|
|
497
684
|
|
|
@@ -510,6 +697,50 @@ function printAuditHelp() {
|
|
|
510
697
|
atris security-review audit --out DIR write the run here (outside the repo)
|
|
511
698
|
atris security-review audit status the last run's landing
|
|
512
699
|
|
|
700
|
+
on atris models (no other AI subscription needed, paid in atris credits):
|
|
701
|
+
|
|
702
|
+
atris sentinel --engine atris atris:fast searches, atris:pro checks
|
|
703
|
+
atris sentinel --engine atris --yes skip the price question
|
|
704
|
+
atris sentinel --engine local only codex and claude on this machine
|
|
705
|
+
|
|
706
|
+
with no --engine, local engines run when codex or claude is installed.
|
|
707
|
+
atris models run only when neither is installed and you are logged in.
|
|
708
|
+
every run on atris models shows the most it can cost first and stops
|
|
709
|
+
before it could pass that. lines that look like secrets are hidden and
|
|
710
|
+
only tracked files inside the review are ever sent.
|
|
711
|
+
|
|
712
|
+
saving results to your atris account (on when you are logged in):
|
|
713
|
+
|
|
714
|
+
atris sentinel --no-upload keep this run on this machine only
|
|
715
|
+
atris sentinel upload [run-folder] save a past run (default: the last one)
|
|
716
|
+
|
|
717
|
+
only the summary is saved: plain sentences, file:line locations and counts.
|
|
718
|
+
source code and code lines never leave in an upload.
|
|
719
|
+
|
|
720
|
+
free checks, no model and no credits:
|
|
721
|
+
|
|
722
|
+
atris sentinel agents [path] check agent instructions, settings, hooks, mcp and skills
|
|
723
|
+
atris sentinel agents --home also check ~/.claude and ~/.codex
|
|
724
|
+
atris sentinel watch [path] known attacks in your dependencies (osv database)
|
|
725
|
+
atris sentinel watch --fix update affected packages on a new branch, never pushed
|
|
726
|
+
atris sentinel guard install [--global] stop risky agent commands before they run
|
|
727
|
+
atris sentinel guard test see which sample commands the guard stops
|
|
728
|
+
|
|
729
|
+
results go to your atris dashboard when you are logged in. add --no-upload
|
|
730
|
+
to keep them on this machine. secret values and file contents never leave.
|
|
731
|
+
|
|
732
|
+
reviews on a timer:
|
|
733
|
+
|
|
734
|
+
atris sentinel schedule --every day [path] quick review once a day, saved to your account
|
|
735
|
+
atris sentinel schedule --every week [path] once a week
|
|
736
|
+
atris sentinel schedule --engine atris --max-credits 20
|
|
737
|
+
timed runs on atris models, at most 20 credits each
|
|
738
|
+
atris sentinel schedule --uninstall [path] stop the timer
|
|
739
|
+
|
|
740
|
+
every timed run also runs the free agents and watch checks.
|
|
741
|
+
the engine is picked when the timer goes on. on atris models you see the
|
|
742
|
+
price and say yes then. a timed run never switches to atris models later.
|
|
743
|
+
|
|
513
744
|
How it works: every source file x attack class with a reason to be read
|
|
514
745
|
becomes an area in a coverage ledger. Read-only engine hunters each own a
|
|
515
746
|
few areas. A coverage critic names gaps. Every candidate goes to a different
|
|
@@ -553,4 +784,4 @@ function printHelp() {
|
|
|
553
784
|
return 0;
|
|
554
785
|
}
|
|
555
786
|
|
|
556
|
-
module.exports = { securityReviewCommand };
|
|
787
|
+
module.exports = { securityReviewCommand, auditCommand };
|