@secondlayer/sentinel 0.4.0 → 0.6.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";
@@ -12774,7 +12781,10 @@ function mutedUntilOf(w, now = Date.now()) {
12774
12781
  function createClient(opts) {
12775
12782
  const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
12776
12783
  const doFetch = opts.fetch ?? globalThis.fetch;
12777
- const authHeader = () => ({ authorization: `Bearer ${opts.token}` });
12784
+ const authHeader = () => ({
12785
+ authorization: `Bearer ${opts.token}`,
12786
+ ...opts.client ? { "x-sentinel-client": opts.client } : {}
12787
+ });
12778
12788
  async function call(method, path, body) {
12779
12789
  const res = await doFetch(`${baseUrl}${path}`, {
12780
12790
  method,
@@ -12791,6 +12801,7 @@ function createClient(opts) {
12791
12801
  }
12792
12802
  const plan = (id) => `/v1/plans/${encodeURIComponent(id)}`;
12793
12803
  const appPlan = (id) => `/plan/${encodeURIComponent(id)}`;
12804
+ const appProject = (id) => `/projects/${encodeURIComponent(id)}`;
12794
12805
  return {
12795
12806
  me: () => call("GET", "/v1/me"),
12796
12807
  plans: async () => (await call("GET", "/v1/plans")).plans,
@@ -12806,6 +12817,8 @@ function createClient(opts) {
12806
12817
  testsFromRepo: (id) => call("POST", `${appPlan(id)}/tests`, { fromRepo: true }),
12807
12818
  previewPlan: (contractId) => call("POST", "/plan/preview", { contractId }),
12808
12819
  createPlan: (contractId) => call("POST", "/plan", { contractId }),
12820
+ previewImport: (url) => call("POST", "/projects/import/preview", { url }),
12821
+ importRepo: (input) => call("POST", "/projects/import", input),
12809
12822
  billing: () => call("GET", "/billing"),
12810
12823
  watches: (planId) => call("GET", `${appPlan(planId)}/watches`),
12811
12824
  setRule: (planId, watchId, rule) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/rule`, { rule }),
@@ -12822,9 +12835,70 @@ function createClient(opts) {
12822
12835
  watchContract: (contractId) => call("POST", "/contracts/watch", {
12823
12836
  contractId
12824
12837
  }),
12825
- stopMonitoring: (planId) => call("DELETE", `${appPlan(planId)}/monitor`)
12838
+ stopMonitoring: (planId) => call("DELETE", `${appPlan(planId)}/monitor`),
12839
+ watchActivity: (planId, intentId, opts = {}) => call("GET", `${appPlan(planId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
12840
+ functions: async (planId) => (await call("GET", `${appPlan(planId)}/functions`)).functions,
12841
+ functionActivity: (planId, fn) => call("GET", `${appPlan(planId)}/functions/${encodeURIComponent(fn)}/activity`),
12842
+ alertPreview: (planId, example) => call("GET", `${appPlan(planId)}/alert-preview${query({ example })}`),
12843
+ threshold: (planId, asset) => call("GET", `${appPlan(planId)}/watches/asset/${encodeURIComponent(asset)}/threshold`),
12844
+ setThreshold: (planId, asset, amount) => call("POST", `${appPlan(planId)}/watches/asset/${encodeURIComponent(asset)}/threshold`, { amount }),
12845
+ retryHistory: (planId) => call("POST", `${appPlan(planId)}/history/retry`, {}),
12846
+ readAlert: (alertId) => call("POST", `/alerts/${encodeURIComponent(alertId)}/read`),
12847
+ readAllAlerts: () => call("POST", "/alerts/read-all"),
12848
+ ask: (q) => call("POST", "/ask", { q }),
12849
+ acknowledge: (planId, finding, title, reason, note = null) => call("POST", `${appPlan(planId)}/acknowledge`, {
12850
+ finding,
12851
+ title,
12852
+ reason,
12853
+ note
12854
+ }),
12855
+ reopen: (planId, finding, title) => call("POST", `${appPlan(planId)}/reopen`, { finding, title }),
12856
+ ackHistory: async (planId) => (await call("GET", `${appPlan(planId)}/acks`)).events,
12857
+ notes: async (planId, finding) => (await call("GET", `${appPlan(planId)}/findings/${finding}/notes`)).notes,
12858
+ addNote: (planId, finding, title, body) => call("POST", `${appPlan(planId)}/findings/${finding}/notes`, {
12859
+ title,
12860
+ body
12861
+ }),
12862
+ tests: (planId, files) => call("POST", `${appPlan(planId)}/tests`, { files }),
12863
+ fuzzCheck: (planId, harness) => call("POST", `${appPlan(planId)}/fuzzcheck`, { harness }),
12864
+ fuzz: (planId, invariants) => call("POST", `${appPlan(planId)}/fuzz`, { invariants }),
12865
+ latestCommit: (planId) => call("GET", `${appPlan(planId)}/latest`),
12866
+ updatePlan: (planId, commit) => call("POST", `${appPlan(planId)}/update`, commit ? { commit } : {}),
12867
+ setAutoPlan: (planId, auto) => call("POST", `${appPlan(planId)}/wait`, { auto }),
12868
+ setLink: (planId, dependency, url) => call("POST", `${appPlan(planId)}/link`, { id: dependency, url }),
12869
+ planLinked: (planId) => call("POST", `${appPlan(planId)}/plan-linked`, {}),
12870
+ renameContract: (planId, name) => call("POST", `${appPlan(planId)}/settings`, { name }),
12871
+ removeContract: (planId, confirm) => call("POST", `${appPlan(planId)}/remove`, {
12872
+ confirm
12873
+ }),
12874
+ projects: () => call("GET", "/projects"),
12875
+ project: (projectId) => call("GET", appProject(projectId)),
12876
+ projectOverview: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/overview${opts.watches ? "?watches=1" : ""}`),
12877
+ renameProject: (projectId, name) => call("POST", `${appProject(projectId)}/rename`, { name }),
12878
+ removeProject: (projectId, confirm) => call("POST", `${appProject(projectId)}/remove`, {
12879
+ confirm
12880
+ }),
12881
+ answerMergeSuggestion: (from, into, accept) => call("POST", "/projects/merge", { from, into, accept }),
12882
+ mergeProject: (projectId, into) => call("POST", `${appProject(projectId)}/merge-into`, { into }),
12883
+ moveProjectContract: (projectId, contractId, into) => call("POST", `${appProject(projectId)}/contracts/${encodeURIComponent(contractId)}/move`, { into }),
12884
+ removeProjectContract: (projectId, contractId, confirm) => call("POST", `${appProject(projectId)}/contracts/${encodeURIComponent(contractId)}/remove`, { confirm }),
12885
+ previewProjectContract: (projectId, contractId) => call("POST", `${appProject(projectId)}/contracts/preview`, { contractId }),
12886
+ addProjectContract: (projectId, contractId, opts = {}) => call("POST", `${appProject(projectId)}/contracts`, { contractId, ...opts }),
12887
+ setProjectDestinations: (projectId, dest) => call("POST", `${appProject(projectId)}/destinations`, dest),
12888
+ setProjectMonitoring: (projectId, on) => call("POST", `${appProject(projectId)}/monitoring`, { on }),
12889
+ projectWatches: (projectId) => call("GET", `${appProject(projectId)}/watches`),
12890
+ projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
12891
+ projectChanges: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/changes${query(opts)}`)
12826
12892
  };
12827
12893
  }
12894
+ function query(params) {
12895
+ const q = new URLSearchParams;
12896
+ for (const [k, v] of Object.entries(params))
12897
+ if (v !== undefined && v !== null)
12898
+ q.set(k, String(v));
12899
+ const s = q.toString();
12900
+ return s ? `?${s}` : "";
12901
+ }
12828
12902
  var HOUR_MS = 3600000;
12829
12903
  var muteEnd = (hours) => new Date(Date.now() + Math.min(hours * HOUR_MS, 7 * 24 * HOUR_MS - 60000)).toISOString();
12830
12904
  async function watchContext(client, planId) {
@@ -20342,9 +20416,9 @@ function assertionsOf(output) {
20342
20416
  var FENCE = "```";
20343
20417
  var FOOTER = "Sentinel's findings are a starting point for your own review, not an audit sign-off.";
20344
20418
  var KIND_LINE = {
20345
- exploitable: "Exploitable (verified, re-verified, reproduced in the sandbox)",
20419
+ exploitable: "Reproducible (verified, re-verified, reproduced in the sandbox)",
20346
20420
  trust: "Trust assumption (watched: Sentinel alerts when it's used)",
20347
- worth_a_look: "Worth a look (not proven exploitable)",
20421
+ worth_a_look: "Worth a look (not reproduced)",
20348
20422
  refuted: "Refuted"
20349
20423
  };
20350
20424
  var tick = (s) => `\`${s}\``;
@@ -20435,16 +20509,16 @@ function issueMd(f, withCondition) {
20435
20509
  "## The issue",
20436
20510
  f.headline && f.headline !== f.title ? `${f.headline} (${f.title})` : f.title,
20437
20511
  ...f.blastRadius ? ["", `**Impact:** ${f.blastRadius}`] : [],
20438
- ...withCondition && f.precondition ? ["", `**Exploitable when:** ${f.precondition}`] : [],
20512
+ ...withCondition && f.precondition ? ["", `**Reproduces when:** ${f.precondition}`] : [],
20439
20513
  ""
20440
20514
  ];
20441
20515
  }
20442
20516
  function prelaunchBrief(f, ctx, kind) {
20443
- const kindLine = kind === "exploitable" ? "Exploitable · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
20517
+ const kindLine = kind === "exploitable" ? "Reproducible · look before launch" : kind === "trust" ? "Trust assumption" : "Worth a look before launch";
20444
20518
  return [
20445
20519
  "# Task: review a Sentinel finding on pre-launch code",
20446
20520
  "",
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.`,
20521
+ `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
20522
  "",
20449
20523
  "## Start here",
20450
20524
  `1. ${where(f, ctx)}`,
@@ -20476,7 +20550,7 @@ function trustBrief(f, ctx) {
20476
20550
  `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
20551
  "3. Only then discuss options that fit this codebase's design.",
20478
20552
  "",
20479
- ...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write an exploit for it."),
20553
+ ...reportBackMd(ctx, "- There is no PoC for a trust assumption. Don't write a PoC for it."),
20480
20554
  ...contextMd(f, ctx, "trust"),
20481
20555
  "- Class: trust assumption (a power held by design)",
20482
20556
  "- Evidence: checked by Sentinel's adversarial verifier. No PoC; nothing to run.",
@@ -20594,10 +20668,6 @@ function agentBrief(f, ctx) {
20594
20668
  var CLAIMED_NOTE = "Exploit detail is hidden until you verify control of this contract in the web app.";
20595
20669
  var REVERIFY_ESTIMATE = "about 10 min";
20596
20670
  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
20671
  var noteFor = (p) => p.access === "claimed" ? { note: CLAIMED_NOTE } : {};
20602
20672
  var ruleInput = discriminatedUnion("kind", [
20603
20673
  object({
@@ -20640,6 +20710,7 @@ var paidRun = (willDo) => ({
20640
20710
  function intentView(i) {
20641
20711
  const lead = i.rows[0];
20642
20712
  return {
20713
+ id: i.id,
20643
20714
  watch: i.title,
20644
20715
  subject: lead ? subjectName(lead.subject, lead.units) : i.title,
20645
20716
  kind: i.kind,
@@ -20685,6 +20756,14 @@ function buildTools(client) {
20685
20756
  const planId = string2().describe("Plan id, from list_contracts");
20686
20757
  const subject = string2().describe("A watched subject: a function name or a token symbol, from list_watches");
20687
20758
  const confirm = boolean2().optional().describe("Set true to actually do it. Without it you only get what it would do.");
20759
+ const projectId = string2().describe("Project id, from list_projects");
20760
+ const projectNamed = async (id) => {
20761
+ const { projects } = await client.projects();
20762
+ const p = projects.find((x) => x.id === id);
20763
+ if (!p)
20764
+ throw new SentinelError(404, `No project ${id}. Yours: ${projects.map((x) => `${x.name} (${x.id})`).join(", ") || "none"}`);
20765
+ return p;
20766
+ };
20688
20767
  return {
20689
20768
  list_contracts: {
20690
20769
  description: "Your claimed contracts: plan id, monitoring status, worst evidence.",
@@ -20750,13 +20829,19 @@ function buildTools(client) {
20750
20829
  }
20751
20830
  },
20752
20831
  get_run_status: {
20753
- description: "Running and finished follow-up runs on a plan, with the phase of each running one.",
20832
+ description: "Unfinished and finished follow-up runs on a plan, with the phase of each unfinished one (`queued` while it waits its turn).",
20754
20833
  input: { planId },
20755
20834
  run: async (a) => {
20756
20835
  const runs = await client.runs(a.planId);
20757
20836
  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 }))
20837
+ running: runs.filter((r) => runInProgress(r.status)).map((r) => ({
20838
+ id: r.id,
20839
+ kind: r.kind,
20840
+ status: r.status,
20841
+ startedAt: r.startedAt,
20842
+ phase: runPhase(r)
20843
+ })),
20844
+ finished: runs.filter((r) => !runInProgress(r.status)).map((r) => ({ id: r.id, kind: r.kind, status: r.status, finishedAt: r.finishedAt }))
20760
20845
  };
20761
20846
  }
20762
20847
  },
@@ -20831,6 +20916,45 @@ function buildTools(client) {
20831
20916
  });
20832
20917
  }
20833
20918
  },
20919
+ preview_repo_import: {
20920
+ 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.",
20921
+ input: {
20922
+ url: string2().describe("A public GitHub repo link")
20923
+ },
20924
+ run: async (a) => {
20925
+ const p = await client.previewImport(a.url);
20926
+ return {
20927
+ ...p,
20928
+ 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"}).`
20929
+ };
20930
+ }
20931
+ },
20932
+ import_repo: {
20933
+ 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.",
20934
+ input: {
20935
+ url: string2().describe("The repo link given to preview_repo_import"),
20936
+ commit: string2().regex(/^[0-9a-f]{40}$/).describe("The commit preview_repo_import returned: only that code is imported"),
20937
+ contracts: array(string2()).min(1).describe("Contract names from the preview"),
20938
+ project: string2().optional().describe('One of your project ids, or "new" (the default: a project named after the repo)'),
20939
+ confirm
20940
+ },
20941
+ run: async (a) => {
20942
+ const names = a.contracts;
20943
+ const target = a.project ?? "new";
20944
+ 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}.`;
20945
+ if (a.confirm !== true) {
20946
+ const b = await client.billing();
20947
+ 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.");
20948
+ }
20949
+ const r = await client.importRepo({
20950
+ url: a.url,
20951
+ commit: a.commit,
20952
+ contracts: names,
20953
+ project: a.project
20954
+ });
20955
+ return done(willDo, r.notStarted ? "Some plans didn't start." : "Started.", r);
20956
+ }
20957
+ },
20834
20958
  challenge: {
20835
20959
  description: "Re-verify one finding against your argument. Owners only. Without confirm it only describes what it will do.",
20836
20960
  input: {
@@ -20860,9 +20984,18 @@ function buildTools(client) {
20860
20984
  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
20985
  },
20862
20986
  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.")
20987
+ 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.",
20988
+ input: {
20989
+ planId,
20990
+ files: array(object({ path: string2(), content: string2() })).optional().describe("Your test files (path and TypeScript source); leave out to run the repo's suite"),
20991
+ confirm
20992
+ },
20993
+ run: async (a) => {
20994
+ const files = a.files;
20995
+ if (a.confirm !== true)
20996
+ 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.");
20997
+ return files?.length ? client.tests(a.planId, files) : client.testsFromRepo(a.planId);
20998
+ }
20866
20999
  },
20867
21000
  list_watches: {
20868
21001
  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 +21168,466 @@ function buildTools(client) {
21035
21168
  await client.stopMonitoring(await planOfContract(client, a.contractId));
21036
21169
  return done(willDo, "Stopped.");
21037
21170
  }
21171
+ },
21172
+ get_watch_activity: {
21173
+ 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.",
21174
+ input: {
21175
+ planId,
21176
+ watchId: string2().describe("An intent watch id, from list_watches"),
21177
+ window: union([literal(7), literal(90)]).optional().describe("Days, 7 (default) or 90"),
21178
+ filter: _enum(["over", "all"]).optional().describe("over: only what crossed a rule's line"),
21179
+ cursor: string2().optional().describe("nextCursor from a previous page"),
21180
+ asset: string2().optional().describe("Money out: which asset (an id from list_watches)")
21181
+ },
21182
+ run: async (a) => client.watchActivity(a.planId, a.watchId, {
21183
+ window: a.window,
21184
+ filter: a.filter,
21185
+ cursor: a.cursor,
21186
+ asset: a.asset
21187
+ })
21188
+ },
21189
+ list_functions: {
21190
+ 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).",
21191
+ input: { planId },
21192
+ run: async (a) => ({ functions: await client.functions(a.planId) })
21193
+ },
21194
+ get_function_activity: {
21195
+ 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).",
21196
+ input: { planId, fn: string2().describe("A public function name, from list_functions") },
21197
+ run: async (a) => client.functionActivity(a.planId, a.fn)
21198
+ },
21199
+ preview_alert: {
21200
+ 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.",
21201
+ input: { planId, example: _enum(["money_out", "finding"]).optional() },
21202
+ run: async (a) => client.alertPreview(a.planId, a.example)
21203
+ },
21204
+ set_threshold: {
21205
+ 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.",
21206
+ input: {
21207
+ planId,
21208
+ subject: string2().describe("A Money out asset: its token symbol, from list_watches"),
21209
+ amount: string2().regex(/^\d{1,30}$/).optional().describe("The new limit in the token's base units"),
21210
+ confirm
21211
+ },
21212
+ run: async (a) => {
21213
+ const ctx = await watchContext(client, a.planId);
21214
+ const g = resolveSubject(ctx.watches.filter((w) => w.subject.kind === "asset"), a.subject);
21215
+ const s = g.watches[0].subject;
21216
+ if (s.kind !== "asset")
21217
+ throw new SentinelError(400, `${a.subject} isn't an asset.`);
21218
+ const current = await client.threshold(ctx.planId, s.asset);
21219
+ const limit = (amount) => assetClause({
21220
+ asset: s.asset,
21221
+ units: g.watches[0].units,
21222
+ threshold: { ...current.threshold, amount }
21223
+ });
21224
+ if (a.amount === undefined)
21225
+ return { subject: g.name, limit: limit(current.threshold.amount), ...current };
21226
+ const willDo = `Change ${g.name}'s Money out limit: ${limit(current.threshold.amount)} becomes ${limit(a.amount)}.`;
21227
+ if (a.confirm !== true)
21228
+ return preview(willDo, "Live now.");
21229
+ const r = await client.setThreshold(ctx.planId, s.asset, a.amount);
21230
+ return done(willDo, "Live now.", { threshold: r.threshold });
21231
+ }
21232
+ },
21233
+ retry_history: {
21234
+ description: "Read a plan's watch history again after a failed read (activity says history: failed). Without confirm it only describes.",
21235
+ input: { planId, confirm },
21236
+ run: async (a) => {
21237
+ const willDo = "Read the history of this plan's watches again, in the background.";
21238
+ if (a.confirm !== true)
21239
+ return preview(willDo, "Starts now.");
21240
+ await client.retryHistory(a.planId);
21241
+ return done(willDo, "Started; activity fills in over the next few minutes.");
21242
+ }
21243
+ },
21244
+ mark_alert_read: {
21245
+ description: "Mark one alert read. Without confirm it only describes.",
21246
+ input: { alertId: string2().describe("Alert id, from list_alerts"), confirm },
21247
+ run: async (a) => {
21248
+ const willDo = `Mark alert ${a.alertId} read.`;
21249
+ if (a.confirm !== true)
21250
+ return preview(willDo, "Takes effect now.");
21251
+ await client.readAlert(a.alertId);
21252
+ return done(willDo, "Done.");
21253
+ }
21254
+ },
21255
+ mark_all_alerts_read: {
21256
+ description: "Mark every alert read. Without confirm it only describes.",
21257
+ input: { confirm },
21258
+ run: async (a) => {
21259
+ if (a.confirm !== true)
21260
+ return preview("Mark every alert read.", "Takes effect now.");
21261
+ const r = await client.readAllAlerts();
21262
+ return done("Mark every alert read.", `${r.count} marked read.`);
21263
+ }
21264
+ },
21265
+ ask_sentinel: {
21266
+ description: "Ask a question about Sentinel itself (how monitoring, plans or billing work), answered from its docs.",
21267
+ input: { question: string2().min(3).max(200) },
21268
+ run: async (a) => client.ask(a.question)
21269
+ },
21270
+ acknowledge_finding: {
21271
+ 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.",
21272
+ input: {
21273
+ planId,
21274
+ index: number2().int().min(0).describe("Finding index from get_plan"),
21275
+ reason: _enum(["accepted", "fixed", "disagree"]),
21276
+ note: string2().max(500).optional(),
21277
+ confirm
21278
+ },
21279
+ run: async (a) => {
21280
+ const f = await client.finding(a.planId, a.index);
21281
+ const reason = a.reason;
21282
+ const willDo = `Label "${f.title}" ${reason}. It changes no verdict.`;
21283
+ if (a.confirm !== true)
21284
+ return preview(willDo, "Your team sees it now.");
21285
+ const r = await client.acknowledge(a.planId, a.index, f.title, reason, a.note ?? null);
21286
+ return done(willDo, "Labelled.", r.watchId ? { suggestedWatchId: r.watchId } : {});
21287
+ }
21288
+ },
21289
+ reopen_finding: {
21290
+ description: "Put an acknowledged finding back on the list. Verified owners. Without confirm it only describes.",
21291
+ input: {
21292
+ planId,
21293
+ index: number2().int().min(0).describe("Finding index from get_plan"),
21294
+ confirm
21295
+ },
21296
+ run: async (a) => {
21297
+ const f = await client.finding(a.planId, a.index);
21298
+ const willDo = `Put "${f.title}" back on the list.`;
21299
+ if (a.confirm !== true)
21300
+ return preview(willDo, "Takes effect now.");
21301
+ await client.reopen(a.planId, a.index, f.title);
21302
+ return done(willDo, "Reopened.");
21303
+ }
21304
+ },
21305
+ get_ack_history: {
21306
+ description: "Every acknowledgement change on a plan's contract, oldest first. Verified owners.",
21307
+ input: { planId },
21308
+ run: async (a) => ({ events: await client.ackHistory(a.planId) })
21309
+ },
21310
+ list_notes: {
21311
+ description: "Your team's notes on one finding (and Sentinel's, when a later check reopened it). Verified owners.",
21312
+ input: { planId, index: number2().int().min(0).describe("Finding index from get_plan") },
21313
+ run: async (a) => ({ notes: await client.notes(a.planId, a.index) })
21314
+ },
21315
+ add_note: {
21316
+ description: "Add a note to one finding. Verified owners. Without confirm it only describes.",
21317
+ input: {
21318
+ planId,
21319
+ index: number2().int().min(0).describe("Finding index from get_plan"),
21320
+ body: string2().min(1).max(2000),
21321
+ confirm
21322
+ },
21323
+ run: async (a) => {
21324
+ const f = await client.finding(a.planId, a.index);
21325
+ const willDo = `Add a note to "${f.title}".`;
21326
+ if (a.confirm !== true)
21327
+ return preview(willDo, "Your team sees it now.");
21328
+ const r = await client.addNote(a.planId, a.index, f.title, a.body);
21329
+ return done(willDo, "Noted.", { note: r.note });
21330
+ }
21331
+ },
21332
+ run_fuzz: {
21333
+ 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.",
21334
+ input: {
21335
+ planId,
21336
+ invariants: string2().min(10).describe("Clarity define-… invariant or property functions"),
21337
+ confirm
21338
+ },
21339
+ run: async (a) => {
21340
+ const check = await client.fuzzCheck(a.planId, a.invariants);
21341
+ if (!check.ok)
21342
+ return { started: false, compiles: false, message: check.message ?? null };
21343
+ if (a.confirm !== true)
21344
+ return {
21345
+ ...paidRun("Fuzz the contract against these invariants in the airgapped sandbox."),
21346
+ compiles: true
21347
+ };
21348
+ return client.fuzz(a.planId, a.invariants);
21349
+ }
21350
+ },
21351
+ get_latest_commit: {
21352
+ 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).",
21353
+ input: { planId },
21354
+ run: async (a) => client.latestCommit(a.planId)
21355
+ },
21356
+ update_plan: {
21357
+ 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.",
21358
+ input: {
21359
+ planId,
21360
+ commit: string2().regex(/^[0-9a-f]{40}$/).optional(),
21361
+ confirm
21362
+ },
21363
+ run: async (a) => {
21364
+ const latest = await client.latestCommit(a.planId);
21365
+ const willDo = `Plan ${a.commit ? `commit ${a.commit.slice(0, 7)}` : "the branch's latest commit"}.`;
21366
+ if (a.confirm !== true)
21367
+ return {
21368
+ ...preview(willDo, "free" in latest && latest.free ? "Free: monitoring is on." : PAID_RUN_NOTE),
21369
+ latest
21370
+ };
21371
+ return client.updatePlan(a.planId, a.commit);
21372
+ }
21373
+ },
21374
+ set_auto_plan: {
21375
+ 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.",
21376
+ input: { planId, auto: boolean2(), confirm },
21377
+ run: async (a) => {
21378
+ const willDo = a.auto ? "Plan this on its own once every contract it waits on is deployed." : "Don't plan this on its own.";
21379
+ if (a.confirm !== true)
21380
+ return preview(willDo, "Takes effect now.");
21381
+ return done(willDo, "Saved.", await client.setAutoPlan(a.planId, a.auto === true));
21382
+ }
21383
+ },
21384
+ link_dependency: {
21385
+ 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.",
21386
+ input: {
21387
+ planId,
21388
+ contract: string2().describe("The contract the plan waits on (from get_plan)"),
21389
+ url: string2().nullable(),
21390
+ confirm
21391
+ },
21392
+ run: async (a) => {
21393
+ const willDo = a.url === null ? `Clear the code linked for ${a.contract}.` : `Link ${a.url} as the code of ${a.contract}.`;
21394
+ if (a.confirm !== true)
21395
+ return preview(willDo, "Takes effect now.");
21396
+ return done(willDo, "Saved.", await client.setLink(a.planId, a.contract, a.url));
21397
+ }
21398
+ },
21399
+ plan_with_links: {
21400
+ 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.",
21401
+ input: { planId, confirm },
21402
+ run: async (a) => a.confirm === true ? client.planLinked(a.planId) : paidRun("Plan this contract now with the linked code.")
21403
+ },
21404
+ rename_contract: {
21405
+ description: "Your own name for a plan's contract; an empty name clears it. Owners only. Without confirm it only describes.",
21406
+ input: { planId, name: string2().max(60), confirm },
21407
+ run: async (a) => {
21408
+ const name = a.name.trim();
21409
+ const willDo = name ? `Call this contract "${name}".` : "Clear your name for this contract.";
21410
+ if (a.confirm !== true)
21411
+ return preview(willDo, "Takes effect now.");
21412
+ return done(willDo, "Saved.", await client.renameContract(a.planId, name));
21413
+ }
21414
+ },
21415
+ remove_contract: {
21416
+ 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.",
21417
+ input: {
21418
+ planId,
21419
+ name: string2().optional().describe("The contract's name, typed to confirm"),
21420
+ confirm
21421
+ },
21422
+ run: async (a) => {
21423
+ const p = await client.plan(a.planId);
21424
+ const willDo = `Take ${p.contractId} off your account: its monitoring, alerts and claim go.`;
21425
+ if (a.confirm !== true || a.name === undefined)
21426
+ return {
21427
+ ...preview(willDo, "Can't be undone."),
21428
+ confirmWith: `name: "${contractName(p.contractId)}"`
21429
+ };
21430
+ return done(willDo, "Removed.", await client.removeContract(a.planId, a.name));
21431
+ }
21432
+ },
21433
+ list_projects: {
21434
+ 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).",
21435
+ input: {},
21436
+ run: async () => client.projects()
21437
+ },
21438
+ get_project: {
21439
+ description: "One project: each contract (added: you claimed it, with its plan; found: one Sentinel found in your code), its role and monitoring.",
21440
+ input: { projectId },
21441
+ run: async (a) => client.project(a.projectId)
21442
+ },
21443
+ get_project_overview: {
21444
+ description: "One project with each claimed contract's plan (as get_plan) and, with watches: true, its watches (as list_watches), in one read.",
21445
+ input: { projectId, watches: boolean2().optional() },
21446
+ run: async (a) => client.projectOverview(a.projectId, { watches: a.watches === true })
21447
+ },
21448
+ rename_project: {
21449
+ description: "Rename a project. Without confirm it only describes.",
21450
+ input: { projectId, name: string2().min(1).max(60), confirm },
21451
+ run: async (a) => {
21452
+ const willDo = `Rename project ${a.projectId} to "${a.name.trim()}".`;
21453
+ if (a.confirm !== true)
21454
+ return preview(willDo, "Takes effect now.");
21455
+ return done(willDo, "Renamed.", await client.renameProject(a.projectId, a.name));
21456
+ }
21457
+ },
21458
+ merge_project: {
21459
+ 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.",
21460
+ input: {
21461
+ projectId,
21462
+ into: string2().describe("The project to merge into, an id from list_projects"),
21463
+ confirm
21464
+ },
21465
+ run: async (a) => {
21466
+ const from = await projectNamed(a.projectId);
21467
+ const to = await projectNamed(a.into);
21468
+ const willDo = `Move every contract of ${from.name} (${from.contracts.length}) into ${to.name}; ${from.name} goes.`;
21469
+ if (a.confirm !== true)
21470
+ return preview(willDo, "Takes effect now.");
21471
+ return done(willDo, "Merged.", await client.mergeProject(from.id, to.id));
21472
+ }
21473
+ },
21474
+ answer_merge_suggestion: {
21475
+ description: "Accept (merge `from` into `into`) or dismiss a merge Sentinel suggested, from list_projects' suggestions. Without confirm it only describes.",
21476
+ input: {
21477
+ from: string2().describe("The suggestion's from project id"),
21478
+ into: string2().describe("The suggestion's into project id"),
21479
+ accept: boolean2(),
21480
+ confirm
21481
+ },
21482
+ run: async (a) => {
21483
+ const r = await client.projects();
21484
+ if (!r.suggestions.some((s) => s.from === a.from && s.into === a.into))
21485
+ throw new SentinelError(404, `No open suggestion to merge ${a.from} into ${a.into}.`);
21486
+ const nameOf = (id) => r.projects.find((p) => p.id === id)?.name ?? String(id);
21487
+ 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)}.`;
21488
+ if (a.confirm !== true)
21489
+ return preview(willDo, "Takes effect now.");
21490
+ return done(willDo, "Done.", await client.answerMergeSuggestion(a.from, a.into, a.accept === true));
21491
+ }
21492
+ },
21493
+ move_project_contract: {
21494
+ 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.',
21495
+ input: { projectId, contractId: string2(), into: string2(), confirm },
21496
+ run: async (a) => {
21497
+ const to = a.into === "new" ? "a new project named after it" : (await projectNamed(a.into)).name;
21498
+ const willDo = `Move ${a.contractId} to ${to}.`;
21499
+ if (a.confirm !== true)
21500
+ return preview(willDo, "Takes effect now.");
21501
+ return done(willDo, "Moved.", await client.moveProjectContract(a.projectId, a.contractId, a.into));
21502
+ }
21503
+ },
21504
+ remove_project_contract: {
21505
+ 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.",
21506
+ input: {
21507
+ projectId,
21508
+ contractId: string2(),
21509
+ name: string2().optional().describe("A contract you added: its name, typed to confirm"),
21510
+ confirm
21511
+ },
21512
+ run: async (a) => {
21513
+ const member = (await client.project(a.projectId)).contracts.find((c) => c.contractId === a.contractId);
21514
+ if (!member)
21515
+ throw new SentinelError(404, `${a.contractId} isn't in project ${a.projectId}.`);
21516
+ const added = member.origin === "added";
21517
+ 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).`;
21518
+ if (a.confirm !== true || added && a.name === undefined)
21519
+ return {
21520
+ ...preview(willDo, added ? "Can't be undone." : "Takes effect now."),
21521
+ ...added ? { confirmWith: `name: "${contractName(member.contractId)}"` } : {}
21522
+ };
21523
+ return done(willDo, "Removed.", await client.removeProjectContract(a.projectId, a.contractId, added ? a.name : undefined));
21524
+ }
21525
+ },
21526
+ remove_project: {
21527
+ 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.",
21528
+ input: {
21529
+ projectId,
21530
+ name: string2().optional().describe("The project's name, typed to confirm"),
21531
+ confirm
21532
+ },
21533
+ run: async (a) => {
21534
+ const p = await client.project(a.projectId);
21535
+ const added = p.contracts.filter((c) => c.origin === "added").length;
21536
+ const willDo = `Remove ${p.name} and the ${added} contract(s) you added in it.`;
21537
+ if (a.confirm !== true || a.name === undefined)
21538
+ return { ...preview(willDo, "Can't be undone."), confirmWith: `name: "${p.name}"` };
21539
+ return done(willDo, "Removed.", await client.removeProject(p.id, a.name));
21540
+ }
21541
+ },
21542
+ add_project_contract: {
21543
+ 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.',
21544
+ input: {
21545
+ projectId,
21546
+ contractId: string2(),
21547
+ into: string2().optional().describe('A project id from list_projects, or "new"'),
21548
+ confirm
21549
+ },
21550
+ run: async (a) => {
21551
+ const into = a.into ?? a.projectId;
21552
+ if (a.confirm !== true) {
21553
+ const [p, b] = await Promise.all([
21554
+ client.previewProjectContract(a.projectId, a.contractId),
21555
+ client.billing()
21556
+ ]);
21557
+ return {
21558
+ ...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."),
21559
+ suggestion: p.suggestion
21560
+ };
21561
+ }
21562
+ const r = await client.addProjectContract(a.projectId, a.contractId, into === a.projectId ? {} : { project: into });
21563
+ return done(`Plan ${a.contractId}.`, r.cached ? "Already planned." : "Started.", r);
21564
+ }
21565
+ },
21566
+ list_project_watches: {
21567
+ 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).",
21568
+ input: { projectId },
21569
+ run: async (a) => client.projectWatches(a.projectId)
21570
+ },
21571
+ get_project_watch_activity: {
21572
+ description: "One project watch's activity on one member (`contract`, default the first that holds it): counts and the latest events.",
21573
+ input: {
21574
+ projectId,
21575
+ watchId: string2().describe("A watch id from list_project_watches"),
21576
+ contract: string2().optional(),
21577
+ window: union([literal(7), literal(90)]).optional()
21578
+ },
21579
+ run: async (a) => client.projectActivity(a.projectId, a.watchId, {
21580
+ contract: a.contract,
21581
+ window: a.window
21582
+ })
21583
+ },
21584
+ list_project_changes: {
21585
+ description: 'The account\'s changes to what Sentinel watches across a project, newest first: each watch turned on or off, rule edits (rule and prevRule, the one it replaced), mutes and cooldowns, with how each was made (source) and when. `watch` keeps one watch (an id from list_project_watches); filter "mutes" keeps mutes and cooldowns; pass `next` back as cursor for older ones.',
21586
+ input: {
21587
+ projectId,
21588
+ watch: string2().optional(),
21589
+ filter: literal("mutes").optional(),
21590
+ limit: number2().int().min(1).max(200).optional(),
21591
+ cursor: string2().optional()
21592
+ },
21593
+ run: async (a) => client.projectChanges(a.projectId, {
21594
+ watch: a.watch,
21595
+ filter: a.filter,
21596
+ limit: a.limit,
21597
+ cursor: a.cursor
21598
+ })
21599
+ },
21600
+ set_project_destinations: {
21601
+ 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.",
21602
+ input: {
21603
+ projectId,
21604
+ email: boolean2().optional(),
21605
+ webhookUrl: string2().optional(),
21606
+ slackUrl: string2().optional(),
21607
+ confirm
21608
+ },
21609
+ run: async (a) => {
21610
+ const dest = {
21611
+ ...a.email === undefined ? {} : { email: a.email },
21612
+ ...a.webhookUrl ? { webhookUrl: a.webhookUrl } : {},
21613
+ ...a.slackUrl ? { slackUrl: a.slackUrl } : {}
21614
+ };
21615
+ 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.`;
21616
+ if (a.confirm !== true)
21617
+ return preview(willDo, "Takes effect now.");
21618
+ const r = await client.setProjectDestinations(a.projectId, dest);
21619
+ return done(willDo, "Saved on each contract; a new webhook secret is shown once.", r);
21620
+ }
21621
+ },
21622
+ set_project_monitoring: {
21623
+ description: "Turn monitoring on or off for every contract you added in a project. Monitored events are billed. Without confirm it only describes.",
21624
+ input: { projectId, on: boolean2(), confirm },
21625
+ run: async (a) => {
21626
+ const willDo = `Turn monitoring ${a.on ? "on" : "off"} for every contract you added in this project.`;
21627
+ if (a.confirm !== true)
21628
+ return preview(willDo, a.on ? "Live now; events are billed." : "Off now.");
21629
+ return done(willDo, "Done.", await client.setProjectMonitoring(a.projectId, a.on === true));
21630
+ }
21038
21631
  }
21039
21632
  };
21040
21633
  }
@@ -21049,7 +21642,7 @@ async function runTool(tool, args) {
21049
21642
  }
21050
21643
  }
21051
21644
  function buildServer(client) {
21052
- const server = new McpServer({ name: "sentinel", version: "0.4.0" });
21645
+ const server = new McpServer({ name: "sentinel", version: "0.6.0" });
21053
21646
  for (const [name, tool] of Object.entries(buildTools(client))) {
21054
21647
  server.registerTool(name, { description: tool.description, inputSchema: tool.input }, (args) => runTool(tool, args));
21055
21648
  }
@@ -21064,6 +21657,7 @@ if (!credential) {
21064
21657
  }
21065
21658
  var client = createClient({
21066
21659
  token: credential.token,
21067
- baseUrl: process.env.SENTINEL_API_URL
21660
+ baseUrl: process.env.SENTINEL_API_URL,
21661
+ client: "mcp"
21068
21662
  });
21069
21663
  await buildServer(client).connect(new StdioServerTransport);