patchwork-os 1.2.0-beta.2.canary.782 → 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,134 @@
1
+ /**
2
+ * Effective policy — one deterministic calculation answering
3
+ * "what can this recipe / worker / tool actually do right now?"
4
+ *
5
+ * It COMPOSES the existing pure decision functions; it replaces none of them:
6
+ *
7
+ * kill switch → readKillSwitch (killSwitchPolicy.ts)
8
+ * worker → decideWorkerAction + resolveGateOutcome (workers/workerGate.ts)
9
+ * privacy → decideBoundary (privacy/dataPolicy.ts)
10
+ * tool tier → classifyTool (riskTier.ts)
11
+ * profile → resolveProfile (profile.ts)
12
+ *
13
+ * The runtime and `patchwork policy explain` call the SAME functions in this
14
+ * file (`shouldConsultApproval`, `tierVerdict`, `computeEffectivePolicy`), so
15
+ * the explanation cannot drift from enforcement. `effectivePolicy.test.ts`
16
+ * asserts preview == enforcement over a matrix of inputs.
17
+ *
18
+ * Stage order is the runtime order (see ARCHITECTURE note in the Phase 0
19
+ * report): kill switch → authority (worker, tool tier, trigger) → privacy →
20
+ * approval → dispatch. A REFUSE at any stage is final; an APPROVAL at any
21
+ * stage is final unless a later stage refuses.
22
+ */
23
+ import type { BoundaryDecision } from "../privacy/dataPolicy.js";
24
+ import type { RiskTier } from "../riskTier.js";
25
+ import type { WorkerGateAction } from "../workers/workerGate.js";
26
+ import type { GovernanceProfile } from "./profile.js";
27
+ export type TriggerKind = "manual" | "cron" | "webhook" | "file_watch" | "git_hook" | "on_file_save" | "on_test_run" | "chained" | "nested" | "replay" | "local" | (string & {});
28
+ export interface ToolFacts {
29
+ id: string;
30
+ /** From `classifyTool(id)`. */
31
+ tier: RiskTier;
32
+ /** From the recipe tool registry; `undefined` when the id is not registered. */
33
+ isWrite?: boolean;
34
+ /** True when the registry (or an explicit map) supplied the tier; false when inferred from the name. */
35
+ tierDeclared: boolean;
36
+ /** False when nothing is registered under this id. */
37
+ registered: boolean;
38
+ /**
39
+ * From `classifyActionClass(id)` (workers/actionClass.ts): the same
40
+ * reversibility the worker gate uses. Governed gates every NON-reversible
41
+ * write regardless of tier; reversible writes (file.write, rollback-able)
42
+ * flow below the tier threshold, as the threat model intends.
43
+ */
44
+ reversibility?: "reversible" | "compensable" | "irreversible";
45
+ /** True for `agent:` steps (tool id "agent"). */
46
+ isAgentStep?: boolean;
47
+ /** For agent steps: whether containment is enforced for this step. */
48
+ agentContained?: boolean;
49
+ /** For agent steps: widenings the step requested. */
50
+ agentWidenings?: string[];
51
+ }
52
+ export interface WorkerFacts {
53
+ id: string;
54
+ action: WorkerGateAction;
55
+ /** Rule id from the decision record, when known. */
56
+ ruleId?: string;
57
+ standingPermissionId?: string;
58
+ }
59
+ export interface PrivacyFacts {
60
+ classification: string;
61
+ destination: string;
62
+ decision: BoundaryDecision;
63
+ reason?: string;
64
+ }
65
+ export interface EffectivePolicyInput {
66
+ profile: GovernanceProfile;
67
+ recipe: {
68
+ name: string;
69
+ requireApproval?: boolean;
70
+ };
71
+ trigger: TriggerKind;
72
+ tool: ToolFacts;
73
+ /** Present only when a worker owns the recipe and its gate is active. */
74
+ worker?: WorkerFacts;
75
+ /** Present only for agent steps with a registered destination. */
76
+ privacy?: PrivacyFacts;
77
+ killSwitch: {
78
+ engaged: boolean;
79
+ reason: string;
80
+ };
81
+ /** Per-recipe tool allowlist, when declared (`tools:` on the recipe). */
82
+ recipeToolAllowlist?: string[];
83
+ /**
84
+ * What the orchestrator actually injected into the runner. When absent the
85
+ * calculation derives it from the profile (the `explain` path); the runner
86
+ * passes the real values so the two cannot disagree about what is wired.
87
+ */
88
+ gate?: {
89
+ approvalFnInjected: boolean;
90
+ workerGateInjected: boolean;
91
+ };
92
+ }
93
+ export type StageVerdict = "ALLOW" | "APPROVAL" | "REFUSE" | "SKIP" | "N/A";
94
+ export interface PolicyStage {
95
+ stage: "kill_switch" | "tool_registration" | "recipe_tool_allowlist" | "worker_authority" | "tool_tier" | "trigger" | "privacy" | "standing_permission";
96
+ verdict: StageVerdict;
97
+ reason: string;
98
+ }
99
+ export type FinalVerdict = "ALLOW" | "HUMAN_APPROVAL_REQUIRED" | "REFUSED";
100
+ export interface EffectivePolicy {
101
+ final: FinalVerdict;
102
+ stages: PolicyStage[];
103
+ /** True when the runtime will call the approval fn for this step. */
104
+ consultsApproval: boolean;
105
+ }
106
+ /**
107
+ * Does the TRIGGER put this step in front of the approval fn at all?
108
+ * This is the predicate the flat and chained runners evaluate. Kept as one
109
+ * function so `policy explain` and the runner cannot disagree about it.
110
+ */
111
+ export declare function shouldConsultApproval(args: {
112
+ profile: GovernanceProfile;
113
+ trigger: TriggerKind;
114
+ recipeRequireApproval?: boolean;
115
+ /** The worker gate is injected (compat: worker flag on + manifest). */
116
+ workerGateInjected: boolean;
117
+ /** An approval fn was injected at all (approvalGate != off, or worker). */
118
+ approvalFnInjected: boolean;
119
+ }): {
120
+ consult: boolean;
121
+ reason: string;
122
+ };
123
+ /**
124
+ * The TIER half: given the approval-gate level and what we know about the
125
+ * tool, does the tier gate want a human? Mirrors `makeRecipeApprovalFn`'s
126
+ * threshold plus the governed rules for unknown write tools and agent steps.
127
+ */
128
+ export declare function tierVerdict(profile: GovernanceProfile, tool: ToolFacts): {
129
+ verdict: "ALLOW" | "APPROVAL";
130
+ reason: string;
131
+ };
132
+ export declare function computeEffectivePolicy(input: EffectivePolicyInput): EffectivePolicy;
133
+ /** Plain-text rendering for `patchwork policy explain`. */
134
+ export declare function formatEffectivePolicy(input: EffectivePolicyInput, result: EffectivePolicy): string;
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Effective policy — one deterministic calculation answering
3
+ * "what can this recipe / worker / tool actually do right now?"
4
+ *
5
+ * It COMPOSES the existing pure decision functions; it replaces none of them:
6
+ *
7
+ * kill switch → readKillSwitch (killSwitchPolicy.ts)
8
+ * worker → decideWorkerAction + resolveGateOutcome (workers/workerGate.ts)
9
+ * privacy → decideBoundary (privacy/dataPolicy.ts)
10
+ * tool tier → classifyTool (riskTier.ts)
11
+ * profile → resolveProfile (profile.ts)
12
+ *
13
+ * The runtime and `patchwork policy explain` call the SAME functions in this
14
+ * file (`shouldConsultApproval`, `tierVerdict`, `computeEffectivePolicy`), so
15
+ * the explanation cannot drift from enforcement. `effectivePolicy.test.ts`
16
+ * asserts preview == enforcement over a matrix of inputs.
17
+ *
18
+ * Stage order is the runtime order (see ARCHITECTURE note in the Phase 0
19
+ * report): kill switch → authority (worker, tool tier, trigger) → privacy →
20
+ * approval → dispatch. A REFUSE at any stage is final; an APPROVAL at any
21
+ * stage is final unless a later stage refuses.
22
+ */
23
+ /**
24
+ * Does the TRIGGER put this step in front of the approval fn at all?
25
+ * This is the predicate the flat and chained runners evaluate. Kept as one
26
+ * function so `policy explain` and the runner cannot disagree about it.
27
+ */
28
+ export function shouldConsultApproval(args) {
29
+ const { profile, trigger, recipeRequireApproval, workerGateInjected, approvalFnInjected, } = args;
30
+ if (!approvalFnInjected) {
31
+ return {
32
+ consult: false,
33
+ reason: "no approval gate configured (approvalGate: off)",
34
+ };
35
+ }
36
+ const automated = trigger !== "manual";
37
+ const gateAutomated = profile.gateAutomatedRuns || workerGateInjected;
38
+ if (automated && !gateAutomated) {
39
+ return {
40
+ consult: false,
41
+ reason: `trigger ${trigger} is not gated (compat profile: manual runs only)`,
42
+ };
43
+ }
44
+ if (recipeRequireApproval === false) {
45
+ if (workerGateInjected) {
46
+ return {
47
+ consult: true,
48
+ reason: "recipe requireApproval:false cannot opt out of worker governance",
49
+ };
50
+ }
51
+ if (!profile.recipeOptOutHonoured) {
52
+ return {
53
+ consult: true,
54
+ reason: "recipe requireApproval:false ignored under governed profile",
55
+ };
56
+ }
57
+ return {
58
+ consult: false,
59
+ reason: "recipe opted out (requireApproval: false)",
60
+ };
61
+ }
62
+ return {
63
+ consult: true,
64
+ reason: automated
65
+ ? `trigger ${trigger} gated like a manual run`
66
+ : "manual run",
67
+ };
68
+ }
69
+ /**
70
+ * The TIER half: given the approval-gate level and what we know about the
71
+ * tool, does the tier gate want a human? Mirrors `makeRecipeApprovalFn`'s
72
+ * threshold plus the governed rules for unknown write tools and agent steps.
73
+ */
74
+ export function tierVerdict(profile, tool) {
75
+ const gate = profile.approvalGate;
76
+ if (gate === "off")
77
+ return { verdict: "ALLOW", reason: "approval gate off" };
78
+ if (tool.isAgentStep) {
79
+ if (profile.agentContainment === "enforced") {
80
+ return tool.agentContained
81
+ ? {
82
+ verdict: tool.agentWidenings?.length ? "APPROVAL" : "ALLOW",
83
+ reason: tool.agentWidenings?.length
84
+ ? `agent step widened containment (${tool.agentWidenings.join(", ")})`
85
+ : "agent step contained (read-only tools, no network, no shell)",
86
+ }
87
+ : { verdict: "APPROVAL", reason: "agent step not contained" };
88
+ }
89
+ // compat: agent classified by name → medium → passes under "high"
90
+ return gate === "all"
91
+ ? { verdict: "APPROVAL", reason: "approval gate: all" }
92
+ : { verdict: "ALLOW", reason: "agent step tier medium < high" };
93
+ }
94
+ if (gate === "all")
95
+ return { verdict: "APPROVAL", reason: "approval gate: all" };
96
+ if (tool.tier === "high")
97
+ return { verdict: "APPROVAL", reason: "tool tier high" };
98
+ // Governed: every NON-reversible write asks, whatever its tier. The
99
+ // reversibility comes from the same action classifier the worker gate
100
+ // uses, so "reversible" here means rollback-able (file.write), not "low".
101
+ if (profile.unknownWriteTools === "gate" &&
102
+ tool.isWrite === true &&
103
+ tool.reversibility !== undefined &&
104
+ tool.reversibility !== "reversible") {
105
+ return {
106
+ verdict: "APPROVAL",
107
+ reason: `${tool.reversibility} write gated under governed profile (${tool.tier} tier)`,
108
+ };
109
+ }
110
+ if (profile.unknownWriteTools === "gate" &&
111
+ tool.isWrite !== false &&
112
+ (!tool.tierDeclared || tool.isWrite === undefined)) {
113
+ return {
114
+ verdict: "APPROVAL",
115
+ reason: tool.registered
116
+ ? "write tool with inferred tier gated under governed profile"
117
+ : "tool of unknown provenance gated under governed profile",
118
+ };
119
+ }
120
+ return { verdict: "ALLOW", reason: `tool tier ${tool.tier} below threshold` };
121
+ }
122
+ export function computeEffectivePolicy(input) {
123
+ const { profile, tool } = input;
124
+ const stages = [];
125
+ // 1. Kill switch — absolute.
126
+ if (input.killSwitch.engaged) {
127
+ stages.push({
128
+ stage: "kill_switch",
129
+ verdict: "REFUSE",
130
+ reason: input.killSwitch.reason,
131
+ });
132
+ return { final: "REFUSED", stages, consultsApproval: false };
133
+ }
134
+ stages.push({
135
+ stage: "kill_switch",
136
+ verdict: "ALLOW",
137
+ reason: input.killSwitch.reason,
138
+ });
139
+ // 2. Registration.
140
+ if (!tool.registered && !tool.isAgentStep) {
141
+ if (profile.unregisteredTools === "refuse") {
142
+ stages.push({
143
+ stage: "tool_registration",
144
+ verdict: "REFUSE",
145
+ reason: `tool ${tool.id} is not registered (governed profile refuses rather than skips)`,
146
+ });
147
+ return { final: "REFUSED", stages, consultsApproval: false };
148
+ }
149
+ // Compat: the runner still consults the approval fn for the step (the
150
+ // old predicate never looked at registration) and only SKIPS at dispatch
151
+ // when nothing answers to the id. Record that, and keep going.
152
+ stages.push({
153
+ stage: "tool_registration",
154
+ verdict: "SKIP",
155
+ reason: `tool ${tool.id} is not registered — dispatch will skip it (compat forward-compat rule)`,
156
+ });
157
+ }
158
+ else {
159
+ stages.push({
160
+ stage: "tool_registration",
161
+ verdict: "ALLOW",
162
+ reason: tool.isAgentStep ? "agent step" : "tool registered",
163
+ });
164
+ }
165
+ // 3. Recipe tool allowlist.
166
+ if (input.recipeToolAllowlist && !tool.isAgentStep) {
167
+ if (!input.recipeToolAllowlist.includes(tool.id)) {
168
+ stages.push({
169
+ stage: "recipe_tool_allowlist",
170
+ verdict: "REFUSE",
171
+ reason: `tool ${tool.id} not in recipe tools: allowlist`,
172
+ });
173
+ return { final: "REFUSED", stages, consultsApproval: false };
174
+ }
175
+ stages.push({
176
+ stage: "recipe_tool_allowlist",
177
+ verdict: "ALLOW",
178
+ reason: "tool declared",
179
+ });
180
+ }
181
+ else {
182
+ stages.push({
183
+ stage: "recipe_tool_allowlist",
184
+ verdict: "N/A",
185
+ reason: "no allowlist declared",
186
+ });
187
+ }
188
+ // 4. Worker authority (forbid is final; gate → approval unless standing).
189
+ let workerWantsApproval = false;
190
+ if (input.worker) {
191
+ const w = input.worker;
192
+ if (w.action === "forbid") {
193
+ stages.push({
194
+ stage: "worker_authority",
195
+ verdict: "REFUSE",
196
+ reason: `worker ${w.id} forbids this action (${w.ruleId ?? "forbid rule"}) — no approval can unlock it`,
197
+ });
198
+ return { final: "REFUSED", stages, consultsApproval: false };
199
+ }
200
+ if (w.action === "gate") {
201
+ if (w.standingPermissionId) {
202
+ stages.push({
203
+ stage: "worker_authority",
204
+ verdict: "APPROVAL",
205
+ reason: `worker ${w.id} has not earned this action`,
206
+ });
207
+ stages.push({
208
+ stage: "standing_permission",
209
+ verdict: "ALLOW",
210
+ reason: `standing permission ${w.standingPermissionId} converts queue → flow`,
211
+ });
212
+ }
213
+ else {
214
+ workerWantsApproval = true;
215
+ stages.push({
216
+ stage: "worker_authority",
217
+ verdict: "APPROVAL",
218
+ reason: `worker ${w.id} has not earned this action (${w.ruleId ?? "trust below threshold"})`,
219
+ });
220
+ stages.push({
221
+ stage: "standing_permission",
222
+ verdict: "N/A",
223
+ reason: "none",
224
+ });
225
+ }
226
+ }
227
+ else {
228
+ stages.push({
229
+ stage: "worker_authority",
230
+ verdict: "ALLOW",
231
+ reason: `worker ${w.id} may act autonomously (${w.ruleId ?? "earned"})`,
232
+ });
233
+ }
234
+ }
235
+ else {
236
+ stages.push({
237
+ stage: "worker_authority",
238
+ verdict: "N/A",
239
+ reason: "no worker owns this recipe",
240
+ });
241
+ }
242
+ // 5. Tool tier.
243
+ const tier = tierVerdict(profile, tool);
244
+ stages.push({
245
+ stage: "tool_tier",
246
+ verdict: tier.verdict,
247
+ reason: tier.reason,
248
+ });
249
+ // 6. Trigger — does the approval fn get consulted at all?
250
+ const consult = shouldConsultApproval({
251
+ profile,
252
+ trigger: input.trigger,
253
+ recipeRequireApproval: input.recipe.requireApproval,
254
+ workerGateInjected: input.gate?.workerGateInjected ?? input.worker !== undefined,
255
+ approvalFnInjected: input.gate?.approvalFnInjected ??
256
+ (profile.approvalGate !== "off" || input.worker !== undefined),
257
+ });
258
+ stages.push({
259
+ stage: "trigger",
260
+ verdict: consult.consult ? "ALLOW" : "SKIP",
261
+ reason: consult.reason,
262
+ });
263
+ // 7. Privacy — refuses everything but ALLOW (executeAgent's rule).
264
+ if (input.privacy) {
265
+ const p = input.privacy;
266
+ if (p.decision === "ALLOW") {
267
+ stages.push({
268
+ stage: "privacy",
269
+ verdict: "ALLOW",
270
+ reason: `${p.classification} → ${p.destination} allowed`,
271
+ });
272
+ }
273
+ else {
274
+ stages.push({
275
+ stage: "privacy",
276
+ verdict: "REFUSE",
277
+ reason: `${p.classification} → ${p.destination}: ${p.decision}${p.reason ? ` (${p.reason})` : ""}`,
278
+ });
279
+ return { final: "REFUSED", stages, consultsApproval: false };
280
+ }
281
+ }
282
+ else {
283
+ stages.push({
284
+ stage: "privacy",
285
+ verdict: "N/A",
286
+ reason: tool.isAgentStep
287
+ ? "no destination registered"
288
+ : "not a model dispatch",
289
+ });
290
+ }
291
+ // Final composition.
292
+ const wantsApproval = consult.consult && (tier.verdict === "APPROVAL" || workerWantsApproval);
293
+ return {
294
+ final: wantsApproval ? "HUMAN_APPROVAL_REQUIRED" : "ALLOW",
295
+ stages,
296
+ consultsApproval: consult.consult,
297
+ };
298
+ }
299
+ /** Plain-text rendering for `patchwork policy explain`. */
300
+ export function formatEffectivePolicy(input, result) {
301
+ const lines = [];
302
+ lines.push(`RECIPE: ${input.recipe.name}`);
303
+ lines.push(`PROFILE: ${input.profile.mode.toUpperCase()}${input.profile.declared ? "" : " (defaulted — no profile: key)"}`);
304
+ lines.push(`TRIGGER: ${String(input.trigger).toUpperCase()}`);
305
+ lines.push(`TOOL: ${input.tool.id}${input.tool.isAgentStep ? " (agent step)" : ""}`);
306
+ lines.push(`TOOL RISK: ${input.tool.tier.toUpperCase()}${input.tool.tierDeclared ? "" : " (inferred from name)"}${input.tool.isWrite ? " · WRITE" : ""}`);
307
+ if (input.worker)
308
+ lines.push(`WORKER: ${input.worker.id}`);
309
+ lines.push("");
310
+ for (const s of result.stages) {
311
+ lines.push(`${s.stage.padEnd(22)} ${s.verdict.padEnd(9)} ${s.reason}`);
312
+ }
313
+ lines.push("");
314
+ const label = result.final === "ALLOW"
315
+ ? "ALLOWED"
316
+ : result.final === "HUMAN_APPROVAL_REQUIRED"
317
+ ? "HUMAN APPROVAL REQUIRED"
318
+ : "REFUSED";
319
+ lines.push(`FINAL RESULT: ${label}`);
320
+ return lines.join("\n");
321
+ }
322
+ //# sourceMappingURL=effectivePolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"effectivePolicy.js","sourceRoot":"","sources":["../../src/governance/effectivePolicy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Kill-switch read with a profile-dependent failure mode.
3
+ *
4
+ * Invariant the governed profile promises: if the kill switch is active, no
5
+ * Patchwork-mediated consequential write occurs. A read that FAILS cannot be
6
+ * allowed to resolve to "released" under that promise — an unreadable flags
7
+ * state is exactly what tampering with `flags.json` or the install would
8
+ * produce. Under `compat` the historical fail-open stays, because that is
9
+ * what every existing call site did and this module exists to replace those
10
+ * sites without changing what they do for existing installs.
11
+ *
12
+ * Every dispatch chokepoint (recipe `executeTool`, MCP `transport.ts`,
13
+ * subprocess spawn, orchestrator enqueue, recipe entry) calls
14
+ * `readKillSwitch()` / `assertKillSwitchReleased()` rather than importing
15
+ * `isWriteKillSwitchActive` and wrapping it in its own `try {} catch {}` —
16
+ * which is how four sites ended up fail-open with a comment saying "same as
17
+ * every other site".
18
+ */
19
+ import { type GovernanceProfile } from "./profile.js";
20
+ export interface KillSwitchReading {
21
+ /** True ⇒ refuse the write. */
22
+ engaged: boolean;
23
+ reason: "engaged" | "released" | "unreadable_fail_closed" | "unreadable_fail_open";
24
+ }
25
+ export declare function readKillSwitch(profile?: GovernanceProfile): KillSwitchReading;
26
+ export declare function killSwitchMessage(r: KillSwitchReading, operation: string): string;
27
+ /** Throw a coded error when the switch is engaged. Same code as `assertWriteAllowed`. */
28
+ export declare function assertKillSwitchReleased(operation: string, profile?: GovernanceProfile): void;
29
+ /** Test seam: replace the underlying reader (e.g. with one that throws). */
30
+ export declare function _setKillSwitchReaderForTesting(fn: (() => boolean) | null): void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Kill-switch read with a profile-dependent failure mode.
3
+ *
4
+ * Invariant the governed profile promises: if the kill switch is active, no
5
+ * Patchwork-mediated consequential write occurs. A read that FAILS cannot be
6
+ * allowed to resolve to "released" under that promise — an unreadable flags
7
+ * state is exactly what tampering with `flags.json` or the install would
8
+ * produce. Under `compat` the historical fail-open stays, because that is
9
+ * what every existing call site did and this module exists to replace those
10
+ * sites without changing what they do for existing installs.
11
+ *
12
+ * Every dispatch chokepoint (recipe `executeTool`, MCP `transport.ts`,
13
+ * subprocess spawn, orchestrator enqueue, recipe entry) calls
14
+ * `readKillSwitch()` / `assertKillSwitchReleased()` rather than importing
15
+ * `isWriteKillSwitchActive` and wrapping it in its own `try {} catch {}` —
16
+ * which is how four sites ended up fail-open with a comment saying "same as
17
+ * every other site".
18
+ */
19
+ import { isWriteKillSwitchActive } from "../featureFlags.js";
20
+ import { activeProfile } from "./profile.js";
21
+ let reader = isWriteKillSwitchActive;
22
+ export function readKillSwitch(profile = activeProfile()) {
23
+ try {
24
+ const engaged = reader();
25
+ return { engaged, reason: engaged ? "engaged" : "released" };
26
+ }
27
+ catch {
28
+ if (profile.killSwitchFailClosed) {
29
+ return { engaged: true, reason: "unreadable_fail_closed" };
30
+ }
31
+ return { engaged: false, reason: "unreadable_fail_open" };
32
+ }
33
+ }
34
+ export function killSwitchMessage(r, operation) {
35
+ return r.reason === "unreadable_fail_closed"
36
+ ? `kill_switch_blocked: ${operation} refused — kill-switch state unreadable and the governed profile fails closed`
37
+ : `kill_switch_blocked: ${operation} refused — write kill switch engaged. Release with \`patchwork kill-switch release\`.`;
38
+ }
39
+ /** Throw a coded error when the switch is engaged. Same code as `assertWriteAllowed`. */
40
+ export function assertKillSwitchReleased(operation, profile = activeProfile()) {
41
+ const r = readKillSwitch(profile);
42
+ if (r.engaged) {
43
+ const err = new Error(killSwitchMessage(r, operation));
44
+ err.code = "kill_switch_blocked";
45
+ throw err;
46
+ }
47
+ }
48
+ /** Test seam: replace the underlying reader (e.g. with one that throws). */
49
+ export function _setKillSwitchReaderForTesting(fn) {
50
+ reader = fn ?? isWriteKillSwitchActive;
51
+ }
52
+ //# sourceMappingURL=killSwitchPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"killSwitchPolicy.js","sourceRoot":"","sources":["../../src/governance/killSwitchPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAA0B,MAAM,cAAc,CAAC;AAYrE,IAAI,MAAM,GAAkB,uBAAuB,CAAC;AAEpD,MAAM,UAAU,cAAc,CAC5B,UAA6B,aAAa,EAAE;IAE5C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;YACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,wBAAwB,EAAE,CAAC;QAC7D,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,CAAoB,EACpB,SAAiB;IAEjB,OAAO,CAAC,CAAC,MAAM,KAAK,wBAAwB;QAC1C,CAAC,CAAC,wBAAwB,SAAS,+EAA+E;QAClH,CAAC,CAAC,wBAAwB,SAAS,uFAAuF,CAAC;AAC/H,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,wBAAwB,CACtC,SAAiB,EACjB,UAA6B,aAAa,EAAE;IAE5C,MAAM,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QACd,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;QACtD,GAAiC,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAChE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,8BAA8B,CAC5C,EAA0B;IAE1B,MAAM,GAAG,EAAE,IAAI,uBAAuB,CAAC;AACzC,CAAC"}