sentinelayer-cli 0.39.2 → 0.40.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/package.json +1 -1
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +92 -31
- package/src/legacy-cli.js +2 -2
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/checkpoints.js +194 -0
package/src/mcp/smoke.js
CHANGED
|
@@ -5,6 +5,7 @@ import { requestHostedMcpAccessToken } from "./token-service.js";
|
|
|
5
5
|
|
|
6
6
|
const PASS = "PASS";
|
|
7
7
|
const FAIL = "FAIL";
|
|
8
|
+
const SESSION_EVENTS_LIST_TOOL = "sessions_events_list";
|
|
8
9
|
|
|
9
10
|
function joinUrl(base, suffix) {
|
|
10
11
|
const normalizedBase = String(base || "").replace(/\/+$/, "");
|
|
@@ -233,13 +234,13 @@ export async function runHostedMcpSmoke({
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
if (normalizedSessionId) {
|
|
236
|
-
if (!toolNames.includes(
|
|
237
|
+
if (!toolNames.includes(SESSION_EVENTS_LIST_TOOL)) {
|
|
237
238
|
probes.push({
|
|
238
239
|
id: "session_events_list",
|
|
239
|
-
label:
|
|
240
|
+
label: `MCP ${SESSION_EVENTS_LIST_TOOL}`,
|
|
240
241
|
status: 0,
|
|
241
242
|
verdict: FAIL,
|
|
242
|
-
detail:
|
|
243
|
+
detail: `${SESSION_EVENTS_LIST_TOOL} tool was not advertised by tools/list`,
|
|
243
244
|
sessionId: normalizedSessionId,
|
|
244
245
|
});
|
|
245
246
|
} else {
|
|
@@ -254,7 +255,7 @@ export async function runHostedMcpSmoke({
|
|
|
254
255
|
id: "mcp-smoke-events",
|
|
255
256
|
method: "tools/call",
|
|
256
257
|
params: {
|
|
257
|
-
name:
|
|
258
|
+
name: SESSION_EVENTS_LIST_TOOL,
|
|
258
259
|
arguments: {
|
|
259
260
|
sessionId: normalizedSessionId,
|
|
260
261
|
limit: normalizedLimit,
|
|
@@ -266,7 +267,7 @@ export async function runHostedMcpSmoke({
|
|
|
266
267
|
if (!eventsResponse.reached) {
|
|
267
268
|
probes.push({
|
|
268
269
|
id: "session_events_list",
|
|
269
|
-
label:
|
|
270
|
+
label: `MCP ${SESSION_EVENTS_LIST_TOOL}`,
|
|
270
271
|
status: eventsResponse.status,
|
|
271
272
|
verdict: FAIL,
|
|
272
273
|
detail: eventsResponse.detail,
|
|
@@ -275,7 +276,7 @@ export async function runHostedMcpSmoke({
|
|
|
275
276
|
} else if (eventsResponse.status !== 200) {
|
|
276
277
|
probes.push({
|
|
277
278
|
id: "session_events_list",
|
|
278
|
-
label:
|
|
279
|
+
label: `MCP ${SESSION_EVENTS_LIST_TOOL}`,
|
|
279
280
|
status: eventsResponse.status,
|
|
280
281
|
verdict: FAIL,
|
|
281
282
|
detail: `expected HTTP 200, got HTTP ${eventsResponse.status}`,
|
|
@@ -284,7 +285,7 @@ export async function runHostedMcpSmoke({
|
|
|
284
285
|
} else if (eventsResponse.json?.error) {
|
|
285
286
|
probes.push({
|
|
286
287
|
id: "session_events_list",
|
|
287
|
-
label:
|
|
288
|
+
label: `MCP ${SESSION_EVENTS_LIST_TOOL}`,
|
|
288
289
|
status: eventsResponse.status,
|
|
289
290
|
verdict: FAIL,
|
|
290
291
|
detail: summarizeJsonRpcError(eventsResponse.json.error, redactionSecrets),
|
|
@@ -297,7 +298,7 @@ export async function runHostedMcpSmoke({
|
|
|
297
298
|
.filter((value) => Number.isFinite(value));
|
|
298
299
|
probes.push({
|
|
299
300
|
id: "session_events_list",
|
|
300
|
-
label:
|
|
301
|
+
label: `MCP ${SESSION_EVENTS_LIST_TOOL}`,
|
|
301
302
|
status: eventsResponse.status,
|
|
302
303
|
verdict: PASS,
|
|
303
304
|
detail: `${events.length} event(s) returned`,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { requestJsonMutation } from "../auth/http.js";
|
|
4
|
+
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
5
|
+
|
|
6
|
+
// The client half of Senti Pocket "ring the owner": an authed agent/CLI DESCRIBES a decision the owner must make and the
|
|
7
|
+
// pocket-gateway (POST /dial/ring-owner) builds the canonical NeedCarterSignal server-side + rings the owner's phone.
|
|
8
|
+
// SECURITY: this AUTHORS NOTHING — it only rings. The ring TARGET is the CALLER's own verified identity (the gateway
|
|
9
|
+
// derives it from the bearer, NEVER a body field), so a caller can only ring THEIR OWN phone. The answered-call write
|
|
10
|
+
// keeps its own governed confirm (dialing relaxes nothing).
|
|
11
|
+
export const RING_OWNER_KINDS = Object.freeze(["decisionYours", "pickOption", "go", "info", "checkpointReady"]);
|
|
12
|
+
|
|
13
|
+
function normalizeString(value) {
|
|
14
|
+
return String(value || "").trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the pocket-gateway base URL — DISTINCT from the senti apiUrl (do NOT reuse resolveApiUrl; that is the API host).
|
|
19
|
+
* Precedence: env SENTI_POCKET_URL -> env POCKET_GATEWAY_URL -> a config value if the caller passed one. Absent -> "".
|
|
20
|
+
* A present value MUST be an http(s) URL (fail-closed on a bogus scheme). Trailing slashes trimmed.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePocketGatewayUrl({ env = process.env, configUrl = "" } = {}) {
|
|
23
|
+
const raw =
|
|
24
|
+
normalizeString(env.SENTI_POCKET_URL) ||
|
|
25
|
+
normalizeString(env.POCKET_GATEWAY_URL) ||
|
|
26
|
+
normalizeString(configUrl);
|
|
27
|
+
if (!raw) return "";
|
|
28
|
+
if (!/^https?:\/\//i.test(raw)) throw new Error("pocket gateway URL must be an http(s) URL");
|
|
29
|
+
return raw.replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ring the owner about a session decision via the pocket-gateway POST /dial/ring-owner.
|
|
34
|
+
* @param {string} question the decision/question to put to the owner
|
|
35
|
+
* @param {object} opts kind/sessionId/options/whatWeNeed/checkpointId/requestedBy/idempotencyKey/gatewayUrl + injectable
|
|
36
|
+
* resolveAuthSession + requestMutation (hermetic tests) + env
|
|
37
|
+
* @returns the gateway result ({ dialId, dispatched, reason?, kind?, idempotent? }).
|
|
38
|
+
*/
|
|
39
|
+
export async function ringOwner(
|
|
40
|
+
question,
|
|
41
|
+
{
|
|
42
|
+
kind = "decisionYours",
|
|
43
|
+
sessionId = "",
|
|
44
|
+
options = [],
|
|
45
|
+
whatWeNeed = "",
|
|
46
|
+
checkpointId = "",
|
|
47
|
+
requestedBy = "",
|
|
48
|
+
idempotencyKey = "",
|
|
49
|
+
gatewayUrl = "",
|
|
50
|
+
configUrl = "",
|
|
51
|
+
env = process.env,
|
|
52
|
+
cwd = process.cwd(),
|
|
53
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
54
|
+
requestMutation = requestJsonMutation,
|
|
55
|
+
} = {},
|
|
56
|
+
) {
|
|
57
|
+
const q = normalizeString(question);
|
|
58
|
+
if (!q) throw new Error("question is required.");
|
|
59
|
+
const sid = normalizeString(sessionId);
|
|
60
|
+
if (!sid) throw new Error("session id is required (--session <id>).");
|
|
61
|
+
const normKind = normalizeString(kind) || "decisionYours";
|
|
62
|
+
if (!RING_OWNER_KINDS.includes(normKind)) {
|
|
63
|
+
throw new Error(`kind must be one of: ${RING_OWNER_KINDS.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
if (normKind === "pickOption" && (!Array.isArray(options) || options.length === 0)) {
|
|
66
|
+
throw new Error("pickOption requires at least one --option.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const gw = normalizeString(gatewayUrl)
|
|
70
|
+
? normalizeString(gatewayUrl).replace(/\/+$/, "")
|
|
71
|
+
: resolvePocketGatewayUrl({ env, configUrl });
|
|
72
|
+
if (!gw) {
|
|
73
|
+
throw new Error("pocket gateway URL not configured. Set SENTI_POCKET_URL to the pocket-gateway base URL.");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const auth = await resolveAuthSession({ cwd, env, autoRotate: false });
|
|
77
|
+
if (!auth?.token) {
|
|
78
|
+
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const context = { sessionId: sid };
|
|
82
|
+
if (normalizeString(whatWeNeed)) context.whatWeNeed = normalizeString(whatWeNeed);
|
|
83
|
+
if (normalizeString(checkpointId)) context.checkpointId = normalizeString(checkpointId);
|
|
84
|
+
const body = { question: q, kind: normKind, context };
|
|
85
|
+
if (normKind === "pickOption") body.options = options.map((o) => String(o));
|
|
86
|
+
if (normalizeString(requestedBy)) body.requestedBy = normalizeString(requestedBy);
|
|
87
|
+
if (normalizeString(idempotencyKey)) body.idempotencyKey = normalizeString(idempotencyKey);
|
|
88
|
+
|
|
89
|
+
// requestJsonMutation auto-derives a transport Idempotency-Key from operationName (retry-safe at the wire); the
|
|
90
|
+
// gateway's own ring-level dedupe (PR-B4) keys on body.idempotencyKey OR a content hash, so an identical retry rings once.
|
|
91
|
+
return requestMutation(`${gw}/dial/ring-owner`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
operationName: "pocket.ring_owner",
|
|
94
|
+
headers: { Authorization: `Bearer ${auth.token}` },
|
|
95
|
+
body,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -73,6 +73,12 @@ const SEVERITY_ORDER = new Map([
|
|
|
73
73
|
|
|
74
74
|
const TEST_OR_FIXTURE_PATH_PATTERN = /(?:^|[\\/])(?:test|tests|__tests__|fixtures?)(?:[\\/]|$)/i;
|
|
75
75
|
const LOCAL_REVIEW_SOURCE_PATH_PATTERN = /(?:^|[\\/])src[\\/]review[\\/]local-review\.js$/i;
|
|
76
|
+
const SQL_STRING_CONCATENATION_PATTERN =
|
|
77
|
+
/["'`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^\n]{0,160}["'`]\s*\+/i;
|
|
78
|
+
const SQL_CONCAT_EXCLUDE_PATH_PATTERN = new RegExp(
|
|
79
|
+
`${TEST_LIKE_PATH_PATTERN.source}|${LOCAL_REVIEW_SOURCE_PATH_PATTERN.source}`,
|
|
80
|
+
"i"
|
|
81
|
+
);
|
|
76
82
|
const WORK_ITEM_MARKER_EXCLUDE_PATH_PATTERN = new RegExp(
|
|
77
83
|
`${TEST_OR_FIXTURE_PATH_PATTERN.source}|${LOCAL_REVIEW_SOURCE_PATH_PATTERN.source}`,
|
|
78
84
|
"i"
|
|
@@ -249,11 +255,9 @@ const DETERMINISTIC_REVIEW_RULES = Object.freeze([
|
|
|
249
255
|
severity: "P2",
|
|
250
256
|
message: "Possible SQL string concatenation detected.",
|
|
251
257
|
suggestedFix: "Use parameterized queries and prepared statements.",
|
|
252
|
-
regex:
|
|
258
|
+
regex: SQL_STRING_CONCATENATION_PATTERN,
|
|
253
259
|
sourceOnly: true,
|
|
254
|
-
|
|
255
|
-
// regex literals and must not flag itself.
|
|
256
|
-
excludePathPattern: LOCAL_REVIEW_SOURCE_PATH_PATTERN,
|
|
260
|
+
excludePathPattern: SQL_CONCAT_EXCLUDE_PATH_PATTERN,
|
|
257
261
|
},
|
|
258
262
|
{
|
|
259
263
|
id: "SL-SEC-018",
|
|
@@ -774,7 +778,7 @@ async function runPatternChecks({ rootPath, filePaths, maxFindings = MAX_FINDING
|
|
|
774
778
|
}
|
|
775
779
|
}
|
|
776
780
|
|
|
777
|
-
const sqlConcat =
|
|
781
|
+
const sqlConcat = SQL_STRING_CONCATENATION_PATTERN.exec(text);
|
|
778
782
|
if (
|
|
779
783
|
sqlConcat &&
|
|
780
784
|
findings.length < maxFindings &&
|
|
@@ -376,6 +376,79 @@ function summarizeFindings(findings = []) {
|
|
|
376
376
|
};
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function normalizeEvidenceNumber(value) {
|
|
380
|
+
const parsed = Number(value);
|
|
381
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function resolveAiResultCostUsd(result = {}) {
|
|
385
|
+
return Math.max(
|
|
386
|
+
normalizeEvidenceNumber(result?.costUsd),
|
|
387
|
+
normalizeEvidenceNumber(result?.usage?.costUsd),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildAiCallEvidence(result = {}, { dryRun = false } = {}) {
|
|
392
|
+
if (dryRun) {
|
|
393
|
+
return {
|
|
394
|
+
confirmedCalls: 0,
|
|
395
|
+
usageBackedCalls: 0,
|
|
396
|
+
ledgerBackedCalls: 0,
|
|
397
|
+
pricedCalls: 0,
|
|
398
|
+
inputTokens: 0,
|
|
399
|
+
outputTokens: 0,
|
|
400
|
+
totalTokens: 0,
|
|
401
|
+
providerCostUsd: 0,
|
|
402
|
+
customerCostUsd: 0,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const usage = result?.usage && typeof result.usage === "object" ? result.usage : {};
|
|
407
|
+
const ledger = result?.billing?.ok === true && result?.billing?.ledgerEntry
|
|
408
|
+
? result.billing.ledgerEntry
|
|
409
|
+
: {};
|
|
410
|
+
const usageInputTokens = normalizeEvidenceNumber(usage.inputTokens);
|
|
411
|
+
const usageOutputTokens = normalizeEvidenceNumber(usage.outputTokens);
|
|
412
|
+
const usageTotalTokens = normalizeEvidenceNumber(usage.totalTokens);
|
|
413
|
+
const ledgerInputTokens = normalizeEvidenceNumber(ledger.inputTokens);
|
|
414
|
+
const ledgerOutputTokens = normalizeEvidenceNumber(ledger.outputTokens);
|
|
415
|
+
const ledgerTotalTokens = normalizeEvidenceNumber(ledger.totalTokens);
|
|
416
|
+
const inputTokens = Math.max(usageInputTokens, ledgerInputTokens);
|
|
417
|
+
const outputTokens = Math.max(usageOutputTokens, ledgerOutputTokens);
|
|
418
|
+
const totalTokens = Math.max(usageTotalTokens, ledgerTotalTokens, inputTokens + outputTokens);
|
|
419
|
+
const ledgerProviderCostUsd = normalizeEvidenceNumber(ledger.providerCostUsd);
|
|
420
|
+
const providerCostUsd = Math.max(resolveAiResultCostUsd(result), ledgerProviderCostUsd);
|
|
421
|
+
const customerCostUsd = normalizeEvidenceNumber(ledger.customerCostUsd);
|
|
422
|
+
const usageBacked = usageInputTokens + usageOutputTokens + usageTotalTokens > 0;
|
|
423
|
+
const ledgerBacked =
|
|
424
|
+
ledgerInputTokens + ledgerOutputTokens + ledgerTotalTokens > 0 || ledgerProviderCostUsd > 0;
|
|
425
|
+
const priced = providerCostUsd > 0 || customerCostUsd > 0;
|
|
426
|
+
const confirmed = usageBacked || ledgerBacked || priced;
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
confirmedCalls: confirmed ? 1 : 0,
|
|
430
|
+
usageBackedCalls: usageBacked ? 1 : 0,
|
|
431
|
+
ledgerBackedCalls: ledgerBacked ? 1 : 0,
|
|
432
|
+
pricedCalls: priced ? 1 : 0,
|
|
433
|
+
inputTokens,
|
|
434
|
+
outputTokens,
|
|
435
|
+
totalTokens,
|
|
436
|
+
providerCostUsd,
|
|
437
|
+
customerCostUsd,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function combineAiCallEvidence(entries = []) {
|
|
442
|
+
const combined = buildAiCallEvidence({}, { dryRun: true });
|
|
443
|
+
for (const entry of entries) {
|
|
444
|
+
if (!entry || typeof entry !== "object") continue;
|
|
445
|
+
for (const key of Object.keys(combined)) {
|
|
446
|
+
combined[key] += normalizeEvidenceNumber(entry[key]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return combined;
|
|
450
|
+
}
|
|
451
|
+
|
|
379
452
|
function buildPersonaHealthFindings(personaHealth = {}) {
|
|
380
453
|
if (personaHealth.healthy || personaHealth.total <= 0) {
|
|
381
454
|
return [];
|
|
@@ -386,13 +459,16 @@ function buildPersonaHealthFindings(personaHealth = {}) {
|
|
|
386
459
|
const total = Number(personaHealth.total || 0);
|
|
387
460
|
const errorRatio = Number(personaHealth.errorRatio || 0);
|
|
388
461
|
const totalCostUsd = Number(personaHealth.totalCostUsd || 0);
|
|
462
|
+
const aiCallEvidence = personaHealth.aiCallEvidence || {};
|
|
463
|
+
const okCount = Number(personaHealth.ok || 0);
|
|
464
|
+
const unverifiedOkCount = Number(personaHealth.unverifiedOk || 0);
|
|
389
465
|
const allPersonasFailed = total > 0 && errorCount === total;
|
|
390
466
|
const severity = allPersonasFailed ? "P0" : "P1";
|
|
391
467
|
let message = `Omar AI coverage degraded: ${errorCount}/${total} dispatched personas errored (${Math.round(errorRatio * 100)}%).`;
|
|
392
468
|
if (allPersonasFailed) {
|
|
393
469
|
message = `Omar AI coverage failed closed: all ${total} dispatched personas errored before producing findings.`;
|
|
394
|
-
} else if (errorCount === 0 &&
|
|
395
|
-
message =
|
|
470
|
+
} else if (errorCount === 0 && unverifiedOkCount > 0) {
|
|
471
|
+
message = `Omar AI coverage failed closed: ${unverifiedOkCount}/${okCount} successful personas lacked token-bearing usage, billing-ledger, or priced-call evidence.`;
|
|
396
472
|
}
|
|
397
473
|
|
|
398
474
|
return [
|
|
@@ -417,6 +493,9 @@ function buildPersonaHealthFindings(personaHealth = {}) {
|
|
|
417
493
|
total,
|
|
418
494
|
errorRatio,
|
|
419
495
|
totalCostUsd,
|
|
496
|
+
okCount,
|
|
497
|
+
unverifiedOkCount,
|
|
498
|
+
aiCallEvidence,
|
|
420
499
|
},
|
|
421
500
|
},
|
|
422
501
|
];
|
|
@@ -636,6 +715,8 @@ async function runOmarPersonaSwarm({
|
|
|
636
715
|
});
|
|
637
716
|
return normalized;
|
|
638
717
|
});
|
|
718
|
+
const costUsd = resolveAiResultCostUsd(result);
|
|
719
|
+
const aiCallEvidence = buildAiCallEvidence(result, { dryRun });
|
|
639
720
|
|
|
640
721
|
if (onEvent) {
|
|
641
722
|
for (const finding of findings) {
|
|
@@ -659,11 +740,11 @@ async function runOmarPersonaSwarm({
|
|
|
659
740
|
fileCount: scopedFiles.length,
|
|
660
741
|
findings: findings.length,
|
|
661
742
|
summary: result?.summary || summarizeFindings(findings),
|
|
662
|
-
costUsd
|
|
743
|
+
costUsd,
|
|
663
744
|
durationMs: Date.now() - subagentStart,
|
|
664
745
|
},
|
|
665
746
|
usage: {
|
|
666
|
-
costUsd
|
|
747
|
+
costUsd,
|
|
667
748
|
durationMs: Date.now() - subagentStart,
|
|
668
749
|
toolCalls: result?.usage?.toolCalls || 0,
|
|
669
750
|
},
|
|
@@ -678,7 +759,8 @@ async function runOmarPersonaSwarm({
|
|
|
678
759
|
files: scopedFiles,
|
|
679
760
|
findings,
|
|
680
761
|
summary: result?.summary || summarizeFindings(findings),
|
|
681
|
-
costUsd
|
|
762
|
+
costUsd,
|
|
763
|
+
aiCallEvidence,
|
|
682
764
|
model: result?.model || model || null,
|
|
683
765
|
billing: result?.billing || null,
|
|
684
766
|
artifacts: result?.artifacts || null,
|
|
@@ -739,6 +821,9 @@ async function runOmarPersonaSwarm({
|
|
|
739
821
|
);
|
|
740
822
|
const findings = settledSubagents.flatMap((result) => result.findings || []);
|
|
741
823
|
const totalCostUsd = settledSubagents.reduce((sum, result) => sum + (result.costUsd || 0), 0);
|
|
824
|
+
const aiCallEvidence = combineAiCallEvidence(
|
|
825
|
+
settledSubagents.map((result) => result.aiCallEvidence),
|
|
826
|
+
);
|
|
742
827
|
const summary = summarizeFindings(findings);
|
|
743
828
|
const okCount = settledSubagents.filter((result) => result.status === "ok").length;
|
|
744
829
|
const errorCount = settledSubagents.filter((result) => result.status === "error").length;
|
|
@@ -811,6 +896,7 @@ async function runOmarPersonaSwarm({
|
|
|
811
896
|
findings,
|
|
812
897
|
summary,
|
|
813
898
|
costUsd: totalCostUsd,
|
|
899
|
+
aiCallEvidence,
|
|
814
900
|
model: model || null,
|
|
815
901
|
durationMs: Date.now() - startedAt,
|
|
816
902
|
error: okCount > 0 ? null : settledSubagents.find((result) => result.error)?.error || null,
|
|
@@ -829,6 +915,7 @@ async function runOmarPersonaSwarm({
|
|
|
829
915
|
files: result.files,
|
|
830
916
|
findings: (result.findings || []).length,
|
|
831
917
|
costUsd: result.costUsd || 0,
|
|
918
|
+
aiCallEvidence: result.aiCallEvidence || null,
|
|
832
919
|
billing: result.billing || null,
|
|
833
920
|
durationMs: result.durationMs || 0,
|
|
834
921
|
error: result.error || null,
|
|
@@ -1142,6 +1229,8 @@ export async function runOmarGateOrchestrator({
|
|
|
1142
1229
|
confidenceFloor: personaConfidenceFloor,
|
|
1143
1230
|
personaConfidenceFloor,
|
|
1144
1231
|
}));
|
|
1232
|
+
const personaCost = resolveAiResultCostUsd(result);
|
|
1233
|
+
const aiCallEvidence = buildAiCallEvidence(result, { dryRun });
|
|
1145
1234
|
|
|
1146
1235
|
if (onEvent) {
|
|
1147
1236
|
for (const finding of findings) {
|
|
@@ -1174,14 +1263,13 @@ export async function runOmarGateOrchestrator({
|
|
|
1174
1263
|
identity,
|
|
1175
1264
|
findings: findings.length,
|
|
1176
1265
|
summary: result?.summary || {},
|
|
1177
|
-
costUsd:
|
|
1266
|
+
costUsd: personaCost,
|
|
1178
1267
|
durationMs: Date.now() - personaStart,
|
|
1179
1268
|
},
|
|
1180
1269
|
runId,
|
|
1181
1270
|
}));
|
|
1182
1271
|
}
|
|
1183
1272
|
|
|
1184
|
-
const personaCost = result?.costUsd || 0;
|
|
1185
1273
|
runningCostUsd += personaCost;
|
|
1186
1274
|
|
|
1187
1275
|
return {
|
|
@@ -1190,6 +1278,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1190
1278
|
findings,
|
|
1191
1279
|
summary: result?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
1192
1280
|
costUsd: personaCost,
|
|
1281
|
+
aiCallEvidence,
|
|
1193
1282
|
model: result?.model || model || null,
|
|
1194
1283
|
billing: result?.billing || null,
|
|
1195
1284
|
artifacts: result?.artifacts || null,
|
|
@@ -1269,28 +1358,35 @@ export async function runOmarGateOrchestrator({
|
|
|
1269
1358
|
);
|
|
1270
1359
|
|
|
1271
1360
|
const totalCost = settled.reduce((sum, r) => sum + (r.costUsd || 0), 0);
|
|
1361
|
+
const aiCallEvidence = combineAiCallEvidence(settled.map((result) => result.aiCallEvidence));
|
|
1272
1362
|
const totalDuration = Date.now() - startTime;
|
|
1273
1363
|
|
|
1274
|
-
// Silent-failure detection
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
// auth failures or LLM proxy outages as "clean scan".
|
|
1364
|
+
// Silent-failure detection requires evidence of an actual provider call.
|
|
1365
|
+
// Customer cost is not evidence by itself because internal/zero-priced
|
|
1366
|
+
// billing can still carry token-bearing usage and provider-cost receipts.
|
|
1278
1367
|
const personaErrorCount = settled.filter((r) => r.status === "error").length;
|
|
1279
1368
|
const personaSkippedCount = settled.filter((r) => r.status === "skipped").length;
|
|
1280
1369
|
const personaOkCount = settled.filter((r) => r.status === "ok").length;
|
|
1370
|
+
const personaVerifiedOkCount = settled.filter(
|
|
1371
|
+
(r) => r.status === "ok" && Number(r.aiCallEvidence?.confirmedCalls || 0) > 0
|
|
1372
|
+
).length;
|
|
1373
|
+
const personaUnverifiedOkCount = personaOkCount - personaVerifiedOkCount;
|
|
1281
1374
|
const totalPersonas = settled.length;
|
|
1282
1375
|
const errorRatio = totalPersonas > 0 ? personaErrorCount / totalPersonas : 0;
|
|
1283
1376
|
const aiCoverageHealthy =
|
|
1284
1377
|
totalPersonas === 0 ||
|
|
1285
|
-
(personaOkCount > 0 &&
|
|
1378
|
+
(personaOkCount > 0 && personaUnverifiedOkCount === 0 && errorRatio < 0.5 && !dryRun);
|
|
1286
1379
|
|
|
1287
1380
|
const personaHealth = {
|
|
1288
1381
|
ok: personaOkCount,
|
|
1289
1382
|
error: personaErrorCount,
|
|
1290
1383
|
skipped: personaSkippedCount,
|
|
1291
1384
|
total: totalPersonas,
|
|
1385
|
+
verifiedOk: personaVerifiedOkCount,
|
|
1386
|
+
unverifiedOk: personaUnverifiedOkCount,
|
|
1292
1387
|
errorRatio,
|
|
1293
1388
|
totalCostUsd: totalCost,
|
|
1389
|
+
aiCallEvidence,
|
|
1294
1390
|
healthy: aiCoverageHealthy || dryRun,
|
|
1295
1391
|
warnings: [],
|
|
1296
1392
|
};
|
|
@@ -1304,9 +1400,9 @@ export async function runOmarGateOrchestrator({
|
|
|
1304
1400
|
`${personaErrorCount}/${totalPersonas} personas errored (${Math.round(errorRatio * 100)}%). AI coverage is degraded.`
|
|
1305
1401
|
);
|
|
1306
1402
|
}
|
|
1307
|
-
if (
|
|
1403
|
+
if (personaUnverifiedOkCount > 0) {
|
|
1308
1404
|
personaHealth.warnings.push(
|
|
1309
|
-
|
|
1405
|
+
`${personaUnverifiedOkCount}/${personaOkCount} successful personas lacked token-bearing usage, billing-ledger, or priced-call evidence.`
|
|
1310
1406
|
);
|
|
1311
1407
|
}
|
|
1312
1408
|
}
|
|
@@ -1330,6 +1426,7 @@ export async function runOmarGateOrchestrator({
|
|
|
1330
1426
|
findings: (r.findings || []).length,
|
|
1331
1427
|
summary: r.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
|
|
1332
1428
|
costUsd: r.costUsd,
|
|
1429
|
+
aiCallEvidence: r.aiCallEvidence || null,
|
|
1333
1430
|
durationMs: r.durationMs,
|
|
1334
1431
|
model: r.model || null,
|
|
1335
1432
|
billing: r.billing || null,
|
|
@@ -201,6 +201,19 @@ function tokenizePath(relativePath) {
|
|
|
201
201
|
return [...tokens];
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
function specMentionsExactPath(markdown, relativePath) {
|
|
205
|
+
const normalizedPath = toPosixPath(relativePath).toLowerCase().replace(/^\.\/+/, "");
|
|
206
|
+
if (!normalizedPath) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const escapedPath = normalizedPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
210
|
+
const normalizedMarkdown = toPosixPath(markdown).toLowerCase();
|
|
211
|
+
return new RegExp(
|
|
212
|
+
`(^|[^a-z0-9._/-])(?:\\./)?${escapedPath}(?=$|[^a-z0-9._/-])`,
|
|
213
|
+
"m"
|
|
214
|
+
).test(normalizedMarkdown);
|
|
215
|
+
}
|
|
216
|
+
|
|
204
217
|
function isSourceFile(relativePath) {
|
|
205
218
|
return SOURCE_EXTENSIONS.has(path.extname(String(relativePath || "")).toLowerCase());
|
|
206
219
|
}
|
|
@@ -393,7 +406,9 @@ export async function runSpecBindingChecks({
|
|
|
393
406
|
tokenMatchesByPath.set(relativePath, true);
|
|
394
407
|
continue;
|
|
395
408
|
}
|
|
396
|
-
const tokenMatches =
|
|
409
|
+
const tokenMatches =
|
|
410
|
+
specMentionsExactPath(spec.markdown, relativePath) ||
|
|
411
|
+
tokenizePath(relativePath).some((token) => normalizedSpecText.includes(token));
|
|
397
412
|
tokenMatchesByPath.set(relativePath, tokenMatches);
|
|
398
413
|
if (evaluateScopeDrift && !tokenMatches) {
|
|
399
414
|
pushFinding(
|
package/src/scan/generator.js
CHANGED
|
@@ -2,7 +2,7 @@ import YAML from "yaml";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SCAN_WORKFLOW_PATH = ".github/workflows/omar-gate.yml";
|
|
4
4
|
export const DEFAULT_SCAN_SECRET_NAME = "SENTINELAYER_TOKEN";
|
|
5
|
-
export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@
|
|
5
|
+
export const SENTINELAYER_ACTION_REF = "mrrCarter/sentinelayer-v1-action@8afb7dbb42a5e1c25233d422c4cabe401ba02380";
|
|
6
6
|
export const SUPPORTED_E2E_HINTS = Object.freeze(["auto", "yes", "no"]);
|
|
7
7
|
export const SUPPORTED_PLAYWRIGHT_MODES = Object.freeze(["auto", "off", "baseline", "audit"]);
|
|
8
8
|
|