patchwork-os 1.2.0-beta.2.canary.781 → 1.2.0-beta.2.canary.783

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.
Files changed (135) hide show
  1. package/dist/activityLog.js +6 -1
  2. package/dist/activityLog.js.map +1 -1
  3. package/dist/approvalQueue.js +15 -0
  4. package/dist/approvalQueue.js.map +1 -1
  5. package/dist/bridge.js +32 -1
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/claudeDriver.d.ts +10 -0
  8. package/dist/claudeDriver.js +27 -4
  9. package/dist/claudeDriver.js.map +1 -1
  10. package/dist/claudeOrchestrator.d.ts +5 -0
  11. package/dist/claudeOrchestrator.js +94 -6
  12. package/dist/claudeOrchestrator.js.map +1 -1
  13. package/dist/commands/patchworkInit.js +10 -0
  14. package/dist/commands/patchworkInit.js.map +1 -1
  15. package/dist/commands/policyExplain.d.ts +50 -0
  16. package/dist/commands/policyExplain.js +221 -0
  17. package/dist/commands/policyExplain.js.map +1 -0
  18. package/dist/commands/profile.d.ts +17 -0
  19. package/dist/commands/profile.js +85 -0
  20. package/dist/commands/profile.js.map +1 -0
  21. package/dist/commands/recipe.d.ts +8 -0
  22. package/dist/commands/recipe.js +64 -0
  23. package/dist/commands/recipe.js.map +1 -1
  24. package/dist/commands/recipeInstall.js +19 -0
  25. package/dist/commands/recipeInstall.js.map +1 -1
  26. package/dist/connectors/baseConnector.js +5 -0
  27. package/dist/connectors/baseConnector.js.map +1 -1
  28. package/dist/connectors/secrets.js +10 -2
  29. package/dist/connectors/secrets.js.map +1 -1
  30. package/dist/connectors/tokenStorage.d.ts +9 -0
  31. package/dist/connectors/tokenStorage.js +27 -0
  32. package/dist/connectors/tokenStorage.js.map +1 -1
  33. package/dist/decisionTraceLog.js +4 -3
  34. package/dist/decisionTraceLog.js.map +1 -1
  35. package/dist/drivers/claude/envSanitizer.d.ts +39 -0
  36. package/dist/drivers/claude/envSanitizer.js +114 -0
  37. package/dist/drivers/claude/envSanitizer.js.map +1 -1
  38. package/dist/drivers/claude/subprocess.d.ts +12 -0
  39. package/dist/drivers/claude/subprocess.js +114 -25
  40. package/dist/drivers/claude/subprocess.js.map +1 -1
  41. package/dist/drivers/claude/subprocessSettings.d.ts +4 -1
  42. package/dist/drivers/claude/subprocessSettings.js +24 -7
  43. package/dist/drivers/claude/subprocessSettings.js.map +1 -1
  44. package/dist/drivers/codex/subprocess.d.ts +19 -0
  45. package/dist/drivers/codex/subprocess.js +40 -8
  46. package/dist/drivers/codex/subprocess.js.map +1 -1
  47. package/dist/drivers/gemini/index.d.ts +24 -0
  48. package/dist/drivers/gemini/index.js +76 -13
  49. package/dist/drivers/gemini/index.js.map +1 -1
  50. package/dist/drivers/types.d.ts +14 -0
  51. package/dist/drivers/types.js.map +1 -1
  52. package/dist/errors.d.ts +1 -0
  53. package/dist/errors.js +1 -0
  54. package/dist/errors.js.map +1 -1
  55. package/dist/governance/doctorReport.d.ts +45 -0
  56. package/dist/governance/doctorReport.js +251 -0
  57. package/dist/governance/doctorReport.js.map +1 -0
  58. package/dist/governance/effectivePolicy.d.ts +134 -0
  59. package/dist/governance/effectivePolicy.js +322 -0
  60. package/dist/governance/effectivePolicy.js.map +1 -0
  61. package/dist/governance/killSwitchPolicy.d.ts +30 -0
  62. package/dist/governance/killSwitchPolicy.js +52 -0
  63. package/dist/governance/killSwitchPolicy.js.map +1 -0
  64. package/dist/governance/pluginPolicy.d.ts +111 -0
  65. package/dist/governance/pluginPolicy.js +247 -0
  66. package/dist/governance/pluginPolicy.js.map +1 -0
  67. package/dist/governance/profile.d.ts +153 -0
  68. package/dist/governance/profile.js +181 -0
  69. package/dist/governance/profile.js.map +1 -0
  70. package/dist/governance/secretValues.d.ts +89 -0
  71. package/dist/governance/secretValues.js +286 -0
  72. package/dist/governance/secretValues.js.map +1 -0
  73. package/dist/governance/toolFacts.d.ts +11 -0
  74. package/dist/governance/toolFacts.js +46 -0
  75. package/dist/governance/toolFacts.js.map +1 -0
  76. package/dist/governance/untrustedContent.d.ts +43 -0
  77. package/dist/governance/untrustedContent.js +81 -0
  78. package/dist/governance/untrustedContent.js.map +1 -0
  79. package/dist/index.js +73 -5
  80. package/dist/index.js.map +1 -1
  81. package/dist/logger.js +7 -6
  82. package/dist/logger.js.map +1 -1
  83. package/dist/patchworkConfig.d.ts +22 -0
  84. package/dist/patchworkConfig.js.map +1 -1
  85. package/dist/pluginLoader.d.ts +10 -2
  86. package/dist/pluginLoader.js +12 -8
  87. package/dist/pluginLoader.js.map +1 -1
  88. package/dist/recipeOrchestration.d.ts +0 -6
  89. package/dist/recipeOrchestration.js +80 -23
  90. package/dist/recipeOrchestration.js.map +1 -1
  91. package/dist/recipeRoutes.js +52 -0
  92. package/dist/recipeRoutes.js.map +1 -1
  93. package/dist/recipes/agentExecutor.d.ts +43 -3
  94. package/dist/recipes/agentExecutor.js +55 -7
  95. package/dist/recipes/agentExecutor.js.map +1 -1
  96. package/dist/recipes/approvalRequest.d.ts +8 -0
  97. package/dist/recipes/approvalRequest.js.map +1 -1
  98. package/dist/recipes/chainedRunner.d.ts +18 -5
  99. package/dist/recipes/chainedRunner.js +107 -12
  100. package/dist/recipes/chainedRunner.js.map +1 -1
  101. package/dist/recipes/haltCategory.d.ts +4 -0
  102. package/dist/recipes/haltCategory.js +4 -0
  103. package/dist/recipes/haltCategory.js.map +1 -1
  104. package/dist/recipes/schema.d.ts +5 -1
  105. package/dist/recipes/schemaGenerator.js +12 -1
  106. package/dist/recipes/schemaGenerator.js.map +1 -1
  107. package/dist/recipes/stepObservation.d.ts +15 -0
  108. package/dist/recipes/stepObservation.js +27 -22
  109. package/dist/recipes/stepObservation.js.map +1 -1
  110. package/dist/recipes/templateEngine.d.ts +11 -1
  111. package/dist/recipes/templateEngine.js +12 -2
  112. package/dist/recipes/templateEngine.js.map +1 -1
  113. package/dist/recipes/toolRegistry.d.ts +3 -1
  114. package/dist/recipes/toolRegistry.js +9 -3
  115. package/dist/recipes/toolRegistry.js.map +1 -1
  116. package/dist/recipes/tools/http.d.ts +5 -2
  117. package/dist/recipes/tools/http.js +29 -10
  118. package/dist/recipes/tools/http.js.map +1 -1
  119. package/dist/recipes/validation.d.ts +10 -1
  120. package/dist/recipes/validation.js +51 -1
  121. package/dist/recipes/validation.js.map +1 -1
  122. package/dist/recipes/yamlRunner.d.ts +46 -5
  123. package/dist/recipes/yamlRunner.js +173 -21
  124. package/dist/recipes/yamlRunner.js.map +1 -1
  125. package/dist/schemas/recipe.v1.json +60 -3
  126. package/dist/server.js +15 -0
  127. package/dist/server.js.map +1 -1
  128. package/dist/ssrfGuard.d.ts +89 -5
  129. package/dist/ssrfGuard.js +250 -5
  130. package/dist/ssrfGuard.js.map +1 -1
  131. package/dist/tools/httpClient.js +23 -165
  132. package/dist/tools/httpClient.js.map +1 -1
  133. package/dist/transport.js +41 -0
  134. package/dist/transport.js.map +1 -1
  135. package/package.json +1 -1
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Plugin policy — may THIS recipe load THAT plugin?
3
+ *
4
+ * A recipe's `servers:` list names code that is imported into the bridge
5
+ * process and handed the tool registry. Before the governed profile,
6
+ * installing a recipe therefore equalled arbitrary in-process code execution:
7
+ * nothing between "a YAML file arrived" and "its `index.mjs` ran" asked the
8
+ * operator anything. Under `governed` (`profile.pluginPolicy === "allowlist"`)
9
+ * a spec is loaded only when it appears in `config.plugins.allow`, and the
10
+ * check is made at FOUR points — install, dashboard save, `recipe lint` and
11
+ * runtime load — because a file on disk may have arrived by any route, so the
12
+ * runtime never trusts that something upstream validated it.
13
+ *
14
+ * Under `compat` every spec is allowed, byte-identical to the previous
15
+ * behaviour. This module holds no state: every verdict is a pure function of
16
+ * (spec, profile, allowlist) plus, for integrity, the entrypoint bytes.
17
+ */
18
+ import type { PatchworkConfig } from "../patchworkConfig.js";
19
+ import type { GovernanceProfile } from "./profile.js";
20
+ export interface AllowEntry {
21
+ spec: string;
22
+ version?: string;
23
+ integrity?: string;
24
+ }
25
+ export interface PluginPolicyInput {
26
+ profile: Pick<GovernanceProfile, "mode" | "pluginPolicy">;
27
+ allow: AllowEntry[] | undefined;
28
+ }
29
+ export interface PluginVerdict {
30
+ spec: string;
31
+ allowed: boolean;
32
+ reason: string;
33
+ entry?: AllowEntry;
34
+ }
35
+ /** The governed posture, for "what WOULD happen" evaluations under compat. */
36
+ export declare const GOVERNED_PLUGIN_POLICY_PROFILE: PluginPolicyInput["profile"];
37
+ export declare const PLUGIN_NOT_ALLOWLISTED = "plugin_not_allowlisted";
38
+ export declare const PLUGIN_INTEGRITY_MISMATCH = "plugin_integrity_mismatch";
39
+ /** Error thrown by the runtime when a spec is refused. `code` is stable. */
40
+ export declare class PluginPolicyError extends Error {
41
+ readonly code: string;
42
+ readonly specs: string[];
43
+ constructor(code: string, message: string, specs: string[]);
44
+ }
45
+ /**
46
+ * Canonical form of a spec for comparison. Path specs resolve to an absolute
47
+ * path with the trailing separator dropped, so `./x`, `x/` written as `./x/`
48
+ * and the absolute form all agree. Package specs are compared verbatim after
49
+ * trimming — a package name has no equivalent spellings.
50
+ */
51
+ export declare function normalisePluginSpec(spec: string, cwd?: string): string;
52
+ /**
53
+ * Pure: (spec, profile, allowlist) → verdict. Exact match on the normalised
54
+ * spec; no globbing, no prefix matching — a policy that matches loosely is
55
+ * a policy an attacker can satisfy by naming a sibling directory.
56
+ */
57
+ export declare function evaluatePluginSpec(spec: string, input: PluginPolicyInput, cwd?: string): PluginVerdict;
58
+ /** Per-spec verdicts for `policy explain` / `doctor`. */
59
+ export declare function explainPluginPolicy(specs: string[], cfg: {
60
+ profile: PluginPolicyInput["profile"];
61
+ allow?: AllowEntry[];
62
+ }, cwd?: string): PluginVerdict[];
63
+ /** Convenience: the refused subset, for callers that build one error. */
64
+ export declare function refusedPluginSpecs(specs: string[], input: PluginPolicyInput, cwd?: string): PluginVerdict[];
65
+ /** Build the runtime error for a set of refused verdicts. */
66
+ export declare function pluginNotAllowlistedError(refused: PluginVerdict[]): PluginPolicyError;
67
+ export interface IntegrityResult {
68
+ ok: boolean;
69
+ /** "verified" | "skipped" | "mismatch" | "unreadable" | "malformed" */
70
+ status: "verified" | "skipped" | "mismatch" | "unreadable" | "malformed";
71
+ reason: string;
72
+ actual?: string;
73
+ }
74
+ export declare function sha256Integrity(bytes: Buffer): string;
75
+ /**
76
+ * `sha256-<base64>` over the entrypoint file bytes. Absent integrity is
77
+ * SKIPPED (and says so) rather than failed: the allowlist entry itself is the
78
+ * operator's decision, and integrity is an optional tightening of it.
79
+ */
80
+ export declare function verifyPluginIntegrity(entrypointPath: string, integrity: string | undefined): IntegrityResult;
81
+ /** Extract `servers:` from a parsed recipe object; non-list ⇒ []. */
82
+ export declare function pluginSpecsOf(recipe: unknown): string[];
83
+ /** Extract `servers:` from recipe YAML text; unparseable ⇒ []. */
84
+ export declare function pluginSpecsOfYaml(text: string): string[];
85
+ export declare function policyInputFromConfig(profile: PluginPolicyInput["profile"], cfg: Pick<PatchworkConfig, "plugins"> | undefined): PluginPolicyInput;
86
+ export interface RecipePluginScanRow {
87
+ /** Recipe file, relative to `recipesDir`. */
88
+ file: string;
89
+ /** Recipe `name:` when parseable. */
90
+ name?: string;
91
+ verdicts: PluginVerdict[];
92
+ }
93
+ export interface RecipePluginScan {
94
+ recipesDir: string;
95
+ /** Recipe files inspected (the denominator). */
96
+ recipesScanned: number;
97
+ /** Recipes declaring at least one `servers:` entry. */
98
+ recipesWithPlugins: number;
99
+ refusedSpecs: number;
100
+ rows: RecipePluginScanRow[];
101
+ }
102
+ /**
103
+ * Every installed recipe's `servers:` specs with their verdicts, for the
104
+ * doctor governance section. Reports only recipes that declare plugins, but
105
+ * counts every file read so an empty result is distinguishable from an
106
+ * unreadable directory.
107
+ */
108
+ export declare function scanInstalledRecipePlugins(recipesDir: string, cfg: {
109
+ profile: PluginPolicyInput["profile"];
110
+ allow?: AllowEntry[];
111
+ }, cwd?: string): RecipePluginScan;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Plugin policy — may THIS recipe load THAT plugin?
3
+ *
4
+ * A recipe's `servers:` list names code that is imported into the bridge
5
+ * process and handed the tool registry. Before the governed profile,
6
+ * installing a recipe therefore equalled arbitrary in-process code execution:
7
+ * nothing between "a YAML file arrived" and "its `index.mjs` ran" asked the
8
+ * operator anything. Under `governed` (`profile.pluginPolicy === "allowlist"`)
9
+ * a spec is loaded only when it appears in `config.plugins.allow`, and the
10
+ * check is made at FOUR points — install, dashboard save, `recipe lint` and
11
+ * runtime load — because a file on disk may have arrived by any route, so the
12
+ * runtime never trusts that something upstream validated it.
13
+ *
14
+ * Under `compat` every spec is allowed, byte-identical to the previous
15
+ * behaviour. This module holds no state: every verdict is a pure function of
16
+ * (spec, profile, allowlist) plus, for integrity, the entrypoint bytes.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
20
+ import path from "node:path";
21
+ import { parse as parseYaml } from "yaml";
22
+ /** The governed posture, for "what WOULD happen" evaluations under compat. */
23
+ export const GOVERNED_PLUGIN_POLICY_PROFILE = Object.freeze({ mode: "governed", pluginPolicy: "allowlist" });
24
+ export const PLUGIN_NOT_ALLOWLISTED = "plugin_not_allowlisted";
25
+ export const PLUGIN_INTEGRITY_MISMATCH = "plugin_integrity_mismatch";
26
+ /** Error thrown by the runtime when a spec is refused. `code` is stable. */
27
+ export class PluginPolicyError extends Error {
28
+ code;
29
+ specs;
30
+ constructor(code, message, specs) {
31
+ super(message);
32
+ this.name = "PluginPolicyError";
33
+ this.code = code;
34
+ this.specs = specs;
35
+ }
36
+ }
37
+ function isPathSpec(spec) {
38
+ return (spec.startsWith("./") ||
39
+ spec.startsWith("../") ||
40
+ spec === "." ||
41
+ spec === ".." ||
42
+ path.isAbsolute(spec));
43
+ }
44
+ /**
45
+ * Canonical form of a spec for comparison. Path specs resolve to an absolute
46
+ * path with the trailing separator dropped, so `./x`, `x/` written as `./x/`
47
+ * and the absolute form all agree. Package specs are compared verbatim after
48
+ * trimming — a package name has no equivalent spellings.
49
+ */
50
+ export function normalisePluginSpec(spec, cwd = process.cwd()) {
51
+ const trimmed = spec.trim();
52
+ if (isPathSpec(trimmed)) {
53
+ const resolved = path.resolve(cwd, trimmed);
54
+ return resolved.length > 1 ? resolved.replace(/[\\/]+$/, "") : resolved;
55
+ }
56
+ return trimmed;
57
+ }
58
+ /**
59
+ * Pure: (spec, profile, allowlist) → verdict. Exact match on the normalised
60
+ * spec; no globbing, no prefix matching — a policy that matches loosely is
61
+ * a policy an attacker can satisfy by naming a sibling directory.
62
+ */
63
+ export function evaluatePluginSpec(spec, input, cwd = process.cwd()) {
64
+ const trimmed = spec.trim();
65
+ if (input.profile.pluginPolicy !== "allowlist") {
66
+ return { spec: trimmed, allowed: true, reason: "compat profile: open" };
67
+ }
68
+ const wanted = normalisePluginSpec(trimmed, cwd);
69
+ for (const entry of input.allow ?? []) {
70
+ if (typeof entry?.spec !== "string")
71
+ continue;
72
+ if (normalisePluginSpec(entry.spec, cwd) === wanted) {
73
+ return {
74
+ spec: trimmed,
75
+ allowed: true,
76
+ reason: `allowlisted as "${entry.spec}"`,
77
+ entry,
78
+ };
79
+ }
80
+ }
81
+ const size = (input.allow ?? []).length;
82
+ return {
83
+ spec: trimmed,
84
+ allowed: false,
85
+ reason: size === 0
86
+ ? "governed profile: plugins.allow is empty — no recipe plugin may load"
87
+ : `governed profile: "${trimmed}" is not in plugins.allow (${size} entr${size === 1 ? "y" : "ies"})`,
88
+ };
89
+ }
90
+ /** Per-spec verdicts for `policy explain` / `doctor`. */
91
+ export function explainPluginPolicy(specs, cfg, cwd = process.cwd()) {
92
+ return specs.map((s) => evaluatePluginSpec(s, { profile: cfg.profile, allow: cfg.allow }, cwd));
93
+ }
94
+ /** Convenience: the refused subset, for callers that build one error. */
95
+ export function refusedPluginSpecs(specs, input, cwd = process.cwd()) {
96
+ return specs
97
+ .map((s) => evaluatePluginSpec(s, input, cwd))
98
+ .filter((v) => !v.allowed);
99
+ }
100
+ /** Build the runtime error for a set of refused verdicts. */
101
+ export function pluginNotAllowlistedError(refused) {
102
+ const names = refused.map((v) => v.spec);
103
+ return new PluginPolicyError(PLUGIN_NOT_ALLOWLISTED, `recipe plugin${names.length === 1 ? "" : "s"} ${names.map((n) => `"${n}"`).join(", ")} ` +
104
+ "not allowlisted under the governed profile — add to config.json " +
105
+ "`plugins.allow` (spec, optional integrity) or switch to the compat profile", names);
106
+ }
107
+ export function sha256Integrity(bytes) {
108
+ return `sha256-${createHash("sha256").update(bytes).digest("base64")}`;
109
+ }
110
+ /**
111
+ * `sha256-<base64>` over the entrypoint file bytes. Absent integrity is
112
+ * SKIPPED (and says so) rather than failed: the allowlist entry itself is the
113
+ * operator's decision, and integrity is an optional tightening of it.
114
+ */
115
+ export function verifyPluginIntegrity(entrypointPath, integrity) {
116
+ if (integrity === undefined || integrity === null || integrity === "") {
117
+ return {
118
+ ok: true,
119
+ status: "skipped",
120
+ reason: "no integrity recorded on the allowlist entry",
121
+ };
122
+ }
123
+ const m = /^sha256-([A-Za-z0-9+/]+={0,2})$/.exec(integrity.trim());
124
+ if (!m) {
125
+ return {
126
+ ok: false,
127
+ status: "malformed",
128
+ reason: `integrity must be "sha256-<base64>", got ${JSON.stringify(integrity)}`,
129
+ };
130
+ }
131
+ let bytes;
132
+ try {
133
+ bytes = readFileSync(entrypointPath);
134
+ }
135
+ catch (err) {
136
+ return {
137
+ ok: false,
138
+ status: "unreadable",
139
+ reason: `cannot read entrypoint ${entrypointPath}: ${err instanceof Error ? err.message : String(err)}`,
140
+ };
141
+ }
142
+ const actual = sha256Integrity(bytes);
143
+ if (actual !== `sha256-${m[1]}`) {
144
+ return {
145
+ ok: false,
146
+ status: "mismatch",
147
+ reason: `entrypoint ${entrypointPath} hashes to ${actual}, allowlist says ${integrity.trim()}`,
148
+ actual,
149
+ };
150
+ }
151
+ return { ok: true, status: "verified", reason: "sha256 matches", actual };
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Recipe scanning (install / save / doctor)
155
+ // ---------------------------------------------------------------------------
156
+ /** Extract `servers:` from a parsed recipe object; non-list ⇒ []. */
157
+ export function pluginSpecsOf(recipe) {
158
+ if (!recipe || typeof recipe !== "object" || Array.isArray(recipe))
159
+ return [];
160
+ const servers = recipe.servers;
161
+ if (!Array.isArray(servers))
162
+ return [];
163
+ return servers.filter((s) => typeof s === "string");
164
+ }
165
+ /** Extract `servers:` from recipe YAML text; unparseable ⇒ []. */
166
+ export function pluginSpecsOfYaml(text) {
167
+ try {
168
+ return pluginSpecsOf(parseYaml(text));
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ }
174
+ export function policyInputFromConfig(profile, cfg) {
175
+ return { profile, allow: cfg?.plugins?.allow };
176
+ }
177
+ function walkRecipeFiles(dir, out, depth = 0) {
178
+ if (depth > 4)
179
+ return;
180
+ let entries;
181
+ try {
182
+ entries = readdirSync(dir);
183
+ }
184
+ catch {
185
+ return;
186
+ }
187
+ for (const name of entries) {
188
+ if (name.startsWith("."))
189
+ continue;
190
+ const full = path.join(dir, name);
191
+ let st;
192
+ try {
193
+ st = statSync(full);
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ if (st.isDirectory())
199
+ walkRecipeFiles(full, out, depth + 1);
200
+ else if (/\.(ya?ml|json)$/i.test(name))
201
+ out.push(full);
202
+ }
203
+ }
204
+ /**
205
+ * Every installed recipe's `servers:` specs with their verdicts, for the
206
+ * doctor governance section. Reports only recipes that declare plugins, but
207
+ * counts every file read so an empty result is distinguishable from an
208
+ * unreadable directory.
209
+ */
210
+ export function scanInstalledRecipePlugins(recipesDir, cfg, cwd = process.cwd()) {
211
+ const files = [];
212
+ if (existsSync(recipesDir))
213
+ walkRecipeFiles(recipesDir, files);
214
+ const rows = [];
215
+ let refused = 0;
216
+ for (const file of files) {
217
+ let parsed;
218
+ try {
219
+ const text = readFileSync(file, "utf-8");
220
+ parsed = file.toLowerCase().endsWith(".json")
221
+ ? JSON.parse(text)
222
+ : parseYaml(text);
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ const specs = pluginSpecsOf(parsed);
228
+ if (specs.length === 0)
229
+ continue;
230
+ const verdicts = explainPluginPolicy(specs, cfg, cwd);
231
+ refused += verdicts.filter((v) => !v.allowed).length;
232
+ const name = parsed.name;
233
+ rows.push({
234
+ file: path.relative(recipesDir, file),
235
+ ...(typeof name === "string" ? { name } : {}),
236
+ verdicts,
237
+ });
238
+ }
239
+ return {
240
+ recipesDir,
241
+ recipesScanned: files.length,
242
+ recipesWithPlugins: rows.length,
243
+ refusedSpecs: refused,
244
+ rows,
245
+ };
246
+ }
247
+ //# sourceMappingURL=pluginPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pluginPolicy.js","sourceRoot":"","sources":["../../src/governance/pluginPolicy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Governance profile — the ONE concept that turns Patchwork's opt-in controls
3
+ * into a coherent posture.
4
+ *
5
+ * Phase 0 finding: every headline control (approval gate, automated-trigger
6
+ * gating, worker authority, policy matrix, agent sandbox, plugin loading, the
7
+ * kill-switch failure mode) was independently opt-in and independently
8
+ * fail-open, so a fresh install enforced nothing while looking governed on the
9
+ * dashboard. This module resolves a single `profile` setting into the existing
10
+ * primitives — it adds no new decision function of its own.
11
+ *
12
+ * Two modes, deliberately only two:
13
+ *
14
+ * - `compat` — byte-identical to pre-profile behaviour. This is what an
15
+ * install with no `profile` key resolves to, so an existing
16
+ * installation never changes behaviour by upgrading.
17
+ * - `governed` — conservative defaults suitable for real business data.
18
+ * `patchwork init` writes this for NEW installs;
19
+ * `patchwork profile governed` opts an existing one in.
20
+ *
21
+ * The resolved profile is a plain value. Runtime enforcement, `patchwork
22
+ * policy explain` and `patchwork doctor` all read the SAME resolved value, so
23
+ * the explanation cannot describe a posture the runtime is not applying.
24
+ *
25
+ * Every field below maps to something that already exists; the comment on
26
+ * each names the primitive it feeds. Nothing here is a new flag.
27
+ */
28
+ export declare const PROFILE_MODES: readonly ["governed", "compat"];
29
+ export type ProfileMode = (typeof PROFILE_MODES)[number];
30
+ export interface GovernanceProfile {
31
+ mode: ProfileMode;
32
+ /** Whether `profile:` was explicitly present in config (vs defaulted). */
33
+ declared: boolean;
34
+ /**
35
+ * Feeds `Server.approvalGate` / `makeRecipeApprovalFn`. Governed raises the
36
+ * operator's setting to at least `"high"`; it never lowers an explicit
37
+ * `"all"`.
38
+ */
39
+ approvalGate: "off" | "high" | "all";
40
+ /**
41
+ * Feeds `RunnerDeps.gateAutomatedRuns`. Governed: cron / webhook / file-watch
42
+ * / git-hook / test-run triggers are consulted exactly like a manual run.
43
+ * Compat: manual only (the worker gate may still set it, as before).
44
+ */
45
+ gateAutomatedRuns: boolean;
46
+ /**
47
+ * Feeds `FLAG_WORKER_AUTONOMY`: a worker manifest that owns a recipe governs
48
+ * it. Compat leaves the flag as the operator set it.
49
+ */
50
+ workerAuthority: boolean;
51
+ /** Feeds `FLAG_ENFORCE_POLICY` (`patchwork.policy.yml`). */
52
+ policyEnforce: boolean;
53
+ /**
54
+ * Feeds the subprocess drivers (`src/drivers/*`): under `enforced` an agent
55
+ * step that declares no `sandbox:` runs contained — read-only tool
56
+ * allowlist, no WebFetch/WebSearch/Bash, allowlisted environment, no bridge
57
+ * MCP access. A recipe may widen this EXPLICITLY per step, and the widening
58
+ * is visible in `policy explain`.
59
+ */
60
+ agentContainment: "enforced" | "opt-in";
61
+ /**
62
+ * Feeds `loadRecipeServers` / recipe install / dashboard save / lint:
63
+ * `allowlist` refuses any `servers:` entry not in `config.plugins.allow`.
64
+ */
65
+ pluginPolicy: "allowlist" | "open";
66
+ /**
67
+ * Feeds `readKillSwitch`: when the kill-switch state cannot be read, treat
68
+ * it as ENGAGED (refuse) rather than released.
69
+ */
70
+ killSwitchFailClosed: boolean;
71
+ /**
72
+ * A write tool whose tier was INFERRED from its name (no explicit
73
+ * `riskDefault`, or a plugin / MCP tool) is queued for approval rather than
74
+ * trusted on the heuristic.
75
+ */
76
+ unknownWriteTools: "gate" | "allow";
77
+ /**
78
+ * A tool id nothing is registered under: governed halts the run (a plugin
79
+ * that failed to load must not produce a green run that did nothing);
80
+ * compat keeps the documented forward-compat SKIP.
81
+ */
82
+ unregisteredTools: "refuse" | "skip";
83
+ /** Connector-derived values are wrapped in an untrusted-content envelope. */
84
+ untrustedEnvelope: boolean;
85
+ /**
86
+ * Whether a recipe's own `requireApproval: false` may switch off the tier
87
+ * gate. Governed: no — a recipe cannot opt itself out of the workspace
88
+ * policy.
89
+ */
90
+ recipeOptOutHonoured: boolean;
91
+ }
92
+ export interface ProfileConfigInput {
93
+ profile?: unknown;
94
+ approvalGate?: unknown;
95
+ }
96
+ /**
97
+ * Pure: config → profile. Unknown or absent `profile` ⇒ compat (never a
98
+ * guess). An explicit but unrecognised value is reported by `doctor`, not
99
+ * silently promoted to governed.
100
+ */
101
+ export declare function resolveProfile(cfg: ProfileConfigInput | undefined): GovernanceProfile;
102
+ export declare const COMPAT_PROFILE: GovernanceProfile;
103
+ export declare const GOVERNED_PROFILE: GovernanceProfile;
104
+ export declare function isGoverned(p: GovernanceProfile | undefined): boolean;
105
+ export declare function setActiveProfile(p: GovernanceProfile): void;
106
+ export declare function activeProfile(): GovernanceProfile;
107
+ /** Test seam. */
108
+ export declare function _resetActiveProfileForTesting(): void;
109
+ /**
110
+ * Tools a contained agent step may use. Read-only by construction: nothing
111
+ * here writes a file, runs a command, or reaches the network. Names are the
112
+ * Claude Code built-in tool names; other drivers map them to their own
113
+ * equivalents (Gemini/Codex have coarser knobs — see each driver).
114
+ */
115
+ export declare const CONTAINED_AGENT_ALLOWED_TOOLS: readonly string[];
116
+ /**
117
+ * Tools that are DENIED in every mode when the profile is governed, even when
118
+ * a recipe widens `allowedTools`. Network egress and shell are the two
119
+ * capabilities that turn a prompt injection into exfiltration; a recipe that
120
+ * genuinely needs them must say so with `sandbox: { network: true }` /
121
+ * `sandbox: { shell: true }`, which `policy explain` reports as a widening.
122
+ */
123
+ export declare const CONTAINED_AGENT_DENIED_TOOLS: readonly string[];
124
+ export interface AgentContainment {
125
+ /** True when the driver must apply the allowlist (not merely the deny list). */
126
+ enforced: boolean;
127
+ allowedTools: string[];
128
+ deniedTools: string[];
129
+ /** Pass only an explicit environment allowlist to the child process. */
130
+ envAllowlist: boolean;
131
+ /** Whether the child may reach the bridge's own MCP tool surface. */
132
+ mcpAccess: boolean;
133
+ /** Human-readable widenings the recipe requested, for `policy explain`. */
134
+ widenings: string[];
135
+ }
136
+ export interface StepSandboxRequest {
137
+ /** Recipe-declared `sandbox: true` / `allowedTools:` (existing fields). */
138
+ sandbox?: boolean;
139
+ allowedTools?: string[];
140
+ disallowedTools?: string[];
141
+ /** Explicit widenings a recipe may request under a governed profile. */
142
+ network?: boolean;
143
+ shell?: boolean;
144
+ mcpAccess?: boolean;
145
+ }
146
+ /**
147
+ * Pure: (profile, step request) → containment the driver must apply.
148
+ *
149
+ * Under `compat` this reproduces today's behaviour exactly: containment only
150
+ * when the step opted in via `sandbox: true`. Under `governed` the default is
151
+ * contained, and each widening is recorded so the explanation can show it.
152
+ */
153
+ export declare function resolveAgentContainment(profile: GovernanceProfile, req: StepSandboxRequest | undefined): AgentContainment;