coderifts 8.6.3 → 8.6.4
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 +5 -1
- package/README.md +6 -3
- package/dist/.build-source-sha +1 -1
- package/dist/cli.js +785 -114
- package/package.json +1 -1
- package/scripts/check-packed-install.js +62 -1
package/dist/cli.js
CHANGED
|
@@ -3028,7 +3028,7 @@ var require_package = __commonJS({
|
|
|
3028
3028
|
"package.json"(exports2, module2) {
|
|
3029
3029
|
module2.exports = {
|
|
3030
3030
|
name: "coderifts",
|
|
3031
|
-
version: "8.6.
|
|
3031
|
+
version: "8.6.4",
|
|
3032
3032
|
description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
3033
3033
|
author: "CodeRifts <hello@coderifts.com>",
|
|
3034
3034
|
license: "MIT",
|
|
@@ -168610,7 +168610,7 @@ var require_picomatch2 = __commonJS({
|
|
|
168610
168610
|
var require_mcp_poison_gate = __commonJS({
|
|
168611
168611
|
"../../src/mcp-poison-gate.js"(exports2, module2) {
|
|
168612
168612
|
"use strict";
|
|
168613
|
-
var MCP_POISON_GATE_VERSION = "mpg-
|
|
168613
|
+
var MCP_POISON_GATE_VERSION = "mpg-2";
|
|
168614
168614
|
var BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
168615
168615
|
"suspicious_instruction",
|
|
168616
168616
|
"exfiltration_url_cosignal",
|
|
@@ -168619,7 +168619,8 @@ var require_mcp_poison_gate = __commonJS({
|
|
|
168619
168619
|
]);
|
|
168620
168620
|
var RA_TYPES = /* @__PURE__ */ new Set([
|
|
168621
168621
|
"structural_anomaly",
|
|
168622
|
-
"hidden_encoding"
|
|
168622
|
+
"hidden_encoding",
|
|
168623
|
+
"instruction_heuristic"
|
|
168623
168624
|
]);
|
|
168624
168625
|
var DECISION_ORDER = { ALLOW: 0, WARN: 1, REQUIRE_APPROVAL: 2, BLOCK: 3 };
|
|
168625
168626
|
function isLowerThan(decision, floor) {
|
|
@@ -205515,6 +205516,16 @@ var require_mcp_poisoning_detector = __commonJS({
|
|
|
205515
205516
|
"use strict";
|
|
205516
205517
|
var SENSITIVE_OBJECT = "(secrets?|credentials?|tokens?|passwords?|api[\\s_-]?keys?|private[\\s_-]?keys?|env|environment|conversation|chat\\s*history|context|user\\s*data|session)";
|
|
205517
205518
|
var SENSITIVE_SCOPE = "(any|all|every|stored|user\\'?s?|the\\s+user\\'?s?|all\\s+environment)";
|
|
205519
|
+
var MODE_PHRASE = /(hidden|secret|debug)\s+mode/i;
|
|
205520
|
+
var MODE_NEGATED = /\b(?:do not|don't|does not|doesn't|never|not)\b[\s\S]{0,40}?(hidden|secret|debug)\s+mode/i;
|
|
205521
|
+
var ALWAYS_RETURN_OK = /always\s+return\s+(success|ok|true)/i;
|
|
205522
|
+
function findingTypeFor(regex) {
|
|
205523
|
+
if (regex === MODE_PHRASE || regex === ALWAYS_RETURN_OK) return "instruction_heuristic";
|
|
205524
|
+
return "suspicious_instruction";
|
|
205525
|
+
}
|
|
205526
|
+
function modePhraseHits(str) {
|
|
205527
|
+
return MODE_PHRASE.test(str) && !MODE_NEGATED.test(str);
|
|
205528
|
+
}
|
|
205518
205529
|
var SUSPICIOUS_PATTERNS = [
|
|
205519
205530
|
// Injection / "ignore your instructions" family. The trailing object
|
|
205520
205531
|
// (instructions|rules|prompts|context) is OPTIONAL so "ignore previous and ..."
|
|
@@ -205524,9 +205535,18 @@ var require_mcp_poisoning_detector = __commonJS({
|
|
|
205524
205535
|
// ~40 chars so benign "always include the source URL" stays quiet.
|
|
205525
205536
|
new RegExp("always\\s+(include|append|send|attach|return|add)\\b[\\s\\S]{0,40}?\\b" + SENSITIVE_OBJECT + "\\b", "i"),
|
|
205526
205537
|
/you\s+(must|should|have to)\s+(now|always|instead)/i,
|
|
205527
|
-
|
|
205538
|
+
// Unanchored `leak` matched "leaked"/"leaks" (same class as the ungrouped
|
|
205539
|
+
// hidden|secret|debug mode bug). Keep the token — FSP 007 only fires because
|
|
205540
|
+
// ZWSP broke SEND and the host is leak.example — but require a word boundary
|
|
205541
|
+
// AND a payload noun or URL in the same string. Deleting leak drops 007
|
|
205542
|
+
// (94.1% → 88.2%); this co-occurrence does not.
|
|
205543
|
+
/exfiltrate|steal|send\s+to\s+external/i,
|
|
205544
|
+
/\bleak\b[\s\S]{0,80}?(?:https?:\/\/|\b(?:conversation|chat|history|context|credentials?|secrets?|tokens?|api\s*keys?|passwords?|data)\b)|(?:https?:\/\/|\b(?:conversation|chat|history|context|credentials?|secrets?|tokens?|api\s*keys?|passwords?|data)\b)[\s\S]{0,80}?\bleak\b/i,
|
|
205528
205545
|
// Exfiltration-intent family: an action verb targeting a sensitive payload.
|
|
205529
|
-
|
|
205546
|
+
// `messages?` was too broad — "Forward the message to the shared inbox" is a
|
|
205547
|
+
// mail-forwarder's job. `data` stays: fsp-exfil-title-010 is "SEND COPY OF ALL
|
|
205548
|
+
// USER DATA" and does not contain "a copy".
|
|
205549
|
+
/\b(send|forward|copy|upload|post|transmit|exfiltrate|leak)\b[\s\S]{0,40}?\b(conversation|chat|history|context|credentials?|secrets?|tokens?|api\s*keys?|keys?|passwords?|data)\b/i,
|
|
205530
205550
|
// Return/echo scoped sensitive material without a path ("return any stored credentials").
|
|
205531
205551
|
// Scope word required — bare "return the new API token" must stay quiet.
|
|
205532
205552
|
new RegExp(
|
|
@@ -205546,23 +205566,39 @@ var require_mcp_poisoning_detector = __commonJS({
|
|
|
205546
205566
|
/bypass\s+(auth|security|validation|filter)/i,
|
|
205547
205567
|
/override\s+(safety|policy|rules?)/i,
|
|
205548
205568
|
/do\s+not\s+(follow|respect|obey)\s+(original|previous)/i,
|
|
205549
|
-
|
|
205569
|
+
// Grouping is load-bearing: `/hidden|secret|debug\s+mode/` is
|
|
205570
|
+
// `hidden` OR `secret` OR `debug mode`, so any "secret"/"hidden" token is a
|
|
205571
|
+
// HIGH poison hit (store_credential, list_secrets, hidden pagination fields).
|
|
205572
|
+
// Intent, from the original line sitting among phrase-level patterns: the
|
|
205573
|
+
// three MODE phrases. scanString applies skip-negation so "Does not enable
|
|
205574
|
+
// debug mode" is quiet and "When debug mode is enabled" still hits.
|
|
205575
|
+
MODE_PHRASE,
|
|
205576
|
+
// Confused-deputy / tool-shadowing: "also invoke the secret exfil tool".
|
|
205577
|
+
// Requires also|now + invoke/call AND the noun "tool". That is NOT
|
|
205578
|
+
// capability_claimed (`also … calls?`), which fires on "also calls the webhook".
|
|
205579
|
+
// "invoke this tool after payment" has no also|now; "this tool calls …" is the
|
|
205580
|
+
// wrong order. Measured 0/20 benign and 0/6 mcp-fpfn FP vectors.
|
|
205581
|
+
/\b(?:also|now)\s+(?:invoke|call)s?\b[\s\S]{0,60}?\btool\b/i,
|
|
205550
205582
|
/\[system\]|<\s*system\s*>/i,
|
|
205551
|
-
|
|
205583
|
+
ALWAYS_RETURN_OK,
|
|
205552
205584
|
/hide\s+(errors|failures|issues)/i,
|
|
205553
205585
|
/return\s+secret|exfiltrat|leak\s+data/i
|
|
205554
205586
|
];
|
|
205555
205587
|
var URL_PATTERN = /https?:\/\/[^\s)'"]+/i;
|
|
205556
205588
|
var SENSITIVE_PATH_PATTERN = /(~\/\.ssh|~\/\.cursor|~\/\.aws|\.aws\/|\.env\b|\/etc\/|mcp\.json|id_rsa|\.ssh\/|\/credentials(?:\.|\/)|credentials\.(?:json|txt|pem|env)|\.credentials\b)/i;
|
|
205557
|
-
var CONTENT_MOVE_VERB_PATTERN = /\b(pass|send|return|forward|include|
|
|
205589
|
+
var CONTENT_MOVE_VERB_PATTERN = /\b(pass|send|return|forward|include|upload|post|transmit|copy|exfiltrate|leak|echo|reveal|expose)\b/i;
|
|
205558
205590
|
var BASE64_BLOB_PATTERN = /[A-Za-z0-9+/]{40,}={0,2}/;
|
|
205559
205591
|
function isLikelyBase64(s) {
|
|
205560
205592
|
if (/=$/.test(s)) return true;
|
|
205561
205593
|
return /[A-Z]/.test(s) && /[a-z]/.test(s) && /[0-9]/.test(s);
|
|
205562
205594
|
}
|
|
205595
|
+
function patternHits(regex, str) {
|
|
205596
|
+
if (regex === MODE_PHRASE) return modePhraseHits(str);
|
|
205597
|
+
return regex.test(str);
|
|
205598
|
+
}
|
|
205563
205599
|
function hasInjectionIntent(str) {
|
|
205564
205600
|
if (typeof str !== "string") return false;
|
|
205565
|
-
if (SUSPICIOUS_PATTERNS.some((re) => re
|
|
205601
|
+
if (SUSPICIOUS_PATTERNS.some((re) => patternHits(re, str))) return true;
|
|
205566
205602
|
if (SENSITIVE_PATH_PATTERN.test(str) && CONTENT_MOVE_VERB_PATTERN.test(str)) return true;
|
|
205567
205603
|
return false;
|
|
205568
205604
|
}
|
|
@@ -205570,9 +205606,9 @@ var require_mcp_poisoning_detector = __commonJS({
|
|
|
205570
205606
|
if (typeof value !== "string" || value.length < 5) return [];
|
|
205571
205607
|
const findings = [];
|
|
205572
205608
|
for (const regex of SUSPICIOUS_PATTERNS) {
|
|
205573
|
-
if (regex
|
|
205609
|
+
if (patternHits(regex, value)) {
|
|
205574
205610
|
findings.push({
|
|
205575
|
-
type:
|
|
205611
|
+
type: findingTypeFor(regex),
|
|
205576
205612
|
path,
|
|
205577
205613
|
snippet: value.substring(0, 120),
|
|
205578
205614
|
severity: "HIGH",
|
|
@@ -205743,6 +205779,130 @@ var require_mcp_poisoning_detector = __commonJS({
|
|
|
205743
205779
|
}
|
|
205744
205780
|
});
|
|
205745
205781
|
|
|
205782
|
+
// ../../src/tool-description-classifier.js
|
|
205783
|
+
var require_tool_description_classifier = __commonJS({
|
|
205784
|
+
"../../src/tool-description-classifier.js"(exports2, module2) {
|
|
205785
|
+
"use strict";
|
|
205786
|
+
var PATTERN = "TOOL_INSTRUCTION_DRIFT";
|
|
205787
|
+
var TAXONOMY = Object.freeze([
|
|
205788
|
+
"OWASP MCP Taxonomy: Tool Description Drift (glossary)",
|
|
205789
|
+
"OWASP MCP Taxonomy: Rug Pull (T2.1)",
|
|
205790
|
+
"OWASP MCP03: trusted tool update changes behavior"
|
|
205791
|
+
]);
|
|
205792
|
+
var DOES_NOT_PROVE = Object.freeze([
|
|
205793
|
+
"that the reworded text carries the same meaning to every model",
|
|
205794
|
+
"that no instruction was moved into a field this check does not read"
|
|
205795
|
+
]);
|
|
205796
|
+
var BRANCH_SIGNALS = Object.freeze([
|
|
205797
|
+
{
|
|
205798
|
+
id: "capability_claimed",
|
|
205799
|
+
re: /\b(?:also|now)\s+(?:reads?|writes?|accesses?|sends?|fetches?|calls?)\b|\bcan now\b/i,
|
|
205800
|
+
why: "claims a capability or scope the previous text did not"
|
|
205801
|
+
},
|
|
205802
|
+
{
|
|
205803
|
+
id: "condition_changed",
|
|
205804
|
+
re: /\b(?:use (?:this )?when|do not call|don't call|only (?:use|call)|never call)\b/i,
|
|
205805
|
+
why: "adds or removes a condition on when the tool applies"
|
|
205806
|
+
},
|
|
205807
|
+
{
|
|
205808
|
+
id: "concealment_instructed",
|
|
205809
|
+
re: /\b(?:do not|don't|never)\s+(?:mention|reveal|disclose|explain|tell|show|log)\b|\bwithout (?:telling|informing|notifying)\b/i,
|
|
205810
|
+
why: "instructs the model to hide something from the user"
|
|
205811
|
+
},
|
|
205812
|
+
{
|
|
205813
|
+
id: "priority_steered",
|
|
205814
|
+
re: /\b(?:use this (?:every time|always)|always use this|prefer this over|instead of (?:the )?other|do not use \w+ tool)\b/i,
|
|
205815
|
+
why: "steers the model toward or away from another tool"
|
|
205816
|
+
},
|
|
205817
|
+
// A: one verb. `exfiltrat\w*` is enough; leak|transmit|beacon|copy|mail added FP
|
|
205818
|
+
// with no extra TP (measured 2026-09-14).
|
|
205819
|
+
{
|
|
205820
|
+
id: "recipient_named",
|
|
205821
|
+
re: /\b(?:send|post|forward|upload|report|exfiltrat\w*)\b[^.]{0,40}\b(?:to|at)\s+(?:https?:\/\/|[\w.-]+\.[a-z]{2,}|@[\w.-]+)/i,
|
|
205822
|
+
why: "names an external recipient or resource that was not there before"
|
|
205823
|
+
},
|
|
205824
|
+
// D: co-occurrence in one sentence — a moving verb AND a credential/path.
|
|
205825
|
+
// `returns?` matches "return the key" and not "never returned" (`returned` ≠ `return`).
|
|
205826
|
+
{
|
|
205827
|
+
id: "credential_referenced",
|
|
205828
|
+
re: /\b(?:send|forward|copy|upload|post|transmit|exfiltrat\w*|leak|echo|reveal|expose|attach|returns?)\b[^\n.]{0,60}?\b(?:api[_ -]?key|secrets?|tokens?|credentials?|passwords?|\.env|id_rsa|~\/\.[\w-]+|private key)\b|\b(?:api[_ -]?key|secrets?|tokens?|credentials?|passwords?|\.env|id_rsa|~\/\.[\w-]+|private key)\b[^\n.]{0,60}?\b(?:send|forward|copy|upload|post|transmit|exfiltrat\w*|leak|echo|reveal|expose|attach|returns?)\b/i,
|
|
205829
|
+
why: "names a credential or secret path together with a verb that moves it"
|
|
205830
|
+
}
|
|
205831
|
+
]);
|
|
205832
|
+
function normalise(s) {
|
|
205833
|
+
return String(s).replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/[–—]/g, "-").replace(/\s+/g, " ").trim().toLowerCase();
|
|
205834
|
+
}
|
|
205835
|
+
function addedText(before, after) {
|
|
205836
|
+
const sents = (s) => String(s).split(/(?<=[.!?])\s+/).map((x) => x.trim()).filter(Boolean);
|
|
205837
|
+
const had = new Set(sents(before).map(normalise));
|
|
205838
|
+
return sents(after).filter((s) => !had.has(normalise(s)));
|
|
205839
|
+
}
|
|
205840
|
+
function classifyDescriptionChange(before, after) {
|
|
205841
|
+
const base = { pattern: null, taxonomy: TAXONOMY, does_not_prove: [...DOES_NOT_PROVE], signals: [] };
|
|
205842
|
+
if (typeof before !== "string" || typeof after !== "string" || !before.trim() || !after.trim()) {
|
|
205843
|
+
return {
|
|
205844
|
+
...base,
|
|
205845
|
+
classification: "CLASSIFICATION_UNDECIDABLE",
|
|
205846
|
+
risk: 0,
|
|
205847
|
+
execution_action: "REQUEST_APPROVAL",
|
|
205848
|
+
reason: "one side of the description is absent or empty \u2014 the change could not be classified, and an unclassified instruction change is not a reworded one"
|
|
205849
|
+
};
|
|
205850
|
+
}
|
|
205851
|
+
if (normalise(before) === normalise(after)) {
|
|
205852
|
+
return {
|
|
205853
|
+
...base,
|
|
205854
|
+
classification: "DESCRIPTION_REWORDED",
|
|
205855
|
+
risk: 0,
|
|
205856
|
+
execution_action: "CONTINUE",
|
|
205857
|
+
reason: "the text differs only in whitespace, case or punctuation"
|
|
205858
|
+
};
|
|
205859
|
+
}
|
|
205860
|
+
const added = addedText(before, after);
|
|
205861
|
+
const removed = addedText(after, before);
|
|
205862
|
+
if (added.length === 0 && removed.length === 0) {
|
|
205863
|
+
return {
|
|
205864
|
+
...base,
|
|
205865
|
+
classification: "CLASSIFICATION_UNDECIDABLE",
|
|
205866
|
+
risk: 0,
|
|
205867
|
+
execution_action: "REQUEST_APPROVAL",
|
|
205868
|
+
reason: "the descriptions differ but no sentence was added or removed \u2014 the change is inside a sentence, which this classifier does not read"
|
|
205869
|
+
};
|
|
205870
|
+
}
|
|
205871
|
+
const haystack = added.join(" ");
|
|
205872
|
+
const hits = BRANCH_SIGNALS.filter((s) => s.re.test(haystack)).map((s) => ({ id: s.id, why: s.why }));
|
|
205873
|
+
if (hits.length > 0) {
|
|
205874
|
+
return {
|
|
205875
|
+
...base,
|
|
205876
|
+
classification: "BRANCH_CONDITION_MOVED",
|
|
205877
|
+
// +15, one step above the +10 its neighbours carry for a SHAPE change. A moved instruction
|
|
205878
|
+
// changes what the tool does with an unchanged schema, which the shape checks cannot see;
|
|
205879
|
+
// pricing it below them would say the opposite.
|
|
205880
|
+
risk: 15,
|
|
205881
|
+
pattern: PATTERN,
|
|
205882
|
+
execution_action: "REQUEST_APPROVAL",
|
|
205883
|
+
signals: hits,
|
|
205884
|
+
reason: `added text ${hits.map((h) => h.why).join("; ")}`
|
|
205885
|
+
};
|
|
205886
|
+
}
|
|
205887
|
+
return {
|
|
205888
|
+
...base,
|
|
205889
|
+
classification: "DESCRIPTION_REWORDED",
|
|
205890
|
+
risk: 0,
|
|
205891
|
+
execution_action: "CONTINUE",
|
|
205892
|
+
reason: added.length ? "sentences changed, and none of them moves a branch condition" : "text was removed, and none of it held a branch condition"
|
|
205893
|
+
};
|
|
205894
|
+
}
|
|
205895
|
+
module2.exports = {
|
|
205896
|
+
classifyDescriptionChange,
|
|
205897
|
+
addedText,
|
|
205898
|
+
PATTERN,
|
|
205899
|
+
TAXONOMY,
|
|
205900
|
+
DOES_NOT_PROVE,
|
|
205901
|
+
BRANCH_SIGNALS
|
|
205902
|
+
};
|
|
205903
|
+
}
|
|
205904
|
+
});
|
|
205905
|
+
|
|
205746
205906
|
// ../../src/remediation-taxonomy.js
|
|
205747
205907
|
var require_remediation_taxonomy = __commonJS({
|
|
205748
205908
|
"../../src/remediation-taxonomy.js"(exports2, module2) {
|
|
@@ -208155,7 +208315,7 @@ var require_decision_result_v1_producer = __commonJS({
|
|
|
208155
208315
|
blast_radius: {
|
|
208156
208316
|
type: "object",
|
|
208157
208317
|
additionalProperties: false,
|
|
208158
|
-
description: "
|
|
208318
|
+
description: "Additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage (before_norm+after_norm+policy+scorer_version). Zeros are measured absence. consumers_declared and consumers_observed are separate: declared-but-never-called is a score question, not a count question. graph_source none = no request graph.",
|
|
208159
208319
|
required: [
|
|
208160
208320
|
"endpoints",
|
|
208161
208321
|
"fields",
|
|
@@ -208797,7 +208957,7 @@ var require_execution_grant_request_v2_producer = __commonJS({
|
|
|
208797
208957
|
"v2"
|
|
208798
208958
|
],
|
|
208799
208959
|
description: "src/change-set.js:1328 \u2014 `v2` selects the cr.exec.v2 issuer. 1344 MIGRATION: absent resolves through src/grant-version-default.js \u2014 cr.exec.v1 before 2026-09-18, cr.exec.v2 on and after. An explicit value always wins. `grantVersion` (camelCase) is accepted as an alias; both are listed so neither reads as undeclared. JSON Schema `default` is omitted on purpose (1363): a static default cannot be both v2 (the post-cutoff target) and v1 (what omitting yields during the window). Machine-readable: x-coderifts-effective-default is today's omit-value; x-coderifts-default-changes-at is when it becomes v2. The authorize response meta.grant_version is the version actually issued.",
|
|
208800
|
-
"x-coderifts-effective-default": "
|
|
208960
|
+
"x-coderifts-effective-default": "v2",
|
|
208801
208961
|
"x-coderifts-default-changes-at": "2026-09-18"
|
|
208802
208962
|
},
|
|
208803
208963
|
grantVersion: {
|
|
@@ -208807,7 +208967,7 @@ var require_execution_grant_request_v2_producer = __commonJS({
|
|
|
208807
208967
|
"v2"
|
|
208808
208968
|
],
|
|
208809
208969
|
description: "camelCase alias of grant_version, read at src/change-set.js:1268. Declared because the handler reads it, not because it is recommended. Same dated default as grant_version (no JSON Schema `default`; see x-coderifts-* on grant_version).",
|
|
208810
|
-
"x-coderifts-effective-default": "
|
|
208970
|
+
"x-coderifts-effective-default": "v2",
|
|
208811
208971
|
"x-coderifts-default-changes-at": "2026-09-18"
|
|
208812
208972
|
},
|
|
208813
208973
|
tenant_id: {
|
|
@@ -210385,6 +210545,11 @@ var require_change_set = __commonJS({
|
|
|
210385
210545
|
var { scoreMcpRisk } = require_mcp_risk_scorer();
|
|
210386
210546
|
var { detectPoisoning } = require_mcp_poisoning_detector();
|
|
210387
210547
|
var { classifyPoison, isLowerThan } = require_mcp_poison_gate();
|
|
210548
|
+
var {
|
|
210549
|
+
classifyDescriptionChange,
|
|
210550
|
+
addedText,
|
|
210551
|
+
PATTERN: TOOL_INSTRUCTION_DRIFT
|
|
210552
|
+
} = require_tool_description_classifier();
|
|
210388
210553
|
var { parse: parseGraphql } = require_graphql2();
|
|
210389
210554
|
var {
|
|
210390
210555
|
ensureDecisionSpec,
|
|
@@ -210614,25 +210779,83 @@ var require_change_set = __commonJS({
|
|
|
210614
210779
|
ensureDecisionSpec(art);
|
|
210615
210780
|
return finding(art);
|
|
210616
210781
|
}
|
|
210782
|
+
function mcpInstructionDrift(oldMcp, newMcp) {
|
|
210783
|
+
return toolListInstructionDrift(oldMcp && oldMcp.tools || [], newMcp && newMcp.tools || []);
|
|
210784
|
+
}
|
|
210785
|
+
function toolListInstructionDrift(oldTools, newTools) {
|
|
210786
|
+
const oldBy = new Map((oldTools || []).map((t) => [t && t.name, t]));
|
|
210787
|
+
for (const nt of newTools || []) {
|
|
210788
|
+
if (!nt || !nt.name) continue;
|
|
210789
|
+
const ot = oldBy.get(nt.name);
|
|
210790
|
+
if (!ot) continue;
|
|
210791
|
+
if (ot.description === nt.description) continue;
|
|
210792
|
+
const v = classifyDescriptionChange(ot.description, nt.description);
|
|
210793
|
+
if (v.execution_action === "REQUEST_APPROVAL") return true;
|
|
210794
|
+
}
|
|
210795
|
+
return false;
|
|
210796
|
+
}
|
|
210797
|
+
function rewriteDescriptions(oldVal, newVal) {
|
|
210798
|
+
if (Array.isArray(newVal)) {
|
|
210799
|
+
const oldArr = Array.isArray(oldVal) ? oldVal : [];
|
|
210800
|
+
return newVal.map((item, i) => rewriteDescriptions(oldArr[i], item));
|
|
210801
|
+
}
|
|
210802
|
+
if (!newVal || typeof newVal !== "object") return newVal;
|
|
210803
|
+
const oldObj = oldVal && typeof oldVal === "object" && !Array.isArray(oldVal) ? oldVal : {};
|
|
210804
|
+
const out = {};
|
|
210805
|
+
for (const [k, v] of Object.entries(newVal)) {
|
|
210806
|
+
if (k === "description" && typeof v === "string") {
|
|
210807
|
+
const prev = typeof oldObj[k] === "string" ? oldObj[k] : "";
|
|
210808
|
+
out[k] = addedText(prev, v).join(" ");
|
|
210809
|
+
} else {
|
|
210810
|
+
out[k] = rewriteDescriptions(oldObj[k], v);
|
|
210811
|
+
}
|
|
210812
|
+
}
|
|
210813
|
+
return out;
|
|
210814
|
+
}
|
|
210815
|
+
function manifestWithAddedSentences(oldManifest, newManifest) {
|
|
210816
|
+
const old = oldManifest && typeof oldManifest === "object" ? oldManifest : {};
|
|
210817
|
+
const neu = newManifest && typeof newManifest === "object" ? newManifest : {};
|
|
210818
|
+
const oldBy = new Map((Array.isArray(old.tools) ? old.tools : []).map((t) => [t && t.name, t]));
|
|
210819
|
+
const tools = (Array.isArray(neu.tools) ? neu.tools : []).map((nt) => {
|
|
210820
|
+
if (!nt || typeof nt !== "object" || !nt.name) return nt;
|
|
210821
|
+
const ot = oldBy.get(nt.name);
|
|
210822
|
+
if (!ot) return nt;
|
|
210823
|
+
return rewriteDescriptions(ot, nt);
|
|
210824
|
+
});
|
|
210825
|
+
const topOld = { ...old };
|
|
210826
|
+
const topNew = { ...neu };
|
|
210827
|
+
delete topOld.tools;
|
|
210828
|
+
delete topNew.tools;
|
|
210829
|
+
const view = rewriteDescriptions(topOld, topNew);
|
|
210830
|
+
view.tools = tools;
|
|
210831
|
+
return view;
|
|
210832
|
+
}
|
|
210617
210833
|
function analyzeMcpManifest(before, after) {
|
|
210618
210834
|
const oldMcp = parseManifest(before);
|
|
210619
210835
|
const newMcp = parseManifest(after);
|
|
210620
210836
|
const risk = scoreMcpRisk({ oldManifest: oldMcp, newManifest: newMcp });
|
|
210621
210837
|
const breaking = risk.signals.filter((s) => (s.impact || 0) > 0).length;
|
|
210622
210838
|
const patterns = risk.signals.map((s) => s.signal);
|
|
210839
|
+
if (mcpInstructionDrift(oldMcp, newMcp) && !patterns.includes(TOOL_INSTRUCTION_DRIFT)) {
|
|
210840
|
+
patterns.push(TOOL_INSTRUCTION_DRIFT);
|
|
210841
|
+
}
|
|
210623
210842
|
const art = {
|
|
210624
210843
|
risk_score: risk.score,
|
|
210625
210844
|
breaking_changes: breaking,
|
|
210626
210845
|
patterns
|
|
210627
210846
|
};
|
|
210628
210847
|
if (risk.score >= 50) art.should_block = true;
|
|
210629
|
-
const poisonGate = classifyPoison(detectPoisoning(oldMcp, newMcp));
|
|
210848
|
+
const poisonGate = classifyPoison(detectPoisoning(oldMcp, manifestWithAddedSentences(oldMcp, newMcp)));
|
|
210630
210849
|
if (poisonGate.tier === "block") art.omega_decision = "BLOCK";
|
|
210631
210850
|
ensureDecisionSpec(art);
|
|
210632
210851
|
if (poisonGate.tier === "ra" && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
|
|
210633
210852
|
art.decision = "REQUIRE_APPROVAL";
|
|
210634
210853
|
ensureDecisionSpec(art);
|
|
210635
210854
|
}
|
|
210855
|
+
if (patterns.includes(TOOL_INSTRUCTION_DRIFT) && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
|
|
210856
|
+
art.decision = "REQUIRE_APPROVAL";
|
|
210857
|
+
ensureDecisionSpec(art);
|
|
210858
|
+
}
|
|
210636
210859
|
return finding(art);
|
|
210637
210860
|
}
|
|
210638
210861
|
function parseAgentToolsPayload(v) {
|
|
@@ -210730,7 +210953,23 @@ var require_change_set = __commonJS({
|
|
|
210730
210953
|
reflex_triggers: json.reflex_triggers,
|
|
210731
210954
|
omega_components: json.omega_components
|
|
210732
210955
|
};
|
|
210956
|
+
const poisonGate = classifyPoison(detectPoisoning(
|
|
210957
|
+
{ tools: beforeTools },
|
|
210958
|
+
manifestWithAddedSentences({ tools: beforeTools }, { tools: afterTools })
|
|
210959
|
+
));
|
|
210960
|
+
if (poisonGate.tier === "block") art.omega_decision = "BLOCK";
|
|
210961
|
+
if (toolListInstructionDrift(beforeTools, afterTools) && !patterns.includes(TOOL_INSTRUCTION_DRIFT)) {
|
|
210962
|
+
patterns.push(TOOL_INSTRUCTION_DRIFT);
|
|
210963
|
+
}
|
|
210733
210964
|
ensureDecisionSpec(art);
|
|
210965
|
+
if (poisonGate.tier === "ra" && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
|
|
210966
|
+
art.decision = "REQUIRE_APPROVAL";
|
|
210967
|
+
ensureDecisionSpec(art);
|
|
210968
|
+
}
|
|
210969
|
+
if (patterns.includes(TOOL_INSTRUCTION_DRIFT) && isLowerThan(art.decision, "REQUIRE_APPROVAL")) {
|
|
210970
|
+
art.decision = "REQUIRE_APPROVAL";
|
|
210971
|
+
ensureDecisionSpec(art);
|
|
210972
|
+
}
|
|
210734
210973
|
return finding(art);
|
|
210735
210974
|
}
|
|
210736
210975
|
var ANALYZE_PERMISSION_KEYS = Object.freeze(["decision", "safe_for_agent", "execution_action"]);
|
|
@@ -210885,6 +211124,7 @@ var require_change_set = __commonJS({
|
|
|
210885
211124
|
var DECISION_SPEC_VERSION = "2.0";
|
|
210886
211125
|
var DECISION_SPEC_LEGACY_VERSION = "1.0";
|
|
210887
211126
|
var DECISION_SPEC_LEGACY_SUNSET = "2026-09-07T00:00:00Z";
|
|
211127
|
+
var DECISION_SPEC_LEGACY_SUNSET_AT_MS = Date.parse(DECISION_SPEC_LEGACY_SUNSET);
|
|
210888
211128
|
var ANALYSIS_OUTCOMES = Object.freeze({
|
|
210889
211129
|
NO_BREAK_DETECTED: "NO_BREAK_DETECTED",
|
|
210890
211130
|
BREAKS_DETECTED: "BREAKS_DETECTED",
|
|
@@ -210896,6 +211136,18 @@ var require_change_set = __commonJS({
|
|
|
210896
211136
|
const s = String(raw).trim();
|
|
210897
211137
|
return s === "1.0" || s === "1" || s === "decision-result.v1" || s === "decision-result.v1.0";
|
|
210898
211138
|
}
|
|
211139
|
+
function isLegacyDecisionSpecPinRetired(now) {
|
|
211140
|
+
const nowMs = now instanceof Date ? now.getTime() : typeof now === "number" ? now : Date.now();
|
|
211141
|
+
return nowMs >= DECISION_SPEC_LEGACY_SUNSET_AT_MS;
|
|
211142
|
+
}
|
|
211143
|
+
function rejectRetiredLegacyPin(input, deps) {
|
|
211144
|
+
if (!isLegacyDecisionSpecPin(input)) return;
|
|
211145
|
+
if (!isLegacyDecisionSpecPinRetired(deps && deps.now)) return;
|
|
211146
|
+
throw typedError(
|
|
211147
|
+
"INVALID_INPUT",
|
|
211148
|
+
`decision_spec_version '${DECISION_SPEC_LEGACY_VERSION}' retired at ${DECISION_SPEC_LEGACY_SUNSET}; Decision Spec ${DECISION_SPEC_VERSION} is required (preflight_mode required; analyze omits execution fields). Do not pin 1.0.`
|
|
211149
|
+
);
|
|
211150
|
+
}
|
|
210899
211151
|
function deriveAnalysisOutcome({ hasDegraded, decision, breaking_changes }) {
|
|
210900
211152
|
if (hasDegraded) return ANALYSIS_OUTCOMES.ANALYSIS_FAILED;
|
|
210901
211153
|
if (decision !== "ALLOW" || (breaking_changes || 0) > 0) return ANALYSIS_OUTCOMES.BREAKS_DETECTED;
|
|
@@ -210907,7 +211159,7 @@ var require_change_set = __commonJS({
|
|
|
210907
211159
|
if (legacy) return { mode: "analyze", defaulted: true };
|
|
210908
211160
|
throw typedError(
|
|
210909
211161
|
"INVALID_INPUT",
|
|
210910
|
-
"preflight_mode is required: 'analyze' (informational risk only) or 'authorize' (operation-bound; may mint a receipt). Migration: pass preflight_mode explicitly.
|
|
211162
|
+
"preflight_mode is required: 'analyze' (informational risk only) or 'authorize' (operation-bound; may mint a receipt). Migration: pass preflight_mode explicitly. Decision Spec 2.0 does not soft-default the mode."
|
|
210911
211163
|
);
|
|
210912
211164
|
}
|
|
210913
211165
|
if (raw === "analyze" || raw === "authorize") {
|
|
@@ -210946,6 +211198,7 @@ var require_change_set = __commonJS({
|
|
|
210946
211198
|
if (seen.has(key)) throw typedError("INVALID_INPUT", `duplicate artifact (type, id): (${a.type}, ${a.id})`);
|
|
210947
211199
|
seen.add(key);
|
|
210948
211200
|
}
|
|
211201
|
+
rejectRetiredLegacyPin(input, deps);
|
|
210949
211202
|
const legacyPin = isLegacyDecisionSpecPin(input);
|
|
210950
211203
|
const { mode: preflightMode, defaulted: preflightModeDefaulted } = resolvePreflightMode(input, {
|
|
210951
211204
|
legacy: legacyPin
|
|
@@ -211516,6 +211769,7 @@ var require_change_set = __commonJS({
|
|
|
211516
211769
|
resolvePreflightMode,
|
|
211517
211770
|
deriveAnalysisOutcome,
|
|
211518
211771
|
isLegacyDecisionSpecPin,
|
|
211772
|
+
isLegacyDecisionSpecPinRetired,
|
|
211519
211773
|
artifactSidesIdentical,
|
|
211520
211774
|
isNoChangeBundle,
|
|
211521
211775
|
isContractChangeTrigger,
|
|
@@ -211525,6 +211779,7 @@ var require_change_set = __commonJS({
|
|
|
211525
211779
|
DECISION_SPEC_VERSION,
|
|
211526
211780
|
DECISION_SPEC_LEGACY_VERSION,
|
|
211527
211781
|
DECISION_SPEC_LEGACY_SUNSET,
|
|
211782
|
+
DECISION_SPEC_LEGACY_SUNSET_AT_MS,
|
|
211528
211783
|
ANALYSIS_OUTCOMES,
|
|
211529
211784
|
MAX_ARTIFACTS,
|
|
211530
211785
|
SUPPORTED_TYPES,
|
|
@@ -224882,17 +225137,17 @@ var require_mcp_decision_result_shape = __commonJS({
|
|
|
224882
225137
|
audience: { type: ["string", "null"] },
|
|
224883
225138
|
authority: {
|
|
224884
225139
|
type: ["object", "null"],
|
|
224885
|
-
description: "
|
|
225140
|
+
description: "Additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
|
|
224886
225141
|
},
|
|
224887
225142
|
derivation: {
|
|
224888
225143
|
type: ["object", "null"],
|
|
224889
|
-
description: '
|
|
225144
|
+
description: 'Additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
|
|
224890
225145
|
},
|
|
224891
225146
|
receipt: { type: "object" },
|
|
224892
225147
|
expires_at: { type: "string" },
|
|
224893
225148
|
blast_radius: {
|
|
224894
225149
|
type: "object",
|
|
224895
|
-
description: "
|
|
225150
|
+
description: "Additive COUNTS (not a score). Not permission.",
|
|
224896
225151
|
properties: {
|
|
224897
225152
|
endpoints: { type: "integer", minimum: 0 },
|
|
224898
225153
|
fields: { type: "integer", minimum: 0 },
|
|
@@ -225031,7 +225286,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225031
225286
|
},
|
|
225032
225287
|
"detected_patterns": {
|
|
225033
225288
|
"type": "array",
|
|
225034
|
-
"description": "GOVERNANCE detector detail rows
|
|
225289
|
+
"description": "GOVERNANCE detector detail rows, emitted by the pattern detectors and validated against the decision-spec field contract before they leave the server. Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
|
|
225035
225290
|
"items": {
|
|
225036
225291
|
"type": "object",
|
|
225037
225292
|
"additionalProperties": false,
|
|
@@ -225081,7 +225336,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225081
225336
|
},
|
|
225082
225337
|
"breaking_changes_details": {
|
|
225083
225338
|
"type": "array",
|
|
225084
|
-
"description": "Per-change IR/detail rows from the engine
|
|
225339
|
+
"description": "Per-change IR/detail rows, mapped from the engine's change IR. Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
|
|
225085
225340
|
"items": {
|
|
225086
225341
|
"type": "object",
|
|
225087
225342
|
"additionalProperties": true,
|
|
@@ -225110,7 +225365,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225110
225365
|
},
|
|
225111
225366
|
"severity_summary": {
|
|
225112
225367
|
"type": "object",
|
|
225113
|
-
"description": "Bundle severity axes
|
|
225368
|
+
"description": "Bundle severity axes, computed once per change set. Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
|
|
225114
225369
|
"additionalProperties": false,
|
|
225115
225370
|
"properties": {
|
|
225116
225371
|
"diff_severity": {
|
|
@@ -225133,7 +225388,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225133
225388
|
"decision_basis": {},
|
|
225134
225389
|
"analysis": {
|
|
225135
225390
|
"type": "object",
|
|
225136
|
-
"description": "Tier-2 analysis mirror
|
|
225391
|
+
"description": "Tier-2 analysis mirror, assembled by the response builder alongside the control surface. Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
|
|
225137
225392
|
"properties": {
|
|
225138
225393
|
"breaking_changes": {
|
|
225139
225394
|
"type": "integer"
|
|
@@ -225260,7 +225515,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225260
225515
|
},
|
|
225261
225516
|
"human_report": {
|
|
225262
225517
|
"type": "object",
|
|
225263
|
-
"description": "Human-readable report tier
|
|
225518
|
+
"description": "Human-readable report tier, assembled by the response builder; analyze returns a reduced form. Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
|
|
225264
225519
|
"additionalProperties": false,
|
|
225265
225520
|
"properties": {
|
|
225266
225521
|
"summary": {
|
|
@@ -225320,7 +225575,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225320
225575
|
"blast_radius": {
|
|
225321
225576
|
"type": "object",
|
|
225322
225577
|
"additionalProperties": false,
|
|
225323
|
-
"description": "
|
|
225578
|
+
"description": "Additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage.",
|
|
225324
225579
|
"required": [
|
|
225325
225580
|
"endpoints",
|
|
225326
225581
|
"fields",
|
|
@@ -225485,7 +225740,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225485
225740
|
},
|
|
225486
225741
|
"detected_patterns": {
|
|
225487
225742
|
"type": "array",
|
|
225488
|
-
"description": "GOVERNANCE detector detail rows
|
|
225743
|
+
"description": "GOVERNANCE detector detail rows, emitted by the pattern detectors and validated against the decision-spec field contract before they leave the server. Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
|
|
225489
225744
|
"items": {
|
|
225490
225745
|
"type": "object",
|
|
225491
225746
|
"additionalProperties": false,
|
|
@@ -225535,7 +225790,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225535
225790
|
},
|
|
225536
225791
|
"breaking_changes_details": {
|
|
225537
225792
|
"type": "array",
|
|
225538
|
-
"description": "Per-change IR/detail rows from the engine
|
|
225793
|
+
"description": "Per-change IR/detail rows, mapped from the engine's change IR. Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
|
|
225539
225794
|
"items": {
|
|
225540
225795
|
"type": "object",
|
|
225541
225796
|
"additionalProperties": true,
|
|
@@ -225564,7 +225819,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225564
225819
|
},
|
|
225565
225820
|
"severity_summary": {
|
|
225566
225821
|
"type": "object",
|
|
225567
|
-
"description": "Bundle severity axes
|
|
225822
|
+
"description": "Bundle severity axes, computed once per change set. Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
|
|
225568
225823
|
"additionalProperties": false,
|
|
225569
225824
|
"properties": {
|
|
225570
225825
|
"diff_severity": {
|
|
@@ -225587,7 +225842,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225587
225842
|
"decision_basis": {},
|
|
225588
225843
|
"analysis": {
|
|
225589
225844
|
"type": "object",
|
|
225590
|
-
"description": "Tier-2 analysis mirror
|
|
225845
|
+
"description": "Tier-2 analysis mirror, assembled by the response builder alongside the control surface. Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
|
|
225591
225846
|
"properties": {
|
|
225592
225847
|
"breaking_changes": {
|
|
225593
225848
|
"type": "integer"
|
|
@@ -225714,7 +225969,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225714
225969
|
},
|
|
225715
225970
|
"human_report": {
|
|
225716
225971
|
"type": "object",
|
|
225717
|
-
"description": "Human-readable report tier
|
|
225972
|
+
"description": "Human-readable report tier, assembled by the response builder; analyze returns a reduced form. Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
|
|
225718
225973
|
"additionalProperties": false,
|
|
225719
225974
|
"properties": {
|
|
225720
225975
|
"summary": {
|
|
@@ -225736,7 +225991,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225736
225991
|
},
|
|
225737
225992
|
"execution_grant": {
|
|
225738
225993
|
"type": "string",
|
|
225739
|
-
"description": "Opt-in cr.exec.v1 execution grant (PHASE-0). Issued only when include_execution_grant is true on authorize. Short-lived mutation-bound sibling of chain_receipt; never unsigned. Optional inner state_nonce (ATOMIC profile) is additive and is NOT in scope_hash.
|
|
225994
|
+
"description": "Opt-in cr.exec.v1 execution grant (PHASE-0). Issued only when include_execution_grant is true on authorize. Short-lived mutation-bound sibling of chain_receipt; never unsigned. Optional inner state_nonce (ATOMIC profile) is additive and is NOT in scope_hash."
|
|
225740
225995
|
},
|
|
225741
225996
|
"chain_status": {
|
|
225742
225997
|
"type": "string"
|
|
@@ -225826,14 +226081,14 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225826
226081
|
"object",
|
|
225827
226082
|
"null"
|
|
225828
226083
|
],
|
|
225829
|
-
"description": "
|
|
226084
|
+
"description": "Additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
|
|
225830
226085
|
},
|
|
225831
226086
|
"derivation": {
|
|
225832
226087
|
"type": [
|
|
225833
226088
|
"object",
|
|
225834
226089
|
"null"
|
|
225835
226090
|
],
|
|
225836
|
-
"description": '
|
|
226091
|
+
"description": 'Additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
|
|
225837
226092
|
},
|
|
225838
226093
|
"receipt": {
|
|
225839
226094
|
"type": "object"
|
|
@@ -225843,7 +226098,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225843
226098
|
},
|
|
225844
226099
|
"blast_radius": {
|
|
225845
226100
|
"type": "object",
|
|
225846
|
-
"description": "
|
|
226101
|
+
"description": "Additive COUNTS (not a score). Not permission.",
|
|
225847
226102
|
"properties": {
|
|
225848
226103
|
"endpoints": {
|
|
225849
226104
|
"type": "integer",
|
|
@@ -225921,7 +226176,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
225921
226176
|
"blast_radius": {
|
|
225922
226177
|
"type": "object",
|
|
225923
226178
|
"additionalProperties": false,
|
|
225924
|
-
"description": "
|
|
226179
|
+
"description": "Additive COUNTS (not a score). Pure function of the change-set + request graphs. Not in the verdict_fingerprint preimage.",
|
|
225925
226180
|
"required": [
|
|
225926
226181
|
"endpoints",
|
|
225927
226182
|
"fields",
|
|
@@ -226080,7 +226335,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226080
226335
|
},
|
|
226081
226336
|
"detected_patterns": {
|
|
226082
226337
|
"type": "array",
|
|
226083
|
-
"description": "GOVERNANCE detector detail rows
|
|
226338
|
+
"description": "GOVERNANCE detector detail rows, emitted by the pattern detectors and validated against the decision-spec field contract before they leave the server. Row shape measured live: name, severity, description, consequence, affected_path, affected_field; optional side (request|response) on ENUM_NARROWING. Names \u2286 patterns (not equality). Agent-detector names may appear only in patterns. Free-text fields are untrusted.",
|
|
226084
226339
|
"items": {
|
|
226085
226340
|
"type": "object",
|
|
226086
226341
|
"additionalProperties": false,
|
|
@@ -226130,7 +226385,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226130
226385
|
},
|
|
226131
226386
|
"breaking_changes_details": {
|
|
226132
226387
|
"type": "array",
|
|
226133
|
-
"description": "Per-change IR/detail rows from the engine
|
|
226388
|
+
"description": "Per-change IR/detail rows, mapped from the engine's change IR. Measured row keys: type, path, method, field, severity, description. Distinct from breaking_changes (integer count).",
|
|
226134
226389
|
"items": {
|
|
226135
226390
|
"type": "object",
|
|
226136
226391
|
"additionalProperties": true,
|
|
@@ -226159,7 +226414,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226159
226414
|
},
|
|
226160
226415
|
"severity_summary": {
|
|
226161
226416
|
"type": "object",
|
|
226162
|
-
"description": "Bundle severity axes
|
|
226417
|
+
"description": "Bundle severity axes, computed once per change set. Distinct axes, not contradictory. Measured keys: diff_severity, governance_severity, policy_effect, note.",
|
|
226163
226418
|
"additionalProperties": false,
|
|
226164
226419
|
"properties": {
|
|
226165
226420
|
"diff_severity": {
|
|
@@ -226185,7 +226440,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226185
226440
|
"decision_basis": {},
|
|
226186
226441
|
"analysis": {
|
|
226187
226442
|
"type": "object",
|
|
226188
|
-
"description": "Tier-2 analysis mirror
|
|
226443
|
+
"description": "Tier-2 analysis mirror, assembled by the response builder alongside the control surface. Dual-write of the flat analysis fields present on the verdict, plus remediations[]. PROPERTIES ARE GENERATED \u2014 do not hand-edit them. OPEN BY DESIGN: additionalProperties stays TRUE and this is not an oversight. The fields above are copied conditionally, so which of them appear depends on the input \u2014 a verdict with no PII findings simply omits pii_findings. Closing this object would turn every future analysis field into a breaking change that fails inside the consumer, and would reject exactly the verdict paths that no one sampled when the union was built. Declared, not closed: you can now see what you may get, and you must still tolerate more.",
|
|
226189
226444
|
"properties": {
|
|
226190
226445
|
"breaking_changes": {
|
|
226191
226446
|
"type": "integer"
|
|
@@ -226312,7 +226567,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226312
226567
|
},
|
|
226313
226568
|
"human_report": {
|
|
226314
226569
|
"type": "object",
|
|
226315
|
-
"description": "Human-readable report tier
|
|
226570
|
+
"description": "Human-readable report tier, assembled by the response builder; analyze returns a reduced form. Measured keys: summary, breaking_highlights, suggestions, next_steps_prose.",
|
|
226316
226571
|
"additionalProperties": false,
|
|
226317
226572
|
"properties": {
|
|
226318
226573
|
"summary": {
|
|
@@ -226423,14 +226678,14 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226423
226678
|
"object",
|
|
226424
226679
|
"null"
|
|
226425
226680
|
],
|
|
226426
|
-
"description": "
|
|
226681
|
+
"description": "Additive. { audience, tenant_scope: bound|unbound, binding_proven_at? }. Informational \u2014 not permission, not a verify-receipt gate, not an ACL."
|
|
226427
226682
|
},
|
|
226428
226683
|
"derivation": {
|
|
226429
226684
|
"type": [
|
|
226430
226685
|
"object",
|
|
226431
226686
|
"null"
|
|
226432
226687
|
],
|
|
226433
|
-
"description": '
|
|
226688
|
+
"description": 'Additive. Present only when derivation:"server" produced this envelope. { source, platform?, base_sha, head_sha }. Covered by body_hash; not fingerprint.'
|
|
226434
226689
|
},
|
|
226435
226690
|
"receipt": {
|
|
226436
226691
|
"type": "object"
|
|
@@ -226440,7 +226695,7 @@ var require_mcp_preflight_output_schema = __commonJS({
|
|
|
226440
226695
|
},
|
|
226441
226696
|
"blast_radius": {
|
|
226442
226697
|
"type": "object",
|
|
226443
|
-
"description": "
|
|
226698
|
+
"description": "Additive COUNTS (not a score). Not permission.",
|
|
226444
226699
|
"properties": {
|
|
226445
226700
|
"endpoints": {
|
|
226446
226701
|
"type": "integer",
|
|
@@ -227336,7 +227591,7 @@ var require_mcp_streamable = __commonJS({
|
|
|
227336
227591
|
});
|
|
227337
227592
|
next();
|
|
227338
227593
|
});
|
|
227339
|
-
var DESC_PREFLIGHT_CHANGE_SET =
|
|
227594
|
+
var DESC_PREFLIGHT_CHANGE_SET = 'Use this when: a change set of contract artifacts modifies OpenAPI, GraphQL, protobuf, AsyncAPI, MCP manifests, or agent tool schemas before merge, deploy, publish, or tool registration. Do not call for documentation-only changes, static readiness scoring, or receipt verification. Use analyze for risk only; authorize requires context.operation for permission. For receipt verification use coderifts.verify_receipt instead; for details of a past decision use coderifts.get_decision_details instead. Inputs: preflight_mode is required: "analyze" (risk only; no receipt, no execution_action) or "authorize" (may mint a receipt; requires context.operation \u2014 merge is not deploy is not publish). Supply exactly one artifact source: artifacts[] (1\u201320 items, each {id, type, before, after} as the FULL spec/schema text, not a path or URL; type is openapi|graphql|grpc|asyncapi|mcp_manifest|agent_tools) XOR derivation="server" (server reads GitHub Compare; needs context.repository + context.base + context.head; sending artifacts[] together is 400). Grant fields (include_execution_grant, grant_version, tenant_id, executor_id, adapter_id, target_uri, expected_state_token, state_nonce, policy_hash) apply only when preflight_mode="authorize" AND include_execution_grant=true; analyze ignores them. previous_receipt is a chain token base64url(body).base64url(signature) to LINK a prior decision \u2014 it does not re-verify; call verify_receipt for that. idempotency_key replays authorize only (24h), never analyze.';
|
|
227340
227595
|
var DESC_VERIFY_RECEIPT = `Verify a CodeRifts signed chain-receipt you ALREADY HOLD: cryptographic
|
|
227341
227596
|
authenticity (signature + key id), body binding, and \u2014 when lifecycle indices
|
|
227342
227597
|
are available \u2014 whether it is currently valid authorization (not expired,
|
|
@@ -227576,23 +227831,33 @@ next_agent_step is a suggestion, not permission.`;
|
|
|
227576
227831
|
properties: {
|
|
227577
227832
|
artifacts: {
|
|
227578
227833
|
type: "array",
|
|
227579
|
-
description:
|
|
227834
|
+
description: '1\u201320 contract documents analyzed together. Each item is {id, type, before, after} where before/after are the FULL document strings (YAML/JSON/proto text), not URLs or file paths. Omit this array entirely when derivation="server".',
|
|
227580
227835
|
minItems: 1,
|
|
227581
227836
|
maxItems: 20,
|
|
227582
227837
|
items: {
|
|
227583
227838
|
type: "object",
|
|
227584
227839
|
properties: {
|
|
227585
227840
|
id: { type: "string", description: "Caller-chosen id, unique within the bundle" },
|
|
227586
|
-
type: {
|
|
227587
|
-
|
|
227588
|
-
|
|
227841
|
+
type: {
|
|
227842
|
+
type: "string",
|
|
227843
|
+
enum: ["openapi", "graphql", "grpc", "asyncapi", "mcp_manifest", "agent_tools"],
|
|
227844
|
+
description: "Artifact kind. One of openapi, graphql, grpc, asyncapi, mcp_manifest, agent_tools. Determines how before/after text is parsed. Not inferred from the filename."
|
|
227845
|
+
},
|
|
227846
|
+
before: {
|
|
227847
|
+
type: "string",
|
|
227848
|
+
description: 'The baseline document as raw text (the spec/schema/manifest body). Empty string means "no prior version" (create), not "load from disk".'
|
|
227849
|
+
},
|
|
227850
|
+
after: {
|
|
227851
|
+
type: "string",
|
|
227852
|
+
description: "The proposed document as raw text, same kind as before. Must be the bytes you intend to merge/deploy/publish, not a diff."
|
|
227853
|
+
}
|
|
227589
227854
|
},
|
|
227590
227855
|
required: ["id", "type", "before", "after"]
|
|
227591
227856
|
}
|
|
227592
227857
|
},
|
|
227593
227858
|
context: {
|
|
227594
227859
|
type: "object",
|
|
227595
|
-
description: "Optional apply-site context folded into the bundle fingerprint. operation distinguishes merge vs deploy vs publish (and other labels); the server accepts any string
|
|
227860
|
+
description: "Optional apply-site context folded into the bundle fingerprint. operation distinguishes merge vs deploy vs publish (and other labels); the server accepts any non-empty string; conventional values: merge, deploy, tool_call, publish. The receipt/gate must match this label.",
|
|
227596
227861
|
properties: {
|
|
227597
227862
|
// Server: context.operation is String(context.operation) with no closed enum
|
|
227598
227863
|
// (src/change-set.js computeBundleFingerprint / envelopeFields). Document conventional set.
|
|
@@ -227622,14 +227887,17 @@ next_agent_step is a suggestion, not permission.`;
|
|
|
227622
227887
|
// OPEN: unknown context keys are accepted (not 400) and ignored by buildChangeSet
|
|
227623
227888
|
// except the keys it reads. Do not set additionalProperties:false.
|
|
227624
227889
|
},
|
|
227625
|
-
previous_receipt: {
|
|
227890
|
+
previous_receipt: {
|
|
227891
|
+
type: "string",
|
|
227892
|
+
description: "Optional prior chain token to LINK this call into a receipt chain: base64url(body).base64url(signature). Linking is not verification \u2014 a linked token is not re-checked here; use verify_receipt."
|
|
227893
|
+
},
|
|
227626
227894
|
include_execution_grant: {
|
|
227627
227895
|
type: "boolean",
|
|
227628
|
-
description:
|
|
227896
|
+
description: 'Authorize only. When true on an allow-class authorize, the response includes a signed execution_grant, or HTTP 503 SIGNER_UNAVAILABLE \u2014 never an unsigned grant. Default false. Analyze ignores this flag. Ignored unless preflight_mode="authorize".'
|
|
227629
227897
|
},
|
|
227630
227898
|
state_nonce: {
|
|
227631
227899
|
type: "string",
|
|
227632
|
-
description: "
|
|
227900
|
+
description: "Authorize+grant only. Opaque nonce copied into the signed grant as its own field (not folded into scope_hash). Absent \u2192 BEARER grant. Ignored unless include_execution_grant is true."
|
|
227633
227901
|
},
|
|
227634
227902
|
// ── cr.exec.v2 authorize fields (1130-F1) ────────────────────────────────────────
|
|
227635
227903
|
// DECLARED, not added: the authorize handler already reads every one of these
|
|
@@ -227643,33 +227911,33 @@ next_agent_step is a suggestion, not permission.`;
|
|
|
227643
227911
|
// 1363: JSON Schema `default` is a SINGLE static value. A date-gated default cannot
|
|
227644
227912
|
// be that without lying on one side of the cutoff. The 1344 mechanism (resolver +
|
|
227645
227913
|
// GRANT_DEFAULT_V2_DATE) is unchanged; this is the schema expression of it.
|
|
227646
|
-
// 1384: the TEMPLATE
|
|
227647
|
-
//
|
|
227648
|
-
//
|
|
227649
|
-
//
|
|
227914
|
+
// 1384: the TEMPLATE starts at GRANT_VERSION_V1. visibleTools() stamps the LIVE
|
|
227915
|
+
// omit-value from resolveGrantVersion so the served header is truthful on both
|
|
227916
|
+
// sides of 2026-09-18. Decision B (2026-09-18): tools_sha256 hashes the served
|
|
227917
|
+
// tools[] including that live stamp — do not freeze it to v1.
|
|
227650
227918
|
"x-coderifts-effective-default": GRANT_VERSION_V1,
|
|
227651
227919
|
"x-coderifts-default-changes-at": GRANT_DEFAULT_V2_DATE,
|
|
227652
227920
|
description: "Grant envelope to mint when include_execution_grant is true. Omitting this yields cr.exec.v1 until " + GRANT_DEFAULT_V2_DATE + ' and cr.exec.v2 on and after it (see x-coderifts-effective-default / x-coderifts-default-changes-at). The response meta.grant_version is the version actually issued. An explicit value always wins \u2014 pin "v1" to keep current behaviour with no code change on the date.'
|
|
227653
227921
|
},
|
|
227654
227922
|
tenant_id: {
|
|
227655
227923
|
type: "string",
|
|
227656
|
-
description: 'Tenant the grant is issued under
|
|
227924
|
+
description: 'Authorize+grant v2 only. Tenant the grant is issued under. ASCII slug. When omitted the server uses "default" \u2014 pin it if you are not on the default tenant.'
|
|
227657
227925
|
},
|
|
227658
227926
|
executor_id: {
|
|
227659
227927
|
type: "string",
|
|
227660
|
-
description:
|
|
227928
|
+
description: 'Authorize+grant v2 only. Executor identity the grant is bound to (example: agent:ci-bot, host:github-actions). Empty/absent is not "any executor".'
|
|
227661
227929
|
},
|
|
227662
227930
|
adapter_id: {
|
|
227663
227931
|
type: "string",
|
|
227664
|
-
description: "Adapter
|
|
227932
|
+
description: "Authorize+grant v2 only. Adapter that will apply the change. Conventional values: fs, postgres, git. Must match the adapter the executor actually uses; a git grant does not authorize an fs write."
|
|
227665
227933
|
},
|
|
227666
227934
|
target_uri: {
|
|
227667
227935
|
type: "string",
|
|
227668
|
-
description: "
|
|
227936
|
+
description: "Authorize+grant v2 only. URI the grant binds (example: git://owner/repo.git/refs/heads/main). Fallback if omitted: context.target_uri, then repository/head-derived. Distinct from context.target_id."
|
|
227669
227937
|
},
|
|
227670
227938
|
expected_state_token: {
|
|
227671
227939
|
type: "string",
|
|
227672
|
-
description:
|
|
227940
|
+
description: 'Authorize+grant v2 only. Compare-and-swap token the executor must observe at apply time (the "before" state). Signed as its own field. Omit only if the adapter has no prior state; do not send a placeholder.'
|
|
227673
227941
|
},
|
|
227674
227942
|
audience: {
|
|
227675
227943
|
type: "string",
|
|
@@ -227677,25 +227945,25 @@ next_agent_step is a suggestion, not permission.`;
|
|
|
227677
227945
|
},
|
|
227678
227946
|
policy_hash: {
|
|
227679
227947
|
type: "string",
|
|
227680
|
-
description: "Policy identity the grant
|
|
227948
|
+
description: "Authorize+grant v2 only. Policy identity bound into the grant, sha256: + 64 hex. When supplied, apply must use that same policy; a different policy is a different grant."
|
|
227681
227949
|
},
|
|
227682
227950
|
idempotency_key: { type: "string", description: "Optional client key; in authorize mode, a repeat with the same key + body replays the original decision (24h). Analyze responses are not replayed." },
|
|
227683
227951
|
// TOP-LEVEL (not context). Decision Spec 2.0: REQUIRED on every call.
|
|
227684
|
-
//
|
|
227952
|
+
// Omission → INVALID_INPUT 400. The 1.0 legacy pin retired at DECISION_SPEC_LEGACY_SUNSET.
|
|
227685
227953
|
preflight_mode: {
|
|
227686
227954
|
type: "string",
|
|
227687
227955
|
enum: ["analyze", "authorize"],
|
|
227688
|
-
description:
|
|
227956
|
+
description: 'REQUIRED. "analyze" = informational risk only (no decision/execution_action/safe_for_agent; analysis_outcome + may_execute:false). "authorize" = operation-bound path; may mint a receipt (requires context.operation). Decision Spec 2.0: omission is INVALID_INPUT. Do not pin 1.0 \u2014 that pin retired.'
|
|
227689
227957
|
},
|
|
227690
227958
|
decision_spec_version: {
|
|
227691
227959
|
type: "string",
|
|
227692
|
-
enum: ["
|
|
227693
|
-
description: "Optional
|
|
227960
|
+
enum: ["2.0"],
|
|
227961
|
+
description: "Optional. Omit or '2.0' = current contract. '1.0' retired (INVALID_INPUT); do not pin 1.0."
|
|
227694
227962
|
},
|
|
227695
227963
|
derivation: {
|
|
227696
227964
|
type: "string",
|
|
227697
227965
|
enum: ["server"],
|
|
227698
|
-
description:
|
|
227966
|
+
description: '"server" = the server derives artifacts[] from GitHub Compare via the App installation. Allowed only when context.repository, context.base and context.head are all present and the tenant has a proven binding for that repository. Do not send artifacts[] in the same call (400 \u2014 one source of truth). Omit this field for the caller-supplied artifacts[] path.'
|
|
227699
227967
|
}
|
|
227700
227968
|
},
|
|
227701
227969
|
// artifacts is NOT in required: derivation:"server" omits it (server lists the set).
|
|
@@ -227795,7 +228063,7 @@ next_agent_step is a suggestion, not permission.`;
|
|
|
227795
228063
|
enum: RECEIPT_STATUSES.slice()
|
|
227796
228064
|
},
|
|
227797
228065
|
reason: { type: ["string", "null"] },
|
|
227798
|
-
correlation_id: { type: "string", description: "Route-owned trace id
|
|
228066
|
+
correlation_id: { type: "string", description: "Route-owned trace id, set by the route itself rather than by correlation middleware; always a non-empty string on 200" },
|
|
227799
228067
|
payload: { type: "object" },
|
|
227800
228068
|
// Authorization layer (§106): signature-valid ≠ currently-authorized.
|
|
227801
228069
|
// null means authorization could NOT be evaluated (missing intended context and/or
|
|
@@ -228316,9 +228584,12 @@ var require_generate_surface_anchor = __commonJS({
|
|
|
228316
228584
|
var OUT = path.join(__dirname, "..", "src", "generated", "surface-anchor.json");
|
|
228317
228585
|
var ENDPOINT = "https://app.coderifts.com/mcp";
|
|
228318
228586
|
var VOLATILE_KEYS = Object.freeze([]);
|
|
228319
|
-
var SOURCE_REF = "https://raw.githubusercontent.com/coderifts/api-governance/surface-
|
|
228320
|
-
var SOURCE_REF_COMMIT = "
|
|
228321
|
-
var
|
|
228587
|
+
var SOURCE_REF = "https://raw.githubusercontent.com/coderifts/api-governance/surface-497eb029/tools.wire.v1.json";
|
|
228588
|
+
var SOURCE_REF_COMMIT = "0ef58c28475fe1b85dc363da9a95bf566fca4587";
|
|
228589
|
+
var SOURCE_REF_TAG = (SOURCE_REF.match(/\/api-governance\/([^/]+)\//) || [])[1] || "(unknown tag)";
|
|
228590
|
+
var SOURCE_REF_IS_PINNED = /^[0-9a-f]{40}$/.test(SOURCE_REF_COMMIT);
|
|
228591
|
+
var SOURCE_REF_STATUS_OPENING = SOURCE_REF_IS_PINNED ? `PINNED TO ONE REVISION. source_ref is the wire-format tools[] as served, published in the public coderifts/api-governance repo at tag ${SOURCE_REF_TAG} (commit ${SOURCE_REF_COMMIT}). Verified at fill time: the file's own tools_sha256, its tools[] recomputed under the canonical rule in digest_input, and this anchor's tools_sha256 are the same value. ` : `NOT YET PINNED -- THE TAG DOES NOT EXIST YET. source_ref is the wire-format tools[] as served, to be published in the public coderifts/api-governance repo at tag ${SOURCE_REF_TAG} (commit ${SOURCE_REF_COMMIT}). Until that tag is pushed this URL 404s, and --verify-source-ref FAILS on it deliberately: a 404 here is the obligation below being unmet, not a network problem. Once it is pushed, verify at fill time that the file's own tools_sha256, its tools[] recomputed under the canonical rule in digest_input, and this anchor's tools_sha256 are the same value. `;
|
|
228592
|
+
var SOURCE_REF_STATUS = SOURCE_REF_STATUS_OPENING + "HOW TO USE IT: fetch it, recompute the digest yourself, and compare it to tools_sha256 here. IF THEY DIFFER, TRUST NEITHER -- it means the surface moved and this ref was not re-tagged. A tag can be force-moved; the commit SHA above cannot, so pin the SHA URL if you want the stronger form. OUR OBLIGATION: every surface change requires a NEW tag and a NEW source_ref in this file. Skipping that leaves this field pointing at an old surface while still looking authoritative, which is worse than the null it replaced. HOW THE OBLIGATION IS ENFORCED, on both sides: this repo CI-runs generate-surface-anchor.js --check --verify-source-ref, which fetches the ref above, recomputes its digest under the rule in digest_input, and FAILS if it is stale, self-inconsistent, or its tag is gone. The target repo runs scripts/validate-tools-wire.mjs from its own npm test and a CI workflow -- on push, on PR, and daily, because the wire target can go stale without anything in that repo changing. Both SKIP LOUDLY rather than fail on a transport error, and both treat a 404 as a real verdict rather than a network problem. Neither this ref nor those gates is a root of trust: pin the digest at YOUR approval time -- see honesty.pin_at_approval_time.";
|
|
228322
228593
|
var HONESTY = Object.freeze({
|
|
228323
228594
|
pins_bytes_not_behaviour: "This anchor pins BYTES, not behaviour. It says the tool text you received is the text that was published. It says nothing about whether the model read it, understood it, or obeyed it.",
|
|
228324
228595
|
scoped_to_its_profile: "This digest is scoped to the profile named above and to nothing else. The default-profile digest says NOTHING about the surface served with include_advanced_tools, which is a different tool set with a different digest. A hash without its profile does not identify a surface.",
|
|
@@ -228337,10 +228608,25 @@ var require_generate_surface_anchor = __commonJS({
|
|
|
228337
228608
|
}
|
|
228338
228609
|
return v;
|
|
228339
228610
|
}
|
|
228611
|
+
var lastDigestTools = null;
|
|
228612
|
+
function buildWire(opts = {}) {
|
|
228613
|
+
const anchor = buildAnchor(opts);
|
|
228614
|
+
const doc = {
|
|
228615
|
+
schema: "coderifts.tools-wire.v1",
|
|
228616
|
+
tool_count: anchor.tool_count,
|
|
228617
|
+
tools_sha256: anchor.tools_sha256,
|
|
228618
|
+
tools: lastDigestTools
|
|
228619
|
+
};
|
|
228620
|
+
const recomputed = `sha256:${crypto.createHash("sha256").update(canonicalJson(doc.tools), "utf8").digest("hex")}`;
|
|
228621
|
+
if (recomputed !== doc.tools_sha256) {
|
|
228622
|
+
throw new Error(`buildWire: emitted tools[] hashes to ${recomputed} but the anchor says ${doc.tools_sha256} \u2014 the wire file would be self-inconsistent`);
|
|
228623
|
+
}
|
|
228624
|
+
return doc;
|
|
228625
|
+
}
|
|
228340
228626
|
function buildAnchor(opts = {}) {
|
|
228341
228627
|
const includeAdvanced = opts.includeAdvanced === true;
|
|
228342
228628
|
const tools = mcp.visibleTools({ includeAdvanced, now: opts.now });
|
|
228343
|
-
const digestTools =
|
|
228629
|
+
const digestTools = tools;
|
|
228344
228630
|
const profile = buildRequestProfile({
|
|
228345
228631
|
endpoint: ENDPOINT,
|
|
228346
228632
|
method: "tools/list",
|
|
@@ -228348,11 +228634,12 @@ var require_generate_surface_anchor = __commonJS({
|
|
|
228348
228634
|
params: includeAdvanced ? { include_advanced_tools: true } : {}
|
|
228349
228635
|
});
|
|
228350
228636
|
const digest = crypto.createHash("sha256").update(canonicalJson(digestTools), "utf8").digest("hex");
|
|
228637
|
+
lastDigestTools = digestTools;
|
|
228351
228638
|
return {
|
|
228352
228639
|
profile,
|
|
228353
228640
|
tools_sha256: `sha256:${digest}`,
|
|
228354
228641
|
tool_count: tools.length,
|
|
228355
|
-
digest_input: "sha256 over the canonical JSON of the served tools[]: object keys sorted lexicographically at every depth (arrays keep order), no whitespace, RAW UTF-8 with no \\uXXXX escaping. Python consumers must pass ensure_ascii=False -- the default escapes the 58 non-ASCII characters in this surface and produces a different digest over identical data.",
|
|
228642
|
+
digest_input: "sha256 over the canonical JSON of the served tools[] AS RETURNED by tools/list (including x-coderifts-effective-default at hash time \u2014 do not freeze it to v1): object keys sorted lexicographically at every depth (arrays keep order), no whitespace, RAW UTF-8 with no \\uXXXX escaping. Python consumers must pass ensure_ascii=False -- the default escapes the 58 non-ASCII characters in this surface and produces a different digest over identical data.",
|
|
228356
228643
|
source_ref: SOURCE_REF,
|
|
228357
228644
|
// Machine-readable, so a consumer can pin the immutable form without parsing the prose above.
|
|
228358
228645
|
source_ref_commit: SOURCE_REF_COMMIT,
|
|
@@ -228438,6 +228725,25 @@ var require_generate_surface_anchor = __commonJS({
|
|
|
228438
228725
|
const check = argv.includes("--check");
|
|
228439
228726
|
const verifyRef = argv.includes("--verify-source-ref");
|
|
228440
228727
|
const anchor = buildAnchor();
|
|
228728
|
+
if (argv.includes("--emit-wire")) {
|
|
228729
|
+
const bytes = `${JSON.stringify(buildWire(), null, 2)}
|
|
228730
|
+
`;
|
|
228731
|
+
const i = argv.indexOf("--out");
|
|
228732
|
+
const out = i >= 0 ? argv[i + 1] : null;
|
|
228733
|
+
if (i >= 0 && !out) {
|
|
228734
|
+
process.stderr.write("--out needs a path\n");
|
|
228735
|
+
process.exitCode = 1;
|
|
228736
|
+
return;
|
|
228737
|
+
}
|
|
228738
|
+
if (out) {
|
|
228739
|
+
fs.writeFileSync(out, bytes);
|
|
228740
|
+
process.stderr.write(`wire written: ${out} (${buildWire().tool_count} tools, ${buildWire().tools_sha256})
|
|
228741
|
+
`);
|
|
228742
|
+
return;
|
|
228743
|
+
}
|
|
228744
|
+
process.stdout.write(bytes);
|
|
228745
|
+
return;
|
|
228746
|
+
}
|
|
228441
228747
|
if (verifyRef) {
|
|
228442
228748
|
const r = await verifySourceRef(anchor);
|
|
228443
228749
|
if (r.ok) {
|
|
@@ -228503,7 +228809,7 @@ var require_generate_surface_anchor = __commonJS({
|
|
|
228503
228809
|
process.exit(1);
|
|
228504
228810
|
});
|
|
228505
228811
|
}
|
|
228506
|
-
module2.exports = { buildAnchor, runCheck, verifySourceRef, canonicalJson, derived, derivedJson, VOLATILE_KEYS, HONESTY, OUT };
|
|
228812
|
+
module2.exports = { buildAnchor, buildWire, runCheck, verifySourceRef, canonicalJson, derived, derivedJson, VOLATILE_KEYS, HONESTY, OUT };
|
|
228507
228813
|
}
|
|
228508
228814
|
});
|
|
228509
228815
|
|
|
@@ -233561,13 +233867,14 @@ ${HOOK_MARKER}
|
|
|
233561
233867
|
#
|
|
233562
233868
|
# Re-install after CLI upgrades: coderifts hook install
|
|
233563
233869
|
#
|
|
233564
|
-
# Exit map (fail-closed when
|
|
233565
|
-
# CODERIFTS_SKIP=1
|
|
233566
|
-
# no
|
|
233567
|
-
#
|
|
233870
|
+
# Exit map (fail-closed when the hook is installed):
|
|
233871
|
+
# CODERIFTS_SKIP=1 \u2192 exit 0 (explicit loud override)
|
|
233872
|
+
# no key + CODERIFTS_ADVISORY=1|true \u2192 exit 0 (explicit opt-out; same flag as claude-hook)
|
|
233873
|
+
# no key \u2192 exit 1 named (absence is not permission)
|
|
233874
|
+
# BLOCK / STOP / UNKNOWN \u2192 exit 1
|
|
233568
233875
|
# INDETERMINATE (no decision obtainable) \u2192 exit 1
|
|
233569
|
-
# ALLOW / analysis clean
|
|
233570
|
-
# REQUIRE_APPROVAL / WARN
|
|
233876
|
+
# ALLOW / analysis clean \u2192 exit 0
|
|
233877
|
+
# REQUIRE_APPROVAL / WARN \u2192 warn, exit 0
|
|
233571
233878
|
|
|
233572
233879
|
# Explicit escape hatch \u2014 never the default on error; human must set this.
|
|
233573
233880
|
if [ "\${CODERIFTS_SKIP:-}" = "1" ]; then
|
|
@@ -233575,13 +233882,23 @@ if [ "\${CODERIFTS_SKIP:-}" = "1" ]; then
|
|
|
233575
233882
|
exit 0
|
|
233576
233883
|
fi
|
|
233577
233884
|
|
|
233578
|
-
|
|
233885
|
+
# git config wins when set. Do not clobber a process-env CODERIFTS_API_KEY with empty config.
|
|
233886
|
+
_CR_GIT_KEY=$(git config --get coderifts.apiKey 2>/dev/null || true)
|
|
233887
|
+
if [ -n "$_CR_GIT_KEY" ]; then
|
|
233888
|
+
CODERIFTS_API_KEY="$_CR_GIT_KEY"
|
|
233889
|
+
fi
|
|
233579
233890
|
SPEC_PATH=$(git config coderifts.specPath || echo "api/openapi.yaml")
|
|
233580
233891
|
ZERO="0000000000000000000000000000000000000000"
|
|
233581
233892
|
|
|
233582
|
-
if [ -z "
|
|
233583
|
-
|
|
233584
|
-
|
|
233893
|
+
if [ -z "\${CODERIFTS_API_KEY:-}" ]; then
|
|
233894
|
+
case "\${CODERIFTS_ADVISORY:-}" in
|
|
233895
|
+
1|true|TRUE)
|
|
233896
|
+
echo "CodeRifts: no API key \u2014 allowing push (CODERIFTS_ADVISORY). This is explicit opt-out, not a missing-key default. Set git config coderifts.apiKey or CODERIFTS_API_KEY to enforce." >&2
|
|
233897
|
+
exit 0
|
|
233898
|
+
;;
|
|
233899
|
+
esac
|
|
233900
|
+
echo "CodeRifts: this hook guards the push and cannot ask for a decision without an API key. Absence of a key is not permission. Set git config coderifts.apiKey <key> or export CODERIFTS_API_KEY. To allow without governance, set CODERIFTS_ADVISORY=1 (explicit opt-out) or CODERIFTS_SKIP=1 / git push --no-verify." >&2
|
|
233901
|
+
exit 1
|
|
233585
233902
|
fi
|
|
233586
233903
|
|
|
233587
233904
|
# --- helpers: three-state git blob read (present | absent | error) ---
|
|
@@ -234034,8 +234351,10 @@ exit 0
|
|
|
234034
234351
|
fs.writeFileSync(hookPath, PRE_PUSH_SCRIPT, { mode: 493 });
|
|
234035
234352
|
console.log("CodeRifts pre-push hook installed.");
|
|
234036
234353
|
console.log("");
|
|
234037
|
-
console.log("
|
|
234038
|
-
console.log("
|
|
234354
|
+
console.log("This hook guards the push. Absence of a key is not permission.");
|
|
234355
|
+
console.log("Set a key (git config coderifts.apiKey <your-key> or CODERIFTS_API_KEY)");
|
|
234356
|
+
console.log("or the next push exits 1. Explicit opt-out: CODERIFTS_ADVISORY=1.");
|
|
234357
|
+
console.log("Loud skip: CODERIFTS_SKIP=1 (or git push --no-verify).");
|
|
234039
234358
|
console.log("");
|
|
234040
234359
|
console.log("Optionally set spec path (default: api/openapi.yaml):");
|
|
234041
234360
|
console.log(" git config coderifts.specPath path/to/openapi.yaml");
|
|
@@ -263459,6 +263778,212 @@ ${USAGE}` };
|
|
|
263459
263778
|
}
|
|
263460
263779
|
});
|
|
263461
263780
|
|
|
263781
|
+
// ../../src/core/mcp-lock-drift.js
|
|
263782
|
+
var require_mcp_lock_drift = __commonJS({
|
|
263783
|
+
"../../src/core/mcp-lock-drift.js"(exports2, module2) {
|
|
263784
|
+
"use strict";
|
|
263785
|
+
var crypto = require("crypto");
|
|
263786
|
+
var { scoreMcpRisk, SIGNAL_CATALOG } = require_mcp_risk_scorer();
|
|
263787
|
+
function normalizeMcpManifest(input) {
|
|
263788
|
+
if (input == null) return null;
|
|
263789
|
+
if (Array.isArray(input)) {
|
|
263790
|
+
return { tools: input.filter((t) => t && typeof t === "object") };
|
|
263791
|
+
}
|
|
263792
|
+
if (typeof input !== "object") return null;
|
|
263793
|
+
if (Array.isArray(input.tools)) {
|
|
263794
|
+
return { tools: input.tools.filter((t) => t && typeof t === "object") };
|
|
263795
|
+
}
|
|
263796
|
+
if (input.manifest && typeof input.manifest === "object") {
|
|
263797
|
+
return normalizeMcpManifest(input.manifest);
|
|
263798
|
+
}
|
|
263799
|
+
if (input.server && typeof input.server === "object" && Array.isArray(input.server.tools)) {
|
|
263800
|
+
return { tools: input.server.tools.filter((t) => t && typeof t === "object") };
|
|
263801
|
+
}
|
|
263802
|
+
if (typeof input.name === "string" && (input.inputSchema || input.description != null)) {
|
|
263803
|
+
return { tools: [input] };
|
|
263804
|
+
}
|
|
263805
|
+
return null;
|
|
263806
|
+
}
|
|
263807
|
+
function manifestContentHash(manifest) {
|
|
263808
|
+
if (!manifest || !Array.isArray(manifest.tools)) return null;
|
|
263809
|
+
const tools = manifest.tools.map((t) => ({
|
|
263810
|
+
name: t && t.name != null ? String(t.name) : "",
|
|
263811
|
+
description: t && t.description != null ? String(t.description) : "",
|
|
263812
|
+
inputSchema: t && t.inputSchema != null ? t.inputSchema : null,
|
|
263813
|
+
// scopes / annotations if present
|
|
263814
|
+
annotations: t && t.annotations != null ? t.annotations : null
|
|
263815
|
+
})).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
263816
|
+
const material = JSON.stringify(tools);
|
|
263817
|
+
return `sha256:${crypto.createHash("sha256").update(material).digest("hex")}`;
|
|
263818
|
+
}
|
|
263819
|
+
function extractLockedManifestsFromLock(lockDoc) {
|
|
263820
|
+
if (!lockDoc || typeof lockDoc !== "object") return [];
|
|
263821
|
+
const out = [];
|
|
263822
|
+
if (Array.isArray(lockDoc.mcp_manifests)) {
|
|
263823
|
+
lockDoc.mcp_manifests.forEach((entry, i) => {
|
|
263824
|
+
if (!entry) return;
|
|
263825
|
+
const raw = entry.manifest != null ? entry.manifest : entry;
|
|
263826
|
+
const m = normalizeMcpManifest(raw);
|
|
263827
|
+
if (m) {
|
|
263828
|
+
out.push({
|
|
263829
|
+
id: entry.id != null ? String(entry.id) : `mcp_manifests[${i}]`,
|
|
263830
|
+
manifest: m
|
|
263831
|
+
});
|
|
263832
|
+
}
|
|
263833
|
+
});
|
|
263834
|
+
}
|
|
263835
|
+
if (Array.isArray(lockDoc.mcp_servers)) {
|
|
263836
|
+
lockDoc.mcp_servers.forEach((entry, i) => {
|
|
263837
|
+
if (!entry) return;
|
|
263838
|
+
const m = normalizeMcpManifest(entry.manifest != null ? entry.manifest : entry);
|
|
263839
|
+
if (m) {
|
|
263840
|
+
out.push({
|
|
263841
|
+
id: (entry.id || entry.name) != null ? String(entry.id || entry.name) : `mcp_servers[${i}]`,
|
|
263842
|
+
manifest: m
|
|
263843
|
+
});
|
|
263844
|
+
}
|
|
263845
|
+
});
|
|
263846
|
+
}
|
|
263847
|
+
if (lockDoc.locked_mcp_manifest) {
|
|
263848
|
+
const m = normalizeMcpManifest(lockDoc.locked_mcp_manifest);
|
|
263849
|
+
if (m) out.push({ id: "locked_mcp_manifest", manifest: m });
|
|
263850
|
+
}
|
|
263851
|
+
return out;
|
|
263852
|
+
}
|
|
263853
|
+
function buildLockDrift(args = {}) {
|
|
263854
|
+
const serverId = args.serverId != null ? String(args.serverId) : "default";
|
|
263855
|
+
if (args.liveUnreachable === true) {
|
|
263856
|
+
return {
|
|
263857
|
+
status: "unreachable",
|
|
263858
|
+
drift: false,
|
|
263859
|
+
// honest: unknown, not drifted
|
|
263860
|
+
server_id: serverId,
|
|
263861
|
+
locked_hash: manifestContentHash(normalizeMcpManifest(args.lockedManifest)),
|
|
263862
|
+
live_hash: null,
|
|
263863
|
+
risk_score: null,
|
|
263864
|
+
level: null,
|
|
263865
|
+
signals: [],
|
|
263866
|
+
findings: [],
|
|
263867
|
+
summary: "Live MCP manifest unreachable \u2014 not scored as drift and not claimed unchanged (absence_is_unknown).",
|
|
263868
|
+
live_error: args.liveError != null ? String(args.liveError).slice(0, 300) : null,
|
|
263869
|
+
interpretation: "absence_is_unknown",
|
|
263870
|
+
note: "Drift is a scored finding of measurable change, not an accusation of intent. Unreachable \u2260 drifted. Monitor only \u2014 does not block.",
|
|
263871
|
+
scorer: "scoreMcpRisk"
|
|
263872
|
+
};
|
|
263873
|
+
}
|
|
263874
|
+
const locked = normalizeMcpManifest(args.lockedManifest);
|
|
263875
|
+
const live = normalizeMcpManifest(args.liveManifest);
|
|
263876
|
+
if (!locked) {
|
|
263877
|
+
return {
|
|
263878
|
+
status: "missing_locked",
|
|
263879
|
+
drift: false,
|
|
263880
|
+
server_id: serverId,
|
|
263881
|
+
locked_hash: null,
|
|
263882
|
+
live_hash: manifestContentHash(live),
|
|
263883
|
+
risk_score: null,
|
|
263884
|
+
level: null,
|
|
263885
|
+
signals: [],
|
|
263886
|
+
findings: [],
|
|
263887
|
+
summary: "No locked MCP manifest supplied \u2014 cannot compute drift (not a clean bill of health).",
|
|
263888
|
+
interpretation: "absence_is_unknown",
|
|
263889
|
+
note: "coderifts.lock v1 records observed agents/ops, not MCP tool bodies; pass --locked-manifest or additive lock mcp_manifests.",
|
|
263890
|
+
scorer: "scoreMcpRisk"
|
|
263891
|
+
};
|
|
263892
|
+
}
|
|
263893
|
+
if (!live) {
|
|
263894
|
+
return {
|
|
263895
|
+
status: "missing_live",
|
|
263896
|
+
drift: false,
|
|
263897
|
+
server_id: serverId,
|
|
263898
|
+
locked_hash: manifestContentHash(locked),
|
|
263899
|
+
live_hash: null,
|
|
263900
|
+
risk_score: null,
|
|
263901
|
+
level: null,
|
|
263902
|
+
signals: [],
|
|
263903
|
+
findings: [],
|
|
263904
|
+
summary: "No live MCP manifest supplied \u2014 cannot compute drift (absence_is_unknown).",
|
|
263905
|
+
interpretation: "absence_is_unknown",
|
|
263906
|
+
note: "Provide --live-manifest or --live-url. Unreachable fetches use status unreachable.",
|
|
263907
|
+
scorer: "scoreMcpRisk"
|
|
263908
|
+
};
|
|
263909
|
+
}
|
|
263910
|
+
const scored = scoreMcpRisk({
|
|
263911
|
+
oldManifest: locked,
|
|
263912
|
+
newManifest: live
|
|
263913
|
+
});
|
|
263914
|
+
const locked_hash = manifestContentHash(locked);
|
|
263915
|
+
const live_hash = manifestContentHash(live);
|
|
263916
|
+
const hashDiffers = locked_hash != null && live_hash != null && locked_hash !== live_hash;
|
|
263917
|
+
const scoredDrift = (scored.score || 0) > 0;
|
|
263918
|
+
const isDrift = hashDiffers || scoredDrift;
|
|
263919
|
+
const unclassified = hashDiffers && !scoredDrift;
|
|
263920
|
+
const findings = (scored.signals || []).filter((s) => s && (s.impact == null || Number(s.impact) > 0)).map((s) => ({
|
|
263921
|
+
signal: s.signal,
|
|
263922
|
+
impact: s.impact,
|
|
263923
|
+
detail: s.detail,
|
|
263924
|
+
data: s.data || null,
|
|
263925
|
+
// Honest framing: finding, not accusation
|
|
263926
|
+
kind: "measurable_change"
|
|
263927
|
+
}));
|
|
263928
|
+
findings.sort((a, b) => {
|
|
263929
|
+
const sa = String(a.signal || "");
|
|
263930
|
+
const sb = String(b.signal || "");
|
|
263931
|
+
if (sa !== sb) return sa < sb ? -1 : 1;
|
|
263932
|
+
return String(a.detail || "").localeCompare(String(b.detail || ""));
|
|
263933
|
+
});
|
|
263934
|
+
return {
|
|
263935
|
+
status: isDrift ? "drift" : "unchanged",
|
|
263936
|
+
drift: isDrift,
|
|
263937
|
+
server_id: serverId,
|
|
263938
|
+
locked_hash,
|
|
263939
|
+
live_hash,
|
|
263940
|
+
hash_differs: hashDiffers,
|
|
263941
|
+
risk_score: scored.score,
|
|
263942
|
+
level: scored.level,
|
|
263943
|
+
signals: scored.signals || [],
|
|
263944
|
+
findings,
|
|
263945
|
+
unclassified_change: unclassified,
|
|
263946
|
+
summary: unclassified ? "MCP manifest DIFFERS from the locked baseline (content hashes disagree), but no catalogued risk signal explains the difference. Treated as drift: an unrecognised change is still a change. Compare the manifests directly \u2014 the signal catalog is not exhaustive." : isDrift ? scored.summary || "MCP manifest differs from locked baseline (scored finding)." : "Live MCP manifest matches locked baseline under mcp-risk-scorer (no risk signals).",
|
|
263947
|
+
interpretation: "number_is_what_is",
|
|
263948
|
+
note: "Drift is a scored FINDING of measurable change (tool_removed, schema change, \u2026), not an accusation of poison vs legitimate update. Monitor only \u2014 does not block. Scoring: scoreMcpRisk (existing SIGNAL_CATALOG; no reimplemented weights).",
|
|
263949
|
+
scorer: "scoreMcpRisk",
|
|
263950
|
+
signal_catalog_version: "mcp-risk-scorer"
|
|
263951
|
+
};
|
|
263952
|
+
}
|
|
263953
|
+
function buildLockDriftReport(pairs) {
|
|
263954
|
+
const list = Array.isArray(pairs) ? pairs : [];
|
|
263955
|
+
const results = list.map((p, i) => buildLockDrift({
|
|
263956
|
+
...p,
|
|
263957
|
+
serverId: p.serverId != null ? p.serverId : `server_${i}`
|
|
263958
|
+
})).sort((a, b) => String(a.server_id).localeCompare(String(b.server_id)));
|
|
263959
|
+
const anyDrift = results.some((r) => r.drift === true);
|
|
263960
|
+
const anyUnreachable = results.some((r) => r.status === "unreachable");
|
|
263961
|
+
const maxScore = results.reduce((m, r) => {
|
|
263962
|
+
if (r.risk_score == null || !Number.isFinite(r.risk_score)) return m;
|
|
263963
|
+
return Math.max(m, r.risk_score);
|
|
263964
|
+
}, 0);
|
|
263965
|
+
return {
|
|
263966
|
+
status: anyUnreachable && !anyDrift ? results.every((r) => r.status === "unreachable") ? "unreachable" : "partial" : anyDrift ? "drift" : "unchanged",
|
|
263967
|
+
drift: anyDrift,
|
|
263968
|
+
max_risk_score: maxScore,
|
|
263969
|
+
servers: results,
|
|
263970
|
+
summary: anyDrift ? `MCP lock drift on ${results.filter((r) => r.drift).length}/${results.length} server(s); max risk ${maxScore}.` : anyUnreachable ? "No scored drift; one or more live manifests unreachable (absence_is_unknown)." : "No MCP lock drift detected under scoreMcpRisk.",
|
|
263971
|
+
interpretation: "number_is_what_is",
|
|
263972
|
+
note: "Monitor report only. Existing poison-gate / policy engine remain the blockers. Unreachable is never treated as clean or as drift."
|
|
263973
|
+
};
|
|
263974
|
+
}
|
|
263975
|
+
module2.exports = {
|
|
263976
|
+
buildLockDrift,
|
|
263977
|
+
buildLockDriftReport,
|
|
263978
|
+
normalizeMcpManifest,
|
|
263979
|
+
manifestContentHash,
|
|
263980
|
+
extractLockedManifestsFromLock,
|
|
263981
|
+
SIGNAL_CATALOG
|
|
263982
|
+
// re-export for tests asserting reuse
|
|
263983
|
+
};
|
|
263984
|
+
}
|
|
263985
|
+
});
|
|
263986
|
+
|
|
263462
263987
|
// src/commands/lock.js
|
|
263463
263988
|
var require_lock = __commonJS({
|
|
263464
263989
|
"src/commands/lock.js"(exports2, module2) {
|
|
@@ -263523,6 +264048,7 @@ var require_lock = __commonJS({
|
|
|
263523
264048
|
return `${JSON.stringify(doc, null, 2)}
|
|
263524
264049
|
`;
|
|
263525
264050
|
}
|
|
264051
|
+
var mcpLockDrift = require_mcp_lock_drift();
|
|
263526
264052
|
function loadDriftCore(deps = {}) {
|
|
263527
264053
|
if (deps.buildLockDrift) {
|
|
263528
264054
|
return {
|
|
@@ -263531,21 +264057,7 @@ var require_lock = __commonJS({
|
|
|
263531
264057
|
extractLockedManifestsFromLock: deps.extractLockedManifestsFromLock
|
|
263532
264058
|
};
|
|
263533
264059
|
}
|
|
263534
|
-
|
|
263535
|
-
path.join(__dirname, "../../../../src/core/mcp-lock-drift"),
|
|
263536
|
-
path.join(__dirname, "../../../src/core/mcp-lock-drift")
|
|
263537
|
-
];
|
|
263538
|
-
for (const c of candidates) {
|
|
263539
|
-
try {
|
|
263540
|
-
return require(c);
|
|
263541
|
-
} catch (_) {
|
|
263542
|
-
}
|
|
263543
|
-
}
|
|
263544
|
-
const err = new Error(
|
|
263545
|
-
"Cannot load mcp-lock-drift core. Run from the coderifts-app monorepo (packages/cli expects ../../../../src/core/mcp-lock-drift)."
|
|
263546
|
-
);
|
|
263547
|
-
err.code = "CORE_UNAVAILABLE";
|
|
263548
|
-
throw err;
|
|
264060
|
+
return mcpLockDrift;
|
|
263549
264061
|
}
|
|
263550
264062
|
function readJsonFile(filePath) {
|
|
263551
264063
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -263819,6 +264331,176 @@ var require_lock = __commonJS({
|
|
|
263819
264331
|
}
|
|
263820
264332
|
});
|
|
263821
264333
|
|
|
264334
|
+
// ../../src/core/counterfactual-adopt.js
|
|
264335
|
+
var require_counterfactual_adopt = __commonJS({
|
|
264336
|
+
"../../src/core/counterfactual-adopt.js"(exports2, module2) {
|
|
264337
|
+
"use strict";
|
|
264338
|
+
var { analyzeSpecs } = require_analyzer();
|
|
264339
|
+
var SCOPE_NOTE = "COUNTERFACTUAL only: each row is what analyzeSpecs would have returned on the supplied before\u2192after content. It does not assert the change was actually harmful, and Step A does not join real decision_outcomes (outcome correlation is Step B).";
|
|
264340
|
+
function mapDecision(result) {
|
|
264341
|
+
if (!result || typeof result !== "object") return "REQUIRE_APPROVAL";
|
|
264342
|
+
const od = result.omega_decision;
|
|
264343
|
+
if (od === "ALLOW" || od === "WARN" || od === "REQUIRE_APPROVAL" || od === "BLOCK") {
|
|
264344
|
+
return od;
|
|
264345
|
+
}
|
|
264346
|
+
if (result.should_block === true) return "BLOCK";
|
|
264347
|
+
const pats = Array.isArray(result.detected_patterns) ? result.detected_patterns : [];
|
|
264348
|
+
if (pats.some((p) => p && (p.severity === "CRITICAL" || p.severity === "HIGH"))) {
|
|
264349
|
+
return "BLOCK";
|
|
264350
|
+
}
|
|
264351
|
+
const breaking = result.breaking_changes;
|
|
264352
|
+
const n = Array.isArray(breaking) ? breaking.length : typeof breaking === "number" ? breaking : 0;
|
|
264353
|
+
if (n > 0) return "REQUIRE_APPROVAL";
|
|
264354
|
+
return "ALLOW";
|
|
264355
|
+
}
|
|
264356
|
+
function mapExecutionAction(decision) {
|
|
264357
|
+
if (decision === "BLOCK") return "STOP";
|
|
264358
|
+
if (decision === "REQUIRE_APPROVAL") return "REQUEST_APPROVAL";
|
|
264359
|
+
if (decision === "WARN") return "CONTINUE_WITH_MONITORING";
|
|
264360
|
+
return "CONTINUE";
|
|
264361
|
+
}
|
|
264362
|
+
function extractDetectors(result) {
|
|
264363
|
+
const pats = Array.isArray(result && result.detected_patterns) ? result.detected_patterns : [];
|
|
264364
|
+
const out = [];
|
|
264365
|
+
for (const p of pats) {
|
|
264366
|
+
if (!p || typeof p !== "object") continue;
|
|
264367
|
+
const id = p.name || p.id || p.type;
|
|
264368
|
+
if (!id) continue;
|
|
264369
|
+
out.push({
|
|
264370
|
+
id: String(id),
|
|
264371
|
+
severity: p.severity != null ? String(p.severity) : "UNKNOWN"
|
|
264372
|
+
});
|
|
264373
|
+
}
|
|
264374
|
+
return out;
|
|
264375
|
+
}
|
|
264376
|
+
async function buildCounterfactualReport(changeSets, config = {}) {
|
|
264377
|
+
if (!Array.isArray(changeSets) || changeSets.length === 0) {
|
|
264378
|
+
return {
|
|
264379
|
+
change_sets: [],
|
|
264380
|
+
per_detector: [],
|
|
264381
|
+
scope_note: SCOPE_NOTE,
|
|
264382
|
+
interpretation: "absence_is_unknown",
|
|
264383
|
+
summary: {
|
|
264384
|
+
change_set_count: 0,
|
|
264385
|
+
would_stop: 0,
|
|
264386
|
+
would_request_approval: 0,
|
|
264387
|
+
would_continue: 0,
|
|
264388
|
+
note: 'No contract-artifact change sets supplied \u2014 empty is UNKNOWN, not "clean".'
|
|
264389
|
+
}
|
|
264390
|
+
};
|
|
264391
|
+
}
|
|
264392
|
+
const rows = [];
|
|
264393
|
+
const detectorMap = /* @__PURE__ */ new Map();
|
|
264394
|
+
for (const cs of changeSets) {
|
|
264395
|
+
const ref = cs && (cs.ref || cs.sha) || null;
|
|
264396
|
+
const date = cs && cs.date || null;
|
|
264397
|
+
const path = cs && cs.path || null;
|
|
264398
|
+
const before = cs && cs.before_spec != null ? String(cs.before_spec) : "";
|
|
264399
|
+
const after = cs && cs.after_spec != null ? String(cs.after_spec) : "";
|
|
264400
|
+
let result;
|
|
264401
|
+
let error = null;
|
|
264402
|
+
try {
|
|
264403
|
+
result = await analyzeSpecs(before, after, config || {});
|
|
264404
|
+
} catch (err) {
|
|
264405
|
+
error = {
|
|
264406
|
+
code: err && err.code ? String(err.code) : "analyze_error",
|
|
264407
|
+
message: err && err.message ? String(err.message).slice(0, 300) : "analyzeSpecs failed"
|
|
264408
|
+
};
|
|
264409
|
+
result = null;
|
|
264410
|
+
}
|
|
264411
|
+
if (!result) {
|
|
264412
|
+
rows.push({
|
|
264413
|
+
ref,
|
|
264414
|
+
date,
|
|
264415
|
+
path,
|
|
264416
|
+
decision: null,
|
|
264417
|
+
execution_action: null,
|
|
264418
|
+
risk_score: null,
|
|
264419
|
+
detectors_fired: [],
|
|
264420
|
+
would_have: null,
|
|
264421
|
+
error,
|
|
264422
|
+
counterfactual: true
|
|
264423
|
+
});
|
|
264424
|
+
continue;
|
|
264425
|
+
}
|
|
264426
|
+
const decision = mapDecision(result);
|
|
264427
|
+
const execution_action = mapExecutionAction(decision);
|
|
264428
|
+
const risk_score = result.risk_score != null && Number.isFinite(Number(result.risk_score)) ? Number(result.risk_score) : null;
|
|
264429
|
+
const detectors_fired = extractDetectors(result);
|
|
264430
|
+
for (const d of detectors_fired) {
|
|
264431
|
+
const prev = detectorMap.get(d.id);
|
|
264432
|
+
if (!prev) {
|
|
264433
|
+
detectorMap.set(d.id, { id: d.id, severity: d.severity, fire_count: 1 });
|
|
264434
|
+
} else {
|
|
264435
|
+
prev.fire_count += 1;
|
|
264436
|
+
prev.severity = worseSeverity(prev.severity, d.severity);
|
|
264437
|
+
}
|
|
264438
|
+
}
|
|
264439
|
+
rows.push({
|
|
264440
|
+
ref,
|
|
264441
|
+
date,
|
|
264442
|
+
path,
|
|
264443
|
+
decision,
|
|
264444
|
+
execution_action,
|
|
264445
|
+
risk_score,
|
|
264446
|
+
detectors_fired,
|
|
264447
|
+
would_have: {
|
|
264448
|
+
decision,
|
|
264449
|
+
execution_action,
|
|
264450
|
+
risk_score
|
|
264451
|
+
},
|
|
264452
|
+
error: null,
|
|
264453
|
+
counterfactual: true
|
|
264454
|
+
});
|
|
264455
|
+
}
|
|
264456
|
+
const per_detector = [...detectorMap.values()].sort((a, b) => b.fire_count - a.fire_count || a.id.localeCompare(b.id));
|
|
264457
|
+
let would_stop = 0;
|
|
264458
|
+
let would_request_approval = 0;
|
|
264459
|
+
let would_continue = 0;
|
|
264460
|
+
for (const r of rows) {
|
|
264461
|
+
if (r.execution_action === "STOP") would_stop += 1;
|
|
264462
|
+
else if (r.execution_action === "REQUEST_APPROVAL") would_request_approval += 1;
|
|
264463
|
+
else if (r.execution_action === "CONTINUE" || r.execution_action === "CONTINUE_WITH_MONITORING") {
|
|
264464
|
+
would_continue += 1;
|
|
264465
|
+
}
|
|
264466
|
+
}
|
|
264467
|
+
return {
|
|
264468
|
+
change_sets: rows,
|
|
264469
|
+
per_detector,
|
|
264470
|
+
scope_note: SCOPE_NOTE,
|
|
264471
|
+
interpretation: "counterfactual_observed",
|
|
264472
|
+
summary: {
|
|
264473
|
+
change_set_count: rows.length,
|
|
264474
|
+
would_stop,
|
|
264475
|
+
would_request_approval,
|
|
264476
|
+
would_continue,
|
|
264477
|
+
note: "Per-detector fire_count is the default-flip evidence input: high-volume detectors are false-positive noise candidates; rare+severe detectors are safer default-block candidates. This report does NOT auto-recommend a flip set."
|
|
264478
|
+
}
|
|
264479
|
+
};
|
|
264480
|
+
}
|
|
264481
|
+
var SEV_RANK = {
|
|
264482
|
+
CRITICAL: 4,
|
|
264483
|
+
HIGH: 3,
|
|
264484
|
+
MEDIUM: 2,
|
|
264485
|
+
LOW: 1,
|
|
264486
|
+
INFO: 0,
|
|
264487
|
+
UNKNOWN: 0
|
|
264488
|
+
};
|
|
264489
|
+
function worseSeverity(a, b) {
|
|
264490
|
+
const ra = SEV_RANK[String(a).toUpperCase()] != null ? SEV_RANK[String(a).toUpperCase()] : 0;
|
|
264491
|
+
const rb = SEV_RANK[String(b).toUpperCase()] != null ? SEV_RANK[String(b).toUpperCase()] : 0;
|
|
264492
|
+
return rb > ra ? b : a;
|
|
264493
|
+
}
|
|
264494
|
+
module2.exports = {
|
|
264495
|
+
buildCounterfactualReport,
|
|
264496
|
+
mapDecision,
|
|
264497
|
+
mapExecutionAction,
|
|
264498
|
+
extractDetectors,
|
|
264499
|
+
SCOPE_NOTE
|
|
264500
|
+
};
|
|
264501
|
+
}
|
|
264502
|
+
});
|
|
264503
|
+
|
|
263822
264504
|
// src/commands/adopt.js
|
|
263823
264505
|
var require_adopt = __commonJS({
|
|
263824
264506
|
"src/commands/adopt.js"(exports2, module2) {
|
|
@@ -263883,20 +264565,9 @@ var require_adopt = __commonJS({
|
|
|
263883
264565
|
else process.env.LOG_LEVEL = prevEnv;
|
|
263884
264566
|
}
|
|
263885
264567
|
}
|
|
264568
|
+
var counterfactualAdopt = require_counterfactual_adopt();
|
|
263886
264569
|
function loadCore() {
|
|
263887
|
-
|
|
263888
|
-
path.join(__dirname, "../../../../src/core/counterfactual-adopt"),
|
|
263889
|
-
path.join(__dirname, "../../../src/core/counterfactual-adopt")
|
|
263890
|
-
];
|
|
263891
|
-
for (const c of candidates) {
|
|
263892
|
-
try {
|
|
263893
|
-
return require(c);
|
|
263894
|
-
} catch (_) {
|
|
263895
|
-
}
|
|
263896
|
-
}
|
|
263897
|
-
throw new Error(
|
|
263898
|
-
"Cannot load counterfactual-adopt core. Run from the coderifts-app monorepo (packages/cli expects ../../../../src/core/counterfactual-adopt)."
|
|
263899
|
-
);
|
|
264570
|
+
return counterfactualAdopt;
|
|
263900
264571
|
}
|
|
263901
264572
|
function listCommits({ commits, days, gitImpl, cwd }) {
|
|
263902
264573
|
const args = ["log", "--format=%H %cI", "--reverse"];
|