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.
- package/dist/privacy/boundaryReceiptLog.d.ts +89 -0
- package/dist/privacy/boundaryReceiptLog.js +139 -0
- package/dist/privacy/boundaryReceiptLog.js.map +1 -0
- package/dist/privacy/destinationRegistry.d.ts +16 -1
- package/dist/privacy/destinationRegistry.js +125 -9
- package/dist/privacy/destinationRegistry.js.map +1 -1
- package/dist/recipes/agentExecutor.d.ts +23 -1
- package/dist/recipes/agentExecutor.js +116 -54
- package/dist/recipes/agentExecutor.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +7 -0
- package/dist/recipes/yamlRunner.js +89 -3
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 {
|
|
32
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EACL,eAAe,
|
|
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
|
|
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>;
|