cipher-security 2.0.8 → 2.2.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/bin/cipher.js +11 -1
- package/lib/agent-runtime/handlers/architect.js +199 -0
- package/lib/agent-runtime/handlers/base.js +240 -0
- package/lib/agent-runtime/handlers/blue.js +220 -0
- package/lib/agent-runtime/handlers/incident.js +161 -0
- package/lib/agent-runtime/handlers/privacy.js +190 -0
- package/lib/agent-runtime/handlers/purple.js +209 -0
- package/lib/agent-runtime/handlers/recon.js +174 -0
- package/lib/agent-runtime/handlers/red.js +246 -0
- package/lib/agent-runtime/handlers/researcher.js +170 -0
- package/lib/agent-runtime/handlers.js +35 -0
- package/lib/agent-runtime/index.js +196 -0
- package/lib/agent-runtime/parser.js +316 -0
- package/lib/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/feedback-loop.js +13 -6
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +585 -0
- package/lib/autonomous/modes/researcher.js +322 -0
- package/lib/autonomous/researcher.js +12 -45
- package/lib/autonomous/runner.js +9 -537
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- package/package.json +1 -1
package/lib/benchmark/agent.js
CHANGED
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
* CIPHER Benchmark — Security agent for autonomous solving.
|
|
6
6
|
*
|
|
7
7
|
* Runs a multi-turn tool-use conversation loop with an LLM,
|
|
8
|
-
* dispatching tool calls to the sandbox.
|
|
8
|
+
* dispatching tool calls to the sandbox. Supports file injection
|
|
9
|
+
* from benchmark configs and multi-service targets.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import { getToolsForWinCondition, dispatchTool } from './tools.js';
|
|
13
|
+
import { ConfidenceTracker, createCognitiveDispatcher } from './cognitive.js';
|
|
14
|
+
import { generateSystemPrompt } from './prompts.js';
|
|
12
15
|
|
|
13
16
|
export class AgentResult {
|
|
14
17
|
constructor(opts = {}) {
|
|
15
18
|
this.flagFound = opts.flagFound ?? false;
|
|
16
19
|
this.flagValue = opts.flagValue ?? '';
|
|
20
|
+
this.answerFound = opts.answerFound ?? false;
|
|
21
|
+
this.answerValue = opts.answerValue ?? '';
|
|
17
22
|
this.tokensIn = opts.tokensIn ?? 0;
|
|
18
23
|
this.tokensOut = opts.tokensOut ?? 0;
|
|
19
24
|
this.toolCalls = opts.toolCalls ?? 0;
|
|
@@ -31,17 +36,66 @@ export class SecurityAgent {
|
|
|
31
36
|
this._preToolHook = preToolHook;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Inject benchmark files into the sandbox before solving.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{ name: string, path: string }>} files - Files from BenchmarkConfig
|
|
43
|
+
* @param {string} benchmarkPath - Base path of the benchmark
|
|
44
|
+
*/
|
|
45
|
+
injectFiles(files, benchmarkPath) {
|
|
46
|
+
if (!files || files.length === 0) return;
|
|
47
|
+
const { join } = require('node:path');
|
|
48
|
+
const { existsSync } = require('node:fs');
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const hostPath = join(benchmarkPath, file.path || file.name || file);
|
|
52
|
+
const containerPath = `/tmp/challenge/${file.name || file.path || file}`;
|
|
53
|
+
if (existsSync(hostPath)) {
|
|
54
|
+
try {
|
|
55
|
+
// Ensure container directory exists
|
|
56
|
+
const dir = containerPath.replace(/\/[^/]+$/, '');
|
|
57
|
+
this._sandbox.execTool(`mkdir -p '${dir}'`);
|
|
58
|
+
this._sandbox.copyFileIn(hostPath, containerPath);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Non-fatal — log and continue
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
44
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Run the agent against a target.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} opts
|
|
70
|
+
* @param {string} opts.targetUrl - Primary target URL
|
|
71
|
+
* @param {string} opts.challengeDescription - Challenge description
|
|
72
|
+
* @param {string} [opts.winCondition='flag'] - 'flag' or 'question'
|
|
73
|
+
* @param {Array<{ name: string, url: string }>} [opts.serviceUrls] - All service URLs
|
|
74
|
+
* @param {Array} [opts.files] - Benchmark files to inject
|
|
75
|
+
* @param {string} [opts.benchmarkPath] - Benchmark base path for file injection
|
|
76
|
+
* @returns {Promise<AgentResult>}
|
|
77
|
+
*/
|
|
78
|
+
async run({ targetUrl, challengeDescription, winCondition = 'flag', serviceUrls = [], files = [], benchmarkPath = '', tags = [] }) {
|
|
79
|
+
// Inject files if provided
|
|
80
|
+
if (files.length > 0 && benchmarkPath) {
|
|
81
|
+
this.injectFiles(files, benchmarkPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Initialize cognitive architecture
|
|
85
|
+
const tracker = new ConfidenceTracker();
|
|
86
|
+
const cognitiveDispatch = createCognitiveDispatcher(tracker);
|
|
87
|
+
|
|
88
|
+
// Generate tag-aware system prompt
|
|
89
|
+
const systemPrompt = generateSystemPrompt({
|
|
90
|
+
targetUrl,
|
|
91
|
+
challengeDescription,
|
|
92
|
+
tags,
|
|
93
|
+
winCondition,
|
|
94
|
+
serviceUrls,
|
|
95
|
+
hasFiles: files.length > 0,
|
|
96
|
+
phase: tracker.phase,
|
|
97
|
+
});
|
|
98
|
+
const tools = getToolsForWinCondition(winCondition);
|
|
45
99
|
const messages = [{ role: 'user', content: systemPrompt }];
|
|
46
100
|
let totalIn = 0;
|
|
47
101
|
let totalOut = 0;
|
|
@@ -51,11 +105,10 @@ export class SecurityAgent {
|
|
|
51
105
|
for (let turn = 0; turn < this._maxTurns; turn++) {
|
|
52
106
|
let response;
|
|
53
107
|
try {
|
|
54
|
-
// Anthropic SDK style
|
|
55
108
|
response = await this._client.messages.create({
|
|
56
109
|
model: this._model,
|
|
57
110
|
max_tokens: 4096,
|
|
58
|
-
tools
|
|
111
|
+
tools,
|
|
59
112
|
messages,
|
|
60
113
|
});
|
|
61
114
|
} catch (err) {
|
|
@@ -65,27 +118,33 @@ export class SecurityAgent {
|
|
|
65
118
|
totalIn += response.usage?.input_tokens || 0;
|
|
66
119
|
totalOut += response.usage?.output_tokens || 0;
|
|
67
120
|
|
|
68
|
-
// Process content blocks
|
|
69
121
|
const assistantContent = response.content || [];
|
|
70
122
|
messages.push({ role: 'assistant', content: assistantContent });
|
|
71
123
|
|
|
72
124
|
const toolUseBlocks = assistantContent.filter((b) => b.type === 'tool_use');
|
|
73
125
|
|
|
74
126
|
if (toolUseBlocks.length === 0) {
|
|
75
|
-
// No tool calls — agent is done or stuck
|
|
76
127
|
const text = assistantContent.find((b) => b.type === 'text')?.text || '';
|
|
77
128
|
steps.push(`[text] ${text.slice(0, 200)}`);
|
|
78
|
-
if (response.stop_reason === 'end_turn')
|
|
129
|
+
if (response.stop_reason === 'end_turn') {
|
|
130
|
+
// Nudge the model to keep going if it hasn't found the flag yet
|
|
131
|
+
if (turn < this._maxTurns - 1) {
|
|
132
|
+
messages.push({
|
|
133
|
+
role: 'user',
|
|
134
|
+
content: 'You have not found the flag yet. Continue investigating — use your tools to take the next action. Do not just describe what you would do; actually do it by calling a tool.',
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
79
140
|
continue;
|
|
80
141
|
}
|
|
81
142
|
|
|
82
|
-
// Process tool calls
|
|
83
143
|
const toolResults = [];
|
|
84
144
|
for (const block of toolUseBlocks) {
|
|
85
145
|
toolCallCount++;
|
|
86
146
|
steps.push(`[tool] ${block.name}: ${JSON.stringify(block.input).slice(0, 150)}`);
|
|
87
147
|
|
|
88
|
-
// Pre-tool hook (for supervised mode)
|
|
89
148
|
if (this._preToolHook) {
|
|
90
149
|
const approved = await this._preToolHook(block.name, block.input);
|
|
91
150
|
if (!approved) {
|
|
@@ -94,17 +153,20 @@ export class SecurityAgent {
|
|
|
94
153
|
}
|
|
95
154
|
}
|
|
96
155
|
|
|
97
|
-
const result = dispatchTool(block.name, block.input, this._sandbox);
|
|
156
|
+
const result = cognitiveDispatch(block.name, block.input) || dispatchTool(block.name, block.input, this._sandbox);
|
|
98
157
|
steps.push(`[result] ${result.output.slice(0, 200)}`);
|
|
99
158
|
|
|
100
159
|
if (result.flagSubmitted) {
|
|
101
160
|
return new AgentResult({
|
|
102
|
-
flagFound: true,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
flagFound: true, flagValue: result.flagSubmitted,
|
|
162
|
+
tokensIn: totalIn, tokensOut: totalOut, toolCalls: toolCallCount, steps,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (result.answerSubmitted) {
|
|
167
|
+
return new AgentResult({
|
|
168
|
+
answerFound: true, answerValue: result.answerSubmitted,
|
|
169
|
+
tokensIn: totalIn, tokensOut: totalOut, toolCalls: toolCallCount, steps,
|
|
108
170
|
});
|
|
109
171
|
}
|
|
110
172
|
|
|
@@ -41,3 +41,6 @@ export const SHANNON_BASELINE = new CompetitorBaseline({
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
export const ALL_BASELINES = [PENTESTGPT_BASELINE, MAPTA_BASELINE, SHANNON_BASELINE];
|
|
44
|
+
|
|
45
|
+
// NYU CTF baselines (re-exported from nyu-ctf.js for convenience)
|
|
46
|
+
export { NYU_BASELINES, CRAKEN_BASELINE, DCIPHER_BASELINE, ENIGMA_BASELINE } from './nyu-ctf.js';
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Claude Code Solver — Delegates CTF challenges to a Claude Code subagent.
|
|
7
|
+
*
|
|
8
|
+
* Instead of using the Anthropic API directly (expensive) or Ollama (inconsistent),
|
|
9
|
+
* this solver spawns a Claude Code subagent that uses bash/curl/python to interact
|
|
10
|
+
* with the Docker-hosted challenge target.
|
|
11
|
+
*
|
|
12
|
+
* The subagent has full access to the host's tools — it can curl the Docker network,
|
|
13
|
+
* run Python exploit scripts, use netcat, etc. This is the most capable solver
|
|
14
|
+
* because it leverages Claude Code's excellent tool use.
|
|
15
|
+
*
|
|
16
|
+
* @module benchmark/claude-code-solver
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { SolverResult } from './models.js';
|
|
20
|
+
import { SolverAdapter } from './solver.js';
|
|
21
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Docker network discovery
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find the Docker network for a benchmark's compose project.
|
|
29
|
+
* @param {string} benchmarkDir - Directory name of the benchmark
|
|
30
|
+
* @returns {string|null} Docker network name
|
|
31
|
+
*/
|
|
32
|
+
function findBenchmarkNetwork(benchmarkDir) {
|
|
33
|
+
try {
|
|
34
|
+
const result = spawnSync('docker', ['network', 'ls', '--format', '{{.Name}}'], {
|
|
35
|
+
timeout: 5000, stdio: 'pipe',
|
|
36
|
+
});
|
|
37
|
+
const networks = (result.stdout || '').toString().split('\n').filter(Boolean);
|
|
38
|
+
// Look for network matching the benchmark name
|
|
39
|
+
const prefix = `cipher-bench-${benchmarkDir}`.toLowerCase();
|
|
40
|
+
return networks.find(n => n.toLowerCase().includes(prefix.replace(/-/g, ''))) ||
|
|
41
|
+
networks.find(n => n.toLowerCase().includes(benchmarkDir.toLowerCase())) || null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the target URL by inspecting the Docker compose services.
|
|
49
|
+
* @param {string} benchmarkPath - Path to benchmark directory
|
|
50
|
+
* @returns {{ url: string, serviceName: string, port: number }|null}
|
|
51
|
+
*/
|
|
52
|
+
function getTargetFromCompose(benchmarkPath) {
|
|
53
|
+
try {
|
|
54
|
+
const yaml = require('yaml');
|
|
55
|
+
const { readFileSync } = require('node:fs');
|
|
56
|
+
const { join } = require('node:path');
|
|
57
|
+
|
|
58
|
+
const composePath = join(benchmarkPath, 'docker-compose.yml');
|
|
59
|
+
const data = yaml.parse(readFileSync(composePath, 'utf8'));
|
|
60
|
+
const services = data.services || {};
|
|
61
|
+
|
|
62
|
+
for (const [name, svc] of Object.entries(services)) {
|
|
63
|
+
if (svc.ports?.length) {
|
|
64
|
+
const portSpec = String(svc.ports[0]);
|
|
65
|
+
const hostPort = portSpec.includes(':') ? portSpec.split(':')[0] : portSpec;
|
|
66
|
+
return {
|
|
67
|
+
url: `http://localhost:${hostPort}`,
|
|
68
|
+
serviceName: name,
|
|
69
|
+
port: parseInt(hostPort, 10),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// No exposed ports — use internal Docker network
|
|
75
|
+
const firstService = Object.keys(services)[0];
|
|
76
|
+
return firstService ? { url: `http://${firstService}:80`, serviceName: firstService, port: 80 } : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// ClaudeCodeSolver
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Solver that delegates challenges to a Claude Code subagent.
|
|
88
|
+
*
|
|
89
|
+
* The subagent uses bash, curl, python3, and other host tools to
|
|
90
|
+
* interact with the Docker-hosted challenge target. This avoids
|
|
91
|
+
* the API cost of direct Claude API calls and the inconsistency
|
|
92
|
+
* of smaller models like Ollama.
|
|
93
|
+
*/
|
|
94
|
+
export class ClaudeCodeSolver extends SolverAdapter {
|
|
95
|
+
/**
|
|
96
|
+
* @param {object} [opts]
|
|
97
|
+
* @param {number} [opts.timeoutS=300] - Max time for the subagent
|
|
98
|
+
* @param {string} [opts.agent='ctf-solver'] - Subagent name
|
|
99
|
+
* @param {Function} [opts.subagentFn] - Direct subagent function (when running inside GSD)
|
|
100
|
+
*/
|
|
101
|
+
constructor({ timeoutS = 300, agent = 'ctf-solver', subagentFn } = {}) {
|
|
102
|
+
super();
|
|
103
|
+
this._timeoutS = timeoutS;
|
|
104
|
+
this._agent = agent;
|
|
105
|
+
this._subagentFn = subagentFn || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get name() { return 'claude-code'; }
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Solve a benchmark challenge using a Claude Code subagent.
|
|
112
|
+
*
|
|
113
|
+
* @param {import('./models.js').BenchmarkConfig} config
|
|
114
|
+
* @param {string} targetUrl - Target URL (from Docker compose)
|
|
115
|
+
* @param {string} expectedFlag - Expected flag for validation
|
|
116
|
+
* @returns {Promise<SolverResult>}
|
|
117
|
+
*/
|
|
118
|
+
async solve(config, targetUrl, expectedFlag) {
|
|
119
|
+
const start = performance.now();
|
|
120
|
+
const steps = [];
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Build the task prompt for the subagent
|
|
124
|
+
const task = this._buildTask(config, targetUrl);
|
|
125
|
+
steps.push(`[subagent] Delegating to ${this._agent}: ${config.name}`);
|
|
126
|
+
steps.push(`[target] ${targetUrl}`);
|
|
127
|
+
|
|
128
|
+
// Execute via subagent tool
|
|
129
|
+
// The subagent function is available in the GSD runtime context
|
|
130
|
+
// For standalone use, we'll exec pi/claude with the task
|
|
131
|
+
const result = await this._execSubagent(task);
|
|
132
|
+
steps.push(`[subagent] Completed in ${((performance.now() - start) / 1000).toFixed(1)}s`);
|
|
133
|
+
|
|
134
|
+
// Parse the result for flag
|
|
135
|
+
const flagMatch = result.match(/FLAG_FOUND:\s*(.+)/);
|
|
136
|
+
if (flagMatch) {
|
|
137
|
+
const flag = flagMatch[1].trim();
|
|
138
|
+
steps.push(`[flag] Found: ${flag}`);
|
|
139
|
+
return new SolverResult({
|
|
140
|
+
flagFound: true,
|
|
141
|
+
flagValue: flag,
|
|
142
|
+
durationS: (performance.now() - start) / 1000,
|
|
143
|
+
steps,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for common flag patterns in output
|
|
148
|
+
const patterns = [
|
|
149
|
+
/FLAG\{[^}]+\}/,
|
|
150
|
+
/flag\{[^}]+\}/,
|
|
151
|
+
/picoCTF\{[^}]+\}/,
|
|
152
|
+
/CTF\{[^}]+\}/,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
for (const pattern of patterns) {
|
|
156
|
+
const match = result.match(pattern);
|
|
157
|
+
if (match) {
|
|
158
|
+
steps.push(`[flag] Extracted from output: ${match[0]}`);
|
|
159
|
+
return new SolverResult({
|
|
160
|
+
flagFound: true,
|
|
161
|
+
flagValue: match[0],
|
|
162
|
+
durationS: (performance.now() - start) / 1000,
|
|
163
|
+
steps,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
steps.push(`[result] No flag found in subagent output (${result.length} chars)`);
|
|
169
|
+
// Include last 500 chars of output for debugging
|
|
170
|
+
steps.push(`[output-tail] ${result.slice(-500)}`);
|
|
171
|
+
|
|
172
|
+
return new SolverResult({
|
|
173
|
+
durationS: (performance.now() - start) / 1000,
|
|
174
|
+
steps,
|
|
175
|
+
});
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return new SolverResult({
|
|
178
|
+
durationS: (performance.now() - start) / 1000,
|
|
179
|
+
error: String(e),
|
|
180
|
+
steps: [...steps, `[error] ${e}`],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build the task prompt for the subagent.
|
|
187
|
+
*/
|
|
188
|
+
_buildTask(config, targetUrl) {
|
|
189
|
+
const parts = [
|
|
190
|
+
`Solve this CTF challenge:`,
|
|
191
|
+
``,
|
|
192
|
+
`**Challenge:** ${config.name}`,
|
|
193
|
+
`**Description:** ${config.description}`,
|
|
194
|
+
`**Level:** ${config.level}/5`,
|
|
195
|
+
`**Tags:** ${config.tags.join(', ')}`,
|
|
196
|
+
`**Target:** ${targetUrl}`,
|
|
197
|
+
``,
|
|
198
|
+
`The target is running in Docker and accessible at ${targetUrl}.`,
|
|
199
|
+
`Use curl, python3, netcat, or any tools you need.`,
|
|
200
|
+
``,
|
|
201
|
+
`Find the flag and output it as: FLAG_FOUND: <flag>`,
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
return parts.join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Execute the subagent. Tries the subagent tool first,
|
|
209
|
+
* falls back to direct CLI execution.
|
|
210
|
+
*/
|
|
211
|
+
async _execSubagent(task) {
|
|
212
|
+
// Option 1: Direct subagent function (when called from within GSD/pi)
|
|
213
|
+
if (this._subagentFn) {
|
|
214
|
+
const result = await this._subagentFn({
|
|
215
|
+
agent: this._agent,
|
|
216
|
+
task,
|
|
217
|
+
});
|
|
218
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Option 2: Try GSD runtime module
|
|
222
|
+
try {
|
|
223
|
+
const { subagent } = await import('../../node_modules/gsd-pi/pkg/tools/subagent.js').catch(() => ({}));
|
|
224
|
+
if (subagent) {
|
|
225
|
+
const result = await subagent({
|
|
226
|
+
agent: this._agent,
|
|
227
|
+
task,
|
|
228
|
+
});
|
|
229
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
230
|
+
}
|
|
231
|
+
} catch { /* not in GSD context */ }
|
|
232
|
+
|
|
233
|
+
// Option 3: Fallback to CLI execution
|
|
234
|
+
try {
|
|
235
|
+
for (const cmd of ['gsd', 'pi']) {
|
|
236
|
+
const result = spawnSync(cmd, [
|
|
237
|
+
'--print', task,
|
|
238
|
+
], {
|
|
239
|
+
timeout: this._timeoutS * 1000,
|
|
240
|
+
stdio: 'pipe',
|
|
241
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
242
|
+
env: { ...process.env },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (result.status !== null && result.status !== 127) {
|
|
246
|
+
return (result.stdout || '').toString() + (result.stderr || '').toString();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new Error('Neither gsd nor pi CLI found');
|
|
250
|
+
} catch (e) {
|
|
251
|
+
throw new Error(`Subagent execution failed: ${e.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|