@secondlayer/sentinel 0.1.0 → 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/index.js CHANGED
@@ -79,10 +79,14 @@ class SentinelError extends Error {
79
79
  this.name = "SentinelError";
80
80
  }
81
81
  }
82
- var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : `asset:${s.asset}`;
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
- watches: (await client.watches(planId)).watches
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 subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : `asset:${s.asset}`;
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 kindTag(rule) {
12750
- switch (rule.kind) {
12751
- case "signature":
12752
- return "finding";
12753
- case "outflowSingle":
12754
- case "outflowWindow":
12755
- case "valueNewActor":
12756
- return "outflow";
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
- watches: (await client.watches(planId)).watches
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 subjectView(g) {
20330
- const status = subjectStatus(g.watches);
20641
+ function intentView(i) {
20642
+ const lead = i.rows[0];
20331
20643
  return {
20332
- subject: g.name,
20333
- kind: [...new Set(g.watches.map((w) => kindTag(w.rule)))],
20334
- status,
20335
- mutedUntil: g.watches.map((w) => mutedUntilOf(w)).find(Boolean) ?? null,
20336
- rules: g.watches.map((w) => ({
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, including proof and the verifier's note when you are a verified owner.",
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
- return { ...noteFor(p), finding };
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, by subject (a function or a token): kind, status, each rule in token units, and any mute.",
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
- subjects: groupSubjects(ctx.watches).map(subjectView)
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.1.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
  }