@cassiomc1/forgeloop 1.13.0 → 1.14.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/AGENT_COMPATIBILITY.md +8 -0
- package/DOCS_INDEX.md +35 -3
- package/ENG/nodejs-backend-development-eng.md +2 -2
- package/ENG/sec-code-eng.md +7 -7
- package/EXECUTION_STATE.md +12 -0
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +40 -25
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +19 -1
- package/completions/forgeloop.bash +37 -1
- package/completions/forgeloop.fish +123 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +366 -6
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +32 -11
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +35 -8
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +38 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +750 -5
- package/docs/protocol-requirements.json +24 -0
- package/package.json +30 -3
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +4 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +241 -0
- package/src/core/command-executors.js +110 -0
- package/src/core/command-input.js +115 -43
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/gate-provenance.js +124 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/phase.js +128 -10
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +128 -52
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +118 -61
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_SECURITY_REVIEW_OUTPUT_LIMIT,
|
|
3
|
+
E_SECURITY_REVIEW_RESULT_INVALID,
|
|
4
|
+
} from "../error-codes.js";
|
|
5
|
+
import { deepFreeze, normalizePortableText } from "../portable-context.js";
|
|
6
|
+
import {
|
|
7
|
+
SECURITY_REVIEW_BLOCKED_RESULT_FIELDS,
|
|
8
|
+
SECURITY_REVIEW_CATEGORIES,
|
|
9
|
+
SECURITY_REVIEW_CONFIDENCE,
|
|
10
|
+
SECURITY_REVIEW_LIMITS,
|
|
11
|
+
SECURITY_REVIEW_SCOPES,
|
|
12
|
+
SECURITY_REVIEW_SEVERITIES,
|
|
13
|
+
SECURITY_REVIEW_TRUST,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
|
|
16
|
+
function resultError(message, details = null, code = E_SECURITY_REVIEW_RESULT_INVALID) {
|
|
17
|
+
const error = new Error(message);
|
|
18
|
+
error.code = code;
|
|
19
|
+
error.details = details;
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function limitError(message, details = null) {
|
|
24
|
+
return resultError(message, details, E_SECURITY_REVIEW_OUTPUT_LIMIT);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function text(label, value, maxLength, { optional = false } = {}) {
|
|
28
|
+
try {
|
|
29
|
+
return normalizePortableText(value, { label, maxLength, optional });
|
|
30
|
+
} catch {
|
|
31
|
+
throw resultError(`${label} is invalid.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function consumeSnapshotBudget(value, label, state, depth) {
|
|
36
|
+
if (depth > SECURITY_REVIEW_LIMITS.maxSnapshotDepth) {
|
|
37
|
+
throw limitError(`${label} exceeds the maximum result nesting depth.`);
|
|
38
|
+
}
|
|
39
|
+
state.nodes += 1;
|
|
40
|
+
if (state.nodes > SECURITY_REVIEW_LIMITS.maxSnapshotNodes) {
|
|
41
|
+
throw limitError("Security review result exceeds its maximum snapshot node count.");
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
state.chars += value.length;
|
|
45
|
+
if (state.chars > SECURITY_REVIEW_LIMITS.maxSnapshotChars) {
|
|
46
|
+
throw limitError("Security review result exceeds its maximum snapshot character count.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function strictSnapshotArray(value, label, seen, state, depth) {
|
|
52
|
+
seen.add(value);
|
|
53
|
+
try {
|
|
54
|
+
const output = [];
|
|
55
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
56
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) throw resultError(`${label} contains a sparse array.`);
|
|
57
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
58
|
+
if (!descriptor || descriptor.get || descriptor.set) throw resultError(`${label}[${index}] is an accessor.`);
|
|
59
|
+
output.push(strictSnapshot(value[index], `${label}[${index}]`, seen, state, depth + 1));
|
|
60
|
+
}
|
|
61
|
+
return output;
|
|
62
|
+
} finally {
|
|
63
|
+
seen.delete(value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function strictSnapshotObject(value, label, seen, state, depth) {
|
|
68
|
+
const prototype = Object.getPrototypeOf(value);
|
|
69
|
+
if (value instanceof Date || value instanceof Map || value instanceof Set || value instanceof Promise
|
|
70
|
+
|| (prototype !== Object.prototype && prototype !== null)) {
|
|
71
|
+
throw resultError(`${label} contains a non-plain object.`);
|
|
72
|
+
}
|
|
73
|
+
seen.add(value);
|
|
74
|
+
try {
|
|
75
|
+
const output = {};
|
|
76
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
77
|
+
if (typeof key !== "string") throw resultError(`${label} contains a symbol key.`);
|
|
78
|
+
if (key === "toJSON") throw resultError(`${label} contains a custom toJSON method.`);
|
|
79
|
+
state.chars += key.length;
|
|
80
|
+
if (state.chars > SECURITY_REVIEW_LIMITS.maxSnapshotChars) {
|
|
81
|
+
throw limitError("Security review result exceeds its maximum snapshot character count.");
|
|
82
|
+
}
|
|
83
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
84
|
+
if (!descriptor || descriptor.get || descriptor.set) throw resultError(`${label}.${key} is an accessor.`);
|
|
85
|
+
output[key] = strictSnapshot(value[key], `${label}.${key}`, seen, state, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
return output;
|
|
88
|
+
} finally {
|
|
89
|
+
seen.delete(value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function strictSnapshot(value, label = "security review result", seen = new Set(), state = { nodes: 0, chars: 0 }, depth = 0) {
|
|
94
|
+
consumeSnapshotBudget(value, label, state, depth);
|
|
95
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
96
|
+
if (typeof value === "number") {
|
|
97
|
+
if (!Number.isFinite(value)) throw resultError(`${label} contains a non-finite number.`);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
if (typeof value !== "object") throw resultError(`${label} contains an unsupported value.`);
|
|
101
|
+
if (seen.has(value)) throw resultError(`${label} contains a circular reference.`);
|
|
102
|
+
return Array.isArray(value)
|
|
103
|
+
? strictSnapshotArray(value, label, seen, state, depth)
|
|
104
|
+
: strictSnapshotObject(value, label, seen, state, depth);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rejectUnknown(source, allowed, label) {
|
|
108
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
109
|
+
if (typeof key !== "string" || !allowed.includes(key)) {
|
|
110
|
+
throw resultError(`${label} contains an unsupported field.`, { field: typeof key === "string" ? key : String(key) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertSafeText(label, value, maxLength) {
|
|
116
|
+
const normalized = text(label, value, maxLength);
|
|
117
|
+
if (/(?:authorization\s*:\s*bearer|cookie\s*:|(?:token|secret|password)=|-----begin|file:\/\/(?:\/|%2f)|(?:^|\/)Users\/|(?:^|\/)home\/)/i.test(normalized)) {
|
|
118
|
+
throw resultError(`${label} contains sensitive or non-portable content.`);
|
|
119
|
+
}
|
|
120
|
+
return normalized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizePath(value, label) {
|
|
124
|
+
const path = assertSafeText(label, value, SECURITY_REVIEW_LIMITS.maxPathChars).replaceAll("\\", "/");
|
|
125
|
+
if (path.startsWith("/") || /^[a-z]:\//i.test(path) || path.startsWith("//") || /^file:/i.test(path)
|
|
126
|
+
|| path.split("/").includes("..") || path === "." || path.endsWith("/") || path.includes("//")) {
|
|
127
|
+
throw resultError(`${label} must be a project-relative non-escaping path.`);
|
|
128
|
+
}
|
|
129
|
+
return path;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function assertFindingScope(path, scope, requestedPaths, index) {
|
|
133
|
+
if (scope === "FULL" || requestedPaths.length === 0) return;
|
|
134
|
+
const inScope = requestedPaths.some((root) => path === root || path.startsWith(`${root}/`));
|
|
135
|
+
if (!inScope) {
|
|
136
|
+
throw resultError(`findings[${index}].path is outside the requested ${scope.toLowerCase()} scope.`, {
|
|
137
|
+
path,
|
|
138
|
+
requestedPaths,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeFinding(raw, index, scope, requestedPaths) {
|
|
144
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw resultError(`findings[${index}] is invalid.`);
|
|
145
|
+
rejectUnknown(raw, ["id", "category", "severity", "title", "summary", "path", "line", "ruleId", "confidence"], `findings[${index}]`);
|
|
146
|
+
const finding = {
|
|
147
|
+
id: assertSafeText(`findings[${index}].id`, raw.id, SECURITY_REVIEW_LIMITS.maxFindingIdChars),
|
|
148
|
+
category: assertSafeText(`findings[${index}].category`, raw.category, SECURITY_REVIEW_LIMITS.maxCategoryChars),
|
|
149
|
+
severity: assertSafeText(`findings[${index}].severity`, raw.severity, SECURITY_REVIEW_LIMITS.maxSeverityChars),
|
|
150
|
+
title: assertSafeText(`findings[${index}].title`, raw.title, SECURITY_REVIEW_LIMITS.maxTitleChars),
|
|
151
|
+
summary: assertSafeText(`findings[${index}].summary`, raw.summary, SECURITY_REVIEW_LIMITS.maxSummaryChars),
|
|
152
|
+
};
|
|
153
|
+
if (!SECURITY_REVIEW_CATEGORIES.includes(finding.category)) throw resultError(`findings[${index}].category is unsupported.`);
|
|
154
|
+
if (!SECURITY_REVIEW_SEVERITIES.includes(finding.severity)) throw resultError(`findings[${index}].severity is unsupported.`);
|
|
155
|
+
if (raw.path !== undefined) {
|
|
156
|
+
finding.path = normalizePath(raw.path, `findings[${index}].path`);
|
|
157
|
+
assertFindingScope(finding.path, scope, requestedPaths, index);
|
|
158
|
+
}
|
|
159
|
+
if (raw.line !== undefined) {
|
|
160
|
+
if (!Number.isInteger(raw.line) || raw.line < 1 || raw.line > 10_000_000) throw resultError(`findings[${index}].line is invalid.`);
|
|
161
|
+
finding.line = raw.line;
|
|
162
|
+
}
|
|
163
|
+
if (raw.ruleId !== undefined) finding.ruleId = assertSafeText(`findings[${index}].ruleId`, raw.ruleId, SECURITY_REVIEW_LIMITS.maxRuleIdChars);
|
|
164
|
+
if (raw.confidence !== undefined) {
|
|
165
|
+
finding.confidence = assertSafeText(`findings[${index}].confidence`, raw.confidence, SECURITY_REVIEW_LIMITS.maxConfidenceChars);
|
|
166
|
+
if (!SECURITY_REVIEW_CONFIDENCE.includes(finding.confidence)) throw resultError(`findings[${index}].confidence is unsupported.`);
|
|
167
|
+
}
|
|
168
|
+
return deepFreeze(finding);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeDiagnostics(raw) {
|
|
172
|
+
if (raw === undefined) return [];
|
|
173
|
+
if (!Array.isArray(raw) || raw.length > SECURITY_REVIEW_LIMITS.maxDiagnostics) throw limitError("diagnostics exceeds its limit.");
|
|
174
|
+
return raw.map((entry, index) => assertSafeText(`diagnostics[${index}]`, entry, SECURITY_REVIEW_LIMITS.maxDiagnosticChars));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function deriveSummary(findings) {
|
|
178
|
+
const bySeverity = Object.fromEntries(SECURITY_REVIEW_SEVERITIES.map((severity) => [severity, 0]));
|
|
179
|
+
const byCategory = Object.fromEntries(SECURITY_REVIEW_CATEGORIES.map((category) => [category, 0]));
|
|
180
|
+
for (const finding of findings) {
|
|
181
|
+
bySeverity[finding.severity] += 1;
|
|
182
|
+
byCategory[finding.category] += 1;
|
|
183
|
+
}
|
|
184
|
+
return deepFreeze({ total: findings.length, bySeverity: deepFreeze(bySeverity), byCategory: deepFreeze(byCategory) });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeSummary(raw, expected) {
|
|
188
|
+
if (raw === undefined) return expected;
|
|
189
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw resultError("summary is invalid.");
|
|
190
|
+
rejectUnknown(raw, ["total", "bySeverity", "byCategory"], "summary");
|
|
191
|
+
if (JSON.stringify(raw) !== JSON.stringify(expected)) throw resultError("summary does not match normalized findings.");
|
|
192
|
+
return expected;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeRequestedPaths(requestedPaths) {
|
|
196
|
+
if (!Array.isArray(requestedPaths)) throw resultError("requestedPaths must be an array.");
|
|
197
|
+
const normalized = requestedPaths.map((path, index) => normalizePath(path, `requestedPaths[${index}]`));
|
|
198
|
+
if (new Set(normalized).size !== normalized.length) throw resultError("requestedPaths must not contain duplicates.");
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function normalizeSecurityReviewResult(raw, {
|
|
203
|
+
provider, taskId, reviewId, scope, requestedPaths = [],
|
|
204
|
+
} = {}) {
|
|
205
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw resultError("security review result must be an object.");
|
|
206
|
+
if (!SECURITY_REVIEW_SCOPES.includes(scope)) throw resultError("scope is unsupported.");
|
|
207
|
+
const normalizedRequestedPaths = normalizeRequestedPaths(requestedPaths);
|
|
208
|
+
if (scope === "SELECTED" && normalizedRequestedPaths.length === 0) {
|
|
209
|
+
throw resultError("SELECTED scope requires requestedPaths.");
|
|
210
|
+
}
|
|
211
|
+
const snapshot = strictSnapshot(raw);
|
|
212
|
+
for (const field of SECURITY_REVIEW_BLOCKED_RESULT_FIELDS) {
|
|
213
|
+
if (Object.prototype.hasOwnProperty.call(snapshot, field)) throw resultError("security review result contains a reserved authority field.", { field });
|
|
214
|
+
}
|
|
215
|
+
rejectUnknown(snapshot, ["findings", "diagnostics", "durationMs", "summary"], "security review result");
|
|
216
|
+
if (!Array.isArray(snapshot.findings)) throw resultError("findings must be an array.");
|
|
217
|
+
if (snapshot.findings.length > SECURITY_REVIEW_LIMITS.maxFindings) throw limitError("Too many findings.", { count: snapshot.findings.length });
|
|
218
|
+
const findings = snapshot.findings.map((finding, index) => normalizeFinding(finding, index, scope, normalizedRequestedPaths));
|
|
219
|
+
const ids = findings.map((finding) => finding.id);
|
|
220
|
+
if (new Set(ids).size !== ids.length) throw resultError("findings must not contain duplicate IDs.");
|
|
221
|
+
const summary = deriveSummary(findings);
|
|
222
|
+
normalizeSummary(snapshot.summary, summary);
|
|
223
|
+
let durationMs;
|
|
224
|
+
if (snapshot.durationMs !== undefined) {
|
|
225
|
+
if (!Number.isInteger(snapshot.durationMs) || snapshot.durationMs < 0 || snapshot.durationMs > SECURITY_REVIEW_LIMITS.maxDurationMs) {
|
|
226
|
+
throw resultError("durationMs is invalid.");
|
|
227
|
+
}
|
|
228
|
+
durationMs = snapshot.durationMs;
|
|
229
|
+
}
|
|
230
|
+
const result = {
|
|
231
|
+
taskId: assertSafeText("taskId", taskId, SECURITY_REVIEW_LIMITS.maxTaskIdChars),
|
|
232
|
+
reviewId: assertSafeText("reviewId", reviewId, SECURITY_REVIEW_LIMITS.maxReviewIdChars),
|
|
233
|
+
scope,
|
|
234
|
+
requestedPaths: Object.freeze(normalizedRequestedPaths),
|
|
235
|
+
provider: deepFreeze({ id: provider.id, ...(provider.version === undefined ? {} : { version: provider.version }) }),
|
|
236
|
+
findings: Object.freeze(findings),
|
|
237
|
+
summary,
|
|
238
|
+
diagnostics: Object.freeze(normalizeDiagnostics(snapshot.diagnostics)),
|
|
239
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
240
|
+
...SECURITY_REVIEW_TRUST,
|
|
241
|
+
};
|
|
242
|
+
const serialized = JSON.stringify(result);
|
|
243
|
+
if (serialized.length > SECURITY_REVIEW_LIMITS.maxResultChars) throw limitError("Security review result exceeds its size limit.", { chars: serialized.length });
|
|
244
|
+
return deepFreeze(result);
|
|
245
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_SECURITY_REVIEW_PROVIDER_INVALID,
|
|
3
|
+
E_SECURITY_REVIEW_PROVIDER_UNAVAILABLE,
|
|
4
|
+
} from "../error-codes.js";
|
|
5
|
+
import { assertPortableContextSafe, deepFreeze, normalizePortableText } from "../portable-context.js";
|
|
6
|
+
import {
|
|
7
|
+
SECURITY_REVIEW_CATEGORIES,
|
|
8
|
+
SECURITY_REVIEW_LIMITS,
|
|
9
|
+
SECURITY_REVIEW_SCOPES,
|
|
10
|
+
securityReviewRequestError,
|
|
11
|
+
} from "./constants.js";
|
|
12
|
+
|
|
13
|
+
export const SECURITY_REVIEW_PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
|
|
14
|
+
|
|
15
|
+
function providerError(message, details = null) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = E_SECURITY_REVIEW_PROVIDER_INVALID;
|
|
18
|
+
error.details = details;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unavailableError(message, details = null) {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
error.code = E_SECURITY_REVIEW_PROVIDER_UNAVAILABLE;
|
|
25
|
+
error.details = details;
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertPlainObject(value, label) {
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
31
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
32
|
+
throw securityReviewRequestError(`${label} must be a plain object.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rejectUnknown(source, allowed, label) {
|
|
38
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
39
|
+
if (typeof key !== "string" || !allowed.includes(key)) {
|
|
40
|
+
throw securityReviewRequestError(`${label}.${String(key)} is not supported.`, { field: String(key) });
|
|
41
|
+
}
|
|
42
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
43
|
+
if (!descriptor || descriptor.get || descriptor.set) {
|
|
44
|
+
throw securityReviewRequestError(`${label}.${key} must not be an accessor.`, { field: key });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function text(label, value, maxLength, { optional = false } = {}) {
|
|
50
|
+
try {
|
|
51
|
+
return normalizePortableText(value, { label, maxLength, optional });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw securityReviewRequestError(`${label} is invalid: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeRelativePath(value, index) {
|
|
58
|
+
const path = text(`paths[${index}]`, value, SECURITY_REVIEW_LIMITS.maxPathChars);
|
|
59
|
+
if (path.startsWith("/") || /^[a-z]:[\\/]/i.test(path) || path.startsWith("\\\\")
|
|
60
|
+
|| /^file:/i.test(path) || path.split(/[\\/]+/).includes("..")) {
|
|
61
|
+
throw securityReviewRequestError(`paths[${index}] must be a project-relative non-escaping path.`);
|
|
62
|
+
}
|
|
63
|
+
const normalized = path.replaceAll("\\", "/");
|
|
64
|
+
if (normalized === "." || normalized.endsWith("/") || normalized.includes("//")) {
|
|
65
|
+
throw securityReviewRequestError(`paths[${index}] must identify a normalized project-relative path.`);
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeStringList(value, { label, maxItems, maxChars, allowed = null } = {}) {
|
|
71
|
+
if (value === undefined) return [];
|
|
72
|
+
if (!Array.isArray(value) || value.length > maxItems) {
|
|
73
|
+
throw securityReviewRequestError(`${label} must be an array with at most ${maxItems} items.`);
|
|
74
|
+
}
|
|
75
|
+
const result = value.map((entry, index) => {
|
|
76
|
+
const normalized = text(`${label}[${index}]`, entry, maxChars);
|
|
77
|
+
if (allowed && !allowed.includes(normalized)) {
|
|
78
|
+
throw securityReviewRequestError(`${label}[${index}] is unsupported.`);
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
});
|
|
82
|
+
if (new Set(result).size !== result.length) throw securityReviewRequestError(`${label} must not contain duplicates.`);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeTimeout(value) {
|
|
87
|
+
if (value === undefined || value === null) return SECURITY_REVIEW_LIMITS.defaultTimeoutMs;
|
|
88
|
+
if (!Number.isInteger(value) || value < 1) throw securityReviewRequestError("timeoutMs must be a positive integer.");
|
|
89
|
+
return Math.min(value, SECURITY_REVIEW_LIMITS.maxTimeoutMs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeRevision(value) {
|
|
93
|
+
if (value === undefined || value === null) return undefined;
|
|
94
|
+
const source = assertPlainObject(value, "revision");
|
|
95
|
+
rejectUnknown(source, ["base", "head"], "revision");
|
|
96
|
+
const base = text("revision.base", source.base, SECURITY_REVIEW_LIMITS.maxRevisionChars, { optional: true });
|
|
97
|
+
const head = text("revision.head", source.head, SECURITY_REVIEW_LIMITS.maxRevisionChars, { optional: true });
|
|
98
|
+
if (base === undefined && head === undefined) throw securityReviewRequestError("revision must contain base or head.");
|
|
99
|
+
return deepFreeze({ ...(base === undefined ? {} : { base }), ...(head === undefined ? {} : { head }) });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeSecurityReviewRequest(input) {
|
|
103
|
+
const source = assertPlainObject(input, "security review request");
|
|
104
|
+
rejectUnknown(source, ["projectPath", "taskId", "reviewId", "scope", "paths", "categories", "requirements", "revision", "timeoutMs"], "security review request");
|
|
105
|
+
const projectPath = text("projectPath", source.projectPath, SECURITY_REVIEW_LIMITS.maxProjectPathChars);
|
|
106
|
+
const taskId = text("taskId", source.taskId, SECURITY_REVIEW_LIMITS.maxTaskIdChars);
|
|
107
|
+
const reviewId = text("reviewId", source.reviewId, SECURITY_REVIEW_LIMITS.maxReviewIdChars);
|
|
108
|
+
const scope = text("scope", source.scope, 32);
|
|
109
|
+
if (!SECURITY_REVIEW_SCOPES.includes(scope)) throw securityReviewRequestError("scope is unsupported.");
|
|
110
|
+
const paths = normalizeStringList(source.paths, {
|
|
111
|
+
label: "paths", maxItems: SECURITY_REVIEW_LIMITS.maxPaths, maxChars: SECURITY_REVIEW_LIMITS.maxPathChars,
|
|
112
|
+
}).map((value, index) => normalizeRelativePath(value, index));
|
|
113
|
+
if (scope === "SELECTED" && paths.length === 0) throw securityReviewRequestError("SELECTED scope requires at least one path.");
|
|
114
|
+
const categories = normalizeStringList(source.categories, {
|
|
115
|
+
label: "categories", maxItems: SECURITY_REVIEW_LIMITS.maxCategories,
|
|
116
|
+
maxChars: SECURITY_REVIEW_LIMITS.maxCategoryChars, allowed: SECURITY_REVIEW_CATEGORIES,
|
|
117
|
+
});
|
|
118
|
+
const requirements = normalizeStringList(source.requirements, {
|
|
119
|
+
label: "requirements", maxItems: SECURITY_REVIEW_LIMITS.maxRequirements,
|
|
120
|
+
maxChars: SECURITY_REVIEW_LIMITS.maxRequirementChars,
|
|
121
|
+
});
|
|
122
|
+
const normalized = {
|
|
123
|
+
projectPath, taskId, reviewId, scope,
|
|
124
|
+
paths: Object.freeze(paths),
|
|
125
|
+
categories: Object.freeze(categories),
|
|
126
|
+
requirements: Object.freeze(requirements),
|
|
127
|
+
timeoutMs: normalizeTimeout(source.timeoutMs),
|
|
128
|
+
...(source.revision === undefined || source.revision === null ? {} : { revision: normalizeRevision(source.revision) }),
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
assertPortableContextSafe(normalized, { label: "security review request" });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw securityReviewRequestError(`security review request failed safety verification: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
return deepFreeze(normalized);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assertProviderDataProperties(provider, label) {
|
|
139
|
+
for (const key of ["id", "version", "review"]) {
|
|
140
|
+
const descriptor = Object.getOwnPropertyDescriptor(provider, key);
|
|
141
|
+
if (descriptor?.get || descriptor?.set) throw providerError(`${label}.${key} must not be an accessor.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function assertSecurityReviewProvider(provider, { label = "security-review provider" } = {}) {
|
|
146
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
147
|
+
throw providerError(`${label} must be an object.`);
|
|
148
|
+
}
|
|
149
|
+
assertProviderDataProperties(provider, label);
|
|
150
|
+
if (typeof provider.id !== "string" || !SECURITY_REVIEW_PROVIDER_ID_PATTERN.test(provider.id)) {
|
|
151
|
+
throw providerError(`${label}.id is invalid.`, { id: provider.id });
|
|
152
|
+
}
|
|
153
|
+
if (typeof provider.review !== "function") throw providerError(`${label}.review must be a function.`);
|
|
154
|
+
if (provider.version !== undefined && typeof provider.version !== "string") {
|
|
155
|
+
throw providerError(`${label}.version must be a string when present.`);
|
|
156
|
+
}
|
|
157
|
+
return provider;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function assertSecurityReviewProviderIdentity(providerIdentity, { label = "security-review provider identity", expectedId } = {}) {
|
|
161
|
+
if (!providerIdentity || typeof providerIdentity !== "object" || Array.isArray(providerIdentity)) {
|
|
162
|
+
throw providerError(`${label} must be an object.`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof providerIdentity.id !== "string" || !SECURITY_REVIEW_PROVIDER_ID_PATTERN.test(providerIdentity.id)) {
|
|
165
|
+
throw providerError(`${label}.id is invalid.`);
|
|
166
|
+
}
|
|
167
|
+
if (expectedId !== undefined && providerIdentity.id !== expectedId) {
|
|
168
|
+
throw providerError(`${label}.id must match registry key "${expectedId}".`);
|
|
169
|
+
}
|
|
170
|
+
return deepFreeze({ id: providerIdentity.id, ...(providerIdentity.version === undefined ? {} : { version: providerIdentity.version }) });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function assertSecurityReviewProviderRegistration(provider, expectedId, options = {}) {
|
|
174
|
+
const validated = assertSecurityReviewProvider(provider, options);
|
|
175
|
+
if (validated.id !== expectedId) throw providerError(`Provider id "${validated.id}" does not match registry key "${expectedId}".`);
|
|
176
|
+
return validated;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function createSecurityReviewProviderRegistry(providers = {}) {
|
|
180
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
|
|
181
|
+
throw providerError("Security-review provider registry source must be an object or Map.");
|
|
182
|
+
}
|
|
183
|
+
const entries = providers instanceof Map ? [...providers.entries()] : Object.entries(providers);
|
|
184
|
+
const registry = {};
|
|
185
|
+
for (const [name, provider] of entries) {
|
|
186
|
+
if (typeof name !== "string" || !SECURITY_REVIEW_PROVIDER_ID_PATTERN.test(name)) {
|
|
187
|
+
throw providerError(`Invalid security-review provider ID: ${name}.`);
|
|
188
|
+
}
|
|
189
|
+
if (typeof provider === "function") registry[name] = provider;
|
|
190
|
+
else registry[name] = assertSecurityReviewProviderRegistration(provider, name, { label: `security-review provider "${name}"` });
|
|
191
|
+
}
|
|
192
|
+
return deepFreeze({ ...registry });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function resolveSecurityReviewProvider(providers, name, input = {}) {
|
|
196
|
+
if (typeof name !== "string" || !SECURITY_REVIEW_PROVIDER_ID_PATTERN.test(name)) {
|
|
197
|
+
throw unavailableError("Security-review provider name is invalid.", { name });
|
|
198
|
+
}
|
|
199
|
+
const registry = providers ?? {};
|
|
200
|
+
const entry = registry instanceof Map ? registry.get(name) : registry[name];
|
|
201
|
+
if (!entry) throw unavailableError(`Security-review provider "${name}" is not registered.`, { name });
|
|
202
|
+
const provider = typeof entry === "function" ? await entry(input) : entry;
|
|
203
|
+
return assertSecurityReviewProviderRegistration(provider, name, { label: `security-review provider "${name}"` });
|
|
204
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_SECURITY_REVIEW_CANCELLED,
|
|
3
|
+
E_SECURITY_REVIEW_EXECUTION_FAILED,
|
|
4
|
+
E_SECURITY_REVIEW_OUTPUT_LIMIT,
|
|
5
|
+
E_SECURITY_REVIEW_PROVIDER_INVALID,
|
|
6
|
+
E_SECURITY_REVIEW_PROVIDER_UNAVAILABLE,
|
|
7
|
+
E_SECURITY_REVIEW_REQUEST_INVALID,
|
|
8
|
+
E_SECURITY_REVIEW_RESULT_INVALID,
|
|
9
|
+
E_SECURITY_REVIEW_TIMEOUT,
|
|
10
|
+
} from "../error-codes.js";
|
|
11
|
+
import { normalizeSecurityReviewRequest, resolveSecurityReviewProvider } from "./provider.js";
|
|
12
|
+
import { SECURITY_REVIEW_LIMITS } from "./constants.js";
|
|
13
|
+
import { normalizeSecurityReviewResult } from "./normalize.js";
|
|
14
|
+
|
|
15
|
+
function error(code, message, details = null) {
|
|
16
|
+
const output = new Error(message);
|
|
17
|
+
output.code = code;
|
|
18
|
+
output.details = details;
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timeoutError(providerName, reviewId) {
|
|
23
|
+
return error(E_SECURITY_REVIEW_TIMEOUT, `Security review provider "${providerName}" timed out.`, { provider: providerName, reviewId });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cancelledError(providerName, reviewId) {
|
|
27
|
+
return error(E_SECURITY_REVIEW_CANCELLED, `Security review provider "${providerName}" was cancelled.`, { provider: providerName, reviewId });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeFailure(providerName) {
|
|
31
|
+
return error(E_SECURITY_REVIEW_EXECUTION_FAILED, `Security review provider "${providerName}" failed.`, { provider: providerName });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function bounded(factory, { controller, callerSignal, deadline, providerName, reviewId }) {
|
|
35
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
36
|
+
if (callerSignal?.aborted) {
|
|
37
|
+
controller.abort();
|
|
38
|
+
throw cancelledError(providerName, reviewId);
|
|
39
|
+
}
|
|
40
|
+
if (remaining() <= 0) {
|
|
41
|
+
controller.abort();
|
|
42
|
+
throw timeoutError(providerName, reviewId);
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
let settled = false;
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
49
|
+
};
|
|
50
|
+
const finish = (callback, value) => {
|
|
51
|
+
if (settled) return;
|
|
52
|
+
settled = true;
|
|
53
|
+
cleanup();
|
|
54
|
+
callback(value);
|
|
55
|
+
};
|
|
56
|
+
const onAbort = () => {
|
|
57
|
+
controller.abort();
|
|
58
|
+
finish(reject, cancelledError(providerName, reviewId));
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
controller.abort();
|
|
62
|
+
finish(reject, timeoutError(providerName, reviewId));
|
|
63
|
+
}, remaining());
|
|
64
|
+
callerSignal?.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
Promise.resolve()
|
|
66
|
+
.then(() => factory(remaining()))
|
|
67
|
+
.then((value) => finish(resolve, value), (failure) => finish(reject, failure));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function runSecurityReview({
|
|
72
|
+
projectPath, taskId, providerName, reviewId, scope, paths, categories, requirements, revision,
|
|
73
|
+
timeoutMs, runtimeContext, signal,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const normalized = normalizeSecurityReviewRequest({
|
|
76
|
+
projectPath, taskId, reviewId, scope, paths, categories, requirements, revision, timeoutMs,
|
|
77
|
+
});
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const deadline = Date.now() + normalized.timeoutMs;
|
|
80
|
+
const providers = runtimeContext?.securityReviewProviders;
|
|
81
|
+
try {
|
|
82
|
+
const provider = await bounded(
|
|
83
|
+
(remaining) => resolveSecurityReviewProvider(providers, providerName, {
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
timeoutMs: remaining,
|
|
86
|
+
request: normalized,
|
|
87
|
+
}),
|
|
88
|
+
{ controller, callerSignal: signal, deadline, providerName, reviewId: normalized.reviewId },
|
|
89
|
+
);
|
|
90
|
+
const raw = await bounded(
|
|
91
|
+
(remaining) => provider.review(Object.freeze({ ...normalized, signal: controller.signal, timeoutMs: remaining })),
|
|
92
|
+
{ controller, callerSignal: signal, deadline, providerName, reviewId: normalized.reviewId },
|
|
93
|
+
);
|
|
94
|
+
if (controller.signal.aborted) {
|
|
95
|
+
if (signal?.aborted) throw cancelledError(providerName, normalized.reviewId);
|
|
96
|
+
throw timeoutError(providerName, normalized.reviewId);
|
|
97
|
+
}
|
|
98
|
+
if (Date.now() >= deadline) {
|
|
99
|
+
controller.abort();
|
|
100
|
+
throw timeoutError(providerName, normalized.reviewId);
|
|
101
|
+
}
|
|
102
|
+
const result = normalizeSecurityReviewResult(raw, {
|
|
103
|
+
provider,
|
|
104
|
+
taskId: normalized.taskId,
|
|
105
|
+
reviewId: normalized.reviewId,
|
|
106
|
+
scope: normalized.scope,
|
|
107
|
+
requestedPaths: normalized.paths,
|
|
108
|
+
});
|
|
109
|
+
if (Date.now() >= deadline) {
|
|
110
|
+
controller.abort();
|
|
111
|
+
throw timeoutError(providerName, normalized.reviewId);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
} catch (failure) {
|
|
115
|
+
if ([
|
|
116
|
+
E_SECURITY_REVIEW_REQUEST_INVALID,
|
|
117
|
+
E_SECURITY_REVIEW_RESULT_INVALID,
|
|
118
|
+
E_SECURITY_REVIEW_OUTPUT_LIMIT,
|
|
119
|
+
E_SECURITY_REVIEW_TIMEOUT,
|
|
120
|
+
E_SECURITY_REVIEW_CANCELLED,
|
|
121
|
+
E_SECURITY_REVIEW_PROVIDER_INVALID,
|
|
122
|
+
].includes(failure?.code)) {
|
|
123
|
+
if ([E_SECURITY_REVIEW_TIMEOUT, E_SECURITY_REVIEW_CANCELLED].includes(failure.code)) controller.abort();
|
|
124
|
+
throw failure;
|
|
125
|
+
}
|
|
126
|
+
if (failure?.code === E_SECURITY_REVIEW_PROVIDER_UNAVAILABLE
|
|
127
|
+
&& failure.message.includes("not registered")) throw failure;
|
|
128
|
+
controller.abort();
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
130
|
+
throw safeFailure(providerName);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { SECURITY_REVIEW_LIMITS };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const FAILURE_CLASSES = Object.freeze([
|
|
2
|
+
"incorrect implementation", "stale expectation", "race condition", "state divergence",
|
|
3
|
+
"dependency incompatibility", "configuration defect", "fixture defect", "timeout",
|
|
4
|
+
"network/environment", "serialization mismatch", "ownership/protocol mismatch", "unknown",
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export const DIAGNOSIS_STRATEGIES = Object.freeze([
|
|
8
|
+
"inspect caller", "inspect callee", "inspect state transition", "inspect dependency behavior",
|
|
9
|
+
"inspect config", "inspect concurrency path", "inspect fixture", "inspect package boundary",
|
|
10
|
+
"run narrower experiment", "run independent checker", "inspect historical regression",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const REVIEW_FOCUSES = Object.freeze([
|
|
14
|
+
"needs_security_review", "needs_api_compatibility_review", "needs_concurrency_review",
|
|
15
|
+
"needs_package_review", "needs_docs_review", "needs_migration_review",
|
|
16
|
+
"needs_performance_review", "needs_dependency_review", "needs_full_diff_context",
|
|
17
|
+
"needs_primary_model",
|
|
18
|
+
]);
|
|
19
|
+
|