swarm-orchestrator 7.0.0-alpha.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/LICENSE +15 -0
- package/README.md +199 -0
- package/agents/reviewer.agent.md +50 -0
- package/agents/worker.agent.md +41 -0
- package/config/default-agents.yaml +56 -0
- package/config/pm-agent.yaml +36 -0
- package/config/quality-gates.yaml +88 -0
- package/config/repair-agent.yaml +34 -0
- package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
- package/config/semgrep-rules/exception-swallowing.yaml +13 -0
- package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
- package/config/semgrep-rules/mock-mutation.yaml +6 -0
- package/config/semgrep-rules/test-modification.yaml +6 -0
- package/config/user-agents.yaml +23 -0
- package/dist/src/adapters/adapter-factory.js +29 -0
- package/dist/src/adapters/agent-adapter.js +32 -0
- package/dist/src/adapters/claude-code-adapter.js +126 -0
- package/dist/src/adapters/claude-code-teams.js +119 -0
- package/dist/src/adapters/codex-adapter.js +118 -0
- package/dist/src/adapters/copilot-adapter.js +224 -0
- package/dist/src/adapters/fatal-error-classifier.js +88 -0
- package/dist/src/adapters/index.js +44 -0
- package/dist/src/adapters/persistent-session.js +204 -0
- package/dist/src/adapters/process-supervisor.js +199 -0
- package/dist/src/agents-exporter.js +398 -0
- package/dist/src/analytics-log.js +161 -0
- package/dist/src/baseline-scanner.js +65 -0
- package/dist/src/bootstrap-evidence.js +88 -0
- package/dist/src/bootstrap-orchestrator.js +205 -0
- package/dist/src/bootstrap-types.js +5 -0
- package/dist/src/branch-merger.js +427 -0
- package/dist/src/cli/attest-handlers.js +44 -0
- package/dist/src/cli/cost-prompt.js +51 -0
- package/dist/src/cli/demo-handlers.js +274 -0
- package/dist/src/cli/flags.js +152 -0
- package/dist/src/cli/index.js +57 -0
- package/dist/src/cli/live-status.js +186 -0
- package/dist/src/cli/misc-handlers.js +242 -0
- package/dist/src/cli/plan-handlers.js +350 -0
- package/dist/src/cli/share-handlers.js +163 -0
- package/dist/src/cli/status-handlers.js +457 -0
- package/dist/src/cli/swarm-handlers.js +638 -0
- package/dist/src/cli/usage.js +72 -0
- package/dist/src/cli.js +217 -0
- package/dist/src/commit-pattern-detector.js +143 -0
- package/dist/src/commit-quality-analyzer.js +38 -0
- package/dist/src/config-loader.js +339 -0
- package/dist/src/context-broker.js +358 -0
- package/dist/src/copilot-cli-wrapper.js +258 -0
- package/dist/src/cost-estimator.js +241 -0
- package/dist/src/defaults.js +18 -0
- package/dist/src/demo-mode.js +180 -0
- package/dist/src/deployment-handler.js +78 -0
- package/dist/src/deployment-manager.js +228 -0
- package/dist/src/execution-queue.js +196 -0
- package/dist/src/external-tool-manager.js +184 -0
- package/dist/src/gate-prompt-builder.js +117 -0
- package/dist/src/gate-remediation.js +173 -0
- package/dist/src/github-issues-ingester.js +152 -0
- package/dist/src/github-mcp-integrator.js +164 -0
- package/dist/src/hook-generator.js +229 -0
- package/dist/src/index.js +48 -0
- package/dist/src/knowledge-base.js +300 -0
- package/dist/src/logger.js +111 -0
- package/dist/src/meta-analyzer.js +208 -0
- package/dist/src/metrics-collector.js +209 -0
- package/dist/src/metrics-types.js +5 -0
- package/dist/src/multi-repo-coordinator.js +179 -0
- package/dist/src/orchestrator/async-meta-analysis.js +84 -0
- package/dist/src/orchestrator/fatal-run-error.js +43 -0
- package/dist/src/orchestrator/final-gates-remediation.js +258 -0
- package/dist/src/orchestrator/git-state-utils.js +119 -0
- package/dist/src/orchestrator/pause-controller.js +55 -0
- package/dist/src/orchestrator/replan-runner.js +258 -0
- package/dist/src/orchestrator/step-executor.js +391 -0
- package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
- package/dist/src/owasp-mapper.js +153 -0
- package/dist/src/owasp-report-renderer.js +39 -0
- package/dist/src/plan-files.js +142 -0
- package/dist/src/plan-generator.js +1207 -0
- package/dist/src/pm-agent.js +296 -0
- package/dist/src/post-run-reporter.js +203 -0
- package/dist/src/pr-automation.js +103 -0
- package/dist/src/pr-manager.js +318 -0
- package/dist/src/presenter/index.js +209 -0
- package/dist/src/prompt-builder.js +173 -0
- package/dist/src/quality-gates/config-loader.js +196 -0
- package/dist/src/quality-gates/default-config.js +243 -0
- package/dist/src/quality-gates/file-utils.js +152 -0
- package/dist/src/quality-gates/gate-runner.js +171 -0
- package/dist/src/quality-gates/gates/accessibility.js +355 -0
- package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
- package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
- package/dist/src/quality-gates/gates/readme-claims.js +62 -0
- package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
- package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
- package/dist/src/quality-gates/gates/test-coverage.js +240 -0
- package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
- package/dist/src/quality-gates/gates/test-isolation.js +74 -0
- package/dist/src/quality-gates/index.js +17 -0
- package/dist/src/quality-gates/registry.js +205 -0
- package/dist/src/quality-gates/types.js +2 -0
- package/dist/src/quick-fix-mode.js +302 -0
- package/dist/src/recipe-loader.js +122 -0
- package/dist/src/repair-agent.js +396 -0
- package/dist/src/repo-analyzer.js +342 -0
- package/dist/src/report-generator.js +146 -0
- package/dist/src/report-renderer.js +90 -0
- package/dist/src/requirement-filter.js +52 -0
- package/dist/src/sarif-formatter.js +140 -0
- package/dist/src/scheduling/dependency-analyzer.js +178 -0
- package/dist/src/scheduling/work-stealing-queue.js +123 -0
- package/dist/src/secret-redactor.js +90 -0
- package/dist/src/session-executor.js +587 -0
- package/dist/src/session-manager.js +221 -0
- package/dist/src/share/transcript-verification.js +128 -0
- package/dist/src/share-parser.js +477 -0
- package/dist/src/spinner.js +146 -0
- package/dist/src/step-runner.js +365 -0
- package/dist/src/swarm-orchestrator.js +686 -0
- package/dist/src/task-classifier.js +134 -0
- package/dist/src/test-command-discovery.js +123 -0
- package/dist/src/text-similarity.js +24 -0
- package/dist/src/tier-maps.js +158 -0
- package/dist/src/types.js +5 -0
- package/dist/src/url-shortener.js +135 -0
- package/dist/src/verification/attestation.js +166 -0
- package/dist/src/verification/cheat-detector.js +240 -0
- package/dist/src/verification/command-runner.js +70 -0
- package/dist/src/verification/composite-score.js +157 -0
- package/dist/src/verification/cosign-attestation.js +137 -0
- package/dist/src/verification/diff-analysis.js +89 -0
- package/dist/src/verification/differential-gate.js +173 -0
- package/dist/src/verification/index.js +41 -0
- package/dist/src/verification/mutation-gate.js +242 -0
- package/dist/src/verification/property-gate.js +201 -0
- package/dist/src/verification/test-synthesizer.js +243 -0
- package/dist/src/verifier/outcome-checks.js +396 -0
- package/dist/src/verifier/transcript-checks.js +207 -0
- package/dist/src/verifier/verification-reporters.js +148 -0
- package/dist/src/verifier-engine.js +491 -0
- package/dist/src/wave-resizer.js +131 -0
- package/dist/src/wave-scheduler.js +44 -0
- package/dist/src/worktree-manager.js +368 -0
- package/dist/src/worktree-reserved-paths.js +97 -0
- package/package.json +80 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/backend_master.agent.md +72 -0
- package/plugin/agents/devops_pro.agent.md +45 -0
- package/plugin/agents/frontend_expert.agent.md +59 -0
- package/plugin/agents/integrator_finalizer.agent.md +44 -0
- package/plugin/agents/meta_reviewer.agent.md +44 -0
- package/plugin/agents/security_auditor.agent.md +43 -0
- package/plugin/agents/tester_elite.agent.md +43 -0
- package/plugin/hooks/hooks.json +19 -0
- package/plugin/hooks/post-tool-use.json +9 -0
- package/plugin/hooks/pre-tool-use.json +9 -0
- package/plugin/plugin.json +47 -0
- package/plugin/skills/swarm-gates/SKILL.md +53 -0
- package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
- package/plugin/skills/swarm-verify/SKILL.md +44 -0
- package/templates/README.md +35 -0
- package/templates/cli-tool.json +31 -0
- package/templates/fullstack.json +52 -0
- package/templates/library.json +38 -0
- package/templates/react-app.json +45 -0
- package/templates/recipes/add-api-docs.json +35 -0
- package/templates/recipes/add-auth.json +47 -0
- package/templates/recipes/add-ci.json +39 -0
- package/templates/recipes/add-tests.json +39 -0
- package/templates/recipes/migrate-to-ts.json +42 -0
- package/templates/recipes/refactor-modularize.json +44 -0
- package/templates/recipes/security-audit.json +35 -0
- package/templates/rest-api.json +38 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runWaveLoop = runWaveLoop;
|
|
4
|
+
const async_meta_analysis_1 = require("./async-meta-analysis");
|
|
5
|
+
const defaults_1 = require("../defaults");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const work_stealing_queue_1 = require("../scheduling/work-stealing-queue");
|
|
8
|
+
const fatal_run_error_1 = require("./fatal-run-error");
|
|
9
|
+
const logger = (0, logger_1.getLogger)('orchestrator');
|
|
10
|
+
/**
|
|
11
|
+
* Run the greedy as-soon-as-ready scheduler loop. The custom fleet-wave executor was
|
|
12
|
+
* removed for v7. Native `/fleet` pass-through remains available per step through
|
|
13
|
+
* `useInnerFleet`.
|
|
14
|
+
*
|
|
15
|
+
* @param host - Orchestrator surface.
|
|
16
|
+
* @param _plan - Initial plan, retained for API compatibility. The loop reads `context.plan`.
|
|
17
|
+
* @param agents - Available agent map.
|
|
18
|
+
* @param context - Mutable execution context.
|
|
19
|
+
* @param options - Scheduler options.
|
|
20
|
+
*/
|
|
21
|
+
async function runWaveLoop(host, _plan, agents, context, options) {
|
|
22
|
+
const completed = new Set();
|
|
23
|
+
const scheduler = new work_stealing_queue_1.WorkStealingQueue(context.plan, {
|
|
24
|
+
maxWorkers: context.executionQueue?.getStats().maxConcurrency ?? 1,
|
|
25
|
+
});
|
|
26
|
+
const runningPromises = new Map();
|
|
27
|
+
let dispatchCounter = 0;
|
|
28
|
+
if (options?.lean && context.knowledgeBase) {
|
|
29
|
+
for (const step of context.plan.steps) {
|
|
30
|
+
const matches = context.knowledgeBase.findSimilarTasks(step.task);
|
|
31
|
+
if (matches.length > 0) {
|
|
32
|
+
const ref = matches[0];
|
|
33
|
+
const commitRef = ref.evidence[0] || 'unknown';
|
|
34
|
+
step.task += `\nReference: similar task completed in session ${ref.id}, commit ${commitRef}.`;
|
|
35
|
+
context.leanSavedRequests = (context.leanSavedRequests || 0) + 1;
|
|
36
|
+
logger.info(` [lean] Step ${step.stepNumber}: found similar pattern "${ref.insight.slice(0, 50)}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const pendingMerge = [];
|
|
41
|
+
const onStepComplete = async (stepNumber) => {
|
|
42
|
+
scheduler.markCompleted(stepNumber);
|
|
43
|
+
completed.add(stepNumber);
|
|
44
|
+
context.adaptiveConcurrency?.recordSuccess();
|
|
45
|
+
const result = context.results.find((entry) => entry.stepNumber === stepNumber);
|
|
46
|
+
if (result && result.branchName && result.status === 'completed') {
|
|
47
|
+
pendingMerge.push(result);
|
|
48
|
+
}
|
|
49
|
+
if (scheduler.stats().running === 0 || pendingMerge.length >= 3) {
|
|
50
|
+
if (pendingMerge.length > 0) {
|
|
51
|
+
context.contextBroker.forceReleaseStaleLocks();
|
|
52
|
+
await host.mergeWaveBranches(pendingMerge, context, options);
|
|
53
|
+
pendingMerge.length = 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (context.metaAnalyzer && context.knowledgeBase) {
|
|
57
|
+
setImmediate(() => {
|
|
58
|
+
(0, async_meta_analysis_1.runAsyncMetaAnalysis)(context, context.plan, context.runDir, Array.from(completed));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
options?.onProgress?.(context, `step-done:${stepNumber}`);
|
|
62
|
+
};
|
|
63
|
+
// Set when any in-flight step throws FatalRunError. Causes the loop to
|
|
64
|
+
// stop dispatching new work and exit as soon as running steps drain.
|
|
65
|
+
// Re-thrown by runWaveLoop after the drain so swarm-orchestrator's
|
|
66
|
+
// top-level handler can record the abort and skip the post-run pipeline.
|
|
67
|
+
let fatalRunError;
|
|
68
|
+
const onStepFailed = (stepNumber, errorMsg, err) => {
|
|
69
|
+
scheduler.markFailed(stepNumber);
|
|
70
|
+
if (err && (0, fatal_run_error_1.isFatalRunError)(err) && !fatalRunError) {
|
|
71
|
+
fatalRunError = err;
|
|
72
|
+
}
|
|
73
|
+
const isRateLimit = /rate limit|quota|429|throttle/i.test(errorMsg);
|
|
74
|
+
context.adaptiveConcurrency?.recordFailure(isRateLimit ? 'rate_limit' : 'error');
|
|
75
|
+
const newLimit = context.adaptiveConcurrency?.getCurrentLimit() || 3;
|
|
76
|
+
context.executionQueue?.setMaxConcurrency(newLimit);
|
|
77
|
+
scheduler.setMaxWorkers(newLimit);
|
|
78
|
+
options?.onProgress?.(context, `step-failed:${stepNumber}`);
|
|
79
|
+
};
|
|
80
|
+
while (scheduler.hasPendingWork()) {
|
|
81
|
+
if (host.pauseController.isPauseRequested()) {
|
|
82
|
+
logger.info('\n⏸️ Pause requested. Waiting for resume...');
|
|
83
|
+
await host.pauseController.waitForResume();
|
|
84
|
+
logger.info('\n▶️ Resuming execution...');
|
|
85
|
+
}
|
|
86
|
+
if (fatalRunError) {
|
|
87
|
+
// A fatal account-level error was raised by an in-flight step. Drain
|
|
88
|
+
// anything still running, then exit — dispatching further steps would
|
|
89
|
+
// hit the same wall and waste the per-instance budget.
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
scheduler.syncPlan(context.plan);
|
|
93
|
+
const ready = scheduler.nextDispatches();
|
|
94
|
+
if (ready.length === 0 && scheduler.isBlocked()) {
|
|
95
|
+
const blocked = scheduler.blockedSteps();
|
|
96
|
+
logger.error(`\n❌ ${blocked.length} step(s) blocked by failed dependencies: ${blocked.join(', ')}`);
|
|
97
|
+
// Mark each blocked step as failed AND emit step-completed via the
|
|
98
|
+
// broker so any later replan step that depends on this number
|
|
99
|
+
// resolves its waitForDependencies immediately instead of timing out
|
|
100
|
+
// after DEFAULT_DEPENDENCY_WAIT_MS (10 min). Without this, every
|
|
101
|
+
// failed top-level step costs ~10 minutes per replan attempt.
|
|
102
|
+
const ts = new Date().toISOString();
|
|
103
|
+
for (const stepNumber of blocked) {
|
|
104
|
+
scheduler.markFailed(stepNumber);
|
|
105
|
+
const result = context.results.find((r) => r.stepNumber === stepNumber);
|
|
106
|
+
if (result && result.status === 'pending') {
|
|
107
|
+
result.status = 'failed';
|
|
108
|
+
result.error = 'blocked by failed dependencies';
|
|
109
|
+
result.endTime = ts;
|
|
110
|
+
}
|
|
111
|
+
const stepDef = context.plan.steps.find((s) => s.stepNumber === stepNumber);
|
|
112
|
+
context.contextBroker.addStepContext({
|
|
113
|
+
stepNumber,
|
|
114
|
+
agentName: stepDef?.agentName ?? result?.agentName ?? 'unknown',
|
|
115
|
+
timestamp: ts,
|
|
116
|
+
data: {
|
|
117
|
+
filesChanged: [],
|
|
118
|
+
outputsSummary: 'step blocked by failed dependencies',
|
|
119
|
+
branchName: result?.branchName ?? '',
|
|
120
|
+
commitShas: [],
|
|
121
|
+
verificationPassed: false,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (ready.length > 0) {
|
|
128
|
+
dispatchCounter++;
|
|
129
|
+
context.metricsCollector?.startWave(dispatchCounter);
|
|
130
|
+
options?.onProgress?.(context, `wave-start:${dispatchCounter}`);
|
|
131
|
+
const stats = scheduler.stats();
|
|
132
|
+
const stepsLabel = ready.length === 1 ? `step ${ready[0]}` : `steps ${ready.join(', ')}`;
|
|
133
|
+
const modeLabel = stats.parallelEnabled ? 'parallel' : 'sequential';
|
|
134
|
+
// Wave header is presenter-owned. Suppress the line for single-step,
|
|
135
|
+
// single-wave runs where it adds noise without information.
|
|
136
|
+
logger.debug(`wave ${dispatchCounter}: ${stepsLabel} (${modeLabel})`);
|
|
137
|
+
const totalWaves = context.totalWaves ?? 0;
|
|
138
|
+
if (totalWaves > 1 || ready.length > 1) {
|
|
139
|
+
const presenter = require('../presenter');
|
|
140
|
+
const p = presenter.getPresenter();
|
|
141
|
+
p.print(` ${p.dim(`wave ${dispatchCounter}`)} ${stepsLabel} ${p.dim(p.glyph('sep'))} ${modeLabel}`);
|
|
142
|
+
}
|
|
143
|
+
for (const stepNumber of ready) {
|
|
144
|
+
const step = context.plan.steps.find((candidate) => candidate.stepNumber === stepNumber);
|
|
145
|
+
if (!step) {
|
|
146
|
+
throw new Error(`Step ${stepNumber} disappeared from the active plan; rerun planning before scheduling`);
|
|
147
|
+
}
|
|
148
|
+
const agent = host.resolveAgent(agents, step.agentName);
|
|
149
|
+
if (!agent) {
|
|
150
|
+
throw new Error(`Agent ${step.agentName} not found for step ${stepNumber}`);
|
|
151
|
+
}
|
|
152
|
+
scheduler.markRunning(stepNumber);
|
|
153
|
+
const promise = context.executionQueue
|
|
154
|
+
.enqueue(`step-${stepNumber}`, () => host.executeStepInSwarm(step, agent, context, options), {
|
|
155
|
+
priority: 100 - stepNumber,
|
|
156
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
157
|
+
metadata: {
|
|
158
|
+
stepNumber: step.stepNumber,
|
|
159
|
+
agentName: agent.name,
|
|
160
|
+
wave: dispatchCounter,
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
.then(() => onStepComplete(stepNumber), (err) => onStepFailed(stepNumber, err.message, err))
|
|
164
|
+
.finally(() => {
|
|
165
|
+
runningPromises.delete(stepNumber);
|
|
166
|
+
});
|
|
167
|
+
runningPromises.set(stepNumber, promise);
|
|
168
|
+
}
|
|
169
|
+
context.queueStats = context.executionQueue.getStats();
|
|
170
|
+
options?.onProgress?.(context, `wave-done:${dispatchCounter}`);
|
|
171
|
+
if (runningPromises.size > 0) {
|
|
172
|
+
await Promise.race(runningPromises.values());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
const handler = () => {
|
|
178
|
+
context.contextBroker.removeListener('step-completed', handler);
|
|
179
|
+
resolve();
|
|
180
|
+
};
|
|
181
|
+
context.contextBroker.once('step-completed', handler);
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
context.contextBroker.removeListener('step-completed', handler);
|
|
184
|
+
resolve();
|
|
185
|
+
}, defaults_1.DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (runningPromises.size > 0) {
|
|
190
|
+
await Promise.allSettled(runningPromises.values());
|
|
191
|
+
}
|
|
192
|
+
context.totalWaves = dispatchCounter;
|
|
193
|
+
if (pendingMerge.length > 0) {
|
|
194
|
+
context.contextBroker.forceReleaseStaleLocks();
|
|
195
|
+
await host.mergeWaveBranches(pendingMerge, context, options);
|
|
196
|
+
pendingMerge.length = 0;
|
|
197
|
+
}
|
|
198
|
+
if (fatalRunError) {
|
|
199
|
+
// Re-throw after the in-flight drain and the merge of any successful
|
|
200
|
+
// pre-fatal work, so swarm-orchestrator's top-level handler sees a
|
|
201
|
+
// FatalRunError rather than a generic step failure.
|
|
202
|
+
throw fatalRunError;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OwaspMapper = void 0;
|
|
4
|
+
const riskAssessors = [
|
|
5
|
+
// ASI-01: Agent Goal Hijack
|
|
6
|
+
(_results, meta) => ({
|
|
7
|
+
asiId: 'ASI-01',
|
|
8
|
+
riskName: 'Agent Goal Hijack',
|
|
9
|
+
status: meta.strictIsolation ? 'mitigated' : 'partial',
|
|
10
|
+
evidence: meta.strictIsolation
|
|
11
|
+
? ['Strict isolation enabled', 'Bounded worker and reviewer roles with refusal rules']
|
|
12
|
+
: ['Worker and reviewer roles bounded by prompts; strict isolation not enabled'],
|
|
13
|
+
rationale: meta.strictIsolation
|
|
14
|
+
? 'Strict isolation and bounded role prompts reduce goal-hijack blast radius.'
|
|
15
|
+
: 'Role prompts bound agent scope, but strict isolation was not enabled for this run.'
|
|
16
|
+
}),
|
|
17
|
+
// ASI-02: Tool Misuse
|
|
18
|
+
(_results, meta) => ({
|
|
19
|
+
asiId: 'ASI-02',
|
|
20
|
+
riskName: 'Tool Misuse',
|
|
21
|
+
status: meta.strictIsolation ? 'mitigated' : 'partial',
|
|
22
|
+
evidence: meta.strictIsolation
|
|
23
|
+
? ['Strict isolation enabled', 'Adapter interface restricts CLI flags', 'Cross-step context sharing blocked']
|
|
24
|
+
: ['Adapter interface restricts CLI flags', 'Strict isolation not enabled; cross-step context may be shared'],
|
|
25
|
+
rationale: meta.strictIsolation
|
|
26
|
+
? 'Strict isolation limits each agent to its own step context. Adapter interface restricts available CLI flags.'
|
|
27
|
+
: 'Adapter interface restricts CLI flags, but strict isolation was not enabled. Agents may share cross-step context.'
|
|
28
|
+
}),
|
|
29
|
+
// ASI-03: Privilege Escalation (always mitigated; structural)
|
|
30
|
+
() => ({
|
|
31
|
+
asiId: 'ASI-03',
|
|
32
|
+
riskName: 'Privilege Escalation',
|
|
33
|
+
status: 'mitigated',
|
|
34
|
+
evidence: ['Each agent runs in its own git worktree and branch', 'No shared filesystem state between agents'],
|
|
35
|
+
rationale: 'Branch isolation is structural. Each agent operates in a dedicated worktree with no shared filesystem state.'
|
|
36
|
+
}),
|
|
37
|
+
// ASI-04: Data Leakage (not applicable)
|
|
38
|
+
() => ({
|
|
39
|
+
asiId: 'ASI-04',
|
|
40
|
+
riskName: 'Data Leakage',
|
|
41
|
+
status: 'not_applicable',
|
|
42
|
+
evidence: [],
|
|
43
|
+
rationale: 'Agents are stateless subprocesses on local branches with no external data access.'
|
|
44
|
+
}),
|
|
45
|
+
// ASI-05: Uncontrolled Code Execution
|
|
46
|
+
(_results, meta) => {
|
|
47
|
+
const allPassedClean = meta.repairedSteps === 0 && meta.failedSteps === 0 && meta.totalSteps > 0;
|
|
48
|
+
return {
|
|
49
|
+
asiId: 'ASI-05',
|
|
50
|
+
riskName: 'Uncontrolled Code Execution',
|
|
51
|
+
status: allPassedClean ? 'mitigated' : 'partial',
|
|
52
|
+
evidence: allPassedClean
|
|
53
|
+
? [
|
|
54
|
+
`All ${meta.passedSteps} steps passed verification (build, test, git diff)`,
|
|
55
|
+
'No unverified merges occurred'
|
|
56
|
+
]
|
|
57
|
+
: [
|
|
58
|
+
`${meta.repairedSteps} step(s) required repair before passing`,
|
|
59
|
+
`${meta.failedSteps} step(s) failed verification`,
|
|
60
|
+
'Outcome verification gates blocked unverified merges'
|
|
61
|
+
],
|
|
62
|
+
rationale: allPassedClean
|
|
63
|
+
? 'All steps passed outcome verification gates (build, test, git diff) on first attempt. No unverified code was merged.'
|
|
64
|
+
: 'Outcome verification gates are in place, but some steps required repair or failed. Verification prevented uncontrolled merges.'
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
// ASI-06: Knowledge Poisoning (not applicable)
|
|
68
|
+
() => ({
|
|
69
|
+
asiId: 'ASI-06',
|
|
70
|
+
riskName: 'Knowledge Poisoning',
|
|
71
|
+
status: 'not_applicable',
|
|
72
|
+
evidence: [],
|
|
73
|
+
rationale: 'No persistent memory or RAG pipeline in agent sessions. Agents are stateless subprocesses.'
|
|
74
|
+
}),
|
|
75
|
+
// ASI-07: Insecure Communication (not applicable)
|
|
76
|
+
() => ({
|
|
77
|
+
asiId: 'ASI-07',
|
|
78
|
+
riskName: 'Insecure Communication',
|
|
79
|
+
status: 'not_applicable',
|
|
80
|
+
evidence: [],
|
|
81
|
+
rationale: 'No inter-agent networking. Agents are isolated subprocesses with no direct communication channels.'
|
|
82
|
+
}),
|
|
83
|
+
// ASI-08: Cascading Failures
|
|
84
|
+
(_results, meta) => ({
|
|
85
|
+
asiId: 'ASI-08',
|
|
86
|
+
riskName: 'Cascading Failures',
|
|
87
|
+
status: meta.retriesExhausted === 0 ? 'mitigated' : 'partial',
|
|
88
|
+
evidence: meta.retriesExhausted === 0
|
|
89
|
+
? [
|
|
90
|
+
'Greedy scheduler with adaptive concurrency',
|
|
91
|
+
'Failure classification in RepairAgent',
|
|
92
|
+
'No steps exhausted retry limits'
|
|
93
|
+
]
|
|
94
|
+
: [
|
|
95
|
+
'Greedy scheduler with adaptive concurrency',
|
|
96
|
+
`${meta.retriesExhausted} step(s) exhausted retry limits`,
|
|
97
|
+
'Max retry limits prevented unbounded cascading'
|
|
98
|
+
],
|
|
99
|
+
rationale: meta.retriesExhausted === 0
|
|
100
|
+
? 'Adaptive concurrency and failure classification contained all failures within retry limits.'
|
|
101
|
+
: `${meta.retriesExhausted} step(s) exhausted retries. Max retry limits prevented unbounded cascading, but failures did propagate.`
|
|
102
|
+
}),
|
|
103
|
+
// ASI-09: Supply Chain (not applicable)
|
|
104
|
+
() => ({
|
|
105
|
+
asiId: 'ASI-09',
|
|
106
|
+
riskName: 'Supply Chain',
|
|
107
|
+
status: 'not_applicable',
|
|
108
|
+
evidence: [],
|
|
109
|
+
rationale: 'Agents are first-party CLI tools invoked as subprocesses. No third-party plugins or agent marketplace in use.'
|
|
110
|
+
}),
|
|
111
|
+
// ASI-10: Rogue Agents
|
|
112
|
+
(_results, meta) => {
|
|
113
|
+
const fullyVerified = meta.failedSteps === 0;
|
|
114
|
+
return {
|
|
115
|
+
asiId: 'ASI-10',
|
|
116
|
+
riskName: 'Rogue Agents',
|
|
117
|
+
status: fullyVerified ? 'mitigated' : 'partial',
|
|
118
|
+
evidence: fullyVerified
|
|
119
|
+
? [
|
|
120
|
+
'Transcript analysis as secondary verification signal',
|
|
121
|
+
'All steps passed outcome-based verification',
|
|
122
|
+
'Max retry limits enforced'
|
|
123
|
+
]
|
|
124
|
+
: [
|
|
125
|
+
'Transcript analysis as secondary signal only',
|
|
126
|
+
`${meta.failedSteps} step(s) failed verification`,
|
|
127
|
+
'Outcome checks gated merges'
|
|
128
|
+
],
|
|
129
|
+
rationale: fullyVerified
|
|
130
|
+
? 'Outcome verification and transcript analysis completed successfully for all steps.'
|
|
131
|
+
: 'Outcome checks served as the primary gate against rogue behavior.'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
class OwaspMapper {
|
|
136
|
+
map(verificationResults, executionMeta) {
|
|
137
|
+
const risks = riskAssessors.map(assess => assess(verificationResults, executionMeta));
|
|
138
|
+
const notApplicableRisks = risks.filter(r => r.status === 'not_applicable').length;
|
|
139
|
+
const mitigatedRisks = risks.filter(r => r.status === 'mitigated').length;
|
|
140
|
+
const partialRisks = risks.filter(r => r.status === 'partial').length;
|
|
141
|
+
return {
|
|
142
|
+
generatedAt: new Date().toISOString(),
|
|
143
|
+
executionId: executionMeta.executionId,
|
|
144
|
+
toolVersion: executionMeta.toolVersion,
|
|
145
|
+
applicableRisks: 10 - notApplicableRisks,
|
|
146
|
+
mitigatedRisks,
|
|
147
|
+
partialRisks,
|
|
148
|
+
notApplicableRisks,
|
|
149
|
+
risks
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.OwaspMapper = OwaspMapper;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OwaspReportRenderer = void 0;
|
|
4
|
+
const STATUS_LABELS = {
|
|
5
|
+
mitigated: 'MITIGATED',
|
|
6
|
+
partial: 'PARTIAL',
|
|
7
|
+
not_applicable: 'NOT APPLICABLE'
|
|
8
|
+
};
|
|
9
|
+
class OwaspReportRenderer {
|
|
10
|
+
static toMarkdown(report) {
|
|
11
|
+
const lines = [
|
|
12
|
+
'# OWASP ASI Compliance Report',
|
|
13
|
+
'',
|
|
14
|
+
`Run: ${report.executionId} | Tool: swarm-orchestrator v${report.toolVersion} | Date: ${report.generatedAt}`,
|
|
15
|
+
'',
|
|
16
|
+
'## Summary',
|
|
17
|
+
'',
|
|
18
|
+
`Applicable risks: ${report.applicableRisks}/10`,
|
|
19
|
+
`Mitigated: ${report.mitigatedRisks} | Partial: ${report.partialRisks} | Not applicable: ${report.notApplicableRisks}`,
|
|
20
|
+
'',
|
|
21
|
+
'## Risk Assessment',
|
|
22
|
+
''
|
|
23
|
+
];
|
|
24
|
+
for (const risk of report.risks) {
|
|
25
|
+
lines.push(`### ${risk.asiId}: ${risk.riskName}`);
|
|
26
|
+
lines.push(`Status: ${STATUS_LABELS[risk.status]}`);
|
|
27
|
+
if (risk.evidence.length > 0) {
|
|
28
|
+
lines.push(`Evidence: ${risk.evidence.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push(`Rationale: ${risk.rationale}`);
|
|
31
|
+
lines.push('');
|
|
32
|
+
}
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
static toJson(report) {
|
|
36
|
+
return JSON.stringify(report, null, 2);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.OwaspReportRenderer = OwaspReportRenderer;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.savePlanFile = savePlanFile;
|
|
37
|
+
exports.loadPlanFile = loadPlanFile;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
function getPlanDir() {
|
|
41
|
+
return path.join(process.cwd(), 'plans');
|
|
42
|
+
}
|
|
43
|
+
function ensurePlanDirectory() {
|
|
44
|
+
const planDir = getPlanDir();
|
|
45
|
+
if (!fs.existsSync(planDir)) {
|
|
46
|
+
fs.mkdirSync(planDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
return planDir;
|
|
49
|
+
}
|
|
50
|
+
function generatePlanFilename(plan) {
|
|
51
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
52
|
+
const goalSlug = plan.goal
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
55
|
+
.replace(/^-|-$/g, '')
|
|
56
|
+
.substring(0, 50);
|
|
57
|
+
return `plan-${timestamp}-${goalSlug}.json`;
|
|
58
|
+
}
|
|
59
|
+
function resolvePlanPath(planRef) {
|
|
60
|
+
if (path.isAbsolute(planRef)) {
|
|
61
|
+
return planRef;
|
|
62
|
+
}
|
|
63
|
+
const looksLikePath = planRef.includes(path.sep) || planRef.startsWith('./') || planRef.startsWith('../');
|
|
64
|
+
if (looksLikePath) {
|
|
65
|
+
return path.resolve(process.cwd(), planRef);
|
|
66
|
+
}
|
|
67
|
+
// Bare filename: prefer the file in cwd if it exists, otherwise fall back
|
|
68
|
+
// to the workspace plans/ directory. This matches the help text examples
|
|
69
|
+
// (`swarm swarm plan.json`) without breaking the historical `plans/<name>`
|
|
70
|
+
// shortcut.
|
|
71
|
+
const cwdCandidate = path.resolve(process.cwd(), planRef);
|
|
72
|
+
if (fs.existsSync(cwdCandidate)) {
|
|
73
|
+
return cwdCandidate;
|
|
74
|
+
}
|
|
75
|
+
return path.join(getPlanDir(), planRef);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Writes an execution plan JSON file under the current workspace plans directory.
|
|
79
|
+
*
|
|
80
|
+
* @param plan - Plan to persist.
|
|
81
|
+
* @param filename - Optional filename. When omitted, a timestamped filename is generated.
|
|
82
|
+
* @returns Absolute path to the written plan file.
|
|
83
|
+
*/
|
|
84
|
+
function savePlanFile(plan, filename) {
|
|
85
|
+
const planDir = ensurePlanDirectory();
|
|
86
|
+
const planFilename = filename || generatePlanFilename(plan);
|
|
87
|
+
const planPath = path.join(planDir, planFilename);
|
|
88
|
+
fs.writeFileSync(planPath, JSON.stringify(plan, null, 2), 'utf8');
|
|
89
|
+
return planPath;
|
|
90
|
+
}
|
|
91
|
+
function isExecutionPlanShape(value) {
|
|
92
|
+
if (!value || typeof value !== 'object')
|
|
93
|
+
return false;
|
|
94
|
+
const candidate = value;
|
|
95
|
+
return typeof candidate.goal === 'string' && Array.isArray(candidate.steps);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Loads an execution plan from an absolute path, relative path, or plans directory filename.
|
|
99
|
+
*
|
|
100
|
+
* Accepts two on-disk shapes:
|
|
101
|
+
* - Bare ExecutionPlan: `{ goal, createdAt, steps, metadata? }` (the format
|
|
102
|
+
* `savePlanFile` writes under `plans/`).
|
|
103
|
+
* - Structured-output envelope: `{ goal, planFile, plan: ExecutionPlan }` (the
|
|
104
|
+
* format `swarm plan --output json` and `swarm plan import --output json`
|
|
105
|
+
* emit). The envelope is unwrapped to the inner plan.
|
|
106
|
+
*
|
|
107
|
+
* @param planRef - Plan filename or path.
|
|
108
|
+
* @returns Parsed execution plan.
|
|
109
|
+
* @throws Error when the file is missing, not parseable as JSON, or matches
|
|
110
|
+
* neither the bare nor the envelope shape.
|
|
111
|
+
*/
|
|
112
|
+
function loadPlanFile(planRef) {
|
|
113
|
+
const planPath = resolvePlanPath(planRef);
|
|
114
|
+
if (!fs.existsSync(planPath)) {
|
|
115
|
+
const isBareName = !path.isAbsolute(planRef) &&
|
|
116
|
+
!planRef.includes(path.sep) &&
|
|
117
|
+
!planRef.startsWith('./') &&
|
|
118
|
+
!planRef.startsWith('../');
|
|
119
|
+
const hint = isBareName
|
|
120
|
+
? ` (looked in cwd and ${getPlanDir()}); pass an explicit path like ./${planRef} or plans/${planRef} if the file lives elsewhere`
|
|
121
|
+
: '';
|
|
122
|
+
throw new Error(`Plan file not found: ${planPath}${hint}`);
|
|
123
|
+
}
|
|
124
|
+
const raw = fs.readFileSync(planPath, 'utf8');
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(raw);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
throw new Error(`Plan file is not valid JSON: ${planPath}; check the file for syntax errors or stray prose mixed into the JSON output`, { cause: err });
|
|
131
|
+
}
|
|
132
|
+
if (isExecutionPlanShape(parsed)) {
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
if (parsed && typeof parsed === 'object' && 'plan' in parsed) {
|
|
136
|
+
const inner = parsed.plan;
|
|
137
|
+
if (isExecutionPlanShape(inner)) {
|
|
138
|
+
return inner;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`Plan file at ${planPath} does not match the expected schema; expected either a plan object with "goal" and "steps" fields, or a structured-output envelope with a "plan" field containing the same. Regenerate via \`swarm plan\` or \`swarm bootstrap\`.`);
|
|
142
|
+
}
|