@piecemaker-legal/piecemaker 1.1.0 → 2.0.1
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/{abnfDiagram-VCTEODGH-C-dTfD4s.js → abnfDiagram-VCTEODGH-C2JDmlZx.js} +1 -1
- package/dist/assets/{arc-BwDrelvt.js → arc-XhSg5Mrj.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-CuViaxCV.js → architectureDiagram-5GKGNRK7-DKREuXxk.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-CODUNfgC.js → blockDiagram-I7D4REHJ-CNK0Tp5d.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-CiQtyYDd.js → c4Diagram-7LVT6UL2-C8fJzWVp.js} +1 -1
- package/dist/assets/channel-DO5hoFdU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-DZjjPgAs.js → chunk-2Q5K7J3B-DlE8nLJo.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-C-SPnj5I.js → chunk-5VM5RSS4-CSm-2TZ4.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-D5b0-fye.js → chunk-F27PBJKO-CSgnXT2c.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-CWflUKd3.js → chunk-IMKFNOWR-C6bkt6C1.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DFltof8Z.js → chunk-JWPE2WC7-jUR52MBO.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-moIw94hw.js → chunk-POPQ4Y6H-D0UAL7O7.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG--H3pige-.js → chunk-SVP7TREG-C9dtOEii.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-BfujErBt.js → chunk-TICWLB2K-BEzWKT39.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-C5jFgw8p.js → chunk-XXDRQBXY-uD1oklVY.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-BsQtwznI.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BsQtwznI.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-CsT3oVRI.js → cose-bilkent-JH36ORCC-aGjlSdMe.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-gdpEHNLr.js → cynefin-OW5HDTMX-D3G4OYOn.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-v-JmI3Zj.js → cynefinDiagram-5FMLGOSQ-9swRDUnu.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-BvJ_SGF3.js → dagre-GXQ25YYZ-lvaAGdqY.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-B7EHfyrx.js → diagram-S7CK7UJ4-BaZ6YIkR.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-DX5RvUaF.js → diagram-UQ7AKVKN-COJ2Gb38.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-D_PJ-E3G.js → diagram-VSXAHHWV-CYZEkbzP.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-CAtayL03.js → diagram-VX7I27RA-fFeQJ4ae.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BhCCqDLU.js → diagram-Z3DM3KII-B7knszDV.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-BCb926kC.js → ebnfDiagram-PWID7BFC-Dk0l329Y.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-5Fw1sblj.js → erDiagram-RLTQ6QDP-CtpYxui8.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-DDUdOLnK.js → flowDiagram-HODETNUW-aVWueRJD.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-B4IG1idB.js → ganttDiagram-EL5Y4UJY-DfrX-B7p.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-S6wvUCsC.js → gitGraphDiagram-WWUBYQGX-BFB1ZPF3.js} +1 -1
- package/dist/assets/{index-CcC0QCGH.js → index-DqRygn9E.js} +170 -169
- package/dist/assets/index-Ds4HaAjz.css +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-BNv6fEro.js → infoDiagram-27XIBGKW-CuYkzm43.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BJZwt-TZ.js → ishikawaDiagram-5VMMS53U-EMiw8Akh.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-BAGhL9Uv.js → journeyDiagram-3NMN7TZE-DCoLV1f8.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-DmoLE6ku.js → kanban-definition-UXKFOSKX-CyA4Yinu.js} +1 -1
- package/dist/assets/{layout-ALipSwVQ.js → layout-BlaB0grA.js} +1 -1
- package/dist/assets/{linear-BJkOgP0g.js → linear-DLX3dJDH.js} +1 -1
- package/dist/assets/{mermaid.core-C20iPvcV.js → mermaid.core-Cy51CT89.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-CSI558Pw.js → mindmap-definition-YA3MSWOX-OBXjfL7I.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-DFtdseIb.js → pegDiagram-XKGWAZYB-C_BJjUh1.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-ByPKZBBy.js → pieDiagram-E7YTZNPT-jIpbr3Fi.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-tH7z7eR2.js → quadrantDiagram-AXDQQJYC-C1A15xI5.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-D7ZdvS0P.js → railroadDiagram-O6MQD6OU-Rslzn1pI.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-nSTqp6wv.js → requirementDiagram-BXWQKSXE-LlfURO5-.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-Bg3ivRCq.js → sankeyDiagram-P5KCCOFB-B5BPF4-O.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-BFs4BVKR.js → sequenceDiagram-WJ2MYXX4-orYZe9zX.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DuSN3zyt.js → sizeCapture-INFHLROL-Co-K5Cfb.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH---hK4wFR.js → stateDiagram-D77RDMKH-COEx1aH2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-DxBEemHS.js → stateDiagram-v2-MP3YSRHH-DaeTZvmy.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-C6RXO1Rv.js → swimlanes-42K2YHIH-BHQaDhfZ.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BHW8q5M5.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-Ci0Toxgn.js → timeline-definition-24CTP7MA-COqCRwGX.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-CKF_VmKX.js → vennDiagram-4TSXK5OY-nym5XSnr.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-Xao4cR_d.js → wardleyDiagram-VM6X3IG4-DEPPey_W.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-BAZUt04h.js → xychartDiagram-S5SC5T6Z-uSJ404kL.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-server/server/index.js +3 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/cli/desktop-shortcut.js +80 -0
- package/dist-server/server/modules/cli/desktop-shortcut.js.map +1 -0
- package/dist-server/server/modules/cli/pwa-launcher.js +27 -4
- package/dist-server/server/modules/cli/pwa-launcher.js.map +1 -1
- package/dist-server/server/piecemaker/index.js +7 -3
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/index.js +13 -6
- package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
- package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
- package/dist-server/server/piecemaker/library/index.js +14 -41
- package/dist-server/server/piecemaker/library/index.js.map +1 -1
- package/dist-server/server/piecemaker/library/store.js +11 -3
- package/dist-server/server/piecemaker/library/store.js.map +1 -1
- package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
- package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/service.js +8 -7
- package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/store.js +5 -3
- package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
- package/package.json +1 -1
- package/product.config.json +3 -0
- package/scripts/piecemaker/build-app-icon.py +88 -0
- package/scripts/piecemaker/cli/lib/app-window.mjs +47 -0
- package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
- package/scripts/piecemaker/cli/lib/pwa.mjs +10 -13
- package/scripts/piecemaker/cli/piecemaker.mjs +15 -1
- package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
- package/scripts/release/assert-piecemaker-version.mjs +25 -0
- package/server/index.ts +4 -1
- package/server/modules/cli/desktop-shortcut.ts +89 -0
- package/server/modules/cli/pwa-launcher.ts +29 -4
- package/server/piecemaker/anonymizer/client-config.cjs +45 -162
- package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
- package/server/piecemaker/anonymizer/providers.cjs +14 -135
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
- package/server/piecemaker/anonymizer/service.cjs +144 -28
- package/server/piecemaker/index.ts +8 -3
- package/server/piecemaker/knowledge/index.ts +14 -6
- package/server/piecemaker/knowledge/local-routes.ts +72 -0
- package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
- package/server/piecemaker/library/index.ts +14 -34
- package/server/piecemaker/library/store.ts +11 -3
- package/server/piecemaker/library/workspace-installation.ts +20 -3
- package/server/piecemaker/timesheet/service.ts +9 -7
- package/server/piecemaker/timesheet/store.ts +5 -3
- package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
- package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
- package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
- package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
- package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
- package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
- package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
- package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
- package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
- package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
- package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
- package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
- package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
- package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
- package/dist/assets/channel-DIjrikYC.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-Yagpznfi.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-Yagpznfi.js +0 -1
- package/dist/assets/index-D_Dc_nQv.css +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BC3fwmoQ.js +0 -8
- package/server/piecemaker/anonymizer/proxy.cjs +0 -365
- package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
- package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
- package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
- package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
- package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
- package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
- package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Proxy PII local — le seul point de passage entre les clients IA lancés par
|
|
3
|
-
* CloudCLI (chat comme terminal) et les API des fournisseurs.
|
|
4
|
-
*
|
|
5
|
-
* Sans Python, sans venv, sans service de session à installer : il vit dans
|
|
6
|
-
* le cycle de vie du serveur CloudCLI plutôt qu'à côté.
|
|
7
|
-
*
|
|
8
|
-
* Sens sortant : nom réel → code. Aucun nom de partie ne quitte la machine.
|
|
9
|
-
* Sens entrant : code → nom réel. Claude Code retrouve un texte lisible, ses
|
|
10
|
-
* outils continuent d'ouvrir les fichiers sous leur vrai nom, et
|
|
11
|
-
* l'interface n'a plus qu'à surligner ce qui a été protégé.
|
|
12
|
-
*
|
|
13
|
-
* Sans mapping, le proxy est un relais transparent : le brancher ne peut pas
|
|
14
|
-
* casser une installation qui n'a encore anonymisé aucun dossier.
|
|
15
|
-
*
|
|
16
|
-
* Une route par fournisseur, distinguée par le préfixe d'URL. Un chemin qui ne
|
|
17
|
-
* correspond à aucune route est refusé : router par défaut reviendrait à
|
|
18
|
-
* laisser passer non filtré ce que l'on n'a pas su reconnaître.
|
|
19
|
-
*/
|
|
20
|
-
const http = require('node:http');
|
|
21
|
-
const https = require('node:https');
|
|
22
|
-
|
|
23
|
-
const { anonymize, deanonymize } = require('./dictionary.cjs');
|
|
24
|
-
const { createSseRewriter, rewriteJsonBody } = require('./rewrite.cjs');
|
|
25
|
-
const { createCollecteurTexte } = require('../harness/flux-sse.cjs');
|
|
26
|
-
|
|
27
|
-
const DEFAULT_UPSTREAM = 'https://api.anthropic.com';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Routes servies par défaut. `/chatgpt` vise le point d'entrée Codex de
|
|
31
|
-
* ChatGPT, où le CLI parle le protocole Responses avec l'authentification
|
|
32
|
-
* ChatGPT.
|
|
33
|
-
*/
|
|
34
|
-
const DEFAULT_ROUTES = [
|
|
35
|
-
{ provider: 'claude', prefix: '/anthropic', upstream: 'https://api.anthropic.com' },
|
|
36
|
-
{ provider: 'codex', prefix: '/chatgpt', upstream: 'https://chatgpt.com/backend-api/codex' },
|
|
37
|
-
{ provider: 'opencode', prefix: '/openai', upstream: 'https://api.openai.com' },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
/** Port fixe : une base écrite dans un fichier de configuration doit survivre à un redémarrage. */
|
|
41
|
-
const DEFAULT_PORT = 4111;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* En-têtes que le proxy possède et ne relaie donc jamais tels quels. Le corps
|
|
45
|
-
* change de taille à la réécriture (`content-length`), et `accept-encoding` est
|
|
46
|
-
* retiré pour recevoir du texte clair : compresser en amont rendrait la
|
|
47
|
-
* dé-anonymisation impossible sans décompresser tout le flux.
|
|
48
|
-
*/
|
|
49
|
-
const DROPPED_REQUEST_HEADERS = new Set(['host', 'content-length', 'accept-encoding', 'connection']);
|
|
50
|
-
const DROPPED_RESPONSE_HEADERS = new Set(['content-length', 'content-encoding', 'connection', 'transfer-encoding']);
|
|
51
|
-
|
|
52
|
-
function pickHeaders(source, dropped) {
|
|
53
|
-
const output = {};
|
|
54
|
-
for (const [key, value] of Object.entries(source)) {
|
|
55
|
-
if (dropped.has(key.toLowerCase())) continue;
|
|
56
|
-
output[key] = value;
|
|
57
|
-
}
|
|
58
|
-
return output;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function readBody(stream, limitBytes) {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
const chunks = [];
|
|
64
|
-
let size = 0;
|
|
65
|
-
stream.on('data', (chunk) => {
|
|
66
|
-
size += chunk.length;
|
|
67
|
-
if (size > limitBytes) {
|
|
68
|
-
reject(new Error(`Corps de requête trop volumineux (> ${limitBytes} octets).`));
|
|
69
|
-
stream.destroy();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
chunks.push(chunk);
|
|
73
|
-
});
|
|
74
|
-
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
75
|
-
stream.on('error', reject);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function isJsonLike(contentType) {
|
|
80
|
-
return /\bjson\b/i.test(String(contentType || ''));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isEventStream(contentType) {
|
|
84
|
-
return /text\/event-stream/i.test(String(contentType || ''));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Texte assistant d'un corps JSON complet (non streamé), pour le harnais de
|
|
89
|
-
* citations : formes Anthropic Messages (`content[]`, blocs `{type:'text'}`)
|
|
90
|
-
* et Responses d'OpenAI (`output[].content[]`, blocs `{type:'output_text'}`).
|
|
91
|
-
* Ne jette jamais : un corps illisible ou d'une autre forme renvoie `''`.
|
|
92
|
-
*/
|
|
93
|
-
function extractAssistantTextFromJson(raw) {
|
|
94
|
-
try {
|
|
95
|
-
const parsed = JSON.parse(typeof raw === 'string' ? raw : raw.toString('utf8'));
|
|
96
|
-
if (!parsed || typeof parsed !== 'object') return '';
|
|
97
|
-
if (Array.isArray(parsed.content)) {
|
|
98
|
-
return parsed.content
|
|
99
|
-
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
100
|
-
.map((block) => block.text)
|
|
101
|
-
.join('');
|
|
102
|
-
}
|
|
103
|
-
if (Array.isArray(parsed.output)) {
|
|
104
|
-
return parsed.output
|
|
105
|
-
.flatMap((item) => (Array.isArray(item?.content) ? item.content : []))
|
|
106
|
-
.filter((block) => block?.type === 'output_text' && typeof block.text === 'string')
|
|
107
|
-
.map((block) => block.text)
|
|
108
|
-
.join('');
|
|
109
|
-
}
|
|
110
|
-
return '';
|
|
111
|
-
} catch {
|
|
112
|
-
return '';
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @param {object} options
|
|
118
|
-
* @param {{ get: () => object }} options.dictionary Chargeur de mapping à chaud.
|
|
119
|
-
* @param {Array<{provider: string, prefix: string, upstream: string}>} [options.routes]
|
|
120
|
-
* Table de routage ; le préfixe le plus long l'emporte.
|
|
121
|
-
* @param {number} [options.port] Port préféré ; 0 = port éphémère.
|
|
122
|
-
* @param {object|null} [options.harness] Harnais de citations vérifiées
|
|
123
|
-
* (`../harness/index.cjs`), absent par défaut : sans lui, le proxy se
|
|
124
|
-
* comporte exactement comme s'il n'existait pas.
|
|
125
|
-
*/
|
|
126
|
-
function createAnonymizerProxy({
|
|
127
|
-
dictionary,
|
|
128
|
-
routes = DEFAULT_ROUTES,
|
|
129
|
-
port = DEFAULT_PORT,
|
|
130
|
-
host = '127.0.0.1',
|
|
131
|
-
maxBodyBytes = 64 * 1024 * 1024,
|
|
132
|
-
onError = () => {},
|
|
133
|
-
harness = null,
|
|
134
|
-
} = {}) {
|
|
135
|
-
// Préfixe le plus long d'abord : `/anthropic/v1` doit gagner sur `/anthropic`
|
|
136
|
-
// si les deux sont déclarés un jour.
|
|
137
|
-
const table = [...routes]
|
|
138
|
-
.map((route) => ({ ...route, target: new URL(route.upstream) }))
|
|
139
|
-
.sort((left, right) => right.prefix.length - left.prefix.length);
|
|
140
|
-
const stats = { requests: 0, anonymized: 0, deanonymized: 0, failures: 0, lastError: null };
|
|
141
|
-
|
|
142
|
-
// Observation du harnais de citations, en espace clair des deux côtés
|
|
143
|
-
// (corps brut du client à l'aller, texte livré au client au retour) — voir
|
|
144
|
-
// `../harness/index.cjs`. Ne doit jamais retarder ni faire échouer une
|
|
145
|
-
// requête ou une réponse déjà en cours, même si le harnais fourni est
|
|
146
|
-
// défaillant (double garde : `index.cjs` ne jette déjà jamais, mais le
|
|
147
|
-
// proxy ne doit dépendre de cette garantie).
|
|
148
|
-
function observerRequete(rawBody, contentType, session) {
|
|
149
|
-
if (!harness) return;
|
|
150
|
-
try {
|
|
151
|
-
harness.observerRequete(rawBody, contentType, { session });
|
|
152
|
-
} catch {
|
|
153
|
-
// jamais bloquant pour la requête en cours
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function observerReponse(texteAssistant, session) {
|
|
158
|
-
if (!harness) return;
|
|
159
|
-
try {
|
|
160
|
-
Promise.resolve(harness.observerReponse(texteAssistant, { session })).catch(() => {});
|
|
161
|
-
} catch {
|
|
162
|
-
// jamais bloquant pour une réponse déjà envoyée au client
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function matchRoute(url) {
|
|
167
|
-
return table.find((route) => url === route.prefix || url.startsWith(`${route.prefix}/`) || url.startsWith(`${route.prefix}?`));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const server = http.createServer((clientRequest, clientResponse) => {
|
|
171
|
-
stats.requests += 1;
|
|
172
|
-
const current = dictionary.get();
|
|
173
|
-
|
|
174
|
-
const route = matchRoute(clientRequest.url || '');
|
|
175
|
-
if (!route) {
|
|
176
|
-
stats.failures += 1;
|
|
177
|
-
stats.lastError = `Chemin non routé : ${clientRequest.url}`;
|
|
178
|
-
clientResponse.writeHead(404, { 'content-type': 'application/json' });
|
|
179
|
-
clientResponse.end(JSON.stringify({
|
|
180
|
-
error: {
|
|
181
|
-
type: 'piecemaker_proxy',
|
|
182
|
-
message: `Aucune route d'anonymisation pour ${clientRequest.url}. Requête refusée plutôt que relayée sans filtrage.`,
|
|
183
|
-
},
|
|
184
|
-
}));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const { target } = route;
|
|
188
|
-
const transport = target.protocol === 'http:' ? http : https;
|
|
189
|
-
const forwardedPath = `${target.pathname.replace(/\/$/, '')}${clientRequest.url.slice(route.prefix.length) || '/'}`;
|
|
190
|
-
|
|
191
|
-
void (async () => {
|
|
192
|
-
let body = Buffer.alloc(0);
|
|
193
|
-
try {
|
|
194
|
-
body = await readBody(clientRequest, maxBodyBytes);
|
|
195
|
-
} catch (error) {
|
|
196
|
-
stats.failures += 1;
|
|
197
|
-
stats.lastError = error.message;
|
|
198
|
-
clientResponse.writeHead(413, { 'content-type': 'application/json' });
|
|
199
|
-
clientResponse.end(JSON.stringify({ error: { type: 'piecemaker_proxy', message: error.message } }));
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Corps brut, AVANT anonymisation : le harnais doit voir le même texte
|
|
204
|
-
// que le client a envoyé, pas les codes qui partent vers le fournisseur.
|
|
205
|
-
observerRequete(body, clientRequest.headers['content-type'], route.provider);
|
|
206
|
-
|
|
207
|
-
let outgoing = body;
|
|
208
|
-
if (body.length && !current.empty && isJsonLike(clientRequest.headers['content-type'])) {
|
|
209
|
-
const rewritten = rewriteJsonBody(body, (text) => anonymize(text, current));
|
|
210
|
-
outgoing = Buffer.from(rewritten, 'utf8');
|
|
211
|
-
if (!outgoing.equals(body)) stats.anonymized += 1;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const headers = pickHeaders(clientRequest.headers, DROPPED_REQUEST_HEADERS);
|
|
215
|
-
headers.host = target.host;
|
|
216
|
-
headers['accept-encoding'] = 'identity';
|
|
217
|
-
if (outgoing.length) headers['content-length'] = String(outgoing.length);
|
|
218
|
-
|
|
219
|
-
const upstreamRequest = transport.request(
|
|
220
|
-
{
|
|
221
|
-
protocol: target.protocol,
|
|
222
|
-
hostname: target.hostname,
|
|
223
|
-
port: target.port || (target.protocol === 'http:' ? 80 : 443),
|
|
224
|
-
method: clientRequest.method,
|
|
225
|
-
path: forwardedPath,
|
|
226
|
-
headers,
|
|
227
|
-
},
|
|
228
|
-
(upstreamResponse) => {
|
|
229
|
-
const responseHeaders = pickHeaders(upstreamResponse.headers, DROPPED_RESPONSE_HEADERS);
|
|
230
|
-
clientResponse.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
|
|
231
|
-
|
|
232
|
-
if (current.empty) {
|
|
233
|
-
// Rien à dé-anonymiser, mais le harnais observe quand même ce qui
|
|
234
|
-
// est livré au client — inchangé, aucune réécriture ici.
|
|
235
|
-
if (isEventStream(upstreamResponse.headers['content-type'])) {
|
|
236
|
-
const collecteur = createCollecteurTexte();
|
|
237
|
-
upstreamResponse.setEncoding('utf8');
|
|
238
|
-
upstreamResponse.on('data', (chunk) => {
|
|
239
|
-
collecteur.push(chunk);
|
|
240
|
-
clientResponse.write(chunk);
|
|
241
|
-
});
|
|
242
|
-
upstreamResponse.on('end', () => {
|
|
243
|
-
clientResponse.end();
|
|
244
|
-
observerReponse(collecteur.texte(), route.provider);
|
|
245
|
-
});
|
|
246
|
-
upstreamResponse.on('error', () => clientResponse.end());
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (isJsonLike(upstreamResponse.headers['content-type'])) {
|
|
250
|
-
const chunks = [];
|
|
251
|
-
upstreamResponse.on('data', (chunk) => chunks.push(chunk));
|
|
252
|
-
upstreamResponse.on('end', () => {
|
|
253
|
-
const raw = Buffer.concat(chunks);
|
|
254
|
-
clientResponse.end(raw);
|
|
255
|
-
observerReponse(extractAssistantTextFromJson(raw), route.provider);
|
|
256
|
-
});
|
|
257
|
-
upstreamResponse.on('error', () => clientResponse.end());
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
upstreamResponse.pipe(clientResponse);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const revert = (text) => deanonymize(text, current);
|
|
265
|
-
|
|
266
|
-
if (isEventStream(upstreamResponse.headers['content-type'])) {
|
|
267
|
-
const rewriter = createSseRewriter(revert);
|
|
268
|
-
const collecteur = createCollecteurTexte();
|
|
269
|
-
upstreamResponse.setEncoding('utf8');
|
|
270
|
-
upstreamResponse.on('data', (chunk) => {
|
|
271
|
-
const out = rewriter.push(chunk);
|
|
272
|
-
if (out) {
|
|
273
|
-
clientResponse.write(out);
|
|
274
|
-
collecteur.push(out);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
upstreamResponse.on('end', () => {
|
|
278
|
-
const tail = rewriter.end();
|
|
279
|
-
if (tail) {
|
|
280
|
-
clientResponse.write(tail);
|
|
281
|
-
collecteur.push(tail);
|
|
282
|
-
}
|
|
283
|
-
stats.deanonymized += 1;
|
|
284
|
-
clientResponse.end();
|
|
285
|
-
observerReponse(collecteur.texte(), route.provider);
|
|
286
|
-
});
|
|
287
|
-
upstreamResponse.on('error', () => clientResponse.end());
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (isJsonLike(upstreamResponse.headers['content-type'])) {
|
|
292
|
-
const chunks = [];
|
|
293
|
-
upstreamResponse.on('data', (chunk) => chunks.push(chunk));
|
|
294
|
-
upstreamResponse.on('end', () => {
|
|
295
|
-
const raw = Buffer.concat(chunks);
|
|
296
|
-
const rewritten = rewriteJsonBody(raw, revert);
|
|
297
|
-
stats.deanonymized += 1;
|
|
298
|
-
clientResponse.end(rewritten);
|
|
299
|
-
observerReponse(extractAssistantTextFromJson(rewritten), route.provider);
|
|
300
|
-
});
|
|
301
|
-
upstreamResponse.on('error', () => clientResponse.end());
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
upstreamResponse.pipe(clientResponse);
|
|
306
|
-
},
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
upstreamRequest.on('error', (error) => {
|
|
310
|
-
stats.failures += 1;
|
|
311
|
-
stats.lastError = error.message;
|
|
312
|
-
onError(error);
|
|
313
|
-
if (clientResponse.headersSent) {
|
|
314
|
-
clientResponse.end();
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
clientResponse.writeHead(502, { 'content-type': 'application/json' });
|
|
318
|
-
clientResponse.end(JSON.stringify({
|
|
319
|
-
error: { type: 'piecemaker_proxy', message: `Relais indisponible : ${error.message}` },
|
|
320
|
-
}));
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
if (outgoing.length) upstreamRequest.write(outgoing);
|
|
324
|
-
upstreamRequest.end();
|
|
325
|
-
})();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
function listenOn(candidate) {
|
|
329
|
-
return new Promise((resolve, reject) => {
|
|
330
|
-
const onError = (error) => reject(error);
|
|
331
|
-
server.once('error', onError);
|
|
332
|
-
server.listen(candidate, host, () => {
|
|
333
|
-
server.removeListener('error', onError);
|
|
334
|
-
const address = server.address();
|
|
335
|
-
resolve({ port: address.port, origin: `http://${host}:${address.port}` });
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
stats,
|
|
342
|
-
get listening() { return server.listening; },
|
|
343
|
-
routes: table.map(({ provider, prefix, target }) => ({ provider, prefix, upstream: target.origin })),
|
|
344
|
-
/**
|
|
345
|
-
* Écoute le port demandé, et lui seul. La base écrite dans les
|
|
346
|
-
* configurations des clients leur survit sur disque : un port qui dérive
|
|
347
|
-
* la rend définitivement fausse pour la session suivante. Un port déjà
|
|
348
|
-
* pris signale une instance encore vivante, pas une invitation à bouger.
|
|
349
|
-
*/
|
|
350
|
-
async listen() {
|
|
351
|
-
try {
|
|
352
|
-
return await listenOn(port);
|
|
353
|
-
} catch (error) {
|
|
354
|
-
if (error.code !== 'EADDRINUSE') throw error;
|
|
355
|
-
throw new Error(`Port ${port} déjà occupé : une instance du proxy PII écoute encore. `
|
|
356
|
-
+ `Arrêtez-la (lsof -nP -iTCP:${port} -sTCP:LISTEN) avant de redémarrer.`);
|
|
357
|
-
}
|
|
358
|
-
},
|
|
359
|
-
close() {
|
|
360
|
-
return new Promise((resolve) => server.close(() => resolve()));
|
|
361
|
-
},
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
module.exports = { DEFAULT_UPSTREAM, createAnonymizerProxy };
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import Cocoa
|
|
2
|
-
import WebKit
|
|
3
|
-
|
|
4
|
-
private let kAdminURL = "https://localhost:43098/admin/"
|
|
5
|
-
private let kRetryInterval: TimeInterval = 1.0
|
|
6
|
-
private let kMaxRetries = 30
|
|
7
|
-
private let kSections = [
|
|
8
|
-
(label: "Dossiers", id: "history"),
|
|
9
|
-
(label: "Configuration", id: "configuration"),
|
|
10
|
-
(label: "Tampon et pièces", id: "pieces"),
|
|
11
|
-
(label: "Skills et agents", id: "files"),
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
15
|
-
var window: NSWindow!
|
|
16
|
-
var webView: WKWebView!
|
|
17
|
-
var sectionControl: NSSegmentedControl!
|
|
18
|
-
var statusLabel: NSTextField!
|
|
19
|
-
var retryCount = 0
|
|
20
|
-
|
|
21
|
-
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
22
|
-
startServer()
|
|
23
|
-
|
|
24
|
-
let config = WKWebViewConfiguration()
|
|
25
|
-
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
26
|
-
config.userContentController.add(self, name: "piecemakerShell")
|
|
27
|
-
config.userContentController.addUserScript(WKUserScript(
|
|
28
|
-
source: "document.documentElement.dataset.nativeShell = 'macos';",
|
|
29
|
-
injectionTime: .atDocumentStart,
|
|
30
|
-
forMainFrameOnly: true
|
|
31
|
-
))
|
|
32
|
-
webView = WKWebView(frame: .zero, configuration: config)
|
|
33
|
-
webView.navigationDelegate = self
|
|
34
|
-
|
|
35
|
-
let screen = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 800)
|
|
36
|
-
let w: CGFloat = min(1280, screen.width * 0.85)
|
|
37
|
-
let h: CGFloat = min(900, screen.height * 0.85)
|
|
38
|
-
let x = screen.origin.x + (screen.width - w) / 2
|
|
39
|
-
let y = screen.origin.y + (screen.height - h) / 2
|
|
40
|
-
|
|
41
|
-
window = NSWindow(
|
|
42
|
-
contentRect: NSRect(x: x, y: y, width: w, height: h),
|
|
43
|
-
styleMask: [.titled, .closable, .miniaturizable, .resizable],
|
|
44
|
-
backing: .buffered,
|
|
45
|
-
defer: false
|
|
46
|
-
)
|
|
47
|
-
window.title = "PieceMaker"
|
|
48
|
-
window.contentView = webView
|
|
49
|
-
installTitlebarControls()
|
|
50
|
-
window.makeKeyAndOrderFront(nil)
|
|
51
|
-
|
|
52
|
-
loadAdmin()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
|
|
56
|
-
|
|
57
|
-
private func installTitlebarControls() {
|
|
58
|
-
sectionControl = NSSegmentedControl(
|
|
59
|
-
labels: kSections.map { $0.label },
|
|
60
|
-
trackingMode: .selectOne,
|
|
61
|
-
target: self,
|
|
62
|
-
action: #selector(selectSection(_:))
|
|
63
|
-
)
|
|
64
|
-
sectionControl.controlSize = .small
|
|
65
|
-
sectionControl.segmentStyle = .texturedRounded
|
|
66
|
-
sectionControl.selectedSegment = 0
|
|
67
|
-
sectionControl.setAccessibilityLabel("Sections PieceMaker")
|
|
68
|
-
sectionControl.sizeToFit()
|
|
69
|
-
|
|
70
|
-
let navigationContainer = NSView(frame: NSRect(
|
|
71
|
-
x: 0,
|
|
72
|
-
y: 0,
|
|
73
|
-
width: sectionControl.frame.width + 12,
|
|
74
|
-
height: max(28, sectionControl.frame.height)
|
|
75
|
-
))
|
|
76
|
-
sectionControl.frame.origin = NSPoint(
|
|
77
|
-
x: 6,
|
|
78
|
-
y: (navigationContainer.frame.height - sectionControl.frame.height) / 2
|
|
79
|
-
)
|
|
80
|
-
navigationContainer.addSubview(sectionControl)
|
|
81
|
-
let navigationAccessory = NSTitlebarAccessoryViewController()
|
|
82
|
-
navigationAccessory.layoutAttribute = .left
|
|
83
|
-
navigationAccessory.view = navigationContainer
|
|
84
|
-
window.addTitlebarAccessoryViewController(navigationAccessory)
|
|
85
|
-
|
|
86
|
-
statusLabel = NSTextField(labelWithString: "Connexion…")
|
|
87
|
-
statusLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
|
88
|
-
statusLabel.textColor = .secondaryLabelColor
|
|
89
|
-
statusLabel.alignment = .right
|
|
90
|
-
statusLabel.sizeToFit()
|
|
91
|
-
let statusContainer = NSView(frame: NSRect(x: 0, y: 0, width: 142, height: 28))
|
|
92
|
-
statusLabel.frame = NSRect(x: 0, y: 5, width: 134, height: 18)
|
|
93
|
-
statusContainer.addSubview(statusLabel)
|
|
94
|
-
let statusAccessory = NSTitlebarAccessoryViewController()
|
|
95
|
-
statusAccessory.layoutAttribute = .right
|
|
96
|
-
statusAccessory.view = statusContainer
|
|
97
|
-
window.addTitlebarAccessoryViewController(statusAccessory)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@objc private func selectSection(_ sender: NSSegmentedControl) {
|
|
101
|
-
guard sender.selectedSegment >= 0, sender.selectedSegment < kSections.count else { return }
|
|
102
|
-
let section = kSections[sender.selectedSegment].id
|
|
103
|
-
webView.evaluateJavaScript("window.piecemakerNavigateTo?.('\(section)')")
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private func startServer() {
|
|
107
|
-
var node = "/usr/local/bin/node"
|
|
108
|
-
var cli = NSString(string: "~/PieceMaker/installer/bin/piecemaker.mjs").expandingTildeInPath
|
|
109
|
-
|
|
110
|
-
if let bundlePlist = Bundle.main.url(forResource: "environment", withExtension: "plist"),
|
|
111
|
-
let dict = NSDictionary(contentsOf: bundlePlist) {
|
|
112
|
-
if let n = dict["PIECEMAKER_NODE"] as? String { node = n }
|
|
113
|
-
if let c = dict["PIECEMAKER_CLI"] as? String { cli = c }
|
|
114
|
-
}
|
|
115
|
-
if let n = ProcessInfo.processInfo.environment["PIECEMAKER_NODE"] { node = n }
|
|
116
|
-
if let c = ProcessInfo.processInfo.environment["PIECEMAKER_CLI"] { cli = c }
|
|
117
|
-
|
|
118
|
-
let process = Process()
|
|
119
|
-
process.executableURL = URL(fileURLWithPath: node)
|
|
120
|
-
process.arguments = [cli, "start"]
|
|
121
|
-
process.standardOutput = FileHandle.nullDevice
|
|
122
|
-
process.standardError = FileHandle.nullDevice
|
|
123
|
-
try? process.run()
|
|
124
|
-
process.waitUntilExit()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private func loadAdmin() {
|
|
128
|
-
guard let url = URL(string: kAdminURL) else { return }
|
|
129
|
-
webView.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData))
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
extension AppDelegate: WKScriptMessageHandler {
|
|
134
|
-
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
135
|
-
guard message.name == "piecemakerShell",
|
|
136
|
-
let payload = message.body as? [String: Any],
|
|
137
|
-
let kind = payload["kind"] as? String else { return }
|
|
138
|
-
|
|
139
|
-
if kind == "section", let section = payload["value"] as? String,
|
|
140
|
-
let index = kSections.firstIndex(where: { $0.id == section }) {
|
|
141
|
-
sectionControl.selectedSegment = index
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if kind == "status", let label = payload["label"] as? String {
|
|
146
|
-
statusLabel.stringValue = label
|
|
147
|
-
statusLabel.textColor = payload["value"] as? String == "ok" ? .systemGreen : .systemRed
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
extension AppDelegate: WKNavigationDelegate {
|
|
153
|
-
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
154
|
-
retryCount += 1
|
|
155
|
-
guard retryCount <= kMaxRetries else {
|
|
156
|
-
webView.loadHTMLString(
|
|
157
|
-
"<html><body style='font-family:system-ui;padding:40px;color:#666'>"
|
|
158
|
-
+ "<h2>Serveur inaccessible</h2>"
|
|
159
|
-
+ "<p>Le serveur local PieceMaker n'a pas répondu après \(kMaxRetries) tentatives.</p>"
|
|
160
|
-
+ "<p>Lancez <code>piecemaker start</code> dans le terminal, puis réessayez.</p>"
|
|
161
|
-
+ "</body></html>",
|
|
162
|
-
baseURL: nil
|
|
163
|
-
)
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + kRetryInterval) { [weak self] in
|
|
167
|
-
self?.loadAdmin()
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
func webView(
|
|
172
|
-
_ webView: WKWebView,
|
|
173
|
-
respondTo challenge: URLAuthenticationChallenge
|
|
174
|
-
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
|
175
|
-
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
|
176
|
-
challenge.protectionSpace.host == "localhost",
|
|
177
|
-
let trust = challenge.protectionSpace.serverTrust else {
|
|
178
|
-
return (.performDefaultHandling, nil)
|
|
179
|
-
}
|
|
180
|
-
return (.useCredential, URLCredential(trust: trust))
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let app = NSApplication.shared
|
|
185
|
-
let delegate = AppDelegate()
|
|
186
|
-
app.delegate = delegate
|
|
187
|
-
app.setActivationPolicy(.regular)
|
|
188
|
-
app.activate(ignoringOtherApps: true)
|
|
189
|
-
app.run()
|