patchwork-os 1.2.0-beta.2.canary.714 → 1.2.0-beta.2.canary.720
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/butler/memoryCard.d.ts +2 -1
- package/dist/butler/memoryCard.js +2 -28
- package/dist/butler/memoryCard.js.map +1 -1
- package/dist/butler/outcomeIngester.js +31 -1
- package/dist/butler/outcomeIngester.js.map +1 -1
- package/dist/butler/outcomeShadowLog.d.ts +23 -0
- package/dist/butler/outcomeShadowLog.js +30 -1
- package/dist/butler/outcomeShadowLog.js.map +1 -1
- package/dist/index.js +36 -3
- package/dist/index.js.map +1 -1
- package/dist/privacy/boundaryReceipts.d.ts +78 -0
- package/dist/privacy/boundaryReceipts.js +203 -0
- package/dist/privacy/boundaryReceipts.js.map +1 -0
- package/dist/promptSafety.d.ts +29 -0
- package/dist/promptSafety.js +55 -0
- package/dist/promptSafety.js.map +1 -0
- package/dist/recipeRoutes.js +28 -0
- package/dist/recipeRoutes.js.map +1 -1
- package/dist/tools/recentTracesDigest.js +10 -3
- package/dist/tools/recentTracesDigest.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { BoundaryDecision, Classification } from "./dataPolicy.js";
|
|
2
|
+
export declare const BOUNDARY_RECEIPTS_BASENAME = "boundary_receipts.jsonl";
|
|
3
|
+
export declare function boundaryReceiptsPath(dir?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* One receipt as this module is willing to hand it onward.
|
|
6
|
+
*
|
|
7
|
+
* An explicit field list, not a pass-through of the parsed row. The receipt
|
|
8
|
+
* type has no payload field, but a reader that spread whatever it found on
|
|
9
|
+
* disk would publish one the moment anything else wrote one.
|
|
10
|
+
*/
|
|
11
|
+
export interface BoundaryReceiptView {
|
|
12
|
+
seq?: number;
|
|
13
|
+
at: number;
|
|
14
|
+
decision: BoundaryDecision;
|
|
15
|
+
classification: Classification;
|
|
16
|
+
categories?: string[];
|
|
17
|
+
destinationId: string;
|
|
18
|
+
destinationType?: "local" | "remote";
|
|
19
|
+
redactCategories?: string[];
|
|
20
|
+
reason: string;
|
|
21
|
+
recipeName?: string;
|
|
22
|
+
workspaceId?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BoundaryReceiptsSummary {
|
|
25
|
+
/** The denominator: every decision recorded, INCLUDING the allowed ones. */
|
|
26
|
+
recorded: number;
|
|
27
|
+
/**
|
|
28
|
+
* Decisions that were not a plain ALLOW — refused, rerouted, redacted or
|
|
29
|
+
* escalated. Never rendered without `recorded` beside it.
|
|
30
|
+
*/
|
|
31
|
+
refusals: number;
|
|
32
|
+
byDecision: Record<string, number>;
|
|
33
|
+
byDestination: Record<string, number>;
|
|
34
|
+
byClassification: Record<string, number>;
|
|
35
|
+
/**
|
|
36
|
+
* Recipe -> how many of ITS dispatches were not plain ALLOW.
|
|
37
|
+
*
|
|
38
|
+
* A to-do list rather than another statistic: the remedy for a refusal is
|
|
39
|
+
* usually to fix the step that produced it, and #1469 established that a
|
|
40
|
+
* count with no recipe leaves an operator guessing between every installed
|
|
41
|
+
* recipe.
|
|
42
|
+
*/
|
|
43
|
+
refusalsByRecipe: Record<string, number>;
|
|
44
|
+
/**
|
|
45
|
+
* Refusals carrying no recipe name — rows written before attribution
|
|
46
|
+
* existed. Reported rather than dropped so
|
|
47
|
+
* `sum(refusalsByRecipe) + refusalsUnattributed === refusals` holds.
|
|
48
|
+
*/
|
|
49
|
+
refusalsUnattributed: number;
|
|
50
|
+
earliest?: number;
|
|
51
|
+
latest?: number;
|
|
52
|
+
/** Echoed back so a filtered report cannot be mistaken for the whole ledger. */
|
|
53
|
+
since?: number;
|
|
54
|
+
/** Lines that could not be parsed. Reported, never silently skipped. */
|
|
55
|
+
unreadableLines: number;
|
|
56
|
+
/** True only if a caller-supplied row limit actually cut the ledger short. */
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
recent: BoundaryReceiptView[];
|
|
59
|
+
}
|
|
60
|
+
export interface SummariseReceiptsOptions {
|
|
61
|
+
/** Directory holding the ledger. Defaults to `${PATCHWORK_HOME}`. */
|
|
62
|
+
dir?: string;
|
|
63
|
+
/** Only count decisions at or after this timestamp. */
|
|
64
|
+
since?: number;
|
|
65
|
+
/** How many receipts to carry in `recent`. Does not affect the counts. */
|
|
66
|
+
recentLimit?: number;
|
|
67
|
+
/** Stop after this many rows. Sets `truncated` when it bites. */
|
|
68
|
+
maxRows?: number;
|
|
69
|
+
}
|
|
70
|
+
export declare function summariseBoundaryReceipts(opts?: SummariseReceiptsOptions): BoundaryReceiptsSummary;
|
|
71
|
+
/**
|
|
72
|
+
* Render the denominator FIRST, and never a bare refusal count.
|
|
73
|
+
*
|
|
74
|
+
* Same doctrine as `formatPrivacyShadow`, for the same reason: "2 refusals"
|
|
75
|
+
* invites the reading that everything else was fine, and on an empty ledger it
|
|
76
|
+
* would state a clean bill of health for a boundary that has never run.
|
|
77
|
+
*/
|
|
78
|
+
export declare function formatBoundaryReceipts(s: BoundaryReceiptsSummary): string;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the information-boundary receipt ledger (ADR-0021).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this file exists at all
|
|
5
|
+
*
|
|
6
|
+
* `boundary_receipts.jsonl` was WRITE-ONLY in this repository. Every ADR-0021
|
|
7
|
+
* enforcement decision was recorded, and `BoundaryReceiptLog.recent()` /
|
|
8
|
+
* `.summary()` — the two methods whose own comment says they are "the shape a
|
|
9
|
+
* dashboard or CLI summary wants" — had no production caller, no CLI verb, no
|
|
10
|
+
* HTTP route and no page. The only thing in the workspace that read this
|
|
11
|
+
* ledger lived in the separate, non-MIT control plane.
|
|
12
|
+
*
|
|
13
|
+
* ADR-0019:88-92 forbids precisely that: the local ledgers must stay fully
|
|
14
|
+
* usable standalone, and degrading them in the direction that pushes someone
|
|
15
|
+
* toward the paid product is the failure the open-core boundary was written to
|
|
16
|
+
* prevent. ADR-0021:404-406 gives the sharper version — "people need to be able
|
|
17
|
+
* to read the code that decides whether their confidential information leaves
|
|
18
|
+
* the machine". Reading the code is worthless if you cannot read what it
|
|
19
|
+
* decided. Nobody chose this; it happened by default, which is what made it
|
|
20
|
+
* worth fixing before adding anything else.
|
|
21
|
+
*
|
|
22
|
+
* ## Why it reads the FILE and not the class
|
|
23
|
+
*
|
|
24
|
+
* `BoundaryReceiptLog` trims its in-memory array to 500 rows on load and again
|
|
25
|
+
* on every write. A summary built on `.summary()` therefore answers over the
|
|
26
|
+
* most recent 500 receipts while presenting as a total — a wrong denominator
|
|
27
|
+
* on the one screen whose entire job is to state the denominator. This module
|
|
28
|
+
* reads the ledger from disk for the same reason `summarisePrivacyShadow`
|
|
29
|
+
* does, and the two now share a shape deliberately.
|
|
30
|
+
*
|
|
31
|
+
* ## What it must never do
|
|
32
|
+
*
|
|
33
|
+
* Surface a payload. The receipt type has no field for one by construction;
|
|
34
|
+
* this reader additionally copies only whitelisted keys, so a hand-edited or
|
|
35
|
+
* future-writer row carrying a prompt cannot turn the viewer into the thing
|
|
36
|
+
* that publishes it.
|
|
37
|
+
*/
|
|
38
|
+
import { readFileSync } from "node:fs";
|
|
39
|
+
import path from "node:path";
|
|
40
|
+
import { patchworkPath } from "../patchworkHome.js";
|
|
41
|
+
export const BOUNDARY_RECEIPTS_BASENAME = "boundary_receipts.jsonl";
|
|
42
|
+
/** How many receipts the detail list carries. The summary counts all of them. */
|
|
43
|
+
const DEFAULT_RECENT = 20;
|
|
44
|
+
export function boundaryReceiptsPath(dir) {
|
|
45
|
+
return dir
|
|
46
|
+
? path.join(dir, BOUNDARY_RECEIPTS_BASENAME)
|
|
47
|
+
: patchworkPath(BOUNDARY_RECEIPTS_BASENAME);
|
|
48
|
+
}
|
|
49
|
+
function view(r) {
|
|
50
|
+
if (typeof r.at !== "number" || typeof r.decision !== "string")
|
|
51
|
+
return null;
|
|
52
|
+
const str = (k) => typeof r[k] === "string" ? r[k] : undefined;
|
|
53
|
+
const arr = (k) => Array.isArray(r[k])
|
|
54
|
+
? r[k].filter((x) => typeof x === "string")
|
|
55
|
+
: undefined;
|
|
56
|
+
const dt = str("destinationType");
|
|
57
|
+
return {
|
|
58
|
+
...(typeof r.seq === "number" ? { seq: r.seq } : {}),
|
|
59
|
+
at: r.at,
|
|
60
|
+
decision: r.decision,
|
|
61
|
+
classification: (str("classification") ?? "internal"),
|
|
62
|
+
...(arr("categories")?.length ? { categories: arr("categories") } : {}),
|
|
63
|
+
destinationId: str("destinationId") ?? "",
|
|
64
|
+
...(dt === "local" || dt === "remote" ? { destinationType: dt } : {}),
|
|
65
|
+
...(arr("redactCategories")?.length
|
|
66
|
+
? { redactCategories: arr("redactCategories") }
|
|
67
|
+
: {}),
|
|
68
|
+
reason: str("reason") ?? "",
|
|
69
|
+
...(str("recipeName") ? { recipeName: str("recipeName") } : {}),
|
|
70
|
+
...(str("workspaceId") ? { workspaceId: str("workspaceId") } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function summariseBoundaryReceipts(opts = {}) {
|
|
74
|
+
const s = {
|
|
75
|
+
recorded: 0,
|
|
76
|
+
refusals: 0,
|
|
77
|
+
byDecision: {},
|
|
78
|
+
byDestination: {},
|
|
79
|
+
byClassification: {},
|
|
80
|
+
refusalsByRecipe: {},
|
|
81
|
+
refusalsUnattributed: 0,
|
|
82
|
+
unreadableLines: 0,
|
|
83
|
+
truncated: false,
|
|
84
|
+
recent: [],
|
|
85
|
+
...(opts.since !== undefined ? { since: opts.since } : {}),
|
|
86
|
+
};
|
|
87
|
+
let text;
|
|
88
|
+
try {
|
|
89
|
+
text = readFileSync(boundaryReceiptsPath(opts.dir), "utf-8");
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// No ledger yet. Rendered as "nothing recorded" — never as "0 refusals",
|
|
93
|
+
// which would read as a clean bill of health for a boundary that has not
|
|
94
|
+
// run.
|
|
95
|
+
return s;
|
|
96
|
+
}
|
|
97
|
+
const kept = [];
|
|
98
|
+
for (const line of text.split("\n")) {
|
|
99
|
+
const t = line.trim();
|
|
100
|
+
if (!t)
|
|
101
|
+
continue;
|
|
102
|
+
if (opts.maxRows !== undefined && s.recorded >= opts.maxRows) {
|
|
103
|
+
s.truncated = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(t);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
s.unreadableLines += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const v = view(parsed);
|
|
115
|
+
if (!v) {
|
|
116
|
+
s.unreadableLines += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (opts.since !== undefined && v.at < opts.since)
|
|
120
|
+
continue;
|
|
121
|
+
s.recorded += 1;
|
|
122
|
+
s.byDecision[v.decision] = (s.byDecision[v.decision] ?? 0) + 1;
|
|
123
|
+
if (v.destinationId) {
|
|
124
|
+
s.byDestination[v.destinationId] =
|
|
125
|
+
(s.byDestination[v.destinationId] ?? 0) + 1;
|
|
126
|
+
}
|
|
127
|
+
s.byClassification[v.classification] =
|
|
128
|
+
(s.byClassification[v.classification] ?? 0) + 1;
|
|
129
|
+
if (v.decision !== "ALLOW") {
|
|
130
|
+
s.refusals += 1;
|
|
131
|
+
if (v.recipeName) {
|
|
132
|
+
s.refusalsByRecipe[v.recipeName] =
|
|
133
|
+
(s.refusalsByRecipe[v.recipeName] ?? 0) + 1;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
s.refusalsUnattributed += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (s.earliest === undefined || v.at < s.earliest)
|
|
140
|
+
s.earliest = v.at;
|
|
141
|
+
if (s.latest === undefined || v.at > s.latest)
|
|
142
|
+
s.latest = v.at;
|
|
143
|
+
kept.push(v);
|
|
144
|
+
}
|
|
145
|
+
s.recent = kept.slice(-(opts.recentLimit ?? DEFAULT_RECENT)).reverse();
|
|
146
|
+
return s;
|
|
147
|
+
}
|
|
148
|
+
const PAD = 16;
|
|
149
|
+
/**
|
|
150
|
+
* Render the denominator FIRST, and never a bare refusal count.
|
|
151
|
+
*
|
|
152
|
+
* Same doctrine as `formatPrivacyShadow`, for the same reason: "2 refusals"
|
|
153
|
+
* invites the reading that everything else was fine, and on an empty ledger it
|
|
154
|
+
* would state a clean bill of health for a boundary that has never run.
|
|
155
|
+
*/
|
|
156
|
+
export function formatBoundaryReceipts(s) {
|
|
157
|
+
const L = [];
|
|
158
|
+
if (s.recorded === 0) {
|
|
159
|
+
L.push("[privacy-receipts] nothing recorded");
|
|
160
|
+
L.push(s.unreadableLines > 0
|
|
161
|
+
? ` The ledger exists but no line in it could be read (${s.unreadableLines} unreadable).`
|
|
162
|
+
: " The boundary has written no receipt in this workspace.");
|
|
163
|
+
L.push(" That means no agent step has dispatched since it was enabled, or no");
|
|
164
|
+
L.push(" destination is registered under `privacy.destinations` — the boundary");
|
|
165
|
+
L.push(" is inert until one is. It does NOT mean nothing was refused.");
|
|
166
|
+
return L.join("\n");
|
|
167
|
+
}
|
|
168
|
+
L.push(`[privacy-receipts] ${s.recorded} boundary decisions recorded`);
|
|
169
|
+
if (s.since !== undefined) {
|
|
170
|
+
L.push(` window since ${new Date(s.since).toISOString()}`);
|
|
171
|
+
}
|
|
172
|
+
else if (s.earliest !== undefined && s.latest !== undefined) {
|
|
173
|
+
L.push(` window ${new Date(s.earliest).toISOString()} → ${new Date(s.latest).toISOString()}`);
|
|
174
|
+
}
|
|
175
|
+
L.push(` ${s.refusals} of ${s.recorded} were refused, rerouted or held for approval`);
|
|
176
|
+
L.push(" by decision");
|
|
177
|
+
for (const [k, v] of Object.entries(s.byDecision).sort((a, b) => b[1] - a[1])) {
|
|
178
|
+
L.push(` ${k.padEnd(PAD)} ${String(v).padStart(5)}`);
|
|
179
|
+
}
|
|
180
|
+
L.push(" by destination");
|
|
181
|
+
for (const [k, v] of Object.entries(s.byDestination).sort((a, b) => b[1] - a[1])) {
|
|
182
|
+
L.push(` ${k.padEnd(PAD)} ${String(v).padStart(5)}`);
|
|
183
|
+
}
|
|
184
|
+
const recipes = Object.entries(s.refusalsByRecipe).sort((a, b) => b[1] - a[1]);
|
|
185
|
+
if (recipes.length || s.refusalsUnattributed) {
|
|
186
|
+
L.push(" refusals by recipe — the fix list");
|
|
187
|
+
for (const [k, v] of recipes) {
|
|
188
|
+
L.push(` ${k.padEnd(PAD)} ${String(v).padStart(5)}`);
|
|
189
|
+
}
|
|
190
|
+
if (s.refusalsUnattributed) {
|
|
191
|
+
L.push(` ${"(unattributed)".padEnd(PAD)} ${String(s.refusalsUnattributed).padStart(5)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (s.unreadableLines > 0) {
|
|
195
|
+
L.push(` ${s.unreadableLines} unreadable line(s) skipped`);
|
|
196
|
+
}
|
|
197
|
+
if (s.truncated) {
|
|
198
|
+
L.push(" TRUNCATED — a row limit cut this short; counts are a floor");
|
|
199
|
+
}
|
|
200
|
+
L.push(" (declared metadata only — this ledger has no field for the prompt)");
|
|
201
|
+
return L.join("\n");
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=boundaryReceipts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundaryReceipts.js","sourceRoot":"","sources":["../../src/privacy/boundaryReceipts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-structure safety, shared by every surface that renders stored text
|
|
3
|
+
* into the instructions block.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is its own module
|
|
6
|
+
*
|
|
7
|
+
* It used to live in `src/butler/memoryCard.ts`, and the session traces digest
|
|
8
|
+
* — which renders into the SAME prompt from `buildInstructions()`, a few lines
|
|
9
|
+
* away — did not call it. One prompt-rendering path was hardened and its
|
|
10
|
+
* neighbour was not: the partial-surface failure this codebase keeps
|
|
11
|
+
* rediscovering. `AGENT_STEP_TOOL` was centralised for exactly this reason —
|
|
12
|
+
* two sites holding the same rule independently is how they drift.
|
|
13
|
+
*
|
|
14
|
+
* The digest is the sharper of the two cases. Butler facts are graded by
|
|
15
|
+
* provenance, and anything merely READ from a connector is held below the
|
|
16
|
+
* card's origination floor. A decision trace is written by `ctxSaveTrace` — a
|
|
17
|
+
* tool any connected agent can call — and is spliced into every session's
|
|
18
|
+
* instructions within 12 hours, ordered by recency alone, with no verification.
|
|
19
|
+
* Both the write and the read are agent-reachable, so the render is the only
|
|
20
|
+
* place the structure can be defended.
|
|
21
|
+
*
|
|
22
|
+
* ## What this is NOT
|
|
23
|
+
*
|
|
24
|
+
* Not a content filter, and it must never become one. It cannot tell whether a
|
|
25
|
+
* stored lesson is true, sensible or hostile — only that it cannot forge the
|
|
26
|
+
* SHAPE of an instruction. Judging the content would be detection, which
|
|
27
|
+
* ADR-0021 rejects as a boundary for the same reason.
|
|
28
|
+
*/
|
|
29
|
+
export declare function sanitizeForPrompt(s: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt-structure safety, shared by every surface that renders stored text
|
|
3
|
+
* into the instructions block.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is its own module
|
|
6
|
+
*
|
|
7
|
+
* It used to live in `src/butler/memoryCard.ts`, and the session traces digest
|
|
8
|
+
* — which renders into the SAME prompt from `buildInstructions()`, a few lines
|
|
9
|
+
* away — did not call it. One prompt-rendering path was hardened and its
|
|
10
|
+
* neighbour was not: the partial-surface failure this codebase keeps
|
|
11
|
+
* rediscovering. `AGENT_STEP_TOOL` was centralised for exactly this reason —
|
|
12
|
+
* two sites holding the same rule independently is how they drift.
|
|
13
|
+
*
|
|
14
|
+
* The digest is the sharper of the two cases. Butler facts are graded by
|
|
15
|
+
* provenance, and anything merely READ from a connector is held below the
|
|
16
|
+
* card's origination floor. A decision trace is written by `ctxSaveTrace` — a
|
|
17
|
+
* tool any connected agent can call — and is spliced into every session's
|
|
18
|
+
* instructions within 12 hours, ordered by recency alone, with no verification.
|
|
19
|
+
* Both the write and the read are agent-reachable, so the render is the only
|
|
20
|
+
* place the structure can be defended.
|
|
21
|
+
*
|
|
22
|
+
* ## What this is NOT
|
|
23
|
+
*
|
|
24
|
+
* Not a content filter, and it must never become one. It cannot tell whether a
|
|
25
|
+
* stored lesson is true, sensible or hostile — only that it cannot forge the
|
|
26
|
+
* SHAPE of an instruction. Judging the content would be detection, which
|
|
27
|
+
* ADR-0021 rejects as a boundary for the same reason.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Collapse anything that could forge structure in the instructions block.
|
|
31
|
+
*
|
|
32
|
+
* This card renders fact text straight into a SYSTEM PROMPT. The store
|
|
33
|
+
* deliberately accepts any UTF-8 except NUL (a belief is arbitrary text), so
|
|
34
|
+
* the safety boundary has to be here, at the point of rendering — and it has
|
|
35
|
+
* to be here rather than at write time, because rows written before this
|
|
36
|
+
* existed are already on disk.
|
|
37
|
+
*
|
|
38
|
+
* Stripped:
|
|
39
|
+
* - C0/C1 controls including \n and \r — a newline lets a value emit a
|
|
40
|
+
* second line and impersonate a real instruction heading; a lone \r
|
|
41
|
+
* rewrites the line in a terminal render.
|
|
42
|
+
* - U+2028/U+2029 — Unicode line/paragraph separators, newlines by another
|
|
43
|
+
* name in most renderers.
|
|
44
|
+
* - U+202A–U+202E, U+2066–U+2069 — bidi overrides, which reorder displayed
|
|
45
|
+
* text without changing the bytes, so a value can appear to say something
|
|
46
|
+
* other than what is stored.
|
|
47
|
+
*
|
|
48
|
+
* Replaced with a space, not removed: deleting them would silently join words
|
|
49
|
+
* that were separate, which changes the meaning of the belief.
|
|
50
|
+
*/
|
|
51
|
+
const UNSAFE_IN_PROMPT = /[\u0000-\u001F\u007F-\u009F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g;
|
|
52
|
+
export function sanitizeForPrompt(s) {
|
|
53
|
+
return s.replace(UNSAFE_IN_PROMPT, " ").replace(/\s+/g, " ").trim();
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=promptSafety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promptSafety.js","sourceRoot":"","sources":["../src/promptSafety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,gBAAgB,GACpB,qEAAqE,CAAC;AAExE,MAAM,UAAU,iBAAiB,CAAC,CAAS;IACzC,OAAO,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACtE,CAAC"}
|
package/dist/recipeRoutes.js
CHANGED
|
@@ -36,6 +36,7 @@ import { consumeToken, refillBucket, } from "./fp/tokenBucket.js";
|
|
|
36
36
|
import { respondIfUnknownBodyKeys } from "./httpBodyValidation.js";
|
|
37
37
|
import { respond500 } from "./httpErrorResponse.js";
|
|
38
38
|
import { patchworkPath } from "./patchworkHome.js";
|
|
39
|
+
import { summariseBoundaryReceipts } from "./privacy/boundaryReceipts.js";
|
|
39
40
|
import { cancelRun } from "./recipes/runRegistry.js";
|
|
40
41
|
import { invalidateRecipesCache } from "./recipesHttp.js";
|
|
41
42
|
import { validateSafeUrl } from "./ssrfGuard.js";
|
|
@@ -684,6 +685,33 @@ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
|
|
|
684
685
|
}
|
|
685
686
|
return true;
|
|
686
687
|
}
|
|
688
|
+
// GET /privacy/receipts — what the LIVE information boundary actually decided.
|
|
689
|
+
//
|
|
690
|
+
// Sibling of the `patchwork privacy receipts` verb and, like it, reads the
|
|
691
|
+
// ledger FROM DISK rather than through `BoundaryReceiptLog`, whose in-memory
|
|
692
|
+
// array is capped at 500 rows — a cap that would silently become this
|
|
693
|
+
// endpoint's denominator. Imported directly rather than injected because it
|
|
694
|
+
// is a pure file read keyed on PATCHWORK_HOME, which is also how a test
|
|
695
|
+
// redirects it.
|
|
696
|
+
if (parsedUrl.pathname === "/privacy/receipts" && req.method === "GET") {
|
|
697
|
+
try {
|
|
698
|
+
const sp = parsedUrl.searchParams;
|
|
699
|
+
const sinceMsRaw = sp.get("sinceMs");
|
|
700
|
+
const sinceMs = sinceMsRaw ? Number.parseInt(sinceMsRaw, 10) : Number.NaN;
|
|
701
|
+
const limitRaw = sp.get("limit");
|
|
702
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : Number.NaN;
|
|
703
|
+
const summary = summariseBoundaryReceipts({
|
|
704
|
+
...(Number.isFinite(sinceMs) && { since: sinceMs }),
|
|
705
|
+
...(Number.isFinite(limit) && limit > 0 && { recentLimit: limit }),
|
|
706
|
+
});
|
|
707
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
708
|
+
res.end(JSON.stringify(summary));
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
respond500(res, err);
|
|
712
|
+
}
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
687
715
|
// GET /runs/judge-summary — PR3b sibling of /runs/halt-summary.
|
|
688
716
|
// Same query shape (sinceMs, limit, recipe), returns JudgeSummary.
|
|
689
717
|
if (parsedUrl.pathname === "/runs/judge-summary" && req.method === "GET") {
|