@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/README.md +2 -1
- package/dist/cli.js +842 -28
- package/dist/index.js +96 -2
- package/dist/mcp.js +614 -20
- package/dist/sentinel-client.d.ts +483 -2
- package/package.json +1 -1
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 = () => ({
|
|
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: "
|
|
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
|
|
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 ? ["", `**
|
|
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" ? "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
20759
|
-
|
|
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
|
|
20864
|
-
input: {
|
|
20865
|
-
|
|
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.
|
|
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);
|