coderifts 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +29 -14
- package/bin/coderifts.js +6 -6
- package/dist/cli.js +1854 -1732
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3007,7 +3007,7 @@ var require_package = __commonJS({
|
|
|
3007
3007
|
"package.json"(exports2, module2) {
|
|
3008
3008
|
module2.exports = {
|
|
3009
3009
|
name: "coderifts",
|
|
3010
|
-
version: "4.
|
|
3010
|
+
version: "4.3.0",
|
|
3011
3011
|
description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
|
|
3012
3012
|
author: "CodeRifts <hello@coderifts.com>",
|
|
3013
3013
|
license: "MIT",
|
|
@@ -71780,1056 +71780,1769 @@ var require_cjs3 = __commonJS({
|
|
|
71780
71780
|
}
|
|
71781
71781
|
});
|
|
71782
71782
|
|
|
71783
|
-
// src/commands/
|
|
71784
|
-
var
|
|
71785
|
-
"src/commands/
|
|
71783
|
+
// src/commands/claude-hook.js
|
|
71784
|
+
var require_claude_hook = __commonJS({
|
|
71785
|
+
"src/commands/claude-hook.js"(exports2, module2) {
|
|
71786
71786
|
"use strict";
|
|
71787
71787
|
var fs = require("fs");
|
|
71788
71788
|
var path = require("path");
|
|
71789
|
-
var
|
|
71790
|
-
var {
|
|
71791
|
-
var {
|
|
71792
|
-
|
|
71793
|
-
var
|
|
71794
|
-
|
|
71795
|
-
|
|
71789
|
+
var { execSync } = require("child_process");
|
|
71790
|
+
var { getApiKey } = require_config();
|
|
71791
|
+
var { cloudAuthorizePreflight } = require_cloud();
|
|
71792
|
+
var DEFAULT_SPEC_PATH = "api/openapi.yaml";
|
|
71793
|
+
var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
|
|
71794
|
+
"CONTINUE",
|
|
71795
|
+
"CONTINUE_WITH_MONITORING",
|
|
71796
|
+
"REQUEST_APPROVAL",
|
|
71797
|
+
"STOP"
|
|
71798
|
+
]);
|
|
71799
|
+
var USAGE = [
|
|
71800
|
+
"Usage: coderifts claude-hook",
|
|
71801
|
+
"",
|
|
71802
|
+
"Claude Code PreToolUse hook: gates Write|Edit|MultiEdit when they touch the",
|
|
71803
|
+
"configured contract spec path. Reads JSON context from STDIN.",
|
|
71804
|
+
"",
|
|
71805
|
+
"Exit map (Claude Code semantics \u2014 get this exactly right):",
|
|
71806
|
+
" 2 BLOCK \u2014 tool call cancelled (BLOCK/STOP, REQUEST_APPROVAL, unrecognised action,",
|
|
71807
|
+
" governance could not run, CONTINUE_WITH_MONITORING without a wired sink)",
|
|
71808
|
+
" 0 allow \u2014 CONTINUE, or CWM with a host-asserted monitoring sink, or non-contract skip",
|
|
71809
|
+
" 1 NEVER used for security deny (Claude treats exit 1 as non-blocking; action proceeds)",
|
|
71810
|
+
"",
|
|
71811
|
+
"Fail-closed DEFAULT (exit 2, reason enforce_indeterminate) when governance could not run:",
|
|
71812
|
+
" no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
|
|
71813
|
+
" Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
|
|
71814
|
+
"",
|
|
71815
|
+
"Parse-gap DEFAULT (exit 2): unparseable stdin / missing file_path \u2014 refusing (fail-closed);",
|
|
71816
|
+
" set CODERIFTS_ADVISORY=1 to soften. JSONL hook_blocked cause stdin_unparseable|missing_file_path.",
|
|
71817
|
+
"",
|
|
71818
|
+
"Still exit 0 (nothing to govern): non-spec path, identical content.",
|
|
71819
|
+
"",
|
|
71820
|
+
"CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
|
|
71821
|
+
" (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
|
|
71822
|
+
" Host claim \u2014 not delivery proof (same class as GuardConfig.monitoringSinkWired).",
|
|
71823
|
+
"",
|
|
71824
|
+
"CODERIFTS_STRICT=1|true: same fail-closed as default; cannot be weakened by ADVISORY.",
|
|
71825
|
+
"",
|
|
71826
|
+
"Spec path (same source as git pre-push hook):",
|
|
71827
|
+
" git config coderifts.specPath (default: api/openapi.yaml)",
|
|
71828
|
+
"",
|
|
71829
|
+
"Baseline at PreToolUse: before = current file on disk (pre-edit state \u2014 the tool has",
|
|
71830
|
+
"not written yet). after = proposed content from tool_input (Write: content; Edit/MultiEdit:",
|
|
71831
|
+
"old\u2192new applied to disk content).",
|
|
71832
|
+
"",
|
|
71833
|
+
"Push-time equivalent: coderifts hook install (git exit 1 on BLOCK)."
|
|
71834
|
+
].join("\n");
|
|
71835
|
+
function isEnvFlag(env, name) {
|
|
71836
|
+
const v = env && env[name];
|
|
71837
|
+
if (v == null || v === "") return false;
|
|
71838
|
+
const s = String(v).trim().toLowerCase();
|
|
71839
|
+
return s === "1" || s === "true";
|
|
71796
71840
|
}
|
|
71797
|
-
function
|
|
71798
|
-
|
|
71799
|
-
let enforcement;
|
|
71800
|
-
if (enforceSignal(options)) enforcement = "ENFORCING";
|
|
71801
|
-
else if (envVal === "unknown") enforcement = "UNKNOWN";
|
|
71802
|
-
else enforcement = "ADVISORY";
|
|
71803
|
-
const bypass_possible = String(process.env.CODERIFTS_DEPLOY_NO_BYPASS || "").toLowerCase() !== "true";
|
|
71804
|
-
return {
|
|
71805
|
-
enforcement,
|
|
71806
|
-
bypass_possible,
|
|
71807
|
-
step_is_required: enforcement === "ENFORCING",
|
|
71808
|
-
required_step_name: "CodeRifts / deploy-gate",
|
|
71809
|
-
attestation_source: "cli_flag"
|
|
71810
|
-
};
|
|
71841
|
+
function isStrictMode(env = process.env) {
|
|
71842
|
+
return isEnvFlag(env, "CODERIFTS_STRICT");
|
|
71811
71843
|
}
|
|
71812
|
-
function
|
|
71813
|
-
|
|
71814
|
-
|
|
71815
|
-
|
|
71816
|
-
|
|
71817
|
-
|
|
71818
|
-
|
|
71819
|
-
|
|
71820
|
-
|
|
71821
|
-
|
|
71844
|
+
function isAdvisoryMode(env = process.env) {
|
|
71845
|
+
return isEnvFlag(env, "CODERIFTS_ADVISORY");
|
|
71846
|
+
}
|
|
71847
|
+
function isMonitoringSinkWired({ env = process.env, deps = {}, result = null } = {}) {
|
|
71848
|
+
if (deps && deps.monitoringSinkWired === true) return true;
|
|
71849
|
+
if (isEnvFlag(env, "CODERIFTS_MONITORING_SINK_WIRED")) return true;
|
|
71850
|
+
const git = readGitConfig("coderifts.monitoringSinkWired", deps);
|
|
71851
|
+
if (git) {
|
|
71852
|
+
const s = String(git).trim().toLowerCase();
|
|
71853
|
+
if (s === "1" || s === "true") return true;
|
|
71822
71854
|
}
|
|
71823
|
-
return
|
|
71855
|
+
if (result && result.monitoringSinkWired === true) return true;
|
|
71856
|
+
const dr = result && result.decision_result;
|
|
71857
|
+
if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
|
|
71858
|
+
return false;
|
|
71824
71859
|
}
|
|
71825
|
-
|
|
71826
|
-
|
|
71860
|
+
var HOOK_TRIGGER_SOURCE = "claude_hook";
|
|
71861
|
+
function extractFingerprint(result) {
|
|
71862
|
+
const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
71863
|
+
const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
|
|
71864
|
+
const fp = dr.fingerprint || d.fingerprint || d.verdict_fingerprint || d.input_fingerprint;
|
|
71865
|
+
return typeof fp === "string" && fp.length > 0 ? fp : void 0;
|
|
71827
71866
|
}
|
|
71828
|
-
function
|
|
71829
|
-
|
|
71830
|
-
|
|
71831
|
-
|
|
71832
|
-
|
|
71833
|
-
|
|
71834
|
-
|
|
71835
|
-
|
|
71836
|
-
|
|
71837
|
-
|
|
71838
|
-
|
|
71839
|
-
|
|
71840
|
-
}
|
|
71841
|
-
const
|
|
71842
|
-
|
|
71843
|
-
|
|
71844
|
-
|
|
71845
|
-
|
|
71846
|
-
|
|
71847
|
-
|
|
71848
|
-
};
|
|
71849
|
-
let change_set_rebound = false;
|
|
71850
|
-
if (attested_enforcement === "ENFORCING") {
|
|
71851
|
-
if (expected_fingerprint != null) {
|
|
71852
|
-
requiredContext.expected_fingerprint = expected_fingerprint;
|
|
71853
|
-
change_set_rebound = true;
|
|
71854
|
-
}
|
|
71855
|
-
if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
|
|
71856
|
-
}
|
|
71857
|
-
const gate = deployGate({ deployTarget: { environment, artifact_id }, receipt, requiredContext });
|
|
71858
|
-
const enforcement_inescapable = attested_enforcement === "ENFORCING" && !!(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false);
|
|
71859
|
-
const inescapable_deploy = gate.inescapable_deploy === true && change_set_rebound === true;
|
|
71860
|
-
return {
|
|
71861
|
-
deploy_check_status: gate.state,
|
|
71862
|
-
reason: gate.reason,
|
|
71863
|
-
must_re_preflight: REPAIRABLE.has(gate.reason),
|
|
71864
|
-
attested_enforcement,
|
|
71865
|
-
gate: { deploy_allowed: gate.deploy_allowed, reason: gate.reason, inescapable_deploy },
|
|
71866
|
-
report_residuals: deployReportResiduals(gate.state, enforcement_inescapable, attested_enforcement, change_set_rebound),
|
|
71867
|
-
coverage_deploy_input: deployCoverageInput(attested_enforcement, inescapable_deploy)
|
|
71868
|
-
};
|
|
71867
|
+
function fingerprintPrefix(fp) {
|
|
71868
|
+
if (typeof fp !== "string" || fp.length === 0) return void 0;
|
|
71869
|
+
const colon = fp.indexOf(":");
|
|
71870
|
+
if (colon > 0 && colon < fp.length - 1) {
|
|
71871
|
+
const scheme = fp.slice(0, colon + 1);
|
|
71872
|
+
const rest = fp.slice(colon + 1);
|
|
71873
|
+
return scheme + rest.slice(0, 12) + (rest.length > 12 ? "\u2026" : "");
|
|
71874
|
+
}
|
|
71875
|
+
return fp.length > 16 ? `${fp.slice(0, 16)}\u2026` : fp;
|
|
71876
|
+
}
|
|
71877
|
+
function renderAllowProofBlock({ decision, executionAction, decisionId, fingerprint } = {}) {
|
|
71878
|
+
const decPart = decision ? ` decision=${decision}` : "";
|
|
71879
|
+
const eaPart = executionAction ? ` execution_action=${executionAction}` : "";
|
|
71880
|
+
const idPart = decisionId ? ` decision_id=${decisionId}` : "";
|
|
71881
|
+
const lines = [`CodeRifts claude-hook: AUTHORIZED${decPart}${eaPart}${idPart}`];
|
|
71882
|
+
const pref = fingerprintPrefix(fingerprint);
|
|
71883
|
+
if (pref) lines.push(`- receipt: ${pref}`);
|
|
71884
|
+
lines.push("- Interpretation: authorized at verify time for the bound scope only \u2014 see Limits.");
|
|
71885
|
+
lines.push("- authorized; execution/commit not proven by this hook");
|
|
71886
|
+
return lines.join("\n");
|
|
71869
71887
|
}
|
|
71870
|
-
function
|
|
71871
|
-
|
|
71872
|
-
if (
|
|
71873
|
-
|
|
71888
|
+
function hookSessionId(env) {
|
|
71889
|
+
const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
|
|
71890
|
+
if (fromEnv) return fromEnv;
|
|
71891
|
+
if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
|
|
71892
|
+
return hookSessionId._gen;
|
|
71874
71893
|
}
|
|
71875
|
-
function
|
|
71876
|
-
|
|
71877
|
-
|
|
71878
|
-
|
|
71894
|
+
function appendGuardEventLog(env, event, deps = {}) {
|
|
71895
|
+
const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
|
|
71896
|
+
if (p == null || String(p).trim() === "") return;
|
|
71897
|
+
const errLog = deps.errLog || ((m) => console.error(String(m)));
|
|
71898
|
+
const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
|
|
71879
71899
|
try {
|
|
71880
|
-
|
|
71881
|
-
} catch (
|
|
71882
|
-
|
|
71900
|
+
append(String(p).trim(), JSON.stringify(event) + "\n");
|
|
71901
|
+
} catch (err) {
|
|
71902
|
+
try {
|
|
71903
|
+
errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
|
|
71904
|
+
} catch {
|
|
71905
|
+
}
|
|
71883
71906
|
}
|
|
71884
71907
|
}
|
|
71885
|
-
function
|
|
71886
|
-
if (
|
|
71887
|
-
|
|
71888
|
-
return
|
|
71908
|
+
function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
|
|
71909
|
+
if (advisory && !strict) {
|
|
71910
|
+
errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
|
|
71911
|
+
return { exitCode: 0, reason: site, site };
|
|
71889
71912
|
}
|
|
71890
|
-
|
|
71891
|
-
|
|
71892
|
-
|
|
71913
|
+
if (strict) {
|
|
71914
|
+
errLog(
|
|
71915
|
+
`CodeRifts claude-hook: CODERIFTS_STRICT: governance could not run (${why}) \u2014 blocking. Set a key / restore network, or unset CODERIFTS_STRICT.`
|
|
71916
|
+
);
|
|
71917
|
+
return { exitCode: 2, reason: "enforce_indeterminate", site, strictBlocked: true };
|
|
71893
71918
|
}
|
|
71894
|
-
|
|
71895
|
-
|
|
71896
|
-
|
|
71897
|
-
|
|
71898
|
-
return [
|
|
71899
|
-
"# report the deploy outcome after your deploy step:",
|
|
71900
|
-
`coderifts outcome <deploy_succeeded|deploy_failed|rolled_back> --decision ${id}`
|
|
71901
|
-
].join("\n");
|
|
71919
|
+
errLog(
|
|
71920
|
+
`CodeRifts claude-hook: governance could not run (${why}) \u2014 blocking. Set CODERIFTS_ADVISORY=1 to allow without governance (explicit opt-out).`
|
|
71921
|
+
);
|
|
71922
|
+
return { exitCode: 2, reason: "enforce_indeterminate", site };
|
|
71902
71923
|
}
|
|
71903
|
-
function
|
|
71904
|
-
|
|
71905
|
-
|
|
71906
|
-
|
|
71907
|
-
|
|
71908
|
-
|
|
71909
|
-
|
|
71910
|
-
|
|
71911
|
-
|
|
71912
|
-
if (bind.report_residuals.length) lines.push(` Residuals: ${bind.report_residuals.join(", ")}`);
|
|
71913
|
-
if (bind.must_re_preflight) lines.push(chalk.yellow(" Action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it."));
|
|
71914
|
-
lines.push("");
|
|
71915
|
-
lines.push(enforce ? chalk.dim(" Enforcing \u2014 a failing gate exits non-zero (blocks the deploy step).") : chalk.dim(" Advisory (phase 1) \u2014 does not block the deploy (exit 0)."));
|
|
71916
|
-
lines.push("");
|
|
71917
|
-
return lines.join("\n");
|
|
71924
|
+
function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
|
|
71925
|
+
return failClosedOrAdvisory({
|
|
71926
|
+
advisory: false,
|
|
71927
|
+
strict: !!strict,
|
|
71928
|
+
site: reason,
|
|
71929
|
+
softMsg,
|
|
71930
|
+
why,
|
|
71931
|
+
errLog
|
|
71932
|
+
});
|
|
71918
71933
|
}
|
|
71919
|
-
|
|
71920
|
-
const
|
|
71921
|
-
const
|
|
71922
|
-
|
|
71923
|
-
|
|
71924
|
-
|
|
71925
|
-
return;
|
|
71926
|
-
}
|
|
71927
|
-
|
|
71928
|
-
const receipt = readReceiptFile(options.receipt);
|
|
71929
|
-
const observed = observeCDEnforcement({ enforce });
|
|
71930
|
-
const bind = deployBind({ environment, artifact_id: artifactId, receipt, observed_cd_enforcement: observed });
|
|
71931
|
-
const code = clampExit(bind.deploy_check_status, enforce);
|
|
71932
|
-
const decisionId = extractDecisionIdFromReceipt(receipt);
|
|
71933
|
-
if (options.json) {
|
|
71934
|
-
console.log(renderJson({
|
|
71935
|
-
command: "deploy-gate",
|
|
71936
|
-
environment,
|
|
71937
|
-
artifact_id: artifactId,
|
|
71938
|
-
phase: enforce ? "enforcing" : "advisory",
|
|
71939
|
-
exit_code: code,
|
|
71940
|
-
decision_id: decisionId,
|
|
71941
|
-
...bind
|
|
71942
|
-
}));
|
|
71943
|
-
} else {
|
|
71944
|
-
console.log(renderDeployGateTerminal(bind, enforce));
|
|
71945
|
-
if (decisionId) {
|
|
71946
|
-
console.log(formatOutcomeReportHint(decisionId));
|
|
71947
|
-
console.log("");
|
|
71948
|
-
}
|
|
71934
|
+
function readGitConfig(key, deps = {}) {
|
|
71935
|
+
const run = deps.execSync || execSync;
|
|
71936
|
+
const cwd = deps.cwd || process.cwd();
|
|
71937
|
+
try {
|
|
71938
|
+
const v = run(`git config ${key}`, { encoding: "utf8", cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
71939
|
+
const t = String(v || "").trim();
|
|
71940
|
+
return t || null;
|
|
71941
|
+
} catch {
|
|
71942
|
+
return null;
|
|
71949
71943
|
}
|
|
71950
|
-
process.exit(code);
|
|
71951
71944
|
}
|
|
71952
|
-
|
|
71953
|
-
|
|
71954
|
-
|
|
71955
|
-
|
|
71956
|
-
|
|
71957
|
-
|
|
71958
|
-
|
|
71959
|
-
|
|
71960
|
-
renderDeployGateTerminal
|
|
71961
|
-
};
|
|
71962
|
-
}
|
|
71963
|
-
});
|
|
71964
|
-
|
|
71965
|
-
// src/commands/publish-gate.js
|
|
71966
|
-
var require_publish_gate = __commonJS({
|
|
71967
|
-
"src/commands/publish-gate.js"(exports2, module2) {
|
|
71968
|
-
"use strict";
|
|
71969
|
-
var fs = require("fs");
|
|
71970
|
-
var path = require("path");
|
|
71971
|
-
var { execFileSync } = require("child_process");
|
|
71972
|
-
var chalk = require_source();
|
|
71973
|
-
var { getApiKey } = require_config();
|
|
71974
|
-
var { cloudDiff } = require_cloud();
|
|
71975
|
-
if (process.env.NO_COLOR) chalk.level = 0;
|
|
71976
|
-
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
71977
|
-
var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
|
|
71978
|
-
"CONTINUE",
|
|
71979
|
-
"CONTINUE_WITH_MONITORING",
|
|
71980
|
-
"REQUEST_APPROVAL",
|
|
71981
|
-
"STOP"
|
|
71982
|
-
]);
|
|
71983
|
-
var PERMIT_ACTIONS = /* @__PURE__ */ new Set(["CONTINUE", "CONTINUE_WITH_MONITORING"]);
|
|
71984
|
-
function defaultGit(args, cwd) {
|
|
71985
|
-
return execFileSync("git", args, {
|
|
71986
|
-
cwd: cwd || process.cwd(),
|
|
71987
|
-
encoding: "utf8",
|
|
71988
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
71989
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
71990
|
-
}).trim();
|
|
71945
|
+
function resolveApiKey(deps = {}) {
|
|
71946
|
+
const getKey = deps.getApiKey || getApiKey;
|
|
71947
|
+
const fromLogin = getKey();
|
|
71948
|
+
if (fromLogin) return fromLogin;
|
|
71949
|
+
const env = deps.env || process.env;
|
|
71950
|
+
const fromEnv = env.CODERIFTS_API_KEY && String(env.CODERIFTS_API_KEY).trim();
|
|
71951
|
+
if (fromEnv) return fromEnv;
|
|
71952
|
+
return readGitConfig("coderifts.apiKey", deps);
|
|
71991
71953
|
}
|
|
71992
|
-
function
|
|
71993
|
-
|
|
71994
|
-
|
|
71995
|
-
|
|
71996
|
-
} catch (err) {
|
|
71997
|
-
const msg = err && err.stderr ? String(err.stderr) : err && err.message || String(err);
|
|
71998
|
-
return {
|
|
71999
|
-
ok: false,
|
|
72000
|
-
code: "GIT_ERROR",
|
|
72001
|
-
message: `git error reading ${ref}:${filePath}: ${msg.slice(0, 300)}`
|
|
72002
|
-
};
|
|
72003
|
-
}
|
|
71954
|
+
function resolveSpecPath(deps = {}) {
|
|
71955
|
+
const fromGit = readGitConfig("coderifts.specPath", deps);
|
|
71956
|
+
if (fromGit) return fromGit;
|
|
71957
|
+
return DEFAULT_SPEC_PATH;
|
|
72004
71958
|
}
|
|
72005
|
-
function
|
|
72006
|
-
|
|
71959
|
+
function parseStdinJson(raw) {
|
|
71960
|
+
if (raw == null || String(raw).trim() === "") {
|
|
71961
|
+
return { ok: false, reason: "empty stdin" };
|
|
71962
|
+
}
|
|
71963
|
+
let obj;
|
|
72007
71964
|
try {
|
|
72008
|
-
|
|
72009
|
-
const pkg2 = JSON.parse(raw);
|
|
72010
|
-
return pkg2 && typeof pkg2.version === "string" ? pkg2.version : null;
|
|
71965
|
+
obj = JSON.parse(String(raw));
|
|
72011
71966
|
} catch {
|
|
72012
|
-
return
|
|
71967
|
+
return { ok: false, reason: "stdin is not JSON" };
|
|
71968
|
+
}
|
|
71969
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
71970
|
+
return { ok: false, reason: "stdin JSON is not an object" };
|
|
71971
|
+
}
|
|
71972
|
+
const toolName = obj.tool_name || obj.toolName || obj.name || null;
|
|
71973
|
+
let toolInput = obj.tool_input || obj.toolInput || obj.input || null;
|
|
71974
|
+
if (toolInput == null && obj.file_path) toolInput = obj;
|
|
71975
|
+
if (!toolName || typeof toolName !== "string") {
|
|
71976
|
+
return { ok: false, reason: "missing tool_name" };
|
|
72013
71977
|
}
|
|
71978
|
+
if (!toolInput || typeof toolInput !== "object") {
|
|
71979
|
+
return { ok: false, reason: "missing tool_input" };
|
|
71980
|
+
}
|
|
71981
|
+
return { ok: true, toolName: String(toolName), toolInput };
|
|
72014
71982
|
}
|
|
72015
|
-
function
|
|
72016
|
-
|
|
72017
|
-
|
|
72018
|
-
|
|
72019
|
-
|
|
72020
|
-
|
|
72021
|
-
const
|
|
72022
|
-
const
|
|
72023
|
-
if (
|
|
72024
|
-
const
|
|
72025
|
-
|
|
72026
|
-
tried.push(`tag:${tag}`);
|
|
72027
|
-
try {
|
|
72028
|
-
gitImpl(["rev-parse", "--verify", `${tag}^{commit}`], cwd);
|
|
72029
|
-
} catch {
|
|
72030
|
-
continue;
|
|
72031
|
-
}
|
|
72032
|
-
const shown = gitShow(tag, specPath, { gitImpl, cwd });
|
|
72033
|
-
if (!shown.ok) {
|
|
72034
|
-
return {
|
|
72035
|
-
ok: false,
|
|
72036
|
-
code: shown.code || "GIT_ERROR",
|
|
72037
|
-
message: shown.message,
|
|
72038
|
-
tried
|
|
72039
|
-
};
|
|
72040
|
-
}
|
|
72041
|
-
if (shown.content.trim() === "") {
|
|
72042
|
-
return {
|
|
72043
|
-
ok: false,
|
|
72044
|
-
code: "EMPTY_BEFORE",
|
|
72045
|
-
message: `Empty contract artifact at tag ${tag}:${specPath}. Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
|
|
72046
|
-
tried
|
|
72047
|
-
};
|
|
72048
|
-
}
|
|
72049
|
-
return { ok: true, content: shown.content, source: `tag:${tag}`, tried };
|
|
72050
|
-
}
|
|
72051
|
-
} else {
|
|
72052
|
-
tried.push("package.json:version (missing)");
|
|
71983
|
+
function isSpecPath(filePath, specPath) {
|
|
71984
|
+
if (!filePath || !specPath) return false;
|
|
71985
|
+
const fp = path.normalize(String(filePath).replace(/\\/g, "/"));
|
|
71986
|
+
const sp = path.normalize(String(specPath).replace(/\\/g, "/"));
|
|
71987
|
+
if (fp === sp) return true;
|
|
71988
|
+
if (fp.endsWith("/" + sp) || fp.endsWith(sp)) return true;
|
|
71989
|
+
const baseFp = path.basename(fp);
|
|
71990
|
+
const baseSp = path.basename(sp);
|
|
71991
|
+
if (baseFp === baseSp && (fp.endsWith(sp) || sp.endsWith(baseSp))) {
|
|
71992
|
+
const tail = sp.split("/").filter(Boolean).join("/");
|
|
71993
|
+
return fp.replace(/\\/g, "/").endsWith(tail);
|
|
72053
71994
|
}
|
|
72054
|
-
|
|
72055
|
-
|
|
72056
|
-
|
|
72057
|
-
|
|
72058
|
-
|
|
72059
|
-
|
|
72060
|
-
|
|
72061
|
-
continue;
|
|
71995
|
+
return false;
|
|
71996
|
+
}
|
|
71997
|
+
function deriveAfterContent(toolName, toolInput, diskBefore) {
|
|
71998
|
+
const name = String(toolName || "");
|
|
71999
|
+
if (name === "Write" || name === "write") {
|
|
72000
|
+
if (typeof toolInput.content !== "string") {
|
|
72001
|
+
return { ok: false, reason: "Write tool_input.content missing or not a string" };
|
|
72062
72002
|
}
|
|
72063
|
-
|
|
72064
|
-
|
|
72065
|
-
|
|
72066
|
-
|
|
72067
|
-
|
|
72068
|
-
|
|
72069
|
-
|
|
72070
|
-
tried
|
|
72071
|
-
};
|
|
72003
|
+
return { ok: true, after: toolInput.content };
|
|
72004
|
+
}
|
|
72005
|
+
if (name === "Edit" || name === "edit") {
|
|
72006
|
+
const oldS = toolInput.old_string != null ? toolInput.old_string : toolInput.oldString;
|
|
72007
|
+
const newS = toolInput.new_string != null ? toolInput.new_string : toolInput.newString;
|
|
72008
|
+
if (typeof oldS !== "string" || typeof newS !== "string") {
|
|
72009
|
+
return { ok: false, reason: "Edit tool_input.old_string/new_string missing" };
|
|
72072
72010
|
}
|
|
72073
|
-
if (
|
|
72074
|
-
return {
|
|
72075
|
-
ok: false,
|
|
72076
|
-
code: "EMPTY_BEFORE",
|
|
72077
|
-
message: `Empty contract artifact at merge-base ${mb}:${specPath} (${base}). Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
|
|
72078
|
-
tried
|
|
72079
|
-
};
|
|
72011
|
+
if (!diskBefore.includes(oldS)) {
|
|
72012
|
+
return { ok: false, reason: "Edit old_string not found in disk content" };
|
|
72080
72013
|
}
|
|
72081
|
-
return {
|
|
72082
|
-
ok: true,
|
|
72083
|
-
content: shown.content,
|
|
72084
|
-
source: `merge-base:${base}@${mb.slice(0, 12)}`,
|
|
72085
|
-
tried
|
|
72086
|
-
};
|
|
72014
|
+
return { ok: true, after: diskBefore.replace(oldS, newS) };
|
|
72087
72015
|
}
|
|
72088
|
-
|
|
72089
|
-
|
|
72090
|
-
|
|
72091
|
-
|
|
72092
|
-
tried
|
|
72093
|
-
};
|
|
72094
|
-
}
|
|
72095
|
-
function resolveAfterSpec(specPath, {
|
|
72096
|
-
cwd = process.cwd(),
|
|
72097
|
-
readFile = fs.readFileSync,
|
|
72098
|
-
exists = fs.existsSync
|
|
72099
|
-
} = {}) {
|
|
72100
|
-
const resolved = path.isAbsolute(specPath) ? specPath : path.join(cwd, specPath);
|
|
72101
|
-
try {
|
|
72102
|
-
if (!exists(resolved)) {
|
|
72103
|
-
return {
|
|
72104
|
-
ok: false,
|
|
72105
|
-
code: "AFTER_MISSING",
|
|
72106
|
-
message: `Working-tree contract artifact not found: ${specPath}`
|
|
72107
|
-
};
|
|
72016
|
+
if (name === "MultiEdit" || name === "multi_edit" || name === "multiEdit") {
|
|
72017
|
+
const edits = toolInput.edits || toolInput.Edits;
|
|
72018
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
72019
|
+
return { ok: false, reason: "MultiEdit tool_input.edits missing or empty" };
|
|
72108
72020
|
}
|
|
72109
|
-
|
|
72110
|
-
|
|
72111
|
-
|
|
72112
|
-
|
|
72113
|
-
|
|
72114
|
-
|
|
72115
|
-
|
|
72021
|
+
let cur = diskBefore;
|
|
72022
|
+
for (let i = 0; i < edits.length; i++) {
|
|
72023
|
+
const e = edits[i] || {};
|
|
72024
|
+
const oldS = e.old_string != null ? e.old_string : e.oldString;
|
|
72025
|
+
const newS = e.new_string != null ? e.new_string : e.newString;
|
|
72026
|
+
if (typeof oldS !== "string" || typeof newS !== "string") {
|
|
72027
|
+
return { ok: false, reason: `MultiEdit edits[${i}] missing old_string/new_string` };
|
|
72028
|
+
}
|
|
72029
|
+
if (!cur.includes(oldS)) {
|
|
72030
|
+
return { ok: false, reason: `MultiEdit edits[${i}] old_string not found in content` };
|
|
72031
|
+
}
|
|
72032
|
+
cur = cur.replace(oldS, newS);
|
|
72116
72033
|
}
|
|
72117
|
-
return { ok: true,
|
|
72118
|
-
} catch (err) {
|
|
72119
|
-
return {
|
|
72120
|
-
ok: false,
|
|
72121
|
-
code: "AFTER_READ_ERROR",
|
|
72122
|
-
message: `Failed to read working-tree ${specPath}: ${err && err.message}`
|
|
72123
|
-
};
|
|
72034
|
+
return { ok: true, after: cur };
|
|
72124
72035
|
}
|
|
72036
|
+
return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
|
|
72125
72037
|
}
|
|
72126
|
-
function
|
|
72127
|
-
if (
|
|
72128
|
-
|
|
72129
|
-
|
|
72130
|
-
|
|
72131
|
-
|
|
72132
|
-
|
|
72133
|
-
|
|
72134
|
-
|
|
72135
|
-
|
|
72136
|
-
|
|
72137
|
-
|
|
72138
|
-
|
|
72139
|
-
|
|
72140
|
-
|
|
72141
|
-
|
|
72142
|
-
|
|
72143
|
-
allow,
|
|
72144
|
-
execution_action: ea,
|
|
72145
|
-
decision: decision || null,
|
|
72146
|
-
policy: allow ? `permit:execution_action=${ea}` : `block:execution_action=${ea}`
|
|
72147
|
-
};
|
|
72148
|
-
}
|
|
72149
|
-
if (ea != null && ea !== "" && !CLOSED_ACTIONS.has(ea)) {
|
|
72150
|
-
return {
|
|
72151
|
-
allow: false,
|
|
72152
|
-
execution_action: ea,
|
|
72153
|
-
decision: decision || null,
|
|
72154
|
-
policy: `block:unrecognised_execution_action=${ea}`
|
|
72155
|
-
};
|
|
72156
|
-
}
|
|
72157
|
-
if (decision === "BLOCK" || decision === "REQUIRE_APPROVAL") {
|
|
72158
|
-
return {
|
|
72159
|
-
allow: false,
|
|
72160
|
-
execution_action: null,
|
|
72161
|
-
decision,
|
|
72162
|
-
policy: `block:decision=${decision}`
|
|
72163
|
-
};
|
|
72038
|
+
function isV2DecisionBody(d, dr) {
|
|
72039
|
+
if (dr && typeof dr === "object") return true;
|
|
72040
|
+
if (d.preflight_mode != null && d.preflight_mode !== "") return true;
|
|
72041
|
+
const ver = d.decision_spec_version;
|
|
72042
|
+
return typeof ver === "string" && ver.startsWith("2.");
|
|
72043
|
+
}
|
|
72044
|
+
function allowLegacyDecisionMap(d, dr) {
|
|
72045
|
+
return d.decision_spec_version === "1.0" && !isV2DecisionBody(d, dr);
|
|
72046
|
+
}
|
|
72047
|
+
function mapDecisionSeverity(result) {
|
|
72048
|
+
const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
72049
|
+
let ea = null;
|
|
72050
|
+
const dr = d.decision_result;
|
|
72051
|
+
if (dr && typeof dr === "object" && typeof dr.execution_action === "string" && dr.execution_action !== "") {
|
|
72052
|
+
ea = dr.execution_action;
|
|
72053
|
+
} else if (typeof d.execution_action === "string" && d.execution_action !== "") {
|
|
72054
|
+
ea = d.execution_action;
|
|
72164
72055
|
}
|
|
72165
|
-
|
|
72056
|
+
const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
|
|
72057
|
+
if (d.error && !hasDecision && (ea == null || ea === "")) {
|
|
72166
72058
|
return {
|
|
72167
|
-
|
|
72168
|
-
|
|
72169
|
-
|
|
72170
|
-
|
|
72059
|
+
severity: "INDETERMINATE",
|
|
72060
|
+
decision: null,
|
|
72061
|
+
decisionId: null,
|
|
72062
|
+
executionAction: null
|
|
72171
72063
|
};
|
|
72172
72064
|
}
|
|
72173
|
-
|
|
72174
|
-
if (
|
|
72175
|
-
|
|
72176
|
-
|
|
72177
|
-
|
|
72178
|
-
|
|
72179
|
-
|
|
72180
|
-
|
|
72065
|
+
let severity;
|
|
72066
|
+
if (ea != null && ea !== "") {
|
|
72067
|
+
if (!CLOSED_ACTIONS.has(ea)) {
|
|
72068
|
+
severity = "UNKNOWN";
|
|
72069
|
+
} else if (ea === "CONTINUE") {
|
|
72070
|
+
severity = "ALLOW";
|
|
72071
|
+
} else if (ea === "CONTINUE_WITH_MONITORING") {
|
|
72072
|
+
severity = "MONITOR";
|
|
72073
|
+
} else if (ea === "STOP") {
|
|
72074
|
+
severity = "BLOCK";
|
|
72075
|
+
} else {
|
|
72076
|
+
severity = "REQUIRE_APPROVAL";
|
|
72077
|
+
}
|
|
72078
|
+
} else if (allowLegacyDecisionMap(d, dr)) {
|
|
72079
|
+
const od = d.omega_decision || d.decision;
|
|
72080
|
+
if (od == null || od === "") {
|
|
72081
|
+
severity = "INDETERMINATE";
|
|
72082
|
+
} else if (od === "BLOCK") severity = "BLOCK";
|
|
72083
|
+
else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
|
|
72084
|
+
else if (od === "WARN") severity = "WARN";
|
|
72085
|
+
else severity = "ALLOW";
|
|
72086
|
+
} else {
|
|
72087
|
+
severity = "INDETERMINATE";
|
|
72181
72088
|
}
|
|
72089
|
+
const decision = dr && dr.decision || d.decision || d.omega_decision || null;
|
|
72090
|
+
const decisionId = dr && dr.decision_id || d.decision_id || null;
|
|
72182
72091
|
return {
|
|
72183
|
-
|
|
72184
|
-
|
|
72185
|
-
|
|
72186
|
-
|
|
72092
|
+
severity,
|
|
72093
|
+
decision: decision != null && decision !== "" ? String(decision) : null,
|
|
72094
|
+
decisionId: decisionId != null ? String(decisionId) : null,
|
|
72095
|
+
executionAction: ea
|
|
72187
72096
|
};
|
|
72188
72097
|
}
|
|
72189
|
-
function
|
|
72190
|
-
|
|
72191
|
-
const
|
|
72192
|
-
|
|
72193
|
-
|
|
72194
|
-
|
|
72195
|
-
|
|
72196
|
-
|
|
72197
|
-
|
|
72198
|
-
|
|
72199
|
-
|
|
72200
|
-
|
|
72201
|
-
|
|
72202
|
-
|
|
72203
|
-
|
|
72204
|
-
|
|
72098
|
+
function renderDecisionWhy(result, opts = {}) {
|
|
72099
|
+
const maxFixes = Number.isInteger(opts.maxFixes) ? opts.maxFixes : 5;
|
|
72100
|
+
const maxLines = Number.isInteger(opts.maxLines) ? opts.maxLines : 13;
|
|
72101
|
+
const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
72102
|
+
const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
|
|
72103
|
+
const pick = (k) => dr[k] !== void 0 ? dr[k] : d[k];
|
|
72104
|
+
const arr = (v) => Array.isArray(v) ? v : [];
|
|
72105
|
+
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
72106
|
+
const lines = [];
|
|
72107
|
+
const reasonCodes = [];
|
|
72108
|
+
for (const r of arr(pick("blocking_reasons"))) {
|
|
72109
|
+
if (!r || typeof r !== "object") continue;
|
|
72110
|
+
const code = str(r.code);
|
|
72111
|
+
const message = str(r.message);
|
|
72112
|
+
if (code) reasonCodes.push(code);
|
|
72113
|
+
if (!code && !message) continue;
|
|
72114
|
+
lines.push(`- ${code || "REASON"}${message ? `: ${message}` : ""}`);
|
|
72205
72115
|
}
|
|
72206
|
-
const
|
|
72207
|
-
|
|
72208
|
-
|
|
72209
|
-
|
|
72210
|
-
|
|
72211
|
-
|
|
72212
|
-
newSpec = yaml.load(after);
|
|
72213
|
-
} catch (e) {
|
|
72214
|
-
const err = new Error(`Failed to parse specs: ${e.message}`);
|
|
72215
|
-
err.code = "PREFLIGHT_UNREACHABLE";
|
|
72216
|
-
throw err;
|
|
72116
|
+
for (const r of arr(pick("degraded_reasons"))) {
|
|
72117
|
+
if (!r || typeof r !== "object") continue;
|
|
72118
|
+
const code = str(r.code);
|
|
72119
|
+
const message = str(r.message);
|
|
72120
|
+
if (!code && !message) continue;
|
|
72121
|
+
lines.push(`- degraded: ${[code, message].filter(Boolean).join(": ")}`);
|
|
72217
72122
|
}
|
|
72218
|
-
|
|
72219
|
-
|
|
72220
|
-
|
|
72221
|
-
|
|
72222
|
-
|
|
72223
|
-
|
|
72224
|
-
|
|
72225
|
-
const
|
|
72226
|
-
|
|
72227
|
-
|
|
72123
|
+
const action = str(pick("required_action"));
|
|
72124
|
+
if (action) lines.push(`- action: ${action}`);
|
|
72125
|
+
const rt = pick("remediation_transaction");
|
|
72126
|
+
const changes = arr(rt && typeof rt === "object" ? rt.required_changes : null).filter((c) => c && typeof c === "object");
|
|
72127
|
+
const shown = changes.slice(0, maxFixes);
|
|
72128
|
+
for (const c of shown) {
|
|
72129
|
+
const target = str(c.target);
|
|
72130
|
+
const instruction = str(c.instruction);
|
|
72131
|
+
if (!instruction && !target) continue;
|
|
72132
|
+
lines.push(`- fix${target ? ` (${target})` : ""}: ${instruction || str(c.precise_label)}`);
|
|
72228
72133
|
}
|
|
72229
|
-
|
|
72230
|
-
|
|
72231
|
-
const execution_action = breaking > 0 ? "STOP" : "CONTINUE";
|
|
72232
|
-
return {
|
|
72233
|
-
decision,
|
|
72234
|
-
omega_decision: decision,
|
|
72235
|
-
execution_action,
|
|
72236
|
-
decision_result: {
|
|
72237
|
-
decision,
|
|
72238
|
-
execution_action,
|
|
72239
|
-
decision_id: `local-${Date.now()}`
|
|
72240
|
-
},
|
|
72241
|
-
breaking_changes: diffResult.breakingDifferences || [],
|
|
72242
|
-
risk_score: Math.min(breaking * 15, 100)
|
|
72243
|
-
};
|
|
72134
|
+
if (changes.length > shown.length) lines.push(`- fix: +${changes.length - shown.length} more`);
|
|
72135
|
+
return { lines: lines.slice(0, maxLines), reasonCodes };
|
|
72244
72136
|
}
|
|
72245
|
-
async function
|
|
72246
|
-
const
|
|
72247
|
-
const
|
|
72248
|
-
const readFile = deps.readFile || fs.readFileSync.bind(fs);
|
|
72249
|
-
const exists = deps.exists || fs.existsSync.bind(fs);
|
|
72250
|
-
const preflightFn = deps.preflightFn || defaultPreflight;
|
|
72251
|
-
const log = deps.log || console.log.bind(console);
|
|
72252
|
-
const logErr = deps.logErr || console.error.bind(console);
|
|
72253
|
-
let specPath = options.spec;
|
|
72254
|
-
if (!specPath) {
|
|
72137
|
+
async function runClaudeHook(options = {}, deps = {}) {
|
|
72138
|
+
const errLog = deps.errLog || ((m) => console.error(String(m)));
|
|
72139
|
+
const readStdin = deps.readStdin || (() => {
|
|
72255
72140
|
try {
|
|
72256
|
-
|
|
72141
|
+
return fs.readFileSync(0, "utf8");
|
|
72257
72142
|
} catch {
|
|
72258
|
-
|
|
72143
|
+
return "";
|
|
72259
72144
|
}
|
|
72260
|
-
}
|
|
72261
|
-
if (!specPath) specPath = "api/openapi.yaml";
|
|
72262
|
-
try {
|
|
72263
|
-
gitImpl(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
72264
|
-
} catch (err) {
|
|
72265
|
-
const msg = "CodeRifts publish-gate: GIT_ERROR \u2014 not a git repository (or git unavailable). Cannot resolve before-spec without git. Fail-closed.";
|
|
72266
|
-
logErr(chalk.red(msg));
|
|
72267
|
-
return finish({
|
|
72268
|
-
ok: false,
|
|
72269
|
-
exitCode: 1,
|
|
72270
|
-
code: "GIT_ERROR",
|
|
72271
|
-
message: msg,
|
|
72272
|
-
policy: "fail_closed:git_error"
|
|
72273
|
-
}, { log, json: options.json });
|
|
72274
|
-
}
|
|
72275
|
-
const beforeRes = resolveBeforeSpec(specPath, {
|
|
72276
|
-
gitImpl,
|
|
72277
|
-
cwd,
|
|
72278
|
-
readFile,
|
|
72279
|
-
packageVersion: deps.packageVersion
|
|
72280
72145
|
});
|
|
72281
|
-
|
|
72282
|
-
|
|
72283
|
-
|
|
72284
|
-
|
|
72285
|
-
|
|
72286
|
-
|
|
72287
|
-
|
|
72288
|
-
|
|
72289
|
-
|
|
72290
|
-
|
|
72291
|
-
|
|
72292
|
-
|
|
72293
|
-
|
|
72294
|
-
|
|
72295
|
-
|
|
72296
|
-
|
|
72297
|
-
|
|
72298
|
-
|
|
72299
|
-
|
|
72300
|
-
|
|
72301
|
-
|
|
72302
|
-
|
|
72303
|
-
|
|
72304
|
-
|
|
72305
|
-
ok: true,
|
|
72306
|
-
exitCode: 0,
|
|
72307
|
-
code: "UNCHANGED",
|
|
72308
|
-
message: "Contract artifact unchanged vs baseline; publish permitted.",
|
|
72309
|
-
policy: "permit:unchanged",
|
|
72310
|
-
before_source: beforeRes.source,
|
|
72311
|
-
execution_action: "CONTINUE",
|
|
72312
|
-
receipt: null
|
|
72313
|
-
};
|
|
72314
|
-
if (!options.json) {
|
|
72315
|
-
log(chalk.green("CodeRifts publish-gate: ALLOW (unchanged)"));
|
|
72316
|
-
log(` baseline: ${beforeRes.source}`);
|
|
72317
|
-
log(` spec: ${specPath}`);
|
|
72318
|
-
}
|
|
72319
|
-
return finish(payload2, { log, json: options.json });
|
|
72320
|
-
}
|
|
72321
|
-
let result;
|
|
72322
|
-
try {
|
|
72323
|
-
result = await preflightFn(beforeRes.content, afterRes.content, {
|
|
72324
|
-
apiKey: deps.apiKey,
|
|
72325
|
-
cwd
|
|
72326
|
-
});
|
|
72327
|
-
} catch (err) {
|
|
72328
|
-
const msg = `CodeRifts publish-gate: PREFLIGHT_UNREACHABLE \u2014 ${err && err.message}`;
|
|
72329
|
-
logErr(chalk.red(msg));
|
|
72330
|
-
return finish({
|
|
72331
|
-
ok: false,
|
|
72332
|
-
exitCode: 1,
|
|
72333
|
-
code: "PREFLIGHT_UNREACHABLE",
|
|
72334
|
-
message: msg,
|
|
72335
|
-
policy: "fail_closed:preflight_unreachable"
|
|
72336
|
-
}, { log, json: options.json });
|
|
72337
|
-
}
|
|
72338
|
-
const perm = evaluatePublishPermission(result);
|
|
72339
|
-
const receipt = extractReceiptRef(result);
|
|
72340
|
-
if (!perm.allow) {
|
|
72341
|
-
const payload2 = {
|
|
72342
|
-
ok: false,
|
|
72343
|
-
exitCode: 1,
|
|
72344
|
-
code: "BLOCK",
|
|
72345
|
-
message: "Publish not permitted by execution_action / decision.",
|
|
72346
|
-
policy: perm.policy,
|
|
72347
|
-
execution_action: perm.execution_action,
|
|
72348
|
-
decision: perm.decision,
|
|
72349
|
-
before_source: beforeRes.source,
|
|
72350
|
-
receipt,
|
|
72351
|
-
fail_policy: "exit_1_on_block_or_resolver_error_or_unreachable"
|
|
72146
|
+
const readFile = deps.readFile || ((p) => fs.readFileSync(p, "utf8"));
|
|
72147
|
+
const exists = deps.exists || ((p) => fs.existsSync(p));
|
|
72148
|
+
const authorize = deps.cloudAuthorizePreflight || cloudAuthorizePreflight;
|
|
72149
|
+
const cwd = deps.cwd || process.cwd();
|
|
72150
|
+
const env = deps.env || process.env;
|
|
72151
|
+
const strict = isStrictMode(env);
|
|
72152
|
+
const advisory = isAdvisoryMode(env);
|
|
72153
|
+
const sessionId = hookSessionId(env);
|
|
72154
|
+
const logEv = (partial) => {
|
|
72155
|
+
appendGuardEventLog(env, {
|
|
72156
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
72157
|
+
sessionId,
|
|
72158
|
+
trigger_source: HOOK_TRIGGER_SOURCE,
|
|
72159
|
+
...partial
|
|
72160
|
+
}, { errLog, appendFileSync: deps.appendFileSync });
|
|
72161
|
+
};
|
|
72162
|
+
let tool = null;
|
|
72163
|
+
let filePathKnown = null;
|
|
72164
|
+
const emitTerminal = (r) => {
|
|
72165
|
+
const extra = {
|
|
72166
|
+
tool: tool || void 0,
|
|
72167
|
+
path: filePathKnown || void 0,
|
|
72168
|
+
decision: r.decision != null ? r.decision : void 0,
|
|
72169
|
+
decisionId: r.decisionId != null ? r.decisionId : void 0
|
|
72352
72170
|
};
|
|
72353
|
-
if (
|
|
72354
|
-
|
|
72355
|
-
|
|
72356
|
-
|
|
72357
|
-
|
|
72358
|
-
|
|
72359
|
-
|
|
72360
|
-
|
|
72361
|
-
|
|
72362
|
-
|
|
72363
|
-
|
|
72364
|
-
|
|
72171
|
+
if (r.exitCode === 2) {
|
|
72172
|
+
const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
|
|
72173
|
+
logEv({
|
|
72174
|
+
type: "hook_blocked",
|
|
72175
|
+
exit: 2,
|
|
72176
|
+
...extra,
|
|
72177
|
+
reasons,
|
|
72178
|
+
cause: r.reason || r.site
|
|
72179
|
+
});
|
|
72180
|
+
} else if (r.exitCode === 0 && r.site && advisory && !strict) {
|
|
72181
|
+
logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
|
|
72182
|
+
} else {
|
|
72183
|
+
const proof = r.reason === "allow" ? {
|
|
72184
|
+
decision_id: r.decisionId != null ? r.decisionId : extra.decisionId || null,
|
|
72185
|
+
fp_prefix: fingerprintPrefix(r.fingerprint) || null
|
|
72186
|
+
} : void 0;
|
|
72187
|
+
logEv({
|
|
72188
|
+
type: "hook_continue",
|
|
72189
|
+
exit: 0,
|
|
72190
|
+
...extra,
|
|
72191
|
+
cause: r.reason,
|
|
72192
|
+
...proof ? { proof } : {}
|
|
72193
|
+
});
|
|
72365
72194
|
}
|
|
72366
|
-
return
|
|
72367
|
-
}
|
|
72368
|
-
const payload = {
|
|
72369
|
-
ok: true,
|
|
72370
|
-
exitCode: 0,
|
|
72371
|
-
code: "ALLOW",
|
|
72372
|
-
message: "Publish permitted.",
|
|
72373
|
-
policy: perm.policy,
|
|
72374
|
-
execution_action: perm.execution_action,
|
|
72375
|
-
decision: perm.decision,
|
|
72376
|
-
before_source: beforeRes.source,
|
|
72377
|
-
receipt
|
|
72195
|
+
return r;
|
|
72378
72196
|
};
|
|
72379
|
-
|
|
72380
|
-
|
|
72381
|
-
|
|
72382
|
-
|
|
72383
|
-
|
|
72384
|
-
|
|
72385
|
-
|
|
72386
|
-
|
|
72387
|
-
|
|
72388
|
-
|
|
72389
|
-
|
|
72390
|
-
if (json) {
|
|
72391
|
-
log(JSON.stringify(payload, null, 2));
|
|
72197
|
+
const apiKey = resolveApiKey({ ...deps, cwd, env });
|
|
72198
|
+
if (!apiKey) {
|
|
72199
|
+
logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
|
|
72200
|
+
return emitTerminal(failClosedOrAdvisory({
|
|
72201
|
+
advisory,
|
|
72202
|
+
strict,
|
|
72203
|
+
site: "missing_api_key",
|
|
72204
|
+
why: "no API key",
|
|
72205
|
+
softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
|
|
72206
|
+
errLog
|
|
72207
|
+
}));
|
|
72392
72208
|
}
|
|
72393
|
-
|
|
72394
|
-
|
|
72395
|
-
|
|
72396
|
-
|
|
72397
|
-
|
|
72398
|
-
|
|
72399
|
-
|
|
72400
|
-
extractReceiptRef,
|
|
72401
|
-
gitShow,
|
|
72402
|
-
defaultGit,
|
|
72403
|
-
PERMIT_ACTIONS,
|
|
72404
|
-
CLOSED_ACTIONS
|
|
72405
|
-
};
|
|
72406
|
-
}
|
|
72407
|
-
});
|
|
72408
|
-
|
|
72409
|
-
// src/registry-validation-core.js
|
|
72410
|
-
var require_registry_validation_core = __commonJS({
|
|
72411
|
-
"src/registry-validation-core.js"(exports2, module2) {
|
|
72412
|
-
"use strict";
|
|
72413
|
-
var yaml = require_js_yaml();
|
|
72414
|
-
function safeParse(content) {
|
|
72415
|
-
if (!content) return null;
|
|
72416
|
-
try {
|
|
72417
|
-
const trimmed = content.trim();
|
|
72418
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
72419
|
-
return JSON.parse(trimmed);
|
|
72209
|
+
const PARSE_GAP_STDERR = "unparseable input \u2014 refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften";
|
|
72210
|
+
const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
|
|
72211
|
+
const parsed = parseStdinJson(raw);
|
|
72212
|
+
if (!parsed.ok) {
|
|
72213
|
+
if (advisory && !strict) {
|
|
72214
|
+
errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (CODERIFTS_ADVISORY)`);
|
|
72215
|
+
return emitTerminal({ exitCode: 0, reason: "stdin_unparseable", site: "stdin_unparseable" });
|
|
72420
72216
|
}
|
|
72421
|
-
|
|
72422
|
-
|
|
72423
|
-
return null;
|
|
72424
|
-
}
|
|
72425
|
-
}
|
|
72426
|
-
function validateOpenApiSpec(parsed) {
|
|
72427
|
-
if (!parsed || typeof parsed !== "object") {
|
|
72428
|
-
return { valid: false, error: "Not a valid YAML or JSON object" };
|
|
72217
|
+
errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
|
|
72218
|
+
return emitTerminal({ exitCode: 2, reason: "stdin_unparseable" });
|
|
72429
72219
|
}
|
|
72430
|
-
|
|
72431
|
-
|
|
72432
|
-
|
|
72433
|
-
|
|
72220
|
+
const { toolName, toolInput } = parsed;
|
|
72221
|
+
tool = toolName;
|
|
72222
|
+
const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
|
|
72223
|
+
if (!filePath || typeof filePath !== "string") {
|
|
72224
|
+
if (advisory && !strict) {
|
|
72225
|
+
errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
|
|
72226
|
+
return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
|
|
72434
72227
|
}
|
|
72435
|
-
|
|
72228
|
+
errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
|
|
72229
|
+
return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
|
|
72436
72230
|
}
|
|
72437
|
-
|
|
72438
|
-
|
|
72439
|
-
|
|
72440
|
-
|
|
72441
|
-
}
|
|
72442
|
-
return { valid: false, error: `Unsupported Swagger version: ${ver}` };
|
|
72231
|
+
filePathKnown = filePath;
|
|
72232
|
+
const specPath = resolveSpecPath({ ...deps, cwd });
|
|
72233
|
+
if (!isSpecPath(filePath, specPath)) {
|
|
72234
|
+
logEv({ type: "detection_skip", signals: ["not_spec_path"], tool, path: filePath });
|
|
72235
|
+
return emitTerminal({ exitCode: 0, reason: "not_spec_path" });
|
|
72443
72236
|
}
|
|
72444
|
-
|
|
72445
|
-
|
|
72446
|
-
|
|
72447
|
-
|
|
72448
|
-
|
|
72449
|
-
|
|
72450
|
-
|
|
72451
|
-
|
|
72452
|
-
|
|
72453
|
-
|
|
72454
|
-
|
|
72455
|
-
|
|
72456
|
-
|
|
72457
|
-
|
|
72458
|
-
|
|
72459
|
-
}
|
|
72237
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
72238
|
+
let diskBefore = "";
|
|
72239
|
+
if (exists(absPath)) {
|
|
72240
|
+
try {
|
|
72241
|
+
diskBefore = readFile(absPath, "utf8");
|
|
72242
|
+
} catch (e) {
|
|
72243
|
+
logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
|
|
72244
|
+
return emitTerminal(failClosedOrAdvisory({
|
|
72245
|
+
advisory,
|
|
72246
|
+
strict,
|
|
72247
|
+
site: "disk_unreadable",
|
|
72248
|
+
why: `cannot read contract file ${absPath}`,
|
|
72249
|
+
softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
|
|
72250
|
+
errLog
|
|
72251
|
+
}));
|
|
72460
72252
|
}
|
|
72461
72253
|
}
|
|
72462
|
-
|
|
72463
|
-
|
|
72464
|
-
|
|
72465
|
-
|
|
72466
|
-
|
|
72467
|
-
|
|
72468
|
-
|
|
72469
|
-
|
|
72254
|
+
const derived = deriveAfterContent(toolName, toolInput, diskBefore);
|
|
72255
|
+
if (!derived.ok) {
|
|
72256
|
+
logEv({ type: "preflight_unavailable", cause: "edit_apply_failed", tool, path: filePath });
|
|
72257
|
+
return emitTerminal(failClosedOrAdvisory({
|
|
72258
|
+
advisory,
|
|
72259
|
+
strict,
|
|
72260
|
+
site: "edit_apply_failed",
|
|
72261
|
+
why: derived.reason,
|
|
72262
|
+
softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
|
|
72263
|
+
errLog
|
|
72264
|
+
}));
|
|
72470
72265
|
}
|
|
72471
|
-
|
|
72472
|
-
|
|
72473
|
-
|
|
72474
|
-
const scopes = /* @__PURE__ */ new Set();
|
|
72475
|
-
const schemes = parsed.components?.securitySchemes || {};
|
|
72476
|
-
for (const scheme of Object.values(schemes)) {
|
|
72477
|
-
if (scheme.type === "oauth2" && scheme.flows) {
|
|
72478
|
-
for (const flow of Object.values(scheme.flows)) {
|
|
72479
|
-
if (flow.scopes) {
|
|
72480
|
-
for (const scope of Object.keys(flow.scopes)) {
|
|
72481
|
-
scopes.add(scope);
|
|
72482
|
-
}
|
|
72483
|
-
}
|
|
72484
|
-
}
|
|
72485
|
-
}
|
|
72266
|
+
if (diskBefore === derived.after) {
|
|
72267
|
+
logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
|
|
72268
|
+
return emitTerminal({ exitCode: 0, reason: "identical" });
|
|
72486
72269
|
}
|
|
72487
|
-
|
|
72488
|
-
|
|
72489
|
-
|
|
72490
|
-
|
|
72491
|
-
|
|
72492
|
-
|
|
72493
|
-
|
|
72494
|
-
|
|
72495
|
-
|
|
72496
|
-
|
|
72497
|
-
|
|
72498
|
-
|
|
72499
|
-
|
|
72500
|
-
|
|
72501
|
-
|
|
72502
|
-
|
|
72503
|
-
|
|
72504
|
-
|
|
72505
|
-
const op = pathItem[method];
|
|
72506
|
-
if (op?.security && Array.isArray(op.security)) {
|
|
72507
|
-
for (const req of op.security) {
|
|
72508
|
-
for (const scopeList of Object.values(req)) {
|
|
72509
|
-
if (Array.isArray(scopeList)) {
|
|
72510
|
-
for (const s of scopeList) scopes.add(s);
|
|
72511
|
-
}
|
|
72512
|
-
}
|
|
72513
|
-
}
|
|
72514
|
-
}
|
|
72515
|
-
}
|
|
72516
|
-
}
|
|
72517
|
-
return scopes;
|
|
72518
|
-
}
|
|
72519
|
-
function extractRefs(obj, refs = /* @__PURE__ */ new Set()) {
|
|
72520
|
-
if (!obj || typeof obj !== "object") return refs;
|
|
72521
|
-
if (Array.isArray(obj)) {
|
|
72522
|
-
for (const item of obj) extractRefs(item, refs);
|
|
72523
|
-
return refs;
|
|
72524
|
-
}
|
|
72525
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
72526
|
-
if (key === "$ref" && typeof value === "string") {
|
|
72527
|
-
refs.add(value);
|
|
72528
|
-
} else {
|
|
72529
|
-
extractRefs(value, refs);
|
|
72530
|
-
}
|
|
72531
|
-
}
|
|
72532
|
-
return refs;
|
|
72533
|
-
}
|
|
72534
|
-
function refResolves(parsed, ref) {
|
|
72535
|
-
if (!ref.startsWith("#/")) return true;
|
|
72536
|
-
const parts = ref.replace("#/", "").split("/");
|
|
72537
|
-
let current = parsed;
|
|
72538
|
-
for (const part of parts) {
|
|
72539
|
-
if (!current || typeof current !== "object") return false;
|
|
72540
|
-
current = current[part];
|
|
72270
|
+
logEv({ type: "preflight_start", tool, path: filePath });
|
|
72271
|
+
let result;
|
|
72272
|
+
try {
|
|
72273
|
+
result = await authorize(diskBefore, derived.after, apiKey, {
|
|
72274
|
+
operation: "merge",
|
|
72275
|
+
environment: "staging"
|
|
72276
|
+
});
|
|
72277
|
+
} catch (e) {
|
|
72278
|
+
const msg = e && e.message ? String(e.message) : "request failed";
|
|
72279
|
+
logEv({ type: "preflight_unavailable", cause: "api_unreachable", tool, path: filePath });
|
|
72280
|
+
return emitTerminal(failClosedOrAdvisory({
|
|
72281
|
+
advisory,
|
|
72282
|
+
strict,
|
|
72283
|
+
site: "api_unreachable",
|
|
72284
|
+
why: `API unreachable: ${msg}`,
|
|
72285
|
+
softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
|
|
72286
|
+
errLog
|
|
72287
|
+
}));
|
|
72541
72288
|
}
|
|
72542
|
-
|
|
72543
|
-
|
|
72544
|
-
|
|
72545
|
-
|
|
72546
|
-
|
|
72289
|
+
const mapped = mapDecisionSeverity(result);
|
|
72290
|
+
const fingerprint = extractFingerprint(result);
|
|
72291
|
+
const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
|
|
72292
|
+
const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
|
|
72293
|
+
const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
|
|
72294
|
+
const actionForLog = mapped.executionAction || (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN" || mapped.severity === "INDETERMINATE" ? "STOP" : mapped.severity === "REQUIRE_APPROVAL" ? "REQUEST_APPROVAL" : mapped.severity === "MONITOR" || mapped.severity === "WARN" ? "CONTINUE_WITH_MONITORING" : "CONTINUE");
|
|
72295
|
+
logEv({
|
|
72296
|
+
type: "preflight_result",
|
|
72297
|
+
action: actionForLog,
|
|
72298
|
+
decisionId: mapped.decisionId || void 0,
|
|
72299
|
+
fingerprint,
|
|
72300
|
+
tool,
|
|
72301
|
+
path: filePath
|
|
72302
|
+
});
|
|
72303
|
+
if (mapped.severity === "INDETERMINATE") {
|
|
72304
|
+
errLog(
|
|
72305
|
+
"CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
|
|
72306
|
+
);
|
|
72307
|
+
return emitTerminal({
|
|
72308
|
+
exitCode: 2,
|
|
72309
|
+
reason: "indeterminate",
|
|
72310
|
+
severity: "INDETERMINATE",
|
|
72311
|
+
decision: mapped.decision,
|
|
72312
|
+
decisionId: mapped.decisionId
|
|
72313
|
+
});
|
|
72547
72314
|
}
|
|
72548
|
-
|
|
72549
|
-
|
|
72550
|
-
|
|
72551
|
-
|
|
72552
|
-
|
|
72553
|
-
|
|
72554
|
-
|
|
72555
|
-
|
|
72556
|
-
|
|
72557
|
-
|
|
72558
|
-
|
|
72559
|
-
|
|
72560
|
-
|
|
72561
|
-
|
|
72562
|
-
|
|
72563
|
-
|
|
72564
|
-
severity: "error",
|
|
72565
|
-
type: "invalid_spec",
|
|
72566
|
-
message: `Spec '${name}' is not a valid OpenAPI document: ${validation.error}`,
|
|
72567
|
-
specs: [name]
|
|
72568
|
-
});
|
|
72569
|
-
continue;
|
|
72570
|
-
}
|
|
72571
|
-
parsedSpecs.push({ name, parsed });
|
|
72315
|
+
if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
|
|
72316
|
+
const why = renderDecisionWhy(result);
|
|
72317
|
+
errLog(
|
|
72318
|
+
[
|
|
72319
|
+
`CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
|
|
72320
|
+
...why.lines
|
|
72321
|
+
].join("\n")
|
|
72322
|
+
);
|
|
72323
|
+
return emitTerminal({
|
|
72324
|
+
exitCode: 2,
|
|
72325
|
+
reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
|
|
72326
|
+
severity: mapped.severity,
|
|
72327
|
+
decision: mapped.decision,
|
|
72328
|
+
decisionId: mapped.decisionId,
|
|
72329
|
+
reasons: why.reasonCodes
|
|
72330
|
+
});
|
|
72572
72331
|
}
|
|
72573
|
-
|
|
72574
|
-
|
|
72575
|
-
|
|
72576
|
-
|
|
72577
|
-
|
|
72578
|
-
|
|
72579
|
-
|
|
72580
|
-
|
|
72332
|
+
if (mapped.severity === "REQUIRE_APPROVAL") {
|
|
72333
|
+
const why = renderDecisionWhy(result);
|
|
72334
|
+
errLog(
|
|
72335
|
+
[
|
|
72336
|
+
`CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
|
|
72337
|
+
...why.lines
|
|
72338
|
+
].join("\n")
|
|
72339
|
+
);
|
|
72340
|
+
return emitTerminal({
|
|
72341
|
+
exitCode: 2,
|
|
72342
|
+
reason: "approval_required",
|
|
72343
|
+
severity: mapped.severity,
|
|
72344
|
+
decision: mapped.decision,
|
|
72345
|
+
decisionId: mapped.decisionId,
|
|
72346
|
+
reasons: why.reasonCodes
|
|
72347
|
+
});
|
|
72581
72348
|
}
|
|
72582
|
-
|
|
72583
|
-
|
|
72584
|
-
|
|
72585
|
-
|
|
72586
|
-
|
|
72587
|
-
|
|
72588
|
-
|
|
72589
|
-
|
|
72349
|
+
if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
|
|
72350
|
+
const sinkWired = isMonitoringSinkWired({ env, deps, result });
|
|
72351
|
+
if (!sinkWired) {
|
|
72352
|
+
errLog(
|
|
72353
|
+
`CodeRifts claude-hook: BLOCKED monitoring_unwired${decPart}${eaPart}${idPart} \u2014 CONTINUE_WITH_MONITORING requires CODERIFTS_MONITORING_SINK_WIRED=1 (host assertion; not delivery proof)`
|
|
72354
|
+
);
|
|
72355
|
+
return emitTerminal({
|
|
72356
|
+
exitCode: 2,
|
|
72357
|
+
reason: "monitoring_unwired",
|
|
72358
|
+
severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
|
|
72359
|
+
decision: mapped.decision,
|
|
72360
|
+
decisionId: mapped.decisionId
|
|
72590
72361
|
});
|
|
72591
72362
|
}
|
|
72363
|
+
errLog(renderAllowProofBlock({
|
|
72364
|
+
decision: mapped.decision,
|
|
72365
|
+
executionAction: mapped.executionAction,
|
|
72366
|
+
decisionId: mapped.decisionId,
|
|
72367
|
+
fingerprint
|
|
72368
|
+
}));
|
|
72369
|
+
return emitTerminal({
|
|
72370
|
+
exitCode: 0,
|
|
72371
|
+
reason: "allow",
|
|
72372
|
+
severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
|
|
72373
|
+
decision: mapped.decision,
|
|
72374
|
+
decisionId: mapped.decisionId,
|
|
72375
|
+
fingerprint
|
|
72376
|
+
});
|
|
72592
72377
|
}
|
|
72593
|
-
|
|
72594
|
-
|
|
72595
|
-
|
|
72596
|
-
|
|
72597
|
-
|
|
72598
|
-
|
|
72599
|
-
|
|
72600
|
-
|
|
72601
|
-
|
|
72602
|
-
|
|
72603
|
-
|
|
72604
|
-
|
|
72605
|
-
|
|
72606
|
-
|
|
72607
|
-
severity: "warning",
|
|
72608
|
-
type: "schema_conflict",
|
|
72609
|
-
message: `Schema '${schemaName}' has conflicting definitions across specs: ${specNames.join(", ")}`,
|
|
72610
|
-
specs: specNames
|
|
72611
|
-
});
|
|
72612
|
-
}
|
|
72613
|
-
}
|
|
72614
|
-
}
|
|
72615
|
-
for (const { name, parsed } of parsedSpecs) {
|
|
72616
|
-
const defined = extractDefinedScopes(parsed);
|
|
72617
|
-
const used = extractUsedScopes(parsed);
|
|
72618
|
-
for (const scope of used) {
|
|
72619
|
-
if (!defined.has(scope)) {
|
|
72620
|
-
issues.push({
|
|
72621
|
-
severity: "warning",
|
|
72622
|
-
type: "undefined_scope",
|
|
72623
|
-
message: `Scope '${scope}' is used in '${name}' but not defined in securitySchemes`,
|
|
72624
|
-
specs: [name]
|
|
72625
|
-
});
|
|
72626
|
-
}
|
|
72627
|
-
}
|
|
72628
|
-
for (const scope of defined) {
|
|
72629
|
-
if (!used.has(scope)) {
|
|
72630
|
-
issues.push({
|
|
72631
|
-
severity: "info",
|
|
72632
|
-
type: "unused_scope",
|
|
72633
|
-
message: `Scope '${scope}' is defined in '${name}' but never used in any operation`,
|
|
72634
|
-
specs: [name]
|
|
72635
|
-
});
|
|
72636
|
-
}
|
|
72637
|
-
}
|
|
72638
|
-
}
|
|
72639
|
-
for (const { name, parsed } of parsedSpecs) {
|
|
72640
|
-
const refs = extractRefs(parsed);
|
|
72641
|
-
for (const ref of refs) {
|
|
72642
|
-
if (!refResolves(parsed, ref)) {
|
|
72643
|
-
issues.push({
|
|
72644
|
-
severity: "error",
|
|
72645
|
-
type: "unresolved_ref",
|
|
72646
|
-
message: `$ref '${ref}' in '${name}' does not resolve`,
|
|
72647
|
-
specs: [name]
|
|
72648
|
-
});
|
|
72649
|
-
}
|
|
72650
|
-
}
|
|
72651
|
-
}
|
|
72652
|
-
let totalEndpoints = 0;
|
|
72653
|
-
let totalSchemas = 0;
|
|
72654
|
-
for (const { parsed } of parsedSpecs) {
|
|
72655
|
-
totalEndpoints += extractEndpoints(parsed).length;
|
|
72656
|
-
totalSchemas += extractSchemaNames(parsed).length;
|
|
72657
|
-
}
|
|
72658
|
-
const hasErrors = issues.some((i) => i.severity === "error");
|
|
72659
|
-
return {
|
|
72660
|
-
valid: !hasErrors,
|
|
72661
|
-
issues,
|
|
72662
|
-
stats: {
|
|
72663
|
-
specs_count: parsedSpecs.length,
|
|
72664
|
-
endpoints_count: totalEndpoints,
|
|
72665
|
-
schemas_count: totalSchemas
|
|
72666
|
-
}
|
|
72667
|
-
};
|
|
72378
|
+
errLog(renderAllowProofBlock({
|
|
72379
|
+
decision: mapped.decision,
|
|
72380
|
+
executionAction: mapped.executionAction,
|
|
72381
|
+
decisionId: mapped.decisionId,
|
|
72382
|
+
fingerprint
|
|
72383
|
+
}));
|
|
72384
|
+
return emitTerminal({
|
|
72385
|
+
exitCode: 0,
|
|
72386
|
+
reason: "allow",
|
|
72387
|
+
severity: "ALLOW",
|
|
72388
|
+
decision: mapped.decision,
|
|
72389
|
+
decisionId: mapped.decisionId,
|
|
72390
|
+
fingerprint
|
|
72391
|
+
});
|
|
72668
72392
|
}
|
|
72669
72393
|
module2.exports = {
|
|
72670
|
-
|
|
72671
|
-
|
|
72672
|
-
|
|
72673
|
-
|
|
72674
|
-
|
|
72675
|
-
|
|
72676
|
-
|
|
72677
|
-
|
|
72678
|
-
|
|
72679
|
-
|
|
72394
|
+
runClaudeHook,
|
|
72395
|
+
parseStdinJson,
|
|
72396
|
+
isSpecPath,
|
|
72397
|
+
deriveAfterContent,
|
|
72398
|
+
mapDecisionSeverity,
|
|
72399
|
+
renderDecisionWhy,
|
|
72400
|
+
resolveApiKey,
|
|
72401
|
+
resolveSpecPath,
|
|
72402
|
+
readGitConfig,
|
|
72403
|
+
isEnvFlag,
|
|
72404
|
+
isStrictMode,
|
|
72405
|
+
isAdvisoryMode,
|
|
72406
|
+
isMonitoringSinkWired,
|
|
72407
|
+
failClosedOrAdvisory,
|
|
72408
|
+
softOrStrictBlock,
|
|
72409
|
+
DEFAULT_SPEC_PATH,
|
|
72410
|
+
CLOSED_ACTIONS,
|
|
72411
|
+
USAGE,
|
|
72412
|
+
appendGuardEventLog,
|
|
72413
|
+
extractFingerprint,
|
|
72414
|
+
fingerprintPrefix,
|
|
72415
|
+
renderAllowProofBlock,
|
|
72416
|
+
HOOK_TRIGGER_SOURCE
|
|
72680
72417
|
};
|
|
72681
72418
|
}
|
|
72682
72419
|
});
|
|
72683
72420
|
|
|
72684
|
-
// src/commands/
|
|
72685
|
-
var
|
|
72686
|
-
"src/commands/
|
|
72421
|
+
// src/commands/deploy-gate.js
|
|
72422
|
+
var require_deploy_gate2 = __commonJS({
|
|
72423
|
+
"src/commands/deploy-gate.js"(exports2, module2) {
|
|
72687
72424
|
"use strict";
|
|
72688
72425
|
var fs = require("fs");
|
|
72689
72426
|
var path = require("path");
|
|
72690
72427
|
var chalk = require_source();
|
|
72691
|
-
var {
|
|
72692
|
-
var {
|
|
72693
|
-
|
|
72694
|
-
safeParse,
|
|
72695
|
-
validateOpenApiSpec
|
|
72696
|
-
} = require_registry_validation_core();
|
|
72428
|
+
var { deployGate } = require_cjs3();
|
|
72429
|
+
var { renderJson } = require_json2();
|
|
72430
|
+
var { renderDecisionWhy, isEnvFlag } = require_claude_hook();
|
|
72697
72431
|
if (process.env.NO_COLOR) chalk.level = 0;
|
|
72698
|
-
var
|
|
72699
|
-
function
|
|
72700
|
-
|
|
72701
|
-
statSync = fs.statSync.bind(fs)
|
|
72702
|
-
} = {}) {
|
|
72703
|
-
const out = [];
|
|
72704
|
-
function walk(absDir) {
|
|
72705
|
-
let entries;
|
|
72706
|
-
try {
|
|
72707
|
-
entries = readdirSync(absDir, { withFileTypes: true });
|
|
72708
|
-
} catch (err) {
|
|
72709
|
-
const e = new Error(`Cannot read directory ${absDir}: ${err && err.message}`);
|
|
72710
|
-
e.code = "GATE_ERROR";
|
|
72711
|
-
e.path = absDir;
|
|
72712
|
-
throw e;
|
|
72713
|
-
}
|
|
72714
|
-
for (const ent of entries) {
|
|
72715
|
-
const name = ent.name;
|
|
72716
|
-
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
72717
|
-
const abs = path.join(absDir, name);
|
|
72718
|
-
let isDir = ent.isDirectory && ent.isDirectory();
|
|
72719
|
-
let isFile = ent.isFile && ent.isFile();
|
|
72720
|
-
if (!isDir && !isFile) {
|
|
72721
|
-
try {
|
|
72722
|
-
const st = statSync(abs);
|
|
72723
|
-
isDir = st.isDirectory();
|
|
72724
|
-
isFile = st.isFile();
|
|
72725
|
-
} catch (err) {
|
|
72726
|
-
const e = new Error(`Cannot stat ${abs}: ${err && err.message}`);
|
|
72727
|
-
e.code = "GATE_ERROR";
|
|
72728
|
-
e.path = abs;
|
|
72729
|
-
throw e;
|
|
72730
|
-
}
|
|
72731
|
-
}
|
|
72732
|
-
if (isDir) {
|
|
72733
|
-
walk(abs);
|
|
72734
|
-
continue;
|
|
72735
|
-
}
|
|
72736
|
-
if (isFile) {
|
|
72737
|
-
const ext = path.extname(name).toLowerCase();
|
|
72738
|
-
if (SPEC_EXT.has(ext)) out.push(abs);
|
|
72739
|
-
}
|
|
72740
|
-
}
|
|
72741
|
-
}
|
|
72742
|
-
walk(rootDir);
|
|
72743
|
-
return out.sort();
|
|
72432
|
+
var REPAIRABLE = /* @__PURE__ */ new Set(["env_mismatch", "stale_artifact", "operation_mismatch", "receipt_not_authorized", "fingerprint_mismatch", "body_hash_mismatch"]);
|
|
72433
|
+
function enforceSignal(options) {
|
|
72434
|
+
return options && options.enforce === true || String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase() === "true";
|
|
72744
72435
|
}
|
|
72745
|
-
function
|
|
72746
|
-
|
|
72747
|
-
|
|
72748
|
-
return rel;
|
|
72436
|
+
function isDeployAdvisory(env) {
|
|
72437
|
+
const e = env || process.env;
|
|
72438
|
+
return isEnvFlag(e, "CODERIFTS_DEPLOY_ADVISORY") || isEnvFlag(e, "CODERIFTS_ADVISORY");
|
|
72749
72439
|
}
|
|
72750
|
-
function
|
|
72751
|
-
|
|
72752
|
-
|
|
72753
|
-
|
|
72754
|
-
|
|
72755
|
-
|
|
72756
|
-
|
|
72757
|
-
|
|
72758
|
-
|
|
72759
|
-
|
|
72760
|
-
|
|
72761
|
-
|
|
72440
|
+
function observeCDEnforcement(options = {}) {
|
|
72441
|
+
const envVal = String(process.env.CODERIFTS_DEPLOY_ENFORCE || "").toLowerCase();
|
|
72442
|
+
let enforcement;
|
|
72443
|
+
if (enforceSignal(options)) enforcement = "ENFORCING";
|
|
72444
|
+
else if (envVal === "unknown") enforcement = "UNKNOWN";
|
|
72445
|
+
else enforcement = "ADVISORY";
|
|
72446
|
+
const bypass_possible = String(process.env.CODERIFTS_DEPLOY_NO_BYPASS || "").toLowerCase() !== "true";
|
|
72447
|
+
return {
|
|
72448
|
+
enforcement,
|
|
72449
|
+
bypass_possible,
|
|
72450
|
+
step_is_required: enforcement === "ENFORCING",
|
|
72451
|
+
required_step_name: "CodeRifts / deploy-gate",
|
|
72452
|
+
attestation_source: "cli_flag"
|
|
72453
|
+
};
|
|
72454
|
+
}
|
|
72455
|
+
function deployReportResiduals(state, enforcement_inescapable, enforcement, change_set_rebound) {
|
|
72456
|
+
const out = [];
|
|
72457
|
+
if (state !== "success") return out;
|
|
72458
|
+
if (enforcement_inescapable !== true) {
|
|
72459
|
+
if (enforcement === "ENFORCING") out.push("bypass_open");
|
|
72460
|
+
else if (enforcement === "ADVISORY") out.push("deploy_gate_advisory");
|
|
72461
|
+
else if (enforcement === "ABSENT") out.push("deploy_path_ungated");
|
|
72462
|
+
}
|
|
72463
|
+
if (change_set_rebound !== true) {
|
|
72464
|
+
out.push("change_set_not_rebound");
|
|
72762
72465
|
}
|
|
72466
|
+
return out;
|
|
72763
72467
|
}
|
|
72764
|
-
function
|
|
72765
|
-
|
|
72766
|
-
|
|
72767
|
-
|
|
72768
|
-
|
|
72769
|
-
|
|
72770
|
-
const rootDir = path.resolve(dir);
|
|
72771
|
-
let candidates;
|
|
72772
|
-
try {
|
|
72773
|
-
candidates = walkSpecCandidates(rootDir, { readdirSync, statSync });
|
|
72774
|
-
} catch (err) {
|
|
72468
|
+
function deployCoverageInput(enforcement_state, inescapable_deploy) {
|
|
72469
|
+
return { enforcement_state, inescapable_deploy: inescapable_deploy === true, applicability_attested: true };
|
|
72470
|
+
}
|
|
72471
|
+
function deployBind({ environment, artifact_id, receipt, observed_cd_enforcement, expected_fingerprint, expected_body_hash }) {
|
|
72472
|
+
const attested_enforcement = observed_cd_enforcement && observed_cd_enforcement.enforcement || "UNKNOWN";
|
|
72473
|
+
if (!receipt) {
|
|
72775
72474
|
return {
|
|
72776
|
-
|
|
72777
|
-
|
|
72778
|
-
|
|
72779
|
-
|
|
72475
|
+
deploy_check_status: "pending",
|
|
72476
|
+
reason: "no_receipt",
|
|
72477
|
+
must_re_preflight: true,
|
|
72478
|
+
attested_enforcement,
|
|
72479
|
+
gate: null,
|
|
72480
|
+
report_residuals: [],
|
|
72481
|
+
coverage_deploy_input: deployCoverageInput(attested_enforcement, false)
|
|
72780
72482
|
};
|
|
72781
72483
|
}
|
|
72782
|
-
|
|
72783
|
-
|
|
72784
|
-
|
|
72785
|
-
|
|
72786
|
-
|
|
72787
|
-
|
|
72788
|
-
const read = readFileThreeState(abs, readFileSync);
|
|
72789
|
-
if (!read.ok) {
|
|
72790
|
-
return {
|
|
72791
|
-
ok: false,
|
|
72792
|
-
code: "GATE_ERROR",
|
|
72793
|
-
message: read.message,
|
|
72794
|
-
path: read.path || abs
|
|
72795
|
-
};
|
|
72796
|
-
}
|
|
72797
|
-
const name = relPosix(rootDir, abs);
|
|
72798
|
-
const parsed = safeParse(read.content);
|
|
72799
|
-
if (!parsed || typeof parsed !== "object") {
|
|
72800
|
-
specs.push({ name, spec: read.content });
|
|
72801
|
-
continue;
|
|
72484
|
+
const requiredContext = {
|
|
72485
|
+
operation: "deploy",
|
|
72486
|
+
enforcement: {
|
|
72487
|
+
enforcement: attested_enforcement,
|
|
72488
|
+
// fail-closed: bypass is possible unless observation proved it disabled.
|
|
72489
|
+
bypass_possible: !(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false)
|
|
72802
72490
|
}
|
|
72803
|
-
|
|
72804
|
-
|
|
72805
|
-
|
|
72806
|
-
|
|
72807
|
-
|
|
72808
|
-
|
|
72809
|
-
specs.push({ name, spec: read.content });
|
|
72810
|
-
continue;
|
|
72491
|
+
};
|
|
72492
|
+
let change_set_rebound = false;
|
|
72493
|
+
if (attested_enforcement === "ENFORCING") {
|
|
72494
|
+
if (expected_fingerprint != null) {
|
|
72495
|
+
requiredContext.expected_fingerprint = expected_fingerprint;
|
|
72496
|
+
change_set_rebound = true;
|
|
72811
72497
|
}
|
|
72812
|
-
|
|
72498
|
+
if (expected_body_hash != null) requiredContext.expected_body_hash = expected_body_hash;
|
|
72813
72499
|
}
|
|
72500
|
+
const gate = deployGate({ deployTarget: { environment, artifact_id }, receipt, requiredContext });
|
|
72501
|
+
const enforcement_inescapable = attested_enforcement === "ENFORCING" && !!(observed_cd_enforcement && observed_cd_enforcement.bypass_possible === false);
|
|
72502
|
+
const inescapable_deploy = gate.inescapable_deploy === true && change_set_rebound === true;
|
|
72814
72503
|
return {
|
|
72815
|
-
|
|
72816
|
-
|
|
72817
|
-
|
|
72818
|
-
|
|
72819
|
-
|
|
72504
|
+
deploy_check_status: gate.state,
|
|
72505
|
+
reason: gate.reason,
|
|
72506
|
+
must_re_preflight: REPAIRABLE.has(gate.reason),
|
|
72507
|
+
attested_enforcement,
|
|
72508
|
+
gate: { deploy_allowed: gate.deploy_allowed, reason: gate.reason, inescapable_deploy },
|
|
72509
|
+
report_residuals: deployReportResiduals(gate.state, enforcement_inescapable, attested_enforcement, change_set_rebound),
|
|
72510
|
+
coverage_deploy_input: deployCoverageInput(attested_enforcement, inescapable_deploy)
|
|
72820
72511
|
};
|
|
72821
72512
|
}
|
|
72822
|
-
function
|
|
72823
|
-
if (
|
|
72824
|
-
|
|
72825
|
-
|
|
72826
|
-
|
|
72513
|
+
function clampExit(deployCheckStatus, advisory) {
|
|
72514
|
+
if (deployCheckStatus === "success") return 0;
|
|
72515
|
+
if (advisory === true) return 0;
|
|
72516
|
+
return 1;
|
|
72517
|
+
}
|
|
72518
|
+
var ADVISORY_LABEL = "ADVISORY MODE \u2014 this gate did not block";
|
|
72519
|
+
var SOFTEN_HINT = "set CODERIFTS_DEPLOY_ADVISORY=1 to soften";
|
|
72520
|
+
function renderPolicyStderr(bind, receipt, advisory) {
|
|
72521
|
+
const lines = [
|
|
72522
|
+
`CodeRifts deploy-gate: BLOCKED reason=${bind.reason} status=${bind.deploy_check_status}`
|
|
72523
|
+
];
|
|
72524
|
+
if (receipt && typeof receipt === "object") {
|
|
72525
|
+
const why = renderDecisionWhy(receipt);
|
|
72526
|
+
for (const l of why.lines) lines.push(l);
|
|
72827
72527
|
}
|
|
72828
|
-
|
|
72528
|
+
if (bind.must_re_preflight) {
|
|
72529
|
+
lines.push("- action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it.");
|
|
72530
|
+
}
|
|
72531
|
+
lines.push(advisory ? ADVISORY_LABEL : SOFTEN_HINT);
|
|
72532
|
+
return lines.join("\n");
|
|
72829
72533
|
}
|
|
72830
|
-
function
|
|
72831
|
-
const
|
|
72832
|
-
|
|
72534
|
+
function renderInfraStderr({ site, why, retryable = true, advisory = false }) {
|
|
72535
|
+
const lines = [
|
|
72536
|
+
`CodeRifts deploy-gate: INFRA ${site} \u2014 ${why}`,
|
|
72537
|
+
`- retryable: ${retryable ? "true" : "false"}`,
|
|
72538
|
+
"- this is not a policy BLOCK"
|
|
72539
|
+
];
|
|
72540
|
+
lines.push(advisory ? ADVISORY_LABEL : SOFTEN_HINT);
|
|
72541
|
+
return lines.join("\n");
|
|
72542
|
+
}
|
|
72543
|
+
function inspectReceiptFile(filePath) {
|
|
72544
|
+
if (!filePath) return { kind: "absent", receipt: null };
|
|
72545
|
+
const resolved = path.resolve(filePath);
|
|
72546
|
+
if (!fs.existsSync(resolved)) return { kind: "missing_file", receipt: null };
|
|
72547
|
+
try {
|
|
72548
|
+
const parsed = JSON.parse(fs.readFileSync(resolved, "utf-8"));
|
|
72549
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
72550
|
+
return { kind: "malformed", receipt: null };
|
|
72551
|
+
}
|
|
72552
|
+
return { kind: "ok", receipt: parsed };
|
|
72553
|
+
} catch (_) {
|
|
72554
|
+
return { kind: "malformed", receipt: null };
|
|
72555
|
+
}
|
|
72556
|
+
}
|
|
72557
|
+
function readReceiptFile(filePath) {
|
|
72558
|
+
return inspectReceiptFile(filePath).receipt;
|
|
72559
|
+
}
|
|
72560
|
+
function extractDecisionIdFromReceipt(receipt) {
|
|
72561
|
+
if (!receipt || typeof receipt !== "object") return null;
|
|
72562
|
+
if (typeof receipt.decision_id === "string" && receipt.decision_id.trim()) {
|
|
72563
|
+
return receipt.decision_id.trim();
|
|
72564
|
+
}
|
|
72565
|
+
const nested = receipt.decision_result;
|
|
72566
|
+
if (nested && typeof nested === "object" && typeof nested.decision_id === "string" && nested.decision_id.trim()) {
|
|
72567
|
+
return nested.decision_id.trim();
|
|
72568
|
+
}
|
|
72569
|
+
return null;
|
|
72570
|
+
}
|
|
72571
|
+
function formatOutcomeReportHint(decisionId) {
|
|
72572
|
+
const id = String(decisionId);
|
|
72573
|
+
return [
|
|
72574
|
+
"# report the deploy outcome after your deploy step:",
|
|
72575
|
+
`coderifts outcome <deploy_succeeded|deploy_failed|rolled_back> --decision ${id}`
|
|
72576
|
+
].join("\n");
|
|
72577
|
+
}
|
|
72578
|
+
function renderDeployGateTerminal(bind, enforce) {
|
|
72579
|
+
const g = bind.gate;
|
|
72580
|
+
const color = bind.deploy_check_status === "success" ? chalk.green : bind.deploy_check_status === "failure" ? chalk.red : chalk.yellow;
|
|
72581
|
+
const lines = [];
|
|
72582
|
+
lines.push("");
|
|
72583
|
+
lines.push(chalk.bold(` CodeRifts deploy-gate \u2014 ${color(bind.deploy_check_status.toUpperCase())}`));
|
|
72584
|
+
lines.push(` Reason: ${bind.reason}`);
|
|
72585
|
+
lines.push(` Enforcement: ${bind.attested_enforcement}`);
|
|
72586
|
+
lines.push(` inescapable_deploy: ${g ? g.inescapable_deploy : false}`);
|
|
72587
|
+
if (bind.report_residuals.length) lines.push(` Residuals: ${bind.report_residuals.join(", ")}`);
|
|
72588
|
+
if (bind.must_re_preflight) lines.push(chalk.yellow(" Action: re-preflight this { environment, artifact } \u2014 the receipt does not authorize it."));
|
|
72589
|
+
lines.push("");
|
|
72590
|
+
lines.push(enforce ? chalk.dim(" Attested ENFORCING (inescapable_deploy claim). Exit is fail-closed by default.") : chalk.dim(" Fail-closed default \u2014 a failing gate exits non-zero. " + SOFTEN_HINT + "."));
|
|
72591
|
+
lines.push("");
|
|
72592
|
+
return lines.join("\n");
|
|
72593
|
+
}
|
|
72594
|
+
async function runDeployGate(options = {}) {
|
|
72595
|
+
const env = options.envObj || process.env;
|
|
72596
|
+
const advisory = isDeployAdvisory(env);
|
|
72597
|
+
const errLog = options.errLog || ((m) => console.error(String(m)));
|
|
72598
|
+
const outLog = options.outLog || ((m) => console.log(String(m)));
|
|
72599
|
+
const exitFn = options.exitFn || ((code2) => {
|
|
72600
|
+
process.exit(code2);
|
|
72601
|
+
});
|
|
72602
|
+
const environment = options.env;
|
|
72603
|
+
const artifactId = options.artifact;
|
|
72604
|
+
if (!environment || !artifactId) {
|
|
72605
|
+
errLog(renderInfraStderr({
|
|
72606
|
+
site: "missing_inputs",
|
|
72607
|
+
why: "--env and --artifact are required",
|
|
72608
|
+
retryable: true,
|
|
72609
|
+
advisory
|
|
72610
|
+
}));
|
|
72611
|
+
exitFn(advisory ? 0 : 1);
|
|
72612
|
+
return { exitCode: advisory ? 0 : 1, failure_class: "infra" };
|
|
72613
|
+
}
|
|
72614
|
+
const enforce = enforceSignal(options);
|
|
72615
|
+
const inspected = inspectReceiptFile(options.receipt);
|
|
72616
|
+
if (inspected.kind === "malformed") {
|
|
72617
|
+
errLog(renderInfraStderr({
|
|
72618
|
+
site: "malformed_receipt",
|
|
72619
|
+
why: `receipt file is not valid JSON: ${options.receipt}`,
|
|
72620
|
+
retryable: true,
|
|
72621
|
+
advisory
|
|
72622
|
+
}));
|
|
72623
|
+
exitFn(advisory ? 0 : 1);
|
|
72624
|
+
return { exitCode: advisory ? 0 : 1, failure_class: "infra" };
|
|
72625
|
+
}
|
|
72626
|
+
const receipt = inspected.receipt;
|
|
72627
|
+
const observed = observeCDEnforcement({ enforce });
|
|
72628
|
+
const bind = deployBind({ environment, artifact_id: artifactId, receipt, observed_cd_enforcement: observed });
|
|
72629
|
+
const code = clampExit(bind.deploy_check_status, advisory);
|
|
72630
|
+
const decisionId = extractDecisionIdFromReceipt(receipt);
|
|
72631
|
+
const failureClass = bind.deploy_check_status === "success" ? null : "policy";
|
|
72632
|
+
if (bind.deploy_check_status !== "success") {
|
|
72633
|
+
errLog(renderPolicyStderr(bind, receipt, advisory));
|
|
72634
|
+
} else if (advisory) {
|
|
72635
|
+
errLog(ADVISORY_LABEL);
|
|
72636
|
+
}
|
|
72637
|
+
if (options.json) {
|
|
72638
|
+
outLog(renderJson({
|
|
72639
|
+
command: "deploy-gate",
|
|
72640
|
+
environment,
|
|
72641
|
+
artifact_id: artifactId,
|
|
72642
|
+
phase: advisory ? "advisory" : "fail_closed",
|
|
72643
|
+
exit_code: code,
|
|
72644
|
+
decision_id: decisionId,
|
|
72645
|
+
failure_class: failureClass,
|
|
72646
|
+
retryable: false,
|
|
72647
|
+
...bind
|
|
72648
|
+
}));
|
|
72649
|
+
} else {
|
|
72650
|
+
outLog(renderDeployGateTerminal(bind, enforce));
|
|
72651
|
+
if (decisionId) {
|
|
72652
|
+
outLog(formatOutcomeReportHint(decisionId));
|
|
72653
|
+
outLog("");
|
|
72654
|
+
}
|
|
72655
|
+
}
|
|
72656
|
+
exitFn(code);
|
|
72657
|
+
return { exitCode: code, failure_class: failureClass };
|
|
72658
|
+
}
|
|
72659
|
+
module2.exports = {
|
|
72660
|
+
runDeployGate,
|
|
72661
|
+
deployBind,
|
|
72662
|
+
observeCDEnforcement,
|
|
72663
|
+
clampExit,
|
|
72664
|
+
readReceiptFile,
|
|
72665
|
+
inspectReceiptFile,
|
|
72666
|
+
extractDecisionIdFromReceipt,
|
|
72667
|
+
formatOutcomeReportHint,
|
|
72668
|
+
renderDeployGateTerminal,
|
|
72669
|
+
renderPolicyStderr,
|
|
72670
|
+
renderInfraStderr,
|
|
72671
|
+
isDeployAdvisory,
|
|
72672
|
+
ADVISORY_LABEL,
|
|
72673
|
+
SOFTEN_HINT
|
|
72674
|
+
};
|
|
72675
|
+
}
|
|
72676
|
+
});
|
|
72677
|
+
|
|
72678
|
+
// src/commands/publish-gate.js
|
|
72679
|
+
var require_publish_gate = __commonJS({
|
|
72680
|
+
"src/commands/publish-gate.js"(exports2, module2) {
|
|
72681
|
+
"use strict";
|
|
72682
|
+
var fs = require("fs");
|
|
72683
|
+
var path = require("path");
|
|
72684
|
+
var { execFileSync } = require("child_process");
|
|
72685
|
+
var chalk = require_source();
|
|
72686
|
+
var { getApiKey } = require_config();
|
|
72687
|
+
var { cloudDiff } = require_cloud();
|
|
72688
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
72689
|
+
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
72690
|
+
var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
|
|
72691
|
+
"CONTINUE",
|
|
72692
|
+
"CONTINUE_WITH_MONITORING",
|
|
72693
|
+
"REQUEST_APPROVAL",
|
|
72694
|
+
"STOP"
|
|
72695
|
+
]);
|
|
72696
|
+
var PERMIT_ACTIONS = /* @__PURE__ */ new Set(["CONTINUE", "CONTINUE_WITH_MONITORING"]);
|
|
72697
|
+
function defaultGit(args, cwd) {
|
|
72698
|
+
return execFileSync("git", args, {
|
|
72699
|
+
cwd: cwd || process.cwd(),
|
|
72700
|
+
encoding: "utf8",
|
|
72701
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
72702
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
72703
|
+
}).trim();
|
|
72704
|
+
}
|
|
72705
|
+
function gitShow(ref, filePath, { gitImpl = defaultGit, cwd } = {}) {
|
|
72706
|
+
try {
|
|
72707
|
+
const out = gitImpl(["show", `${ref}:${filePath}`], cwd);
|
|
72708
|
+
return { ok: true, content: out == null ? "" : String(out) };
|
|
72709
|
+
} catch (err) {
|
|
72710
|
+
const msg = err && err.stderr ? String(err.stderr) : err && err.message || String(err);
|
|
72711
|
+
return {
|
|
72712
|
+
ok: false,
|
|
72713
|
+
code: "GIT_ERROR",
|
|
72714
|
+
message: `git error reading ${ref}:${filePath}: ${msg.slice(0, 300)}`
|
|
72715
|
+
};
|
|
72716
|
+
}
|
|
72717
|
+
}
|
|
72718
|
+
function readPackageVersion(cwd, readFile = fs.readFileSync) {
|
|
72719
|
+
const pkgPath = path.join(cwd || process.cwd(), "package.json");
|
|
72720
|
+
try {
|
|
72721
|
+
const raw = readFile(pkgPath, "utf8");
|
|
72722
|
+
const pkg2 = JSON.parse(raw);
|
|
72723
|
+
return pkg2 && typeof pkg2.version === "string" ? pkg2.version : null;
|
|
72724
|
+
} catch {
|
|
72725
|
+
return null;
|
|
72726
|
+
}
|
|
72727
|
+
}
|
|
72728
|
+
function resolveBeforeSpec(specPath, {
|
|
72729
|
+
gitImpl = defaultGit,
|
|
72730
|
+
cwd = process.cwd(),
|
|
72731
|
+
readFile = fs.readFileSync,
|
|
72732
|
+
packageVersion = null
|
|
72733
|
+
} = {}) {
|
|
72734
|
+
const version = packageVersion != null ? packageVersion : readPackageVersion(cwd, readFile);
|
|
72735
|
+
const tried = [];
|
|
72736
|
+
if (version) {
|
|
72737
|
+
const tags = [`v${version}`, version];
|
|
72738
|
+
for (const tag of tags) {
|
|
72739
|
+
tried.push(`tag:${tag}`);
|
|
72740
|
+
try {
|
|
72741
|
+
gitImpl(["rev-parse", "--verify", `${tag}^{commit}`], cwd);
|
|
72742
|
+
} catch {
|
|
72743
|
+
continue;
|
|
72744
|
+
}
|
|
72745
|
+
const shown = gitShow(tag, specPath, { gitImpl, cwd });
|
|
72746
|
+
if (!shown.ok) {
|
|
72747
|
+
return {
|
|
72748
|
+
ok: false,
|
|
72749
|
+
code: shown.code || "GIT_ERROR",
|
|
72750
|
+
message: shown.message,
|
|
72751
|
+
tried
|
|
72752
|
+
};
|
|
72753
|
+
}
|
|
72754
|
+
if (shown.content.trim() === "") {
|
|
72755
|
+
return {
|
|
72756
|
+
ok: false,
|
|
72757
|
+
code: "EMPTY_BEFORE",
|
|
72758
|
+
message: `Empty contract artifact at tag ${tag}:${specPath}. Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
|
|
72759
|
+
tried
|
|
72760
|
+
};
|
|
72761
|
+
}
|
|
72762
|
+
return { ok: true, content: shown.content, source: `tag:${tag}`, tried };
|
|
72763
|
+
}
|
|
72764
|
+
} else {
|
|
72765
|
+
tried.push("package.json:version (missing)");
|
|
72766
|
+
}
|
|
72767
|
+
const bases = ["origin/main", "origin/master", "main", "master"];
|
|
72768
|
+
for (const base of bases) {
|
|
72769
|
+
tried.push(`merge-base:${base}`);
|
|
72770
|
+
let mb;
|
|
72771
|
+
try {
|
|
72772
|
+
mb = gitImpl(["merge-base", "HEAD", base], cwd);
|
|
72773
|
+
} catch {
|
|
72774
|
+
continue;
|
|
72775
|
+
}
|
|
72776
|
+
if (!mb || mb === ZERO_SHA) continue;
|
|
72777
|
+
const shown = gitShow(mb, specPath, { gitImpl, cwd });
|
|
72778
|
+
if (!shown.ok) {
|
|
72779
|
+
return {
|
|
72780
|
+
ok: false,
|
|
72781
|
+
code: shown.code || "GIT_ERROR",
|
|
72782
|
+
message: shown.message,
|
|
72783
|
+
tried
|
|
72784
|
+
};
|
|
72785
|
+
}
|
|
72786
|
+
if (shown.content.trim() === "") {
|
|
72787
|
+
return {
|
|
72788
|
+
ok: false,
|
|
72789
|
+
code: "EMPTY_BEFORE",
|
|
72790
|
+
message: `Empty contract artifact at merge-base ${mb}:${specPath} (${base}). Refusing to treat empty before as NEW_ARTIFACT (would false-pass enforce).`,
|
|
72791
|
+
tried
|
|
72792
|
+
};
|
|
72793
|
+
}
|
|
72794
|
+
return {
|
|
72795
|
+
ok: true,
|
|
72796
|
+
content: shown.content,
|
|
72797
|
+
source: `merge-base:${base}@${mb.slice(0, 12)}`,
|
|
72798
|
+
tried
|
|
72799
|
+
};
|
|
72800
|
+
}
|
|
72801
|
+
return {
|
|
72802
|
+
ok: false,
|
|
72803
|
+
code: "BEFORE_UNRESOLVED",
|
|
72804
|
+
message: `Could not resolve a non-empty before-spec for ${specPath}. Tried: (a) git tag of package.json version, (b) merge-base with origin/main. Attempts: ${tried.join(", ")}. Fail-closed \u2014 will not publish without a baseline.`,
|
|
72805
|
+
tried
|
|
72806
|
+
};
|
|
72807
|
+
}
|
|
72808
|
+
function resolveAfterSpec(specPath, {
|
|
72809
|
+
cwd = process.cwd(),
|
|
72810
|
+
readFile = fs.readFileSync,
|
|
72811
|
+
exists = fs.existsSync
|
|
72812
|
+
} = {}) {
|
|
72813
|
+
const resolved = path.isAbsolute(specPath) ? specPath : path.join(cwd, specPath);
|
|
72814
|
+
try {
|
|
72815
|
+
if (!exists(resolved)) {
|
|
72816
|
+
return {
|
|
72817
|
+
ok: false,
|
|
72818
|
+
code: "AFTER_MISSING",
|
|
72819
|
+
message: `Working-tree contract artifact not found: ${specPath}`
|
|
72820
|
+
};
|
|
72821
|
+
}
|
|
72822
|
+
const content = readFile(resolved, "utf8");
|
|
72823
|
+
if (content == null || String(content).trim() === "") {
|
|
72824
|
+
return {
|
|
72825
|
+
ok: false,
|
|
72826
|
+
code: "AFTER_EMPTY",
|
|
72827
|
+
message: `Working-tree contract artifact is empty: ${specPath}`
|
|
72828
|
+
};
|
|
72829
|
+
}
|
|
72830
|
+
return { ok: true, content: String(content), path: resolved };
|
|
72831
|
+
} catch (err) {
|
|
72832
|
+
return {
|
|
72833
|
+
ok: false,
|
|
72834
|
+
code: "AFTER_READ_ERROR",
|
|
72835
|
+
message: `Failed to read working-tree ${specPath}: ${err && err.message}`
|
|
72836
|
+
};
|
|
72837
|
+
}
|
|
72838
|
+
}
|
|
72839
|
+
function evaluatePublishPermission(result) {
|
|
72840
|
+
if (!result || typeof result !== "object") {
|
|
72841
|
+
return {
|
|
72842
|
+
allow: false,
|
|
72843
|
+
execution_action: null,
|
|
72844
|
+
decision: null,
|
|
72845
|
+
policy: "fail_closed:unreadable_response"
|
|
72846
|
+
};
|
|
72847
|
+
}
|
|
72848
|
+
const env = result.decision_result && typeof result.decision_result === "object" ? result.decision_result : null;
|
|
72849
|
+
let ea = null;
|
|
72850
|
+
if (env && typeof env.execution_action === "string") ea = env.execution_action;
|
|
72851
|
+
else if (typeof result.execution_action === "string") ea = result.execution_action;
|
|
72852
|
+
const decision = env && env.decision || result.decision || result.omega_decision || null;
|
|
72853
|
+
if (ea && CLOSED_ACTIONS.has(ea)) {
|
|
72854
|
+
const allow = PERMIT_ACTIONS.has(ea);
|
|
72855
|
+
return {
|
|
72856
|
+
allow,
|
|
72857
|
+
execution_action: ea,
|
|
72858
|
+
decision: decision || null,
|
|
72859
|
+
policy: allow ? `permit:execution_action=${ea}` : `block:execution_action=${ea}`
|
|
72860
|
+
};
|
|
72861
|
+
}
|
|
72862
|
+
if (ea != null && ea !== "" && !CLOSED_ACTIONS.has(ea)) {
|
|
72863
|
+
return {
|
|
72864
|
+
allow: false,
|
|
72865
|
+
execution_action: ea,
|
|
72866
|
+
decision: decision || null,
|
|
72867
|
+
policy: `block:unrecognised_execution_action=${ea}`
|
|
72868
|
+
};
|
|
72869
|
+
}
|
|
72870
|
+
if (decision === "BLOCK" || decision === "REQUIRE_APPROVAL") {
|
|
72871
|
+
return {
|
|
72872
|
+
allow: false,
|
|
72873
|
+
execution_action: null,
|
|
72874
|
+
decision,
|
|
72875
|
+
policy: `block:decision=${decision}`
|
|
72876
|
+
};
|
|
72877
|
+
}
|
|
72878
|
+
if (decision === "ALLOW" || decision === "WARN" || decision === "PASS") {
|
|
72879
|
+
return {
|
|
72880
|
+
allow: true,
|
|
72881
|
+
execution_action: null,
|
|
72882
|
+
decision,
|
|
72883
|
+
policy: `permit:decision=${decision}`
|
|
72884
|
+
};
|
|
72885
|
+
}
|
|
72886
|
+
const omega = result.omega_decision;
|
|
72887
|
+
if (omega === "BLOCK" || omega === "REQUIRE_APPROVAL") {
|
|
72888
|
+
return {
|
|
72889
|
+
allow: false,
|
|
72890
|
+
execution_action: null,
|
|
72891
|
+
decision: omega,
|
|
72892
|
+
policy: `block:omega_decision=${omega}`
|
|
72893
|
+
};
|
|
72894
|
+
}
|
|
72895
|
+
return {
|
|
72896
|
+
allow: false,
|
|
72897
|
+
execution_action: ea,
|
|
72898
|
+
decision: decision || omega || null,
|
|
72899
|
+
policy: "fail_closed:no_permission_signal"
|
|
72900
|
+
};
|
|
72901
|
+
}
|
|
72902
|
+
function extractReceiptRef(result) {
|
|
72903
|
+
if (!result || typeof result !== "object") return null;
|
|
72904
|
+
const env = result.decision_result;
|
|
72905
|
+
if (env && env.receipt && typeof env.receipt.token === "string") {
|
|
72906
|
+
return env.receipt.token.slice(0, 24) + (env.receipt.token.length > 24 ? "\u2026" : "");
|
|
72907
|
+
}
|
|
72908
|
+
if (env && typeof env.decision_id === "string") return env.decision_id;
|
|
72909
|
+
if (typeof result.decision_id === "string") return result.decision_id;
|
|
72910
|
+
if (typeof result.fingerprint === "string") return result.fingerprint;
|
|
72911
|
+
if (env && typeof env.fingerprint === "string") return env.fingerprint;
|
|
72912
|
+
return null;
|
|
72913
|
+
}
|
|
72914
|
+
async function defaultPreflight(before, after, { apiKey } = {}) {
|
|
72915
|
+
const key = process.env.CODERIFTS_FORCE_LOCAL_PREFLIGHT ? null : apiKey != null ? apiKey : getApiKey();
|
|
72916
|
+
if (key) {
|
|
72917
|
+
return cloudDiff(before, after, key);
|
|
72918
|
+
}
|
|
72919
|
+
const yaml = require_js_yaml();
|
|
72920
|
+
const { diffSpecs } = require_api2();
|
|
72921
|
+
let oldSpec;
|
|
72922
|
+
let newSpec;
|
|
72923
|
+
try {
|
|
72924
|
+
oldSpec = yaml.load(before);
|
|
72925
|
+
newSpec = yaml.load(after);
|
|
72926
|
+
} catch (e) {
|
|
72927
|
+
const err = new Error(`Failed to parse specs: ${e.message}`);
|
|
72928
|
+
err.code = "PREFLIGHT_UNREACHABLE";
|
|
72929
|
+
throw err;
|
|
72930
|
+
}
|
|
72931
|
+
let diffResult;
|
|
72932
|
+
try {
|
|
72933
|
+
diffResult = await diffSpecs({
|
|
72934
|
+
sourceSpec: { content: JSON.stringify(oldSpec), location: "before.json", format: "openapi3" },
|
|
72935
|
+
destinationSpec: { content: JSON.stringify(newSpec), location: "after.json", format: "openapi3" }
|
|
72936
|
+
});
|
|
72937
|
+
} catch (e) {
|
|
72938
|
+
const err = new Error(`Local preflight engine error: ${e.message}`);
|
|
72939
|
+
err.code = "PREFLIGHT_UNREACHABLE";
|
|
72940
|
+
throw err;
|
|
72941
|
+
}
|
|
72942
|
+
const breaking = (diffResult.breakingDifferences || []).length;
|
|
72943
|
+
const decision = breaking > 0 ? "BLOCK" : "ALLOW";
|
|
72944
|
+
const execution_action = breaking > 0 ? "STOP" : "CONTINUE";
|
|
72945
|
+
return {
|
|
72946
|
+
decision,
|
|
72947
|
+
omega_decision: decision,
|
|
72948
|
+
execution_action,
|
|
72949
|
+
decision_result: {
|
|
72950
|
+
decision,
|
|
72951
|
+
execution_action,
|
|
72952
|
+
decision_id: `local-${Date.now()}`
|
|
72953
|
+
},
|
|
72954
|
+
breaking_changes: diffResult.breakingDifferences || [],
|
|
72955
|
+
risk_score: Math.min(breaking * 15, 100)
|
|
72956
|
+
};
|
|
72957
|
+
}
|
|
72958
|
+
async function runPublishGate(options = {}, deps = {}) {
|
|
72959
|
+
const cwd = deps.cwd || process.cwd();
|
|
72960
|
+
const gitImpl = deps.gitImpl || defaultGit;
|
|
72961
|
+
const readFile = deps.readFile || fs.readFileSync.bind(fs);
|
|
72962
|
+
const exists = deps.exists || fs.existsSync.bind(fs);
|
|
72963
|
+
const preflightFn = deps.preflightFn || defaultPreflight;
|
|
72964
|
+
const log = deps.log || console.log.bind(console);
|
|
72965
|
+
const logErr = deps.logErr || console.error.bind(console);
|
|
72966
|
+
let specPath = options.spec;
|
|
72967
|
+
if (!specPath) {
|
|
72968
|
+
try {
|
|
72969
|
+
specPath = gitImpl(["config", "coderifts.specPath"], cwd);
|
|
72970
|
+
} catch {
|
|
72971
|
+
specPath = "";
|
|
72972
|
+
}
|
|
72973
|
+
}
|
|
72974
|
+
if (!specPath) specPath = "api/openapi.yaml";
|
|
72975
|
+
try {
|
|
72976
|
+
gitImpl(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
72977
|
+
} catch (err) {
|
|
72978
|
+
const msg = "CodeRifts publish-gate: GIT_ERROR \u2014 not a git repository (or git unavailable). Cannot resolve before-spec without git. Fail-closed.";
|
|
72979
|
+
logErr(chalk.red(msg));
|
|
72980
|
+
return finish({
|
|
72981
|
+
ok: false,
|
|
72982
|
+
exitCode: 1,
|
|
72983
|
+
code: "GIT_ERROR",
|
|
72984
|
+
message: msg,
|
|
72985
|
+
policy: "fail_closed:git_error"
|
|
72986
|
+
}, { log, json: options.json });
|
|
72987
|
+
}
|
|
72988
|
+
const beforeRes = resolveBeforeSpec(specPath, {
|
|
72989
|
+
gitImpl,
|
|
72990
|
+
cwd,
|
|
72991
|
+
readFile,
|
|
72992
|
+
packageVersion: deps.packageVersion
|
|
72993
|
+
});
|
|
72994
|
+
if (!beforeRes.ok) {
|
|
72995
|
+
logErr(chalk.red(`CodeRifts publish-gate: ${beforeRes.code} \u2014 ${beforeRes.message}`));
|
|
72996
|
+
return finish({
|
|
72997
|
+
ok: false,
|
|
72998
|
+
exitCode: 1,
|
|
72999
|
+
code: beforeRes.code,
|
|
73000
|
+
message: beforeRes.message,
|
|
73001
|
+
policy: `fail_closed:${beforeRes.code}`,
|
|
73002
|
+
tried: beforeRes.tried
|
|
73003
|
+
}, { log, json: options.json });
|
|
73004
|
+
}
|
|
73005
|
+
const afterRes = resolveAfterSpec(specPath, { cwd, readFile, exists });
|
|
73006
|
+
if (!afterRes.ok) {
|
|
73007
|
+
logErr(chalk.red(`CodeRifts publish-gate: ${afterRes.code} \u2014 ${afterRes.message}`));
|
|
73008
|
+
return finish({
|
|
73009
|
+
ok: false,
|
|
73010
|
+
exitCode: 1,
|
|
73011
|
+
code: afterRes.code,
|
|
73012
|
+
message: afterRes.message,
|
|
73013
|
+
policy: `fail_closed:${afterRes.code}`
|
|
73014
|
+
}, { log, json: options.json });
|
|
73015
|
+
}
|
|
73016
|
+
if (beforeRes.content === afterRes.content) {
|
|
73017
|
+
const payload2 = {
|
|
73018
|
+
ok: true,
|
|
73019
|
+
exitCode: 0,
|
|
73020
|
+
code: "UNCHANGED",
|
|
73021
|
+
message: "Contract artifact unchanged vs baseline; publish permitted.",
|
|
73022
|
+
policy: "permit:unchanged",
|
|
73023
|
+
before_source: beforeRes.source,
|
|
73024
|
+
execution_action: "CONTINUE",
|
|
73025
|
+
receipt: null
|
|
73026
|
+
};
|
|
73027
|
+
if (!options.json) {
|
|
73028
|
+
log(chalk.green("CodeRifts publish-gate: ALLOW (unchanged)"));
|
|
73029
|
+
log(` baseline: ${beforeRes.source}`);
|
|
73030
|
+
log(` spec: ${specPath}`);
|
|
73031
|
+
}
|
|
73032
|
+
return finish(payload2, { log, json: options.json });
|
|
73033
|
+
}
|
|
73034
|
+
let result;
|
|
73035
|
+
try {
|
|
73036
|
+
result = await preflightFn(beforeRes.content, afterRes.content, {
|
|
73037
|
+
apiKey: deps.apiKey,
|
|
73038
|
+
cwd
|
|
73039
|
+
});
|
|
73040
|
+
} catch (err) {
|
|
73041
|
+
const msg = `CodeRifts publish-gate: PREFLIGHT_UNREACHABLE \u2014 ${err && err.message}`;
|
|
73042
|
+
logErr(chalk.red(msg));
|
|
73043
|
+
return finish({
|
|
73044
|
+
ok: false,
|
|
73045
|
+
exitCode: 1,
|
|
73046
|
+
code: "PREFLIGHT_UNREACHABLE",
|
|
73047
|
+
message: msg,
|
|
73048
|
+
policy: "fail_closed:preflight_unreachable"
|
|
73049
|
+
}, { log, json: options.json });
|
|
73050
|
+
}
|
|
73051
|
+
const perm = evaluatePublishPermission(result);
|
|
73052
|
+
const receipt = extractReceiptRef(result);
|
|
73053
|
+
if (!perm.allow) {
|
|
73054
|
+
const payload2 = {
|
|
73055
|
+
ok: false,
|
|
73056
|
+
exitCode: 1,
|
|
73057
|
+
code: "BLOCK",
|
|
73058
|
+
message: "Publish not permitted by execution_action / decision.",
|
|
73059
|
+
policy: perm.policy,
|
|
73060
|
+
execution_action: perm.execution_action,
|
|
73061
|
+
decision: perm.decision,
|
|
73062
|
+
before_source: beforeRes.source,
|
|
73063
|
+
receipt,
|
|
73064
|
+
fail_policy: "exit_1_on_block_or_resolver_error_or_unreachable"
|
|
73065
|
+
};
|
|
73066
|
+
if (!options.json) {
|
|
73067
|
+
logErr("");
|
|
73068
|
+
logErr(chalk.red("========================================"));
|
|
73069
|
+
logErr(chalk.red(" CodeRifts: PUBLISH BLOCKED"));
|
|
73070
|
+
logErr(chalk.red("========================================"));
|
|
73071
|
+
logErr(` Policy: ${perm.policy}`);
|
|
73072
|
+
logErr(` execution_action: ${perm.execution_action || "(none)"}`);
|
|
73073
|
+
logErr(` decision: ${perm.decision || "(none)"}`);
|
|
73074
|
+
logErr(` baseline: ${beforeRes.source}`);
|
|
73075
|
+
logErr(` fail policy: exit 1 on BLOCK / resolver error / preflight unreachability`);
|
|
73076
|
+
logErr(chalk.red("========================================"));
|
|
73077
|
+
logErr("");
|
|
73078
|
+
}
|
|
73079
|
+
return finish(payload2, { log, json: options.json });
|
|
73080
|
+
}
|
|
73081
|
+
const payload = {
|
|
73082
|
+
ok: true,
|
|
73083
|
+
exitCode: 0,
|
|
73084
|
+
code: "ALLOW",
|
|
73085
|
+
message: "Publish permitted.",
|
|
73086
|
+
policy: perm.policy,
|
|
73087
|
+
execution_action: perm.execution_action,
|
|
73088
|
+
decision: perm.decision,
|
|
73089
|
+
before_source: beforeRes.source,
|
|
73090
|
+
receipt
|
|
73091
|
+
};
|
|
73092
|
+
if (!options.json) {
|
|
73093
|
+
log(chalk.green("CodeRifts publish-gate: ALLOW"));
|
|
73094
|
+
log(` Policy: ${perm.policy}`);
|
|
73095
|
+
log(` execution_action: ${perm.execution_action || "(mapped from decision)"}`);
|
|
73096
|
+
log(` baseline: ${beforeRes.source}`);
|
|
73097
|
+
if (receipt) log(` Receipt reference: ${receipt}`);
|
|
73098
|
+
else log(" Receipt reference: (none issued on this path)");
|
|
73099
|
+
}
|
|
73100
|
+
return finish(payload, { log, json: options.json });
|
|
73101
|
+
}
|
|
73102
|
+
function finish(payload, { log, json }) {
|
|
73103
|
+
if (json) {
|
|
73104
|
+
log(JSON.stringify(payload, null, 2));
|
|
73105
|
+
}
|
|
73106
|
+
return payload;
|
|
73107
|
+
}
|
|
73108
|
+
module2.exports = {
|
|
73109
|
+
runPublishGate,
|
|
73110
|
+
resolveBeforeSpec,
|
|
73111
|
+
resolveAfterSpec,
|
|
73112
|
+
evaluatePublishPermission,
|
|
73113
|
+
extractReceiptRef,
|
|
73114
|
+
gitShow,
|
|
73115
|
+
defaultGit,
|
|
73116
|
+
PERMIT_ACTIONS,
|
|
73117
|
+
CLOSED_ACTIONS
|
|
73118
|
+
};
|
|
73119
|
+
}
|
|
73120
|
+
});
|
|
73121
|
+
|
|
73122
|
+
// src/registry-validation-core.js
|
|
73123
|
+
var require_registry_validation_core = __commonJS({
|
|
73124
|
+
"src/registry-validation-core.js"(exports2, module2) {
|
|
73125
|
+
"use strict";
|
|
73126
|
+
var yaml = require_js_yaml();
|
|
73127
|
+
function safeParse(content) {
|
|
73128
|
+
if (!content) return null;
|
|
73129
|
+
try {
|
|
73130
|
+
const trimmed = content.trim();
|
|
73131
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
73132
|
+
return JSON.parse(trimmed);
|
|
73133
|
+
}
|
|
73134
|
+
return yaml.load(trimmed);
|
|
73135
|
+
} catch (_) {
|
|
73136
|
+
return null;
|
|
73137
|
+
}
|
|
73138
|
+
}
|
|
73139
|
+
function validateOpenApiSpec(parsed) {
|
|
73140
|
+
if (!parsed || typeof parsed !== "object") {
|
|
73141
|
+
return { valid: false, error: "Not a valid YAML or JSON object" };
|
|
73142
|
+
}
|
|
73143
|
+
if (parsed.openapi) {
|
|
73144
|
+
const ver = String(parsed.openapi);
|
|
73145
|
+
if (ver.startsWith("3.")) {
|
|
73146
|
+
return { valid: true, version: ver };
|
|
73147
|
+
}
|
|
73148
|
+
return { valid: false, error: `Unsupported OpenAPI version: ${ver}` };
|
|
73149
|
+
}
|
|
73150
|
+
if (parsed.swagger) {
|
|
73151
|
+
const ver = String(parsed.swagger);
|
|
73152
|
+
if (ver.startsWith("2.")) {
|
|
73153
|
+
return { valid: true, version: ver };
|
|
73154
|
+
}
|
|
73155
|
+
return { valid: false, error: `Unsupported Swagger version: ${ver}` };
|
|
73156
|
+
}
|
|
73157
|
+
return { valid: false, error: "Missing required field: 'openapi' or 'swagger'" };
|
|
73158
|
+
}
|
|
73159
|
+
function extractEndpoints(parsed) {
|
|
73160
|
+
const endpoints = [];
|
|
73161
|
+
const paths = parsed.paths || {};
|
|
73162
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
73163
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
73164
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
73165
|
+
for (const method of httpMethods) {
|
|
73166
|
+
if (pathItem[method]) {
|
|
73167
|
+
endpoints.push({
|
|
73168
|
+
path,
|
|
73169
|
+
method: method.toUpperCase(),
|
|
73170
|
+
operationId: pathItem[method].operationId || ""
|
|
73171
|
+
});
|
|
73172
|
+
}
|
|
73173
|
+
}
|
|
73174
|
+
}
|
|
73175
|
+
return endpoints;
|
|
73176
|
+
}
|
|
73177
|
+
function extractSchemaNames(parsed) {
|
|
73178
|
+
const schemas = [];
|
|
73179
|
+
const components = parsed.components?.schemas || {};
|
|
73180
|
+
for (const [name, schema] of Object.entries(components)) {
|
|
73181
|
+
const hash = JSON.stringify(schema);
|
|
73182
|
+
schemas.push({ name, hash });
|
|
73183
|
+
}
|
|
73184
|
+
return schemas;
|
|
73185
|
+
}
|
|
73186
|
+
function extractDefinedScopes(parsed) {
|
|
73187
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
73188
|
+
const schemes = parsed.components?.securitySchemes || {};
|
|
73189
|
+
for (const scheme of Object.values(schemes)) {
|
|
73190
|
+
if (scheme.type === "oauth2" && scheme.flows) {
|
|
73191
|
+
for (const flow of Object.values(scheme.flows)) {
|
|
73192
|
+
if (flow.scopes) {
|
|
73193
|
+
for (const scope of Object.keys(flow.scopes)) {
|
|
73194
|
+
scopes.add(scope);
|
|
73195
|
+
}
|
|
73196
|
+
}
|
|
73197
|
+
}
|
|
73198
|
+
}
|
|
73199
|
+
}
|
|
73200
|
+
return scopes;
|
|
73201
|
+
}
|
|
73202
|
+
function extractUsedScopes(parsed) {
|
|
73203
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
73204
|
+
if (Array.isArray(parsed.security)) {
|
|
73205
|
+
for (const req of parsed.security) {
|
|
73206
|
+
for (const scopeList of Object.values(req)) {
|
|
73207
|
+
if (Array.isArray(scopeList)) {
|
|
73208
|
+
for (const s of scopeList) scopes.add(s);
|
|
73209
|
+
}
|
|
73210
|
+
}
|
|
73211
|
+
}
|
|
73212
|
+
}
|
|
73213
|
+
const paths = parsed.paths || {};
|
|
73214
|
+
const httpMethods = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
73215
|
+
for (const pathItem of Object.values(paths)) {
|
|
73216
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
73217
|
+
for (const method of httpMethods) {
|
|
73218
|
+
const op = pathItem[method];
|
|
73219
|
+
if (op?.security && Array.isArray(op.security)) {
|
|
73220
|
+
for (const req of op.security) {
|
|
73221
|
+
for (const scopeList of Object.values(req)) {
|
|
73222
|
+
if (Array.isArray(scopeList)) {
|
|
73223
|
+
for (const s of scopeList) scopes.add(s);
|
|
73224
|
+
}
|
|
73225
|
+
}
|
|
73226
|
+
}
|
|
73227
|
+
}
|
|
73228
|
+
}
|
|
73229
|
+
}
|
|
73230
|
+
return scopes;
|
|
73231
|
+
}
|
|
73232
|
+
function extractRefs(obj, refs = /* @__PURE__ */ new Set()) {
|
|
73233
|
+
if (!obj || typeof obj !== "object") return refs;
|
|
73234
|
+
if (Array.isArray(obj)) {
|
|
73235
|
+
for (const item of obj) extractRefs(item, refs);
|
|
73236
|
+
return refs;
|
|
73237
|
+
}
|
|
73238
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
73239
|
+
if (key === "$ref" && typeof value === "string") {
|
|
73240
|
+
refs.add(value);
|
|
73241
|
+
} else {
|
|
73242
|
+
extractRefs(value, refs);
|
|
73243
|
+
}
|
|
73244
|
+
}
|
|
73245
|
+
return refs;
|
|
73246
|
+
}
|
|
73247
|
+
function refResolves(parsed, ref) {
|
|
73248
|
+
if (!ref.startsWith("#/")) return true;
|
|
73249
|
+
const parts = ref.replace("#/", "").split("/");
|
|
73250
|
+
let current = parsed;
|
|
73251
|
+
for (const part of parts) {
|
|
73252
|
+
if (!current || typeof current !== "object") return false;
|
|
73253
|
+
current = current[part];
|
|
73254
|
+
}
|
|
73255
|
+
return current !== void 0;
|
|
73256
|
+
}
|
|
73257
|
+
function validateRegistry(specs) {
|
|
73258
|
+
if (!Array.isArray(specs) || specs.length === 0) {
|
|
73259
|
+
return { valid: true, issues: [], stats: { specs_count: 0, endpoints_count: 0, schemas_count: 0 } };
|
|
73260
|
+
}
|
|
73261
|
+
const issues = [];
|
|
73262
|
+
const parsedSpecs = [];
|
|
73263
|
+
for (const { name, spec } of specs) {
|
|
73264
|
+
const parsed = safeParse(spec);
|
|
73265
|
+
if (!parsed) {
|
|
73266
|
+
issues.push({
|
|
73267
|
+
severity: "error",
|
|
73268
|
+
type: "parse_error",
|
|
73269
|
+
message: `Could not parse spec '${name}' as valid YAML or JSON`,
|
|
73270
|
+
specs: [name]
|
|
73271
|
+
});
|
|
73272
|
+
continue;
|
|
73273
|
+
}
|
|
73274
|
+
const validation = validateOpenApiSpec(parsed);
|
|
73275
|
+
if (!validation.valid) {
|
|
73276
|
+
issues.push({
|
|
73277
|
+
severity: "error",
|
|
73278
|
+
type: "invalid_spec",
|
|
73279
|
+
message: `Spec '${name}' is not a valid OpenAPI document: ${validation.error}`,
|
|
73280
|
+
specs: [name]
|
|
73281
|
+
});
|
|
73282
|
+
continue;
|
|
73283
|
+
}
|
|
73284
|
+
parsedSpecs.push({ name, parsed });
|
|
73285
|
+
}
|
|
73286
|
+
const endpointMap = /* @__PURE__ */ new Map();
|
|
73287
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
73288
|
+
const endpoints = extractEndpoints(parsed);
|
|
73289
|
+
for (const ep of endpoints) {
|
|
73290
|
+
const key = `${ep.method} ${ep.path}`;
|
|
73291
|
+
if (!endpointMap.has(key)) endpointMap.set(key, []);
|
|
73292
|
+
endpointMap.get(key).push({ spec: name, operationId: ep.operationId });
|
|
73293
|
+
}
|
|
73294
|
+
}
|
|
73295
|
+
for (const [endpoint, owners] of endpointMap) {
|
|
73296
|
+
if (owners.length > 1) {
|
|
73297
|
+
const specNames = owners.map((o) => o.spec);
|
|
73298
|
+
issues.push({
|
|
73299
|
+
severity: "warning",
|
|
73300
|
+
type: "endpoint_collision",
|
|
73301
|
+
message: `Endpoint '${endpoint}' is defined in multiple specs: ${specNames.join(", ")}`,
|
|
73302
|
+
specs: specNames
|
|
73303
|
+
});
|
|
73304
|
+
}
|
|
73305
|
+
}
|
|
73306
|
+
const schemaMap = /* @__PURE__ */ new Map();
|
|
73307
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
73308
|
+
const schemas = extractSchemaNames(parsed);
|
|
73309
|
+
for (const s of schemas) {
|
|
73310
|
+
if (!schemaMap.has(s.name)) schemaMap.set(s.name, []);
|
|
73311
|
+
schemaMap.get(s.name).push({ spec: name, hash: s.hash });
|
|
73312
|
+
}
|
|
73313
|
+
}
|
|
73314
|
+
for (const [schemaName, definitions] of schemaMap) {
|
|
73315
|
+
if (definitions.length > 1) {
|
|
73316
|
+
const uniqueHashes = new Set(definitions.map((d) => d.hash));
|
|
73317
|
+
if (uniqueHashes.size > 1) {
|
|
73318
|
+
const specNames = definitions.map((d) => d.spec);
|
|
73319
|
+
issues.push({
|
|
73320
|
+
severity: "warning",
|
|
73321
|
+
type: "schema_conflict",
|
|
73322
|
+
message: `Schema '${schemaName}' has conflicting definitions across specs: ${specNames.join(", ")}`,
|
|
73323
|
+
specs: specNames
|
|
73324
|
+
});
|
|
73325
|
+
}
|
|
73326
|
+
}
|
|
73327
|
+
}
|
|
73328
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
73329
|
+
const defined = extractDefinedScopes(parsed);
|
|
73330
|
+
const used = extractUsedScopes(parsed);
|
|
73331
|
+
for (const scope of used) {
|
|
73332
|
+
if (!defined.has(scope)) {
|
|
73333
|
+
issues.push({
|
|
73334
|
+
severity: "warning",
|
|
73335
|
+
type: "undefined_scope",
|
|
73336
|
+
message: `Scope '${scope}' is used in '${name}' but not defined in securitySchemes`,
|
|
73337
|
+
specs: [name]
|
|
73338
|
+
});
|
|
73339
|
+
}
|
|
73340
|
+
}
|
|
73341
|
+
for (const scope of defined) {
|
|
73342
|
+
if (!used.has(scope)) {
|
|
73343
|
+
issues.push({
|
|
73344
|
+
severity: "info",
|
|
73345
|
+
type: "unused_scope",
|
|
73346
|
+
message: `Scope '${scope}' is defined in '${name}' but never used in any operation`,
|
|
73347
|
+
specs: [name]
|
|
73348
|
+
});
|
|
73349
|
+
}
|
|
73350
|
+
}
|
|
73351
|
+
}
|
|
73352
|
+
for (const { name, parsed } of parsedSpecs) {
|
|
73353
|
+
const refs = extractRefs(parsed);
|
|
73354
|
+
for (const ref of refs) {
|
|
73355
|
+
if (!refResolves(parsed, ref)) {
|
|
73356
|
+
issues.push({
|
|
73357
|
+
severity: "error",
|
|
73358
|
+
type: "unresolved_ref",
|
|
73359
|
+
message: `$ref '${ref}' in '${name}' does not resolve`,
|
|
73360
|
+
specs: [name]
|
|
73361
|
+
});
|
|
73362
|
+
}
|
|
73363
|
+
}
|
|
73364
|
+
}
|
|
73365
|
+
let totalEndpoints = 0;
|
|
73366
|
+
let totalSchemas = 0;
|
|
73367
|
+
for (const { parsed } of parsedSpecs) {
|
|
73368
|
+
totalEndpoints += extractEndpoints(parsed).length;
|
|
73369
|
+
totalSchemas += extractSchemaNames(parsed).length;
|
|
73370
|
+
}
|
|
73371
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
73372
|
+
return {
|
|
73373
|
+
valid: !hasErrors,
|
|
73374
|
+
issues,
|
|
73375
|
+
stats: {
|
|
73376
|
+
specs_count: parsedSpecs.length,
|
|
73377
|
+
endpoints_count: totalEndpoints,
|
|
73378
|
+
schemas_count: totalSchemas
|
|
73379
|
+
}
|
|
73380
|
+
};
|
|
73381
|
+
}
|
|
73382
|
+
module2.exports = {
|
|
73383
|
+
validateRegistry,
|
|
73384
|
+
safeParse,
|
|
73385
|
+
validateOpenApiSpec,
|
|
73386
|
+
// Exported for testing
|
|
73387
|
+
extractEndpoints,
|
|
73388
|
+
extractSchemaNames,
|
|
73389
|
+
extractDefinedScopes,
|
|
73390
|
+
extractUsedScopes,
|
|
73391
|
+
extractRefs,
|
|
73392
|
+
refResolves
|
|
73393
|
+
};
|
|
73394
|
+
}
|
|
73395
|
+
});
|
|
73396
|
+
|
|
73397
|
+
// src/commands/registry-gate.js
|
|
73398
|
+
var require_registry_gate = __commonJS({
|
|
73399
|
+
"src/commands/registry-gate.js"(exports2, module2) {
|
|
73400
|
+
"use strict";
|
|
73401
|
+
var fs = require("fs");
|
|
73402
|
+
var path = require("path");
|
|
73403
|
+
var chalk = require_source();
|
|
73404
|
+
var { matchGlob } = require_cjs3();
|
|
73405
|
+
var {
|
|
73406
|
+
validateRegistry,
|
|
73407
|
+
safeParse,
|
|
73408
|
+
validateOpenApiSpec
|
|
73409
|
+
} = require_registry_validation_core();
|
|
73410
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
73411
|
+
var SPEC_EXT = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
|
|
73412
|
+
function walkSpecCandidates(rootDir, {
|
|
73413
|
+
readdirSync = fs.readdirSync.bind(fs),
|
|
73414
|
+
statSync = fs.statSync.bind(fs)
|
|
73415
|
+
} = {}) {
|
|
73416
|
+
const out = [];
|
|
73417
|
+
function walk(absDir) {
|
|
73418
|
+
let entries;
|
|
73419
|
+
try {
|
|
73420
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
73421
|
+
} catch (err) {
|
|
73422
|
+
const e = new Error(`Cannot read directory ${absDir}: ${err && err.message}`);
|
|
73423
|
+
e.code = "GATE_ERROR";
|
|
73424
|
+
e.path = absDir;
|
|
73425
|
+
throw e;
|
|
73426
|
+
}
|
|
73427
|
+
for (const ent of entries) {
|
|
73428
|
+
const name = ent.name;
|
|
73429
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
73430
|
+
const abs = path.join(absDir, name);
|
|
73431
|
+
let isDir = ent.isDirectory && ent.isDirectory();
|
|
73432
|
+
let isFile = ent.isFile && ent.isFile();
|
|
73433
|
+
if (!isDir && !isFile) {
|
|
73434
|
+
try {
|
|
73435
|
+
const st = statSync(abs);
|
|
73436
|
+
isDir = st.isDirectory();
|
|
73437
|
+
isFile = st.isFile();
|
|
73438
|
+
} catch (err) {
|
|
73439
|
+
const e = new Error(`Cannot stat ${abs}: ${err && err.message}`);
|
|
73440
|
+
e.code = "GATE_ERROR";
|
|
73441
|
+
e.path = abs;
|
|
73442
|
+
throw e;
|
|
73443
|
+
}
|
|
73444
|
+
}
|
|
73445
|
+
if (isDir) {
|
|
73446
|
+
walk(abs);
|
|
73447
|
+
continue;
|
|
73448
|
+
}
|
|
73449
|
+
if (isFile) {
|
|
73450
|
+
const ext = path.extname(name).toLowerCase();
|
|
73451
|
+
if (SPEC_EXT.has(ext)) out.push(abs);
|
|
73452
|
+
}
|
|
73453
|
+
}
|
|
73454
|
+
}
|
|
73455
|
+
walk(rootDir);
|
|
73456
|
+
return out.sort();
|
|
73457
|
+
}
|
|
73458
|
+
function relPosix(rootDir, absPath) {
|
|
73459
|
+
let rel = path.relative(rootDir, absPath);
|
|
73460
|
+
if (path.sep !== "/") rel = rel.split(path.sep).join("/");
|
|
73461
|
+
return rel;
|
|
73462
|
+
}
|
|
73463
|
+
function readFileThreeState(absPath, readFileSync = fs.readFileSync.bind(fs)) {
|
|
73464
|
+
try {
|
|
73465
|
+
const content = readFileSync(absPath, "utf8");
|
|
73466
|
+
return { ok: true, content: content == null ? "" : String(content) };
|
|
73467
|
+
} catch (err) {
|
|
73468
|
+
const msg = err && err.message ? String(err.message) : String(err);
|
|
73469
|
+
return {
|
|
73470
|
+
ok: false,
|
|
73471
|
+
code: "GATE_ERROR",
|
|
73472
|
+
message: `unreadable file: ${absPath} (${msg.slice(0, 200)})`,
|
|
73473
|
+
path: absPath
|
|
73474
|
+
};
|
|
73475
|
+
}
|
|
73476
|
+
}
|
|
73477
|
+
function discoverRegistrySpecs(dir, {
|
|
73478
|
+
glob = null,
|
|
73479
|
+
readdirSync = fs.readdirSync.bind(fs),
|
|
73480
|
+
readFileSync = fs.readFileSync.bind(fs),
|
|
73481
|
+
statSync = fs.statSync.bind(fs)
|
|
73482
|
+
} = {}) {
|
|
73483
|
+
const rootDir = path.resolve(dir);
|
|
73484
|
+
let candidates;
|
|
73485
|
+
try {
|
|
73486
|
+
candidates = walkSpecCandidates(rootDir, { readdirSync, statSync });
|
|
73487
|
+
} catch (err) {
|
|
73488
|
+
return {
|
|
73489
|
+
ok: false,
|
|
73490
|
+
code: err.code || "GATE_ERROR",
|
|
73491
|
+
message: err.message || String(err),
|
|
73492
|
+
path: err.path
|
|
73493
|
+
};
|
|
73494
|
+
}
|
|
73495
|
+
if (glob) {
|
|
73496
|
+
candidates = candidates.filter((abs) => matchGlob(glob, relPosix(rootDir, abs)));
|
|
73497
|
+
}
|
|
73498
|
+
const specs = [];
|
|
73499
|
+
let skippedNonSpec = 0;
|
|
73500
|
+
for (const abs of candidates) {
|
|
73501
|
+
const read = readFileThreeState(abs, readFileSync);
|
|
73502
|
+
if (!read.ok) {
|
|
73503
|
+
return {
|
|
73504
|
+
ok: false,
|
|
73505
|
+
code: "GATE_ERROR",
|
|
73506
|
+
message: read.message,
|
|
73507
|
+
path: read.path || abs
|
|
73508
|
+
};
|
|
73509
|
+
}
|
|
73510
|
+
const name = relPosix(rootDir, abs);
|
|
73511
|
+
const parsed = safeParse(read.content);
|
|
73512
|
+
if (!parsed || typeof parsed !== "object") {
|
|
73513
|
+
specs.push({ name, spec: read.content });
|
|
73514
|
+
continue;
|
|
73515
|
+
}
|
|
73516
|
+
const shape = validateOpenApiSpec(parsed);
|
|
73517
|
+
if (!shape.valid) {
|
|
73518
|
+
if (shape.error && shape.error.includes("Missing required field: 'openapi' or 'swagger'")) {
|
|
73519
|
+
skippedNonSpec += 1;
|
|
73520
|
+
continue;
|
|
73521
|
+
}
|
|
73522
|
+
specs.push({ name, spec: read.content });
|
|
73523
|
+
continue;
|
|
73524
|
+
}
|
|
73525
|
+
specs.push({ name, spec: read.content });
|
|
73526
|
+
}
|
|
73527
|
+
return {
|
|
73528
|
+
ok: true,
|
|
73529
|
+
specs,
|
|
73530
|
+
skippedNonSpec,
|
|
73531
|
+
candidates: candidates.length,
|
|
73532
|
+
rootDir
|
|
73533
|
+
};
|
|
73534
|
+
}
|
|
73535
|
+
function findingsFailGate(issues, { warnOnly = false, errorsOnly = false } = {}) {
|
|
73536
|
+
if (warnOnly) return false;
|
|
73537
|
+
for (const i of issues || []) {
|
|
73538
|
+
if (i.severity === "error") return true;
|
|
73539
|
+
if (i.severity === "warning" && !errorsOnly) return true;
|
|
73540
|
+
}
|
|
73541
|
+
return false;
|
|
73542
|
+
}
|
|
73543
|
+
function countBySeverity(issues) {
|
|
73544
|
+
const c = { error: 0, warning: 0, info: 0 };
|
|
73545
|
+
for (const i of issues || []) {
|
|
72833
73546
|
if (i.severity === "error") c.error += 1;
|
|
72834
73547
|
else if (i.severity === "warning") c.warning += 1;
|
|
72835
73548
|
else if (i.severity === "info") c.info += 1;
|
|
@@ -99945,7 +100658,7 @@ var require_enforce = __commonJS({
|
|
|
99945
100658
|
var DEPLOY_GUIDANCE = [
|
|
99946
100659
|
"Deploy is declared-only on the server (never server-observed ENFORCING).",
|
|
99947
100660
|
"Add a CD step that runs: coderifts deploy-gate --env <env> --artifact <id> --receipt <file> --enforce",
|
|
99948
|
-
" (
|
|
100661
|
+
" (fail-closed by default; --enforce attests ENFORCING for inescapable_deploy. CODERIFTS_DEPLOY_ADVISORY=1 to soften.)",
|
|
99949
100662
|
"Also set policy.require_source_binding: true in .coderifts.yml for the deploy declaration leg."
|
|
99950
100663
|
].join("\n");
|
|
99951
100664
|
var CONTENT_GUIDANCE = [
|
|
@@ -101101,809 +101814,218 @@ var require_adopt = __commonJS({
|
|
|
101101
101814
|
lines.push(chalk.bold("Per change set (counterfactual)"));
|
|
101102
101815
|
lines.push(
|
|
101103
101816
|
pad("REF", 12) + pad("PATH", 28) + pad("ACTION", 18) + pad("RISK", 6) + "DETECTORS"
|
|
101104
|
-
);
|
|
101105
|
-
lines.push("-".repeat(90));
|
|
101106
|
-
for (const row of report.change_sets) {
|
|
101107
|
-
const ref = (row.ref || "").slice(0, 10);
|
|
101108
|
-
const pth = (row.path || "").slice(0, 26);
|
|
101109
|
-
const act = row.execution_action || (row.error ? "ERROR" : "\u2014");
|
|
101110
|
-
const risk = row.risk_score != null ? String(row.risk_score) : "\u2014";
|
|
101111
|
-
const dets = (row.detectors_fired || []).map((d) => d.id).join(",") || "\u2014";
|
|
101112
|
-
lines.push(pad(ref, 12) + pad(pth, 28) + pad(act, 18) + pad(risk, 6) + dets.slice(0, 40));
|
|
101113
|
-
}
|
|
101114
|
-
lines.push("");
|
|
101115
|
-
lines.push(chalk.bold("Per-detector aggregate (default-flip evidence \u2014 not a recommendation)"));
|
|
101116
|
-
if (!report.per_detector || report.per_detector.length === 0) {
|
|
101117
|
-
lines.push(chalk.dim("(no detectors fired on any change set)"));
|
|
101118
|
-
} else {
|
|
101119
|
-
lines.push(pad("DETECTOR", 36) + pad("SEVERITY", 12) + "FIRE_COUNT");
|
|
101120
|
-
lines.push("-".repeat(60));
|
|
101121
|
-
for (const d of report.per_detector) {
|
|
101122
|
-
lines.push(pad(d.id, 36) + pad(d.severity, 12) + String(d.fire_count));
|
|
101123
|
-
}
|
|
101124
|
-
}
|
|
101125
|
-
lines.push("");
|
|
101126
|
-
lines.push(chalk.dim("High fire_count \u2192 candidate false-positive noise (not safe to default-block without review)."));
|
|
101127
|
-
lines.push(chalk.dim("Rare + CRITICAL/HIGH \u2192 safer default-block candidates. Human decides; no auto-flip."));
|
|
101128
|
-
lines.push("");
|
|
101129
|
-
return lines.join("\n");
|
|
101130
|
-
}
|
|
101131
|
-
function pad(s, n) {
|
|
101132
|
-
const t = String(s || "");
|
|
101133
|
-
if (t.length >= n) return `${t.slice(0, n - 1)} `;
|
|
101134
|
-
return t + " ".repeat(n - t.length);
|
|
101135
|
-
}
|
|
101136
|
-
async function runAdopt(options = {}, deps = {}) {
|
|
101137
|
-
const log = deps.log || console.log.bind(console);
|
|
101138
|
-
const logErr = deps.logErr || console.error.bind(console);
|
|
101139
|
-
const load = deps.loadCore || loadCore;
|
|
101140
|
-
const collected = collectChangeSetsFromGit(options, deps);
|
|
101141
|
-
if (!collected.ok) {
|
|
101142
|
-
logErr(chalk.red(`CodeRifts adopt: ${collected.code} \u2014 ${collected.message}`));
|
|
101143
|
-
return { ok: false, exitCode: 1, code: collected.code, message: collected.message };
|
|
101144
|
-
}
|
|
101145
|
-
let core;
|
|
101146
|
-
try {
|
|
101147
|
-
core = load();
|
|
101148
|
-
} catch (err) {
|
|
101149
|
-
logErr(chalk.red(`CodeRifts adopt: ${err.message}`));
|
|
101150
|
-
return { ok: false, exitCode: 1, code: "CORE_LOAD_ERROR", message: err.message };
|
|
101151
|
-
}
|
|
101152
|
-
const runCore = () => core.buildCounterfactualReport(collected.changeSets, deps.config || {});
|
|
101153
|
-
const report = options.json ? await withJsonQuietAnalyzerLogs(runCore) : await runCore();
|
|
101154
|
-
if (options.json) {
|
|
101155
|
-
log(JSON.stringify({
|
|
101156
|
-
...report,
|
|
101157
|
-
meta: {
|
|
101158
|
-
commit_count: collected.commit_count,
|
|
101159
|
-
change_set_count: collected.changeSets.length
|
|
101160
|
-
}
|
|
101161
|
-
}, null, 2));
|
|
101162
|
-
} else {
|
|
101163
|
-
log(renderHuman(report, { commit_count: collected.commit_count }));
|
|
101164
|
-
}
|
|
101165
|
-
return {
|
|
101166
|
-
ok: true,
|
|
101167
|
-
exitCode: 0,
|
|
101168
|
-
report,
|
|
101169
|
-
meta: { commit_count: collected.commit_count }
|
|
101170
|
-
};
|
|
101171
|
-
}
|
|
101172
|
-
module2.exports = {
|
|
101173
|
-
runAdopt,
|
|
101174
|
-
collectChangeSetsFromGit,
|
|
101175
|
-
isContractPath,
|
|
101176
|
-
withJsonQuietAnalyzerLogs,
|
|
101177
|
-
renderHuman,
|
|
101178
|
-
defaultGit,
|
|
101179
|
-
SOURCE_CODE_EXT_RE
|
|
101180
|
-
};
|
|
101181
|
-
}
|
|
101182
|
-
});
|
|
101183
|
-
|
|
101184
|
-
// src/commands/outcome.js
|
|
101185
|
-
var require_outcome = __commonJS({
|
|
101186
|
-
"src/commands/outcome.js"(exports2, module2) {
|
|
101187
|
-
"use strict";
|
|
101188
|
-
var chalk = require_source();
|
|
101189
|
-
var { getApiKey } = require_config();
|
|
101190
|
-
var { cloudPostOutcome } = require_cloud();
|
|
101191
|
-
if (process.env.NO_COLOR) chalk.level = 0;
|
|
101192
|
-
var OUTCOME_KINDS = Object.freeze([
|
|
101193
|
-
"deploy_succeeded",
|
|
101194
|
-
"deploy_failed",
|
|
101195
|
-
"rolled_back",
|
|
101196
|
-
"consumer_break_reported",
|
|
101197
|
-
"remediation_verified_working",
|
|
101198
|
-
"false_positive_reported",
|
|
101199
|
-
"other_reported"
|
|
101200
|
-
]);
|
|
101201
|
-
var OUTCOME_KIND_SET = new Set(OUTCOME_KINDS);
|
|
101202
|
-
var USAGE = [
|
|
101203
|
-
"Usage: coderifts outcome <kind> --decision <decision_id> [--observed-at <ISO>] [--details <json>] [--json]",
|
|
101204
|
-
"",
|
|
101205
|
-
"Report a post-hoc observed outcome for a past decision_id to the CodeRifts cloud API.",
|
|
101206
|
-
"This records the CALLER'S assertion (e.g. your deploy job knows success/failure) \u2014",
|
|
101207
|
-
"the command does NOT itself verify that a deploy succeeded or failed.",
|
|
101208
|
-
"",
|
|
101209
|
-
"kind (required, closed set):",
|
|
101210
|
-
` ${OUTCOME_KINDS.join(", ")}`,
|
|
101211
|
-
"",
|
|
101212
|
-
"Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
|
|
101213
|
-
"POST /api/v1/outcomes \u2014 source is always reported (server-side); reporter is derived from the key."
|
|
101214
|
-
].join("\n");
|
|
101215
|
-
function isValidOutcomeKind(kind) {
|
|
101216
|
-
return typeof kind === "string" && OUTCOME_KIND_SET.has(kind);
|
|
101217
|
-
}
|
|
101218
|
-
function isValidObservedAt(iso) {
|
|
101219
|
-
if (typeof iso !== "string" || !iso.trim()) return false;
|
|
101220
|
-
const t = Date.parse(iso);
|
|
101221
|
-
return Number.isFinite(t);
|
|
101222
|
-
}
|
|
101223
|
-
function parseDetails(raw) {
|
|
101224
|
-
if (raw == null || raw === "") return { ok: true, details: null };
|
|
101225
|
-
if (typeof raw !== "string") return { ok: false, error: "--details must be a JSON string" };
|
|
101226
|
-
try {
|
|
101227
|
-
const v = JSON.parse(raw);
|
|
101228
|
-
if (v !== null && typeof v !== "object") {
|
|
101229
|
-
return { ok: false, error: "--details must be a JSON object or array" };
|
|
101230
|
-
}
|
|
101231
|
-
return { ok: true, details: v };
|
|
101232
|
-
} catch (e) {
|
|
101233
|
-
return { ok: false, error: `--details is not valid JSON: ${e && e.message || "parse error"}` };
|
|
101234
|
-
}
|
|
101235
|
-
}
|
|
101236
|
-
async function runOutcome(kind, options = {}, deps = {}) {
|
|
101237
|
-
const getKey = deps.getApiKey || getApiKey;
|
|
101238
|
-
const postOutcome = deps.cloudPostOutcome || cloudPostOutcome;
|
|
101239
|
-
const log = deps.log || console.log;
|
|
101240
|
-
const errLog = deps.errLog || console.error;
|
|
101241
|
-
const nowIso = deps.nowIso || (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
101242
|
-
if (kind == null || String(kind).trim() === "") {
|
|
101243
|
-
errLog(chalk.red("Error: missing outcome kind"));
|
|
101244
|
-
errLog(USAGE);
|
|
101245
|
-
return { exitCode: 1, error: "missing_kind" };
|
|
101246
|
-
}
|
|
101247
|
-
const outcomeKind = String(kind).trim();
|
|
101248
|
-
if (!isValidOutcomeKind(outcomeKind)) {
|
|
101249
|
-
errLog(chalk.red(`Error: invalid outcome kind '${outcomeKind}'`));
|
|
101250
|
-
errLog(chalk.dim(` Valid kinds: ${OUTCOME_KINDS.join(", ")}`));
|
|
101251
|
-
return { exitCode: 1, error: "invalid_kind" };
|
|
101252
|
-
}
|
|
101253
|
-
const decisionId = options.decision != null ? String(options.decision).trim() : "";
|
|
101254
|
-
if (!decisionId) {
|
|
101255
|
-
errLog(chalk.red("Error: --decision <decision_id> is required"));
|
|
101256
|
-
errLog(USAGE);
|
|
101257
|
-
return { exitCode: 1, error: "missing_decision" };
|
|
101258
|
-
}
|
|
101259
|
-
let observedAt;
|
|
101260
|
-
if (options.observedAt != null && String(options.observedAt).trim() !== "") {
|
|
101261
|
-
observedAt = String(options.observedAt).trim();
|
|
101262
|
-
if (!isValidObservedAt(observedAt)) {
|
|
101263
|
-
errLog(chalk.red("Error: --observed-at must be a valid ISO-8601 timestamp"));
|
|
101264
|
-
return { exitCode: 1, error: "invalid_observed_at" };
|
|
101265
|
-
}
|
|
101266
|
-
} else {
|
|
101267
|
-
observedAt = nowIso();
|
|
101268
|
-
}
|
|
101269
|
-
const det = parseDetails(options.details);
|
|
101270
|
-
if (!det.ok) {
|
|
101271
|
-
errLog(chalk.red(`Error: ${det.error}`));
|
|
101272
|
-
return { exitCode: 1, error: "invalid_details" };
|
|
101273
|
-
}
|
|
101274
|
-
const apiKey = getKey();
|
|
101275
|
-
if (!apiKey) {
|
|
101276
|
-
errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
|
|
101277
|
-
return { exitCode: 1, error: "missing_api_key" };
|
|
101278
|
-
}
|
|
101279
|
-
const body = {
|
|
101280
|
-
decision_id: decisionId,
|
|
101281
|
-
outcome_kind: outcomeKind,
|
|
101282
|
-
observed_at: observedAt
|
|
101283
|
-
};
|
|
101284
|
-
if (det.details != null) body.details = det.details;
|
|
101285
|
-
let response;
|
|
101286
|
-
try {
|
|
101287
|
-
response = await postOutcome(apiKey, body);
|
|
101288
|
-
} catch (e) {
|
|
101289
|
-
const msg = e && e.message ? String(e.message) : "request failed";
|
|
101290
|
-
errLog(chalk.red(`Error: ${msg}`));
|
|
101291
|
-
if (e && e.statusCode) errLog(chalk.dim(` (HTTP ${e.statusCode})`));
|
|
101292
|
-
if (e && e.code) errLog(chalk.dim(` (${e.code})`));
|
|
101293
|
-
return { exitCode: 1, error: msg, body };
|
|
101294
|
-
}
|
|
101295
|
-
if (options.json) {
|
|
101296
|
-
log(JSON.stringify(response, null, 2));
|
|
101297
|
-
} else {
|
|
101298
|
-
const o = response && response.outcome ? response.outcome : response;
|
|
101299
|
-
log(chalk.bold("CodeRifts outcome recorded"));
|
|
101300
|
-
log(` kind: ${outcomeKind}`);
|
|
101301
|
-
log(` decision_id: ${decisionId}`);
|
|
101302
|
-
log(` observed_at: ${observedAt}`);
|
|
101303
|
-
if (o && o.id) log(` id: ${o.id}`);
|
|
101304
|
-
if (response && response.source) log(` source: ${response.source}`);
|
|
101305
|
-
log(chalk.dim(" Caller assertion only \u2014 this command did not verify the deploy."));
|
|
101306
|
-
}
|
|
101307
|
-
return { exitCode: 0, response, body };
|
|
101308
|
-
}
|
|
101309
|
-
module2.exports = {
|
|
101310
|
-
runOutcome,
|
|
101311
|
-
isValidOutcomeKind,
|
|
101312
|
-
isValidObservedAt,
|
|
101313
|
-
parseDetails,
|
|
101314
|
-
OUTCOME_KINDS,
|
|
101315
|
-
USAGE
|
|
101316
|
-
};
|
|
101317
|
-
}
|
|
101318
|
-
});
|
|
101319
|
-
|
|
101320
|
-
// src/commands/claude-hook.js
|
|
101321
|
-
var require_claude_hook = __commonJS({
|
|
101322
|
-
"src/commands/claude-hook.js"(exports2, module2) {
|
|
101323
|
-
"use strict";
|
|
101324
|
-
var fs = require("fs");
|
|
101325
|
-
var path = require("path");
|
|
101326
|
-
var { execSync } = require("child_process");
|
|
101327
|
-
var { getApiKey } = require_config();
|
|
101328
|
-
var { cloudAuthorizePreflight } = require_cloud();
|
|
101329
|
-
var DEFAULT_SPEC_PATH = "api/openapi.yaml";
|
|
101330
|
-
var CLOSED_ACTIONS = /* @__PURE__ */ new Set([
|
|
101331
|
-
"CONTINUE",
|
|
101332
|
-
"CONTINUE_WITH_MONITORING",
|
|
101333
|
-
"REQUEST_APPROVAL",
|
|
101334
|
-
"STOP"
|
|
101335
|
-
]);
|
|
101336
|
-
var USAGE = [
|
|
101337
|
-
"Usage: coderifts claude-hook",
|
|
101338
|
-
"",
|
|
101339
|
-
"Claude Code PreToolUse hook: gates Write|Edit|MultiEdit when they touch the",
|
|
101340
|
-
"configured contract spec path. Reads JSON context from STDIN.",
|
|
101341
|
-
"",
|
|
101342
|
-
"Exit map (Claude Code semantics \u2014 get this exactly right):",
|
|
101343
|
-
" 2 BLOCK \u2014 tool call cancelled (BLOCK/STOP, REQUEST_APPROVAL, unrecognised action,",
|
|
101344
|
-
" governance could not run, CONTINUE_WITH_MONITORING without a wired sink)",
|
|
101345
|
-
" 0 allow \u2014 CONTINUE, or CWM with a host-asserted monitoring sink, or non-contract skip",
|
|
101346
|
-
" 1 NEVER used for security deny (Claude treats exit 1 as non-blocking; action proceeds)",
|
|
101347
|
-
"",
|
|
101348
|
-
"Fail-closed DEFAULT (exit 2, reason enforce_indeterminate) when governance could not run:",
|
|
101349
|
-
" no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
|
|
101350
|
-
" Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
|
|
101351
|
-
"",
|
|
101352
|
-
"Parse-gap DEFAULT (exit 2): unparseable stdin / missing file_path \u2014 refusing (fail-closed);",
|
|
101353
|
-
" set CODERIFTS_ADVISORY=1 to soften. JSONL hook_blocked cause stdin_unparseable|missing_file_path.",
|
|
101354
|
-
"",
|
|
101355
|
-
"Still exit 0 (nothing to govern): non-spec path, identical content.",
|
|
101356
|
-
"",
|
|
101357
|
-
"CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
|
|
101358
|
-
" (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
|
|
101359
|
-
" Host claim \u2014 not delivery proof (same class as GuardConfig.monitoringSinkWired).",
|
|
101360
|
-
"",
|
|
101361
|
-
"CODERIFTS_STRICT=1|true: same fail-closed as default; cannot be weakened by ADVISORY.",
|
|
101362
|
-
"",
|
|
101363
|
-
"Spec path (same source as git pre-push hook):",
|
|
101364
|
-
" git config coderifts.specPath (default: api/openapi.yaml)",
|
|
101365
|
-
"",
|
|
101366
|
-
"Baseline at PreToolUse: before = current file on disk (pre-edit state \u2014 the tool has",
|
|
101367
|
-
"not written yet). after = proposed content from tool_input (Write: content; Edit/MultiEdit:",
|
|
101368
|
-
"old\u2192new applied to disk content).",
|
|
101369
|
-
"",
|
|
101370
|
-
"Push-time equivalent: coderifts hook install (git exit 1 on BLOCK)."
|
|
101371
|
-
].join("\n");
|
|
101372
|
-
function isEnvFlag(env, name) {
|
|
101373
|
-
const v = env && env[name];
|
|
101374
|
-
if (v == null || v === "") return false;
|
|
101375
|
-
const s = String(v).trim().toLowerCase();
|
|
101376
|
-
return s === "1" || s === "true";
|
|
101377
|
-
}
|
|
101378
|
-
function isStrictMode(env = process.env) {
|
|
101379
|
-
return isEnvFlag(env, "CODERIFTS_STRICT");
|
|
101380
|
-
}
|
|
101381
|
-
function isAdvisoryMode(env = process.env) {
|
|
101382
|
-
return isEnvFlag(env, "CODERIFTS_ADVISORY");
|
|
101383
|
-
}
|
|
101384
|
-
function isMonitoringSinkWired({ env = process.env, deps = {}, result = null } = {}) {
|
|
101385
|
-
if (deps && deps.monitoringSinkWired === true) return true;
|
|
101386
|
-
if (isEnvFlag(env, "CODERIFTS_MONITORING_SINK_WIRED")) return true;
|
|
101387
|
-
const git = readGitConfig("coderifts.monitoringSinkWired", deps);
|
|
101388
|
-
if (git) {
|
|
101389
|
-
const s = String(git).trim().toLowerCase();
|
|
101390
|
-
if (s === "1" || s === "true") return true;
|
|
101391
|
-
}
|
|
101392
|
-
if (result && result.monitoringSinkWired === true) return true;
|
|
101393
|
-
const dr = result && result.decision_result;
|
|
101394
|
-
if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
|
|
101395
|
-
return false;
|
|
101396
|
-
}
|
|
101397
|
-
var HOOK_TRIGGER_SOURCE = "claude_hook";
|
|
101398
|
-
function extractFingerprint(result) {
|
|
101399
|
-
const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
|
|
101400
|
-
const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
|
|
101401
|
-
const fp = dr.fingerprint || d.fingerprint || d.verdict_fingerprint || d.input_fingerprint;
|
|
101402
|
-
return typeof fp === "string" && fp.length > 0 ? fp : void 0;
|
|
101403
|
-
}
|
|
101404
|
-
function hookSessionId(env) {
|
|
101405
|
-
const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
|
|
101406
|
-
if (fromEnv) return fromEnv;
|
|
101407
|
-
if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
|
|
101408
|
-
return hookSessionId._gen;
|
|
101409
|
-
}
|
|
101410
|
-
function appendGuardEventLog(env, event, deps = {}) {
|
|
101411
|
-
const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
|
|
101412
|
-
if (p == null || String(p).trim() === "") return;
|
|
101413
|
-
const errLog = deps.errLog || ((m) => console.error(String(m)));
|
|
101414
|
-
const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
|
|
101415
|
-
try {
|
|
101416
|
-
append(String(p).trim(), JSON.stringify(event) + "\n");
|
|
101417
|
-
} catch (err) {
|
|
101418
|
-
try {
|
|
101419
|
-
errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
|
|
101420
|
-
} catch {
|
|
101421
|
-
}
|
|
101422
|
-
}
|
|
101423
|
-
}
|
|
101424
|
-
function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
|
|
101425
|
-
if (advisory && !strict) {
|
|
101426
|
-
errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
|
|
101427
|
-
return { exitCode: 0, reason: site, site };
|
|
101428
|
-
}
|
|
101429
|
-
if (strict) {
|
|
101430
|
-
errLog(
|
|
101431
|
-
`CodeRifts claude-hook: CODERIFTS_STRICT: governance could not run (${why}) \u2014 blocking. Set a key / restore network, or unset CODERIFTS_STRICT.`
|
|
101432
|
-
);
|
|
101433
|
-
return { exitCode: 2, reason: "enforce_indeterminate", site, strictBlocked: true };
|
|
101434
|
-
}
|
|
101435
|
-
errLog(
|
|
101436
|
-
`CodeRifts claude-hook: governance could not run (${why}) \u2014 blocking. Set CODERIFTS_ADVISORY=1 to allow without governance (explicit opt-out).`
|
|
101437
|
-
);
|
|
101438
|
-
return { exitCode: 2, reason: "enforce_indeterminate", site };
|
|
101439
|
-
}
|
|
101440
|
-
function softOrStrictBlock({ strict, reason, softMsg, why, errLog }) {
|
|
101441
|
-
return failClosedOrAdvisory({
|
|
101442
|
-
advisory: false,
|
|
101443
|
-
strict: !!strict,
|
|
101444
|
-
site: reason,
|
|
101445
|
-
softMsg,
|
|
101446
|
-
why,
|
|
101447
|
-
errLog
|
|
101448
|
-
});
|
|
101449
|
-
}
|
|
101450
|
-
function readGitConfig(key, deps = {}) {
|
|
101451
|
-
const run = deps.execSync || execSync;
|
|
101452
|
-
const cwd = deps.cwd || process.cwd();
|
|
101453
|
-
try {
|
|
101454
|
-
const v = run(`git config ${key}`, { encoding: "utf8", cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
101455
|
-
const t = String(v || "").trim();
|
|
101456
|
-
return t || null;
|
|
101457
|
-
} catch {
|
|
101458
|
-
return null;
|
|
101459
|
-
}
|
|
101460
|
-
}
|
|
101461
|
-
function resolveApiKey(deps = {}) {
|
|
101462
|
-
const getKey = deps.getApiKey || getApiKey;
|
|
101463
|
-
const fromLogin = getKey();
|
|
101464
|
-
if (fromLogin) return fromLogin;
|
|
101465
|
-
const env = deps.env || process.env;
|
|
101466
|
-
const fromEnv = env.CODERIFTS_API_KEY && String(env.CODERIFTS_API_KEY).trim();
|
|
101467
|
-
if (fromEnv) return fromEnv;
|
|
101468
|
-
return readGitConfig("coderifts.apiKey", deps);
|
|
101469
|
-
}
|
|
101470
|
-
function resolveSpecPath(deps = {}) {
|
|
101471
|
-
const fromGit = readGitConfig("coderifts.specPath", deps);
|
|
101472
|
-
if (fromGit) return fromGit;
|
|
101473
|
-
return DEFAULT_SPEC_PATH;
|
|
101474
|
-
}
|
|
101475
|
-
function parseStdinJson(raw) {
|
|
101476
|
-
if (raw == null || String(raw).trim() === "") {
|
|
101477
|
-
return { ok: false, reason: "empty stdin" };
|
|
101478
|
-
}
|
|
101479
|
-
let obj;
|
|
101480
|
-
try {
|
|
101481
|
-
obj = JSON.parse(String(raw));
|
|
101482
|
-
} catch {
|
|
101483
|
-
return { ok: false, reason: "stdin is not JSON" };
|
|
101484
|
-
}
|
|
101485
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
101486
|
-
return { ok: false, reason: "stdin JSON is not an object" };
|
|
101487
|
-
}
|
|
101488
|
-
const toolName = obj.tool_name || obj.toolName || obj.name || null;
|
|
101489
|
-
let toolInput = obj.tool_input || obj.toolInput || obj.input || null;
|
|
101490
|
-
if (toolInput == null && obj.file_path) toolInput = obj;
|
|
101491
|
-
if (!toolName || typeof toolName !== "string") {
|
|
101492
|
-
return { ok: false, reason: "missing tool_name" };
|
|
101493
|
-
}
|
|
101494
|
-
if (!toolInput || typeof toolInput !== "object") {
|
|
101495
|
-
return { ok: false, reason: "missing tool_input" };
|
|
101496
|
-
}
|
|
101497
|
-
return { ok: true, toolName: String(toolName), toolInput };
|
|
101498
|
-
}
|
|
101499
|
-
function isSpecPath(filePath, specPath) {
|
|
101500
|
-
if (!filePath || !specPath) return false;
|
|
101501
|
-
const fp = path.normalize(String(filePath).replace(/\\/g, "/"));
|
|
101502
|
-
const sp = path.normalize(String(specPath).replace(/\\/g, "/"));
|
|
101503
|
-
if (fp === sp) return true;
|
|
101504
|
-
if (fp.endsWith("/" + sp) || fp.endsWith(sp)) return true;
|
|
101505
|
-
const baseFp = path.basename(fp);
|
|
101506
|
-
const baseSp = path.basename(sp);
|
|
101507
|
-
if (baseFp === baseSp && (fp.endsWith(sp) || sp.endsWith(baseSp))) {
|
|
101508
|
-
const tail = sp.split("/").filter(Boolean).join("/");
|
|
101509
|
-
return fp.replace(/\\/g, "/").endsWith(tail);
|
|
101510
|
-
}
|
|
101511
|
-
return false;
|
|
101512
|
-
}
|
|
101513
|
-
function deriveAfterContent(toolName, toolInput, diskBefore) {
|
|
101514
|
-
const name = String(toolName || "");
|
|
101515
|
-
if (name === "Write" || name === "write") {
|
|
101516
|
-
if (typeof toolInput.content !== "string") {
|
|
101517
|
-
return { ok: false, reason: "Write tool_input.content missing or not a string" };
|
|
101518
|
-
}
|
|
101519
|
-
return { ok: true, after: toolInput.content };
|
|
101520
|
-
}
|
|
101521
|
-
if (name === "Edit" || name === "edit") {
|
|
101522
|
-
const oldS = toolInput.old_string != null ? toolInput.old_string : toolInput.oldString;
|
|
101523
|
-
const newS = toolInput.new_string != null ? toolInput.new_string : toolInput.newString;
|
|
101524
|
-
if (typeof oldS !== "string" || typeof newS !== "string") {
|
|
101525
|
-
return { ok: false, reason: "Edit tool_input.old_string/new_string missing" };
|
|
101526
|
-
}
|
|
101527
|
-
if (!diskBefore.includes(oldS)) {
|
|
101528
|
-
return { ok: false, reason: "Edit old_string not found in disk content" };
|
|
101529
|
-
}
|
|
101530
|
-
return { ok: true, after: diskBefore.replace(oldS, newS) };
|
|
101531
|
-
}
|
|
101532
|
-
if (name === "MultiEdit" || name === "multi_edit" || name === "multiEdit") {
|
|
101533
|
-
const edits = toolInput.edits || toolInput.Edits;
|
|
101534
|
-
if (!Array.isArray(edits) || edits.length === 0) {
|
|
101535
|
-
return { ok: false, reason: "MultiEdit tool_input.edits missing or empty" };
|
|
101536
|
-
}
|
|
101537
|
-
let cur = diskBefore;
|
|
101538
|
-
for (let i = 0; i < edits.length; i++) {
|
|
101539
|
-
const e = edits[i] || {};
|
|
101540
|
-
const oldS = e.old_string != null ? e.old_string : e.oldString;
|
|
101541
|
-
const newS = e.new_string != null ? e.new_string : e.newString;
|
|
101542
|
-
if (typeof oldS !== "string" || typeof newS !== "string") {
|
|
101543
|
-
return { ok: false, reason: `MultiEdit edits[${i}] missing old_string/new_string` };
|
|
101544
|
-
}
|
|
101545
|
-
if (!cur.includes(oldS)) {
|
|
101546
|
-
return { ok: false, reason: `MultiEdit edits[${i}] old_string not found in content` };
|
|
101547
|
-
}
|
|
101548
|
-
cur = cur.replace(oldS, newS);
|
|
101817
|
+
);
|
|
101818
|
+
lines.push("-".repeat(90));
|
|
101819
|
+
for (const row of report.change_sets) {
|
|
101820
|
+
const ref = (row.ref || "").slice(0, 10);
|
|
101821
|
+
const pth = (row.path || "").slice(0, 26);
|
|
101822
|
+
const act = row.execution_action || (row.error ? "ERROR" : "\u2014");
|
|
101823
|
+
const risk = row.risk_score != null ? String(row.risk_score) : "\u2014";
|
|
101824
|
+
const dets = (row.detectors_fired || []).map((d) => d.id).join(",") || "\u2014";
|
|
101825
|
+
lines.push(pad(ref, 12) + pad(pth, 28) + pad(act, 18) + pad(risk, 6) + dets.slice(0, 40));
|
|
101826
|
+
}
|
|
101827
|
+
lines.push("");
|
|
101828
|
+
lines.push(chalk.bold("Per-detector aggregate (default-flip evidence \u2014 not a recommendation)"));
|
|
101829
|
+
if (!report.per_detector || report.per_detector.length === 0) {
|
|
101830
|
+
lines.push(chalk.dim("(no detectors fired on any change set)"));
|
|
101831
|
+
} else {
|
|
101832
|
+
lines.push(pad("DETECTOR", 36) + pad("SEVERITY", 12) + "FIRE_COUNT");
|
|
101833
|
+
lines.push("-".repeat(60));
|
|
101834
|
+
for (const d of report.per_detector) {
|
|
101835
|
+
lines.push(pad(d.id, 36) + pad(d.severity, 12) + String(d.fire_count));
|
|
101549
101836
|
}
|
|
101550
|
-
return { ok: true, after: cur };
|
|
101551
101837
|
}
|
|
101552
|
-
|
|
101553
|
-
|
|
101554
|
-
|
|
101555
|
-
|
|
101556
|
-
|
|
101557
|
-
const ver = d.decision_spec_version;
|
|
101558
|
-
return typeof ver === "string" && ver.startsWith("2.");
|
|
101838
|
+
lines.push("");
|
|
101839
|
+
lines.push(chalk.dim("High fire_count \u2192 candidate false-positive noise (not safe to default-block without review)."));
|
|
101840
|
+
lines.push(chalk.dim("Rare + CRITICAL/HIGH \u2192 safer default-block candidates. Human decides; no auto-flip."));
|
|
101841
|
+
lines.push("");
|
|
101842
|
+
return lines.join("\n");
|
|
101559
101843
|
}
|
|
101560
|
-
function
|
|
101561
|
-
|
|
101844
|
+
function pad(s, n) {
|
|
101845
|
+
const t = String(s || "");
|
|
101846
|
+
if (t.length >= n) return `${t.slice(0, n - 1)} `;
|
|
101847
|
+
return t + " ".repeat(n - t.length);
|
|
101562
101848
|
}
|
|
101563
|
-
function
|
|
101564
|
-
const
|
|
101565
|
-
|
|
101566
|
-
const
|
|
101567
|
-
|
|
101568
|
-
|
|
101569
|
-
|
|
101570
|
-
|
|
101849
|
+
async function runAdopt(options = {}, deps = {}) {
|
|
101850
|
+
const log = deps.log || console.log.bind(console);
|
|
101851
|
+
const logErr = deps.logErr || console.error.bind(console);
|
|
101852
|
+
const load = deps.loadCore || loadCore;
|
|
101853
|
+
const collected = collectChangeSetsFromGit(options, deps);
|
|
101854
|
+
if (!collected.ok) {
|
|
101855
|
+
logErr(chalk.red(`CodeRifts adopt: ${collected.code} \u2014 ${collected.message}`));
|
|
101856
|
+
return { ok: false, exitCode: 1, code: collected.code, message: collected.message };
|
|
101571
101857
|
}
|
|
101572
|
-
|
|
101573
|
-
|
|
101574
|
-
|
|
101575
|
-
|
|
101576
|
-
|
|
101577
|
-
|
|
101578
|
-
executionAction: null
|
|
101579
|
-
};
|
|
101858
|
+
let core;
|
|
101859
|
+
try {
|
|
101860
|
+
core = load();
|
|
101861
|
+
} catch (err) {
|
|
101862
|
+
logErr(chalk.red(`CodeRifts adopt: ${err.message}`));
|
|
101863
|
+
return { ok: false, exitCode: 1, code: "CORE_LOAD_ERROR", message: err.message };
|
|
101580
101864
|
}
|
|
101581
|
-
|
|
101582
|
-
|
|
101583
|
-
|
|
101584
|
-
|
|
101585
|
-
|
|
101586
|
-
|
|
101587
|
-
|
|
101588
|
-
|
|
101589
|
-
|
|
101590
|
-
|
|
101591
|
-
} else {
|
|
101592
|
-
severity = "REQUIRE_APPROVAL";
|
|
101593
|
-
}
|
|
101594
|
-
} else if (allowLegacyDecisionMap(d, dr)) {
|
|
101595
|
-
const od = d.omega_decision || d.decision;
|
|
101596
|
-
if (od == null || od === "") {
|
|
101597
|
-
severity = "INDETERMINATE";
|
|
101598
|
-
} else if (od === "BLOCK") severity = "BLOCK";
|
|
101599
|
-
else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
|
|
101600
|
-
else if (od === "WARN") severity = "WARN";
|
|
101601
|
-
else severity = "ALLOW";
|
|
101865
|
+
const runCore = () => core.buildCounterfactualReport(collected.changeSets, deps.config || {});
|
|
101866
|
+
const report = options.json ? await withJsonQuietAnalyzerLogs(runCore) : await runCore();
|
|
101867
|
+
if (options.json) {
|
|
101868
|
+
log(JSON.stringify({
|
|
101869
|
+
...report,
|
|
101870
|
+
meta: {
|
|
101871
|
+
commit_count: collected.commit_count,
|
|
101872
|
+
change_set_count: collected.changeSets.length
|
|
101873
|
+
}
|
|
101874
|
+
}, null, 2));
|
|
101602
101875
|
} else {
|
|
101603
|
-
|
|
101876
|
+
log(renderHuman(report, { commit_count: collected.commit_count }));
|
|
101604
101877
|
}
|
|
101605
|
-
const decision = dr && dr.decision || d.decision || d.omega_decision || null;
|
|
101606
|
-
const decisionId = dr && dr.decision_id || d.decision_id || null;
|
|
101607
101878
|
return {
|
|
101608
|
-
|
|
101609
|
-
|
|
101610
|
-
|
|
101611
|
-
|
|
101879
|
+
ok: true,
|
|
101880
|
+
exitCode: 0,
|
|
101881
|
+
report,
|
|
101882
|
+
meta: { commit_count: collected.commit_count }
|
|
101612
101883
|
};
|
|
101613
101884
|
}
|
|
101614
|
-
|
|
101615
|
-
|
|
101616
|
-
|
|
101617
|
-
|
|
101618
|
-
|
|
101619
|
-
|
|
101620
|
-
|
|
101621
|
-
|
|
101622
|
-
|
|
101623
|
-
|
|
101624
|
-
|
|
101625
|
-
|
|
101626
|
-
|
|
101627
|
-
|
|
101628
|
-
|
|
101629
|
-
|
|
101630
|
-
|
|
101631
|
-
|
|
101632
|
-
|
|
101633
|
-
|
|
101634
|
-
|
|
101635
|
-
|
|
101636
|
-
|
|
101637
|
-
|
|
101638
|
-
|
|
101639
|
-
|
|
101640
|
-
|
|
101641
|
-
|
|
101642
|
-
|
|
101643
|
-
|
|
101644
|
-
|
|
101645
|
-
|
|
101646
|
-
|
|
101647
|
-
|
|
101648
|
-
|
|
101649
|
-
|
|
101650
|
-
|
|
101651
|
-
|
|
101885
|
+
module2.exports = {
|
|
101886
|
+
runAdopt,
|
|
101887
|
+
collectChangeSetsFromGit,
|
|
101888
|
+
isContractPath,
|
|
101889
|
+
withJsonQuietAnalyzerLogs,
|
|
101890
|
+
renderHuman,
|
|
101891
|
+
defaultGit,
|
|
101892
|
+
SOURCE_CODE_EXT_RE
|
|
101893
|
+
};
|
|
101894
|
+
}
|
|
101895
|
+
});
|
|
101896
|
+
|
|
101897
|
+
// src/commands/outcome.js
|
|
101898
|
+
var require_outcome = __commonJS({
|
|
101899
|
+
"src/commands/outcome.js"(exports2, module2) {
|
|
101900
|
+
"use strict";
|
|
101901
|
+
var chalk = require_source();
|
|
101902
|
+
var { getApiKey } = require_config();
|
|
101903
|
+
var { cloudPostOutcome } = require_cloud();
|
|
101904
|
+
if (process.env.NO_COLOR) chalk.level = 0;
|
|
101905
|
+
var OUTCOME_KINDS = Object.freeze([
|
|
101906
|
+
"deploy_succeeded",
|
|
101907
|
+
"deploy_failed",
|
|
101908
|
+
"rolled_back",
|
|
101909
|
+
"consumer_break_reported",
|
|
101910
|
+
"remediation_verified_working",
|
|
101911
|
+
"false_positive_reported",
|
|
101912
|
+
"other_reported"
|
|
101913
|
+
]);
|
|
101914
|
+
var OUTCOME_KIND_SET = new Set(OUTCOME_KINDS);
|
|
101915
|
+
var USAGE = [
|
|
101916
|
+
"Usage: coderifts outcome <kind> --decision <decision_id> [--observed-at <ISO>] [--details <json>] [--json]",
|
|
101917
|
+
"",
|
|
101918
|
+
"Report a post-hoc observed outcome for a past decision_id to the CodeRifts cloud API.",
|
|
101919
|
+
"This records the CALLER'S assertion (e.g. your deploy job knows success/failure) \u2014",
|
|
101920
|
+
"the command does NOT itself verify that a deploy succeeded or failed.",
|
|
101921
|
+
"",
|
|
101922
|
+
"kind (required, closed set):",
|
|
101923
|
+
` ${OUTCOME_KINDS.join(", ")}`,
|
|
101924
|
+
"",
|
|
101925
|
+
"Requires a cloud API key (coderifts login or CODERIFTS_API_KEY).",
|
|
101926
|
+
"POST /api/v1/outcomes \u2014 source is always reported (server-side); reporter is derived from the key."
|
|
101927
|
+
].join("\n");
|
|
101928
|
+
function isValidOutcomeKind(kind) {
|
|
101929
|
+
return typeof kind === "string" && OUTCOME_KIND_SET.has(kind);
|
|
101652
101930
|
}
|
|
101653
|
-
|
|
101654
|
-
|
|
101655
|
-
const
|
|
101656
|
-
|
|
101657
|
-
|
|
101658
|
-
|
|
101659
|
-
|
|
101660
|
-
|
|
101661
|
-
|
|
101662
|
-
|
|
101663
|
-
|
|
101664
|
-
|
|
101665
|
-
const cwd = deps.cwd || process.cwd();
|
|
101666
|
-
const env = deps.env || process.env;
|
|
101667
|
-
const strict = isStrictMode(env);
|
|
101668
|
-
const advisory = isAdvisoryMode(env);
|
|
101669
|
-
const sessionId = hookSessionId(env);
|
|
101670
|
-
const logEv = (partial) => {
|
|
101671
|
-
appendGuardEventLog(env, {
|
|
101672
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
101673
|
-
sessionId,
|
|
101674
|
-
trigger_source: HOOK_TRIGGER_SOURCE,
|
|
101675
|
-
...partial
|
|
101676
|
-
}, { errLog, appendFileSync: deps.appendFileSync });
|
|
101677
|
-
};
|
|
101678
|
-
let tool = null;
|
|
101679
|
-
let filePathKnown = null;
|
|
101680
|
-
const emitTerminal = (r) => {
|
|
101681
|
-
const extra = {
|
|
101682
|
-
tool: tool || void 0,
|
|
101683
|
-
path: filePathKnown || void 0,
|
|
101684
|
-
decision: r.decision != null ? r.decision : void 0,
|
|
101685
|
-
decisionId: r.decisionId != null ? r.decisionId : void 0
|
|
101686
|
-
};
|
|
101687
|
-
if (r.exitCode === 2) {
|
|
101688
|
-
const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
|
|
101689
|
-
logEv({
|
|
101690
|
-
type: "hook_blocked",
|
|
101691
|
-
exit: 2,
|
|
101692
|
-
...extra,
|
|
101693
|
-
reasons,
|
|
101694
|
-
cause: r.reason || r.site
|
|
101695
|
-
});
|
|
101696
|
-
} else if (r.exitCode === 0 && r.site && advisory && !strict) {
|
|
101697
|
-
logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
|
|
101698
|
-
} else {
|
|
101699
|
-
logEv({ type: "hook_continue", exit: 0, ...extra, cause: r.reason });
|
|
101931
|
+
function isValidObservedAt(iso) {
|
|
101932
|
+
if (typeof iso !== "string" || !iso.trim()) return false;
|
|
101933
|
+
const t = Date.parse(iso);
|
|
101934
|
+
return Number.isFinite(t);
|
|
101935
|
+
}
|
|
101936
|
+
function parseDetails(raw) {
|
|
101937
|
+
if (raw == null || raw === "") return { ok: true, details: null };
|
|
101938
|
+
if (typeof raw !== "string") return { ok: false, error: "--details must be a JSON string" };
|
|
101939
|
+
try {
|
|
101940
|
+
const v = JSON.parse(raw);
|
|
101941
|
+
if (v !== null && typeof v !== "object") {
|
|
101942
|
+
return { ok: false, error: "--details must be a JSON object or array" };
|
|
101700
101943
|
}
|
|
101701
|
-
return
|
|
101702
|
-
}
|
|
101703
|
-
|
|
101704
|
-
if (!apiKey) {
|
|
101705
|
-
logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
|
|
101706
|
-
return emitTerminal(failClosedOrAdvisory({
|
|
101707
|
-
advisory,
|
|
101708
|
-
strict,
|
|
101709
|
-
site: "missing_api_key",
|
|
101710
|
-
why: "no API key",
|
|
101711
|
-
softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
|
|
101712
|
-
errLog
|
|
101713
|
-
}));
|
|
101944
|
+
return { ok: true, details: v };
|
|
101945
|
+
} catch (e) {
|
|
101946
|
+
return { ok: false, error: `--details is not valid JSON: ${e && e.message || "parse error"}` };
|
|
101714
101947
|
}
|
|
101715
|
-
|
|
101716
|
-
|
|
101717
|
-
const
|
|
101718
|
-
|
|
101719
|
-
|
|
101720
|
-
|
|
101721
|
-
|
|
101722
|
-
|
|
101723
|
-
errLog(
|
|
101724
|
-
|
|
101948
|
+
}
|
|
101949
|
+
async function runOutcome(kind, options = {}, deps = {}) {
|
|
101950
|
+
const getKey = deps.getApiKey || getApiKey;
|
|
101951
|
+
const postOutcome = deps.cloudPostOutcome || cloudPostOutcome;
|
|
101952
|
+
const log = deps.log || console.log;
|
|
101953
|
+
const errLog = deps.errLog || console.error;
|
|
101954
|
+
const nowIso = deps.nowIso || (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
101955
|
+
if (kind == null || String(kind).trim() === "") {
|
|
101956
|
+
errLog(chalk.red("Error: missing outcome kind"));
|
|
101957
|
+
errLog(USAGE);
|
|
101958
|
+
return { exitCode: 1, error: "missing_kind" };
|
|
101725
101959
|
}
|
|
101726
|
-
const
|
|
101727
|
-
|
|
101728
|
-
|
|
101729
|
-
|
|
101730
|
-
|
|
101731
|
-
errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
|
|
101732
|
-
return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
|
|
101733
|
-
}
|
|
101734
|
-
errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
|
|
101735
|
-
return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
|
|
101960
|
+
const outcomeKind = String(kind).trim();
|
|
101961
|
+
if (!isValidOutcomeKind(outcomeKind)) {
|
|
101962
|
+
errLog(chalk.red(`Error: invalid outcome kind '${outcomeKind}'`));
|
|
101963
|
+
errLog(chalk.dim(` Valid kinds: ${OUTCOME_KINDS.join(", ")}`));
|
|
101964
|
+
return { exitCode: 1, error: "invalid_kind" };
|
|
101736
101965
|
}
|
|
101737
|
-
|
|
101738
|
-
|
|
101739
|
-
|
|
101740
|
-
|
|
101741
|
-
return
|
|
101966
|
+
const decisionId = options.decision != null ? String(options.decision).trim() : "";
|
|
101967
|
+
if (!decisionId) {
|
|
101968
|
+
errLog(chalk.red("Error: --decision <decision_id> is required"));
|
|
101969
|
+
errLog(USAGE);
|
|
101970
|
+
return { exitCode: 1, error: "missing_decision" };
|
|
101742
101971
|
}
|
|
101743
|
-
|
|
101744
|
-
|
|
101745
|
-
|
|
101746
|
-
|
|
101747
|
-
|
|
101748
|
-
|
|
101749
|
-
logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
|
|
101750
|
-
return emitTerminal(failClosedOrAdvisory({
|
|
101751
|
-
advisory,
|
|
101752
|
-
strict,
|
|
101753
|
-
site: "disk_unreadable",
|
|
101754
|
-
why: `cannot read contract file ${absPath}`,
|
|
101755
|
-
softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
|
|
101756
|
-
errLog
|
|
101757
|
-
}));
|
|
101972
|
+
let observedAt;
|
|
101973
|
+
if (options.observedAt != null && String(options.observedAt).trim() !== "") {
|
|
101974
|
+
observedAt = String(options.observedAt).trim();
|
|
101975
|
+
if (!isValidObservedAt(observedAt)) {
|
|
101976
|
+
errLog(chalk.red("Error: --observed-at must be a valid ISO-8601 timestamp"));
|
|
101977
|
+
return { exitCode: 1, error: "invalid_observed_at" };
|
|
101758
101978
|
}
|
|
101979
|
+
} else {
|
|
101980
|
+
observedAt = nowIso();
|
|
101759
101981
|
}
|
|
101760
|
-
const
|
|
101761
|
-
if (!
|
|
101762
|
-
|
|
101763
|
-
return
|
|
101764
|
-
advisory,
|
|
101765
|
-
strict,
|
|
101766
|
-
site: "edit_apply_failed",
|
|
101767
|
-
why: derived.reason,
|
|
101768
|
-
softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
|
|
101769
|
-
errLog
|
|
101770
|
-
}));
|
|
101982
|
+
const det = parseDetails(options.details);
|
|
101983
|
+
if (!det.ok) {
|
|
101984
|
+
errLog(chalk.red(`Error: ${det.error}`));
|
|
101985
|
+
return { exitCode: 1, error: "invalid_details" };
|
|
101771
101986
|
}
|
|
101772
|
-
|
|
101773
|
-
|
|
101774
|
-
|
|
101987
|
+
const apiKey = getKey();
|
|
101988
|
+
if (!apiKey) {
|
|
101989
|
+
errLog(chalk.red("Error: no API key. Run `coderifts login` or set CODERIFTS_API_KEY."));
|
|
101990
|
+
return { exitCode: 1, error: "missing_api_key" };
|
|
101775
101991
|
}
|
|
101776
|
-
|
|
101777
|
-
|
|
101992
|
+
const body = {
|
|
101993
|
+
decision_id: decisionId,
|
|
101994
|
+
outcome_kind: outcomeKind,
|
|
101995
|
+
observed_at: observedAt
|
|
101996
|
+
};
|
|
101997
|
+
if (det.details != null) body.details = det.details;
|
|
101998
|
+
let response;
|
|
101778
101999
|
try {
|
|
101779
|
-
|
|
101780
|
-
operation: "merge",
|
|
101781
|
-
environment: "staging"
|
|
101782
|
-
});
|
|
102000
|
+
response = await postOutcome(apiKey, body);
|
|
101783
102001
|
} catch (e) {
|
|
101784
102002
|
const msg = e && e.message ? String(e.message) : "request failed";
|
|
101785
|
-
|
|
101786
|
-
|
|
101787
|
-
|
|
101788
|
-
|
|
101789
|
-
site: "api_unreachable",
|
|
101790
|
-
why: `API unreachable: ${msg}`,
|
|
101791
|
-
softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
|
|
101792
|
-
errLog
|
|
101793
|
-
}));
|
|
101794
|
-
}
|
|
101795
|
-
const mapped = mapDecisionSeverity(result);
|
|
101796
|
-
const fingerprint = extractFingerprint(result);
|
|
101797
|
-
const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
|
|
101798
|
-
const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
|
|
101799
|
-
const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
|
|
101800
|
-
const actionForLog = mapped.executionAction || (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN" || mapped.severity === "INDETERMINATE" ? "STOP" : mapped.severity === "REQUIRE_APPROVAL" ? "REQUEST_APPROVAL" : mapped.severity === "MONITOR" || mapped.severity === "WARN" ? "CONTINUE_WITH_MONITORING" : "CONTINUE");
|
|
101801
|
-
logEv({
|
|
101802
|
-
type: "preflight_result",
|
|
101803
|
-
action: actionForLog,
|
|
101804
|
-
decisionId: mapped.decisionId || void 0,
|
|
101805
|
-
fingerprint,
|
|
101806
|
-
tool,
|
|
101807
|
-
path: filePath
|
|
101808
|
-
});
|
|
101809
|
-
if (mapped.severity === "INDETERMINATE") {
|
|
101810
|
-
errLog(
|
|
101811
|
-
"CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
|
|
101812
|
-
);
|
|
101813
|
-
return emitTerminal({
|
|
101814
|
-
exitCode: 2,
|
|
101815
|
-
reason: "indeterminate",
|
|
101816
|
-
severity: "INDETERMINATE",
|
|
101817
|
-
decision: mapped.decision,
|
|
101818
|
-
decisionId: mapped.decisionId
|
|
101819
|
-
});
|
|
101820
|
-
}
|
|
101821
|
-
if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
|
|
101822
|
-
const why = renderDecisionWhy(result);
|
|
101823
|
-
errLog(
|
|
101824
|
-
[
|
|
101825
|
-
`CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
|
|
101826
|
-
...why.lines
|
|
101827
|
-
].join("\n")
|
|
101828
|
-
);
|
|
101829
|
-
return emitTerminal({
|
|
101830
|
-
exitCode: 2,
|
|
101831
|
-
reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
|
|
101832
|
-
severity: mapped.severity,
|
|
101833
|
-
decision: mapped.decision,
|
|
101834
|
-
decisionId: mapped.decisionId,
|
|
101835
|
-
reasons: why.reasonCodes
|
|
101836
|
-
});
|
|
101837
|
-
}
|
|
101838
|
-
if (mapped.severity === "REQUIRE_APPROVAL") {
|
|
101839
|
-
const why = renderDecisionWhy(result);
|
|
101840
|
-
errLog(
|
|
101841
|
-
[
|
|
101842
|
-
`CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
|
|
101843
|
-
...why.lines
|
|
101844
|
-
].join("\n")
|
|
101845
|
-
);
|
|
101846
|
-
return emitTerminal({
|
|
101847
|
-
exitCode: 2,
|
|
101848
|
-
reason: "approval_required",
|
|
101849
|
-
severity: mapped.severity,
|
|
101850
|
-
decision: mapped.decision,
|
|
101851
|
-
decisionId: mapped.decisionId,
|
|
101852
|
-
reasons: why.reasonCodes
|
|
101853
|
-
});
|
|
102003
|
+
errLog(chalk.red(`Error: ${msg}`));
|
|
102004
|
+
if (e && e.statusCode) errLog(chalk.dim(` (HTTP ${e.statusCode})`));
|
|
102005
|
+
if (e && e.code) errLog(chalk.dim(` (${e.code})`));
|
|
102006
|
+
return { exitCode: 1, error: msg, body };
|
|
101854
102007
|
}
|
|
101855
|
-
if (
|
|
101856
|
-
|
|
101857
|
-
|
|
101858
|
-
|
|
101859
|
-
|
|
101860
|
-
|
|
101861
|
-
|
|
101862
|
-
|
|
101863
|
-
|
|
101864
|
-
|
|
101865
|
-
|
|
101866
|
-
decisionId: mapped.decisionId
|
|
101867
|
-
});
|
|
101868
|
-
}
|
|
101869
|
-
return emitTerminal({
|
|
101870
|
-
exitCode: 0,
|
|
101871
|
-
reason: "allow",
|
|
101872
|
-
severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
|
|
101873
|
-
decision: mapped.decision,
|
|
101874
|
-
decisionId: mapped.decisionId
|
|
101875
|
-
});
|
|
102008
|
+
if (options.json) {
|
|
102009
|
+
log(JSON.stringify(response, null, 2));
|
|
102010
|
+
} else {
|
|
102011
|
+
const o = response && response.outcome ? response.outcome : response;
|
|
102012
|
+
log(chalk.bold("CodeRifts outcome recorded"));
|
|
102013
|
+
log(` kind: ${outcomeKind}`);
|
|
102014
|
+
log(` decision_id: ${decisionId}`);
|
|
102015
|
+
log(` observed_at: ${observedAt}`);
|
|
102016
|
+
if (o && o.id) log(` id: ${o.id}`);
|
|
102017
|
+
if (response && response.source) log(` source: ${response.source}`);
|
|
102018
|
+
log(chalk.dim(" Caller assertion only \u2014 this command did not verify the deploy."));
|
|
101876
102019
|
}
|
|
101877
|
-
return
|
|
101878
|
-
exitCode: 0,
|
|
101879
|
-
reason: "allow",
|
|
101880
|
-
severity: "ALLOW",
|
|
101881
|
-
decision: mapped.decision,
|
|
101882
|
-
decisionId: mapped.decisionId
|
|
101883
|
-
});
|
|
102020
|
+
return { exitCode: 0, response, body };
|
|
101884
102021
|
}
|
|
101885
102022
|
module2.exports = {
|
|
101886
|
-
|
|
101887
|
-
|
|
101888
|
-
|
|
101889
|
-
|
|
101890
|
-
|
|
101891
|
-
|
|
101892
|
-
resolveApiKey,
|
|
101893
|
-
resolveSpecPath,
|
|
101894
|
-
readGitConfig,
|
|
101895
|
-
isEnvFlag,
|
|
101896
|
-
isStrictMode,
|
|
101897
|
-
isAdvisoryMode,
|
|
101898
|
-
isMonitoringSinkWired,
|
|
101899
|
-
failClosedOrAdvisory,
|
|
101900
|
-
softOrStrictBlock,
|
|
101901
|
-
DEFAULT_SPEC_PATH,
|
|
101902
|
-
CLOSED_ACTIONS,
|
|
101903
|
-
USAGE,
|
|
101904
|
-
appendGuardEventLog,
|
|
101905
|
-
extractFingerprint,
|
|
101906
|
-
HOOK_TRIGGER_SOURCE
|
|
102023
|
+
runOutcome,
|
|
102024
|
+
isValidOutcomeKind,
|
|
102025
|
+
isValidObservedAt,
|
|
102026
|
+
parseDetails,
|
|
102027
|
+
OUTCOME_KINDS,
|
|
102028
|
+
USAGE
|
|
101907
102029
|
};
|
|
101908
102030
|
}
|
|
101909
102031
|
});
|
|
@@ -103796,7 +103918,7 @@ program.command("diff <old-spec> <new-spec>").description("Compare two OpenAPI s
|
|
|
103796
103918
|
const { diff } = require_diff();
|
|
103797
103919
|
await diff(oldSpec, newSpec, options);
|
|
103798
103920
|
});
|
|
103799
|
-
program.command("deploy-gate").description("Gate a deploy on the current { environment, artifact } using a preflight receipt (
|
|
103921
|
+
program.command("deploy-gate").description("Gate a deploy on the current { environment, artifact } using a preflight receipt (fail-closed by default; CODERIFTS_DEPLOY_ADVISORY=1 to soften)").option("--env <environment>", "Target environment (e.g. production, staging)").option("--artifact <artifact_id>", "Immutable artifact identity being deployed (content digest or commit SHA)").option("--receipt <file>", "Path to the deploy-scoped receipt JSON produced by preflight").option("--json", "Output the binding result as JSON").option("--enforce", "Accepted silently (exit is fail-closed by default). Still attests ENFORCING for inescapable_deploy.").action(async (options) => {
|
|
103800
103922
|
const { runDeployGate } = require_deploy_gate2();
|
|
103801
103923
|
await runDeployGate(options);
|
|
103802
103924
|
});
|