@piecemaker-legal/piecemaker 2.0.10 → 2.0.12
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/assets/{FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js → FindReplaceDialog-KU5TEIBJ-BBxf1he7.js} +1 -1
- package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js → FootnotePropertiesDialog-W3YHM4BV-BYqp6U4x.js} +1 -1
- package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js → HyperlinkDialog-ZYH7ZHRF-BVC9zOMs.js} +4 -4
- package/dist/assets/{ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js → ImagePositionDialog-HEWYR6Q3-Xn2mC64y.js} +1 -1
- package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js → ImagePropertiesDialog-RAB47C6M-CFcdqEif.js} +1 -1
- package/dist/assets/{PageSetupDialog-AIGIXO3E-_PYIO8eb.js → PageSetupDialog-AIGIXO3E-DECaeRS9.js} +1 -1
- package/dist/assets/{SplitCellDialog-IFBKYCD6-Cb9uQOB8.js → SplitCellDialog-IFBKYCD6-ByYa04H_.js} +1 -1
- package/dist/assets/{TablePropertiesDialog-TMLKGOOW-12vmjb6B.js → TablePropertiesDialog-TMLKGOOW-BuyqZB5A.js} +1 -1
- package/dist/assets/{WatermarkDialog-KRL7WMIS-C-d8rzt2.js → WatermarkDialog-KRL7WMIS-ByZ1Acr2.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-B4OsX7eF.js → abnfDiagram-VCTEODGH-BtttzdZA.js} +1 -1
- package/dist/assets/{arc-BK1oUIk7.js → arc-DN4m1tLs.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-CfaFI6L7.js → architectureDiagram-5GKGNRK7-Bt-zFumG.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-DQEI2azY.js → blockDiagram-I7D4REHJ-D14V8v4j.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-DbravpVi.js → c4Diagram-7LVT6UL2-BynDo1-W.js} +1 -1
- package/dist/assets/channel-C4gJXzYa.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BLpZ9huC.js → chunk-2Q5K7J3B-B8gKhSj9.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BLlEdmX9.js → chunk-5VM5RSS4-GvOpglRR.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-D-ZNhI6q.js → chunk-F27PBJKO-CAgZy0qc.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-vzSiKnhh.js → chunk-IMKFNOWR-gf8Ux8Zd.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DfF0k4nb.js → chunk-JWPE2WC7-D4zFFRMp.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-BnwCYsPP.js → chunk-POPQ4Y6H-Ct8X_LaT.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-DRxc8fhq.js → chunk-SVP7TREG-g5iH1bs5.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-Dd2Q-md-.js → chunk-TICWLB2K-89-VFKTn.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DBjkEUp_.js → chunk-XXDRQBXY-CaQbdObb.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-Cm08FeYk.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-Cm08FeYk.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-By6PS4oe.js → cose-bilkent-JH36ORCC-BiYOGJAU.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-YxWpjc5W.js → cynefin-OW5HDTMX-F6mJ-4tj.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-DrtXzz3p.js → cynefinDiagram-5FMLGOSQ-q-Rggn6r.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-BDpTLKqZ.js → dagre-GXQ25YYZ-COqImSUH.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-C4LvqeCs.js → diagram-S7CK7UJ4-BAg1dyEc.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-BTpnMeHp.js → diagram-UQ7AKVKN-DFJS8qCV.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-DucHHWev.js → diagram-VSXAHHWV-BfldL35r.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-DYHZNmmH.js → diagram-VX7I27RA-kLjovpYe.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BHIH2BGN.js → diagram-Z3DM3KII-Dhwvi3Iu.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-CGiEkwj0.js → ebnfDiagram-PWID7BFC-Bp4YE1i7.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-AJMHi2Us.js → erDiagram-RLTQ6QDP-BKDdaSWz.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-BqzANBh2.js → flowDiagram-HODETNUW-zHProVxL.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-B0xPDY2_.js → ganttDiagram-EL5Y4UJY-B-B-BI6j.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-CzPX5CNO.js → gitGraphDiagram-WWUBYQGX-BTHMg5CH.js} +1 -1
- package/dist/assets/index-C5slQNvE.css +1 -0
- package/dist/assets/{index-Bk2iXKnJ.js → index-CJP4dZ-c.js} +146 -146
- package/dist/assets/{infoDiagram-27XIBGKW-Cjc44H0O.js → infoDiagram-27XIBGKW-mabp4H9I.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BWhr2v7r.js → ishikawaDiagram-5VMMS53U-CLNhcCrM.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-Bl4r6RrP.js → journeyDiagram-3NMN7TZE-CGEi2RB6.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-FV-T0Vy1.js → kanban-definition-UXKFOSKX-B2fLX1fK.js} +1 -1
- package/dist/assets/{layout-CTfb1sAF.js → layout-CloKWsFq.js} +1 -1
- package/dist/assets/{linear-Dfgi6vlo.js → linear-BFyydI0c.js} +1 -1
- package/dist/assets/{mermaid.core-DOCGsdlo.js → mermaid.core-Dxl3LVAh.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-CymQ86ir.js → mindmap-definition-YA3MSWOX-FT4pm55s.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-1VMq_vi0.js → pegDiagram-XKGWAZYB-okyIfJWM.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-C5GgXtJU.js → pieDiagram-E7YTZNPT-NF61CQz8.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-C6zpryYf.js → quadrantDiagram-AXDQQJYC-uVfvZzzt.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-D-sv4SWi.js → railroadDiagram-O6MQD6OU-Dp8IpeoH.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CkjY-z4B.js → requirementDiagram-BXWQKSXE-D8VV0oQe.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-De-d8Q6L.js → sankeyDiagram-P5KCCOFB-D-xhxjob.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-DCsGSJa9.js → sequenceDiagram-WJ2MYXX4-BAQBK95h.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-xINZhgER.js → sizeCapture-INFHLROL-C6rk9SVx.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-CuLUTWSL.js → stateDiagram-D77RDMKH-DshqaEOj.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-Cnbs-M92.js → stateDiagram-v2-MP3YSRHH-BohZIrk5.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-BwxARD7N.js → swimlanes-42K2YHIH-DgcPDy4l.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-CuM3Su0K.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-BIOvnn-l.js → timeline-definition-24CTP7MA-OgpIe-gQ.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-BSD6Pfmt.js → vennDiagram-4TSXK5OY-BZzGd6jt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-7Zz2iRkF.js → wardleyDiagram-VM6X3IG4-1tg-H7l_.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-Ctx17Y1D.js → xychartDiagram-S5SC5T6Z-ByTsX8g0.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +61 -1
- package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
- package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +0 -48
- package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
- package/dist-server/server/piecemaker/agent-instructions.js +148 -0
- package/dist-server/server/piecemaker/agent-instructions.js.map +1 -0
- package/dist-server/server/piecemaker/index.js +4 -0
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/pipeline.js +25 -29
- package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/service.js +1 -16
- package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
- package/dist-server/server/piecemaker/project-registry.js +5 -5
- package/dist-server/server/piecemaker/project-registry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/piecemaker/cli/lib/plugins.mjs +1 -0
- package/server/piecemaker/agent-instructions.ts +148 -0
- package/server/piecemaker/anonymizer/dictionary.cjs +34 -3
- package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +1 -1
- package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +2 -1
- package/server/piecemaker/anonymizer/hudsucker-smoke.cjs +343 -0
- package/server/piecemaker/anonymizer/rewrite.cjs +2 -2
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +7 -4
- package/server/piecemaker/anonymizer/rewriter-worker.cjs +85 -0
- package/server/piecemaker/anonymizer/service.cjs +10 -8
- package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +15 -32
- package/server/piecemaker/index.ts +5 -1
- package/server/piecemaker/knowledge/pipeline.ts +24 -31
- package/server/piecemaker/knowledge/service.ts +1 -14
- package/server/piecemaker/project-registry.ts +6 -6
- package/server/piecemaker/router.cjs +0 -1
- package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +9 -162
- package/server/piecemaker/vendor/installer/lib/commandes.mjs +1 -2
- package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +62 -3
- package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +5 -5
- package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +23 -69
- package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +3 -3
- package/server/piecemaker/vendor/mcp/README.md +1 -1
- package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +12 -75
- package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +0 -40
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +2 -2
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +2 -90
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +26 -7
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +0 -28
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +0 -13
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +3 -3
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +10 -143
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +1 -1
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +0 -65
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +189 -41
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +11 -2
- package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +4 -18
- package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +9 -5
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +3 -179
- package/server/piecemaker/vendor/websocket-server/case-registry.cjs +0 -10
- package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +3 -0
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +23 -372
- package/server/piecemaker/vendor/websocket-server/renamed-originals.cjs +136 -0
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +115 -96
- package/dist/assets/channel-DVyNr2Yg.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +0 -1
- package/dist/assets/index-Dc9wNuS4.css +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +0 -8
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +0 -194
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +0 -71
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +0 -260
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit.mjs +0 -83
|
@@ -30,6 +30,19 @@ const EMPTY = Object.freeze({
|
|
|
30
30
|
empty: true,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
const MEMO_MIN_LENGTH = 256;
|
|
34
|
+
const MEMO_BUDGET = 16 * 1024 * 1024;
|
|
35
|
+
const memos = new WeakMap();
|
|
36
|
+
|
|
37
|
+
function memoFor(dictionary) {
|
|
38
|
+
let memo = memos.get(dictionary);
|
|
39
|
+
if (!memo) {
|
|
40
|
+
memo = { entries: new Map(), size: 0 };
|
|
41
|
+
memos.set(dictionary, memo);
|
|
42
|
+
}
|
|
43
|
+
return memo;
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
function plainObject(value) {
|
|
34
47
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
35
48
|
}
|
|
@@ -49,8 +62,9 @@ function plainObject(value) {
|
|
|
49
62
|
* ne part pas codé serait un mensonge à l'écran.
|
|
50
63
|
*/
|
|
51
64
|
function buildDictionary(document, stamp) {
|
|
52
|
-
const mapping = plainObject(document?.mapping);
|
|
53
|
-
const reverse = plainObject(document?.reverse_mapping)
|
|
65
|
+
const mapping = Object.freeze({ ...plainObject(document?.mapping) });
|
|
66
|
+
const reverse = Object.freeze(Object.fromEntries(Object.entries(plainObject(document?.reverse_mapping))
|
|
67
|
+
.map(([code, variants]) => [code, Array.isArray(variants) ? Object.freeze([...variants]) : variants])));
|
|
54
68
|
const canonical = {};
|
|
55
69
|
for (const [code, variants] of Object.entries(reverse)) {
|
|
56
70
|
const name = Array.isArray(variants) ? variants[0] : variants;
|
|
@@ -89,7 +103,24 @@ function buildDictionary(document, stamp) {
|
|
|
89
103
|
|
|
90
104
|
/** Nom réel → code, sur tout ce qui part vers l'API. */
|
|
91
105
|
function anonymize(text, dictionary) {
|
|
92
|
-
|
|
106
|
+
if (dictionary.empty) return text;
|
|
107
|
+
if (typeof text !== 'string' || text.length < MEMO_MIN_LENGTH) return applyMapping(text, dictionary.mapping);
|
|
108
|
+
const memo = memoFor(dictionary);
|
|
109
|
+
const cached = memo.entries.get(text);
|
|
110
|
+
if (cached !== undefined) {
|
|
111
|
+
memo.entries.delete(text);
|
|
112
|
+
memo.entries.set(text, cached);
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
const result = applyMapping(text, dictionary.mapping);
|
|
116
|
+
memo.entries.set(text, result);
|
|
117
|
+
memo.size += text.length + (result === text ? 0 : result.length);
|
|
118
|
+
for (const [key, value] of memo.entries) {
|
|
119
|
+
if (memo.size <= MEMO_BUDGET) break;
|
|
120
|
+
memo.entries.delete(key);
|
|
121
|
+
memo.size -= key.length + (value === key ? 0 : value.length);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
93
124
|
}
|
|
94
125
|
|
|
95
126
|
/** Code → nom réel, sur tout ce qui revient de l'API vers la machine. */
|
|
@@ -49,7 +49,7 @@ async function startHudsuckerSession({
|
|
|
49
49
|
databasePath,
|
|
50
50
|
upstreamPort,
|
|
51
51
|
harness = null,
|
|
52
|
-
hosts = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'],
|
|
52
|
+
hosts = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com', 'api.mistral.ai'],
|
|
53
53
|
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-hudsucker-')),
|
|
54
54
|
} = {}) {
|
|
55
55
|
const binary = hudsuckerBinary();
|
|
@@ -70,6 +70,7 @@ fn provider_for(host: &str) -> &'static str {
|
|
|
70
70
|
match host {
|
|
71
71
|
"chatgpt.com" => "codex",
|
|
72
72
|
"api.openai.com" => "opencode",
|
|
73
|
+
"api.mistral.ai" => "mistral",
|
|
73
74
|
_ => "claude",
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -471,7 +472,7 @@ async fn main() {
|
|
|
471
472
|
let hosts = parse_list(&arg("--hosts"));
|
|
472
473
|
let upstream = parse_map(&arg("--upstream-map"));
|
|
473
474
|
if listen.is_empty() || rewriter.is_empty() || ca_cert.is_empty() || ca_key.is_empty() || hosts.is_empty() {
|
|
474
|
-
eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:0 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com");
|
|
475
|
+
eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:0 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com,api.mistral.ai");
|
|
475
476
|
std::process::exit(2);
|
|
476
477
|
}
|
|
477
478
|
exit_when_parent_dies();
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
const { execFileSync, spawn } = require('node:child_process');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const http = require('node:http');
|
|
4
|
+
const https = require('node:https');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const tls = require('node:tls');
|
|
8
|
+
|
|
9
|
+
const Database = require('better-sqlite3');
|
|
10
|
+
|
|
11
|
+
const { startRewriterWorker } = require('./rewriter-worker.cjs');
|
|
12
|
+
|
|
13
|
+
const HOST = 'api.anthropic.com';
|
|
14
|
+
const NAME = 'Jean Dupont';
|
|
15
|
+
const CODE = 'PERSONNE_PHYSIQUE_01';
|
|
16
|
+
const CHECK_TIMEOUT_MS = 10000;
|
|
17
|
+
const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
|
|
18
|
+
|
|
19
|
+
function defaultBinary() {
|
|
20
|
+
const name = process.platform === 'win32' ? 'piecemaker-hudsucker.exe' : 'piecemaker-hudsucker';
|
|
21
|
+
return [
|
|
22
|
+
path.join(__dirname, 'bin', name),
|
|
23
|
+
path.join(__dirname, 'hudsucker-proxy', 'target', 'release', name),
|
|
24
|
+
].find((candidate) => fs.existsSync(candidate)) || path.join(__dirname, 'bin', name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mappingDatabase(directory) {
|
|
28
|
+
const file = path.join(directory, 'auth.db');
|
|
29
|
+
const database = new Database(file);
|
|
30
|
+
database.exec(`
|
|
31
|
+
CREATE TABLE piecemaker_nodes (project_id TEXT, id TEXT, label TEXT, PRIMARY KEY (project_id, id));
|
|
32
|
+
CREATE TABLE piecemaker_mappings (project_id TEXT, node_id TEXT, real_value TEXT, masked_value TEXT, updated_at TEXT);
|
|
33
|
+
`);
|
|
34
|
+
database.prepare('INSERT INTO piecemaker_nodes VALUES (?, ?, ?)').run('smoke', 'n0', NAME);
|
|
35
|
+
database.prepare('INSERT INTO piecemaker_mappings VALUES (?, ?, ?, ?, ?)').run('smoke', 'n0', NAME, CODE, '2026-01-01T00:00:00.000Z');
|
|
36
|
+
database.close();
|
|
37
|
+
return file;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function temporaryAuthority(directory) {
|
|
41
|
+
const cert = path.join(directory, 'ca.crt');
|
|
42
|
+
const key = path.join(directory, 'ca.key');
|
|
43
|
+
execFileSync('openssl', [
|
|
44
|
+
'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
|
|
45
|
+
'-keyout', key, '-out', cert, '-days', '1',
|
|
46
|
+
'-subj', '/CN=PieceMaker Smoke CA',
|
|
47
|
+
'-addext', 'basicConstraints=critical,CA:true,pathlen:0',
|
|
48
|
+
'-addext', 'keyUsage=critical,keyCertSign,cRLSign',
|
|
49
|
+
], { stdio: 'ignore' });
|
|
50
|
+
return { cert, key };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sseEvent(text) {
|
|
54
|
+
return `event: content_block_delta\ndata: ${JSON.stringify({
|
|
55
|
+
type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text },
|
|
56
|
+
})}\n\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function startUpstream() {
|
|
60
|
+
const seen = [];
|
|
61
|
+
const server = http.createServer((request, response) => {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
request.on('data', (chunk) => chunks.push(chunk));
|
|
64
|
+
request.on('end', () => {
|
|
65
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
66
|
+
seen.push(body);
|
|
67
|
+
if (request.headers['x-smoke-mode'] === 'sse') {
|
|
68
|
+
response.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
69
|
+
for (const text of ['Le client ', 'PERSONNE_', 'PHYSIQUE', '_01', ' a signé.']) response.write(sseEvent(text));
|
|
70
|
+
response.end('event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
74
|
+
response.end(JSON.stringify({ echo: body }));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
server.listen(0, '127.0.0.1', () => resolve({ seen, server, port: server.address().port }));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sandboxProfile(directory) {
|
|
83
|
+
const writable = JSON.stringify(fs.realpathSync(directory));
|
|
84
|
+
return [
|
|
85
|
+
'(version 1)',
|
|
86
|
+
'(allow default)',
|
|
87
|
+
'(deny network-outbound)',
|
|
88
|
+
'(allow network-outbound (remote ip "localhost:*"))',
|
|
89
|
+
'(allow network-outbound (remote unix-socket))',
|
|
90
|
+
'(deny file-write*)',
|
|
91
|
+
`(allow file-write* (subpath ${writable}) (literal "/dev/null") (literal "/dev/tty"))`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function launch(binary, args, directory, sandboxed) {
|
|
96
|
+
const env = {
|
|
97
|
+
...process.env,
|
|
98
|
+
HOME: directory,
|
|
99
|
+
USERPROFILE: directory,
|
|
100
|
+
HTTPS_PROXY: '', HTTP_PROXY: '', ALL_PROXY: '', https_proxy: '', http_proxy: '', all_proxy: '',
|
|
101
|
+
};
|
|
102
|
+
const options = { cwd: directory, env, stdio: ['pipe', 'pipe', 'pipe'] };
|
|
103
|
+
if (sandboxed) return spawn(SANDBOX_EXEC, ['-p', sandboxProfile(directory), binary, ...args], options);
|
|
104
|
+
return spawn(binary, args, options);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function waitForListening(child, logs) {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
let buffer = '';
|
|
110
|
+
const timer = setTimeout(() => reject(new Error(`aucune écoute après 15 s : ${logs.text || buffer}`)), 15000);
|
|
111
|
+
child.stdout.on('data', (chunk) => {
|
|
112
|
+
buffer += chunk;
|
|
113
|
+
const match = buffer.match(/listening 127\.0\.0\.1:(\d+)/);
|
|
114
|
+
if (!match) return;
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
if (Number(match[1]) === 0) reject(new Error('port annoncé 0 : binaire antérieur au source, à recompiler'));
|
|
117
|
+
else resolve(Number(match[1]));
|
|
118
|
+
});
|
|
119
|
+
child.once('error', (error) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(error);
|
|
122
|
+
});
|
|
123
|
+
child.once('exit', (code, signal) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(new Error(`sorti (${code ?? signal}) : ${logs.text || buffer}`));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function withTimeout(promise, label) {
|
|
131
|
+
let timer;
|
|
132
|
+
return Promise.race([
|
|
133
|
+
promise,
|
|
134
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} : délai dépassé`)), CHECK_TIMEOUT_MS); }),
|
|
135
|
+
]).finally(() => clearTimeout(timer));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function health(port) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
http.get({ host: '127.0.0.1', port, path: '/health' }, (response) => {
|
|
141
|
+
let body = '';
|
|
142
|
+
response.on('data', (chunk) => { body += chunk; });
|
|
143
|
+
response.on('end', () => resolve({ status: response.statusCode, body }));
|
|
144
|
+
}).on('error', reject);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function refuseDirectResolution(hostname, _options, callback) {
|
|
149
|
+
callback(new Error(`résolution directe de ${hostname} interdite : tout doit passer par le proxy`));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function postThroughProxy(port, ca, { body, contentType = 'application/json', mode = 'json' }) {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const connect = http.request({ host: '127.0.0.1', port, method: 'CONNECT', path: `${HOST}:443`, headers: { host: `${HOST}:443` } });
|
|
155
|
+
connect.once('error', reject);
|
|
156
|
+
connect.once('connect', (answer, socket) => {
|
|
157
|
+
if (answer.statusCode !== 200) {
|
|
158
|
+
socket.destroy();
|
|
159
|
+
reject(new Error(`CONNECT refusé (${answer.statusCode})`));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const request = https.request({
|
|
163
|
+
host: HOST,
|
|
164
|
+
path: '/v1/messages',
|
|
165
|
+
method: 'POST',
|
|
166
|
+
lookup: refuseDirectResolution,
|
|
167
|
+
headers: { 'content-type': contentType, 'content-length': Buffer.byteLength(body), 'x-smoke-mode': mode },
|
|
168
|
+
createConnection: () => tls.connect({ socket, servername: HOST, ca }),
|
|
169
|
+
}, (response) => {
|
|
170
|
+
let text = '';
|
|
171
|
+
response.setEncoding('utf8');
|
|
172
|
+
response.on('data', (chunk) => { text += chunk; });
|
|
173
|
+
response.on('end', () => {
|
|
174
|
+
socket.destroy();
|
|
175
|
+
resolve({ status: response.statusCode, body: text });
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
request.once('error', reject);
|
|
179
|
+
request.end(body);
|
|
180
|
+
});
|
|
181
|
+
connect.end();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function streamedText(body) {
|
|
186
|
+
return body
|
|
187
|
+
.split('\n')
|
|
188
|
+
.filter((line) => line.startsWith('data: '))
|
|
189
|
+
.map((line) => {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(line.slice(6))?.delta?.text || '';
|
|
192
|
+
} catch {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.join('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function waitForExit(child, timeoutMs) {
|
|
200
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
203
|
+
child.once('exit', () => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
resolve(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function runHudsuckerSmokeTest({ binary: requestedBinary = defaultBinary(), caCert = null, caKey = null, sandbox = true, report = () => {} } = {}) {
|
|
211
|
+
const binary = path.resolve(requestedBinary);
|
|
212
|
+
const checks = [];
|
|
213
|
+
const record = (name, ok, detail = '') => {
|
|
214
|
+
checks.push({ name, ok, detail });
|
|
215
|
+
report({ name, ok, detail });
|
|
216
|
+
return ok;
|
|
217
|
+
};
|
|
218
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-hudsucker-smoke-'));
|
|
219
|
+
const sandboxed = sandbox && process.platform === 'darwin' && fs.existsSync(SANDBOX_EXEC);
|
|
220
|
+
let child = null;
|
|
221
|
+
let bridge = null;
|
|
222
|
+
let upstream = null;
|
|
223
|
+
try {
|
|
224
|
+
if (!fs.existsSync(binary)) {
|
|
225
|
+
record('binaire présent', false, binary);
|
|
226
|
+
return { ok: false, sandboxed, checks };
|
|
227
|
+
}
|
|
228
|
+
const authority = caCert && caKey ? { cert: caCert, key: caKey } : temporaryAuthority(directory);
|
|
229
|
+
for (const file of [authority.cert, authority.key]) {
|
|
230
|
+
if (!fs.existsSync(file)) {
|
|
231
|
+
record('autorité locale présente', false, file);
|
|
232
|
+
return { ok: false, sandboxed, checks };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const ca = fs.readFileSync(authority.cert);
|
|
236
|
+
|
|
237
|
+
bridge = await startRewriterWorker({ databasePath: mappingDatabase(directory), homeDir: directory });
|
|
238
|
+
upstream = await startUpstream();
|
|
239
|
+
|
|
240
|
+
const logs = { text: '' };
|
|
241
|
+
child = launch(binary, [
|
|
242
|
+
'--listen', '127.0.0.1:0',
|
|
243
|
+
'--rewriter', `http://127.0.0.1:${bridge.port}`,
|
|
244
|
+
'--ca-cert', authority.cert,
|
|
245
|
+
'--ca-key', authority.key,
|
|
246
|
+
'--hosts', HOST,
|
|
247
|
+
'--upstream-map', `${HOST}=127.0.0.1:${upstream.port}`,
|
|
248
|
+
], directory, sandboxed);
|
|
249
|
+
child.stderr.on('data', (chunk) => { logs.text += chunk; });
|
|
250
|
+
|
|
251
|
+
let port;
|
|
252
|
+
try {
|
|
253
|
+
port = await waitForListening(child, logs);
|
|
254
|
+
record('démarrage', true, `écoute sur 127.0.0.1:${port}${sandboxed ? ', dans sandbox-exec' : ''}`);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
record('démarrage', false, error.message);
|
|
257
|
+
return { ok: false, sandboxed, checks };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const step = async (name, run) => {
|
|
261
|
+
try {
|
|
262
|
+
const detail = await withTimeout(run(), name);
|
|
263
|
+
record(name, true, detail || '');
|
|
264
|
+
} catch (error) {
|
|
265
|
+
record(name, false, error.message);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
await step('santé', async () => {
|
|
270
|
+
const { status, body } = await health(port);
|
|
271
|
+
if (status !== 200 || !body.includes('"rewriter":"ok"')) throw new Error(`HTTP ${status} ${body}`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await step('anonymisation JSON aller-retour', async () => {
|
|
275
|
+
const before = upstream.seen.length;
|
|
276
|
+
const response = await postThroughProxy(port, ca, {
|
|
277
|
+
body: JSON.stringify({ messages: [{ role: 'user', content: `Contrat de ${NAME}` }] }),
|
|
278
|
+
});
|
|
279
|
+
const sent = upstream.seen[before] || '';
|
|
280
|
+
if (response.status !== 200) throw new Error(`HTTP ${response.status} ${response.body}`);
|
|
281
|
+
if (sent.includes(NAME) || !sent.includes(CODE)) throw new Error(`le serveur distant a reçu : ${sent}`);
|
|
282
|
+
if (!response.body.includes(NAME) || response.body.includes(CODE)) throw new Error(`réponse non restituée : ${response.body}`);
|
|
283
|
+
return `le distant voit ${CODE}, le client lit « ${NAME} »`;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await step('flux SSE restitué', async () => {
|
|
287
|
+
const response = await postThroughProxy(port, ca, { body: JSON.stringify({ stream: true, messages: [] }), mode: 'sse' });
|
|
288
|
+
const text = streamedText(response.body);
|
|
289
|
+
if (response.status !== 200 || text !== `Le client ${NAME} a signé.`) throw new Error(`HTTP ${response.status}, texte « ${text} »`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await step('refus d’un corps non JSON', async () => {
|
|
293
|
+
const before = upstream.seen.length;
|
|
294
|
+
const response = await postThroughProxy(port, ca, { body: `Contrat de ${NAME}`, contentType: 'text/plain' });
|
|
295
|
+
if (response.status !== 415 || upstream.seen.length !== before) throw new Error(`HTTP ${response.status}, transmis : ${upstream.seen.length - before}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await step('refus si le réécrivain tombe', async () => {
|
|
299
|
+
await bridge.close();
|
|
300
|
+
bridge = null;
|
|
301
|
+
const before = upstream.seen.length;
|
|
302
|
+
const response = await postThroughProxy(port, ca, { body: JSON.stringify({ messages: [{ role: 'user', content: NAME }] }) });
|
|
303
|
+
if (response.status !== 502 || upstream.seen.length !== before) throw new Error(`HTTP ${response.status}, transmis : ${upstream.seen.length - before}`);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await step('arrêt avec son parent', async () => {
|
|
307
|
+
child.stdin.end();
|
|
308
|
+
if (!(await waitForExit(child, 3000))) throw new Error('toujours en vie 3 s après la fermeture de son entrée');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return { ok: checks.every((check) => check.ok), sandboxed, checks };
|
|
312
|
+
} catch (error) {
|
|
313
|
+
record('banc de test', false, error.message);
|
|
314
|
+
return { ok: false, sandboxed, checks };
|
|
315
|
+
} finally {
|
|
316
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
317
|
+
child.kill('SIGKILL');
|
|
318
|
+
await waitForExit(child, 2000);
|
|
319
|
+
}
|
|
320
|
+
if (bridge) await bridge.close();
|
|
321
|
+
if (upstream) await new Promise((resolve) => upstream.server.close(resolve));
|
|
322
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function argument(name) {
|
|
327
|
+
const index = process.argv.indexOf(name);
|
|
328
|
+
return index === -1 ? null : process.argv[index + 1] || null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
runHudsuckerSmokeTest({
|
|
333
|
+
binary: argument('--binary') || defaultBinary(),
|
|
334
|
+
caCert: argument('--ca-cert'),
|
|
335
|
+
caKey: argument('--ca-key'),
|
|
336
|
+
sandbox: !process.argv.includes('--no-sandbox'),
|
|
337
|
+
report: ({ name, ok, detail }) => console.log(`${ok ? 'OK ' : 'ÉCHEC'} ${name}${detail ? ` — ${detail}` : ''}`),
|
|
338
|
+
}).then((result) => {
|
|
339
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = { runHudsuckerSmokeTest };
|
|
@@ -218,7 +218,7 @@ function createSseRewriter(transform) {
|
|
|
218
218
|
return lines(JSON.stringify(parsed), undefined);
|
|
219
219
|
}
|
|
220
220
|
const keys = [...held.keys()].filter((key) => key.startsWith(keyPrefix));
|
|
221
|
-
return withFlush(keys, JSON.stringify(parsed), parsed.type);
|
|
221
|
+
return withFlush(keys, JSON.stringify(rewriteJsonValue(parsed, transform)), parsed.type);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
function rewriteResponses(parsed) {
|
|
@@ -236,7 +236,7 @@ function createSseRewriter(transform) {
|
|
|
236
236
|
const keys = eventType === 'response.completed'
|
|
237
237
|
? [...held.keys()]
|
|
238
238
|
: eventType.endsWith('.done') ? [key] : [];
|
|
239
|
-
return withFlush(keys, JSON.stringify(parsed), parsed.type);
|
|
239
|
+
return withFlush(keys, JSON.stringify(rewriteJsonValue(parsed, transform)), parsed.type);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
function rewriteChatChunk(parsed) {
|
|
@@ -84,6 +84,7 @@ function startRewriterBridge({ dictionary, harness = null }) {
|
|
|
84
84
|
const server = http.createServer((request, response) => {
|
|
85
85
|
const provider = header(request, 'x-piecemaker-provider') || 'claude';
|
|
86
86
|
const current = () => dictionary.get();
|
|
87
|
+
const outbound = () => (dictionary.getForOutbound ? dictionary.getForOutbound() : dictionary.get());
|
|
87
88
|
|
|
88
89
|
if (request.method === 'GET' && request.url === '/health') {
|
|
89
90
|
response.writeHead(200);
|
|
@@ -95,14 +96,14 @@ function startRewriterBridge({ dictionary, harness = null }) {
|
|
|
95
96
|
readBody(request).then((body) => {
|
|
96
97
|
stats.requests += 1;
|
|
97
98
|
const contentType = header(request, 'content-type') || 'application/json';
|
|
98
|
-
|
|
99
|
-
const dict = current();
|
|
99
|
+
const dict = outbound();
|
|
100
100
|
const rewritten = !body.length || dict.empty
|
|
101
101
|
? body
|
|
102
102
|
: Buffer.from(rewriteJsonBody(body, (text) => anonymize(text, dict)), 'utf8');
|
|
103
103
|
if (!rewritten.equals(body)) stats.anonymized += 1;
|
|
104
104
|
response.writeHead(200, { 'content-type': 'application/octet-stream', 'content-length': rewritten.length });
|
|
105
105
|
response.end(rewritten);
|
|
106
|
+
setImmediate(() => observeRequest(harness, body, contentType, provider));
|
|
106
107
|
}).catch((error) => {
|
|
107
108
|
stats.failures += 1;
|
|
108
109
|
stats.lastError = error.message;
|
|
@@ -165,15 +166,16 @@ function startRewriterBridge({ dictionary, harness = null }) {
|
|
|
165
166
|
const id = header(request, 'x-piecemaker-ws');
|
|
166
167
|
const direction = header(request, 'x-piecemaker-direction');
|
|
167
168
|
readBody(request).then((body) => {
|
|
168
|
-
const dict = current();
|
|
169
169
|
const text = body.toString('utf8');
|
|
170
170
|
if (direction === 'out') {
|
|
171
|
-
|
|
171
|
+
const dict = outbound();
|
|
172
172
|
const rewritten = dict.empty ? text : rewriteJsonBody(text, (value) => anonymize(value, dict));
|
|
173
173
|
response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
174
174
|
response.end(rewritten);
|
|
175
|
+
setImmediate(() => observeRequest(harness, body, 'application/json', provider));
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
178
|
+
const dict = current();
|
|
177
179
|
let entry = sessions.get(id);
|
|
178
180
|
if (!entry) {
|
|
179
181
|
entry = { rewriter: createSseRewriter((value) => deanonymize(value, dict)) };
|
|
@@ -208,6 +210,7 @@ function startRewriterBridge({ dictionary, harness = null }) {
|
|
|
208
210
|
response.writeHead(404);
|
|
209
211
|
response.end();
|
|
210
212
|
});
|
|
213
|
+
server.requestTimeout = 0;
|
|
211
214
|
|
|
212
215
|
return new Promise((resolve, reject) => {
|
|
213
216
|
server.once('error', reject);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const { SHARE_ENV, Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
|
|
3
|
+
|
|
4
|
+
const STATS_INTERVAL_MS = 1000;
|
|
5
|
+
const CLOSE_TIMEOUT_MS = 2000;
|
|
6
|
+
|
|
7
|
+
function runInsideWorker() {
|
|
8
|
+
const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
|
|
9
|
+
const { startRewriterBridge } = require('./rewriter-bridge.cjs');
|
|
10
|
+
const { createHarnessJuridique } = require('../harness/index.cjs');
|
|
11
|
+
|
|
12
|
+
const dictionary = createSqliteDictionaryLoader({ databasePath: workerData.databasePath });
|
|
13
|
+
const harness = createHarnessJuridique({ homeDir: workerData.homeDir, verifyResponses: false });
|
|
14
|
+
|
|
15
|
+
startRewriterBridge({ dictionary, harness }).then((bridge) => {
|
|
16
|
+
let published = '';
|
|
17
|
+
const publishStats = () => {
|
|
18
|
+
const snapshot = JSON.stringify(bridge.stats);
|
|
19
|
+
if (snapshot === published) return;
|
|
20
|
+
published = snapshot;
|
|
21
|
+
parentPort.postMessage({ type: 'stats', stats: bridge.stats });
|
|
22
|
+
};
|
|
23
|
+
const timer = setInterval(publishStats, STATS_INTERVAL_MS);
|
|
24
|
+
parentPort.on('message', (message) => {
|
|
25
|
+
if (message?.type !== 'close') return;
|
|
26
|
+
clearInterval(timer);
|
|
27
|
+
publishStats();
|
|
28
|
+
bridge.close().finally(() => {
|
|
29
|
+
dictionary.close();
|
|
30
|
+
parentPort.postMessage({ type: 'closed' });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
parentPort.postMessage({ type: 'listening', port: bridge.port });
|
|
34
|
+
}, (error) => {
|
|
35
|
+
dictionary.close();
|
|
36
|
+
parentPort.postMessage({ type: 'error', message: error.message });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function startRewriterWorker({ databasePath, homeDir }) {
|
|
41
|
+
const worker = new Worker(path.join(__dirname, 'rewriter-worker.cjs'), {
|
|
42
|
+
workerData: { databasePath, homeDir },
|
|
43
|
+
env: SHARE_ENV,
|
|
44
|
+
});
|
|
45
|
+
const stats = { requests: 0, anonymized: 0, deanonymized: 0, failures: 0, lastError: null };
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const fail = (error) => {
|
|
49
|
+
worker.terminate();
|
|
50
|
+
reject(error);
|
|
51
|
+
};
|
|
52
|
+
worker.on('error', (error) => {
|
|
53
|
+
stats.failures += 1;
|
|
54
|
+
stats.lastError = error.message;
|
|
55
|
+
fail(error);
|
|
56
|
+
});
|
|
57
|
+
worker.once('exit', (code) => reject(new Error(`réécrivain arrêté (${code})`)));
|
|
58
|
+
worker.on('message', (message) => {
|
|
59
|
+
if (message?.type === 'stats') Object.assign(stats, message.stats);
|
|
60
|
+
if (message?.type === 'error') fail(new Error(message.message));
|
|
61
|
+
if (message?.type !== 'listening') return;
|
|
62
|
+
resolve({
|
|
63
|
+
port: message.port,
|
|
64
|
+
stats,
|
|
65
|
+
close() {
|
|
66
|
+
return new Promise((done) => {
|
|
67
|
+
const timer = setTimeout(() => worker.terminate().then(() => done()), CLOSE_TIMEOUT_MS);
|
|
68
|
+
worker.once('exit', () => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
done();
|
|
71
|
+
});
|
|
72
|
+
worker.on('message', (reply) => {
|
|
73
|
+
if (reply?.type === 'closed') worker.terminate();
|
|
74
|
+
});
|
|
75
|
+
worker.postMessage({ type: 'close' });
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isMainThread && workerData?.databasePath) runInsideWorker();
|
|
84
|
+
|
|
85
|
+
module.exports = { startRewriterWorker };
|
|
@@ -5,16 +5,16 @@ const path = require('node:path');
|
|
|
5
5
|
const tls = require('node:tls');
|
|
6
6
|
|
|
7
7
|
const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
|
|
8
|
-
const {
|
|
8
|
+
const { startRewriterWorker } = require('./rewriter-worker.cjs');
|
|
9
9
|
const { removeLegacyProxyConfig } = require('./client-config.cjs');
|
|
10
|
-
const { createHarnessJuridique } = require('../harness/index.cjs');
|
|
11
10
|
|
|
12
11
|
const DEFAULT_UPSTREAM = 'https://api.anthropic.com';
|
|
13
|
-
const INTERCEPTED_HOSTS = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'];
|
|
12
|
+
const INTERCEPTED_HOSTS = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com', 'api.mistral.ai'];
|
|
14
13
|
const COVERAGE = {
|
|
15
14
|
claude: { state: 'filtered', detail: null, file: null },
|
|
16
15
|
codex: { state: 'filtered', detail: null, file: null },
|
|
17
16
|
opencode: { state: 'filtered', detail: null, file: null },
|
|
17
|
+
mistral: { state: 'filtered', detail: null, file: null },
|
|
18
18
|
cursor: { state: 'blocked', detail: 'not-interceptable', file: null },
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -52,7 +52,9 @@ function hudsuckerBinary() {
|
|
|
52
52
|
path.join(__dirname, 'hudsucker-proxy', 'target', 'release', name),
|
|
53
53
|
path.join(__dirname, 'hudsucker-proxy', 'target', 'debug', name),
|
|
54
54
|
];
|
|
55
|
-
|
|
55
|
+
const present = candidates.filter((candidate) => fs.existsSync(candidate));
|
|
56
|
+
if (!present.length) return candidates[0];
|
|
57
|
+
return present.reduce((newest, candidate) => (fs.statSync(candidate).mtimeMs > fs.statSync(newest).mtimeMs ? candidate : newest));
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
function certificatePaths() {
|
|
@@ -136,8 +138,8 @@ function proxyEnvironment(proxyUrl, caFile, trustBundle) {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = console }) {
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
+
const databasePath = process.env.DATABASE_PATH || path.join(homeDir, 'auth.db');
|
|
142
|
+
const dictionary = createSqliteDictionaryLoader({ databasePath });
|
|
141
143
|
const binDir = path.join(homeDir, 'bin');
|
|
142
144
|
const state = { enabled: false, reason: 'not_started', origin: null, upstream: null, coverage: {} };
|
|
143
145
|
let proxy = null;
|
|
@@ -165,7 +167,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
|
|
|
165
167
|
const hosts = [...new Set([...INTERCEPTED_HOSTS, new URL(upstream).hostname])];
|
|
166
168
|
|
|
167
169
|
try {
|
|
168
|
-
bridge = await
|
|
170
|
+
bridge = await startRewriterWorker({ databasePath, homeDir });
|
|
169
171
|
} catch (error) {
|
|
170
172
|
state.reason = `listen_failed: ${error.message}`;
|
|
171
173
|
logger.warn?.(`[piecemaker] réécriture PII non démarrée : ${error.message}`);
|
|
@@ -221,7 +223,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
|
|
|
221
223
|
upstream,
|
|
222
224
|
coverage: COVERAGE,
|
|
223
225
|
});
|
|
224
|
-
logger.log?.(`[piecemaker] anonymisation active sur ${origin} (claude, codex, opencode ; cursor bloqué)`);
|
|
226
|
+
logger.log?.(`[piecemaker] anonymisation active sur ${origin} (claude, codex, opencode, mistral ; cursor bloqué)`);
|
|
225
227
|
return state;
|
|
226
228
|
}
|
|
227
229
|
|