patchwork-os 1.2.0-beta.2.canary.627 → 1.2.0-beta.2.canary.629

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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Information-boundary receipts (ADR-0021 Phase 3).
3
+ *
4
+ * > Every boundary decision produces a record, in the same shape and store as
5
+ * > gate decisions: what was declared, where it was going, what was removed,
6
+ * > what was retained, why.
7
+ *
8
+ * Patchwork's standing claim is that every consequential decision leaves a
9
+ * receipt. The autonomy gate extends that to *what the AI did*; this extends it
10
+ * to *what the AI was told* — which is the half nobody could previously audit.
11
+ *
12
+ * ## The one thing this file must never do
13
+ *
14
+ * **It must never contain the payload.** A receipt records that a decision was
15
+ * made about data with a given classification; writing the prompt itself would
16
+ * turn the privacy audit log into the largest unclassified copy of exactly the
17
+ * material the boundary exists to protect, sitting in plain JSONL. Only
18
+ * declared metadata is stored: classification, category NAMES, destination id,
19
+ * decision, reason. There is no field for the text, deliberately, so a future
20
+ * caller cannot pass one by accident.
21
+ *
22
+ * ## Fail-soft, always
23
+ *
24
+ * A receipt that cannot be written must never block or alter a decision. The
25
+ * boundary already refuses correctly without any sink attached (pinned by a
26
+ * test in agentBoundary.test.ts); this store is observability, not enforcement.
27
+ * Every write path here swallows its own errors for that reason.
28
+ */
29
+ import type { BoundaryDecision, Classification } from "./dataPolicy.js";
30
+ export interface BoundaryReceipt {
31
+ seq: number;
32
+ at: number;
33
+ decision: BoundaryDecision;
34
+ /** What the step DECLARED it was carrying. */
35
+ classification: Classification;
36
+ /** Category names only — never their contents. */
37
+ categories?: string[];
38
+ /** Where it was going. */
39
+ destinationId: string;
40
+ destinationType: "local" | "remote";
41
+ /** Which categories the decision required be removed. */
42
+ redactCategories?: string[];
43
+ reason: string;
44
+ /** Recipe/step context when the caller knows it. */
45
+ recipeName?: string;
46
+ stepId?: string;
47
+ }
48
+ export interface RecordBoundaryReceiptInput {
49
+ decision: BoundaryDecision;
50
+ classification: Classification;
51
+ categories?: string[];
52
+ destinationId: string;
53
+ destinationType: "local" | "remote";
54
+ redactCategories?: string[];
55
+ reason: string;
56
+ recipeName?: string;
57
+ stepId?: string;
58
+ }
59
+ export interface BoundaryReceiptLogOptions {
60
+ dir: string;
61
+ memoryCap?: number;
62
+ now?: () => number;
63
+ logger?: {
64
+ warn?: (msg: string) => void;
65
+ };
66
+ }
67
+ export declare class BoundaryReceiptLog {
68
+ private readonly opts;
69
+ private receipts;
70
+ private seq;
71
+ private readonly file;
72
+ private readonly memoryCap;
73
+ private readonly now;
74
+ constructor(opts: BoundaryReceiptLogOptions);
75
+ private loadExisting;
76
+ private trim;
77
+ /**
78
+ * Record one boundary decision.
79
+ *
80
+ * Returns the stored receipt. Never throws: a failure to persist is logged
81
+ * and swallowed, because this is observability and the decision it describes
82
+ * has already been made and enforced.
83
+ */
84
+ record(input: RecordBoundaryReceiptInput): BoundaryReceipt;
85
+ /** Most recent first. */
86
+ recent(limit?: number): BoundaryReceipt[];
87
+ /** Counts per decision — the shape a dashboard or CLI summary wants. */
88
+ summary(): Record<BoundaryDecision, number>;
89
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Information-boundary receipts (ADR-0021 Phase 3).
3
+ *
4
+ * > Every boundary decision produces a record, in the same shape and store as
5
+ * > gate decisions: what was declared, where it was going, what was removed,
6
+ * > what was retained, why.
7
+ *
8
+ * Patchwork's standing claim is that every consequential decision leaves a
9
+ * receipt. The autonomy gate extends that to *what the AI did*; this extends it
10
+ * to *what the AI was told* — which is the half nobody could previously audit.
11
+ *
12
+ * ## The one thing this file must never do
13
+ *
14
+ * **It must never contain the payload.** A receipt records that a decision was
15
+ * made about data with a given classification; writing the prompt itself would
16
+ * turn the privacy audit log into the largest unclassified copy of exactly the
17
+ * material the boundary exists to protect, sitting in plain JSONL. Only
18
+ * declared metadata is stored: classification, category NAMES, destination id,
19
+ * decision, reason. There is no field for the text, deliberately, so a future
20
+ * caller cannot pass one by accident.
21
+ *
22
+ * ## Fail-soft, always
23
+ *
24
+ * A receipt that cannot be written must never block or alter a decision. The
25
+ * boundary already refuses correctly without any sink attached (pinned by a
26
+ * test in agentBoundary.test.ts); this store is observability, not enforcement.
27
+ * Every write path here swallows its own errors for that reason.
28
+ */
29
+ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
30
+ import path from "node:path";
31
+ /** Clip so a runaway reason cannot bloat the audit log. */
32
+ const MAX_REASON = 500;
33
+ const DEFAULT_MEMORY_CAP = 500;
34
+ export class BoundaryReceiptLog {
35
+ opts;
36
+ receipts = [];
37
+ seq = 0;
38
+ file;
39
+ memoryCap;
40
+ now;
41
+ constructor(opts) {
42
+ this.opts = opts;
43
+ this.file = path.join(opts.dir, "boundary_receipts.jsonl");
44
+ this.memoryCap = opts.memoryCap ?? DEFAULT_MEMORY_CAP;
45
+ this.now = opts.now ?? Date.now;
46
+ try {
47
+ // 0o700 like the gate log: these name destinations and classifications,
48
+ // which is a map of what this machine considers sensitive.
49
+ mkdirSync(opts.dir, { recursive: true, mode: 0o700 });
50
+ }
51
+ catch (err) {
52
+ opts.logger?.warn?.(`[boundary-receipts] could not create ${opts.dir}: ${err instanceof Error ? err.message : String(err)}`);
53
+ }
54
+ this.loadExisting();
55
+ }
56
+ loadExisting() {
57
+ try {
58
+ const text = readFileSync(this.file, "utf-8");
59
+ for (const line of text.split("\n")) {
60
+ const t = line.trim();
61
+ if (!t)
62
+ continue;
63
+ try {
64
+ const r = JSON.parse(t);
65
+ if (typeof r.seq === "number") {
66
+ this.receipts.push(r);
67
+ if (r.seq > this.seq)
68
+ this.seq = r.seq;
69
+ }
70
+ }
71
+ catch {
72
+ // one malformed line must not make the whole log unreadable
73
+ }
74
+ }
75
+ this.trim();
76
+ }
77
+ catch {
78
+ // no file yet — normal on first run
79
+ }
80
+ }
81
+ trim() {
82
+ if (this.receipts.length > this.memoryCap) {
83
+ this.receipts = this.receipts.slice(-this.memoryCap);
84
+ }
85
+ }
86
+ /**
87
+ * Record one boundary decision.
88
+ *
89
+ * Returns the stored receipt. Never throws: a failure to persist is logged
90
+ * and swallowed, because this is observability and the decision it describes
91
+ * has already been made and enforced.
92
+ */
93
+ record(input) {
94
+ const receipt = {
95
+ seq: ++this.seq,
96
+ at: this.now(),
97
+ decision: input.decision,
98
+ classification: input.classification,
99
+ destinationId: input.destinationId,
100
+ destinationType: input.destinationType,
101
+ reason: input.reason.slice(0, MAX_REASON),
102
+ ...(input.categories?.length ? { categories: input.categories } : {}),
103
+ ...(input.redactCategories?.length
104
+ ? { redactCategories: input.redactCategories }
105
+ : {}),
106
+ ...(input.recipeName ? { recipeName: input.recipeName } : {}),
107
+ ...(input.stepId ? { stepId: input.stepId } : {}),
108
+ };
109
+ this.receipts.push(receipt);
110
+ this.trim();
111
+ try {
112
+ appendFileSync(this.file, `${JSON.stringify(receipt)}\n`, {
113
+ mode: 0o600,
114
+ });
115
+ }
116
+ catch (err) {
117
+ this.opts.logger?.warn?.(`[boundary-receipts] append failed: ${err instanceof Error ? err.message : String(err)}`);
118
+ }
119
+ return receipt;
120
+ }
121
+ /** Most recent first. */
122
+ recent(limit = 50) {
123
+ return this.receipts.slice(-limit).reverse();
124
+ }
125
+ /** Counts per decision — the shape a dashboard or CLI summary wants. */
126
+ summary() {
127
+ const out = {
128
+ ALLOW: 0,
129
+ ALLOW_REDACTED: 0,
130
+ LOCAL_ONLY: 0,
131
+ REQUIRE_APPROVAL: 0,
132
+ DENY: 0,
133
+ };
134
+ for (const r of this.receipts)
135
+ out[r.decision]++;
136
+ return out;
137
+ }
138
+ }
139
+ //# sourceMappingURL=boundaryReceiptLog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundaryReceiptLog.js","sourceRoot":"","sources":["../../src/privacy/boundaryReceiptLog.ts"],"names":[],"mappings":"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"}
@@ -29,6 +29,12 @@
29
29
  * this is going" must never read as "it is fine to send".
30
30
  */
31
31
  import { type Classification, type Destination } from "./dataPolicy.js";
32
+ /**
33
+ * Whether this driver is one whose destination depends on the configured
34
+ * endpoint. Exported so `executeAgent` can skip resolving the endpoint (and
35
+ * the `config.json` read behind it) for drivers where it cannot matter.
36
+ */
37
+ export declare function isLocalFamilyDriver(driver: string | undefined): boolean;
32
38
  export interface DestinationConfig {
33
39
  type?: string;
34
40
  classifications?: unknown;
@@ -61,6 +67,15 @@ export interface ResolvedDestination {
61
67
  /** True when some registered LOCAL destination is cleared for `forClass`. */
62
68
  localDestinationAccepts: boolean;
63
69
  }
70
+ export interface ResolveDestinationOptions {
71
+ /**
72
+ * The endpoint the local driver will actually POST to, when one is
73
+ * configured (`LOCAL_ENDPOINT` / `config.json` `localEndpoint`). Absent
74
+ * means the driver talks to its own default, which is loopback for every
75
+ * driver in `LOCAL_DRIVERS`.
76
+ */
77
+ endpoint?: string;
78
+ }
64
79
  /**
65
80
  * Resolve the destination for a dispatch.
66
81
  *
@@ -69,4 +84,4 @@ export interface ResolvedDestination {
69
84
  * on an unrecognised driver would silently disable the boundary for exactly the
70
85
  * dispatches nobody anticipated.
71
86
  */
72
- export declare function resolveDestination(registry: ParsedRegistry, driver: string | undefined, forClass: Classification): ResolvedDestination | null;
87
+ export declare function resolveDestination(registry: ParsedRegistry, driver: string | undefined, forClass: Classification, opts?: ResolveDestinationOptions): ResolvedDestination | null;
@@ -28,9 +28,40 @@
28
28
  * profile rather than to "no destination", because "we do not recognise where
29
29
  * this is going" must never read as "it is fine to send".
30
30
  */
31
- import { CLASSIFICATIONS, } from "./dataPolicy.js";
32
- /** Drivers that keep the prompt on this machine. */
31
+ import { isLoopbackOrPrivateEndpoint } from "../localEndpointGuard.js";
32
+ import { CLASSIFICATIONS, classificationRank, } from "./dataPolicy.js";
33
+ /**
34
+ * Drivers whose CLIENT code runs on this machine.
35
+ *
36
+ * Membership is NOT on its own evidence that the DATA stays here — see
37
+ * `endpointIsOnBox` and the inference branch in `resolveDestination`.
38
+ */
33
39
  const LOCAL_DRIVERS = new Set(["local", "ollama", "lmstudio", "llamacpp"]);
40
+ /**
41
+ * Whether this driver is one whose destination depends on the configured
42
+ * endpoint. Exported so `executeAgent` can skip resolving the endpoint (and
43
+ * the `config.json` read behind it) for drivers where it cannot matter.
44
+ */
45
+ export function isLocalFamilyDriver(driver) {
46
+ return LOCAL_DRIVERS.has((driver ?? "").toLowerCase());
47
+ }
48
+ /**
49
+ * Whether the endpoint a local driver will POST to actually stays on the
50
+ * machine (or the private network the operator controls).
51
+ *
52
+ * No endpoint configured ⇒ on-box: every driver in `LOCAL_DRIVERS` defaults to
53
+ * a loopback address, and this is the overwhelmingly common case, so treating
54
+ * it as remote would refuse ordinary local-only installs.
55
+ *
56
+ * An endpoint that does not parse ⇒ NOT on-box. "We cannot tell where this
57
+ * goes" must never read as "it stays here" — same fail-closed direction as the
58
+ * unknown-driver branch below.
59
+ */
60
+ function endpointIsOnBox(endpoint) {
61
+ if (endpoint === undefined || endpoint.trim() === "")
62
+ return true;
63
+ return isLoopbackOrPrivateEndpoint(endpoint.trim());
64
+ }
34
65
  function parseClassifications(raw) {
35
66
  if (!Array.isArray(raw))
36
67
  return null;
@@ -79,12 +110,30 @@ export function parseRegistry(cfg) {
79
110
  }
80
111
  return { destinations, driversFor, invalid };
81
112
  }
82
- /** The strictest remote destination — fewest classifications wins. */
113
+ /**
114
+ * The strictest remote destination.
115
+ *
116
+ * Ranked by the HIGHEST classification each is cleared for, not by how many it
117
+ * lists. Counting was wrong in the direction that matters: a destination
118
+ * cleared for `[restricted]` has one entry and one cleared for
119
+ * `[public, internal]` has two, so "fewest wins" would pick the one trusted
120
+ * with the most sensitive data as the safe fallback for an unrecognised
121
+ * driver. Ties break on the smaller list.
122
+ */
83
123
  function strictestRemote(destinations) {
84
124
  const remotes = destinations.filter((d) => d.type === "remote");
85
125
  if (remotes.length === 0)
86
126
  return null;
87
- return remotes.reduce((a, b) => b.classifications.length < a.classifications.length ? b : a);
127
+ const ceiling = (d) => d.classifications.length === 0
128
+ ? -1
129
+ : Math.max(...d.classifications.map(classificationRank));
130
+ return remotes.reduce((a, b) => {
131
+ const ca = ceiling(a);
132
+ const cb = ceiling(b);
133
+ if (cb !== ca)
134
+ return cb < ca ? b : a;
135
+ return b.classifications.length < a.classifications.length ? b : a;
136
+ });
88
137
  }
89
138
  /**
90
139
  * Resolve the destination for a dispatch.
@@ -94,19 +143,69 @@ function strictestRemote(destinations) {
94
143
  * on an unrecognised driver would silently disable the boundary for exactly the
95
144
  * dispatches nobody anticipated.
96
145
  */
97
- export function resolveDestination(registry, driver, forClass) {
98
- if (registry.destinations.length === 0)
146
+ export function resolveDestination(registry, driver, forClass, opts = {}) {
147
+ if (registry.destinations.length === 0) {
148
+ // NOTHING configured is the inert case, and is fine.
149
+ //
150
+ // Something configured that ALL failed to parse is not. Returning null
151
+ // there reverts the boundary to inert on a typo — `type: "cloud"` instead
152
+ // of `"remote"` — while the operator believes they have opted in. That is
153
+ // the fail-open this module's own header says it prevents, reachable by a
154
+ // single misspelled word.
155
+ //
156
+ // So: any invalid entry with no valid entry surviving yields a synthetic
157
+ // destination cleared for NOTHING. Every dispatch is refused, loudly and
158
+ // immediately, which is the correct reading of "the operator asked for
159
+ // enforcement and we cannot tell what they meant".
160
+ if (registry.invalid.length > 0) {
161
+ return {
162
+ destination: {
163
+ id: `unparseable-config(${registry.invalid.map((i) => i.id).join(", ")})`,
164
+ type: "remote",
165
+ classifications: [],
166
+ },
167
+ localDestinationAccepts: false,
168
+ };
169
+ }
99
170
  return null;
171
+ }
100
172
  const d = (driver ?? "").toLowerCase();
101
173
  const localAccepts = registry.destinations.some((dest) => dest.type === "local" && dest.classifications.includes(forClass));
174
+ // #1398: a `type: "local"` destination is disqualified for THIS dispatch when
175
+ // the endpoint the driver will actually POST to is off-box.
176
+ //
177
+ // This is applied to the explicit driver mapping as well as to inference,
178
+ // and that is the load-bearing part. An operator's `drivers: ["local"]` entry
179
+ // on a local destination is a STATIC claim about where a driver goes; the
180
+ // resolved endpoint is what actually happens at dispatch. When the two
181
+ // disagree the endpoint wins, because the alternative lets a stale mapping
182
+ // launder an off-box send into a receipt that says the data never left —
183
+ // the precise failure this issue describes, merely reached by config rather
184
+ // than by driver name.
185
+ const localDisqualified = LOCAL_DRIVERS.has(d) && !endpointIsOnBox(opts.endpoint);
186
+ const eligible = localDisqualified
187
+ ? registry.destinations.filter((x) => x.type !== "local")
188
+ : registry.destinations;
102
189
  // Explicit driver mapping wins.
103
- for (const dest of registry.destinations) {
190
+ for (const dest of eligible) {
104
191
  if ((registry.driversFor.get(dest.id) ?? []).includes(d)) {
105
192
  return { destination: dest, localDestinationAccepts: localAccepts };
106
193
  }
107
194
  }
108
195
  // Otherwise infer by driver family.
109
- if (LOCAL_DRIVERS.has(d)) {
196
+ //
197
+ // #1398: a driver in LOCAL_DRIVERS is only evidence of a local DESTINATION
198
+ // when the endpoint it will actually POST to is on-box. `LOCAL_ENDPOINT` is
199
+ // configurable and `LOCAL_ENDPOINT_ALLOW_REMOTE` exists precisely because
200
+ // pointing it off-box is a supported deployment — so the driver NAME is a
201
+ // statement about which client code runs, never about where the bytes land.
202
+ //
203
+ // Note what is deliberately NOT consulted here: `LOCAL_ENDPOINT_ALLOW_REMOTE`.
204
+ // That flag is permission to SEND to a remote box; it is not evidence that
205
+ // the box is local. Reading it as "the operator allowed this, so treat it as
206
+ // local" would re-open exactly the hole this closes, and would do so on the
207
+ // deployments most likely to be sending real data off-machine.
208
+ if (LOCAL_DRIVERS.has(d) && endpointIsOnBox(opts.endpoint)) {
110
209
  const local = registry.destinations.find((x) => x.type === "local");
111
210
  if (local) {
112
211
  return { destination: local, localDestinationAccepts: localAccepts };
@@ -114,10 +213,27 @@ export function resolveDestination(registry, driver, forClass) {
114
213
  }
115
214
  // Unknown or remote driver → strictest remote. Fail closed: "we do not
116
215
  // recognise where this is going" must never read as "it is fine to send".
117
- const remote = strictestRemote(registry.destinations);
216
+ const remote = strictestRemote(eligible);
118
217
  if (remote) {
119
218
  return { destination: remote, localDestinationAccepts: localAccepts };
120
219
  }
220
+ // #1398: a local driver aimed off-box, with ONLY local destinations
221
+ // registered. There is no registered destination that describes where this
222
+ // data is actually going, so there is nothing to fall back TO — returning
223
+ // the local profile here would hand a `restricted`-cleared local destination
224
+ // to a dispatch leaving the machine, which is the worst version of this bug
225
+ // rather than a mitigation of it. Synthesise a remote destination cleared
226
+ // for nothing, the same shape the unparseable-config branch uses.
227
+ if (localDisqualified) {
228
+ return {
229
+ destination: {
230
+ id: "local-driver-remote-endpoint",
231
+ type: "remote",
232
+ classifications: [],
233
+ },
234
+ localDestinationAccepts: localAccepts,
235
+ };
236
+ }
121
237
  // Only local destinations are registered but the driver is not local. The
122
238
  // strictest available answer is the local profile, which will refuse
123
239
  // anything it is not cleared for.
@@ -1 +1 @@
1
- {"version":3,"file":"destinationRegistry.js","sourceRoot":"","sources":["../../src/privacy/destinationRegistry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"destinationRegistry.js","sourceRoot":"","sources":["../../src/privacy/destinationRegistry.ts"],"names":[],"mappings":"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"}
@@ -58,7 +58,10 @@ export interface AgentExecutorDeps {
58
58
  recordBoundaryDecisionFn?: (r: {
59
59
  decision: string;
60
60
  reason: string;
61
- destinationId?: string;
61
+ destinationId: string;
62
+ destinationType: "local" | "remote";
63
+ classification: string;
64
+ categories?: string[];
62
65
  redactCategories?: string[];
63
66
  }) => void;
64
67
  anthropicFn: (prompt: string, model: string) => Promise<AgentResult>;
@@ -138,4 +141,23 @@ export interface AgentExecutorInput {
138
141
  * (keeps cost-routing quotes in parity with actual reconcile billing).
139
142
  */
140
143
  export declare const DEFAULT_MODEL = "claude-haiku-4-5-20251001";
144
+ /**
145
+ * Resolve the driver that will ACTUALLY serve this call, once.
146
+ *
147
+ * #1398: the information boundary used to judge `input.driver` — the CONFIGURED
148
+ * string, which is frequently `undefined` — while `servedBy` recorded the one
149
+ * that really ran. When those differ the boundary judged a destination that
150
+ * never received the data, and nothing in the resulting receipt exposes the
151
+ * mismatch. Resolving here and handing the same value to both the boundary and
152
+ * the dispatch removes the second, divergent copy of this logic.
153
+ *
154
+ * Mirrors the dispatch chain below exactly, including its precedence:
155
+ * explicit driver → `config.json` model/driver → API key → CLI probe.
156
+ *
157
+ * An UNRECOGNISED driver string is returned unchanged rather than thrown on.
158
+ * The dispatch chain still throws for it at the same point it always has, so
159
+ * the boundary continues to evaluate unknown drivers (→ strictest remote, fail
160
+ * closed) and still writes a receipt, exactly as before this change.
161
+ */
162
+ export declare function resolveEffectiveDriver(driver: string | undefined, deps: Pick<AgentExecutorDeps, "loadPatchworkConfig" | "probeClaudeCli">): string;
141
163
  export declare function executeAgent(input: AgentExecutorInput, deps: AgentExecutorDeps): Promise<AgentResult>;