nexus-agents 8.8.2 → 8.9.1
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/dist/{ast-rule-runner-WANOR3MX.js → ast-rule-runner-BEUFFQWL.js} +3 -3
- package/dist/{child-mcp-config-N4HLLCEH.js → child-mcp-config-QFPSL7Y2.js} +2 -2
- package/dist/{chunk-VADACTVB.js → chunk-2RZWKIDU.js} +2 -2
- package/dist/{chunk-QCSXKQTK.js → chunk-36BA6D6J.js} +2 -2
- package/dist/{chunk-BYUVLVN7.js → chunk-3W4L63ML.js} +1 -1
- package/dist/chunk-3W4L63ML.js.map +1 -0
- package/dist/{chunk-3YKFLM2B.js → chunk-46YGVSBD.js} +2 -2
- package/dist/chunk-46YGVSBD.js.map +1 -0
- package/dist/{chunk-7J7UYJXC.js → chunk-4GSFZS6M.js} +7 -7
- package/dist/{chunk-ERL3XHQO.js → chunk-AOBIUJX5.js} +3 -3
- package/dist/{chunk-276GRITW.js → chunk-BTPPGHG3.js} +4 -4
- package/dist/{chunk-II5NJRKH.js → chunk-CMQO6WEQ.js} +2 -2
- package/dist/{chunk-WXU4Y5T3.js → chunk-D3TZZO6L.js} +2 -2
- package/dist/chunk-DJFYMDMD.js +7 -0
- package/dist/{chunk-K2PVLCHR.js → chunk-DLVIES6W.js} +2 -2
- package/dist/{chunk-3XQK4MEF.js → chunk-FDZ3SLMV.js} +2 -2
- package/dist/{chunk-W6SM3MU5.js → chunk-FMKEGR6N.js} +3 -3
- package/dist/{chunk-F2GERNMF.js → chunk-FOYSPWOA.js} +2 -2
- package/dist/{chunk-MXQG3A5W.js → chunk-FPHFCZ2E.js} +976 -132
- package/dist/chunk-FPHFCZ2E.js.map +1 -0
- package/dist/{chunk-TZWJ7QLW.js → chunk-HQ2A4DGU.js} +2 -2
- package/dist/{chunk-3CLKYFE3.js → chunk-I3J7WUMV.js} +2 -2
- package/dist/{chunk-64VMTY3M.js → chunk-K2GDWPVK.js} +2 -2
- package/dist/{chunk-QY3HOLGD.js → chunk-KE5IGD6G.js} +7 -7
- package/dist/{chunk-N347WXTT.js → chunk-M33EQB7P.js} +2 -2
- package/dist/chunk-M33EQB7P.js.map +1 -0
- package/dist/{chunk-CQCVEC7P.js → chunk-MYERGZK3.js} +945 -1111
- package/dist/chunk-MYERGZK3.js.map +1 -0
- package/dist/{chunk-SNSPYZQT.js → chunk-MZUUIMRQ.js} +5 -5
- package/dist/{chunk-7QQ5F7T5.js → chunk-OJSG734P.js} +8 -8
- package/dist/{chunk-YQUM3UWK.js → chunk-P2DV7PDG.js} +4 -4
- package/dist/{chunk-SQCFKQUR.js → chunk-P64GQUQW.js} +9 -9
- package/dist/{chunk-GXLI2E4U.js → chunk-PCPQ75EC.js} +4 -4
- package/dist/{chunk-Q5V3KTUL.js → chunk-Q55JVMUD.js} +2 -2
- package/dist/{chunk-EALWAQAQ.js → chunk-S2B4UBHK.js} +5 -5
- package/dist/{chunk-NR62IQDC.js → chunk-SGOBMAZK.js} +4 -4
- package/dist/{chunk-NR62IQDC.js.map → chunk-SGOBMAZK.js.map} +1 -1
- package/dist/{chunk-FBWOWMEB.js → chunk-UXCE2OBL.js} +21 -19
- package/dist/chunk-UXCE2OBL.js.map +1 -0
- package/dist/{chunk-7AU5WLAJ.js → chunk-V7PXKAZD.js} +2 -2
- package/dist/{chunk-FKKVZRSY.js → chunk-VQRXDFVF.js} +2 -2
- package/dist/{chunk-6HETGJSB.js → chunk-WANBZFP2.js} +2 -2
- package/dist/{chunk-ZGHJMFQ6.js → chunk-WFZARRYN.js} +3 -3
- package/dist/{chunk-AOS7F4QR.js → chunk-WKYDGROH.js} +2 -2
- package/dist/{cli-circuit-breaker-AAHYLFXZ.js → cli-circuit-breaker-BOZNYIZE.js} +3 -3
- package/dist/cli.js +82 -64
- package/dist/cli.js.map +1 -1
- package/dist/{composite-router-VAVDLJGT.js → composite-router-NG7BLVVR.js} +2 -2
- package/dist/{consensus-vote-L4CN33XE.js → consensus-vote-3FCOFDON.js} +17 -17
- package/dist/{context-retriever-MYKBBDAN.js → context-retriever-QKMSSV6W.js} +8 -8
- package/dist/{doctor-deep-AKOPPCXB.js → doctor-deep-CPMR6TDF.js} +3 -3
- package/dist/{doctor-live-3PVIYQST.js → doctor-live-GWE5IA7D.js} +9 -9
- package/dist/{expert-bridge-G3KN7QWL.js → expert-bridge-6WZ333NT.js} +4 -4
- package/dist/{factory-KVMLFEWJ.js → factory-2DD2YGBU.js} +5 -5
- package/dist/{factory-ROTP63IZ.js → factory-73WKMDVR.js} +9 -9
- package/dist/{improvement-review-WTZ4JV5V.js → improvement-review-RMDGMKIB.js} +6 -6
- package/dist/index.d.ts +158 -15
- package/dist/index.js +169 -658
- package/dist/index.js.map +1 -1
- package/dist/{init-opencode-C63LASGX.js → init-opencode-7PKVI3LD.js} +6 -6
- package/dist/issue-triage-MLBUABPA.js +17 -0
- package/dist/{pr-reviewer-helpers-4VDGMGV5.js → pr-reviewer-helpers-7DSP3YTT.js} +5 -5
- package/dist/{registry-command-IRDP2L2R.js → registry-command-YXFZ7LVE.js} +2 -2
- package/dist/{repo-security-plan-NS47HQEF.js → repo-security-plan-M36GODNF.js} +3 -3
- package/dist/{research-helpers-synthesize-5NBL637T.js → research-helpers-synthesize-7CI4MFYP.js} +4 -4
- package/dist/{routing-memory-2IELT5GT.js → routing-memory-MIK3LGDH.js} +2 -2
- package/dist/{session-memory-CH3CVLIT.js → session-memory-T2X4BM33.js} +3 -3
- package/dist/{setup-config-MKZZDUJP.js → setup-config-6JSBY6OG.js} +3 -3
- package/dist/{setup-custom-api-2I52S7XU.js → setup-custom-api-2OXG6ERI.js} +4 -4
- package/dist/setup-data-dir-CWU367E6.js +29 -0
- package/dist/{tool-memory-STS3DWOK.js → tool-memory-AL3RAR67.js} +5 -5
- package/dist/{unified-registry-NJS4ZWC3.js → unified-registry-5YUSIEML.js} +10 -10
- package/dist/{weather-report-YG4T4Q23.js → weather-report-W34L4HW5.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-3YKFLM2B.js.map +0 -1
- package/dist/chunk-BYUVLVN7.js.map +0 -1
- package/dist/chunk-CQCVEC7P.js.map +0 -1
- package/dist/chunk-FBWOWMEB.js.map +0 -1
- package/dist/chunk-H5DTHO7O.js +0 -7
- package/dist/chunk-MXQG3A5W.js.map +0 -1
- package/dist/chunk-N347WXTT.js.map +0 -1
- package/dist/issue-triage-PIE3YVCV.js +0 -17
- package/dist/setup-data-dir-NU3YBIZB.js +0 -29
- /package/dist/{ast-rule-runner-WANOR3MX.js.map → ast-rule-runner-BEUFFQWL.js.map} +0 -0
- /package/dist/{child-mcp-config-N4HLLCEH.js.map → child-mcp-config-QFPSL7Y2.js.map} +0 -0
- /package/dist/{chunk-VADACTVB.js.map → chunk-2RZWKIDU.js.map} +0 -0
- /package/dist/{chunk-QCSXKQTK.js.map → chunk-36BA6D6J.js.map} +0 -0
- /package/dist/{chunk-7J7UYJXC.js.map → chunk-4GSFZS6M.js.map} +0 -0
- /package/dist/{chunk-ERL3XHQO.js.map → chunk-AOBIUJX5.js.map} +0 -0
- /package/dist/{chunk-276GRITW.js.map → chunk-BTPPGHG3.js.map} +0 -0
- /package/dist/{chunk-II5NJRKH.js.map → chunk-CMQO6WEQ.js.map} +0 -0
- /package/dist/{chunk-WXU4Y5T3.js.map → chunk-D3TZZO6L.js.map} +0 -0
- /package/dist/{chunk-H5DTHO7O.js.map → chunk-DJFYMDMD.js.map} +0 -0
- /package/dist/{chunk-K2PVLCHR.js.map → chunk-DLVIES6W.js.map} +0 -0
- /package/dist/{chunk-3XQK4MEF.js.map → chunk-FDZ3SLMV.js.map} +0 -0
- /package/dist/{chunk-W6SM3MU5.js.map → chunk-FMKEGR6N.js.map} +0 -0
- /package/dist/{chunk-F2GERNMF.js.map → chunk-FOYSPWOA.js.map} +0 -0
- /package/dist/{chunk-TZWJ7QLW.js.map → chunk-HQ2A4DGU.js.map} +0 -0
- /package/dist/{chunk-3CLKYFE3.js.map → chunk-I3J7WUMV.js.map} +0 -0
- /package/dist/{chunk-64VMTY3M.js.map → chunk-K2GDWPVK.js.map} +0 -0
- /package/dist/{chunk-QY3HOLGD.js.map → chunk-KE5IGD6G.js.map} +0 -0
- /package/dist/{chunk-SNSPYZQT.js.map → chunk-MZUUIMRQ.js.map} +0 -0
- /package/dist/{chunk-7QQ5F7T5.js.map → chunk-OJSG734P.js.map} +0 -0
- /package/dist/{chunk-YQUM3UWK.js.map → chunk-P2DV7PDG.js.map} +0 -0
- /package/dist/{chunk-SQCFKQUR.js.map → chunk-P64GQUQW.js.map} +0 -0
- /package/dist/{chunk-GXLI2E4U.js.map → chunk-PCPQ75EC.js.map} +0 -0
- /package/dist/{chunk-Q5V3KTUL.js.map → chunk-Q55JVMUD.js.map} +0 -0
- /package/dist/{chunk-EALWAQAQ.js.map → chunk-S2B4UBHK.js.map} +0 -0
- /package/dist/{chunk-7AU5WLAJ.js.map → chunk-V7PXKAZD.js.map} +0 -0
- /package/dist/{chunk-FKKVZRSY.js.map → chunk-VQRXDFVF.js.map} +0 -0
- /package/dist/{chunk-6HETGJSB.js.map → chunk-WANBZFP2.js.map} +0 -0
- /package/dist/{chunk-ZGHJMFQ6.js.map → chunk-WFZARRYN.js.map} +0 -0
- /package/dist/{chunk-AOS7F4QR.js.map → chunk-WKYDGROH.js.map} +0 -0
- /package/dist/{cli-circuit-breaker-AAHYLFXZ.js.map → cli-circuit-breaker-BOZNYIZE.js.map} +0 -0
- /package/dist/{composite-router-VAVDLJGT.js.map → composite-router-NG7BLVVR.js.map} +0 -0
- /package/dist/{consensus-vote-L4CN33XE.js.map → consensus-vote-3FCOFDON.js.map} +0 -0
- /package/dist/{context-retriever-MYKBBDAN.js.map → context-retriever-QKMSSV6W.js.map} +0 -0
- /package/dist/{doctor-deep-AKOPPCXB.js.map → doctor-deep-CPMR6TDF.js.map} +0 -0
- /package/dist/{doctor-live-3PVIYQST.js.map → doctor-live-GWE5IA7D.js.map} +0 -0
- /package/dist/{expert-bridge-G3KN7QWL.js.map → expert-bridge-6WZ333NT.js.map} +0 -0
- /package/dist/{factory-KVMLFEWJ.js.map → factory-2DD2YGBU.js.map} +0 -0
- /package/dist/{factory-ROTP63IZ.js.map → factory-73WKMDVR.js.map} +0 -0
- /package/dist/{improvement-review-WTZ4JV5V.js.map → improvement-review-RMDGMKIB.js.map} +0 -0
- /package/dist/{init-opencode-C63LASGX.js.map → init-opencode-7PKVI3LD.js.map} +0 -0
- /package/dist/{issue-triage-PIE3YVCV.js.map → issue-triage-MLBUABPA.js.map} +0 -0
- /package/dist/{pr-reviewer-helpers-4VDGMGV5.js.map → pr-reviewer-helpers-7DSP3YTT.js.map} +0 -0
- /package/dist/{registry-command-IRDP2L2R.js.map → registry-command-YXFZ7LVE.js.map} +0 -0
- /package/dist/{repo-security-plan-NS47HQEF.js.map → repo-security-plan-M36GODNF.js.map} +0 -0
- /package/dist/{research-helpers-synthesize-5NBL637T.js.map → research-helpers-synthesize-7CI4MFYP.js.map} +0 -0
- /package/dist/{routing-memory-2IELT5GT.js.map → routing-memory-MIK3LGDH.js.map} +0 -0
- /package/dist/{session-memory-CH3CVLIT.js.map → session-memory-T2X4BM33.js.map} +0 -0
- /package/dist/{setup-config-MKZZDUJP.js.map → setup-config-6JSBY6OG.js.map} +0 -0
- /package/dist/{setup-custom-api-2I52S7XU.js.map → setup-custom-api-2OXG6ERI.js.map} +0 -0
- /package/dist/{setup-data-dir-NU3YBIZB.js.map → setup-data-dir-CWU367E6.js.map} +0 -0
- /package/dist/{tool-memory-STS3DWOK.js.map → tool-memory-AL3RAR67.js.map} +0 -0
- /package/dist/{unified-registry-NJS4ZWC3.js.map → unified-registry-5YUSIEML.js.map} +0 -0
- /package/dist/{weather-report-YG4T4Q23.js.map → weather-report-W34L4HW5.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GitHubProvider,
|
|
3
3
|
ScmError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-Q55JVMUD.js";
|
|
5
5
|
import {
|
|
6
6
|
ROLE_DEFAULT_TRUST,
|
|
7
7
|
ReputationCache,
|
|
@@ -9,22 +9,223 @@ import {
|
|
|
9
9
|
TrustTierSchema,
|
|
10
10
|
assessReputation,
|
|
11
11
|
gateWithReputation,
|
|
12
|
+
resolveEnvMode,
|
|
12
13
|
resolveReputationGatingMode,
|
|
13
14
|
sanitizeInput
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-M33EQB7P.js";
|
|
15
16
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
MAX_STRIPPED_ELEMENTS_PER_EVENT,
|
|
18
|
+
createAuditTrail,
|
|
19
|
+
emitPolicyEvent,
|
|
20
|
+
emitReputationEvent,
|
|
21
|
+
emitSanitizationEvent,
|
|
22
|
+
emitTrustEvent
|
|
23
|
+
} from "./chunk-46YGVSBD.js";
|
|
18
24
|
import {
|
|
19
25
|
hasToken
|
|
20
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-CMQO6WEQ.js";
|
|
21
27
|
import {
|
|
22
28
|
CLI_SUBPROCESS_TIMEOUTS,
|
|
23
29
|
createLogger,
|
|
24
30
|
err,
|
|
31
|
+
getErrorMessage,
|
|
25
32
|
getTimeProvider,
|
|
26
33
|
ok
|
|
27
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-AOBIUJX5.js";
|
|
35
|
+
|
|
36
|
+
// src/security/firewall/firewall-policy-mode.ts
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
var FirewallPolicyModeSchema = z.enum(["off", "audit", "enforce"]);
|
|
39
|
+
var DEFAULT_FIREWALL_POLICY_MODE = "off";
|
|
40
|
+
var FIREWALL_POLICY_ENV_VAR = "NEXUS_FIREWALL_POLICY";
|
|
41
|
+
function resolveFirewallPolicyMode(env = process.env, logger6) {
|
|
42
|
+
return resolveEnvMode(
|
|
43
|
+
env[FIREWALL_POLICY_ENV_VAR],
|
|
44
|
+
FirewallPolicyModeSchema,
|
|
45
|
+
DEFAULT_FIREWALL_POLICY_MODE,
|
|
46
|
+
FIREWALL_POLICY_ENV_VAR,
|
|
47
|
+
{
|
|
48
|
+
...logger6 !== void 0 ? { logger: logger6 } : {},
|
|
49
|
+
// An explicit-but-invalid value lands on `audit`, NOT on the unset
|
|
50
|
+
// default `off`. Unset means "the operator has not opted in" and `off` is
|
|
51
|
+
// right for it; a typo means "the operator opted in and mistyped how",
|
|
52
|
+
// and answering that with the most permissive mode hands them a firewall
|
|
53
|
+
// that refuses nothing while they believe it is on. `audit` reports
|
|
54
|
+
// without refusing, so it cannot break an external caller any more than
|
|
55
|
+
// `off` can, and it emits the `wouldRefuse` telemetry the rollout is
|
|
56
|
+
// supposed to be collecting — which `off` does not.
|
|
57
|
+
invalidFallback: "audit"
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/security/audit-bridge.ts
|
|
63
|
+
var logger = createLogger({ component: "SecurityAuditBridge" });
|
|
64
|
+
function systemActor(id, name) {
|
|
65
|
+
return name !== void 0 ? { type: "system", id, name } : { type: "system", id };
|
|
66
|
+
}
|
|
67
|
+
function userActor(username) {
|
|
68
|
+
return { type: "user", id: username };
|
|
69
|
+
}
|
|
70
|
+
function mapTrust(e) {
|
|
71
|
+
const severity = e.wasDowngraded ? "warning" : "info";
|
|
72
|
+
return {
|
|
73
|
+
category: "authorization",
|
|
74
|
+
severity,
|
|
75
|
+
outcome: "success",
|
|
76
|
+
action: "security.trust_classification",
|
|
77
|
+
actor: userActor(e.username),
|
|
78
|
+
description: e.reason,
|
|
79
|
+
metadata: {
|
|
80
|
+
assignedTier: e.assignedTier,
|
|
81
|
+
userRole: e.userRole,
|
|
82
|
+
// Absent when no allowlist was consulted (#4992) — never a default false.
|
|
83
|
+
...e.isAllowlisted !== void 0 ? { isAllowlisted: e.isAllowlisted } : {},
|
|
84
|
+
wasDowngraded: e.wasDowngraded,
|
|
85
|
+
component: e.component
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function pipelinePolicyMetadata(e) {
|
|
90
|
+
return {
|
|
91
|
+
...e.actionType !== void 0 ? { actionType: e.actionType } : {},
|
|
92
|
+
...e.mode !== void 0 ? { mode: e.mode } : {},
|
|
93
|
+
...e.ruleIds !== void 0 ? { ruleIds: e.ruleIds } : {},
|
|
94
|
+
...e.stageType !== void 0 ? { stageType: e.stageType } : {},
|
|
95
|
+
...e.recordKind !== void 0 ? { recordKind: e.recordKind } : {},
|
|
96
|
+
...e.violationCount !== void 0 ? { violationCount: e.violationCount } : {}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function mapPolicyGate(e) {
|
|
100
|
+
const outcome = e.allowed ? "success" : "denied";
|
|
101
|
+
return {
|
|
102
|
+
category: "authorization",
|
|
103
|
+
severity: e.allowed ? "info" : "warning",
|
|
104
|
+
outcome,
|
|
105
|
+
action: "security.policy_gate",
|
|
106
|
+
actor: systemActor(e.component),
|
|
107
|
+
policyName: "security.policy_gate",
|
|
108
|
+
policyDecision: e.allowed ? "allow" : "deny",
|
|
109
|
+
...e.violationRules.length > 0 ? { violationType: e.violationRules.join(",") } : {},
|
|
110
|
+
metadata: {
|
|
111
|
+
requiresApproval: e.requiresApproval,
|
|
112
|
+
inputTrustTier: e.inputTrustTier,
|
|
113
|
+
violationRules: e.violationRules,
|
|
114
|
+
...pipelinePolicyMetadata(e)
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function mapCorroboration(e) {
|
|
119
|
+
return {
|
|
120
|
+
category: "security",
|
|
121
|
+
severity: e.satisfied ? "info" : "warning",
|
|
122
|
+
outcome: e.satisfied ? "success" : "denied",
|
|
123
|
+
action: "security.corroboration",
|
|
124
|
+
actor: systemActor(e.component),
|
|
125
|
+
metadata: {
|
|
126
|
+
actionType: e.actionType,
|
|
127
|
+
sourceCount: e.sourceCount,
|
|
128
|
+
missingRequirements: e.missingRequirements
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function mapReputation(e) {
|
|
133
|
+
return {
|
|
134
|
+
category: "security",
|
|
135
|
+
severity: e.isSuspicious ? "warning" : "info",
|
|
136
|
+
outcome: "success",
|
|
137
|
+
action: "security.reputation",
|
|
138
|
+
actor: userActor(e.username),
|
|
139
|
+
metadata: {
|
|
140
|
+
reputationScore: e.reputationScore,
|
|
141
|
+
isSuspicious: e.isSuspicious,
|
|
142
|
+
effectiveTier: e.effectiveTier,
|
|
143
|
+
signalCount: e.signalCount,
|
|
144
|
+
component: e.component
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function mapSanitization(e) {
|
|
149
|
+
return {
|
|
150
|
+
category: "security",
|
|
151
|
+
severity: e.injectionFlagCount > 0 ? "warning" : "info",
|
|
152
|
+
outcome: "success",
|
|
153
|
+
action: "security.sanitization",
|
|
154
|
+
actor: systemActor(e.component, e.source),
|
|
155
|
+
metadata: {
|
|
156
|
+
source: e.source,
|
|
157
|
+
wasModified: e.wasModified,
|
|
158
|
+
strippedCount: e.strippedCount,
|
|
159
|
+
injectionFlagCount: e.injectionFlagCount,
|
|
160
|
+
strippedElements: e.strippedElements
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function mapGraphExecution(e) {
|
|
165
|
+
return {
|
|
166
|
+
category: "system",
|
|
167
|
+
severity: "info",
|
|
168
|
+
outcome: "success",
|
|
169
|
+
action: "security.graph_execution",
|
|
170
|
+
actor: systemActor(e.component),
|
|
171
|
+
description: e.detail,
|
|
172
|
+
metadata: {
|
|
173
|
+
graphEvent: e.graphEvent,
|
|
174
|
+
...e.nodeId !== void 0 ? { nodeId: e.nodeId } : {},
|
|
175
|
+
stepNumber: e.stepNumber
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function mapClawGuard(e) {
|
|
180
|
+
return {
|
|
181
|
+
category: "authorization",
|
|
182
|
+
severity: "warning",
|
|
183
|
+
outcome: "success",
|
|
184
|
+
action: "security.clawguard_violation",
|
|
185
|
+
actor: systemActor(e.component),
|
|
186
|
+
metadata: {
|
|
187
|
+
toolName: e.toolName,
|
|
188
|
+
warning: e.warning,
|
|
189
|
+
policySource: e.policySource,
|
|
190
|
+
mode: e.mode,
|
|
191
|
+
requestId: e.requestId
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function securityAuditEventToInput(event) {
|
|
196
|
+
switch (event.type) {
|
|
197
|
+
case "trust_classification":
|
|
198
|
+
return mapTrust(event);
|
|
199
|
+
case "policy_gate":
|
|
200
|
+
return mapPolicyGate(event);
|
|
201
|
+
case "corroboration":
|
|
202
|
+
return mapCorroboration(event);
|
|
203
|
+
case "reputation":
|
|
204
|
+
return mapReputation(event);
|
|
205
|
+
case "sanitization":
|
|
206
|
+
return mapSanitization(event);
|
|
207
|
+
case "graph_execution":
|
|
208
|
+
return mapGraphExecution(event);
|
|
209
|
+
case "clawguard_violation":
|
|
210
|
+
return mapClawGuard(event);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function createDurableAuditSink(auditLogger) {
|
|
214
|
+
return (event) => {
|
|
215
|
+
try {
|
|
216
|
+
auditLogger.log(securityAuditEventToInput(event));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.warn("Failed to mirror security event to durable audit log", {
|
|
219
|
+
type: event.type,
|
|
220
|
+
error: getErrorMessage(error)
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function createDurableAuditTrail(auditLogger) {
|
|
226
|
+
if (auditLogger === void 0) return void 0;
|
|
227
|
+
return createAuditTrail(createDurableAuditSink(auditLogger));
|
|
228
|
+
}
|
|
28
229
|
|
|
29
230
|
// src/security/trust-classifier.ts
|
|
30
231
|
function mapAuthorAssociation(association) {
|
|
@@ -109,47 +310,180 @@ function getRequiredTrustTier(actionType) {
|
|
|
109
310
|
}
|
|
110
311
|
}
|
|
111
312
|
|
|
112
|
-
// src/security/
|
|
313
|
+
// src/security/firewall/firewall-types.ts
|
|
113
314
|
import { z as z2 } from "zod";
|
|
315
|
+
var FirewallStagesSchema = z2.object({
|
|
316
|
+
sanitization: z2.boolean().default(true),
|
|
317
|
+
trustClassification: z2.boolean().default(true),
|
|
318
|
+
reputationAssessment: z2.boolean().default(false),
|
|
319
|
+
policyEnforcement: z2.boolean().default(true),
|
|
320
|
+
corroboration: z2.boolean().default(false),
|
|
321
|
+
audit: z2.boolean().default(true)
|
|
322
|
+
});
|
|
323
|
+
var FirewallConfigSchema = z2.object({
|
|
324
|
+
stages: FirewallStagesSchema.default(() => ({
|
|
325
|
+
sanitization: true,
|
|
326
|
+
trustClassification: true,
|
|
327
|
+
reputationAssessment: false,
|
|
328
|
+
policyEnforcement: true,
|
|
329
|
+
corroboration: false,
|
|
330
|
+
audit: true
|
|
331
|
+
})),
|
|
332
|
+
allowlistedMaintainers: z2.array(z2.string().min(1)).default([]),
|
|
333
|
+
maxInputLength: z2.number().int().positive().default(5e4),
|
|
334
|
+
context: z2.object({
|
|
335
|
+
hasWriteAccess: z2.boolean().default(false),
|
|
336
|
+
hasSecretAccess: z2.boolean().default(false)
|
|
337
|
+
}).default(() => ({
|
|
338
|
+
hasWriteAccess: false,
|
|
339
|
+
hasSecretAccess: false
|
|
340
|
+
}))
|
|
341
|
+
});
|
|
342
|
+
var ATLDataSchema = z2.object({
|
|
343
|
+
tier: z2.enum(["1", "2", "3", "4"]),
|
|
344
|
+
source: z2.string().min(1),
|
|
345
|
+
user: z2.string().min(1),
|
|
346
|
+
sanitized: z2.boolean(),
|
|
347
|
+
rep: z2.number().min(0).max(1).optional()
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/security/firewall/agent-trust-labels.ts
|
|
351
|
+
var ATL_PREFIX = "[ATL:";
|
|
352
|
+
var ATL_SUFFIX = "]";
|
|
353
|
+
var ATL_PATTERN = /^\[ATL:(.+)\]$/;
|
|
354
|
+
function generateATL(data) {
|
|
355
|
+
const validated = ATLDataSchema.parse(data);
|
|
356
|
+
const parts = [
|
|
357
|
+
`tier=${validated.tier}`,
|
|
358
|
+
`source=${encodeATLValue(validated.source)}`,
|
|
359
|
+
`user=${encodeATLValue(validated.user)}`,
|
|
360
|
+
`sanitized=${String(validated.sanitized)}`
|
|
361
|
+
];
|
|
362
|
+
if (validated.rep !== void 0) {
|
|
363
|
+
parts.push(`rep=${validated.rep.toFixed(2)}`);
|
|
364
|
+
}
|
|
365
|
+
return `${ATL_PREFIX}${parts.join(",")}${ATL_SUFFIX}`;
|
|
366
|
+
}
|
|
367
|
+
function parseATL(atl) {
|
|
368
|
+
const match = ATL_PATTERN.exec(atl.trim());
|
|
369
|
+
if (match === null) return void 0;
|
|
370
|
+
const body = match[1];
|
|
371
|
+
if (body === void 0) return void 0;
|
|
372
|
+
const raw = parseKeyValuePairs(body);
|
|
373
|
+
if (raw === void 0) return void 0;
|
|
374
|
+
return validateParsedATL(raw);
|
|
375
|
+
}
|
|
376
|
+
function parseKeyValuePairs(body) {
|
|
377
|
+
const pairs = body.split(",");
|
|
378
|
+
const raw = {};
|
|
379
|
+
for (const pair of pairs) {
|
|
380
|
+
const eqIndex = pair.indexOf("=");
|
|
381
|
+
if (eqIndex < 1) return void 0;
|
|
382
|
+
raw[pair.slice(0, eqIndex)] = pair.slice(eqIndex + 1);
|
|
383
|
+
}
|
|
384
|
+
return raw;
|
|
385
|
+
}
|
|
386
|
+
function validateParsedATL(raw) {
|
|
387
|
+
const tier = raw["tier"];
|
|
388
|
+
const source = raw["source"];
|
|
389
|
+
const user = raw["user"];
|
|
390
|
+
const sanitized = raw["sanitized"];
|
|
391
|
+
if (tier === void 0 || source === void 0 || user === void 0 || sanitized === void 0) {
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
const parsed = {
|
|
395
|
+
tier,
|
|
396
|
+
source: decodeATLValue(source),
|
|
397
|
+
user: decodeATLValue(user),
|
|
398
|
+
sanitized: sanitized === "true",
|
|
399
|
+
...raw["rep"] !== void 0 ? { rep: parseFloat(raw["rep"]) } : {}
|
|
400
|
+
};
|
|
401
|
+
const result = ATLDataSchema.safeParse(parsed);
|
|
402
|
+
return result.success ? result.data : void 0;
|
|
403
|
+
}
|
|
404
|
+
function encodeATLValue(value) {
|
|
405
|
+
return value.replace(/,/g, "%2C").replace(/=/g, "%3D").replace(/\]/g, "%5D");
|
|
406
|
+
}
|
|
407
|
+
function decodeATLValue(value) {
|
|
408
|
+
return value.replace(/%2C/g, ",").replace(/%3D/g, "=").replace(/%5D/g, "]");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/security/firewall/firewall-passthrough.ts
|
|
412
|
+
function createPassthroughSanitized(meta) {
|
|
413
|
+
return {
|
|
414
|
+
content: meta.content,
|
|
415
|
+
originalLength: meta.content.length,
|
|
416
|
+
trustTier: "3",
|
|
417
|
+
userRole: mapAuthorAssociation(meta.authorAssociation),
|
|
418
|
+
injectionFlags: [],
|
|
419
|
+
strippedElements: [],
|
|
420
|
+
wasModified: false,
|
|
421
|
+
sanitizedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function createPassthroughClassification(meta) {
|
|
425
|
+
return {
|
|
426
|
+
trustTier: "3",
|
|
427
|
+
userRole: mapAuthorAssociation(meta.authorAssociation),
|
|
428
|
+
isAllowlisted: false,
|
|
429
|
+
wasDowngraded: false,
|
|
430
|
+
reason: "Trust classification disabled \u2014 default Tier 3"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/security/firewall/firewall-trust-reason.ts
|
|
435
|
+
function describeGate(reason, demoted, gate) {
|
|
436
|
+
if (gate === void 0) return reason;
|
|
437
|
+
if (demoted) {
|
|
438
|
+
return `${reason}; demoted to Tier ${gate.enforcedTier} by reputation gating (${gate.mode})`;
|
|
439
|
+
}
|
|
440
|
+
if (gate.demotionSuppressed) {
|
|
441
|
+
return `${reason}; would demote to Tier ${gate.reconciledTier} (reputation gating: ${gate.mode})`;
|
|
442
|
+
}
|
|
443
|
+
return reason;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/security/policy-gate.ts
|
|
447
|
+
import { z as z4 } from "zod";
|
|
114
448
|
|
|
115
449
|
// src/security/action-schema.ts
|
|
116
|
-
import { z } from "zod";
|
|
117
|
-
var RepoFileSource =
|
|
118
|
-
type:
|
|
119
|
-
path:
|
|
120
|
-
line:
|
|
121
|
-
commit:
|
|
450
|
+
import { z as z3 } from "zod";
|
|
451
|
+
var RepoFileSource = z3.object({
|
|
452
|
+
type: z3.literal("repoFile"),
|
|
453
|
+
path: z3.string().min(1),
|
|
454
|
+
line: z3.number().int().positive().optional(),
|
|
455
|
+
commit: z3.string().regex(/^[a-f0-9]{7,40}$/).optional()
|
|
122
456
|
});
|
|
123
|
-
var IssueCommentSource =
|
|
124
|
-
type:
|
|
125
|
-
issueNumber:
|
|
126
|
-
commentId:
|
|
127
|
-
author:
|
|
457
|
+
var IssueCommentSource = z3.object({
|
|
458
|
+
type: z3.literal("issueComment"),
|
|
459
|
+
issueNumber: z3.number().int().positive(),
|
|
460
|
+
commentId: z3.number().int().positive(),
|
|
461
|
+
author: z3.string().min(1),
|
|
128
462
|
authorTrustTier: TrustTierSchema
|
|
129
463
|
});
|
|
130
|
-
var IssueBodySource =
|
|
131
|
-
type:
|
|
132
|
-
issueNumber:
|
|
133
|
-
author:
|
|
464
|
+
var IssueBodySource = z3.object({
|
|
465
|
+
type: z3.literal("issueBody"),
|
|
466
|
+
issueNumber: z3.number().int().positive(),
|
|
467
|
+
author: z3.string().min(1),
|
|
134
468
|
authorTrustTier: TrustTierSchema
|
|
135
469
|
});
|
|
136
|
-
var CIResultSource =
|
|
137
|
-
type:
|
|
138
|
-
runId:
|
|
139
|
-
status:
|
|
140
|
-
job:
|
|
470
|
+
var CIResultSource = z3.object({
|
|
471
|
+
type: z3.literal("ciResult"),
|
|
472
|
+
runId: z3.number().int().positive(),
|
|
473
|
+
status: z3.enum(["pass", "fail"]),
|
|
474
|
+
job: z3.string().min(1)
|
|
141
475
|
});
|
|
142
|
-
var PolicyDocSource =
|
|
143
|
-
type:
|
|
144
|
-
path:
|
|
145
|
-
section:
|
|
476
|
+
var PolicyDocSource = z3.object({
|
|
477
|
+
type: z3.literal("policyDoc"),
|
|
478
|
+
path: z3.string().min(1),
|
|
479
|
+
section: z3.string().min(1)
|
|
146
480
|
});
|
|
147
|
-
var MaintainerCommandSource =
|
|
148
|
-
type:
|
|
149
|
-
username:
|
|
150
|
-
commentId:
|
|
481
|
+
var MaintainerCommandSource = z3.object({
|
|
482
|
+
type: z3.literal("maintainerCommand"),
|
|
483
|
+
username: z3.string().min(1),
|
|
484
|
+
commentId: z3.number().int().positive()
|
|
151
485
|
});
|
|
152
|
-
var SourceCitationSchema =
|
|
486
|
+
var SourceCitationSchema = z3.discriminatedUnion("type", [
|
|
153
487
|
IssueBodySource,
|
|
154
488
|
RepoFileSource,
|
|
155
489
|
IssueCommentSource,
|
|
@@ -157,66 +491,66 @@ var SourceCitationSchema = z.discriminatedUnion("type", [
|
|
|
157
491
|
PolicyDocSource,
|
|
158
492
|
MaintainerCommandSource
|
|
159
493
|
]);
|
|
160
|
-
var SummarizeIssueAction =
|
|
161
|
-
type:
|
|
162
|
-
summary:
|
|
163
|
-
sources:
|
|
494
|
+
var SummarizeIssueAction = z3.object({
|
|
495
|
+
type: z3.literal("SummarizeIssue"),
|
|
496
|
+
summary: z3.string().min(10).max(2e3),
|
|
497
|
+
sources: z3.array(SourceCitationSchema).min(1).max(20)
|
|
164
498
|
});
|
|
165
|
-
var ProposeLabelsAction =
|
|
166
|
-
type:
|
|
167
|
-
labels:
|
|
168
|
-
reason:
|
|
169
|
-
sources:
|
|
499
|
+
var ProposeLabelsAction = z3.object({
|
|
500
|
+
type: z3.literal("ProposeLabels"),
|
|
501
|
+
labels: z3.array(z3.string()).min(1).max(5),
|
|
502
|
+
reason: z3.string().min(10).max(500),
|
|
503
|
+
sources: z3.array(SourceCitationSchema).min(1).max(20)
|
|
170
504
|
});
|
|
171
|
-
var DraftReplyAction =
|
|
172
|
-
type:
|
|
173
|
-
body:
|
|
174
|
-
requiresApproval:
|
|
175
|
-
sources:
|
|
505
|
+
var DraftReplyAction = z3.object({
|
|
506
|
+
type: z3.literal("DraftReply"),
|
|
507
|
+
body: z3.string().min(10).max(2e3),
|
|
508
|
+
requiresApproval: z3.literal(true),
|
|
509
|
+
sources: z3.array(SourceCitationSchema).min(1).max(20)
|
|
176
510
|
});
|
|
177
|
-
var RequestHumanApprovalAction =
|
|
178
|
-
type:
|
|
179
|
-
reason:
|
|
180
|
-
context:
|
|
511
|
+
var RequestHumanApprovalAction = z3.object({
|
|
512
|
+
type: z3.literal("RequestHumanApproval"),
|
|
513
|
+
reason: z3.string().min(10).max(500),
|
|
514
|
+
context: z3.string().min(10).max(2e3)
|
|
181
515
|
});
|
|
182
|
-
var GeneratePatchPlanAction =
|
|
183
|
-
type:
|
|
184
|
-
files:
|
|
185
|
-
|
|
186
|
-
path:
|
|
187
|
-
operation:
|
|
188
|
-
description:
|
|
516
|
+
var GeneratePatchPlanAction = z3.object({
|
|
517
|
+
type: z3.literal("GeneratePatchPlan"),
|
|
518
|
+
files: z3.array(
|
|
519
|
+
z3.object({
|
|
520
|
+
path: z3.string().min(1),
|
|
521
|
+
operation: z3.enum(["modify", "create", "delete"]),
|
|
522
|
+
description: z3.string().min(10).max(500)
|
|
189
523
|
})
|
|
190
524
|
).min(1).max(10),
|
|
191
|
-
rationale:
|
|
192
|
-
requiresApproval:
|
|
193
|
-
sources:
|
|
525
|
+
rationale: z3.string().min(10).max(1e3),
|
|
526
|
+
requiresApproval: z3.literal(true),
|
|
527
|
+
sources: z3.array(SourceCitationSchema).min(2).max(20)
|
|
194
528
|
});
|
|
195
|
-
var ClassifyIssueAction =
|
|
196
|
-
type:
|
|
197
|
-
category:
|
|
198
|
-
confidence:
|
|
199
|
-
sources:
|
|
529
|
+
var ClassifyIssueAction = z3.object({
|
|
530
|
+
type: z3.literal("ClassifyIssue"),
|
|
531
|
+
category: z3.enum(["bug", "feature", "question", "documentation", "security", "performance"]),
|
|
532
|
+
confidence: z3.number().min(0).max(1),
|
|
533
|
+
sources: z3.array(SourceCitationSchema).min(1).max(20)
|
|
200
534
|
});
|
|
201
|
-
var IdentifyDuplicatesAction =
|
|
202
|
-
type:
|
|
203
|
-
candidates:
|
|
204
|
-
similarity:
|
|
205
|
-
sources:
|
|
535
|
+
var IdentifyDuplicatesAction = z3.object({
|
|
536
|
+
type: z3.literal("IdentifyDuplicates"),
|
|
537
|
+
candidates: z3.array(z3.number().int().positive()).min(1).max(10),
|
|
538
|
+
similarity: z3.array(z3.number().min(0).max(1)),
|
|
539
|
+
sources: z3.array(SourceCitationSchema).min(1).max(20)
|
|
206
540
|
});
|
|
207
|
-
var RefuseActionAction =
|
|
208
|
-
type:
|
|
209
|
-
reason:
|
|
210
|
-
escalateTo:
|
|
541
|
+
var RefuseActionAction = z3.object({
|
|
542
|
+
type: z3.literal("RefuseAction"),
|
|
543
|
+
reason: z3.string().min(10).max(500),
|
|
544
|
+
escalateTo: z3.enum(["maintainer", "security"])
|
|
211
545
|
});
|
|
212
|
-
var HandoffMessageAction =
|
|
213
|
-
type:
|
|
214
|
-
targetCapability:
|
|
215
|
-
reason:
|
|
216
|
-
inputTrustTier:
|
|
217
|
-
sources:
|
|
546
|
+
var HandoffMessageAction = z3.object({
|
|
547
|
+
type: z3.literal("HandoffMessage"),
|
|
548
|
+
targetCapability: z3.string().min(1).max(100),
|
|
549
|
+
reason: z3.string().min(5).max(500),
|
|
550
|
+
inputTrustTier: z3.enum(["1", "2", "3", "4"]),
|
|
551
|
+
sources: z3.array(SourceCitationSchema).min(1).max(20)
|
|
218
552
|
});
|
|
219
|
-
var AgentActionSchema =
|
|
553
|
+
var AgentActionSchema = z3.discriminatedUnion("type", [
|
|
220
554
|
SummarizeIssueAction,
|
|
221
555
|
ProposeLabelsAction,
|
|
222
556
|
DraftReplyAction,
|
|
@@ -275,13 +609,13 @@ function requiresCitation(actionType) {
|
|
|
275
609
|
}
|
|
276
610
|
|
|
277
611
|
// src/security/policy-gate.ts
|
|
278
|
-
var ViolationSchema =
|
|
612
|
+
var ViolationSchema = z4.object({
|
|
279
613
|
/** Machine-readable rule identifier. */
|
|
280
|
-
rule:
|
|
614
|
+
rule: z4.string().min(1),
|
|
281
615
|
/** Human-readable description of the violation. */
|
|
282
|
-
message:
|
|
616
|
+
message: z4.string().min(1),
|
|
283
617
|
/** Severity: 'block' prevents execution, 'warn' logs only. */
|
|
284
|
-
severity:
|
|
618
|
+
severity: z4.enum(["block", "warn"])
|
|
285
619
|
});
|
|
286
620
|
function getActionSources(action) {
|
|
287
621
|
if ("sources" in action) {
|
|
@@ -554,6 +888,468 @@ function getCorroborationRules(actionType) {
|
|
|
554
888
|
return ACTION_CORROBORATION_RULES[actionType];
|
|
555
889
|
}
|
|
556
890
|
|
|
891
|
+
// src/security/firewall/firewall-pipeline.ts
|
|
892
|
+
var logger2 = createLogger({ component: "HostileInputFirewall" });
|
|
893
|
+
var HostileInputFirewall = class {
|
|
894
|
+
stages;
|
|
895
|
+
/**
|
|
896
|
+
* Construction-time allowlist, or `undefined` when none was supplied (#4992).
|
|
897
|
+
* The Zod default of `[]` is deliberately NOT taken here: an absent list and
|
|
898
|
+
* an empty list must stay distinguishable so `isAllowlisted` can be omitted
|
|
899
|
+
* rather than recorded as `false` when nothing was consulted.
|
|
900
|
+
*/
|
|
901
|
+
allowlisted;
|
|
902
|
+
maxInputLength;
|
|
903
|
+
adapter;
|
|
904
|
+
reputationCache;
|
|
905
|
+
auditTrail;
|
|
906
|
+
context;
|
|
907
|
+
/** #5382 rollout gate; resolved once at construction, not per call. */
|
|
908
|
+
policyMode;
|
|
909
|
+
/** #5381 reputation-demotion gate; a DIFFERENT knob from `policyMode`. */
|
|
910
|
+
reputationGatingMode;
|
|
911
|
+
/** #5405 seam: how a reputation assessment is obtained. */
|
|
912
|
+
assessReputationFn;
|
|
913
|
+
/** #4992: whether the sanitizer's content tier downgrades the classifier tier. */
|
|
914
|
+
contentDowngrade;
|
|
915
|
+
/** #4992 review: whether a durable logger was configured (not per-call delivery). */
|
|
916
|
+
auditSink;
|
|
917
|
+
constructor(config) {
|
|
918
|
+
const validated = FirewallConfigSchema.parse({
|
|
919
|
+
stages: config.stages,
|
|
920
|
+
allowlistedMaintainers: config.allowlistedMaintainers,
|
|
921
|
+
maxInputLength: config.maxInputLength,
|
|
922
|
+
context: config.context
|
|
923
|
+
});
|
|
924
|
+
this.stages = validated.stages;
|
|
925
|
+
this.allowlisted = config.allowlistedMaintainers !== void 0 ? validated.allowlistedMaintainers : void 0;
|
|
926
|
+
this.maxInputLength = validated.maxInputLength;
|
|
927
|
+
this.adapter = config.adapter;
|
|
928
|
+
this.reputationCache = new ReputationCache();
|
|
929
|
+
this.auditTrail = createAuditTrail(
|
|
930
|
+
config.auditLogger !== void 0 ? createDurableAuditSink(config.auditLogger) : void 0
|
|
931
|
+
);
|
|
932
|
+
this.auditSink = config.auditLogger !== void 0 ? "configured" : "none";
|
|
933
|
+
this.context = validated.context;
|
|
934
|
+
this.policyMode = config.policyMode ?? resolveFirewallPolicyMode(config.env ?? process.env);
|
|
935
|
+
this.reputationGatingMode = config.reputationGatingMode ?? resolveReputationGatingMode(config.env ?? process.env);
|
|
936
|
+
this.assessReputationFn = config.reputationAssessor ?? ((metadata) => assessReputation(metadata, this.reputationCache));
|
|
937
|
+
this.contentDowngrade = config.contentDowngrade ?? true;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Processes untrusted input through the firewall pipeline.
|
|
941
|
+
* Returns a structured FirewallResult or a typed FirewallError.
|
|
942
|
+
*
|
|
943
|
+
* `options` carries the per-call facts (#4992): the repository's maintainer
|
|
944
|
+
* allowlist and the caller's access posture. Each replaces its
|
|
945
|
+
* construction-time counterpart for this call only, so one shared instance
|
|
946
|
+
* never holds a repository's allowlist or a caller's posture process-wide.
|
|
947
|
+
*/
|
|
948
|
+
process(input, options) {
|
|
949
|
+
const start = Date.now();
|
|
950
|
+
this.auditTrail.clear();
|
|
951
|
+
const allowlist = options?.allowlistedMaintainers ?? this.allowlisted;
|
|
952
|
+
const context = options?.context ?? this.context;
|
|
953
|
+
const metaResult = this.runExtraction(input);
|
|
954
|
+
if (!metaResult.ok) return metaResult;
|
|
955
|
+
const meta = metaResult.value;
|
|
956
|
+
const sanitized = this.runSanitization(meta, allowlist);
|
|
957
|
+
const trust = this.runClassification(meta, sanitized, allowlist);
|
|
958
|
+
const reputation = this.runReputation(meta, sanitized, options?.reputation);
|
|
959
|
+
const { tier: effectiveTrustTier, gate: reputationGate } = this.runReputationGate(
|
|
960
|
+
trust.trustTier,
|
|
961
|
+
reputation,
|
|
962
|
+
options?.reputation !== void 0
|
|
963
|
+
);
|
|
964
|
+
this.recordTrustDecision(meta, trust, effectiveTrustTier, reputationGate, allowlist);
|
|
965
|
+
const atl = this.buildATL(meta, effectiveTrustTier, sanitized, reputation);
|
|
966
|
+
const ruleOfTwoViolation = this.runPolicyEnforcement(meta, effectiveTrustTier, context);
|
|
967
|
+
const refusal = this.refuseIfEnforcing(ruleOfTwoViolation, meta, effectiveTrustTier);
|
|
968
|
+
if (refusal !== void 0) return err(refusal);
|
|
969
|
+
return ok(
|
|
970
|
+
this.assembleResult({
|
|
971
|
+
sanitized,
|
|
972
|
+
trust,
|
|
973
|
+
// Measured or absent: present only when an allowlist was consulted.
|
|
974
|
+
isAllowlisted: allowlist !== void 0 ? trust.isAllowlisted : void 0,
|
|
975
|
+
reputation,
|
|
976
|
+
effectiveTrustTier,
|
|
977
|
+
reputationGate,
|
|
978
|
+
atl,
|
|
979
|
+
ruleOfTwoViolation,
|
|
980
|
+
start
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Evaluates the Rule of Two against the call's access posture and surfaces
|
|
986
|
+
* the violation (previously the `policyEnforcement` stage was declared but
|
|
987
|
+
* never read — #3198). Returns `undefined` when the stage is disabled or the
|
|
988
|
+
* rule holds.
|
|
989
|
+
*/
|
|
990
|
+
runPolicyEnforcement(meta, effectiveTrustTier, context) {
|
|
991
|
+
if (!this.stages.policyEnforcement) return void 0;
|
|
992
|
+
const violation = checkRuleOfTwo({
|
|
993
|
+
inputTrustTier: effectiveTrustTier,
|
|
994
|
+
hasWriteAccess: context.hasWriteAccess,
|
|
995
|
+
hasSecretAccess: context.hasSecretAccess
|
|
996
|
+
});
|
|
997
|
+
if (violation !== void 0) {
|
|
998
|
+
logger2.warn("Firewall surfaced a Rule-of-Two violation", {
|
|
999
|
+
user: meta.username,
|
|
1000
|
+
effectiveTrustTier,
|
|
1001
|
+
rule: violation.rule
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
return violation;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Builds the successful result. Optional fields are spread in only when they
|
|
1008
|
+
* were evaluated, so absence keeps meaning "not measured" (see the field
|
|
1009
|
+
* docs on {@link FirewallResult}).
|
|
1010
|
+
*/
|
|
1011
|
+
assembleResult(parts) {
|
|
1012
|
+
const { isAllowlisted, reputation, reputationGate, ruleOfTwoViolation } = parts;
|
|
1013
|
+
const blocking = ruleOfTwoViolation?.severity === "block";
|
|
1014
|
+
return {
|
|
1015
|
+
sanitized: parts.sanitized,
|
|
1016
|
+
trust: parts.trust,
|
|
1017
|
+
...isAllowlisted !== void 0 ? { isAllowlisted } : {},
|
|
1018
|
+
...reputation !== void 0 ? { reputation } : {},
|
|
1019
|
+
effectiveTrustTier: parts.effectiveTrustTier,
|
|
1020
|
+
...reputationGate !== void 0 ? { reputationGate } : {},
|
|
1021
|
+
atl: parts.atl,
|
|
1022
|
+
...ruleOfTwoViolation !== void 0 ? { ruleOfTwoViolation } : {},
|
|
1023
|
+
policyMode: this.policyMode,
|
|
1024
|
+
// Reached only when we did NOT refuse, so this is "audit mode saw
|
|
1025
|
+
// something enforce would have stopped". Under `off` it stays false: the
|
|
1026
|
+
// signal is already on `ruleOfTwoViolation`, and reporting a would-be
|
|
1027
|
+
// refusal for a mode that has not opted in would overstate the gate.
|
|
1028
|
+
wouldRefuse: blocking && this.policyMode === "audit",
|
|
1029
|
+
auditEvents: this.auditTrail.query().map((e) => ({ id: e.id, type: e.type })),
|
|
1030
|
+
auditSink: this.auditSink,
|
|
1031
|
+
durationMs: Date.now() - parts.start
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* The fail-closed half of the #5382 gate: under `enforce`, a blocking policy
|
|
1036
|
+
* violation refuses the input instead of riding along as a signal on an
|
|
1037
|
+
* `ok()` result that a caller checking only `result.ok` walks straight past.
|
|
1038
|
+
*
|
|
1039
|
+
* This gates the RESPONSE to a violation, never its detection. It cannot
|
|
1040
|
+
* manufacture a refusal where the `policyEnforcement` stage never ran, and it
|
|
1041
|
+
* cannot refuse a non-blocking (`warn`) violation — so `enforce` is not a
|
|
1042
|
+
* kill switch, and an allowlisted maintainer stays served.
|
|
1043
|
+
*
|
|
1044
|
+
* @returns the refusal, or `undefined` when the input passes or the mode has
|
|
1045
|
+
* not opted in.
|
|
1046
|
+
*/
|
|
1047
|
+
refuseIfEnforcing(violation, meta, effectiveTrustTier) {
|
|
1048
|
+
if (violation?.severity !== "block" || this.policyMode !== "enforce") return void 0;
|
|
1049
|
+
logger2.warn("Firewall REFUSED input under enforce mode", {
|
|
1050
|
+
user: meta.username,
|
|
1051
|
+
effectiveTrustTier,
|
|
1052
|
+
rule: violation.rule
|
|
1053
|
+
});
|
|
1054
|
+
return {
|
|
1055
|
+
code: "POLICY_REFUSED",
|
|
1056
|
+
message: `Refused by firewall policy: ${violation.rule} \u2014 ${violation.message}`,
|
|
1057
|
+
stage: "policy"
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Validates corroboration for a decided action (#5382).
|
|
1062
|
+
*
|
|
1063
|
+
* Separate from {@link process} because the two operate at different points
|
|
1064
|
+
* in the lifecycle, which is the real shape of the divergence epic #5281
|
|
1065
|
+
* found: `process()` is INPUT-shaped — it sanitizes, classifies and labels
|
|
1066
|
+
* untrusted content — while corroboration is ACTION-shaped, asking whether a
|
|
1067
|
+
* decision the consumer has now reached is backed by sources of sufficient
|
|
1068
|
+
* tier. There is no `AgentAction` in scope during `process()`, so the
|
|
1069
|
+
* `stages.corroboration` flag could never have been wired there; this is the
|
|
1070
|
+
* entry point that makes it readable.
|
|
1071
|
+
*
|
|
1072
|
+
* It is also the shape #5383 needs: production validates corroboration per
|
|
1073
|
+
* action (`issue-triage.ts:391`), so those callers cannot migrate onto the
|
|
1074
|
+
* firewall unless it offers a per-action surface.
|
|
1075
|
+
*
|
|
1076
|
+
* Returns `evaluated: false` when the stage is disabled — never a satisfied
|
|
1077
|
+
* verdict for a check that did not run. Under `enforce` an unsatisfied action
|
|
1078
|
+
* is refused with `POLICY_REFUSED`; under `audit` the would-be refusal is
|
|
1079
|
+
* reported via `wouldRefuse` and the action is allowed through.
|
|
1080
|
+
*/
|
|
1081
|
+
validateAction(action) {
|
|
1082
|
+
if (!this.stages.corroboration) {
|
|
1083
|
+
return ok({
|
|
1084
|
+
evaluated: false,
|
|
1085
|
+
reason: "corroboration-stage-disabled",
|
|
1086
|
+
policyMode: this.policyMode
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
const corroboration = validateCorroboration(action);
|
|
1090
|
+
if (!corroboration.satisfied && this.policyMode === "enforce") {
|
|
1091
|
+
logger2.warn("Firewall REFUSED an uncorroborated action under enforce mode", {
|
|
1092
|
+
actionType: corroboration.actionType,
|
|
1093
|
+
missing: corroboration.missing
|
|
1094
|
+
});
|
|
1095
|
+
return err({
|
|
1096
|
+
code: "POLICY_REFUSED",
|
|
1097
|
+
message: `Refused by firewall policy: ${corroboration.actionType} lacks required corroboration \u2014 ${corroboration.missing.join("; ")}`,
|
|
1098
|
+
stage: "corroboration"
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
return ok({
|
|
1102
|
+
evaluated: true,
|
|
1103
|
+
satisfied: corroboration.satisfied,
|
|
1104
|
+
missing: corroboration.missing,
|
|
1105
|
+
corroboratingSources: corroboration.corroboratingSources,
|
|
1106
|
+
policyMode: this.policyMode,
|
|
1107
|
+
wouldRefuse: !corroboration.satisfied && this.policyMode === "audit"
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/** Returns the internal audit trail for inspection. */
|
|
1111
|
+
getAuditTrail() {
|
|
1112
|
+
return this.auditTrail;
|
|
1113
|
+
}
|
|
1114
|
+
// ==========================================================================
|
|
1115
|
+
// Pipeline Stages (private)
|
|
1116
|
+
// ==========================================================================
|
|
1117
|
+
runExtraction(input) {
|
|
1118
|
+
try {
|
|
1119
|
+
const meta = this.adapter.extractMetadata(input);
|
|
1120
|
+
return ok(meta);
|
|
1121
|
+
} catch (cause) {
|
|
1122
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1123
|
+
return err({
|
|
1124
|
+
code: "EXTRACTION_FAILED",
|
|
1125
|
+
message: `Adapter extraction failed: ${message}`,
|
|
1126
|
+
stage: "extraction"
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
runSanitization(meta, allowlist) {
|
|
1131
|
+
if (!this.stages.sanitization) {
|
|
1132
|
+
return createPassthroughSanitized(meta);
|
|
1133
|
+
}
|
|
1134
|
+
const result = sanitizeInput(
|
|
1135
|
+
meta.content,
|
|
1136
|
+
mapAuthorAssociation(meta.authorAssociation),
|
|
1137
|
+
meta.username,
|
|
1138
|
+
{
|
|
1139
|
+
allowlistedMaintainers: [...allowlist ?? []],
|
|
1140
|
+
maxInputLength: this.maxInputLength
|
|
1141
|
+
}
|
|
1142
|
+
);
|
|
1143
|
+
if (this.stages.audit) {
|
|
1144
|
+
emitSanitizationEvent(this.auditTrail, {
|
|
1145
|
+
source: meta.sourceType,
|
|
1146
|
+
wasModified: result.wasModified,
|
|
1147
|
+
strippedCount: result.strippedElements.length,
|
|
1148
|
+
injectionFlagCount: result.injectionFlags.length,
|
|
1149
|
+
strippedElements: result.strippedElements.slice(0, MAX_STRIPPED_ELEMENTS_PER_EVENT).map((e) => ({ tag: e.tag, reason: e.reason }))
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return result;
|
|
1153
|
+
}
|
|
1154
|
+
runClassification(meta, sanitized, allowlist) {
|
|
1155
|
+
if (!this.stages.trustClassification) {
|
|
1156
|
+
return createPassthroughClassification(meta);
|
|
1157
|
+
}
|
|
1158
|
+
return classifyTrust({
|
|
1159
|
+
username: meta.username,
|
|
1160
|
+
authorAssociation: meta.authorAssociation,
|
|
1161
|
+
// #4992: `contentDowngrade: false` withholds the sanitizer's content tier
|
|
1162
|
+
// from the classifier; the flags themselves were still measured above.
|
|
1163
|
+
...this.contentDowngrade ? { sanitizedInput: sanitized } : {},
|
|
1164
|
+
config: {
|
|
1165
|
+
allowlistedMaintainers: [...allowlist ?? []]
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Emits the trust audit event for the tier the consumer acts on — after the
|
|
1171
|
+
* reputation gate, so the record cannot describe a tier nobody enforced
|
|
1172
|
+
* (#4992 review). A demotion is named in `reason`.
|
|
1173
|
+
*/
|
|
1174
|
+
recordTrustDecision(meta, trust, effectiveTrustTier, gate, allowlist) {
|
|
1175
|
+
if (!this.stages.audit) return;
|
|
1176
|
+
const demoted = effectiveTrustTier !== trust.trustTier;
|
|
1177
|
+
emitTrustEvent(this.auditTrail, {
|
|
1178
|
+
username: meta.username,
|
|
1179
|
+
assignedTier: effectiveTrustTier,
|
|
1180
|
+
userRole: trust.userRole,
|
|
1181
|
+
// Recorded only when an allowlist was consulted (#4992).
|
|
1182
|
+
...allowlist !== void 0 ? { isAllowlisted: trust.isAllowlisted } : {},
|
|
1183
|
+
wasDowngraded: trust.wasDowngraded || demoted,
|
|
1184
|
+
reason: describeGate(trust.reason, demoted, gate)
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Reconcile the classifier tier with reputation, under the rollout mode
|
|
1189
|
+
* production honours (#3106 reconciliation, #5381 gating).
|
|
1190
|
+
*
|
|
1191
|
+
* #3106's reconciliation is demotion-only; Tier-1/allowlist wins; it equals
|
|
1192
|
+
* the classifier tier when reputation is absent. #5381 puts it behind
|
|
1193
|
+
* `NEXUS_REPUTATION_GATING`, so `audit` is audit-only here too — previously
|
|
1194
|
+
* the firewall enforced unconditionally while production suppressed, on
|
|
1195
|
+
* identical configuration.
|
|
1196
|
+
*
|
|
1197
|
+
* Returns no gate at all when the stage is off, so absence in the result
|
|
1198
|
+
* means "not evaluated" rather than "evaluated, nothing suppressed" —
|
|
1199
|
+
* `demotionSuppressed` is a required boolean and would otherwise report
|
|
1200
|
+
* `false` for a check that never ran.
|
|
1201
|
+
*/
|
|
1202
|
+
runReputationGate(classifierTier, reputation, callerSupplied) {
|
|
1203
|
+
if (!this.stages.reputationAssessment && !callerSupplied) return { tier: classifierTier };
|
|
1204
|
+
const gate = gateWithReputation(classifierTier, reputation, this.reputationGatingMode);
|
|
1205
|
+
return { tier: gate.enforcedTier, gate };
|
|
1206
|
+
}
|
|
1207
|
+
runReputation(meta, sanitized, supplied) {
|
|
1208
|
+
if (supplied !== void 0) {
|
|
1209
|
+
if (supplied.assessment !== void 0 && this.stages.audit) {
|
|
1210
|
+
emitReputationEvent(this.auditTrail, {
|
|
1211
|
+
username: meta.username,
|
|
1212
|
+
reputationScore: supplied.assessment.reputationScore,
|
|
1213
|
+
isSuspicious: supplied.assessment.isSuspicious,
|
|
1214
|
+
effectiveTier: supplied.assessment.effectiveTrustTier,
|
|
1215
|
+
signalCount: supplied.assessment.suspiciousSignals.length
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return supplied.assessment;
|
|
1219
|
+
}
|
|
1220
|
+
if (!this.stages.reputationAssessment) return void 0;
|
|
1221
|
+
const metadata = {
|
|
1222
|
+
username: meta.username,
|
|
1223
|
+
authorAssociation: meta.authorAssociation.toUpperCase(),
|
|
1224
|
+
injectionFlags: sanitized.injectionFlags
|
|
1225
|
+
};
|
|
1226
|
+
const result = this.assessReputationFn(metadata);
|
|
1227
|
+
if (this.stages.audit) {
|
|
1228
|
+
emitReputationEvent(this.auditTrail, {
|
|
1229
|
+
username: meta.username,
|
|
1230
|
+
reputationScore: result.reputationScore,
|
|
1231
|
+
isSuspicious: result.isSuspicious,
|
|
1232
|
+
effectiveTier: result.effectiveTrustTier,
|
|
1233
|
+
signalCount: result.suspiciousSignals.length
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
buildATL(meta, effectiveTrustTier, sanitized, reputation) {
|
|
1239
|
+
const data = {
|
|
1240
|
+
tier: effectiveTrustTier,
|
|
1241
|
+
source: meta.sourceType,
|
|
1242
|
+
user: meta.username,
|
|
1243
|
+
sanitized: sanitized.wasModified,
|
|
1244
|
+
...reputation !== void 0 ? { rep: reputation.reputationScore / 100 } : {}
|
|
1245
|
+
};
|
|
1246
|
+
return generateATL(data);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// src/security/firewall/github-adapter.ts
|
|
1251
|
+
import { z as z5 } from "zod";
|
|
1252
|
+
var GitHubIssueSchema = z5.object({
|
|
1253
|
+
type: z5.literal("issue"),
|
|
1254
|
+
username: z5.string().min(1),
|
|
1255
|
+
authorAssociation: z5.string().min(1),
|
|
1256
|
+
title: z5.string().default(""),
|
|
1257
|
+
body: z5.string().default("")
|
|
1258
|
+
});
|
|
1259
|
+
var GitHubCommentSchema = z5.object({
|
|
1260
|
+
type: z5.literal("comment"),
|
|
1261
|
+
username: z5.string().min(1),
|
|
1262
|
+
authorAssociation: z5.string().min(1),
|
|
1263
|
+
body: z5.string().default("")
|
|
1264
|
+
});
|
|
1265
|
+
var GitHubPRSchema = z5.object({
|
|
1266
|
+
type: z5.literal("pull_request"),
|
|
1267
|
+
username: z5.string().min(1),
|
|
1268
|
+
authorAssociation: z5.string().min(1),
|
|
1269
|
+
title: z5.string().default(""),
|
|
1270
|
+
body: z5.string().default("")
|
|
1271
|
+
});
|
|
1272
|
+
var GitHubInputSchema = z5.discriminatedUnion("type", [
|
|
1273
|
+
GitHubIssueSchema,
|
|
1274
|
+
GitHubCommentSchema,
|
|
1275
|
+
GitHubPRSchema
|
|
1276
|
+
]);
|
|
1277
|
+
function toSourceType(type) {
|
|
1278
|
+
const mapping = {
|
|
1279
|
+
issue: "github-issue",
|
|
1280
|
+
comment: "github-comment",
|
|
1281
|
+
pull_request: "github-pr"
|
|
1282
|
+
};
|
|
1283
|
+
return mapping[type];
|
|
1284
|
+
}
|
|
1285
|
+
function extractContent(input) {
|
|
1286
|
+
if (input.type === "comment") return input.body;
|
|
1287
|
+
const title = input.title;
|
|
1288
|
+
const body = input.body;
|
|
1289
|
+
return title && body ? `${title}
|
|
1290
|
+
|
|
1291
|
+
${body}` : title || body;
|
|
1292
|
+
}
|
|
1293
|
+
function createGitHubAdapter() {
|
|
1294
|
+
return {
|
|
1295
|
+
platform: "github",
|
|
1296
|
+
extractMetadata(input) {
|
|
1297
|
+
const result = GitHubInputSchema.safeParse(input);
|
|
1298
|
+
if (!result.success) {
|
|
1299
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1300
|
+
throw new Error(`GitHub input validation failed: ${issues}`);
|
|
1301
|
+
}
|
|
1302
|
+
const parsed = result.data;
|
|
1303
|
+
const role = mapAuthorAssociation(parsed.authorAssociation);
|
|
1304
|
+
return {
|
|
1305
|
+
username: parsed.username,
|
|
1306
|
+
authorAssociation: role,
|
|
1307
|
+
content: extractContent(parsed),
|
|
1308
|
+
sourceType: toSourceType(parsed.type)
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/dogfooding/untrusted-input-firewall.ts
|
|
1315
|
+
var logger3 = createLogger({ component: "UntrustedInputFirewall" });
|
|
1316
|
+
var singleton;
|
|
1317
|
+
var configuredAuditLogger;
|
|
1318
|
+
function configureUntrustedInputFirewall(deps) {
|
|
1319
|
+
configuredAuditLogger = deps.auditLogger;
|
|
1320
|
+
singleton = void 0;
|
|
1321
|
+
}
|
|
1322
|
+
function getUntrustedInputFirewall() {
|
|
1323
|
+
singleton ??= new HostileInputFirewall({
|
|
1324
|
+
adapter: createGitHubAdapter(),
|
|
1325
|
+
contentDowngrade: false,
|
|
1326
|
+
...configuredAuditLogger !== void 0 ? { auditLogger: configuredAuditLogger } : {}
|
|
1327
|
+
});
|
|
1328
|
+
return singleton;
|
|
1329
|
+
}
|
|
1330
|
+
function runUntrustedInputFirewall(input, options) {
|
|
1331
|
+
const result = getUntrustedInputFirewall().process(input, options);
|
|
1332
|
+
if (!result.ok) {
|
|
1333
|
+
const { code, stage, message } = result.error;
|
|
1334
|
+
logger3.warn("Untrusted-input firewall rejected the input", {
|
|
1335
|
+
user: input.username,
|
|
1336
|
+
sourceType: input.type,
|
|
1337
|
+
code,
|
|
1338
|
+
stage
|
|
1339
|
+
});
|
|
1340
|
+
return err(new Error(`Untrusted-input firewall ${code} at stage ${stage}: ${message}`));
|
|
1341
|
+
}
|
|
1342
|
+
if (result.value.wouldRefuse) {
|
|
1343
|
+
logger3.warn("Untrusted-input firewall would refuse under enforce (audit mode)", {
|
|
1344
|
+
user: input.username,
|
|
1345
|
+
sourceType: input.type,
|
|
1346
|
+
trustTier: result.value.effectiveTrustTier,
|
|
1347
|
+
rule: result.value.ruleOfTwoViolation?.rule
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
return ok(result.value);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
557
1353
|
// src/scm/url-parsers.ts
|
|
558
1354
|
function parsePRUrl(url) {
|
|
559
1355
|
const httpPattern = /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/;
|
|
@@ -595,7 +1391,7 @@ function parseIssueUrl(url) {
|
|
|
595
1391
|
}
|
|
596
1392
|
|
|
597
1393
|
// src/scm/github-provider-traits.ts
|
|
598
|
-
var
|
|
1394
|
+
var logger4 = createLogger({ component: "GitHubProviderTraits" });
|
|
599
1395
|
var cachedGhToken = null;
|
|
600
1396
|
var ghTokenPromise;
|
|
601
1397
|
async function resolveGhToken() {
|
|
@@ -672,7 +1468,7 @@ var GitHubReviewer = class {
|
|
|
672
1468
|
provider;
|
|
673
1469
|
async getPullRequestDetail(prNumber) {
|
|
674
1470
|
const repo = this.provider.repo;
|
|
675
|
-
|
|
1471
|
+
logger4.debug("Getting PR detail", { repo, prNumber });
|
|
676
1472
|
const prResult = await execGhApi(`repos/${repo}/pulls/${String(prNumber)}`);
|
|
677
1473
|
if (!prResult.ok) return prResult;
|
|
678
1474
|
const filesResult = await execGhApi(`repos/${repo}/pulls/${String(prNumber)}/files`);
|
|
@@ -707,7 +1503,7 @@ var GitHubReviewer = class {
|
|
|
707
1503
|
request_changes: "REQUEST_CHANGES",
|
|
708
1504
|
comment: "COMMENT"
|
|
709
1505
|
};
|
|
710
|
-
|
|
1506
|
+
logger4.info("Creating review", { repo, prNumber, decision });
|
|
711
1507
|
const { execFile } = await import("node:child_process");
|
|
712
1508
|
const { promisify } = await import("node:util");
|
|
713
1509
|
const exec = promisify(execFile);
|
|
@@ -734,7 +1530,7 @@ var GitHubReviewer = class {
|
|
|
734
1530
|
}
|
|
735
1531
|
async getIssueDetail(issueNumber) {
|
|
736
1532
|
const repo = this.provider.repo;
|
|
737
|
-
|
|
1533
|
+
logger4.debug("Getting issue detail", { repo, issueNumber });
|
|
738
1534
|
const result = await execGhApi(`repos/${repo}/issues/${String(issueNumber)}`);
|
|
739
1535
|
if (!result.ok) return result;
|
|
740
1536
|
try {
|
|
@@ -756,7 +1552,7 @@ var GitHubReviewer = class {
|
|
|
756
1552
|
}
|
|
757
1553
|
async listCommentDetails(issueNumber) {
|
|
758
1554
|
const repo = this.provider.repo;
|
|
759
|
-
|
|
1555
|
+
logger4.debug("Listing comment details", { repo, issueNumber });
|
|
760
1556
|
const result = await execGhApi(`repos/${repo}/issues/${String(issueNumber)}/comments`);
|
|
761
1557
|
if (!result.ok) return result;
|
|
762
1558
|
try {
|
|
@@ -777,7 +1573,7 @@ var GitHubReviewer = class {
|
|
|
777
1573
|
};
|
|
778
1574
|
var GitHubUserInfo = class {
|
|
779
1575
|
async fetchUserMetadata(username) {
|
|
780
|
-
|
|
1576
|
+
logger4.debug("Fetching user metadata", { username });
|
|
781
1577
|
const result = await execGhApi(`users/${username}`);
|
|
782
1578
|
if (!result.ok) return result;
|
|
783
1579
|
try {
|
|
@@ -810,17 +1606,17 @@ function createFullGitHubProvider(repo) {
|
|
|
810
1606
|
}
|
|
811
1607
|
|
|
812
1608
|
// src/dogfooding/issue-triage-types.ts
|
|
813
|
-
import { z as
|
|
1609
|
+
import { z as z6 } from "zod";
|
|
814
1610
|
var DEFAULT_ISSUE_TRIAGE_CONFIG = {
|
|
815
1611
|
dryRun: true,
|
|
816
1612
|
maxComments: 50,
|
|
817
1613
|
enableReputation: true
|
|
818
1614
|
};
|
|
819
|
-
var IssueTriageConfigSchema =
|
|
820
|
-
dryRun:
|
|
821
|
-
githubToken:
|
|
822
|
-
maxComments:
|
|
823
|
-
enableReputation:
|
|
1615
|
+
var IssueTriageConfigSchema = z6.object({
|
|
1616
|
+
dryRun: z6.boolean().default(true),
|
|
1617
|
+
githubToken: z6.string().optional(),
|
|
1618
|
+
maxComments: z6.number().int().min(1).max(100).default(50),
|
|
1619
|
+
enableReputation: z6.boolean().default(true)
|
|
824
1620
|
});
|
|
825
1621
|
|
|
826
1622
|
// src/dogfooding/issue-triage-helpers.ts
|
|
@@ -886,7 +1682,7 @@ function extractLabelsFromBody(title, body) {
|
|
|
886
1682
|
}
|
|
887
1683
|
|
|
888
1684
|
// src/dogfooding/issue-triage.ts
|
|
889
|
-
var
|
|
1685
|
+
var logger5 = createLogger({ component: "IssueTriage" });
|
|
890
1686
|
function applySafetyActions(actions, gateDecision, reputation, issue) {
|
|
891
1687
|
const withRefusal = appendRefusalIfHostile(actions, gateDecision, issue);
|
|
892
1688
|
return appendApprovalIfBorderline(withRefusal, gateDecision, reputation, issue);
|
|
@@ -932,7 +1728,7 @@ var IssueTriage = class {
|
|
|
932
1728
|
*/
|
|
933
1729
|
async triageIssue(issueUrl) {
|
|
934
1730
|
const startTime = getTimeProvider().now();
|
|
935
|
-
|
|
1731
|
+
logger5.info("Starting issue triage", { issueUrl });
|
|
936
1732
|
const parseResult = parseIssueUrl(issueUrl);
|
|
937
1733
|
if (!parseResult.ok) return parseResult;
|
|
938
1734
|
const { owner, repo, issueNumber } = parseResult.value;
|
|
@@ -941,15 +1737,16 @@ var IssueTriage = class {
|
|
|
941
1737
|
const { issue: issueResult, comments, accountAgeDays } = fetchResult.value;
|
|
942
1738
|
const safeTitle = this.sanitizeContent(issueResult.title, issueResult.author);
|
|
943
1739
|
const safeBody = this.sanitizeContent(issueResult.body, issueResult.author);
|
|
944
|
-
const trustResult = this.classifyAuthor(issueResult);
|
|
945
1740
|
const reputation = this.assessAuthorReputation(issueResult, comments, accountAgeDays);
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
)
|
|
1741
|
+
const firewall = this.classifyAuthor(issueResult, reputation);
|
|
1742
|
+
if (!firewall.ok) return firewall;
|
|
1743
|
+
const trustResult = firewall.value.trust;
|
|
1744
|
+
const gateDecision = firewall.value.reputationGate;
|
|
1745
|
+
if (gateDecision === void 0) {
|
|
1746
|
+
return err(new Error("Untrusted-input firewall returned no reputation gate decision"));
|
|
1747
|
+
}
|
|
951
1748
|
if (gateDecision.demotionSuppressed) {
|
|
952
|
-
|
|
1749
|
+
logger5.warn("Reputation demotion suppressed by gating mode (would block under enforce)", {
|
|
953
1750
|
issueNumber,
|
|
954
1751
|
author: issueResult.author,
|
|
955
1752
|
mode: gateDecision.mode,
|
|
@@ -964,12 +1761,14 @@ var IssueTriage = class {
|
|
|
964
1761
|
issue: issueResult,
|
|
965
1762
|
actions: validatedActions,
|
|
966
1763
|
trustResult,
|
|
1764
|
+
isAllowlisted: firewall.value.isAllowlisted,
|
|
1765
|
+
auditSink: firewall.value.auditSink,
|
|
967
1766
|
reputation,
|
|
968
1767
|
gateDecision,
|
|
969
1768
|
safeContent: { title: safeTitle, body: safeBody },
|
|
970
1769
|
startTime
|
|
971
1770
|
});
|
|
972
|
-
|
|
1771
|
+
logger5.info("Issue triage completed", {
|
|
973
1772
|
issueNumber,
|
|
974
1773
|
category: result.category,
|
|
975
1774
|
actionCount: result.proposedActions.length,
|
|
@@ -985,7 +1784,7 @@ var IssueTriage = class {
|
|
|
985
1784
|
if (text.length === 0) return text;
|
|
986
1785
|
const result = sanitizeInput(text, "unknown", author);
|
|
987
1786
|
if (result.wasModified) {
|
|
988
|
-
|
|
1787
|
+
logger5.warn("Issue content sanitized", {
|
|
989
1788
|
author,
|
|
990
1789
|
strippedCount: result.strippedElements.length,
|
|
991
1790
|
injectionFlags: result.injectionFlags
|
|
@@ -994,14 +1793,44 @@ var IssueTriage = class {
|
|
|
994
1793
|
return result.content;
|
|
995
1794
|
}
|
|
996
1795
|
/**
|
|
997
|
-
* Classifies the issue author's trust tier
|
|
998
|
-
* (
|
|
1796
|
+
* Classifies the issue author's trust tier through the shared firewall
|
|
1797
|
+
* (#4992; originally Issue #828 — trust-classifier wiring).
|
|
1798
|
+
*
|
|
1799
|
+
* The access posture is passed per call so the firewall's Rule-of-Two check
|
|
1800
|
+
* sees the same facts `validateActions` enforces on. No maintainer allowlist
|
|
1801
|
+
* is passed: there is no source for one today (no config field, no env var),
|
|
1802
|
+
* so none is consulted and `isAllowlisted` stays absent on the result rather
|
|
1803
|
+
* than being recorded as `false`.
|
|
999
1804
|
*/
|
|
1000
|
-
classifyAuthor(issue) {
|
|
1001
|
-
return
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1805
|
+
classifyAuthor(issue, reputation) {
|
|
1806
|
+
return runUntrustedInputFirewall(
|
|
1807
|
+
{
|
|
1808
|
+
type: "issue",
|
|
1809
|
+
username: issue.author,
|
|
1810
|
+
authorAssociation: issue.authorAssociation,
|
|
1811
|
+
title: issue.title,
|
|
1812
|
+
body: issue.body
|
|
1813
|
+
},
|
|
1814
|
+
{ context: this.accessContext(), reputation: { assessment: reputation } }
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* The access posture this triage runs under — the two Rule-of-Two conjuncts
|
|
1819
|
+
* that are about the agent rather than the input. Shared by the firewall
|
|
1820
|
+
* (#4992) and `validateActions` so the two cannot disagree.
|
|
1821
|
+
*/
|
|
1822
|
+
accessContext() {
|
|
1823
|
+
return {
|
|
1824
|
+
hasWriteAccess: !this.config.dryRun,
|
|
1825
|
+
// #4681: read the token from where it ACTUALLY comes from. The previous
|
|
1826
|
+
// form (`this.config.githubToken !== undefined`) was written to make this
|
|
1827
|
+
// conjunct "real", but no production caller ever sets `githubToken` — the
|
|
1828
|
+
// SCM provider resolves the live credential from GITHUB_TOKEN/GH_TOKEN.
|
|
1829
|
+
// So the conjunct was permanently false and Rule of Two could never trip,
|
|
1830
|
+
// which is precisely the constant it was introduced to remove.
|
|
1831
|
+
// `hasToken()` is the canonical resolver-backed check.
|
|
1832
|
+
hasSecretAccess: this.config.githubToken !== void 0 || hasToken("github")
|
|
1833
|
+
};
|
|
1005
1834
|
}
|
|
1006
1835
|
/**
|
|
1007
1836
|
* Assesses the issue author's reputation using the reputation model.
|
|
@@ -1064,15 +1893,7 @@ var IssueTriage = class {
|
|
|
1064
1893
|
validateActions(actions, gateDecision) {
|
|
1065
1894
|
const context = {
|
|
1066
1895
|
inputTrustTier: gateDecision.enforcedTier,
|
|
1067
|
-
|
|
1068
|
-
// #4681: read the token from where it ACTUALLY comes from. The previous
|
|
1069
|
-
// form (`this.config.githubToken !== undefined`) was written to make this
|
|
1070
|
-
// conjunct "real", but no production caller ever sets `githubToken` — the
|
|
1071
|
-
// SCM provider resolves the live credential from GITHUB_TOKEN/GH_TOKEN.
|
|
1072
|
-
// So the conjunct was permanently false and Rule of Two could never trip,
|
|
1073
|
-
// which is precisely the constant it was introduced to remove.
|
|
1074
|
-
// `hasToken()` is the canonical resolver-backed check.
|
|
1075
|
-
hasSecretAccess: this.config.githubToken !== void 0 || hasToken("github")
|
|
1896
|
+
...this.accessContext()
|
|
1076
1897
|
};
|
|
1077
1898
|
return actions.map((action) => {
|
|
1078
1899
|
const policyDecision = evaluatePolicy(action, context);
|
|
@@ -1098,13 +1919,25 @@ var IssueTriage = class {
|
|
|
1098
1919
|
* Builds the final triage result.
|
|
1099
1920
|
*/
|
|
1100
1921
|
buildResult(opts) {
|
|
1101
|
-
const {
|
|
1922
|
+
const {
|
|
1923
|
+
issue,
|
|
1924
|
+
actions,
|
|
1925
|
+
trustResult,
|
|
1926
|
+
isAllowlisted,
|
|
1927
|
+
auditSink,
|
|
1928
|
+
reputation,
|
|
1929
|
+
gateDecision,
|
|
1930
|
+
safeContent,
|
|
1931
|
+
startTime
|
|
1932
|
+
} = opts;
|
|
1102
1933
|
const [category, confidence] = categorizeIssue(safeContent.title, safeContent.body);
|
|
1103
1934
|
const isTier1 = trustResult.trustTier === "1";
|
|
1104
1935
|
const trustAssessment = {
|
|
1105
1936
|
trustTier: trustResult.trustTier,
|
|
1106
1937
|
userRole: trustResult.userRole,
|
|
1107
|
-
|
|
1938
|
+
// Measured or absent (#4992): never the classifier's default `false`.
|
|
1939
|
+
...isAllowlisted !== void 0 ? { isAllowlisted } : {},
|
|
1940
|
+
auditSink,
|
|
1108
1941
|
reputationScore: reputation?.reputationScore,
|
|
1109
1942
|
suspiciousSignals: isTier1 ? [] : reputation?.suspiciousSignals ?? [],
|
|
1110
1943
|
isSuspicious: isTier1 ? false : reputation?.isSuspicious ?? false,
|
|
@@ -1222,11 +2055,19 @@ function createIssueTriage(config) {
|
|
|
1222
2055
|
}
|
|
1223
2056
|
|
|
1224
2057
|
export {
|
|
2058
|
+
createDurableAuditSink,
|
|
2059
|
+
createDurableAuditTrail,
|
|
2060
|
+
FirewallPolicyModeSchema,
|
|
2061
|
+
DEFAULT_FIREWALL_POLICY_MODE,
|
|
2062
|
+
FIREWALL_POLICY_ENV_VAR,
|
|
2063
|
+
resolveFirewallPolicyMode,
|
|
1225
2064
|
mapAuthorAssociation,
|
|
1226
2065
|
classifyTrust,
|
|
1227
2066
|
canInfluenceDecisions,
|
|
1228
2067
|
requiresCorroboration,
|
|
1229
2068
|
getRequiredTrustTier,
|
|
2069
|
+
generateATL,
|
|
2070
|
+
parseATL,
|
|
1230
2071
|
SourceCitationSchema,
|
|
1231
2072
|
AgentActionSchema,
|
|
1232
2073
|
validateAgentAction,
|
|
@@ -1235,11 +2076,14 @@ export {
|
|
|
1235
2076
|
requiresHumanApproval,
|
|
1236
2077
|
requiresCitation,
|
|
1237
2078
|
ViolationSchema,
|
|
1238
|
-
checkRuleOfTwo,
|
|
1239
2079
|
evaluatePolicy,
|
|
1240
2080
|
canProceed,
|
|
1241
2081
|
validateCorroboration,
|
|
1242
2082
|
getCorroborationRules,
|
|
2083
|
+
HostileInputFirewall,
|
|
2084
|
+
createGitHubAdapter,
|
|
2085
|
+
configureUntrustedInputFirewall,
|
|
2086
|
+
runUntrustedInputFirewall,
|
|
1243
2087
|
parsePRUrl,
|
|
1244
2088
|
GitHubReviewer,
|
|
1245
2089
|
GitHubUserInfo,
|
|
@@ -1247,4 +2091,4 @@ export {
|
|
|
1247
2091
|
IssueTriage,
|
|
1248
2092
|
createIssueTriage
|
|
1249
2093
|
};
|
|
1250
|
-
//# sourceMappingURL=chunk-
|
|
2094
|
+
//# sourceMappingURL=chunk-FPHFCZ2E.js.map
|