atris 3.82.3 → 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 +24 -0
- 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/sentinel-agents.js +584 -0
- package/lib/sentinel-cloud.js +3 -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);
|
|
@@ -590,6 +590,7 @@ async function auditScheduleCommand(argv, deps) {
|
|
|
590
590
|
if (!installed.ok) { cloud.removeApproval(target, deps.homeDir); console.error(` could not install the timer: ${installed.error}`); return 1; }
|
|
591
591
|
console.log(` scheduled reviews are on for ${tildePath(target)}, once a ${every}`);
|
|
592
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');
|
|
593
594
|
if (approval.engine === 'local') console.log(' runs use only codex and claude on this machine. they never switch to atris models');
|
|
594
595
|
else console.log(` runs use atris models and stop at ${approval.max_credits} credits`);
|
|
595
596
|
console.log(` log: ${tildePath(plan.log_path)}`);
|
|
@@ -608,6 +609,11 @@ async function auditCommand(argv, deps = {}) {
|
|
|
608
609
|
console.log(renderAuditLanding({ dir: last.dir, meta: last.meta, records: last.records }));
|
|
609
610
|
return 0;
|
|
610
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
|
+
}
|
|
611
617
|
if (argv[0] === 'upload') return auditUploadCommand(argv.slice(1), deps);
|
|
612
618
|
if (argv[0] === 'schedule') return auditScheduleCommand(argv.slice(1), deps);
|
|
613
619
|
let opts;
|
|
@@ -618,6 +624,11 @@ async function auditCommand(argv, deps = {}) {
|
|
|
618
624
|
if (opts.scheduled) {
|
|
619
625
|
const approval = cloud.readApproval(root, deps.homeDir);
|
|
620
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}`); }
|
|
621
632
|
opts.hunter = null;
|
|
622
633
|
opts.verifier = null;
|
|
623
634
|
opts.engine = approval.engine;
|
|
@@ -706,6 +717,18 @@ function printAuditHelp() {
|
|
|
706
717
|
only the summary is saved: plain sentences, file:line locations and counts.
|
|
707
718
|
source code and code lines never leave in an upload.
|
|
708
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
|
+
|
|
709
732
|
reviews on a timer:
|
|
710
733
|
|
|
711
734
|
atris sentinel schedule --every day [path] quick review once a day, saved to your account
|
|
@@ -714,6 +737,7 @@ function printAuditHelp() {
|
|
|
714
737
|
timed runs on atris models, at most 20 credits each
|
|
715
738
|
atris sentinel schedule --uninstall [path] stop the timer
|
|
716
739
|
|
|
740
|
+
every timed run also runs the free agents and watch checks.
|
|
717
741
|
the engine is picked when the timer goes on. on atris models you see the
|
|
718
742
|
price and say yes then. a timed run never switches to atris models later.
|
|
719
743
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The free sentinel commands: agents, guard, watch and flush. Each reports
|
|
4
|
+
// to the dashboard through POST /api/sentinel/events (lib/sentinel-events.js).
|
|
5
|
+
//
|
|
6
|
+
// atris sentinel agents [path] [--home] check an agent setup
|
|
7
|
+
// atris sentinel guard install|uninstall|status|test [--global]
|
|
8
|
+
// atris sentinel watch [path] [--fix] known attacks in dependencies
|
|
9
|
+
// atris sentinel flush send queued guard events
|
|
10
|
+
//
|
|
11
|
+
// An upload problem is a warning line. It never changes the exit code.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
function tildePath(p, home = require('os').homedir()) {
|
|
17
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseFlags(argv, known) {
|
|
21
|
+
const opts = { paths: [] };
|
|
22
|
+
for (const a of argv) {
|
|
23
|
+
if (a.startsWith('--')) {
|
|
24
|
+
const name = a.slice(2);
|
|
25
|
+
if (!known.includes(name)) throw new Error(`unknown flag: ${a}`);
|
|
26
|
+
opts[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = true;
|
|
27
|
+
} else if (a.startsWith('-') && a !== '-') {
|
|
28
|
+
throw new Error(`unknown flag: ${a}`);
|
|
29
|
+
} else opts.paths.push(a);
|
|
30
|
+
}
|
|
31
|
+
return opts;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveRoot(opts, deps) {
|
|
35
|
+
const root = path.resolve(opts.paths[0] || deps.root || process.cwd());
|
|
36
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) throw new Error(`${root} is not a folder`);
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tokenFor(deps) {
|
|
41
|
+
if (deps.token !== undefined) return deps.token;
|
|
42
|
+
return require('../lib/sentinel-cloud').authToken();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function uploadLine(res, what) {
|
|
46
|
+
if (res.skipped === 'no-upload') return ' kept on this machine. nothing was sent (--no-upload)';
|
|
47
|
+
if (res.skipped === 'not-logged-in') return ' not logged in. nothing was sent. run atris login to see this on your dashboard';
|
|
48
|
+
if (!res.ok) return ` warning: could not send ${what} to your atris dashboard: ${res.reason}. the check itself is done`;
|
|
49
|
+
const resolved = res.resolved ? ` and marked ${res.resolved} as fixed` : '';
|
|
50
|
+
return ` sent ${res.sent - (res.resolved || 0)} ${what} to your atris dashboard${resolved}. no file contents or secret values were sent`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------- agents
|
|
54
|
+
|
|
55
|
+
async function runAgents(root, { home = false, upload = true, deps = {} } = {}) {
|
|
56
|
+
const agents = require('../lib/sentinel-agents');
|
|
57
|
+
const events = require('../lib/sentinel-events');
|
|
58
|
+
const homeDir = deps.homeDir || process.env.HOME || require('os').homedir();
|
|
59
|
+
const result = agents.runAgentCheck(root, { home, homeDir });
|
|
60
|
+
const evs = agents.toEvents(result, { repo: deps.repo });
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await events.reportFindings(root, 'agent_check', evs, { token: upload ? tokenFor(deps) : '', base: deps.base, env: deps.env || process.env, upload });
|
|
64
|
+
} catch (e) { res = { ok: false, reason: e.message }; }
|
|
65
|
+
return { result, events: evs, upload: res };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function agentsCommand(argv, deps = {}) {
|
|
69
|
+
let opts;
|
|
70
|
+
try { opts = parseFlags(argv, ['home', 'json', 'no-upload']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
71
|
+
let root;
|
|
72
|
+
try { root = resolveRoot(opts, deps); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
73
|
+
const { result, upload } = await runAgents(root, { home: Boolean(opts.home), upload: !opts.noUpload, deps });
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify({ ok: result.grade !== 'fail', grade: result.grade, areas: result.areas, files: result.files, findings: result.findings, upload }, null, 2));
|
|
76
|
+
} else {
|
|
77
|
+
console.log(require('../lib/sentinel-agents').renderReport(result));
|
|
78
|
+
console.log(uploadLine(upload, 'the findings'));
|
|
79
|
+
console.log('');
|
|
80
|
+
}
|
|
81
|
+
return result.grade === 'fail' ? 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------- watch
|
|
85
|
+
|
|
86
|
+
async function runWatchCheck(root, { upload = true, deps = {} } = {}) {
|
|
87
|
+
const watch = require('../lib/sentinel-watch');
|
|
88
|
+
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 } : {}) });
|
|
90
|
+
const evs = watch.toEvents(result, { repo: deps.repo });
|
|
91
|
+
let res;
|
|
92
|
+
if (result.offline) res = { ok: true, skipped: 'offline', sent: 0 };
|
|
93
|
+
else {
|
|
94
|
+
try {
|
|
95
|
+
res = await events.reportFindings(root, 'threat', evs, { token: upload ? tokenFor(deps) : '', base: deps.base, env: deps.env || process.env, upload });
|
|
96
|
+
} catch (e) { res = { ok: false, reason: e.message }; }
|
|
97
|
+
}
|
|
98
|
+
return { result, events: evs, upload: res };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function watchCommand(argv, deps = {}) {
|
|
102
|
+
let opts;
|
|
103
|
+
try { opts = parseFlags(argv, ['fix', 'json', 'no-upload']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
104
|
+
let root;
|
|
105
|
+
try { root = resolveRoot(opts, deps); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
106
|
+
const watch = require('../lib/sentinel-watch');
|
|
107
|
+
const { result, upload } = await runWatchCheck(root, { upload: !opts.noUpload, deps });
|
|
108
|
+
let fix = null;
|
|
109
|
+
if (opts.fix && !result.offline && result.affected.length) {
|
|
110
|
+
try { fix = watch.applyFix(root, result, { log: (m) => console.error(` ${m}`) }); } catch (e) { fix = { ok: false, reason: String(e.message || e).split('\n')[0] }; }
|
|
111
|
+
}
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
console.log(JSON.stringify({ ok: !result.affected.some((a) => a.severity === 'critical' || a.severity === 'high'), ...result, upload, fix }, null, 2));
|
|
114
|
+
} else {
|
|
115
|
+
console.log(watch.renderReport(result));
|
|
116
|
+
if (upload.skipped !== 'offline') console.log(uploadLine(upload, 'the threats'));
|
|
117
|
+
if (fix) {
|
|
118
|
+
console.log('');
|
|
119
|
+
if (!fix.ok) console.log(` the fix did not run: ${fix.reason}`);
|
|
120
|
+
else if (!fix.branch) console.log(` the fix changed nothing${fix.message ? `: ${fix.message}` : ''}`);
|
|
121
|
+
else {
|
|
122
|
+
console.log(` updated ${fix.changed} file${fix.changed === 1 ? '' : 's'} on a new branch: ${fix.branch}`);
|
|
123
|
+
console.log(` review it with: git diff ${fix.from || 'main'}...${fix.branch}`);
|
|
124
|
+
console.log(` go back with: git checkout ${fix.from || 'main'}`);
|
|
125
|
+
console.log(' nothing was pushed');
|
|
126
|
+
}
|
|
127
|
+
if (fix.manual && fix.manual.length) {
|
|
128
|
+
console.log(` update these yourself: ${fix.manual.map((a) => `${a.name} to ${a.fixed}`).join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
console.log('');
|
|
132
|
+
}
|
|
133
|
+
if (fix && !fix.ok) return 1;
|
|
134
|
+
return result.affected.some((a) => a.severity === 'critical' || a.severity === 'high') ? 1 : 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------- guard
|
|
138
|
+
|
|
139
|
+
function guardHelp() {
|
|
140
|
+
console.log(`
|
|
141
|
+
atris sentinel guard, an action guard for coding agents
|
|
142
|
+
|
|
143
|
+
atris sentinel guard install guard claude code in this project
|
|
144
|
+
atris sentinel guard install --global guard every claude code session on this computer
|
|
145
|
+
atris sentinel guard status [--global] is it on and what it stopped today
|
|
146
|
+
atris sentinel guard test run sample commands through the rules
|
|
147
|
+
atris sentinel guard uninstall [--global]
|
|
148
|
+
|
|
149
|
+
it stops shell commands that send secrets out, delete home or root, force
|
|
150
|
+
push to main, drop databases or turn off security. normal curl, git push
|
|
151
|
+
and rm in project folders still run. it uses fixed rules and no model.
|
|
152
|
+
trusted sites for secrets can be added in ~/.atris/sentinel/guard.json.
|
|
153
|
+
|
|
154
|
+
what it promises: it stops the common ways an agent leaks a key or runs a
|
|
155
|
+
destructive command. a network call that carries a secret runs only through
|
|
156
|
+
curl, wget or httpie with options on a short safe list, to trusted sites. a
|
|
157
|
+
command that names claude settings or the guard's files runs only when it
|
|
158
|
+
is a read-only program like cat, grep or git log. what it cannot promise: a program that hides its
|
|
159
|
+
intent can still get past fixed rules. it does not read encoded payloads
|
|
160
|
+
or scripts written to disk and then run. it does not read interpreters
|
|
161
|
+
like python -c either. it is a seatbelt, not a sandbox.
|
|
162
|
+
`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function guardCommand(argv, deps = {}) {
|
|
167
|
+
const sub = argv[0] || 'status';
|
|
168
|
+
const rest = argv.slice(1);
|
|
169
|
+
if (sub === 'help' || argv.includes('--help') || argv.includes('-h')) return guardHelp();
|
|
170
|
+
let opts;
|
|
171
|
+
try { opts = parseFlags(rest, ['global', 'json']); } catch (e) { console.error(` ${e.message}`); return 2; }
|
|
172
|
+
const inst = require('../lib/sentinel-guard-install');
|
|
173
|
+
const env = deps.env || process.env;
|
|
174
|
+
const root = deps.root || process.cwd();
|
|
175
|
+
const where = opts.global ? 'every claude code session on this computer' : `claude code in ${tildePath(path.resolve(root), env.HOME || undefined)}`;
|
|
176
|
+
if (sub === 'install') {
|
|
177
|
+
let st;
|
|
178
|
+
try { st = inst.install({ global: Boolean(opts.global), root, env, atrisBin: deps.atrisBin }); } catch (e) { console.error(` could not install the guard: ${e.message}. your settings were not changed`); return 1; }
|
|
179
|
+
console.log(`\n the sentinel guard is on for ${where}`);
|
|
180
|
+
console.log(` settings: ${tildePath(st.settings, env.HOME || undefined)}`);
|
|
181
|
+
console.log(' it checks each shell command and file edit before it runs. try it with atris sentinel guard test');
|
|
182
|
+
if (st.hooks_off) console.log(' warning: these settings turn all hooks off. the guard will not run until that is removed');
|
|
183
|
+
console.log(' it starts with the next claude code session\n');
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
if (sub === 'uninstall') {
|
|
187
|
+
try { inst.uninstall({ global: Boolean(opts.global), root, env }); } catch (e) { console.error(` could not remove the guard: ${e.message}. your settings were not changed`); return 1; }
|
|
188
|
+
console.log(` the sentinel guard is off for ${where}`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
if (sub === 'status') {
|
|
192
|
+
const st = inst.status({ global: Boolean(opts.global), root, env });
|
|
193
|
+
const blocks = inst.recentBlocks({ env });
|
|
194
|
+
const events = require('../lib/sentinel-events');
|
|
195
|
+
const waiting = events.queueSize(env);
|
|
196
|
+
if (opts.json) { console.log(JSON.stringify({ ...st, blocked_today: blocks.length, waiting_to_send: waiting }, null, 2)); return 0; }
|
|
197
|
+
console.log('');
|
|
198
|
+
if (st.installed) console.log(` the sentinel guard is on for ${where}`);
|
|
199
|
+
else if (!st.readable) console.log(` could not read ${tildePath(st.settings, env.HOME || undefined)}. the guard state is unknown`);
|
|
200
|
+
else console.log(` the sentinel guard is off for ${where}. turn it on with atris sentinel guard install${opts.global ? ' --global' : ''}`);
|
|
201
|
+
if (st.installed && !st.current) console.log(' the installed copy is older than this atris. run install again to update it');
|
|
202
|
+
if (st.hooks_off) console.log(' warning: these settings turn all hooks off. the guard does not run');
|
|
203
|
+
console.log(` stopped ${blocks.length} action${blocks.length === 1 ? '' : 's'} in the last day${blocks.length ? `. latest: ${blocks[blocks.length - 1].reason.replace(/^blocked: /, '')}` : ''}`);
|
|
204
|
+
if (waiting) console.log(` ${waiting} event${waiting === 1 ? '' : 's'} waiting to be sent to your dashboard`);
|
|
205
|
+
console.log('');
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (sub === 'test') {
|
|
209
|
+
const guard = require('../lib/sentinel-guard');
|
|
210
|
+
const rows = guard.runSamples({ env });
|
|
211
|
+
if (opts.json) { console.log(JSON.stringify(rows, null, 2)); return rows.every((r) => r.ok) ? 0 : 1; }
|
|
212
|
+
console.log('\n sample commands through the sentinel guard rules\n');
|
|
213
|
+
for (const r of rows) {
|
|
214
|
+
const mark = r.ok ? ' ' : '!';
|
|
215
|
+
console.log(` ${mark} ${r.got === 'block' ? 'blocked' : 'allowed'} ${r.command}`);
|
|
216
|
+
if (r.got === 'block') console.log(` ${r.reason.replace(/^blocked: /, '')}`);
|
|
217
|
+
}
|
|
218
|
+
const bad = rows.filter((r) => !r.ok);
|
|
219
|
+
const blocked = rows.filter((r) => r.got === 'block').length;
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(` ${blocked} of ${rows.length} samples blocked, ${rows.length - blocked} allowed`);
|
|
222
|
+
console.log(bad.length ? ` ${bad.length} sample${bad.length === 1 ? '' : 's'} did not do what was expected (marked !)` : ' every sample did what was expected');
|
|
223
|
+
console.log('');
|
|
224
|
+
return bad.length ? 1 : 0;
|
|
225
|
+
}
|
|
226
|
+
console.error(` unknown guard command: ${sub}. use install, uninstall, status or test`);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------- flush
|
|
231
|
+
|
|
232
|
+
async function flushCommand(argv, deps = {}) {
|
|
233
|
+
const quiet = argv.includes('--quiet');
|
|
234
|
+
const events = require('../lib/sentinel-events');
|
|
235
|
+
const token = tokenFor(deps);
|
|
236
|
+
let res;
|
|
237
|
+
try { res = await events.flushQueue({ token, base: deps.base, env: deps.env || process.env }); } catch (e) { res = { ok: false, reason: e.message, sent: 0 }; }
|
|
238
|
+
if (!quiet) {
|
|
239
|
+
if (res.ok) console.log(` sent ${res.sent} queued event${res.sent === 1 ? '' : 's'} to your atris dashboard`);
|
|
240
|
+
else console.log(` warning: queued events were not sent: ${res.reason}. they stay queued for next time`);
|
|
241
|
+
}
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------- scheduled
|
|
246
|
+
|
|
247
|
+
// What a scheduled run does before the paid review: the two free checks,
|
|
248
|
+
// wrapped in scan_started and scan_finished events. Never throws.
|
|
249
|
+
async function runScheduledFreeChecks(root, deps = {}) {
|
|
250
|
+
const events = require('../lib/sentinel-events');
|
|
251
|
+
const upload = deps.upload !== false;
|
|
252
|
+
const token = upload ? tokenFor(deps) : '';
|
|
253
|
+
const repo = events.repoName(root);
|
|
254
|
+
const started = new Date();
|
|
255
|
+
const say = deps.log || ((m) => console.log(m));
|
|
256
|
+
const out = { agents: null, watch: null, sent: [] };
|
|
257
|
+
const send = async (list) => {
|
|
258
|
+
if (!upload) return { ok: true, skipped: 'no-upload' };
|
|
259
|
+
if (!token) return { ok: false, reason: 'not logged in' };
|
|
260
|
+
try { return await events.postEvents(list, { token, base: deps.base }); } catch (e) { return { ok: false, reason: e.message }; }
|
|
261
|
+
};
|
|
262
|
+
const startEv = events.makeEvent({ kind: 'scan_started', repo, severity: 'info', title: 'Sentinel started its scheduled check of this project.', status: 'resolved', fingerprint: events.fingerprint('scan_started', repo, started.toISOString()), occurredAt: started });
|
|
263
|
+
out.sent.push(await send([startEv]));
|
|
264
|
+
try {
|
|
265
|
+
out.agents = await runAgents(root, { home: false, upload, deps: { ...deps, token } });
|
|
266
|
+
say(` agent setup check: ${out.agents.result.grade}, ${out.agents.result.findings.length} finding${out.agents.result.findings.length === 1 ? '' : 's'}`);
|
|
267
|
+
} catch (e) { say(` warning: the agent setup check did not finish: ${e.message}`); }
|
|
268
|
+
try {
|
|
269
|
+
out.watch = await runWatchCheck(root, { upload, deps: { ...deps, token } });
|
|
270
|
+
if (out.watch.result.offline) say(' warning: the dependency check could not reach the osv database this time');
|
|
271
|
+
else say(` dependency check: ${out.watch.result.affected.length} affected package${out.watch.result.affected.length === 1 ? '' : 's'}`);
|
|
272
|
+
} catch (e) { say(` warning: the dependency check did not finish: ${e.message}`); }
|
|
273
|
+
const agentsOpen = out.agents ? out.agents.result.findings.length : 0;
|
|
274
|
+
const threats = out.watch && !out.watch.result.offline ? out.watch.result.affected.length : 0;
|
|
275
|
+
const bad = (out.agents && out.agents.result.grade === 'fail') || (out.watch && out.watch.result.affected.some((a) => a.severity === 'critical' || a.severity === 'high'));
|
|
276
|
+
const title = events.fillPlain('Sentinel finished its scheduled check. {a} agent setup findings and {t} dependency threats are open.', { a: ['count', String(agentsOpen), 'some'], t: ['count', String(threats), 'some'] });
|
|
277
|
+
const finished = new Date();
|
|
278
|
+
const endEv = events.makeEvent({ kind: 'scan_finished', repo, severity: bad ? 'high' : 'info', title, status: 'resolved', fingerprint: events.fingerprint('scan_finished', repo, started.toISOString()), occurredAt: finished });
|
|
279
|
+
out.sent.push(await send([endEv]));
|
|
280
|
+
if (upload && token) { try { await events.flushQueue({ token, base: deps.base, env: deps.env || process.env }); } catch {} }
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { agentsCommand, guardCommand, watchCommand, flushCommand, runScheduledFreeChecks, runAgents, runWatchCheck };
|
package/commands/task.js
CHANGED
|
@@ -5963,6 +5963,9 @@ function isUninitializedTaskFolder(cwd = process.cwd()) {
|
|
|
5963
5963
|
|
|
5964
5964
|
function cmdNext(args) {
|
|
5965
5965
|
const root = process.cwd();
|
|
5966
|
+
const { buildAgentNext, printAgentNext } = require('../lib/agent-next');
|
|
5967
|
+
const agent = buildAgentNext({ root, args });
|
|
5968
|
+
if (agent) return printAgentNext(agent, { asJson: wantsJson(args), alias: 'command' });
|
|
5966
5969
|
// Empty folder talks like bare atris. Walk up like the task db so a
|
|
5967
5970
|
// project subdir stays in the room; stop before inheriting /tmp.
|
|
5968
5971
|
// A leftover projection with live tasks still names that work.
|
|
@@ -12244,7 +12247,7 @@ async function run(args) {
|
|
|
12244
12247
|
// `task next --help` is a help request, not a run. Next can open
|
|
12245
12248
|
// the task db or print the desk, so never list or claim just to show usage.
|
|
12246
12249
|
if (sub === 'next' && argsWantHelp(raw.slice(1))) {
|
|
12247
|
-
console.log('Usage: atris task next [--tag <tag>] [--create-next]');
|
|
12250
|
+
console.log('Usage: atris task next [--tag <tag>] [--agent <member>|--human] [--create-next]');
|
|
12248
12251
|
return;
|
|
12249
12252
|
}
|
|
12250
12253
|
// `task show --help` is a help request, not a run. Show can dump
|
|
@@ -12254,13 +12257,17 @@ async function run(args) {
|
|
|
12254
12257
|
return;
|
|
12255
12258
|
}
|
|
12256
12259
|
const result = await runTaskCommand(raw);
|
|
12257
|
-
const
|
|
12260
|
+
const readOnlyAgentNext = sub === 'next' && require('../lib/agent-next').agentCaller(raw.slice(1));
|
|
12261
|
+
const skipsRender = hasFlag(raw, '--dry-run') || readOnlyAgentNext;
|
|
12258
12262
|
if (MUTATING_TASK_COMMANDS.has(sub) && !skipsRender) autoRenderTodoFromDb();
|
|
12259
12263
|
return result;
|
|
12260
12264
|
}
|
|
12261
12265
|
|
|
12262
12266
|
module.exports = {
|
|
12263
12267
|
run,
|
|
12268
|
+
taskQueueScopeFromArgs,
|
|
12269
|
+
normalizeTaskQueueScope,
|
|
12270
|
+
filterTasksByScope,
|
|
12264
12271
|
chooseTaskOwner,
|
|
12265
12272
|
readTeamMembers,
|
|
12266
12273
|
createTaskApiServer,
|