@hongmaple0820/scale-engine 0.49.0 → 0.50.2
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/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/api/DashboardHttpConfig.d.ts +28 -0
- package/dist/api/DashboardHttpConfig.js +110 -0
- package/dist/api/DashboardHttpConfig.js.map +1 -0
- package/dist/api/cli.js +102 -11
- package/dist/api/cli.js.map +1 -1
- package/dist/api/http.d.ts +1 -0
- package/dist/api/http.js +52 -0
- package/dist/api/http.js.map +1 -0
- package/dist/artifact/types.d.ts +5 -0
- package/dist/artifact/types.js.map +1 -1
- package/dist/bootstrap/DependencyBootstrap.d.ts +1 -0
- package/dist/bootstrap/DependencyBootstrap.js +14 -3
- package/dist/bootstrap/DependencyBootstrap.js.map +1 -1
- package/dist/cli/cortexApplyCommand.d.ts +26 -0
- package/dist/cli/cortexApplyCommand.js +74 -0
- package/dist/cli/cortexApplyCommand.js.map +1 -0
- package/dist/cli/cortexCandidateCommands.d.ts +42 -0
- package/dist/cli/cortexCandidateCommands.js +119 -0
- package/dist/cli/cortexCandidateCommands.js.map +1 -0
- package/dist/cli/cortexCommands.d.ts +31 -0
- package/dist/cli/cortexCommands.js +102 -17
- package/dist/cli/cortexCommands.js.map +1 -1
- package/dist/cli/engineBootstrap.d.ts +1 -1
- package/dist/cli/engineBootstrap.js +2 -0
- package/dist/cli/engineBootstrap.js.map +1 -1
- package/dist/cli/evalCommands.js +1 -0
- package/dist/cli/evalCommands.js.map +1 -1
- package/dist/cli/phaseCommands.d.ts +28 -0
- package/dist/cli/phaseCommands.js +148 -9
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/cli/runtimeSkillCommands.js +12 -2
- package/dist/cli/runtimeSkillCommands.js.map +1 -1
- package/dist/cli/shieldCommands.d.ts +1 -0
- package/dist/cli/shieldCommands.js +20 -7
- package/dist/cli/shieldCommands.js.map +1 -1
- package/dist/cli/workflowEvidenceCommands.d.ts +120 -0
- package/dist/cli/workflowEvidenceCommands.js +228 -2
- package/dist/cli/workflowEvidenceCommands.js.map +1 -1
- package/dist/cortex/AutoFixEventObservations.d.ts +11 -0
- package/dist/cortex/AutoFixEventObservations.js +72 -0
- package/dist/cortex/AutoFixEventObservations.js.map +1 -0
- package/dist/cortex/GateEvidenceObservations.d.ts +22 -0
- package/dist/cortex/GateEvidenceObservations.js +179 -0
- package/dist/cortex/GateEvidenceObservations.js.map +1 -0
- package/dist/cortex/GovernanceMetrics.d.ts +2 -0
- package/dist/cortex/GovernanceMetrics.js +112 -22
- package/dist/cortex/GovernanceMetrics.js.map +1 -1
- package/dist/cortex/InstinctApplicationRecorder.d.ts +28 -0
- package/dist/cortex/InstinctApplicationRecorder.js +145 -0
- package/dist/cortex/InstinctApplicationRecorder.js.map +1 -0
- package/dist/cortex/InstinctCandidateAudit.d.ts +3 -0
- package/dist/cortex/InstinctCandidateAudit.js +39 -0
- package/dist/cortex/InstinctCandidateAudit.js.map +1 -0
- package/dist/cortex/InstinctCandidateReview.d.ts +32 -0
- package/dist/cortex/InstinctCandidateReview.js +125 -0
- package/dist/cortex/InstinctCandidateReview.js.map +1 -0
- package/dist/cortex/InstinctExtractor.d.ts +1 -0
- package/dist/cortex/InstinctExtractor.js +24 -17
- package/dist/cortex/InstinctExtractor.js.map +1 -1
- package/dist/cortex/InstinctRuntimeEvidence.d.ts +14 -0
- package/dist/cortex/InstinctRuntimeEvidence.js +120 -0
- package/dist/cortex/InstinctRuntimeEvidence.js.map +1 -0
- package/dist/cortex/InstinctStore.d.ts +31 -4
- package/dist/cortex/InstinctStore.js +120 -20
- package/dist/cortex/InstinctStore.js.map +1 -1
- package/dist/cortex/SessionInjector.d.ts +1 -0
- package/dist/cortex/SessionInjector.js +54 -4
- package/dist/cortex/SessionInjector.js.map +1 -1
- package/dist/dashboard/DashboardServer.d.ts +237 -0
- package/dist/dashboard/DashboardServer.js +1083 -19
- package/dist/dashboard/DashboardServer.js.map +1 -1
- package/dist/dashboard/spa/assets/index-VYBCLBje.js +11 -0
- package/dist/dashboard/spa/assets/index-VhwY_ac1.css +1 -0
- package/dist/dashboard/spa/assets/naive-ui-BQy2AJkt.js +3340 -0
- package/dist/dashboard/spa/assets/vendor-BPU6aOYA.js +3 -0
- package/dist/dashboard/spa/assets/vue-CQQMb5Wi.js +17 -0
- package/dist/dashboard/spa/index.html +16 -0
- package/dist/env/EnvironmentDoctor.js +12 -7
- package/dist/env/EnvironmentDoctor.js.map +1 -1
- package/dist/eval/WorkflowEval.d.ts +9 -0
- package/dist/eval/WorkflowEval.js +348 -2
- package/dist/eval/WorkflowEval.js.map +1 -1
- package/dist/memory/MemoryBrain.d.ts +13 -0
- package/dist/memory/MemoryBrain.js +47 -0
- package/dist/memory/MemoryBrain.js.map +1 -1
- package/dist/memory/MemoryFabric.d.ts +14 -1
- package/dist/memory/MemoryFabric.js +72 -8
- package/dist/memory/MemoryFabric.js.map +1 -1
- package/dist/memory/MemoryLearning.d.ts +1 -0
- package/dist/memory/MemoryLearning.js +6 -3
- package/dist/memory/MemoryLearning.js.map +1 -1
- package/dist/memory/MemoryProviders.d.ts +8 -1
- package/dist/memory/MemoryProviders.js +143 -29
- package/dist/memory/MemoryProviders.js.map +1 -1
- package/dist/runtime/AiOsRuntime.d.ts +14 -1
- package/dist/runtime/AiOsRuntime.js +59 -3
- package/dist/runtime/AiOsRuntime.js.map +1 -1
- package/dist/runtime/RuntimeDoctor.js +3 -1
- package/dist/runtime/RuntimeDoctor.js.map +1 -1
- package/dist/runtime/RuntimeEvidenceLedger.d.ts +6 -0
- package/dist/runtime/RuntimeEvidenceLedger.js +52 -1
- package/dist/runtime/RuntimeEvidenceLedger.js.map +1 -1
- package/dist/runtime/SessionLedger.d.ts +2 -0
- package/dist/runtime/SessionLedger.js +4 -0
- package/dist/runtime/SessionLedger.js.map +1 -1
- package/dist/setup/SetupVerification.js +53 -5
- package/dist/setup/SetupVerification.js.map +1 -1
- package/dist/shield/PolicyCompiler.js +73 -12
- package/dist/shield/PolicyCompiler.js.map +1 -1
- package/dist/shield/ProtectedPaths.js +4 -2
- package/dist/shield/ProtectedPaths.js.map +1 -1
- package/dist/skills/SkillCatalog.d.ts +2 -0
- package/dist/skills/SkillCatalog.js +8 -0
- package/dist/skills/SkillCatalog.js.map +1 -1
- package/dist/skills/SkillDoctor.d.ts +19 -2
- package/dist/skills/SkillDoctor.js +163 -13
- package/dist/skills/SkillDoctor.js.map +1 -1
- package/dist/tools/SafeCommandRunner.d.ts +1 -0
- package/dist/tools/SafeCommandRunner.js +1 -0
- package/dist/tools/SafeCommandRunner.js.map +1 -1
- package/dist/tools/ToolCapabilityRegistry.js +25 -3
- package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
- package/dist/tools/ToolOrchestrator.js +21 -0
- package/dist/tools/ToolOrchestrator.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow/AgentLoopReadiness.d.ts +103 -0
- package/dist/workflow/AgentLoopReadiness.js +371 -0
- package/dist/workflow/AgentLoopReadiness.js.map +1 -0
- package/dist/workflow/EcosystemReadinessGate.d.ts +46 -0
- package/dist/workflow/EcosystemReadinessGate.js +126 -0
- package/dist/workflow/EcosystemReadinessGate.js.map +1 -0
- package/dist/workflow/EngineeringStandards.js +48 -3
- package/dist/workflow/EngineeringStandards.js.map +1 -1
- package/dist/workflow/GateCatalog.js +9 -0
- package/dist/workflow/GateCatalog.js.map +1 -1
- package/dist/workflow/GovernanceTemplatePacks.js +2 -26
- package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
- package/dist/workflow/GovernanceTemplates.js +8 -1
- package/dist/workflow/GovernanceTemplates.js.map +1 -1
- package/dist/workflow/ReleaseDeploymentLedger.d.ts +63 -0
- package/dist/workflow/ReleaseDeploymentLedger.js +154 -0
- package/dist/workflow/ReleaseDeploymentLedger.js.map +1 -0
- package/dist/workflow/ReviewAnalyzer.js +50 -3
- package/dist/workflow/ReviewAnalyzer.js.map +1 -1
- package/dist/workflow/SessionPreamble.d.ts +7 -0
- package/dist/workflow/SessionPreamble.js +48 -9
- package/dist/workflow/SessionPreamble.js.map +1 -1
- package/dist/workflow/VerificationCommands.d.ts +1 -0
- package/dist/workflow/VerificationCommands.js.map +1 -1
- package/dist/workflow/VerificationProfile.d.ts +5 -0
- package/dist/workflow/VerificationProfile.js +26 -0
- package/dist/workflow/VerificationProfile.js.map +1 -1
- package/dist/workflow/VerificationSchema.d.ts +3 -0
- package/dist/workflow/VerificationSchema.js +6 -0
- package/dist/workflow/VerificationSchema.js.map +1 -1
- package/dist/workflow/WorkflowEffectiveness.d.ts +97 -0
- package/dist/workflow/WorkflowEffectiveness.js +302 -0
- package/dist/workflow/WorkflowEffectiveness.js.map +1 -0
- package/dist/workflow/WorkflowEffectivenessRenderer.d.ts +2 -0
- package/dist/workflow/WorkflowEffectivenessRenderer.js +67 -0
- package/dist/workflow/WorkflowEffectivenessRenderer.js.map +1 -0
- package/dist/workflow/WorkflowEffectivenessScoring.d.ts +6 -0
- package/dist/workflow/WorkflowEffectivenessScoring.js +243 -0
- package/dist/workflow/WorkflowEffectivenessScoring.js.map +1 -0
- package/dist/workflow/gates/GateSystem.d.ts +16 -0
- package/dist/workflow/gates/GateSystem.js +208 -41
- package/dist/workflow/gates/GateSystem.js.map +1 -1
- package/dist/workflow/gates/MetaGovernanceGates.js +269 -8
- package/dist/workflow/gates/MetaGovernanceGates.js.map +1 -1
- package/docs/reference/cli.md +2 -1
- package/docs/start/agent-governance-demo.md +1 -1
- package/docs/workflow/ASSESSMENT_INDEX.md +326 -0
- package/docs/workflow/COMPARATIVE_ANALYSIS.md +422 -0
- package/docs/workflow/EXECUTIVE_SUMMARY.md +310 -0
- package/docs/workflow/IMPROVEMENT_CHECKLIST.md +518 -0
- package/docs/workflow/IMPROVEMENT_ROADMAP.md +707 -0
- package/docs/workflow/README.md +9 -1
- package/docs/workflow/templates/github-actions-scale-preflight.yml +4 -1
- package/package.json +10 -3
- package/scripts/workflow/run-vitest.mjs +123 -0
|
@@ -7,12 +7,32 @@ import { registerMetaGovernanceGates } from './MetaGovernanceGates.js';
|
|
|
7
7
|
import { registerEnhancedGates } from './EnhancedGates.js';
|
|
8
8
|
import { META_GOVERNANCE_GATE_STAGES, ENHANCED_GATE_STAGES } from '../GateCatalog.js';
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
+
import { mkdtempSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
10
13
|
import { RuntimeEvidenceLedger } from '../../runtime/RuntimeEvidenceLedger.js';
|
|
11
14
|
import { compressCommandOutput } from '../../tools/CommandOutputCompressor.js';
|
|
12
15
|
import { CommandRunLedger } from '../../tools/CommandRunLedger.js';
|
|
13
16
|
import { auditDependencies } from '../../guardrails/DependencyAuditor.js';
|
|
14
17
|
import { runSafeCommand } from '../../tools/SafeCommandRunner.js';
|
|
15
18
|
import { logger } from '../../core/logger.js';
|
|
19
|
+
const DEFAULT_BUILD_TIMEOUT_MS = 120_000;
|
|
20
|
+
const DEFAULT_LINT_TIMEOUT_MS = 60_000;
|
|
21
|
+
const DEFAULT_TEST_TIMEOUT_MS = 900_000;
|
|
22
|
+
const DEFAULT_COVERAGE_TIMEOUT_MS = 300_000;
|
|
23
|
+
const DEFAULT_PRODUCT_SMOKE_TIMEOUT_MS = 180_000;
|
|
24
|
+
const GATE_TIMEOUT_ENV = {
|
|
25
|
+
G0: 'SCALE_GATE_BUILD_TIMEOUT_MS',
|
|
26
|
+
G4: 'SCALE_GATE_LINT_TIMEOUT_MS',
|
|
27
|
+
G5: 'SCALE_GATE_TEST_TIMEOUT_MS',
|
|
28
|
+
G6: 'SCALE_GATE_COVERAGE_TIMEOUT_MS',
|
|
29
|
+
G8: 'SCALE_GATE_SMOKE_TIMEOUT_MS',
|
|
30
|
+
};
|
|
31
|
+
export function resolveGateTimeoutMs(stage, fallbackMs) {
|
|
32
|
+
const value = process.env[GATE_TIMEOUT_ENV[stage] ?? ''] ?? process.env.SCALE_GATE_COMMAND_TIMEOUT_MS;
|
|
33
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
34
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
|
|
35
|
+
}
|
|
16
36
|
function tail(value, maxLength = 1000) {
|
|
17
37
|
return value.length > maxLength ? value.slice(-maxLength) : value;
|
|
18
38
|
}
|
|
@@ -22,7 +42,7 @@ function sha256(value) {
|
|
|
22
42
|
export async function runShellCommand(command, timeout, cwd = process.cwd(), options = {}) {
|
|
23
43
|
const start = Date.now();
|
|
24
44
|
try {
|
|
25
|
-
const result = await runSafeCommand(command, { timeout, cwd });
|
|
45
|
+
const result = await runSafeCommand(command, { timeout, cwd, env: options.env });
|
|
26
46
|
const end = Date.now();
|
|
27
47
|
return finalizeCommandResult(command, {
|
|
28
48
|
code: result.exitCode,
|
|
@@ -98,8 +118,10 @@ export class GateSystem {
|
|
|
98
118
|
this.gates = new Map();
|
|
99
119
|
this.results = new Map();
|
|
100
120
|
this.eventBus = eventBus;
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
121
|
+
this.rootDir = commandConfig.cwd ?? process.cwd();
|
|
122
|
+
this.scaleDir = commandConfig.scaleDir ?? commandConfig.runtimeEvidence?.scaleDir ?? '.scale';
|
|
123
|
+
this.evidenceStore = new EvidenceStore(this.scaleDir);
|
|
124
|
+
this.commands = detectVerificationCommands(this.rootDir, commandConfig);
|
|
103
125
|
this.artifactWriter = artifactWriter ?? new WorkflowArtifactWriter();
|
|
104
126
|
this.registerDefaultGates();
|
|
105
127
|
}
|
|
@@ -132,9 +154,10 @@ export class GateSystem {
|
|
|
132
154
|
if (cacheableGates.includes(stage)) {
|
|
133
155
|
try {
|
|
134
156
|
const { ScanCache } = await import('../../cache/ScanCache.js');
|
|
135
|
-
const scanCache = new ScanCache();
|
|
157
|
+
const scanCache = new ScanCache(this.rootDir);
|
|
136
158
|
const changedFiles = await this.getChangedFiles();
|
|
137
159
|
const fileHashes = scanCache.hashFiles(changedFiles);
|
|
160
|
+
fileHashes['__gate_context__'] = this.cacheContextForGate(stage);
|
|
138
161
|
const cacheKey = scanCache.computeKey(fileHashes);
|
|
139
162
|
const cached = scanCache.get(stage, cacheKey);
|
|
140
163
|
if (cached) {
|
|
@@ -192,7 +215,8 @@ export class GateSystem {
|
|
|
192
215
|
return result;
|
|
193
216
|
}
|
|
194
217
|
}
|
|
195
|
-
catch {
|
|
218
|
+
catch (err) {
|
|
219
|
+
logger.debug({ err, stage }, 'Gate cache lookup failed; executing gate directly');
|
|
196
220
|
// Cache infrastructure failed — fall through to direct execution
|
|
197
221
|
}
|
|
198
222
|
}
|
|
@@ -250,7 +274,8 @@ export class GateSystem {
|
|
|
250
274
|
const record = this.evidenceStore.saveGateResult(result);
|
|
251
275
|
result.evidenceRecordId = record.id;
|
|
252
276
|
}
|
|
253
|
-
catch {
|
|
277
|
+
catch (err) {
|
|
278
|
+
logger.debug({ err, gate: result.gate }, 'Gate evidence persistence failed');
|
|
254
279
|
// Evidence persistence must not mask the gate decision itself.
|
|
255
280
|
}
|
|
256
281
|
}
|
|
@@ -309,14 +334,36 @@ export class GateSystem {
|
|
|
309
334
|
}
|
|
310
335
|
async getChangedFiles() {
|
|
311
336
|
const { execSync } = await import('node:child_process');
|
|
337
|
+
const { resolve } = await import('node:path');
|
|
312
338
|
try {
|
|
313
|
-
const output = execSync('git diff --name-only HEAD', { encoding: 'utf-8', stdio: 'pipe' });
|
|
314
|
-
return output.trim().split('\n').filter(Boolean);
|
|
339
|
+
const output = execSync('git diff --name-only HEAD', { cwd: this.rootDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
340
|
+
return output.trim().split('\n').filter(Boolean).map(file => resolve(this.rootDir, file));
|
|
315
341
|
}
|
|
316
342
|
catch {
|
|
317
343
|
return [];
|
|
318
344
|
}
|
|
319
345
|
}
|
|
346
|
+
cacheContextForGate(stage) {
|
|
347
|
+
const commandByStage = {
|
|
348
|
+
G0: this.commands.build,
|
|
349
|
+
G4: this.commands.lint,
|
|
350
|
+
G5: this.commands.test,
|
|
351
|
+
G6: this.commands.coverage,
|
|
352
|
+
};
|
|
353
|
+
const command = commandByStage[stage];
|
|
354
|
+
return JSON.stringify({
|
|
355
|
+
gateCacheSchema: 3,
|
|
356
|
+
rootDir: this.rootDir,
|
|
357
|
+
scaleDir: this.scaleDir,
|
|
358
|
+
stage,
|
|
359
|
+
command: command?.command,
|
|
360
|
+
source: command?.source,
|
|
361
|
+
reason: command?.reason,
|
|
362
|
+
cwd: command?.cwd,
|
|
363
|
+
tddStrict: this.commands.tddStrict,
|
|
364
|
+
tddEvidence: this.commands.tddEvidence,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
320
367
|
registerDefaultGates() {
|
|
321
368
|
this.registerGate(new ExplorationGate(this.artifactWriter));
|
|
322
369
|
this.registerGate(new PlanningGate(this.artifactWriter));
|
|
@@ -325,7 +372,11 @@ export class GateSystem {
|
|
|
325
372
|
this.registerGate(new LintGate(this.commands.lint, this.commands.runtimeEvidence));
|
|
326
373
|
this.registerGate(new TestGate(this.commands.test, this.commands.runtimeEvidence));
|
|
327
374
|
this.registerGate(new CoverageGate(this.commands.coverage, this.commands.runtimeEvidence));
|
|
328
|
-
this.registerGate(new SecurityGate(
|
|
375
|
+
this.registerGate(new SecurityGate({
|
|
376
|
+
rootDir: this.rootDir,
|
|
377
|
+
scaleDir: this.scaleDir,
|
|
378
|
+
changedFilesProvider: () => this.getChangedFiles(),
|
|
379
|
+
}));
|
|
329
380
|
this.registerGate(new ProductSmokeGate(this.commands.smoke, this.commands.runtimeEvidence));
|
|
330
381
|
}
|
|
331
382
|
}
|
|
@@ -385,10 +436,16 @@ function commandEvidence(label, command, passed, commandResult, fallbackDetail =
|
|
|
385
436
|
});
|
|
386
437
|
}
|
|
387
438
|
function gateCommandOptions(stage, command, runtimeEvidence) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
439
|
+
const options = {};
|
|
440
|
+
if (stage === 'G5') {
|
|
441
|
+
const isolatedScaleDir = mkdtempSync(join(tmpdir(), 'scale-g5-'));
|
|
442
|
+
options.env = {
|
|
443
|
+
SCALE_DIR: isolatedScaleDir,
|
|
444
|
+
SCALE_PROJECT_DIR: runtimeEvidence?.projectDir ?? command.cwd ?? process.cwd(),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (runtimeEvidence) {
|
|
448
|
+
options.commandRunEvidence = {
|
|
392
449
|
projectDir: runtimeEvidence.projectDir ?? command.cwd ?? process.cwd(),
|
|
393
450
|
scaleDir: runtimeEvidence.scaleDir,
|
|
394
451
|
taskId: runtimeEvidence.taskId,
|
|
@@ -396,8 +453,9 @@ function gateCommandOptions(stage, command, runtimeEvidence) {
|
|
|
396
453
|
profile: runtimeEvidence.profile,
|
|
397
454
|
gate: stage,
|
|
398
455
|
source: command.source,
|
|
399
|
-
}
|
|
400
|
-
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return options;
|
|
401
459
|
}
|
|
402
460
|
export class ExplorationGate {
|
|
403
461
|
constructor(artifactWriter) {
|
|
@@ -509,7 +567,8 @@ export class ExplorationGate {
|
|
|
509
567
|
await fs.access(candidate);
|
|
510
568
|
return candidate;
|
|
511
569
|
}
|
|
512
|
-
catch {
|
|
570
|
+
catch (err) {
|
|
571
|
+
logger.debug({ err, candidate }, 'Knowledge file candidate not found');
|
|
513
572
|
// Try the next platform-specific knowledge file.
|
|
514
573
|
}
|
|
515
574
|
}
|
|
@@ -522,7 +581,8 @@ export class ExplorationGate {
|
|
|
522
581
|
await fs.access(candidate);
|
|
523
582
|
return true;
|
|
524
583
|
}
|
|
525
|
-
catch {
|
|
584
|
+
catch (err) {
|
|
585
|
+
logger.debug({ err, candidate }, 'Knowledge graph artifact candidate not found');
|
|
526
586
|
// Try the next graphify artifact candidate.
|
|
527
587
|
}
|
|
528
588
|
}
|
|
@@ -595,7 +655,8 @@ export class PlanningGate {
|
|
|
595
655
|
const entries = await fs.readdir(specDir);
|
|
596
656
|
return entries.some(entry => entry.endsWith('.md'));
|
|
597
657
|
}
|
|
598
|
-
catch {
|
|
658
|
+
catch (err) {
|
|
659
|
+
logger.debug({ err }, 'Security scan skipped unreadable path');
|
|
599
660
|
return false;
|
|
600
661
|
}
|
|
601
662
|
}
|
|
@@ -738,7 +799,7 @@ export class BuildGate {
|
|
|
738
799
|
const blockers = [];
|
|
739
800
|
let commandResult = null;
|
|
740
801
|
try {
|
|
741
|
-
commandResult = await runShellCommand(this.command.command,
|
|
802
|
+
commandResult = await runShellCommand(this.command.command, resolveGateTimeoutMs(this.stage, DEFAULT_BUILD_TIMEOUT_MS), this.command.cwd, gateCommandOptions(this.stage, this.command, this.runtimeEvidence));
|
|
742
803
|
if (commandResult.code !== 0) {
|
|
743
804
|
blockers.push(`Build failed: ${commandResult.stderr}`);
|
|
744
805
|
}
|
|
@@ -776,7 +837,7 @@ export class LintGate {
|
|
|
776
837
|
const blockers = [];
|
|
777
838
|
let commandResult = null;
|
|
778
839
|
try {
|
|
779
|
-
commandResult = await runShellCommand(this.command.command,
|
|
840
|
+
commandResult = await runShellCommand(this.command.command, resolveGateTimeoutMs(this.stage, DEFAULT_LINT_TIMEOUT_MS), this.command.cwd, gateCommandOptions(this.stage, this.command, this.runtimeEvidence));
|
|
780
841
|
if (commandResult.code !== 0) {
|
|
781
842
|
blockers.push(`Lint failed: ${commandResult.stderr}`);
|
|
782
843
|
}
|
|
@@ -814,7 +875,7 @@ export class TestGate {
|
|
|
814
875
|
const blockers = [];
|
|
815
876
|
let commandResult = null;
|
|
816
877
|
try {
|
|
817
|
-
commandResult = await runShellCommand(this.command.command,
|
|
878
|
+
commandResult = await runShellCommand(this.command.command, resolveGateTimeoutMs(this.stage, DEFAULT_TEST_TIMEOUT_MS), this.command.cwd, gateCommandOptions(this.stage, this.command, this.runtimeEvidence));
|
|
818
879
|
if (commandResult.code !== 0) {
|
|
819
880
|
blockers.push(`Tests failed: ${commandResult.stderr}`);
|
|
820
881
|
}
|
|
@@ -853,13 +914,12 @@ export class CoverageGate {
|
|
|
853
914
|
let detail = '';
|
|
854
915
|
let commandResult = null;
|
|
855
916
|
try {
|
|
856
|
-
commandResult = await runShellCommand(this.command.command,
|
|
917
|
+
commandResult = await runShellCommand(this.command.command, resolveGateTimeoutMs(this.stage, DEFAULT_COVERAGE_TIMEOUT_MS), this.command.cwd, gateCommandOptions(this.stage, this.command, this.runtimeEvidence));
|
|
857
918
|
if (commandResult.code !== 0) {
|
|
858
919
|
blockers.push(`Coverage command failed: ${commandResult.stderr}`);
|
|
859
920
|
}
|
|
860
|
-
const
|
|
861
|
-
if (
|
|
862
|
-
const coverage = parseFloat(coverageMatch[1]);
|
|
921
|
+
const coverage = parseCoveragePercentage(commandResult.stdout);
|
|
922
|
+
if (coverage !== null) {
|
|
863
923
|
detail = `Coverage: ${coverage}%`;
|
|
864
924
|
if (coverage < 80) {
|
|
865
925
|
blockers.push(`Coverage ${coverage}% below 80% threshold`);
|
|
@@ -890,6 +950,26 @@ export class CoverageGate {
|
|
|
890
950
|
};
|
|
891
951
|
}
|
|
892
952
|
}
|
|
953
|
+
function parseCoveragePercentage(output) {
|
|
954
|
+
const allFilesLine = output
|
|
955
|
+
.split(/\r?\n/)
|
|
956
|
+
.find(line => /^\s*All files\s*\|/.test(line));
|
|
957
|
+
if (!allFilesLine)
|
|
958
|
+
return null;
|
|
959
|
+
const values = allFilesLine
|
|
960
|
+
.split('|')
|
|
961
|
+
.slice(1)
|
|
962
|
+
.map(part => part.trim().match(/^(\d+(?:\.\d+)?)/)?.[1])
|
|
963
|
+
.filter((value) => Boolean(value))
|
|
964
|
+
.map(Number);
|
|
965
|
+
if (values.length >= 5)
|
|
966
|
+
return values[values.length - 1];
|
|
967
|
+
if (values.length >= 4)
|
|
968
|
+
return values[3];
|
|
969
|
+
if (values.length > 0)
|
|
970
|
+
return values[values.length - 1];
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
893
973
|
export class SecurityGate {
|
|
894
974
|
constructor(options = {}) {
|
|
895
975
|
this.stage = 'G7';
|
|
@@ -905,9 +985,12 @@ export class SecurityGate {
|
|
|
905
985
|
this.dependencyAudit = options.dependencyAudit ?? true;
|
|
906
986
|
this.dependencyAuditMode = options.dependencyAuditMode;
|
|
907
987
|
this.dependencyAuditChangedPackages = options.dependencyAuditChangedPackages;
|
|
988
|
+
this.changedFiles = options.changedFiles;
|
|
989
|
+
this.changedFilesProvider = options.changedFilesProvider;
|
|
908
990
|
}
|
|
909
991
|
async execute() {
|
|
910
|
-
const
|
|
992
|
+
const changedFiles = await this.resolveChangedFiles();
|
|
993
|
+
const findings = await this.scan(changedFiles);
|
|
911
994
|
const dependencyReport = this.dependencyAudit ? auditDependencies({
|
|
912
995
|
projectDir: this.rootDir,
|
|
913
996
|
scaleDir: this.scaleDir,
|
|
@@ -915,11 +998,12 @@ export class SecurityGate {
|
|
|
915
998
|
changedPackages: this.dependencyAuditChangedPackages,
|
|
916
999
|
}) : null;
|
|
917
1000
|
const blockers = findings
|
|
918
|
-
.filter(finding =>
|
|
1001
|
+
.filter(finding => this.blocksFinding(finding, changedFiles))
|
|
919
1002
|
.map(finding => `${finding.severity} ${finding.ruleId} in ${finding.file}:${finding.line} - ${finding.description}`);
|
|
920
1003
|
blockers.push(...(dependencyReport?.blockers ?? []));
|
|
921
1004
|
const passed = blockers.length === 0;
|
|
922
1005
|
const summary = this.summarize(findings);
|
|
1006
|
+
const changedHigh = findings.filter(finding => finding.severity === 'HIGH' && this.isChangedFinding(finding, changedFiles)).length;
|
|
923
1007
|
const evidenceItems = [
|
|
924
1008
|
createEvidence({
|
|
925
1009
|
kind: 'scan',
|
|
@@ -927,14 +1011,14 @@ export class SecurityGate {
|
|
|
927
1011
|
passed,
|
|
928
1012
|
path: this.scanDirs.join(','),
|
|
929
1013
|
detail: findings.length > 0
|
|
930
|
-
? `${findings.length} finding(s): critical=${summary.CRITICAL}, high=${summary.HIGH}, medium=${summary.MEDIUM}, low=${summary.LOW}, strict=${this.strict}`
|
|
1014
|
+
? `${findings.length} finding(s): critical=${summary.CRITICAL}, high=${summary.HIGH}, medium=${summary.MEDIUM}, low=${summary.LOW}, changedHigh=${changedHigh}, strict=${this.strict}`
|
|
931
1015
|
: 'no built-in security findings detected',
|
|
932
1016
|
source: 'built-in-security-scan',
|
|
933
1017
|
}),
|
|
934
1018
|
...findings.slice(0, this.maxFindings).map(finding => createEvidence({
|
|
935
1019
|
kind: 'scan',
|
|
936
1020
|
label: `Security finding ${finding.ruleId}`,
|
|
937
|
-
passed:
|
|
1021
|
+
passed: !this.blocksFinding(finding, changedFiles),
|
|
938
1022
|
path: finding.file,
|
|
939
1023
|
detail: `${finding.severity} line ${finding.line}: ${finding.description}; ${finding.evidence}`,
|
|
940
1024
|
source: 'built-in-security-scan',
|
|
@@ -950,14 +1034,20 @@ export class SecurityGate {
|
|
|
950
1034
|
blockers
|
|
951
1035
|
};
|
|
952
1036
|
}
|
|
953
|
-
async scan() {
|
|
1037
|
+
async scan(changedFiles) {
|
|
954
1038
|
const findings = [];
|
|
955
1039
|
try {
|
|
956
|
-
const fs = await import('fs/promises');
|
|
957
|
-
const { join, relative } = await import('path');
|
|
958
|
-
const files =
|
|
1040
|
+
const fs = await import('node:fs/promises');
|
|
1041
|
+
const { join, relative } = await import('node:path');
|
|
1042
|
+
const files = new Set();
|
|
1043
|
+
for (const file of changedFiles) {
|
|
1044
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file))
|
|
1045
|
+
continue;
|
|
1046
|
+
files.add(join(this.rootDir, file));
|
|
1047
|
+
}
|
|
959
1048
|
for (const dir of this.scanDirs) {
|
|
960
|
-
|
|
1049
|
+
for (const file of await this.walkDir(join(this.rootDir, dir)))
|
|
1050
|
+
files.add(file);
|
|
961
1051
|
}
|
|
962
1052
|
for (const file of files) {
|
|
963
1053
|
if (findings.length >= this.maxFindings)
|
|
@@ -972,11 +1062,37 @@ export class SecurityGate {
|
|
|
972
1062
|
findings.push(...this.scanFile(displayPath, content).slice(0, this.maxFindings - findings.length));
|
|
973
1063
|
}
|
|
974
1064
|
}
|
|
975
|
-
catch {
|
|
1065
|
+
catch (err) {
|
|
1066
|
+
logger.debug({ err, rootDir: this.rootDir, scanDirs: this.scanDirs }, 'Security scan skipped unreadable path');
|
|
976
1067
|
// A missing scan directory should not mask the rest of the verification run.
|
|
977
1068
|
}
|
|
978
1069
|
return findings;
|
|
979
1070
|
}
|
|
1071
|
+
async resolveChangedFiles() {
|
|
1072
|
+
const raw = this.changedFilesProvider
|
|
1073
|
+
? await this.changedFilesProvider()
|
|
1074
|
+
: (this.changedFiles ?? []);
|
|
1075
|
+
return new Set(raw.map(file => this.normalizeChangedFile(file)).filter(Boolean));
|
|
1076
|
+
}
|
|
1077
|
+
normalizeChangedFile(file) {
|
|
1078
|
+
const normalized = file.replace(/\\/g, '/');
|
|
1079
|
+
const root = this.rootDir.replace(/\\/g, '/').replace(/\/$/, '');
|
|
1080
|
+
if (normalized === root)
|
|
1081
|
+
return '';
|
|
1082
|
+
if (normalized.startsWith(`${root}/`))
|
|
1083
|
+
return normalized.slice(root.length + 1);
|
|
1084
|
+
return normalized.replace(/^\.\//, '');
|
|
1085
|
+
}
|
|
1086
|
+
blocksFinding(finding, changedFiles) {
|
|
1087
|
+
if (finding.severity === 'CRITICAL')
|
|
1088
|
+
return true;
|
|
1089
|
+
if (finding.severity !== 'HIGH')
|
|
1090
|
+
return false;
|
|
1091
|
+
return this.strict || this.isChangedFinding(finding, changedFiles);
|
|
1092
|
+
}
|
|
1093
|
+
isChangedFinding(finding, changedFiles) {
|
|
1094
|
+
return changedFiles.has(finding.file);
|
|
1095
|
+
}
|
|
980
1096
|
scanFile(file, content) {
|
|
981
1097
|
const findings = [];
|
|
982
1098
|
const lines = content.split('\n');
|
|
@@ -1019,8 +1135,8 @@ export class SecurityGate {
|
|
|
1019
1135
|
}
|
|
1020
1136
|
}
|
|
1021
1137
|
}
|
|
1022
|
-
catch {
|
|
1023
|
-
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
logger.debug({ err, dir }, 'Security scan skipped unreadable directory');
|
|
1024
1140
|
}
|
|
1025
1141
|
return results;
|
|
1026
1142
|
}
|
|
@@ -1163,13 +1279,64 @@ export class SecurityGate {
|
|
|
1163
1279
|
}
|
|
1164
1280
|
isRuleDefinition(file, line) {
|
|
1165
1281
|
const trimmed = line.trim();
|
|
1166
|
-
|
|
1282
|
+
if (this.isSecurityRuleSource(file) && this.isSecurityRuleDefinitionLine(trimmed)) {
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
return /\/.*(?:dangerouslySetInnerHTML|\\\.innerHTML|document\\\.write|password|api\[_-\]\?key|secret|token|shell:\s*true|@ts-ignore|catch).*\/[dgimsuy]*\.test\(\s*(?:line|trimmed)\s*\)/i.test(trimmed);
|
|
1286
|
+
}
|
|
1287
|
+
isSecurityRuleSource(file) {
|
|
1288
|
+
return this.isGeneratedShieldHook(file) || [
|
|
1289
|
+
'src/guardrails/advancedDetectors.ts',
|
|
1290
|
+
'src/guardrails/ast/confirmers.ts',
|
|
1291
|
+
'src/guardrails/OWASPDetector.ts',
|
|
1292
|
+
'src/shield/PolicyCompiler.ts',
|
|
1293
|
+
'src/shield/ProtectedPaths.ts',
|
|
1294
|
+
'src/skills/SkillRepository.ts',
|
|
1295
|
+
'src/cli/shieldCommands.ts',
|
|
1296
|
+
'src/workflow/gates/GateSystem.ts',
|
|
1297
|
+
'src/workflow/ReviewAnalyzer.ts',
|
|
1298
|
+
'src/workflow/SecurityAudit.ts',
|
|
1299
|
+
].some(ruleSource => file.endsWith(ruleSource));
|
|
1300
|
+
}
|
|
1301
|
+
isGeneratedShieldHook(file) {
|
|
1302
|
+
return /(^|\/)\.claude\/hooks\/shield-[^/]+\.js$/i.test(file);
|
|
1303
|
+
}
|
|
1304
|
+
isSecurityRuleDefinitionLine(trimmed) {
|
|
1305
|
+
if (/^id:\s*['"`][^'"`]+['"`],?$/.test(trimmed))
|
|
1306
|
+
return true;
|
|
1307
|
+
if (/^\{?\s*pattern:\s*\/.*\/[dgimsuy]*,?/.test(trimmed))
|
|
1308
|
+
return true;
|
|
1309
|
+
if (/^patterns:\s*\[/.test(trimmed))
|
|
1310
|
+
return true;
|
|
1311
|
+
if (/^\/.*\/[dgimsuy]*,?(?:\s*\/\/.*)?$/.test(trimmed))
|
|
1312
|
+
return true;
|
|
1313
|
+
if (/^\{?\s*(?:re|pattern):\s*\/.*\/[dgimsuy]*/i.test(trimmed))
|
|
1314
|
+
return true;
|
|
1315
|
+
if (/^(?:const|let|var)\s+\w*pattern\w*\s*=\s*\/.*\/[dgimsuy]*/i.test(trimmed))
|
|
1316
|
+
return true;
|
|
1317
|
+
if (/^(?:const|let|var)\s+\w+\s*=\s*\[.*(?:rm\s+-rf|curl\s*\|\s*bash|wget\s*\|\s*bash|Invoke-Expression|chmod\s+777|git reset --hard|git push --force)/i.test(trimmed))
|
|
1318
|
+
return true;
|
|
1319
|
+
if (/^['"`].*(?:rm\s+-rf|curl\s*\|\s*bash|wget\s*\|\s*bash|Invoke-Expression|chmod\s+777|git reset --hard|git push --force|DROP TABLE|DELETE without WHERE).*['"`],?$/i.test(trimmed))
|
|
1320
|
+
return true;
|
|
1321
|
+
if (/^['"`][^'"`]+['"`](?:\s*,\s*['"`][^'"`]+['"`])*[,]?$/.test(trimmed) && /(?:rm\s+-rf|curl\s*\|\s*bash|wget\s*\|\s*bash|Invoke-Expression|chmod\s+777|git reset --hard|git push --force|DROP TABLE|DELETE without WHERE)/i.test(trimmed))
|
|
1322
|
+
return true;
|
|
1323
|
+
if (/\bexpect:\s*['"`]block['"`]/.test(trimmed) && /\b(?:input|command|label):/.test(trimmed))
|
|
1324
|
+
return true;
|
|
1325
|
+
if (/^(?:name|title|description|recommendation|remediation):\s*['"`].*(?:dangerouslySetInnerHTML|innerHTML|document\.write|eval\(|new Function|curl pipe|shell:\s*true)/i.test(trimmed))
|
|
1326
|
+
return true;
|
|
1327
|
+
return /^(?:if\s*\(|return\s+)?\/?.*(?:dangerouslySetInnerHTML|innerHTML|document\.write|eval\(|new Function|curl|wget|Invoke-WebRequest|Invoke-Expression|rm\s+-rf|shell:\s*true).*\/[dgimsuy]*\.test\(/i.test(trimmed) ||
|
|
1328
|
+
/^(?:\/\/|\/\*\*?|\*)\s*.*(?:dangerouslySetInnerHTML|innerHTML|document\.write|eval\(|new Function|curl pipe|shell:\s*true)/i.test(trimmed);
|
|
1167
1329
|
}
|
|
1168
1330
|
isSecurityTestFixture(file, line) {
|
|
1169
1331
|
if (!this.isTestPath(file))
|
|
1170
1332
|
return false;
|
|
1171
|
-
|
|
1172
|
-
|
|
1333
|
+
const riskyFixtureText = /['"`].*(?:password|api[_-]?key|secret|token|auth|credential|private[_-]?key|git add|rm\s+-rf|curl\b.*\|.*(?:bash|sh)|Invoke-WebRequest\b.*\|\s*iex|shell: true|dangerouslySetInnerHTML|innerHTML|document\.write|eval\(|new Function|@ts-ignore|catch)/i.test(line);
|
|
1334
|
+
if (!riskyFixtureText)
|
|
1335
|
+
return false;
|
|
1336
|
+
const fixtureField = /\b(?:text|content|diff|source|command|input|tool_input)\b\s*[:=]/.test(line);
|
|
1337
|
+
const fixtureCodePath = /^\s*['"`][^'"`]+\.(?:ts|tsx|js|jsx|mjs|cjs)['"`]\s*:\s*['"`]/.test(line);
|
|
1338
|
+
const fixtureLiteral = /^\s*['"`].*['"`]\s*,?$/.test(line);
|
|
1339
|
+
return fixtureField || fixtureCodePath || fixtureLiteral;
|
|
1173
1340
|
}
|
|
1174
1341
|
}
|
|
1175
1342
|
function parseProductSmokeReport(commandResult) {
|
|
@@ -1213,7 +1380,7 @@ export class ProductSmokeGate {
|
|
|
1213
1380
|
const blockers = [];
|
|
1214
1381
|
let commandResult = null;
|
|
1215
1382
|
try {
|
|
1216
|
-
commandResult = await runShellCommand(this.command.command,
|
|
1383
|
+
commandResult = await runShellCommand(this.command.command, resolveGateTimeoutMs(this.stage, DEFAULT_PRODUCT_SMOKE_TIMEOUT_MS), this.command.cwd, gateCommandOptions(this.stage, this.command, this.runtimeEvidence));
|
|
1217
1384
|
if (commandResult.code !== 0) {
|
|
1218
1385
|
blockers.push(`Product smoke failed: ${commandResult.stderr || commandResult.stdout || `exit code ${commandResult.code}`}`);
|
|
1219
1386
|
}
|