patchwork-os 0.2.0-beta.0 → 0.2.0-beta.1
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.md +4 -1
- package/dist/analyticsAggregator.d.ts +5 -1
- package/dist/analyticsAggregator.js +15 -4
- package/dist/analyticsAggregator.js.map +1 -1
- package/dist/analyticsPrefs.d.ts +11 -0
- package/dist/analyticsPrefs.js +33 -0
- package/dist/analyticsPrefs.js.map +1 -1
- package/dist/bridge.js +36 -26
- package/dist/bridge.js.map +1 -1
- package/dist/claudeDriver.d.ts +0 -16
- package/dist/claudeDriver.js +19 -20
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeMdPatch.d.ts +9 -3
- package/dist/claudeMdPatch.js +79 -13
- package/dist/claudeMdPatch.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +12 -0
- package/dist/claudeOrchestrator.js +2 -0
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/marketplace.d.ts +15 -10
- package/dist/commands/marketplace.js +27 -115
- package/dist/commands/marketplace.js.map +1 -1
- package/dist/commitIssueLinkLog.d.ts +8 -0
- package/dist/commitIssueLinkLog.js +53 -1
- package/dist/commitIssueLinkLog.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.js +63 -372
- package/dist/connectorRoutes.js.map +1 -1
- package/dist/connectors/jira.js +18 -1
- package/dist/connectors/jira.js.map +1 -1
- package/dist/drivers/claude/subprocess.d.ts +12 -2
- package/dist/drivers/claude/subprocess.js +79 -6
- package/dist/drivers/claude/subprocess.js.map +1 -1
- package/dist/drivers/gemini/api.d.ts +18 -0
- package/dist/drivers/gemini/api.js +29 -0
- package/dist/drivers/gemini/api.js.map +1 -0
- package/dist/drivers/index.d.ts +3 -1
- package/dist/drivers/index.js +9 -1
- package/dist/drivers/index.js.map +1 -1
- package/dist/drivers/local/index.d.ts +26 -0
- package/dist/drivers/local/index.js +41 -0
- package/dist/drivers/local/index.js.map +1 -0
- package/dist/httpErrorResponse.d.ts +36 -0
- package/dist/httpErrorResponse.js +46 -0
- package/dist/httpErrorResponse.js.map +1 -0
- package/dist/inboxRoutes.js +90 -11
- package/dist/inboxRoutes.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +13 -0
- package/dist/oauth.js +13 -0
- package/dist/oauth.js.map +1 -1
- package/dist/oauthRoutes.js +3 -8
- package/dist/oauthRoutes.js.map +1 -1
- package/dist/patchworkConfig.d.ts +14 -1
- package/dist/patchworkConfig.js +99 -4
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/preToolUseHook.js +7 -1
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/prompts.js +4 -0
- package/dist/prompts.js.map +1 -1
- package/dist/recipeOrchestration.js +13 -3
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +5 -0
- package/dist/recipeRoutes.js +57 -33
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +10 -1
- package/dist/recipes/agentExecutor.js +5 -4
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/tools/gmail.js +18 -1
- package/dist/recipes/tools/gmail.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +15 -2
- package/dist/recipes/yamlRunner.js +11 -3
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +14 -0
- package/dist/recipesHttp.js +59 -1
- package/dist/recipesHttp.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +249 -245
- package/dist/server.js.map +1 -1
- package/dist/tools/runCommand.js +5 -0
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/terminal.js +4 -0
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/utils.d.ts +4 -0
- package/dist/tools/utils.js +59 -0
- package/dist/tools/utils.js.map +1 -1
- package/package.json +1 -1
- package/scripts/start-all.sh +4 -2
- package/templates/recipes/approval-queue-ui-test.yaml +205 -0
package/dist/server.js
CHANGED
|
@@ -7,11 +7,12 @@ import { saveBridgeConfigDriver } from "./config.js";
|
|
|
7
7
|
import { tryHandleConnectorRoute, tryHandlePublicConnectorRoute, } from "./connectorRoutes.js";
|
|
8
8
|
import { timingSafeStringEqual } from "./crypto.js";
|
|
9
9
|
import { renderDashboardHtml } from "./dashboard.js";
|
|
10
|
+
import { respond500 } from "./httpErrorResponse.js";
|
|
10
11
|
import { tryHandleInboxRoute } from "./inboxRoutes.js";
|
|
11
12
|
import { tryHandleMcpRoute } from "./mcpRoutes.js";
|
|
12
13
|
import { tryHandleOAuthRoute } from "./oauthRoutes.js";
|
|
13
|
-
import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
14
|
-
import { tryHandleRecipeRoute } from "./recipeRoutes.js";
|
|
14
|
+
import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveApiKeyToSecureStore, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
15
|
+
import { readBodyWithCap, readJsonBody, respond413, tryHandleRecipeRoute, } from "./recipeRoutes.js";
|
|
15
16
|
import { PACKAGE_VERSION } from "./version.js";
|
|
16
17
|
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
17
18
|
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -87,6 +88,8 @@ export class Server extends EventEmitter {
|
|
|
87
88
|
saveRecipeContentFn = null;
|
|
88
89
|
/** Patchwork: set by bridge to delete a recipe by name. */
|
|
89
90
|
deleteRecipeContentFn = null;
|
|
91
|
+
/** Patchwork: set by bridge to archive a recipe (move to .archive/). */
|
|
92
|
+
archiveRecipeFn = null;
|
|
90
93
|
/** Patchwork: set by bridge to promote a variant recipe to the canonical name. */
|
|
91
94
|
promoteRecipeVariantFn = null;
|
|
92
95
|
/** Patchwork: set by bridge to duplicate a recipe as a variant. */
|
|
@@ -347,10 +350,7 @@ export class Server extends EventEmitter {
|
|
|
347
350
|
res.end(JSON.stringify(body, null, 2));
|
|
348
351
|
}
|
|
349
352
|
catch (err) {
|
|
350
|
-
res
|
|
351
|
-
res.end(JSON.stringify({
|
|
352
|
-
error: err instanceof Error ? err.message : String(err),
|
|
353
|
-
}));
|
|
353
|
+
respond500(res, err);
|
|
354
354
|
}
|
|
355
355
|
return;
|
|
356
356
|
}
|
|
@@ -399,10 +399,7 @@ export class Server extends EventEmitter {
|
|
|
399
399
|
res.end(body);
|
|
400
400
|
}
|
|
401
401
|
catch (err) {
|
|
402
|
-
res
|
|
403
|
-
res.end(JSON.stringify({
|
|
404
|
-
error: err instanceof Error ? err.message : String(err),
|
|
405
|
-
}));
|
|
402
|
+
respond500(res, err);
|
|
406
403
|
}
|
|
407
404
|
return;
|
|
408
405
|
}
|
|
@@ -418,10 +415,7 @@ export class Server extends EventEmitter {
|
|
|
418
415
|
res.end(JSON.stringify(data));
|
|
419
416
|
}
|
|
420
417
|
catch (err) {
|
|
421
|
-
res
|
|
422
|
-
res.end(JSON.stringify({
|
|
423
|
-
error: err instanceof Error ? err.message : String(err),
|
|
424
|
-
}));
|
|
418
|
+
respond500(res, err);
|
|
425
419
|
}
|
|
426
420
|
return;
|
|
427
421
|
}
|
|
@@ -436,10 +430,7 @@ export class Server extends EventEmitter {
|
|
|
436
430
|
res.end(JSON.stringify({ events: data, count: data.length }));
|
|
437
431
|
}
|
|
438
432
|
catch (err) {
|
|
439
|
-
res
|
|
440
|
-
res.end(JSON.stringify({
|
|
441
|
-
error: err instanceof Error ? err.message : String(err),
|
|
442
|
-
}));
|
|
433
|
+
respond500(res, err);
|
|
443
434
|
}
|
|
444
435
|
return;
|
|
445
436
|
}
|
|
@@ -475,10 +466,7 @@ export class Server extends EventEmitter {
|
|
|
475
466
|
res.end(JSON.stringify(data));
|
|
476
467
|
}
|
|
477
468
|
catch (err) {
|
|
478
|
-
res
|
|
479
|
-
res.end(JSON.stringify({
|
|
480
|
-
error: err instanceof Error ? err.message : String(err),
|
|
481
|
-
}));
|
|
469
|
+
respond500(res, err);
|
|
482
470
|
}
|
|
483
471
|
return;
|
|
484
472
|
}
|
|
@@ -549,12 +537,7 @@ export class Server extends EventEmitter {
|
|
|
549
537
|
}
|
|
550
538
|
}
|
|
551
539
|
catch (err) {
|
|
552
|
-
|
|
553
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
554
|
-
res.end(JSON.stringify({
|
|
555
|
-
error: err instanceof Error ? err.message : String(err),
|
|
556
|
-
}));
|
|
557
|
-
}
|
|
540
|
+
respond500(res, err);
|
|
558
541
|
}
|
|
559
542
|
})();
|
|
560
543
|
return;
|
|
@@ -577,10 +560,7 @@ export class Server extends EventEmitter {
|
|
|
577
560
|
res.end(JSON.stringify(data));
|
|
578
561
|
}
|
|
579
562
|
catch (err) {
|
|
580
|
-
res
|
|
581
|
-
res.end(JSON.stringify({
|
|
582
|
-
error: err instanceof Error ? err.message : String(err),
|
|
583
|
-
}));
|
|
563
|
+
respond500(res, err);
|
|
584
564
|
}
|
|
585
565
|
return;
|
|
586
566
|
}
|
|
@@ -594,10 +574,7 @@ export class Server extends EventEmitter {
|
|
|
594
574
|
res.end(JSON.stringify(data));
|
|
595
575
|
}
|
|
596
576
|
catch (err) {
|
|
597
|
-
res
|
|
598
|
-
res.end(JSON.stringify({
|
|
599
|
-
error: err instanceof Error ? err.message : String(err),
|
|
600
|
-
}));
|
|
577
|
+
respond500(res, err);
|
|
601
578
|
}
|
|
602
579
|
return;
|
|
603
580
|
}
|
|
@@ -625,10 +602,7 @@ export class Server extends EventEmitter {
|
|
|
625
602
|
}
|
|
626
603
|
}
|
|
627
604
|
catch (err) {
|
|
628
|
-
res
|
|
629
|
-
res.end(JSON.stringify({
|
|
630
|
-
error: err instanceof Error ? err.message : String(err),
|
|
631
|
-
}));
|
|
605
|
+
respond500(res, err);
|
|
632
606
|
}
|
|
633
607
|
return;
|
|
634
608
|
}
|
|
@@ -681,10 +655,7 @@ export class Server extends EventEmitter {
|
|
|
681
655
|
res.end(JSON.stringify(data));
|
|
682
656
|
}
|
|
683
657
|
catch (err) {
|
|
684
|
-
res
|
|
685
|
-
res.end(JSON.stringify({
|
|
686
|
-
error: err instanceof Error ? err.message : String(err),
|
|
687
|
-
}));
|
|
658
|
+
respond500(res, err);
|
|
688
659
|
}
|
|
689
660
|
return;
|
|
690
661
|
}
|
|
@@ -703,67 +674,64 @@ export class Server extends EventEmitter {
|
|
|
703
674
|
}
|
|
704
675
|
}
|
|
705
676
|
catch (err) {
|
|
706
|
-
res
|
|
707
|
-
res.end(JSON.stringify({
|
|
708
|
-
error: err instanceof Error ? err.message : String(err),
|
|
709
|
-
}));
|
|
677
|
+
respond500(res, err);
|
|
710
678
|
}
|
|
711
679
|
return;
|
|
712
680
|
}
|
|
713
681
|
if (parsedUrl.pathname?.startsWith("/hooks/") && req.method === "POST") {
|
|
714
682
|
const hookPath = parsedUrl.pathname.substring("/hooks".length);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
683
|
+
// 256 KB — webhook payloads from GitHub/Linear/etc. are typically
|
|
684
|
+
// 1–25 KB; 256 KB matches RECIPE_ROUTE_BODY_CAPS.content as a
|
|
685
|
+
// generous ceiling that still bounds an authenticated DoS vector.
|
|
686
|
+
const HOOKS_BODY_CAP = 256 * 1024;
|
|
687
|
+
const read = await readBodyWithCap(req, HOOKS_BODY_CAP);
|
|
688
|
+
if (!read.ok) {
|
|
689
|
+
respond413(res, HOOKS_BODY_CAP);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
let payload;
|
|
693
|
+
if (read.body.trim()) {
|
|
694
|
+
try {
|
|
695
|
+
payload = JSON.parse(read.body);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
payload = read.body;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!this.webhookFn) {
|
|
702
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
703
|
+
res.end(JSON.stringify({
|
|
704
|
+
ok: false,
|
|
705
|
+
error: "Webhooks unavailable — start bridge with --claude-driver subprocess",
|
|
706
|
+
}));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const result = await this.webhookFn(hookPath, payload);
|
|
710
|
+
const status = result.ok
|
|
711
|
+
? 200
|
|
712
|
+
: result.error === "not_found"
|
|
713
|
+
? 404
|
|
714
|
+
: 400;
|
|
715
|
+
// Record in ring buffer so the dashboard can show "last
|
|
716
|
+
// payload" per recipe. Skip not_found so unknown paths don't
|
|
717
|
+
// pollute the buffer with garbage / scanner traffic.
|
|
718
|
+
if (result.error !== "not_found") {
|
|
719
|
+
const existing = this.webhookPayloads.get(hookPath) ?? [];
|
|
720
|
+
existing.unshift({
|
|
721
|
+
receivedAt: Date.now(),
|
|
722
|
+
payload,
|
|
723
|
+
ok: result.ok,
|
|
724
|
+
error: result.error,
|
|
725
|
+
taskId: result.taskId,
|
|
726
|
+
recipeName: result.name,
|
|
727
|
+
});
|
|
728
|
+
if (existing.length > Server.MAX_WEBHOOK_PAYLOADS) {
|
|
729
|
+
existing.length = Server.MAX_WEBHOOK_PAYLOADS;
|
|
730
|
+
}
|
|
731
|
+
this.webhookPayloads.set(hookPath, existing);
|
|
732
|
+
}
|
|
733
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
734
|
+
res.end(JSON.stringify(result));
|
|
767
735
|
return;
|
|
768
736
|
}
|
|
769
737
|
if (parsedUrl.pathname?.startsWith("/webhook-payloads/") &&
|
|
@@ -916,6 +884,7 @@ export class Server extends EventEmitter {
|
|
|
916
884
|
loadRecipeContentFn: this.loadRecipeContentFn,
|
|
917
885
|
saveRecipeContentFn: this.saveRecipeContentFn,
|
|
918
886
|
deleteRecipeContentFn: this.deleteRecipeContentFn,
|
|
887
|
+
archiveRecipeFn: this.archiveRecipeFn,
|
|
919
888
|
duplicateRecipeFn: this.duplicateRecipeFn,
|
|
920
889
|
promoteRecipeVariantFn: this.promoteRecipeVariantFn,
|
|
921
890
|
lintRecipeContentFn: this.lintRecipeContentFn,
|
|
@@ -943,10 +912,7 @@ export class Server extends EventEmitter {
|
|
|
943
912
|
res.end(JSON.stringify(data));
|
|
944
913
|
}
|
|
945
914
|
catch (err) {
|
|
946
|
-
res
|
|
947
|
-
res.end(JSON.stringify({
|
|
948
|
-
error: err instanceof Error ? err.message : String(err),
|
|
949
|
-
}));
|
|
915
|
+
respond500(res, err);
|
|
950
916
|
}
|
|
951
917
|
return;
|
|
952
918
|
}
|
|
@@ -962,19 +928,28 @@ export class Server extends EventEmitter {
|
|
|
962
928
|
res.end(JSON.stringify(data));
|
|
963
929
|
}
|
|
964
930
|
catch (err) {
|
|
965
|
-
res
|
|
966
|
-
res.end(JSON.stringify({
|
|
967
|
-
error: err instanceof Error ? err.message : String(err),
|
|
968
|
-
}));
|
|
931
|
+
respond500(res, err);
|
|
969
932
|
}
|
|
970
933
|
return;
|
|
971
934
|
}
|
|
972
935
|
if (parsedUrl.pathname === "/settings" && req.method === "POST") {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
936
|
+
// 16 KB — settings POSTs are short-string fields (URLs, API keys,
|
|
937
|
+
// gate level). 16 KB is generous; an authenticated attacker can't
|
|
938
|
+
// stream gigabytes here.
|
|
939
|
+
const SETTINGS_BODY_CAP = 16 * 1024;
|
|
940
|
+
const parsed = await readJsonBody(req, SETTINGS_BODY_CAP);
|
|
941
|
+
if (!parsed.ok) {
|
|
942
|
+
if (parsed.code === "too_large") {
|
|
943
|
+
respond413(res, SETTINGS_BODY_CAP);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
947
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
{
|
|
952
|
+
const body = parsed.value ?? {};
|
|
978
953
|
const hasWebhookUpdate = body.webhookUrl !== undefined;
|
|
979
954
|
const raw = hasWebhookUpdate
|
|
980
955
|
? (body.webhookUrl?.trim() ?? "")
|
|
@@ -1032,6 +1007,8 @@ export class Server extends EventEmitter {
|
|
|
1032
1007
|
"openai",
|
|
1033
1008
|
"grok",
|
|
1034
1009
|
"gemini",
|
|
1010
|
+
"gemini-api",
|
|
1011
|
+
"local",
|
|
1035
1012
|
"none",
|
|
1036
1013
|
];
|
|
1037
1014
|
if (!validDrivers.includes(driverRaw)) {
|
|
@@ -1043,7 +1020,17 @@ export class Server extends EventEmitter {
|
|
|
1043
1020
|
}
|
|
1044
1021
|
const driver = driverRaw;
|
|
1045
1022
|
cfg.driver = driver;
|
|
1046
|
-
|
|
1023
|
+
try {
|
|
1024
|
+
saveBridgeConfigDriver(driver, this.bridgeConfigPath);
|
|
1025
|
+
}
|
|
1026
|
+
catch (writeErr) {
|
|
1027
|
+
this.logger.error(`[/config/patchwork] saveBridgeConfigDriver failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
|
|
1028
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1029
|
+
res.end(JSON.stringify({
|
|
1030
|
+
error: "Failed to write bridge driver config",
|
|
1031
|
+
}));
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1047
1034
|
}
|
|
1048
1035
|
if (body.model !== undefined) {
|
|
1049
1036
|
const validModels = [
|
|
@@ -1077,9 +1064,32 @@ export class Server extends EventEmitter {
|
|
|
1077
1064
|
res.end(JSON.stringify({ error: "Invalid apiKey provider or key" }));
|
|
1078
1065
|
return;
|
|
1079
1066
|
}
|
|
1080
|
-
|
|
1067
|
+
// Provider keys go to the secure store (Keychain/DPAPI/Secret
|
|
1068
|
+
// Service / AES-256-GCM file fallback) — never persisted to
|
|
1069
|
+
// ~/.patchwork/config.json. Empty string clears.
|
|
1070
|
+
try {
|
|
1071
|
+
saveApiKeyToSecureStore(provider, key);
|
|
1072
|
+
}
|
|
1073
|
+
catch (writeErr) {
|
|
1074
|
+
this.logger.error(`[/config/patchwork] saveApiKeyToSecureStore failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
|
|
1075
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1076
|
+
res.end(JSON.stringify({
|
|
1077
|
+
error: "Failed to write provider API key",
|
|
1078
|
+
}));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
savePatchworkConfig(cfg, configPath);
|
|
1084
|
+
}
|
|
1085
|
+
catch (writeErr) {
|
|
1086
|
+
this.logger.error(`[/config/patchwork] savePatchworkConfig failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
|
|
1087
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1088
|
+
res.end(JSON.stringify({
|
|
1089
|
+
error: "Failed to write patchwork config",
|
|
1090
|
+
}));
|
|
1091
|
+
return;
|
|
1081
1092
|
}
|
|
1082
|
-
savePatchworkConfig(cfg, configPath);
|
|
1083
1093
|
if (hasWebhookUpdate) {
|
|
1084
1094
|
this.approvalWebhookUrl = raw || undefined;
|
|
1085
1095
|
}
|
|
@@ -1105,44 +1115,45 @@ export class Server extends EventEmitter {
|
|
|
1105
1115
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1106
1116
|
res.end(JSON.stringify({ ok: true, restartRequired }));
|
|
1107
1117
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
});
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
this.logger.error(`[/config/patchwork] error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
1121
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1122
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
1123
|
+
}
|
|
1115
1124
|
return;
|
|
1116
1125
|
}
|
|
1117
1126
|
// CC hook notify endpoint — lightweight alternative to full MCP session for hook wiring.
|
|
1118
1127
|
if (parsedUrl.pathname === "/notify" && req.method === "POST") {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1129
|
-
res.end(JSON.stringify({
|
|
1130
|
-
ok: false,
|
|
1131
|
-
error: "Automation not enabled",
|
|
1132
|
-
}));
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
const result = this.notifyFn(event, args);
|
|
1136
|
-
res.writeHead(result.ok ? 200 : 400, {
|
|
1137
|
-
"Content-Type": "application/json",
|
|
1138
|
-
});
|
|
1139
|
-
res.end(JSON.stringify(result));
|
|
1140
|
-
}
|
|
1141
|
-
catch {
|
|
1142
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1143
|
-
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
1128
|
+
// 8 KB — notify payloads carry an event name + small arg map
|
|
1129
|
+
// (taskId, prompt, tool name). 8 KB fits everything we send today
|
|
1130
|
+
// with headroom.
|
|
1131
|
+
const NOTIFY_BODY_CAP = 8 * 1024;
|
|
1132
|
+
const parsed = await readJsonBody(req, NOTIFY_BODY_CAP);
|
|
1133
|
+
if (!parsed.ok) {
|
|
1134
|
+
if (parsed.code === "too_large") {
|
|
1135
|
+
respond413(res, NOTIFY_BODY_CAP);
|
|
1136
|
+
return;
|
|
1144
1137
|
}
|
|
1138
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1139
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const event = parsed.value?.event ?? "";
|
|
1143
|
+
const args = parsed.value?.args ?? {};
|
|
1144
|
+
if (!this.notifyFn) {
|
|
1145
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1146
|
+
res.end(JSON.stringify({
|
|
1147
|
+
ok: false,
|
|
1148
|
+
error: "Automation not enabled",
|
|
1149
|
+
}));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const result = this.notifyFn(event, args);
|
|
1153
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
1154
|
+
"Content-Type": "application/json",
|
|
1145
1155
|
});
|
|
1156
|
+
res.end(JSON.stringify(result));
|
|
1146
1157
|
return;
|
|
1147
1158
|
}
|
|
1148
1159
|
// Single-approval detail lookup for the dashboard detail page.
|
|
@@ -1160,10 +1171,7 @@ export class Server extends EventEmitter {
|
|
|
1160
1171
|
res.end(JSON.stringify(data));
|
|
1161
1172
|
}
|
|
1162
1173
|
catch (err) {
|
|
1163
|
-
res
|
|
1164
|
-
res.end(JSON.stringify({
|
|
1165
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1166
|
-
}));
|
|
1174
|
+
respond500(res, err);
|
|
1167
1175
|
}
|
|
1168
1176
|
return;
|
|
1169
1177
|
}
|
|
@@ -1177,101 +1185,99 @@ export class Server extends EventEmitter {
|
|
|
1177
1185
|
if (parsedUrl.pathname === "/approvals" ||
|
|
1178
1186
|
parsedUrl.pathname === "/cc-permissions" ||
|
|
1179
1187
|
/^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname)) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
try {
|
|
1195
|
-
const result = await routeApprovalRequest({
|
|
1196
|
-
method: req.method ?? "GET",
|
|
1197
|
-
path: parsedUrl.pathname,
|
|
1198
|
-
body: parsedBody,
|
|
1199
|
-
query: parsedUrl.searchParams,
|
|
1200
|
-
approvalToken: req.headers["x-approval-token"],
|
|
1201
|
-
}, {
|
|
1202
|
-
queue: getApprovalQueue(),
|
|
1203
|
-
workspace: process.cwd(),
|
|
1204
|
-
managedSettingsPath: this.managedSettingsPath,
|
|
1205
|
-
onDecision: this.onApprovalDecision,
|
|
1206
|
-
webhookUrl: this.approvalWebhookUrl,
|
|
1207
|
-
approvalGate: this.approvalGate,
|
|
1208
|
-
pushServiceUrl: this.pushServiceUrl,
|
|
1209
|
-
pushServiceToken: this.pushServiceToken,
|
|
1210
|
-
pushServiceBaseUrl: this.pushServiceBaseUrl,
|
|
1211
|
-
activityLog: this.activityLog,
|
|
1212
|
-
// RecipeRunLog satisfies RecipeRunQuerier structurally
|
|
1213
|
-
// — the cast bridges TS contravariance: RecipeRunQuerier's
|
|
1214
|
-
// narrow query interface (`status?: string`) is deliberately
|
|
1215
|
-
// loose so tests can mock it; RecipeRunLog's stricter
|
|
1216
|
-
// RunStatus union is a strict subset and fails the param
|
|
1217
|
-
// contravariance check despite being safe at runtime.
|
|
1218
|
-
recipeRunLog: this.recipeRunLog,
|
|
1219
|
-
enableTimeOfDayAnomaly: this.enableTimeOfDayAnomaly,
|
|
1220
|
-
});
|
|
1221
|
-
res.writeHead(result.status, {
|
|
1222
|
-
"Content-Type": "application/json",
|
|
1223
|
-
});
|
|
1224
|
-
res.end(JSON.stringify(result.body));
|
|
1225
|
-
}
|
|
1226
|
-
catch (err) {
|
|
1227
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1228
|
-
res.end(JSON.stringify({
|
|
1229
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1230
|
-
}));
|
|
1188
|
+
// 32 KB — approvals carry decision + reason + optional permission
|
|
1189
|
+
// rule patches; 32 KB matches RECIPE_ROUTE_BODY_CAPS.run. Critical
|
|
1190
|
+
// here: /approve/:id and /reject/:id are reachable via the
|
|
1191
|
+
// x-approval-token phone-path bypass at line 609-612 — without a
|
|
1192
|
+
// cap an unbounded body read happens *before* token validation.
|
|
1193
|
+
const APPROVALS_BODY_CAP = 32 * 1024;
|
|
1194
|
+
const parsed = await readJsonBody(req, APPROVALS_BODY_CAP);
|
|
1195
|
+
if (!parsed.ok) {
|
|
1196
|
+
if (parsed.code === "too_large") {
|
|
1197
|
+
respond413(res, APPROVALS_BODY_CAP);
|
|
1198
|
+
return;
|
|
1231
1199
|
}
|
|
1232
|
-
|
|
1200
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1201
|
+
res.end(JSON.stringify({ error: "invalid JSON body" }));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const parsedBody = parsed.value;
|
|
1205
|
+
try {
|
|
1206
|
+
const result = await routeApprovalRequest({
|
|
1207
|
+
method: req.method ?? "GET",
|
|
1208
|
+
path: parsedUrl.pathname,
|
|
1209
|
+
body: parsedBody,
|
|
1210
|
+
query: parsedUrl.searchParams,
|
|
1211
|
+
approvalToken: req.headers["x-approval-token"],
|
|
1212
|
+
}, {
|
|
1213
|
+
queue: getApprovalQueue(),
|
|
1214
|
+
workspace: process.cwd(),
|
|
1215
|
+
managedSettingsPath: this.managedSettingsPath,
|
|
1216
|
+
onDecision: this.onApprovalDecision,
|
|
1217
|
+
webhookUrl: this.approvalWebhookUrl,
|
|
1218
|
+
approvalGate: this.approvalGate,
|
|
1219
|
+
pushServiceUrl: this.pushServiceUrl,
|
|
1220
|
+
pushServiceToken: this.pushServiceToken,
|
|
1221
|
+
pushServiceBaseUrl: this.pushServiceBaseUrl,
|
|
1222
|
+
activityLog: this.activityLog,
|
|
1223
|
+
// RecipeRunLog satisfies RecipeRunQuerier structurally
|
|
1224
|
+
// — the cast bridges TS contravariance: RecipeRunQuerier's
|
|
1225
|
+
// narrow query interface (`status?: string`) is deliberately
|
|
1226
|
+
// loose so tests can mock it; RecipeRunLog's stricter
|
|
1227
|
+
// RunStatus union is a strict subset and fails the param
|
|
1228
|
+
// contravariance check despite being safe at runtime.
|
|
1229
|
+
recipeRunLog: this.recipeRunLog,
|
|
1230
|
+
enableTimeOfDayAnomaly: this.enableTimeOfDayAnomaly,
|
|
1231
|
+
});
|
|
1232
|
+
res.writeHead(result.status, {
|
|
1233
|
+
"Content-Type": "application/json",
|
|
1234
|
+
});
|
|
1235
|
+
res.end(JSON.stringify(result.body));
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
respond500(res, err);
|
|
1239
|
+
}
|
|
1233
1240
|
return;
|
|
1234
1241
|
}
|
|
1235
1242
|
// Quick-task launch endpoint — mirrors /notify pattern. Bearer auth already checked above.
|
|
1236
1243
|
if (parsedUrl.pathname === "/launch-quick-task" &&
|
|
1237
1244
|
req.method === "POST") {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1273
|
-
})();
|
|
1245
|
+
// 4 KB — body is `{ presetId?: string; source?: string }`, two
|
|
1246
|
+
// short strings. 4 KB is plenty.
|
|
1247
|
+
const QUICK_TASK_BODY_CAP = 4 * 1024;
|
|
1248
|
+
const parsed = await readJsonBody(req, QUICK_TASK_BODY_CAP);
|
|
1249
|
+
if (!parsed.ok) {
|
|
1250
|
+
if (parsed.code === "too_large") {
|
|
1251
|
+
respond413(res, QUICK_TASK_BODY_CAP);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1255
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const presetId = parsed.value?.presetId;
|
|
1259
|
+
const source = parsed.value?.source ?? "cli";
|
|
1260
|
+
if (typeof presetId !== "string" || !presetId) {
|
|
1261
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1262
|
+
res.end(JSON.stringify({
|
|
1263
|
+
ok: false,
|
|
1264
|
+
error: "presetId required",
|
|
1265
|
+
}));
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (!this.launchQuickTaskFn) {
|
|
1269
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1270
|
+
res.end(JSON.stringify({
|
|
1271
|
+
ok: false,
|
|
1272
|
+
error: "Quick tasks unavailable — requires --claude-driver subprocess",
|
|
1273
|
+
}));
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const result = await this.launchQuickTaskFn(presetId, source);
|
|
1277
|
+
res.writeHead(result.ok ? 200 : 429, {
|
|
1278
|
+
"Content-Type": "application/json",
|
|
1274
1279
|
});
|
|
1280
|
+
res.end(JSON.stringify(result));
|
|
1275
1281
|
return;
|
|
1276
1282
|
}
|
|
1277
1283
|
// MCP Streamable HTTP transport — POST/GET/DELETE /mcp.
|
|
@@ -1283,11 +1289,9 @@ export class Server extends EventEmitter {
|
|
|
1283
1289
|
req.method === "GET" ||
|
|
1284
1290
|
req.method === "DELETE") {
|
|
1285
1291
|
this.httpMcpHandler(req, res).catch((err) => {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
1290
|
-
}
|
|
1292
|
+
// respond500 logs the underlying error detail server-side; no
|
|
1293
|
+
// need to also funnel it through this.logger.
|
|
1294
|
+
respond500(res, err, "/mcp HTTP handler");
|
|
1291
1295
|
});
|
|
1292
1296
|
return;
|
|
1293
1297
|
}
|