@keygraph/shannon 1.0.0-beta.2 → 1.1.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/dist/index.mjs +88 -202
- package/infra/compose.yml +0 -27
- package/package.json +3 -3
- package/infra/router-config.json +0 -31
package/dist/index.mjs
CHANGED
|
@@ -76,59 +76,30 @@ function isTemporalReady() {
|
|
|
76
76
|
"localhost:7233"
|
|
77
77
|
]).includes("SERVING");
|
|
78
78
|
}
|
|
79
|
-
/** Check if the router container is running and healthy. */
|
|
80
|
-
function isRouterReady() {
|
|
81
|
-
return runOutput("docker", [
|
|
82
|
-
"inspect",
|
|
83
|
-
"--format",
|
|
84
|
-
"{{.State.Health.Status}}",
|
|
85
|
-
"shannon-router"
|
|
86
|
-
]) === "healthy";
|
|
87
|
-
}
|
|
88
79
|
/**
|
|
89
|
-
* Ensure Temporal
|
|
90
|
-
* If Temporal is already up but router is needed and missing, starts router only.
|
|
80
|
+
* Ensure Temporal is running via compose.
|
|
91
81
|
*/
|
|
92
|
-
async function ensureInfra(
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
async function ensureInfra() {
|
|
83
|
+
if (isTemporalReady()) return;
|
|
84
|
+
const composeFile = getComposeFile();
|
|
85
|
+
console.log("Starting Shannon infrastructure...");
|
|
86
|
+
execFileSync("docker", [
|
|
97
87
|
"compose",
|
|
98
88
|
"-f",
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (let i = 0; i < 30; i++) {
|
|
109
|
-
if (isTemporalReady()) {
|
|
110
|
-
console.log("Temporal is ready!");
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
if (i === 29) {
|
|
114
|
-
console.error("Timeout waiting for Temporal");
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
await setTimeout$1(2e3);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (routerNeeded) {
|
|
121
|
-
console.log("Waiting for router to be ready...");
|
|
122
|
-
for (let i = 0; i < 15; i++) {
|
|
123
|
-
if (isRouterReady()) {
|
|
124
|
-
console.log("Router is ready!");
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
await setTimeout$1(2e3);
|
|
89
|
+
composeFile,
|
|
90
|
+
"up",
|
|
91
|
+
"-d"
|
|
92
|
+
], { stdio: "inherit" });
|
|
93
|
+
console.log("Waiting for Temporal to be ready...");
|
|
94
|
+
for (let i = 0; i < 30; i++) {
|
|
95
|
+
if (isTemporalReady()) {
|
|
96
|
+
console.log("Temporal is ready!");
|
|
97
|
+
return;
|
|
128
98
|
}
|
|
129
|
-
|
|
130
|
-
process.exit(1);
|
|
99
|
+
await setTimeout$1(2e3);
|
|
131
100
|
}
|
|
101
|
+
console.error("Timeout waiting for Temporal");
|
|
102
|
+
process.exit(1);
|
|
132
103
|
}
|
|
133
104
|
/**
|
|
134
105
|
* Build the worker image locally (local mode only).
|
|
@@ -180,37 +151,39 @@ function addHostFlag() {
|
|
|
180
151
|
}
|
|
181
152
|
/**
|
|
182
153
|
* Spawn the worker container in detached mode and return the process.
|
|
154
|
+
* When `opts.debug` is true, omits `--rm` so the container persists for log inspection.
|
|
183
155
|
*/
|
|
184
156
|
function spawnWorker(opts) {
|
|
185
|
-
const args = [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"--rm",
|
|
189
|
-
"--name",
|
|
190
|
-
opts.containerName,
|
|
191
|
-
"--network",
|
|
192
|
-
"shannon-net"
|
|
193
|
-
];
|
|
157
|
+
const args = ["run", "-d"];
|
|
158
|
+
if (!opts.debug) args.push("--rm");
|
|
159
|
+
args.push("--name", opts.containerName, "--network", "shannon-net");
|
|
194
160
|
args.push(...addHostFlag());
|
|
195
161
|
if (os.platform() === "linux" && process.getuid && process.getgid) args.push("-e", `SHANNON_HOST_UID=${process.getuid()}`, "-e", `SHANNON_HOST_GID=${process.getgid()}`);
|
|
196
162
|
args.push("-v", `${opts.workspacesDir}:/app/workspaces`);
|
|
197
|
-
args.push("-v", `${opts.repo.hostPath}:${opts.repo.containerPath}`);
|
|
163
|
+
args.push("-v", `${opts.repo.hostPath}:${opts.repo.containerPath}:ro`);
|
|
164
|
+
const workspacePath = path.join(opts.workspacesDir, opts.workspace);
|
|
165
|
+
args.push("-v", `${path.join(workspacePath, "deliverables")}:${opts.repo.containerPath}/.shannon/deliverables`);
|
|
166
|
+
args.push("-v", `${path.join(workspacePath, "scratchpad")}:${opts.repo.containerPath}/.shannon/scratchpad`);
|
|
167
|
+
args.push("-v", `${path.join(workspacePath, ".playwright-cli")}:${opts.repo.containerPath}/.shannon/.playwright-cli`);
|
|
198
168
|
if (opts.promptsDir) args.push("-v", `${opts.promptsDir}:/app/apps/worker/prompts:ro`);
|
|
199
169
|
if (opts.config) args.push("-v", `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
|
|
200
170
|
if (opts.outputDir) args.push("-v", `${opts.outputDir}:/app/output`);
|
|
201
|
-
if (opts.
|
|
202
|
-
else if (opts.credentials) args.push("-v", `${opts.credentials}:/app/credentials/google-sa-key.json:ro`);
|
|
171
|
+
if (opts.credentials) args.push("-v", `${opts.credentials}:/app/credentials/google-sa-key.json:ro`);
|
|
203
172
|
args.push(...opts.envFlags);
|
|
204
|
-
args.push("--shm-size", "2gb", "--
|
|
173
|
+
args.push("--shm-size", "2gb", "--security-opt", "seccomp=unconfined");
|
|
205
174
|
args.push(getWorkerImage(opts.version));
|
|
206
175
|
args.push("node", "apps/worker/dist/temporal/worker.js", opts.url, opts.repo.containerPath);
|
|
207
176
|
args.push("--task-queue", opts.taskQueue);
|
|
208
177
|
if (opts.config) args.push("--config", opts.config.containerPath);
|
|
209
178
|
if (opts.outputDir) args.push("--output", "/app/output");
|
|
210
|
-
|
|
179
|
+
args.push("--workspace", opts.workspace);
|
|
211
180
|
if (opts.pipelineTesting) args.push("--pipeline-testing");
|
|
212
181
|
return spawn("docker", args, {
|
|
213
|
-
stdio:
|
|
182
|
+
stdio: [
|
|
183
|
+
"ignore",
|
|
184
|
+
"ignore",
|
|
185
|
+
"inherit"
|
|
186
|
+
],
|
|
214
187
|
...os.platform() === "win32" && { env: {
|
|
215
188
|
...process.env,
|
|
216
189
|
MSYS_NO_PATHCONV: "1"
|
|
@@ -240,8 +213,6 @@ function stopInfra(clean) {
|
|
|
240
213
|
"compose",
|
|
241
214
|
"-f",
|
|
242
215
|
getComposeFile(),
|
|
243
|
-
"--profile",
|
|
244
|
-
"router",
|
|
245
216
|
"down"
|
|
246
217
|
];
|
|
247
218
|
if (clean) args.push("-v");
|
|
@@ -318,16 +289,6 @@ function getCredentialsPath() {
|
|
|
318
289
|
return path.join(SHANNON_HOME$2, "google-sa-key.json");
|
|
319
290
|
}
|
|
320
291
|
/**
|
|
321
|
-
* In dev mode, return the credentials directory if it exists and has files.
|
|
322
|
-
* In npx mode, there is no credentials directory (single file mount instead).
|
|
323
|
-
*/
|
|
324
|
-
function getCredentialsDir() {
|
|
325
|
-
if (getMode() !== "local") return void 0;
|
|
326
|
-
const dir = path.resolve("credentials");
|
|
327
|
-
if (!fs.existsSync(dir)) return void 0;
|
|
328
|
-
return fs.readdirSync(dir).length > 0 ? dir : void 0;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
292
|
* Initialize state directories.
|
|
332
293
|
* Local mode: creates ./workspaces/ and ./credentials/
|
|
333
294
|
* NPX mode: creates ~/.shannon/workspaces/
|
|
@@ -456,11 +417,6 @@ async function setup() {
|
|
|
456
417
|
{
|
|
457
418
|
value: "vertex",
|
|
458
419
|
label: "Claude via Google Vertex AI"
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
value: "router",
|
|
462
|
-
label: "Router",
|
|
463
|
-
hint: "experimental"
|
|
464
420
|
}
|
|
465
421
|
]
|
|
466
422
|
});
|
|
@@ -476,7 +432,6 @@ async function setupProvider(provider) {
|
|
|
476
432
|
case "custom_base_url": return setupCustomBaseUrl();
|
|
477
433
|
case "bedrock": return setupBedrock();
|
|
478
434
|
case "vertex": return setupVertex();
|
|
479
|
-
case "router": return setupRouter();
|
|
480
435
|
}
|
|
481
436
|
}
|
|
482
437
|
async function setupAnthropic() {
|
|
@@ -674,49 +629,6 @@ async function setupVertex() {
|
|
|
674
629
|
}
|
|
675
630
|
};
|
|
676
631
|
}
|
|
677
|
-
async function setupRouter() {
|
|
678
|
-
const routerProvider = await p.select({
|
|
679
|
-
message: "Router provider",
|
|
680
|
-
options: [{
|
|
681
|
-
value: "openai",
|
|
682
|
-
label: "OpenAI"
|
|
683
|
-
}, {
|
|
684
|
-
value: "openrouter",
|
|
685
|
-
label: "OpenRouter"
|
|
686
|
-
}]
|
|
687
|
-
});
|
|
688
|
-
if (p.isCancel(routerProvider)) return cancelAndExit();
|
|
689
|
-
const apiKey = await promptSecret(routerProvider === "openai" ? "Enter your OpenAI API key" : "Enter your OpenRouter API key");
|
|
690
|
-
let defaultModel;
|
|
691
|
-
if (routerProvider === "openai") {
|
|
692
|
-
const model = await p.select({
|
|
693
|
-
message: "Default model",
|
|
694
|
-
options: [{
|
|
695
|
-
value: "gpt-5.2",
|
|
696
|
-
label: "GPT-5.2"
|
|
697
|
-
}, {
|
|
698
|
-
value: "gpt-5-mini",
|
|
699
|
-
label: "GPT-5 Mini"
|
|
700
|
-
}]
|
|
701
|
-
});
|
|
702
|
-
if (p.isCancel(model)) return cancelAndExit();
|
|
703
|
-
defaultModel = `openai,${model}`;
|
|
704
|
-
} else {
|
|
705
|
-
const model = await p.select({
|
|
706
|
-
message: "Default model",
|
|
707
|
-
options: [{
|
|
708
|
-
value: "google/gemini-3-flash-preview",
|
|
709
|
-
label: "Google Gemini 3 Flash Preview"
|
|
710
|
-
}]
|
|
711
|
-
});
|
|
712
|
-
if (p.isCancel(model)) return cancelAndExit();
|
|
713
|
-
defaultModel = `openrouter,${model}`;
|
|
714
|
-
}
|
|
715
|
-
const router = { default: defaultModel };
|
|
716
|
-
if (routerProvider === "openai") router.openai_key = apiKey;
|
|
717
|
-
else router.openrouter_key = apiKey;
|
|
718
|
-
return { router };
|
|
719
|
-
}
|
|
720
632
|
async function promptSecret(message) {
|
|
721
633
|
const value = await p.password({
|
|
722
634
|
message,
|
|
@@ -804,21 +716,6 @@ const CONFIG_MAP = [
|
|
|
804
716
|
toml: "custom_base_url.auth_token",
|
|
805
717
|
type: "string"
|
|
806
718
|
},
|
|
807
|
-
{
|
|
808
|
-
env: "ROUTER_DEFAULT",
|
|
809
|
-
toml: "router.default",
|
|
810
|
-
type: "string"
|
|
811
|
-
},
|
|
812
|
-
{
|
|
813
|
-
env: "OPENAI_API_KEY",
|
|
814
|
-
toml: "router.openai_key",
|
|
815
|
-
type: "string"
|
|
816
|
-
},
|
|
817
|
-
{
|
|
818
|
-
env: "OPENROUTER_API_KEY",
|
|
819
|
-
toml: "router.openrouter_key",
|
|
820
|
-
type: "string"
|
|
821
|
-
},
|
|
822
719
|
{
|
|
823
720
|
env: "ANTHROPIC_SMALL_MODEL",
|
|
824
721
|
toml: "models.small",
|
|
@@ -917,13 +814,6 @@ function validateProviderFields(config, provider, errors) {
|
|
|
917
814
|
validateModelTiers(config, "vertex", errors);
|
|
918
815
|
break;
|
|
919
816
|
}
|
|
920
|
-
case "router": {
|
|
921
|
-
if (!keys.includes("default")) errors.push("[router] missing required key: default");
|
|
922
|
-
if (!keys.includes("openai_key") && !keys.includes("openrouter_key")) errors.push("[router] requires either openai_key or openrouter_key");
|
|
923
|
-
const models = config.models;
|
|
924
|
-
if (models && typeof models === "object" && Object.keys(models).length > 0) errors.push("[models] is not supported with [router]");
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
817
|
}
|
|
928
818
|
}
|
|
929
819
|
/** Bedrock and Vertex require a [models] section with all three tiers. */
|
|
@@ -976,8 +866,7 @@ function validateConfig(config) {
|
|
|
976
866
|
"anthropic",
|
|
977
867
|
"custom_base_url",
|
|
978
868
|
"bedrock",
|
|
979
|
-
"vertex"
|
|
980
|
-
"router"
|
|
869
|
+
"vertex"
|
|
981
870
|
].filter((s) => {
|
|
982
871
|
const section = config[s];
|
|
983
872
|
return section && typeof section === "object" && Object.keys(section).length > 0;
|
|
@@ -1025,7 +914,6 @@ const FORWARD_VARS = [
|
|
|
1025
914
|
"ANTHROPIC_API_KEY",
|
|
1026
915
|
"ANTHROPIC_BASE_URL",
|
|
1027
916
|
"ANTHROPIC_AUTH_TOKEN",
|
|
1028
|
-
"ROUTER_DEFAULT",
|
|
1029
917
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
1030
918
|
"CLAUDE_CODE_USE_BEDROCK",
|
|
1031
919
|
"AWS_REGION",
|
|
@@ -1037,9 +925,7 @@ const FORWARD_VARS = [
|
|
|
1037
925
|
"ANTHROPIC_SMALL_MODEL",
|
|
1038
926
|
"ANTHROPIC_MEDIUM_MODEL",
|
|
1039
927
|
"ANTHROPIC_LARGE_MODEL",
|
|
1040
|
-
"CLAUDE_CODE_MAX_OUTPUT_TOKENS"
|
|
1041
|
-
"OPENAI_API_KEY",
|
|
1042
|
-
"OPENROUTER_API_KEY"
|
|
928
|
+
"CLAUDE_CODE_MAX_OUTPUT_TOKENS"
|
|
1043
929
|
];
|
|
1044
930
|
/**
|
|
1045
931
|
* Load credentials into process.env.
|
|
@@ -1065,10 +951,6 @@ function buildEnvFlags() {
|
|
|
1065
951
|
}
|
|
1066
952
|
return flags;
|
|
1067
953
|
}
|
|
1068
|
-
/** Check if router credentials are present in the environment. */
|
|
1069
|
-
function isRouterConfigured() {
|
|
1070
|
-
return !!(process.env.ROUTER_DEFAULT && (process.env.OPENAI_API_KEY || process.env.OPENROUTER_API_KEY));
|
|
1071
|
-
}
|
|
1072
954
|
/** Check if a custom Anthropic-compatible base URL is configured. */
|
|
1073
955
|
function isCustomBaseUrlConfigured() {
|
|
1074
956
|
return !!(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN);
|
|
@@ -1081,7 +963,6 @@ function detectProviders() {
|
|
|
1081
963
|
if (isCustomBaseUrlConfigured()) providers.push("Custom Base URL");
|
|
1082
964
|
if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") providers.push("AWS Bedrock");
|
|
1083
965
|
if (process.env.CLAUDE_CODE_USE_VERTEX === "1") providers.push("Google Vertex");
|
|
1084
|
-
if (isRouterConfigured()) providers.push("Router");
|
|
1085
966
|
return providers;
|
|
1086
967
|
}
|
|
1087
968
|
/**
|
|
@@ -1102,13 +983,10 @@ function validateCredentials() {
|
|
|
1102
983
|
valid: true,
|
|
1103
984
|
mode: "oauth"
|
|
1104
985
|
};
|
|
1105
|
-
if (isCustomBaseUrlConfigured()) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
mode: "custom-base-url"
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
986
|
+
if (isCustomBaseUrlConfigured()) return {
|
|
987
|
+
valid: true,
|
|
988
|
+
mode: "custom-base-url"
|
|
989
|
+
};
|
|
1112
990
|
if (process.env.CLAUDE_CODE_USE_BEDROCK === "1") {
|
|
1113
991
|
const missing = [];
|
|
1114
992
|
if (!process.env.AWS_REGION) missing.push("AWS_REGION");
|
|
@@ -1148,13 +1026,6 @@ function validateCredentials() {
|
|
|
1148
1026
|
mode: "vertex"
|
|
1149
1027
|
};
|
|
1150
1028
|
}
|
|
1151
|
-
if (isRouterConfigured()) {
|
|
1152
|
-
process.env.ANTHROPIC_API_KEY = "router-mode";
|
|
1153
|
-
return {
|
|
1154
|
-
valid: true,
|
|
1155
|
-
mode: "router"
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
1029
|
return {
|
|
1159
1030
|
valid: false,
|
|
1160
1031
|
mode: "api-key",
|
|
@@ -1218,14 +1089,6 @@ function resolveConfig(configArg) {
|
|
|
1218
1089
|
containerPath: `/app/configs/${path.basename(hostPath)}`
|
|
1219
1090
|
};
|
|
1220
1091
|
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Ensure the deliverables directory exists and is writable by the container user.
|
|
1223
|
-
*/
|
|
1224
|
-
function ensureDeliverables(repoHostPath) {
|
|
1225
|
-
const deliverables = path.join(repoHostPath, "deliverables");
|
|
1226
|
-
fs.mkdirSync(deliverables, { recursive: true });
|
|
1227
|
-
fs.chmodSync(deliverables, 511);
|
|
1228
|
-
}
|
|
1229
1092
|
//#endregion
|
|
1230
1093
|
//#region src/splash.ts
|
|
1231
1094
|
/**
|
|
@@ -1282,31 +1145,43 @@ async function start(args) {
|
|
|
1282
1145
|
console.error(`ERROR: ${creds.error}`);
|
|
1283
1146
|
process.exit(1);
|
|
1284
1147
|
}
|
|
1285
|
-
const useRouter = args.router || isRouterConfigured();
|
|
1286
1148
|
const repo = resolveRepo(args.repo);
|
|
1287
1149
|
const config = args.config ? resolveConfig(args.config) : void 0;
|
|
1288
|
-
ensureDeliverables(repo.hostPath);
|
|
1289
1150
|
const workspacesDir = getWorkspacesDir();
|
|
1290
1151
|
fs.mkdirSync(workspacesDir, { recursive: true });
|
|
1291
1152
|
fs.chmodSync(workspacesDir, 511);
|
|
1292
|
-
if (useRouter) {
|
|
1293
|
-
process.env.ANTHROPIC_BASE_URL = "http://shannon-router:3456";
|
|
1294
|
-
process.env.ANTHROPIC_AUTH_TOKEN = "shannon-router-key";
|
|
1295
|
-
}
|
|
1296
1153
|
ensureImage(args.version);
|
|
1297
|
-
await ensureInfra(
|
|
1154
|
+
await ensureInfra();
|
|
1298
1155
|
const suffix = randomSuffix();
|
|
1299
1156
|
const taskQueue = `shannon-${suffix}`;
|
|
1300
1157
|
const containerName = `shannon-worker-${suffix}`;
|
|
1301
1158
|
const workspace = args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, "-")}_shannon-${Date.now()}`;
|
|
1302
|
-
const
|
|
1159
|
+
const workspacePath = path.join(workspacesDir, workspace);
|
|
1160
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
1161
|
+
fs.chmodSync(workspacePath, 511);
|
|
1162
|
+
for (const dir of [
|
|
1163
|
+
"deliverables",
|
|
1164
|
+
"scratchpad",
|
|
1165
|
+
".playwright-cli"
|
|
1166
|
+
]) {
|
|
1167
|
+
const dirPath = path.join(workspacePath, dir);
|
|
1168
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1169
|
+
fs.chmodSync(dirPath, 511);
|
|
1170
|
+
}
|
|
1171
|
+
const shannonDir = path.join(repo.hostPath, ".shannon");
|
|
1172
|
+
for (const dir of [
|
|
1173
|
+
"deliverables",
|
|
1174
|
+
"scratchpad",
|
|
1175
|
+
".playwright-cli"
|
|
1176
|
+
]) fs.mkdirSync(path.join(shannonDir, dir), { recursive: true });
|
|
1303
1177
|
const credentialsPath = getCredentialsPath();
|
|
1304
|
-
const hasCredentials =
|
|
1178
|
+
const hasCredentials = fs.existsSync(credentialsPath);
|
|
1179
|
+
if (hasCredentials) process.env.GOOGLE_APPLICATION_CREDENTIALS = "/app/credentials/google-sa-key.json";
|
|
1305
1180
|
const outputDir = args.output ? path.resolve(args.output) : void 0;
|
|
1306
1181
|
if (outputDir) fs.mkdirSync(outputDir, { recursive: true });
|
|
1307
1182
|
const promptsDir = isLocal() ? path.resolve("apps/worker/prompts") : void 0;
|
|
1308
1183
|
displaySplash(isLocal() ? void 0 : args.version);
|
|
1309
|
-
spawnWorker({
|
|
1184
|
+
const proc = spawnWorker({
|
|
1310
1185
|
version: args.version,
|
|
1311
1186
|
url: args.url,
|
|
1312
1187
|
repo,
|
|
@@ -1315,16 +1190,20 @@ async function start(args) {
|
|
|
1315
1190
|
containerName,
|
|
1316
1191
|
envFlags: buildEnvFlags(),
|
|
1317
1192
|
...config && { config },
|
|
1318
|
-
...credentialsDir && { credentialsDir },
|
|
1319
1193
|
...hasCredentials && { credentials: credentialsPath },
|
|
1320
1194
|
...promptsDir && { promptsDir },
|
|
1321
1195
|
...outputDir && { outputDir },
|
|
1322
|
-
|
|
1323
|
-
...args.pipelineTesting && { pipelineTesting: true }
|
|
1324
|
-
|
|
1325
|
-
console.error(`Failed to start worker: ${err.message}`);
|
|
1326
|
-
process.exit(1);
|
|
1196
|
+
workspace,
|
|
1197
|
+
...args.pipelineTesting && { pipelineTesting: true },
|
|
1198
|
+
...args.debug && { debug: true }
|
|
1327
1199
|
});
|
|
1200
|
+
if (await new Promise((resolve) => {
|
|
1201
|
+
proc.once("exit", (code) => resolve(code ?? 1));
|
|
1202
|
+
proc.once("error", (err) => {
|
|
1203
|
+
console.error(`Failed to start worker: ${err.message}`);
|
|
1204
|
+
resolve(1);
|
|
1205
|
+
});
|
|
1206
|
+
}) !== 0) process.exit(1);
|
|
1328
1207
|
const sessionJson = path.join(workspacesDir, workspace, "session.json");
|
|
1329
1208
|
const isResume = fs.existsSync(sessionJson);
|
|
1330
1209
|
let initialResumeCount = 0;
|
|
@@ -1351,7 +1230,7 @@ async function start(args) {
|
|
|
1351
1230
|
started = true;
|
|
1352
1231
|
workflowId = resumeAttempts.at(-1)?.workflowId ?? session.session?.originalWorkflowId ?? "";
|
|
1353
1232
|
process.stdout.write("\r\x1B[K");
|
|
1354
|
-
printInfo(args,
|
|
1233
|
+
printInfo(args, workspace, workflowId, repo.hostPath, workspacesDir);
|
|
1355
1234
|
return;
|
|
1356
1235
|
}
|
|
1357
1236
|
} catch {}
|
|
@@ -1366,6 +1245,7 @@ async function start(args) {
|
|
|
1366
1245
|
try {
|
|
1367
1246
|
execFileSync("docker", ["stop", containerName], { stdio: "pipe" });
|
|
1368
1247
|
} catch {}
|
|
1248
|
+
if (args.debug) printDebugHint(containerName);
|
|
1369
1249
|
};
|
|
1370
1250
|
process.on("SIGINT", () => {
|
|
1371
1251
|
cleanup();
|
|
@@ -1377,7 +1257,14 @@ async function start(args) {
|
|
|
1377
1257
|
});
|
|
1378
1258
|
process.on("exit", cleanup);
|
|
1379
1259
|
}
|
|
1380
|
-
function
|
|
1260
|
+
function printDebugHint(containerName) {
|
|
1261
|
+
console.log("");
|
|
1262
|
+
console.log(` Worker container preserved: ${containerName}`);
|
|
1263
|
+
console.log(` Inspect logs: docker logs ${containerName}`);
|
|
1264
|
+
console.log(` Remove: docker rm ${containerName}`);
|
|
1265
|
+
console.log("");
|
|
1266
|
+
}
|
|
1267
|
+
function printInfo(args, workspace, workflowId, repoPath, workspacesDir) {
|
|
1381
1268
|
const logsCmd = isLocal() ? `./shannon logs ${workspace}` : `npx @keygraph/shannon logs ${workspace}`;
|
|
1382
1269
|
const reportsPath = path.join(workspacesDir, workspace);
|
|
1383
1270
|
console.log(` Target: ${args.url}`);
|
|
@@ -1385,7 +1272,6 @@ function printInfo(args, routerActive, workspace, workflowId, repoPath, workspac
|
|
|
1385
1272
|
console.log(` Workspace: ${workspace}`);
|
|
1386
1273
|
if (args.config) console.log(` Config: ${path.resolve(args.config)}`);
|
|
1387
1274
|
if (args.pipelineTesting) console.log(" Mode: Pipeline Testing");
|
|
1388
|
-
if (routerActive) console.log(" Router: Enabled");
|
|
1389
1275
|
console.log("");
|
|
1390
1276
|
console.log(" Monitor:");
|
|
1391
1277
|
if (workflowId) console.log(` Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}`);
|
|
@@ -1533,7 +1419,7 @@ Options for 'start':
|
|
|
1533
1419
|
-o, --output <path> Copy deliverables to this directory after run
|
|
1534
1420
|
-w, --workspace <name> Named workspace (auto-resumes if exists)
|
|
1535
1421
|
--pipeline-testing Use minimal prompts for fast testing
|
|
1536
|
-
--
|
|
1422
|
+
--debug Preserve worker container after exit for log inspection
|
|
1537
1423
|
|
|
1538
1424
|
Examples:
|
|
1539
1425
|
${prefix} start -u https://example.com -r ${mode === "local" ? "my-repo" : "./my-repo"}
|
|
@@ -1553,7 +1439,7 @@ function parseStartArgs(argv) {
|
|
|
1553
1439
|
let workspace;
|
|
1554
1440
|
let output;
|
|
1555
1441
|
let pipelineTesting = false;
|
|
1556
|
-
let
|
|
1442
|
+
let debug = false;
|
|
1557
1443
|
for (let i = 0; i < argv.length; i++) {
|
|
1558
1444
|
const arg = argv[i];
|
|
1559
1445
|
const next = argv[i + 1];
|
|
@@ -1596,8 +1482,8 @@ function parseStartArgs(argv) {
|
|
|
1596
1482
|
case "--pipeline-testing":
|
|
1597
1483
|
pipelineTesting = true;
|
|
1598
1484
|
break;
|
|
1599
|
-
case "--
|
|
1600
|
-
|
|
1485
|
+
case "--debug":
|
|
1486
|
+
debug = true;
|
|
1601
1487
|
break;
|
|
1602
1488
|
default:
|
|
1603
1489
|
console.error(`Unknown option: ${arg}`);
|
|
@@ -1614,7 +1500,7 @@ function parseStartArgs(argv) {
|
|
|
1614
1500
|
url,
|
|
1615
1501
|
repo,
|
|
1616
1502
|
pipelineTesting,
|
|
1617
|
-
|
|
1503
|
+
debug,
|
|
1618
1504
|
...config && { config },
|
|
1619
1505
|
...workspace && { workspace },
|
|
1620
1506
|
...output && { output }
|
package/infra/compose.yml
CHANGED
|
@@ -19,32 +19,5 @@ services:
|
|
|
19
19
|
retries: 10
|
|
20
20
|
start_period: 30s
|
|
21
21
|
|
|
22
|
-
router:
|
|
23
|
-
image: node:20-slim
|
|
24
|
-
container_name: shannon-router
|
|
25
|
-
profiles: ["router"]
|
|
26
|
-
command: >
|
|
27
|
-
sh -c "apt-get update && apt-get install -y gettext-base &&
|
|
28
|
-
npm install -g @musistudio/claude-code-router &&
|
|
29
|
-
mkdir -p /root/.claude-code-router &&
|
|
30
|
-
envsubst < /config/router-config.json > /root/.claude-code-router/config.json &&
|
|
31
|
-
ccr start"
|
|
32
|
-
ports:
|
|
33
|
-
- "127.0.0.1:3456:3456"
|
|
34
|
-
volumes:
|
|
35
|
-
- ./router-config.json:/config/router-config.json:ro
|
|
36
|
-
environment:
|
|
37
|
-
- HOST=0.0.0.0
|
|
38
|
-
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
|
39
|
-
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
|
40
|
-
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
|
|
41
|
-
- ROUTER_DEFAULT=${ROUTER_DEFAULT:-openai,gpt-4o}
|
|
42
|
-
healthcheck:
|
|
43
|
-
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3456/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
|
|
44
|
-
interval: 10s
|
|
45
|
-
timeout: 5s
|
|
46
|
-
retries: 5
|
|
47
|
-
start_period: 30s
|
|
48
|
-
|
|
49
22
|
volumes:
|
|
50
23
|
temporal-data:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keygraph/shannon",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Shannon - Autonomous white-box AI pentester for web applications and APIs by Keygraph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@clack/prompts": "^1.1.0",
|
|
16
16
|
"chokidar": "^5.0.0",
|
|
17
17
|
"dotenv": "^17.3.1",
|
|
18
|
-
"smol-toml": "^1.6.
|
|
18
|
+
"smol-toml": "^1.6.1"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"security",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"tsdown": "^0.21.
|
|
43
|
+
"tsdown": "^0.21.5"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsdown",
|
package/infra/router-config.json
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"HOST": "0.0.0.0",
|
|
3
|
-
"APIKEY": "shannon-router-key",
|
|
4
|
-
"LOG": true,
|
|
5
|
-
"LOG_LEVEL": "info",
|
|
6
|
-
"NON_INTERACTIVE_MODE": true,
|
|
7
|
-
"API_TIMEOUT_MS": 600000,
|
|
8
|
-
"Providers": [
|
|
9
|
-
{
|
|
10
|
-
"name": "openai",
|
|
11
|
-
"api_base_url": "https://api.openai.com/v1/chat/completions",
|
|
12
|
-
"api_key": "$OPENAI_API_KEY",
|
|
13
|
-
"models": ["gpt-5.2", "gpt-5-mini"],
|
|
14
|
-
"transformer": {
|
|
15
|
-
"use": [["maxcompletiontokens", { "max_completion_tokens": 16384 }]]
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"name": "openrouter",
|
|
20
|
-
"api_base_url": "https://openrouter.ai/api/v1/chat/completions",
|
|
21
|
-
"api_key": "$OPENROUTER_API_KEY",
|
|
22
|
-
"models": ["google/gemini-3-flash-preview"],
|
|
23
|
-
"transformer": {
|
|
24
|
-
"use": ["openrouter"]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
],
|
|
28
|
-
"Router": {
|
|
29
|
-
"default": "$ROUTER_DEFAULT"
|
|
30
|
-
}
|
|
31
|
-
}
|