sentinel-verify 1.0.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/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/cli/commands/hook.js +133 -0
- package/dist/cli/commands/hook.js.map +1 -0
- package/dist/cli/commands/init.js +21 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/testHallucination.js +35 -0
- package/dist/cli/commands/testHallucination.js.map +1 -0
- package/dist/cli/commands/testSecurity.js +35 -0
- package/dist/cli/commands/testSecurity.js.map +1 -0
- package/dist/cli/commands/testStyle.js +41 -0
- package/dist/cli/commands/testStyle.js.map +1 -0
- package/dist/cli/commands/testVerify.js +36 -0
- package/dist/cli/commands/testVerify.js.map +1 -0
- package/dist/cli/diffInput.js +17 -0
- package/dist/cli/diffInput.js.map +1 -0
- package/dist/cli/index.js +93 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/env.js +16 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/sentinelrc.js +104 -0
- package/dist/config/sentinelrc.js.map +1 -0
- package/dist/dashboard/page.js +270 -0
- package/dist/dashboard/page.js.map +1 -0
- package/dist/dashboard/server.js +65 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/mcp/server.js +141 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/providers/anthropic.js +72 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/index.js +37 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.js +102 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/storage/db.js +123 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/verify/combine.js +42 -0
- package/dist/verify/combine.js.map +1 -0
- package/dist/verify/diff.js +27 -0
- package/dist/verify/diff.js.map +1 -0
- package/dist/verify/evidence.js +25 -0
- package/dist/verify/evidence.js.map +1 -0
- package/dist/verify/hallucination.js +299 -0
- package/dist/verify/hallucination.js.map +1 -0
- package/dist/verify/security.js +274 -0
- package/dist/verify/security.js.map +1 -0
- package/dist/verify/style.js +489 -0
- package/dist/verify/style.js.map +1 -0
- package/dist/verify/taskMatch.js +107 -0
- package/dist/verify/taskMatch.js.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Purpose: Sentinel's MCP server — in agent mode the tools return deterministic EVIDENCE for the calling agent to judge; Sentinel makes no model calls here.
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { SENTINEL_VERSION } from "../version.js";
|
|
6
|
+
import { gatherTaskMatchEvidence, TOOL_NAME } from "../verify/taskMatch.js";
|
|
7
|
+
import { gatherHallucinationEvidence, HALLUCINATION_TOOL_NAME } from "../verify/hallucination.js";
|
|
8
|
+
import { gatherSecurityEvidence, SECURITY_TOOL_NAME } from "../verify/security.js";
|
|
9
|
+
import { gatherStyleEvidence, STYLE_TOOL_NAME } from "../verify/style.js";
|
|
10
|
+
import { loadLocalEnv } from "../config/env.js";
|
|
11
|
+
import { loadSentinelrc, isToolEnabled } from "../config/sentinelrc.js";
|
|
12
|
+
export const SERVER_NAME = "sentinel";
|
|
13
|
+
const diffInput = z
|
|
14
|
+
.string()
|
|
15
|
+
.describe("The code diff to check (unified diff format, e.g. output of `git diff`).");
|
|
16
|
+
// Shared evidence fields, present on every tool's output.
|
|
17
|
+
const evidenceBaseSchema = {
|
|
18
|
+
mode: z.literal("evidence"),
|
|
19
|
+
instruction: z.string(),
|
|
20
|
+
projectContext: z.string(),
|
|
21
|
+
projectRules: z.array(z.string()),
|
|
22
|
+
};
|
|
23
|
+
const findingSchema = z.object({
|
|
24
|
+
location: z.string(),
|
|
25
|
+
reason: z.string(),
|
|
26
|
+
source: z.literal("static"),
|
|
27
|
+
});
|
|
28
|
+
/** Wraps a synchronous evidence function into an MCP handler with error reporting. */
|
|
29
|
+
function evidenceHandler(toolName, gather) {
|
|
30
|
+
return async (args) => {
|
|
31
|
+
try {
|
|
32
|
+
const evidence = gather(args);
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: JSON.stringify(evidence, null, 2) }],
|
|
35
|
+
// The cast is safe: every evidence payload is a plain JSON object.
|
|
36
|
+
structuredContent: { ...evidence },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `${toolName} failed: ${message}` }],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Starts the MCP server on stdio and keeps running until the client disconnects.
|
|
50
|
+
*
|
|
51
|
+
* Agent-native design: an MCP client is itself a capable model, so Sentinel
|
|
52
|
+
* returns structured evidence and the judgment instruction instead of calling
|
|
53
|
+
* a second model. No provider (Ollama/Anthropic) is ever contacted from here.
|
|
54
|
+
*
|
|
55
|
+
* Important: when speaking MCP over stdio, stdout belongs to the protocol.
|
|
56
|
+
* Any human-facing logging from this process must go to stderr.
|
|
57
|
+
*/
|
|
58
|
+
export async function startMcpServer() {
|
|
59
|
+
// The MCP client launches this process in the project directory.
|
|
60
|
+
loadLocalEnv(process.cwd());
|
|
61
|
+
// Fail fast on a malformed .sentinelrc — a broken config must never be
|
|
62
|
+
// silently ignored. Tools disabled there are not registered at all.
|
|
63
|
+
const config = loadSentinelrc(process.cwd());
|
|
64
|
+
const server = new McpServer({
|
|
65
|
+
name: SERVER_NAME,
|
|
66
|
+
version: SENTINEL_VERSION,
|
|
67
|
+
});
|
|
68
|
+
const enabledTools = [];
|
|
69
|
+
if (isToolEnabled(config, TOOL_NAME)) {
|
|
70
|
+
enabledTools.push(TOOL_NAME);
|
|
71
|
+
server.registerTool(TOOL_NAME, {
|
|
72
|
+
title: "Verify task match (evidence)",
|
|
73
|
+
description: "Call this after making changes to self-check that the diff actually accomplishes the stated task. " +
|
|
74
|
+
"Returns evidence (diff statistics, project context and rules) plus an instruction — YOU judge " +
|
|
75
|
+
"whether the diff functionally matches the task; Sentinel does not run its own model in agent mode.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
task: z
|
|
78
|
+
.string()
|
|
79
|
+
.describe("The task or intent the change was supposed to accomplish, in plain language."),
|
|
80
|
+
diff: diffInput,
|
|
81
|
+
},
|
|
82
|
+
outputSchema: {
|
|
83
|
+
...evidenceBaseSchema,
|
|
84
|
+
diffFiles: z.array(z.string()),
|
|
85
|
+
addedLineCount: z.number(),
|
|
86
|
+
},
|
|
87
|
+
}, evidenceHandler(TOOL_NAME, ({ task, diff }) => gatherTaskMatchEvidence(process.cwd(), task, diff)));
|
|
88
|
+
}
|
|
89
|
+
if (isToolEnabled(config, HALLUCINATION_TOOL_NAME)) {
|
|
90
|
+
enabledTools.push(HALLUCINATION_TOOL_NAME);
|
|
91
|
+
server.registerTool(HALLUCINATION_TOOL_NAME, {
|
|
92
|
+
title: "Check for hallucinations (evidence)",
|
|
93
|
+
description: "Call this after making changes to catch hallucinated code. Returns filesystem-verified evidence: " +
|
|
94
|
+
"imported packages missing from package.json/node_modules, imported files or symbols that do not " +
|
|
95
|
+
"exist (JS/TS), plus the declared dependency list. The staticFindings are facts — combine them with " +
|
|
96
|
+
"your own judgment of API usage per the instruction in the response.",
|
|
97
|
+
inputSchema: { diff: diffInput },
|
|
98
|
+
outputSchema: {
|
|
99
|
+
...evidenceBaseSchema,
|
|
100
|
+
staticFindings: z.array(findingSchema),
|
|
101
|
+
declaredDependencies: z.array(z.string()).nullable(),
|
|
102
|
+
},
|
|
103
|
+
}, evidenceHandler(HALLUCINATION_TOOL_NAME, ({ diff }) => gatherHallucinationEvidence(process.cwd(), diff)));
|
|
104
|
+
}
|
|
105
|
+
if (isToolEnabled(config, SECURITY_TOOL_NAME)) {
|
|
106
|
+
enabledTools.push(SECURITY_TOOL_NAME);
|
|
107
|
+
server.registerTool(SECURITY_TOOL_NAME, {
|
|
108
|
+
title: "Check for security risks (evidence)",
|
|
109
|
+
description: "Call this after making changes to catch security risks. Returns deterministic evidence: leaked " +
|
|
110
|
+
"secrets (masked — API keys, tokens, private keys, connection strings) and unsafe patterns (eval, " +
|
|
111
|
+
"shell injection, disabled TLS verification, hardcoded credentials). Combine the staticFindings with " +
|
|
112
|
+
"your own judgment of context-dependent risks per the instruction in the response.",
|
|
113
|
+
inputSchema: { diff: diffInput },
|
|
114
|
+
outputSchema: {
|
|
115
|
+
...evidenceBaseSchema,
|
|
116
|
+
staticFindings: z.array(findingSchema),
|
|
117
|
+
},
|
|
118
|
+
}, evidenceHandler(SECURITY_TOOL_NAME, ({ diff }) => gatherSecurityEvidence(process.cwd(), diff)));
|
|
119
|
+
}
|
|
120
|
+
if (isToolEnabled(config, STYLE_TOOL_NAME)) {
|
|
121
|
+
enabledTools.push(STYLE_TOOL_NAME);
|
|
122
|
+
server.registerTool(STYLE_TOOL_NAME, {
|
|
123
|
+
title: "Check style consistency (evidence)",
|
|
124
|
+
description: "Call this after making changes to check the diff against this project's real conventions. Returns " +
|
|
125
|
+
"the measured style profile (indentation, quotes, semicolons, naming, module style — possibly pinned " +
|
|
126
|
+
"by .sentinelrc) and deterministic diff-vs-profile findings. Judge unmeasured dimensions yourself " +
|
|
127
|
+
"per the instruction in the response.",
|
|
128
|
+
inputSchema: { diff: diffInput },
|
|
129
|
+
outputSchema: {
|
|
130
|
+
...evidenceBaseSchema,
|
|
131
|
+
profile: z.string(),
|
|
132
|
+
staticFindings: z.array(findingSchema),
|
|
133
|
+
},
|
|
134
|
+
}, evidenceHandler(STYLE_TOOL_NAME, ({ diff }) => gatherStyleEvidence(process.cwd(), diff)));
|
|
135
|
+
}
|
|
136
|
+
const transport = new StdioServerTransport();
|
|
137
|
+
await server.connect(transport);
|
|
138
|
+
console.error(`[sentinel] MCP server running on stdio (${SERVER_NAME} v${SENTINEL_VERSION}, agent-native evidence mode). ` +
|
|
139
|
+
`Tools: ${enabledTools.join(", ") || "(all disabled by .sentinelrc)"}`);
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Purpose: Anthropic implementation of the LlmProvider interface — talks to the user's own Anthropic account via the official SDK.
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
|
|
4
|
+
/** Default verification model. Override with the SENTINEL_MODEL environment variable. */
|
|
5
|
+
const DEFAULT_MODEL = "claude-opus-4-8";
|
|
6
|
+
export class AnthropicProvider {
|
|
7
|
+
name = "anthropic";
|
|
8
|
+
model;
|
|
9
|
+
client;
|
|
10
|
+
constructor(model) {
|
|
11
|
+
this.model = model ?? DEFAULT_MODEL;
|
|
12
|
+
// The zero-argument constructor resolves credentials from the environment:
|
|
13
|
+
// ANTHROPIC_API_KEY (including one loaded from the project's .env file),
|
|
14
|
+
// or a locally stored Anthropic auth profile if one exists.
|
|
15
|
+
this.client = new Anthropic();
|
|
16
|
+
}
|
|
17
|
+
async completeStructured(request) {
|
|
18
|
+
let response;
|
|
19
|
+
try {
|
|
20
|
+
response = await this.client.messages.parse({
|
|
21
|
+
model: this.model,
|
|
22
|
+
max_tokens: 16000,
|
|
23
|
+
thinking: { type: "adaptive" },
|
|
24
|
+
system: request.system,
|
|
25
|
+
messages: [{ role: "user", content: request.prompt }],
|
|
26
|
+
output_config: {
|
|
27
|
+
format: zodOutputFormat(request.schema),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(friendlyApiError(error), { cause: error });
|
|
33
|
+
}
|
|
34
|
+
if (response.stop_reason === "refusal") {
|
|
35
|
+
throw new Error("The model declined to process this request (safety refusal). Try rephrasing the task description.");
|
|
36
|
+
}
|
|
37
|
+
if (response.parsed_output == null) {
|
|
38
|
+
throw new Error(`The model did not return a valid structured answer (stop reason: ${response.stop_reason}).`);
|
|
39
|
+
}
|
|
40
|
+
// The SDK validated the output against request.schema; the generic just
|
|
41
|
+
// doesn't survive the indirection through our provider interface.
|
|
42
|
+
return response.parsed_output;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Translates SDK errors into messages a user can act on, most specific first. */
|
|
46
|
+
function friendlyApiError(error) {
|
|
47
|
+
// No credentials at all — the SDK raises a plain Error for this, so we match its message.
|
|
48
|
+
if (error instanceof Error &&
|
|
49
|
+
error.message.startsWith("Could not resolve authentication method")) {
|
|
50
|
+
return ("No Anthropic API credentials found. Set ANTHROPIC_API_KEY in your shell, " +
|
|
51
|
+
"or put ANTHROPIC_API_KEY=sk-ant-... in a .env file in your project root " +
|
|
52
|
+
"(Sentinel loads it automatically and it stays out of git).");
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof Anthropic.AuthenticationError) {
|
|
55
|
+
return ("Anthropic rejected the API credentials. Check that your ANTHROPIC_API_KEY " +
|
|
56
|
+
"is valid and has not been revoked, then try again.");
|
|
57
|
+
}
|
|
58
|
+
if (error instanceof Anthropic.NotFoundError) {
|
|
59
|
+
return `Model not found. Check the SENTINEL_MODEL value (currently requested model was rejected by the API).`;
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof Anthropic.RateLimitError) {
|
|
62
|
+
return "Anthropic rate limit reached. Wait a moment and try again.";
|
|
63
|
+
}
|
|
64
|
+
if (error instanceof Anthropic.APIConnectionError) {
|
|
65
|
+
return "Could not reach the Anthropic API. Check your network connection.";
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof Anthropic.APIError) {
|
|
68
|
+
return `Anthropic API error (${error.status ?? "unknown status"}): ${error.message}`;
|
|
69
|
+
}
|
|
70
|
+
return error instanceof Error ? error.message : String(error);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Purpose: picks the LLM provider — free local Ollama by default; Anthropic only when the user explicitly provides an API key or sets SENTINEL_PROVIDER.
|
|
2
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
3
|
+
import { OllamaProvider } from "./ollama.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the provider to use for verification calls.
|
|
6
|
+
*
|
|
7
|
+
* Selection order:
|
|
8
|
+
* 1. SENTINEL_PROVIDER, if set ("ollama" or "anthropic"; "openai" is planned).
|
|
9
|
+
* 2. Otherwise, ANTHROPIC_API_KEY present -> Anthropic (opt-in via the key).
|
|
10
|
+
* 3. Otherwise -> Ollama: fully local, fully free, no key required.
|
|
11
|
+
*
|
|
12
|
+
* Other variables:
|
|
13
|
+
* - SENTINEL_MODEL: optional model override for whichever provider is chosen.
|
|
14
|
+
* - OLLAMA_HOST: where Ollama listens (default http://127.0.0.1:11434).
|
|
15
|
+
*/
|
|
16
|
+
export function resolveProvider() {
|
|
17
|
+
const requested = process.env.SENTINEL_PROVIDER?.toLowerCase();
|
|
18
|
+
const model = process.env.SENTINEL_MODEL;
|
|
19
|
+
if (requested) {
|
|
20
|
+
switch (requested) {
|
|
21
|
+
case "ollama":
|
|
22
|
+
return new OllamaProvider(model);
|
|
23
|
+
case "anthropic":
|
|
24
|
+
return new AnthropicProvider(model);
|
|
25
|
+
case "openai":
|
|
26
|
+
throw new Error('Provider "openai" is planned but not implemented yet. ' +
|
|
27
|
+
"Unset SENTINEL_PROVIDER to use free local Ollama, or set it to \"anthropic\".");
|
|
28
|
+
default:
|
|
29
|
+
throw new Error(`Unknown provider "${requested}". Supported values for SENTINEL_PROVIDER: ollama, anthropic.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
33
|
+
return new AnthropicProvider(model);
|
|
34
|
+
}
|
|
35
|
+
return new OllamaProvider(model);
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,yJAAyJ;AAGzJ,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,WAAW,EAAE,CAAC;IAC/D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IAEzC,IAAI,SAAS,EAAE,CAAC;QACd,QAAQ,SAAS,EAAE,CAAC;YAClB,KAAK,QAAQ;gBACX,OAAO,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC;YACnC,KAAK,WAAW;gBACd,OAAO,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC;YACtC,KAAK,QAAQ;gBACX,MAAM,IAAI,KAAK,CACb,wDAAwD;oBACtD,+EAA+E,CAClF,CAAC;YACJ;gBACE,MAAM,IAAI,KAAK,CACb,qBAAqB,SAAS,+DAA+D,CAC9F,CAAC;QACN,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC;QAClC,OAAO,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Purpose: Ollama implementation of the LlmProvider interface — fully local, fully free verification with no API key.
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
/** Default local model. Recommended: code-tuned and small enough to download quickly. */
|
|
4
|
+
export const DEFAULT_OLLAMA_MODEL = "qwen2.5-coder";
|
|
5
|
+
const DEFAULT_HOST = "http://127.0.0.1:11434";
|
|
6
|
+
/** Generous ceiling for one verification on slow hardware. */
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 150_000;
|
|
8
|
+
export class OllamaProvider {
|
|
9
|
+
name = "ollama";
|
|
10
|
+
model;
|
|
11
|
+
host;
|
|
12
|
+
constructor(model) {
|
|
13
|
+
this.model = model ?? DEFAULT_OLLAMA_MODEL;
|
|
14
|
+
// OLLAMA_HOST follows Ollama's own convention and may omit the scheme.
|
|
15
|
+
let host = process.env.OLLAMA_HOST ?? DEFAULT_HOST;
|
|
16
|
+
if (!/^https?:\/\//.test(host)) {
|
|
17
|
+
host = `http://${host}`;
|
|
18
|
+
}
|
|
19
|
+
this.host = host.replace(/\/+$/, "");
|
|
20
|
+
}
|
|
21
|
+
async completeStructured(request) {
|
|
22
|
+
try {
|
|
23
|
+
return await this.requestOnce(request, 0);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Small local models occasionally produce malformed output — one retry
|
|
27
|
+
// with a little sampling variety usually recovers; then we give up.
|
|
28
|
+
if (error instanceof MalformedOutputError) {
|
|
29
|
+
return await this.requestOnce(request, 0.4);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async requestOnce(request, temperature) {
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await fetch(`${this.host}/api/chat`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
// Small models can get stuck generating forever under a schema
|
|
41
|
+
// constraint — cap the wait instead of hanging indefinitely.
|
|
42
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
model: this.model,
|
|
45
|
+
stream: false,
|
|
46
|
+
// Ollama constrains the output to this JSON schema.
|
|
47
|
+
format: z.toJSONSchema(request.schema),
|
|
48
|
+
// num_predict caps runaway generation; verdicts are short.
|
|
49
|
+
options: { temperature, num_predict: 2048 },
|
|
50
|
+
messages: [
|
|
51
|
+
{ role: "system", content: request.system },
|
|
52
|
+
{ role: "user", content: request.prompt },
|
|
53
|
+
],
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
59
|
+
throw new Error(`The local model "${this.model}" took longer than ${REQUEST_TIMEOUT_MS / 1000}s to answer. ` +
|
|
60
|
+
`Try again, or use a stronger model (e.g. ollama pull ${DEFAULT_OLLAMA_MODEL}).`, { cause: error });
|
|
61
|
+
}
|
|
62
|
+
throw new Error(unreachableMessage(this.host), { cause: error });
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const body = await response.text();
|
|
66
|
+
if (response.status === 404 && body.includes("not found")) {
|
|
67
|
+
throw new Error(`Ollama is running, but the model "${this.model}" is not installed. ` +
|
|
68
|
+
`Run: ollama pull ${this.model}`);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Ollama returned an error (HTTP ${response.status}): ${body}`);
|
|
71
|
+
}
|
|
72
|
+
const data = (await response.json());
|
|
73
|
+
const content = data.message?.content ?? "";
|
|
74
|
+
let json;
|
|
75
|
+
try {
|
|
76
|
+
json = JSON.parse(content);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new MalformedOutputError(`The local model "${this.model}" did not return valid JSON. ` +
|
|
80
|
+
`Smaller models occasionally do this — try again, or use a stronger model ` +
|
|
81
|
+
`(e.g. ollama pull ${DEFAULT_OLLAMA_MODEL}).`);
|
|
82
|
+
}
|
|
83
|
+
const parsed = request.schema.safeParse(json);
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
throw new MalformedOutputError(`The local model "${this.model}" returned JSON in an unexpected shape. ` +
|
|
86
|
+
`Try again, or use a stronger model (e.g. ollama pull ${DEFAULT_OLLAMA_MODEL}).`);
|
|
87
|
+
}
|
|
88
|
+
return parsed.data;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The model answered, but not with usable JSON — worth exactly one retry. */
|
|
92
|
+
class MalformedOutputError extends Error {
|
|
93
|
+
}
|
|
94
|
+
function unreachableMessage(host) {
|
|
95
|
+
return (`Could not reach Ollama at ${host}. Sentinel is free by default and uses a local model via Ollama.\n` +
|
|
96
|
+
`To fix, either:\n` +
|
|
97
|
+
` 1. Install Ollama from https://ollama.com, then run:\n` +
|
|
98
|
+
` ollama pull ${DEFAULT_OLLAMA_MODEL}\n` +
|
|
99
|
+
` and make sure the Ollama app (or \`ollama serve\`) is running.\n` +
|
|
100
|
+
` 2. Or set ANTHROPIC_API_KEY (shell env or a git-ignored .env file) to use Anthropic instead.`);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=ollama.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/providers/ollama.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":"AAAA,gJAAgJ"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Purpose: opens (and migrates) Sentinel's local SQLite database at .sentinel/sentinel.db — all persistence is local, nothing leaves the machine.
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export const SENTINEL_DIR = ".sentinel";
|
|
6
|
+
export const DB_FILENAME = "sentinel.db";
|
|
7
|
+
/**
|
|
8
|
+
* Migrations, applied in order. MIGRATIONS[n] upgrades a database from schema
|
|
9
|
+
* version n to n+1. The current schema version is simply MIGRATIONS.length.
|
|
10
|
+
*/
|
|
11
|
+
const MIGRATIONS = [
|
|
12
|
+
// 0 -> 1: table for verification results (Phase 1: verify_task_match).
|
|
13
|
+
`
|
|
14
|
+
CREATE TABLE IF NOT EXISTS verifications (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
tool TEXT NOT NULL,
|
|
17
|
+
task TEXT NOT NULL,
|
|
18
|
+
diff_hash TEXT NOT NULL,
|
|
19
|
+
verdict TEXT NOT NULL,
|
|
20
|
+
reason TEXT NOT NULL,
|
|
21
|
+
provider TEXT NOT NULL,
|
|
22
|
+
model TEXT NOT NULL,
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
|
24
|
+
);
|
|
25
|
+
`,
|
|
26
|
+
// 1 -> 2: optional JSON details (Phase 2: per-issue breakdown for check_hallucination).
|
|
27
|
+
`ALTER TABLE verifications ADD COLUMN details TEXT;`,
|
|
28
|
+
];
|
|
29
|
+
export const CURRENT_SCHEMA_VERSION = MIGRATIONS.length;
|
|
30
|
+
/**
|
|
31
|
+
* Ensures the .sentinel/ folder exists inside the given project directory.
|
|
32
|
+
* Also drops a .gitignore inside it so the local database is never committed.
|
|
33
|
+
* Returns the absolute path to the .sentinel/ folder.
|
|
34
|
+
*/
|
|
35
|
+
export function ensureSentinelDir(projectDir) {
|
|
36
|
+
const dir = join(projectDir, SENTINEL_DIR);
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
// Self-ignoring folder (same trick as .terraform/): keeps local data out of git
|
|
39
|
+
// without touching the user's own .gitignore.
|
|
40
|
+
const gitignorePath = join(dir, ".gitignore");
|
|
41
|
+
if (!existsSync(gitignorePath)) {
|
|
42
|
+
writeFileSync(gitignorePath, "# Sentinel local data — never commit.\n*\n");
|
|
43
|
+
}
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Opens the local SQLite database for a project, creating the file on first
|
|
48
|
+
* run and applying any pending schema migrations. Callers must close() the
|
|
49
|
+
* returned handle.
|
|
50
|
+
*/
|
|
51
|
+
export function openDatabase(projectDir) {
|
|
52
|
+
const dir = ensureSentinelDir(projectDir);
|
|
53
|
+
const db = new DatabaseSync(join(dir, DB_FILENAME));
|
|
54
|
+
// WAL mode: safer concurrent reads while a verification is being written.
|
|
55
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
56
|
+
db.exec(`
|
|
57
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
58
|
+
key TEXT PRIMARY KEY,
|
|
59
|
+
value TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
`);
|
|
62
|
+
db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', '0')").run();
|
|
63
|
+
// Apply pending migrations, one version at a time.
|
|
64
|
+
let version = getSchemaVersion(db);
|
|
65
|
+
while (version < CURRENT_SCHEMA_VERSION) {
|
|
66
|
+
db.exec("BEGIN");
|
|
67
|
+
try {
|
|
68
|
+
db.exec(MIGRATIONS[version]);
|
|
69
|
+
version += 1;
|
|
70
|
+
db.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(String(version));
|
|
71
|
+
db.exec("COMMIT");
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
db.exec("ROLLBACK");
|
|
75
|
+
db.close();
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return db;
|
|
80
|
+
}
|
|
81
|
+
/** Reads the schema version stored in the database. */
|
|
82
|
+
export function getSchemaVersion(db) {
|
|
83
|
+
const row = db
|
|
84
|
+
.prepare("SELECT value FROM meta WHERE key = 'schema_version'")
|
|
85
|
+
.get();
|
|
86
|
+
return row ? Number(row.value) : -1;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Aggregates run counts per tool. "Flagged" means something was found: a
|
|
90
|
+
* non-clean verdict in standalone mode, or an evidence handoff whose stored
|
|
91
|
+
* findings list is non-empty (Sentinel didn't judge, but it found things).
|
|
92
|
+
*/
|
|
93
|
+
export function getToolSummary(db) {
|
|
94
|
+
return db
|
|
95
|
+
.prepare(`SELECT tool,
|
|
96
|
+
COUNT(*) AS total,
|
|
97
|
+
SUM(CASE
|
|
98
|
+
WHEN verdict IN ('match', 'no_issues') THEN 0
|
|
99
|
+
WHEN verdict = 'evidence_provided' THEN
|
|
100
|
+
CASE WHEN details IS NOT NULL AND json_valid(details) AND json_array_length(details) > 0
|
|
101
|
+
THEN 1 ELSE 0 END
|
|
102
|
+
ELSE 1
|
|
103
|
+
END) AS flagged,
|
|
104
|
+
SUM(CASE WHEN verdict = 'evidence_provided' THEN 1 ELSE 0 END) AS evidence
|
|
105
|
+
FROM verifications GROUP BY tool ORDER BY tool`)
|
|
106
|
+
.all();
|
|
107
|
+
}
|
|
108
|
+
/** Most recent verifications, newest first. */
|
|
109
|
+
export function getRecentVerifications(db, limit = 200) {
|
|
110
|
+
return db
|
|
111
|
+
.prepare(`SELECT id, tool, task, verdict, reason, details, provider, model, created_at
|
|
112
|
+
FROM verifications ORDER BY id DESC LIMIT ?`)
|
|
113
|
+
.all(limit);
|
|
114
|
+
}
|
|
115
|
+
/** Persists one verification result and returns its row id. */
|
|
116
|
+
export function insertVerification(db, record) {
|
|
117
|
+
const result = db
|
|
118
|
+
.prepare(`INSERT INTO verifications (tool, task, diff_hash, verdict, reason, details, provider, model)
|
|
119
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
120
|
+
.run(record.tool, record.task, record.diffHash, record.verdict, record.reason, record.details ?? null, record.provider, record.model);
|
|
121
|
+
return Number(result.lastInsertRowid);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Purpose: shared combiner logic — static findings are authoritative, and model findings that duplicate them are removed deterministically (never by prompt instruction alone).
|
|
2
|
+
/** Normalizes an issue location to a comparable "file:line" key, or null if it has none. */
|
|
3
|
+
export function locationKey(location) {
|
|
4
|
+
const match = /([\w@$./-]+:\d+)/.exec(location);
|
|
5
|
+
return match ? match[1] : null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Drops model issues that duplicate a static finding:
|
|
9
|
+
* 1. pointing at an already-flagged file:line, or
|
|
10
|
+
* 2. mentioning an identifier the static check already flagged (static
|
|
11
|
+
* reasons quote their identifiers with double quotes), or
|
|
12
|
+
* 3. having no line number while quoting only identifiers that appear on
|
|
13
|
+
* statically-flagged lines — the typical vague echo of a static finding.
|
|
14
|
+
* (A line-less issue that names at least one new identifier survives.)
|
|
15
|
+
*/
|
|
16
|
+
export function dropDuplicateModelIssues(staticIssues, modelIssues) {
|
|
17
|
+
const staticKeys = new Set(staticIssues.map((issue) => locationKey(issue.location)));
|
|
18
|
+
const staticIdentifiers = [
|
|
19
|
+
...new Set(staticIssues.flatMap((issue) => [...issue.reason.matchAll(/"([^"]+)"/g)].map((m) => m[1]))),
|
|
20
|
+
];
|
|
21
|
+
// Identifiers occurring in the code snippets of statically-flagged lines.
|
|
22
|
+
const flaggedLineIdentifiers = new Set(staticIssues.flatMap((issue) => {
|
|
23
|
+
const snippet = issue.location.split(" — ")[1] ?? "";
|
|
24
|
+
return [...snippet.matchAll(/[A-Za-z_$][\w$]{2,}/g)].map((m) => m[0]);
|
|
25
|
+
}));
|
|
26
|
+
return modelIssues.filter((issue) => {
|
|
27
|
+
const key = locationKey(issue.location);
|
|
28
|
+
if (key !== null && staticKeys.has(key))
|
|
29
|
+
return false;
|
|
30
|
+
const text = `${issue.location} ${issue.reason}`;
|
|
31
|
+
if (staticIdentifiers.some((identifier) => text.includes(identifier)))
|
|
32
|
+
return false;
|
|
33
|
+
if (key === null) {
|
|
34
|
+
const quoted = [...text.matchAll(/[`'"]([A-Za-z_$][\w$]{2,})[`'"]/g)].map((m) => m[1]);
|
|
35
|
+
if (quoted.length > 0 && quoted.every((name) => flaggedLineIdentifiers.has(name))) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=combine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combine.js","sourceRoot":"","sources":["../../src/verify/combine.ts"],"names":[],"mappings":"AAAA,gLAAgL;AAUhL,4FAA4F;AAC5F,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CACtC,YAAiC,EACjC,WAAgC;IAEhC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACrF,MAAM,iBAAiB,GAAG;QACxB,GAAG,IAAI,GAAG,CACR,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC3F;KACF,CAAC;IACF,0EAA0E;IAC1E,MAAM,sBAAsB,GAAG,IAAI,GAAG,CACpC,YAAY,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACrD,OAAO,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QAClC,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,GAAG,KAAK,IAAI,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACtD,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjD,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QACpF,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,kCAAkC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACvF,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;gBAClF,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Purpose: shared unified-diff parsing — extracts added lines with file and line number for the verification tools.
|
|
2
|
+
/** Extracts the added lines of a unified diff, with their file and line number. */
|
|
3
|
+
export function parseDiff(diff) {
|
|
4
|
+
const added = [];
|
|
5
|
+
const touchedFiles = new Set();
|
|
6
|
+
let currentFile = "";
|
|
7
|
+
let lineNo = 0;
|
|
8
|
+
for (const raw of diff.split("\n")) {
|
|
9
|
+
if (raw.startsWith("+++ ")) {
|
|
10
|
+
currentFile = raw.replace(/^\+\+\+ (b\/)?/, "").trim();
|
|
11
|
+
touchedFiles.add(currentFile);
|
|
12
|
+
}
|
|
13
|
+
else if (raw.startsWith("@@")) {
|
|
14
|
+
const match = /\+(\d+)/.exec(raw);
|
|
15
|
+
lineNo = match ? Number(match[1]) - 1 : 0;
|
|
16
|
+
}
|
|
17
|
+
else if (raw.startsWith("+")) {
|
|
18
|
+
lineNo += 1;
|
|
19
|
+
added.push({ file: currentFile, lineNo, text: raw.slice(1) });
|
|
20
|
+
}
|
|
21
|
+
else if (!raw.startsWith("-")) {
|
|
22
|
+
lineNo += 1; // context line
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { added, touchedFiles };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/verify/diff.ts"],"names":[],"mappings":"AAAA,oHAAoH;AAQpH,mFAAmF;AACnF,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACvC,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACvD,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,CAAC;aAAM,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,CAAC,CAAC,CAAC,eAAe;QAC9B,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Purpose: shared pieces of agent-native evidence mode — in MCP mode Sentinel gathers deterministic evidence and the calling agent does the judgment; no model call is ever made here.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { openDatabase, insertVerification } from "../storage/db.js";
|
|
4
|
+
/** Stored in place of a verdict when Sentinel handed evidence to the calling agent instead of judging. */
|
|
5
|
+
export const EVIDENCE_VERDICT = "evidence_provided";
|
|
6
|
+
/** Logs an evidence handoff locally (findings only — Sentinel made no judgment). */
|
|
7
|
+
export function storeEvidence(projectDir, tool, diff, findings) {
|
|
8
|
+
const db = openDatabase(projectDir);
|
|
9
|
+
try {
|
|
10
|
+
insertVerification(db, {
|
|
11
|
+
tool,
|
|
12
|
+
task: "",
|
|
13
|
+
diffHash: createHash("sha256").update(diff).digest("hex"),
|
|
14
|
+
verdict: EVIDENCE_VERDICT,
|
|
15
|
+
reason: `Evidence returned to the calling agent: ${findings.length} static finding(s). Judgment was the agent's.`,
|
|
16
|
+
details: JSON.stringify(findings),
|
|
17
|
+
provider: "agent",
|
|
18
|
+
model: "agent",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
db.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evidence.js","sourceRoot":"","sources":["../../src/verify/evidence.ts"],"names":[],"mappings":"AAAA,uLAAuL;AAEvL,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGpE,0GAA0G;AAC1G,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAcpD,oFAAoF;AACpF,MAAM,UAAU,aAAa,CAC3B,UAAkB,EAClB,IAAY,EACZ,IAAY,EACZ,QAA6B;IAE7B,MAAM,EAAE,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACpC,IAAI,CAAC;QACH,kBAAkB,CAAC,EAAE,EAAE;YACrB,IAAI;YACJ,IAAI,EAAE,EAAE;YACR,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;YACzD,OAAO,EAAE,gBAAgB;YACzB,MAAM,EAAE,2CAA2C,QAAQ,CAAC,MAAM,+CAA+C;YACjH,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;YACjC,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,OAAO;SACf,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
|