@secondlayer/sentinel 0.4.0 → 0.5.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/mcp.js CHANGED
@@ -12659,6 +12659,13 @@ class SentinelError extends Error {
12659
12659
  this.name = "SentinelError";
12660
12660
  }
12661
12661
  }
12662
+ var runInProgress = (status) => status === "queued" || status === "running";
12663
+ function runPhase(run) {
12664
+ if (run.status === "queued")
12665
+ return "queued";
12666
+ const last = run.events.at(-1);
12667
+ return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
12668
+ }
12662
12669
  var rowsOf = (intents) => intents.flatMap((i) => i.rows);
12663
12670
  var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
12664
12671
  var CONTROL_NAME = "Control changes";
@@ -12791,6 +12798,7 @@ function createClient(opts) {
12791
12798
  }
12792
12799
  const plan = (id) => `/v1/plans/${encodeURIComponent(id)}`;
12793
12800
  const appPlan = (id) => `/plan/${encodeURIComponent(id)}`;
12801
+ const appProject = (id) => `/projects/${encodeURIComponent(id)}`;
12794
12802
  return {
12795
12803
  me: () => call("GET", "/v1/me"),
12796
12804
  plans: async () => (await call("GET", "/v1/plans")).plans,
@@ -12806,6 +12814,8 @@ function createClient(opts) {
12806
12814
  testsFromRepo: (id) => call("POST", `${appPlan(id)}/tests`, { fromRepo: true }),
12807
12815
  previewPlan: (contractId) => call("POST", "/plan/preview", { contractId }),
12808
12816
  createPlan: (contractId) => call("POST", "/plan", { contractId }),
12817
+ previewImport: (url) => call("POST", "/projects/import/preview", { url }),
12818
+ importRepo: (input) => call("POST", "/projects/import", input),
12809
12819
  billing: () => call("GET", "/billing"),
12810
12820
  watches: (planId) => call("GET", `${appPlan(planId)}/watches`),
12811
12821
  setRule: (planId, watchId, rule) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/rule`, { rule }),
@@ -12822,9 +12832,69 @@ function createClient(opts) {
12822
12832
  watchContract: (contractId) => call("POST", "/contracts/watch", {
12823
12833
  contractId
12824
12834
  }),
12825
- stopMonitoring: (planId) => call("DELETE", `${appPlan(planId)}/monitor`)
12835
+ stopMonitoring: (planId) => call("DELETE", `${appPlan(planId)}/monitor`),
12836
+ watchActivity: (planId, intentId, opts = {}) => call("GET", `${appPlan(planId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
12837
+ functions: async (planId) => (await call("GET", `${appPlan(planId)}/functions`)).functions,
12838
+ functionActivity: (planId, fn) => call("GET", `${appPlan(planId)}/functions/${encodeURIComponent(fn)}/activity`),
12839
+ alertPreview: (planId, example) => call("GET", `${appPlan(planId)}/alert-preview${query({ example })}`),
12840
+ threshold: (planId, asset) => call("GET", `${appPlan(planId)}/watches/asset/${encodeURIComponent(asset)}/threshold`),
12841
+ setThreshold: (planId, asset, amount) => call("POST", `${appPlan(planId)}/watches/asset/${encodeURIComponent(asset)}/threshold`, { amount }),
12842
+ retryHistory: (planId) => call("POST", `${appPlan(planId)}/history/retry`, {}),
12843
+ readAlert: (alertId) => call("POST", `/alerts/${encodeURIComponent(alertId)}/read`),
12844
+ readAllAlerts: () => call("POST", "/alerts/read-all"),
12845
+ ask: (q) => call("POST", "/ask", { q }),
12846
+ acknowledge: (planId, finding, title, reason, note = null) => call("POST", `${appPlan(planId)}/acknowledge`, {
12847
+ finding,
12848
+ title,
12849
+ reason,
12850
+ note
12851
+ }),
12852
+ reopen: (planId, finding, title) => call("POST", `${appPlan(planId)}/reopen`, { finding, title }),
12853
+ ackHistory: async (planId) => (await call("GET", `${appPlan(planId)}/acks`)).events,
12854
+ notes: async (planId, finding) => (await call("GET", `${appPlan(planId)}/findings/${finding}/notes`)).notes,
12855
+ addNote: (planId, finding, title, body) => call("POST", `${appPlan(planId)}/findings/${finding}/notes`, {
12856
+ title,
12857
+ body
12858
+ }),
12859
+ tests: (planId, files) => call("POST", `${appPlan(planId)}/tests`, { files }),
12860
+ fuzzCheck: (planId, harness) => call("POST", `${appPlan(planId)}/fuzzcheck`, { harness }),
12861
+ fuzz: (planId, invariants) => call("POST", `${appPlan(planId)}/fuzz`, { invariants }),
12862
+ latestCommit: (planId) => call("GET", `${appPlan(planId)}/latest`),
12863
+ updatePlan: (planId, commit) => call("POST", `${appPlan(planId)}/update`, commit ? { commit } : {}),
12864
+ setAutoPlan: (planId, auto) => call("POST", `${appPlan(planId)}/wait`, { auto }),
12865
+ setLink: (planId, dependency, url) => call("POST", `${appPlan(planId)}/link`, { id: dependency, url }),
12866
+ planLinked: (planId) => call("POST", `${appPlan(planId)}/plan-linked`, {}),
12867
+ renameContract: (planId, name) => call("POST", `${appPlan(planId)}/settings`, { name }),
12868
+ removeContract: (planId, confirm) => call("POST", `${appPlan(planId)}/remove`, {
12869
+ confirm
12870
+ }),
12871
+ projects: () => call("GET", "/projects"),
12872
+ project: (projectId) => call("GET", appProject(projectId)),
12873
+ projectOverview: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/overview${opts.watches ? "?watches=1" : ""}`),
12874
+ renameProject: (projectId, name) => call("POST", `${appProject(projectId)}/rename`, { name }),
12875
+ removeProject: (projectId, confirm) => call("POST", `${appProject(projectId)}/remove`, {
12876
+ confirm
12877
+ }),
12878
+ answerMergeSuggestion: (from, into, accept) => call("POST", "/projects/merge", { from, into, accept }),
12879
+ mergeProject: (projectId, into) => call("POST", `${appProject(projectId)}/merge-into`, { into }),
12880
+ moveProjectContract: (projectId, contractId, into) => call("POST", `${appProject(projectId)}/contracts/${encodeURIComponent(contractId)}/move`, { into }),
12881
+ removeProjectContract: (projectId, contractId, confirm) => call("POST", `${appProject(projectId)}/contracts/${encodeURIComponent(contractId)}/remove`, { confirm }),
12882
+ previewProjectContract: (projectId, contractId) => call("POST", `${appProject(projectId)}/contracts/preview`, { contractId }),
12883
+ addProjectContract: (projectId, contractId, opts = {}) => call("POST", `${appProject(projectId)}/contracts`, { contractId, ...opts }),
12884
+ setProjectDestinations: (projectId, dest) => call("POST", `${appProject(projectId)}/destinations`, dest),
12885
+ setProjectMonitoring: (projectId, on) => call("POST", `${appProject(projectId)}/monitoring`, { on }),
12886
+ projectWatches: (projectId) => call("GET", `${appProject(projectId)}/watches`),
12887
+ projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`)
12826
12888
  };
12827
12889
  }
12890
+ function query(params) {
12891
+ const q = new URLSearchParams;
12892
+ for (const [k, v] of Object.entries(params))
12893
+ if (v !== undefined && v !== null)
12894
+ q.set(k, String(v));
12895
+ const s = q.toString();
12896
+ return s ? `?${s}` : "";
12897
+ }
12828
12898
  var HOUR_MS = 3600000;
12829
12899
  var muteEnd = (hours) => new Date(Date.now() + Math.min(hours * HOUR_MS, 7 * 24 * HOUR_MS - 60000)).toISOString();
12830
12900
  async function watchContext(client, planId) {
@@ -20342,9 +20412,9 @@ function assertionsOf(output) {
20342
20412
  var FENCE = "```";
20343
20413
  var FOOTER = "Sentinel's findings are a starting point for your own review, not an audit sign-off.";
20344
20414
  var KIND_LINE = {
20345
- exploitable: "Exploitable (verified, re-verified, reproduced in the sandbox)",
20415
+ exploitable: "Reproducible (verified, re-verified, reproduced in the sandbox)",
20346
20416
  trust: "Trust assumption (watched: Sentinel alerts when it's used)",
20347
- worth_a_look: "Worth a look (not proven exploitable)",
20417
+ worth_a_look: "Worth a look (not reproduced)",
20348
20418
  refuted: "Refuted"
20349
20419
  };
20350
20420
  var tick = (s) => `\`${s}\``;
@@ -20435,16 +20505,16 @@ function issueMd(f, withCondition) {
20435
20505
  "## The issue",
20436
20506
  f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
20437
20507
  ...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
20438
- ...withCondition && f.precondition ? ["", `**Exploitable when:** ${f.precondition}`] : [],
20508
+ ...withCondition && f.precondition ? ["", `**Reproduces when:** ${f.precondition}`] : [],
20439
20509
  ""
20440
20510
  ];
20441
20511
  }
20442
20512
  function prelaunchBrief(f, ctx, kind) {
20443
- const kindLine = kind === "exploitable" ? "Exploitable · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
20513
+ const kindLine = kind === "exploitable" ? "Reproducible · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
20444
20514
  return [
20445
20515
  "# Task: review a Sentinel finding on pre-launch code",
20446
20516
  "",
20447
- `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.`,
20517
+ `Sentinel found an issue${f.targetFn ? ` in ${tick(f.targetFn)}` : ""}: ${f.title}. PoCs and reproduction 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.`,
20448
20518
  "",
20449
20519
  "## Start here",
20450
20520
  `1. ${where(f, ctx)}`,
@@ -20476,7 +20546,7 @@ function trustBrief(f, ctx) {
20476
20546
  `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.`,
20477
20547
  "3. Only then discuss options that fit this codebase's design.",
20478
20548
  "",
20479
- ...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write an exploit for it."),
20549
+ ...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write a PoC for it."),
20480
20550
  ...contextMd(f, ctx, "trust"),
20481
20551
  "- Class: trust assumption (a power held by design)",
20482
20552
  "- Evidence: checked by Sentinel's adversarial verifier. No PoC; nothing to run.",
@@ -20594,10 +20664,6 @@ function agentBrief(f, ctx) {
20594
20664
  var CLAIMED_NOTE = "Exploit detail is hidden until you verify control of this contract in the web app.";
20595
20665
  var REVERIFY_ESTIMATE = "about 10 min";
20596
20666
  var PAID_RUN_NOTE = "A paid run from your balance, charged only if it finishes (or a free daily run when billing isn't linked).";
20597
- function phaseOf(run) {
20598
- const last = run.events.at(-1);
20599
- return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
20600
- }
20601
20667
  var noteFor = (p) => p.access === "claimed" ? { note: CLAIMED_NOTE } : {};
20602
20668
  var ruleInput = discriminatedUnion("kind", [
20603
20669
  object({
@@ -20640,6 +20706,7 @@ var paidRun = (willDo) => ({
20640
20706
  function intentView(i) {
20641
20707
  const lead = i.rows[0];
20642
20708
  return {
20709
+ id: i.id,
20643
20710
  watch: i.title,
20644
20711
  subject: lead ? subjectName(lead.subject, lead.units) : i.title,
20645
20712
  kind: i.kind,
@@ -20685,6 +20752,14 @@ function buildTools(client) {
20685
20752
  const planId = string2().describe("Plan id, from list_contracts");
20686
20753
  const subject = string2().describe("A watched subject: a function name or a token symbol, from list_watches");
20687
20754
  const confirm = boolean2().optional().describe("Set true to actually do it. Without it you only get what it would do.");
20755
+ const projectId = string2().describe("Project id, from list_projects");
20756
+ const projectNamed = async (id) => {
20757
+ const { projects } = await client.projects();
20758
+ const p = projects.find((x) => x.id === id);
20759
+ if (!p)
20760
+ throw new SentinelError(404, `No project ${id}. Yours: ${projects.map((x) => `${x.name} (${x.id})`).join(", ") || "none"}`);
20761
+ return p;
20762
+ };
20688
20763
  return {
20689
20764
  list_contracts: {
20690
20765
  description: "Your claimed contracts: plan id, monitoring status, worst evidence.",
@@ -20750,13 +20825,19 @@ function buildTools(client) {
20750
20825
  }
20751
20826
  },
20752
20827
  get_run_status: {
20753
- description: "Running and finished follow-up runs on a plan, with the phase of each running one.",
20828
+ description: "Unfinished and finished follow-up runs on a plan, with the phase of each unfinished one (`queued` while it waits its turn).",
20754
20829
  input: { planId },
20755
20830
  run: async (a) => {
20756
20831
  const runs = await client.runs(a.planId);
20757
20832
  return {
20758
- running: runs.filter((r) => r.status === "running").map((r) => ({ id: r.id, kind: r.kind, startedAt: r.startedAt, phase: phaseOf(r) })),
20759
- finished: runs.filter((r) => r.status !== "running").map((r) => ({ id: r.id, kind: r.kind, status: r.status, finishedAt: r.finishedAt }))
20833
+ running: runs.filter((r) => runInProgress(r.status)).map((r) => ({
20834
+ id: r.id,
20835
+ kind: r.kind,
20836
+ status: r.status,
20837
+ startedAt: r.startedAt,
20838
+ phase: runPhase(r)
20839
+ })),
20840
+ finished: runs.filter((r) => !runInProgress(r.status)).map((r) => ({ id: r.id, kind: r.kind, status: r.status, finishedAt: r.finishedAt }))
20760
20841
  };
20761
20842
  }
20762
20843
  },
@@ -20831,6 +20912,45 @@ function buildTools(client) {
20831
20912
  });
20832
20913
  }
20833
20914
  },
20915
+ preview_repo_import: {
20916
+ description: "Read a public GitHub Clarinet repo (github.com/<owner>/<repo>, or /tree/<branch>/<folder> for one project of a monorepo): its contracts in deploy order, which are worth monitoring, which already have a plan, and what importing the suggested ones costs. Writes and charges nothing. Pass its commit to import_repo.",
20917
+ input: {
20918
+ url: string2().describe("A public GitHub repo link")
20919
+ },
20920
+ run: async (a) => {
20921
+ const p = await client.previewImport(a.url);
20922
+ return {
20923
+ ...p,
20924
+ next: `To import, call import_repo with url, commit "${p.commit}" and the contract names (the suggested ones: ${p.contracts.filter((c) => c.suggested).map((c) => c.name).join(", ") || "none"}).`
20925
+ };
20926
+ }
20927
+ },
20928
+ import_repo: {
20929
+ description: "Plan contracts of a repo from preview_repo_import into one project: one paid plan per new contract (a recent plan is reused free), at the preview's commit only. Without confirm it only says what it would do; confirm: true imports and spends balance.",
20930
+ input: {
20931
+ url: string2().describe("The repo link given to preview_repo_import"),
20932
+ commit: string2().regex(/^[0-9a-f]{40}$/).describe("The commit preview_repo_import returned: only that code is imported"),
20933
+ contracts: array(string2()).min(1).describe("Contract names from the preview"),
20934
+ project: string2().optional().describe('One of your project ids, or "new" (the default: a project named after the repo)'),
20935
+ confirm
20936
+ },
20937
+ run: async (a) => {
20938
+ const names = a.contracts;
20939
+ const target = a.project ?? "new";
20940
+ const willDo = `Import ${names.length} contract${names.length === 1 ? "" : "s"} (${names.join(", ")}) at ${a.commit.slice(0, 7)} into ${target === "new" ? "a new project" : target}.`;
20941
+ if (a.confirm !== true) {
20942
+ const b = await client.billing();
20943
+ return preview(willDo, b.prices ? `Each new plan costs $${b.prices.run.toFixed(2)}, charged only if it finishes; one planned recently is reused free.` : "Billing isn't linked: the free daily plan limit applies.");
20944
+ }
20945
+ const r = await client.importRepo({
20946
+ url: a.url,
20947
+ commit: a.commit,
20948
+ contracts: names,
20949
+ project: a.project
20950
+ });
20951
+ return done(willDo, r.notStarted ? "Some plans didn't start." : "Started.", r);
20952
+ }
20953
+ },
20834
20954
  challenge: {
20835
20955
  description: "Re-verify one finding against your argument. Owners only. Without confirm it only describes what it will do.",
20836
20956
  input: {
@@ -20860,9 +20980,18 @@ function buildTools(client) {
20860
20980
  run: async (a) => a.confirm === true ? client.directed(a.planId, a.prompt) : paidRun("Re-plan this contract around what you asked to have watched.")
20861
20981
  },
20862
20982
  run_tests: {
20863
- description: "Run the Clarinet suite from a pre-launch plan's repo in the sandbox. Owners only. Without confirm it only describes what it will do.",
20864
- input: { planId, confirm },
20865
- 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.")
20983
+ description: "Run Clarinet tests against a plan's contract in the sandbox: your own test files (`files`), or without them the suite from a pre-launch plan's repo. Owners only. Without confirm it only describes what it will do.",
20984
+ input: {
20985
+ planId,
20986
+ files: array(object({ path: string2(), content: string2() })).optional().describe("Your test files (path and TypeScript source); leave out to run the repo's suite"),
20987
+ confirm
20988
+ },
20989
+ run: async (a) => {
20990
+ const files = a.files;
20991
+ if (a.confirm !== true)
20992
+ return paidRun(files?.length ? `Run ${files.length} test file(s) against this plan's contract in the airgapped sandbox.` : "Run the Clarinet suite from this pre-launch plan's repo in the airgapped sandbox.");
20993
+ return files?.length ? client.tests(a.planId, files) : client.testsFromRepo(a.planId);
20994
+ }
20866
20995
  },
20867
20996
  list_watches: {
20868
20997
  description: "What a plan watches, as intent watches (Money out once per contract with a limit per token, Control changes, Admin operations, Settings, one per finding, the fns you added): on or off, the threshold or summary, the fns covered, what it can see (coverage: direct-calls means a call made from another contract isn't visible yet), each rule in token units, any mute, and the subject name the other watch tools take.",
@@ -21035,6 +21164,450 @@ function buildTools(client) {
21035
21164
  await client.stopMonitoring(await planOfContract(client, a.contractId));
21036
21165
  return done(willDo, "Stopped.");
21037
21166
  }
21167
+ },
21168
+ get_watch_activity: {
21169
+ description: "What one watch saw and what crossed its rules: counts and the latest events (time, function, amount, caller, result). `watchId` is a watch's id from list_watches.",
21170
+ input: {
21171
+ planId,
21172
+ watchId: string2().describe("An intent watch id, from list_watches"),
21173
+ window: union([literal(7), literal(90)]).optional().describe("Days, 7 (default) or 90"),
21174
+ filter: _enum(["over", "all"]).optional().describe("over: only what crossed a rule's line"),
21175
+ cursor: string2().optional().describe("nextCursor from a previous page"),
21176
+ asset: string2().optional().describe("Money out: which asset (an id from list_watches)")
21177
+ },
21178
+ run: async (a) => client.watchActivity(a.planId, a.watchId, {
21179
+ window: a.window,
21180
+ filter: a.filter,
21181
+ cursor: a.cursor,
21182
+ asset: a.asset
21183
+ })
21184
+ },
21185
+ list_functions: {
21186
+ description: "Every public function of a plan's contract: what it does (its consequence), a one-line description, and the watch covering it (null: nothing watches it).",
21187
+ input: { planId },
21188
+ run: async (a) => ({ functions: await client.functions(a.planId) })
21189
+ },
21190
+ get_function_activity: {
21191
+ description: "What a watch on a function nobody watches yet would have seen. `collected: false` until its history is read (try again in a few minutes).",
21192
+ input: { planId, fn: string2().describe("A public function name, from list_functions") },
21193
+ run: async (a) => client.functionActivity(a.planId, a.fn)
21194
+ },
21195
+ preview_alert: {
21196
+ description: "A sample alert for a plan, as email and Slack render it, plus the webhook body. `example` picks a Money out or a finding alert; without it, the loudest watch.",
21197
+ input: { planId, example: _enum(["money_out", "finding"]).optional() },
21198
+ run: async (a) => client.alertPreview(a.planId, a.example)
21199
+ },
21200
+ set_threshold: {
21201
+ description: "Read or set one asset's Money out limit (an asset subject from list_watches: its symbol). Without amount it only reads the limit. With amount (base units, a digit string) and no confirm it describes the change; confirm: true sets it, live at once.",
21202
+ input: {
21203
+ planId,
21204
+ subject: string2().describe("A Money out asset: its token symbol, from list_watches"),
21205
+ amount: string2().regex(/^\d{1,30}$/).optional().describe("The new limit in the token's base units"),
21206
+ confirm
21207
+ },
21208
+ run: async (a) => {
21209
+ const ctx = await watchContext(client, a.planId);
21210
+ const g = resolveSubject(ctx.watches.filter((w) => w.subject.kind === "asset"), a.subject);
21211
+ const s = g.watches[0].subject;
21212
+ if (s.kind !== "asset")
21213
+ throw new SentinelError(400, `${a.subject} isn't an asset.`);
21214
+ const current = await client.threshold(ctx.planId, s.asset);
21215
+ const limit = (amount) => assetClause({
21216
+ asset: s.asset,
21217
+ units: g.watches[0].units,
21218
+ threshold: { ...current.threshold, amount }
21219
+ });
21220
+ if (a.amount === undefined)
21221
+ return { subject: g.name, limit: limit(current.threshold.amount), ...current };
21222
+ const willDo = `Change ${g.name}'s Money out limit: ${limit(current.threshold.amount)} becomes ${limit(a.amount)}.`;
21223
+ if (a.confirm !== true)
21224
+ return preview(willDo, "Live now.");
21225
+ const r = await client.setThreshold(ctx.planId, s.asset, a.amount);
21226
+ return done(willDo, "Live now.", { threshold: r.threshold });
21227
+ }
21228
+ },
21229
+ retry_history: {
21230
+ description: "Read a plan's watch history again after a failed read (activity says history: failed). Without confirm it only describes.",
21231
+ input: { planId, confirm },
21232
+ run: async (a) => {
21233
+ const willDo = "Read the history of this plan's watches again, in the background.";
21234
+ if (a.confirm !== true)
21235
+ return preview(willDo, "Starts now.");
21236
+ await client.retryHistory(a.planId);
21237
+ return done(willDo, "Started; activity fills in over the next few minutes.");
21238
+ }
21239
+ },
21240
+ mark_alert_read: {
21241
+ description: "Mark one alert read. Without confirm it only describes.",
21242
+ input: { alertId: string2().describe("Alert id, from list_alerts"), confirm },
21243
+ run: async (a) => {
21244
+ const willDo = `Mark alert ${a.alertId} read.`;
21245
+ if (a.confirm !== true)
21246
+ return preview(willDo, "Takes effect now.");
21247
+ await client.readAlert(a.alertId);
21248
+ return done(willDo, "Done.");
21249
+ }
21250
+ },
21251
+ mark_all_alerts_read: {
21252
+ description: "Mark every alert read. Without confirm it only describes.",
21253
+ input: { confirm },
21254
+ run: async (a) => {
21255
+ if (a.confirm !== true)
21256
+ return preview("Mark every alert read.", "Takes effect now.");
21257
+ const r = await client.readAllAlerts();
21258
+ return done("Mark every alert read.", `${r.count} marked read.`);
21259
+ }
21260
+ },
21261
+ ask_sentinel: {
21262
+ description: "Ask a question about Sentinel itself (how monitoring, plans or billing work), answered from its docs.",
21263
+ input: { question: string2().min(3).max(200) },
21264
+ run: async (a) => client.ask(a.question)
21265
+ },
21266
+ acknowledge_finding: {
21267
+ description: "Label a finding for your team: accepted (a risk you accept), fixed (awaiting deploy) or disagree. Changes no verdict; accepting a trust finding keeps a suggested watch on its function. Verified owners. Without confirm it only describes.",
21268
+ input: {
21269
+ planId,
21270
+ index: number2().int().min(0).describe("Finding index from get_plan"),
21271
+ reason: _enum(["accepted", "fixed", "disagree"]),
21272
+ note: string2().max(500).optional(),
21273
+ confirm
21274
+ },
21275
+ run: async (a) => {
21276
+ const f = await client.finding(a.planId, a.index);
21277
+ const reason = a.reason;
21278
+ const willDo = `Label "${f.title}" ${reason}. It changes no verdict.`;
21279
+ if (a.confirm !== true)
21280
+ return preview(willDo, "Your team sees it now.");
21281
+ const r = await client.acknowledge(a.planId, a.index, f.title, reason, a.note ?? null);
21282
+ return done(willDo, "Labelled.", r.watchId ? { suggestedWatchId: r.watchId } : {});
21283
+ }
21284
+ },
21285
+ reopen_finding: {
21286
+ description: "Put an acknowledged finding back on the list. Verified owners. Without confirm it only describes.",
21287
+ input: {
21288
+ planId,
21289
+ index: number2().int().min(0).describe("Finding index from get_plan"),
21290
+ confirm
21291
+ },
21292
+ run: async (a) => {
21293
+ const f = await client.finding(a.planId, a.index);
21294
+ const willDo = `Put "${f.title}" back on the list.`;
21295
+ if (a.confirm !== true)
21296
+ return preview(willDo, "Takes effect now.");
21297
+ await client.reopen(a.planId, a.index, f.title);
21298
+ return done(willDo, "Reopened.");
21299
+ }
21300
+ },
21301
+ get_ack_history: {
21302
+ description: "Every acknowledgement change on a plan's contract, oldest first. Verified owners.",
21303
+ input: { planId },
21304
+ run: async (a) => ({ events: await client.ackHistory(a.planId) })
21305
+ },
21306
+ list_notes: {
21307
+ description: "Your team's notes on one finding (and Sentinel's, when a later check reopened it). Verified owners.",
21308
+ input: { planId, index: number2().int().min(0).describe("Finding index from get_plan") },
21309
+ run: async (a) => ({ notes: await client.notes(a.planId, a.index) })
21310
+ },
21311
+ add_note: {
21312
+ description: "Add a note to one finding. Verified owners. Without confirm it only describes.",
21313
+ input: {
21314
+ planId,
21315
+ index: number2().int().min(0).describe("Finding index from get_plan"),
21316
+ body: string2().min(1).max(2000),
21317
+ confirm
21318
+ },
21319
+ run: async (a) => {
21320
+ const f = await client.finding(a.planId, a.index);
21321
+ const willDo = `Add a note to "${f.title}".`;
21322
+ if (a.confirm !== true)
21323
+ return preview(willDo, "Your team sees it now.");
21324
+ const r = await client.addNote(a.planId, a.index, f.title, a.body);
21325
+ return done(willDo, "Noted.", { note: r.note });
21326
+ }
21327
+ },
21328
+ run_fuzz: {
21329
+ description: "Fuzz a plan's contract against your Clarity invariant or property functions in the sandbox. Without confirm it only compile-checks them (free) and describes the run. Owners only.",
21330
+ input: {
21331
+ planId,
21332
+ invariants: string2().min(10).describe("Clarity define-… invariant or property functions"),
21333
+ confirm
21334
+ },
21335
+ run: async (a) => {
21336
+ const check = await client.fuzzCheck(a.planId, a.invariants);
21337
+ if (!check.ok)
21338
+ return { started: false, compiles: false, message: check.message ?? null };
21339
+ if (a.confirm !== true)
21340
+ return {
21341
+ ...paidRun("Fuzz the contract against these invariants in the airgapped sandbox."),
21342
+ compiles: true
21343
+ };
21344
+ return client.fuzz(a.planId, a.invariants);
21345
+ }
21346
+ },
21347
+ get_latest_commit: {
21348
+ description: "Whether a pre-launch plan's branch moved on: how far behind it is, what an update would review (carried, focused or full), and whether it's free (monitoring on).",
21349
+ input: { planId },
21350
+ run: async (a) => client.latestCommit(a.planId)
21351
+ },
21352
+ update_plan: {
21353
+ description: "Plan the branch's latest commit (or `commit`, a full sha); the new plan replaces this one when it finishes. Free while monitoring is on, else priced as a new plan. Owners only. Without confirm it only describes.",
21354
+ input: {
21355
+ planId,
21356
+ commit: string2().regex(/^[0-9a-f]{40}$/).optional(),
21357
+ confirm
21358
+ },
21359
+ run: async (a) => {
21360
+ const latest = await client.latestCommit(a.planId);
21361
+ const willDo = `Plan ${a.commit ? `commit ${a.commit.slice(0, 7)}` : "the branch's latest commit"}.`;
21362
+ if (a.confirm !== true)
21363
+ return {
21364
+ ...preview(willDo, "free" in latest && latest.free ? "Free: monitoring is on." : PAID_RUN_NOTE),
21365
+ latest
21366
+ };
21367
+ return client.updatePlan(a.planId, a.commit);
21368
+ }
21369
+ },
21370
+ set_auto_plan: {
21371
+ description: "For a plan waiting on contracts that aren't deployed yet: plan it on its own once they deploy (a paid plan), or not. Owners only. Without confirm it only describes.",
21372
+ input: { planId, auto: boolean2(), confirm },
21373
+ run: async (a) => {
21374
+ const willDo = a.auto ? "Plan this on its own once every contract it waits on is deployed." : "Don't plan this on its own.";
21375
+ if (a.confirm !== true)
21376
+ return preview(willDo, "Takes effect now.");
21377
+ return done(willDo, "Saved.", await client.setAutoPlan(a.planId, a.auto === true));
21378
+ }
21379
+ },
21380
+ link_dependency: {
21381
+ description: "Link code for one contract a waiting plan depends on (a .clar file or repo link; a repo link also fills the others it holds), or clear it with url: null. Owners only. Without confirm it only describes.",
21382
+ input: {
21383
+ planId,
21384
+ contract: string2().describe("The contract the plan waits on (from get_plan)"),
21385
+ url: string2().nullable(),
21386
+ confirm
21387
+ },
21388
+ run: async (a) => {
21389
+ const willDo = a.url === null ? `Clear the code linked for ${a.contract}.` : `Link ${a.url} as the code of ${a.contract}.`;
21390
+ if (a.confirm !== true)
21391
+ return preview(willDo, "Takes effect now.");
21392
+ return done(willDo, "Saved.", await client.setLink(a.planId, a.contract, a.url));
21393
+ }
21394
+ },
21395
+ plan_with_links: {
21396
+ description: "Plan a waiting contract now, with the linked code loaded at its real principals. A paid plan. Owners only. Without confirm it only describes.",
21397
+ input: { planId, confirm },
21398
+ run: async (a) => a.confirm === true ? client.planLinked(a.planId) : paidRun("Plan this contract now with the linked code.")
21399
+ },
21400
+ rename_contract: {
21401
+ description: "Your own name for a plan's contract; an empty name clears it. Owners only. Without confirm it only describes.",
21402
+ input: { planId, name: string2().max(60), confirm },
21403
+ run: async (a) => {
21404
+ const name = a.name.trim();
21405
+ const willDo = name ? `Call this contract "${name}".` : "Clear your name for this contract.";
21406
+ if (a.confirm !== true)
21407
+ return preview(willDo, "Takes effect now.");
21408
+ return done(willDo, "Saved.", await client.renameContract(a.planId, name));
21409
+ }
21410
+ },
21411
+ remove_contract: {
21412
+ description: "Take a contract off your account: its monitoring, alerts and claim go. `name` is the contract's name, typed, as the app asks (the preview says which). Can't be undone. Without confirm it only describes.",
21413
+ input: {
21414
+ planId,
21415
+ name: string2().optional().describe("The contract's name, typed to confirm"),
21416
+ confirm
21417
+ },
21418
+ run: async (a) => {
21419
+ const p = await client.plan(a.planId);
21420
+ const willDo = `Take ${p.contractId} off your account: its monitoring, alerts and claim go.`;
21421
+ if (a.confirm !== true || a.name === undefined)
21422
+ return {
21423
+ ...preview(willDo, "Can't be undone."),
21424
+ confirmWith: `name: "${contractName(p.contractId)}"`
21425
+ };
21426
+ return done(willDo, "Removed.", await client.removeContract(a.planId, a.name));
21427
+ }
21428
+ },
21429
+ list_projects: {
21430
+ description: "Your projects (the contracts of one protocol, grouped): id, name, contracts, monitoring, open findings, alerts in 7 days; and the merges Sentinel suggests (answer them with answer_merge_suggestion).",
21431
+ input: {},
21432
+ run: async () => client.projects()
21433
+ },
21434
+ get_project: {
21435
+ description: "One project: each contract (added: you claimed it, with its plan; found: one Sentinel found in your code), its role and monitoring.",
21436
+ input: { projectId },
21437
+ run: async (a) => client.project(a.projectId)
21438
+ },
21439
+ get_project_overview: {
21440
+ description: "One project with each claimed contract's plan (as get_plan) and, with watches: true, its watches (as list_watches), in one read.",
21441
+ input: { projectId, watches: boolean2().optional() },
21442
+ run: async (a) => client.projectOverview(a.projectId, { watches: a.watches === true })
21443
+ },
21444
+ rename_project: {
21445
+ description: "Rename a project. Without confirm it only describes.",
21446
+ input: { projectId, name: string2().min(1).max(60), confirm },
21447
+ run: async (a) => {
21448
+ const willDo = `Rename project ${a.projectId} to "${a.name.trim()}".`;
21449
+ if (a.confirm !== true)
21450
+ return preview(willDo, "Takes effect now.");
21451
+ return done(willDo, "Renamed.", await client.renameProject(a.projectId, a.name));
21452
+ }
21453
+ },
21454
+ merge_project: {
21455
+ description: "Merge one of your projects into another: every contract moves; monitoring, watches and verification stay as they are. Both ids come from list_projects. Without confirm it only describes.",
21456
+ input: {
21457
+ projectId,
21458
+ into: string2().describe("The project to merge into, an id from list_projects"),
21459
+ confirm
21460
+ },
21461
+ run: async (a) => {
21462
+ const from = await projectNamed(a.projectId);
21463
+ const to = await projectNamed(a.into);
21464
+ const willDo = `Move every contract of ${from.name} (${from.contracts.length}) into ${to.name}; ${from.name} goes.`;
21465
+ if (a.confirm !== true)
21466
+ return preview(willDo, "Takes effect now.");
21467
+ return done(willDo, "Merged.", await client.mergeProject(from.id, to.id));
21468
+ }
21469
+ },
21470
+ answer_merge_suggestion: {
21471
+ description: "Accept (merge `from` into `into`) or dismiss a merge Sentinel suggested, from list_projects' suggestions. Without confirm it only describes.",
21472
+ input: {
21473
+ from: string2().describe("The suggestion's from project id"),
21474
+ into: string2().describe("The suggestion's into project id"),
21475
+ accept: boolean2(),
21476
+ confirm
21477
+ },
21478
+ run: async (a) => {
21479
+ const r = await client.projects();
21480
+ if (!r.suggestions.some((s) => s.from === a.from && s.into === a.into))
21481
+ throw new SentinelError(404, `No open suggestion to merge ${a.from} into ${a.into}.`);
21482
+ const nameOf = (id) => r.projects.find((p) => p.id === id)?.name ?? String(id);
21483
+ const willDo = a.accept ? `Merge ${nameOf(a.from)} into ${nameOf(a.into)}: every contract moves.` : `Dismiss the suggestion to merge ${nameOf(a.from)} into ${nameOf(a.into)}.`;
21484
+ if (a.confirm !== true)
21485
+ return preview(willDo, "Takes effect now.");
21486
+ return done(willDo, "Done.", await client.answerMergeSuggestion(a.from, a.into, a.accept === true));
21487
+ }
21488
+ },
21489
+ move_project_contract: {
21490
+ description: 'Move one contract to another of your projects (an id from list_projects), or to a new project named after it ("new", a contract you added). Without confirm it only describes.',
21491
+ input: { projectId, contractId: string2(), into: string2(), confirm },
21492
+ run: async (a) => {
21493
+ const to = a.into === "new" ? "a new project named after it" : (await projectNamed(a.into)).name;
21494
+ const willDo = `Move ${a.contractId} to ${to}.`;
21495
+ if (a.confirm !== true)
21496
+ return preview(willDo, "Takes effect now.");
21497
+ return done(willDo, "Moved.", await client.moveProjectContract(a.projectId, a.contractId, a.into));
21498
+ }
21499
+ },
21500
+ remove_project_contract: {
21501
+ description: "Remove a contract from a project. One you added goes the way remove_contract takes it (monitoring, alerts and claim; `name`: its name, typed, as the preview says); one Sentinel found just leaves the project. Without confirm it only describes.",
21502
+ input: {
21503
+ projectId,
21504
+ contractId: string2(),
21505
+ name: string2().optional().describe("A contract you added: its name, typed to confirm"),
21506
+ confirm
21507
+ },
21508
+ run: async (a) => {
21509
+ const member = (await client.project(a.projectId)).contracts.find((c) => c.contractId === a.contractId);
21510
+ if (!member)
21511
+ throw new SentinelError(404, `${a.contractId} isn't in project ${a.projectId}.`);
21512
+ const added = member.origin === "added";
21513
+ const willDo = added ? `Remove ${a.contractId}: its monitoring, alerts and claim go.` : `Take ${a.contractId} out of the project (Sentinel found it; you never claimed it).`;
21514
+ if (a.confirm !== true || added && a.name === undefined)
21515
+ return {
21516
+ ...preview(willDo, added ? "Can't be undone." : "Takes effect now."),
21517
+ ...added ? { confirmWith: `name: "${contractName(member.contractId)}"` } : {}
21518
+ };
21519
+ return done(willDo, "Removed.", await client.removeProjectContract(a.projectId, a.contractId, added ? a.name : undefined));
21520
+ }
21521
+ },
21522
+ remove_project: {
21523
+ description: "Remove a project and every contract you added in it (their monitoring, alerts and claims). `name` is the project's name, typed, as the app asks. Can't be undone. Without confirm it only describes.",
21524
+ input: {
21525
+ projectId,
21526
+ name: string2().optional().describe("The project's name, typed to confirm"),
21527
+ confirm
21528
+ },
21529
+ run: async (a) => {
21530
+ const p = await client.project(a.projectId);
21531
+ const added = p.contracts.filter((c) => c.origin === "added").length;
21532
+ const willDo = `Remove ${p.name} and the ${added} contract(s) you added in it.`;
21533
+ if (a.confirm !== true || a.name === undefined)
21534
+ return { ...preview(willDo, "Can't be undone."), confirmWith: `name: "${p.name}"` };
21535
+ return done(willDo, "Removed.", await client.removeProject(p.id, a.name));
21536
+ }
21537
+ },
21538
+ add_project_contract: {
21539
+ description: 'Add a contract (SP….name, or a GitHub/Gist .clar link) to a project and start its plan. Without confirm it only shows where it fits (the project Sentinel suggests and why) and what it costs; confirm: true plans it into `into` (a project id, or "new"; default this project) and spends balance.',
21540
+ input: {
21541
+ projectId,
21542
+ contractId: string2(),
21543
+ into: string2().optional().describe('A project id from list_projects, or "new"'),
21544
+ confirm
21545
+ },
21546
+ run: async (a) => {
21547
+ const into = a.into ?? a.projectId;
21548
+ if (a.confirm !== true) {
21549
+ const [p, b] = await Promise.all([
21550
+ client.previewProjectContract(a.projectId, a.contractId),
21551
+ client.billing()
21552
+ ]);
21553
+ return {
21554
+ ...preview(`Plan ${p.contractId} into ${into === "new" ? "a new project" : into}.`, b.prices ? `Costs $${b.prices.run.toFixed(2)}, charged only if it finishes.` : "Billing isn't linked: the free daily plan limit applies."),
21555
+ suggestion: p.suggestion
21556
+ };
21557
+ }
21558
+ const r = await client.addProjectContract(a.projectId, a.contractId, into === a.projectId ? {} : { project: into });
21559
+ return done(`Plan ${a.contractId}.`, r.cached ? "Already planned." : "Started.", r);
21560
+ }
21561
+ },
21562
+ list_project_watches: {
21563
+ description: "A project's watches as one list across its contracts: each watch's id, title, on or off, summary, and the member plan to change it through (the plan watch tools).",
21564
+ input: { projectId },
21565
+ run: async (a) => client.projectWatches(a.projectId)
21566
+ },
21567
+ get_project_watch_activity: {
21568
+ description: "One project watch's activity on one member (`contract`, default the first that holds it): counts and the latest events.",
21569
+ input: {
21570
+ projectId,
21571
+ watchId: string2().describe("A watch id from list_project_watches"),
21572
+ contract: string2().optional(),
21573
+ window: union([literal(7), literal(90)]).optional()
21574
+ },
21575
+ run: async (a) => client.projectActivity(a.projectId, a.watchId, {
21576
+ contract: a.contract,
21577
+ window: a.window
21578
+ })
21579
+ },
21580
+ set_project_destinations: {
21581
+ description: "Where a project's alerts go, set on each contract you added in it: email on or off, a webhook URL, a Slack URL. A URL left out is cleared. Without confirm it only describes.",
21582
+ input: {
21583
+ projectId,
21584
+ email: boolean2().optional(),
21585
+ webhookUrl: string2().optional(),
21586
+ slackUrl: string2().optional(),
21587
+ confirm
21588
+ },
21589
+ run: async (a) => {
21590
+ const dest = {
21591
+ ...a.email === undefined ? {} : { email: a.email },
21592
+ ...a.webhookUrl ? { webhookUrl: a.webhookUrl } : {},
21593
+ ...a.slackUrl ? { slackUrl: a.slackUrl } : {}
21594
+ };
21595
+ const willDo = `Send this project's alerts to: ${[dest.email ? "email" : null, dest.webhookUrl ? "webhook" : null, dest.slackUrl ? "Slack" : null].filter(Boolean).join(", ") || "no new place"}; a URL left out is cleared.`;
21596
+ if (a.confirm !== true)
21597
+ return preview(willDo, "Takes effect now.");
21598
+ const r = await client.setProjectDestinations(a.projectId, dest);
21599
+ return done(willDo, "Saved on each contract; a new webhook secret is shown once.", r);
21600
+ }
21601
+ },
21602
+ set_project_monitoring: {
21603
+ description: "Turn monitoring on or off for every contract you added in a project. Monitored events are billed. Without confirm it only describes.",
21604
+ input: { projectId, on: boolean2(), confirm },
21605
+ run: async (a) => {
21606
+ const willDo = `Turn monitoring ${a.on ? "on" : "off"} for every contract you added in this project.`;
21607
+ if (a.confirm !== true)
21608
+ return preview(willDo, a.on ? "Live now; events are billed." : "Off now.");
21609
+ return done(willDo, "Done.", await client.setProjectMonitoring(a.projectId, a.on === true));
21610
+ }
21038
21611
  }
21039
21612
  };
21040
21613
  }
@@ -21049,7 +21622,7 @@ async function runTool(tool, args) {
21049
21622
  }
21050
21623
  }
21051
21624
  function buildServer(client) {
21052
- const server = new McpServer({ name: "sentinel", version: "0.4.0" });
21625
+ const server = new McpServer({ name: "sentinel", version: "0.5.0" });
21053
21626
  for (const [name, tool] of Object.entries(buildTools(client))) {
21054
21627
  server.registerTool(name, { description: tool.description, inputSchema: tool.input }, (args) => runTool(tool, args));
21055
21628
  }