patchwork-os 0.2.0-beta.2 → 0.2.0-beta.4
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.bridge.md +5 -5
- package/README.md +244 -30
- package/dist/activityLog.d.ts +6 -0
- package/dist/activityLog.js +10 -1
- package/dist/activityLog.js.map +1 -1
- package/dist/analyticsPrefs.d.ts +35 -2
- package/dist/analyticsPrefs.js +120 -21
- package/dist/analyticsPrefs.js.map +1 -1
- package/dist/analyticsSend.js +5 -1
- package/dist/analyticsSend.js.map +1 -1
- package/dist/approvalHttp.js +25 -8
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +44 -1
- package/dist/approvalQueue.js +117 -0
- package/dist/approvalQueue.js.map +1 -1
- package/dist/automation.d.ts +3 -3
- package/dist/automation.js +12 -5
- package/dist/automation.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +140 -8
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeLockDiscovery.d.ts +27 -1
- package/dist/bridgeLockDiscovery.js +38 -11
- package/dist/bridgeLockDiscovery.js.map +1 -1
- package/dist/claudeOrchestrator.js +27 -10
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/dashboard.js +8 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/install.js +3 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/patchworkInit.d.ts +5 -0
- package/dist/commands/patchworkInit.js +89 -7
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +51 -0
- package/dist/commands/recipe.js +353 -2
- package/dist/commands/recipe.js.map +1 -1
- package/dist/commands/recipeInstall.js +6 -3
- package/dist/commands/recipeInstall.js.map +1 -1
- package/dist/commands/task.js +2 -2
- package/dist/commands/task.js.map +1 -1
- package/dist/commitIssueLinkLog.d.ts +16 -0
- package/dist/commitIssueLinkLog.js +87 -4
- package/dist/commitIssueLinkLog.js.map +1 -1
- package/dist/config.d.ts +29 -3
- package/dist/config.js +77 -21
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.js +1 -1
- package/dist/connectorRoutes.js.map +1 -1
- package/dist/connectors/asana.js +4 -3
- package/dist/connectors/asana.js.map +1 -1
- package/dist/connectors/confluence.js +35 -0
- package/dist/connectors/confluence.js.map +1 -1
- package/dist/connectors/datadog.js +33 -4
- package/dist/connectors/datadog.js.map +1 -1
- package/dist/connectors/discord.js +5 -4
- package/dist/connectors/discord.js.map +1 -1
- package/dist/connectors/gitlab.js +7 -1
- package/dist/connectors/gitlab.js.map +1 -1
- package/dist/connectors/mcpOAuth.js +71 -6
- package/dist/connectors/mcpOAuth.js.map +1 -1
- package/dist/connectors/slack.d.ts +1 -1
- package/dist/connectors/slack.js +56 -4
- package/dist/connectors/slack.js.map +1 -1
- package/dist/connectors/tokenStorage.js +56 -14
- package/dist/connectors/tokenStorage.js.map +1 -1
- package/dist/decisionTraceLog.d.ts +28 -0
- package/dist/decisionTraceLog.js +115 -7
- package/dist/decisionTraceLog.js.map +1 -1
- package/dist/drivers/claude/subprocess.js +22 -3
- package/dist/drivers/claude/subprocess.js.map +1 -1
- package/dist/drivers/gemini/index.js +19 -3
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/extensionClient.d.ts +29 -4
- package/dist/extensionClient.js +26 -11
- package/dist/extensionClient.js.map +1 -1
- package/dist/featureFlags.d.ts +76 -0
- package/dist/featureFlags.js +153 -3
- package/dist/featureFlags.js.map +1 -1
- package/dist/fileLockSync.d.ts +67 -0
- package/dist/fileLockSync.js +126 -0
- package/dist/fileLockSync.js.map +1 -0
- package/dist/fp/automationInterpreter.d.ts +6 -0
- package/dist/fp/automationInterpreter.js +15 -2
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationState.d.ts +1 -1
- package/dist/fp/automationState.js +10 -0
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/commandDescription.js +7 -1
- package/dist/fp/commandDescription.js.map +1 -1
- package/dist/fsWatchWithFallback.d.ts +36 -0
- package/dist/fsWatchWithFallback.js +127 -0
- package/dist/fsWatchWithFallback.js.map +1 -0
- package/dist/index.js +797 -75
- package/dist/index.js.map +1 -1
- package/dist/installGuard.js +6 -2
- package/dist/installGuard.js.map +1 -1
- package/dist/lockfile.js +31 -4
- package/dist/lockfile.js.map +1 -1
- package/dist/patchworkConfig.js +13 -3
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/pluginLoader.js +10 -1
- package/dist/pluginLoader.js.map +1 -1
- package/dist/pluginWatcher.js +6 -13
- package/dist/pluginWatcher.js.map +1 -1
- package/dist/preToolUseHook.js +3 -2
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/processTree.d.ts +34 -0
- package/dist/processTree.js +105 -0
- package/dist/processTree.js.map +1 -0
- package/dist/prompts.js +3 -3
- package/dist/prompts.js.map +1 -1
- package/dist/recipeOrchestration.js +35 -1
- package/dist/recipeOrchestration.js.map +1 -1
- package/dist/recipeRoutes.d.ts +37 -0
- package/dist/recipeRoutes.js +236 -33
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +25 -5
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/chainedRunner.js +16 -2
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/connectorPreflight.d.ts +53 -0
- package/dist/recipes/connectorPreflight.js +143 -0
- package/dist/recipes/connectorPreflight.js.map +1 -0
- package/dist/recipes/githubInstallSource.d.ts +62 -0
- package/dist/recipes/githubInstallSource.js +125 -0
- package/dist/recipes/githubInstallSource.js.map +1 -0
- package/dist/recipes/haltCategory.d.ts +80 -0
- package/dist/recipes/haltCategory.js +125 -0
- package/dist/recipes/haltCategory.js.map +1 -0
- package/dist/recipes/idempotencyKey.d.ts +126 -0
- package/dist/recipes/idempotencyKey.js +297 -0
- package/dist/recipes/idempotencyKey.js.map +1 -0
- package/dist/recipes/installer.js +48 -2
- package/dist/recipes/installer.js.map +1 -1
- package/dist/recipes/judgeSummary.d.ts +50 -0
- package/dist/recipes/judgeSummary.js +47 -0
- package/dist/recipes/judgeSummary.js.map +1 -0
- package/dist/recipes/judgeVerdict.d.ts +48 -0
- package/dist/recipes/judgeVerdict.js +174 -0
- package/dist/recipes/judgeVerdict.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +9 -0
- package/dist/recipes/migrations/index.js +133 -0
- package/dist/recipes/migrations/index.js.map +1 -1
- package/dist/recipes/parser.js +82 -4
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/runBudget.d.ts +70 -0
- package/dist/recipes/runBudget.js +109 -0
- package/dist/recipes/runBudget.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +17 -0
- package/dist/recipes/scheduler.js +34 -2
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +30 -0
- package/dist/recipes/toolRegistry.js +19 -0
- package/dist/recipes/toolRegistry.js.map +1 -1
- package/dist/recipes/tools/http.d.ts +10 -0
- package/dist/recipes/tools/http.js +176 -0
- package/dist/recipes/tools/http.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +1 -0
- package/dist/recipes/tools/index.js +1 -0
- package/dist/recipes/tools/index.js.map +1 -1
- package/dist/recipes/validation.js +1 -1
- package/dist/recipes/validation.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +75 -8
- package/dist/recipes/yamlRunner.js +174 -28
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/resources.js +21 -13
- package/dist/resources.js.map +1 -1
- package/dist/runLog.d.ts +28 -0
- package/dist/runLog.js +19 -3
- package/dist/runLog.js.map +1 -1
- package/dist/sanitizeParsedJson.d.ts +39 -0
- package/dist/sanitizeParsedJson.js +55 -0
- package/dist/sanitizeParsedJson.js.map +1 -0
- package/dist/server.d.ts +79 -0
- package/dist/server.js +356 -3
- package/dist/server.js.map +1 -1
- package/dist/sessionCheckpoint.d.ts +8 -0
- package/dist/sessionCheckpoint.js +18 -2
- package/dist/sessionCheckpoint.js.map +1 -1
- package/dist/streamableHttp.js +17 -6
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tools/bridgeDoctor.js +6 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/detectUnusedCode.js +9 -7
- package/dist/tools/detectUnusedCode.js.map +1 -1
- package/dist/tools/editText.js +2 -1
- package/dist/tools/editText.js.map +1 -1
- package/dist/tools/fileOperations.js +2 -1
- package/dist/tools/fileOperations.js.map +1 -1
- package/dist/tools/fileWatcher.js +8 -2
- package/dist/tools/fileWatcher.js.map +1 -1
- package/dist/tools/fixAllLintErrors.js +10 -5
- package/dist/tools/fixAllLintErrors.js.map +1 -1
- package/dist/tools/formatDocument.js +10 -5
- package/dist/tools/formatDocument.js.map +1 -1
- package/dist/tools/getCodeCoverage.js +7 -3
- package/dist/tools/getCodeCoverage.js.map +1 -1
- package/dist/tools/handoffNote.js +2 -1
- package/dist/tools/handoffNote.js.map +1 -1
- package/dist/tools/headless/lspClient.js +3 -0
- package/dist/tools/headless/lspClient.js.map +1 -1
- package/dist/tools/lsp.js +17 -0
- package/dist/tools/lsp.js.map +1 -1
- package/dist/tools/openDiff.js +4 -1
- package/dist/tools/openDiff.js.map +1 -1
- package/dist/tools/openFile.js +4 -1
- package/dist/tools/openFile.js.map +1 -1
- package/dist/tools/organizeImports.js +5 -3
- package/dist/tools/organizeImports.js.map +1 -1
- package/dist/tools/previewEdit.js +7 -2
- package/dist/tools/previewEdit.js.map +1 -1
- package/dist/tools/recentTracesDigest.js +56 -11
- package/dist/tools/recentTracesDigest.js.map +1 -1
- package/dist/tools/refactorExtractFunction.js +4 -1
- package/dist/tools/refactorExtractFunction.js.map +1 -1
- package/dist/tools/refactorPreview.js +10 -2
- package/dist/tools/refactorPreview.js.map +1 -1
- package/dist/tools/replaceBlock.js +2 -1
- package/dist/tools/replaceBlock.js.map +1 -1
- package/dist/tools/searchAndReplace.js +2 -1
- package/dist/tools/searchAndReplace.js.map +1 -1
- package/dist/tools/spawnWorkspace.js +15 -7
- package/dist/tools/spawnWorkspace.js.map +1 -1
- package/dist/tools/testRunners/vitestJest.js +3 -1
- package/dist/tools/testRunners/vitestJest.js.map +1 -1
- package/dist/tools/transaction.js +4 -1
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/utils.js +68 -8
- package/dist/tools/utils.js.map +1 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +18 -4
- package/dist/transport.js.map +1 -1
- package/dist/winShim.d.ts +34 -0
- package/dist/winShim.js +94 -0
- package/dist/winShim.js.map +1 -0
- package/dist/writeFileAtomic.d.ts +23 -0
- package/dist/writeFileAtomic.js +94 -0
- package/dist/writeFileAtomic.js.map +1 -0
- package/package.json +17 -6
- package/scripts/postinstall.mjs +42 -2
- package/scripts/smoke/run-all.mjs +213 -0
- package/scripts/start-all.mjs +572 -0
- package/scripts/start-all.ps1 +209 -0
- package/scripts/start-all.sh +73 -17
- package/scripts/start-orchestrator.ps1 +158 -0
- package/scripts/start-remote.mjs +122 -0
- package/templates/automation-policies/recipe-authoring.json +1 -1
- package/templates/automation-policies/security-first.json +1 -1
- package/templates/automation-policies/strict-lint.json +1 -1
- package/templates/automation-policies/test-driven.json +1 -1
- package/templates/automation-policy.example.json +1 -1
- package/templates/co.patchwork-os.bridge.plist +1 -1
- package/templates/recipes/approval-queue-ui-test.yaml +1 -1
- package/templates/recipes/ctx-loop-test.yaml +1 -1
- package/templates/recipes/webhook/apple-watch-health-log.yaml +145 -0
- package/dist/commands/marketplace.d.ts +0 -16
- package/dist/commands/marketplace.js +0 -32
- package/dist/commands/marketplace.js.map +0 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +0 -10
- package/dist/recipes/legacyRecipeCompat.js +0 -131
- package/dist/recipes/legacyRecipeCompat.js.map +0 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-name prefix → connector id (matching the IDs returned by
|
|
3
|
+
* `/connections` — see src/connectors/gmail.ts handleConnectionsList).
|
|
4
|
+
* Both `google-calendar` and the dashboard's `googleCalendar` spelling
|
|
5
|
+
* are accepted on the caller side; this map is authoritative for the
|
|
6
|
+
* bridge.
|
|
7
|
+
*/
|
|
8
|
+
export const TOOL_PREFIX_TO_CONNECTOR = {
|
|
9
|
+
slack_: "slack",
|
|
10
|
+
github_: "github",
|
|
11
|
+
jira_: "jira",
|
|
12
|
+
linear_: "linear",
|
|
13
|
+
gmail_: "gmail",
|
|
14
|
+
calendar_: "google-calendar",
|
|
15
|
+
drive_: "google-drive",
|
|
16
|
+
intercom_: "intercom",
|
|
17
|
+
hubspot_: "hubspot",
|
|
18
|
+
datadog_: "datadog",
|
|
19
|
+
stripe_: "stripe",
|
|
20
|
+
sentry_: "sentry",
|
|
21
|
+
zendesk_: "zendesk",
|
|
22
|
+
asana_: "asana",
|
|
23
|
+
notion_: "notion",
|
|
24
|
+
confluence_: "confluence",
|
|
25
|
+
discord_: "discord",
|
|
26
|
+
gitlab_: "gitlab",
|
|
27
|
+
pagerduty_: "pagerduty",
|
|
28
|
+
};
|
|
29
|
+
function toolsOfStep(step) {
|
|
30
|
+
// agent: false steps carry a single `tool` field. agent: true steps
|
|
31
|
+
// optionally list permitted tools in `tools[]`. Other shapes (nested
|
|
32
|
+
// recipe calls, sub-agents) are not first-class in the schema today.
|
|
33
|
+
if (step.agent === false) {
|
|
34
|
+
return [step.tool];
|
|
35
|
+
}
|
|
36
|
+
if (step.agent === true && Array.isArray(step.tools)) {
|
|
37
|
+
return step.tools;
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compiled list of tool-name prefixes (without the trailing underscore)
|
|
43
|
+
* used by `promptMentionsConnector`. Built once at module load — the
|
|
44
|
+
* source map is small and stable, so caching this avoids regex churn
|
|
45
|
+
* inside the per-step loop.
|
|
46
|
+
*/
|
|
47
|
+
const PROMPT_PREFIX_PATTERNS = Object.entries(TOOL_PREFIX_TO_CONNECTOR).map(([prefix, connector]) => ({
|
|
48
|
+
// Strip trailing underscore — when the prompt mentions a tool name like
|
|
49
|
+
// `slack_post_message` the underscore is part of the literal we look
|
|
50
|
+
// for, but when an agent prompt is more conversational ("post to slack
|
|
51
|
+
// using slack.post_message"), we want to match the prefix without the
|
|
52
|
+
// separator too.
|
|
53
|
+
prefix: prefix.replace(/_$/, ""),
|
|
54
|
+
connector,
|
|
55
|
+
}));
|
|
56
|
+
/**
|
|
57
|
+
* Inspect an agent step's `prompt` for references to tool names from
|
|
58
|
+
* known connectors. Catches the common case where the LLM is told
|
|
59
|
+
* which tool to call inside the prompt body rather than via the
|
|
60
|
+
* `tools[]` allowlist — e.g.:
|
|
61
|
+
*
|
|
62
|
+
* - id: notify
|
|
63
|
+
* agent:
|
|
64
|
+
* prompt: Use slack_post_message to send "{{summary}}" to #ops.
|
|
65
|
+
*
|
|
66
|
+
* That prompt previously fell through `toolsOfStep` entirely (no
|
|
67
|
+
* `tool`, empty `tools[]`) and the install panel told the user "no
|
|
68
|
+
* connectors needed" despite the recipe relying on Slack at runtime.
|
|
69
|
+
*
|
|
70
|
+
* Detection is deliberately lossy — we match `<prefix>_` followed by a
|
|
71
|
+
* word char (the literal tool-name shape `slack_post_message`) and we
|
|
72
|
+
* also match `<prefix>.` to catch prose like "use slack.fetch". False
|
|
73
|
+
* positives are tolerable: surfacing one extra "you may want to
|
|
74
|
+
* authorise X" hint is strictly better than the pre-fix silent miss.
|
|
75
|
+
*
|
|
76
|
+
* Audit 2026-05-17.
|
|
77
|
+
*/
|
|
78
|
+
function promptMentionsConnectors(prompt) {
|
|
79
|
+
const found = new Set();
|
|
80
|
+
// Only look at the prompt body — vars / outputs / context refs go
|
|
81
|
+
// through `{{...}}` interpolation which the runtime resolves later.
|
|
82
|
+
for (const { prefix, connector } of PROMPT_PREFIX_PATTERNS) {
|
|
83
|
+
// Anchor on a word boundary so `unrelated_slack_word` doesn't
|
|
84
|
+
// match (\\bslack[_.]\\w would also match `slack_alert`, which is
|
|
85
|
+
// the intended target).
|
|
86
|
+
const re = new RegExp(`\\b${escapeForRegex(prefix)}[_.]\\w`, "i");
|
|
87
|
+
if (re.test(prompt))
|
|
88
|
+
found.add(connector);
|
|
89
|
+
}
|
|
90
|
+
return [...found];
|
|
91
|
+
}
|
|
92
|
+
function escapeForRegex(s) {
|
|
93
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Walk a recipe's steps and return the connector ids it likely needs.
|
|
97
|
+
* Stable order (sorted) so the response is deterministic.
|
|
98
|
+
*/
|
|
99
|
+
export function detectRequiredConnectors(recipe) {
|
|
100
|
+
const required = new Set();
|
|
101
|
+
for (const step of recipe.steps) {
|
|
102
|
+
// Explicit `tool` / `tools[]` fields (canonical detection path).
|
|
103
|
+
for (const tool of toolsOfStep(step)) {
|
|
104
|
+
for (const [prefix, connector] of Object.entries(TOOL_PREFIX_TO_CONNECTOR)) {
|
|
105
|
+
if (tool.startsWith(prefix)) {
|
|
106
|
+
required.add(connector);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Prompt body scan for agent-mode steps — catches recipes that
|
|
111
|
+
// tell the LLM which tool to call inline (e.g. "Use slack_post_message
|
|
112
|
+
// to ...") without listing it in `tools[]`. See
|
|
113
|
+
// `promptMentionsConnectors` above for the rationale.
|
|
114
|
+
if (step.agent === true && typeof step.prompt === "string") {
|
|
115
|
+
for (const connector of promptMentionsConnectors(step.prompt)) {
|
|
116
|
+
required.add(connector);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...required].sort();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Compare required connectors against the bridge's `/connections`
|
|
124
|
+
* payload. Returns the ids of connectors the recipe needs but the
|
|
125
|
+
* user hasn't connected (or whose status is not "connected").
|
|
126
|
+
*
|
|
127
|
+
* Lenient on input shape — the live `/connections` response has a
|
|
128
|
+
* stable contract, but tests + future surfaces may pass in something
|
|
129
|
+
* shaped slightly differently. Anything we can't classify is treated
|
|
130
|
+
* as "not connected" so a malformed connections payload doesn't
|
|
131
|
+
* silently make every recipe look healthy.
|
|
132
|
+
*/
|
|
133
|
+
export function findMissingConnectors(required, connections) {
|
|
134
|
+
const connectedSet = new Set();
|
|
135
|
+
for (const c of connections) {
|
|
136
|
+
if (typeof c?.id !== "string")
|
|
137
|
+
continue;
|
|
138
|
+
if (c.status === "connected")
|
|
139
|
+
connectedSet.add(c.id);
|
|
140
|
+
}
|
|
141
|
+
return required.filter((id) => !connectedSet.has(id));
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=connectorPreflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectorPreflight.js","sourceRoot":"","sources":["../../src/recipes/connectorPreflight.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses + allowlists the `github:<owner>/<repo>/(recipes|bundles)/<name>`
|
|
3
|
+
* source format used by `POST /recipes/install`.
|
|
4
|
+
*
|
|
5
|
+
* Before this module existed, the install handler hard-coded
|
|
6
|
+
* `github:patchworkos/recipes/...` everywhere — every URL, every
|
|
7
|
+
* prefix match. Third-party orgs / forks / private mirrors could not
|
|
8
|
+
* host recipe catalogs even though the rest of the install pipeline
|
|
9
|
+
* (SSRF guard, parser, scheduler) is org-agnostic.
|
|
10
|
+
*
|
|
11
|
+
* Allowlist policy:
|
|
12
|
+
* - Always includes `patchworkos/recipes` (backward compat).
|
|
13
|
+
* - Operator opts in additional `<owner>/<repo>` entries via the
|
|
14
|
+
* `PATCHWORK_RECIPE_REPO_ALLOWLIST` env var (comma-separated).
|
|
15
|
+
* - Allowlist matching is case-insensitive (GitHub itself is).
|
|
16
|
+
* - Both owner and repo segments must match the strict regex
|
|
17
|
+
* `[a-z0-9_.-]{1,100}` AFTER lowercasing — guards against
|
|
18
|
+
* traversal segments smuggled into the source string.
|
|
19
|
+
*
|
|
20
|
+
* The default-only behaviour matches the audit recommendation: real
|
|
21
|
+
* multi-org support is opt-in, so existing single-org deployments
|
|
22
|
+
* don't see a behaviour change.
|
|
23
|
+
*/
|
|
24
|
+
export type GithubInstallKind = "recipe" | "bundle";
|
|
25
|
+
export interface ParsedGithubInstallSource {
|
|
26
|
+
kind: GithubInstallKind;
|
|
27
|
+
owner: string;
|
|
28
|
+
repo: string;
|
|
29
|
+
/** Recipe name (single basename) or bundle name. */
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
export type GithubInstallParseResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
parsed: ParsedGithubInstallSource;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
code: "bad_shape" | "bad_segment" | "not_allowlisted";
|
|
38
|
+
error: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Read the runtime allowlist. Combines the always-on default with
|
|
42
|
+
* whatever the operator has set in PATCHWORK_RECIPE_REPO_ALLOWLIST.
|
|
43
|
+
* Entries are lowercased + de-duplicated; trailing whitespace, empty
|
|
44
|
+
* fragments, and shapes that don't look like `owner/repo` are
|
|
45
|
+
* silently dropped (logging here is the install handler's job, not
|
|
46
|
+
* this pure helper's).
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadAllowlist(env?: NodeJS.ProcessEnv): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Parse a `github:owner/repo/(recipes|bundles)/name` source string
|
|
51
|
+
* against the active allowlist. Pure — does NOT fetch anything; the
|
|
52
|
+
* install handler is responsible for the network leg and the SSRF
|
|
53
|
+
* guard. Returns a discriminated union the caller can map to a 400
|
|
54
|
+
* (bad_shape / bad_segment) or 403 (not_allowlisted) response.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseGithubInstallSource(source: string, allowlist?: ReadonlyArray<string>): GithubInstallParseResult;
|
|
57
|
+
/**
|
|
58
|
+
* Build the raw.githubusercontent URL for a parsed install source.
|
|
59
|
+
* Always pulls `main` branch HEAD — version pinning is on the
|
|
60
|
+
* deferred audit backlog.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildGithubRawUrl(parsed: ParsedGithubInstallSource): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses + allowlists the `github:<owner>/<repo>/(recipes|bundles)/<name>`
|
|
3
|
+
* source format used by `POST /recipes/install`.
|
|
4
|
+
*
|
|
5
|
+
* Before this module existed, the install handler hard-coded
|
|
6
|
+
* `github:patchworkos/recipes/...` everywhere — every URL, every
|
|
7
|
+
* prefix match. Third-party orgs / forks / private mirrors could not
|
|
8
|
+
* host recipe catalogs even though the rest of the install pipeline
|
|
9
|
+
* (SSRF guard, parser, scheduler) is org-agnostic.
|
|
10
|
+
*
|
|
11
|
+
* Allowlist policy:
|
|
12
|
+
* - Always includes `patchworkos/recipes` (backward compat).
|
|
13
|
+
* - Operator opts in additional `<owner>/<repo>` entries via the
|
|
14
|
+
* `PATCHWORK_RECIPE_REPO_ALLOWLIST` env var (comma-separated).
|
|
15
|
+
* - Allowlist matching is case-insensitive (GitHub itself is).
|
|
16
|
+
* - Both owner and repo segments must match the strict regex
|
|
17
|
+
* `[a-z0-9_.-]{1,100}` AFTER lowercasing — guards against
|
|
18
|
+
* traversal segments smuggled into the source string.
|
|
19
|
+
*
|
|
20
|
+
* The default-only behaviour matches the audit recommendation: real
|
|
21
|
+
* multi-org support is opt-in, so existing single-org deployments
|
|
22
|
+
* don't see a behaviour change.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_ALLOWLIST = ["patchworkos/recipes"];
|
|
25
|
+
const SEGMENT_RE = /^[a-z0-9_.-]{1,100}$/;
|
|
26
|
+
/**
|
|
27
|
+
* Read the runtime allowlist. Combines the always-on default with
|
|
28
|
+
* whatever the operator has set in PATCHWORK_RECIPE_REPO_ALLOWLIST.
|
|
29
|
+
* Entries are lowercased + de-duplicated; trailing whitespace, empty
|
|
30
|
+
* fragments, and shapes that don't look like `owner/repo` are
|
|
31
|
+
* silently dropped (logging here is the install handler's job, not
|
|
32
|
+
* this pure helper's).
|
|
33
|
+
*/
|
|
34
|
+
export function loadAllowlist(env = process.env) {
|
|
35
|
+
const fromEnv = (env.PATCHWORK_RECIPE_REPO_ALLOWLIST ?? "")
|
|
36
|
+
.split(",")
|
|
37
|
+
.map((s) => s.trim().toLowerCase())
|
|
38
|
+
.filter((s) => s.length > 0 && s.includes("/"));
|
|
39
|
+
return Array.from(new Set([...DEFAULT_ALLOWLIST, ...fromEnv]));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse a `github:owner/repo/(recipes|bundles)/name` source string
|
|
43
|
+
* against the active allowlist. Pure — does NOT fetch anything; the
|
|
44
|
+
* install handler is responsible for the network leg and the SSRF
|
|
45
|
+
* guard. Returns a discriminated union the caller can map to a 400
|
|
46
|
+
* (bad_shape / bad_segment) or 403 (not_allowlisted) response.
|
|
47
|
+
*/
|
|
48
|
+
export function parseGithubInstallSource(source, allowlist = loadAllowlist()) {
|
|
49
|
+
if (!source.startsWith("github:")) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
code: "bad_shape",
|
|
53
|
+
error: "source must start with 'github:'",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// After the `github:` prefix we expect <owner>/<repo>/<kind>/<name>.
|
|
57
|
+
// We split into exactly 4 segments — extra trailing slashes or
|
|
58
|
+
// missing components are rejected with `bad_shape` so the response
|
|
59
|
+
// is actionable.
|
|
60
|
+
const tail = source.slice("github:".length);
|
|
61
|
+
const segments = tail.split("/");
|
|
62
|
+
if (segments.length !== 4) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
code: "bad_shape",
|
|
66
|
+
error: "source must match 'github:<owner>/<repo>/(recipes|bundles)/<name>'",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const [ownerRaw, repoRaw, kindRaw, nameRaw] = segments;
|
|
70
|
+
const owner = ownerRaw.toLowerCase();
|
|
71
|
+
const repo = repoRaw.toLowerCase();
|
|
72
|
+
if (!SEGMENT_RE.test(owner) || !SEGMENT_RE.test(repo)) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: "bad_segment",
|
|
76
|
+
error: "owner and repo must match [a-z0-9_.-]{1,100}",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (kindRaw !== "recipes" && kindRaw !== "bundles") {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
code: "bad_shape",
|
|
83
|
+
error: "third path segment must be 'recipes' or 'bundles'",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Reuse the strict basename predicate inline rather than importing
|
|
87
|
+
// recipeInstall.ts here (circular deps), but match its rules:
|
|
88
|
+
// single segment, no `..`, no slashes, conservative charset, ≤100.
|
|
89
|
+
if (!SEGMENT_RE.test(nameRaw.toLowerCase())) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
code: "bad_segment",
|
|
93
|
+
error: "name must match [a-z0-9_.-]{1,100}",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const allowSet = new Set(allowlist.map((s) => s.toLowerCase()));
|
|
97
|
+
if (!allowSet.has(`${owner}/${repo}`)) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
code: "not_allowlisted",
|
|
101
|
+
error: `'${owner}/${repo}' is not in the recipe-repo allowlist. Set PATCHWORK_RECIPE_REPO_ALLOWLIST=${owner}/${repo} to opt in.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
parsed: {
|
|
107
|
+
kind: kindRaw === "recipes" ? "recipe" : "bundle",
|
|
108
|
+
owner,
|
|
109
|
+
repo,
|
|
110
|
+
name: nameRaw,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build the raw.githubusercontent URL for a parsed install source.
|
|
116
|
+
* Always pulls `main` branch HEAD — version pinning is on the
|
|
117
|
+
* deferred audit backlog.
|
|
118
|
+
*/
|
|
119
|
+
export function buildGithubRawUrl(parsed) {
|
|
120
|
+
if (parsed.kind === "recipe") {
|
|
121
|
+
return `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/recipes/${parsed.name}/${parsed.name}.yaml`;
|
|
122
|
+
}
|
|
123
|
+
return `https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/bundles/${parsed.name}/patchwork-bundle.json`;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=githubInstallSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"githubInstallSource.js","sourceRoot":"","sources":["../../src/recipes/githubInstallSource.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halt-category derivation.
|
|
3
|
+
*
|
|
4
|
+
* PR1c of the Val-inspired plan. PR1 attached a `haltReason` sentence to
|
|
5
|
+
* every error-status StepResult; this module categorises those sentences
|
|
6
|
+
* into a small bounded enum so the dashboard / metrics layer can count
|
|
7
|
+
* them over time. Foundation for "is the haltReason work actually
|
|
8
|
+
* surfacing useful signal, or is everything landing in `unknown`?"
|
|
9
|
+
*
|
|
10
|
+
* The mapping is intentionally pattern-based against the 5 phrases
|
|
11
|
+
* emitted by yamlRunner.ts. Keep this file and those phrases in sync.
|
|
12
|
+
* When a new error site is added, add a category here AND a test.
|
|
13
|
+
*/
|
|
14
|
+
export type HaltCategory = "agent_silent_fail" | "agent_narration_only" | "agent_threw" | "tool_threw" | "tool_error"
|
|
15
|
+
/** Write blocked by the global kill-switch (#422). Distinct from a real tool failure. */
|
|
16
|
+
| "kill_switch"
|
|
17
|
+
/** Recipe's `tokensMax` budget breached (PR2b). */
|
|
18
|
+
| "budget_exceeded"
|
|
19
|
+
/** Whole-recipe failure (e.g. circular dependencies) — has no step row. */
|
|
20
|
+
| "run_level" | "unknown";
|
|
21
|
+
export declare function categoriseHaltReason(reason: string | undefined): HaltCategory;
|
|
22
|
+
export interface HaltSummary {
|
|
23
|
+
/** Total error-status step results scanned. */
|
|
24
|
+
total: number;
|
|
25
|
+
/** Per-category counts; categories with zero hits are omitted. */
|
|
26
|
+
byCategory: Partial<Record<HaltCategory, number>>;
|
|
27
|
+
/** Most recent 5 halt reasons (verbatim) for surfacing in the UI. */
|
|
28
|
+
recent: Array<{
|
|
29
|
+
reason: string;
|
|
30
|
+
category: HaltCategory;
|
|
31
|
+
runSeq: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
interface HaltSummaryInputRun {
|
|
35
|
+
seq: number;
|
|
36
|
+
/** Top-level run status — `run_level` halts are runs with status === "error" but no error stepResults (e.g. circular-dep failure before any step ran). */
|
|
37
|
+
status?: "running" | "done" | "error" | "cancelled" | "interrupted";
|
|
38
|
+
/** Top-level errorMessage — surfaced as a `run_level` halt when no per-step halts cover it. */
|
|
39
|
+
errorMessage?: string;
|
|
40
|
+
stepResults?: Array<{
|
|
41
|
+
status: "ok" | "skipped" | "error";
|
|
42
|
+
haltReason?: string;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Aggregate halt categories across a set of runs. Runs are expected to be
|
|
47
|
+
* sorted newest-first so `recent` reflects the most recent halts.
|
|
48
|
+
*
|
|
49
|
+
* A run contributes:
|
|
50
|
+
* - one entry per error-status stepResult that has a `haltReason`
|
|
51
|
+
* - plus one `run_level` entry if `status === "error"` and there were no
|
|
52
|
+
* per-step halts that already explained it (avoids double-counting).
|
|
53
|
+
*/
|
|
54
|
+
export declare function summariseHalts(runs: HaltSummaryInputRun[]): HaltSummary;
|
|
55
|
+
/**
|
|
56
|
+
* Format a `HaltSummary` as Prometheus text-exposition lines for the
|
|
57
|
+
* `bridge_recipe_halts{category="..."} N` gauge. Returns an empty array
|
|
58
|
+
* when the summary is empty (no HELP/TYPE block emitted in that case so
|
|
59
|
+
* Prom scrapers don't see an orphan declaration).
|
|
60
|
+
*
|
|
61
|
+
* Surfaced via `/metrics` so users with their own observability stack
|
|
62
|
+
* can dashboard halts without using Patchwork's UI.
|
|
63
|
+
*/
|
|
64
|
+
export declare function haltSummaryToPrometheus(summary: HaltSummary): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Derive a one-sentence haltReason from a step's error-status + raw error
|
|
67
|
+
* string. Used by `chainedRunner` to mirror the convention emitted by
|
|
68
|
+
* `yamlRunner`. Returns `undefined` for non-error rows or missing error.
|
|
69
|
+
*
|
|
70
|
+
* Pattern-matches the same phrases `categoriseHaltReason` knows about,
|
|
71
|
+
* so chained-run haltReasons categorise into the same buckets.
|
|
72
|
+
*/
|
|
73
|
+
export declare function deriveHaltReasonFromError(opts: {
|
|
74
|
+
stepId: string;
|
|
75
|
+
toolName?: string;
|
|
76
|
+
isAgent?: boolean;
|
|
77
|
+
status: "ok" | "skipped" | "error";
|
|
78
|
+
error?: string;
|
|
79
|
+
}): string | undefined;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Halt-category derivation.
|
|
3
|
+
*
|
|
4
|
+
* PR1c of the Val-inspired plan. PR1 attached a `haltReason` sentence to
|
|
5
|
+
* every error-status StepResult; this module categorises those sentences
|
|
6
|
+
* into a small bounded enum so the dashboard / metrics layer can count
|
|
7
|
+
* them over time. Foundation for "is the haltReason work actually
|
|
8
|
+
* surfacing useful signal, or is everything landing in `unknown`?"
|
|
9
|
+
*
|
|
10
|
+
* The mapping is intentionally pattern-based against the 5 phrases
|
|
11
|
+
* emitted by yamlRunner.ts. Keep this file and those phrases in sync.
|
|
12
|
+
* When a new error site is added, add a category here AND a test.
|
|
13
|
+
*/
|
|
14
|
+
export function categoriseHaltReason(reason) {
|
|
15
|
+
if (!reason)
|
|
16
|
+
return "unknown";
|
|
17
|
+
// Order matters: more specific phrases (silent-fail, narration, kill
|
|
18
|
+
// switch) must match before the general "Agent step ... threw" /
|
|
19
|
+
// "Tool ... threw" patterns. The phrases below mirror
|
|
20
|
+
// yamlRunner.ts:558-606,677-684,693-708 and
|
|
21
|
+
// featureFlags.ts:assertWriteAllowed.
|
|
22
|
+
if (/silent-fail/i.test(reason))
|
|
23
|
+
return "agent_silent_fail";
|
|
24
|
+
if (/narration|whitespace|no content/i.test(reason))
|
|
25
|
+
return "agent_narration_only";
|
|
26
|
+
if (/kill[- _]?switch/i.test(reason))
|
|
27
|
+
return "kill_switch";
|
|
28
|
+
if (/budget[_ ]?exceeded|exceeded its token budget/i.test(reason))
|
|
29
|
+
return "budget_exceeded";
|
|
30
|
+
if (/^Agent step .* threw/i.test(reason))
|
|
31
|
+
return "agent_threw";
|
|
32
|
+
if (/^Tool .* threw/i.test(reason))
|
|
33
|
+
return "tool_threw";
|
|
34
|
+
if (/^Tool .* reported an error/i.test(reason))
|
|
35
|
+
return "tool_error";
|
|
36
|
+
return "unknown";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Aggregate halt categories across a set of runs. Runs are expected to be
|
|
40
|
+
* sorted newest-first so `recent` reflects the most recent halts.
|
|
41
|
+
*
|
|
42
|
+
* A run contributes:
|
|
43
|
+
* - one entry per error-status stepResult that has a `haltReason`
|
|
44
|
+
* - plus one `run_level` entry if `status === "error"` and there were no
|
|
45
|
+
* per-step halts that already explained it (avoids double-counting).
|
|
46
|
+
*/
|
|
47
|
+
export function summariseHalts(runs) {
|
|
48
|
+
const byCategory = {};
|
|
49
|
+
const recent = [];
|
|
50
|
+
let total = 0;
|
|
51
|
+
for (const run of runs) {
|
|
52
|
+
let stepHaltsForRun = 0;
|
|
53
|
+
for (const step of run.stepResults ?? []) {
|
|
54
|
+
if (step.status !== "error" || !step.haltReason)
|
|
55
|
+
continue;
|
|
56
|
+
stepHaltsForRun++;
|
|
57
|
+
total++;
|
|
58
|
+
const cat = categoriseHaltReason(step.haltReason);
|
|
59
|
+
byCategory[cat] = (byCategory[cat] ?? 0) + 1;
|
|
60
|
+
if (recent.length < 5) {
|
|
61
|
+
recent.push({
|
|
62
|
+
reason: step.haltReason,
|
|
63
|
+
category: cat,
|
|
64
|
+
runSeq: run.seq,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (stepHaltsForRun === 0 && run.status === "error" && run.errorMessage) {
|
|
69
|
+
total++;
|
|
70
|
+
byCategory.run_level = (byCategory.run_level ?? 0) + 1;
|
|
71
|
+
if (recent.length < 5) {
|
|
72
|
+
recent.push({
|
|
73
|
+
reason: run.errorMessage,
|
|
74
|
+
category: "run_level",
|
|
75
|
+
runSeq: run.seq,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { total, byCategory, recent };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Format a `HaltSummary` as Prometheus text-exposition lines for the
|
|
84
|
+
* `bridge_recipe_halts{category="..."} N` gauge. Returns an empty array
|
|
85
|
+
* when the summary is empty (no HELP/TYPE block emitted in that case so
|
|
86
|
+
* Prom scrapers don't see an orphan declaration).
|
|
87
|
+
*
|
|
88
|
+
* Surfaced via `/metrics` so users with their own observability stack
|
|
89
|
+
* can dashboard halts without using Patchwork's UI.
|
|
90
|
+
*/
|
|
91
|
+
export function haltSummaryToPrometheus(summary) {
|
|
92
|
+
if (summary.total === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const lines = [
|
|
95
|
+
"# HELP bridge_recipe_halts Recipe halts in the in-memory run-log window, by category",
|
|
96
|
+
"# TYPE bridge_recipe_halts gauge",
|
|
97
|
+
];
|
|
98
|
+
for (const [category, count] of Object.entries(summary.byCategory)) {
|
|
99
|
+
lines.push(`bridge_recipe_halts{category="${category}"} ${count}`);
|
|
100
|
+
}
|
|
101
|
+
return lines;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Derive a one-sentence haltReason from a step's error-status + raw error
|
|
105
|
+
* string. Used by `chainedRunner` to mirror the convention emitted by
|
|
106
|
+
* `yamlRunner`. Returns `undefined` for non-error rows or missing error.
|
|
107
|
+
*
|
|
108
|
+
* Pattern-matches the same phrases `categoriseHaltReason` knows about,
|
|
109
|
+
* so chained-run haltReasons categorise into the same buckets.
|
|
110
|
+
*/
|
|
111
|
+
export function deriveHaltReasonFromError(opts) {
|
|
112
|
+
if (opts.status !== "error" || !opts.error)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (/silent-fail/i.test(opts.error)) {
|
|
115
|
+
return `Step "${opts.stepId}" returned no usable output (silent-fail).`;
|
|
116
|
+
}
|
|
117
|
+
if (/narration|whitespace|no content/i.test(opts.error)) {
|
|
118
|
+
return `Step "${opts.stepId}" returned only narration or whitespace — no content.`;
|
|
119
|
+
}
|
|
120
|
+
if (opts.isAgent) {
|
|
121
|
+
return `Agent step "${opts.stepId}" threw before completing: ${opts.error}`;
|
|
122
|
+
}
|
|
123
|
+
return `Tool "${opts.toolName ?? "?"}" in step "${opts.stepId}" reported an error: ${opts.error}`;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=haltCategory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"haltCategory.js","sourceRoot":"","sources":["../../src/recipes/haltCategory.ts"],"names":[],"mappings":"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"}
|