@piecemaker-legal/piecemaker 1.1.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/assets/{abnfDiagram-VCTEODGH-DEXWh3LH.js → abnfDiagram-VCTEODGH-BeQS_HnO.js} +1 -1
  2. package/dist/assets/{arc-CoTRLXKW.js → arc-ClYjD3aM.js} +1 -1
  3. package/dist/assets/{architectureDiagram-5GKGNRK7-Cg-jO9oR.js → architectureDiagram-5GKGNRK7-Csdihb1L.js} +1 -1
  4. package/dist/assets/{blockDiagram-I7D4REHJ-ChXwQSIq.js → blockDiagram-I7D4REHJ-BuXey9ht.js} +1 -1
  5. package/dist/assets/{c4Diagram-7LVT6UL2-Bw2VGggd.js → c4Diagram-7LVT6UL2-DdO-ovOy.js} +1 -1
  6. package/dist/assets/channel-DhuuSCHK.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-C1WdqrLP.js → chunk-2Q5K7J3B-D3hzWzSc.js} +1 -1
  8. package/dist/assets/{chunk-5VM5RSS4-BerUZBa1.js → chunk-5VM5RSS4-DeT3cR6k.js} +1 -1
  9. package/dist/assets/{chunk-F27PBJKO-5W7ax2gB.js → chunk-F27PBJKO-bazGi1ru.js} +1 -1
  10. package/dist/assets/{chunk-IMKFNOWR-JgDN1h4T.js → chunk-IMKFNOWR-BE3ZXrPd.js} +1 -1
  11. package/dist/assets/{chunk-JWPE2WC7-ulNHnEdQ.js → chunk-JWPE2WC7-dTAsgByE.js} +1 -1
  12. package/dist/assets/{chunk-POPQ4Y6H-D4aTDC2T.js → chunk-POPQ4Y6H-DKYrzMSd.js} +1 -1
  13. package/dist/assets/{chunk-SVP7TREG-BA5zX4XF.js → chunk-SVP7TREG-BTtrMvhE.js} +1 -1
  14. package/dist/assets/{chunk-TICWLB2K-DhOULVhX.js → chunk-TICWLB2K-CZPHShPS.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-CkpCgrVg.js → chunk-XXDRQBXY-B0byFHaU.js} +1 -1
  16. package/dist/assets/classDiagram-ZZMXUADV-CO0E82Dl.js +1 -0
  17. package/dist/assets/classDiagram-v2-VYDZK3BY-CO0E82Dl.js +1 -0
  18. package/dist/assets/{cose-bilkent-JH36ORCC-0xUSfjfV.js → cose-bilkent-JH36ORCC-BbLCzlaU.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BE6fNu2_.js → cynefin-OW5HDTMX-CQ8ui4YD.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-5FMLGOSQ-rFMnZ9b3.js → cynefinDiagram-5FMLGOSQ-T-GL1aIf.js} +1 -1
  21. package/dist/assets/{dagre-GXQ25YYZ-C1p4ffd7.js → dagre-GXQ25YYZ-CXxVKznH.js} +1 -1
  22. package/dist/assets/{diagram-S7CK7UJ4-BV_3Xj4O.js → diagram-S7CK7UJ4-B_-1Mf_7.js} +1 -1
  23. package/dist/assets/{diagram-UQ7AKVKN-Bb_Evkrm.js → diagram-UQ7AKVKN-Bs-OB8xI.js} +1 -1
  24. package/dist/assets/{diagram-VSXAHHWV-BzgupjxD.js → diagram-VSXAHHWV-BpUqq93o.js} +1 -1
  25. package/dist/assets/{diagram-VX7I27RA-D1c67z76.js → diagram-VX7I27RA-CZ6hSUo6.js} +1 -1
  26. package/dist/assets/{diagram-Z3DM3KII-DsJAJGKE.js → diagram-Z3DM3KII-DVZiDO6S.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-PWID7BFC-B1ge_obq.js → ebnfDiagram-PWID7BFC-D_1izoKK.js} +1 -1
  28. package/dist/assets/{erDiagram-RLTQ6QDP-DrV-tggw.js → erDiagram-RLTQ6QDP-_jSi4sg-.js} +1 -1
  29. package/dist/assets/{flowDiagram-HODETNUW-CVzDiSQ3.js → flowDiagram-HODETNUW-BtBM7ez3.js} +1 -1
  30. package/dist/assets/{ganttDiagram-EL5Y4UJY-CGRdSN9D.js → ganttDiagram-EL5Y4UJY-Bz2nxw7c.js} +1 -1
  31. package/dist/assets/{gitGraphDiagram-WWUBYQGX-CAfvFXzL.js → gitGraphDiagram-WWUBYQGX-8Q3I3hoE.js} +1 -1
  32. package/dist/assets/{index-DaxH81ZJ.js → index-CZUj0Tlu.js} +152 -152
  33. package/dist/assets/index-DcfdIMj-.css +1 -0
  34. package/dist/assets/{infoDiagram-27XIBGKW-DTshEkGK.js → infoDiagram-27XIBGKW-Cc6eKjfU.js} +1 -1
  35. package/dist/assets/{ishikawaDiagram-5VMMS53U-DkbshVxp.js → ishikawaDiagram-5VMMS53U-Bd3w97qM.js} +1 -1
  36. package/dist/assets/{journeyDiagram-3NMN7TZE-v5qE8xpn.js → journeyDiagram-3NMN7TZE-Cy6XbK5X.js} +1 -1
  37. package/dist/assets/{kanban-definition-UXKFOSKX-a9CNMFLt.js → kanban-definition-UXKFOSKX-kyLqWDpr.js} +1 -1
  38. package/dist/assets/{layout-DrW4R1TH.js → layout-DXpPSEft.js} +1 -1
  39. package/dist/assets/{linear-Cs3qpXT_.js → linear-DH_h9cDZ.js} +1 -1
  40. package/dist/assets/{mermaid.core-C7Ti-Fty.js → mermaid.core-BghXiB7p.js} +6 -6
  41. package/dist/assets/{mindmap-definition-YA3MSWOX-B0VDdIcG.js → mindmap-definition-YA3MSWOX-C2xxEXsP.js} +1 -1
  42. package/dist/assets/{pegDiagram-XKGWAZYB-CV1L7MjP.js → pegDiagram-XKGWAZYB-DuppHX4-.js} +1 -1
  43. package/dist/assets/{pieDiagram-E7YTZNPT-DSCuzVTV.js → pieDiagram-E7YTZNPT-BCukPZ2a.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-AXDQQJYC-hzX-9AKo.js → quadrantDiagram-AXDQQJYC-Dm_ojY_7.js} +1 -1
  45. package/dist/assets/{railroadDiagram-O6MQD6OU-BbmzIqcp.js → railroadDiagram-O6MQD6OU-BzdOv7BJ.js} +1 -1
  46. package/dist/assets/{requirementDiagram-BXWQKSXE-CQCRRBSL.js → requirementDiagram-BXWQKSXE-DgewPP_g.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-P5KCCOFB-kiaLcW6X.js → sankeyDiagram-P5KCCOFB-vEIMvCSt.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bgx8ST7Q.js → sequenceDiagram-WJ2MYXX4-BL_31NoY.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-BTjJ-ygN.js → sizeCapture-INFHLROL-Ch6u4xo6.js} +1 -1
  50. package/dist/assets/{stateDiagram-D77RDMKH-D48RR1FL.js → stateDiagram-D77RDMKH-ByquMuJv.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-MP3YSRHH-B-XwSR-z.js → stateDiagram-v2-MP3YSRHH-_QRJP_vn.js} +1 -1
  52. package/dist/assets/{swimlanes-42K2YHIH-BhE-QuEZ.js → swimlanes-42K2YHIH-BZyXEHOb.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-VR7AAH4N-sqZwZwbP.js +8 -0
  54. package/dist/assets/{timeline-definition-24CTP7MA-Cm7EUWWt.js → timeline-definition-24CTP7MA-CLBivayK.js} +1 -1
  55. package/dist/assets/{vennDiagram-4TSXK5OY-X-nC9wZ0.js → vennDiagram-4TSXK5OY-3dJ2PRqR.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VM6X3IG4-D33YvEQZ.js → wardleyDiagram-VM6X3IG4-C279LT36.js} +1 -1
  57. package/dist/assets/{xychartDiagram-S5SC5T6Z-lziwBKsM.js → xychartDiagram-S5SC5T6Z-FoqPDrei.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/sw.js +1 -1
  60. package/dist-server/server/index.js +3 -1
  61. package/dist-server/server/index.js.map +1 -1
  62. package/dist-server/server/piecemaker/harness/citations.routes.js +18 -1
  63. package/dist-server/server/piecemaker/harness/citations.routes.js.map +1 -1
  64. package/dist-server/server/piecemaker/harness/legifrance-html.js +112 -0
  65. package/dist-server/server/piecemaker/harness/legifrance-html.js.map +1 -0
  66. package/dist-server/server/piecemaker/index.js +7 -3
  67. package/dist-server/server/piecemaker/index.js.map +1 -1
  68. package/dist-server/server/piecemaker/knowledge/index.js +13 -6
  69. package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
  70. package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
  71. package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
  72. package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
  73. package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
  74. package/dist-server/server/piecemaker/library/index.js +14 -41
  75. package/dist-server/server/piecemaker/library/index.js.map +1 -1
  76. package/dist-server/server/piecemaker/library/store.js +11 -3
  77. package/dist-server/server/piecemaker/library/store.js.map +1 -1
  78. package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
  79. package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
  80. package/dist-server/server/piecemaker/timesheet/service.js +8 -7
  81. package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
  82. package/dist-server/server/piecemaker/timesheet/store.js +5 -3
  83. package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
  84. package/package.json +1 -1
  85. package/product.config.json +2 -0
  86. package/scripts/piecemaker/build-app-icon.py +88 -0
  87. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
  88. package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
  89. package/scripts/release/assert-piecemaker-version.mjs +25 -0
  90. package/server/index.ts +4 -1
  91. package/server/piecemaker/anonymizer/client-config.cjs +45 -162
  92. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
  93. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
  94. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
  95. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
  96. package/server/piecemaker/anonymizer/providers.cjs +14 -135
  97. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
  98. package/server/piecemaker/anonymizer/service.cjs +144 -28
  99. package/server/piecemaker/harness/citations.routes.ts +23 -1
  100. package/server/piecemaker/harness/legifrance-html.ts +107 -0
  101. package/server/piecemaker/index.ts +8 -3
  102. package/server/piecemaker/knowledge/index.ts +14 -6
  103. package/server/piecemaker/knowledge/local-routes.ts +72 -0
  104. package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
  105. package/server/piecemaker/library/index.ts +14 -34
  106. package/server/piecemaker/library/store.ts +11 -3
  107. package/server/piecemaker/library/workspace-installation.ts +20 -3
  108. package/server/piecemaker/timesheet/service.ts +9 -7
  109. package/server/piecemaker/timesheet/store.ts +5 -3
  110. package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
  111. package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
  112. package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
  113. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
  114. package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
  115. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
  116. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
  117. package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
  118. package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
  119. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
  120. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
  121. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
  122. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
  123. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
  124. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
  125. package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
  126. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
  127. package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
  128. package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
  129. package/dist/assets/channel-DdzrPZ6Y.js +0 -1
  130. package/dist/assets/classDiagram-ZZMXUADV-Dim2o8E5.js +0 -1
  131. package/dist/assets/classDiagram-v2-VYDZK3BY-Dim2o8E5.js +0 -1
  132. package/dist/assets/index-D_Dc_nQv.css +0 -1
  133. package/dist/assets/swimlanesDiagram-VR7AAH4N-DR525d5V.js +0 -8
  134. package/server/piecemaker/anonymizer/proxy.cjs +0 -365
  135. package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
  136. package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
  137. package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
  138. package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
  139. package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
  140. package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
  141. package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
@@ -1,30 +1,11 @@
1
1
  /**
2
- * Branchement des quatre clients IA de CloudCLI sur le proxy PII.
2
+ * Branchement des clients IA sur le proxy d'anonymisation.
3
3
  *
4
- * Chaque client a son propre levier, et un seul est universel : la variable
5
- * d'environnement du serveur, dont héritent les quatre chemins de lancement.
6
- * Elle ne suffit pourtant que là où le CLI honore une base d'URL, d'où un
7
- * second levier — le fichier de configuration utilisateur — pour Codex et
8
- * opencode, et un troisième pour Cursor, qui n'en honore aucun.
4
+ * Le serveur pose `HTTPS_PROXY` et le certificat de l'autorité locale.
5
+ * Les URL des fournisseurs ne sont pas réécrites.
9
6
  *
10
- * | client | levier |
11
- * |----------|----------------------------------------------------|
12
- * | claude | `ANTHROPIC_BASE_URL` + `~/.claude/settings.json` |
13
- * | codex | `~/.codex/config.toml` (bloc géré) |
14
- * | opencode | `provider.*.options.baseURL` de sa configuration |
15
- * | cursor | aucun — voir plus bas |
16
- *
17
- * **Cursor.** `cursor-agent` parle le protocole d'authentification de Cursor et
18
- * ignore toute base d'URL : il n'est pas interceptable. Le laisser tourner
19
- * enverrait des noms de parties en clair. À défaut de pouvoir le filtrer, on
20
- * l'empêche de partir : un exécutable `cursor-agent` posé en tête de `PATH`
21
- * refuse de s'exécuter tant qu'un mapping existe, et relaie le vrai binaire
22
- * sinon. C'est le seul fournisseur pour lequel la garantie se paie d'un refus,
23
- * et cela reste préférable à une fuite silencieuse.
24
- *
25
- * La configuration Claude et Codex est écrite par `./client-config.cjs`,
26
- * propre à ce dépôt : écriture atomique, détection de conflit et
27
- * réversibilité, sous l'identifiant de bloc `piecemaker_proxy`.
7
+ * Cursor ignore tout proxy HTTP. Tant qu'un mapping existe, un shim en tête
8
+ * de `PATH` refuse de lancer `cursor-agent`.
28
9
  */
29
10
  const fs = require('node:fs');
30
11
  const os = require('node:os');
@@ -37,110 +18,12 @@ const {
37
18
  bypassCodexProxy,
38
19
  } = require('./client-config.cjs');
39
20
 
40
- /** Une base que PieceMaker a écrite — donc que PieceMaker peut réécrire ou retirer. */
41
- function isOwnLoopbackUrl(value, suffix) {
42
- try {
43
- const url = new URL(String(value || ''));
44
- return ['127.0.0.1', 'localhost'].includes(url.hostname)
45
- && url.pathname.replace(/\/$/, '') === suffix;
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- function opencodeConfigFile(userHome) {
52
- if (process.env.OPENCODE_CONFIG) return process.env.OPENCODE_CONFIG;
53
- const base = process.env.XDG_CONFIG_HOME || path.join(userHome, '.config');
54
- return path.join(base, 'opencode', 'opencode.json');
55
- }
56
-
57
- function writeJsonAtomic(file, value) {
58
- fs.mkdirSync(path.dirname(file), { recursive: true });
59
- const temporary = `${file}.piecemaker-${process.pid}`;
60
- fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
61
- fs.renameSync(temporary, file);
62
- }
63
-
64
- /**
65
- * opencode lit `provider.<id>.options.baseURL`. On câble les deux familles que
66
- * le proxy sait router ; les autres fournisseurs éventuellement configurés
67
- * restent intacts, et donc hors couverture — le statut le dit.
68
- */
69
- const OPENCODE_ROUTES = [
70
- { id: 'anthropic', suffix: '/anthropic' },
71
- { id: 'openai', suffix: '/openai' },
72
- ];
73
-
74
- function configureOpencode({ origin, userHome }) {
75
- const file = opencodeConfigFile(userHome);
76
- let config = {};
77
- try {
78
- if (fs.existsSync(file)) {
79
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
80
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('objet JSON attendu');
81
- config = parsed;
82
- }
83
- } catch {
84
- return { configured: false, changed: false, conflict: true, file, reason: 'config-invalid' };
85
- }
86
-
87
- if (config.provider !== undefined
88
- && (!config.provider || typeof config.provider !== 'object' || Array.isArray(config.provider))) {
89
- return { configured: false, changed: false, conflict: true, file, reason: 'provider-invalid' };
90
- }
91
- if (!config.provider) config.provider = {};
92
-
93
- let changed = false;
94
- for (const { id, suffix } of OPENCODE_ROUTES) {
95
- const target = `${origin}${suffix}`;
96
- const entry = config.provider[id];
97
- if (entry !== undefined && (!entry || typeof entry !== 'object' || Array.isArray(entry))) {
98
- return { configured: false, changed: false, conflict: true, file, reason: `provider-${id}-invalid` };
99
- }
100
- const options = entry?.options;
101
- if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
102
- return { configured: false, changed: false, conflict: true, file, reason: `options-${id}-invalid` };
103
- }
104
- const existing = options?.baseURL;
105
- // Une base tierce délibérément posée par l'utilisateur n'est pas écrasée :
106
- // on signale le conflit plutôt que de casser sa configuration en silence.
107
- if (existing && existing !== target && !isOwnLoopbackUrl(existing, suffix)) {
108
- return { configured: false, changed: false, conflict: true, file, reason: `base-url-conflict:${id}` };
109
- }
110
- if (existing === target) continue;
111
- config.provider[id] = { ...entry, options: { ...options, baseURL: target } };
112
- changed = true;
113
- }
114
-
115
- if (changed) writeJsonAtomic(file, config);
116
- return { configured: true, changed, conflict: false, file };
21
+ function configureOpencode() {
22
+ return { configured: true, changed: false, conflict: false, file: null };
117
23
  }
118
24
 
119
- function bypassOpencode({ userHome }) {
120
- const file = opencodeConfigFile(userHome);
121
- if (!fs.existsSync(file)) return { bypassed: true, changed: false, conflict: false, file };
122
-
123
- let config;
124
- try {
125
- config = JSON.parse(fs.readFileSync(file, 'utf8'));
126
- if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('objet JSON attendu');
127
- } catch {
128
- return { bypassed: false, changed: false, conflict: true, file, reason: 'config-invalid' };
129
- }
130
-
131
- let changed = false;
132
- for (const { id, suffix } of OPENCODE_ROUTES) {
133
- const options = config.provider?.[id]?.options;
134
- if (!options || typeof options !== 'object' || Array.isArray(options)) continue;
135
- if (!isOwnLoopbackUrl(options.baseURL, suffix)) continue;
136
- delete options.baseURL;
137
- if (Object.keys(options).length === 0) delete config.provider[id].options;
138
- if (Object.keys(config.provider[id]).length === 0) delete config.provider[id];
139
- changed = true;
140
- }
141
- if (changed && config.provider && Object.keys(config.provider).length === 0) delete config.provider;
142
- if (changed) writeJsonAtomic(file, config);
143
- return { bypassed: true, changed, conflict: false, file };
25
+ function bypassOpencode() {
26
+ return { bypassed: true, changed: false, conflict: false, file: null };
144
27
  }
145
28
 
146
29
  /** Première ligne du garde-fou Cursor : ce que le shim écrit avant de renoncer. */
@@ -177,7 +60,7 @@ exec cursor-agent "$@"
177
60
  * dans le statut, ce qui est l'information utile, plutôt que d'empêcher le
178
61
  * serveur de démarrer.
179
62
  */
180
- async function configureProviders({ origin, userHome = os.homedir(), binDir, mappingFile }) {
63
+ async function configureProviders({ origin, caFile, userHome = os.homedir(), binDir, mappingFile }) {
181
64
  const report = {};
182
65
 
183
66
  const attempt = (name, run) => {
@@ -188,12 +71,11 @@ async function configureProviders({ origin, userHome = os.homedir(), binDir, map
188
71
  }
189
72
  };
190
73
 
191
- attempt('claude', () => configureClaudeCodeProxy({ baseUrl: `${origin}/anthropic`, userHome }));
74
+ attempt('claude', () => configureClaudeCodeProxy({ proxyUrl: origin, caFile, userHome }));
192
75
  attempt('codex', () => configureCodexProxy({
193
- baseUrl: `${origin}/chatgpt`,
194
76
  codexHome: process.env.CODEX_HOME || path.join(userHome, '.codex'),
195
77
  }));
196
- attempt('opencode', () => configureOpencode({ origin, userHome }));
78
+ attempt('opencode', () => configureOpencode());
197
79
  attempt('cursor', () => ({
198
80
  configured: false,
199
81
  changed: false,
@@ -218,8 +100,8 @@ async function bypassProviders({ userHome = os.homedir(), binDir }) {
218
100
  };
219
101
 
220
102
  attempt('claude', () => bypassClaudeCodeProxy({ userHome }));
221
- attempt('codex', () => bypassCodexProxy({ codexHome: process.env.CODEX_HOME || path.join(userHome, '.codex') }));
222
- attempt('opencode', () => bypassOpencode({ userHome }));
103
+ attempt('codex', () => bypassCodexProxy());
104
+ attempt('opencode', () => bypassOpencode());
223
105
  attempt('cursor', () => {
224
106
  const file = path.join(binDir, 'cursor-agent');
225
107
  if (fs.existsSync(file)) fs.rmSync(file);
@@ -230,12 +112,9 @@ async function bypassProviders({ userHome = os.homedir(), binDir }) {
230
112
 
231
113
  module.exports = {
232
114
  CURSOR_REFUSAL,
233
- OPENCODE_ROUTES,
234
115
  bypassOpencode,
235
116
  bypassProviders,
236
117
  configureOpencode,
237
118
  configureProviders,
238
119
  installCursorGuard,
239
- isOwnLoopbackUrl,
240
- opencodeConfigFile,
241
120
  };
@@ -0,0 +1,221 @@
1
+ const http = require('node:http');
2
+
3
+ const { anonymize, deanonymize } = require('./dictionary.cjs');
4
+ const { createSseRewriter, rewriteJsonBody } = require('./rewrite.cjs');
5
+ const { createCollecteurTexte } = require('../harness/flux-sse.cjs');
6
+
7
+ const MAX_BODY = 64 * 1024 * 1024;
8
+
9
+ function readBody(request) {
10
+ return new Promise((resolve, reject) => {
11
+ const chunks = [];
12
+ let size = 0;
13
+ request.on('data', (chunk) => {
14
+ size += chunk.length;
15
+ if (size > MAX_BODY) {
16
+ reject(new Error('corps trop volumineux'));
17
+ request.destroy();
18
+ return;
19
+ }
20
+ chunks.push(chunk);
21
+ });
22
+ request.on('end', () => resolve(Buffer.concat(chunks)));
23
+ request.on('error', reject);
24
+ });
25
+ }
26
+
27
+ function header(request, name) {
28
+ const value = request.headers[name];
29
+ return Array.isArray(value) ? value[0] : value || '';
30
+ }
31
+
32
+ function observeRequest(harness, body, contentType, provider) {
33
+ if (!harness) return;
34
+ try {
35
+ harness.observerRequete(body, contentType, { session: provider });
36
+ } catch {
37
+ // une observation ne doit pas empêcher le relais
38
+ }
39
+ }
40
+
41
+ function observeResponse(harness, text, provider) {
42
+ if (!harness) return;
43
+ try {
44
+ Promise.resolve(harness.observerReponse(text, { session: provider })).catch(() => {});
45
+ } catch {
46
+ // une observation ne doit pas empêcher le relais
47
+ }
48
+ }
49
+
50
+ function assistantText(raw) {
51
+ try {
52
+ const parsed = JSON.parse(raw.toString('utf8'));
53
+ if (Array.isArray(parsed?.content)) {
54
+ return parsed.content
55
+ .filter((block) => block?.type === 'text' && typeof block.text === 'string')
56
+ .map((block) => block.text)
57
+ .join('');
58
+ }
59
+ if (Array.isArray(parsed?.output)) {
60
+ return parsed.output
61
+ .flatMap((item) => (Array.isArray(item?.content) ? item.content : []))
62
+ .filter((block) => block?.type === 'output_text' && typeof block.text === 'string')
63
+ .map((block) => block.text)
64
+ .join('');
65
+ }
66
+ } catch {
67
+ return '';
68
+ }
69
+ return '';
70
+ }
71
+
72
+ function ssePayloads(tail) {
73
+ return String(tail || '')
74
+ .split('\n')
75
+ .filter((line) => line.startsWith('data: '))
76
+ .map((line) => line.slice(6))
77
+ .filter((line) => line && line !== '[DONE]');
78
+ }
79
+
80
+ function startRewriterBridge({ dictionary, harness = null }) {
81
+ const sessions = new Map();
82
+ const stats = { requests: 0, anonymized: 0, deanonymized: 0, failures: 0, lastError: null };
83
+
84
+ const server = http.createServer((request, response) => {
85
+ const provider = header(request, 'x-piecemaker-provider') || 'claude';
86
+ const current = () => dictionary.get();
87
+
88
+ if (request.method === 'POST' && request.url === '/v1/request') {
89
+ readBody(request).then((body) => {
90
+ stats.requests += 1;
91
+ const contentType = header(request, 'content-type') || 'application/json';
92
+ observeRequest(harness, body, contentType, provider);
93
+ const dict = current();
94
+ const rewritten = !body.length || dict.empty
95
+ ? body
96
+ : Buffer.from(rewriteJsonBody(body, (text) => anonymize(text, dict)), 'utf8');
97
+ if (!rewritten.equals(body)) stats.anonymized += 1;
98
+ response.writeHead(200, { 'content-type': 'application/octet-stream', 'content-length': rewritten.length });
99
+ response.end(rewritten);
100
+ }).catch((error) => {
101
+ stats.failures += 1;
102
+ stats.lastError = error.message;
103
+ response.writeHead(500);
104
+ response.end();
105
+ });
106
+ return;
107
+ }
108
+
109
+ if (request.method === 'POST' && request.url === '/v1/response') {
110
+ readBody(request).then((body) => {
111
+ const dict = current();
112
+ const rewritten = !body.length || dict.empty
113
+ ? body
114
+ : Buffer.from(rewriteJsonBody(body, (text) => deanonymize(text, dict)), 'utf8');
115
+ if (!rewritten.equals(body)) stats.deanonymized += 1;
116
+ observeResponse(harness, assistantText(rewritten), provider);
117
+ response.writeHead(200, { 'content-type': 'application/octet-stream', 'content-length': rewritten.length });
118
+ response.end(rewritten);
119
+ }).catch((error) => {
120
+ stats.failures += 1;
121
+ stats.lastError = error.message;
122
+ response.writeHead(500);
123
+ response.end();
124
+ });
125
+ return;
126
+ }
127
+
128
+ if (request.method === 'POST' && request.url === '/v1/sse') {
129
+ const dict = current();
130
+ const rewriter = createSseRewriter((text) => deanonymize(text, dict));
131
+ const collecteur = createCollecteurTexte();
132
+ response.writeHead(200, {
133
+ 'content-type': 'text/event-stream',
134
+ 'cache-control': 'no-cache',
135
+ });
136
+ response.flushHeaders();
137
+ request.setEncoding('utf8');
138
+ request.on('data', (chunk) => {
139
+ const out = rewriter.push(chunk);
140
+ if (!out) return;
141
+ response.write(out);
142
+ collecteur.push(out);
143
+ });
144
+ request.on('end', () => {
145
+ const tail = rewriter.end();
146
+ if (tail) {
147
+ response.write(tail);
148
+ collecteur.push(tail);
149
+ }
150
+ stats.deanonymized += 1;
151
+ response.end();
152
+ observeResponse(harness, collecteur.texte(), provider);
153
+ });
154
+ request.on('error', () => response.end());
155
+ return;
156
+ }
157
+
158
+ if (request.method === 'POST' && request.url === '/v1/ws') {
159
+ const id = header(request, 'x-piecemaker-ws');
160
+ const direction = header(request, 'x-piecemaker-direction');
161
+ readBody(request).then((body) => {
162
+ const dict = current();
163
+ const text = body.toString('utf8');
164
+ if (direction === 'out') {
165
+ observeRequest(harness, body, 'application/json', provider);
166
+ const rewritten = dict.empty ? text : rewriteJsonBody(text, (value) => anonymize(value, dict));
167
+ response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
168
+ response.end(rewritten);
169
+ return;
170
+ }
171
+ let entry = sessions.get(id);
172
+ if (!entry) {
173
+ entry = { rewriter: createSseRewriter((value) => deanonymize(value, dict)) };
174
+ sessions.set(id, entry);
175
+ }
176
+ if (!text.includes('\n')) {
177
+ const payloads = ssePayloads(entry.rewriter.push(`data: ${text}\n\n`));
178
+ response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
179
+ response.end(payloads.join('\n'));
180
+ return;
181
+ }
182
+ const rewritten = dict.empty ? text : rewriteJsonBody(text, (value) => deanonymize(value, dict));
183
+ response.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
184
+ response.end(rewritten);
185
+ }).catch(() => {
186
+ response.writeHead(500);
187
+ response.end();
188
+ });
189
+ return;
190
+ }
191
+
192
+ if (request.method === 'POST' && request.url === '/v1/ws-end') {
193
+ const id = header(request, 'x-piecemaker-ws');
194
+ const entry = sessions.get(id);
195
+ sessions.delete(id);
196
+ const messages = entry ? ssePayloads(entry.rewriter.end()) : [];
197
+ response.writeHead(200, { 'content-type': 'application/json' });
198
+ response.end(JSON.stringify({ messages }));
199
+ return;
200
+ }
201
+
202
+ response.writeHead(404);
203
+ response.end();
204
+ });
205
+
206
+ return new Promise((resolve, reject) => {
207
+ server.once('error', reject);
208
+ server.listen(0, '127.0.0.1', () => {
209
+ const { port } = server.address();
210
+ resolve({
211
+ port,
212
+ stats,
213
+ close() {
214
+ return new Promise((done) => server.close(() => done()));
215
+ },
216
+ });
217
+ });
218
+ });
219
+ }
220
+
221
+ module.exports = { startRewriterBridge };
@@ -15,17 +15,24 @@
15
15
  * Le proxy vit et meurt avec le serveur : à l'arrêt, les bases écrites sont
16
16
  * retirées pour ne pas laisser les clients pointer un port fermé.
17
17
  */
18
+ const { spawn } = require('node:child_process');
18
19
  const fs = require('node:fs');
19
20
  const os = require('node:os');
20
21
  const path = require('node:path');
21
22
 
22
23
  const { createSqliteDictionaryLoader } = require('./sqlite-dictionary.cjs');
23
- const { DEFAULT_UPSTREAM, createAnonymizerProxy } = require('./proxy.cjs');
24
+ const { startRewriterBridge } = require('./rewriter-bridge.cjs');
24
25
  const { bypassProviders, configureProviders } = require('./providers.cjs');
25
26
  const { createHarnessJuridique } = require('../harness/index.cjs');
26
27
 
27
28
  const ENV_VAR = 'ANTHROPIC_BASE_URL';
28
29
  const OPENAI_ENV_VAR = 'OPENAI_BASE_URL';
30
+ const DEFAULT_UPSTREAM = 'https://api.anthropic.com';
31
+ const DEFAULT_PORT = 4111;
32
+ const PROXY_ENV_KEYS = [
33
+ 'HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY', 'NO_PROXY', 'NODE_USE_ENV_PROXY',
34
+ 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'CODEX_CA_CERTIFICATE', 'REQUESTS_CA_BUNDLE',
35
+ ];
29
36
 
30
37
  /** Interrupteur : `PIECEMAKER_ANONYMIZER=off`, ou `anonymizer.enabled: false` dans `config.json`. */
31
38
  function isDisabled(homeDir) {
@@ -76,6 +83,66 @@ function summarizeCoverage(report) {
76
83
  return coverage;
77
84
  }
78
85
 
86
+ function hudsuckerBinary() {
87
+ if (process.env.PIECEMAKER_HUDSUCKER_BIN) return process.env.PIECEMAKER_HUDSUCKER_BIN;
88
+ const name = process.platform === 'win32' ? 'piecemaker-hudsucker.exe' : 'piecemaker-hudsucker';
89
+ const packaged = path.join(__dirname, 'bin', name);
90
+ const release = path.join(__dirname, 'hudsucker-proxy', 'target', 'release', name);
91
+ const debug = path.join(__dirname, 'hudsucker-proxy', 'target', 'debug', name);
92
+ if (fs.existsSync(packaged)) return packaged;
93
+ if (fs.existsSync(release)) return release;
94
+ if (fs.existsSync(debug)) return debug;
95
+ return packaged;
96
+ }
97
+
98
+ function certificatePaths() {
99
+ const directory = path.join(os.homedir(), '.piecemaker', 'certs');
100
+ return {
101
+ cert: path.join(directory, 'piecemaker-ca.crt'),
102
+ key: path.join(directory, 'piecemaker-ca.key'),
103
+ };
104
+ }
105
+
106
+ function waitForOutput(child, needle, timeoutMs) {
107
+ return new Promise((resolve, reject) => {
108
+ let buffer = '';
109
+ let settled = false;
110
+ const finish = (failed, value) => {
111
+ if (settled) return;
112
+ settled = true;
113
+ clearTimeout(timer);
114
+ child.stdout?.off('data', take);
115
+ child.stderr?.off('data', take);
116
+ child.off('exit', onExit);
117
+ if (failed) reject(value);
118
+ else resolve(value);
119
+ };
120
+ const timer = setTimeout(() => finish(true, new Error(`hudsucker n'a pas écouté : ${buffer}`)), timeoutMs);
121
+ const take = (chunk) => {
122
+ buffer += chunk.toString();
123
+ if (buffer.includes(needle)) finish(false, buffer);
124
+ };
125
+ const onExit = (code) => finish(true, new Error(`hudsucker sorti (${code}) : ${buffer}`));
126
+ child.stdout?.on('data', take);
127
+ child.stderr?.on('data', take);
128
+ child.once('exit', onExit);
129
+ });
130
+ }
131
+
132
+ function proxyEnvironment(proxyUrl, caFile) {
133
+ return {
134
+ HTTPS_PROXY: proxyUrl,
135
+ HTTP_PROXY: proxyUrl,
136
+ ALL_PROXY: proxyUrl,
137
+ NO_PROXY: 'localhost,127.0.0.1,::1',
138
+ NODE_USE_ENV_PROXY: '1',
139
+ NODE_EXTRA_CA_CERTS: caFile,
140
+ SSL_CERT_FILE: caFile,
141
+ CODEX_CA_CERTIFICATE: caFile,
142
+ REQUESTS_CA_BUNDLE: caFile,
143
+ };
144
+ }
145
+
79
146
  function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = console, required = false }) {
80
147
  const dictionary = createSqliteDictionaryLoader({ databasePath: process.env.DATABASE_PATH || path.join(homeDir, 'auth.db') });
81
148
  const legacyMappingFile = path.join(homeDir, 'central-mapping.json');
@@ -96,6 +163,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
96
163
  coverage: {},
97
164
  };
98
165
  let proxy = null;
166
+ let bridge = null;
99
167
  let previousEnv = null;
100
168
 
101
169
  async function start() {
@@ -105,34 +173,68 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
105
173
  return state;
106
174
  }
107
175
 
176
+ const binary = hudsuckerBinary();
177
+ const certificates = certificatePaths();
178
+ if (!fs.existsSync(binary) || !fs.existsSync(certificates.cert) || !fs.existsSync(certificates.key)) {
179
+ state.reason = 'binary_or_ca_missing';
180
+ logger.warn?.('[piecemaker] hudsucker ou autorité locale absent');
181
+ return state;
182
+ }
183
+
108
184
  const claudeUpstream = resolveUpstream(process.env[ENV_VAR]);
109
- proxy = createAnonymizerProxy({
110
- dictionary,
111
- routes: [
112
- { provider: 'claude', prefix: '/anthropic', upstream: claudeUpstream },
113
- { provider: 'codex', prefix: '/chatgpt', upstream: 'https://chatgpt.com/backend-api/codex' },
114
- { provider: 'opencode', prefix: '/openai', upstream: 'https://api.openai.com' },
115
- ],
116
- harness,
117
- onError: (error) => logger.warn?.(`[piecemaker] proxy PII : ${error.message}`),
118
- });
185
+ const hosts = ['api.anthropic.com', 'api.openai.com', 'chatgpt.com'];
186
+ try {
187
+ const extra = new URL(claudeUpstream).hostname;
188
+ if (extra && !hosts.includes(extra)) hosts.push(extra);
189
+ } catch {
190
+ // l'amont par défaut reste la liste fixe
191
+ }
192
+
193
+ try {
194
+ bridge = await startRewriterBridge({ dictionary, harness });
195
+ } catch (error) {
196
+ state.reason = `listen_failed: ${error.message}`;
197
+ logger.warn?.(`[piecemaker] réécriture PII non démarrée : ${error.message}`);
198
+ return state;
199
+ }
119
200
 
120
- let origin;
121
- let listenedPort;
201
+ const listenedPort = DEFAULT_PORT;
202
+ const origin = `http://127.0.0.1:${listenedPort}`;
203
+ const child = spawn(binary, [
204
+ '--listen', `127.0.0.1:${listenedPort}`,
205
+ '--rewriter', `http://127.0.0.1:${bridge.port}`,
206
+ '--ca-cert', certificates.cert,
207
+ '--ca-key', certificates.key,
208
+ '--hosts', hosts.join(','),
209
+ ], {
210
+ stdio: ['ignore', 'pipe', 'pipe'],
211
+ env: {
212
+ ...process.env,
213
+ HTTPS_PROXY: '',
214
+ HTTP_PROXY: '',
215
+ ALL_PROXY: '',
216
+ https_proxy: '',
217
+ http_proxy: '',
218
+ all_proxy: '',
219
+ },
220
+ });
221
+ proxy = child;
222
+ child.stdout?.on('data', (chunk) => logger.log?.(String(chunk).trim()));
223
+ child.stderr?.on('data', (chunk) => logger.warn?.(`[piecemaker] hudsucker : ${String(chunk).trim()}`));
122
224
  try {
123
- ({ origin, port: listenedPort } = await proxy.listen());
225
+ await waitForOutput(child, `listening 127.0.0.1:${listenedPort}`, 15000);
124
226
  } catch (error) {
227
+ child.kill('SIGKILL');
125
228
  proxy = null;
229
+ await bridge.close();
230
+ bridge = null;
126
231
  state.reason = `listen_failed: ${error.message}`;
127
232
  logger.warn?.(`[piecemaker] proxy PII non démarré : ${error.message}`);
128
233
  return state;
129
234
  }
130
235
 
131
- // Publie le port réellement obtenu (le scan de proxy.cjs peut dévier du
132
- // port préféré si une instance précédente n'a pas encore libéré le sien)
133
- // dans la config partagée, pour que d'autres consommateurs indépendants
134
- // du process serveur (ex. le sous-service Docker "mike") puissent le
135
- // retrouver sans le redéduire eux-mêmes ni le coder en dur.
236
+ // Publie le port dans la config partagée pour que les consommateurs
237
+ // indépendants du process serveur puissent le retrouver.
136
238
  try {
137
239
  const configFile = path.join(homeDir, 'config.json');
138
240
  const current = JSON.parse(fs.readFileSync(configFile, 'utf8'));
@@ -148,19 +250,24 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
148
250
  [OPENAI_ENV_VAR]: process.env[OPENAI_ENV_VAR],
149
251
  PATH: process.env.PATH,
150
252
  };
151
- process.env[ENV_VAR] = `${origin}/anthropic`;
152
- process.env[OPENAI_ENV_VAR] = `${origin}/openai`;
153
- // En tête de chemin : le shim doit être trouvé avant le vrai `cursor-agent`.
253
+ for (const key of PROXY_ENV_KEYS) previousEnv[key] = process.env[key];
254
+ Object.assign(process.env, proxyEnvironment(origin, certificates.cert));
154
255
  process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH || ''}`;
155
256
 
156
- const report = await configureProviders({ origin, userHome, binDir, mappingFile: legacyMappingFile });
257
+ const report = await configureProviders({
258
+ origin,
259
+ caFile: certificates.cert,
260
+ userHome,
261
+ binDir,
262
+ mappingFile: legacyMappingFile,
263
+ });
157
264
  Object.assign(state, {
158
265
  enabled: true,
159
266
  reason: 'running',
160
267
  origin,
161
268
  upstream: claudeUpstream,
162
269
  startedAt: new Date().toISOString(),
163
- routes: proxy.routes,
270
+ routes: hosts.map((host) => ({ provider: host, upstream: `https://${host}` })),
164
271
  coverage: summarizeCoverage(report),
165
272
  });
166
273
 
@@ -175,11 +282,20 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
175
282
  }
176
283
 
177
284
  async function stop() {
178
- if (!proxy) {
285
+ if (!proxy && !bridge) {
179
286
  dictionary.close();
180
287
  return;
181
288
  }
182
- await proxy.close();
289
+ if (proxy && proxy.exitCode === null) {
290
+ proxy.kill('SIGTERM');
291
+ await Promise.race([
292
+ new Promise((resolve) => proxy.once('exit', resolve)),
293
+ new Promise((resolve) => setTimeout(resolve, 2000)),
294
+ ]);
295
+ if (proxy.exitCode === null) proxy.kill('SIGKILL');
296
+ }
297
+ if (bridge) await bridge.close();
298
+ bridge = null;
183
299
  await bypassProviders({ userHome, binDir });
184
300
  if (previousEnv) {
185
301
  for (const [key, value] of Object.entries(previousEnv)) {
@@ -197,7 +313,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
197
313
  const current = dictionary.get();
198
314
  return {
199
315
  ...state,
200
- listening: Boolean(proxy?.listening),
316
+ listening: Boolean(proxy && proxy.exitCode === null && !proxy.killed),
201
317
  dictionary: {
202
318
  file: dictionary.file,
203
319
  exists: dictionary.exists(),
@@ -206,7 +322,7 @@ function createAnonymizerService({ homeDir, userHome = os.homedir(), logger = co
206
322
  updatedAt: current.updatedAt,
207
323
  empty: current.empty,
208
324
  },
209
- stats: proxy ? { ...proxy.stats } : null,
325
+ stats: bridge ? { ...bridge.stats } : null,
210
326
  };
211
327
  }
212
328