mythos-sentinel 0.1.0
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/LICENSE +21 -0
- package/README.md +362 -0
- package/action.yml +43 -0
- package/assets/banner.png +0 -0
- package/bin/mythos-sentinel-mcp.js +7 -0
- package/bin/mythos-sentinel.js +8 -0
- package/docs/ARCHITECTURE.md +55 -0
- package/docs/BASE_X402.md +33 -0
- package/docs/BAZAAR_ADAPTER.md +41 -0
- package/docs/DASHBOARD.md +22 -0
- package/docs/FALLBACK_ROUTING.md +37 -0
- package/docs/MCP.md +70 -0
- package/docs/PASSIVE_SCORING.md +33 -0
- package/docs/ROUTESCORE.md +101 -0
- package/docs/RUNTIME_MCP_PROXY.md +90 -0
- package/docs/SPEND_FIREWALL.md +50 -0
- package/docs/TELEMETRY.md +74 -0
- package/docs/THREAT_MODEL.md +28 -0
- package/docs/X402_RECEIPTS.md +54 -0
- package/examples/base/mythos.policy.json +142 -0
- package/examples/claude_desktop/mcp.json +8 -0
- package/examples/codex/AGENTS.md +31 -0
- package/examples/cursor/mcp.json +8 -0
- package/examples/github/verify.yml +29 -0
- package/examples/routescore/services.yml +19 -0
- package/examples/skill/mythos.skill.json +20 -0
- package/package.json +79 -0
- package/schemas/agent-receipt.schema.json +17 -0
- package/schemas/policy.schema.json +322 -0
- package/schemas/sentinel-report.schema.json +14 -0
- package/schemas/skill.manifest.schema.json +42 -0
- package/src/cli.js +570 -0
- package/src/core/fs.js +88 -0
- package/src/core/path-utils.js +54 -0
- package/src/core/policy.js +326 -0
- package/src/core/receipt.js +52 -0
- package/src/core/routescore.js +576 -0
- package/src/core/snapshot.js +35 -0
- package/src/core/telemetry.js +214 -0
- package/src/core/x402-receipts.js +303 -0
- package/src/index.js +19 -0
- package/src/mcp/proxy.js +493 -0
- package/src/mcp/server.js +226 -0
- package/src/report/format.js +53 -0
- package/src/report/sarif.js +50 -0
- package/src/scanner/rules.js +185 -0
- package/src/scanner/scan.js +118 -0
- package/src/ui/server.js +346 -0
- package/src/ui/static/app.js +210 -0
- package/src/ui/static/index.html +342 -0
- package/src/ui/static/styles.css +904 -0
- package/src/version.js +2 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { VERSION, PRODUCT } from './version.js';
|
|
4
|
+
import { loadPolicy, defaultPolicy, checkPayment, checkCommand, checkFilesystemAccess, checkNetwork } from './core/policy.js';
|
|
5
|
+
import { exists, writeJson, readJson, ensureDir } from './core/fs.js';
|
|
6
|
+
import { scanPath } from './scanner/scan.js';
|
|
7
|
+
import { formatScanReport, formatPaymentDecision } from './report/format.js';
|
|
8
|
+
import { toSarif } from './report/sarif.js';
|
|
9
|
+
import { createSnapshot } from './core/snapshot.js';
|
|
10
|
+
import { createReceipt, writeReceipt, verifyReceipt } from './core/receipt.js';
|
|
11
|
+
import { runMcpServer } from './mcp/server.js';
|
|
12
|
+
import { runMcpProxy } from './mcp/proxy.js';
|
|
13
|
+
import { startDashboard } from './ui/server.js';
|
|
14
|
+
import { executeFallbackRoute, fetchBazaarResources, fetchBazaarSearch, importServicesFile, listServiceCategories, loadRouteScoreServices, recommendService, routeService, saveCustomServices, seedX402Services, serviceForDomain, scoreService } from './core/routescore.js';
|
|
15
|
+
import { appendTelemetryEvent, readTelemetryEvents, setTelemetryEnabled, telemetryEnabled, telemetryPrivacy, telemetrySummary } from './core/telemetry.js';
|
|
16
|
+
import { ingestX402ReceiptFile, readX402Receipts, summarizeX402Receipts } from './core/x402-receipts.js';
|
|
17
|
+
|
|
18
|
+
export async function runCli(argv) {
|
|
19
|
+
const command = argv[0] || 'help';
|
|
20
|
+
const args = parseArgs(argv.slice(1));
|
|
21
|
+
|
|
22
|
+
switch (command) {
|
|
23
|
+
case 'help':
|
|
24
|
+
case '--help':
|
|
25
|
+
case '-h':
|
|
26
|
+
printHelp();
|
|
27
|
+
return;
|
|
28
|
+
case 'version':
|
|
29
|
+
case '--version':
|
|
30
|
+
case '-v':
|
|
31
|
+
console.log(`${PRODUCT} ${VERSION}`);
|
|
32
|
+
return;
|
|
33
|
+
case 'init':
|
|
34
|
+
return initCommand(args);
|
|
35
|
+
case 'scan':
|
|
36
|
+
return scanCommand(args);
|
|
37
|
+
case 'check-payment':
|
|
38
|
+
return paymentCommand(args);
|
|
39
|
+
case 'routescore':
|
|
40
|
+
return routeScoreCommand(args);
|
|
41
|
+
case 'telemetry':
|
|
42
|
+
return telemetryCommand(args);
|
|
43
|
+
case 'x402-receipt':
|
|
44
|
+
case 'x402-receipts':
|
|
45
|
+
case 'x402':
|
|
46
|
+
return x402ReceiptCommand(args);
|
|
47
|
+
case 'check-command':
|
|
48
|
+
return commandGuardCommand(args);
|
|
49
|
+
case 'check-file':
|
|
50
|
+
return fileGuardCommand(args);
|
|
51
|
+
case 'check-network':
|
|
52
|
+
return networkGuardCommand(args);
|
|
53
|
+
case 'snapshot':
|
|
54
|
+
return snapshotCommand(args);
|
|
55
|
+
case 'receipt':
|
|
56
|
+
return receiptCommand(args);
|
|
57
|
+
case 'verify':
|
|
58
|
+
return verifyCommand(args);
|
|
59
|
+
case 'mcp':
|
|
60
|
+
return runMcpServer();
|
|
61
|
+
case 'proxy':
|
|
62
|
+
case 'mcp-proxy':
|
|
63
|
+
return runMcpProxy({ policyPath: args.policy || 'mythos.policy.json', configPath: args.config });
|
|
64
|
+
case 'ui':
|
|
65
|
+
case 'dashboard':
|
|
66
|
+
return uiCommand(args);
|
|
67
|
+
case 'doctor':
|
|
68
|
+
return doctorCommand();
|
|
69
|
+
case 'policy':
|
|
70
|
+
return policyCommand(args);
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Unknown command: ${command}. Run mythos-sentinel help.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function initCommand(args) {
|
|
77
|
+
const policyPath = args.policy || 'mythos.policy.json';
|
|
78
|
+
const force = Boolean(args.force);
|
|
79
|
+
const base = Boolean(args.base);
|
|
80
|
+
const policy = structuredClone(defaultPolicy);
|
|
81
|
+
policy.project = path.basename(process.cwd());
|
|
82
|
+
if (base) {
|
|
83
|
+
policy.payments.x402.trustedDomains = ['api.coinbase.com', 'api.developer.coinbase.com', 'api.exa.ai', 'www.x402.org', 'x402.org'];
|
|
84
|
+
policy.payments.x402.maxPerRequestUSDC = 0.25;
|
|
85
|
+
policy.payments.x402.maxDailyUSDC = 5;
|
|
86
|
+
policy.network.allowedDomains.push('mainnet.base.org', 'base.org');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if ((await exists(policyPath)) && !force) throw new Error(`${policyPath} already exists. Use --force to overwrite.`);
|
|
90
|
+
await writeJson(policyPath, policy);
|
|
91
|
+
await ensureDir('.mythos/reports');
|
|
92
|
+
await ensureDir('.mythos/snapshots');
|
|
93
|
+
await fs.writeFile('.mythos/README.md', mythosReadme(), 'utf8');
|
|
94
|
+
console.log(`Created ${policyPath}`);
|
|
95
|
+
console.log('Created .mythos/reports and .mythos/snapshots');
|
|
96
|
+
console.log(base ? 'Base/x402 guard enabled.' : 'Run with --base to preconfigure x402/Base policy.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function scanCommand(args) {
|
|
100
|
+
const target = args._[0] || '.';
|
|
101
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
102
|
+
const report = await scanPath(target, { policy, failOn: args['fail-on'] });
|
|
103
|
+
const out = args.out;
|
|
104
|
+
if (args.sarif) {
|
|
105
|
+
const sarif = toSarif(report);
|
|
106
|
+
if (out) await writeJson(out, sarif);
|
|
107
|
+
else console.log(JSON.stringify(sarif, null, 2));
|
|
108
|
+
} else if (args.json) {
|
|
109
|
+
if (out) await writeJson(out, report);
|
|
110
|
+
else console.log(JSON.stringify(report, null, 2));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(formatScanReport(report));
|
|
113
|
+
if (out) await writeJson(out, report);
|
|
114
|
+
}
|
|
115
|
+
if (!report.summary.ok) process.exitCode = 2;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function paymentCommand(args) {
|
|
119
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
120
|
+
const decision = checkPayment({
|
|
121
|
+
domain: required(args.domain, '--domain'),
|
|
122
|
+
amountUSDC: required(args.amount, '--amount'),
|
|
123
|
+
dailySpentUSDC: args['daily-spent'] || 0,
|
|
124
|
+
unknownDailySpentUSDC: args['unknown-daily-spent'] || 0,
|
|
125
|
+
routeScore: args['route-score'],
|
|
126
|
+
category: args.category,
|
|
127
|
+
knownService: Boolean(args['known-service']) || Boolean(serviceForDomain(args.domain, seedX402Services))
|
|
128
|
+
}, policy);
|
|
129
|
+
if (args.json) console.log(JSON.stringify(decision, null, 2));
|
|
130
|
+
else console.log(formatPaymentDecision(decision));
|
|
131
|
+
if (!decision.ok) process.exitCode = 3;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async function routeScoreCommand(args) {
|
|
136
|
+
const sub = args._[0] || 'list';
|
|
137
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
138
|
+
const services = await loadRouteScoreServices({ rootDir: process.cwd(), filePath: args.catalog });
|
|
139
|
+
const summary = await telemetrySummary({ rootDir: process.cwd(), policy, services });
|
|
140
|
+
|
|
141
|
+
if (sub === 'categories') {
|
|
142
|
+
const categories = listServiceCategories();
|
|
143
|
+
if (args.json) console.log(JSON.stringify(categories, null, 2));
|
|
144
|
+
else {
|
|
145
|
+
console.log(`RouteScore categories (${categories.length})`);
|
|
146
|
+
for (const category of categories) console.log(`- ${category.id} · ${category.label} · aliases: ${(category.aliases || []).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (sub === 'list') {
|
|
152
|
+
const scored = services.map((service) => scoreService(service, summary.telemetry[service.id] || {}));
|
|
153
|
+
if (args.json) console.log(JSON.stringify(scored, null, 2));
|
|
154
|
+
else {
|
|
155
|
+
console.log(`RouteScore catalog (${scored.length} services)`);
|
|
156
|
+
for (const service of scored) {
|
|
157
|
+
const source = service.source ? ` · ${service.source}` : '';
|
|
158
|
+
console.log(`- ${service.name} (${service.category}) ${service.domain} · score ${service.score}/100 · ${service.recommendation}${source}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (sub === 'recommend') {
|
|
165
|
+
const rec = recommendService({ category: args.category, maxPriceUSDC: args['max-price'], query: args.query, services, telemetry: summary.telemetry });
|
|
166
|
+
if (args.json) console.log(JSON.stringify(rec, null, 2));
|
|
167
|
+
else if (!rec.best) console.log(`No service found for category=${rec.category}`);
|
|
168
|
+
else printRecommendation(rec);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (sub === 'route') {
|
|
173
|
+
const plan = routeService({ category: args.category, maxPriceUSDC: args['max-price'], query: args.query, minScore: args['min-score'] || 0, services, telemetry: summary.telemetry });
|
|
174
|
+
attachPaymentDecisions(plan, policy);
|
|
175
|
+
if (args.json) console.log(JSON.stringify(plan, null, 2));
|
|
176
|
+
else printRoutePlan(plan);
|
|
177
|
+
if (!plan.ok) process.exitCode = 3;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (sub === 'fallback') {
|
|
182
|
+
const plan = routeService({ category: args.category, maxPriceUSDC: args['max-price'], query: args.query, minScore: args['min-score'] || 0, services, telemetry: summary.telemetry });
|
|
183
|
+
attachPaymentDecisions(plan, policy);
|
|
184
|
+
const failIds = new Set(String(args['simulate-fail'] || '').split(',').map((x) => x.trim()).filter(Boolean));
|
|
185
|
+
const result = await executeFallbackRoute({
|
|
186
|
+
plan,
|
|
187
|
+
executor: async (service) => {
|
|
188
|
+
if (failIds.has(service.id) || failIds.has(service.domain) || failIds.has('primary') && service.id === plan.selected?.id) return { ok: false, error: 'simulated failure' };
|
|
189
|
+
return { ok: true, result: { service: service.id, endpoint: service.endpoint, simulated: true } };
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
193
|
+
else printFallbackResult(result);
|
|
194
|
+
if (!result.ok) process.exitCode = 3;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (sub === 'import') {
|
|
199
|
+
const file = required(args._[1] || args.file, 'services file');
|
|
200
|
+
const imported = await importServicesFile(file, { rootDir: process.cwd(), source: args.source || 'custom' });
|
|
201
|
+
const existing = await loadRouteScoreServices({ rootDir: process.cwd(), includeSeed: false, filePath: args.catalog });
|
|
202
|
+
const saved = await saveCustomServices([...existing, ...imported], { rootDir: process.cwd(), filePath: args.catalog, replace: true });
|
|
203
|
+
if (args.json) console.log(JSON.stringify(saved, null, 2));
|
|
204
|
+
else console.log(`Imported ${imported.length} services. Local RouteScore catalog now has ${saved.count} custom/live services at ${saved.path}`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (sub === 'sync-bazaar') {
|
|
209
|
+
const mode = args.query ? 'search' : 'resources';
|
|
210
|
+
const fetched = args.query
|
|
211
|
+
? await fetchBazaarSearch({ query: args.query, limit: args.limit || 20, network: args.network, asset: args.asset })
|
|
212
|
+
: await fetchBazaarResources({ limit: args.limit || 100, offset: args.offset || 0, type: args.type || 'http' });
|
|
213
|
+
const existing = args.replace ? [] : await loadRouteScoreServices({ rootDir: process.cwd(), includeSeed: false, filePath: args.catalog });
|
|
214
|
+
const saved = await saveCustomServices([...existing, ...fetched.services], { rootDir: process.cwd(), filePath: args.catalog, replace: true });
|
|
215
|
+
const payload = { ok: true, mode, fetched: fetched.services.length, saved: saved.count, path: saved.path, sourceUrl: fetched.url, pagination: fetched.pagination || null };
|
|
216
|
+
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
217
|
+
else {
|
|
218
|
+
console.log(`Synced ${fetched.services.length} Bazaar services from ${mode}.`);
|
|
219
|
+
console.log(`Local RouteScore catalog: ${saved.path} (${saved.count} custom/live services).`);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (sub === 'search-bazaar') {
|
|
225
|
+
const fetched = await fetchBazaarSearch({ query: args.query || args._.slice(1).join(' '), limit: args.limit || 10, network: args.network, asset: args.asset });
|
|
226
|
+
if (args.save) {
|
|
227
|
+
const existing = await loadRouteScoreServices({ rootDir: process.cwd(), includeSeed: false, filePath: args.catalog });
|
|
228
|
+
const saved = await saveCustomServices([...existing, ...fetched.services], { rootDir: process.cwd(), filePath: args.catalog, replace: true });
|
|
229
|
+
fetched.saved = { path: saved.path, count: saved.count };
|
|
230
|
+
}
|
|
231
|
+
if (args.json) console.log(JSON.stringify(fetched, null, 2));
|
|
232
|
+
else {
|
|
233
|
+
console.log(`Bazaar search returned ${fetched.services.length} services${fetched.searchMethod ? ` (${fetched.searchMethod})` : ''}.`);
|
|
234
|
+
for (const service of fetched.services) console.log(`- ${service.name} (${service.category}) ${service.domain} · $${service.priceUSDC} · ${service.endpoint}`);
|
|
235
|
+
if (fetched.saved) console.log(`Saved to ${fetched.saved.path} (${fetched.saved.count} custom/live services).`);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new Error(`Unknown routescore command: ${sub}`);
|
|
241
|
+
}
|
|
242
|
+
function printRecommendation(rec) {
|
|
243
|
+
console.log(`Best: ${rec.best.name}`);
|
|
244
|
+
console.log(`Endpoint: ${rec.best.endpoint}`);
|
|
245
|
+
console.log(`Score: ${rec.best.score}/100 · ${rec.best.recommendation}`);
|
|
246
|
+
console.log(`Price: ${rec.best.priceUSDC} USDC · Category: ${rec.best.category}`);
|
|
247
|
+
for (const reason of rec.best.reasons) console.log(`- ${reason}`);
|
|
248
|
+
if (rec.alternatives?.length) {
|
|
249
|
+
console.log('Fallbacks:');
|
|
250
|
+
for (const alt of rec.alternatives.slice(0, 3)) console.log(`- ${alt.name} · ${alt.score}/100 · $${alt.priceUSDC}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function printRoutePlan(plan) {
|
|
255
|
+
if (!plan.selected) {
|
|
256
|
+
console.log(`No route found for category=${plan.category}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
console.log(`Route: ${plan.selected.name}`);
|
|
260
|
+
console.log(`Endpoint: ${plan.selected.endpoint}`);
|
|
261
|
+
console.log(`Score: ${plan.selected.score}/100 · ${plan.selected.recommendation}`);
|
|
262
|
+
console.log(`Price: ${plan.selected.priceUSDC} USDC · Category: ${plan.selected.category}`);
|
|
263
|
+
if (plan.paymentDecision) console.log(`Policy: ${plan.paymentDecision.decision}${plan.paymentDecision.ok ? ' ✅' : ' ⚠️'}`);
|
|
264
|
+
if (plan.fallbacks?.length) {
|
|
265
|
+
console.log('Fallback plan:');
|
|
266
|
+
for (const fallback of plan.fallbacks) console.log(`- ${fallback.name} · ${fallback.score}/100 · $${fallback.priceUSDC} · ${fallback.endpoint}`);
|
|
267
|
+
}
|
|
268
|
+
console.log(plan.note);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
async function x402ReceiptCommand(args) {
|
|
274
|
+
const sub = args._[0] || 'summary';
|
|
275
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
276
|
+
|
|
277
|
+
if (sub === 'ingest') {
|
|
278
|
+
const file = required(args.file || args._[1], '--file');
|
|
279
|
+
const result = await ingestX402ReceiptFile(file, { rootDir: process.cwd(), policy, source: args.source || 'cli' });
|
|
280
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
281
|
+
else {
|
|
282
|
+
console.log(`x402 receipt ingested: ${result.receipt.receiptId}`);
|
|
283
|
+
console.log(`Domain: ${result.receipt.domain} · Amount: ${result.receipt.amountUSDC} ${result.receipt.asset} · Status: ${result.receipt.settlementStatus}`);
|
|
284
|
+
console.log(`Telemetry: ${result.telemetry.stored ? 'stored locally' : result.telemetry.reason || 'not stored'}`);
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (sub === 'list' || sub === 'events') {
|
|
290
|
+
const receipts = await readX402Receipts({ rootDir: process.cwd(), limit: args.limit || 50 });
|
|
291
|
+
if (args.json || sub === 'events') console.log(JSON.stringify(receipts, null, 2));
|
|
292
|
+
else {
|
|
293
|
+
console.log(`x402 receipts (${receipts.length})`);
|
|
294
|
+
for (const receipt of receipts) console.log(`- ${receipt.receiptId} · ${receipt.domain} · ${receipt.amountUSDC} ${receipt.asset} · ${receipt.settlementStatus}`);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (sub === 'summary') {
|
|
300
|
+
const summary = await summarizeX402Receipts({ rootDir: process.cwd(), limit: args.limit || 5000 });
|
|
301
|
+
if (args.json) console.log(JSON.stringify(summary, null, 2));
|
|
302
|
+
else {
|
|
303
|
+
console.log(`x402 receipts: ${summary.receiptCount}`);
|
|
304
|
+
console.log(`Settled: ${summary.settled} · Failed: ${summary.failed} · Pending/unknown: ${summary.pending}`);
|
|
305
|
+
console.log(`Observed spend: ${summary.totalAmountUSDC.toFixed(6)} USDC`);
|
|
306
|
+
for (const domain of summary.domains.slice(0, 10)) console.log(`- ${domain.domain}: ${domain.count} receipts · ${domain.totalAmountUSDC.toFixed(6)} USDC`);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
throw new Error(`Unknown x402-receipt command: ${sub}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function attachPaymentDecisions(plan, policy) {
|
|
315
|
+
const services = [plan.selected, ...(plan.fallbacks || [])].filter(Boolean);
|
|
316
|
+
plan.paymentDecisions = services.map((service) => ({
|
|
317
|
+
serviceId: service.id,
|
|
318
|
+
domain: service.domain,
|
|
319
|
+
decision: checkPayment({
|
|
320
|
+
domain: service.domain,
|
|
321
|
+
amountUSDC: service.priceUSDC,
|
|
322
|
+
routeScore: service.score,
|
|
323
|
+
category: service.category,
|
|
324
|
+
knownService: true
|
|
325
|
+
}, policy)
|
|
326
|
+
}));
|
|
327
|
+
plan.paymentDecision = plan.paymentDecisions[0]?.decision || null;
|
|
328
|
+
return plan;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
function printFallbackResult(result) {
|
|
333
|
+
if (!result.plan?.selected) {
|
|
334
|
+
console.log('No fallback route available.');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
console.log(`Fallback result: ${result.ok ? 'PASS' : 'FAIL'}`);
|
|
338
|
+
console.log(`Selected: ${result.selected?.name || 'none'}`);
|
|
339
|
+
console.log(`Fallback used: ${result.fallbackUsed ? 'yes' : 'no'}`);
|
|
340
|
+
for (const attempt of result.attempts || []) console.log(`- attempt ${attempt.index + 1}: ${attempt.service.name} · ${attempt.ok ? 'ok' : 'failed'} · ${attempt.latencyMs}ms${attempt.error ? ` · ${attempt.error}` : ''}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function telemetryCommand(args) {
|
|
344
|
+
const sub = args._[0] || 'status';
|
|
345
|
+
const policyPath = args.policy || 'mythos.policy.json';
|
|
346
|
+
const policy = await loadPolicy(policyPath);
|
|
347
|
+
|
|
348
|
+
if (sub === 'status') {
|
|
349
|
+
const summary = await telemetrySummary({ rootDir: process.cwd(), policy });
|
|
350
|
+
const payload = { enabled: telemetryEnabled(policy), privacy: telemetryPrivacy(policy), eventCount: summary.eventCount, services: summary.aggregates.length };
|
|
351
|
+
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
352
|
+
else {
|
|
353
|
+
console.log(`Telemetry: ${payload.enabled ? 'enabled' : 'disabled'}`);
|
|
354
|
+
console.log(`Events: ${payload.eventCount}`);
|
|
355
|
+
console.log(`Services observed: ${payload.services}`);
|
|
356
|
+
console.log(payload.privacy.note);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (sub === 'summary') {
|
|
362
|
+
const summary = await telemetrySummary({ rootDir: process.cwd(), policy, limit: args.limit || 5000 });
|
|
363
|
+
if (args.json) console.log(JSON.stringify(summary, null, 2));
|
|
364
|
+
else {
|
|
365
|
+
console.log(`Telemetry events: ${summary.eventCount}`);
|
|
366
|
+
for (const item of summary.aggregates) {
|
|
367
|
+
console.log(`- ${item.serviceId}: ${item.samples} samples · ${(item.successRate * 100).toFixed(1)}% success · ${item.medianLatencyMs ?? 'n/a'}ms median · ${item.totalAmountUSDC.toFixed(4)} USDC observed`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (sub === 'events') {
|
|
374
|
+
const events = await readTelemetryEvents({ rootDir: process.cwd(), policy, limit: args.limit || 50 });
|
|
375
|
+
console.log(JSON.stringify(events, null, 2));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (sub === 'record-demo') {
|
|
380
|
+
const result = await appendTelemetryEvent({
|
|
381
|
+
rootDir: process.cwd(),
|
|
382
|
+
policy,
|
|
383
|
+
event: {
|
|
384
|
+
source: 'cli_demo',
|
|
385
|
+
mode: 'manual',
|
|
386
|
+
domain: args.domain || 'api.exa.ai',
|
|
387
|
+
decision: args.decision || 'allow',
|
|
388
|
+
ok: args.ok === undefined ? true : String(args.ok) !== 'false',
|
|
389
|
+
latencyMs: args.latency || 900,
|
|
390
|
+
amountUSDC: args.amount || 0.005,
|
|
391
|
+
schemaOk: true,
|
|
392
|
+
priceMatchedQuote: true
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
console.log(JSON.stringify(result, null, 2));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
400
|
+
const updated = await setTelemetryEnabled({ rootDir: process.cwd(), policy, enabled: sub === 'enable' });
|
|
401
|
+
await writeJson(policyPath, updated);
|
|
402
|
+
console.log(`Telemetry ${sub === 'enable' ? 'enabled' : 'disabled'} in ${policyPath}`);
|
|
403
|
+
console.log(telemetryPrivacy(updated).note);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
throw new Error(`Unknown telemetry command: ${sub}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function commandGuardCommand(args) {
|
|
411
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
412
|
+
const command = args.command || args._.join(' ');
|
|
413
|
+
const decision = checkCommand({ command: required(command, '--command or command after --') }, policy);
|
|
414
|
+
if (args.json) console.log(JSON.stringify(decision, null, 2));
|
|
415
|
+
else console.log(formatPaymentDecision(decision));
|
|
416
|
+
if (!decision.ok) process.exitCode = decision.decision === 'approval_required' ? 5 : 3;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function fileGuardCommand(args) {
|
|
420
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
421
|
+
const decision = checkFilesystemAccess({
|
|
422
|
+
filePath: required(args.path || args.file || args._[0], '--path'),
|
|
423
|
+
operation: args.operation || args.op || 'read'
|
|
424
|
+
}, policy);
|
|
425
|
+
if (args.json) console.log(JSON.stringify(decision, null, 2));
|
|
426
|
+
else console.log(formatPaymentDecision(decision));
|
|
427
|
+
if (!decision.ok) process.exitCode = 3;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function networkGuardCommand(args) {
|
|
431
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
432
|
+
const decision = checkNetwork({ domain: required(args.domain || args._[0], '--domain') }, policy);
|
|
433
|
+
if (args.json) console.log(JSON.stringify(decision, null, 2));
|
|
434
|
+
else console.log(formatPaymentDecision(decision));
|
|
435
|
+
if (!decision.ok) process.exitCode = 3;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function snapshotCommand(args) {
|
|
439
|
+
const target = args._[0] || '.';
|
|
440
|
+
const out = args.out || `.mythos/snapshots/${Date.now()}.json`;
|
|
441
|
+
const snapshot = await createSnapshot(target);
|
|
442
|
+
await writeJson(out, snapshot);
|
|
443
|
+
console.log(`Snapshot written to ${out} (${snapshot.files.length} files)`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function receiptCommand(args) {
|
|
447
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
448
|
+
const receipt = await createReceipt({
|
|
449
|
+
beforePath: required(args.before, '--before'),
|
|
450
|
+
afterPath: args.after,
|
|
451
|
+
rootDir: args.path || '.',
|
|
452
|
+
summary: args.summary || '',
|
|
453
|
+
agent: args.agent || 'unknown-agent',
|
|
454
|
+
provider: args.provider || 'unknown-provider',
|
|
455
|
+
tool: args.tool || 'unknown-tool',
|
|
456
|
+
policy
|
|
457
|
+
});
|
|
458
|
+
const out = args.out || 'mythos-receipt.json';
|
|
459
|
+
await writeReceipt(out, receipt);
|
|
460
|
+
console.log(`Receipt written to ${out}`);
|
|
461
|
+
console.log(`Changed files: ${receipt.diff.changedCount} | Verification: ${receipt.verification.ok ? 'PASS' : 'FAIL'}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function verifyCommand(args) {
|
|
465
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
466
|
+
const result = await verifyReceipt({
|
|
467
|
+
receiptPath: required(args.receipt, '--receipt'),
|
|
468
|
+
rootDir: args.path || '.',
|
|
469
|
+
policy,
|
|
470
|
+
failOn: args['fail-on']
|
|
471
|
+
});
|
|
472
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
473
|
+
else {
|
|
474
|
+
console.log(`Receipt verification: ${result.ok ? 'PASS ✅' : 'FAIL ⛔'}`);
|
|
475
|
+
console.log(`Workspace drift since receipt: ${result.drift.changedCount}`);
|
|
476
|
+
console.log(`Scan highest severity: ${result.scanSummary.highestSeverity}`);
|
|
477
|
+
if (result.failingFindings?.length) {
|
|
478
|
+
console.log('Failing findings:');
|
|
479
|
+
for (const finding of result.failingFindings) console.log(`- ${finding.severity.toUpperCase()} ${finding.id} ${finding.file}:${finding.line}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!result.ok) process.exitCode = 4;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function policyCommand(args) {
|
|
486
|
+
const sub = args._[0] || 'show';
|
|
487
|
+
const policy = await loadPolicy(args.policy || 'mythos.policy.json');
|
|
488
|
+
if (sub === 'show') console.log(JSON.stringify(policy, null, 2));
|
|
489
|
+
else if (sub === 'schema') console.log(await fs.readFile(new URL('../schemas/policy.schema.json', import.meta.url), 'utf8'));
|
|
490
|
+
else throw new Error(`Unknown policy command: ${sub}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function uiCommand(args) {
|
|
494
|
+
await startDashboard({
|
|
495
|
+
port: args.port || 4317,
|
|
496
|
+
host: args.host || '127.0.0.1',
|
|
497
|
+
open: Boolean(args.open),
|
|
498
|
+
demo: Boolean(args.demo)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function doctorCommand() {
|
|
503
|
+
console.log(`${PRODUCT} ${VERSION}`);
|
|
504
|
+
console.log(`Node: ${process.version}`);
|
|
505
|
+
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
506
|
+
console.log(`CWD: ${process.cwd()}`);
|
|
507
|
+
console.log(`Policy: ${(await exists('mythos.policy.json')) ? 'found' : 'not found'}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function required(value, name) {
|
|
511
|
+
if (value === undefined || value === null || value === '') throw new Error(`Missing required ${name}`);
|
|
512
|
+
return value;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function parseArgs(argv) {
|
|
516
|
+
const result = { _: [] };
|
|
517
|
+
for (let i = 0; i < argv.length; i++) {
|
|
518
|
+
const token = argv[i];
|
|
519
|
+
if (token === '--') {
|
|
520
|
+
result._.push(...argv.slice(i + 1));
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
if (token.startsWith('--')) {
|
|
524
|
+
const [rawKey, rawValue] = token.slice(2).split('=');
|
|
525
|
+
if (rawValue !== undefined) result[rawKey] = rawValue;
|
|
526
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith('-')) result[rawKey] = argv[++i];
|
|
527
|
+
else result[rawKey] = true;
|
|
528
|
+
} else if (token.startsWith('-') && token.length > 1) {
|
|
529
|
+
result[token.slice(1)] = true;
|
|
530
|
+
} else {
|
|
531
|
+
result._.push(token);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function printHelp() {
|
|
538
|
+
console.log(`
|
|
539
|
+
Mythos Sentinel ${VERSION}
|
|
540
|
+
|
|
541
|
+
Security layer for autonomous agents, MCP tools, skills, and x402/Base payments.
|
|
542
|
+
|
|
543
|
+
Usage:
|
|
544
|
+
mythos-sentinel init [--base] [--force]
|
|
545
|
+
mythos-sentinel scan [path] [--policy mythos.policy.json] [--json] [--sarif] [--out report.json] [--fail-on high]
|
|
546
|
+
mythos-sentinel check-payment --domain api.example.com --amount 0.05 [--daily-spent 1.2] [--route-score 91]
|
|
547
|
+
mythos-sentinel check-command -- "npm install left-pad"
|
|
548
|
+
mythos-sentinel check-file --path src/index.js --operation write
|
|
549
|
+
mythos-sentinel check-network --domain api.github.com
|
|
550
|
+
mythos-sentinel routescore list|categories|recommend|route|fallback [--category web_search] [--max-price 0.05]
|
|
551
|
+
mythos-sentinel routescore import services.yml
|
|
552
|
+
mythos-sentinel routescore sync-bazaar [--query search] [--limit 50]
|
|
553
|
+
mythos-sentinel telemetry status|enable|disable|summary|events
|
|
554
|
+
mythos-sentinel x402-receipt ingest --file receipt.json | summary | list
|
|
555
|
+
mythos-sentinel proxy [--policy mythos.policy.json] [--config proxy.json]
|
|
556
|
+
mythos-sentinel snapshot [path] --out .mythos/snapshots/before.json
|
|
557
|
+
mythos-sentinel receipt --before before.json --summary "agent task" --agent codex --provider openai --tool codex-cli
|
|
558
|
+
mythos-sentinel verify --receipt mythos-receipt.json
|
|
559
|
+
mythos-sentinel mcp
|
|
560
|
+
mythos-sentinel ui [--host 127.0.0.1] [--port 4317] [--open] [--demo]
|
|
561
|
+
mythos-sentinel doctor
|
|
562
|
+
|
|
563
|
+
Exit codes:
|
|
564
|
+
0 pass, 2 scan policy failure, 3 guard/payment blocked, 4 receipt verification failure, 5 human approval required
|
|
565
|
+
`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function mythosReadme() {
|
|
569
|
+
return `# .mythos\n\nThis directory stores Sentinel reports, receipts, and snapshots.\n\nDo not commit secret material here. Commit receipts only when they are intentionally public.\n`;
|
|
570
|
+
}
|
package/src/core/fs.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { normalizePath, toPosixRelative, matchesAnyGlob } from './path-utils.js';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_IGNORES = [
|
|
8
|
+
'.git/**', 'node_modules/**', 'dist/**', 'build/**', 'coverage/**', '.next/**', '.turbo/**',
|
|
9
|
+
'.vercel/**', '.cache/**', '.mythos/reports/**', '.mythos/snapshots/**', '*.png', '*.jpg',
|
|
10
|
+
'*.jpeg', '*.gif', '*.webp', '*.ico', '*.pdf', '*.zip', '*.gz', '*.tar', '*.lock', '*.sarif', 'mythos-receipt.json'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export async function exists(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(filePath);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function ensureDir(dirPath) {
|
|
23
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sha256Buffer(buffer) {
|
|
27
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function sha256File(filePath) {
|
|
31
|
+
return sha256Buffer(await fs.readFile(filePath));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isLikelyText(buffer) {
|
|
35
|
+
if (!buffer.length) return true;
|
|
36
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
|
|
37
|
+
if (sample.includes(0)) return false;
|
|
38
|
+
let suspicious = 0;
|
|
39
|
+
for (const byte of sample) {
|
|
40
|
+
if (byte < 7 || (byte > 13 && byte < 32)) suspicious += 1;
|
|
41
|
+
}
|
|
42
|
+
return suspicious / sample.length < 0.1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function readTextMaybe(filePath, maxBytes = 512 * 1024) {
|
|
46
|
+
const stat = await fs.stat(filePath);
|
|
47
|
+
if (stat.size > maxBytes) return { skipped: true, reason: `file too large (${stat.size} bytes)` };
|
|
48
|
+
const buffer = await fs.readFile(filePath);
|
|
49
|
+
if (!isLikelyText(buffer)) return { skipped: true, reason: 'binary file' };
|
|
50
|
+
return { text: buffer.toString('utf8'), size: stat.size };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function walkFiles(rootDir, options = {}) {
|
|
54
|
+
const ignores = [...DEFAULT_IGNORES, ...(options.ignore || [])];
|
|
55
|
+
const root = path.resolve(rootDir);
|
|
56
|
+
const results = [];
|
|
57
|
+
|
|
58
|
+
async function visit(absPath) {
|
|
59
|
+
const rel = toPosixRelative(root, absPath);
|
|
60
|
+
if (rel !== '.' && matchesAnyGlob(rel, ignores)) return;
|
|
61
|
+
let stat;
|
|
62
|
+
try {
|
|
63
|
+
stat = await fs.lstat(absPath);
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (stat.isSymbolicLink()) return;
|
|
68
|
+
if (stat.isDirectory()) {
|
|
69
|
+
const entries = await fs.readdir(absPath);
|
|
70
|
+
for (const entry of entries) await visit(path.join(absPath, entry));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (stat.isFile()) results.push({ absPath, rel: normalizePath(rel), size: stat.size });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!fsSync.existsSync(root)) throw new Error(`Path does not exist: ${rootDir}`);
|
|
77
|
+
await visit(root);
|
|
78
|
+
return results.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function writeJson(filePath, data) {
|
|
82
|
+
await ensureDir(path.dirname(filePath));
|
|
83
|
+
await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function readJson(filePath) {
|
|
87
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
88
|
+
}
|