@secondlayer/sentinel 0.1.1 → 0.2.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/dist/cli.js +387 -42
- package/dist/index.js +23 -3
- package/dist/kind.d.ts +42 -0
- package/dist/mcp.js +358 -30
- package/dist/sentinel-client.d.ts +86 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -85,10 +85,14 @@ class SentinelError extends Error {
|
|
|
85
85
|
this.name = "SentinelError";
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
var
|
|
88
|
+
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
89
|
+
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
90
|
+
var CONTROL_NAME = "Control changes";
|
|
89
91
|
function subjectName(s, units) {
|
|
90
92
|
if (s.kind === "fn")
|
|
91
93
|
return s.fn;
|
|
94
|
+
if (s.kind === "group")
|
|
95
|
+
return CONTROL_NAME;
|
|
92
96
|
if (units?.symbol)
|
|
93
97
|
return units.symbol;
|
|
94
98
|
if (s.asset.toLowerCase() === "stx")
|
|
@@ -120,7 +124,7 @@ function resolveSubject(watches, name) {
|
|
|
120
124
|
const hits = groups.filter((g) => {
|
|
121
125
|
const s = g.watches[0].subject;
|
|
122
126
|
const asset = s.kind === "asset" ? s.asset.toLowerCase() : null;
|
|
123
|
-
return g.name.toLowerCase() === q || g.key.toLowerCase() === q || asset === q || asset !== null && asset.split("::")[1] === q;
|
|
127
|
+
return g.name.toLowerCase() === q || g.key.toLowerCase() === q || s.kind === "group" && q === s.group || asset === q || asset !== null && asset.split("::")[1] === q;
|
|
124
128
|
});
|
|
125
129
|
if (hits.length === 1)
|
|
126
130
|
return hits[0];
|
|
@@ -172,26 +176,16 @@ function ruleSummary(rule, units) {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
case "callerAllowlist":
|
|
184
|
-
case "actor":
|
|
185
|
-
return "who-acts";
|
|
186
|
-
case "audit":
|
|
187
|
-
return rule.triggerClass === "governance.proposal_submitted" ? "governance" : "upgrade";
|
|
179
|
+
function intentLine(i) {
|
|
180
|
+
const t = i.threshold;
|
|
181
|
+
if (i.kind !== "money_out" || !t)
|
|
182
|
+
return i.summary;
|
|
183
|
+
const units = i.units ?? null;
|
|
184
|
+
if (t.amount === null) {
|
|
185
|
+
const sug = t.suggested ? ` (suggested ${amountText(t.suggested.amount, units)}, ${t.suggested.basis})` : "";
|
|
186
|
+
return `no threshold yet: every outflow alerts${sug}`;
|
|
188
187
|
}
|
|
189
|
-
}
|
|
190
|
-
function subjectStatus(ws) {
|
|
191
|
-
const st = ws.map((w) => w.status);
|
|
192
|
-
if (st.includes("live"))
|
|
193
|
-
return "live";
|
|
194
|
-
return st.length > 0 && st.every((x) => x === "paused") ? "paused" : "suggested";
|
|
188
|
+
return `${amountText(t.amount, units)} or more in one tx · ${t.basis}`;
|
|
195
189
|
}
|
|
196
190
|
var isMutableRule = (rule) => rule.kind !== "signature" && rule.kind !== "audit";
|
|
197
191
|
function mutedUntilOf(w, now = Date.now()) {
|
|
@@ -256,11 +250,13 @@ var HOUR_MS = 3600000;
|
|
|
256
250
|
var muteEnd = (hours) => new Date(Date.now() + Math.min(hours * HOUR_MS, 7 * 24 * HOUR_MS - 60000)).toISOString();
|
|
257
251
|
async function watchContext(client, planId) {
|
|
258
252
|
const plan = await client.plan(planId);
|
|
253
|
+
const { intents } = await client.watches(planId);
|
|
259
254
|
return {
|
|
260
255
|
planId,
|
|
261
256
|
contractId: plan.contractId,
|
|
262
257
|
access: plan.access,
|
|
263
|
-
|
|
258
|
+
intents,
|
|
259
|
+
watches: rowsOf(intents)
|
|
264
260
|
};
|
|
265
261
|
}
|
|
266
262
|
async function savedMonitor(client, planId) {
|
|
@@ -423,6 +419,26 @@ async function whoami(client, io) {
|
|
|
423
419
|
return 0;
|
|
424
420
|
}
|
|
425
421
|
|
|
422
|
+
// client/kind.ts
|
|
423
|
+
var isGreen = (f) => f.poc === "green";
|
|
424
|
+
function findingKind(f) {
|
|
425
|
+
if (f.disposition === "refuted" || f.verdict === "refuted")
|
|
426
|
+
return "refuted";
|
|
427
|
+
if (f.class === "centralization" || f.disposition === "waived")
|
|
428
|
+
return "trust";
|
|
429
|
+
if (f.class === "bug" && f.verdict === "confirmed" && f.reverified === true && isGreen(f)) {
|
|
430
|
+
return "exploitable";
|
|
431
|
+
}
|
|
432
|
+
return "worth_a_look";
|
|
433
|
+
}
|
|
434
|
+
var URGENT_CONSEQUENCES = new Set(["authority", "code", "value"]);
|
|
435
|
+
var KIND_LABEL = {
|
|
436
|
+
exploitable: "Exploitable",
|
|
437
|
+
trust: "Trust assumption",
|
|
438
|
+
worth_a_look: "Worth a look",
|
|
439
|
+
refuted: "refuted"
|
|
440
|
+
};
|
|
441
|
+
|
|
426
442
|
// client/labels.ts
|
|
427
443
|
var isPrelaunchKey = (id) => id.startsWith("github:") || id.startsWith("gist:");
|
|
428
444
|
function contractName(id) {
|
|
@@ -439,6 +455,294 @@ var MONITOR_LABELS = {
|
|
|
439
455
|
};
|
|
440
456
|
var monitoringLabel = (status) => status ? MONITOR_LABELS[status] ?? status : "not on yet";
|
|
441
457
|
|
|
458
|
+
// client/handoff.ts
|
|
459
|
+
var DEFAULT_FILE = "sentinel-poc.ts";
|
|
460
|
+
var pocFileName = (f) => f.proof?.file ?? DEFAULT_FILE;
|
|
461
|
+
function runCommand(opts) {
|
|
462
|
+
const lines = [`# save the PoC above as ${opts.file}, then in your own checkout:`];
|
|
463
|
+
if (opts.commit)
|
|
464
|
+
lines.push(`git checkout ${opts.commit} # the commit Sentinel reviewed`);
|
|
465
|
+
const where = opts.substrate === "fork" ? "needs a Stacks RPC" : "airgapped, no network";
|
|
466
|
+
lines.push(`bun run ${opts.file} # @stacks/clarinet-sdk — ${where}`);
|
|
467
|
+
return lines.join(`
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
function handoffBundle(f, ctx) {
|
|
471
|
+
const proof = f.proof;
|
|
472
|
+
const file = pocFileName(f);
|
|
473
|
+
const poc = proof?.script ? {
|
|
474
|
+
file,
|
|
475
|
+
substrate: proof.substrate ?? null,
|
|
476
|
+
reproduced: !!proof.reproduced,
|
|
477
|
+
exitCode: proof.exitCode ?? null,
|
|
478
|
+
source: proof.script
|
|
479
|
+
} : null;
|
|
480
|
+
return {
|
|
481
|
+
contract: ctx.contract,
|
|
482
|
+
fn: f.targetFn ?? null,
|
|
483
|
+
commit: ctx.commit ?? null,
|
|
484
|
+
poc,
|
|
485
|
+
runCommand: poc ? runCommand({ commit: ctx.commit, file, substrate: poc.substrate }) : null
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function assertionsOf(output) {
|
|
489
|
+
const ratio = output.match(/(\d+)\s*\/\s*(\d+)\s*assertions?/i);
|
|
490
|
+
if (ratio)
|
|
491
|
+
return `${ratio[1]}/${ratio[2]}`;
|
|
492
|
+
const passed = output.match(/(\d+)\s*assertions?\s*passed/i);
|
|
493
|
+
return passed ? `${passed[1]} passed` : null;
|
|
494
|
+
}
|
|
495
|
+
var FENCE = "```";
|
|
496
|
+
var FOOTER = "Sentinel's findings are a starting point for your own review, not an audit sign-off.";
|
|
497
|
+
var KIND_LINE = {
|
|
498
|
+
exploitable: "Exploitable (verified, re-verified, reproduced in the sandbox)",
|
|
499
|
+
trust: "Trust assumption (watched: Sentinel alerts when it's used)",
|
|
500
|
+
worth_a_look: "Worth a look (not proven exploitable)",
|
|
501
|
+
refuted: "Refuted"
|
|
502
|
+
};
|
|
503
|
+
var tick = (s) => `\`${s}\``;
|
|
504
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
505
|
+
var num = (n) => n.toLocaleString("en-US");
|
|
506
|
+
function printed(output) {
|
|
507
|
+
const lines = output.replace(/\[SENTINEL-POC-TRACE\][\s\S]*?\[\/SENTINEL-POC-TRACE\]/g, "").split(`
|
|
508
|
+
`).filter((l) => l.trim());
|
|
509
|
+
return lines.slice(-40).join(`
|
|
510
|
+
`);
|
|
511
|
+
}
|
|
512
|
+
function movedMd(trace) {
|
|
513
|
+
const keys = Object.keys(trace.series ?? {}).filter((k) => trace.steps.some((s) => s.values?.[k] !== undefined));
|
|
514
|
+
const name = (k) => trace.series?.[k] ?? k;
|
|
515
|
+
const unit = trace.unit ? ` ${trace.unit}` : "";
|
|
516
|
+
const changes = (i) => {
|
|
517
|
+
const prev = trace.steps[i - 1]?.values;
|
|
518
|
+
const now = trace.steps[i].values;
|
|
519
|
+
if (!prev || !now)
|
|
520
|
+
return [];
|
|
521
|
+
return keys.filter((k) => now[k] !== undefined && prev[k] !== undefined && now[k] !== prev[k]).map((k) => `${name(k)} ${num(prev[k])} → ${num(now[k])}${unit}`);
|
|
522
|
+
};
|
|
523
|
+
const first = trace.steps.findIndex((_, i) => changes(i).length > 0);
|
|
524
|
+
const rows = trace.steps.map((s, i) => {
|
|
525
|
+
const call = [s.call && tick(s.call), s.result && `→ ${tick(s.result)}`].filter(Boolean).join(" ");
|
|
526
|
+
const start = keys.filter((k) => s.values?.[k] !== undefined).map((k) => `${name(k)} ${num(s.values?.[k])}${unit}`).join(" · ");
|
|
527
|
+
const change = i === 0 ? start || "start" : changes(i).join(" · ") || "no change";
|
|
528
|
+
return `| ${i + 1} ${cell(s.label)} | ${cell(call || "—")} | ${cell(change)} |`;
|
|
529
|
+
});
|
|
530
|
+
return [
|
|
531
|
+
"## What the PoC moved",
|
|
532
|
+
first > 0 ? `**${changes(first).join("; ")} at step ${first + 1} (${trace.steps[first].label}).**` : "No tracked value changed across the steps.",
|
|
533
|
+
"",
|
|
534
|
+
"| Step | Call | Value change |",
|
|
535
|
+
"|---|---|---|",
|
|
536
|
+
...rows,
|
|
537
|
+
""
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
function variantOf(f) {
|
|
541
|
+
if (f.proof?.reproduced)
|
|
542
|
+
return "reproduced";
|
|
543
|
+
if (f.proof && !f.proof.unavailable && !f.proof.errored && f.proof.script)
|
|
544
|
+
return "notrepro";
|
|
545
|
+
if (f.verdict !== "confirmed")
|
|
546
|
+
return "lead";
|
|
547
|
+
return "analysis";
|
|
548
|
+
}
|
|
549
|
+
function where(f, ctx) {
|
|
550
|
+
const at = ctx.commit ? ` at commit ${tick(ctx.commit.slice(0, 7))}` : "";
|
|
551
|
+
return f.targetFn ? `Open ${tick(f.targetFn)} in ${tick(contractName(ctx.contract))}${at} and show the function with ~10 lines of context. Point at the lines this finding is about and explain in your own words what they do.` : `Find the code this finding describes in ${tick(contractName(ctx.contract))}${at} and show it with ~10 lines of context. Explain in your own words what it does.`;
|
|
552
|
+
}
|
|
553
|
+
function contextMd(f, ctx, kind) {
|
|
554
|
+
return [
|
|
555
|
+
"## Context",
|
|
556
|
+
`- Contract: ${tick(ctx.contract)}`,
|
|
557
|
+
...ctx.commit ? [`- Commit reviewed: ${tick(ctx.commit.slice(0, 7))}`] : [],
|
|
558
|
+
...f.targetFn ? [`- Function: ${tick(f.targetFn)}`] : [],
|
|
559
|
+
`- Kind: ${KIND_LINE[kind]}`
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
function reportBackMd(ctx, extra) {
|
|
563
|
+
return [
|
|
564
|
+
"## How to report back",
|
|
565
|
+
`- Cite every claim with ${tick("file:line")}${ctx.commit ? ` at commit ${tick(ctx.commit.slice(0, 7))}` : ""}.`,
|
|
566
|
+
"- Show the code you discuss, quoted from the repo, not paraphrased.",
|
|
567
|
+
extra,
|
|
568
|
+
"- Keep two sections: **Facts** (what the code and the run show) and **Assessment** (your conclusions, each with a confidence level). Don't mix them.",
|
|
569
|
+
"- Don't change contract code until the team has chosen an approach.",
|
|
570
|
+
""
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
function tailMd(ctx, withPoc) {
|
|
574
|
+
return [
|
|
575
|
+
"",
|
|
576
|
+
"## Pull it again",
|
|
577
|
+
`${FENCE}shell`,
|
|
578
|
+
`npx @secondlayer/sentinel finding ${ctx.planId} ${ctx.index}${withPoc ? " --poc --run" : ""}`,
|
|
579
|
+
FENCE,
|
|
580
|
+
`MCP: ${tick(`get_finding { planId: "${ctx.planId}", index: ${ctx.index} }`)} returns this finding as JSON${withPoc ? ", the PoC source and the run command" : ""}.`,
|
|
581
|
+
"",
|
|
582
|
+
"---",
|
|
583
|
+
FOOTER
|
|
584
|
+
];
|
|
585
|
+
}
|
|
586
|
+
function issueMd(f, withCondition) {
|
|
587
|
+
return [
|
|
588
|
+
"## The issue",
|
|
589
|
+
f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
|
|
590
|
+
...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
|
|
591
|
+
...withCondition && f.precondition ? ["", `**Exploitable when:** ${f.precondition}`] : [],
|
|
592
|
+
""
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
function prelaunchBrief(f, ctx, kind) {
|
|
596
|
+
const kindLine = kind === "exploitable" ? "Exploitable · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
|
|
597
|
+
return [
|
|
598
|
+
"# Task: review a Sentinel finding on pre-launch code",
|
|
599
|
+
"",
|
|
600
|
+
`Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. PoCs and exploit detail open once Sentinel confirms this code isn't already live on mainnet; possible approaches are shown now, so this brief carries the finding and a direction, not the PoC. Look at the code before launch: the watch arms at launch, but it isn't a substitute for the review.`,
|
|
601
|
+
"",
|
|
602
|
+
"## Start here",
|
|
603
|
+
`1. ${where(f, ctx)}`,
|
|
604
|
+
"2. Only then discuss approaches that fit this codebase's design.",
|
|
605
|
+
"",
|
|
606
|
+
"## How to report back",
|
|
607
|
+
`- Cite every claim with ${tick("file:line")}${ctx.commit ? ` at commit ${tick(ctx.commit.slice(0, 7))}` : ""}.`,
|
|
608
|
+
"- Keep **Facts** and **Assessment** apart, with a confidence level on each conclusion.",
|
|
609
|
+
"",
|
|
610
|
+
"## Context",
|
|
611
|
+
`- Contract: ${tick(contractName(ctx.contract))} (pre-launch)${ctx.commit ? `, commit ${tick(ctx.commit.slice(0, 7))}` : ""}`,
|
|
612
|
+
`- ${f.targetFn ? `Function: ${tick(f.targetFn)} · ` : ""}Kind: ${kindLine}`,
|
|
613
|
+
"",
|
|
614
|
+
...issueMd(f, false),
|
|
615
|
+
...f.recommendedAction ? ["## One direction to consider (not a verified patch)", f.recommendedAction, ""] : [],
|
|
616
|
+
"---",
|
|
617
|
+
FOOTER
|
|
618
|
+
].join(`
|
|
619
|
+
`);
|
|
620
|
+
}
|
|
621
|
+
function trustBrief(f, ctx) {
|
|
622
|
+
return [
|
|
623
|
+
"# Task: review a trust assumption and decide whether to reduce it",
|
|
624
|
+
"",
|
|
625
|
+
`Sentinel found a power held by design${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. It's a trust assumption, not a bug, so there is nothing to reproduce. Review it and decide whether the trust it asks of your users fits your design.`,
|
|
626
|
+
"",
|
|
627
|
+
"## Start here",
|
|
628
|
+
`1. ${where(f, ctx)}`,
|
|
629
|
+
`2. Find every place that reads what ${f.targetFn ? tick(f.targetFn) : "this power"} changes, and show each one, so the reach of this power is clear.`,
|
|
630
|
+
"3. Only then discuss options that fit this codebase's design.",
|
|
631
|
+
"",
|
|
632
|
+
...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write an exploit for it."),
|
|
633
|
+
...contextMd(f, ctx, "trust"),
|
|
634
|
+
"- Class: trust assumption (a power held by design)",
|
|
635
|
+
"- Evidence: checked by Sentinel's adversarial verifier. No PoC; nothing to run.",
|
|
636
|
+
"",
|
|
637
|
+
"## What it is",
|
|
638
|
+
f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
|
|
639
|
+
...f.powers?.length ? [
|
|
640
|
+
"",
|
|
641
|
+
"**Powers:**",
|
|
642
|
+
...f.powers.map((p) => `- ${p.holder} can ${p.can}${p.fn ? ` (${tick(p.fn)})` : ""}`)
|
|
643
|
+
] : [],
|
|
644
|
+
...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
|
|
645
|
+
...f.precondition ? ["", `**Usable when:** ${f.precondition}`] : [],
|
|
646
|
+
"",
|
|
647
|
+
...f.recommendedAction ? ["## Options to consider (not a patch)", `- ${f.recommendedAction}`, ""] : [],
|
|
648
|
+
"## If you change it",
|
|
649
|
+
"- A test for the new behavior.",
|
|
650
|
+
"- A test that an account without the role is still refused.",
|
|
651
|
+
...tailMd(ctx, false)
|
|
652
|
+
].join(`
|
|
653
|
+
`);
|
|
654
|
+
}
|
|
655
|
+
function refutedBrief(f, ctx) {
|
|
656
|
+
return [
|
|
657
|
+
"# Note: a Sentinel finding refuted on re-check",
|
|
658
|
+
"",
|
|
659
|
+
`Sentinel's verifier re-examined this${f.targetFn ? ` (${tick(f.targetFn)})` : ""} and showed it can't happen: ${f.title}. It's kept for the record and isn't watched.`,
|
|
660
|
+
"",
|
|
661
|
+
...contextMd(f, ctx, "refuted"),
|
|
662
|
+
...tailMd(ctx, false)
|
|
663
|
+
].join(`
|
|
664
|
+
`);
|
|
665
|
+
}
|
|
666
|
+
function bugBrief(f, ctx, kind) {
|
|
667
|
+
const v = variantOf(f);
|
|
668
|
+
const bundle = handoffBundle(f, { contract: ctx.contract, commit: ctx.commit });
|
|
669
|
+
const poc = bundle.poc;
|
|
670
|
+
const p = f.proof;
|
|
671
|
+
const asserted = p?.output ? assertionsOf(p.output) : null;
|
|
672
|
+
const counted = asserted?.includes("/") ? `${asserted} assertions, ` : asserted ? `${asserted.replace(" passed", "")} assertions passed, ` : "";
|
|
673
|
+
const ran = `${counted}exit ${p?.exitCode ?? "?"}`;
|
|
674
|
+
const sandbox = p?.substrate === "fork" ? "a mainnet-fork sandbox" : `an airgapped sandbox (${tick("--network none")})`;
|
|
675
|
+
const what = `Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}`;
|
|
676
|
+
const open = {
|
|
677
|
+
reproduced: `${what}. It reproduced with the PoC below. Review the affected code, choose an approach that fits your design, then confirm the PoC no longer succeeds.`,
|
|
678
|
+
notrepro: `${what}. Its verifier confirmed it by analysis, but the PoC below ran and did not reproduce it. Review the affected code and the run, and decide whether it applies to your deployment.`,
|
|
679
|
+
analysis: `${what}. Its verifier confirmed it by analysis; no PoC result yet. ${poc ? "Run the PoC below in your own tooling, then review" : "Review"} the affected code and choose an approach that fits your design.`,
|
|
680
|
+
lead: `An auditor flagged an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. The verifier hasn't confirmed it, so treat it as a lead.`
|
|
681
|
+
}[v];
|
|
682
|
+
const evidence = {
|
|
683
|
+
reproduced: `confirmed by Sentinel's adversarial verifier${f.reverified ? ", re-verified by a second independent pass" : ""}, then reproduced in ${sandbox}: ${ran}.${f.reverified ? "" : " Not re-verified yet."}`,
|
|
684
|
+
notrepro: `confirmed by Sentinel's adversarial verifier. The PoC ran in ${sandbox} and did not reproduce it (${ran}). Verified by analysis only.`,
|
|
685
|
+
analysis: "confirmed by Sentinel's adversarial verifier. No PoC result yet.",
|
|
686
|
+
lead: "found by an auditor, not yet confirmed by the verifier. Treat it as a lead."
|
|
687
|
+
}[v];
|
|
688
|
+
const runStep = poc ? v === "notrepro" ? `2. Save the PoC below as ${tick(poc.file)} and run it. Show the output, compare it with Sentinel's run below, and say whether it reproduces in this codebase.` : v === "reproduced" ? `2. Save the PoC below as ${tick(poc.file)} and run it. Show the output and say whether it reproduces.` : `2. Save the PoC below as ${tick(poc.file)} and run it. Show the output and say whether it reproduces. If its setup doesn't match this codebase, adapt it and list what you changed.` : null;
|
|
689
|
+
const out = p?.output ? printed(p.output) : "";
|
|
690
|
+
return [
|
|
691
|
+
"# Task: investigate a Sentinel finding and decide on an approach",
|
|
692
|
+
"",
|
|
693
|
+
`${open}${poc ? " Simnet only; nothing touches mainnet." : ""}`,
|
|
694
|
+
"",
|
|
695
|
+
"## Start here",
|
|
696
|
+
`1. ${where(f, ctx)}`,
|
|
697
|
+
...runStep ? [runStep] : [],
|
|
698
|
+
`${runStep ? 3 : 2}. Only then discuss approaches that fit this codebase's design.`,
|
|
699
|
+
"",
|
|
700
|
+
...reportBackMd(ctx, poc ? "- Show the exact command you ran and its full output." : "- Say what you checked and how."),
|
|
701
|
+
...contextMd(f, ctx, kind),
|
|
702
|
+
f.class === "bug" ? "- Class: bug (not a trust assumption; no privileged role needed)" : `- Class: ${f.class}`,
|
|
703
|
+
`- Evidence: ${evidence}`,
|
|
704
|
+
"",
|
|
705
|
+
...v === "reproduced" && p?.trace ? movedMd(p.trace) : [],
|
|
706
|
+
...issueMd(f, true),
|
|
707
|
+
...poc ? [
|
|
708
|
+
v === "notrepro" ? `## PoC as it ran (save as ${tick(poc.file)})` : `## PoC (save as ${tick(poc.file)})`,
|
|
709
|
+
`${FENCE}typescript`,
|
|
710
|
+
poc.source,
|
|
711
|
+
FENCE,
|
|
712
|
+
"",
|
|
713
|
+
"## Run it",
|
|
714
|
+
`${FENCE}shell`,
|
|
715
|
+
bundle.runCommand ?? "",
|
|
716
|
+
FENCE,
|
|
717
|
+
...out && (v === "reproduced" || v === "notrepro") ? [
|
|
718
|
+
v === "reproduced" ? "Sentinel's output, before any change:" : "Sentinel's run printed:",
|
|
719
|
+
FENCE,
|
|
720
|
+
out,
|
|
721
|
+
FENCE
|
|
722
|
+
] : [],
|
|
723
|
+
""
|
|
724
|
+
] : [],
|
|
725
|
+
...f.recommendedAction ? ["## One direction to consider (not a verified patch)", f.recommendedAction, ""] : [],
|
|
726
|
+
...poc ? [
|
|
727
|
+
"## Confirm a change",
|
|
728
|
+
"- Re-run the PoC. With your change in place it should no longer succeed.",
|
|
729
|
+
"- Consider keeping the PoC as a regression test, with its assertions flipped to expect the failure."
|
|
730
|
+
] : [],
|
|
731
|
+
...tailMd(ctx, !!poc)
|
|
732
|
+
].join(`
|
|
733
|
+
`);
|
|
734
|
+
}
|
|
735
|
+
function agentBrief(f, ctx) {
|
|
736
|
+
const kind = f.kind ?? findingKind(f);
|
|
737
|
+
if (ctx.prelaunch && kind !== "refuted")
|
|
738
|
+
return prelaunchBrief(f, ctx, kind);
|
|
739
|
+
if (kind === "refuted")
|
|
740
|
+
return refutedBrief(f, ctx);
|
|
741
|
+
if (kind === "trust")
|
|
742
|
+
return trustBrief(f, ctx);
|
|
743
|
+
return bugBrief(f, ctx, kind);
|
|
744
|
+
}
|
|
745
|
+
|
|
442
746
|
// cli/sentinel.ts
|
|
443
747
|
var USAGE = `sentinel <command>
|
|
444
748
|
|
|
@@ -452,7 +756,8 @@ var USAGE = `sentinel <command>
|
|
|
452
756
|
|
|
453
757
|
plans your contracts: monitoring, worst evidence, plan id
|
|
454
758
|
plan <id> [--json] verdict and findings (--json: the raw v1 response)
|
|
455
|
-
finding <id> <n> [--json] one finding
|
|
759
|
+
finding <id> <n> [--poc] [--run] [--brief] [--json] one finding; --poc adds the PoC source, --run the command
|
|
760
|
+
to re-run it, --brief the agent brief (the finding page's "Copy agent brief")
|
|
456
761
|
runs <id> follow-up runs on a plan
|
|
457
762
|
watch <id> follow running runs until none are left
|
|
458
763
|
reverify <id> [--yes] check a plan's leads again (stronger second pass, $3); only starts with --yes
|
|
@@ -486,9 +791,9 @@ credential: SENTINEL_API_KEY, then --token <key>, then sentinel login. env: SENT
|
|
|
486
791
|
`;
|
|
487
792
|
var POLL_MS = 5000;
|
|
488
793
|
var VALUE_FLAGS = new Set(["--email", "--webhook", "--slack", "--contract"]);
|
|
489
|
-
var
|
|
794
|
+
var cell2 = (v, empty = "—") => v === null || v === undefined || v === "" ? empty : String(v);
|
|
490
795
|
var table = (rows) => {
|
|
491
|
-
const text = rows.map((r) => r.map((c) =>
|
|
796
|
+
const text = rows.map((r) => r.map((c) => cell2(c)));
|
|
492
797
|
const w = text[0]?.map((_, i) => Math.max(...text.map((r) => r[i].length))) ?? [];
|
|
493
798
|
return text.map((r) => r.map((c, i) => c.padEnd(w[i])).join(" ").trimEnd());
|
|
494
799
|
};
|
|
@@ -499,7 +804,7 @@ var phaseOf = (r) => {
|
|
|
499
804
|
var verdictLine = (p) => {
|
|
500
805
|
const r = p.report;
|
|
501
806
|
const head = `${p.contractId} ${p.status}`;
|
|
502
|
-
return r ? `${head} ${
|
|
807
|
+
return r ? `${head} ${cell2(r.severity)}/${cell2(r.class)} alert=${cell2(r.alertLevel)}` : head;
|
|
503
808
|
};
|
|
504
809
|
async function run(argv, io) {
|
|
505
810
|
const flags = [];
|
|
@@ -524,7 +829,17 @@ ${USAGE}` : USAGE);
|
|
|
524
829
|
}
|
|
525
830
|
const [cmd, id, n] = args;
|
|
526
831
|
const json = flags.includes("--json");
|
|
527
|
-
const known = new Set([
|
|
832
|
+
const known = new Set([
|
|
833
|
+
"--json",
|
|
834
|
+
"--yes",
|
|
835
|
+
"--help",
|
|
836
|
+
"--force",
|
|
837
|
+
"--with-token",
|
|
838
|
+
"--from-repo",
|
|
839
|
+
"--poc",
|
|
840
|
+
"--run",
|
|
841
|
+
"--brief"
|
|
842
|
+
]);
|
|
528
843
|
if (flags.some((f) => !known.has(f)))
|
|
529
844
|
return usage(`Unknown flag ${flags.find((f) => !known.has(f))}`);
|
|
530
845
|
if (flags.includes("--help") || !cmd || cmd === "help") {
|
|
@@ -564,8 +879,7 @@ ${USAGE}` : USAGE);
|
|
|
564
879
|
io.out("Exploit detail is hidden until you verify control in the web app.");
|
|
565
880
|
const rows = (p.report?.findings ?? []).map((f, i) => [
|
|
566
881
|
i,
|
|
567
|
-
f.
|
|
568
|
-
f.class,
|
|
882
|
+
f.kind ? KIND_LABEL[f.kind] : f.class,
|
|
569
883
|
f.verdict,
|
|
570
884
|
f.disposition,
|
|
571
885
|
f.title
|
|
@@ -581,14 +895,50 @@ ${USAGE}` : USAGE);
|
|
|
581
895
|
if (n === undefined || !/^\d+$/.test(n))
|
|
582
896
|
return usage("sentinel finding needs a plan id and a finding number");
|
|
583
897
|
const f = await io.client.finding(id, Number(n));
|
|
898
|
+
const wantPoc = flags.includes("--poc");
|
|
899
|
+
const wantRun = flags.includes("--run");
|
|
900
|
+
const wantBrief = flags.includes("--brief");
|
|
901
|
+
const p = wantPoc || wantRun || wantBrief ? await io.client.plan(id) : null;
|
|
902
|
+
const commit = p?.report?.target?.commit ?? null;
|
|
903
|
+
const bundle = p && (wantPoc || wantRun) ? handoffBundle(f, { contract: p.contractId, commit }) : null;
|
|
904
|
+
if (p && wantBrief) {
|
|
905
|
+
io.out(agentBrief(f, {
|
|
906
|
+
contract: p.contractId,
|
|
907
|
+
commit,
|
|
908
|
+
planId: id,
|
|
909
|
+
index: Number(n),
|
|
910
|
+
prelaunch: p.access === "prelaunch"
|
|
911
|
+
}));
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
584
914
|
if (json) {
|
|
585
|
-
io.out(JSON.stringify(f, null, 2));
|
|
915
|
+
io.out(JSON.stringify(bundle ? { finding: f, poc: bundle.poc, runCommand: bundle.runCommand } : f, null, 2));
|
|
586
916
|
return 0;
|
|
587
917
|
}
|
|
918
|
+
const kind = f.kind ? KIND_LABEL[f.kind] : f.class;
|
|
919
|
+
const alerts = f.alertLevel ? `alerts: ${f.alertLevel}` : null;
|
|
588
920
|
io.out(`${f.title}
|
|
589
|
-
${[
|
|
921
|
+
${[kind, f.verdict, f.disposition, alerts].filter(Boolean).map((v) => cell2(v)).join(" · ")}`);
|
|
590
922
|
if (f.headline)
|
|
591
923
|
io.out(f.headline);
|
|
924
|
+
if (wantPoc) {
|
|
925
|
+
if (bundle?.poc) {
|
|
926
|
+
io.out(`
|
|
927
|
+
# PoC · ${bundle.poc.file}`);
|
|
928
|
+
io.out(bundle.poc.source);
|
|
929
|
+
} else
|
|
930
|
+
io.out(`
|
|
931
|
+
No PoC is stored for this finding.`);
|
|
932
|
+
}
|
|
933
|
+
if (wantRun) {
|
|
934
|
+
if (bundle?.runCommand) {
|
|
935
|
+
io.out(`
|
|
936
|
+
# Run it yourself`);
|
|
937
|
+
io.out(bundle.runCommand);
|
|
938
|
+
} else
|
|
939
|
+
io.out(`
|
|
940
|
+
No run command: no PoC is stored for this finding.`);
|
|
941
|
+
}
|
|
592
942
|
return 0;
|
|
593
943
|
}
|
|
594
944
|
case "runs": {
|
|
@@ -808,7 +1158,7 @@ async function monitoring(cmd, pos, on, flags, values, io, usage) {
|
|
|
808
1158
|
return 0;
|
|
809
1159
|
}
|
|
810
1160
|
const r = await io.client.watchContract(first);
|
|
811
|
-
io.out("
|
|
1161
|
+
io.out("intents" in r ? `Monitoring ${r.contractId}. Manage it with plan ${r.planId}.` : `Sentinel is still making the plan (${r.planId}). Run this again in a few minutes.`);
|
|
812
1162
|
return 0;
|
|
813
1163
|
}
|
|
814
1164
|
if (!first)
|
|
@@ -825,21 +1175,16 @@ async function monitoring(cmd, pos, on, flags, values, io, usage) {
|
|
|
825
1175
|
const ctx = await watchContext(io.client, planId);
|
|
826
1176
|
if (cmd === "watches") {
|
|
827
1177
|
if (flags.includes("--json")) {
|
|
828
|
-
io.out(JSON.stringify({
|
|
1178
|
+
io.out(JSON.stringify({ intents: ctx.intents }, null, 2));
|
|
829
1179
|
return 0;
|
|
830
1180
|
}
|
|
831
1181
|
if (ctx.access === "claimed") {
|
|
832
1182
|
io.out("Exploit detail is hidden until you verify control in the web app.");
|
|
833
1183
|
}
|
|
834
|
-
for (const
|
|
835
|
-
const muted =
|
|
836
|
-
io.out([
|
|
837
|
-
|
|
838
|
-
[...new Set(g.watches.map((w) => kindTag(w.rule)))].join("+"),
|
|
839
|
-
subjectStatus(g.watches),
|
|
840
|
-
muted ? `muted until ${muted}` : ""
|
|
841
|
-
].filter(Boolean).join(" "));
|
|
842
|
-
for (const w of g.watches)
|
|
1184
|
+
for (const i of ctx.intents) {
|
|
1185
|
+
const muted = i.rows.map((w) => mutedUntilOf(w)).find(Boolean);
|
|
1186
|
+
io.out([i.title, i.tag, i.on ? "on" : "off", intentLine(i), muted ? `muted until ${muted}` : ""].filter(Boolean).join(" "));
|
|
1187
|
+
for (const w of i.rows)
|
|
843
1188
|
io.out(` ${w.status.padEnd(14)}${ruleSummary(w.rule, w.units)}`);
|
|
844
1189
|
}
|
|
845
1190
|
return 0;
|
package/dist/index.js
CHANGED
|
@@ -79,10 +79,14 @@ class SentinelError extends Error {
|
|
|
79
79
|
this.name = "SentinelError";
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
var
|
|
82
|
+
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
83
|
+
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
84
|
+
var CONTROL_NAME = "Control changes";
|
|
83
85
|
function subjectName(s, units) {
|
|
84
86
|
if (s.kind === "fn")
|
|
85
87
|
return s.fn;
|
|
88
|
+
if (s.kind === "group")
|
|
89
|
+
return CONTROL_NAME;
|
|
86
90
|
if (units?.symbol)
|
|
87
91
|
return units.symbol;
|
|
88
92
|
if (s.asset.toLowerCase() === "stx")
|
|
@@ -114,7 +118,7 @@ function resolveSubject(watches, name) {
|
|
|
114
118
|
const hits = groups.filter((g) => {
|
|
115
119
|
const s = g.watches[0].subject;
|
|
116
120
|
const asset = s.kind === "asset" ? s.asset.toLowerCase() : null;
|
|
117
|
-
return g.name.toLowerCase() === q || g.key.toLowerCase() === q || asset === q || asset !== null && asset.split("::")[1] === q;
|
|
121
|
+
return g.name.toLowerCase() === q || g.key.toLowerCase() === q || s.kind === "group" && q === s.group || asset === q || asset !== null && asset.split("::")[1] === q;
|
|
118
122
|
});
|
|
119
123
|
if (hits.length === 1)
|
|
120
124
|
return hits[0];
|
|
@@ -181,6 +185,17 @@ function kindTag(rule) {
|
|
|
181
185
|
return rule.triggerClass === "governance.proposal_submitted" ? "governance" : "upgrade";
|
|
182
186
|
}
|
|
183
187
|
}
|
|
188
|
+
function intentLine(i) {
|
|
189
|
+
const t = i.threshold;
|
|
190
|
+
if (i.kind !== "money_out" || !t)
|
|
191
|
+
return i.summary;
|
|
192
|
+
const units = i.units ?? null;
|
|
193
|
+
if (t.amount === null) {
|
|
194
|
+
const sug = t.suggested ? ` (suggested ${amountText(t.suggested.amount, units)}, ${t.suggested.basis})` : "";
|
|
195
|
+
return `no threshold yet: every outflow alerts${sug}`;
|
|
196
|
+
}
|
|
197
|
+
return `${amountText(t.amount, units)} or more in one tx · ${t.basis}`;
|
|
198
|
+
}
|
|
184
199
|
function subjectStatus(ws) {
|
|
185
200
|
const st = ws.map((w) => w.status);
|
|
186
201
|
if (st.includes("live"))
|
|
@@ -250,11 +265,13 @@ var HOUR_MS = 3600000;
|
|
|
250
265
|
var muteEnd = (hours) => new Date(Date.now() + Math.min(hours * HOUR_MS, 7 * 24 * HOUR_MS - 60000)).toISOString();
|
|
251
266
|
async function watchContext(client, planId) {
|
|
252
267
|
const plan = await client.plan(planId);
|
|
268
|
+
const { intents } = await client.watches(planId);
|
|
253
269
|
return {
|
|
254
270
|
planId,
|
|
255
271
|
contractId: plan.contractId,
|
|
256
272
|
access: plan.access,
|
|
257
|
-
|
|
273
|
+
intents,
|
|
274
|
+
watches: rowsOf(intents)
|
|
258
275
|
};
|
|
259
276
|
}
|
|
260
277
|
async function savedMonitor(client, planId) {
|
|
@@ -272,6 +289,7 @@ async function planOfContract(client, contractId) {
|
|
|
272
289
|
return hit.planId;
|
|
273
290
|
}
|
|
274
291
|
export {
|
|
292
|
+
CONTROL_NAME,
|
|
275
293
|
CREDITS_API_URL,
|
|
276
294
|
DEFAULT_BASE_URL,
|
|
277
295
|
NOT_LOGGED_IN,
|
|
@@ -282,6 +300,7 @@ export {
|
|
|
282
300
|
createClient,
|
|
283
301
|
creditsUrl,
|
|
284
302
|
groupSubjects,
|
|
303
|
+
intentLine,
|
|
285
304
|
isMutableRule,
|
|
286
305
|
kindTag,
|
|
287
306
|
muteEnd,
|
|
@@ -291,6 +310,7 @@ export {
|
|
|
291
310
|
readTokenFile,
|
|
292
311
|
resolveSubject,
|
|
293
312
|
resolveToken,
|
|
313
|
+
rowsOf,
|
|
294
314
|
ruleSummary,
|
|
295
315
|
savedChoices,
|
|
296
316
|
savedMonitor,
|
package/dist/kind.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a finding is, for the people watching the contract (docs/product/system.md § "How findings are
|
|
3
|
+
* shown"). The user sees a kind, never an audit severity; severity lives on as the alert level of the
|
|
4
|
+
* finding's watch. Dependency-free and the ONE derivation: the API stamps `kind` and `alertLevel` on every
|
|
5
|
+
* finding it returns, so the page, the CLI and the MCP tools never derive it differently.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* exploitable a bug, verified, independently RE-verified, and reproduced green in the sandbox. Rare.
|
|
9
|
+
* trust a power held by design (centralization / trust assumption, accepted or not)
|
|
10
|
+
* worth_a_look everything else not refuted: analysis only, not reproduced yet, a lead, or reproduced
|
|
11
|
+
* but not re-verified yet
|
|
12
|
+
* refuted refuted on re-check
|
|
13
|
+
*/
|
|
14
|
+
export type FindingKind = "exploitable" | "trust" | "worth_a_look" | "refuted";
|
|
15
|
+
/** How a finding's watch pages: urgent, or normal. */
|
|
16
|
+
export type AlertLevel = "urgent" | "normal";
|
|
17
|
+
/** A watched function's dominant consequence (engine/watch-select/facts.ts `Consequence`, copied: no deps). */
|
|
18
|
+
export type Consequence = "authority" | "code" | "value" | "param" | "none";
|
|
19
|
+
/** The finding fields the kind reads. The app's ReportFinding and the client's Finding both satisfy it. */
|
|
20
|
+
export type KindInput = {
|
|
21
|
+
class: string;
|
|
22
|
+
verdict: string;
|
|
23
|
+
disposition: string;
|
|
24
|
+
/** The PoC status after the credibility gates (a later reproduction's, once one lands). */
|
|
25
|
+
poc?: string;
|
|
26
|
+
/** A second, independent verifier pass confirmed it. Absent: not re-verified (yet). */
|
|
27
|
+
reverified?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** Reproduced green in the sandbox AND past the credibility gates. A PoC that printed "reproduced" but
|
|
30
|
+
* that Gate 3 downgraded (an incomplete freeze proof) is `pending`, so it is not green here. */
|
|
31
|
+
export declare const isGreen: (f: Pick<KindInput, "poc">) => boolean;
|
|
32
|
+
export declare function findingKind(f: KindInput): FindingKind;
|
|
33
|
+
/**
|
|
34
|
+
* The alert level of a finding's watch. A trust assumption pages urgently only when its function changes
|
|
35
|
+
* who controls the contract, swaps code it calls, or moves value; an unknown consequence is `normal`
|
|
36
|
+
* (the caller logs it). A refuted finding has no watch.
|
|
37
|
+
*/
|
|
38
|
+
export declare function alertLevelFor(kind: Exclude<FindingKind, "refuted">, consequence?: Consequence | null): AlertLevel;
|
|
39
|
+
/** A kind as every surface names it (the page's chip, the plan list, the CLI, the agent brief). */
|
|
40
|
+
export declare const KIND_LABEL: Record<FindingKind, string>;
|
|
41
|
+
/** Plan-list order: Exploitable first, then trust assumptions, then worth a look; refuted last. */
|
|
42
|
+
export declare const KIND_ORDER: FindingKind[];
|
package/dist/mcp.js
CHANGED
|
@@ -12659,10 +12659,14 @@ class SentinelError extends Error {
|
|
|
12659
12659
|
this.name = "SentinelError";
|
|
12660
12660
|
}
|
|
12661
12661
|
}
|
|
12662
|
-
var
|
|
12662
|
+
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
12663
|
+
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
12664
|
+
var CONTROL_NAME = "Control changes";
|
|
12663
12665
|
function subjectName(s, units) {
|
|
12664
12666
|
if (s.kind === "fn")
|
|
12665
12667
|
return s.fn;
|
|
12668
|
+
if (s.kind === "group")
|
|
12669
|
+
return CONTROL_NAME;
|
|
12666
12670
|
if (units?.symbol)
|
|
12667
12671
|
return units.symbol;
|
|
12668
12672
|
if (s.asset.toLowerCase() === "stx")
|
|
@@ -12694,7 +12698,7 @@ function resolveSubject(watches, name) {
|
|
|
12694
12698
|
const hits = groups.filter((g) => {
|
|
12695
12699
|
const s = g.watches[0].subject;
|
|
12696
12700
|
const asset = s.kind === "asset" ? s.asset.toLowerCase() : null;
|
|
12697
|
-
return g.name.toLowerCase() === q || g.key.toLowerCase() === q || asset === q || asset !== null && asset.split("::")[1] === q;
|
|
12701
|
+
return g.name.toLowerCase() === q || g.key.toLowerCase() === q || s.kind === "group" && q === s.group || asset === q || asset !== null && asset.split("::")[1] === q;
|
|
12698
12702
|
});
|
|
12699
12703
|
if (hits.length === 1)
|
|
12700
12704
|
return hits[0];
|
|
@@ -12746,20 +12750,16 @@ function ruleSummary(rule, units) {
|
|
|
12746
12750
|
}
|
|
12747
12751
|
}
|
|
12748
12752
|
}
|
|
12749
|
-
function
|
|
12750
|
-
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12754
|
-
|
|
12755
|
-
|
|
12756
|
-
|
|
12757
|
-
case "callerAllowlist":
|
|
12758
|
-
case "actor":
|
|
12759
|
-
return "who-acts";
|
|
12760
|
-
case "audit":
|
|
12761
|
-
return rule.triggerClass === "governance.proposal_submitted" ? "governance" : "upgrade";
|
|
12753
|
+
function intentLine(i) {
|
|
12754
|
+
const t = i.threshold;
|
|
12755
|
+
if (i.kind !== "money_out" || !t)
|
|
12756
|
+
return i.summary;
|
|
12757
|
+
const units = i.units ?? null;
|
|
12758
|
+
if (t.amount === null) {
|
|
12759
|
+
const sug = t.suggested ? ` (suggested ${amountText(t.suggested.amount, units)}, ${t.suggested.basis})` : "";
|
|
12760
|
+
return `no threshold yet: every outflow alerts${sug}`;
|
|
12762
12761
|
}
|
|
12762
|
+
return `${amountText(t.amount, units)} or more in one tx · ${t.basis}`;
|
|
12763
12763
|
}
|
|
12764
12764
|
function subjectStatus(ws) {
|
|
12765
12765
|
const st = ws.map((w) => w.status);
|
|
@@ -12830,11 +12830,13 @@ var HOUR_MS = 3600000;
|
|
|
12830
12830
|
var muteEnd = (hours) => new Date(Date.now() + Math.min(hours * HOUR_MS, 7 * 24 * HOUR_MS - 60000)).toISOString();
|
|
12831
12831
|
async function watchContext(client, planId) {
|
|
12832
12832
|
const plan = await client.plan(planId);
|
|
12833
|
+
const { intents } = await client.watches(planId);
|
|
12833
12834
|
return {
|
|
12834
12835
|
planId,
|
|
12835
12836
|
contractId: plan.contractId,
|
|
12836
12837
|
access: plan.access,
|
|
12837
|
-
|
|
12838
|
+
intents,
|
|
12839
|
+
watches: rowsOf(intents)
|
|
12838
12840
|
};
|
|
12839
12841
|
}
|
|
12840
12842
|
async function savedMonitor(client, planId) {
|
|
@@ -20279,6 +20281,316 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
20279
20281
|
}
|
|
20280
20282
|
};
|
|
20281
20283
|
|
|
20284
|
+
// client/kind.ts
|
|
20285
|
+
var isGreen = (f) => f.poc === "green";
|
|
20286
|
+
function findingKind(f) {
|
|
20287
|
+
if (f.disposition === "refuted" || f.verdict === "refuted")
|
|
20288
|
+
return "refuted";
|
|
20289
|
+
if (f.class === "centralization" || f.disposition === "waived")
|
|
20290
|
+
return "trust";
|
|
20291
|
+
if (f.class === "bug" && f.verdict === "confirmed" && f.reverified === true && isGreen(f)) {
|
|
20292
|
+
return "exploitable";
|
|
20293
|
+
}
|
|
20294
|
+
return "worth_a_look";
|
|
20295
|
+
}
|
|
20296
|
+
var URGENT_CONSEQUENCES = new Set(["authority", "code", "value"]);
|
|
20297
|
+
|
|
20298
|
+
// client/labels.ts
|
|
20299
|
+
var isPrelaunchKey = (id) => id.startsWith("github:") || id.startsWith("gist:");
|
|
20300
|
+
function contractName(id) {
|
|
20301
|
+
if (isPrelaunchKey(id))
|
|
20302
|
+
return id.split(":").pop()?.split("/").pop()?.replace(/\.clar$/, "") ?? id;
|
|
20303
|
+
return id.split(".")[1] ?? id;
|
|
20304
|
+
}
|
|
20305
|
+
|
|
20306
|
+
// client/handoff.ts
|
|
20307
|
+
var DEFAULT_FILE = "sentinel-poc.ts";
|
|
20308
|
+
var pocFileName = (f) => f.proof?.file ?? DEFAULT_FILE;
|
|
20309
|
+
function runCommand(opts) {
|
|
20310
|
+
const lines = [`# save the PoC above as ${opts.file}, then in your own checkout:`];
|
|
20311
|
+
if (opts.commit)
|
|
20312
|
+
lines.push(`git checkout ${opts.commit} # the commit Sentinel reviewed`);
|
|
20313
|
+
const where = opts.substrate === "fork" ? "needs a Stacks RPC" : "airgapped, no network";
|
|
20314
|
+
lines.push(`bun run ${opts.file} # @stacks/clarinet-sdk — ${where}`);
|
|
20315
|
+
return lines.join(`
|
|
20316
|
+
`);
|
|
20317
|
+
}
|
|
20318
|
+
function handoffBundle(f, ctx) {
|
|
20319
|
+
const proof = f.proof;
|
|
20320
|
+
const file = pocFileName(f);
|
|
20321
|
+
const poc = proof?.script ? {
|
|
20322
|
+
file,
|
|
20323
|
+
substrate: proof.substrate ?? null,
|
|
20324
|
+
reproduced: !!proof.reproduced,
|
|
20325
|
+
exitCode: proof.exitCode ?? null,
|
|
20326
|
+
source: proof.script
|
|
20327
|
+
} : null;
|
|
20328
|
+
return {
|
|
20329
|
+
contract: ctx.contract,
|
|
20330
|
+
fn: f.targetFn ?? null,
|
|
20331
|
+
commit: ctx.commit ?? null,
|
|
20332
|
+
poc,
|
|
20333
|
+
runCommand: poc ? runCommand({ commit: ctx.commit, file, substrate: poc.substrate }) : null
|
|
20334
|
+
};
|
|
20335
|
+
}
|
|
20336
|
+
function assertionsOf(output) {
|
|
20337
|
+
const ratio = output.match(/(\d+)\s*\/\s*(\d+)\s*assertions?/i);
|
|
20338
|
+
if (ratio)
|
|
20339
|
+
return `${ratio[1]}/${ratio[2]}`;
|
|
20340
|
+
const passed = output.match(/(\d+)\s*assertions?\s*passed/i);
|
|
20341
|
+
return passed ? `${passed[1]} passed` : null;
|
|
20342
|
+
}
|
|
20343
|
+
var FENCE = "```";
|
|
20344
|
+
var FOOTER = "Sentinel's findings are a starting point for your own review, not an audit sign-off.";
|
|
20345
|
+
var KIND_LINE = {
|
|
20346
|
+
exploitable: "Exploitable (verified, re-verified, reproduced in the sandbox)",
|
|
20347
|
+
trust: "Trust assumption (watched: Sentinel alerts when it's used)",
|
|
20348
|
+
worth_a_look: "Worth a look (not proven exploitable)",
|
|
20349
|
+
refuted: "Refuted"
|
|
20350
|
+
};
|
|
20351
|
+
var tick = (s) => `\`${s}\``;
|
|
20352
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
20353
|
+
var num = (n) => n.toLocaleString("en-US");
|
|
20354
|
+
function printed(output) {
|
|
20355
|
+
const lines = output.replace(/\[SENTINEL-POC-TRACE\][\s\S]*?\[\/SENTINEL-POC-TRACE\]/g, "").split(`
|
|
20356
|
+
`).filter((l) => l.trim());
|
|
20357
|
+
return lines.slice(-40).join(`
|
|
20358
|
+
`);
|
|
20359
|
+
}
|
|
20360
|
+
function movedMd(trace) {
|
|
20361
|
+
const keys = Object.keys(trace.series ?? {}).filter((k) => trace.steps.some((s) => s.values?.[k] !== undefined));
|
|
20362
|
+
const name = (k) => trace.series?.[k] ?? k;
|
|
20363
|
+
const unit = trace.unit ? ` ${trace.unit}` : "";
|
|
20364
|
+
const changes = (i) => {
|
|
20365
|
+
const prev = trace.steps[i - 1]?.values;
|
|
20366
|
+
const now = trace.steps[i].values;
|
|
20367
|
+
if (!prev || !now)
|
|
20368
|
+
return [];
|
|
20369
|
+
return keys.filter((k) => now[k] !== undefined && prev[k] !== undefined && now[k] !== prev[k]).map((k) => `${name(k)} ${num(prev[k])} → ${num(now[k])}${unit}`);
|
|
20370
|
+
};
|
|
20371
|
+
const first = trace.steps.findIndex((_, i) => changes(i).length > 0);
|
|
20372
|
+
const rows = trace.steps.map((s, i) => {
|
|
20373
|
+
const call = [s.call && tick(s.call), s.result && `→ ${tick(s.result)}`].filter(Boolean).join(" ");
|
|
20374
|
+
const start = keys.filter((k) => s.values?.[k] !== undefined).map((k) => `${name(k)} ${num(s.values?.[k])}${unit}`).join(" · ");
|
|
20375
|
+
const change = i === 0 ? start || "start" : changes(i).join(" · ") || "no change";
|
|
20376
|
+
return `| ${i + 1} ${cell(s.label)} | ${cell(call || "—")} | ${cell(change)} |`;
|
|
20377
|
+
});
|
|
20378
|
+
return [
|
|
20379
|
+
"## What the PoC moved",
|
|
20380
|
+
first > 0 ? `**${changes(first).join("; ")} at step ${first + 1} (${trace.steps[first].label}).**` : "No tracked value changed across the steps.",
|
|
20381
|
+
"",
|
|
20382
|
+
"| Step | Call | Value change |",
|
|
20383
|
+
"|---|---|---|",
|
|
20384
|
+
...rows,
|
|
20385
|
+
""
|
|
20386
|
+
];
|
|
20387
|
+
}
|
|
20388
|
+
function variantOf(f) {
|
|
20389
|
+
if (f.proof?.reproduced)
|
|
20390
|
+
return "reproduced";
|
|
20391
|
+
if (f.proof && !f.proof.unavailable && !f.proof.errored && f.proof.script)
|
|
20392
|
+
return "notrepro";
|
|
20393
|
+
if (f.verdict !== "confirmed")
|
|
20394
|
+
return "lead";
|
|
20395
|
+
return "analysis";
|
|
20396
|
+
}
|
|
20397
|
+
function where(f, ctx) {
|
|
20398
|
+
const at = ctx.commit ? ` at commit ${tick(ctx.commit.slice(0, 7))}` : "";
|
|
20399
|
+
return f.targetFn ? `Open ${tick(f.targetFn)} in ${tick(contractName(ctx.contract))}${at} and show the function with ~10 lines of context. Point at the lines this finding is about and explain in your own words what they do.` : `Find the code this finding describes in ${tick(contractName(ctx.contract))}${at} and show it with ~10 lines of context. Explain in your own words what it does.`;
|
|
20400
|
+
}
|
|
20401
|
+
function contextMd(f, ctx, kind) {
|
|
20402
|
+
return [
|
|
20403
|
+
"## Context",
|
|
20404
|
+
`- Contract: ${tick(ctx.contract)}`,
|
|
20405
|
+
...ctx.commit ? [`- Commit reviewed: ${tick(ctx.commit.slice(0, 7))}`] : [],
|
|
20406
|
+
...f.targetFn ? [`- Function: ${tick(f.targetFn)}`] : [],
|
|
20407
|
+
`- Kind: ${KIND_LINE[kind]}`
|
|
20408
|
+
];
|
|
20409
|
+
}
|
|
20410
|
+
function reportBackMd(ctx, extra) {
|
|
20411
|
+
return [
|
|
20412
|
+
"## How to report back",
|
|
20413
|
+
`- Cite every claim with ${tick("file:line")}${ctx.commit ? ` at commit ${tick(ctx.commit.slice(0, 7))}` : ""}.`,
|
|
20414
|
+
"- Show the code you discuss, quoted from the repo, not paraphrased.",
|
|
20415
|
+
extra,
|
|
20416
|
+
"- Keep two sections: **Facts** (what the code and the run show) and **Assessment** (your conclusions, each with a confidence level). Don't mix them.",
|
|
20417
|
+
"- Don't change contract code until the team has chosen an approach.",
|
|
20418
|
+
""
|
|
20419
|
+
];
|
|
20420
|
+
}
|
|
20421
|
+
function tailMd(ctx, withPoc) {
|
|
20422
|
+
return [
|
|
20423
|
+
"",
|
|
20424
|
+
"## Pull it again",
|
|
20425
|
+
`${FENCE}shell`,
|
|
20426
|
+
`npx @secondlayer/sentinel finding ${ctx.planId} ${ctx.index}${withPoc ? " --poc --run" : ""}`,
|
|
20427
|
+
FENCE,
|
|
20428
|
+
`MCP: ${tick(`get_finding { planId: "${ctx.planId}", index: ${ctx.index} }`)} returns this finding as JSON${withPoc ? ", the PoC source and the run command" : ""}.`,
|
|
20429
|
+
"",
|
|
20430
|
+
"---",
|
|
20431
|
+
FOOTER
|
|
20432
|
+
];
|
|
20433
|
+
}
|
|
20434
|
+
function issueMd(f, withCondition) {
|
|
20435
|
+
return [
|
|
20436
|
+
"## The issue",
|
|
20437
|
+
f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
|
|
20438
|
+
...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
|
|
20439
|
+
...withCondition && f.precondition ? ["", `**Exploitable when:** ${f.precondition}`] : [],
|
|
20440
|
+
""
|
|
20441
|
+
];
|
|
20442
|
+
}
|
|
20443
|
+
function prelaunchBrief(f, ctx, kind) {
|
|
20444
|
+
const kindLine = kind === "exploitable" ? "Exploitable · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
|
|
20445
|
+
return [
|
|
20446
|
+
"# Task: review a Sentinel finding on pre-launch code",
|
|
20447
|
+
"",
|
|
20448
|
+
`Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. PoCs and exploit detail open once Sentinel confirms this code isn't already live on mainnet; possible approaches are shown now, so this brief carries the finding and a direction, not the PoC. Look at the code before launch: the watch arms at launch, but it isn't a substitute for the review.`,
|
|
20449
|
+
"",
|
|
20450
|
+
"## Start here",
|
|
20451
|
+
`1. ${where(f, ctx)}`,
|
|
20452
|
+
"2. Only then discuss approaches that fit this codebase's design.",
|
|
20453
|
+
"",
|
|
20454
|
+
"## How to report back",
|
|
20455
|
+
`- Cite every claim with ${tick("file:line")}${ctx.commit ? ` at commit ${tick(ctx.commit.slice(0, 7))}` : ""}.`,
|
|
20456
|
+
"- Keep **Facts** and **Assessment** apart, with a confidence level on each conclusion.",
|
|
20457
|
+
"",
|
|
20458
|
+
"## Context",
|
|
20459
|
+
`- Contract: ${tick(contractName(ctx.contract))} (pre-launch)${ctx.commit ? `, commit ${tick(ctx.commit.slice(0, 7))}` : ""}`,
|
|
20460
|
+
`- ${f.targetFn ? `Function: ${tick(f.targetFn)} · ` : ""}Kind: ${kindLine}`,
|
|
20461
|
+
"",
|
|
20462
|
+
...issueMd(f, false),
|
|
20463
|
+
...f.recommendedAction ? ["## One direction to consider (not a verified patch)", f.recommendedAction, ""] : [],
|
|
20464
|
+
"---",
|
|
20465
|
+
FOOTER
|
|
20466
|
+
].join(`
|
|
20467
|
+
`);
|
|
20468
|
+
}
|
|
20469
|
+
function trustBrief(f, ctx) {
|
|
20470
|
+
return [
|
|
20471
|
+
"# Task: review a trust assumption and decide whether to reduce it",
|
|
20472
|
+
"",
|
|
20473
|
+
`Sentinel found a power held by design${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. It's a trust assumption, not a bug, so there is nothing to reproduce. Review it and decide whether the trust it asks of your users fits your design.`,
|
|
20474
|
+
"",
|
|
20475
|
+
"## Start here",
|
|
20476
|
+
`1. ${where(f, ctx)}`,
|
|
20477
|
+
`2. Find every place that reads what ${f.targetFn ? tick(f.targetFn) : "this power"} changes, and show each one, so the reach of this power is clear.`,
|
|
20478
|
+
"3. Only then discuss options that fit this codebase's design.",
|
|
20479
|
+
"",
|
|
20480
|
+
...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write an exploit for it."),
|
|
20481
|
+
...contextMd(f, ctx, "trust"),
|
|
20482
|
+
"- Class: trust assumption (a power held by design)",
|
|
20483
|
+
"- Evidence: checked by Sentinel's adversarial verifier. No PoC; nothing to run.",
|
|
20484
|
+
"",
|
|
20485
|
+
"## What it is",
|
|
20486
|
+
f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
|
|
20487
|
+
...f.powers?.length ? [
|
|
20488
|
+
"",
|
|
20489
|
+
"**Powers:**",
|
|
20490
|
+
...f.powers.map((p) => `- ${p.holder} can ${p.can}${p.fn ? ` (${tick(p.fn)})` : ""}`)
|
|
20491
|
+
] : [],
|
|
20492
|
+
...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
|
|
20493
|
+
...f.precondition ? ["", `**Usable when:** ${f.precondition}`] : [],
|
|
20494
|
+
"",
|
|
20495
|
+
...f.recommendedAction ? ["## Options to consider (not a patch)", `- ${f.recommendedAction}`, ""] : [],
|
|
20496
|
+
"## If you change it",
|
|
20497
|
+
"- A test for the new behavior.",
|
|
20498
|
+
"- A test that an account without the role is still refused.",
|
|
20499
|
+
...tailMd(ctx, false)
|
|
20500
|
+
].join(`
|
|
20501
|
+
`);
|
|
20502
|
+
}
|
|
20503
|
+
function refutedBrief(f, ctx) {
|
|
20504
|
+
return [
|
|
20505
|
+
"# Note: a Sentinel finding refuted on re-check",
|
|
20506
|
+
"",
|
|
20507
|
+
`Sentinel's verifier re-examined this${f.targetFn ? ` (${tick(f.targetFn)})` : ""} and showed it can't happen: ${f.title}. It's kept for the record and isn't watched.`,
|
|
20508
|
+
"",
|
|
20509
|
+
...contextMd(f, ctx, "refuted"),
|
|
20510
|
+
...tailMd(ctx, false)
|
|
20511
|
+
].join(`
|
|
20512
|
+
`);
|
|
20513
|
+
}
|
|
20514
|
+
function bugBrief(f, ctx, kind) {
|
|
20515
|
+
const v = variantOf(f);
|
|
20516
|
+
const bundle = handoffBundle(f, { contract: ctx.contract, commit: ctx.commit });
|
|
20517
|
+
const poc = bundle.poc;
|
|
20518
|
+
const p = f.proof;
|
|
20519
|
+
const asserted = p?.output ? assertionsOf(p.output) : null;
|
|
20520
|
+
const counted = asserted?.includes("/") ? `${asserted} assertions, ` : asserted ? `${asserted.replace(" passed", "")} assertions passed, ` : "";
|
|
20521
|
+
const ran = `${counted}exit ${p?.exitCode ?? "?"}`;
|
|
20522
|
+
const sandbox = p?.substrate === "fork" ? "a mainnet-fork sandbox" : `an airgapped sandbox (${tick("--network none")})`;
|
|
20523
|
+
const what = `Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}`;
|
|
20524
|
+
const open = {
|
|
20525
|
+
reproduced: `${what}. It reproduced with the PoC below. Review the affected code, choose an approach that fits your design, then confirm the PoC no longer succeeds.`,
|
|
20526
|
+
notrepro: `${what}. Its verifier confirmed it by analysis, but the PoC below ran and did not reproduce it. Review the affected code and the run, and decide whether it applies to your deployment.`,
|
|
20527
|
+
analysis: `${what}. Its verifier confirmed it by analysis; no PoC result yet. ${poc ? "Run the PoC below in your own tooling, then review" : "Review"} the affected code and choose an approach that fits your design.`,
|
|
20528
|
+
lead: `An auditor flagged an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. The verifier hasn't confirmed it, so treat it as a lead.`
|
|
20529
|
+
}[v];
|
|
20530
|
+
const evidence = {
|
|
20531
|
+
reproduced: `confirmed by Sentinel's adversarial verifier${f.reverified ? ", re-verified by a second independent pass" : ""}, then reproduced in ${sandbox}: ${ran}.${f.reverified ? "" : " Not re-verified yet."}`,
|
|
20532
|
+
notrepro: `confirmed by Sentinel's adversarial verifier. The PoC ran in ${sandbox} and did not reproduce it (${ran}). Verified by analysis only.`,
|
|
20533
|
+
analysis: "confirmed by Sentinel's adversarial verifier. No PoC result yet.",
|
|
20534
|
+
lead: "found by an auditor, not yet confirmed by the verifier. Treat it as a lead."
|
|
20535
|
+
}[v];
|
|
20536
|
+
const runStep = poc ? v === "notrepro" ? `2. Save the PoC below as ${tick(poc.file)} and run it. Show the output, compare it with Sentinel's run below, and say whether it reproduces in this codebase.` : v === "reproduced" ? `2. Save the PoC below as ${tick(poc.file)} and run it. Show the output and say whether it reproduces.` : `2. Save the PoC below as ${tick(poc.file)} and run it. Show the output and say whether it reproduces. If its setup doesn't match this codebase, adapt it and list what you changed.` : null;
|
|
20537
|
+
const out = p?.output ? printed(p.output) : "";
|
|
20538
|
+
return [
|
|
20539
|
+
"# Task: investigate a Sentinel finding and decide on an approach",
|
|
20540
|
+
"",
|
|
20541
|
+
`${open}${poc ? " Simnet only; nothing touches mainnet." : ""}`,
|
|
20542
|
+
"",
|
|
20543
|
+
"## Start here",
|
|
20544
|
+
`1. ${where(f, ctx)}`,
|
|
20545
|
+
...runStep ? [runStep] : [],
|
|
20546
|
+
`${runStep ? 3 : 2}. Only then discuss approaches that fit this codebase's design.`,
|
|
20547
|
+
"",
|
|
20548
|
+
...reportBackMd(ctx, poc ? "- Show the exact command you ran and its full output." : "- Say what you checked and how."),
|
|
20549
|
+
...contextMd(f, ctx, kind),
|
|
20550
|
+
f.class === "bug" ? "- Class: bug (not a trust assumption; no privileged role needed)" : `- Class: ${f.class}`,
|
|
20551
|
+
`- Evidence: ${evidence}`,
|
|
20552
|
+
"",
|
|
20553
|
+
...v === "reproduced" && p?.trace ? movedMd(p.trace) : [],
|
|
20554
|
+
...issueMd(f, true),
|
|
20555
|
+
...poc ? [
|
|
20556
|
+
v === "notrepro" ? `## PoC as it ran (save as ${tick(poc.file)})` : `## PoC (save as ${tick(poc.file)})`,
|
|
20557
|
+
`${FENCE}typescript`,
|
|
20558
|
+
poc.source,
|
|
20559
|
+
FENCE,
|
|
20560
|
+
"",
|
|
20561
|
+
"## Run it",
|
|
20562
|
+
`${FENCE}shell`,
|
|
20563
|
+
bundle.runCommand ?? "",
|
|
20564
|
+
FENCE,
|
|
20565
|
+
...out && (v === "reproduced" || v === "notrepro") ? [
|
|
20566
|
+
v === "reproduced" ? "Sentinel's output, before any change:" : "Sentinel's run printed:",
|
|
20567
|
+
FENCE,
|
|
20568
|
+
out,
|
|
20569
|
+
FENCE
|
|
20570
|
+
] : [],
|
|
20571
|
+
""
|
|
20572
|
+
] : [],
|
|
20573
|
+
...f.recommendedAction ? ["## One direction to consider (not a verified patch)", f.recommendedAction, ""] : [],
|
|
20574
|
+
...poc ? [
|
|
20575
|
+
"## Confirm a change",
|
|
20576
|
+
"- Re-run the PoC. With your change in place it should no longer succeed.",
|
|
20577
|
+
"- Consider keeping the PoC as a regression test, with its assertions flipped to expect the failure."
|
|
20578
|
+
] : [],
|
|
20579
|
+
...tailMd(ctx, !!poc)
|
|
20580
|
+
].join(`
|
|
20581
|
+
`);
|
|
20582
|
+
}
|
|
20583
|
+
function agentBrief(f, ctx) {
|
|
20584
|
+
const kind = f.kind ?? findingKind(f);
|
|
20585
|
+
if (ctx.prelaunch && kind !== "refuted")
|
|
20586
|
+
return prelaunchBrief(f, ctx, kind);
|
|
20587
|
+
if (kind === "refuted")
|
|
20588
|
+
return refutedBrief(f, ctx);
|
|
20589
|
+
if (kind === "trust")
|
|
20590
|
+
return trustBrief(f, ctx);
|
|
20591
|
+
return bugBrief(f, ctx, kind);
|
|
20592
|
+
}
|
|
20593
|
+
|
|
20282
20594
|
// mcp/server.ts
|
|
20283
20595
|
var CLAIMED_NOTE = "Exploit detail is hidden until you verify control of this contract in the web app.";
|
|
20284
20596
|
var REVERIFY_ESTIMATE = "about 10 min";
|
|
@@ -20326,14 +20638,19 @@ var paidRun = (willDo) => ({
|
|
|
20326
20638
|
cost: PAID_RUN_NOTE,
|
|
20327
20639
|
next: NEXT
|
|
20328
20640
|
});
|
|
20329
|
-
function
|
|
20330
|
-
const
|
|
20641
|
+
function intentView(i) {
|
|
20642
|
+
const lead = i.rows[0];
|
|
20331
20643
|
return {
|
|
20332
|
-
|
|
20333
|
-
|
|
20334
|
-
|
|
20335
|
-
|
|
20336
|
-
|
|
20644
|
+
watch: i.title,
|
|
20645
|
+
subject: lead ? subjectName(lead.subject, lead.units) : i.title,
|
|
20646
|
+
kind: i.kind,
|
|
20647
|
+
on: i.on,
|
|
20648
|
+
alertLevel: i.alertLevel,
|
|
20649
|
+
summary: intentLine(i),
|
|
20650
|
+
members: i.members,
|
|
20651
|
+
status: subjectStatus(i.rows),
|
|
20652
|
+
mutedUntil: i.rows.map((w) => mutedUntilOf(w)).find(Boolean) ?? null,
|
|
20653
|
+
rules: i.rows.map((w) => ({
|
|
20337
20654
|
watchId: w.id,
|
|
20338
20655
|
kind: w.rule.kind,
|
|
20339
20656
|
status: w.status,
|
|
@@ -20373,7 +20690,7 @@ function buildTools(client) {
|
|
|
20373
20690
|
}))
|
|
20374
20691
|
},
|
|
20375
20692
|
get_plan: {
|
|
20376
|
-
description: "A monitoring plan: verdict, findings (title, headline, severity, class, verdict, disposition) and watches.",
|
|
20693
|
+
description: "A monitoring plan: verdict, findings (title, headline, severity, class, verdict, disposition, kind, alertLevel) and watches.",
|
|
20377
20694
|
input: { planId },
|
|
20378
20695
|
run: async (a) => {
|
|
20379
20696
|
const p = await client.plan(a.planId);
|
|
@@ -20396,21 +20713,32 @@ function buildTools(client) {
|
|
|
20396
20713
|
severity: f.severity,
|
|
20397
20714
|
class: f.class,
|
|
20398
20715
|
verdict: f.verdict,
|
|
20399
|
-
disposition: f.disposition
|
|
20716
|
+
disposition: f.disposition,
|
|
20717
|
+
kind: f.kind,
|
|
20718
|
+
alertLevel: f.alertLevel
|
|
20400
20719
|
})),
|
|
20401
20720
|
watches: p.report?.plan?.watches ?? []
|
|
20402
20721
|
};
|
|
20403
20722
|
}
|
|
20404
20723
|
},
|
|
20405
20724
|
get_finding: {
|
|
20406
|
-
description: "One finding in full,
|
|
20725
|
+
description: "One finding in full (its kind and its watch's alert level) plus the handoff: the agent brief (the finding page's \"Copy agent brief\", a Markdown task to start your own review from), the stored PoC source and the exact command to re-run it against the reviewed source (the same as `sentinel finding --brief --poc --run`). Proof and the verifier's note need a verified owner.",
|
|
20407
20726
|
input: { planId, index: number2().int().min(0).describe("Finding index from get_plan") },
|
|
20408
20727
|
run: async (a) => {
|
|
20409
20728
|
const [finding, p] = await Promise.all([
|
|
20410
20729
|
client.finding(a.planId, a.index),
|
|
20411
20730
|
client.plan(a.planId)
|
|
20412
20731
|
]);
|
|
20413
|
-
|
|
20732
|
+
const commit = p.report?.target?.commit ?? null;
|
|
20733
|
+
const bundle = handoffBundle(finding, { contract: p.contractId, commit });
|
|
20734
|
+
const brief = p.access === "claimed" ? null : agentBrief(finding, {
|
|
20735
|
+
contract: p.contractId,
|
|
20736
|
+
commit,
|
|
20737
|
+
planId: a.planId,
|
|
20738
|
+
index: a.index,
|
|
20739
|
+
prelaunch: p.access === "prelaunch"
|
|
20740
|
+
});
|
|
20741
|
+
return { ...noteFor(p), finding, brief, poc: bundle.poc, runCommand: bundle.runCommand };
|
|
20414
20742
|
}
|
|
20415
20743
|
},
|
|
20416
20744
|
get_run_status: {
|
|
@@ -20529,13 +20857,13 @@ function buildTools(client) {
|
|
|
20529
20857
|
run: async (a) => a.confirm === true ? client.testsFromRepo(a.planId) : paidRun("Run the Clarinet suite from this pre-launch plan's repo in the airgapped sandbox.")
|
|
20530
20858
|
},
|
|
20531
20859
|
list_watches: {
|
|
20532
|
-
description: "What a plan watches,
|
|
20860
|
+
description: "What a plan watches, as intent watches (Money out per token, Control changes, one per finding, the fns you added): on or off, the threshold or summary, the fns covered, each rule in token units, any mute, and the subject name the other watch tools take.",
|
|
20533
20861
|
input: { planId },
|
|
20534
20862
|
run: async (a) => {
|
|
20535
20863
|
const ctx = await watchContext(client, a.planId);
|
|
20536
20864
|
return {
|
|
20537
20865
|
...ctx.access === "claimed" ? { note: CLAIMED_NOTE } : {},
|
|
20538
|
-
|
|
20866
|
+
watches: ctx.intents.map(intentView)
|
|
20539
20867
|
};
|
|
20540
20868
|
}
|
|
20541
20869
|
},
|
|
@@ -20713,7 +21041,7 @@ async function runTool(tool, args) {
|
|
|
20713
21041
|
}
|
|
20714
21042
|
}
|
|
20715
21043
|
function buildServer(client) {
|
|
20716
|
-
const server = new McpServer({ name: "sentinel", version: "0.
|
|
21044
|
+
const server = new McpServer({ name: "sentinel", version: "0.2.0" });
|
|
20717
21045
|
for (const [name, tool] of Object.entries(buildTools(client))) {
|
|
20718
21046
|
server.registerTool(name, { description: tool.description, inputSchema: tool.input }, (args) => runTool(tool, args));
|
|
20719
21047
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Client for the Sentinel v1 API (docs/api.md). Shared by the MCP server and the CLI. No dependencies:
|
|
3
3
|
* global fetch. Auth is one function, `authHeader`, so another credential can be swapped in later.
|
|
4
4
|
*/
|
|
5
|
+
import type { AlertLevel, FindingKind } from "./kind";
|
|
5
6
|
export declare const DEFAULT_BASE_URL = "https://api.runsentinel.app";
|
|
6
7
|
export declare class SentinelError extends Error {
|
|
7
8
|
readonly status: number;
|
|
@@ -47,8 +48,25 @@ export type Finding = {
|
|
|
47
48
|
verdict: string;
|
|
48
49
|
disposition: string;
|
|
49
50
|
poc?: string;
|
|
51
|
+
/** What the finding is for the people watching (client/kind.ts), stamped by the API. */
|
|
52
|
+
kind?: FindingKind;
|
|
53
|
+
/** How the finding's watch pages; absent on a refuted finding. */
|
|
54
|
+
alertLevel?: AlertLevel;
|
|
55
|
+
reverified?: boolean;
|
|
56
|
+
/** "fn" when `targetFn` names the function; "contract" when no single function carries it. */
|
|
57
|
+
scope?: "fn" | "contract";
|
|
58
|
+
/** Trust findings: the powers held by design, one entry each. */
|
|
59
|
+
powers?: Power[];
|
|
60
|
+
/** The watch row's subject, a short fn-level phrase (verified owners only). */
|
|
61
|
+
watchSubject?: string;
|
|
50
62
|
[field: string]: unknown;
|
|
51
63
|
};
|
|
64
|
+
/** One power a trust finding describes (monitoring/adjudication.ts `Power`, copied: no deps). */
|
|
65
|
+
export type Power = {
|
|
66
|
+
holder: string;
|
|
67
|
+
can: string;
|
|
68
|
+
fn?: string;
|
|
69
|
+
};
|
|
52
70
|
/** A plan at the caller's access level: `access` is absent on the teaser. */
|
|
53
71
|
export type PlanView = {
|
|
54
72
|
view: string;
|
|
@@ -114,6 +132,11 @@ export type WatchSubject = {
|
|
|
114
132
|
} | {
|
|
115
133
|
kind: "asset";
|
|
116
134
|
asset: string;
|
|
135
|
+
}
|
|
136
|
+
/** Control changes: one group over every fn that changes who controls the contract. */
|
|
137
|
+
| {
|
|
138
|
+
kind: "group";
|
|
139
|
+
group: "control";
|
|
117
140
|
};
|
|
118
141
|
export type WatchStatus = "suggested" | "live" | "paused";
|
|
119
142
|
export type Units = {
|
|
@@ -146,8 +169,57 @@ export type StoredWatch = {
|
|
|
146
169
|
updatedAt: string;
|
|
147
170
|
units: Units | null;
|
|
148
171
|
};
|
|
172
|
+
export type IntentKind = "money_out" | "control" | "finding" | "owner_fn";
|
|
173
|
+
/**
|
|
174
|
+
* One intent watch: what a team sees (Money out per asset, Control changes, one per finding, a fn the team
|
|
175
|
+
* added). `rows` are the stored watches behind it: what a rule edit, a switch or a mute acts on.
|
|
176
|
+
*/
|
|
177
|
+
export type IntentWatch<R = StoredWatch & {
|
|
178
|
+
on: boolean;
|
|
179
|
+
}> = {
|
|
180
|
+
id: string;
|
|
181
|
+
kind: IntentKind;
|
|
182
|
+
title: string;
|
|
183
|
+
tag: string;
|
|
184
|
+
summary: string;
|
|
185
|
+
on: boolean;
|
|
186
|
+
alertLevel: "urgent" | "normal";
|
|
187
|
+
watchIds: string[];
|
|
188
|
+
rows: R[];
|
|
189
|
+
members: {
|
|
190
|
+
group: string;
|
|
191
|
+
fns: string[];
|
|
192
|
+
}[];
|
|
193
|
+
/** Money out: the asset, its token units and its threshold (null amount: every outflow alerts). */
|
|
194
|
+
asset?: string;
|
|
195
|
+
units?: Units | null;
|
|
196
|
+
threshold?: {
|
|
197
|
+
amount: string | null;
|
|
198
|
+
basis: string;
|
|
199
|
+
confirmed: boolean;
|
|
200
|
+
suggested: {
|
|
201
|
+
amount: string;
|
|
202
|
+
basis: string;
|
|
203
|
+
} | null;
|
|
204
|
+
};
|
|
205
|
+
findingRef?: {
|
|
206
|
+
title: string;
|
|
207
|
+
index: number | null;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
/** The stored rows behind a list of intent watches, in order. */
|
|
211
|
+
export declare const rowsOf: <R>(intents: {
|
|
212
|
+
rows: R[];
|
|
213
|
+
}[]) => R[];
|
|
149
214
|
export type WatchesView = {
|
|
150
|
-
|
|
215
|
+
intents: IntentWatch[];
|
|
216
|
+
/** "N of M on", over intent watches. */
|
|
217
|
+
counts: {
|
|
218
|
+
on: number;
|
|
219
|
+
total: number;
|
|
220
|
+
};
|
|
221
|
+
/** Finding-based watches hidden until the account verifies control (a count only). */
|
|
222
|
+
locked: number;
|
|
151
223
|
events: {
|
|
152
224
|
id: string;
|
|
153
225
|
watchId: string;
|
|
@@ -167,13 +239,13 @@ export type StartedMonitoring = {
|
|
|
167
239
|
contractId: string;
|
|
168
240
|
planId: string;
|
|
169
241
|
status: "draft" | "requested" | "live";
|
|
170
|
-
|
|
242
|
+
intents: IntentWatch<StoredWatchView>[];
|
|
171
243
|
webhookUrl: string | null;
|
|
172
244
|
slackUrl: string | null;
|
|
173
245
|
emailAlerts: boolean;
|
|
174
246
|
hasSigningSecret: boolean;
|
|
175
247
|
};
|
|
176
|
-
type StoredWatchView = Pick<StoredWatch, "id" | "subject" | "rule" | "status" | "title">;
|
|
248
|
+
type StoredWatchView = Pick<StoredWatch, "id" | "subject" | "rule" | "suggestedRule" | "delivery" | "provenance" | "status" | "title" | "reason" | "units">;
|
|
177
249
|
export type WatchChoice = {
|
|
178
250
|
watchId?: string;
|
|
179
251
|
fn: string;
|
|
@@ -200,7 +272,8 @@ export type DeliveryResult = {
|
|
|
200
272
|
detail: string;
|
|
201
273
|
};
|
|
202
274
|
export declare const subjectKey: (s: WatchSubject) => string;
|
|
203
|
-
|
|
275
|
+
export declare const CONTROL_NAME = "Control changes";
|
|
276
|
+
/** The subject's display name: the fn name, the token's symbol (never the raw token id), or the group's. */
|
|
204
277
|
export declare function subjectName(s: WatchSubject, units: Units | null): string;
|
|
205
278
|
type HasSubject = {
|
|
206
279
|
subject: WatchSubject;
|
|
@@ -220,7 +293,8 @@ export declare class SubjectError extends Error {
|
|
|
220
293
|
export declare function groupSubjects<W extends HasSubject>(watches: W[]): SubjectMatch<W>[];
|
|
221
294
|
/**
|
|
222
295
|
* The watches on the subject `name` points at: its display name (fn name, token symbol, or the part
|
|
223
|
-
* after
|
|
296
|
+
* after `::`, or "Control changes" / "control"), its subjectKey (`fn:…` / `asset:…` / `group:control`) or a
|
|
297
|
+
* full asset id, case-insensitive. A name that
|
|
224
298
|
* matches nothing, or more than one subject, throws a SubjectError listing the candidates.
|
|
225
299
|
*/
|
|
226
300
|
export declare function resolveSubject<W extends HasSubject>(watches: W[], name: string): SubjectMatch<W>;
|
|
@@ -231,6 +305,8 @@ export declare function ruleSummary(rule: WatchRule, units: Units | null): strin
|
|
|
231
305
|
export type WatchKindTag = "finding" | "outflow" | "who-acts" | "governance" | "upgrade";
|
|
232
306
|
/** The plain tag a rule is listed under. */
|
|
233
307
|
export declare function kindTag(rule: WatchRule): WatchKindTag;
|
|
308
|
+
/** An intent watch's one line: a Money out threshold in token units (or its fail-safe), else its summary. */
|
|
309
|
+
export declare function intentLine(i: Pick<IntentWatch<unknown>, "kind" | "summary" | "threshold" | "units">): string;
|
|
234
310
|
/** A subject's status from its rules: live if any is, else paused if all are, else suggested. */
|
|
235
311
|
export declare function subjectStatus(ws: {
|
|
236
312
|
status: WatchStatus;
|
|
@@ -355,7 +431,11 @@ export type WatchContext = {
|
|
|
355
431
|
contractId: string;
|
|
356
432
|
/** The plan's access level for this caller; a claimed one hides exploit detail. */
|
|
357
433
|
access: PlanView["access"];
|
|
358
|
-
|
|
434
|
+
intents: IntentWatch[];
|
|
435
|
+
/** The stored rows behind the intents: what subjects resolve over. */
|
|
436
|
+
watches: (StoredWatch & {
|
|
437
|
+
on: boolean;
|
|
438
|
+
})[];
|
|
359
439
|
};
|
|
360
440
|
/** The caller's watches for a plan. */
|
|
361
441
|
export declare function watchContext(client: SentinelClient, planId: string): Promise<WatchContext>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secondlayer/sentinel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Sentinel from a terminal or an agent: the CLI, an MCP server and a client for your Stacks contract monitoring.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://runsentinel.app/docs",
|